Skip to content

scm: ancestry, landed before the commit verb that would have baked its absence in - #9282

Merged
briansrls merged 10 commits into
mainfrom
session/scm-ancestry
Aug 26, 2026
Merged

briansrls merged 10 commits into
mainfrom
session/scm-ancestry

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

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, not a graph — merge-base, ahead/behind, commits-since, log ranges and merge itself had no referent here.

This lands before the commit verb rather than after it, because a commit-authoring verb would have had to decide all of this implicitly, in passing, at the moment it wrote its first RepositoryCommit.

dag/gunbc/scm/ancestry.dag is a new module — nothing is removed from main by this PR.

Ancestry is keyed by a history record, not by a program root

This is the correction that reshaped the PR, and it is the one thing to review first.

An earlier revision keyed ancestry on ObjectId — the commit's root. A root does not identify a commit. Two commits legitimately share one root whenever a repository returns to an earlier program: a revert A → B → A has three records and two distinct roots, so root-keyed ancestry cannot distinguish the third record from the first. It would have silently answered for the wrong one, by first match.

So a history record gets its own identity, RepositoryCommitRef, distinct from the ObjectId of the program it points at. ObjectId remains right for objects and is wrong for records.

a_revert_to_an_earlier_program_still_traces_every_record and the_reverted_to_record_is_an_ancestor_and_not_the_revert_itself are the witnesses for exactly this, and neither is expressible under the old key.

One carrier, because two authorities were free to disagree

An earlier revision passed the walk a List and a lookup callback. Those are two authorities for one fact (§3), and they could disagree in three ways actually reachable: an empty history returned the accumulator's initial value as a verdict; a start absent from the list but known to the lookup traced normally, so the arm named AncestryStartNotInHistory was decided by the wrong fact; and a parent absent from the list but known to the lookup was traversed instead of reaching BrokenAt.

RepositoryHistory replaces both. Membership, relation and step bound all derive from its rows, because there is no second place to look. A membership check would have closed one of the three and conceded the other two were writable.

Stated at its honest rung, not above it (§4b): this does not make all disagreement unrepresentable. Two steps may still carry the same at, and history_step_at resolves by first match, so a carrier can disagree with itself under a duplicated key. Uniqueness is an invariant of whatever builds the carrier, not of the type — there is no producer in production code today, and it stays authorable in a fixture. Next-rung trigger is an unforgeable-construction wall making one admitted builder its sole constructor. The module's annotation carries this; the codec references it rather than restating it.

It is carried, not recovered from position — and that is forced, not 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 yet defines what appending even means. Recovering ancestry from position would not be recovering it, it would be inventing it, and the invention would be invisible precisely because it agrees with position by construction.

The receipt is by execution, and it is the sharpest one here. Replacing the walk with a faithful position-implies-ancestry implementation leaves the linear-history positive control GREEN — in a linear history position and ancestry coincide — and is caught only by adjacent_position_does_not_make_one_commit_an_ancestor_of_another. That witness exists for a claim the obvious test structurally cannot see.

(A first mutation attempt was not faithful — it left the target witness green and broke the positive control, which proves the mutation faulty rather than the witness weak. Recorded because a bad mutation proves nothing and should not be reported as a receipt.)

The root is a structural state

RootCommit is an arm, not a parent field holding a "nothing" value. The latter makes 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<Proposal>, 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 §4b rules worse than absent, because it is cited as a capability the model does not have.

Widening trigger, stated in the type's own annotation: if merge ever takes two Commits, this coproduct gains a several-parents arm, and that change is a compile error at every consumer rather than a silent reinterpretation.

A deleted Boolean

ancestry_is_root was removed. It restated what the RootCommit arm already says, had zero production consumers, and the module's own annotation had already described it as deleted while the code still defined it — a false claim in prose this PR authors. It is deleted on the independent ground that it is a redundant second spelling, not merely to make the prose true.

The witness pairing it served is preserved: a witness-local reader still asserts RootCommit reads as a root and DescendsFrom does not, so the negative half cannot rot into a vacuous pass.

Author and timestamp are explicitly separated, not left to ride along

Parent is structural — it relates two things the model already 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 admission question currently gating the store binding 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, by pigeonhole

A chain longer than the history must have revisited a commit. The refusal is derived from the bound rather than asserted by a visited-set this module would then have to keep correct.

log.dag is updated because this diff falsifies it

log.dag states 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 carries two claims with different fates. RepositoryCommit still has no parent field — that arrives with the codec — so a log row still cannot name a parent; 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. Deleting the bullet whole would have removed a true statement in order to repair a false one.

status.dag makes a structurally similar claim and is not falsified — not here and not by the codec PR either. Its stated reason is NO BRANCH and NO REMOTE, and ahead/behind is a relation between two named endpoints: ancestry is necessary for it and not sufficient. Neither PR supplies a second endpoint, so the sentence and its reason both stand. The falsification test is whether the specific thing claimed unproducible is now producible, which comes apart from "a related fact arrived" exactly when the arriving fact is necessary-but-not-sufficient.

Receipts

  • 13 witnesses PASS, 0 failures, run locally against the built binary.
  • MUT (faithful position-implies-ancestry): target witness false, positive control stays green.
  • MUT (broken parent reported as a completed trace): broken-chain witness false, linear trace stays green — it discriminates.
  • Restored: all green.
  • Parse sweep clean over the tree, including the §4c annotation-grain check that caught two real defects in this branch's own history (one in-body block, one trailing block).

