Conversation
Records the design before the code, and splits build path A in two on the strength of what the write path actually does. `compile.py` has three write paths, not two: `merge→create` composes an article from scratch, while `merge` and `merge-batch` run against existing article text through prompts whose only actions are `append_to_section` and `new_section`. Carrying the document date and one block per source lets the create path state the newer version, and cannot help the merge paths at all — there is no action that retracts. So the ordering signal and the replace primitive fix different paths, and only the second can destroy correct content. A1 is the signal half: an optional `date` on submit written as frontmatter, the date read from raw frontmatter at write time, per-source blocks instead of one flattened bag, and newest-first budget allocation so truncation drops the oldest source rather than the newest. A2 adds the replace primitive and is bought only if A1 fails the fixture positives. A1 therefore does not satisfy D1 — its best output is latest-wins, with no superseded trail — and the spec says so, so a clean A1 score is not mistaken for the trail landing. Settles what design-options.md left open. The trigger is explicit contradiction only: under A1 the writer sees extractions rather than raw text, so a claim absent from a later source is indistinguishable from one the summarizer dropped, and triggering on absence would turn every lossy extraction into a false supersession. Dropped claims escalate to a report instead — already shipped for revised documents, new for two documents whose titles differ only by a version marker. D3 exposes the date on submit rather than inferring it, because ingest time is not authorship time and backfilling an older version would otherwise invert the order. D4 reads raw frontmatter at write time, a deliberate exception to the extraction layer's write-phase boundary, taken because a provenance field costs a `schema_version` bump and that refuses every existing file. D5 keeps `write_prompt_version` reported and never gated. Also flags that the fixture cannot yet score any of this: 38 documents in one compile routes each version chain into a single `merge→create` call, so it measures the path A1 can fix and never exercises the merge paths. It needs one compile per version. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…depend on Re-reading the spec against the code turned up four citations that do not match what is there, three of which change what A1 has to touch. The Background enumerated three write paths in compile.py and missed the create-ops loop at :492/:495, which calls create_new_article while the article file is absent and merge_into_article once it exists -- exactly what a version chain hits when its members arrive as separate create ops. The single-source merge pointer named :553, the branch head, rather than :558, the call. WP4 counted four call sites. Both writers take (extraction: ExtractionResult, source_path: str), so changing that signature reaches the three single-source callers too: compile.py:492, :495 and :558. Seven sites move, not four. WP1 named merge.py:95 as the only place the "- Source:" prefix is rendered. _estimate_full_extraction_size at merge.py:72 spells it out a second time, and BG3's truncation notice fires on the two disagreeing, so both sites gain the "- Date:" line together. The single renderer does serve both writers (merge.py:320, :596), so no write path is missed there. RT6 cited design-options.md:57-60 for the shared-helper argument; the passage is at :60-63.
A1 step 1 of the supersession spec: the write phase needs to know when each source was written, and no ingest route recorded it. The submit handler now writes a `date` into the document's YAML frontmatter, and the reader that parses it back is shared rather than duplicated. The two ends share no definition of the format, so the Go writer's rules are pinned against the real PyYAML reader from both sides. The scan is textual and answers "may this already be dated" rather than "is it": it errs towards yes, so a block it cannot read is left alone instead of having the ingest clock stamped over a date the document authored -- which is the corpus defect A1 exists to fix. Unreadable means flow syntax, a sequence, tab indentation, a colon without the space YAML needs, a top-level value the reader refuses, a `date` whose value is on the lines below it, or a character the reader breaks a line on. An explicit caller date outranks the document's own, so it is written even there, as a stacked block: inserting into frontmatter that does not parse would cost the document every label it had. `read_document_frontmatter` moves out of storage.index into _frontmatter so the write phase can share it (RT6), and widens two guards a raw document can actually trip: a date no calendar accepts, which took the whole document catalog down for every document beside it, and a leading BOM, which made a document read as having no frontmatter at all. `ContentHash` and `FileTitle` are still computed over the content as submitted, before anything is prepended -- hashing the written bytes would put the stamp time inside the hash and leave the 409 duplicate path unreachable. Verified by a round-trip differential over 68 shapes through both implementations: the clock never replaces a date the reader can see, every stamp reads back as a date, an explicit date always survives, and no input line is lost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A1 step 2 of the supersession spec. The write phase received one flattened extraction and one comma-joined source string, so two things were unrecoverable from its payload: which document made a given claim, and which document is the later one. An article composed from a plan and its revision could state both. `create_new_article` and `merge_into_article` now take `Sequence[SourceBlock]`, one block per source, each carrying the day the document dates itself. All seven call sites move together -- the create-op pair, merge->create, single merge and merge-batch in compile.py, and both branches of the pipeline's write phase, which build the payload once for whichever runs. `_combine_extractions` had no callers left and is deleted with its test file; the enumeration guarantee it carried (#41) is re-pinned as a block-rendering test, where per-source blocks keep it structurally rather than by field priority alone. Blocks come back oldest to newest, undated last, both orders path-derived so the payload is reproducible across a 16-worker run whose raw-scan order is not. Duplicates collapse on checksum (WP7), the survivor being first in path order; the collapsed path is then absent from the article's `sources:`, which costs the derived KB no content -- the bytes are identical to the survivor's -- only the second path's name. The ops bookkeeping still reads every merge's rel rather than the surviving blocks, or a collapsed duplicate would stay uncompiled and be retried forever. The date is read from raw frontmatter at write time (D4), a deliberate exception to the extraction layer's write-phase boundary: the alternative costs a `schema_version` bump, which refuses every existing extraction file. A document that cannot be read degrades to undated rather than failing the write. Stamps are narrowed to their day, because `datetime` is a subclass of `date` that refuses to be compared with one, and the corpus holds both. Budget allocation across blocks is interim and says so: every block is rendered whole when they all fit, and only otherwise is the budget split evenly with the unused share carried forward. BG1's newest-first priority replaces it in step 3. The first version split unconditionally and truncated an 11-member enumeration out of a payload with 10,560 characters of budget unspent. Two of the three write paths already guaranteed in code that every source reaches the article's `sources:` key. The full rewrite did not, and a source missing from that key is a document `derive` refuses to copy, so its user message now renders the same list. No `write_prompt_version` churn: user messages are outside that hash by design, and the prompt file's singular wording is step 4's. Verified by 26 mutations of the new rules. Three survivors were adjudicated, not waved through: two are equivalent given the path pre-sort and a stable sort, and the third does not overrun the budget because the separators are deducted before any share is computed. Three real survivors were closed with tests -- both bookkeeping paths, and the two single-source sites building blocks without reading the date at all. Plus a both-routes parity test (VF6) comparing whole blocks from the CLI and the worker in one process, over documents dated against their path order. Rebased onto #45, which landed the grounding constraint on the same three write prompts. Its three new prompt-parity tests called the flattened signatures this commit replaces, so they now pass one block each; the constraint they assert is untouched. And `_sources` in the new grounding checker documented its comma-splitting as covering the `", ".join(rels)` that `create_new_article` used to be handed, which no longer happens -- the splitting stays, because the rewrite path's frontmatter is model-authored and articles compiled before this change carry that joined string, but the docstring now says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A1 step 3 of the supersession spec. Step 2 split the block budget evenly between sources with the unused share carried forward, which is the wrong policy for the thing this increment exists to fix: whether a claim has been superseded is a question about what the latest document says, and an even split cuts the newest source to fit the oldest. Budget is now claimed in `_budget_priority`'s order -- dated blocks newest first, then undated ones in path order -- while blocks still render oldest to newest (WP5). A block is kept only if it fits whole in what is left, separator included, and the first one that does not ends the walk, so what is dropped is whole blocks from the bottom of the queue (BG2). A block cut to its header and a halved list is worse than an absent one: the writer cannot tell a thin source from a truncated one, and a partial enumeration is what it turns into a confident wrong list (#41, #42). A budget too small for even the first block is spent truncating that one by field priority rather than sending the article and no new information (BG4), and every source that contributed nothing is named on stderr (BG3). The queue is deliberately not the render order reversed. `build_source_blocks` emits dated blocks then undated ones, so reversing hands the highest priority to the sources that make no recency claim: measured on two 3,050-char blocks against a 3,123 budget, the undated block survived and the 2021 document was dropped -- BG1 inverted on a guess, on a shape the spec provisions in three places (RT12 leaves the files route undated, WP8 makes an unparseable date undated, and a raw file that cannot be read degrades to undated). The spec said it too, so BG1 now states the queue, BG2 stops calling the dropped blocks "trailing" -- true only while every block is dated -- and BG4 names the first block in that order rather than "the newest". Sources sharing a day break to path order, the same direction the undated ones take, and both keys are read off the blocks rather than inherited from the order they arrive in: 160 of the reference KB's 395 multi-source articles have two sources dated the same day, so that tiebreak decides real drops. `_fit_block_to_budget` rejected a budget of exactly its header, which this step's own arithmetic is what made reachable: an extraction whose eight priority fields are all empty estimates at its header, so a block measured as fitting whole came back without its `- Date:` line, silently, with the rest of the budget unspent. WP6 reads an absent date as "makes no ordering claim", which is the opposite of what that document says about itself. One character, and a test on the boundary. A dropped source is still named under the article's `sources:`, now pinned rather than only described. `derive` reads that key to decide which raw documents reach a derived KB, so dropping the name would cost the document its only route into one. The price is an article naming a source the writer was shown nothing from, and on the reference corpus that is not rare: modelling a full compile through the real budget arithmetic, 152 of 577 measurable articles (46.6% of the multi-source ones) drop at least one whole block and 29 hit BG4. That is BG2 working as specified -- the relief is fewer sources per write op or a larger prompt budget, neither of which is A1 -- but the figures belong on the record before FX4 scores anything. Verified by 19 mutations of the new rules, 18 caught. The survivor -- clamping a whole-fitting block to the budget minus its separator -- is equivalent because that line is only reached when the block already fits, which is a claim the header-sized-budget fix above is what makes true: 4,000 random block shapes at six budgets from the estimate upward, 0 that did not render whole. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Step 2 gave the payload one dated block per source, oldest to newest with the undated last, and step 3 made the newest one survive a tight budget. Nothing said so to the model. A sequence whose meaning is never stated is one it may read either way, and the undated tail is the trap: those blocks sort last for reproducibility, not recency, so an unexplained order invites reading the sources that make no recency claim as the newest material (spec WP6, S5, Q2). _SOURCE_ORDER states the direction, what a `- Date:` line dates, and that an undated block's position carries no ordering claim. In the system prompt because that is where an instruction is applied reliably, and a code constant appended to all three write-stage prompts for the reasons _GROUNDING already carries: one of the three is built in code, and a fourth prompt file would fail an operator's existing KAAS_PROMPTS_DIR on the first write after upgrade. It states facts and asks for nothing. A1 carries the ordering signal and adds no action: the merge paths cannot retract, and the rewrite path must not start, since it returns a whole article and is the one place A1 could destroy correct content (NG1, G4). What to *do* about a contradiction is A2's replace primitive, and saying it here would also spend the fixture arm that exists to decide whether A2 is needed at all (FX7). Two approximations in what the statement asserts are recorded rather than closed: BG2 drops whole blocks the `Sources:` list still names, and the bare-source-line branch can emit a dated block's header without its date -- which now reads as a claim rather than as missing text. Editing every write-stage system prompt moves write_prompt_version, so the first report after this names every article (PV1). It gates nothing per D5, and the argument for keeping it ungated is unchanged: an article that exists is re-composed through the merge paths, which are still the additive ones. Both copies of that reasoning are updated -- write_prompt_version's docstring (PV2) and storage/lag.py's, which ended on the same pre-A1 sentence. Also corrects the singular-source wording both writers inherited from the flattened payload. merge-rewrite.md asked for "source" where the payload now lists N of them, and the create prompt's frontmatter template shows one `sources:` item; neither path repairs that list in code, so a source missing from it is a document derive will not copy into a derived KB. py 1661 passed, core/merge.py 100%.
Shape A is already reported: a re-fetched document is marked revised and its articles named. Shape B -- v1 and v2 ingested as two documents, which no `id` connects and only the title betrays -- was not reported at all, so an article holding both versions looked like an article holding one document twice. The rule is the one test-set.md validated on 996 documents: same title after a trailing version marker is stripped, same `source`, different `id`. Its two exclusions come from that corpus rather than from taste. Cross-source collisions are excluded by making `source` part of the grouping key, so a design document and the recording of the meeting about it are not paired while two recordings of one meeting still are. A title that is a person's name groups with nothing, because a recurring one-to-one is titled that way; the names come from the KB's person pages and from the configured allowlist, since the pages are stubs a later phase writes and a first compile has none. RP3's stated trigger is dropped, and the spec now says why. "The earlier member asserts something the later one does not" selects nothing: over the corpus's stored extractions a claim-text comparison fires on 35 of the 35 checkable pairs at every threshold from 0.55 up, and the similarity distributions do not separate real version pairs from recurring meeting series (median 0.47 against 0.36). Claims get restated in new words between versions, so absence is the normal case. The cheap proxy fails the other way: "the later member asserts fewer claims" holds for 14 of 35 and excludes P6, the one adjudicated success, whose claim count grows. An LLM judge would settle it and RP5 forbids one, so the report names the group and leaves the reading to a human. A recurring series satisfies any title rule -- 37 of the 41 (article, group) pairs on the corpus are one, against 4 real chains -- so the version marker is reported as a triage key and marked groups are listed first. It is not a filter: P7, P8 and P9 are positives whose two versions share a title verbatim. Nothing here reaches a prompt (RP5), and that is structural rather than reviewed: the rule lives in storage/, core/merge.py is asserted over its imports not to reach it, the writer's entry points take source blocks and nothing else, and the report is built after the last write op. "Share an article" spans this run's ops and the paths already under the article's `sources:`, because the staged fixture compiles version N into the wiki version N-1 produced, and a report built from one run would be silent on exactly the merge paths A1 is measured on. Moves the date narrowing out of core/merge.py into the shared frontmatter reader. The report orders a group's members by the same dates the writer's blocks use, and two narrowings that disagreed would order them opposite ways. Also corrects what the corpus actually says. Reconstructing the rule showed the throwaway script that measured it missed a three-part `v1.0.0` marker and compared titles case-sensitively, so shape B has 42 matching titles rather than 40, leaving 38 groups after the cross-source rule and 37 after the person rule. And the migrated extractions are schema_version 1 where the loader now reads 2, so FX3's label drafting needs them re-serialized first -- free, but not free of work. py 1701 passed, storage/lineage.py 100%, commands/compile.py 99%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rsion FX1 asked for the /tmp/supersession/ scripts with tests. Two of the three were one-shot migrations that already ran against a gitignored KB with absolute paths baked in, and have since been deleted; porting them would mean owning tests for code that cannot run again. What lands is the two that do run again: select_cases.py finds the lineage chains and stratifies them, stage_fixture.py builds FX2's stages. test-set.md's Regenerating section says the conversion is not reproducible instead of pointing at /tmp paths that will vanish. The selector had to be rebuilt rather than moved. The script that measured the corpus is gone and only cases.json survived, so each rule was reconstructed against that file: all 79 non-identical chains' diffstats and all 134 strata now reproduce exactly. Getting there recovered four things the numbers in test-set.md depended on and did not state. The diff runs over the body, never the frontmatter -- every version differs in date, id and checksum, which is three changed lines between two files whose prose is identical, enough to move a group out of the noise stratum. The line counts include the break that ended the closing delimiter, because the original split on the delimiter rather than on lines. The append-only test runs before the similarity test, since a large enough append drives similarity below 0.55 on its own and testing similarity first put two negative controls in the positive set. And `sources:` entries holding a comma-joined batch are split apart, which is what the shared-article column had been undercounting: 43 duplicate-stratum groups against the 34 recorded, 6 append-only against 4, on a corpus where 46 articles carry such an entry. Shape B goes through storage.lineage, exclusions included, so the selector reports 131 chains where the old file has 134: it drops three cross-source pairs and a set of one-to-ones, and adds the v1.0.0 chain the original's marker missed. Corrects the strata table, and 101 of 675 articles at 15% becomes 123 of 682 at 18%. Staging is what puts the merge paths under test. One version per stage, materialised into the KB the previous stage wrote, so from stage 2 onward every write is a merge into an article an earlier version composed -- on the fixture that plans four stages of 18, 18, 1 and 1 documents, the last two being P4's four-version chain. The script prints the compiles rather than running them: a pass costs about 10 USD and each arm pays it again, so spending stays the operator's decision. FX4's baseline arm needs the pre-A1 code, which is no longer checked out, and both the script and the spec now say it runs from a worktree at bd8252e. Verified without spending: the selector finds exactly 18 chains in the fixture, which is its 10 positives, 4 negative controls and 4 duplicate controls; two staged materialisations then pass `kb-ai check` with 36 match, 0 missing, 0 mismatched. py 1740 passed, scripts/ 99% and 98%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ontradiction and drop The test set's single `superseded` list conflated two failures that A1 is not equally responsible for. Q1 scopes A1's trigger to explicit contradiction and sends dropped claims to the RP1-RP3 report, so a single list would have judged A1 against work it does not contain. Split it into `superseded-contradiction`, which gates FX7, and `superseded-drop`, which is measured on the same runs and gates nothing. Draft the labels for P2-P5 and P7-P10 in labels.md: 58 contradictions, 36 drops, 48 controls, of which 38 contradictions are still stated as current today. Two consequences are recorded rather than buried. P1, the one adjudicated failure, is a drop case and no longer gates the work it motivated. And the fixture needs no re-serialization: the schema check lives inside `parse`, which only `extraction.load` calls.
Both blocking rulings from FX3's label drafting are settled, so the labels are down to item-by-item confirmation. P2 is withdrawn from the positives and kept as the documented wrong-date counter-case. Its file dated 2026-04-14 is a rolling document carrying a `# 2026-05-04` body section and an `extracted_at` three weeks after the file it supposedly precedes, so the chain runs backwards relative to its content. Scoring it would credit A1 for asserting an order that is wrong -- the direction that flatters the work. The drafted label is retained verbatim, in the fixture's direction, along with the inversion to apply if the case is revived, because P2 is the corpus's only inverted chain. P7 keeps its 8 contradictions: the measurement-time reading is accepted, decided on the article's own behaviour rather than on a preference between two defensible readings, since the article flattens the weekly table into undated current-state prose and that is exactly where v1's figures become wrong. The scoring set is therefore seven drafted cases plus P6, carrying 50 contradictions, 31 drops and 42 controls, with 31 contradictions stated as current today. Records the design gap P2 exposed as an A2 question rather than leaving it in the label file: a frontmatter date can be wrong and not merely missing, which is a case Q2 and D3 did not provision for, and A1 has no way to notice because the writer is told a date and never the text it came from.
Subtracting P2 left the drop count at 31 where the seven remaining cases carry 30 (5+4+5+0+6+5+5). The contradiction, control and staleness totals were right.
…cation Objective corrections only; the disputes those audits raised are recorded separately once every case has reported. P3: D5's quote is at v1 L1113, not L1114, which is a closing tag. K3's v2 cite moves to L97/L109, because L28 asserts source-to-structured-docs without the AI authorship the control is about. D1 is narrowed to the repository count -- v2 restates the multi-team half at L20, so the original compound row was half not a drop -- and its stray fifth cell is removed so the row renders. P8: the relocated-list cites for MCP and AI Coding adoption are v2 L291 and L311, eight lines off as drafted. The intro said four rows move to `架构` when two move to `架构 / api 团队` and `架构 / 效能`, so it now says they replace named individuals with team names. And the drop table claimed all six drops are stated as current when D3's headcount is absent from the article -- only its 25%/two-month clause survives.
K2's v2 range is L53-62; L54-63 as drafted started one line late and omitted the `Fee Conversion` label cell. C5's replacement said "5 capital-protected Earn products" when v2's own table at L333-384 spans Earn, Margin Staked SOL and Spot X, so it overstated the narrowing -- v1's spot leg is partly retained and only RWA and the horizontal-comparison framing are genuinely gone. Neither change touches a classification or an article verdict.
P7's verification found that the article's fifth source, the 04-14 infra biweekly that is also P2's v1, asserts v1's entire 0430 column verbatim. So 7 of P7's 8 staleness observations have a second possible cause, and its headline was wrong on both halves: C4's and C6's replacements are in the article, so 5 of 8 corrections were lost rather than 8 of 8. Generalised into a note, because P10's judgement call 5 had already caught the same shape for one case. Only P3 and P8 compile from their chain alone; P5's article merges 16 sources, which is the strongest reason not to read its 0-of-5 pass as evidence about the pipeline. The bound matters as much as the finding: FX4 already re-runs the compile over the staged fixture rather than trusting the existing wiki, and only P5 and P7 carry a co-source into that run. So the 62% figure is motivation, not measurement, and FX5 needs staleness defined against the newest source in the compile set. Also corrects C2's article evidence (L57 sits under a `Completed (Apr 30)` header, so the row rests on L480 alone), C3's (v1's 13.3% is absent, not present -- the article recomputes ~27-34%), and C6's (L192 is explicitly `as of April 30` and carries the replacement scope in the same sentence).
…ruling queue All 128 drafted items across the seven scoring cases were re-derived from the sources in fresh contexts, one per case, with no access to the drafting reasoning. Result: 111 verified, 4 line-corrected, 13 disputed, 0 unverifiable. The quoting held up well -- only four cites had drifted and no quote was invented -- and the findings concentrate in classification and in the `Article today` column, which is the one that scores. labels.md gains a verification section with two tables: 15 objective corrections already applied in place, and a queue of 17 items needing a Captain ruling, ordered by how much each moves the measurement and each carrying its own location, finding, recommendation and consequence. Three would move the totals -- P8's four contradictions collapsing as de-specifications rather than contradictions, P9-C1 being preserved by v2 rather than superseded, and P10-C6 comparing a mean against a median -- taking the set to 44 contradictions with 27 stale and six gating cases instead of seven. test-set.md and spec.md now point at that queue instead of implying 122 unchecked rows, and FX3 records that FX5 needs staleness defined against the newest source in the compile set.
系統户 for 系统户; the traditional form appears nowhere in the fixture.
The P10 verifier's full report flagged that C5's delta is largely a basis artifact, and I confirmed it by comparing all 24 monthly values: v2 L378 declares its median series >120-filtered, but its twelve monthly medians (L51-62) are identical in all twelve months to the full report's UNFILTERED column (L89-100), not its filtered one. So C5 compares v1's <=90-filtered 13.8/17.8 against v2's unfiltered 15.3/19.8. The row survives -- v2 declares the same span and statistic, so it contradicts on its face -- but it must stop being cited as evidence of real slowdown, and it strengthens the case that FX5 has to state measurement basis. Queue is now 18 items, 13 of them disputes; the rest are corrections and one spec change. Also records A16, a quote boundary in P10-C4.
…t finding The P4 verifier's full report surfaced one thing I had not captured: v1 asserts both the monthly fund-flow table and its replacement, two lines apart (L1761 uta_liq_trans_log_202605, L1763 translog_realtime). Confirmed independently -- the measured token is clean, 1 hit in v1 and 0 in v2-v4, but translog_realtime already has 2 hits in v1. L1763 is also the line control K4 scores on. Queued as V18. That makes four instances of one shape, so it is now a note rather than a per-case remark: P4-C3 in v1, P4-C8 and P4-C7 in v4, and P2's rolling file asserting both 100% and 90%. Only C7's was documented when drafted. Every contradiction row is phrased 'the earlier version asserts X, the later asserts incompatible Y', and that is stronger than these documents support. The practical consequence is for FX5: an extractor reading the later version alone can legitimately emit the older claim, because the later version still makes it. P4-C8 is the sharp case -- grading the article stale would fail a pipeline for repeating what v4 itself says in five places. So a stale verdict needs the later version to be unambiguous, not merely to contain the correction somewhere.
…ass did not cover The 128-item pass covered only the seven drafted positives. The controls and the adjudicated cases were never checked, and the controls are what protects A1 from being wrongly FAILED: if a chain labelled purely additive actually contains a contradiction, A1 reporting it correctly scores as a false positive. All eight hold. N1's only non-carried body line is its own H1 (a retitle, not a contradicted claim), N2 is an append-at-top rolling log whose shared 0428 section is unedited apart from extraction artifacts, N3 and N4 differ by one blank line. U1-U4 all confirmed: bodies byte-identical after stripping frontmatter and checksum fields matching exactly, with only date differing; U1 spans a six-week gap. P1 verified exactly as written -- v2 has 0 hits for both 现有情况 and 140, and the article has precisely four occurrences at L39, L44, L241 and L702 with both quoted strings verbatim. Notably P1 is NOT confounded: none of its nine co-sources asserts the ~140 figure, the only other hit being a substring of a numeric id. So the motivating case's evidence is cleaner on that axis than the drafted positives'. Two new findings. N2 carries P2's wrong-date pathology -- a file dated 04-17 holding 0422 and 0428 sections -- making it a second named fixture instance, though it does not break the control because the pair still orders correctly. And corpus-wide, 91 sources entries comma-pack multiple paths into one YAML item while 22 articles duplicate a source path; recorded against FX4 as a check for both arms, since G2 is the claim that a source is an attributable unit. The seven scoring cases contain none, so the co-source table stands.
P6 is the only adjudicated success in the set, and its explanation is load-bearing for the build decision: if the failures cluster on documents lacking an internal version marker, path A is the whole fix and paths B and C are over-buying. It holds as written. Both framing quotes are where the doc says, the article leads with the v1.7 framing and names v1.5-v1.7, and the superseded framing is absent in both languages -- 0 hits for 重量级 and 0 for `heavyweight`, which matters because grepping Chinese against an English article would otherwise have passed vacuously. Two precision fixes: the v2 quote drops the source's own bold markers, and the scope widening has a cleaner second location the adjudication never cited (the 定位 line, where v2 adds 整合 bgw / bgwg / bgwtp / LiteLLM). Also checked the one thing that could have read as stale -- the article repeats BGW's problem list, but v1.7 keeps that six-row table under a new per-gateway subheading, so v1.7 subordinates v1.5's framing rather than retracting its detail. P6 is not confounded, like P1: three sources rather than the chain's two, and the third asserts none of the framing and carries no version or date marker at all. It is not staged in the fixture, so FX4's re-compile sees two of three sources and cannot reproduce §14 -- score P6 on the framing, not on completeness. The finding lands on the design lesson rather than on the case. P6's v1 contradicts itself about its own version: title and H1 say v1.5, the body marker says v1.6. The accidental ordering signal the success is credited to is present in both documents but self-inconsistent in the earlier one, so a reader taking the title sees v1.5->v1.7 where a reader taking the marker sees v1.6->v1.7. The case stands, and the finding argues for path A rather than against it. Recorded as a fifth instance of within-version ambiguity, the first about a version rather than a claim. Verification coverage is now complete: every case, control and adjudicated case has been re-derived from the sources. Also fixed a dead anchor left when the section was renamed to three adjudicated cases.
…rulings V16 asked whether the compile read a source P5's article does not declare, or whether the article's source list is incomplete. No artifact answers it: the compile state records no target article, no classify cache survives, and the list itself is model-written on the rewrite path. Resolved on content instead. None of the 68 lines the hardening plan alone asserts reaches the article, and two residues at issue are single corpus hits in v1 -- including D5's own dropped clause -- so D2-D5 score as lost drops and the fixture has no provenance gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rontmatter Re-measuring the source-list defect for V16 found a worse one beside it: seven of the 682 articles do not open with a frontmatter delimiter, because the writer's preamble prose stands above it, so no key reaches any reader -- no title, no date for WP2's signal, and no sources for derive to copy, which fails G2 outright rather than mis-shaping it. Nothing catches it today: check counts documents against extractions and skipped all 682 on the schema-version gate. Also withdraws the 22 duplicated-path articles figure, which reproduces under no reading tried, for 21 literal and 30 after splitting comma-packed items, with the definitions attached. The 91 comma-packed entries reproduces exactly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ions Captain accepted V1, so P8 contributes no contradictions and leaves the gating set: six drafted cases plus P6, totals 46/30/42 with 29 stale. Two arguments the queue row did not have decided it. v2 asserts the mapping the rows read as a substitution -- it writes Lucas (架构) and hangs @lucas.wan on the whole infrastructure heading -- and the article writes both sides anyway, so under either staleness rule it is not asserting a superseded value. v2 also keeps person names in its own team column and still names Victor / Lucas elsewhere. The rows stay as a re-attribution list with their evidence and their IDs retired, because they are the set's clearest instance of a change that reads like supersession and is not one. P8 keeps its 6 drops and 6 controls, which is worth having: it is the one case with no accidental ordering cue at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Captain accepted V2. v2 keeps both halves of v1's two-part end state, at L175-178 and L184, and L184 is already the line control K1 is scored on -- so the label set had one passage both preserved and superseded. Checked at ruling time that the stale verdict was wrong on the facts too: the article's two-component framing is what v2 asserts as well. The substance goes into K1's evidence rather than a new control, since K1 already covers the second half, and the three-pillar restructure joins call 7's additive list as a taxonomy on another axis. Totals 45/30/42 with 28 stale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…diction C6 paired v1's mean development duration against v2's median 研发周期, which v2 defines as development plus test: different statistic, different span. Two checks at ruling time closed the alternatives. v2 does carry a same-scope development duration as a median (1-3 to 4-7 days), compatible with v1's mean and predicted by v2's own note that the mean runs high on a long tail. And v1 reports that duration only as a mean, so there is no same-basis pair to re-cut the row onto. Recorded as drop D6 and as a lost drop, since the article still carries the mean framing. V12 lands in the same item: v2 does report means elsewhere, so that claim is now restricted to development duration. Totals 44/31/42 with 27 stale, which is the arithmetic verification predicted for V1 through V3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five of the nineteen items are settled -- V1, V2, V3 and V12 ruled, V16 resolved by investigation -- so the queue heading and preamble now say 14 open rather than 18, and name which five of those still move a scored number. Also corrects the staleness rate to 61%, which is what 27 of 44 reads after V3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…urces V19 asked for staleness to be restated against "the newest source in the compile set". Ruled accepted, tightened to the newest source *that speaks to the item*: read per document the criterion clears an item whenever the latest source is merely silent on it, which is the common case. Ruling it established two things the item did not claim. Its own rationale does not hold. V19 invokes V4, but the wording only clears an observation where a co-source is newer than the chain head, and in the fixture none is except P5's -- P7's is 2026-04-14 against a chain head of 2026-05-14, P10's 2026-03-05 against 2026-03-06. So it answers V4 in no case, and the co-source note no longer implies a definition can repair that confound. P5 cannot be scored on the column at all yet, which is now V20. Both of its versions carry date: 2026-03-13; build_source_blocks breaks the tie on the path while _SOURCE_ORDER states "blocks run oldest to newest" caveated for undated blocks only, and since "-" sorts before "." the payload renders the v3 file first and thereby asserts that v1 is the newest source. The article carries v3's figures, so against the stated order P5 reads 5 of 5 stale rather than 0 of 5. It is held out of the gating count until V20 is ruled, and V20 is recorded as WP9 because the fix belongs in the payload, not in the scoring. Scope of V20, measured: 159 of 397 multi-source articles in the reference KB carry two or more same-day sources (411 pairs), of which only 8 have a version-suffix shape -- so the tie-break is mostly uninformative rather than inverted. Three cases render a source after their own chain head, but only P5's is a chain member; the same-day co-sources P7 and P10 render last assert none of the superseded values. No total moved: 44 contradictions, 31 drops, 42 controls, 27 stale.
… the score V4 found that P7's article merges the 04-14 infra biweekly (P2's v1), which asserts v1's entire 0430 column, and recommended scoring only C8. The finding is accepted and verified exactly; the scoring restriction is rejected. Premise verified with fixed-string counts and the lines read in context: L898 40.95/24.57, L904 13.3, L911 3000C, L916 合同已提交, L919 117.3, L921 3300/83.1, L925 7197, L928 17054 -- one hit each except 7197 with two. The restriction was rejected because it attributes staleness to a document pair, while supersession is a property of the compile set. The set holds 04-14 asserting the old value and v2 asserting otherwise, so a writer that emits it undated mishandled ordering whichever document supplied it -- and 04-14 is itself superseded inside the fixture by P2's v2, so A1 orders it too. Under V19's ruled criterion every stale row holds, the newest source speaking to each being v2. For FX7 the confound cancels, the co-source sitting in both arms. So P7 keeps all 8 rows at 6 of 8 stale, and only the causal claim narrows to C8. Two repairs came out of ruling it. C8's test is now the full phrase 识别—分析—跟踪—复盘, which is 0 hits in all three non-chain sources, because the bare token 闭环 has 7 hits in the 双周会 alone on a different loop -- the same string-matching hazard already recorded on C2. And C6 gains a residual: the newest source in the set restates 3000C at 05-20 L374, six days after v2 reported that gap closed to 7C, with the scope unstated. That is the one row where the newest source speaking may assert the superseded value. Also recorded: 3,864 carries a comma in the 05-14 meeting, so a scorer searching 3864 misses it, and the corrections absent from every co-source are C1's 95.18, C2's 44.4/41.13, C3's 41.5/8.3, C5's 25046, C6's 7C and C7's ES outcome. No total moved: 44 contradictions, 31 drops, 42 controls, 27 stale.
…rong V5 found that v4 still names 系统户 as the settling counterparty in five places, none in a 待决策 block, and offered either an open call or dropping the row. The residual is recorded and the row stands. It survives because the replacement is the cleanest in the chain: v2 L5447-5453 and v3 L5697-5703 are the same table row replaced cell for cell -- subject 交易系统户 to 交易系统内的差额账户, role 作为事故用户的对手方 in both, and detail 1 from 资金都和系统户结算,允许透支 to 资金都和差额账户结算,允许透支. The five residual mentions are weaker than C7's rather than stronger, because they describe a different level than the counterparty role the row scores: three are transType accounting directions in §3.3 engine (which ledger account moves per transType), one is hedged with 可以考虑, and one is a worked example that three lines later says 差额公司出, sitting under an open question highlighted light-yellow. Scored on the table row, the basis call 2 already uses for C7, and written up as call 8. Two line-level corrections came out of ruling it. The cited pair is v2 to v3, not v2 to v4. Term counts across the chain are 0/0 at v1, 7/0 at v2, 5/2 at v3 and 5/2 at v4 for 系统户 and 差额账户, and v4 only shifts the two lines by one. This confirms V15's "rewrite X1 as v2->v3" from a second direction, so V15's row and X1's cites are updated too. And 允许透支 is not part of the change: it already stands in v2's cell, so the row wrongly implied overdraft arrived with the replacement. X1's entry had this right, which is internal corroboration. No total moved: P4 stays 9 of 10 stale, and the set is 44 contradictions, 31 drops, 42 controls, 27 stale.
V20 asked what to do about two source blocks that share a date: WP5 breaks the tie on the path and WP6 then tells the writer that blocks run oldest to newest without qualification, so the pair gets a positive ordering claim resting on a filename. The fix is to withdraw the claim rather than state a better one. WP6's undated caveat now covers same-day blocks, they keep rendering in path order for reproducibility, and BG1 restates its own basis as newest known day first with ties broken on path for stability. Both candidate tie-breakers were rejected on measurement. Of 384 same-day pairs on the reference KB a filename version marker appears in 9, names a document revision in 2, and survives inspection in 1 -- the other of the two, the testnet report marked v100, carries the tested skill's version 1.0.0 against a sibling reporting 3.1.0. A body-stated date is present in both members of 9 pairs, unambiguous and differing in 5, a revision pair in 1. A rule in the system prompt applies to every payload, so a signal worth 1 pair in 384 cannot be it. Reading body dates is also A2's deferred question and the corpus does not settle it: of the 10 same-day sources whose body date contradicts their frontmatter, 7 point earlier and 2 later. Ruling it reversed the item's own consequence. P5 does not become gate-scoreable, because withdrawing the claim leaves its chain unordered rather than ordered, so its 0 of 5 would be luck instead of evidence. P5 stays out of the gating column for good, and the reason underneath is new: v3's body dates itself 2026-03-19 against a frontmatter date of 2026-03-13, six days apart, so the shared date is an error in the metadata rather than a property of the corpus. That is raised as V21. Two numbers were corrected on the way. The population figure recorded for this item counted byte-identical duplicates as two blocks, which WP7 collapses into one, so the basis is 156 articles and 384 pairs rather than 159 and 411. And BG1 needs no change of tie-break direction: it already breaks path-ascending exactly as the render does, so the conflict was never between two orders, only between WP6 calling a block older while BG1 called it newer. P5's five contradictions stay in the set totals while P5 is not judged on the staleness column, so the stale rate now has two readings -- 27 of 44 (61%) over all cases carrying contradictions, 27 of 39 (69%) over the five that gate. V21 picks the published one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…atus cell is not the status Seventh case through the confirm pass, and the one that could be measured end to end instead of argued. Parsing both files into endpoint rows gives 278 rows in v1 and 299 in v3, 273 shared, with name and method identical in every one -- the case's central claim turned into a count. Two measurements under it were wrong: the status cell differs on 22 of the 273, always by the removal glyph v3 drops, so a field-equality test reads 22 changed rows where a decision test reads none; and the warning-glyph rise is +6, not +8, eight added in the new modules against two lost with the leverage tokens. The 280-against-278 basis reconciles on two untabled WebSocket endpoints both documents count. The case gains a notation finding. The removal glyph is 24 rows in v1 and 0 in v3, v1 is the only one of the article's 16 declared sources that marks removals with it, and it survives into the article -- while in the other direction v3's own rewritten P2P wording reaches two article lines. So the compiled output took its notation from the superseded version and all five figure pairs from v3. The glyph asserts no proposition, so call 4 still keeps it out of the score. Row-level: four of the five contradictions had their article side cited one line short, two of the six controls sit on cells v3 abridged so a byte test reads over-deletion while the control holds, both of those dropped clauses reach the article on the control's own line, K1 holds on content rather than bytes, and D6's two arms cannot be cited at one grain. Five of the 17 abridgements are not the trailing-clause deletions the blurb claimed, one of them a rewrite. No verdict moved: P5 holds at 5C / 6D / 6K, 0 of 5 stale, totals unchanged at 44/32/42 with 27 of 39 stale. All five calls answered, so P5 leaves nothing with Captain. 106 of 118 rows settled; P8's 12 are what is left. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ference set is fully labelled Last case through the confirm pass, and the only one it could close completely. 71 of 88 table rows are byte-identical, and the 17 that are not decompose into two table headers, the four owner rows V1 struck plus one scope expansion, the four rows of the deleted resource-constraint table, the three deleted milestone rows, and three items the calls had already set aside. Nothing in this pair is unlabelled -- the sweep that found five unrestated propositions in P9 and six in P10 finds none here -- so P8's drop arm is complete at 6 rather than capped at 6. The 212-line figure is reproducible once its basis is named: diff reports 155 changed lines on v1's side and 65 on v2's, 8 of them frontmatter. Two tighter figures sit beside it, because 212 counts reserialisation: 40 of v1's 1,665 non-blank body lines have no counterpart in v2, 22 of v2's are new, and all eight surviving table tags were rewritten with header-row and column widths. Two evidentiary points are stronger than drafted. The article declares only the two chain versions, so with P3 this is one of the only two cases free of a co-source confound. And v2 deletes the only two in-body dates later than v1's own frontmatter -- they are D4 and D5 -- so the newer body is dated less than the older one, which pushes a body-reading ordering heuristic the wrong way. Call 3 is escalated and left with Captain: V1 ruled a de-specification is not a contradiction and said nothing about drops, and V8's test, the one that promoted P5-D6, is satisfied by v1's week-level MCP commitment becoming a bare quarter. On that reading it is a seventh drop with three article residues and the published drop total moves 32 to 33. An undeclared same-day sibling document asserts the same schedule, which is the V16 shape that kept P5's residues with the chain. Row-level: K3's cite was one cell of three and the whole summary table turns out to be a control, though the article never states its category split; K4 holds on content rather than bytes; D1's residue is three lines and loses its scope clause; D2 shares one of them; D6's second residue was uncited; and one absence test had to be re-based off a string with a hit in v2. Calls 1 and 5 are corrected on their own evidence. No verdict moved: P8 holds at 0C / 6D / 6K, totals unchanged at 44/32/42 with 27 of 39 stale. All 118 scoring rows are now settled. What "done" still needs is test-set.md's Status block. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…h defect was never one The confirm pass finished on the rows in the previous commit; this is the last of the three conditions "done" required. test-set.md's Status block now reads confirmed rather than awaiting-confirmation, its Label column carries each case's pass result and its open calls, labels.md records all three conditions met, and spec.md's FX3 criterion carries the closing line -- with the historical blocker-2 paragraph annotated rather than rewritten, since it is quoted there as the reasoning A1's criteria were written from. One deferred item dissolves on inspection. The note carried since P7's pass said test-set.md's article paths use plural wiki directories where the corpus is singular, wrong on all ten rows. The section's own preamble already says the paths are given as they stand in ~/.knowledge and that corpus copies sit under the singular names, and both trees check out: ~/.knowledge/wiki is plural, the corpus is singular, and all fourteen paths resolve as written. The caveat now records the re-check instead of the table being rewritten. Aggregate, cross-checked by counting status cells rather than by adding up the commit messages: 118 scoring rows, 64 confirmed and 54 amended, none rejected, plus P2's 18 counter-case rows. No pass moved a total, so the set still publishes 44 contradictions, 32 drops and 42 controls with 27 of 39 stale. Five judgement calls stay open with Captain -- P3 call 3, P8 call 3, P9 calls 3 and 9, P10 call 6 -- each a promotion whose other branch moves one of those numbers up. FX5 can now score against these labels, which unblocks the FX4 baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FX4 asks both arms to check two defects nothing in the pipeline catches, and a note in the spec is not a check. This is the script. Comma-packed and duplicated `sources` entries are the first: G2's claim is that a source is an attributable unit, and an entry holding `raw/a.md, raw/b.md` is not one, while a path listed twice is the double-count the U1-U4 controls exist to catch. Frontmatter that does not start at byte 0 is the sharper one, because for those articles every key is invisible to every reader -- `split_frontmatter` returns None for content that does not open with a delimiter line, so there is no title for the catalog, no date for WP2 and no sources for `derive` to copy. The walk goes over the filesystem rather than `existing_articles()` on purpose: the index is built from the very frontmatter the second defect hides, so an article with an unreachable block is invisible to every reader that goes through it. A BOM is reported apart from those articles rather than with them, since every reader here strips it and no key is lost. On `data/kb-knowledge` it reproduces the spec's figures exactly -- 682 articles, 91 packed entries in 46, 47 duplicated paths in 30, 7 unreachable -- which is what qualifies it to be believed on a fresh run. It adds one datum the spec did not have: 8 articles carry readable frontmatter with no `sources` key at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The confirm pass settled 118 rows and moved no total, leaving five judgement calls open because each had one branch that moves a published figure. All five are ruled, and ten rows arrive with them: one contradiction and nine drops. V23 promotes P3's knowledge-repo naming row to C9 -- both sides carry exclusive strings, which is what P4-R3 was found to lack, and only one of its three article residues falls under the one-defect-counted-once precedent. It is the only ruling that touches the gate: 44C to 45C, and the headline 27 of 39 to 28 of 40 (70%). V24 promotes P8's `Q2 W6` to `Q2` de-specification to drop D7, not to a contradiction, which V1 already closed. V9's exclusion does not fire because the sibling carrying the same string is undeclared -- the V16 shape, which leaves the residue with the chain. V25 keeps P9-C6 and re-grounds it: read on the tier table being exhaustive it collides with P3 call 4, so it is ruled on v2's own re-terming of the same capability instead. Nothing moves; what changes is what the row is tested on. V26 takes three of P9's five unrestated propositions as D6-D8, and V27 five of P10's six as D7-D11. Both hold back the item that would gate on an absence -- P9's answer-card componentisation, which v2 answers with A2UI, and P10's ToB volatility, declined on V3's and V17's different-statistics ground -- and both leave a dimension as a note rather than a row. Two principles decided the branches, and they are recorded as such: V8's standard is applied uniformly, so "the diff did not show it" is no longer a reason a proposition stays unlabelled; and the gate may only grow on affirmative evidence, never on an absence. Also here, from starting the FX4 baseline arm: the two article-shape checks are a script now, both arms re-extract because the cached extractions predate the schema the code reads, and the cost estimate's "one merge op per document" is not an upper bound -- 18 documents fan out into 26 articles, so an arm lands near 14-15 USD. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the prediction was wrong FX4 predicted that comma-packed and unreachable-frontmatter `sources` were old-writer residue a fresh run would not reproduce. The finished pre-A1 baseline arm reproduces both, at rates no better than the corpus they were found in: 2/27 (7.4%) comma-packed against 46/682 (6.7%), and 1/27 (3.7%) unreachable against 7/682 (1.0%). Small denominators, so the sign is the finding. The mechanism implicates both arms. `_apply_diff` appends one list item per path and cannot pack; the full-rewrite path has the model re-emit the article's frontmatter, and every packed entry sits in an article that took a merge-batch write. Step 2's rendered `sources:` list tells the model what to emit without constraining it. So the A1 arm gets audited on the same checks rather than assumed clean, and serializing `sources` in code is a fix outside A1. FX5 gains three scoring rules the arm exposed: - Cases resolve to an arm's article by its `sources` set, not by the name test-set.md cites. N2 landed as zero-trust-security-initiative.md, not the platform slug in the table; classification picks the slug, so the arms can diverge from each other too. - infra-team-h1-2026-decisions.md is excluded by name: it wrote in stage 1 and then lost every later merge to a write timeout, so it carries v1 of both its chains and would read as a supersession failure. Exposure is near nil (P2 scores nothing under V10, N2's scoring article wrote with both versions). - That exclusion is not assumed symmetric. The timeout constant is 300.0 in both arms, but three other groups timed out and then landed, and this one failed 21% under MAX_PROMPT_CHARS. If the A1 arm writes it, the exclusion is one-armed.
…solves to two articles FX5 is measured against the finished FX4 baseline arm (27 articles, 17.99 USD, code at bd8252e). The gating column -- Staleness over superseded-contradiction -- reads 24 of 40 (60%) against the 28 of 40 (70%) the label pass published for the historical articles, so the arm is better than the artifact it replaces. Band 21-25 on the four queued rulings that move the column. The reason nothing on this branch predicted: the pre-A1 writer already performs supersession unprompted on 18 of 45 rows, in four distinct styles -- explicit wording, version-labelled parallel presentation, as-of snapshot dating, and coequal presentation that names the conflict and refuses to order it, which the D1 option list has no name for. It is unreliable rather than absent: P9's article compared one of three phase deadlines stated on a single v2 legend line and kept v1 for the other two. Two findings changed the criterion before a row was scored. A case resolves to two articles rather than one (10 of 18 chains split across decision/ and project/, both carrying the whole chain), so scoring is the union over a case's articles -- arm-independent, unlike designating a primary, and on P3 the two articles are stale on almost disjoint rows, 5 of 9 each and 8 of 9 unioned. And one article carrying a whole chain is invisible to source-based resolution because FX4's frontmatter defect hides its sources; it is the article carrying P10's trails, so it was resolved by hand. Three spec clauses were checked rather than assumed and did not hold: the frozen article needs no by-name exclusion, V9's co-source exclusion changed no verdict anywhere on this fixture, and Size is unmeasurable on both arms as scripted. test-set.md's Trail and Size rows now say what is open and what is unavailable, NG3 no longer expects Trail 0 on P4, and labels.md's closed queue points at V31, which questions P3-C7's own scope. Seven rulings are open with Captain (V28-V34); the drafted position on each is what these numbers use, so none of them blocks the A1 arm. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
V28 is the only one of the seven that moves a number, and it is ruled against its drafted position: a Trail requires a directional statement about which value is dead. Version- or basis-labelled parallel presentation and as-of date stamps do not count, because P5 reads (Staleness 0 of 5, Trail 4 of 5) -- forging the current-plus-trail signature while deciding nothing -- and P7 reads (5 of 8, 6 of 8), a pair no D1 option produces. The three tiers decompose as 7 directional + 4 basis-labelled + 7 as-of = the 18 the rubric counted, and the two excluded tiers stay on the record so FX7 cannot claim the baseline performs no supersession. V31 and V34 are ruled at the draft on evidence that still exists. V29, V30, V32 and V33 close permanently at the draft, with a dissent recorded on V33: they are reads of lines in baseline-arm articles, and /tmp was cleared, so no baseline-arm article survives. scoring.md is now the arm's only record, which is also why FX7 compares against a written baseline rather than a readable tree. The gating column keeps 24 of 40 (60%) and the 21-25 band closes onto the point estimate. D1's option list gains coequal presentation, the shape the baseline produces most often and the one it never named. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The baseline arm ran from four shell scripts under /tmp that were never committed. /tmp was cleared on 2026-08-19 and took them, the 27 scored articles and the logs, so the arm can only be re-quoted from scoring.md. This is their replacement, versioned and under test. It carries their policy, all of it paid for by 2026-08-18's two outages: wait for the endpoint before every compile (one outage lasted ten minutes and the driver that gave up into it scored the wrong thing), one retry per stage taken in place (staging the next version first would compile a leftover against an article that version had already moved), and a residual recorded rather than raised (stage 2 finished 16 of 18 and the arm is still the measurement). Plus the two things their absence cost. --out may not be under /tmp, and wiki/ is copied per stage, which is what Size was never measurable without. cases.json is rebuilt rather than shipped: select_cases against the fixture emits the curated 18 chains over all 38 documents -- the 131 candidates on record is its count against data/kb-knowledge, a different KB -- so the driver regenerates it and proves the coverage every run. 35 tests, 99% coverage, 10 of 10 mutations of the new rules caught. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sioned The 2026-08-19 entry recorded cases.json as unrecoverable on the ground that select_cases.py emits 131 candidates rather than the curated 18. That count is against data/kb-knowledge. Against the fixture the same script emits exactly 18 chains over all 38 documents, no orphan and no missing member, and the stage plan comes back 18/18/1/1 -- the split stage_fixture.py and the baseline arm both used. test-set.md L764 already published that count on this branch, so the claim contradicted a document beside it. So the file is derived, deterministic and free, and run_fx4_arm.py rebuilds it per run with the coverage proved by a test against the real fixture. FX1 now covers the thing that spends the money too, not only the things that prepare it, and Size is recorded per stage on the A1 arm while staying unavailable on the comparison -- the baseline is unrecoverable, not merely expensive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found in review. The completion test read data["compiled"] from the compile response, but that count is KB-wide: gate 2 (compile.py:312-317) re-queues every document whose state carries no compiled_at, so a later stage's compile also recomposes an earlier stage's residual. Stages 3 and 4 stage one document each and the baseline left two residual in stage 2, so stage 3 would have reported compiled=3 against staged=1 and been declared done even when its own document failed -- no retry, nothing in the report, and the chain collapsed onto the single-run merge->create path FX2 exists to avoid. An 18 USD arm mis-measured silently. Stage completion is now by name: stage_shortfall requires compiled_at for every member of that stage, read fresh from .compile-state.json after each attempt, and the residual names the documents. A per-document compile error is deliberately not a retry trigger -- those errors can be entirely about documents this stage never staged, while an error on a member of this stage leaves it missing, which is the trigger. Also from the round: --attempts, defaulting to 3 rather than 2 because the baseline got three passes at stage 2 by hand and an arm with fewer attempts than the arm it is compared against confounds FX7; a config block in the report (repo SHA, models, workers, extract strategy) plus the per-document errors, since the baseline's write history had to be rebuilt by hand from log lines; the report checkpointed after every stage instead of once at the end; --out refused when it *resolves* into a volatile directory, the relative spelling having passed the prefix check; --out refused when non-empty unless --resume, staging being additive; and a real --execute integration test, without which an argument swapped between fixture and kb would have copied an empty KB over the fixture with every test still green. Docs: scoring.md's Size section still said "unmeasurable on both arms" and cited a deleted /tmp script; spec.md FX1 said "only the two" and named four; test-set.md's Regenerating block showed only the corpus-scoped select_cases invocation, which is the reading that produced the retracted claim. The fixture is now the last unversioned load-bearing artifact and is recorded as such. 55 tests, 99% coverage, 13 of 13 mutations caught, full suite 1817 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he articles Two findings from re-review, both measured before accepting. A typo'd scheme cost fifteen minutes and lied about why. The exception classes do not separate a config error from an outage: gw.example/v1 raises ValueError, htp://gw.example raises URLError -- an OSError, so wait_for_endpoint read a typo as an outage, burned the whole 900 s deadline and aborted with "endpoint never came back" -- and http://gw .example raises http.client.InvalidURL, which is neither and crashed the arm mid-run. unusable_base_url now checks the shape once in main, before any hook is built, and the probe is left pure. And the report's model record was false. run_compile hardcodes all three write-path models to claude-sonnet-4-6 (compile.py:806-808) and reads LLM_MODEL only for the summarize fallback, so recording that env var published a model which wrote nothing -- inside the block whose whole purpose is FX7 self-description. The driver now sends the three models explicitly, at the same default the baseline ran, and records what it sent. Smaller, each a way to lose or misread an arm: --attempts 0 would have snapshotted every stage, called nothing and written a full-residual report at 0.00 USD, an arm that never ran looking finished; --workers 0 silently took compile's own 16. A stage blocked between attempts now snapshots what its earlier attempts wrote, Size being why snapshots exist. Errors carry an attempt index, so one document failing twice no longer serialises as two documents failing. KB_WORKERS is recorded, chunk-level extraction reading it independently of --workers. Two rules the last commit advertised were pinned by nothing: the per-stage checkpoint (its test asserted a stub calling its own callback) and the $TMPDIR refusal entry (disabled suite-wide by the fixture that makes tmp_path usable). Both now have real tests, and the tautological attempts assert reads 3 rather than the constant it was comparing against itself. scoring.md still published "one retry per stage" as this driver's policy while the default is 3 attempts -- in the one document FX7 reads its baseline from, understating the A1 arm's retry budget on the exact variable that choice was made to protect. 72 tests, 99% coverage, 18 of 18 mutations caught, full suite 1834 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e shifted The A1 arm ran attended at 14.3873 USD over 185 calls, four stages each finishing on its first attempt with an empty residual. Both FX4 article-shape defects are absent, so no case needed hand resolution and 14 of 18 chains resolve to one article against the baseline's 8. Every failure column improved and the one positive column got worse: gating Staleness 24 -> 19 of 40, drops 37 -> 29 of 41, corrections 34 -> 38 of 45, collateral 40 -> 41 of 42, Trail 7 -> 5 of 45. The reason is one behaviour. The arm consumes the ordering signal but attributes each value to its source version instead of asserting which one is dead, so its best cases read (Staleness 0, Trail 0) -- coequal presentation, the shape D1 rejects in terms. Where it stops labelling it fails in the same three places: Key Decisions sections, Action Items tables, and DDL cells copied from an older version into a table headed by a newer one. Two regressions belong to the classifier rather than the writer. P4 and P9 split by version, so 16 of the 45 contradiction rows sit in chains no single article can trail, and they carry 14 of the 19 stale gating rows. Four rulings are drafted and open (V35-V38). V36 -- how a chain the arm distributes should resolve -- moves P4 between 9 and 3 stale rows and the total between 19 and 13 of 40, so the FX7 verdict waits on it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A1 does not clear the positives, so D2's condition fires and A2 moves from optional to required. The gate is test-set.md's -- path A ships first if and only if it clears the positives without tripping the negatives. A1 holds the negatives at 0 of 4 and 0 of 4 and leaves 19 of 40 contradictions stated as current. The four rulings the A1 scoring opened are all taken at their drafted positions, so no published figure moves: V35 name residue is recorded and not scored, V36 scores a distributed chain on the union with the newest-half figure reported beside it, V37 an accurate as-of stamp does not make a row stale when nothing newer is offered, V38 a tense-marked dated pair is not a Trail. Unlike the baseline's queue none closes for want of evidence, the arm's articles having survived. The verdict does not depend on any of them. The band across every drafted and alternative reading is 8-22 of 40 against the baseline's 24, so A1 is better on every reading and clean on none. Two findings carry into A2 rather than into the gate. Split by whether A1's articles saw the whole chain, staleness went 13 to 5 of 24 on P3, P7 and P10 and 11 to 14 of 16 on P4 and P9, which A1 split by version -- so 8 of the 19 stale rows are in an article that had both values in front of it and 11 exist only because a second article carries the older half of a chain. Version splitting is NG6's territory, upstream of this feature, and is now the column's largest single contributor with no owner on this branch. Second, A1's five-row advantage rests on basis-labelled coequal presentation scoring clean; strictly read, both arms land near 34-36 of 40 and it disappears. That cannot be settled -- the criterion can be applied to this arm's surviving articles and never to the baseline's -- so it is recorded as a bound, and it is why test-set.md's Scoring table should be settled before A2's arm is bought. One recorded counterfactual was wrong in four documents and is corrected: V36's alternative totals 8 of 40, not 13, which had applied the newest-half rule to P4 and not to P9 though P9 is split the same way and its v2-only article reads 0 of 6. The ruled figure never moved. The content-iteration diagram also credited the A1 arm with the baseline's Trail count; it is 5 of 45 against 7. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…clean The FX7 verdict named one thing to fix before A2's arm is bought: Staleness and Trail together certified basis-labelled coequal presentation -- the shape D1 rejects in terms -- as a clean latest-wins result, so A1's three best cases read (Staleness 0, Trail 0) and an A2 shipping a real trail would not have been distinguishable from them. V39 adds a third gating column, In force: a row fails when both its values are stated and nothing resolves which one holds. It is a new column rather than a stricter reading of Staleness, deliberately -- redefining the gating column would have replaced this branch's one measured comparison with a one-sided one, the baseline's articles being gone. So 24 against 19 of 40 stands untouched. Measured row by row on the surviving articles in ~/kaas-arms/a1, not derived from the scoring records: In force is 28 of 40 on the A1 arm, 18 inside a single article and 10 across a chain the classifier split, so the gate reads 18 and the rest sits with NG6. Presence was checked on the files -- 11,362 and 25,046 appear in comma form in P7's article, the only 7C match in it is inside 7197C, P4's nanoseconds appear in both articles with no millisecond form in either. Three consequences worth naming. P10 and P5 go from the cleanest results in the set to 7 of 7 and 5 of 5 while their Staleness stays 0. A trail no longer guarantees a pass: P3-C1 and P3-C5 each carry a correct directional statement and assert the superseded value as current elsewhere, which makes intra-article self-contradiction countable for the first time. And the strict reading this branch carried as "roughly 36 of 40" is now measured at 32 on the A1 side, with the baseline's 34 staying an estimate for good. The verdict does not move: the second gating column agrees with the first that A1 does not clear the positives. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…odel An arm's write model is sent explicitly, but nothing checked that the resolved endpoint serves it. KaaS reads LLM_BASE_URL/LLM_API_KEY and falls back to OPENAI_BASE_URL/OPENAI_API_KEY, which on the laptop that ran both arms points at MiniMax -- eight MiniMax models, no Claude model. An arm launched there 400s on every call and "completes" with a full residual at 0.00 USD, which reads like a catastrophic writer failure rather than one unset variable. That cost an hour once. So main now reads the gateway's /models list before anything is written and refuses when the write model is absent, naming both what it needs and what the gateway serves. Same class as the URL-shape check, and placed after the --out refusal so a local mistake costs no round trip. The two readings of that payload are kept apart, because only one is evidence: an empty data list is a gateway saying it serves nothing and the arm refuses, while HTML, a bare string or entries without an id is a gateway that was asked something else -- that warns and runs, since a gateway not exposing /models would otherwise be unusable for an arm whose every write would have succeeded. Unlike the endpoint probe, no failure propagates: wait_for_endpoint owns outages. arm-report.json records models_verified, for the same reason it records the models. An arm that produced a residual has to be readable as a writer failure or as a gateway that was never confirmed. 16 new tests, 88 in the file, 99% coverage (the one miss is the __main__ guard), 11/11 mutations caught, full suite 1851 passed. Also verified the file opens no real socket, with socket.connect stubbed to raise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eways Review round on the pre-flight check: 3 Important + 6 Minor, every one fixed, adjudicated or recorded. The reviewer proved four surviving mutations rather than asserting gaps, and two of them were rules the commit message itself claimed. The two claimed-but-untested rules now have tests. Checking _models(_MODEL) instead of _models(args.model) passed the whole suite, so --model was checked against nothing: an override the gateway serves would have been refused, and one it does not serve would have been accepted and then 400 every write. And moving the check above the used---out refusal also passed, which mattered twice -- the ordering claim was decorative, and in that state the one --execute test that does not stub _model_catalog performed a real DNS lookup for gw.example. Both are pinned, the second by making that test fail if the gateway is asked anything. The guard's premise is now measured instead of assumed, which is this file's own standard for a claim about how a call fails. LiteLLM answers /models with 200, 30 ids, no wildcard entry and claude-sonnet-4-6 spelled exactly, so no arm can be falsely refused there. The MiniMax endpoint the OPENAI_* fallback points at answers 200 in 0.26 s with 8 ids and no Claude model at all -- so the recorded incident is refused rather than degraded to a warning, which was the one way this guard could have been decorative. http.client.HTTPException joins the cutset: InvalidURL, IncompleteRead, BadStatusLine and LineTooLong are neither OSError nor ValueError, and two are reachable through a URL unusable_base_url accepts (a nonnumeric port, and a control character in the path, which the netloc check does not inspect). The docstring claimed every failure reads as None and did not. An autouse fixture now deletes the four gateway variables for this file. There was no such isolation, and on this laptop OPENAI_BASE_URL is the MiniMax endpoint with a live key -- so the incident configuration was ambient during a bare pytest run, and suite hermeticity rested on check ordering rather than on isolation. Adjudicated, not implemented, and both recorded in main's comment: the summarize model is not checked, because compile.py:228 makes it mandatory only for a non-chunked strategy and checking it unconditionally would refuse an arm over a model no chunked run calls, while checking it conditionally restates compile.py's rule where it would drift; and a 401 on /models stays in the warn branch, because a completion-only key that cannot list models is a real configuration. Both would be answered by probing with a 1-token completion, which is noted as the cheaper fix if either premise stops holding. 93 tests, 99% coverage (the miss is the __main__ guard), 12/12 mutations caught including the four the review found surviving and the block move, full suite 1856 passed, and the file still opens no real socket with connect/create_connection/ getaddrinfo all stubbed to raise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
V39 made this caption stale and the V39 commit did not update it. The diagram's second shape -- coequal presentation, the one the A1 arm produces most -- was captioned as clearing Staleness and earning no Trail, which under the two-column rubric left it looking like an unremarkable latest-wins result. It now names the third column and what that column reads: nothing says which value holds. Rendered and checked rather than eyeballed as XML: the line is 126 characters and sits inside its box with margin on both sides at font-size 10. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…under
CI's first run of this branch failed 17 tests with `SystemExit: 2` and the reason
is a platform assumption in the autouse fixture, not in the driver. The driver
refuses an `--out` inside a volatile directory, so the suite cannot use `tmp_path`
unless the rule is narrowed for it -- and the fixture narrowed it by writing the
answer down: `("/tmp", "/private/tmp")`. On macOS that works, because `tmp_path`
lives under `$TMPDIR` (`/var/folders/...`) and dropping that one entry is enough.
On Linux `tempfile.gettempdir()` is `/tmp`, so every scratch path is still refused.
Reproduced locally before fixing, with `TMPDIR=/tmp uv run pytest` -- the same 17
failures as CI -- and the fixture now computes the entries to drop from where
`tmp_path` actually resolves, so it is correct wherever pytest puts its files.
The two tests that exercise the rule itself now install their own list instead of
inheriting the narrowed one. Without that, `/tmp` being dropped on Linux would
leave `test_the_out_directory_may_not_be_under_tmp` unable to refuse anything and
`..._merely_starts_with_the_same_letters_is_fine` passing for the wrong reason --
a green suite that had stopped testing the lesson that cost 18 USD.
Verified in both shapes: 93 passed under the macOS default and under `TMPDIR=/tmp`,
and disabling the production refusal fails 6 tests in each, so the rule is pinned
in both environments rather than only in one. Full suite 1917 passed in both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…er reaches The FX7 verdict decided A2 is required rather than optional, so this is A2's spec. Five decisions carry it, each taken against the option it beat (D6-D10). The one that shapes everything else is D6. A1's gating column is 19 of 40, and 11 of those rows exist only because the classifier split a chain across two articles: the one asserting the older value never received the newer one, so no write prompt can retract it. test-set.md's gate counts them on the Staleness half while V39 already scoped In force to the same-article count, which means a writer-side increment fails that gate by construction. A2 stays writer-side and V40 restates the Staleness half the same way V39 was written -- a scope added, no published figure re-read. The baseline's 24 of 40 and A1's 19 of 40 stand, with A1's writer-owned 8 of 40 and same-article In force 18 of 40 stated beside them, and the split rows keep being published and attributed to classification. D8 sets the primitive's grain from the failure map rather than from taste: every stale row in P10 is in a Key Decisions section or an Action Items table, and P4's three writer-owned survivors are DDL cells copied under a v3 heading, so a section-level replace would reach almost none of the residue while risking every neighbouring cell on a Collateral column that already reads 41 of 42. The action names exact text, matches once or not at all, and the trail is rendered by code so D1's four format rules are mechanical instead of hoped for. D9 replaces A1's G4 now that a path can delete. A trail is append-only -- structural on the diff path, one retry then an abandoned merge on the rewrite path -- and shrinkage is reported with no threshold, because there is no measured distribution of legitimate shrinkage to set one from and a guessed number would kill correct rewrites. D7 keeps the trigger at explicit contradiction and D10 accumulates chained trails newest first, which answers A1's NG3 by reasoning rather than by evidence, V15 having measured that the fixture holds no nested instance. Anchors and figures checked against the tree rather than quoted from memory: every cited merge.py line resolves, the six sites asserting the merge paths cannot retract are enumerated for PV5, and the arm's comparison side is 20 articles in ~/kaas-arms/a1/kb/wiki -- which survives because run_fx4_arm.py refuses a volatile --out, not because anyone remembered to copy it. A1's spec and the FX7 verdict gain forward pointers, and A1's two questions carried to A2 are marked decided there instead of still open here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tood Step 1 of the A2 increment (docs/features/supersession/spec-a2.md): the diff path gains a `supersede` action that replaces exactly-matched article text and renders a `[Superseded <date> by <path>: <was>]` block in its place. No prompt file changes, so no writer can emit the action yet -- write_prompt_version stays at 5a66a8ed04ea and behaviour does not move. The code lands under test before it can be exercised, which is what the spec's sequencing asks for. Covers RA1-RA5 and TR1-TR5. The guards worth naming: an anchor must match once in the body, with no normalization and with existing trail blocks excluded, so a later supersession edits claims and never history; `by` must name a payload source whose date is strictly newer than every other dated block, which puts WP9 in code; supersede applies before the additive actions, since anchors were chosen against the article as it entered the prompt. _apply_diff now takes the payload's SourceBlocks rather than their paths, because the order guard reads their dates and two representations of one payload could disagree about which document is newest. It returns the refusals alongside the content; _merge_diff names them on stderr until the compile report carries them in step 4. Three spec amendments came out of implementing it, each a gap the criteria left rather than a change of direction: - RA3 produces a failure SG3 did not name -- `by` dated but beaten by a single newer block. Reported as its own reason, because folding it into "no strictly-newest block" would report the writer naming the wrong document as the payload having no order. - RA1 fixes which fields are required. An empty `replacement` withdraws a claim and the trail makes that recoverable, so it is allowed; an empty `was` deletes the record instead of the claim, so it is refused. G7 is a property of the record, so the record is the field that cannot be omitted. - TR5 protected only `was`, leaving the same table corruption reachable through the other half of the same edit. It now covers `replacement` too, and refuses an anchor spanning a row boundary -- that shape merges two rows and leaves one `was` standing as the record for every claim they held, so TR5's column count and G7's record fail together. VA5 is listed under step 1 but verifies SG2, which step 4 owns and whose byte deltas are only visible at the compile layer. Left to step 4 as a sequencing slip rather than implemented here. Verification: 1956 tests pass; kb_ai.core.merge at 100% statement coverage with every partial branch in pre-existing code. Assertion strength checked by mutation on a scratch copy -- 14 mutations across RA1-RA5, TR3 and TR5, zero survivors, reverse control green. One mutation first read as surviving because reordering the two patch passes leaves the file's size unchanged and CPython validates a cached .pyc on (mtime, size); with bytecode disabled it is killed by exactly the RA4 test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ter reaches Step 5 of A2's sequencing: GT1 lands in test-set.md as V40, with GT2-GT4. Both gating columns now read the same-article count -- V39 had already scoped In force that way, and Staleness follows for the same reason. 11 of A1's 19 stale rows and 10 of its 28 In force rows sit in an article whose own sources never contradict the value it states, so a gate counting them measures the classifier with a writer-side increment in the denominator. No published figure moves. The baseline's 24 of 40 and A1's 19 of 40 stand undecomposed; A1's writer-owned 8 of 40 and same-article In force 18 of 40 are stated beside them, and A1 fails both columns on the narrower count too, so FX7's verdict is untouched. The baseline's own split is unrecoverable and stays that way -- what V40 fixes is what the next arm's gate reads. GT2 states the gate as four figures, GT4 measures the same-article denominator on the arm being scored rather than inheriting A1's routing, and GT3 publishes a D1 conformance reading beside the gate, since four clean figures with Trail near 0 is latest-wins shipped rather than D1. Step 2 is untouched: write_prompt_version has not moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…claim goes A2 sequencing step 2, the step that changes what the writer does. merge-diff.md gains a `supersede` action with RA1's four fields; merge-rewrite.md states the same rules in prose, since the rewrite path has no action vocabulary. TR6's validator reads the rewrite path's own trail text back and reports what is malformed without rejecting the write. write_prompt_version moves 5a66a8ed04ea -> 3c88b88358d8, and the six sites claiming the merge paths cannot retract are now false, so they are rewritten -- including the two operator-facing report strings, which say "may still carry ... look for [Superseded ...]" (PV6). Three deviations from the spec text, each recorded in spec-a2.md rather than left for a reader to find. TR6 read literally reports a false defect on every correct chain, because S7's preserved v2 entry names a document the v3 payload does not carry -- so it validates only the blocks new in the output, compared against the pre-write article. RA6's "D1's example verbatim" contradicts TR1: D1 writes the block wrapped over two lines and TR1 makes it single-line, so the prompt states it unwrapped, pinned by a test that runs the prompt's own example through the validator. And RA7's anchor-uniqueness rule is diff-only, being vacuous on a path that returns a whole article. Two things a mutation sweep caught that reading the code did not. The candidate scan was line-greedy, so TR4's chains -- which sit adjacent on one line -- had every entry after the first swallowed and never validated; it now runs per opener. And `by` required a run of non-space, which reported as malformed every trail _render_trail itself emits for a source path holding a space, reachable because distill._raw_rel joins the file's path parts verbatim. D9's append-only rule lands in merge-rewrite.md a step early on purpose: SG1 retries "with the constraint restated", which presumes the prompt carried it. One bound left open and pinned by a test rather than closed quietly: an empty `was` passes TR6, where the diff path refuses it on G7's argument. A fourth check is a change to TR6, not a fix to it. Tests: 1974 passed, core/merge.py at 100%. Prompt rules are pinned by a parametrized test over (criterion, wording) pairs, because a rule deleted from a prompt is invisible until an arm is bought -- four of them, including the one sentence standing against coequal presentation, could be deleted silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e merge SG1 with VA4: every [Superseded ...] block the pre-write article carried has to come back verbatim from a full rewrite. A missing one buys a single retry with the append-only constraint restated; still missing, the merge is dropped and the article keeps every byte it had. D9's price, paid on purpose -- this is the one loss no reader can recover from the article itself. The retry states the constraint as a requirement rather than as feedback on the rejected draft, because the draft is not sent back: it would cost a whole article of budget to say what the list of missing blocks already says. Its own text is bounded by what the first send left of the prompt budget, so a merge SG1 means to retry cannot raise PromptTooLargeError instead and lose the history to a crash; the report names every missing block either way. TR6's malformed-trail report now runs only when a rewrite lands, since the format of prose in an abandoned write points an operator at text no file holds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…truncation range Annotates SG1 as landed and records what implementing it measured: the diff path's append-only guarantee is not structural for every article. _merge_diff section-truncates an article past 70% of the prompt budget and then patches the truncated text, which is what gets written back, so section bodies -- trail blocks among them -- are dropped before any action runs. Measured on an 86 731 character article: 51 966 written back, the trail in a low-relevance section gone with its heading kept. Recorded rather than fixed. It predates A2 and contradicts A1's G4 as squarely as it does D9, so the fix is its own change; naming it now means SG2's shrink report will not be read as the guard failing. SG1 also picks up the 80-character report bound it shares with SG3, which the implementation adopted and the rule did not state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ndoned merge stops reading as merged SG2, SG3 and SG4 with VA8. Every merge op now takes an optional sink, and the findings it used to print from inside the merge -- refused supersedes (SG3), abandoned merges (SG1), and the byte delta of an article that shrank (SG2) -- reach the layer that knows which article was written. Collected instead of printed: the sink's owner logs the report, so emitting at both layers would tell an operator the same refusal twice and make a count of report lines wrong. A caller that passes no sink keeps the stderr behaviour it had. SG2 is measured inside the merge op rather than by its two callers, though both hold the pre- and post-write text: one delta must not depend on which route wrote the article. Bytes, not characters -- a rewrite can drop three CJK characters for one ASCII word and grow in characters while shrinking on disk. Both routes stop calling an abandoned merge `merged`. The compile log says merge-abandoned and the worker route reports `abandoned` in its item result, because `merged` tells a client the sources reached the article and SG1 dropped the write so that they did not. The ops still count as completed, per D9. SG4 is asserted the way RP5 already was, with a sentinel anchor: the write entry points take source blocks and nothing else, and the findings are built after the last write op, so a report cannot steer the writer that produced it. VA8 extends VF6 from the payload to the article -- one supersede, both routes, same bytes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…left open Annotates the sequencing entry and records what the implementation had to decide: findings travel in a caller-passed sink and are collected instead of printed, SG2's delta is measured inside the merge op and carries a one-byte floor from the rewrite route's strip, and an abandoned merge is its own status on both routes while its ops still count as completed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…indings before the write An abandoned merge is what EV_MERGE_ABANDONED names; the reason string already says the trail is missing. The value stays "abandoned", so nothing an external consumer reads moves. All four drains now run above store.write_article. A drain below it loses every finding of the run most worth reporting to the exception, and the findings describe what the merge produced rather than what reached disk -- so a failed write reports SG2's delta beside the error it also files. Each site is pinned by its own test: reverting any one of the four alone turns exactly one test red. The single-merge branch had none, because the shared fixture ingests two raw files into one article and can only reach the batch branch. The compile report is formatted from the events and serialised from the same order, rather than rebuilding a MergeEvent from a dict just to print it. SG4's wording narrows to what holds on both routes: no finding is read back by the run that produced it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tybot02
pushed a commit
that referenced
this pull request
Aug 22, 2026
#53) GitHub's code scanning default setup never triggers on a pull request from a fork. Pull request #50, from wangdahoo/kaas, therefore sat permanently blocked: the "main" ruleset requires a CodeQL result, and no CodeQL run was ever created for its head or its merge commit. Every same-repo pull request from #32 to #51 got one; both fork pull requests got none. A workflow file is part of the pull request's merge commit, so it also runs for fork pull requests, subject to the usual approval for outside contributors. The matrix reproduces the four analyses and build modes default setup ran, so the languages covered do not change. Default setup has to be turned off for this to work: while both are active the upload fails with "CodeQL analyses from advanced configurations cannot be processed when the default setup is enabled". Co-authored-by: lucasmaan <305449091+lucasmaan@users.noreply.github.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.
What this delivers
Two increments of the supersession feature — teaching the writer to retract a claim instead of stacking a newer one beside it — plus the date ordering that makes retraction decidable.
Ordering and dates (A1's groundwork)
The supersede primitive (A2 steps 1–4)
[Superseded ...]trail where it stood, on both write routes (merge-diffandmerge-rewrite).mergedwhen the sources never reached the article.Evaluation scaffolding
Documents —
docs/features/supersession/: the spec for both increments, design options, the labelled fixture, the test set with its scoring rules, and A1's verdict (verdict-fx7.md), which is what decided A2 was required rather than optional.What is not in this PR
verdict-a2.md). That step spends real model budget to measure the writer this branch changed; it is measurement, not code, and it is the only open item in the spec's sequencing.spec-a2.md, step 3's notes):_merge_diffsection-truncates an article past 70% of the prompt budget and patches the truncated text, so section bodies — trail blocks among them — can be dropped before any action runs. Measured on an 86,731-character article: 51,966 characters written back. It predates this work and contradicts A1's G4 as squarely as it does A2's D9, so the fix is its own change. It is named in the spec so the shrink report is not misread as the guard failing.Verification
cd py && uv run pytest tests/ -m "not slow"). Coverage:core/merge.py100%,commands/pipeline/_phase_write.py100%,commands/compile.py99%.tests/test_distill.py::test_distill_end_to_end_produces_articleneeds a gateway servinggpt-4o-mini. It fails the same way on the branch point.Notes for reviewers
py/src/kb_ai/prompts/defaults/merge-{diff,rewrite}.md) are the behavioural change;write_prompt_versionmoves with them, so articles written before this branch are marked as written by an older writer rather than silently treated as current.docs/features/supersession/spec-a2.mdcarries the decisions and the deviations from the rule text as written, including the ones that could have gone another way. Reading its## Implementation sequencingfirst is the fastest route into the diff.🤖 Generated with Claude Code