diff --git a/dag/gunbc/scm/ancestry.dag b/dag/gunbc/scm/ancestry.dag new file mode 100644 index 00000000000..7c5941a1778 --- /dev/null +++ b/dag/gunbc/scm/ancestry.dag @@ -0,0 +1,330 @@ +module gunbc.scm.ancestry + +// ANCESTRY: WHICH COMMIT A COMMIT CAME FROM. +// +// The model did not have this. Every `parent` in gunbc.scm is TREE-NODE parentage inside the object +// store -- an edge from a node to its child -- and none of it relates one commit to another. A +// RepositoryCommit is { root, message }, and `commits` is a flat List, so before this module a +// repository's history was a SEQUENCE and not a graph. Every operation git states in terms of +// ancestry -- merge-base, ahead/behind, commits-since, log ranges, and merge itself -- had no +// referent here at all. +// +// IT IS CARRIED, NOT RECOVERED FROM POSITION, and that is forced rather than chosen. Position gives +// a total order and says nothing about descent: that the commit at index 1 follows the one at index +// 0 in the list does not make it its child, and reading it that way would assume history is both +// linear and append-ordered -- neither of which anything in this model establishes (there is no +// commit-authoring verb, so nothing defines what appending means). Recovering ancestry from position +// would therefore not be recovering it; it would be inventing it, and the invention would be +// invisible because it would agree with position by construction. +// +// THIS MODULE LANDS BEFORE ANY PRODUCER, WHICH IS THE CHEAPEST MOMENT IT COULD. RepositoryCommit is +// constructed in exactly two places on main -- its own type home and a witness -- so no production +// code has yet baked in the assumption that history is a sequence. That is model-before-implement in +// the order DESIGN prescribes, not a type built speculatively ahead of its consumer. + +import std.types { Bool, Int, List, String } + +// THE ROOT IS A STRUCTURAL STATE, NOT AN EMPTY PARENT. +// +// The alternative -- a parent field holding some "nothing" value -- makes "this commit begins the +// history" and "this commit's parent is missing" the same value, and those have opposite meanings: the +// first is the ordinary state of every repository's first commit, the second is damage. That is the +// same conflation `log` refused when it made the empty history its own state rather than an empty +// row list, and it is refused here for the same reason and one layer deeper. +// +// EXACTLY ONE PARENT, AND THIS IS THE DECISION THAT WOULD BE UNWRITABLE LATER, so it is derived from +// what the model's own merge does rather than from what git does. +// +// gunbc.scm.merge `merge` takes ONE target Commit and a LIST OF PROPOSALS, and answers `Merged` +// carrying ONE commit. A Proposal is a set of authored requirements -- it is not a commit, it has no +// root, no message and no identity in history. So a merge in THIS model combines one commit with N +// things that were never commits, and the result has exactly one commit antecedent. Git's second +// parent exists because git merges two BRANCHES, which are two commits; that shape has no referent +// here, because nothing in this model can merge two commits at all. +// +// So a parent LIST would not be more general, it would be a state with no producer -- an arm nothing +// can construct, which DESIGN section 4b rules worse than absent because it gets cited as capability +// the model does not have. WIDENING TRIGGER, stated so the reason is checkable rather than trusted: +// if `merge` ever takes two Commits rather than one commit and N proposals, this coproduct gains a +// several-parents arm, and the change is a compile error at every consumer rather than a silent +// reinterpretation -- which is what makes single-parent a safe decision now rather than an +// irreversible one. +// WHICH OCCURRENCE IN THIS HISTORY -- NOT WHICH PROGRAM. This type exists because those are two +// different questions and the model previously answered both with `ObjectId`. +// +// THE BOUNDARY RULE, stated so the next author can place a new lookup without matching it against a +// list of sites: an `ObjectId` is content-addressed, so it genuinely identifies a PROGRAM, and +// keying an object by it is correct -- uniqueness is the POINT of that identity, not an assumption +// about it. A commit record is a program PLUS a message PLUS a position in history, and only the +// last of those is unique. So `ObjectId` is the right key for objects and the wrong key for commit +// records, and the question to ask at a new lookup is which of the two you are keying. +// +// THE HISTORY THAT DECIDES IT is an ordinary revert, not a corner case: +// +// record 0: root A, RootCommit +// record 1: root B, parent -> record 0 +// record 2: root A, parent -> record 1 (a revert to exactly the program in record 0) +// +// Content addressing CORRECTLY gives records 0 and 2 the same root. Root-keyed ancestry then needs +// A to be simultaneously `RootCommit` and `DescendsFrom{B}`, which is a contradiction at the level +// of the type -- one key, two irreconcilable values. The repository already admits this: `log` +// derives references from list POSITION precisely because two commits may share a root and differ +// only in message. +// +// WHAT THE OLD GRAIN DID, and it is why this is a correctness recut rather than a tidy-up: a +// first-match lookup answers with record 0's `RootCommit`, so a walk from record 2 terminates +// immediately and reports it as a root. The revert edge and record 1 vanish. Not a refusal -- a +// well-formed, confident, wrong answer, which is the silent wrongness DESIGN section 5 places +// outside the ladder entirely. +// +// REPRESENTATION IS DELIBERATELY NOT DECIDED HERE, AND THE DEFERRAL NAMES ITS INVARIANTS so that it +// reads as an open question rather than an answered one. The reference is opaque at this layer; +// whether a document spells it as a row position is a fact about a serialized repository and belongs +// to the codec that owns one. +// +// A POSITIONAL SPELLING OWES TWO WALLS, NOT ONE, and they buy different properties: +// +// append-only records are only ever appended -- never inserted, removed or reordered. +// This is what buys STABILITY. An earlier-only rule (a parent may name only +// a preceding record) buys ACYCLICITY and is NOT a substitute: against a list +// whose indices move, earlier-only holds throughout, no cycle ever appears, +// and every stored reference silently repoints to a different commit. That is +// the same well-formed-confident-wrong answer this type exists to remove, +// reintroduced by the repair that removed it. +// +// order-preserving the codec must round-trip `commits` in exactly its authored order. This is +// a CODEC obligation rather than a model one, and it is not hypothetical: the +// defect that produced this recut was a codec losing which record was meant. +// A decoder that rebuilds the list in any other order breaks every positional +// reference at once. +// +// If either wall is hard to make structural, that is the argument for the reference carrying +// something INTRINSIC instead of a position. Naming the concept here is what allows that decision to +// be taken later in one place instead of two. +type RepositoryCommitRef { + reference: String +} + +fn repository_commit_ref_eq(left: RepositoryCommitRef, right: RepositoryCommitRef) -> Bool { + left.reference == right.reference +} + +type CommitAncestry + = RootCommit + | DescendsFrom { parent: RepositoryCommitRef } + +// AUTHOR AND TIMESTAMP ARE NOT DECIDED HERE, AND ARE EXPLICITLY SEPARATED RATHER THAN LEFT TO RIDE +// ALONG. They were reported absent beside `parent` by the log verb, and they are absent for a +// DIFFERENT REASON, which is why they cannot be settled in this change: +// +// PARENT is a STRUCTURAL fact. It relates two things the model already holds, it is decidable +// inside the model, and nothing external is consulted to know it. That is why it can land now. +// +// AUTHOR and TIMESTAMP are EXTERNAL OBSERVATIONS -- who ran the command, and what the clock said. +// Neither is derivable from the graph, and DESIGN is explicit that external reality is observed, +// typed, and admitted or refused, never fabricated. This repository has no modelled clock and no +// modelled process identity to observe them WITH, and acquiring either is the same frozen-host +// question that gates the store binding and process output. +// +// So modelling them here would mean inventing two facts to fill fields, which is the fabricated- +// plausible-output failure at the exact boundary a commit is supposed to be trustworthy. They are +// blocked on a ruling; parent was not. Recording that difference is the point of this note. + + +// The history this walk may traverse, supplied as the commit identities the repository holds. It is +// a PROJECTION of the envelope's own commit list rather than a second authority: nothing here stores +// history, and a caller that passed a different population would be describing a different +// repository, not corrupting this one. +type AncestryStep { + at: RepositoryCommitRef + ancestry: CommitAncestry +} + +// ONE CARRIER, SO THERE IS NOTHING FOR TWO AUTHORITIES TO DISAGREE ABOUT. +// +// The walk previously 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 section 3), and they were free to disagree: +// +// 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 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 instead of reaching `BrokenAt`. +// +// A membership check before the fold would have closed the first of those and left the third +// constructible -- it concedes the bad state is writable and guards one instance. The carrier removes +// those three outright instead: membership, relation and step bound all derive from these rows, +// because there is no second place to look. That is the whole of what this shape buys, and an earlier +// revision of this annotation claimed more -- that it made the disagreement UNREPRESENTABLE. +// +// IT DOES NOT, AND THE RESIDUAL STATE IS NAMED HERE BECAUSE THIS IS THE CARRIER THE FACT IS ABOUT. +// Two steps may carry the same `at`, and `history_step_at` resolves by first match, so one carrier +// can disagree WITH ITSELF under a duplicated key -- the same first-match class this change removed, +// one level down. Uniqueness is an invariant of whatever BUILDS the carrier, not of the type: it has +// no producer in production code today, and it stays authorable in a fixture, which is the honest +// rung (DESIGN section 4b) -- mechanically prevented at each builder rather than structurally +// impossible. A uniqueness CHECK would be the wrong repair, for exactly the reason this paragraph +// already gives about membership checks: it would concede the duplicate is writable and guard one +// instance of it. NEXT-RUNG TRIGGER: an unforgeable-construction wall making one admitted builder the +// sole constructor of RepositoryHistory, at which point a fixture cannot express the collision. +type RepositoryHistory { + steps: List +} + +// The single question the walk asks: is there a row at this reference, and what does it say. It +// replaces the `lookup` parameter, and it cannot disagree with the step budget because the budget is +// this same list. +fn history_step_at(history: RepositoryHistory, at: RepositoryCommitRef) -> AncestryStep? { + fold(history.steps, init: none, f: fn(found, step) { + match found { + Present { value: _ } => found + Absent => + if repository_commit_ref_eq(left: step.at, right: at) { + Present { value: step } + } else { + none + } + } + }) +} + +// THREE OUTCOMES, AND THE THIRD IS THE ONE A NAIVE WALK GETS WRONG BY LOOPING FOREVER. +// +// AncestryTraced carries the chain from the starting commit back to the root, inclusive and in that +// order, so a consumer reads descent without re-deriving it. +// +// AncestryBrokenAt names a commit whose parent is not in this history. That is damage, and it is +// distinguished from reaching the root because they are opposite facts: one is a complete history, +// the other is a truncated one, and a walk that answered "done" for both would report a broken chain +// as a finished one. +// +// AncestryStartNotInHistory is the caller's fact, not the history's, and separating it is the whole +// reason this arm exists. "The commit you asked me to walk from is not in this history" and "commit +// c's parent p is missing" have OPPOSITE OWNERS -- the first is a bad argument, the second is a +// truncated history -- and opposite remedies. Collapsing them additionally forced the walk to invent +// a `missing_parent`, since an unknown start supplies no parent to name: the fabricated-plausible- +// output failure at the boundary this module exists to be trustworthy over. +// +// IT IS REACHABLE ONLY FROM THE START, BY CONSTRUCTION rather than by 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. The unverified cursor is the starting one and no other. +// +// AncestryRevisitsACommit is a REFUSAL, not a truncation. A parent chain that returns to a commit it +// already visited is a cycle, and DESIGN's substrate is bounded and forward -- cyclic relations are +// encoded acyclically, never traversed as cyclic values. Detected by exhausting the population: a +// chain that takes more steps than the history has commits must have revisited one, by pigeonhole, +// so the refusal is DERIVED from the bound rather than asserted by a visited-set this module would +// have to keep correct. +type AncestryWalk + = AncestryTraced { chain: List } + | AncestryBrokenAt { commit: RepositoryCommitRef, missing_parent: RepositoryCommitRef } + | AncestryStartNotInHistory { start: RepositoryCommitRef } + | AncestryRevisitsACommit { detected_from: RepositoryCommitRef } + +// IT CARRIES THE RESOLVED ROW, NOT A BARE REFERENCE, and that is what removes an arm rather than +// handling it. With a bare cursor the step had to look the cursor up again every iteration and answer +// for a not-found case that the walk's own invariant makes impossible -- an arm with no producer, +// which the previous shape filled with `AncestryStartNotInHistory` even mid-walk, where the cursor is +// not the start. Holding the row means the only lookup is the PARENT's, whose absence is a real and +// nameable state. +type AncestryWalkAcc { + chain: List + current: AncestryStep + done: Bool + outcome: AncestryWalk +} + +// THE CHAIN ACCUMULATES BY PREPEND AND IS REVERSED ONCE AT COMPLETION. concat-at-end copies the +// whole accumulator per step, which makes an n-step walk O(n^2) -- the cost shape DESIGN section 6 +// rules is ALWAYS fixed regardless of the realized n, because "n is small here" is not a time-stable +// fact: this walk's n is the length of a repository's history, which is exactly the quantity that +// grows without bound in use. +// +// It is worth recording that gunbc.scm.repository_envelope carries an annotation about having +// committed this same defect and had it caught by review, and this module reintroduced it anyway in +// a diff that imports that module. The lesson there was that fixing a cost shape at the sites a +// review names is not the same as not having the defect; the lesson here is that reading the note is +// not either. The reversal is at the single completion point rather than at each consumer, so the +// exported chain order -- descendant first -- is unchanged and no caller can observe the fix. +fn ancestry_walk_step(acc: AncestryWalkAcc, history: RepositoryHistory) -> AncestryWalkAcc { + if acc.done { + acc + } else { + match acc.current.ancestry { + RootCommit => + AncestryWalkAcc { + chain: acc.chain, + current: acc.current, + done: true, + outcome: AncestryTraced { chain: reverse(acc.chain) }, + } + DescendsFrom { parent: parent } => + match history_step_at(history: history, at: parent) { + Absent => + AncestryWalkAcc { + chain: acc.chain, + current: acc.current, + done: true, + outcome: AncestryBrokenAt { commit: acc.current.at, missing_parent: parent }, + } + Present { value: next } => + AncestryWalkAcc { + chain: concat([parent], acc.chain), + current: next, + done: false, + outcome: acc.outcome, + } + } + } + } +} + +// The walk is bounded by the population it walks: `history` supplies both the membership test and +// the step budget, so the fold is total and terminates by construction rather than by a guard this +// module has to keep correct. +// THERE IS DELIBERATELY NO BOOLEAN ANCESTOR PREDICATE HERE, and its absence is the point. +// +// A `commit_is_ancestor_of` returning Bool answered `false` for FOUR different facts: a completed +// trace proving the candidate is not an ancestor, a broken parent chain, a start outside the +// history, and a cycle. Only the first is a negative ANSWER; the other three are failures to +// establish one. Collapsing them is the not-established-versus-negative conflation DESIGN names, +// and it is the same shape repaired across the JSON and source-analysis lanes. +// +// It is DELETED rather than typed, on consumer count. There is no production consumer -- the walk +// itself is the surface, and `AncestryWalk` already carries all four states distinctly. Publishing +// a typed `AncestorJudgment` today would mint an arm with no producer, which DESIGN section 4b +// rules worse than absent because it gets cited as a capability the model does not have. The same +// reasoning deleted `ancestry_is_root` from this module. +// +// WHEN A REAL CONSUMER ARRIVES -- merge-base, ahead/behind, a publication decision -- it derives its +// answer from `ancestry_walk` and decides for itself what a broken chain means to IT, which is a +// judgment this module cannot make on its behalf. + +fn ancestry_walk(start: RepositoryCommitRef, history: RepositoryHistory) -> AncestryWalk { + match history_step_at(history: history, at: start) { + Absent => AncestryStartNotInHistory { start: start } + Present { value: first } => + match fold( + history.steps, + init: AncestryWalkAcc { + chain: [start], + current: first, + done: false, + outcome: AncestryRevisitsACommit { detected_from: start }, + }, + f: fn(acc, _budget) { ancestry_walk_step(acc: acc, history: history) }, + ) { + AncestryWalkAcc { chain: _, current: _, done: done, outcome: outcome } => + if done { + outcome + } else { + AncestryRevisitsACommit { detected_from: start } + } + } + } +} + diff --git a/dag/gunbc/scm/log.dag b/dag/gunbc/scm/log.dag index 3657357158e..95926213099 100644 --- a/dag/gunbc/scm/log.dag +++ b/dag/gunbc/scm/log.dag @@ -15,10 +15,14 @@ module gunbc.scm.log // // AUTHOR -- no commit in this layer records who made it. // TIMESTAMP -- no commit records when. -// PARENT -- there is NO ancestry relation at all. RepositoryCommit has no parent field and -// nothing else in gunbc.scm names one, so a repository's history is a SEQUENCE and -// not a graph. Every operation git states in terms of ancestry -- merge-base, -// ahead/behind, "the commits since X" -- has no referent here. +// PARENT -- RepositoryCommit has no parent field, so no log row can name one. What changed is +// the REASON, and only half of the original claim survived: gunbc.scm.ancestry now +// names an ancestry relation, so "nothing else in gunbc.scm names one" is false as +// of that module. The envelope does not bind the relation to a commit, so this +// module still has no fact to read and still reports the absence rather than +// synthesising it. The operations git states in terms of ancestry -- merge-base, +// ahead/behind, "the commits since X" -- still have no referent, now because none +// of them is modelled rather than because no relation exists to state them over. // // Each would have to be invented to appear in a row, and inventing them is the fabricated-plausible- // output failure DESIGN section 5 forbids at a user-facing boundary. diff --git a/dag/test/claim/scm_ancestry_witness_test.dag b/dag/test/claim/scm_ancestry_witness_test.dag new file mode 100644 index 00000000000..4a80250d2e1 --- /dev/null +++ b/dag/test/claim/scm_ancestry_witness_test.dag @@ -0,0 +1,352 @@ +module test.claim.scm_ancestry_witness + +import std.types { Bool, String, List } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import gunbc.scm.ancestry { + CommitAncestry, + RootCommit, + DescendsFrom, + RepositoryCommitRef, + repository_commit_ref_eq, + AncestryStep, + RepositoryHistory, + history_step_at, + AncestryWalk, + AncestryTraced, + AncestryBrokenAt, + AncestryStartNotInHistory, + AncestryRevisitsACommit, + ancestry_walk, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// Names qualified: top-level definitions are corpus-wide and the resolver silently selects among +// duplicates (#8707), which this layer's witness files already document hitting. +// +// A REFERENCE NAMES A HISTORY ROW, so this helper no longer reaches the object store at all. The +// previous form hashed a synthetic node to mint an ObjectId, which was the wrong grain: it produced +// the identity of a PROGRAM where the walk needs the identity of a RECORD. +fn ancestry_witness_ref(name: String) -> RepositoryCommitRef { + RepositoryCommitRef { reference: name } +} + + + +// THIS WITNESS DERIVES ITS OWN BOOLEAN FROM THE WALK, which is what the deleted +// `commit_is_ancestor_of` used to do for everyone. The module no longer offers it, because +// collapsing four outcomes into `false` erased the difference between "traced, and not an ancestor" +// and "could not establish anything" -- so each consumer now decides what a refusal means to IT. +// +// WHAT THIS CONSUMER DECIDES, stated rather than buried: only a completed trace can establish +// ancestry, so the three refusal arms answer false HERE. That is safe in these witnesses and not in +// general -- the fixtures below are well-formed by construction, so the refusal arms are unreachable +// from them, and the broken-chain, cycle and unknown-start states are asserted directly by their own +// witnesses rather than through this helper. A production consumer with damaged input must not reuse +// this reasoning. +fn ancestry_witness_traces_through( + candidate: RepositoryCommitRef, + descendant: RepositoryCommitRef, + history: RepositoryHistory, +) -> Bool { + match ancestry_walk( + start: descendant, + history: history, + ) { + AncestryTraced { chain: chain } => + fold(chain, init: false, f: fn(seen, c) { + if seen { true } else { repository_commit_ref_eq(left: c, right: candidate) } + }) + AncestryBrokenAt { commit: _, missing_parent: _ } => false + AncestryStartNotInHistory { start: _ } => false + AncestryRevisitsACommit { detected_from: _ } => false + } +} + +// THE MODULE NO LONGER OFFERS A ROOT READER, so this witness derives one -- which is what any real +// consumer would do. The deletion rests on consumer count, the same criterion that removed the +// Boolean ancestor predicate from this module: zero production consumers, and an arm with no +// producer is worse than absent because it gets cited as a capability. +fn ancestry_witness_reads_as_root(ancestry: CommitAncestry) -> Bool { + match ancestry { + RootCommit => true + DescendsFrom { parent: _ } => false + } +} + +// A linear history: c0 is the root, c1 descends from c0, c2 from c1. +fn ancestry_witness_linear() -> RepositoryHistory { + RepositoryHistory { steps: [ + AncestryStep { at: ancestry_witness_ref(name: "c0"), ancestry: RootCommit }, + AncestryStep { + at: ancestry_witness_ref(name: "c1"), + ancestry: DescendsFrom { parent: ancestry_witness_ref(name: "c0") }, + }, + AncestryStep { + at: ancestry_witness_ref(name: "c2"), + ancestry: DescendsFrom { parent: ancestry_witness_ref(name: "c1") }, + }, + ] } +} + + +// THE ROOT IS ITS OWN STATE. Asserted against the alternative this module refuses -- 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. Those are opposite facts: the first is every repository's +// ordinary first commit, the second is damage. +test fn the_root_commit_is_structurally_a_root_not_a_missing_parent() -> Bool { + ancestry_witness_reads_as_root(ancestry: RootCommit) + && !ancestry_witness_reads_as_root(ancestry: DescendsFrom { parent: ancestry_witness_ref(name: "c0") }) +} + +test fn a_linear_history_traces_from_the_start_back_to_the_root() -> Bool { + match ancestry_walk( + start: ancestry_witness_ref(name: "c2"), + history: ancestry_witness_linear(), + ) { + AncestryTraced { chain: chain } => + chain == [ + ancestry_witness_ref(name: "c2"), + ancestry_witness_ref(name: "c1"), + ancestry_witness_ref(name: "c0"), + ] + AncestryBrokenAt { commit: _, missing_parent: _ } => false + AncestryStartNotInHistory { start: _ } => false + AncestryRevisitsACommit { detected_from: _ } => false + } +} + +// A TRUNCATED HISTORY IS NOT A FINISHED ONE. c1 names c0 as its parent, but c0 is not in this +// history at all. Answering "traced" here would report a broken chain as a complete one -- the same +// shape as reporting an unreadable document as an empty one, which is the empty-observation narrow. +test fn a_parent_outside_the_history_is_broken_not_traced() -> Bool { + let history = RepositoryHistory { steps: [ + AncestryStep { + at: ancestry_witness_ref(name: "c1"), + ancestry: DescendsFrom { parent: ancestry_witness_ref(name: "c0") }, + }, + ] } + match ancestry_walk(start: ancestry_witness_ref(name: "c1"), history: history) { + AncestryBrokenAt { commit: c, missing_parent: p } => + repository_commit_ref_eq(left: c, right: ancestry_witness_ref(name: "c1")) + && repository_commit_ref_eq(left: p, right: ancestry_witness_ref(name: "c0")) + AncestryTraced { chain: _ } => false + AncestryStartNotInHistory { start: _ } => false + AncestryRevisitsACommit { detected_from: _ } => false + } +} + +// A CYCLE IS REFUSED, NOT TRUNCATED AND NOT LOOPED. DESIGN's substrate is bounded and forward, so a +// parent chain that returns to a commit it already visited must refuse. The RED is authorable here +// precisely because nothing constructs commits yet -- a fixture can state a cycle that no producer +// would, which is what makes this an executing wall rather than a decoration. +test fn a_cyclic_parent_chain_is_refused_rather_than_traced() -> Bool { + let history = RepositoryHistory { steps: [ + AncestryStep { + at: ancestry_witness_ref(name: "c0"), + ancestry: DescendsFrom { parent: ancestry_witness_ref(name: "c1") }, + }, + AncestryStep { + at: ancestry_witness_ref(name: "c1"), + ancestry: DescendsFrom { parent: ancestry_witness_ref(name: "c0") }, + }, + ] } + match ancestry_walk(start: ancestry_witness_ref(name: "c0"), history: history) { + AncestryRevisitsACommit { detected_from: d } => + repository_commit_ref_eq(left: d, right: ancestry_witness_ref(name: "c0")) + AncestryTraced { chain: _ } => false + AncestryStartNotInHistory { start: _ } => false + AncestryBrokenAt { commit: _, missing_parent: _ } => false + } +} + +// THE CLAIM THIS MODULE EXISTS FOR: ANCESTRY IS CARRIED, NOT RECOVERED FROM POSITION. +// +// Both commits are in the history, ADJACENT, in the order a log would list them -- and neither +// descends from the other, because both are stated roots. A model that read descent off position +// would call c0 an ancestor of c1 here. Nothing does, because the relation is asserted rather than +// inferred, and this witness is what would go red if someone later "optimised" the walk into an +// index comparison. +// TWO UNRELATED ROOTS, hoisted out of the witness below so its positive companion shares this exact +// fixture by construction rather than by an author remembering to copy it. +fn ancestry_witness_two_roots() -> RepositoryHistory { + RepositoryHistory { steps: [ + AncestryStep { at: ancestry_witness_ref(name: "c0"), ancestry: RootCommit }, + AncestryStep { at: ancestry_witness_ref(name: "c1"), ancestry: RootCommit }, + ] } +} + + +test fn adjacent_position_does_not_make_one_commit_an_ancestor_of_another() -> Bool { + !ancestry_witness_traces_through( + candidate: ancestry_witness_ref(name: "c0"), + descendant: ancestry_witness_ref(name: "c1"), + history: ancestry_witness_two_roots(), + ) +} + +// THE POSITIVE COMPANION FOR THE NEGATION ABOVE, OVER THE SAME FIXTURE. +// +// The negation is satisfied by a walk that FAILS for any reason, because the helper answers false on +// every refusal arm. The suite's other positive control does not rescue it: that control runs over +// the LINEAR fixture and never touches this one, so a failure that struck only this shape -- two +// records that are both roots, which is exactly the shape the revert fixture makes interesting -- +// would leave the negation green and nothing else would notice. +// +// So this asserts something this fixture must answer TRUE: a walk from c1, which is itself a root, +// completes and traces exactly itself. A selective failure on this shape now reddens here. +test fn the_two_root_fixture_still_traces_a_root_to_itself() -> Bool { + match ancestry_walk( + start: ancestry_witness_ref(name: "c1"), + history: ancestry_witness_two_roots(), + ) { + AncestryTraced { chain: chain } => length(chain) == 1 + AncestryBrokenAt { commit: _, missing_parent: _ } => false + AncestryStartNotInHistory { start: _ } => false + AncestryRevisitsACommit { detected_from: _ } => false + } +} + +// THE POSITIVE CONTROL FOR THE ABOVE, and without it a function that answered false for everything +// would satisfy it. A STATED ancestor is found, across two generations rather than one, so the claim +// is about tracing rather than about comparing a commit to its immediate parent. +test fn a_stated_ancestor_two_generations_back_is_found() -> Bool { + ancestry_witness_traces_through( + candidate: ancestry_witness_ref(name: "c0"), + descendant: ancestry_witness_ref(name: "c2"), + history: ancestry_witness_linear(), + ) +} + +// DESCENT HAS A DIRECTION. c2 is not an ancestor of c0, and a walk that ignored direction -- or a +// membership test over the whole history -- would answer true here. +test fn ancestry_has_a_direction_and_a_descendant_is_not_an_ancestor() -> Bool { + !ancestry_witness_traces_through( + candidate: ancestry_witness_ref(name: "c2"), + descendant: ancestry_witness_ref(name: "c0"), + history: ancestry_witness_linear(), + ) +} + +// AN UNKNOWN START IS THE CALLER'S FACT, NOT A TRUNCATED HISTORY, and this is the claim review 56102 +// asked for. Before the split, this answered AncestryBrokenAt with `missing_parent` set to the START +// -- a parent the input never supplied and that nothing in the history names. So the walk fabricated +// an identity, and a consumer acting on "commit X's parent Y is missing" would have gone looking for +// an object that was never claimed to exist. +// +// It asserts the START IDENTITY, not merely that some non-traced arm came back: an implementation +// that answered this arm for every input would satisfy a bare not-traced claim, and the two witnesses +// above -- a real trace and a real truncation -- are what stop that reading. +test fn a_start_outside_the_history_is_the_callers_fact_not_a_broken_chain() -> Bool { + let history = RepositoryHistory { steps: [ + AncestryStep { at: ancestry_witness_ref(name: "c1"), ancestry: RootCommit }, + ] } + match ancestry_walk(start: ancestry_witness_ref(name: "c9"), history: history) { + AncestryStartNotInHistory { start: s } => + repository_commit_ref_eq(left: s, right: ancestry_witness_ref(name: "c9")) + AncestryTraced { chain: _ } => false + AncestryBrokenAt { commit: _, missing_parent: _ } => false + AncestryRevisitsACommit { detected_from: _ } => false + } +} + +// THE REVERT: THE HISTORY THAT ROOT-KEYED ANCESTRY CANNOT REPRESENT. +// +// This is the discriminating control for the identity grain, and it is an ORDINARY revert rather +// than a corner case: r2 restores exactly the program r0 stored, so content addressing correctly +// gives r0 and r2 the SAME root. Under the previous ObjectId-keyed model, a lookup on that shared +// root answered with r0's `RootCommit` on first match, so a walk from r2 terminated immediately and +// reported r2 as a root -- r1 and the revert edge vanished. Not a refusal: a well-formed, confident, +// wrong answer. +// +// Row references make the two occurrences distinct, so the walk traces all three. This witness stays +// enrolled after it greens (DESIGN section 4b(4)): it is the control that keeps the grain from +// drifting back, and deleting it on the grounds that it now passes would recreate exactly the +// specification-without-execution the recut removed. +fn ancestry_witness_revert() -> RepositoryHistory { + RepositoryHistory { steps: [ + AncestryStep { at: ancestry_witness_ref(name: "r0"), ancestry: RootCommit }, + AncestryStep { + at: ancestry_witness_ref(name: "r1"), + ancestry: DescendsFrom { parent: ancestry_witness_ref(name: "r0") }, + }, + AncestryStep { + at: ancestry_witness_ref(name: "r2"), + ancestry: DescendsFrom { parent: ancestry_witness_ref(name: "r1") }, + }, + ] } +} + + +test fn a_revert_to_an_earlier_program_still_traces_every_record() -> Bool { + match ancestry_walk( + start: ancestry_witness_ref(name: "r2"), + history: ancestry_witness_revert(), + ) { + AncestryTraced { chain: chain } => length(chain) == 3 + AncestryBrokenAt { commit: _, missing_parent: _ } => false + AncestryStartNotInHistory { start: _ } => false + AncestryRevisitsACommit { detected_from: _ } => false + } +} + +// THE REVERTED-TO RECORD IS AN ANCESTOR OF THE REVERT, and is not the same record as it. +// +// Asserted separately from the chain length above because a walk could trace three rows and still +// have collapsed the two occurrences -- length alone does not establish that r0 and r2 stayed +// distinct. This is the assertion that a root-keyed model cannot satisfy at all. +test fn the_reverted_to_record_is_an_ancestor_and_not_the_revert_itself() -> Bool { + ancestry_witness_traces_through( + candidate: ancestry_witness_ref(name: "r0"), + descendant: ancestry_witness_ref(name: "r2"), + history: ancestry_witness_revert(), + ) && !repository_commit_ref_eq( + left: ancestry_witness_ref(name: "r0"), + right: ancestry_witness_ref(name: "r2"), + ) +} + +// AN EMPTY HISTORY CANNOT CONTAIN A CYCLE, AND THIS IS THE SPECIMEN THAT WAS SILENTLY WRONG. +// +// This was a live wrong ANSWER, not a missing check, and it predates the identity recut -- the same +// shape sits on the pre-recut head with only the type names different. The walk folded over the +// history to bound itself, so an EMPTY history folded zero times, `done` stayed false, and the +// accumulator's INITIAL value survived to be returned as the verdict: a revisit reported after +// observing nothing at all. A default doing duty as a conclusion. +// +// It is unrepresentable now rather than checked. There is no separate population to be empty while a +// lookup still answers: the walk asks the carrier for a row at the start, an empty carrier has none, +// and StartNotInHistory follows from the absence rather than from a guard someone added in front. +// +// It asserts the START IDENTITY rather than merely that the answer is not a cycle, because an +// implementation answering StartNotInHistory for every input would satisfy the weaker claim. The +// traced and cyclic witnesses above are the positive controls that stop that reading. Per DESIGN +// section 4b(4) this stays enrolled after it greens: it is the executing evidence that the state +// remains unwritable, and deleting it on the grounds that it now passes would recreate +// specification-without-execution one rung up. +test fn an_empty_history_is_not_a_cycle() -> Bool { + match ancestry_walk( + start: ancestry_witness_ref(name: "c0"), + history: RepositoryHistory { steps: [] }, + ) { + AncestryStartNotInHistory { start: s } => + repository_commit_ref_eq(left: s, right: ancestry_witness_ref(name: "c0")) + AncestryRevisitsACommit { detected_from: _ } => false + AncestryTraced { chain: _ } => false + AncestryBrokenAt { commit: _, missing_parent: _ } => false + } +} + +// THE POSITIVE CONTROL FOR THE ROW-EXISTENCE QUESTION ITSELF, over the same carrier the walk uses. +// Without it, a history_step_at that answered Absent for everything would satisfy the claim above +// while making every walk in this file return StartNotInHistory. +test fn a_row_that_is_present_is_found_by_the_same_lookup_the_walk_uses() -> Bool { + match history_step_at( + history: ancestry_witness_linear(), + at: ancestry_witness_ref(name: "c1"), + ) { + Present { value: step } => + repository_commit_ref_eq(left: step.at, right: ancestry_witness_ref(name: "c1")) + Absent => false + } +}