Skip to content

refactor(contracts): prove the hand-mirrored contract halves agree#31

Closed
sturlese wants to merge 1 commit into
fix/acl-empty-openfrom
refactor/contract-parity
Closed

refactor(contracts): prove the hand-mirrored contract halves agree#31
sturlese wants to merge 1 commit into
fix/acl-empty-openfrom
refactor/contract-parity

Conversation

@sturlese

Copy link
Copy Markdown
Owner

Summary

Stacked on #30. ADR 001/011 doctrine means every cross-package contract exists as hand-mirrored copies; the architecture review found three of them drifting (one had already produced the empty-ACL leak fixed in #30). This PR closes the remaining drifts and makes CI prove the agreement instead of trusting the mirror comments.

Changes

  • facts read path: clean/factstore.query_facts now carries the same semantics as the serving copy (answer/metrics.query_metrics): verified-only rows, case-folded metric/entity inputs. No result changes today (the store only holds verified lowercase rows) — but the golden evals exercised the pipeline copy while production served through the answer copy, with nothing guarding the gap.
  • CLEAN_MODEL contract in graph: merges.build_merge_judge fed a provider-prefixed CLEAN_MODEL (anthropic:claude-sonnet-4-5) into OpenAIResponsesModel (broken, while clean supported it) and a CLEAN_LLM typo silently picked the fake (startswith). Now a faithful hand-mirror of clean's llm.build_model (provider passthrough, validated reasoning effort) plus fail-fast backend validation — tested.
  • ANSWER_LLM fail-fast: same doctrine in answer/synthesize.build_synthesizer (a typo fell through silently). Also: dead PAGE_EXCERPT duplicate removed, and the raw superseded-pages SQL inlined in the service orchestrator moved behind index.superseded_paths() — the page index owns its queries.
  • clean frontmatter: ONE definition of the frontmatter block (page.FRONTMATTER_RE) — versions.annotate_page and claims.strip_page_chrome each had their own regex variant of it.
  • clean state schema: documented in state.py, the one module every consumer imports — five modules mutate this dict in place and its shape was recorded nowhere.
  • evals: new eval_contract_parity proves in CI (the one harness that imports both sides) that clean's visible(list) and answer's visible(csv) agree over the full truth table — including the empty-intersection case fix(acl): an empty ACL was served open; forbid labels that break CSV round-trip #30 fixes — and that query_facts / query_metrics return identical rows for the same probes over the produced store. Scorecard: 22 → 24 metrics (README updated).

Testing

  • clean 248 · graph 38 (3 new) · answer 42 (1 new) · fetch 29 · corpus 48 · slack 13 · benchmark 3 — all bare pytest (CI mode).
  • ruff clean; golden scorecard 24/24.

Notes

Deliberately NOT touched, with reasons: shared contract library (contradicts ADR 001/011 — the parity eval is the doctrine-compliant guard), typed state objects (ADR 001 defers them; the schema is now documented instead), slugify cap alignment clean↔graph (changes existing corpus paths — migration-sensitive), and the evals/benchmark harness merge (the golden gate is tuned; not worth the risk in this pass).

ADR 001/011 doctrine: packages share no code and talk through files, so
every cross-package contract exists as hand-mirrored copies. The
architecture review found three of those copies drifting (one had
already produced the empty-ACL leak fixed in the previous commit).
This closes the remaining drifts and makes CI prove the agreement:

- factstore.query_facts now carries the same semantics as the serving
  layer's copy (answer/metrics.query_metrics): verified-only rows and
  case-folded metric/entity inputs. The store only ever holds verified
  lowercase rows, so this makes invariants explicit rather than
  changing results — but until now the golden evals exercised the
  pipeline copy while production served through the answer copy, with
  nothing guarding the gap.
- graph merges.build_merge_judge honored only half the CLEAN_MODEL
  contract: a provider-prefixed value ("anthropic:claude-sonnet-4-5")
  worked in clean but was fed into OpenAIResponsesModel here and broke;
  a CLEAN_LLM typo silently picked the fake (startswith). Now a
  faithful mirror of clean's llm.build_model (provider passthrough,
  validated effort) with fail-fast backend validation, tested.
- answer's ANSWER_LLM dispatch gets the same fail-fast (a typo fell
  through to the fake or the real path); dead PAGE_EXCERPT copy in
  synthesize.py removed; the raw superseded-pages SQL inlined in the
  service orchestrator moved behind index.superseded_paths — the page
  index owns its queries.
- clean now has ONE frontmatter-block definition (page.FRONTMATTER_RE):
  versions.annotate_page and claims.strip_page_chrome parsed the same
  block with two hand-rolled regex variants.
- clean/state.py documents the state schema in the one module every
  consumer imports — five modules mutate this dict in place and its
  shape was recorded nowhere.
- evals: new eval_contract_parity proves, in the one harness that
  imports both sides, that (a) clean's visible(list) and answer's
  visible(csv) agree over the full truth table including the
  empty-intersection case, and (b) query_facts and query_metrics return
  identical rows for the same probes over the produced store. Scorecard
  goes 22 -> 24 metrics (README updated).

Validation: clean 248, graph 38, answer 42, fetch 29, corpus 48,
slack 13, benchmark 3 (bare pytest, CI mode); ruff clean; golden
scorecard 24/24.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant