Skip to content

Refactor A99 semantic control plane toward closed-loop vNext - #3

Draft
ylight23 wants to merge 107 commits into
accuracy99/autonomous-closed-loopfrom
refactor/a99-semantic-control-plane-vnext
Draft

ylight23 wants to merge 107 commits into
accuracy99/autonomous-closed-loopfrom
refactor/a99-semantic-control-plane-vnext

Conversation

@ylight23

Copy link
Copy Markdown
Owner

Refactor the A99 canonical semantic path toward the agreed vNext architecture without adding heading heuristics.

Changes:

  • enforce semantic/source contract before exact UTF-16 binding
  • plumb segment ownership into the deterministic pipeline
  • strengthen sourceAlias/verbatim/composite validation while keeping semantic truth model-owned
  • add a closed-loop semantic adjudication control plane that reopens genuine conflicts and only permits selection of frozen alternatives
  • withhold unresolved conflicts instead of silently collapsing them
  • add focused tests for contract failure, ownership, adjudication, zero-call obvious cases, and unresolved withholding

This is intentionally a compatibility-preserving migration: existing Run/RunAsync APIs remain intact; the new control-plane surface is additive while production wiring can migrate incrementally.

ylight23 and others added 30 commits September 17, 2026 17:25
CanonicalSemanticVnextTests.Resolver_keeps_repeated_occurrences_but_outline_projection_collapses_node
asserted CONTINUATION/shared-node behavior without supplying the
explicit same-node relation hint the resolver contract requires.
CanonicalSemanticGraphResolver intentionally treats text/scope alone
as insufficient identity (each physical occurrence is its own
semantic node unless an explicit same-node/continuation-node hint is
present) -- this is the same pattern already exercised and passing in
CanonicalSemanticVnextRuntimeTests. The resolver was correct; the test
was missing the hint. No resolver/graph/projection behavior changed.

Focused semantic suite: 72/72 PASS. Full suite: 1764/1765 PASS (one
unrelated pre-existing PdfN15RankingLossDiagnosisProbe artifact SHA256
mismatch, tracked separately, not touched).
Run outputs from the canonical-dev v4/v5/v6/v7 execution tracks. The runners
that produced them are gone and nothing reads the artifacts; they are recoverable
from history if a past run ever needs re-examining.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ring

The DOCX route decided hierarchy with PdfHierarchyResolver, which parsed the
numbering shape of the raw text ("1.2.3" -> depth 3) and never consulted the
model. That contradicts the invariant this pipeline is built on: the model
decides meaning, the harness decides coordinates. Level now comes only from the
immediate-parent relations the model returns, validated and counted by
ModelRelationHierarchyResolver; marker facts become evidence sent TO the model
instead of an authority behind its back.

Found while wiring this up, each only reachable against a live provider:

- CompatibilityLevelIsSet was true while CompatibilityLevel stayed null, so the
  projection preferred that null and this route emitted headings with no level
  at all - whatever any resolver decided. Level had never reached the output,
  which is why the wrong hierarchy source went unnoticed.
- CanonicalSemanticContractValidator threw IndexOutOfRange when a proposal named
  several sourceAliases with a single verbatimText. A validator must reject
  malformed output, not abort the document.
- The request carried the whole document in one call (~1.16M tokens for
  DOC-0116). Segmentation with owned/visible alias ownership was specified but
  never implemented here.
- The output budget was inferred by counting a field name in the payload, so
  renaming a request field collapsed it to its floor and truncated the reply.
  Callers now state expectedItemCount.
- provider.zdr must be written explicitly: omitting it inherits the account
  default, which rejects every endpoint of the controlled model. It is now
  configurable and defaults to false, and the runtime description no longer
  claims a ZDR guarantee it does not enforce.

Also removes the legacy pairwise semantic-identity track (Hdsa*, 20 core files
plus tests) - no production code referenced it - and the heuristic candidate
gate, so the LLM's recall ceiling is the source occurrence universe rather than
whatever the scorer let through.

Verified on DOC-0116 via qwen3.7-flash: 99 headings, 96/99 parents resolved by
the model. Release build clean, 905/905 tests, 72/72 focused semantic tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A parser scope and a domain role detector could both delete a heading the
model had proposed. On DOC-0256 that removed DAY 2, DAY 3 and DAY 4: each
sits alone in a one-cell table inside an open appendix, so the scope became
appendix_table, DocumentDomainPolicy read that as a table title, and
PdfProposalValidator.IsEligibleHeading dropped all three before hierarchy
resolution. The route audit reported 23 validated structures against 20
emitted headings with no rejection recorded anywhere, which is why the loss
was invisible.

Authority is now split by what a reason claims. Source validity may suppress:
a missing or out-of-boundary pointer span, an untrusted evidence origin, an
empty text, a failed binding. A claim about meaning may not - it is recorded
as a disagreement on the stage trace and on the output decision, and the
heading survives.

The same split applies at the output boundary, where a scope in the excluded
list and a proposed domain exclusion now only annotate the decision.

Scope: the canonical DOCX route. The PDF recovery path still consults
ProposesOutlineExclusion in PdfSemanticRecoverySelector, PdfVisualTextRecovery
and PdfProposalConflictResolver; those are untouched here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rify

Two changes, both about attributing a result to the right cause.

Fail-closed contract handling. ParseProposal read fields with GetString and
GetInt32, which throw on type confusion. The throw escaped the entry loop, the
segment loop and the route, so one reply carrying "occurrence": "1" or
"sourceAliases": "S0123" ended the whole document with no heading and no
recorded reason. Every read now goes through a typed accessor: an absent
optional field is fine, a present field of the wrong type costs that entry
alone. JsonDocument.Parse is likewise guarded, so a reply that is not JSON at
all costs its segment and is recorded as UNPARSEABLE_REPLY.

Offline replay. FrozenReplyClassifier replays the provider replies captured
from the run that lost DAY 2/3/4, so 3886a5a can be measured without a new
provider call - a fresh call to a stochastic model moves the model and the
harness together and cannot say which one changed the number. Calls past the
end of a recording return an empty heading set and are counted, so a replay
can never quietly claim coverage of a pass that was never recorded.

Measured on the frozen recording, current harness:
  proposals 23 -> validated 23 -> canonical 23   (was 23 -> 20 -> 20)
  gold 24, matched 16 -> 19
  recall 0.6667 -> 0.7917, precision 0.8000 -> 0.8261, F1 0.7273 -> 0.8085
Exactly the three DAY headings are recovered; nothing else moved, and no new
false positive appeared.

Also pins the source universe invariant, on a fixture that can actually
exercise it: on most of this corpus every non-empty paragraph becomes a
candidate, so those documents cannot distinguish "non-candidates survive" from
"there are no non-candidates".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lies

Closes the ledger for this document. Of eight historical false negatives,
none is a clean model omission.

  3  DAY 2/3/4   RESIDUAL_SYSTEM_LOSS_PROVEN -> FIXED_AND_REPLAY_VERIFIED
  1  Planning…   REFERENCE_COORDINATE_DEFECT -> pending approval
  1  DAY 1       SEGMENT_BOUNDARY_INCONSISTENCY
  3  Africa/…    MODEL_WRONG_TEXT_BOUNDARY

DAY 1. The replay classifier now records the request packets, so the trace
reads what the model actually saw rather than a reconstruction. Per-entry
evidence for DAY 1 and DAY 2/3/4 is byte-identical - same scope, tableDepth,
style, markers, attention - and DAY 1 sits 73% through its segment, not at the
tail, so the position hypothesis is rejected. The mechanism is the annex:
segment 0 proposed "Annex 1: Meeting Agenda" and then nothing for its
remaining 32 owned entries, while segment 1 begins inside that annex with a
visible window starting at S0101, which excludes the annex heading. One
container, split across two requests, framed two different ways. The visible
window is chosen by positional margin alone; carrying the governing structural
ancestor would remove the asymmetry.

Africa/Asia/Eurostat. Not an omission. Every sibling whose source paragraph IS
the heading was proposed - Commonwealth of Independent States, Latin America
and the Caribbean, Western Asia. The three misses are exactly those where the
heading is a prefix of a longer paragraph. The contract already expresses
this: verbatimText binds as an exact substring. The prompt does not - it
contains no mention of substring, partial, prefix or selectionMode, and reads
as "name the alias and echo its text".

Neither is fixed here. Both changes alter the request packet, which invalidates
the frozen recording, so neither can be scored offline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Blast radius of 3886a5a, measured with frozen provider replies only.
PROVIDER_CALLS = 0. No prompt, packet, or gold change.

Method. The after-set is the replay through the current harness. The
before-set is a counterfactual computed from the same replay by re-applying
the two clauses 3886a5a removed, not the recorded output of an older run -
those artifacts come from harness versions that differ in more than the veto.
The counterfactual reproduces DOC-0256's real pre-fix run exactly, 20 canonical
and 16 matched, which is what licenses using it on the other documents.

Scoring axis correction. Every strict-gold-v4 file declares
occurrenceEvaluable = false and semanticEvaluable = true, so joining on
sourceId was never an authorised capability. All scores now join on exact
heading text, which is unique on the gold side and equals semanticHeadingTotal.
On the authorised axis DOC-0256 scores recall 0.8333, precision 0.8696,
F1 0.8511 - and the "Planning for the ICP 2024 cycle" gold coordinate defect
stops being an FN/FP pair, because text matches wherever it sits. The earlier
occurrence-axis numbers are marked superseded rather than deleted, and the
DOC-0256 ledger drops from 8 false negatives to 7.

Result. Nothing that used to reach canonical output was lost on any document.
DOC-0116 recovers recall 0.0909 -> 0.9545. DOC-0252 is untouched. DOC-0258
gains one false positive. DOC-0001 is blocked: its gold text is mojibake and
three of seven rows are not recoverable.

The veto was also hiding real model errors - four table-of-contents lines on
DOC-0116, one agenda row on DOC-0258 - so the reasoner double-claims a section
from both its TOC line and its body occurrence. That is now visible and stays
visible; reinstating the veto to hide it would restore the original fault.

Verdict: SEMANTIC_VETO_FIX_EXPOSES_MODEL_FP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DOC-0001 is the one document the replay cohort cannot score: its gold holds
UTF-8 bytes stored as cp1252, and three of seven rows contain 0x81/0x90, which
are undefined in cp1252, so no decoding repair can recover them.

So this does not repair the damaged strings. It re-derives the heading set from
the source document, which is style-conformant: seven paragraphs carry an OOXML
built-in heading style with an explicit w:outlineLvl and the other seven are
body prose, so the heading set is closed by construction. Text comes from the
source; level is w:outlineLvl + 1. No provider call, no model.

The superseded gold is used only to confirm the pairing, never as the source of
the new text, and it corroborates cleanly: all seven of its rows land on one of
those seven paragraphs, and every stored level agrees. It also resolves the
secondary defect - gold addressed occurrences as paragraph[N], a 0-based
ordinal, which maps exactly onto body[1]/p[N+1].

Nothing is applied. The proposal is PENDING_USER_REVIEW and the corrupted file
is retained as superseded provenance, because deleting it would erase the
evidence that the damage existed.

Also records the three interventions as separate experiments - I7 structural
ancestor carry, I8 partial-span discovery, I9 TOC/body double-claim - each
marked must-not-combine, since I8 tests boundary selection within one
occurrence while I9 tests which occurrence of a section to claim, and a
combined prompt change would make any gain unattributable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R1, approved. DOC-0001's gold stored UTF-8 bytes decoded as cp1252, and three
of seven rows held 0x81/0x90, undefined in cp1252 and so unrecoverable by
re-decoding. The fix is not a repair: bench/01-style-chuan.key is generated
with the document, carries ordinal, level and text for all seven headings, and
the damaged gold already cited it with a matching sha. The reference was never
damaged - only the reading of it was - so the re-author is a re-read, and it
avoids leaning on "Heading1/2/3 implies semantic heading", which would
contradict the standing invariant that a heading style is not semantic truth.
The source DOCX corroborates independently: exactly seven paragraphs carry a
built-in heading style and w:outlineLvl + 1 matches the key's level on all
seven.

occurrenceEvaluable stays false. Canonical source ids do not by themselves make
occurrence-level evaluation authorised. policyVersion is unchanged: this fixes
a file under the existing policy rather than defining a new one, which
A99StrictGoldV4Tests pins and which caught an earlier attempt to bump it. The
damaged file is retained under superseded/.

DOC-0001 rejoins the cohort at precision 1.0, recall 0.8571, F1 0.9231, missing
only "Chương 2. Tổ chức thực hiện". The veto fix is a no-op on this document.

Documentation. NumberingAudit claimed that missing a candidate loses it for
good "because the model never sees it". That describes the candidate-first
architecture that was removed, and left in place it invites the next person to
"fix recall" by widening HeadingCandidate - rebuilding the hidden gate that
3886a5a just took out. HeadingHeuristics now states the invariant directly:
HeadingCandidate is attention only, Normal stays in the semantic universe, and
only Empty is excluded, enforced in DocxAuthorityPipeline.Build and pinned by
SourceUniverseCeilingTests.

