Repository navigation
scm: the checkout verb at repository grain, and what it deliberately does not consult - #9322
gunbai-bot[bot] wants to merge 46 commits into
Conversation
…s absence in
The model did not relate one commit to another. Every `parent` in gunbc.scm is
TREE-NODE parentage inside the object store; none of it is ancestry. A
RepositoryCommit is { root, message } and `commits` is a flat List, so a
repository's history was a SEQUENCE and not a graph -- merge-base, ahead/behind,
commits-since, log ranges and merge itself had no referent here.
IT IS CARRIED, NOT RECOVERED FROM POSITION, and that is forced rather than
chosen. Position gives a total order and says nothing about descent: reading
index 1 as the child of index 0 assumes history is linear AND append-ordered,
neither of which anything establishes -- there is no commit-authoring verb, so
nothing defines what appending means. Recovering ancestry from position would not
be recovering it, it would be inventing it, and the invention would be invisible
because it agrees with position by construction.
THE ROOT IS A STRUCTURAL STATE, not a parent field holding a "nothing" value,
which would make "this commit begins the history" and "this commit's parent is
missing" the same value -- the ordinary first commit and damage. Same refusal
`log` made for the empty history, one layer deeper.
EXACTLY ONE PARENT, DERIVED FROM THIS MODEL'S OWN MERGE RATHER THAN FROM GIT.
`merge` takes ONE target Commit and a LIST OF PROPOSALS and answers ONE commit. A
Proposal is a set of authored requirements: not a commit, no root, no message, no
identity in history. So a merge here combines one commit with N things that were
never commits, and has exactly one commit antecedent. Git's second parent exists
because git merges two BRANCHES -- two commits -- and nothing in this model can
merge two commits at all. A parent LIST would therefore not be more general, it
would be an arm with no producer, which DESIGN 4b rules worse than absent because
it is cited as capability the model lacks. WIDENING TRIGGER: if merge ever takes
two Commits, this coproduct gains a several-parents arm and the change is a
compile error at every consumer rather than a silent reinterpretation.
AUTHOR AND TIMESTAMP ARE EXPLICITLY SEPARATED, not left to ride along. Parent is
STRUCTURAL -- it relates two things the model holds and is decidable inside it.
Author and timestamp are EXTERNAL OBSERVATIONS, and this repository has no
modelled clock and no modelled process identity to observe them with; acquiring
either is the same frozen-host question gating the store and process output.
Modelling them here would mean inventing two facts to fill fields, at exactly the
boundary a commit is supposed to be trustworthy.
A cycle REFUSES rather than truncating or looping, detected by exhausting the
population: a chain longer than the history must have revisited a commit, by
pigeonhole, so the refusal is derived from the bound rather than asserted by a
visited-set this module would have to keep correct.
Receipts, by execution: seven witnesses PASS. THE SHARPEST, and it establishes
the carried-not-position claim by execution rather than argument: replacing the
walk with a FAITHFUL position-implies-ancestry implementation leaves the
linear-history positive control GREEN -- because in a linear history position and
ancestry coincide -- and is caught ONLY by the adjacent-roots witness. A second
mutation reporting a broken parent as a completed trace reds only the broken-chain
witness, leaving the linear trace green. Parse sweep clean.
NOT DONE HERE, and named rather than left implicit: binding ancestry into
RepositoryCommit is a WIRE-FORMAT change -- that type is codec'd with a member-set
guard, and the envelope's header states the v1 body must stay frozen once
documents exist. That binding is the versioned-codec step, and it is a different
change from settling the model.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…letion review 56072, and it is correct. `ancestry_walk_step` built the chain with concat-at-end, copying the whole accumulator per step -- O(n^2) over history length. DESIGN section 6 rules this class ALWAYS fixed regardless of the realized n, and preempts the "n is small here" defense by name: n is not a time-stable fact, and this particular n is the length of a repository's history, which is exactly the quantity that grows without bound in use. The fix is prepend plus one reverse at the single completion point, so the exported chain order -- descendant first -- is unchanged and no caller can observe it. The reversal is at completion rather than at each consumer because there is one completion and N consumers. WORTH RECORDING WHERE THIS CAME FROM. gunbc.scm.repository_envelope carries an annotation about having committed this exact defect and had it caught by review, whose stated lesson was that fixing a cost shape at the sites a review names is not the same as not having the defect. This module imports that module and reintroduced the shape anyway. The additional lesson, now in the source: reading the note is not the same either. The second half of the review is noted and not changed. commit_is_ancestor_of folds over the traced chain to find an element, which is O(n) over a bounded chain, not a quadratic shape -- and with the walk now linear the end-to-end cost is O(n) rather than the O(n^2) the review flagged for the pair. Receipts: all seven witnesses green after the change, run against a freshly built compiler rather than the stale local one. The order-sensitive witnesses -- a_linear_history_traces_from_the_start_back_to_the_root and a_stated_ancestor_two_generations_back_is_found -- are what would go red on a missing or doubled reverse. Parse sweep clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…history
review 56102, and it is correct on both the conflation and its consequence. The
outer AncestryUnknownCommit arm answered
AncestryBrokenAt { commit: cursor, missing_parent: cursor }
naming one commit as BOTH the site of damage and the parent that is missing. Two
things were wrong with that, and the second is worse than the first.
THE CONFLATION. "The commit you asked me to walk from is not in this history" and
"commit c's parent p is missing" have OPPOSITE OWNERS -- a bad argument versus a
truncated history -- and opposite remedies. It is the one-reason-symbol-over-two-
opposite-states pattern this module's own header prose names, committed in the
module that names it.
THE FABRICATION. An unknown start supplies no parent, so there was no value to
put in `missing_parent` and the walk INVENTED one. A consumer acting on the
result would go looking for an object nothing ever claimed exists. That is the
fabricated-plausible-output failure at exactly the boundary this module is meant
to be trustworthy over, and it is why this could not be left as a naming quibble.
The repair is a fourth arm, AncestryStartNotInHistory { start }, carrying only
what the input supplied. Its reachability is by CONSTRUCTION rather than
convention, and that is now stated in the source: the step advances the cursor
only after lookup(parent) has already answered AncestryKnown, so every cursor
after the first is a commit this history holds, and the unverified cursor is the
starting one and no other. The reviewer derived this independently, which is the
part of the finding I would not have caught from the type alone.
Receipts: eight witnesses green, run against a freshly built compiler. The new
one asserts the START IDENTITY rather than merely that some non-traced arm came
back -- an implementation answering this arm for every input would satisfy a bare
not-traced claim, and the real-trace and real-truncation witnesses beside it are
what stop that reading. Parse sweep clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s still free THE WINDOW IS THE REASON THIS COMES BEFORE THE STORE BINDING, and it is in the carrier rather than in anyone's preference. repository_envelope states that the v1 body is frozen "once a v1 document exists anywhere" -- a CONDITION, not a date. Measured: encode_repository is referenced in exactly two places, its own module and one witness. Nothing writes a repository to disk anywhere in the tree. So no v1 document exists, the format is not frozen, and THE STORE BINDING IS PRECISELY THE EVENT THAT WILL FREEZE IT. Queuing the codec behind the store would mean waiting for the event that closes the window and then trying to change the format through it. Done in this order, the first document ever written already carries ancestry and needs no migration. That is the same argument as ancestry-before-commit, one layer out: there, no producer of a commit exists, so the shape can still be settled; here, no persisted document exists, so the format can still change. Both windows are open only because nothing has run yet, and both close on the same event. A WITNESS-BUILT DOCUMENT DOES NOT SATISfy THE FREEZE CONDITION, and the clause decides this rather than being read charitably: the same paragraph glosses itself as "once a document exists IN THE WORLD". A witness document is rebuilt from the current encoder every run, so it cannot ever disagree with that encoder -- it is not an artifact anyone must stay compatible with. Freezing over it would be freezing over something that can never produce the incompatibility the freeze exists to prevent. This is therefore an in-place v1 widening and NOT a second format arm; a v2 arm here would be the fabricated migration the module's own header refuses to build. A PARENT IS A COMMIT REFERENCE, NOT A NODE REFERENCE, reusing this module's own prior ruling rather than deciding afresh. The checked_out annotation records why it resolves against the COMMIT LIST: a node reference admits a document naming an arbitrary interior object -- a child three levels down -- as the thing pointed at, with nothing refusing because nothing asks. A parent has exactly that property. `root` may be a node reference because a commit's root IS a node; a parent may not, because a parent is a COMMIT. THE POSITION IS A STORAGE LOCATOR, NEVER AN ORDERING FACT. The reference names WHICH commit; adjacency between positions still means nothing, and a commit carrying no ancestry member refuses rather than defaulting to its neighbour. So this is not the position-implies-ancestry defect the ancestry module exists to prevent -- deriving a parent FROM a position would be, and nothing here does. DECODE IS TWO PHASES, and that is forced rather than tidy. A parent reference is a position in the commits array, and nothing requires a parent to appear earlier than its child; requiring it would impose a storage-order rule on a relation that has none. Phase one reads what the document says, phase two resolves once every position exists. PendingCommit is that intermediate state, and its Optional carries ONE meaning -- an unreadable member refuses in phase one and never reaches it. A MISSING `ancestry` MEMBER REFUSES; IT DOES NOT DEFAULT TO A ROOT. Defaulting would make every commit in an older document read as a beginning of history: a well-formed repository that is not the one that was written, which is the silent reinterpretation the identity-rule tag exists to prevent, arriving through the schema axis. A DANGLING parent refuses for the same reason rather than degrading to a root -- that would undo, at the only boundary where it matters, the structural-root distinction ancestry just established. THE CHECKED ENCODER ADJUDICATES A BROKEN PARENT BEFORE THE WRITE, joining the three refusals already there. encode_repository cannot refuse, so its only honest output is the empty reference decode later rejects -- which surfaces to whoever LOADS the document rather than to whoever caused it. RECEIPTS, BY EXECUTION, against a freshly built compiler: - 36 of 36 witnesses green (envelope + ancestry), parse sweep clean at 4024 files. - MUT dangling-parent-reads-as-root: ONLY a_dangling_parent_reference_refuses reds; the four others stay green. - MUT codec-silently-drops-ancestry: ONLY a_stated_parent_survives_the_round_trip reds -- and the_unmodified_wire_still_loads STAYS GREEN, which is exactly why that witness had to be written: a codec that drops ancestry still round-trips a well-formed repository, so nothing already in the file could have caught it. - Restored: all green. The three new exhaustiveness failures the arms caused in total consumers were repaired by naming the case, not by widening a wildcard -- the closed refusal domains doing the job they were split for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r refused The floor refused this branch at strict preparation with eight located diagnostics -- `missing required field 'ancestry' in literal of type 'RepositoryCommit'` across scm_log_witness_test.dag and scm_status_witness_test.dag. status (#9257) and log (#9277) merged to main after this branch was cut, and their fixtures construct RepositoryCommit; merging main in succeeded TEXTUALLY while the type had gained a required member. THIS IS THE WALL FIRING ON ITS OWN AUTHOR, and it is the strongest argument the codec change could have made for itself. The required member REFUSED the old literals rather than defaulting them to RootCommit. Had it defaulted -- the convenient choice, and the one this stack argued against in prose one commit earlier -- six commits in the log fixtures would have silently become roots and every witness would have stayed green over a history that was not the one written. The two commits in the log ordering fixture are stated as a real chain, since that fixture is a history and stating one is the honest reading of it. The rest are roots: those witnesses are about positional references, held-ness and staging, and inventing descent to decorate them would be asserting a relation the claims do not test. RECORDED BECAUSE IT COST A CI CYCLE AND WILL AGAIN: the parse sweep reported 4064 files clean on this exact tree. It only PARSES, so it is structurally blind to name and type resolution -- a merge that is textually clean can be semantically broken and the sweep cannot see it. This is the second time in this lane. A main merge needs a resolve check, not a sweep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
review 56207, both findings, and the second is a hole THIS PR OPENED rather than one it inherited. THE CYCLE ADMISSION. The checked encoder verified that every parent occurs in `commits` and then succeeded. A two-commit cycle satisfies that completely -- membership and acyclicity are different properties and the first does not imply the second -- so the persistence boundary would have serialized a state gunbc.scm.ancestry explicitly classifies as a REFUSAL. Before the ancestry member existed a cycle had no spelling on the wire at all; binding it is what created the ability to persist one, which is why this belongs in this diff and not a follow-up. REFUSED AT BOTH ENDS, NOT JUST THE ONE THE REVIEW NAMED. The review named the encoder. The encoder's argument is a value some caller in this process built; a DOCUMENT is bytes from anywhere, and a hand-authored one can state a cycle whether or not any encoder here would produce it. Guarding only the write path would be a wall that fires solely on its own author -- and the untrusted direction is the one that matters. The decode check runs AFTER parents resolve and BEFORE the checkout, because a cycle is only decidable once every reference denotes a commit. THE DETECTION IS BORROWED, NOT REBUILT. ancestry_walk already decides this, by exhausting the population rather than by keeping a visited set. Re-deriving the test inside the codec would have been a second answer to one question, and the two would drift the first time the walk's bound changed. COST, DECLARED RATHER THAN HIDDEN: one walk per commit is O(n^2) over history. A linear alternative needs a keyed visited set, which this layer's positional list does not give, and the walk is the only acyclicity authority. It sits on a boundary crossed once per write rather than inside a fold; the shape is stated at the site with its dissolution trigger (a keyed commit list) instead of being accepted silently. THE PREDICATE FINDING, and I am resolving it on the fact that decides it rather than on the doctrine cited. `ancestry_is_root` had NO PRODUCTION CONSUMER -- its only caller was the witness asserting it works. A helper whose sole consumer is its own test is an artifact with no final consumer, so it moves into the witness and out of the module's exported surface. I am not adopting the "predicate dissolution" framing: DESIGN states no such rule, and gunbc.scm.load_standing carries two functions of exactly that shape on main, which would be violations if it did. The right reason is consumer count, and by that reason the reviewer's conclusion is correct. Receipts, by execution against a freshly built compiler: both new refusals green, and the POSITIVE CONTROL is the load-bearing one -- scm_env_the_cyclic_fixture_has_every_parent_in_the_commit_list establishes that the fixture satisfies the membership check the encoder already had, so the two refusals are about the CYCLE and not some unrelated defect. The decode witness builds its document with the UNCHECKED encoder, so the bytes are exactly what a writer without this wall would have produced. 42 further witnesses across log, status, envelope and ancestry green with no failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…does not consult Checkout already existed at OBJECT grain -- reconstructing a program from a commit's root closure. What was missing is SELECTION: which commit a repository is positioned at. This adds that half. TWO GRAINS OF ONE VERB, IN THE MODULE THAT OWNS IT. Putting the repository-grain half in its own file would have minted a second name for one verb -- the module would have had to be called something that is not "checkout" while being exactly checkout. Import direction was checked rather than assumed: repository_envelope reaches object_store, ancestry, target_reference, json_member and the closure carriers, none of which reach checkout, so the new edge closes no cycle. CHECKOUT SAYS NOTHING ABOUT ANCESTRY, AND THAT IS DERIVED RATHER THAN OMITTED. The silence is stated in the source because a later reader finding no mention of ancestry in a verb that selects a commit would reasonably read it as an oversight and "fix" it. A commit's program is its ROOT CLOSURE. Ancestry is a chain over COMMITS. Two different graphs over two different populations, and reconstruction walks only the first, so no ancestor needs to exist, or be readable, or be consistent, for a checkout to be exactly right. THE CONSEQUENCE IS NOT COSMETIC AND IT HAS A WITNESS PAIR: a commit whose stated parent is missing CHECKS OUT SUCCESSFULLY, while an ancestry walk from that same commit answers AncestryBrokenAt. One repository, one commit, two questions, two different answers, both correct -- the history is truncated and the program is not. The second witness is what makes the first mean something: without it the successful checkout could be a fixture that accidentally has no ancestry rather than checkout correctly ignoring it. Making checkout refuse there would invent a dependency the model does not have, and would mean a damaged history silently costs a user access to programs that are wholly intact. THE REFUSAL ARMS CARRY NO REPOSITORY -- the root-commit discipline one layer out. Had a failed checkout answered an envelope with nothing selected, "this checkout did not happen" and "this repository has nothing checked out" would share a representation, the second being an ordinary state the envelope's own header calls not an error. THE TWO REFUSALS ARE SEPARATE BECAUSE THEIR OWNERS ARE OPPOSITE. An unresolved reference is the CALLER'S fact; an unreconstructable program is the STORE'S. Collapsing them would hand a user "no such commit" for a commit sitting in front of them in the log. IT WRITES NOTHING. A new envelope with the selection moved, argument untouched, so the verb stays a pure transform. Persisting is a separate operation at a boundary this module deliberately does not reach -- the held frozen-host ruling. NO Bool PREDICATE OVER THE COPRODUCT IS EXPORTED. A `checkout_succeeded` convenience was written and REMOVED before this landed: its only consumer would have been the witness asserting it works. That is the same objection review 56207 raised against a sibling helper in gunbc.scm.ancestry, applied here before review had to; the reason is consumer count, and consumers now match the arms so a fourth arm fails to compile at every site rather than being absorbed into a false. Receipts: six witnesses green against a freshly built compiler; parse sweep clean at 4064 files. The mutation is the plausible later "fix" -- make checkout consult ancestry -- and it reds ONLY scm_ck_a_commit_with_a_missing_parent_still_checks_out while the other five stay green, including the companion witness proving that fixture's history really is broken. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ore the walk review 56225, and it is correct. `ancestry_walk` over an EMPTY history folds zero times, so `done` stays false and the fallthrough answered AncestryRevisitsACommit -- a CYCLE, concluded from having looked at nothing at all. WHY THE FALLTHROUGH WAS WRONG RATHER THAN MERELY UNLUCKY. It encodes a pigeonhole argument: a chain longer than the population must have revisited a commit. That argument needs at least one step to have been taken. With zero steps it is vacuous -- and an empty population cannot contain a cycle at all, so the answer was not just unproven, it was impossible. That is the fabricated cause this module's own header refuses, committed in its own walker. THE REPAIR IS NOT A SPECIAL CASE FOR EMPTY, and that distinction is the whole content of the fix. The start's membership in `history` is decidable up front, and answering AncestryStartNotInHistory when it is absent is exactly what the first step would have concluded anyway, since lookup would have said so. The empty history then falls out as an INSTANCE -- nothing is a member of nothing -- rather than as a branch a future editor has to remember exists. It also makes the arm's meaning honest. The refusal previously fired from `lookup` returning unknown, which is a statement about the LOOKUP'S DOMAIN; it now also fires from membership in `history`, which is the population the arm is named for. Those two were assumed to agree and nothing said so. Receipts: an_empty_history_is_not_a_cycle asserts the START IDENTITY on the start-not-in-history arm, not merely that the answer is not a cycle -- an implementation answering that arm for every input would satisfy the weaker claim. Its POSITIVE CONTROL, a_start_inside_the_history_still_walks, is the load-bearing half: the empty-history witness supplies a lookup that answers KNOWN for every identity, so a gate that merely trusted `lookup` would have walked happily and passed. The control establishes that the gate did not break the ordinary case. All ancestry witnesses green, plus the codec's cycle refusals which route through this same walker. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s lossy review 56230, and it found a live defect that predates this branch: the checkout verb exposed it rather than created it. WHAT WAS WRONG. RepositoryEnvelope stored the selection as the commit's ROOT. But `commits` may legitimately hold two rows with the same root and different messages -- gunbc.scm.log's own witness two_commits_sharing_a_root_still_get_distinct_references exists because it can -- so a root is NOT a unique key into that list. Every reader resolved it to the FIRST matching row. MEASURED BEFORE THE REPAIR, by execution: check out the second of two shared-root commits, ask `status` which commit you are on, and it reports the FIRST one's message. Nothing refuses. The user is told they are on a commit they did not select -- a wrong answer delivered as an ordinary success, which is the failure DESIGN section 5 forbids at a user-facing boundary. THE WIRE WAS ALREADY RIGHT AND THE CONVERSION WAS THE DEFECT. This module's own annotation says the encoded reference "means the commit at this position in this document and nothing outside it". Decode threw that away by resolving the position to a root, and encode recovered a position by searching for the first row carrying it -- so a repository selected at position 1 with a shared root ROUND-TRIPPED TO POSITION 0. That is data loss on the persistence path, and it was reachable before ancestry or checkout existed. Carrying the reference end to end DELETES both conversions rather than adding a check -- construction over validation -- and the wire FORMAT is unchanged, so this needs no second format arm. Only which position is emitted becomes faithful. IT DOES NOT REDEFINE COMMIT IDENTITY, and that distinction is load-bearing because the envelope's header explicitly says identity IS the root. A SELECTION is a different question from an IDENTITY: it names a row in this document's list. Both statements survive unchanged. CheckedOutCommitMissing now names the REFERENCE rather than a root, because when a selection does not resolve there is no commit and therefore no root to name. Reporting one would have meant inventing it or echoing a value that denotes nothing here. `commit_with_root` is deleted -- it had no remaining consumer once resolution went by reference, and its only behaviour was the first-match rule that caused this. Receipts: 48 witnesses green across envelope, log, status, checkout and ancestry; parse sweep clean. The two new claims are a PAIR and the pairing is the content -- checking out position 1 must report SECOND, and position 0 must report FIRST. The second is not decoration: without it the first would be satisfied by a reader that always returns the LAST row instead of always the first. Both assert through `status` rather than through the envelope field, because the field being right is not the claim; the claim is that the user is told which commit they are on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
review 56230 — fixed in Confirmed by execution before fixingCheck out the second of two shared-root commits, ask Your reasoning was exactly right: The wire was already right; the conversion was the defectThis is the part that made the fix clean rather than a redefinition. The envelope's own annotation says the encoded reference "means the commit at this position in this document and nothing outside it." Decode threw that away by resolving the position to a root, and encode recovered a position by searching for the first row carrying it — so a repository selected at position 1 with a shared root round-tripped to position 0. That is data loss on the persistence path, reachable before ancestry or checkout existed. Carrying the reference end to end deletes both conversions rather than adding a check, and the wire format is unchanged — only which position is emitted becomes faithful. No second format arm needed. It does not redefine commit identityWorth being explicit, since the envelope header states that identity is the root. A selection is a different question from an identity: it names a row in this document's list. Both statements survive unchanged — which is why I did not touch that header. Two consequences fell out: Receipts48 witnesses green across envelope, log, status, checkout and ancestry; parse sweep clean. The two new claims are a pair, and the pairing is the content: position 1 must report — sent from gentle-eagle-360 |
The annotation sat inside the match body of scm_env_a_valid_commit_reference_checks_out. §4c models module-item grain only, so both required lanes refused it — the floor at strict preparation (:420-423, line grain) and the build lane's v2-emission phase at emit (four byte-ranges, which resolve to the four `//` lines of ONE comment block, not four sites). One defect, two lanes, reported in two grains. The annotation is preserved verbatim above the `test fn` declaration; only its position changed. The in-body remainder is now the bare assertion it always was. Verified by execution on a remote dispatch: all 31 witnesses in this file return `true`, including the repaired site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A commit root identifies a PROGRAM. It does not identify an occurrence of that
program in a repository's history, and this model already admits two commit
records sharing one root and differing only in message -- which is why `log`
derives references from list position rather than root lookup.
So `DescendsFrom { parent: ObjectId }` named something that does not uniquely
identify what it points at. The decisive history is an ordinary revert:
record 0: root A, RootCommit
record 1: root B, parent -> record 0
record 2: root A, parent -> record 1
Content addressing correctly gives records 0 and 2 the same root. Root-keyed
ancestry then requires A to be simultaneously `RootCommit` and `DescendsFrom{B}`
-- a contradiction at the level of the type, one key with two irreconcilable
values. Against the first-match lookup this stack was about to grow, a walk from
record 2 answers with record 0's `RootCommit` and terminates immediately: record
1 and the revert edge vanish. Not a refusal, a confident wrong answer.
THE RECUT. `RepositoryCommitRef` is declared here, and the whole walk API --
`DescendsFrom`, `AncestryStep`, all four `AncestryWalk` arms, the accumulator --
is keyed on it. It is declared HERE because this module's subject IS the relation
between history records, so it owns the identity of the thing it relates; the
envelope imports ancestry, so declaring it downstream and reaching back would be
a cycle. ancestry.dag consequently no longer imports object_store at all.
The type's annotation carries the boundary rule rather than a list of sites: an
ObjectId is the right key for content-addressed OBJECTS, where uniqueness is the
point of the identity; a commit record is a program plus a message plus a
position, and only the last is unique.
REPRESENTATION IS DEFERRED, and the deferral names both invariants a positional
spelling would owe -- append-only for STABILITY, and order-preserving as a CODEC
obligation. An earlier-only rule buys acyclicity and is NOT a substitute: against
a list whose indices move it holds throughout while every stored reference
silently repoints.
`commit_is_ancestor_of` is DELETED rather than typed. It answered `false` for
four different facts -- a completed trace proving non-ancestry, a broken chain,
an unknown start, and a cycle -- of which only the first is a negative answer.
There is no production consumer and `AncestryWalk` already carries all four
states distinctly, so a typed judgment today would mint an arm with no producer.
The witnesses derive their own Bool through a local helper whose annotation
carries the rule that matters: any NEGATED use needs a positive control over the
SAME fixture, because a negation is satisfied by a walk that failed for any
reason, and a control over a different fixture catches a total failure while
missing a selective one. The two-root fixture is hoisted so its negation and its
new positive companion share it by construction rather than by copying.
Verified by execution, all 11 witnesses green, including the two that a
root-keyed model cannot satisfy: the revert traces all three records, and the
reverted-to record is an ancestor of the revert while not being the same record.
Per DESIGN 4b(4) those stay enrolled after greening rather than retiring.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge of session/scm-ancestry plus the codec re-key. Verified locally as far as the round-trip witness; full suites still running when this was committed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The block explaining why there is no Boolean ancestor predicate sat at end-of-file with no declaration after it. §4c attaches an annotation to a FOLLOWING module item, so fourteen diagnostics fired -- one per line -- and both required lanes refused before any witness could run. It is an annotation about an ABSENCE, which §4c has no attachment point for: the thing it describes is a function that is not there. It now sits above `ancestry_walk`, which is the surface the block itself points at -- "the walk itself is the surface", "a real consumer derives its answer from ancestry_walk" -- so the deletion rationale reads as part of why that surface has its shape. Content unchanged; only its position. THIS IS THE SECOND §4c AXIS AND THE FIRST ONE'S CHECK DOES NOT COVER IT. The in-body-annotation defect earlier in this stack was found by a brace-depth scan, which by construction cannot see a block whose problem is that NOTHING follows it. Both axes are checked by `v1_src_dag_parse`, which was built and run against this fix -- 4024 files parse-clean -- rather than relied on from memory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…disagree `ancestry_walk` took a `history` list AND a `lookup` function, and that pairing was the defect. `history` was consumed only as the fold's sequence, its element bound to `_unused`, so it supplied the STEP BUDGET and nothing else while every membership question was answered by `lookup`. Two authorities for one fact (DESIGN §3), free to disagree, and they did: an EMPTY history folded zero times, so the accumulator's INITIAL value survived as the verdict and the walk announced a revisit having observed nothing at all -- a default doing duty as a conclusion; a start ABSENT from history but known to lookup traced normally, so the arm named AncestryStartNotInHistory was decided by a lookup fact rather than the history fact it names; a parent ABSENT from history but known to lookup was traversed rather than reaching BrokenAt. THIS IS PRE-EXISTING, NOT INTRODUCED BY THE IDENTITY RECUT. The pre-recut head d36e7f2 carries the identical fold, the identical `_unused` binding and the identical initial-accumulator-as-verdict; only the type names changed. Recorded because the record matters. THE CARRIER, NOT A CHECK. A membership test before the fold would have closed the empty-history case and left the parent mismatch constructible -- it concedes the bad state is writable and guards one instance of it. RepositoryHistory carries the rows, and membership, relation and step bound all derive from them, so the disagreement is unrepresentable rather than validated (§5, construction before validation). Three things follow rather than being added: CommitAncestryLookup is DELETED. "Is there a row, and what does it say" is exactly `AncestryStep?`; keeping both was two spellings of one question. AN UNREACHABLE ARM DISAPPEARS instead of being handled. The accumulator now carries the RESOLVED row rather than a bare cursor, so the step never re-looks-up the cursor and has no not-found case to answer for. The old shape filled that impossible case with AncestryStartNotInHistory -- wrong mid-walk, where the cursor is not the start. THE PIGEONHOLE ARM BECOMES HONEST. Reaching it now requires at least one fold iteration, which requires a non-empty carrier, so it is a derivation rather than a default that survived. The empty-history specimen is enrolled as a witness and stays enrolled after greening (§4b(4)), with a positive control over history_step_at so that a lookup answering Absent for everything cannot satisfy it. Verified by execution: 13 of 13 witnesses green, under a guard that aborts if the branch changes mid-run. Main merged first so the carrier lands once on a current base. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-roots the codec onto the recut ancestry model and removes the first-match lookup at its source rather than repairing it. repository_ancestry_lookup is GONE. It folded the commit list for the first row whose ROOT matched, so on a history containing a revert it answered with the wrong record's ancestry -- confidently, since a first match is a successful lookup. In its place repository_history projects the commit list onto the walk's own carrier: a row's REFERENCE and its ANCESTRY are derived in one pass from one list, so a position can never name a row whose ancestry came from elsewhere. encode_ancestry no longer searches at all. The parent IS a reference, so it emits parent.reference directly, and the commits parameter that made the search possible is gone with it -- there is nothing left to be first among. commit_references, index_references and IndexRefAcc are ABSORBED. The carrier already holds each row's reference beside its ancestry, so a second list of references was a second representation of the same fact. pending_contains_reference replaces pending_root_at_reference. The old form returned the referenced row's ROOT and its only caller discarded it, needing existence rather than identity -- and returning a program from a row-existence check is exactly what made it natural to then STORE that program as the parent. A Bool cannot be misused that way. The dangling-parent and cycle refusals now name a ROW rather than a program, and the round-trip witness asserts the reference the document stated rather than comparing resolved roots -- the comparison that could not distinguish two commits sharing one root. REFERENCE UNIQUENESS IS DISCHARGED BY THE BUILDER, NOT THE CARRIER, and that is recorded beside the builder with its next-rung trigger rather than guarded by a check. history_step_at resolves by first match, so a carrier holding two rows at one reference would answer with the earlier; every reference is minted from this fold's index counter so it has no producer, but it stays authorable in a fixture. A uniqueness check would concede the duplicate is writable and guard one instance of it; the trigger is an unforgeable-construction wall making this the sole constructor. Verified by execution: 50 witnesses across the envelope and ancestry suites, all green, under a guard that aborts if the branch changes mid-run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e true The module's annotation stated that the same consumer-count reasoning which removed the Boolean ancestor predicate had also deleted ancestry_is_root. It had not: the function was still defined a hundred and sixty lines above the sentence claiming its removal, with the witness as its only consumer. A PR does not get to ship a claim its own diff falsifies. THE DELETION IS INDEPENDENTLY JUSTIFIED and that is what makes it the right repair rather than a rationalisation. Zero production consumers, corpus-wide, on exactly the criterion already applied to commit_is_ancestor_of in this same file -- a criterion that existed before the annotation did. Changing code to match prose is only correct when the code change stands on its own; otherwise it is deleting a working function because a comment claimed it was gone. The witness derives the root state locally, KEEPING THE POSITIVE AND NEGATIVE IN ONE ASSERTION. A local reader asserting only the negative half would reintroduce the vacuous-green hole this file closed earlier: a negation is satisfied by any failure, so it needs a positive over the same fixture and mechanism. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # dag/test/claim/scm_ancestry_witness_test.dag
…this diff falsifies Two annotation defects, both of them false statements in prose this PR authors. ancestry.dag claimed the RepositoryHistory carrier made the list-versus-lookup disagreement UNREPRESENTABLE. It does not. Two steps may carry the same `at` and history_step_at resolves by first match, so one carrier can still disagree with itself under a duplicated key -- the same first-match class this change removed, one level down. The narrow truth is that the three enumerated disagreements are removed outright, which is what the shape actually buys. The fact now lives in exactly one place. It was stated in two -- here and beside the codec's builder -- and two annotations that agree are still two authorities for one fact. The carrier is the right home because the carrier is what the fact is about; the codec references it rather than restating it. The residual state, its rung (mechanically prevented at each builder, not structurally impossible), why a uniqueness CHECK would be the wrong repair, and the next-rung trigger are all recorded there. UNREPRESENTABLE survives once, as the disavowal. log.dag stated that there is no ancestry relation at all and that nothing else in gunbc.scm names one. This PR makes the second half false. The bullet is replaced rather than struck, because it carried two claims with different fates. RepositoryCommit still has no parent field -- that arrives with the codec -- so a log row still cannot name one and only the reason changes. And merge-base, ahead/behind and commits-since still have no referent: grepping gunbc/scm for them returns prose only, no declaration. Striking the bullet whole would have deleted a true statement in order to repair a false one. status.dag makes a structurally similar claim and is deliberately NOT touched. Its stated reason is NO BRANCH and NO REMOTE, and ahead/behind relates two named endpoints: ancestry is necessary for it and not sufficient. Neither this PR nor the codec PR supplies a second endpoint, so that sentence and its reason stand. Annotation-only: every changed line is a // annotation, so by DESIGN section 4c the semantic projection is untouched. Parse sweep 4083 files clean.
…e reason it is actually load-bearing repository_history appended with `concat(acc.steps, [step])`, which copies the whole accumulator per row and makes building an n-commit history O(n^2). DESIGN section 6 fixes that shape ALWAYS, regardless of the realized n, because "n is small here" is not a time-stable fact. This module had the receipt in front of it: gunbc.scm.merge carries an annotation recording that it committed this exact defect, had it caught, and repaired it this exact way. It was reintroduced here anyway, which is what that rule exists for -- the shape travels. It now prepends, which is the whole of the cost repair. THE TRAILING `reverse` IS KEPT, and the first version of this annotation gave the wrong reason for it -- claiming it restored "the commit order the codec must preserve". No such obligation exists. RepositoryHistory is never encoded; the wire carries commits plus per-commit ancestry, and the positional reference is into `commits`, never into `steps`. Going to build the discriminating red for that sentence is what showed it had none. Step order is not unobservable, which is the correction that decides the line: it is OBSERVABLE BUT UNSPECIFIED. a_cyclic_commit folds these steps and reports the FIRST whose walk finds a cycle, and that commit reaches a user as the actionable example in a cycle refusal. Reversing the sequence names a different commit -- a changed user-visible output that no witness fails on, because the interface deliberately declines to promise WHICH commit. So dropping the reverse would make one commit repair a cost shape AND silently change a refusal's content, with nothing in the corpus to catch the second. Bare-minimum-cost does not object: it targets a cost SHAPE defect, the shape is what this fixes, and one more linear pass over an already-linear construction is a constant factor rather than a shape. No witness is added for it, deliberately. Asserting which commit a cycle is reported from would be a change detector over an explicitly unspecified choice, and would freeze into the interface precisely what that module refuses to promise. The alignment of steps[i] with commits[i] is recorded as a TRAP for the next reader -- what deleting the line would change -- not as an invariant anyone may rely on. The uniqueness fact is no longer restated here. It lives once, on RepositoryHistory in gunbc.scm.ancestry, and this module references it. Receipts: 11 order-, cycle- and parent-sensitive envelope witnesses green against the committed subject, 0 failures. Parse sweep 4083 files clean.
Re-roots #9322 onto the RepositoryCommitRef recut. The head this branch carried was built on the ObjectId-keyed ancestry, which review 56294 independently flagged as lossy for exactly the reason the recut fixed: two commits legitimately share a root, so a root does not identify a history record and a first match answers for the wrong one. # Conflicts: # dag/gunbc/scm/repository_envelope.dag
…already declares The merge that re-rooted this branch onto the ancestry recut left one module holding two spellings of one concept: `RepositoryEnvelope.checked_out` is a `String?` naming a history ROW, which is exactly what `RepositoryCommitRef` was minted for. NOBODY AUTHORED THAT DUPLICATE. It was created by INTEGRATION -- neither branch had it alone. RepositoryCommitRef did not exist in the checkout branch's world, and String? was not a duplicate spelling until the row-keyed recut landed beside a checkout selection that had independently stopped being a root. That is worth naming as its own category: it is not a pre-existing weakness, because nobody wrote it, and it is not a claim this diff falsified, because nothing went false. TYPING `checked_out` ALONE WOULD BE THE WRONG REPAIR, which is why this is a declared gap and not a small fix. The decode side already keeps bare-text references and this module's own annotation already calls that a REMAINING conflation. Converting one leaf while the other stays bare relocates the fork rather than removing it, and leaves an intermediate state in which a reader must know which half was converted -- the leaves-upward migration DESIGN's replacement doctrine refuses. The root is the reference vocabulary as a whole: checked_out, the decode-side reference arms, and every other bare-text spelling of a row reference, in one motion. So this extends the EXISTING residue annotation rather than adding a second one. The fact now lives on the module that owns the concept instead of in a review thread, where the next author would have had to rediscover why two spellings sit side by side. Annotation-only: every changed line is a // annotation, so by DESIGN section 4c the semantic projection is untouched. Receipts: 8 of 8 checkout witnesses green after the re-root, 0 failures, including both re-rooted onto the carrier. Parse sweep 4084 files clean.
…points at This branch carried #9282 up to d0e8ce5 but not c319762, and the gap was load-bearing rather than cosmetic. repository_envelope's builder note says the residual duplicated-`at` state is 'stated ONCE, on RepositoryHistory in gunbc.scm.ancestry'. Without this commit that branch's ancestry.dag contained no such statement and instead claimed the disagreement was UNREPRESENTABLE -- so the reference pointed at prose that does not exist, beside prose asserting the opposite. No machine could catch it: it is a cross-file prose reference, and each branch was internally consistent. Found by checking what the reference actually resolved to on the branch that authors it.
…classified Integration, not either side, created this obligation. main landed repository_envelope_load_standing total over RepositoryDecodeRefusal as that population stood; this branch widened the population by two (RepositoryCommitParentReferenceUnresolved, RepositoryDecodedAncestryCyclic). Neither change touched the other's lines, so git merged both cleanly and the exhaustive match silently stopped being exhaustive. Both classify as LoadDocumentMalformed. An unresolved parent reference is a document naming a commit it does not contain, the same damage the root and checkout arms already carry. A cycle is not a rule a newer writer might implement and this build might lack -- acyclicity is a substrate law, so no conforming writer emits one at any version; calling it unsupported would tell a reader to stand aside for a document that is damaged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ct the cycle-validation exponent Two findings from external review, both against claims this branch authored. The commits parameter on encode_commit_record was ADDED by this branch, not left behind by it: main carries encode_commit_record(acc, commit). The branch removed the first-match search from encode_ancestry, annotated that "the parameter that made the search possible does not exist to be misused", and threaded the parameter through the enclosing record encoder unread. An unused population parameter at the exact site that used to search that population is the cheapest route back to the first-match defect, needing no new argument at any call site. The cost annotation said one walk per commit is O(n^2). That counts the walks and takes each walk to be linear without opening it: history_step_at folds the whole step list, ancestry_walk performs one such lookup per step, and a_cyclic_commit runs one walk per start. The total is O(n^3), and the ordinary acyclic case realizes it because no start finds a cycle early. All three factors dissolve on the same keyed-representation trigger, so the figure is corrected and the shape stays declared rather than partly repaired. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rigger named Lane-owner ruling B, after independently deriving the nesting: the corrected figure ships, the shape is fixed on the reference-vocabulary keying cut, and that cut comes forward as one piece over the whole vocabulary rather than as leaf conversions. Also corrects a second understatement authored here: the validation runs on encode and decode, not once per write. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
review 56294 — measured against the current head Each is checked mechanically below rather than asserted closed, because "a later change probably fixed that" is exactly the claim that rots. 1. Parent keyed by
|
…bjectId
review 56439. The log witness fixture passed a.identity -- an ObjectId -- where
DescendsFrom.parent declares RepositoryCommitRef. Every other fixture in the
branch spells it RepositoryCommitRef { reference: "0" }; this one did not.
Position 0 is commit a, which is the intended parent.
The compile is NOT the evidence here, and that is worth recording. Measured on
this tree: a record literal carrying BOTH this error and a plain-field error
(root: String where ObjectId is declared) compiles 0 blocking, while an
undefined variable in the same function refuses with EXIT=1. So the function is
compiled and checked, and declared field positions are not the part being
checked. The witness passed CI because gunbc.scm.log reads neither ancestry nor
parent, so the wrong-typed value was written and never read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…wire record Reversal of a disposition this PR's body records, taken on an argument better than the one I declined. I declined this as a pre-existing weakness on the test "did my diff falsify a claim about it". That test is right and I applied it too narrowly, looking only for prose claims. A declaration's name and fields ARE a claim, and this diff falsifies it: before the ancestry widening the dead carrier's two fields corresponded to the wire record's two members; the wire now allows and requires ["root", "message", "ancestry"] and decode refuses a missing one, so the carrier can no longer represent any valid record. That transition is caused here. It sits immediately before the real three-field RepositoryCommit, in the module that owns the wire format, with the next work being the first production commit producer -- a well-typed, production-located declaration under exactly the right name describing the superseded shape. Deleted rather than widened: adding ancestry would preserve a second unused representation of the record. Not renamed to a legacy form either -- no durable repository format exists yet, so that would fabricate a compatibility subject. Single occurrence corpus-wide, no annotation attached, nothing lost with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rdering obligation review 56461, whose citation is this repository's own: gunbc.scm.ancestry states that a positional spelling owes an append-only wall and an order-preserving wall. This codec discharges the second -- guarded by the decode-order pair and the root-signature order witness -- and does not discharge the first. RepositoryEnvelope.commits is an unrestricted List, so an insert, delete or reorder retargets every still-valid parent reference at once, and neither the membership nor the cycle check can see it: every reference still resolves, to the wrong row. What makes it not yet harmful is also what makes it urgent, and stating only the first half would be the reachability-read-as-occupancy error. No commit-authoring verb exists anywhere in the corpus, so no operation can mutate the list and the state is reachable only by hand-authoring. The commit producer is the next work, so this is an ordering obligation rather than a backlog item: the wall must exist before the first producer, because the producer is what makes the state reachable. Trigger is the reference-vocabulary keying cut, which dissolves it by removing positions altogether. The binding still lands here because nothing has ever written a repository document: the format is not frozen, so binding costs no migration, and deferring would move the format change to the far side of the event that freezes it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… claims execute
Found while checking whether this PR's own receipts run, rather than whether
they pass.
A module that declares no live_tree_disposition resolves to ReadsLiveTree by the
floor's fail-closed default -- v2.workflow.floor_discovery_producer answers
`Absent => EntryLiveTreeResolved { disposition: ReadsLiveTree }` -- and a
ReadsLiveTree site is declined before the fold, counted in declined_live and
never executed. This module carried no declaration, so its witnesses were
discovered, counted, and asserting nothing while reading as coverage. Thirteen
sibling gunbc.scm witness modules declare it; this one did not.
The omission predates the ancestry work: the module lacks the declaration on
main too. What this PR added was 37 claims into a module CI never runs, and a
body citing them as receipts.
SubstrateInputsOnly is a true statement about this module, not the convenient
one: its whole import surface is std.types, the json grammars, v2.std.node and
three gunbc.scm carriers, and nothing here opens a file, shells out or reads the
tree.
This may surface failures or cost diagnostics in claims that have never run in
CI. That is the point of enrolling them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ition The previous revision of this annotation asserted that these witnesses "were discovered, counted, asserting nothing" and never executed. THAT WAS NEVER MEASURED. What was measured: the module declared no live_tree_disposition while thirteen siblings do, and the run reported declined_live=902 -- aggregate, and attributable to no module, because the floor's output names no module's disposition anywhere. What was inferred and stated as fact: that these claims were among the 902. The reasoning came from v2.workflow.floor_discovery_producer answering `Absent => ReadsLiveTree`. That is a real code path but not established to be the executing one -- the floor consumer resolves an undeclared module by deriving from its import closure, so a substrate-only module may have been admitted all along. Two computations of one fact, and the annotation read one and asserted it of the other. The declaration stands unchanged and is correct under every mechanism in play, because SubstrateInputsOnly is a true statement about this module's import surface, verified before declaring. Silence is the one answer that cannot be right; that is the reason to declare, and it needs no claim about what the silence did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r these claims execute
Read from source, not from a rerun. The floor's automatic route is a
column-zero text scan in witness_file_from_source: a line must
starts_with("data ") AND contain BOTH LiveTreeDisposition AND ReadsLiveTree.
undeclared matches nothing -> reads_live_tree false -> ROUTED
SubstrateInputsOnly matches nothing -> reads_live_tree false -> ROUTED
Same answer before and after this row. The 37 claims were executing already, so
"declaring it is what makes them execute" is false and is withdrawn. My first
commit said they were declined; my second retracted that but kept the second-
order claim and asserted a derivation the floor does not perform.
Where the row does change something is the SELECTION path:
parse_entry_live_tree_disposition ends Ok(declared.unwrap_or(true)), so silence
reads as live and the module is not selection-eligible. Declaring
SubstrateInputsOnly makes it eligible. Real effect, different effect.
The reason to declare is therefore ambiguity, not execution: three routes read
this fact and disagree about silence, which cli_run.rs records at the scan as
"ONE FACT, TWO COMPUTATIONS, IN ONE BINARY -- a section 3 defect". An authored
answer removes this module from that disagreement without pretending to resolve
it.
Also withdrawn, refuted by arithmetic on my own receipt: that claims are missing
from the floor's counters. 11647 routed + 541 declined_long + 902 declined_live
= 13090 offered, exactly. The partitions are exhaustive and I had the numbers
when I said otherwise.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stacked on #9283. Checkout already existed at object grain — reconstructing a program from a commit's root closure. What was missing is selection: which commit a repository is positioned at.
Two grains of one verb, in the module that owns it
Putting the repository-grain half in its own file would have minted a second name for one verb — the module would have had to be called something that is not "checkout" while being exactly checkout. Import direction was checked rather than assumed:
repository_envelopereachesobject_store,ancestry,target_reference,json_memberand the closure carriers, none of which reachcheckout, so the new edge closes no cycle.Checkout says nothing about ancestry — derived, not omitted
This is stated in the source, because a later reader finding no mention of ancestry in a verb that selects a commit would reasonably read it as an oversight and "fix" it.
A commit's program is its root closure. Ancestry is a chain over commits. Two different graphs over two different populations, and reconstruction walks only the first — so no ancestor needs to exist, or be readable, or be consistent, for a checkout to be exactly right.
The consequence is not cosmetic, and it has a witness pair: a commit whose stated parent is missing checks out successfully, while an ancestry walk from that same commit answers
AncestryBrokenAt. One repository, one commit, two questions, two different answers, both correct — the history is truncated and the program is not.The second witness is what makes the first mean anything: without it, the successful checkout could be a fixture that accidentally has no ancestry rather than checkout correctly ignoring it. Making checkout refuse there would invent a dependency the model does not have, and would mean a damaged history silently costs a user access to programs that are wholly intact.
The refusal arms carry no repository
The root-commit discipline, one layer out. Had a failed checkout answered an envelope with nothing selected, "this checkout did not happen" and "this repository has nothing checked out" would share a representation — the second being an ordinary state the envelope's own header calls not an error.
The two refusals are separate because their owners are opposite. An unresolved reference is the caller's fact; an unreconstructable program is the store's. Collapsing them would hand a user "no such commit" for a commit sitting in front of them in the log.
It writes nothing
A new envelope with the selection moved, argument untouched, so the verb stays a pure transform. Persisting is a separate operation at a boundary this module deliberately does not reach — the held frozen-host ruling.
No Bool predicate over the coproduct is exported
A
checkout_succeededconvenience was written and removed before this landed: its only consumer would have been the witness asserting it works. That is the same objection review 56207 raised against a sibling helper ingunbc.scm.ancestry, applied here before review had to. The reason is consumer count, and consumers now match the arms, so a fourth arm fails to compile at every site rather than being absorbed into afalse.Merged onto the post-#9282 main, through #9283
#9282 landed. Squash-merging it left this branch's merge-base at an old main, so the displayed diff re-showed work already on main; merging the updated #9283 branch through resets it (1827 → 1154).
ancestry.dagandlog.dagare byte-identical to main here.The merge carried in a semantic break and its repair, both from the layer below. main's #9318 landed
repository_envelope_load_standingtotal overRepositoryDecodeRefusal; #9283 widens that population by two, so the clean merge silently made the match non-exhaustive. That is fixed in #9283 and inherited here.I checked rather than assumed the inheritance, because "it is fixed downstack" is exactly the claim that goes stale without anyone touching it. This branch adds no further variants — the coproduct carries the same 10 arms and the classifier the same 15 rows on both branches — and the merged tree compiles 0 blocking, so nothing here reopens it.
Receipts
scm_ck_a_commit_with_a_missing_parent_still_checks_out, while the others stay green, including the companion witness proving that fixture's history really is broken. (Taken before the re-root; the mutation targets checkout's independence from ancestry, which the re-root does not touch.)Corroboration, at symbol grain
The defect is stated by the authority that created it — the same module both keyed the selection on a root and declared, in its own header, that a root does not identify a commit:
gunbc.scm.repository_envelope—RepositoryEnvelope.checked_outwasObjectId?, keyed on the root.gunbc.scm.repository_envelopeencode_optional_reference— recovered a wire position by searching for the first row with that root (viacommit_reference_of).gunbc.scm.repository_envelopeheader — "two commits with identical roots and different messages name the SAME program", echoed bytest.claim.scm_log_witness's own note that the envelope's header says two commits may share a root.Cited by symbol rather than by
file:linedeliberately, per DESIGN §3's standing rule. These were handed to me as three line numbers, and checking them is the rule's own argument: one already pointed at the call site rather than the declaration that does the searching, and two of the three land on unrelated declarations after this diff — a located claim that rots without anyone touching the thing it names.Re-rooted onto the ancestry recut, and what the census caught
This branch was built on the
ObjectId-keyed ancestry. It is now merged onto the recut, and the merge is worth reading because the deletion did its job loudly rather than silently.Three imports refused by name —
CommitAncestryLookup,AncestryKnown,AncestryUnknownCommit, all deleted by the recut. The walk now readsrepository_history(commits:)instead of a lookup callback threaded in by the caller.The fixture repair is the recut in miniature.
scm_ck_truncated_history_repositorypreviously minted a real stored object and named it as the absent parent — underObjectIdkeying that was the only way to spell "a parent that is not one of my commits." A parent now names a history row, so the absence is said directly:reference: "7", out of range for a one-row list, and no object needs to exist.One conflict, resolved by keeping each side's own arm: this branch authored
RepositoryCheckoutNotInCommits { reference: String }, the recut authored theRepositoryCommitRefre-keying ofparentanddetected_from. Verified against every consumer of all three arms and against the producing functions.Review 56294, and which of its findings survive
That review ran against
2a68ceaf, the pre-recut head, and raised three findings. Naming the head each was measured against, because two are already answered:ObjectId, encoding takes the first matching row, losing which commit was the parent. Closed by the recut, which was arrived at independently before this review was read.2a68ceafhadDescendsFrom { parent: ObjectId }; the current head hasDescendsFrom { parent: RepositoryCommitRef }. The reviewer reached the same defect by the same reasoning and used the same word for it — lossy.AncestryWalkcollapsing broken, cyclic and unknown-start into onefalse. Closed by the recut, which deletedcommit_is_ancestor_ofoutright.atstate is declared onRepositoryHistoryat its honest rung — mechanically prevented at each builder, not structurally impossible — with an unforgeable-construction next-rung trigger. The construction-before-validation objection is correct; a declared residue at a named rung with a named trigger is the honest answer to it, and this PR does not claim the residue is gone.An integration-created duplicate, declared rather than fixed here
After the merge,
checked_outis aString?naming a history row whileRepositoryCommitRefexists for exactly that concept. Neither branch had this alone —RepositoryCommitRefdid not exist in this branch's world, andString?was not a duplicate spelling until the recut landed beside it.Typing
checked_outalone would be the wrong repair: the decode side still keeps bare-text references, so converting one leaf relocates the fork instead of removing it and leaves a state where a reader must know which half was converted — the leaves-upward migration the replacement doctrine refuses. The root is the reference vocabulary as a whole, cut in one motion.It is therefore recorded in the module's existing residue annotation, beside the decode-side references it belongs with, rather than fixed opportunistically here.
The class this belongs to
This is a non-unique key resolved by first match, silently. The shape: a value close enough to unique that it works until it doesn't, every reader taking the first match, none refusing, and the wrong answer well-formed and deterministic — so it reproduces consistently and looks like correct behaviour, which is why testing does not catch it.
The repair is the general one: the key is carried, not derived, so the ambiguity has nowhere left to live.