diff --git a/dag/gunbc/scm/commit_closure.dag b/dag/gunbc/scm/commit_closure.dag index c42dce5331e..bab22bd9fc3 100644 --- a/dag/gunbc/scm/commit_closure.dag +++ b/dag/gunbc/scm/commit_closure.dag @@ -115,30 +115,52 @@ import gunbc.scm.object_store { // admit closure A -> token T // encode closure B with T -> ACCEPTED // -// The encoder checks that a token is PRESENT, never that it is ABOUT the closure being encoded. That -// is authority substitution in the exact form this repository has already named: a true admission -// about one subject answering for another because no relation binds them. The existing mutation -// (delete the check) proves the check is READ; it proves nothing about what the token is about. -// -// So the honest rung is MITIGATABLE, not structural: an accidental partial write is caught, a -// mis-attributed one is not. This paragraph exists because the reported rung must equal the rung -// executed evidence establishes, and review 54944 read the wall as "structural" -- which is the -// inflation section 4b calls worse than sitting low, since an inflated class never ranks for -// climbing. -// -// NEXT-RUNG TRIGGER, and it is a shape change rather than a check: the admitted carrier binds the -// closure and its unresolved population together -- -// -// AdmittedPartialCommitClosure sole_constructor { closure, unresolved, reason } -// -// minted by a function that DERIVES `unresolved` from the closure it is given, with the encoder -// taking the admitted carrier instead of a closure plus a free-floating token. Then "a token for A -// used on B" has no spelling at all. Discriminator the repair owes: an admission produced for A, -// attempted against B, refuses or cannot be constructed. -type PartialClosureAdmission sole_constructor { +// THE ADMISSION IS BOUND TO ITS SUBJECT, and that binding is the whole guarantee. +// +// THE DEFECT THIS REPLACES. The predecessor was PartialClosureAdmission { reason } -- a token minted +// by a public function, carrying no subject. The encoder took a closure PLUS an optional token and +// checked only that a token was PRESENT, never that it was ABOUT the closure being encoded: +// +// admit closure A -> token T +// encode closure B with T -> ACCEPTED +// +// That is authority substitution in its exact form: a true admission about one subject answering for +// another because no relation binds them. sole_constructor did not help, because `.dag` has no +// module privacy -- it blocks the record literal while the public mint stays freely callable. And +// the mutation that existed (delete the check) could never have caught it: deleting a check proves +// the check is READ, which a bearer token satisfies perfectly. +// +// WHY THE REPAIR IS A SHAPE, NOT A CHECK. The carrier holds the closure it admits, so the encoder +// takes ONE argument and there is no second closure to disagree with it. "A token for A used on B" +// is not refused at runtime -- it has no spelling. The mismatch is unconstructible rather than +// caught, which is DESIGN section 4b's top rung and the reason no validator appears beside this. +// +// `unresolved` is DERIVED here from the closure rather than supplied, so it cannot disagree with the +// objects it describes; a caller-supplied population would reintroduce the same substitution one +// field down. The mint refuses a COMPLETE closure, because admitting a partial write for something +// with nothing missing is a category error and would make the carrier's own name a lie. +// +// REMAINING RUNG, stated so it is not read as more than it is: `unresolved` is a List, so an EMPTY +// admitted population is representable in the type even though this mint cannot produce one. The +// corpus has no NonEmptyList, and minting one solely for this field would grow net concepts to buy a +// guarantee the mint's refusal already provides -- so the emptiness exclusion sits at mitigatable +// while the SUBJECT BINDING, which is what the defect was about, is structural. Next-rung trigger: a +// NonEmptyList authority earning its place from more than one consumer. +type AdmittedPartialCommitClosure sole_constructor { + closure: CommitClosure + unresolved: List reason: PartialClosureReason } +// THE REFUSAL IS AN ARM OF THE OUTCOME, not a one-member coproduct beside it. There is exactly one +// way to be refused here -- the closure has nothing missing -- and wrapping that in its own type +// would either read as a type alias or invite a second arm invented to justify the wrapper. If a +// second genuine refusal appears, it joins this coproduct and every match fails to compile HERE, +// which is the behaviour the nesting was for. +type PartialClosureAdmissionOutcome + = PartialClosureAdmitted { admitted: AdmittedPartialCommitClosure } + | ClosureCompleteSoNothingToAdmit + // WHY A CLOSED VOCABULARY RATHER THAN A STRING. A reason nobody can enumerate is a reason nobody can // refuse, and the two admitted causes have genuinely different owners: one is a transfer decision, // the other is a graft that object_store deliberately supports. @@ -146,12 +168,34 @@ type PartialClosureReason = ClosureTrimmedForTransfer | ClosureGraftedWithoutChildren -fn admit_partial_closure(reason: PartialClosureReason) -> PartialClosureAdmission { - PartialClosureAdmission { reason: reason } +fn admit_partial_commit_closure( + closure: CommitClosure, + reason: PartialClosureReason +) -> PartialClosureAdmissionOutcome { + let unresolved = unresolved_identities(closure: closure) + if list_length(items: unresolved) == 0 { + ClosureCompleteSoNothingToAdmit + } else { + PartialClosureAdmitted { + admitted: AdmittedPartialCommitClosure { + closure: closure, + unresolved: unresolved, + reason: reason, + } + } + } +} + +fn admitted_reason(admitted: AdmittedPartialCommitClosure) -> PartialClosureReason { + admitted.reason +} + +fn admitted_closure(admitted: AdmittedPartialCommitClosure) -> CommitClosure { + admitted.closure } -fn admission_reason(admission: PartialClosureAdmission) -> PartialClosureReason { - admission.reason +fn admitted_unresolved(admitted: AdmittedPartialCommitClosure) -> List { + admitted.unresolved } // A closure is the store plus the root the commit names. Nothing about how it is written down. diff --git a/dag/gunbc/scm/commit_closure_json_v2.dag b/dag/gunbc/scm/commit_closure_json_v2.dag index 02f2db80764..d4b3f52e614 100644 --- a/dag/gunbc/scm/commit_closure_json_v2.dag +++ b/dag/gunbc/scm/commit_closure_json_v2.dag @@ -5,8 +5,17 @@ import std.types { Bool, Int, List, String, list_length } import std.content_hash { fnv1a64_structural_hex_digest } import gunbc.scm.commit_closure { CommitClosure, - PartialClosureAdmission, + AdmittedPartialCommitClosure, unresolved_identities, + closure_is_complete, +} +import gunbc.scm.load_standing { + LoadStanding, + LoadComplete, + LoadPartial, + LoadUnsupportedProtocol, + LoadDocumentMalformed, + LoadIdentityCollision, } import extdeps.languages.json.emit { JsonValue, @@ -119,7 +128,22 @@ type ClosureDocRefusal | ClosureDocUnexpectedMember { context: ClosureDocMemberContext, key: String } | ClosureDocTargetNotOneArm { found: Int } | ClosureDocUncontainedDigestInvalid { found: String } - | ClosureDocIncompleteWithoutAdmission { identity: String } + +// THE ENCODE DOMAIN IS ITS OWN TYPE, and this split is a correctness fix rather than tidiness. +// +// ClosureDocIncompleteWithoutAdmission is produced by encode_complete_closure_document and by nothing +// else -- no decode path can reach it. It nevertheless sat in ClosureDocRefusal, which the LOAD +// classifier matches exhaustively, so closure_document_load_standing was forced to assign a standing +// to a state the loader cannot produce. It answered LoadDocumentMalformed, and malformed is the ONE +// standing standing_may_supersede_generation permits to overwrite a newer document. An encode-only +// state therefore had a route to "may supersede". +// +// That is a closed match over a dishonest domain: exhaustiveness is satisfied, the compiler is +// content, and the arm is answering for something that cannot occur. Splitting the type does not +// hide the arm -- it makes the question unaskable, because the classifier's parameter can no longer +// name this cause. DESIGN section 4b's top rung: not validated, not proven, unrepresentable. +type ClosureDocEncodeRefusal + = ClosureDocIncompleteWithoutAdmission { identity: String } // THE FORMAT TAG MEANS "I UNDERSTAND THIS EXACT SCHEMA", not "I understand some subset of whatever // this writer may have meant". So once the tag matches, every JSON object in the document has a @@ -428,7 +452,7 @@ fn encoded_root_reference(acc: EncodeAcc, root: ObjectId) -> JsonValue { type ClosureDocEncodeOutcome = ClosureDocEncoded { value: JsonValue } - | ClosureDocEncodeRefused { cause: ClosureDocRefusal } + | ClosureDocEncodeRefused { cause: ClosureDocEncodeRefusal } // AN INCOMPLETE CLOSURE IS NOW REPRESENTABLE, SO THE QUESTION CHANGES FROM "CAN IT BE WRITTEN" TO // "WAS IT DECIDED". @@ -440,44 +464,55 @@ type ClosureDocEncodeOutcome // closure may be PUBLISHED is a policy question that does not belong in a codec (DESIGN section 3). // // What must NOT happen is the refusal quietly becoming optional -- a caller who forgets, and a -// partial document written by accident. So the encoder does not accept a closure plus a caller's -// assurance: it requires a PartialClosureAdmission, which is `sole_constructor` in gunbc.scm. -// commit_closure and therefore cannot be fabricated here. An undeclared partial closure has no token -// to offer and is refused, typed and naming the first identity that would have gone uncontained. -// -// RUNG, STATED HONESTLY BECAUSE THE TOKEN'S STRENGTH IS NOT THE PATH'S. The admission itself is -// unforgeable (structural). This GATE is only mechanically preventable: `encode_closure_document` -// below remains callable directly, so a caller bypassing this entry can still emit uncontained arms -// with no admission. The wall is at the entry, not on the carrier. +// partial document written by accident. The two questions are therefore two ENTRY POINTS rather than +// one entry point with an optional token: +// +// encode_complete_closure_document(closure) refuses if anything is uncontained +// encode_admitted_partial_closure_document(a) total; the admission IS the decision +// +// WHY THE PARTIAL ENTRY TAKES ONE ARGUMENT. Its predecessor took a closure PLUS an optional +// admission and checked only that the admission was PRESENT -- so an admission minted for closure A +// authorized encoding closure B, and no amount of checking inside this function could have noticed, +// because the token carried no subject. The admitted carrier holds its own closure, so there is no +// second closure to disagree with it and the mismatch has no spelling. That is why this entry +// performs no validation at all: there is nothing left to validate. +// +// RUNG, STATED HONESTLY BECAUSE THE CARRIER'S STRENGTH IS NOT THE PATH'S. The subject binding is +// structural. This GATE is still only mechanically preventable: encode_closure_document below +// remains callable directly, so a caller bypassing these entries can emit uncontained arms with no +// admission at all. The wall is on the carrier now, but the module's front door is still ajar. // dissolve-on: module-private functions in .dag, at which point the unchecked encoder stops being // reachable and the gate becomes structural. Until then this comment is the only thing saying so. -fn encode_closure_document_checked( - store: ObjectStore, - root: ObjectId, - admission: PartialClosureAdmission? -) -> ClosureDocEncodeOutcome { - match unresolved_identities(closure: CommitClosure { store: store, root: root }) { +fn encode_complete_closure_document(closure: CommitClosure) -> ClosureDocEncodeOutcome { + match unresolved_identities(closure: closure) { missing => if list_length(items: missing) == 0 { - ClosureDocEncoded { value: encode_closure_document(store: store, root: root) } + ClosureDocEncoded { + value: encode_closure_document(store: closure.store, root: closure.root) + } } else { - match admission { - Present { value: _ } => - ClosureDocEncoded { value: encode_closure_document(store: store, root: root) } - Absent => - ClosureDocEncodeRefused { - cause: ClosureDocIncompleteWithoutAdmission { - identity: first_uncontained_key(missing: missing) - } - } + ClosureDocEncodeRefused { + cause: ClosureDocIncompleteWithoutAdmission { + identity: an_uncontained_key(missing: missing) + } } } } } +// TOTAL, AND THE TOTALITY IS THE CLAIM. Every way of obtaining an AdmittedPartialCommitClosure runs +// through a mint that derived the unresolved population from this very closure, so by the time one +// exists there is nothing this function could refuse that the mint did not already settle. +fn encode_admitted_partial_closure_document(admitted: AdmittedPartialCommitClosure) -> JsonValue { + encode_closure_document(store: admitted.closure.store, root: admitted.closure.root) +} + // The first identity that would have been written as an uncontained arm. Naming one is what makes // the refusal actionable; naming all of them is the caller's own query via unresolved_identities. -fn first_uncontained_key(missing: List) -> String { +// AN uncontained key, not THE FIRST. Same correction as an_uncontained_target in repository_envelope: +// the refusal needs one actionable example and no consumer depends on which, so the name should not +// promise an ordering nothing verifies. +fn an_uncontained_key(missing: List) -> String { fold(missing, init: "", f: fn(acc, id) { if acc == "" { object_id_key(identity: id) } else { acc } }) @@ -995,3 +1030,70 @@ fn decode_closure_document_body(v: JsonValue) -> ClosureDocLoadOutcome { _ => ClosureDocRefused { cause: ClosureDocMemberWrongShape { key: "objects" } } } } + +// --------------------------------------------------------------------------- +// Publication standing +// --------------------------------------------------------------------------- + +// THIS FORMAT'S REFUSALS, CLASSIFIED BESIDE THE REFUSALS THEMSELVES. +// +// The standing vocabulary is format-agnostic and lives in gunbc.scm.load_standing; the mapping from +// THIS format's causes into it belongs HERE, next to the coproduct it reads, so a new refusal +// variant fails to compile where its author already is rather than in a consumer that has never +// heard of it. A consumer computing this by matching on ClosureDocRefusal would be reaching into one +// format's error enum for a format-agnostic answer -- the placement defect that closed gunbc#8940. +// +// WHY UNKNOWN MEMBERS AND UNKNOWN VARIANT TAGS ARE MALFORMED, NOT A NEWER WRITER, which follows from +// this module's own policy rather than from taste: a schema that grows a member grows a NEW FORMAT +// TAG. So a conforming newer writer emits a new tag; it does not emit this one carrying extras. A +// document under a MATCHED tag with an unrecognized member or variant is a broken document of THIS +// schema, and calling it "needs a compatible reader" would name a remedy that cannot arrive, because +// no conforming writer produced it. +// +// WHY AN UNRESOLVED POSITION IS MALFORMED AND NOT A FETCHABLE ABSENCE. A position denotes nothing +// outside the document that defines it, so an unresolved `at` names no object anyone could supply. +// Fetchable absence is the UNCONTAINED arm, which is not a refusal at all and never reaches here. +// +// WHY A COLLISION IS ITS OWN STANDING. object_store states both colliding records are legitimate and +// the forged pairing is prevented by construction, so a collision means the structural locator +// cannot represent both -- not that either is damaged. Classifying it as malformed would let a +// reader DISCARD A WRITER'S VALID GENERATION over an honest hash coincidence. +fn closure_document_load_standing(cause: ClosureDocRefusal) -> LoadStanding { + match cause { + ClosureDocFormatUnrecognized { found: _ } => LoadUnsupportedProtocol + ClosureDocObjectCollision { identity: _ } => LoadIdentityCollision + ClosureDocNotAnObject => LoadDocumentMalformed + ClosureDocMemberMissing { key: _ } => LoadDocumentMalformed + ClosureDocMemberDuplicated { key: _ } => LoadDocumentMalformed + ClosureDocMemberWrongShape { key: _ } => LoadDocumentMalformed + ClosureDocUnknownKindTag { found: _ } => LoadDocumentMalformed + ClosureDocUnknownConnectiveTag { found: _ } => LoadDocumentMalformed + ClosureDocUnknownBehaviorTag { found: _ } => LoadDocumentMalformed + ClosureDocUnknownLabelTag { found: _ } => LoadDocumentMalformed + ClosureDocUnexpectedMember { context: _, key: _ } => LoadDocumentMalformed + ClosureDocEdgeTargetUnresolved { reference: _ } => LoadDocumentMalformed + ClosureDocRootUnresolved { reference: _ } => LoadDocumentMalformed + ClosureDocTargetNotOneArm { found: _ } => LoadDocumentMalformed + ClosureDocUncontainedDigestInvalid { found: _ } => LoadDocumentMalformed + } +} + +// A LOAD THAT SUCCEEDED STILL HAS A STANDING, and it is not always LoadComplete. This is the arm the +// predecessor format could not produce: a document whose uncontained references leave objects the +// store does not hold loads correctly and is PARTIAL, which is a success with an obligation rather +// than a failure. Completeness is derived from the objects, never a flag beside them. +fn loaded_closure_standing(store: ObjectStore, root: ObjectId) -> LoadStanding { + if closure_is_complete(closure: CommitClosure { store: store, root: root }) { + LoadComplete + } else { + LoadPartial + } +} + +fn closure_document_standing(outcome: ClosureDocLoadOutcome) -> LoadStanding { + match outcome { + ClosureDocLoaded { store: store, root: root } => + loaded_closure_standing(store: store, root: root) + ClosureDocRefused { cause: cause } => closure_document_load_standing(cause: cause) + } +} diff --git a/dag/gunbc/scm/load_standing.dag b/dag/gunbc/scm/load_standing.dag new file mode 100644 index 00000000000..57fd6a6babe --- /dev/null +++ b/dag/gunbc/scm/load_standing.dag @@ -0,0 +1,88 @@ +module gunbc.scm.load_standing + +// WHAT A LOADER MAY DO NEXT, IN A VOCABULARY NO FORMAT OWNS. +// +// A codec answers WHAT WENT WRONG. This answers the only question a publication boundary has to act +// on: WHAT MAY THE LOADER DO NEXT. Those are different questions with different vocabularies, and +// fusing them puts a policy decision inside a codec -- the decoder would have to know whether a +// damaged generation may be superseded, which is a fact about head slots and not about JSON. +// +// THIS MODULE NAMES NO FORMAT, AND THAT IS THE POINT. Unsupported-protocol, malformed-document and +// identity-collision are properties ANY serialization can have; nothing about them is JSON-specific. +// An earlier attempt (gunbc#8940, closed) computed exactly this vocabulary INSIDE publication by +// matching on one format's error enum. The classification was right and the PLACEMENT was wrong: +// DESIGN section 3 rules that the dispatch selecting a realization is itself realization, so each +// format classifies ITS OWN refusals into this shared vocabulary and the consumer sees only the +// standing. A new format is then a new classifier beside its own refusals, not an edit here. +// +// THE STANDINGS CARRY NO CAUSE PAYLOAD, deliberately. A cause is format-shaped -- a JSON member key, +// a positional reference, a variant tag -- and threading it through this type would re-import the +// coupling the split exists to remove. The cause stays with the format that produced it, for +// diagnostics; the DECISION needs only the standing. If publication ever needs to render a cause it +// asks the format for a rendering, it does not pattern-match one. + +import std.types { Bool } + +// A LOAD THAT SUCCEEDED BUT DID NOT BRING EVERYTHING IS NOT A FAILURE, and this arm is the whole +// reason this vocabulary could not exist before the closure recut. +// +// The predecessor format stored backward POSITIONS with every identity re-derived on load, so a +// reference that did not resolve inside the document denoted nothing outside it. There was no such +// thing as "loaded, and here is what is missing" -- an unresolved reference was damage, full stop, +// and a fetchable-absence standing had NO MECHANISM THAT COULD PRODUCE IT. Adding it then would have +// been an invented arm (DESIGN: reachability read as occupancy). +// +// With two-arm references it has a producer: an uncontained reference is a well-formed statement +// that this document does not carry an object, and the load succeeds with the missing identities +// nameable as content addresses. So LoadPartial is a SUCCESS arm sitting beside three failure arms, +// and conflating it with LoadDocumentMalformed would be the state-space conflation DESIGN names -- +// "I could not read this" and "I read this, and it is a delta" have opposite remedies. +type LoadStanding + = LoadComplete + | LoadPartial + | LoadUnsupportedProtocol + | LoadDocumentMalformed + | LoadIdentityCollision + +// PERMISSION IS NOT ACTION. This answers "is replacing this generation ADMISSIBLE", never "replace +// it". Even a malformed selected generation gets an explicit recovery path, never a silent fall back +// to whatever else is on disk -- that fallback would be the absorbing arm DESIGN section 5 forbids. +// +// EXACTLY ONE STANDING PERMITS REPLACEMENT, and the three refusals are each refused for a different +// reason rather than by one rule: +// +// LoadComplete nothing is wrong; there is nothing to replace. +// LoadPartial the document is CORRECT and deliberately incomplete. Superseding a valid +// delta because it did not carry everything would discard a writer's good +// generation for doing exactly what it was asked to do. +// LoadUnsupportedProtocol the writer named a protocol this build lacks, so the bytes may be +// perfectly good and unreadable ONLY HERE. Replacing them is an older +// reader destroying a newer writer's work. +// LoadIdentityCollision both colliding records are legitimate (object_store: the forged pairing +// is prevented by construction). A collision means the structural locator +// cannot represent both, not that either is damaged -- so superseding +// discards a valid generation over an honest hash coincidence. +// LoadDocumentMalformed the tag matched, so this build claims to understand the EXACT schema, +// and the document does not conform. This is the only standing where the +// bytes are known-bad to a reader that should have understood them. +fn standing_may_supersede_generation(standing: LoadStanding) -> Bool { + match standing { + LoadDocumentMalformed => true + LoadComplete => false + LoadPartial => false + LoadUnsupportedProtocol => false + LoadIdentityCollision => false + } +} + +// Whether a standing represents a load that produced a usable closure at all. LoadPartial answers +// TRUE: a delta is usable, it simply needs its missing objects supplied before checkout. +fn standing_loaded(standing: LoadStanding) -> Bool { + match standing { + LoadComplete => true + LoadPartial => true + LoadUnsupportedProtocol => false + LoadDocumentMalformed => false + LoadIdentityCollision => false + } +} diff --git a/dag/gunbc/scm/repository_envelope.dag b/dag/gunbc/scm/repository_envelope.dag index 8c6e53a6bb6..4127cde6c5d 100644 --- a/dag/gunbc/scm/repository_envelope.dag +++ b/dag/gunbc/scm/repository_envelope.dag @@ -144,8 +144,19 @@ type RepositoryEnvelope { // An ObjectId and a wire position are not one type because both render as text. On the encode side // the domain type is now carried directly, so the identity no longer round-trips through a string // nobody can resolve back. +// THE ENCODE DOMAIN CARRIES ONLY WHAT ENCODING CAN PRODUCE, and the arm this replaces is the same +// dishonest-domain shape the codec carried one layer down. +// +// RepositoryEncodeClosureDocRefusal wrapped the WHOLE ClosureDocRefusal decode population -- fifteen +// causes, of which encoding can produce exactly one. A reader could not tell from the type which of +// them were real, and a consumer matching it had to handle format-tag and unknown-connective causes +// that no encode path can raise. The type was answering for a domain it does not own. +// +// The single producible cause is an uncontained target found in the store being written, so that is +// the arm. It carries ObjectId rather than a rendered key, which also closes a string round-trip the +// two sibling arms below never had: an identity left as text is one nobody can resolve back. type RepositoryEncodeRefusal - = RepositoryEncodeClosureDocRefusal { cause: ClosureDocRefusal } + = RepositoryEncodeUncontainedTarget { target: ObjectId } | RepositoryCommitRootNotInStore { root: ObjectId } | RepositoryCheckoutNotInCommits { selected: ObjectId } @@ -350,20 +361,28 @@ type RepositoryEncodeOutcome // and admitting it here would widen the change beyond what was decided. When a repository-grain // partial write is actually wanted, it takes an admission exactly as the closure encoder does; until // then this refusal stands rather than being loosened by proximity. -fn first_uncontained_target(store: ObjectStore) -> String? { +// AN uncontained target, not THE FIRST one, and the name says only what the evidence establishes. +// +// This returns whichever member the fold reaches first, but the refusal needs one ACTIONABLE EXAMPLE +// and nothing downstream depends on which. Naming it `first_` promised a stable ordering the witness +// does not check -- and could not cheaply check, since with a single uncontained object every member +// is also the first. Pinning order here would additionally freeze an incidental traversal order into +// the interface, which a later keyed or canonical representation of uncontained_targets should not +// be forced to preserve. +fn an_uncontained_target(store: ObjectStore) -> ObjectId? { fold(uncontained_targets(store: store), init: none, f: fn(acc, id) { match acc { Present { value: _ } => acc - Absent => Present { value: object_id_key(identity: id) } + Absent => Present { value: id } } }) } fn encode_repository_checked(repo: RepositoryEnvelope) -> RepositoryEncodeOutcome { let acc = encode_object_table(store: repo.store) - match first_uncontained_target(store: repo.store) { - Present { value: reference } => - RepositoryEncodeRefused { cause: RepositoryEncodeClosureDocRefusal { cause: ClosureDocEdgeTargetUnresolved { reference: reference } } } + match an_uncontained_target(store: repo.store) { + Present { value: target } => + RepositoryEncodeRefused { cause: RepositoryEncodeUncontainedTarget { target: target } } Absent => match unresolved_commit_root(acc: acc, commits: repo.commits) { Present { value: root } => diff --git a/dag/test/claim/scm_commit_closure_json_v2_witness_test.dag b/dag/test/claim/scm_commit_closure_json_v2_witness_test.dag index c6c9de21ec3..df97d46613c 100644 --- a/dag/test/claim/scm_commit_closure_json_v2_witness_test.dag +++ b/dag/test/claim/scm_commit_closure_json_v2_witness_test.dag @@ -73,6 +73,7 @@ import gunbc.scm.checkout { CheckedOut, CheckoutRefused, } +import gunbc.scm.commit_closure { CommitClosure } import gunbc.scm.commit_closure_json_v2 { ClosureDocLoadOutcome, ClosureDocLoaded, @@ -89,7 +90,7 @@ import gunbc.scm.commit_closure_json_v2 { ClosureDocEncodeOutcome, ClosureDocEncoded, ClosureDocEncodeRefused, - encode_closure_document_checked, + encode_complete_closure_document, } // ------------------------------------------------------------------------------------------------ @@ -495,14 +496,14 @@ fn scm_image_orphan_parent_store() -> ScmImagePut { test fn scm_image_a_root_absent_from_the_store_refuses_to_encode() -> Bool { let full = scm_image_put(store: empty_store(), n: closure_doc_program()) scm_image_encode_refused( - outcome: encode_closure_document_checked(store: empty_store(), root: full.identity, admission: none) + outcome: encode_complete_closure_document(closure: CommitClosure { store: empty_store(), root: full.identity }) ) } test fn scm_image_a_root_whose_child_is_absent_refuses_to_encode() -> Bool { let orphan = scm_image_orphan_parent_store() scm_image_encode_refused( - outcome: encode_closure_document_checked(store: orphan.store, root: orphan.identity, admission: none) + outcome: encode_complete_closure_document(closure: CommitClosure { store: orphan.store, root: orphan.identity }) ) } @@ -510,7 +511,7 @@ test fn scm_image_a_root_whose_child_is_absent_refuses_to_encode() -> Bool { // everything, and DESIGN section 4b requires the accepted arm alongside the discriminating red. test fn scm_image_a_complete_closure_encodes() -> Bool { let full = scm_image_put(store: empty_store(), n: closure_doc_program()) - match encode_closure_document_checked(store: full.store, root: full.identity, admission: none) { + match encode_complete_closure_document(closure: CommitClosure { store: full.store, root: full.identity }) { ClosureDocEncoded { value: v } => serialize_json(v: v) == serialize_json(v: encode_closure_document(store: full.store, root: full.identity)) ClosureDocEncodeRefused { cause: _ } => false diff --git a/dag/test/claim/scm_commit_closure_witness_test.dag b/dag/test/claim/scm_commit_closure_witness_test.dag index c860b348f06..c5b47d7a9da 100644 --- a/dag/test/claim/scm_commit_closure_witness_test.dag +++ b/dag/test/claim/scm_commit_closure_witness_test.dag @@ -13,7 +13,6 @@ module test.claim.scm_commit_closure_witness // merely non-empty. `a_grafted_root_names_exactly_the_child_it_lacks` is the test that decides it. import std.types { Bool, Int, List, list_length } -import std.process { ProcessExit, ExitSuccess, ExitFailure } import v2.std.node { Node, Edge, Named, Conj, Atom, TypeNode, node_synthetic } import gunbc.scm.object_store { ObjectId, @@ -32,11 +31,12 @@ import gunbc.scm.object_store { store_object_count, } import v2.std.collection { empty_map } -import extdeps.languages.json.emit { serialize_json, json_object, json_kv, json_string, JsonValue } +import extdeps.languages.json.emit { serialize_json, json_object, json_kv, json_string, JsonValue, JsonObject } import extdeps.languages.json.parse { parse_json } import gunbc.scm.commit_closure_json_v2 { ClosureDocRefusal, ClosureDocIncompleteWithoutAdmission, + ClosureDocEncodeRefusal, ClosureDocTargetNotOneArm, ClosureDocMemberDuplicated, ClosureDocMemberWrongShape, @@ -50,7 +50,8 @@ import gunbc.scm.commit_closure_json_v2 { ClosureDocLoadOutcome, ClosureDocLoaded, ClosureDocRefused, - encode_closure_document_checked, + encode_complete_closure_document, + encode_admitted_partial_closure_document, decode_closure_document, } import gunbc.scm.commit_closure { @@ -58,9 +59,13 @@ import gunbc.scm.commit_closure { PartialClosureReason, ClosureTrimmedForTransfer, ClosureGraftedWithoutChildren, - admit_partial_closure, - PartialClosureAdmission, - admission_reason, + admit_partial_commit_closure, + AdmittedPartialCommitClosure, + PartialClosureAdmissionOutcome, + PartialClosureAdmitted, + ClosureCompleteSoNothingToAdmit, + admitted_reason, + admitted_unresolved, unresolved_identities, closure_is_complete, } @@ -242,10 +247,64 @@ test fn a_closure_missing_its_own_root_names_the_root() -> Bool { // evidence that belongs with it. What executes here is only that the reason is carried faithfully, // which is the part a later refactor could silently break. test fn an_admission_carries_the_decision_that_produced_it() -> Bool { - let transfer = admit_partial_closure(reason: ClosureTrimmedForTransfer) - let graft = admit_partial_closure(reason: ClosureGraftedWithoutChildren) - reason_is_transfer(r: admission_reason(admission: transfer)) - && !reason_is_transfer(r: admission_reason(admission: graft)) + let full = build_full_store() + let grafted = build_grafted_store() + let closure = CommitClosure { store: grafted, root: full.root } + match admit_partial_commit_closure(closure: closure, reason: ClosureTrimmedForTransfer) { + ClosureCompleteSoNothingToAdmit => false + PartialClosureAdmitted { admitted: transfer } => + match admit_partial_commit_closure(closure: closure, reason: ClosureGraftedWithoutChildren) { + ClosureCompleteSoNothingToAdmit => false + PartialClosureAdmitted { admitted: graft } => + reason_is_transfer(r: admitted_reason(admitted: transfer)) + && !reason_is_transfer(r: admitted_reason(admitted: graft)) + } + } +} + +// THE ADMISSION NAMES THE POPULATION IT ADMITS, derived from the closure rather than supplied. A +// caller-supplied population would reintroduce the same substitution one field down: an admission +// truthfully about closure A, carrying B's missing objects. +test fn an_admission_names_the_objects_it_admits_as_missing() -> Bool { + let full = build_full_store() + let grafted = build_grafted_store() + match admitted_for(store: grafted, root: full.root) { + ClosureCompleteSoNothingToAdmit => false + PartialClosureAdmitted { admitted: a } => + (list_length(items: admitted_unresolved(admitted: a)) == 1) + && names_exactly(missing: admitted_unresolved(admitted: a), expected: full.child) + } +} + +// THE MINT REFUSES A COMPLETE CLOSURE. Admitting a partial write for something with nothing missing +// is a category error, and letting it through would make the carrier's own name a lie -- an +// AdmittedPartialCommitClosure over a closure that is not partial. +// +// This is also the claim that keeps the mint honest about DERIVING the population: a mint that +// trusted a caller could mint this carrier over a complete closure and nothing here would object. +test fn the_mint_refuses_a_complete_closure() -> Bool { + let full = build_full_store() + match admitted_for(store: full.store, root: full.root) { + PartialClosureAdmitted { admitted: _ } => false + ClosureCompleteSoNothingToAdmit => true + } +} + + +// Mints an admission for the closure under test, or reports the refusal. Every claim below routes +// through this, so none of them can accidentally encode a closure the mint never admitted. +fn admitted_for(store: ObjectStore, root: ObjectId) -> PartialClosureAdmissionOutcome { + admit_partial_commit_closure( + closure: CommitClosure { store: store, root: root }, + reason: ClosureTrimmedForTransfer + ) +} + +fn json_is_object(v: JsonValue) -> Bool { + match v { + JsonObject { members: _ } => true + _ => false + } } fn reason_is_transfer(r: PartialClosureReason) -> Bool { @@ -255,33 +314,6 @@ fn reason_is_transfer(r: PartialClosureReason) -> Bool { } } -fn commit_closure_witness_main() -> ProcessExit { - if !a_complete_closure_names_nothing_missing() { - ExitFailure { code: 1, reason: "a_complete_closure_names_nothing_missing" } - } else if !a_grafted_root_names_exactly_the_child_it_lacks() { - ExitFailure { code: 1, reason: "a_grafted_root_names_exactly_the_child_it_lacks" } - } else if !a_closure_missing_its_own_root_names_the_root() { - ExitFailure { code: 1, reason: "a_closure_missing_its_own_root_names_the_root" } - } else if !an_admission_carries_the_decision_that_produced_it() { - ExitFailure { code: 1, reason: "an_admission_carries_the_decision_that_produced_it" } - } else if !a_partial_closure_encodes_only_with_an_admission() { - ExitFailure { code: 1, reason: "a_partial_closure_encodes_only_with_an_admission" } - } else if !an_uncontained_object_survives_the_wire_and_is_still_named() { - ExitFailure { code: 1, reason: "an_uncontained_object_survives_the_wire_and_is_still_named" } - } else if !a_target_must_carry_exactly_one_arm() { - ExitFailure { code: 1, reason: "a_target_must_carry_exactly_one_arm" } - } else if !a_target_carrying_one_arm_still_decodes() { - ExitFailure { code: 1, reason: "a_target_carrying_one_arm_still_decodes" } - } else if !a_duplicated_at_is_refused_against_at() { - ExitFailure { code: 1, reason: "a_duplicated_at_is_refused_against_at" } - } else if !a_non_string_at_is_refused_against_at() { - ExitFailure { code: 1, reason: "a_non_string_at_is_refused_against_at" } - } else if !two_uncontained_children_are_named_in_order() { - ExitFailure { code: 1, reason: "two_uncontained_children_are_named_in_order" } - } else { - ExitSuccess - } -} // ------------------------------------------------------------------------------------------------ // THE WIRE HALF OF THE ACCEPTANCE TEST. @@ -299,25 +331,36 @@ fn commit_closure_witness_main() -> ProcessExit { test fn a_partial_closure_encodes_only_with_an_admission() -> Bool { let full = build_full_store() let grafted = build_grafted_store() - let refused = match encode_closure_document_checked(store: grafted, root: full.root, admission: none) { + let refused = match encode_complete_closure_document( + closure: CommitClosure { store: grafted, root: full.root } + ) { ClosureDocEncodeRefused { cause: c } => cause_is_incomplete_without_admission(cause: c) ClosureDocEncoded { value: _ } => false } - let admitted = match encode_closure_document_checked( - store: grafted, - root: full.root, - admission: Present { value: admit_partial_closure(reason: ClosureTrimmedForTransfer) } - ) { - ClosureDocEncoded { value: _ } => true - ClosureDocEncodeRefused { cause: _ } => false + let admitted = match admitted_for(store: grafted, root: full.root) { + ClosureCompleteSoNothingToAdmit => false + PartialClosureAdmitted { admitted: a } => + json_is_object(v: encode_admitted_partial_closure_document(admitted: a)) } refused && admitted } -fn cause_is_incomplete_without_admission(cause: ClosureDocRefusal) -> Bool { +// THIS HELPER IS NOW TOTAL BY CONSTRUCTION, and saying so is the point rather than an apology. +// +// Before the encode/decode split it discriminated: the cause could have been any of sixteen decode +// refusals and this asked whether it was the encode-only one. ClosureDocEncodeRefusal has a single +// arm, so the match cannot answer anything else, and the runtime question it used to pose is gone. +// +// That is what a climb to structural impossibility LOOKS like from the test side -- the check does +// not get stronger, it becomes unnecessary, because the state it excluded is no longer nameable +// here. DESIGN section 4b(4) says the production machinery a climb obsoletes is deleted while the +// evidence stays enrolled, so this stays as the positive control that encode still refuses an +// unadmitted partial closure; what it no longer carries is the discrimination, which moved to the +// type. The claim that the LOAD classifier cannot name this cause is a compile-time property and is +// recorded by the probe in the commit message, not by a runtime assertion that cannot express it. +fn cause_is_incomplete_without_admission(cause: ClosureDocEncodeRefusal) -> Bool { match cause { ClosureDocIncompleteWithoutAdmission { identity: _ } => true - _ => false } } @@ -330,14 +373,12 @@ fn cause_is_incomplete_without_admission(cause: ClosureDocRefusal) -> Bool { test fn an_uncontained_object_survives_the_wire_and_is_still_named() -> Bool { let full = build_full_store() let grafted = build_grafted_store() - match encode_closure_document_checked( - store: grafted, - root: full.root, - admission: Present { value: admit_partial_closure(reason: ClosureTrimmedForTransfer) } - ) { - ClosureDocEncodeRefused { cause: _ } => false - ClosureDocEncoded { value: encoded } => - match parse_json(s: serialize_json(v: encoded)) { + match admitted_for(store: grafted, root: full.root) { + ClosureCompleteSoNothingToAdmit => false + PartialClosureAdmitted { admitted: a } => + match parse_json( + s: serialize_json(v: encode_admitted_partial_closure_document(admitted: a)) + ) { Absent => false Present { value: reparsed } => match decode_closure_document(v: reparsed) { diff --git a/dag/test/claim/scm_load_standing_witness_test.dag b/dag/test/claim/scm_load_standing_witness_test.dag new file mode 100644 index 00000000000..a86a6107c77 --- /dev/null +++ b/dag/test/claim/scm_load_standing_witness_test.dag @@ -0,0 +1,152 @@ +module test.claim.scm_load_standing_witness + +// THE STANDING THAT COULD NOT EXIST, AND NOW HAS A PRODUCER. +// +// The predecessor format had no mechanism that could yield "loaded, and here is what is missing" -- +// a position denotes nothing outside its own document, so an unresolved reference was damage and a +// fetchable-absence standing would have been an INVENTED ARM. These witnesses execute the arm end to +// end, which is the difference between a disposition that exists and one that is merely declared. +// +// Each test pairs a cause with a cause that MUST land elsewhere. A classifier that collapsed +// everything onto one standing would satisfy any single-arm assertion and is caught only here. + +import std.types { Bool, Int, List } +import v2.std.node { Node, Edge, Named, Conj, Atom, TypeNode, node_synthetic } +import gunbc.scm.object_store { + ObjectId, ObjectStore, StoreOutcome, Stored, LocatorCollision, + CopyOutcome, Copied, CopySourceAbsent, CopyLocatorCollision, + empty_store, copy_object, store_node, +} +import gunbc.scm.load_standing { + LoadStanding, LoadComplete, LoadPartial, LoadUnsupportedProtocol, + LoadDocumentMalformed, LoadIdentityCollision, + standing_may_supersede_generation, standing_loaded, +} +import gunbc.scm.commit_closure_json_v2 { + ClosureDocRefusal, ClosureDocFormatUnrecognized, ClosureDocObjectCollision, + ClosureDocNotAnObject, ClosureDocMemberMissing, ClosureDocUnknownKindTag, + ClosureDocUnexpectedMember, ClosureDocEnvelopeMembers, ClosureDocEdgeTargetUnresolved, + ClosureDocTargetNotOneArm, + closure_document_load_standing, loaded_closure_standing, +} + +fn ls_child() -> Node { + node_synthetic(kind: TypeNode { connective: Atom { identity: ^load_standing_child } }, children: []) +} + +fn ls_parent() -> Node { + node_synthetic( + kind: TypeNode { connective: Conj }, + children: [Edge { label: Named { name: ^only_child }, target: ls_child() }], + ) +} + +type LsBuilt { store: ObjectStore, root: ObjectId } + +fn ls_full() -> LsBuilt { + match store_node(store: empty_store(), n: ls_child()) { + LocatorCollision { identity: i, existing: _, incoming: _ } => LsBuilt { store: empty_store(), root: i } + Stored { store: s1, identity: _ } => + match store_node(store: s1, n: ls_parent()) { + LocatorCollision { identity: i, existing: _, incoming: _ } => LsBuilt { store: empty_store(), root: i } + Stored { store: s2, identity: p } => LsBuilt { store: s2, root: p } + } + } +} + +fn ls_grafted() -> ObjectStore { + let full = ls_full() + match copy_object(from: full.store, to: empty_store(), identity: full.root) { + Copied { store: s, identity: _ } => s + CopySourceAbsent { identity: _ } => empty_store() + CopyLocatorCollision { identity: _, existing: _, incoming: _ } => empty_store() + } +} + +fn standing_eq(a: LoadStanding, b: LoadStanding) -> Bool { + standing_tag(s: a) == standing_tag(s: b) +} + +fn standing_tag(s: LoadStanding) -> Int { + match s { + LoadComplete => 0 + LoadPartial => 1 + LoadUnsupportedProtocol => 2 + LoadDocumentMalformed => 3 + LoadIdentityCollision => 4 + } +} + +// THE ARM THE RECUT EXISTS FOR. A grafted root lacks its child, so the load succeeds and the standing +// is PARTIAL -- a success with an obligation, not a failure. The pairing against LoadComplete is what +// makes it discriminating: a classifier answering Partial for everything would pass a lone assertion. +test fn an_incomplete_closure_loads_partial_and_a_complete_one_does_not() -> Bool { + let full = ls_full() + let partial = loaded_closure_standing(store: ls_grafted(), root: full.root) + let complete = loaded_closure_standing(store: full.store, root: full.root) + standing_eq(a: partial, b: LoadPartial) + && standing_eq(a: complete, b: LoadComplete) + && standing_loaded(standing: partial) + && standing_loaded(standing: complete) +} + +// A PARTIAL LOAD IS CORRECT AND DELIBERATELY INCOMPLETE, so superseding it would discard a valid +// delta for doing exactly what it was asked to do. This is the arm that did not exist to be got +// wrong before, and it is the one most likely to be got wrong now by treating "not everything" as +// "damaged". +test fn a_partial_load_is_never_supersedable() -> Bool { + !standing_may_supersede_generation(standing: LoadPartial) + && !standing_may_supersede_generation(standing: LoadComplete) +} + +// MALFORMED IS THE SOLE SUPERSEDABLE STANDING, and the three refusals are refused for three +// different reasons rather than by one rule. If this were the empty set the negative tests above +// would pass vacuously. +test fn malformed_is_the_only_standing_that_permits_replacement() -> Bool { + standing_may_supersede_generation(standing: LoadDocumentMalformed) + && !standing_may_supersede_generation(standing: LoadUnsupportedProtocol) + && !standing_may_supersede_generation(standing: LoadIdentityCollision) + && !standing_loaded(standing: LoadDocumentMalformed) +} + +// ONLY A NAMED-BUT-ABSENT PROTOCOL MEANS THE WRITER MAY BE NEWER; a matched tag carrying an +// unrecognized member or variant is a BROKEN DOCUMENT OF THIS SCHEMA, because this format's own +// policy is that a schema which grows a member grows a new tag. Pairing them is the whole test. +test fn only_an_unrecognized_format_is_a_protocol_gap() -> Bool { + let protocol = closure_document_load_standing(cause: ClosureDocFormatUnrecognized { found: "v9" }) + let member = closure_document_load_standing( + cause: ClosureDocUnexpectedMember { context: ClosureDocEnvelopeMembers, key: "provenance" } + ) + let tag = closure_document_load_standing(cause: ClosureDocUnknownKindTag { found: "quantum" }) + standing_eq(a: protocol, b: LoadUnsupportedProtocol) + && standing_eq(a: member, b: LoadDocumentMalformed) + && standing_eq(a: tag, b: LoadDocumentMalformed) +} + +// AN UNRESOLVED POSITION IS MALFORMED, NOT A FETCHABLE ABSENCE. A position denotes nothing outside +// its own document, so nobody could supply what it names. Fetchable absence is the UNCONTAINED arm, +// which is not a refusal and never reaches this classifier at all. +test fn an_unresolved_position_is_malformed_not_fetchable() -> Bool { + standing_eq( + a: closure_document_load_standing(cause: ClosureDocEdgeTargetUnresolved { reference: "7" }), + b: LoadDocumentMalformed + ) + && standing_eq( + a: closure_document_load_standing(cause: ClosureDocTargetNotOneArm { found: 2 }), + b: LoadDocumentMalformed + ) +} + +// THE DATA-LOSS GUARD. object_store rules both colliding records legitimate, so a collision means +// the structural locator cannot represent both -- not that either is damaged. Classifying it as +// malformed would let a reader discard a writer's valid generation over an honest hash coincidence. +test fn an_honest_collision_is_its_own_standing_and_never_supersedes() -> Bool { + let collision = closure_document_load_standing( + cause: ClosureDocObjectCollision { identity: ls_full().root } + ) + let malformed = closure_document_load_standing(cause: ClosureDocNotAnObject) + standing_eq(a: collision, b: LoadIdentityCollision) + && standing_eq(a: malformed, b: LoadDocumentMalformed) + && !standing_may_supersede_generation(standing: collision) + && standing_may_supersede_generation(standing: malformed) +} diff --git a/dag/test/claim/scm_repository_envelope_witness_test.dag b/dag/test/claim/scm_repository_envelope_witness_test.dag index 47b631a46dc..c2343ca7045 100644 --- a/dag/test/claim/scm_repository_envelope_witness_test.dag +++ b/dag/test/claim/scm_repository_envelope_witness_test.dag @@ -23,14 +23,16 @@ import v2.std.node { import gunbc.scm.object_store { ObjectId, ObjectStore, StoreOutcome, Stored, LocatorCollision, empty_store, object_id_eq, store_node, store_object_count, + CopyOutcome, Copied, CopySourceAbsent, CopyLocatorCollision, copy_object, } +import gunbc.scm.commit_closure { uncontained_targets } import gunbc.scm.repository_envelope { RepositoryEnvelope, RepositoryCommit, RepositoryDecodeOutcome, RepositoryDecoded, RepositoryDecodeRefused, empty_repository, encode_repository, decode_repository, repository_envelope_format_tag, repository_node_identity_rule_tag, RepositoryEncodeOutcome, RepositoryEncoded, RepositoryEncodeRefused, - RepositoryEncodeRefusal, RepositoryEncodeClosureDocRefusal, + RepositoryEncodeRefusal, RepositoryEncodeUncontainedTarget, RepositoryCommitRootNotInStore, RepositoryCheckoutNotInCommits, RepositoryDecodeRefusal, RepositoryDecodeClosureDocRefusal, RepositoryIdentityRuleUnrecognized, @@ -425,7 +427,7 @@ fn scm_env_encode_cause_tag(outcome: RepositoryEncodeOutcome) -> String { RepositoryEncoded { value: _ } => "encoded" RepositoryEncodeRefused { cause: cause } => match cause { - RepositoryEncodeClosureDocRefusal { cause: _ } => "closure_document" + RepositoryEncodeUncontainedTarget { target: _ } => "uncontained_target" RepositoryCommitRootNotInStore { root: _ } => "commit_root" RepositoryCheckoutNotInCommits { selected: _ } => "checked_out" } @@ -453,6 +455,49 @@ fn scm_env_non_commit_checkout_repository() -> RepositoryEnvelope { } } +// AN UNCONTAINED TARGET REFUSES TO ENCODE, AND THE REFUSAL NAMES IT. +// +// This claim exists because the arm it drives had no witness at all: the encode-cause helper +// enumerated it, and every test asserted one of the OTHER two tags. The arm was reachable -- a +// grafted store whose root's children were never copied produces it -- and simply unoccupied, so +// changing its payload from a rendered String to an ObjectId would have compiled green with nothing +// establishing that the identity survives. +// +// So the tag is not the whole assertion. It also checks the CARRIED target against the store's own +// uncontained population, which is the property the type change was for: an identity left as text is +// one nobody can resolve back, and a tag-only claim could not tell the two apart. +test fn scm_env_an_uncontained_target_refuses_to_encode_and_names_it() -> Bool { + let full = scm_env_put(store: empty_store(), n: scm_env_program_two()) + let grafted = match copy_object(from: full.store, to: empty_store(), identity: full.identity) { + Copied { store: s, identity: _ } => s + CopySourceAbsent { identity: _ } => empty_store() + CopyLocatorCollision { identity: _, existing: _, incoming: _ } => empty_store() + } + let repo = RepositoryEnvelope { + store: grafted, + commits: [RepositoryCommit { root: full.identity, message: "grafted" }], + checked_out: none, + } + let outcome = encode_repository_checked(repo: repo) + (scm_env_encode_cause_tag(outcome: outcome) == "uncontained_target") + && scm_env_named_target_is_uncontained(outcome: outcome, store: grafted) +} + +fn scm_env_named_target_is_uncontained(outcome: RepositoryEncodeOutcome, store: ObjectStore) -> Bool { + match outcome { + RepositoryEncoded { value: _ } => false + RepositoryEncodeRefused { cause: c } => + match c { + RepositoryEncodeUncontainedTarget { target: t } => + fold(uncontained_targets(store: store), init: false, f: fn(acc, id) { + acc || object_id_eq(left: id, right: t) + }) + RepositoryCommitRootNotInStore { root: _ } => false + RepositoryCheckoutNotInCommits { selected: _ } => false + } + } +} + test fn scm_env_a_commit_root_absent_from_the_store_refuses_to_encode() -> Bool { scm_env_encode_cause_tag(outcome: encode_repository_checked(repo: scm_env_absent_root_repository())) == "commit_root" } @@ -552,9 +597,3 @@ test fn scm_env_an_unknown_commit_member_refuses() -> Bool { ) ) == "unexpected_member" } - - - - - -