Not done here, named rather than left implicit

Binding ancestry into RepositoryCommit is a wire-format change: that type is codec'd with a RepositoryCommitMembers member-set guard, and the envelope's header states the v1 body stays frozen once documents exist. That binding is the versioned-codec step, and it is a different change from settling the model — reported rather than silently done or silently skipped.

…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>
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 26, 2026 03:53
…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>
@gunbai-bot

gunbai-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

review 56072 — fixed, and the finding is correct.

ancestry_walk_step built the chain with concat(acc.chain, [parent]), copying the whole accumulator per step. It now prepends and reverses once at the single completion point, so each step is O(1), the walk is O(n), and the exported chain order — descendant first — is unchanged, meaning no caller can observe the fix.

You also preempted the defense I would have been tempted by, and you were right to: DESIGN §6 names "n is small here" specifically, and this particular n is the length of a repository's history, which is exactly the quantity that grows without bound in use.

On the second finding, which I am not changing: commit_is_ancestor_of folds over the traced chain to find an element — that is O(n) over a bounded list, not a quadratic shape. Your framing was that the two together make the walk-then-search O(n²) end-to-end; with the walk now linear, the pair is O(n). If you meant something sharper about the membership test itself, say so and I will look again.

Where this came from, since it is the more useful part of the finding. gunbc.scm.repository_envelope carries an annotation about having committed this exact defect and having 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 is now in the source beside the fix: reading the note is not the same either.

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, a_stated_ancestor_two_generations_back_is_found) are what would go red on a missing or doubled reverse. Parse sweep clean.

— sent from gentle-eagle-360

…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>
@gunbai-bot

gunbai-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

review 56102 — fixed, and you were right that it is small but real. The fabrication is what made it not a naming quibble.

The arm now reads AncestryStartNotInHistory { start }, carrying only what the input supplied. Previously an unknown start had no parent to name, so the walk invented one — a consumer acting on "commit X's parent Y is missing" would have gone looking for an object nothing ever claimed existed.

Your reachability argument is the part I would not have derived from the type alone, and it is now stated in the source as a construction fact rather than a convention: 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.

Also worth recording where this came from: the collapse is the one-reason-symbol-over-two-opposite-states pattern that this module's own header prose names, committed in the module that names it — the second time in this stack a defect was reintroduced in a module carrying the annotation warning about it.

Receipt: eight witnesses green against a freshly built compiler. The new one asserts the start identity, not 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.

— sent from gentle-eagle-360

@gunbai-bot

gunbai-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

HOLD — do not merge. Confirmed wrong-grain type; recut in progress.

Held by the product-direction lane (swift-badger-524) at 2026-08-26T15:45Z. Converting to draft as the mechanical hold, since PRs are being merged manually and this one is green, mergeable and approved.

The defect, verified by reading this PR's head:

  • dag/gunbc/scm/ancestry.dag:55 — DescendsFrom { parent: ObjectId }
  • dag/gunbc/scm/ancestry.dag:148, :198, :228 — lookup: fn(ObjectId) -> CommitAncestryLookup

ObjectId is the identity of the program root, not of a history record. The authorities on main explicitly admit two commits sharing one root — dag/gunbc/scm/log.dag:48 ("commits may share a root and differ only in message") and dag/gunbc/scm/repository_envelope.dag:19. So the ancestry edge is keyed on something that does not uniquely identify what it points at.

An ordinary revert decides it. Records: 0 = root A, RootCommit; 1 = root B, parent 0; 2 = root A, parent 1. Content addressing correctly gives records 0 and 2 the same root A — so root-keyed ancestry requires A to be simultaneously RootCommit and DescendsFrom { parent: B }. It cannot be both. This is a contradiction at the level of the type and is decidable by inspection; no fixture is needed to establish it.

Why held rather than merged-and-followed-up. This PR is two files and currently has zero consumers of the bad grain — lookup is a parameter with no producer; the first-match producer that turns this into a silent wrong answer lives in #9283. So today it publishes a wrong grain, not a wrong answer, and the repair is nearly free. On merge it becomes an attractor (DESIGN §3) and every consumer written afterwards inherits the key.

Resolution, agreed with the authoring lane. A named RepositoryCommitRef declared in ancestry.dag — which owns the relation between history records, and which imports only std.types and gunbc.scm.object_store, so it cannot take the type from downstream without a cycle. #9283 re-roots onto this; #9322 then becomes the second consumer of a landed authority rather than the precedent for it.

Found and escalated by the authoring lane, who asked for the hold rather than letting it merge.

This will be undrafted when the recut lands. Please do not merge in the meantime.

@gunbai-bot
gunbai-bot Bot marked this pull request as draft August 26, 2026 15:43
gunbc-ci-auto-heal and others added 7 commits August 26, 2026 16:23
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>
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>
…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>
…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.
gunbai-bot Bot pushed a commit that referenced this pull request Aug 26, 2026
…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.
gunbai-bot Bot pushed a commit that referenced this pull request Aug 26, 2026
This branch inherited the same gap the codec branch had: it contained #9282 up
to d0e8ce5 but not c319762, so the carrier annotation its own residue
note depends on was still the overclaiming version.
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 26, 2026 21:31
@briansrls
briansrls merged commit 58a8647 into main Aug 26, 2026
9 of 12 checks passed
@briansrls
briansrls deleted the session/scm-ancestry branch August 26, 2026 23:12
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