Dead configuration. MaxCandidateTextLength, CandidateThreshold and
PromoteStandaloneLines were still declared, still exposed as --threshold and
--no-standalone-lines, and still on the web form - and read by nothing. A knob
that silently does nothing is worse than a stale comment, so they are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The route is decided by a single expression returning one of
{ DocxAuthority, PdfAuthority, Unsupported }, so there is no representation for
running both. Three things make it worse than a default-choice question.

A PDF-only input returns Unsupported, so PDF is not a parallel authority at
all - it is a mode of the DOCX pipeline. When PdfAuthority is selected the
source catalog itself is swapped, and sections, chunks, occurrence traces and
provenance are then all built from the PDF, so the DOCX occurrence universe
never reaches the result.

And the architecture required already exists one level below:
CanonicalSemanticProductionInput already accepts visual blocks, proposals and
pages beside the DOCX catalog, and CanonicalSemanticCrossModalReconciler
already merges two modalities into unified occurrences. The DOCX adapter passes
none of them. The channel is built and unused.

Also records that AuthorityRoutePolicyContractTests declares its own copies of
SourceCapabilities, AuthorityRoute and the policy inside the test namespace and
asserts against that copy, so it would pass unchanged if the production policy
were altered or deleted.

Consequence for the evidence gathered so far: every replay ran the DOCX lane,
because no PDF was passed. The cross-document replay numbers must not be
generalised to PDF-accompanied production runs until this is resolved.

Audit only, no code change. The enum is the defect, but replacing it means
first deciding what a combined result contains, which is a product decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The route was decided from SourceCapabilities(HasDocx, HasPdf, ...), and HasPdf
came from PdfTextbookOutline.FindSiblingPdf(inputPath). That helper looks for a
same-named PDF beside the input, then rewrites an eval corpus path segment, and
finally walks up from the process working directory searching
todo10_8/heading_corpus_100 recursively for any file of the same name. So a
file nobody uploaded could take authority away from the file that was uploaded,
and which one won depended on the current working directory. A web host serving
uploads from the repository root would hand a user's report.docx the corpus PDF
of the same name.

It was not only the default that was wrong. Selecting PdfAuthority also swapped
the source catalog for everything downstream - sections, chunks, occurrence
traces and provenance - so the DOCX occurrence universe never reached the
result.

Now one upload, one lane. UploadedSource carries the type of the file the
caller actually passed, detected from its bytes: PDF by %PDF-, DOCX by a ZIP
container that really holds word/document.xml. The extension is a claim by
whoever named the file, not evidence, so a PDF called .docx is rejected at the
boundary instead of failing inside an OOXML reader. Analyst availability no
longer influences which format owns the result - it only ever described how
well a lane could reason.

PdfAuthority is therefore unreachable from this entry point, which accepts a
DOCX upload only. The case is kept and throws, so an injected policy cannot
silently produce a DOCX result labelled as PDF. A PDF upload needs its own
entry point; that is not wired here, and a PDF rendering of a DOCX belongs in
the cross-modal evidence channel CanonicalSemanticProductionInput already
exposes, reconciled against the DOCX occurrences rather than substituted for
them.

The old contract test declared its own copies of SourceCapabilities,
AuthorityRoute and the policy inside the test namespace and asserted against
that copy, so it protected nothing. It is replaced by tests that use the
production types, including the regression itself: a DOCX with a same-named PDF
beside it keeps DOCX authority.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Intent stays out of the authority pipeline. AuthorityExtractionRequest carries
exactly one member, the uploaded file, so the same file always yields the same
canonical document no matter what is asked of it. Asking for level-1 headings
must never send the model looking only for level-1 headings; the canonical
graph holds every real heading and the projection filters it. A test asserts
that structurally rather than behaviourally, so a field carrying intent added
later fails at the boundary instead of quietly making canonical truth depend on
the question.

UploadedFile computes DetectedType from the bytes and Sha256 from what was
actually read. The client cannot set the type, and OriginalFileName is display
and download provenance only - a caller able to declare the type could send a
PDF down the DOCX lane by claiming it was one.

CanonicalExtractionDispatcher sends a file to the extractor that owns its
format and nowhere else: no sibling lookup, no working directory, no discovery.
Each upload is its own job. The PDF lane is registered as not-yet-wired rather
than left absent, so a PDF upload fails with the real reason - every entry into
PdfLayoutEvidenceOutline still calls FindSiblingPdf and cannot run from a PDF
alone - instead of the misleading "unsupported format".

ExtractionIntent v1 is deliberately small: three tasks, an optional verbatim
instruction, a requested field list, and an output format. CanonicalProjector
only selects and reshapes; a field the canonical document does not carry is
refused with the available list, never guessed, so no projection can quietly
become a second extractor.

DocumentExtractionProvenance now records an ExecutionContract, defaulting to
EXPLICIT_UPLOADED_DOCX_CANONICAL, with LEGACY_AUTO_DISCOVERED_PDF_ROUTE named
for results produced when authority came from a PDF found on the filesystem.
Those two cohorts answered different questions and must not be compared as one.

S2 criteria recorded for the thirteen remaining FindSiblingPdf call sites, with
the test being whether the PDF lane can complete from an uploaded PDF alone.
None classified, none removed: the helper's corpus-path rewrite and working
directory walk are class C by construction, but the call sites need individual
classification before anything is cut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moves the prompt, segmentation, contract parsing, placement pass and the
canonical text model out of the DOCX adapter into CanonicalSemanticEngine, and
makes structural materialization take a format-neutral occurrence - identity,
reading order, exact text, optional style name - instead of a DOCX paragraph.

Pure extraction, no behaviour change: the frozen replays still produce 23
proposals, 23 validated and 23 canonical on DOC-0256, which is what would break
first if the prompt, the segmentation or the binder had shifted.

The reason to do this before adding a PDF lane rather than after: a duplicated
semantic stage is free to drift on level derivation, parent wiring and emission
- the three things this pipeline has spent its whole history getting right -
and the drift would surface only as an unexplainable metric gap between a DOCX
and the same document as a PDF.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An uploaded PDF is now extracted from that PDF and nothing else. It reads the
PDF's own text layer, groups positioned lines into source blocks, and runs the
same semantic stage the DOCX lane runs - the shared CanonicalSemanticEngine, so
prompt, segmentation, contract, binder, hierarchy resolver and placement pass
are identical by construction rather than by discipline. Only the source parser
differs, which is the only thing that should.

Two uploads are two documents. A PDF result is never compared with, corrected
by, or merged into the DOCX of the same material; that is something a user
would have to ask for. A test asserts it on a pair that actually sits side by
side in this repository - the exact arrangement the old sibling lookup
exploited - and checks that no DOCX source identity appears in the PDF result.

The source universe is every text block, not a filtered candidate set. Blocks
are grouped with includeRiskLines so a repeated line, a header-zone line or a
table-like line remains an occurrence the model may judge, with the annotation
travelling as evidence. Excluding them would have rebuilt, in the new lane,
precisely the hidden gate the DOCX lane removed in 3886a5a.

PdfCanonicalSourceExtractorNotWired is replaced by a real extractor, and
EXPLICIT_UPLOADED_PDF_CANONICAL joins the execution contracts so a PDF cohort
is never pooled with a DOCX one or with the old auto-discovered-sidecar runs.

Not wired to the legacy PdfLayoutEvidenceOutline: that lane asks the model for
span boundaries through ResolveHeadingSpansAsync and still discovers companion
files from the filesystem. The thirteen FindSiblingPdf call sites remain
unclassified under S2 and this lane does not depend on any of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Keeping risk lines in the source universe was only half the invariant. Once
includeRiskLines let them through, CanMerge saw nothing but PdfLine geometry
and would happily fuse a repeated running header into the heading beneath it
whenever font, left edge and spacing agreed - which for a running header and a
heading they routinely do.

That trades one fault for a worse one. A deleted occurrence is at least
visible as missing; a fused one produces a source occurrence whose text no
heading actually has, and the model is then asked to find a heading inside it.
That is an artificial partial-span problem manufactured by the harness, in
exactly the class I8 has not fixed yet.

Grouping now carries the annotation through instead of working on bare lines,
and refuses to merge across a risk-to-clean boundary. The classification is
read from what the filter produced and never re-derived here: a second
derivation could disagree with the first, and then the block boundary and the
evidence attached to it would describe different things.

So a risk classification may steer attention, routing, context and evidence,
and may not delete source text, fuse a risk occurrence into a clean one, or
change semantic truth.

Verified by removing the guard: three of these tests fail without it - page
number kept separate, repeated header not merged into the heading, table-like
line leaving the heading text intact - plus the mode-distinction test. Two
controls stay green either way, one checking that clean lines still merge so
the guard is not simply disabling grouping, one checking no parser line is lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zero provider calls. Requests are captured at the provider boundary, fully
formed; each segment's packet is built from the source evidence and its own
ownership window rather than from a previous reply, so the empty stand-in reply
cannot have shaped what was captured. The placement pass is deliberately not
captured and the artifact says so: it fires only on unresolved headings, and an
empty reply produces none.

Structural checks pass. 1065 parser lines become 1013 blocks, 1013 catalog
units and 1013 aliases, each owned exactly once across nine segments with no
gap and no duplicate. All 559 risk blocks are singletons and none is fused with
a clean line. No coordinate name appears in the output schema. Two runs produce
identical prompt and payload hashes.

Three findings stop the canary.

P1, blocking: the canonical PDF source text is the un-repaired parser string.
275 of the first 400 blocks read like "M I N UTES OF TH E I NTE RNATIONAL". The
contract requires verbatimText to be an exact substring of the source, so the
model would have to echo that character for character, and any PDF gold written
in readable text could never match. HeadingReadable is not the fix - on this
same document it produces "hybridmeetingon" and ",,,.". PdfLine.MatchText reads
correctly but is per line and is currently matching evidence, not source
identity. What "verbatim" means for a PDF is a contract decision, not a quiet
repair, so nothing was changed.

P2: layout geometry is handed to the model in the field the DOCX lane uses for
author declarations - style carries Left 169.477678125 where DOCX carries
StyleId and OutlineLevel. A declaration is a claim about meaning and a
coordinate is a measurement; sharing a slot invites positional reasoning
exactly where this architecture has worked hardest to exclude it.

P3: the Session family is split across five segments, with the annex restating
the series in the last. This is the DAY 1 failure mode and worse, so a canary
now would measure segmentation asymmetry and PDF semantics together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…style

P1. A PDF has no text. It has ordered glyphs with positions, and every string
is a reconstruction, so "verbatim" cannot mean whichever string a parser
happened to concatenate first - on this document that string reads "M I N UTES
OF TH E I NTE RNATIONAL", and a contract requiring an exact substring of it
would be requiring the model to echo that.

PdfSourceTextProjection declares the canonical one: the same ordered glyphs
with word gaps decided by geometry alone, carrying RawParserText for audit, a
SpanMap back to the glyphs, and a version, because a stored span only means
something under the rule that produced it. It repairs spacing and nothing else
- no dictionary, no spelling correction, no guessing at words - so a wrong
reconstruction stays an auditable parser defect instead of becoming a semantic
error blamed on the model. The map is recorded while the glyphs are in hand
rather than re-derived by aligning two strings afterwards.

HeadingReadable is rejected as the basis: B measured it producing
"hybridmeetingon" and ",,,." on this same file.

A second defect surfaced while fixing the first. Two paths build facts for one
occurrence, and changing only the catalog would have shown the model the raw
string while the binder validated the projection - every proposal failing as
non-verbatim, looking exactly like the model getting the text wrong. Both are
aligned now.

P2. The style slot carried Left 169.477678125 where the DOCX lane carries
StyleId and OutlineLevel. A declaration is a claim about meaning, a coordinate
is a measurement, and sharing a slot both mislabels them and invites positional
reasoning where this architecture works hardest to exclude it. Style now
carries Bold, Italic, RelativeFontSize and LineCount; numbering is omitted
rather than reported as three nulls; geometry stays on the facts for the
harness.

A third defect surfaced there too: the first attempt read weight and size from
ObservedEvidence, which never carries them, so all three fields were constants
- geometry removed and nothing put back, on the format that depends on format
most. They are parser facts now, and they discriminate.

Total request characters fall from 452,511 to 331,481. Structural checks still
pass and the packet is still byte-for-byte deterministic. P3 is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three packet sets, one variable each, so a later measurement can be attributed.
No combined arm: applying I7 and I8 together and finding an improvement would
tell you nothing about which one produced it.

B0 baseline    prompt 8b056f, 2301 chars, payload 328,601, 0 segments carrying
               open structure
B1 I7 only     same prompt hash, payload 331,541, 8 of 9 segments carry it
B2 I8 only     prompt dae4842, 3388 chars, payload byte-identical to B0

I7 sends the structural state already open where a segment begins, taken from
the parser's marker-based stack, which was computed already and never sent. It
reports what was open, not what anything's parent is - a test asserts no entry
contains a parent claim or a level. Segment 0 carries none, because nothing is
open at the start of a document, and that is asserted too.

I8 says a heading may be one exact contiguous substring of a single owned
occurrence, and fences it: same alias, copied character for character, no
normalizing or repairing, no offsets, and two separated pieces of one
occurrence are explicitly not a partial span.

Writing the tests found the clause incomplete. The real DOC-0256 occurrence is
"Africa Gregoire Mboya de Loubassou, African Development Bank", where "Africa"
occurs twice - once alone, once inside "African" - and the contract already
refuses to guess between them. Permission to return a partial span would have
been useless on the exact case it was written for. The clause now also tells
the model to disambiguate with occurrence or exact context, which the contract
has always accepted and the prompt has never mentioned.

Arm freezes are requests per arm, not calls made. A full run of one arm sends
nine semantic requests plus placement rounds; it is not one call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I8 came from this document and this document has an exhaustive semantic gold,
so it is the cheaper and better-grounded place to test the prompt change.
Proving it on the PDF lane would mean measuring a prompt and an unmeasured lane
at the same time.

D0 prompt 8b056f, 2301 chars. D2 prompt dae4842, 3388 chars. Every payload is
byte-identical between the arms, and aliases, source text, ownership,
segmentation and expected item counts are asserted equal segment by segment, so
the only thing that moves is the prompt.

All three targets sit in segment 0, so the whole A/B is two semantic requests -
one segment under two prompts - rather than the four a full-document run would
take. Requests, not calls made; nothing has been sent.

Two findings came out of locating them precisely.

Africa repeats three times inside its own occurrence and Asia and the Pacific
twice, so an exact substring alone cannot identify either. That is what the
disambiguation clause added earlier is for, and the manifest records which
targets need it rather than assuming none do.

Eurostat is not scoreable as gold currently stands. Gold spells it with a
hyphen; the heading in the source is spelled with an en dash. The hyphen form
does appear in that occurrence, at index 103, inside the body prose - so the
gold text matches a mention of the programme rather than the heading. If I8
worked perfectly and the model returned the heading as the source spells it, a
strict text join would still fail for this row. Recorded, not corrected: two of
three targets are cleanly scoreable and the third needs adjudication first.

Controls come from the frozen D0 replies rather than being picked by shape, so
the whole-span control is an occurrence the model demonstrably already proposed
entire, and neither control may be a target - the occurrence under test cannot
also be the thing holding it steady.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… never committed

These eight files have been running and tested since early in this work but
belonged to no commit, which is why two staging mistakes in this session swept
them up by accident. Nothing here is new; it is the work the later commits were
already built on.

ModelRelationHierarchyResolver separates two states that both end with a null
level. "model-out-of-hierarchy" is the model's decision that a heading holds no
position in the section tree - a title, a running header, a table label - while
"unresolved" is the absence of a decision and belongs in a review queue. The
resolver collects the out-of-hierarchy set before accepting any parent, because
such a heading may not be anyone's parent and the claim can arrive after the
child that names it; letting one act as a parent pushes every real section down
a level.

HierarchyResolution carries that reason through the projection to the output,
so a consumer can tell the two null levels apart.

SemanticConflictCensus counts what competing proposals actually disagreed
about, so the question "does this route need an adjudication model" is answered
from measured conflict rather than assumed conflict. On this corpus it answered
zero, which is why no adjudication model was wired.

HeadingLevelScorer and the score verb join on verbatim text rather than source
id and never print an accuracy without its denominator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ylight23 and others added 30 commits September 20, 2026 14:14
Every declared type and every public member in these had zero references from
production and zero from tests:

  Core/Architecture/ArchitectureBoundaryGuards.cs   49 lines, 3 types
      RequireProposalValidation, RequireSourceCatalogOwner and
      RejectDirectAuthorityMaterialization were never called. The boundaries it
      described are real and still hold - they are enforced by the code that
      owns them, not by this catalog.

  Cli/EvaluationProjectionBridge.cs                 63 lines
      ProjectDocumentOutline had no caller. It reflected an evaluation assembly
      into the CLI across the Eval boundary; nothing had crossed it in a while.

  Infrastructure/AI/ProviderContracts.cs            42 lines, 6 types
      ProviderTransport, ModelProviderDescriptor, ChatMessage, ProviderRequest,
      ProviderResponse and IChatModelProvider. A provider abstraction the four
      real classifiers never adopted - each implements IHeaderClassifier
      directly.

