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

15 Commits

Author SHA1 Message Date
Tarik Moussa
e1c0a98262 fix(ingest): make both upsert UNIQUE gaps visible (audit C-15 follow-up)
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>
2026-06-15 22:25:30 +02:00
Tarik Moussa
1f677211f7 docs(adr): finalise D-1 — re-measure F-15 spike on canonical corpus
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>
2026-06-15 22:13:56 +02:00
Tarik Moussa
b52303d22b fix(ingest): retry on bibkey UNIQUE collision (audit C-15)
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>
2026-06-15 22:11:14 +02:00
Tarik Moussa
45c3b16dd7 docs: clarify sparse-not-wired and cite-boost tie-breaker (audit C-14, U-2)
- 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>
2026-06-15 21:47:40 +02:00
Tarik Moussa
f422b0fb84 fix(wiki): token-boundary grounding match + sharper ref-list filter (C-6, R-2)
- 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>
2026-06-15 21:46:02 +02:00
Tarik Moussa
92928bab99 feat(cli): graph-report titles + JSON warning; wire recommend/cocited (U-1,U-3,U-4,C-9)
- 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>
2026-06-15 21:43:19 +02:00
Tarik Moussa
c7fae5c877 fix(sources): drop no-op try/except + url-encode S2 ids (audit R-4, R-5)
- 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>
2026-06-15 21:39:48 +02:00
Tarik Moussa
b4fbd7930e feat(config): wrap credentials in SecretStr (audit S-4)
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>
2026-06-15 21:32:13 +02:00
Tarik Moussa
e7b854db10 fix(wiki): make grounding tokenizer robust to punctuation (audit R-1)
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>
2026-06-15 16:37:30 +02:00
Tarik Moussa
77f9d79da0 fix(mcp): real cited bibkeys in wiki_read + constant-time token compare
- 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>
2026-06-15 16:34:28 +02:00
Tarik Moussa
c4106b0f51 fix(synthesis): make coverage-map gaps opt-in (audit R-8)
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>
2026-06-15 16:32:01 +02:00
Tarik Moussa
cfbfa53398 fix(wiki): protect math in cross-refs; drop no-op DB call; fix mark order
- 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>
2026-06-15 16:24:22 +02:00
Tarik Moussa
ae5e9a528b docs: document 'codex migrate' + MIGRATION_DATABASE_URL as the schema-sync path
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>
2026-06-15 16:20:54 +02:00
Tarik Moussa
10ff5ba3ab chore(infra): point migration helper preflight at 'codex migrate'
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>
2026-06-15 15:54:25 +02:00
Tarik Moussa
cdb7d254df feat(db): idempotent schema + 'codex migrate' command (audit M-1, T-2)
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>
2026-06-15 15:53:25 +02:00