fix: audit remediation Wave 3 — M-1, MED/LOW sweep, ingest robustness + D-1 close-out #14

Merged
user2595 merged 15 commits from fix/audit-wave-3 into main 2026-06-16 04:54:00 +00:00
Owner

Wave 3 of audit remediation — completes the audit + the live migration

Stacked on fix/audit-wave-2; the diff is the Wave-3 commits. Closes the
remaining MED/LOW findings, the schema-migration gap, and finalises the F-15
re-validation (D-1) on the migrated corpus. Full suite: 340 passing, ruff +
mypy clean.

Schema / migration (M-1)

  • schema.sql is now fully idempotent; new codex migrate applies it via a
    privileged MIGRATION_DATABASE_URL (the app role is DML-only) and reports
    InsufficientPrivilege helpfully. Documented in README + .env.example.
  • infra/reingest_canonical_ids.sh migration helper with a preflight that
    aborts before TRUNCATE if the schema isn't ready (it had wiped-then-failed).

Correctness / robustness

  • R-1 grounding tokenizer robust to punctuation/decimals · C-6 token-boundary
    5-gram match (no substring bleed) · R-9 cross-ref injection protects LaTeX
    math · R-8 coverage-map gaps are opt-in (--topic) · R-2 ref-list filter
    excludes theorem labels · C-12/C-13 no-op DB call removed / mark order fixed.
  • C-15 (surfaced by the migration) ingest now retries a papers.bibkey
    UNIQUE collision with an a/b/c suffix instead of failing — 2305.10988
    collided with 2310.17529 (both → BobenkoLutz2023).

Security

  • S-4 all credentials are SecretStr · S-2 constant-time MCP token compare
    · C-3 wiki_read returns real cited bibkeys.

Sources / CLI

  • R-4/R-5 drop no-op try/except, url-encode S2 ids · U-1/U-3 graph-report
    titles + JSON warning · U-4/C-9 wire graph cocited / discover recommend.

Accepted (documented, no code change)

  • R-3 conflict detection (keyword MVP) · R-12 classify_section (word-chunk
    limitation) · C-14/U-2 doc clarifications (sparse not wired; cite-boost tie-breaker).

D-1 — F-15 re-validated on the migrated corpus

Live re-ingest onto canonical bare ids: 29/29 papers bare (url_form_ids=0),
C-1 leaked=0, both verified against the Jetson DB. Graph re-measured: 489 nodes /
590 edges / 472 dangling / 5.4 % spread. Pinkall/Polthier 1993 now resolves to
its DOI and is the rank-1 in-KB hub; Bobenko/Springborn 2007 holds rank 7 —
GO re-affirmed. ADR-F15 spike table updated with before/after numbers.

🤖 Generated with Claude Code

## Wave 3 of audit remediation — completes the audit + the live migration Stacked on `fix/audit-wave-2`; the diff is the Wave-3 commits. Closes the remaining MED/LOW findings, the schema-migration gap, and finalises the F-15 re-validation (D-1) on the migrated corpus. **Full suite: 340 passing**, ruff + mypy clean. ### Schema / migration (M-1) - `schema.sql` is now fully idempotent; new `codex migrate` applies it via a privileged `MIGRATION_DATABASE_URL` (the app role is DML-only) and reports `InsufficientPrivilege` helpfully. Documented in README + `.env.example`. - `infra/reingest_canonical_ids.sh` migration helper with a **preflight** that aborts before `TRUNCATE` if the schema isn't ready (it had wiped-then-failed). ### Correctness / robustness - **R-1** grounding tokenizer robust to punctuation/decimals · **C-6** token-boundary 5-gram match (no substring bleed) · **R-9** cross-ref injection protects LaTeX math · **R-8** coverage-map gaps are opt-in (`--topic`) · **R-2** ref-list filter excludes theorem labels · **C-12/C-13** no-op DB call removed / mark order fixed. - **C-15** (surfaced by the migration) ingest now retries a `papers.bibkey` UNIQUE collision with an a/b/c suffix instead of failing — `2305.10988` collided with `2310.17529` (both → `BobenkoLutz2023`). ### Security - **S-4** all credentials are `SecretStr` · **S-2** constant-time MCP token compare · **C-3** `wiki_read` returns real cited bibkeys. ### Sources / CLI - **R-4/R-5** drop no-op try/except, url-encode S2 ids · **U-1/U-3** graph-report titles + JSON warning · **U-4/C-9** wire `graph cocited` / `discover recommend`. ### Accepted (documented, no code change) - **R-3** conflict detection (keyword MVP) · **R-12** classify_section (word-chunk limitation) · **C-14/U-2** doc clarifications (sparse not wired; cite-boost tie-breaker). ### D-1 — F-15 re-validated on the migrated corpus Live re-ingest onto canonical bare ids: **29/29 papers bare** (`url_form_ids=0`), C-1 leaked=0, both verified against the Jetson DB. Graph re-measured: 489 nodes / 590 edges / 472 dangling / 5.4 % spread. Pinkall/Polthier 1993 now resolves to its DOI and is the **rank-1 in-KB hub**; Bobenko/Springborn 2007 holds rank 7 — GO re-affirmed. ADR-F15 spike table updated with before/after numbers. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
user2595 changed target branch from fix/audit-wave-2 to main 2026-06-16 04:53:51 +00:00
user2595 added 15 commits 2026-06-16 04:53:51 +00:00
M-1: schema.sql could not be re-applied (leading CREATE TABLE/INDEX lacked
IF NOT EXISTS), apply_schema was never called, and the live migration just
failed on a missing chunks.section column. Fixes:

- schema.sql: all CREATE TABLE/INDEX now use IF NOT EXISTS — the whole file is
  re-applyable as a no-op.
- codex migrate: new CLI command that applies schema.sql. Connects via
  MIGRATION_DATABASE_URL (falls back to DATABASE_URL) and catches
  InsufficientPrivilege with guidance — because the app role is DML-only and
  core tables are owned by 'postgres' (the privilege dimension found during the
  live migration incident).
- config: migration_database_url (optional privileged connection).
- db: apply_schema docstring corrected (idempotency now true) + privilege note.
- T-2: static test that schema.sql is fully idempotent; migrate privilege-error
  test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Now that 'codex migrate' applies the idempotent schema via a privileged role,
recommend it (with MIGRATION_DATABASE_URL) as the primary schema-sync fix in the
preflight abort message, keeping the manual owner-ALTER as a fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README step 4 used an inline apply_schema over the app DATABASE_URL, which fails
under the privilege model (least-privilege app role cannot run DDL). Replace it
with 'codex migrate' and document MIGRATION_DATABASE_URL (owner/superuser) in the
README env table and .env.example, so the privileged migrate path is the
standard for future schema upgrades.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- R-9 (MED): _inject_cross_refs now protects LaTeX math spans ($…$, $$…$$,
  \(…\), \[…\]) like inline code, so a concept name inside a formula is no longer
  rewritten to a [[slug]] link and corrupting the LaTeX. Regression test added.
- C-13 (LOW): mark ungrounded claims on the raw body BEFORE injecting cross-refs
  (render then inject), so a concept name inside a claim cannot stop the ⚠ regex
  from matching.
- C-12 (LOW): _try_embed_formulas is now a true no-op — it previously issued a
  per-concept information_schema round-trip that did nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
find_gaps defaulted its probe topics to paper titles, so a title — which
retrieves mostly its own single bibkey — fell below synthesis_gap_min_coverage
and flagged almost every paper as a 'gap'. Coverage-map gaps now run only for
explicitly-requested topics; the CLI gains a repeatable --topic option. The
code-vs-corpus gap path (precise) is unchanged and still default.

Tests: the two cases that relied on default-title coverage gaps now pass explicit
topics; added a regression that no coverage gaps appear without --topic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- C-3 (MED): wiki_read returned 'sources' = [concept_slug] (a placeholder), not
  the cited sources. Now parses the page's inline [BibKey #loc] citations and
  returns the actual set of cited bibkeys.
- S-2 (LOW): _TokenAuth compared the Bearer header with != (length/equality
  timing side-channel). Use secrets.compare_digest for constant-time comparison.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The grounding guard tokenized with .lower().split() and split sentences on bare
.!?, so faithful claims were wrongly marked ungrounded: 'volume,' != 'volume',
and a decimal like 'V = 1.5' split into a 1-word fragment '5' that fell below
the 5-content-word floor.

- _content_words now strips edge sentence-punctuation (.,;:!?"') from tokens
  while preserving math delimiters ()=+; claim and chunk are normalised
  identically so matching stays consistent.
- _SENTENCE_SPLIT_RE splits on .!? only when followed by whitespace/end, so
  decimals no longer create spurious sentence boundaries.

Regression test: a 5-word claim with a trailing comma now grounds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
database_url, migration_database_url, mcp_auth_token and the mathpix app
id/key are now pydantic SecretStr, so they render as '**********' in any
repr/log/traceback instead of leaking the plaintext credential. Consumers
(db.get_conn, cli.migrate, mcp._require_http_token, mathpix.extract_formulas)
call .get_secret_value() at the point of use. Tests updated to the SecretStr
type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- R-4: arxiv.fetch_source had a try/except httpx.RequestError that only re-raised
  — removed (no behaviour change, less noise).
- R-5: semanticscholar URLs now quote(paper_id, safe=':') so a legacy-arXiv '/'
  (arXiv:math/0603097) doesn't corrupt the path and silently return no references.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- U-1: 'graph report' shows the paper title next to each in-KB hub (dangling
  OpenAlex-id hubs are flagged), instead of bare ids.
- U-3: '--json' now includes a 'small_corpus_warning' field (null unless below
  graph_min_corpus_size) instead of silently dropping the warning.
- C-9: new 'codex discover recommend <id>' wires semanticscholar.fetch_recommendations
  (was implemented + tested but unreachable).
- U-4: new 'codex graph cocited <id>' wires graph.find_co_cited (likewise).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- C-6: the content-5-gram check used 'gram in src' on space-joined strings, so the
  first/last gram token could match a prefix/suffix of a longer chunk word
  ('set' inside 'subset'). Both gram and chunk are now space-padded, so a match
  requires whole-token alignment. Regression test added.
- R-2: the reference-list filter's author pattern ('Surname, I.') also matched
  'Theorem A.'/'Lemma B.', so theorem-dense chunks could be dropped as a
  bibliography. The pattern now excludes common math-structure words.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- C-14: embed.py header now states that only the dense path is wired (search
  'hybrid' is dense + Postgres FTS); encode_sparse/encode are reserved for a
  future sparse-retrieval layer, not yet consumed.
- U-2: --cite-boost help clarifies it re-ranks within the top results (a
  tie-breaker), not a hard re-ranking — matching the small alpha effect.

Accepted (documented heuristics, no change): R-3 conflict detection is a
keyword-only signal labelled as such; R-12 classify_section is mostly 'body'
because chunks are word-windows that rarely start at a section header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The id-keyed upsert (ON CONFLICT (id)) does not catch the papers.bibkey UNIQUE
constraint, so a second paper whose auto-generated key matches an existing one
(two same-author/year works → e.g. 'BobenkoLutz2023') failed the whole ingest
with a UniqueViolation. Surfaced by the canonical-id re-ingest: 2305.10988
collided with 2310.17529.

ingest now catches a bibkey UniqueViolation, rolls back, and retries the upsert
with an a/b/c… suffix. Non-bibkey violations re-raise unchanged. Mocks don't
raise, so the existing happy-path tests are untouched; a new test drives the
collision-and-retry path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-ran the citation graph after the C-7 re-ingest (29/29 papers canonical/bare):
489 nodes, 590 edges, 472 dangling, 5.4% spread. The resolution fix's signature:
Pinkall/Polthier 1993 resolves to its DOI and is now the rank-1 *in-KB* hub
(was a dangling OpenAlex node); Bobenko/Springborn 2007 holds rank 7. Foundational
hubs still surface — GO re-affirmed on the shipping topology. Updates the ADR
spike table with before/after numbers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per review: the two latent upsert conflicts should be observable even where not
auto-fixed.
- bibkey collision (auto-suffixed) now logs a warning so a paper landing as
  'BobenkoLutz2023a' instead of '…2023' isn't silent.
- openalex_id collision (two papers claiming the same OpenAlex work — a real data
  conflict, not auto-renamable) now raises a clear ValueError naming the
  openalex_id and the offending paper, instead of the raw psycopg UniqueViolation.

Regression test added for the openalex_id error path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
user2595 merged commit d64edfc324 into main 2026-06-16 04:54:00 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: user2595/codex-py#14
No description provided.