Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refactor the A99 canonical semantic path toward the agreed vNext architecture without adding heading heuristics.
Changes:
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.