diff --git a/dag/gunbc/scm/repository_envelope.dag b/dag/gunbc/scm/repository_envelope.dag index b4a06048d92..76d66ef85bd 100644 --- a/dag/gunbc/scm/repository_envelope.dag +++ b/dag/gunbc/scm/repository_envelope.dag @@ -552,6 +552,101 @@ type RepositoryCommitMint // mint could produce a result and its supposed consumed source in one motion -- a self-consistent // repository whose provenance describes an event that could not have happened, and whose consumed // join would then refuse or admit on a record nothing ever integrated. +// It answers with the UNRESOLVABLE ANCHOR rather than a Bool, because the refusal has to name which +// reference could not be found and a Bool would force the caller to rediscover it. +fn integration_anchor_resolves( + repository: RepositoryEnvelope, + integration: CommitIntegration, +) -> RepositoryCommitRef? { + match integration { + NoSquashIntegration => none + SquashIntegrated { source: s } => + match commit_at_reference(commits: repository.commits, reference: s) { + Present { value: _ } => none + Absent => Present { value: s } + } + } +} + +// A ROOT THE STORE ITSELF JUST BRANDED HAS NOTHING LEFT TO RESOLVE, so this route cannot refuse for +// root resolution and its outcome type does not carry the arms. +// +// THE ROUTE EXISTS BECAUSE A CALLER CAN HOLD STRONGER EVIDENCE THAN `mint_repository_commit` +// ACCEPTS. That function takes a `CorpusManifestTarget`, which is a PROPOSITION about a locator, so +// its return type honestly carries the three ways resolving that proposition can fail. But a caller +// that has just received `CorpusManifestStored { store, manifest }` does not hold a proposition: it +// holds the `CorpusManifestObjectRef` THE STORE HANDED BACK, for an object that store demonstrably +// contains -- had the locator been absent or occupied by another kind, the store call would have +// returned its own refusal instead. Re-asking `find_corpus_manifest_record` there is asking a +// question that has already been answered, and then having to say something about an answer that +// cannot arrive. +// +// THE ALTERNATIVE THAT WAS BUILT FIRST AND REJECTED was for the caller to keep the generic route and +// TRANSLATE the three impossible root refusals into one coarser cause. That is worse than it looks. +// It fabricates a public arm no execution can produce, and it does so by REASONING ABOUT THE +// CALLER'S CONTEXT INSIDE A FUNCTION THAT CANNOT SEE IT -- a translator handed a +// `RepositoryCommitMint` has no evidence its argument came from the store one line earlier. And the +// three arms are not one fact even in the impossible case: a missing root would mean the object +// vanished, while a wrong-kind root would mean the locator now denotes something else, which +// contradicts a collision-aware insertion far more strongly. Collapsing them would erase the +// evidence needed to tell which invariant had failed. +// +// So the question is REMOVED rather than answered more elegantly, which is DESIGN section 5's +// construction over validation and section 4b's top rung: the invalid state has no constructor on +// this route. +// +// EVERY OTHER ADMISSION STILL RUNS. This is not a fast path around the mint's rules -- the allocator, +// the integration anchor's preexistence and the parent's existence are all still decided here, and +// `mint_repository_commit` now reaches them THROUGH this function rather than beside it, so there is +// one authority for those rules rather than two copies free to drift. +type BrandedRootMint + = BrandedRootMinted { repository: RepositoryEnvelope, reference: RepositoryCommitRef } + | BrandedRootAllocatorInvalid { next_ordinal: Int } + | BrandedRootParentMissing { parent: RepositoryCommitRef } + | BrandedRootIntegrationSourceMissing { source: RepositoryCommitRef } + +fn mint_commit_from_stored_manifest( + repository: RepositoryEnvelope, + root: CorpusManifestObjectRef, + message: String, + parent: RepositoryCommitRef?, + integration: CommitIntegration, +) -> BrandedRootMint { + if repository.commit_allocator.next_ordinal < 0 { + BrandedRootAllocatorInvalid { next_ordinal: repository.commit_allocator.next_ordinal } + } else { + match integration_anchor_resolves(repository: repository, integration: integration) { + Present { value: missing } => BrandedRootIntegrationSourceMissing { source: missing } + Absent => + match parent { + Present { value: parent_ref } => + match commit_at_reference(commits: repository.commits, reference: parent_ref) { + Absent => BrandedRootParentMissing { parent: parent_ref } + Present { value: _ } => + mint_repository_commit_admitted( + repository: repository, + root: root, + message: message, + ancestry: DescendsFrom { parent: parent_ref }, + integration: integration, + ) + } + Absent => + mint_repository_commit_admitted( + repository: repository, + root: root, + message: message, + ancestry: RootCommit, + integration: integration, + ) + } + } + } +} + +// THE GENERIC ROUTE RESOLVES THE ROOT AND THEN DELEGATES, so the admission rules live in exactly one +// place. A caller holding only a locator still gets the three root refusals, because for that caller +// they are reachable and real. fn mint_repository_commit( repository: RepositoryEnvelope, root: CorpusManifestTarget, @@ -572,63 +667,42 @@ fn mint_repository_commit( CorpusManifestIsSemanticNode { identity: r } => RepositoryCommitMintRootIsSemanticNode { root: r } CorpusManifestFound(record) => - match integration_anchor_resolves(repository: repository, integration: integration) { - Present { value: missing } => - RepositoryCommitMintIntegrationSourceMissing { source: missing } - Absent => - match parent { - Present { value: parent_ref } => - match commit_at_reference(commits: repository.commits, reference: parent_ref) { - Absent => RepositoryCommitMintParentMissing { parent: parent_ref } - Present { value: _ } => - mint_repository_commit_admitted( - repository: repository, - root: record.identity, - message: message, - ancestry: DescendsFrom { parent: parent_ref }, - integration: integration, - ) - } - Absent => - mint_repository_commit_admitted( - repository: repository, - root: record.identity, - message: message, - ancestry: RootCommit, - integration: integration, - ) - } - } + widen_branded_root_mint( + mint: mint_commit_from_stored_manifest( + repository: repository, + root: record.identity, + message: message, + parent: parent, + integration: integration, + ) + ) } } } -// It answers with the UNRESOLVABLE ANCHOR rather than a Bool, because the refusal has to name which -// reference could not be found and a Bool would force the caller to rediscover it. -fn integration_anchor_resolves( - repository: RepositoryEnvelope, - integration: CommitIntegration, -) -> RepositoryCommitRef? { - match integration { - NoSquashIntegration => none - SquashIntegrated { source: s } => - match commit_at_reference(commits: repository.commits, reference: s) { - Present { value: _ } => none - Absent => Present { value: s } - } +fn widen_branded_root_mint(mint: BrandedRootMint) -> RepositoryCommitMint { + match mint { + BrandedRootMinted { repository: r, reference: c } => + RepositoryCommitMinted { repository: r, reference: c } + BrandedRootAllocatorInvalid { next_ordinal: n } => + RepositoryCommitMintAllocatorInvalid { next_ordinal: n } + BrandedRootParentMissing { parent: p } => RepositoryCommitMintParentMissing { parent: p } + BrandedRootIntegrationSourceMissing { source: s } => + RepositoryCommitMintIntegrationSourceMissing { source: s } } } + fn mint_repository_commit_admitted( repository: RepositoryEnvelope, root: CorpusManifestObjectRef, message: String, ancestry: CommitAncestry, integration: CommitIntegration, -) -> RepositoryCommitMint { +) -> BrandedRootMint { let allocation = mint_id(alloc: repository.commit_allocator) let reference = RepositoryCommitRef { identity: allocation.id } - RepositoryCommitMinted { + BrandedRootMinted { repository: RepositoryEnvelope { store: repository.store, commits: concat(repository.commits, [RepositoryCommit { diff --git a/dag/gunbc/scm/squash_merge.dag b/dag/gunbc/scm/squash_merge.dag new file mode 100644 index 00000000000..2b38ad88bad --- /dev/null +++ b/dag/gunbc/scm/squash_merge.dag @@ -0,0 +1,282 @@ +module gunbc.scm.squash_merge + +// THE MERGE VERB, WHICH IS THE COMPOSITION AND NOTHING ELSE. +// +// gunbc.scm.merge_base decides WHICH BASE a squash is entitled to use, and refuses when there is +// none. gunbc.scm.manifest_merge decides EACH PATH against a base, and refuses when the two sides +// disagree irreconcilably. Both were built to refuse independently, and both did -- but nothing +// joined them, so there was no operation a caller could invoke to merge anything. This module is +// that join, and it deliberately introduces no new decision of its own: every refusal below is one +// of the two modules' refusals carried outward, or a store fact observed at the boundary. +// +// THE ORDER IS FORCED, NOT CHOSEN. The base must be derived BEFORE the manifests are read, because +// deriving a merged manifest against a base the repository is not entitled to use would construct +// the unsafe value and then discard it -- the same reason merge_base runs its consumed-source join +// before it looks for a common ancestor. A resurrection that is computed and then thrown away is +// still a resurrection that existed. +// +// THE REFUSALS ARE NOT FLATTENED INTO ONE "MERGE FAILED" ARM. An already-consumed source, a +// conflicting path, a missing manifest and an invalid allocator have four different remedies and +// four different principals to blame: the first is an unsupported operation on an intact repository, +// the second is a decision only a human can make, the third is a damaged store, the fourth is a +// corrupt envelope. Collapsing them is the absorbing fallback DESIGN section 5 names -- a widened +// arm destroys the signal that would rank the precise deficit for repair. + +import std.types { List, String } +import gunbc.scm.object_store { + ObjectId, + CorpusManifestRecord, + CorpusManifestEntry, + + CorpusManifestObjectRef, + store_corpus_manifest, + CorpusManifestStored, + CorpusManifestDuplicatePath, + CorpusManifestLocatorCollision, + find_corpus_manifest_record, + CorpusManifestFound, + CorpusManifestAbsent, + CorpusManifestIsSemanticNode, + CorpusManifestIsAuthoredSource, +} +import gunbc.scm.ancestry { RepositoryCommitRef } +import gunbc.scm.integration { SquashIntegrated } +import gunbc.scm.merge_base { + MergeBaseOutcome, + MergeBaseDerived, + MergeBaseSourceAlreadyConsumed, + MergeBaseHistoryUnwalkable, + MergeBaseHistoriesDisjoint, + merge_base, +} +import gunbc.scm.manifest_merge { + ManifestPathConflict, + ManifestMergeOutcome, + ManifestMerged, + ManifestConflicted, + merge_manifests, +} +import gunbc.scm.repository_envelope { + RepositoryEnvelope, + RepositoryCommit, + BrandedRootMint, + BrandedRootMinted, + BrandedRootAllocatorInvalid, + BrandedRootParentMissing, + BrandedRootIntegrationSourceMissing, + commit_at_reference, + mint_commit_from_stored_manifest, +} + +// WHICH SIDE'S ROOT FAILED TO RESOLVE IS PART OF THE FACT, NOT CONTEXT FOR A LOG LINE. Three commits +// are read here and all three can be damaged in the same three ways; an outcome naming only the +// locator would send an operator to diff a store against nine possibilities. The locator says WHAT +// is wrong and this says WHOSE. +type MergeSideName + = MergeBaseCommit + | MergeSourceCommit + | MergeTargetCommit + +// THE ARMS, AND WHY EACH IS ITS OWN ARM RATHER THAN A FIELD ON A SHARED ONE: +// +// SquashMergeBaseRefused carried out of merge_base UNCHANGED. Re-wording it here would +// fork the authority for what a missing base MEANS, and that +// wording is the operator's only handle on the remedy. +// SquashMergeConflicted the conflicts ARE the answer, not an error string. This is the +// one refusal whose remedy is a human decision, so it carries the +// complete population manifest_merge produced: a caller given a +// count could not act, and one given the first could not size the +// job. +// SquashMergeRoot* a commit names a manifest the store cannot produce. +// Distinguished by side AND by occupant kind, because "absent" is +// a missing object while "occupied by an authored source" is an +// identity collision or a corrupt row -- different damage, +// different investigation. +// SquashMergeCommitMissing a reference naming no commit. Reachable BEFORE any history is +// walked, so it is not a merge_base refusal and must not be +// reported as one. +// SquashMergeResult* the merged manifest would not store. Kept apart from the mint +// arms below because this happens before any commit is proposed: +// the corpus itself could not be written down. +// SquashMergeAllocatorInvalid a corrupt envelope. +// SquashMergeMintParentMissing a vanished target. +// SquashMergeMintIntegration... a vanished source. +// +// THERE IS NO ROOT-RESOLUTION ARM ON THIS ROUTE, AND THAT IS THE POINT. An earlier head carried one, +// folding the generic mint's three root refusals into a single SquashMergeMintRootUnresolvable on the +// reasoning that they all meant "the store did not keep what it just accepted". That reasoning was +// wrong twice. It fabricated a public cause no execution can produce; and it reasoned about THIS +// caller's context inside a translator that cannot see it -- handed a mint result, nothing tells you +// it came from the store one line earlier. Keeping the three arms distinct instead would have been +// honest about the cause and still dishonest about the DOMAIN, leaving three states in this type that +// no run of `squash_merge` can reach. +// +// The question is removed rather than answered: `store_corpus_manifest` hands back a +// CorpusManifestObjectRef for an object that store demonstrably contains, and +// `mint_commit_from_stored_manifest` consumes that brand instead of re-resolving a locator. Root +// refusal is unrepresentable here because there is nothing left to resolve (DESIGN section 4b rung 4). +type SquashMergeOutcome + = SquashMerged { repository: RepositoryEnvelope, reference: RepositoryCommitRef } + + | SquashMergeBaseRefused { refusal: MergeBaseOutcome } + + | SquashMergeConflicted { conflicts: List } + + | SquashMergeRootMissing { side: MergeSideName, root: ObjectId } + | SquashMergeRootIsAuthoredSource { side: MergeSideName, root: ObjectId } + | SquashMergeRootIsSemanticNode { side: MergeSideName, root: ObjectId } + + | SquashMergeCommitMissing { side: MergeSideName, reference: RepositoryCommitRef } + + | SquashMergeResultDuplicatePath { path: String } + | SquashMergeResultLocatorCollision { identity: ObjectId } + + | SquashMergeAllocatorInvalid + | SquashMergeMintParentMissing { parent: RepositoryCommitRef } + | SquashMergeMintIntegrationSourceMissing { source: RepositoryCommitRef } + +// READING ONE SIDE'S MANIFEST IS ONE FUNCTION, USED THREE TIMES. Writing the resolution inline per +// side would put one rule in three places and let the copies drift, which is the redundancy DESIGN +// section 2 names -- and it is exactly how the "which side" fact gets dropped from one of them. +type MergeSideManifest + = MergeSideManifestRead { record: CorpusManifestRecord } + | MergeSideManifestRefused { outcome: SquashMergeOutcome } + +fn read_side_manifest( + repository: RepositoryEnvelope, + side: MergeSideName, + reference: RepositoryCommitRef, +) -> MergeSideManifest { + match commit_at_reference(commits: repository.commits, reference: reference) { + Absent => + MergeSideManifestRefused { + outcome: SquashMergeCommitMissing { side: side, reference: reference }, + } + Present { value: commit } => + match find_corpus_manifest_record(store: repository.store, identity: commit.root.locator) { + CorpusManifestFound(record) => MergeSideManifestRead { record: record } + CorpusManifestAbsent { identity: r } => + MergeSideManifestRefused { outcome: SquashMergeRootMissing { side: side, root: r } } + CorpusManifestIsAuthoredSource { identity: r } => + MergeSideManifestRefused { + outcome: SquashMergeRootIsAuthoredSource { side: side, root: r }, + } + CorpusManifestIsSemanticNode { identity: r } => + MergeSideManifestRefused { + outcome: SquashMergeRootIsSemanticNode { side: side, root: r }, + } + } + } +} + +// THE MINT'S REFUSALS ARE CARRIED ONE-TO-ONE, AND NOW THERE ARE ONLY THREE TO CARRY. Nothing is +// translated, widened or merged here; the branded route's outcome type and this one have the same +// shape because they describe the same three ways a commit can fail to be minted once its root is +// settled. +fn carry_mint(mint: BrandedRootMint) -> SquashMergeOutcome { + match mint { + BrandedRootMinted { repository: r, reference: c } => + SquashMerged { repository: r, reference: c } + BrandedRootAllocatorInvalid { next_ordinal: _ } => SquashMergeAllocatorInvalid + BrandedRootParentMissing { parent: p } => SquashMergeMintParentMissing { parent: p } + BrandedRootIntegrationSourceMissing { source: s } => + SquashMergeMintIntegrationSourceMissing { source: s } + } +} + +// THE PARENT IS THE TARGET AND ONLY THE TARGET. A squash records ONE lineage edge, and the source is +// recorded as CONSUMED rather than as a second parent -- which is what makes the operator's "kill dev +// history, never rebase" workflow the shape of the model instead of a convention layered over it. The +// source is not lost: the SquashIntegrated below is precisely the record merge_base reads to refuse +// the second merge. +// +// THE COMMIT IS MINTED ONTO THE STORE THE MANIFEST WAS WRITTEN INTO, never onto the caller's +// repository value. Minting against the pre-store envelope would produce a commit whose root names a +// manifest that repository does not contain -- a dangling root written by the one operation that +// knows better. +fn commit_merged_manifest( + repository: RepositoryEnvelope, + target: RepositoryCommitRef, + source: RepositoryCommitRef, + message: String, + entries: List, +) -> SquashMergeOutcome { + match store_corpus_manifest(store: repository.store, entries: entries) { + CorpusManifestDuplicatePath { path: p } => + SquashMergeResultDuplicatePath { path: p as String } + CorpusManifestLocatorCollision { identity: i, existing: _, incoming: _ } => + SquashMergeResultLocatorCollision { identity: i } + CorpusManifestStored { store: stored, manifest: reference } => + carry_mint( + mint: mint_commit_from_stored_manifest( + repository: RepositoryEnvelope { + store: stored, + commits: repository.commits, + commit_allocator: repository.commit_allocator, + checked_out: repository.checked_out, + staged: repository.staged, + }, + root: reference, + message: message, + parent: Present { value: target }, + integration: SquashIntegrated { source: source }, + ) + ) + } +} + +// THE VERB. Base, then manifests, then decision, then commit -- and each step's refusal leaves +// immediately, so no later step ever runs against a value an earlier step declined to vouch for. +fn squash_merge( + repository: RepositoryEnvelope, + target: RepositoryCommitRef, + source: RepositoryCommitRef, + message: String, +) -> SquashMergeOutcome { + match merge_base(commits: repository.commits, target: target, source: source) { + MergeBaseSourceAlreadyConsumed { recorded_at: a, consumed: b, source: c } => + SquashMergeBaseRefused { + refusal: MergeBaseSourceAlreadyConsumed { recorded_at: a, consumed: b, source: c }, + } + MergeBaseHistoryUnwalkable { side: s, walk: w } => + SquashMergeBaseRefused { refusal: MergeBaseHistoryUnwalkable { side: s, walk: w } } + MergeBaseHistoriesDisjoint { target: t, source: s } => + SquashMergeBaseRefused { refusal: MergeBaseHistoriesDisjoint { target: t, source: s } } + MergeBaseDerived { base: base_ref } => + match read_side_manifest(repository: repository, side: MergeBaseCommit, reference: base_ref) { + MergeSideManifestRefused { outcome: o } => o + MergeSideManifestRead { record: base_manifest } => + match read_side_manifest( + repository: repository, + side: MergeSourceCommit, + reference: source, + ) { + MergeSideManifestRefused { outcome: o } => o + MergeSideManifestRead { record: source_manifest } => + match read_side_manifest( + repository: repository, + side: MergeTargetCommit, + reference: target, + ) { + MergeSideManifestRefused { outcome: o } => o + MergeSideManifestRead { record: target_manifest } => + match merge_manifests( + base: base_manifest, + source: source_manifest, + target: target_manifest, + ) { + ManifestConflicted { conflicts: c } => SquashMergeConflicted { conflicts: c } + ManifestMerged { entries: e } => + commit_merged_manifest( + repository: repository, + target: target, + source: source, + message: message, + entries: e, + ) + } + } + } + } + } +} diff --git a/dag/test/claim/scm/scm_merge_base_witness_test.dag b/dag/test/claim/scm/scm_merge_base_witness_test.dag index 7a2b154b6e9..e8518592484 100644 --- a/dag/test/claim/scm/scm_merge_base_witness_test.dag +++ b/dag/test/claim/scm/scm_merge_base_witness_test.dag @@ -59,8 +59,9 @@ import gunbc.scm.repository_envelope { RepositoryDecoded, RepositoryDecodeRefused, } -import gunbc.scm.staging { - StagedEntries, StagedEntriesRefused, staged_entries, +import test.fixture.scm_repository_builder { + MbBuild, MbBuilt, MbSetupFailed, + mb_start, mb_stage, mb_commit, mb_at, mb_head, mb_root_of, } import gunbc.scm.merge_base { MergeBaseOutcome, @@ -75,147 +76,6 @@ import gunbc.scm.merge_base { data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -// THE FIXTURE IS FALLIBLE FOR THE REASON THE STAGING FIXTURE'S HEADER GIVES AND THIS FILE INHERITS -// RATHER THAN RESTATES: a setup step that failed must stop the claim, never hand it a substitute -// specimen. Here the hazard is sharper than usual, because a fixture that silently declined to record -// an integration would leave a repository in which the consumed join CORRECTLY finds nothing -- and -// the refusal control would go green while asserting the opposite of what it claims to test. -type MbBuild - = MbBuilt { repository: RepositoryEnvelope, at: RepositoryCommitRef? } - | MbSetupFailed - -fn mb_start() -> MbBuild { - MbBuilt { - repository: RepositoryEnvelope { - store: empty_store(), - commits: [], - commit_allocator: MintedIdAllocator { next_ordinal: 0 }, - checked_out: none, - staged: none, - }, - at: none, - } -} - -// The manifest is built from the CURRENTLY STAGED entries so a later stage of a second path keeps the -// first, which is what makes S2 a content-descendant of S1 rather than a replacement of it. -fn mb_stage(build: MbBuild, path: String, text: String) -> MbBuild { - match build { - MbSetupFailed => MbSetupFailed - MbBuilt { repository: repository, at: at } => - match staged_entries(repository: repository) { - StagedEntriesRefused { cause: _ } => MbSetupFailed - StagedEntries { entries: entries } => - match store_authored_source(store: repository.store, text: text) { - SourceLocatorCollision { identity: _, existing: _, incoming: _ } => MbSetupFailed - SourceStored { store: with_source, source: source } => - match store_corpus_manifest( - store: with_source, - entries: concat( - filter(entries, fn(e) { (e.path as String) != path }), - [CorpusManifestEntry { - path: path as NonEmptyStr, - source: AuthoredSourceTarget { locator: source.locator }, - }], - ), - ) { - CorpusManifestDuplicatePath { path: _ } => MbSetupFailed - CorpusManifestLocatorCollision { identity: _, existing: _, incoming: _ } => MbSetupFailed - CorpusManifestStored { store: with_manifest, manifest: manifest } => - MbBuilt { - repository: RepositoryEnvelope { - store: with_manifest, - commits: repository.commits, - commit_allocator: repository.commit_allocator, - checked_out: repository.checked_out, - staged: Present { value: manifest }, - }, - at: at, - } - } - } - } - } -} - -fn mb_commit(build: MbBuild, message: String, integration: CommitIntegration) -> MbBuild { - match build { - MbSetupFailed => MbSetupFailed - MbBuilt { repository: repository, at: at } => - match repository.staged { - Absent => MbSetupFailed - Present { value: candidate } => - match mint_repository_commit( - repository: repository, - root: CorpusManifestTarget { locator: candidate.locator }, - message: message, - parent: at, - integration: integration, - ) { - RepositoryCommitMinted { repository: minted, reference: reference } => - MbBuilt { - repository: RepositoryEnvelope { - store: minted.store, - commits: minted.commits, - commit_allocator: minted.commit_allocator, - checked_out: Present { value: reference }, - staged: Present { value: candidate }, - }, - at: Present { value: reference }, - } - RepositoryCommitMintAllocatorInvalid { next_ordinal: _ } => MbSetupFailed - RepositoryCommitMintRootMissing { root: _ } => MbSetupFailed - RepositoryCommitMintRootIsAuthoredSource { root: _ } => MbSetupFailed - RepositoryCommitMintRootIsSemanticNode { root: _ } => MbSetupFailed - RepositoryCommitMintParentMissing { parent: _ } => MbSetupFailed - RepositoryCommitMintIntegrationSourceMissing { source: _ } => MbSetupFailed - } - } - } -} - -// MOVING THE CURSOR AND THE STAGE TOGETHER, because a branch point is both. Re-pointing `at` without -// re-pointing the stage would leave the next commit's content derived from the OTHER lineage's tip, -// which would build a fixture nobody could reach and quietly change what the claims are about. -fn mb_at(build: MbBuild, target: RepositoryCommitRef) -> MbBuild { - match build { - MbSetupFailed => MbSetupFailed - MbBuilt { repository: repository, at: _ } => - match commit_at_reference(commits: repository.commits, reference: target) { - Absent => MbSetupFailed - Present { value: c } => - MbBuilt { - repository: RepositoryEnvelope { - store: repository.store, - commits: repository.commits, - commit_allocator: repository.commit_allocator, - checked_out: Present { value: target }, - staged: Present { value: c.root }, - }, - at: Present { value: target }, - } - } - } -} - -fn mb_head(build: MbBuild) -> RepositoryCommitRef? { - match build { - MbSetupFailed => none - MbBuilt { repository: _, at: at } => at - } -} - -fn mb_root_of(build: MbBuild, reference: RepositoryCommitRef) -> ObjectId? { - match build { - MbSetupFailed => none - MbBuilt { repository: repository, at: _ } => - match commit_at_reference(commits: repository.commits, reference: reference) { - Absent => none - Present { value: c } => Present { value: c.root.locator } - } - } -} - // ONE SCENE, BUILT ONCE, ASSERTED MANY TIMES. Every claim below reads the same construction because // the discriminating facts are RELATIONS BETWEEN its commits -- that I1's root equals S1's while S2's // does not, that both share C0 with the target -- and a per-claim rebuild would let those relations diff --git a/dag/test/claim/scm/scm_squash_merge_witness_test.dag b/dag/test/claim/scm/scm_squash_merge_witness_test.dag new file mode 100644 index 00000000000..0da0438ee96 --- /dev/null +++ b/dag/test/claim/scm/scm_squash_merge_witness_test.dag @@ -0,0 +1,371 @@ +module test.claim.scm_squash_merge_witness + +// CONTROLS FOR THE MERGE VERB, WRITTEN TO WHAT A COMPOSITION GETS WRONG. +// +// THE TWO HALVES ARE ALREADY CONTROLLED ELSEWHERE and are not re-asserted here: merge_base's +// resurrection refusal has its own witness, and manifest_merge's four-line law has its own. What is +// unproven until this file exists is that the JOIN preserves them -- that the verb actually consults +// the base derivation rather than merging against a convenient commit, that a conflict stops the +// commit rather than being counted and passed over, and that the squash records the source as +// consumed so the SECOND merge is refused by the first one's receipt. +// +// THE FAILURE THIS SURFACE PRODUCES IS A SUCCESSFUL MERGE. Every wrong composition below returns a +// well-formed repository with a well-formed new commit; none of them throws. So no claim here +// asserts "a merge happened" -- each states the mutation it refutes. + +import std.types { Bool, String, List, Int, NonEmptyStr } +import std.minted_identity { MintedId, MintedIdAllocator } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import gunbc.scm.object_store { ObjectId, object_id_eq } +import gunbc.scm.integration { NoSquashIntegration, SquashIntegrated } +import gunbc.scm.ancestry { RepositoryCommitRef, RootCommit, DescendsFrom, repository_commit_ref_eq } +import gunbc.scm.repository_envelope { RepositoryEnvelope, commit_at_reference } +import gunbc.scm.staging { StagedEntries, StagedEntriesRefused, staged_entries } +import gunbc.scm.merge_base { + MergeBaseOutcome, MergeBaseDerived, MergeBaseSourceAlreadyConsumed, + MergeBaseHistoryUnwalkable, MergeBaseHistoriesDisjoint, +} +import gunbc.scm.manifest_merge { ManifestPathConflict, PathState, PathAbsent, PathPresent } +import gunbc.scm.squash_merge { + SquashMergeOutcome, + SquashMerged, + SquashMergeBaseRefused, + SquashMergeConflicted, + SquashMergeRootMissing, + SquashMergeRootIsAuthoredSource, + SquashMergeRootIsSemanticNode, + SquashMergeCommitMissing, + SquashMergeResultDuplicatePath, + SquashMergeResultLocatorCollision, + SquashMergeAllocatorInvalid, + SquashMergeMintParentMissing, + SquashMergeMintIntegrationSourceMissing, + MergeSideName, MergeBaseCommit, MergeSourceCommit, MergeTargetCommit, + squash_merge, +} +import test.fixture.scm_repository_builder { + MbBuild, MbBuilt, MbSetupFailed, + mb_start, mb_stage, mb_commit, mb_at, mb_head, mb_root_of, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE SCENE IS A DIVERGENCE, NOT A FAST-FORWARD, and the distinction is load-bearing. If the target +// had not moved since the base, taking the source's manifest wholesale would be correct, and every +// claim below would pass against an implementation that ignores the base entirely. Both sides +// therefore move, and they move on DIFFERENT paths so the merge is mergeable: +// +// C0 a = A0 the shared base +// T1 a = A0, t = T0 parent C0 the target adds its own path +// S1 a = A0, s = S0 parent C0 the source adds a different path +// +// The correct merge of T1 and S1 holds all three paths. An implementation that took the source's +// manifest as the answer loses `t`; one that took the target's loses `s`; one that merged against +// the wrong base can lose either. +type Scene + = Scene { build: MbBuild, base: RepositoryCommitRef, target: RepositoryCommitRef, source: RepositoryCommitRef } + | SceneUnavailable + +fn scene() -> Scene { + let c0 = mb_commit( + build: mb_stage(build: mb_start(), path: "a", text: "A0"), + message: "base", + integration: NoSquashIntegration, + ) + match mb_head(build: c0) { + Absent => SceneUnavailable + Present { value: base } => + let t1 = mb_commit( + build: mb_stage(build: c0, path: "t", text: "T0"), + message: "target", + integration: NoSquashIntegration, + ) + match mb_head(build: t1) { + Absent => SceneUnavailable + Present { value: target } => + let s1 = mb_commit( + build: mb_stage(build: mb_at(build: t1, target: base), path: "s", text: "S0"), + message: "source", + integration: NoSquashIntegration, + ) + match mb_head(build: s1) { + Absent => SceneUnavailable + Present { value: source } => + Scene { build: s1, base: base, target: target, source: source } + } + } + } +} + +fn scene_repository(s: Scene) -> RepositoryEnvelope? { + match s { + SceneUnavailable => none + Scene { build: b, base: _, target: _, source: _ } => + match b { + MbSetupFailed => none + MbBuilt { repository: r, at: _ } => Present { value: r } + } + } +} + +// READING THE COMMITTED CORPUS BACK OUT THROUGH THE ORDINARY STAGING READER rather than through the +// outcome value. A claim that inspected the entries it had just handed to the merge would assert +// that the merge returned its own argument; this asserts what the REPOSITORY now contains. +fn paths_after(repository: RepositoryEnvelope, at: RepositoryCommitRef) -> List { + match commit_at_reference(commits: repository.commits, reference: at) { + Absent => [] + Present { value: c } => + match staged_entries( + repository: RepositoryEnvelope { + store: repository.store, + commits: repository.commits, + commit_allocator: repository.commit_allocator, + checked_out: repository.checked_out, + staged: Present { value: c.root }, + } + ) { + StagedEntriesRefused { cause: _ } => [] + StagedEntries { entries: entries } => map(entries, e => e.path as String) + } + } +} + +// A CONFLICT WHOSE THREE SIDES ARE ALL PRESENT is what a modify-versus-modify looks like, and the +// claim states it rather than asserting a count, because a conflict reported with a fabricated +// absence would still be one conflict on one path. +fn state_is_present(state: PathState) -> Bool { + match state { + PathAbsent => false + PathPresent { source: _ } => true + } +} + +fn conflict_is_all_present(conflict: ManifestPathConflict, path: String) -> Bool { + (conflict.path as String) == path + && state_is_present(state: conflict.base) + && state_is_present(state: conflict.source) + && state_is_present(state: conflict.target) +} + +fn holds(paths: List, path: String) -> Bool { + fold(paths, init: false, f: fn(found, p) { found || p == path }) +} + +// THE MERGE TAKES BOTH SIDES' WORK, WHICH IS THE ONE THING A WRONG COMPOSITION CANNOT FAKE. Taking +// either side's manifest wholesale produces a valid repository and a valid commit; it just silently +// drops the other side's file. +test fn scm_sm_a_merge_carries_both_sides_paths() -> Bool { + match scene() { + SceneUnavailable => false + Scene { build: _, base: _, target: target, source: source } => + match scene_repository(s: scene()) { + Absent => false + Present { value: repository } => + match squash_merge( + repository: repository, + target: target, + source: source, + message: "merge", + ) { + SquashMerged { repository: merged, reference: at } => + let paths = paths_after(repository: merged, at: at) + count(paths) == 3 + && holds(paths: paths, path: "a") + && holds(paths: paths, path: "t") + && holds(paths: paths, path: "s") + SquashMergeConflicted { conflicts: _ } => false + SquashMergeBaseRefused { refusal: _ } => false + SquashMergeRootMissing { side: _, root: _ } => false + SquashMergeRootIsAuthoredSource { side: _, root: _ } => false + SquashMergeRootIsSemanticNode { side: _, root: _ } => false + SquashMergeCommitMissing { side: _, reference: _ } => false + SquashMergeResultDuplicatePath { path: _ } => false + SquashMergeResultLocatorCollision { identity: _ } => false + SquashMergeAllocatorInvalid => false + SquashMergeMintParentMissing { parent: _ } => false + SquashMergeMintIntegrationSourceMissing { source: _ } => false + } + } + } +} + +// THE RESULT'S PARENT IS THE TARGET AND ITS INTEGRATION NAMES THE SOURCE. This is the operator's +// squash workflow as a structural fact rather than a convention: ONE lineage edge, and the source +// recorded as consumed rather than as a second parent. An implementation that parented the merge on +// the SOURCE also produces a valid repository holding all three paths, so the previous claim does +// not separate it from this one. +test fn scm_sm_the_result_parents_the_target_and_records_the_source_consumed() -> Bool { + match scene() { + SceneUnavailable => false + Scene { build: _, base: _, target: target, source: source } => + match scene_repository(s: scene()) { + Absent => false + Present { value: repository } => + match squash_merge( + repository: repository, + target: target, + source: source, + message: "merge", + ) { + SquashMerged { repository: merged, reference: at } => + match commit_at_reference(commits: merged.commits, reference: at) { + Absent => false + Present { value: c } => + match c.integration { + NoSquashIntegration => false + SquashIntegrated { source: recorded } => + repository_commit_ref_eq(left: recorded, right: source) + && match c.ancestry { + RootCommit => false + DescendsFrom { parent: p } => + repository_commit_ref_eq(left: p, right: target) + } + } + } + SquashMergeConflicted { conflicts: _ } => false + SquashMergeBaseRefused { refusal: _ } => false + SquashMergeRootMissing { side: _, root: _ } => false + SquashMergeRootIsAuthoredSource { side: _, root: _ } => false + SquashMergeRootIsSemanticNode { side: _, root: _ } => false + SquashMergeCommitMissing { side: _, reference: _ } => false + SquashMergeResultDuplicatePath { path: _ } => false + SquashMergeResultLocatorCollision { identity: _ } => false + SquashMergeAllocatorInvalid => false + SquashMergeMintParentMissing { parent: _ } => false + SquashMergeMintIntegrationSourceMissing { source: _ } => false + } + } + } +} + +// MERGING THE SAME SOURCE TWICE IS REFUSED BY THE FIRST MERGE'S OWN RECEIPT, and it is refused +// THROUGH THE BASE DERIVATION rather than by a check this module added. That is what makes the +// composition load-bearing: a verb that derived its base by any other route would merge again +// happily, and the resurrection merge_base exists to prevent would arrive through the verb. +test fn scm_sm_merging_the_same_source_twice_is_refused_by_the_first_merges_receipt() -> Bool { + match scene() { + SceneUnavailable => false + Scene { build: _, base: _, target: target, source: source } => + match scene_repository(s: scene()) { + Absent => false + Present { value: repository } => + match squash_merge( + repository: repository, + target: target, + source: source, + message: "merge", + ) { + SquashMerged { repository: merged, reference: at } => + match squash_merge( + repository: merged, + target: at, + source: source, + message: "merge again", + ) { + SquashMergeBaseRefused { refusal: r } => + match r { + MergeBaseSourceAlreadyConsumed { recorded_at: rec, consumed: cons, source: _ } => + repository_commit_ref_eq(left: rec, right: at) + && repository_commit_ref_eq(left: cons, right: source) + MergeBaseDerived { base: _ } => false + MergeBaseHistoryUnwalkable { side: _, walk: _ } => false + MergeBaseHistoriesDisjoint { target: _, source: _ } => false + } + SquashMerged { repository: _, reference: _ } => false + SquashMergeConflicted { conflicts: _ } => false + SquashMergeRootMissing { side: _, root: _ } => false + SquashMergeRootIsAuthoredSource { side: _, root: _ } => false + SquashMergeRootIsSemanticNode { side: _, root: _ } => false + SquashMergeCommitMissing { side: _, reference: _ } => false + SquashMergeResultDuplicatePath { path: _ } => false + SquashMergeResultLocatorCollision { identity: _ } => false + SquashMergeAllocatorInvalid => false + SquashMergeMintParentMissing { parent: _ } => false + SquashMergeMintIntegrationSourceMissing { source: _ } => false + } + SquashMergeConflicted { conflicts: _ } => false + SquashMergeBaseRefused { refusal: _ } => false + SquashMergeRootMissing { side: _, root: _ } => false + SquashMergeRootIsAuthoredSource { side: _, root: _ } => false + SquashMergeRootIsSemanticNode { side: _, root: _ } => false + SquashMergeCommitMissing { side: _, reference: _ } => false + SquashMergeResultDuplicatePath { path: _ } => false + SquashMergeResultLocatorCollision { identity: _ } => false + SquashMergeAllocatorInvalid => false + SquashMergeMintParentMissing { parent: _ } => false + SquashMergeMintIntegrationSourceMissing { source: _ } => false + } + } + } +} + +// A CONFLICT STOPS THE LINE. The scene is re-cut so both sides modify the SAME path to different +// content, which the four-line law refuses. What this claim adds over manifest_merge's own controls +// is that the refusal REACHES THE REPOSITORY: no commit is minted, the conflict population arrives +// intact, and the commit list is unchanged -- a verb that counted the conflicts and committed the +// mergeable remainder would produce a corpus neither author wrote. +test fn scm_sm_a_conflicting_path_mints_no_commit() -> Bool { + let c0 = mb_commit( + build: mb_stage(build: mb_start(), path: "a", text: "A0"), + message: "base", + integration: NoSquashIntegration, + ) + match mb_head(build: c0) { + Absent => false + Present { value: base } => + let t1 = mb_commit( + build: mb_stage(build: c0, path: "a", text: "TARGET"), + message: "target", + integration: NoSquashIntegration, + ) + match mb_head(build: t1) { + Absent => false + Present { value: target } => + let s1 = mb_commit( + build: mb_stage(build: mb_at(build: t1, target: base), path: "a", text: "SOURCE"), + message: "source", + integration: NoSquashIntegration, + ) + match mb_head(build: s1) { + Absent => false + Present { value: source } => + match s1 { + MbSetupFailed => false + MbBuilt { repository: repository, at: _ } => + let before = count(repository.commits) + match squash_merge( + repository: repository, + target: target, + source: source, + message: "merge", + ) { + SquashMergeConflicted { conflicts: conflicts } => + count(conflicts) == 1 + && count(repository.commits) == before + && fold(conflicts, init: false, f: fn(seen, c) { + seen || conflict_is_all_present(conflict: c, path: "a") + }) + SquashMerged { repository: _, reference: _ } => false + SquashMergeBaseRefused { refusal: _ } => false + SquashMergeRootMissing { side: _, root: _ } => false + SquashMergeRootIsAuthoredSource { side: _, root: _ } => false + SquashMergeRootIsSemanticNode { side: _, root: _ } => false + SquashMergeCommitMissing { side: _, reference: _ } => false + SquashMergeResultDuplicatePath { path: _ } => false + SquashMergeResultLocatorCollision { identity: _ } => false + SquashMergeAllocatorInvalid => false + SquashMergeMintParentMissing { parent: _ } => false + SquashMergeMintIntegrationSourceMissing { source: _ } => false + } + } + } + } + } +} + +test fn scm_squash_merge_witnesses_hold() -> Bool { + scm_sm_a_merge_carries_both_sides_paths() + && scm_sm_the_result_parents_the_target_and_records_the_source_consumed() + && scm_sm_merging_the_same_source_twice_is_refused_by_the_first_merges_receipt() + && scm_sm_a_conflicting_path_mints_no_commit() +} diff --git a/dag/test/fixture/scm_repository_builder.dag b/dag/test/fixture/scm_repository_builder.dag new file mode 100644 index 00000000000..930729c044b --- /dev/null +++ b/dag/test/fixture/scm_repository_builder.dag @@ -0,0 +1,182 @@ +module test.fixture.scm_repository_builder + +// BUILDING A REPOSITORY BY THE OPERATIONS THAT ACTUALLY BUILD ONE, FOR EVERY CLAIM THAT NEEDS ONE. +// +// This was authored inside the merge_base witness and then needed verbatim by the squash_merge +// witness. Copying it would have put one construction rule in two files and let them drift -- and the +// drift would be invisible, because each copy would go on passing its own claims while the two +// fixtures quietly stopped describing the same repository. That is the redundancy DESIGN section 2 +// names, so the builder moved here and both witnesses read it. +// +// NOTHING HERE FORGES A COMMIT. Every row is produced by store_authored_source, +// store_corpus_manifest and mint_repository_commit, so a fixture cannot construct a repository the +// real operations could not produce -- which is what makes a claim over one of these scenes evidence +// about the product rather than about the fixture. + +import std.types { Bool, String, List, Int, NonEmptyStr } +import std.minted_identity { MintedId, MintedIdAllocator } +import gunbc.scm.object_store { + ObjectStore, ObjectId, empty_store, + CorpusManifestEntry, CorpusManifestObjectRef, CorpusManifestTarget, + CorpusManifestStored, CorpusManifestDuplicatePath, CorpusManifestLocatorCollision, + store_corpus_manifest, + SourceStored, SourceLocatorCollision, store_authored_source, + AuthoredSourceTarget, +} +import gunbc.scm.integration { CommitIntegration } +import gunbc.scm.ancestry { RepositoryCommitRef } +import gunbc.scm.repository_envelope { + RepositoryEnvelope, + RepositoryCommit, + RepositoryCommitMinted, + RepositoryCommitMintAllocatorInvalid, + RepositoryCommitMintRootIsAuthoredSource, + RepositoryCommitMintRootIsSemanticNode, + RepositoryCommitMintRootMissing, + RepositoryCommitMintParentMissing, + RepositoryCommitMintIntegrationSourceMissing, + mint_repository_commit, + commit_at_reference, +} +import gunbc.scm.staging { StagedEntries, StagedEntriesRefused, staged_entries } + +// THE FIXTURE IS FALLIBLE FOR THE REASON THE STAGING FIXTURE'S HEADER GIVES AND THIS FILE INHERITS +// RATHER THAN RESTATES: a setup step that failed must stop the claim, never hand it a substitute +// specimen. Here the hazard is sharper than usual, because a fixture that silently declined to record +// an integration would leave a repository in which the consumed join CORRECTLY finds nothing -- and +// the refusal control would go green while asserting the opposite of what it claims to test. +type MbBuild + = MbBuilt { repository: RepositoryEnvelope, at: RepositoryCommitRef? } + | MbSetupFailed + +fn mb_start() -> MbBuild { + MbBuilt { + repository: RepositoryEnvelope { + store: empty_store(), + commits: [], + commit_allocator: MintedIdAllocator { next_ordinal: 0 }, + checked_out: none, + staged: none, + }, + at: none, + } +} + +// The manifest is built from the CURRENTLY STAGED entries so a later stage of a second path keeps the +// first, which is what makes S2 a content-descendant of S1 rather than a replacement of it. +fn mb_stage(build: MbBuild, path: String, text: String) -> MbBuild { + match build { + MbSetupFailed => MbSetupFailed + MbBuilt { repository: repository, at: at } => + match staged_entries(repository: repository) { + StagedEntriesRefused { cause: _ } => MbSetupFailed + StagedEntries { entries: entries } => + match store_authored_source(store: repository.store, text: text) { + SourceLocatorCollision { identity: _, existing: _, incoming: _ } => MbSetupFailed + SourceStored { store: with_source, source: source } => + match store_corpus_manifest( + store: with_source, + entries: concat( + filter(entries, fn(e) { (e.path as String) != path }), + [CorpusManifestEntry { + path: path as NonEmptyStr, + source: AuthoredSourceTarget { locator: source.locator }, + }], + ), + ) { + CorpusManifestDuplicatePath { path: _ } => MbSetupFailed + CorpusManifestLocatorCollision { identity: _, existing: _, incoming: _ } => MbSetupFailed + CorpusManifestStored { store: with_manifest, manifest: manifest } => + MbBuilt { + repository: RepositoryEnvelope { + store: with_manifest, + commits: repository.commits, + commit_allocator: repository.commit_allocator, + checked_out: repository.checked_out, + staged: Present { value: manifest }, + }, + at: at, + } + } + } + } + } +} + +fn mb_commit(build: MbBuild, message: String, integration: CommitIntegration) -> MbBuild { + match build { + MbSetupFailed => MbSetupFailed + MbBuilt { repository: repository, at: at } => + match repository.staged { + Absent => MbSetupFailed + Present { value: candidate } => + match mint_repository_commit( + repository: repository, + root: CorpusManifestTarget { locator: candidate.locator }, + message: message, + parent: at, + integration: integration, + ) { + RepositoryCommitMinted { repository: minted, reference: reference } => + MbBuilt { + repository: RepositoryEnvelope { + store: minted.store, + commits: minted.commits, + commit_allocator: minted.commit_allocator, + checked_out: Present { value: reference }, + staged: Present { value: candidate }, + }, + at: Present { value: reference }, + } + RepositoryCommitMintAllocatorInvalid { next_ordinal: _ } => MbSetupFailed + RepositoryCommitMintRootMissing { root: _ } => MbSetupFailed + RepositoryCommitMintRootIsAuthoredSource { root: _ } => MbSetupFailed + RepositoryCommitMintRootIsSemanticNode { root: _ } => MbSetupFailed + RepositoryCommitMintParentMissing { parent: _ } => MbSetupFailed + RepositoryCommitMintIntegrationSourceMissing { source: _ } => MbSetupFailed + } + } + } +} + +// MOVING THE CURSOR AND THE STAGE TOGETHER, because a branch point is both. Re-pointing `at` without +// re-pointing the stage would leave the next commit's content derived from the OTHER lineage's tip, +// which would build a fixture nobody could reach and quietly change what the claims are about. +fn mb_at(build: MbBuild, target: RepositoryCommitRef) -> MbBuild { + match build { + MbSetupFailed => MbSetupFailed + MbBuilt { repository: repository, at: _ } => + match commit_at_reference(commits: repository.commits, reference: target) { + Absent => MbSetupFailed + Present { value: c } => + MbBuilt { + repository: RepositoryEnvelope { + store: repository.store, + commits: repository.commits, + commit_allocator: repository.commit_allocator, + checked_out: Present { value: target }, + staged: Present { value: c.root }, + }, + at: Present { value: target }, + } + } + } +} + +fn mb_head(build: MbBuild) -> RepositoryCommitRef? { + match build { + MbSetupFailed => none + MbBuilt { repository: _, at: at } => at + } +} + +fn mb_root_of(build: MbBuild, reference: RepositoryCommitRef) -> ObjectId? { + match build { + MbSetupFailed => none + MbBuilt { repository: repository, at: _ } => + match commit_at_reference(commits: repository.commits, reference: reference) { + Absent => none + Present { value: c } => Present { value: c.root.locator } + } + } +} diff --git a/src/v1/stage0/src/namespace_wave_admission.rs b/src/v1/stage0/src/namespace_wave_admission.rs index e86dfb6078a..044caef5d10 100644 --- a/src/v1/stage0/src/namespace_wave_admission.rs +++ b/src/v1/stage0/src/namespace_wave_admission.rs @@ -1560,48 +1560,143 @@ pub struct TransitionAdmission { /// the carrier in `product.cable_leg_observation`, base and head bind each spelling identically, /// and all four report CONSUMED, coming due on this roster's next touch. Adjudicate that deletion /// by joining each row against main's tree on its own tuple, not by trusting this sentence. -const LEG_OBSERVATION_REHOME_LABEL: &str = - "gunbc#10671 transceiver layer split: the cable-leg observation carrier moves from the \ - SFF-8636 byte-meaning module extdeps.transceiver.sff_8636 to product.cable_leg_observation"; + +/// THE SCM REPOSITORY BUILDER MOVES TO A SHARED FIXTURE (2026-09-06, gunbc#10676). No ordinal is +/// claimed, for the reason the entries above give. +/// +/// `test.claim.scm_merge_base_witness` authored a repository builder -- `MbBuild` with its two arms +/// `MbBuilt` and `MbSetupFailed`, and the six operations `mb_start`, `mb_stage`, `mb_commit`, +/// `mb_at`, `mb_head`, `mb_root_of` -- to construct scenes through the real store and mint rather +/// than by forging rows. The squash-merge verb's witness needs that construction VERBATIM. Copying +/// it would put one construction rule in two files, and the drift would be INVISIBLE: each copy +/// would keep passing its own claims while the two fixtures quietly stopped describing the same +/// repository (DESIGN.md ยง2). So the builder is rehomed to `test.fixture.scm_repository_builder`, +/// which is where this repository already puts fixtures shared across claims, and both witnesses +/// import it. +/// +/// NINE BINDINGS IN ONE MODULE resolve to the new declarer, which is `TargetChanged` and is not +/// auto-admitted: seven inside `mb_scene` and two inside +/// `scm_mb_the_scene_holds_the_root_relations_the_controls_depend_on`. Every spelling is identical +/// on both sides; only the declaring module differs, which is the membership motion this roster +/// exists to adjudicate. +/// +/// ONE CHANGE CLASS, AND NOTHING IS REQUALIFIED. No spelling changes, no behaviour changes, and the +/// evidence that the move is behaviour-preserving is executed rather than asserted: all twelve +/// `scm_merge_base_witness` claims pass unchanged against the shared fixture. That is the positive +/// control for a rehome -- a builder that had silently changed would show up as a claim that +/// stopped discriminating, not as a compile error. +/// +/// TRIGGER, AND IT IS THESE ROWS' OWN DEATH: they go when gunbc#10676 merges. Main then declares +/// the builder in `test.fixture.scm_repository_builder`, base and head bind each spelling +/// identically, and all nine report CONSUMED, coming due on this roster's next touch. Adjudicate +/// that deletion by joining each row against main's tree on its own +/// (module, in_declaration, spelling, target) tuple, not by trusting this sentence. +/// THE gunbc#10671 ROWS DISSOLVED HERE (2026-09-06), BY THEIR OWN TRIGGER AND ON THE ROSTER TOUCH +/// THEY NAMED. gunbc#10671 merged, so the four cable-leg rows reported CONSUMED and came due on the +/// next roster-touching change, which is this one. +/// +/// ADJUDICATED BY THE JOIN THOSE ROWS DEMANDED RATHER THAN BY THEIR OWN SENTENCE, in all three +/// directions the join has. On main, `product.cable_leg_observation` DECLARES `SecondaryNotObserved` +/// as an arm of its compliance coproduct; `extdeps.transceiver.sff_8636` does NOT declare it -- its +/// only remaining occurrence of the spelling is prose recording that an earlier head authored it, +/// which is exactly the trap a grep-count would have fallen into and a declaration check does not; +/// and both consumers, `test.claim.cable_leg_coding_witness` and +/// `test.claim.cable_order_admission_witness`, import the spelling from the new declarer. So base and +/// head bind it identically, no run can produce those four deltas, and CONSUMED is the correct +/// reading rather than an author error. +const SCM_REPOSITORY_BUILDER_REHOME_LABEL: &str = + "gunbc#10676 scm fixture extraction: the repository builder moves from \ + test.claim.scm_merge_base_witness to test.fixture.scm_repository_builder, so the merge_base \ + and squash_merge witnesses read one construction rule instead of two copies"; pub const NAMESPACE_TRANSITION_ADMISSIONS: &[TransitionAdmission] = &[ TransitionAdmission { - label: LEG_OBSERVATION_REHOME_LABEL, + label: SCM_REPOSITORY_BUILDER_REHOME_LABEL, + subject: AdmissionSubject::Binding { + module: "test.claim.scm_merge_base_witness", + in_declaration: "mb_scene", + spelling: "MbBuilt", + target: "test.fixture.scm_repository_builder", + }, + disposition: NamespaceDeltaDisposition::TargetChanged, + }, + TransitionAdmission { + label: SCM_REPOSITORY_BUILDER_REHOME_LABEL, + subject: AdmissionSubject::Binding { + module: "test.claim.scm_merge_base_witness", + in_declaration: "mb_scene", + spelling: "MbSetupFailed", + target: "test.fixture.scm_repository_builder", + }, + disposition: NamespaceDeltaDisposition::TargetChanged, + }, + TransitionAdmission { + label: SCM_REPOSITORY_BUILDER_REHOME_LABEL, + subject: AdmissionSubject::Binding { + module: "test.claim.scm_merge_base_witness", + in_declaration: "mb_scene", + spelling: "mb_start", + target: "test.fixture.scm_repository_builder", + }, + disposition: NamespaceDeltaDisposition::TargetChanged, + }, + TransitionAdmission { + label: SCM_REPOSITORY_BUILDER_REHOME_LABEL, + subject: AdmissionSubject::Binding { + module: "test.claim.scm_merge_base_witness", + in_declaration: "mb_scene", + spelling: "mb_stage", + target: "test.fixture.scm_repository_builder", + }, + disposition: NamespaceDeltaDisposition::TargetChanged, + }, + TransitionAdmission { + label: SCM_REPOSITORY_BUILDER_REHOME_LABEL, + subject: AdmissionSubject::Binding { + module: "test.claim.scm_merge_base_witness", + in_declaration: "mb_scene", + spelling: "mb_commit", + target: "test.fixture.scm_repository_builder", + }, + disposition: NamespaceDeltaDisposition::TargetChanged, + }, + TransitionAdmission { + label: SCM_REPOSITORY_BUILDER_REHOME_LABEL, subject: AdmissionSubject::Binding { - module: "test.claim.cable_leg_coding_witness", - in_declaration: "delivered_fs_leg", - spelling: "SecondaryNotObserved", - target: "product.cable_leg_observation", + module: "test.claim.scm_merge_base_witness", + in_declaration: "mb_scene", + spelling: "mb_at", + target: "test.fixture.scm_repository_builder", }, disposition: NamespaceDeltaDisposition::TargetChanged, }, TransitionAdmission { - label: LEG_OBSERVATION_REHOME_LABEL, + label: SCM_REPOSITORY_BUILDER_REHOME_LABEL, subject: AdmissionSubject::Binding { - module: "test.claim.cable_leg_coding_witness", - in_declaration: "leg_with_unmodelled_code", - spelling: "SecondaryNotObserved", - target: "product.cable_leg_observation", + module: "test.claim.scm_merge_base_witness", + in_declaration: "mb_scene", + spelling: "mb_head", + target: "test.fixture.scm_repository_builder", }, disposition: NamespaceDeltaDisposition::TargetChanged, }, TransitionAdmission { - label: LEG_OBSERVATION_REHOME_LABEL, + label: SCM_REPOSITORY_BUILDER_REHOME_LABEL, subject: AdmissionSubject::Binding { - module: "test.claim.cable_order_admission_witness", - in_declaration: "correctly_coded_leg", - spelling: "SecondaryNotObserved", - target: "product.cable_leg_observation", + module: "test.claim.scm_merge_base_witness", + in_declaration: "scm_mb_the_scene_holds_the_root_relations_the_controls_depend_on", + spelling: "MbBuilt", + target: "test.fixture.scm_repository_builder", }, disposition: NamespaceDeltaDisposition::TargetChanged, }, TransitionAdmission { - label: LEG_OBSERVATION_REHOME_LABEL, + label: SCM_REPOSITORY_BUILDER_REHOME_LABEL, subject: AdmissionSubject::Binding { - module: "test.claim.cable_order_admission_witness", - in_declaration: "delivered_leg", - spelling: "SecondaryNotObserved", - target: "product.cable_leg_observation", + module: "test.claim.scm_merge_base_witness", + in_declaration: "scm_mb_the_scene_holds_the_root_relations_the_controls_depend_on", + spelling: "mb_root_of", + target: "test.fixture.scm_repository_builder", }, disposition: NamespaceDeltaDisposition::TargetChanged, },