Result traceability: runs bind to commits, validity ledger, results registry, nullius current - #49
Merged
Merged
Conversation
added 30 commits
August 3, 2026 15:06
…ontract Reported as decision D37 from a live project whose goal was formally blocked by it: running init on a mature project — to declare a mode, every seed file already present — rewrote the notebook-sync block inside the user-owned research_contract.md, deleting curated section entries and replacing three verified DOI references with the '(add references ...)' placeholder. Replayed on a copy of the reporting project's files, the pre-fix path destroys 11 of 21 block entries and writes the placeholder over the bibliography. Three independent mechanisms produced that one loss, all fixed: - scaffold_project ran sync_research_contract unconditionally, including over a contract it had NOT created. The seed writes already honored 'init never rewrites user files' via _write_text_if_missing; this call now honors it too, syncing only a contract this invocation just wrote. A deliberate re-sync remains available through refresh_research_contract.py. - the section collector truncated at eight entries, so a project with sixteen sections silently lost half. The cap is gone: a derived block lists what the notebook actually has. - the reference collector recognized only '- '/'* ' bullets on a single line, so a numbered bibliography whose DOI links sit on continuation lines parsed as zero references — and an empty parse writes the placeholder over real entries. Ordered markers and continuation lines are now folded into their item. sync_research_contract additionally reports section_count, reference_count, and dropped_entries, so a legitimately shrinking re-sync is visible instead of silent. 9 regression cases including a byte-for-byte no-mutation assertion over the whole project tree; 8 of them fail against pre-fix HEAD. project-contracts 92 passed, nullius-cli 49 passed.
…in table cells A bare | inside math collides with the Markdown table-cell separator the moment the formula is moved or wrapped into a table, and renderers disagree about it even outside tables. The semantic delimiters also carry the correct spacing and stretching: \lvert...\rvert for an absolute value, \lVert...\rVert for a norm, \mid for a set-builder or conditional bar. The pre-existing --check-table-math-pipes rule only fired on lines already recognized as table rows, so the far more common case — an absolute value in ordinary inline or display math — went unflagged until someone moved it into a table. The rule now also runs under --check-github-math for every non-table line; table rows keep their stricter table-specific message and the two never double-report the same line, so the existing assertion that a prose formula passes --check-table-math-pipes is unchanged. Escaped pipes, \mid/\lvert/\lVert forms, inline code, and prose pipes outside math stay untouched. Smoke suite gains a failing fixture (inline and display) and a passing fixture covering all three delimiters.
…D37 and bare-pipe fixes Dual-family review (codex + host subagent) of the unmerged branch returned findings against both commits; these are their dispositions. project-contracts (D37 fix): - The reference collector now tracks code fences: a fenced example inside a References section became a fictitious bibliography entry, and the closing fence was folded into the entry above it. Fenced content is skipped. - A more-indented list item is folded into the open entry as its annotation instead of becoming a separate reference. - The test fixture no longer carries the reporting project's citations or domain terminology; synthetic neutral entries exercise the same parser paths. Copying a live project's specifics into the generic package was a standing-rule violation. markdown-hygiene (bare-pipe rule): - Array / tabular / multicolumn column specifications are exempt. A column rule is not an absolute value, and none of the named delimiters is valid in a column spec — the rule was demanding LaTeX that cannot compile. - Escaped-pipe detection now counts backslash parity: a lookbehind for one backslash accepted a bare pipe following a TeX line break, so a genuinely bare | after \\ went unflagged. - The remedy message names \vert / \middle\vert / \big\vert for sized and evaluation bars, which the previous three-remedy taxonomy omitted. Real-project parse is unchanged (15 sections, 3 references); 92 project-contracts tests and the markdown-hygiene smoke suite pass, the latter with new fixtures for column specs, sized bars, and the line-break parity case.
The cross-family seat returned SHIP with six LOW findings; four are fixed here, two recorded as accepted with reasons. Fixed: - Column-spec masking now tolerates one level of brace nesting, so an array spec carrying a column-format directive, tabular's repetition form, and a multicolumn spec of the same shape stop being reported as absolute-value bars. The previous character-class body stopped at the inner brace and declined to match, leaving the column rule to the bare-pipe scanner. - Fence pairing follows CommonMark: only a fence of the same marker family closes an open one, so a tilde fence inside a backtick block is content rather than a closer that re-enables reference collection mid-block. - Smoke coverage now exercises the same-line escaped-break parity path the scanner was actually written for (the previous fixture only had the leading-pipe form on a separate line), plus a nested-brace column spec. - Three new collector regression cases: fenced examples are not references, mismatched fence markers do not close each other, and a nested annotation folds into its entry. Accepted with reason, not fixed: - A column specification split across lines is still flagged, because the math checks are line-oriented by construction; masking it would require a multi-line math model that this deterministic linter deliberately does not have. - An unterminated fence suppresses the rest of the References section. That is malformed Markdown, and the sync now reports dropped_entries, so the loss is visible rather than silent. - The table-math rule keeps its own single-backslash lookbehind: it is a separate, stricter rule outside this delta, and the split of responsibilities (no double-reporting) is intentional. Real project parse unchanged (15 sections, 3 references); 95 project-contracts tests and the markdown-hygiene smoke suite pass.
The cross-family seat returned CHANGES NEEDED with four HIGH findings and one LOW, all probe-backed, and judged C unsafe to merge because two of them corrupt VALID Markdown input rather than malformed input. All five are fixed; each new test was verified to fail against the pre-fix code. Data-loss fixes in the reference collector: - Fence tracking now records the marker run LENGTH as well as its character, per CommonMark: a closing fence must use the same character and be at least as long as the opener. A three-backtick line inside a four-backtick block was closing it, promoting fenced text to a reference, losing the entry that followed, and swallowing the next heading. - Indentation is measured in COLUMNS with tabs expanded, not in characters. A tab-indented annotation counted as one character looked less indented than a two-space entry, so it became its own reference and absorbed the real entry after it. - A list item indented four or more columns with no open entry is Markdown indented code, not bibliography, and is no longer promoted to a reference. Column-spec and predicate fixes in the math rule: - The column-spec pattern accepts an optional width argument before the specification, so the standard two-argument tabularx and tabular* forms are exempt alongside array and multicolumn. - The table-math rule now judges by the same parity-aware predicate as the general rule. Two rules judging one hazard by two different definitions of "escaped" left a seam: a bare pipe following a TeX line break inside a table cell was reported by neither. Test discrimination (the LOW finding, and the one that mattered most): - The previous parity fixture put the pipe on the following line, which the old regex would also have rejected, so it locked nothing. It now uses the same-line form the scanner exists for. - New cases: longer fence not closed by a shorter inner run, tab-indented annotation folding without absorbing the next entry, indented code not promoted, tabularx exemption, and the table-cell parity seam. Each was run against the pre-fix tree and fails there. Real project parse unchanged (15 sections, 3 references); 98 project-contracts tests and the markdown-hygiene smoke suite pass.
…hand-rolling CommonMark
Both seats returned CHANGES NEEDED and converged on the same diagnosis from
opposite directions: each round of a cleverer reference collector shipped a
new way to LOSE a real reference. Round 2 lost entries to a shorter fence
run closing a longer block and to character-counted indentation; round 3's
own fix then lost the nested items of a loose list, because "no accumulated
text" was used as a proxy for "no open list item" and a blank line clears
the former but not the latter. Independent CommonMark renderers confirmed
every one of those inputs is valid Markdown, not malformed.
The collector is therefore reduced rather than patched again. Fence
tracking, indentation comparison and indented-code detection are gone; what
remains is that every item line opens an entry and every other non-blank
line folds into the open one. The naivety is the safety property: when this
is wrong it is wrong by ADDING an entry — a fenced example, a nested
annotation listed separately — and never by dropping one. A spurious entry
is visible in the block a human reads; a dropped reference is not. Matching
CommonMark properly would mean a real parser, and this package stays
dependency-light.
The collector test suite now asserts that invariant directly over six
layouts, each of which broke one of the discarded mechanisms.
markdown-hygiene, both seats' HIGH findings:
- A Markdown table cell is ended by ANY unescaped pipe, a column rule
included, so the table rule no longer inherits the column-spec exemption
it gained last round. Sharing the exemption meant a genuine
cell-splitting pipe was reported by neither rule — the exact outcome the
previous disposition claimed to eliminate. Only backslash parity is
shared now; the honest advice inside a table is to move the formula out.
- The column-spec grammar is matched per environment family instead of
through a shared optional-width group. The shared group read array's sole
argument as a width and the following braced group as the specification,
masking a real pipe in cell content; and it put the optional position
before the width, while tabularx and tabular* take width first. Each
family now carries its own argument order.
Real project parse unchanged (15 sections, 3 references); 93
project-contracts tests and the markdown-hygiene smoke suite pass.
Observed in passing, NOT fixed here: the pre-existing raw-asterisk rule
flags the asterisk in environment names such as \begin{tabular*} and
\begin{align*}, which is legitimate LaTeX. Out of scope for this batch.
… guarantee off the parser Round 4 measured the safety property this branch asserted and disproved it. Across ~11000 generated notebooks cross-checked against two CommonMark renderers, 2157/4913 and 2679/6000 lose at least one reference: a line beginning with "#" ends collection permanently, including a "### Primary sources" subsection and a lazy continuation line that merely starts with one, and neither "+" bullets nor tables nor HTML lists are recognized at all. The claim that the collector "can only add, never drop" was false, and it was written into both a source comment and a test name — six hand-picked layouts presented as "any layout", a consistency check wearing the clothes of a falsification check. The response is not a fifth, cleverer parser. Four of those have now each shipped a new way to lose a bibliography. Instead the guarantee moves to where it can actually hold: sync_research_contract REFUSES TO WRITE when the re-derived block would remove entries whose text is still in the notebook. Parser accuracy stops being load-bearing — a shape the scanner cannot read costs a refusal and a diagnostic naming the entries, never the entries. The refusal distinguishes the two reasons an entry can leave the block, because conflating them would refuse every legitimate sync: - `removed_entries`: gone from the notebook too. A real deletion, written and reported. - `unreproduced_entries`: still in the notebook, absent from the new parse. Only the collector can explain that, so this one stops the write. Entries are matched by link target rather than by whole text, since the same reference reads differently in a numbered list, a table cell and the derived block while its DOI stays put; template placeholder lines are not content, so a fresh project still syncs. `--allow-entry-loss` overrides unconditionally: a guard with no way past it becomes a reason to delete the guard. The collector comment now states what the scanner actually recognizes and what it cannot see, and the test claiming "any layout" is renamed to the six layouts it proves. The refusal is verified against the pre-fix tree, where the same input silently rewrites a curated block of 13 entries down to 3 and drops the reference entirely. Also: the table-cell pipe message named only remedies that do not compile inside an array or tabular column rule, and now says to move the formula out of the table; the refresh entry point had no user-facing documentation and is now in the research-team capabilities index with the refusal semantics. 95 project-contracts tests, markdown-hygiene smoke suite, governance sync, git diff --check all pass; real project parses 15 sections / 3 references unchanged.
…nstead of judging
Both seats returned CHANGES NEEDED and converged, independently, on the same
root cause: the guard added last round was itself an unmeasured absolute, and
measurement destroyed it. Between them they demonstrated four ways an ordinary
refresh silently deleted a curated reference, all on input the tool had written
itself, through the documented entry point, with exit code 0 and an all-clear
report:
- `_is_placeholder_entry` asked whether an entry starts with "(" and ends with
")". Every Markdown inline link ends with ")", so every reference opening
with a qualifier — "(Erratum)", "(Preprint)", "(Dataset)" — was declared
non-content and left unprotected. One seat measured 15.5% of 400 trials
losing an entry, and 0 of 400 with the qualifiers removed: the attribution
is that single line.
- Survival was tested by substring containment against the concatenated new
entries, with `any` sufficing. Sequential identifiers stand in a prefix
relation, so a record ending 117532 was "found" inside 1175321; a paper and
its erratum absolved each other through a shared preprint link. 13.5% of 600
trials, with no qualifiers involved at all.
- Only bullet lines were protected, so a NUMBERED bibliography — the style of
the original incident — had none, while the refusal text was telling
operators to hand-edit the block.
- An unlinked entry fell back to whole-line matching, so reformatting it into
table cells made the guard classify it as a deliberate deletion.
The deeper finding is the one that decides the shape of this fix: deciding
whether a vanished entry was deleted on purpose is itself a text heuristic,
and every version of it has been measured destroying real references. So
nothing decides it here any more.
The sync now never removes a line it did not just re-derive. Existing lines
the scan does not reproduce are written back verbatim under a heading that
says so and reported in `retained_entries`. Under-collection costs a stale
line a reader can see and delete; it cannot cost a bibliography. This is
structural rather than a judgement, which is the only reason it can be stated
without measuring the scanner first. Matching still decides whether a line was
REPRODUCED, but a matching mistake now costs a duplicate, not a deletion —
and it was tightened anyway: placeholders by exact text, link targets by exact
identity, every non-structural line protected.
`--allow-entry-loss` becomes `--drop-unreproduced`, the one path that removes
anything. The refusal and its exception are gone: retention is strictly safer,
since refusing left the block stale AND blocked the refresh.
Measured, not asserted. An independent balanced-paren target extractor, over
1500 two-sync trials that only ever RESHAPE the bibliography and never delete
from it: 825 of 825 writes destroyed a reference before, 0 of 1500 after. Over
all 22 real projects: 0 targets lost, and the project whose milestone is
blocked on this now refreshes cleanly (15 sections, 3 references, 3 lines
retained) where it previously refused outright. All four named defects
reproduce on the previous commit and are closed here.
Residual, stated rather than papered over: the scan still under-collects, so
retained lists will be non-empty on 12 of 22 real projects until someone
prunes them; one project's contract carries no sync markers at all and errors
out, which predates this branch.
Also: `--force` stays destructive, but the run report now names what it
replaced, instead of reading clean because the overwrite happened before the
sync looked at the file. `+` bullets are no longer emitted as `- + `, which
rendered as a nested list.
103 project-contracts tests, markdown-hygiene smoke suite, governance sync,
shell-boundary lock, git diff --check all pass.
…instrument that could not see them Both seats returned CHANGES NEEDED, and the host seat's methodological finding matters more than any single defect: my measurement was blind by construction. The sweep I submitted observed LINK TARGETS. The failure mode preserves the target set — the shared target survives on the other entry — so the observable and the matching rule under test share an equivalence class. On the same 800 trials the host ran both: link-target observable flagged 0, entry-identity observable flagged 33. "0 of 1500" is reproducible and correct as a statement about link targets, and it was never evidence for the structural claim. I had independence of implementation and no independence of observable. Re-measured with a line-identity observable over 7084 reshape-only two-sync trials: 6229 trials lost real content on f352a0f, in exactly the six shapes the two seats named. 0 here. What was destroying content, both seats converging: - `derived_targets` was a UNION over all derived references, so an entry was judged reproduced when its targets were covered COLLECTIVELY by other, different entries — the very hole round 5 named and this branch's docstring declared fixed. On real data: CornellPotential's Eichten erratum chain, where a curator groups the erratum under a "### Errata and corrections" subsection, deletes nothing, and loses the erratum on the next refresh. Measured at 4.1% in the conjunction cell, 0% from either ingredient alone. - Target-set matching also treated an erratum as reproduced by the article it corrects, and a curator's annotation as reproduced by the bare entry. - The target regex truncated at the first ")", so two Elsevier DOIs differing only after "(01)" became one key. Latent today; one project already cites such a DOI. - Retention eligibility excluded lines by SHAPE — anything starting with "#", "<!--", "- Source notebook:". That silently deleted a sub-heading a curator added, a reference commented out with its reason, an entry beginning "#1729 internal report", and a genuine notebook reference reading "- Source notebook: [archived scan](...)". The root cause of all four is one asymmetry the previous round got wrong: retention disarms a false NEGATIVE only. A false POSITIVE is neither retained nor re-derived, so it disappears. Matching is therefore exact text and consults no link targets at all; "is this the module's own output" is answered by exact identity with what this module emits, not by how a line looks. Also fixed: retained lines are verbatim, so a Markdown hard break survives; a contract whose sync markers are out of order is refused instead of duplicating the span between them on every sync (unbounded growth, pre-existing); the file's own line endings survive the round trip, since reading with universal newlines rewrote CRLF contracts in regions this function has no business touching (pre-existing). The claim is now bounded rather than absolute, in the source and in the skill doc: a content line is removed only if the new parse derived a line with exactly the same text, or drop_unreproduced was passed — with the module's own emitted lines and the out-of-order refusal named as the exceptions. The host also showed the previous suite had no discrimination against this bug class: an any-overlap mutant passed all 103 tests, because every retention test placed the unreproducible entry's target nowhere else. The suite now carries cases where a SURVIVING derived entry would absolve the vanished one, and five mutants — any-overlap, substring containment, shape-based structure exclusion, rstripped retention, unvalidated marker order — were each verified to fail it. 111 tests; 22 real projects, 0 targets lost; smoke suite, governance sync, shell-boundary lock, git diff --check all pass.
…ted one Round 7 returned four more blocking defects, and the blocking count across rounds 5, 6 and 7 was 5, 3, 4 — not decreasing. This session measured that exact signal on two real projects and concluded that the right response to it is to re-decompose the object, not to review it again; the harness itself mandates the same narrowing at its round cap, whose default is 5. This is round 7. So this commit applies that rule to itself instead of shipping an eighth patch. All four rounds of defects had one cause: a machine-derived region and a human-curated region occupying the same lines, with a text heuristic arbitrating which was which. Every such heuristic — target sets, unions, placeholder shapes, structural shapes, exact bodies — was measured deleting real references, and it could not have gone otherwise. A false positive there is neither retained nor re-derived, so it disappears. The two regions are now separated rather than arbitrated: - `sync_research_contract` writes in place ONLY while the block is still byte-for-byte the one the scaffold template ships — the state it is in immediately after creation, where nothing can be lost. That is a comparison against a fixed known string, not a parse, so it has no equivalence class to be wrong inside. Any other block is refused, untouched. - `propose_research_contract_block` derives the block a refresh would produce and writes it to `artifacts/research_contract_block.proposed.md`. The contract is never opened for writing. A blind spot in the scan now costs an incomplete reading that a person can ignore, never a bibliography. The refresh entry point uses the proposal path; `--drop-unreproduced` and the whole retention layer are gone, along with the CRLF juggling that only existed because the contract was being rewritten at all. Round 7's four findings are closed by removal rather than repair: exact-text matching that still stripped markers and collapsed whitespace; module-output detection that accepted arbitrary checksum values; an observable blind to multiplicity and hierarchy (two identical references under two headings collapsing to one); and global line-ending normalization outside the marked region. None of those code paths exists now. Measured on the claim as now stated, with sha256 as the observable — no equivalence class shared with the code, which is what defeated the previous three measurements: 56784 trials across eight notebook shapes, LF/CRLF/mixed line endings, and every entry shape both seats used to produce losses. Bytes changed: 0. In-place writes refused: 56784. On all 22 real projects the contract is byte-identical and nothing errors — including the one whose contract has no sync markers at all, which the proposal path does not need. CornellPotential proposes 15 sections / 3 references. 98 tests, smoke suite, governance sync, shell-boundary lock, git diff --check.
…write what the project owns Round 8 found that the half of the re-decomposition I had just written was worse than everything it replaced. `propose_research_contract_block` validated its destination for policy and for being inside the project root, then wrote unconditionally — never comparing it to the contract or the notebook. Through the shipped entry point, `--proposal <the contract>` exited 0 and replaced a ten-kilobyte curated contract with a 585-byte generated stub. `--proposal <the notebook>` did the same to the project's narrative record, and then reported a `notebook_sha256` computed after the file was already gone. And it needed no unusual argument at all: a symlink at the DEFAULT proposal location pointing at the contract sent the write straight through it. The field that should have reported this was `"contract_modified": False` — a hardcoded literal the function never observed. The test asserting it therefore could not fail, in any circumstance, whatever the function did. Rounds 4-7 damaged lines inside the block. This removed the file. Fixed: - The destination is refused when it resolves to the contract or the notebook, when it is not a regular file, or when something already exists there that is not a previous proposal (identified by the header sentinel). Comparison is on RESOLVED paths, because the symlink case needed no flag. - `contract_modified` is computed from a before/after digest of the contract. - The notebook digest is computed once, before any write. - The in-place path keeps the file's own line endings again and decodes strictly instead of replacing bytes it cannot read: the regions outside the block belong to the project even when the block is still the template's. Documentation, which the previous commit removed the behavior of and left standing: SKILL.md still described the deleted in-place refresh, including `--drop-unreproduced`, which now exits 2. It now describes the proposal workflow, says the contract is never modified, says why, and states that the scan's blind spots make the proposal incomplete rather than the contract wrong. That is the knowledge the new design depends on the reader having. Tests. The old destination test asked "was the path outside the project root", which is the question `assert_path_within_project` asks — the same equivalence class as the rule under test, so it could only confirm the check exists, while every destination in this finding was INSIDE the project. The observable is now a sha256 digest of every pre-existing project file, over four in-project destinations including the symlinked default. Also locked: the unstated invariant that the template's block carries no render placeholder, which is the only reason comparing the raw template against the rendered one works. Measurement, addressing the other half of round 8's critique — that 56784 trials which all landed in one branch had measured one branch. 324 trials now cover BOTH write paths (72 writes, 576 refusals) across nine destinations, four notebook shapes, and LF/CRLF/mixed endings, with a whole-project digest as the observable: 180 of 324 trials damaged a pre-existing file on 577b47c, 0 of 324 here. 101 tests, smoke suite, governance sync, shell-boundary lock, git diff --check.
…malization paths The two seats diverged: one returned SHIP with seven LOW findings and argued explicitly against another round; the other returned CHANGES NEEDED with three BLOCKING and one MAJOR. Tracing the evidence rather than counting the verdicts, they had found the SAME mechanisms and graded them differently — every one of the cross-family seat's blocking items appears in the other's list as LOW, with measured reachability attached (34 destinations, 2 damaged, both requiring a file the user had created carrying this tool's own generated-output banner; 12 real projects at the default invocation, 0 files changed). So the disagreement is not resolved by picking a seat. The mechanisms are real and cheap to close, which makes the severity question moot: - Destination identity is now `st_dev`/`st_ino`, not a resolved path string. A hardlink is a second NAME for one inode and a case-variant is a second spelling; both reached the contract past a path comparison. - The write goes through a fresh `O_EXCL`/`O_NOFOLLOW` temporary file and a rename, so the gap between checking the destination and writing to it can no longer be filled by a symlink. Following a link is not something the write can do any more, rather than something it is asked not to do. - The in-place path splices the block into the original text instead of normalizing the whole file, so a contract with mixed line endings keeps its owner-authored regions byte-for-byte. Only the block adopts the surrounding ending. - The notebook digest really is taken once now. It was still being recomputed inside the block builder, so the previous commit's claim was false — and fixing it exposed that the proposal path was passing a Path where a digest was expected, which the new test caught immediately. Claims brought back to what the code does: the module comment still promised a `drop_unreproduced` retention guarantee whose flag was deleted two commits ago — the fourth false absolute this file has carried, now replaced by a statement of where the guarantee actually lives. `SKILL.md` claimed the destination check refuses "any path that already holds something the project owns"; it now states the identity test, the generated-output exception, the atomic write, and that a run observing the contract change exits non-zero. The refresh entry point does exit non-zero in that case, which nothing acted on before. Tests. The seat that shipped also reported that reverting `contract_modified` to the hardcoded literal it had blocked on still passed all 101 tests — the protection was locked, the reporting was not. Four negative controls now fail the suite: the literal, path-string identity, the direct write, and removing the destination guard entirely. The digest is locked by counting the calls. Measured: 324 trials across nine destinations, four notebook shapes and LF/CRLF/mixed endings, entering both write paths (72 writes, 576 refusals), observable a sha256 digest of every pre-existing project file — 0 damaged. End-to-end replay of the exact construction both seats named (a hardlink at the default proposal location, contract carrying the sentinel): exit 1, contract 2356 bytes unchanged, all 60 reference entries intact. 105 tests, smoke suite, governance sync, shell-boundary lock, git diff --check.
…ed claims Round 10's shipping seat found one mechanism this branch had introduced and three claimed protections with no discriminating test. All four are closed here as the single follow-up commit that seat prescribed, rather than an eleventh round. The mechanism: `_write_proposal_atomically` used a FIXED `<destination>.partial` sibling and unlinked it unconditionally. The destination guard validated the destination, never that sibling — so `--notebook P/nb.partial --proposal P/nb` deleted the research notebook at exit 0, and two concurrent runs sharing the one name each removed the other's file mid-write. It now uses `tempfile.mkstemp` in the destination directory, so the name is fresh, created O_EXCL by the library, and belongs to nobody. The rename is unchanged and still the reason a link planted after the check cannot capture the write. Three claims that were true but unlocked, each verified against a mutant that previously passed the whole suite: - Atomicity. Gutting the atomic write while keeping its name passed all 105 tests; the one mutant that did fail, failed only because another test patches that attribute for an unrelated purpose. There is now a test that plants a symlink at the destination after the guard returns and asserts the contract survives. - The temp file's neighbours and leftovers: an owner-authored file named `<destination>.partial` survives, and no temporary file remains afterwards. - The refresh entry point's non-zero exit when `contract_modified` comes back true. Nothing in the repo executed that script at all, so its exit statuses — the one signal an operator consumes — were unlocked; deleting the branch left the suite green. `tests/test_refresh_entry_point.py` runs the shipped script as a subprocess for the ordinary and refusing paths, and drives the branch itself for the third, because every filesystem state that would make the contract change is now refused by the guard first and would exit 1 for the wrong reason. And one more comment claiming more than the code delivers, in the file whose history is exactly that: the splice said the block adopts the surrounding line ending, but neither disjunct of that condition can be true — the write only runs when the block is byte-for-byte the LF template, so a CRLF block never reaches it. The guarantee that does hold, and that matters, is that every byte outside the markers is carried through untouched. The comment now says that and nothing more. Five negative controls now fail the suite: the fixed temp name, the gutted atomic write, path-string identity, the `contract_modified` literal, and the deleted exit branch. 111 tests; 324-trial sweep across both write paths still reports 0 pre-existing project files damaged; smoke suite, governance sync, shell-boundary lock, git diff --check.
…for-byte true
The cross-family seat's first attempt produced no verdict at all, which is a
dispatch failure rather than a review, so it was re-run on the same tag. The
re-run returned three blocking findings. Its sandbox denied every temporary
directory, so it could execute neither the mirror, the suite, the mutants nor
its own sweep — its probes were simulated. All three were therefore reproduced
here against the real code before acting, and all three are real:
1. The predicate named and documented "byte-for-byte" used `.strip()`. One
added newline, space or tab at a block boundary produced a non-template
block that was accepted and rewritten anyway. Measured on all three
mutations; all three returned true. This is the fifth time this file has
carried a claim stronger than its code, and the answer is the same as the
other four: make the claim true rather than soften it. The comparison is now
exact, and the state the in-place write exists for — a contract this run
just wrote from the template — still compares equal.
2. The in-place write still re-opened the checked path with `open("w")`, the
same capture that was closed on the proposal path and left asymmetric here.
Planting a symlink after the template check redirected the write.
3. Atomic replacement protected only the destination leaf. Renaming its PARENT
to a symlink after validation redirected both `mkstemp` and `os.replace`,
because each re-resolves the path: a probe replaced an owner's file that
way, `OWNER` becoming derived block bytes. No directory descriptor was
pinned anywhere.
There is now one writer for both paths. It opens the destination's parent ONCE
with `O_NOFOLLOW`, so a directory swapped for a symlink is refused outright,
and does every later operation — the `O_EXCL` temp create and the rename —
relative to that descriptor. Renaming the directory afterwards cannot move
where the write lands. The in-place contract write goes through it too, which
is what closes finding 2 and removes the asymmetry.
Re-verified by direct probe after the fix: the three whitespace mutations are
refused and the untouched template is still accepted; the parent swap raises
`NotADirectoryError` with the owner's file intact; the planted contract symlink
leaves its target byte-identical, because the rename replaces the directory
entry instead of writing through the link.
Five negative controls, each verified to fail the suite: `.strip()` comparison,
`open("w")` for the in-place write, the parent open without `O_NOFOLLOW`, a
fixed temp name, and — from the previous commit — the `contract_modified`
literal.
114 tests; 324-trial sweep across both write paths still 0 pre-existing project
files damaged; 22 real projects, 0 targets lost; smoke suite, governance sync,
shell-boundary lock, git diff --check.
…e O_NOFOLLOW claim honestly
Round 11 shipped, executed every check, and returned four LOW findings. Two of
them are taken here because each lands on a failure this lane has been about.
The docstring said "renaming the directory afterwards cannot move where these
writes land." That is false. `O_NOFOLLOW` constrains only the FINAL component
of the path being opened; components above it are still walked, so swapping a
GRANDparent for a symlink mid-run redirects both the temp create and the
rename. A probe with no patching of the module at all reproduced it on both
write paths — an owner's file that no argument ever named was replaced, and the
writer reported success. This is the sixth claim in this file stronger than its
code, and it was written in the commit whose stated purpose was closing that
habit. The paragraph now states what is actually closed (every construction
that can be built ahead of time, plus a swap of the immediate parent) and what
is not (a live race whose winner already holds write permission inside the
project, and could therefore overwrite the file directly without any of this).
The second is a real regression this branch introduced, and the reason it went
unseen is worth more than the bug. Replacing an inode by rename does not carry
the old one's permissions, so a contract its owner had chmod'd 0600 came back
0644 after `init --force` — a deliberately private file made world-readable,
with its content byte-perfect. Every test in this suite compares content
hashes, so no number of additional tests in that style could have seen it. The
writer now restores an existing destination's mode before the rename, and the
suite gains a SECOND observable — a census of permission bits — alongside the
content digest.
Related, same cause: `rename` needs write permission on the directory rather
than on the file it replaces, so the atomic writer silently overwrote a
read-only destination where the previous `open("w")` raised. This module
refuses rather than clobbers everywhere else; it does here again.
Not taken, with reasons: an orphaned temp file after a kill is never swept
(cosmetic, hidden, no content at risk — and self-healing would mean returning
to the fixed name that was the deletion defect); and a 240-character
destination leaf is refused by ENAMETOOLONG, which is fail-closed.
117 tests; two new negative controls (mode not preserved, read-only not
refused) each fail the suite; the 324-trial sweep across both write paths still
reports 0 pre-existing project files damaged; smoke suite, governance sync,
shell-boundary lock, git diff --check.
…ent, and state what is not carried The cross-family seat's third consecutive verdict rests on source reading — its sandbox has denied every temporary directory for three rounds, so it executed neither the suite, the nine mutation controls, nor any filesystem probe. It has kept finding real defects that way, and this is another: `os.replace` installs a new inode, and only `st_mode` was being carried onto it. Extended attributes and ACLs were not. The mechanism is real. What makes it worth a commit is not its reachability — the contract is only written while it is a file this run just created, which has no user-set attributes yet, and the proposal is regenerable by construction — but that the claim under review said "no file the project already had ... has its attributes changed". That was false beyond the mode bits, and a claim stronger than its code is the seventh instance of the defect this lane exists to stop. Extended attributes are now carried where the standard library exposes them. The limits are stated in the function rather than implied away: macOS builds ship no `os.listxattr`, so Finder tags and resource forks are not carried there; POSIX ACLs are carried on no platform, because the standard library cannot read them; and the residual is bounded by what these two destinations are, not by anything this function does. A single unreadable attribute is skipped rather than costing the write, since content is what is being protected. 118 tests (the new one skips where the platform has no extended attributes, and runs on the Linux CI image); the 324-trial sweep across both write paths still reports 0 pre-existing project files damaged; smoke suite, governance sync, git diff --check.
…Linux claim was measured Round 13 caught a regression I introduced and, more usefully, caught me asserting a number I had no way to produce. My previous message claimed "118 passed on Linux". This machine's Python has no `os.listxattr`, so the new test skips here and the local suite is green whatever the attribute code does. The reviewing seat started a Linux VM, ran the suite there, and found `1 failed, 116 passed` — the parent commit green, this one red. It also pointed out that one container would have turned my expectation into an observation. It would have. I have now run one, and every number below is measured on Ubuntu with CPython 3.12 as an unprivileged user, not inferred from macOS. The defect: `os.setxattr` accepts no `dir_fd` parameter on any platform (measured: `os.setxattr in os.supports_dir_fd` is False). The call raised `TypeError`, which is not an `OSError`, so the handler whose stated job was "one attribute that cannot be set must not cost the write" did not catch it — it escaped the public API. CI runs on ubuntu-latest, where `os.listxattr` exists, so this took a green pipeline red for any destination carrying an attribute. And attributes arrive without a user setting them: `security.*` labels are in the same namespace and reached through the same call. Dropping `dir_fd=` would not have fixed it. `tmp_name` is a bare name meaningful only against `parent_fd`, so the bare call raises `FileNotFoundError` — an `OSError`, silently swallowed, attributes never carried and nothing said. The temp file is now opened against `parent_fd` and the descriptor is what `setxattr` receives (measured: fd-based setxattr works). Two claims in that docstring were also wrong, both now measured rather than asserted: - "POSIX ACLs are carried on no platform, because the standard library cannot read them." False. On Linux `os.listxattr` returns `system.posix_acl_access` and `getxattr` reads it, so ACLs ARE carried. This is the third consecutive rewrite of that paragraph for accuracy, and the reason is the same each time: it was written from expectation. - The ordering claim. Attributes are carried before the mode is restored, and the seat measured attribute loss at modes 0400 and 0444 when that order is reversed. But those destinations are refused outright before any write, so the loss is not reachable while that refusal stands. Measured: 0400 and 0444 refuse, 0600 carries the attribute. The ordering is therefore defensive, not load-bearing, and the docstring now says which. Measured, both platforms: - macOS: 117 passed, 1 skipped (the extended-attribute test skips here). - Linux, unprivileged: 117 passed, 1 skipped, 7 subtests — the skip is the case-variant test, since the filesystem is case-sensitive, and the extended-attribute test runs and passes. - Negative control on Linux: restoring `dir_fd=` to the setxattr call fails `test_extended_attributes_survive_the_replacement`. - Running the suite as root on Linux fails the read-only-destination test, because root bypasses the `os.access` check. Recorded, not fixed: CI does not run as root, and a root researcher overwriting their own read-only file is not the failure this guards. Smoke suite, governance sync, git diff --check.
…d measure the claim that separated them
The two seats split: one shipped after executing everything, the other returned
two blocking and two high findings. Tracing rather than counting, all four are
real and all four are cheap, so the severity disagreement is moot:
- Source attributes were read through the absolute path while the parent was
pinned by descriptor. A rename between the pin and the read could return
ENOENT, or return a DIFFERENT file whose ACL would then be copied onto the
destination. Both files are now addressed by descriptor.
- `os.close` on the attribute descriptor sat outside the non-fatal handler, so
an EIO there propagated into the caller's cleanup and destroyed a content
write that had already succeeded — the exact opposite of this function's
stated contract. Measured: with `close` raising EIO, the write now completes.
- "Everything `os.listxattr` exposes" was inaccurate, as BOTH seats said. A
listed attribute can be unreadable (mode 0200 lists what `getxattr` refuses)
or unsettable (`security.*` for an unprivileged writer). The sentence now
names both exclusions.
- The claim that carrying attributes before restoring the mode is "defensive
rather than load-bearing" was false, and this is the ninth claim in this file
stronger than its code.
That last one is worth the detail, because I nearly dismissed it on a probe of
my own that did not construct the scenario. The reasoning was: the modes where
a reversed order loses attributes (0400, 0444) are refused before any write. The
cross-family seat named a mode that is NOT refused — a file owned by someone
else at 0460 with the caller in its group. My first probe made the file
caller-owned, so the owner bits applied and it was refused, and my second forgot
that a directory without setgid does not give a new file the directory's group.
Neither probe built the case. Built properly — uid 1002 owner, group `shared`,
mode 0460, caller uid 1001 in that group — `os.access(W_OK)` returns True, the
refusal does not fire, and the ordering carries the whole weight:
current order (carry, then chmod): attribute survives, mode 0460 kept
reversed order (chmod, then carry): attribute list EMPTY
So the ordering is load-bearing, the docstring says so, and it says so from a
measurement instead of from a deduction that happened to be wrong.
Verified on the platform and version that matter, following the reviewing
seat's two corrections to my own method: CPython 3.11, which is what
`.github/workflows/ci.yml:162` actually pins — not the 3.12 I used last time —
and on an ext4 volume rather than the container's overlay or a virtiofs bind,
since extended attributes and ACLs are filesystem-dependent. Linux,
unprivileged: 117 passed, 1 skipped, 7 subtests. macOS: 117 passed, 1 skipped.
Not changed, with the reason: `os.setxattr` is absent from `os.supports_fd`
although it accepts a descriptor on 3.11, 3.12 and 3.13. The introspection is
incomplete, not the capability; recorded so a later hardening pass does not
"fix" working code by trusting it.
…escriptor; lock what this lane established Both seats again found the same structure from different angles, and both were right. The cross-family seat: `stat` and `access` inspected the destination, and the source descriptor was opened afterwards with nothing tying the two together. A rename in that window bound the descriptor to a different inode, whose ACL was then copied onto the destination — it reproduced exactly that, granting access from a file the caller never named. Last round closed the path-based READ; this window remained. The executing seat, from the other side: the exclusion the docstring named first — a destination that lists attributes but refuses to read them — became unreachable when the source moved to a descriptor. It swept all 512 modes and found none where the open succeeds and the read then fails. Meanwhile the exclusion this code actually has, a destination that cannot be opened for reading at all, was documented nowhere. Fifth consecutive version of that paragraph to be wrong. One change answers both: the destination is opened FIRST, its mode comes from `fstat` on that descriptor, and the name is then confirmed to still refer to that inode — a mismatch is refused rather than written. Mode, attributes and the refusal decision are now one descriptor's story. The docstring states the exclusion the code has instead of one it inherited from the code it replaced. The executing seat also showed that nothing locked the three behaviours the previous commit introduced: of twelve mutants, the five that survived included reversing the carry/chmod order, making an attribute failure fatal, and deleting the close handler — precisely what that commit changed. The only attribute test used a caller-owned proposal, where the order cannot matter. So the property that cost an entire review round to establish had nothing holding it. Four tests now do, and each was verified against its mutant: - the carry is observed to run before the chmod; - an attribute that cannot be set does not cost the write; - a close error on the attribute descriptor does not cost the write; - a destination swapped during validation is refused. Two of those only run where extended attributes exist, so they were verified on Linux 3.11 in a container, not asserted from here. The close test needed fixing first: without an attribute on the destination the carry early-returns and never opens the descriptor whose close is under test, so the first version passed against its own mutant. With an attribute planted, both mutants fail. Also: the read-only-destination test skips as root, which holds CAP_DAC_OVERRIDE and makes `os.access` ignore the mode — it was a false failure for anyone running the suite in a root container, and CI is unaffected either way. Recorded, not changed, because it is pre-existing and not this commit's to decide: replacement by rename transfers ownership, so a shared contract owned by someone else comes back owned by the caller. Named ACL entries survive by uid — which is what makes the carry worth keeping — but the original owner can fall to `other::---` on their own file. If that is worth addressing it is its own lane. macOS: 119 passed, 3 skipped. Linux 3.11 unprivileged, ext4: 121 passed, 1 skipped, 7 subtests. 324-trial both-path sweep: 0 pre-existing project files damaged. Smoke suite, governance sync, shell-boundary lock, git diff --check.
…e it was about Round 16 split, and each seat found what the other could not. The executing seat re-derived every claim, swept all 512 modes to confirm the documented exclusion is now exactly the code's, and tried to defeat the new identity check at every reachable instant — including unlinking the destination and churning 4000 inodes to reuse its number, which failed 4000 times because the held descriptor pins the inode. It shipped, with six findings for separate commits. The cross-family seat found a gap none of that could see, because it is one level above the writer: `sync_research_contract` validated the CONTENT at one inode and then handed the writer only a PATHNAME. The identity check bound the writer's open to the writer's own stat — not to the content read that decided the write was safe. So a rename in between applied the judgement "that block is still the template, so overwriting it loses nothing" to a file the judgement was never about. Measured on the parent commit: a curated contract swapped in after the check is overwritten, exit clean. That is the D37 incident arriving through the one door the previous fix left open. The descriptor is now opened before the content is read, held across the decision, and passed to the writer, which binds to it rather than opening the name again. Measured after the fix: refused, curated content byte-identical. The executing seat's first finding is closed in the same commit because it is the same property from the other end: reverting the attribute carry to read the source by path passed the entire suite while copying an intruder's attributes onto the destination. There is now a test that swaps the file at the destination immediately before the carry and asserts the intruder's attribute is absent. Verified against a faithful revert on Linux — the first two mutants I tried proved nothing (one referenced a variable that does not exist, so it failed 26 tests with NameError; the other read through `/proc/self/fd`, which resolves to the same inode and is therefore equivalent, and passed). The third, which restores the path parameter and reads by path as a real revert would, fails. Also scoped honestly rather than fixed: the read-only refusal calls `os.access` on the NAME, so an ABA swap bypasses it — both seats found this, and the executing seat measured that nothing leaks when it happens, because the identity check still holds. It is a courtesy against an operator's accident, not a guarantee against a racing writer, and the comment now says exactly that instead of implying the whole decision is descriptor-bound. Left for separate commits, all pre-existing and none introduced here: the write-only fallback branch takes its mode from a path stat with no descriptor to bind it to; two earlier-round tests guard properties they can no longer reach (one swaps the parent before the writer is entered, so the parent open raises ELOOP and the rename is never reached; the other plants a neighbour under a naming scheme the code no longer uses); replacement transfers ownership. macOS 120 passed / 4 skipped. Linux 3.11 unprivileged on ext4: 123 passed / 1 skipped / 7 subtests. 324-trial both-path sweep: 0 pre-existing project files damaged. Smoke suite, governance sync, shell-boundary lock, git diff --check.
… and correct two comments Round 17 is the first round in seventeen where both seats returned clean, and neither did so by lowering its standard: the executing seat ran 44 faithful mutations — every one parsed before running, no NameErrors, no syntax breaks — and reported 32 killed, 12 surviving. The surviving list was the deliverable I asked for, and this commit spends it. The two that matter are the same shape as round 16's, and worse in one respect: this lane both STATED and MEASURED these properties and still never held them. - `newline=""` on the contract read. Dropping that one token passes the entire suite while destroying all 190 CRLF pairs in a contract's owner-authored regions — a file edited on Windows or through core.autocrlf. The write side is inert on POSIX, so the lock is a mixed-ending fixture rather than an assertion about the parameter. - `errors="strict"`. Flipping it to "replace" passes the whole suite while substituting U+FFFD into owner text; one cp1252 byte pasted from a PDF is enough, and the file grew 91 bytes in the seat's probe. Both locks verified against their reverts: each mutation now fails exactly the test that names the property. A third lock covers the descriptor the writer opens for itself — closing it unconditionally was already locked, because that double-closes and takes 22 tests red, but NOT closing it leaked one descriptor per call and was green. Two comments corrected, both caught independently by both seats: - The `os.access` note described the bypass in terms of the statement order from BEFORE the previous commit moved that check below the identity check, and was carried across the reorder unchanged. Under the current order the sequence it describes does not bypass anything; the real window is elsewhere, and it now says where. - The attribute docstring named "a write-only file" as the tenant of its fallback branch. A seat instrumented that branch across the whole suite and 140 ordinary calls: entered exactly once, by a SYMLINK, never by a write-only destination — which both entry points refuse earlier. Naming that exclusion correctly has now taken three attempts, and the paragraph says what the evidence says instead of what seemed likely. Left for later with reasons recorded rather than deferred silently: the proposal path still binds its sentinel check to a pathname while the sync path binds to a descriptor — race-only, needs a hostile rename inside the project, and the matching commit to the one that fixed the sync side; the write-only fallback's mode comes from an unbound path stat, same conditions; a symlink destination yields a 0o777 mode on Linux through that branch; temp-file cleanup on the failure path, and two older tests that no longer reach the property they name. None is reachable without an adversary already able to overwrite the file directly. macOS 123 passed / 4 skipped. Linux 3.11 unprivileged: 126 passed / 1 skipped / 7 subtests. 324-trial both-path sweep: 0 pre-existing project files damaged. Smoke suite, governance sync, shell-boundary lock, git diff --check.
…write fix, converged over seventeen review rounds The reported defect: `nullius init` on a mature project rewrote the notebook-derived block of a user-owned research_contract.md, replacing curated section entries and DOI references with derived content. It blocked a project's milestone outright. The fix that closes it is small — init syncs only a contract the same invocation just created — and both seats confirmed it byte-identical in every round from the fourth on. Everything else in this branch came from what the reviews found while confirming it. The load-bearing lesson, because it recurred: a derived region and a curated region sharing the same lines cannot be told apart by a text heuristic. Four rounds of increasingly careful rules — bounded truncation, fence tracking, placeholder shapes, target sets, exact bodies — each shipped a new way to delete a real reference, and each was measured doing it at 13-15% on generated corpora. The regions are now separated rather than arbitrated: in-place writing happens only while the block is byte-for-byte the scaffold template's, and a mature contract gets a derived block written to a separate proposal file that never opens the contract for writing. Measured on the merged state: 324 trials across both write paths and nine destinations damage 0 pre-existing project files; all 22 real projects keep their contracts byte-identical; the project whose milestone was blocked proposes 15 sections and 3 references cleanly. Also here: markdown-hygiene no longer flags code-formatted link labels, and gained rules for bare pipes in table math and for LaTeX column specifications; the refresh entry point is documented; `--force` reports what it replaced. Convergence was two clean verdicts from independent seats, not a declaration. The seats proved complementary rather than redundant — one executed and measured, including starting a Linux VM to run code that cannot run on this machine; the other read source under a sandbox that could execute nothing and found defects the first had missed. Neither's severity grades were taken on trust; every finding was reproduced before being acted on, and several of my own probes had to be corrected first.
Two schemas land the vocabulary real projects were found reinventing in 11+ spellings: run_origin_v1 (binding-quality ladder exact_clean / exact_tracked_snapshot / head_plus_untracked / aligned_heuristic / unbound; snapshot tree hash as code identity; backfill alignment fields) and validity_event_v1 (supersede/void/reinstate/stamp with required reasons, scoped supersession, ULID event identity). packages/shared gains the path-keyed ledger lock (O_EXCL, bounded retry, fail-closed, no automatic reclamation) and standard ULID minting. packages/orchestrator gains: - validity-ledger: locked idempotent appends (canonical-JSON equality, divergent event_id reuse refused at the writer), reader dedup with fail-closed integrity defects (worst-state void > superseded > active, no_authoritative_identity propagation), replay ordered by (ts_utc, event_id) so merge=union stays safe; - run-origin: snapshot-first capture (tracked_modified derived from the snapshot diff, no inspect-then-snapshot window), stash-create snapshot pinned at refs/nullius/runs/* with create-if-absent collision contract, pure-read untracked enumeration, dirty-submodule counting, honest unbound reporting for missing repos and unborn HEADs. 16 tests, each verified by a killed mutant (10/10; the lock mutant required rebuilding the shared package inside the mutation loop — cross-package mutants never reach vitest through stale dist output).
…d model nullius trace stamp|supersede|void|reinstate writes origin stamps and validity events (one-liner write surface for hand-created runs; --event-id resumes the same logical event after a crash; --reason mandatory for supersede/void; named --scope records partial supersession as annotation). nullius current renders the acceptance sentence as prose; nullius status --json now embeds the SAME traceability read model as its traceability block, so the mandated reconnect path carries run validity, stamp coverage, ledger health, the manuscript pointer (pointer-lines reader, validation deferred to report-validate), the standing binding caveat, and every honestly-unanswerable clause. Best-effort embedding: a traceability read failure degrades to traceability_error, never a broken status receipt. The stamp machinery's own artifacts (run_origin.json, validity ledger, its lock) are excluded from untracked-noise counting — stamping no longer demotes later stamps self-referentially (found live in the smoke test: the second stamp graded head_plus_untracked because the first stamp's mirror was untracked). Real untracked research files still demote; a test locks both directions and its mutant was verified killed. Front-door surfaces synchronized: command inventory, help topics, meta/front_door_authority_map_v1.json; full orchestrator suite green (728 passed, 10 skipped).
… GLM findings Event identity: one ULID per logical stamp for life — the stamp CLI now shares a single id between the origin payload and its ledger event, and an --event-id retry short-circuits on the LEDGER (was this event recorded?), never on payload comparison, so a legitimate crash-recovery retry whose re-capture would differ (time moved, tree moved) is no longer refused as divergent. Measurement honesty: a failing `git stash create` now throws instead of being conflated with a clean tree (the one direction the ladder must never err); submodule untracked content counts toward the dirty grade; a failed status/ls-files measurement renders as UNKNOWN, never as 'clean tracked tree'; an unborn-HEAD repository is its own unanswerable reason; heuristic and unbound stamp qualities are surfaced in the view, the prose, and an explicit per-run unanswerable clause; a present-but-unparseable manuscript pointer names the format problem instead of claiming nothing is promoted. Ledger integrity: line acceptance is semantic validation (ULID pattern, event enum, actor, ISO timestamp, per-event required reason/by_run_id/ payload), not shape-checking — invalid lines count as malformed and never replay; the writer inspects EVERY line carrying an event_id before deciding idempotent-vs-divergent; conflicting stamps for one run are a reported defect (never silent last-wins); integrity conditions render FIRST in the prose; the union-merge .gitattributes is now actually delivered (idempotent, never clobbers a hand-customized line); the parent directory is created before lock acquisition so team/runs-only projects can stamp. Concurrency: ref pinning uses git's atomic create-if-absent (empty oldvalue) closing the read-then-write race; the canonical-root rule is enforced at the stamp CLI (mirror of a canonical run is refused, naming the canonical path). Self-reference exclusion narrowed to exact machinery paths (ledger family under artifacts/runs; run_origin.json only inside the two run roots) — lookalike research files elsewhere count as untracked again. Public surfaces synchronized (help = inventory = authority JSON, --actor/ --event-id on every verb). 15 new tests lock the fixes; 12/12 mutants killed; full suite 743 passed.
…and event-id gates, read-model reach Deep stamp-payload validation: a stamp line now validates the full run_origin_v1 contract (ULID, timestamp, binding-quality enum, dirty shape, per-quality required fields) — an arbitrary object with the right schema_id can no longer masquerade as an origin record; invalid stamps count as malformed and never replay. Stamp targets are confined to the two run roots: a stamp elsewhere would enter the ledger yet stay invisible to every directory scan — a record about a run the read model can never show. The --event-id short-circuit accepts only a stamp event for the SAME run (reusing a void/supersede id fails loudly), validates the ULID before any mirror write, and therefore leaves no half-state on rejection. Read-model reach and honesty: the binding-quality distribution and stamp-conflict list now cover every stamped run the ledger knows, including ledger-only ids whose directory is gone; dirty submodule contents are probed by the view (same honesty as the stamp) and rendered; malformed ledger lines join the top LEDGER INTEGRITY CONDITION section; a current manuscript pointer without a Markdown link is its own unanswerable reason; the ledger block reports merge_union_declared with the negative direction test-locked. The stash-failure injection seals existing fanout subdirectories too (deterministic). 6 new tests; 6/6 mutants killed; full suite 748 passed.
…ink and divergent-id gates Ledger line validation now COMPILES the checked-in JSON Schemas (ajv 2020, validity_event_v1 + run_origin_v1 with their cross-file ref) instead of a hand-written approximation — two review rounds proved a hand validator drifts from the schema in both directions, and that whole class ends here. Additional-property refusal, integer dirty counts, deps/alignment shapes, and optional-sha patterns all now bind exactly as the schemas say. Two checks a JSON Schema cannot express stay in code: the timestamp patterns (ajv formats package deliberately not added) and the cross-field binding that a stamp payload must be ABOUT the run its event names. Stamp targets are symlink-resolved and a symlinked run directory is refused outright (the directory scan skips symlink entries, so stamping one would mint a ledger record invisible to every read). The --event-id preflight also consults the integrity-defect list — a divergent id is rejected before any mirror write instead of after it. Prose honesty: a current-manuscript pointer without a link renders through its unanswerable reason, never as 'id → (link missing)'; the merge=union declaration check is an exact token match (merge=unionish no longer counts); ledger-only quarantined runs appear in no_authoritative_identity. 4 new tests; 7/7 mutants killed; full suite 752 passed.
…ootstrap The current-results registry answers the acceptance sentence's first clause: a RESULT_REGISTRY block in project_index.md (scaffolded for new projects) with one row per headline result, each naming its producing run and that run's effective code identity. Selection stays research judgment; the machine enforces the rest — unique safe ids, exactly one current row per supersession chain with both direction columns consistent, artifact present and SHA-256-matched, and the named run stamped, ACTIVE in the validity ledger, unquarantined, and commit-consistent with its stamp. `nullius result set-current` writes rows (both directions maintained); hand edits stay legal and validate identically — this module is the ONE parser/writer for the block. The traceability view validates the registry on every read, renders current results in the prose, and reports a missing or empty registry as its own honest unanswerable reason. `nullius init` now owns git presence (D7): a full init of a non-repo root bootstraps a repository with a SCAFFOLD-ONLY initial commit (pre-existing research files stay untracked — an explicit track-or-ignore decision); --no-git records the decline in the runtime ledger and the traceability surface keeps reporting the unanswerable code-revision clause every reconnect; --runtime-only checks presence and suggests without creating anything scaffold-owned; an existing repository is left untouched. Stage-2 acceptance hook delivered: appendValidityEvent runs the compiled schema validator before locking, refusing events every reader would only quarantine. 10 new tests; 8/8 mutants killed (1 writer-gate + 7 stage-2); full suite 762 passed.
…ion, containment parity, recoverable bootstrap Round-trip safety (the silent-false-precision class both seats flagged): the writer refuses '|' and newlines in every interpolated field, and a pipe-prefixed line inside the block with the wrong cell count is now a reported malformed_result_row — a written row can no longer become 'no result registered' on the surface. parseRunCell never truncates an exotic run id at a lookalike separator and preserves the +snapshot marker into the parsed row. Chain protection at the writer (validator catches remained, writer refusal added): self-supersession, re-currenting a superseded id, and superseding a non-head are refused with directive messages; the validator gains report-registry-strength chain-shape checks (cyclic_result_supersession, result_chain_head_not_unique) with component rows marked defective. Containment parity: resolveResultArtifact walks every path component for symlinks and realpath-contains the leaf (the report registry's rule), and the WRITER pre-flights with the same resolver — no writer/validator asymmetry, no exit-0 minting of permanently defective rows. A provided linked description must link the hashed artifact; plain-text descriptions keep their words and gain the link. All-row liveness: every row needs its stamp; current rows additionally need active, unquarantined, exactly-bound runs (aligned/unbound stamps are refused as current-result backing). Defective current rows render with an explicit DEFECTIVE marker, never as clean results. D7 hardening: --no-git wins over --runtime-only (the decline is recorded, not ignored); a bootstrap failure after git init removes the fresh .git so a rerun retries cleanly; --allow-empty guarantees a HEAD even scaffold-less; --no-git joins the init help option list. project_index.md writes are atomic-durable; nullius current contains traceability failures locally. 12 new tests; 12/12 mutants killed; full suite green.
…detection, complete admission
The destructive rollback both seats converged on is now provably guarded:
.git is removed ONLY when this invocation created it (existence recorded
before git init; a pre-existing repository that fails the worktree probe —
dubious ownership, crash-truncated HEAD, inherited GIT_DIR — is left
untouched with an honest repair message). rollbackBootstrapGitDir is
exported and unit-tested in both directions.
Cycle detection now runs on the FORWARD supersedes pointers alone, so a
cycle whose reverse columns are hand-broken is still a cycle; head
uniqueness runs per UNDIRECTED component (union-find), so forks with two
heads are flagged and the >1-heads arm is genuinely reachable. The kept
twin of a duplicated id is marked defective (undecidable which line the
human meant).
Admission completeness: reparenting an existing superseder is refused
(the old target's reverse link would orphan); runs with CONFLICTING
stamps are refused at the writer and flagged by the validator; a
hand-written current row with no '@ sha' identity cell is its own defect;
artifact paths containing '(' ')' '#' are refused (link-truncation and
fragment-splitting); descriptions carry at most ONE link and it must name
the hashed artifact; the exact line about to be written is re-parsed and
must round-trip before any write.
Honest wording: rows-but-no-head states say so instead of 'registry is
empty'; snapshot identities render with the +snapshot qualifier end to
end (view field + prose).
9 new tests; 11/11 mutants killed (one required a clean-twin fixture —
the first fixture's other defects masked the duplicate-id path); full
suite green.
added 8 commits
August 7, 2026 06:25
… notebook staleness, coverage warnings
nullius trace backfill retroactively binds legacy runs by timestamp
alignment against the commit history — HEURISTIC by construction: every
record is aligned_heuristic (never exact-sounding) with its alignment
evidence (window to neighbouring commits, nominal-timestamp flag for
hand-rounded and day-only ids, ambiguous candidates), or honestly unbound
with a named reason; validity is NEVER backfilled; mirrors are best-effort
next to read-only legacy directories. Rehearsed on a lightweight COPY of a
real 461-run project: 456 aligned + 5 unbound in 4.8 s.
nullius trace propose-chains / confirm-chains turns the measured
review-driven redo pattern (same-slug r1→r2→…) into a PROPOSAL file the
human edits before anything touches the ledger; confirmation is
SEMANTICALLY idempotent (the supersession's presence on the ledger, not an
event id, decides 'already recorded' — the first mutation round caught the
event-id version double-appending). Runs anyone already decided about —
including a reinstate that put a run BACK to active — are never
relitigated; the redundant validity-state check that condition subsumes
was removed after its mutant survived. Rehearsal reproduced the audit's
numbers exactly: 38 pairs across 20 slugs.
The notebook checker classifies every ## section with the fully-specified
six-step worst-wins procedure (cites-superseded outranks commit currency;
sha-only stamps; project-level baseline set from the results registry with
fail-closed sentinel admission; current-modulo-untracked qualification;
incomparable never guessed). The shared read model gains the notebook
block, the D9 slug-dimension round-cap observation (team-cycle enforcement
stays authoritative), and the stage-1 hook: run-directory mirrors are
compared against the authoritative ledger stamp and divergence is surfaced
('trust the ledger'), never silently trusted. result set-current echoes
the +snapshot qualifier.
7 stage-3 tests; 10/10 mutants killed; full suite green.
…on, fence-aware sections, vanished-mirror surfacing A failed ledger append now removes the just-written backfill mirror — an orphan mirror with no ledger event behind it would look like a valid stamp to a human browsing the run directory (test injects a held ledger lock). Confirmation honors decisions made BETWEEN proposal and confirmation: every pair re-checks the same already-decided rule the proposer used (isAlreadyDecided), and duplicate pairs in one hand-edited proposal confirm once. The rule itself is corrected: the event-list check alone was NOT sufficient — a quarantined ledger assigns worst-state validity from divergent event ids that are deliberately excluded from the event list, so the validity/quarantine check the previous round removed as 'dead code' is restored with the divergent-path reasoning documented (review traced the exact gap; a regression fixture seeds the divergent pair). Notebook section splitting is fence-aware: a '## ...' line inside a fenced code block is content — previously fenced examples could smuggle stamps and citations into the classification. A mirror the stamp reported written that has since VANISHED is surfaced as divergence (silence would hide a deletion); only a ledger payload that recorded run_dir_unwritable legitimately has no mirror. The alignment payload now records its history scope (all_refs) as part of the heuristic's evidence; the confirm echo reports skipped-decided pairs. 5 new tests; 5/5 mutants killed; full suite green.
…, marker fidelity, prefix marking, carried-link rule, ref exclusion D5's fail-closed admission gains its missing leg: a malformed registry line contributes a SENTINEL to the baseline set — the unseen line may BE a current row, so every stamped section stays incomparable until repaired (previously B = ∅ classified everything unqualified-current over a corrupt registry). The three half-delivered stage-2 hooks land fully: the row's +snapshot marker is cross-checked against the stamp's snapshot_commit presence (either direction of mismatch is the D4 conflation → new issue code); the clean prefix chained into a cycle is marked defective (a clean-looking head over a cyclic lineage never renders trustworthy — locked by a pure-function test after the integration fixture's own artifact defect masked the path in the first mutation round); carried-forward descriptions obey the same at-most-one-link rule as provided ones. The alignment timeline excludes the machinery's own refs (refs/nullius/*, refs/stash) — a run can no longer be 'aligned' to another run's pinned snapshot or a stash WIP commit; the payload records its history scope; the ambiguous_candidates schema description is narrowed to what is actually measured. Every cites-runs comment in a section is checked (not just the first), and a git FAILURE during ancestry comparison is labeled comparison-failed, never diverged-history. 5 new tests; 6/6 mutants killed (one via the pure-function fixture); full suite green.
…ed content never matches, transactional mirrors everywhere isAlreadyDecided counts only FULL-scope events: a named-scope supersession annotates without deciding (schema contract), so it no longer shields a still-active run from a round-chain proposal. Fenced content is excluded from the MATCHING body entirely — the previous fix stopped fenced headings from splitting sections but left fenced stamp and citation comments matchable; a fenced example can no longer alter any classification. An unclosed fence extends to EOF per CommonMark. Mirror handling is transactional on BOTH write paths now: trace stamp gains the same append-failure cleanup backfill had, and both paths RESTORE a pre-existing mirror's prior content instead of deleting it (a legacy hand-made mirror survives a failed re-stamp byte-for-byte; only a mirror the invocation created from nothing is removed). 3 new tests; 3/3 mutants killed; full suite green.
…ate-prefix marking, pipeless-row visibility With multiple written-against stamps in one section the LAST one is authoritative (the stated convention is that sections END with their stamp) — a first-match rule let any earlier example outrank the real one. A later chain walker whose trail reaches an ALREADY-processed cyclic node marks its whole trail defective — first-walker-only marking let a second clean prefix render as a trustworthy head over a cyclic lineage. A non-empty, non-comment line inside the registry block that is not a table row (e.g. a hand-edited row missing its leading pipe, which GitHub still renders as content) is reported malformed — the written-row-unseen misdiagnosis closed for the last known entry channel. The marker-mismatch check gains its missing-direction fixture (stamp records a snapshot, row lacks the marker). 4 new tests; 3/3 mutants killed; full suite green.
… writes - checkChains: a trail reaching a DONE node that is already defective (not only cyclic) taints the whole trail — p -> e -> cycle can no longer leave p as an apparently clean current head (codex r3 blocking, native r3 finding 1; mutation-verified: dropping the defective check turns the new transitive fixture red) - mirror writes go through writeBytesAtomicDurable on both writers (stamp + backfill), closing the partial-truncation window on pre-existing legacy mirrors (codex r3 nb, native r3 finding 2) - append-failure mirror restoration is guarded: a failing restore no longer masks the original append error; the divergence scan surfaces the leftover on next read
…ublic docs - research-team: run naming updated to the date+ordinal shape (exact time moves to the origin stamp), stamp-at-creation contract, and a result-registration-at-convergence contract (registry row, ledger supersessions, memo section stamps, clean nullius current before fold) - research-harness: machine-readable current-version authority first in the adopted-version anchor; stamp in the launch contract preflight; new Result Traceability section with the four-step adoption path for existing projects (runtime-only refresh, heuristic backfill, reviewed chain proposals, hand-pasted registry block) - scaffold AGENTS.md: three gate-table rows (stamp at run creation, supersede/void on replacement, set-current/current for reading) - README + zh README: Current results registry sentence in the project-local-truth narrative; both agent-instruction blocks and QUICKSTART gain the four traceability command lines (three copies kept in sync) - scaffold project_index.md: single-line-comment convention documented inside the registry block
writeBytesAtomicDurable renames before its final durability step, so a failure can throw with the destination already replaced. Both mirror writers now best-effort restore the prior state inside the write catch itself — harmless when the rename never happened, and the only defense against clobbering a pre-existing legacy mirror with no ledger event behind it (codex stage-3 r4 blocking). Fault-injection regression for both writers: the mock lands the bytes and then throws, exactly the post-rename contract; mutation-verified (stripping the restore turns both tests red).
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
…ble primitives The atomic-write anti-drift lock (CI) caught four call sites the local acceptance run missed: the chain-proposal file, the ledger trailing- newline repair, and both .gitattributes maintenance writes. All four now use the @nullius/shared primitives (writeBytesAtomicDurable / appendBytesDurable); no behavior change beyond durability.
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 answers
One command, prose a human reads directly:
`nullius current` renders that answer (with `--json` for agents); `nullius status --json` carries the same read model as its traceability block. Where a clause is genuinely unanswerable the command says so explicitly with the repair instruction — honest unanswerability is compliant, silent precision is not.
Design core (from the converged design, 5 review rounds)
Review record
Design: 5 rounds, two seats (codex gpt-5.6 xhigh read-only + GLM/native), converged r5 dual SHIP. Implementation: Stage 1 (4 rounds, blocking 11→6→6→0, dual SHIP), Stage 2 (3 rounds, 8→6→0, dual SHIP), Stage 3 (5 rounds; final codex SHIP zero new findings after transitive defect propagation and commit-uncertain mirror restore fixes, both mutation-verified), Stage 4 wiring reviewed via the stage gates. Every new test mutation-verified (the fix reverted must turn it red); 806 orchestrator tests + 123 project-contracts tests + full `pnpm -r build` green.
Acceptance rehearsal (copies of two real projects)