Two documents claimed these in the present tense and are corrected rather than
left to drift. current.md is an ACTIVE contract, so the sentence now describes
what the boundary is; final-architecture-r11-r15.md is pinned to a base revision,
so its statement stays and gains a note saying the type was removed afterwards
and why.

source-tree-hygiene.md lists all three as KEEP and is left alone: its own header
says it records what was true when written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
spikes/A99.LocalTesseractSpike explored local OCR over PDF page images as a way
to recover text the parser could not. It is not in DocxHeaderExtractor.sln, not
in the deterministic CI workflow, and nothing in src, tests, build files or docs
refers to it.

It also cannot build. Its project reference points at
src/DocxHeaderExtractor.Eval, which was removed during the source-tree cleanup,
so the spike fails on the first using directive. It has been unbuildable for as
long as that project has been gone, and nothing noticed - which is what an
orphan outside the solution looks like.

Deleted rather than repaired: the question it was asking is answered elsewhere
now. The PDF lane reads glyphs through a declared projection with a reverse span
map, and where machine text is genuinely insufficient the semantic freeze
records it as a modality hint rather than reaching for OCR. The spike remains in
history for anyone who wants to see what was tried.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six files whose every type was referenced by nothing but their own test, and the
cascade that fell out once they were gone. Deleting the first layer made the
next layer unreachable, and the scan was re-run after each build until nothing
was left but the CLI entry point.

Layer one - test-only surface:

  Repair/AutoRepairWorkflow.cs          455  a second repair implementation.
      Not the repair loop that runs: that is AgentRepairFeedback and quarantine
      in the harness, and it reaches the lanes through the extraction tool.
  Infrastructure/AI/{OpenRouter,NvidiaNim}VisualQuestion.cs, SglangVlmImageQuestion.cs
      three visual-model adapters no host ever constructed.
  Application/Tasks/TaskRetryExecutor.cs  its two tests move out with it;
      RetryPolicy stays, because TaskContracts and TaskPlanCompiler use it.
  Pipeline/IEContextProjection.cs       the IE consumer projection.

Layer two - unreachable once layer one went:

  Projection/DocumentConsumerProjectionSupport.cs  only IEContextProjection
      called it.
  Core/Models/DocumentConsumerContracts.cs         all seven types, including
      FactExtractionContext and the retrieval records, were reachable only
      through that projection.
  Vision/IVisualQuestion.cs and Vision/IPdfVisualQuestion.cs
      interfaces with implementations and no consumer. Nothing ever took one as
      a parameter or held one in a field; the visual question was asked nowhere.
  Infrastructure/AI/VlmImageQuestion.cs            the last implementation.
  Repair/{AuthorityRepairOutlineRunner,IRepairOutlineRunner,RepairCandidateRunner}.cs
      the runner chain AutoRepairWorkflow was the only way into.

PdfVisualAttemptOutcome is rescued rather than deleted. It sat beside the visual
interfaces, but RouteExecutionAudit.PdfVisualRecoveryTrace is typed on it, so it
moves to the audit contract where it is actually used - the same rescue the
cluster records needed when their analyst went.

LLamaSharp stays referenced: LlamaHeaderExtractor still uses it.

2,056 lines out, 13 in. Build clean, 933 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sixty-eight test files each carried their own way of locating the repository
root, under four names - RepositoryRoot, Root, RepoRoot, FindRepositoryRoot -
in two materially different shapes:

    while (!File.Exists(Path.Combine(dir, "DocxHeaderExtractor.sln")))  36 files
    Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../.."))  32 files

They resolve to the same directory today, which is why both survived. Only one
of them stays correct if the build output ever moves, and a suite where half the
tests find the repository by asking for the solution file and the other half by
counting directories is one that will eventually fail in a way nobody can read
from the failure.

TestRepository.Root() is the surviving rule: it asks for the thing it wants
rather than counting, and it answers once and caches. TestRepository.Path()
takes the forward-slash form the artifacts are named in.

Mechanical: the helper is removed and its call sites point at the shared one.
Every added line in this diff is a TestRepository.Root() call - nothing else
changed.

Fourteen files still reach for AppContext.BaseDirectory in other shapes: some
inline the walk inside the method that needs it, and WebUiScriptSyntaxTests
deliberately walks to a directory containing src/ rather than the solution file,
which is a different question. Those are left alone rather than rewritten into
something they do not mean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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