Skip to content

Result traceability: runs bind to commits, validity ledger, results registry, nullius current - #49

Merged
fkguo merged 39 commits into
mainfrom
claude/nullius-result-provenance-aa0d89
Aug 7, 2026
Merged

Result traceability: runs bind to commits, validity ledger, results registry, nullius current#49
fkguo merged 39 commits into
mainfrom
claude/nullius-result-provenance-aa0d89

Conversation

@fkguo

@fkguo fkguo commented Aug 6, 2026

Copy link
Copy Markdown
Owner

What this answers

One command, prose a human reads directly:

What is the current best result; which exact code revision produced it; where is the current manuscript; which notebook sections belong to the current version and which have not been rewritten since an earlier one; which runs are still valid, which are superseded or void.

`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)

  • The version object IS the git commit — no new version object. Object axis = slug, version axis = baseline commit; dirty trees get an exact identity via `git stash create` snapshots pinned at `refs/nullius/runs/` (tree-hash equality = code identity).
  • Binding-quality ladder (`run_origin_v1` schema): exact_clean / exact_tracked_snapshot / head_plus_untracked / aligned_heuristic / unbound — "exact" only for the first two; heuristic alignment is never presented as exact.
  • Validity ledger `artifacts/runs/validity_ledger.jsonl`: append-only supersede/void/reinstate/stamp events (lock + ULID idempotency, merge=union with read-side canonical dedup; divergent same-id payloads fail closed worst-state). New runs declare what they supersede; superseded_by is derived at read time; old run dirs are never edited.
  • Results registry `project_index.md#Current results`: one row per headline result naming its current run and effective code identity, machine-guarded (round-trip-safe writer CLI, SHA-256, chain-consistency both directions, forward-cycle + component-head checks) but never machine-chosen — selection is research judgment at milestone convergence.
  • Notebook staleness (`` per section): six-step worst-wins classification, fence-aware, fail-closed against malformed registry rows; head_plus_untracked rows participate in ancestry but can only yield the qualified class current-modulo-untracked.
  • Adoption for existing projects: `trace backfill` (timestamp-aligned, honestly heuristic, validity never backfilled) + `trace propose-chains` → human review → `confirm-chains`.
  • Run naming short form `YYYYMMDD-mN-rNNN-slug-rK` — exact time moves into the origin stamp; existing parsers unaffected.

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)

  • 456+5 / 358+1 runs aligned+unbound by backfill, exactly reproducing the read-only audit; 38 / 95 round-chain supersessions proposed and confirmed.
  • `result set-current` correctly refuses a heuristically-aligned legacy run (fail-closed); a fresh short-form run stamped at creation registers with its snapshot identity.
  • `nullius current` answers all five acceptance clauses on real data on one project, and renders the honest-unanswerable clauses (empty registry, unstamped notebook) with repair instructions on the other; the round-cap observation surfaces the measured 11-round livelock object.
  • Fresh-external-root smoke: init → HARNESS + launcher → status --json with traceability block → scaffolded registry block present.

FK Guo 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.
FK Guo 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).
@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

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.
@fkguo
fkguo merged commit 1a48e79 into main Aug 7, 2026
10 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant