Repository navigation
fabric-M0 F0: the durable-completion authority, before the cache - #10641
Conversation
M0's five properties are only worth having if "successful" already means the
required result is safely there, so the completion gate is modeled first and
the cache is left for F4. A cache accelerates a completion contract and cannot
supply one; building the fast path first produces something that looks like
storage without establishing the property M0 needs.
admit_job_publication is total over four outcomes and deliberately ordered. The
computation verdict is read first and short-circuits, so a successful storage
finalization cannot invert a failed build -- unreachable rather than merely not
written. Success is the last arm, reached only when every declared obligation
and the manifest itself were read back at the origin.
Two constructions rather than validations. Digests are typed Sha256Digest and
not the ContentHash union: std.content_hash makes Fnv1a64Structural a structural
fingerprint minted for computation identity, and all three existing
ArtifactRequest variants are keyed by it, so a union-typed field would let a
fingerprint stand where byte integrity is required. Pinning the family at the
carrier is the GateRosterHash precedent and leaves the confusion no
representation. And ArtifactObligationsDeclared{required: []} is a different
constructor from ArtifactObligationsUnreadable: with a bare list, an unreadable
declaration is satisfiable by producing nothing, which is the obligation set
shrinking to match whatever arrived.
ReadbackUnknown is a third arm because a lost acknowledgement does not establish
that a write failed. Folding it into either decided arm is an absorbing
fallback: into failure it discards a possibly-committed result, into success it
greens over bytes nobody read.
The gate roster imports FabricCiGate rather than minting an M0 twin, and carries
no status field, matching fabric_ci_program: standing comes only from an
executed receipt. M0 stays distinct from the FCI-0..6 migration.
Evidence, executed through claim_batch on the discovered path: 14 witnesses
PASS. Mutation controls -- missing artifact silently satisfied breaks 3
witnesses; the pending arm collapsed into failure breaks exactly
a_lost_acknowledgement_is_pending_not_decided; storage success inverting a
failed build breaks exactly a_failed_build_stays_failed. Baseline and restored
are all-PASS with the source byte-identical.
Scope: model grain. Every OriginReadback here is authored, not observed, so this
establishes that the authority refuses correctly when handed the facts and
nothing about whether a runner, origin or teardown path hands it those facts.
F1 needs origin authorization; F3 needs the runner completion boundary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CjN9HVXtvsmHak11cPDHSd
…, keep the work list
Four contract gaps found by adversarial review of the first commit, each confirmed
by reading the source it landed.
RECEIPTS DISCARDED THEIR DIGEST. The receipt branch read
ReadbackConfirmed { observed_digest: _ } => ObligationSatisfied, so any confirmed
readback satisfied a required receipt while artifacts got a real comparison -- the
same check, missing one carrier over. CommittedReceipt now carries recorded_digest
and runs the identical verification. This is distinct from the readback-provenance
debt: that asks whether the observation came from the required read, this asks
whether the observed content matches the required object at all.
THE MANIFEST NAMED NOTHING. It carried attempt and digest only, so the contract
established "these objects each passed, and something labelled this attempt's
manifest passed" and never "that manifest names THESE objects at THESE contents".
Independent checks of several objects do not establish the relations between them,
and reconstruction after the worker is gone needs the relation. ManifestMember
makes membership expressible and it is now checked per required object.
SELECTION DROPPED EVIDENCE BEFORE ANY FOLD SAW IT. committed_artifact_for took
filter(...) |> first, so a second conflicting or undetermined row for one identity
was discarded at selection -- and evidence dropped there cannot be recovered by
preserving everything after it. Ambiguity now refuses and names the count.
THE OUTCOME SHORT-CIRCUIT ATE THE EVIDENCE. A failed computation returned before
any obligation was examined, so a job that failed to compile AND had an unresolved
receipt upload reported the compile error and silently dropped the reconciliation
work. The assessment is now computed unconditionally -- a pure fold over supplied
observations, so it performs no read and delays nothing -- and only then does the
computation verdict choose the outcome. Failure dominance is a property of the
result, not of skipping the assessment.
The same information loss existed one branch deeper and is fixed by the same move:
an unreadable artifact roster used to stop the RECEIPT roster being assessed at
all. ObligationUnenumerable is a verdict rather than an early return, so a roster
nobody could list is reported beside the sibling roster's real findings. All four
non-success arms carry the unresolved population.
Evidence, claim_batch on the discovered path: 23 witnesses PASS. Mutations, each
against one captured baseline with the source restored IDENTICAL after: receipt
digest ignored breaks 4; manifest content never compared breaks
a_manifest_naming_the_wrong_content_prevents_success; ambiguity resolved by
position breaks conflicting_rows_for_one_identity_refuse; failed build dropping
the work list breaks a_failed_build_still_reports_an_unresolved_receipt.
STILL OPEN, and not closed by this commit: the function CHECKS supplied
obligations, it does not DERIVE them, so a caller can still author a declared-empty
roster. The gate's independent-derivation requirement remains an input-boundary
obligation for F2/F3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CjN9HVXtvsmHak11cPDHSd
|
Pushed
The structural fix for the last one — short-circuit the choice of outcome, never the collection of evidence — also fixed the same loss one branch deeper: an unreadable artifact roster used to stop the receipt roster being assessed at all. Evidence,
Two things this does not establish, stated so the green is not read for more than it earns. The function checks supplied obligations, it does not derive them — a caller can still author a declared-empty roster, so the gate's independent-derivation requirement stays an input-boundary obligation for F2/F3. And every — sent from warm-moth-142 |
…ts commit boundary THE ROSTER IS DELETED, and unconsumed was the smaller half of the reason. The roadmap authority already declares this program: gunbc.roadmap.roadmap_authority carries compute-artifact-return-and-materialization -- "Work that cannot hand back what it produced has not succeeded" -- whose red controls are the walls this branch builds (a run reporting success while a declared output is absent refuses; an output whose content does not match what the manifest claims refuses). A second declaration of one program under invented F0..F6 names is the DESIGN 3 fork, and the roadmap row has an owner while the roster had no consumer at all. gunbc.fabric_m0_commit is the consumer the completion authority lacked, and the only place allowed to turn what the store said into the readback that authority takes as given. Deriving it at each call site would put the rule below into every caller -- a second representation of the arm that costs nothing to get wrong locally and everything to get wrong once. THE RULE: a store answer and the ABSENCE of one are different facts. std.durable_compare_and_set distinguishes committed, precondition-failed and store-refused, and each is something the store SAID. A write that timed out or whose acknowledgement was lost obtained none of them, so rendering it as any CasOutcome fabricates a store verdict to fit the branches available. CommitTransportOutcome makes StoreAnswered and StoreDidNotAnswer siblings, never arms of one another, and an unanswered write with the object not yet visible is PENDING -- collapsing it to absence re-executes work that may already be committed while looking maximally conservative. Evidence, claim_batch on the discovered path: 6 commit witnesses PASS, 23 completion witnesses still PASS after the deletion. Mutations against one captured baseline, source restored IDENTICAL: collapsing an unanswered write to absence breaks an_unanswered_write_with_no_object_yet_is_pending. A MUTATION FOUND A COVERAGE HOLE RATHER THAN A WALL, and it is recorded because the first reading was wrong. Collapsing the unavailable-read arm broke NOTHING, which is not a passing control but a report that no witness reached that branch: the existing case exercised StoreAnswered, while the mutation hit StoreDidNotAnswer -- the double-unknown, where the write was never acknowledged AND the confirming read could not run, which is precisely the case with the strongest pull toward a falsely conservative collapse. With an_unanswered_write_with_an_unavailable_read_is_pending added, that same mutation now breaks exactly it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjN9HVXtvsmHak11cPDHSd
…relation std.durable_compare_and_set declares this boundary rather than closing it. Its CasSlotVersion deliberately drops the slot key, and it records the consequence in its own source: an unkeyed observation describes NO slot, cas_decide never reads attempt.key, so an observation read from slot B still commits to slot A when the generations agree. It names the realization as the owner of that relation. gunbc.fabric_m0_commit is that boundary, so the comparison lives here or nowhere. The key is DERIVED from the attempt rather than passed beside it. A caller-supplied key next to a caller-supplied attempt is two spellings of one fact that can disagree, which is the shape the CAS module itself had to correct once when it removed its duplicate key. A foreign-slot answer is UNKNOWN, never absence and never confirmation. This is not a softer refusal: an answer about someone else's slot carries no information about ours, so reporting absence asserts something nobody observed, and reporting confirmation IS the wrong-slot commit the check exists to stop. Unknown with a located cause cannot publish success. Evidence, claim_batch on the discovered path: 8 witnesses PASS, source restored IDENTICAL after mutation. The two foreign-slot witnesses guard DIFFERENT properties, which the mutations establish rather than assume: removing the slot check breaks only an_answer_about_a_foreign_slot_cannot_publish_success, while reporting the mismatch as absence breaks that one AND a_foreign_slot_answer_is_not_reported_as_absence. Neither witness alone covers the wall, and the second is deliberately not discriminating for the first mutation -- counting two reds without separating them would have supported the false reading that both guard the check's existence. NOT ATTEMPTED, and the reason is in the CAS source rather than in effort: the digest-to-payload pairing cannot be closed here. content_hash_of_value is not generic over T, so no total hashing function over an arbitrary payload exists to derive the digest from the value, and a check at this layer would be validation that cannot verify anything. That obligation belongs to the byte-owning store, which does not exist until a real origin does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjN9HVXtvsmHak11cPDHSd
…his invocation A DEFECT IN THE LANDED FOLD, found on review. CasPreconditionFailed mapped straight to ReadbackAbsent, short-circuiting before the independent read, and that is wrong in precisely the case this module exists to serve. A precondition failure proves only that the requested conditional transition did not happen on THIS invocation; it does not prove the desired durable state is missing. The slot key is derived from the attempt, so the occupant of attempt A's slot is most often A's OWN earlier write. That makes the failing path ordinary rather than exotic: A writes, the acknowledgement is lost, the retry asks create-if-absent, the store answers precondition-failed because A's manifest is already sitting there, and the old branch reported ABSENT for a manifest that is committed and readable -- failing a job whose result was safely stored, which is the exact inversion of the property M0 exists to establish. THE REPAIR REMOVES A BRANCH RATHER THAN ADDING ONE. Once the principle is stated -- the read decides what durable state EXISTS, the write outcome decides only what happened to this invocation -- all three StoreAnswered arms collapse into one. The CasOutcome was never legitimately deciding durable state; it was doing a job that belongs to the observation. The fold is now driven by the read and consults the transport for exactly one thing: whether "not found" is ESTABLISHED (the store answered) or merely UNOBSERVED (it did not). A conflicting occupant is still caught, one layer up rather than by this branch: the readback digest is compared against the recorded manifest digest, so a foreign manifest at our slot reports a content mismatch instead of success. The reconciliation is earned by the digests matching, never by the arm. Evidence, claim_batch on the discovered path: 11 commit witnesses PASS, 23 completion witnesses unaffected, source restored IDENTICAL. THE REGRESSION CONTROL IS PERMANENT, NOT RETIRED. Reinstating the old precondition-failed-to-absent branch turns a_precondition_failure_reconciles_when_our_manifest_is_read_back red and NOTHING ELSE -- the conflicting-manifest and no-object witnesses do not move, because they guard the opposite over-correction. That separation is measured rather than assumed; counting three new witnesses and one red mutation would have wrongly credited all three with catching this. The short-circuit is the tempting shape, since "precondition failed means somebody else holds it" reads as conservative, so the probe that was red before the repair stays enrolled as the wall against its return. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjN9HVXtvsmHak11cPDHSd
THE HOLE THIS CLOSES WOULD HAVE HOLLOWED OUT THE MILESTONE. OriginReadback and ManifestReadbackObservation carried no source, so a confirmation derived from a local cache read was indistinguishable from one derived from the durable origin -- and the authority would publish success. "Durably committed" was satisfiable by reading the copy on the machine that is about to be lost. It is not a far-fetched confusion, it is the expected one. The cache and the origin address the SAME immutable object by the SAME content key, so the bytes and the digest are identical and only the source separates them. Same content identity, different AUTHORITY. The sentence that makes it concrete: the local copy is the same CAS, so committing to it counts as durable. It must not. ReadSource is now carried on the confirming read, and a DisposableCacheRead yields Unknown rather than confirmation -- unknown rather than absence, because a cache hit says nothing about what the origin holds in either direction. The two witnesses are a matched pair by construction: identical digest, identical transport, opposite verdict, so the refusal is attributable to the SOURCE and not to anything about the content. Mutating the cache arm to confirm breaks exactly the cache witness and leaves its origin twin green. RUNG, STATED HONESTLY RATHER THAN IMPLIED. This is a DECLARED BOUNDARY, not a construction, and it sits at mitigatable. DurableOriginRead is freely authorable from any module, so a caller can still assert origin provenance for a cache read; nothing here makes that unwritable. What changed is that the distinction is STATABLE and REFUSABLE where before there was nowhere to record it at all -- the class moves from unmodelled to declared, which is a smaller claim than a wall and the honest one. NEXT-RUNG TRIGGER, and it is the same one std.materialization_provider already records against its own readback assertion: constructor confinement, so that a ReadSource of DurableOriginRead can be minted only by the modeled origin read rather than hand-built. That needs a byte-owning origin to exist, so it is genuinely blocked on the origin rather than unbuilt -- and it is deliberately NOT claimed as done here. Evidence, claim_batch on the discovered path: 13 commit witnesses PASS, 23 completion witnesses unaffected, source restored IDENTICAL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjN9HVXtvsmHak11cPDHSd
PARENT CORRECTION ACTED ON. The M0 invariant is a SEPARATION, not a placement: durable truth and warmth are different products, every local byte is rehydratable, the origin's failure domain is DISJOINT from the worker's, and success is readback from the origin whichever origin that is. Off-fleet is one realization satisfying that, not the requirement -- bringing durable bytes back on-fleet must stay a second binding rather than a redesign. AUDIT RESULT: the two modules were already clean of the thing that would have foreclosed it. No provider, vendor or placement vocabulary in either; imports are std.types, std.content_hash and std.durable_compare_and_set, which is itself modeled as a shape rather than a transport. The completion authority never learns where bytes live, and ReadSource separates DurableOriginRead from DisposableCacheRead on AUTHORITY rather than location -- so an on-fleet origin binds as another realization of the same interface. WHAT THE AUDIT DID FIND, same class, smaller: commit_slot_key spelled "fabric-m0/commit/" inline. A versioned namespace is a binding decision owned by whoever selects and configures the origin, so a literal prefix inside the fold is business policy sitting in an interface -- the layer inversion whose tell is exactly this, a literal carried where a parameter belongs. The namespace is now received as CommitNamespace. The KEY stays derived, because that half has a correctness argument the namespace does not: a caller-supplied key beside a caller-supplied attempt is two spellings of one fact that can disagree, which is the shape std.durable_compare_and_set had to correct once already. Receiving the namespace and deriving the key from it keeps one authority for the derivation while leaving the namespace free to be versioned or re-homed. WALL LIVENESS RE-ESTABLISHED AFTER THE REFACTOR, not assumed. The change edits the exact expression the slot check tests, and the witness spells the expected slot LITERALLY while the module derives it -- two independent spellings, which is what keeps the comparison from collapsing into value-equals-itself. Both mutations still break exactly their own witness: removing the slot check breaks the foreign-slot witness, treating a cache read as confirmation breaks the cache witness. 13 PASS, source restored IDENTICAL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjN9HVXtvsmHak11cPDHSd
…presentable
The completion authority carried `readback` as a FIELD of each committed row and
of the commit manifest. A readback therefore existed for every committed object
by construction, and the question "was this object actually read back?" had no
representation: the nearest expressible thing was ReadbackUnknown, a value a
caller had to deliberately choose. Both halves of the digest comparison arrived
from one caller, so a wholly satisfied assessment established that somebody
wrote down a confirmation, not that a read happened. That is the seeded-true
fold answering "complete" over a population it never examined.
Split the read out as its own population and let the AUTHORITY perform the join.
OriginObservation { subject, readback } is supplied separately; each required
subject looks up its own observation; a subject with no observation is
Undetermined -- not Unsatisfied, because an unperformed read says nothing about
whether the object is there -- and two observations for one subject refuse
rather than letting position pick the agreeable one. The unexamined case is now
the DEFAULT state of an empty population rather than an option a caller must
remember to select.
The commit boundary DERIVES the manifest's observation from the transport and
the read rather than accepting one, so the only observation under that subject
is bound to the slot the manifest actually lives at.
What this does not establish, stated plainly: nothing here proves a reader ran.
That is the observation-to-slot boundary std.durable_compare_and_set already
declares outside its guarantee. Manifest membership is likewise still not bound
to the bytes whose digest was read; closing that needs a SHA-256-over-bytes
primitive, which std does not have (sha256_hex_digest PARSES a hex string, it
does not compute a digest from Bytes). That missing primitive is the next-rung
trigger, not a stall.
Evidence: 26/26 completion and 13/13 commit witnesses pass. Seeding the no-read
case to ObligationSatisfied reddens exactly the two new pending witnesses and
nothing else.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CjN9HVXtvsmHak11cPDHSd
…ts trigger as a capability DESIGN 4b obliges a discovered class to file a row. This one is already rostered: gunbc.recurring_failure_mode.subject_and_its_digest_as_independent_parameters describes a value and a summary of it standing side by side with no arm relating them, which is manifest_digest beside members exactly, and it already declares both the repair and a capability trigger. Minting a new class would be net concepts growing by re-invention, so this appends a receipt instead. WHAT THE RECEIPT ADDS. Applying that row's repair in .dag separates two shapes a name search conflates: extdeps.crypto.hash sha256_digest takes HEX, so an author asserts a digest and nothing computes it, while extdeps.tools.sha256sum sha256sum_file_digest_via_shell takes a PATH and COMPUTES over the bytes there with a typed unavailable arm -- a section 3 handler bound to a digest shape, one of N, live in gunbc.instruments.fabric_control_plane_live_probe. No .dag function takes Bytes and returns a digest, so the repair is reachable exactly where the subject bytes exist at a path. ANSWERED EXPLICITLY RATHER THAN BY DEFAULT: this instance is NOT blocked. Its subjects are stored objects, so a durable-origin readback that stages what it fetched puts those bytes at a path and the chain is buildable at a mitigatable rung, with the in-substrate SHA-256-over-Bytes primitive as the CLIMB. The constraint that realization must respect is recorded with it: the digested file must be the origin readback's own output and never a cache copy, or a correct computation over the wrong provenance launders the cache-read refusal in fabric_m0_commit manifest_readback_for_slot into a confirmation. RECORDED, NOT REPAIRED: this row's trigger ends by naming gunbc.guarantee_stall wet_route_model_lags_seed_stall as the population it retires, and no such row exists; content_digest_makes_annotations_semantically_load_bearing cites the same absent name. The nearest survivor, self_host_wet_route_receipt_lifetime_stall, is about route families and receipt lifetime and is not this population, so repointing it would invent the join this class files. Under 4b(3) a trigger naming a population that does not resolve is retired by nothing. An earlier draft of the receipt claimed every digest in the corpus is asserted or verified out-of-band. That folded the two shapes together and is withdrawn in the text that lands. Evidence: 26/26 completion witnesses pass; docs/design-failure-modes.md regenerated from the authority via generated_artifact_gate main_wet_one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjN9HVXtvsmHak11cPDHSd
# Conflicts: # docs/design-failure-modes.md
Ledger-Repair-Judged: docs/design-failure-modes.md Ledger-Rows-Repaired: docs/design-failure-modes.md subject_and_its_digest_as_independent_parameters Ledger-Repair-Judged: docs/design-rung-drops.md
resolve_output_obligations now takes requirements and refuses a foreign work_key (WorkKeyDoesNotMatchContract). Declared frontier: warm-moth-142 admit_job_publication (#10641). Reclassify content-addressed digests as opaque. Co-authored-by: Cursor <cursoragent@cursor.com>
This module minted AttemptId = NonEmptyStr where brand("AttemptId") for the
executor of a job. std.scoped_authorization already declares AttemptIdentity for
that entity -- the consumer identity a grant is issued to, where a grant for
attempt A refuses attempt B -- and gunbc.auth.approval_broker uses that same type
for who executes a work claim, in WorkClaimedBy, WorkCompletedBy and
WorkAbandoned. One concept under two names is the section 3 nicknaming
violation, and I introduced it by coining before DFS-ing the concept DAG.
Dissolved rather than carried. A second spelling outlives the mistake, and the
fabric-m0 gate is about to be wired to a carrier that binds work and attempt, so
a nickname here would propagate into that join rather than stopping at this
module. The concept's home is confirmed by existing consumers:
test.claim.approval_capability_witness and test.claim.approval_broker_witness
already import AttemptIdentity from std.scoped_authorization.
Independently reached by snappy-lark-51, who owns the OutputContractRef cut and
said it would flag the same fork.
No cycle: std.scoped_authorization imports only std.types, std.content_hash,
std.effect_grant and std.durable_compare_and_set.
Evidence: 26/26 completion and 13/13 commit witnesses pass. A rename that
typechecks can still have collapsed a comparison, so the attempt binding was
re-proved live rather than assumed -- dropping the attempt conjunct from the
committed-row filter reddens exactly
a_foreign_attempts_artifact_does_not_satisfy_this_one, and the module restores
byte-identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CjN9HVXtvsmHak11cPDHSd
resolve_output_obligations now takes requirements and refuses a foreign work_key (WorkKeyDoesNotMatchContract). Declared frontier: warm-moth-142 admit_job_publication (#10641). Reclassify content-addressed digests as opaque. Co-authored-by: Cursor <cursoragent@cursor.com>
# Conflicts: # docs/design-failure-modes.md
Ledger-Repair-Judged: docs/design-failure-modes.md Ledger-Rows-Repaired: docs/design-failure-modes.md subject_and_its_digest_as_independent_parameters Ledger-Repair-Judged: docs/design-rung-drops.md
…esolution (#10713) * Split OutputContractRef: opaque identity stays; obligations become a sealed roster OutputContractRef held both plain names and content-serialised obligation payloads, so no consumer could tell which it held. Keep the opaque name, put DeclaredOutputObligations on WorkContract, and mint terminal-success rosters only through resolve_output_obligations (sole_constructor RED). Co-authored-by: Cursor <cursoragent@cursor.com> * Stamp judgment policy on ResolvedOutputObligations; path-scope the sole_constructor rung Warm-moth-142: revision identity alone does not answer start-vs-gate; the sealed result now carries OutputObligationsJudgment. Also correct the unwritable claim to path-scoped rung 4 on source→.dag with a named degradation when a Rust seed consumer appears. Co-authored-by: Cursor <cursoragent@cursor.com> * Move OutputContractRef annotations to module-item grain Body and field-inline // comments refuse under the annotation channel; CI floor failed on WorkContract and required_build_contract_from_materials. Co-authored-by: Cursor <cursoragent@cursor.com> * Make Unreadable reachable and declare the gate frontier resolve_output_obligations now takes requirements and refuses a foreign work_key (WorkKeyDoesNotMatchContract). Declared frontier: warm-moth-142 admit_job_publication (#10641). Reclassify content-addressed digests as opaque. Co-authored-by: Cursor <cursoragent@cursor.com> * Unit-variant Unreadable cause needs {} construction form A bare `= WorkKeyDoesNotMatchContract` parsed as a type alias, so resolve and heal both refused unresolved type / undefined variable. Co-authored-by: Cursor <cursoragent@cursor.com> * Match WorkKeyDoesNotMatchContract with unit-variant {} form Co-authored-by: Cursor <cursoragent@cursor.com> * Brand OutputObligationsRevision so foreign digests are ill-typed Bare ContentHash alias let any digest inhabit the sealed revision field; brand matches every other identity carrier in work.dag (§3 / §6). Co-authored-by: Cursor <cursoragent@cursor.com> * Mint OutputObligationsRevision as branded NonEmptyStr ContentHash is a sum; casting a variant into ContentHash-where-brand refused at runtime. Same identity shape as WorkKey: serialize then brand. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop unused serialize_content_hash import from resolution witness Co-authored-by: Cursor <cursoragent@cursor.com> * Record that obligation revision mints only Fnv1a64 structural serialize_content_hash is family-asymmetric; this carrier never enters the other arms, so revision identity is not riding the incidental width split. Co-authored-by: Cursor <cursoragent@cursor.com> * Keep artifact vs terminal as kinds on one obligation roster Concatenating required-build manifests made split and collapsed id lists the same WorkKey. Kind is identity material; resolution still returns ids. Co-authored-by: Cursor <cursoragent@cursor.com> * Name obligation kinds at construction instead of defaulting to artifact. An ids-only constructor silently stamped OutputArtifactObligation, so terminal subjects (floor summary, outputs-verdict) minted the wrong work key. Training now owes a stable adapter id rather than reusing the contract digest. Co-authored-by: Cursor <cursoragent@cursor.com> * Admit #10706 OutsideModeledGuarantee citations as next-rung triggers. Declarations was failing the floor on three stamp required_capability refs and one fixture absence. Those names must stay unresolved; PLANTED_CONTROL is the wrong roster because resolve there means a lost control. Same join as Co-authored-by: Cursor <cursoragent@cursor.com> * Seal resolved obligations with kind, not ids alone. The work key already treated artifact vs terminal as distinct owed subjects; dropping kind at resolve made same-id mixed rosters collapse for the gate. Co-authored-by: Cursor <cursoragent@cursor.com> * Enumerate the next-rung trigger helpers on the declaration-index seed-growth roster. The four production fns and the discriminating test landed in the same boundary the justification already lists at item grain; leaving them off the roster was a silent delta. Co-authored-by: Cursor <cursoragent@cursor.com> * Enroll NEXT_RUNG_TRIGGER_CITATIONS on the declaration-index seed-growth roster. The join helpers were listed; the production roster they close over was not. PRE_EXISTING_CITATION_DEBT is already a DeclarationRef on the same justification. Co-authored-by: Cursor <cursoragent@cursor.com> * Keep the sealed-roster kind note and drop the #10718 occupancy comment. PLANTED_CONTROL stays empty; #10706 stamp sites remain on NEXT_RUNG_TRIGGER_CITATIONS. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
product.fabric.work (gunbc#10713) landed the carrier this gate was waiting on, and it retires the §3c consumption frontier that PR names: admit_job_publication now takes an OutputObligationsResolution instead of a caller-supplied coproduct. THE DEFECT THIS CLOSES. The caller used to CHOOSE between a declared roster and an unreadable one, so a caller that never read the obligations could declare empty and mint terminal success. ResolvedOutputObligations is sole_constructor and its only mint is resolve_output_obligations, so an empty roster is now a RESOLVED value that cannot be asserted here. The witnesses had to change shape to match: they construct a real WorkContract and resolve, because a hand-built resolution is unwritable. WHAT THE SEAL DOES NOT CLOSE, AND THIS GATE NOW DOES. A sealed resolution is unforgeable but still SUBSTITUTABLE -- a caller may hold a perfectly valid, internally consistent resolution for work B while asking the gate to judge work A. Nothing upstream can catch that, because only the gate knows which work it was asked about. admit_job_publication therefore takes the WorkKey it is judging and refuses a mismatch. THREE TYPES AND A DUPLICATED VERDICT PATH DELETED, all my own coinage. RequiredArtifactId and RequiredReceiptId forced two committed-row types and two verdict functions differing only in type name, so the duplication looked like domain structure while being invented vocabulary. One OutputObligation replaces them. Kind is part of the key, not decoration: an artifact and a terminal result may share an id, so every join compares id AND kind. PublishObligationsUnreadable is RE-HOMED, NOT DELETED. Making output_obligations a required field of the contract makes "the roster could not be read" unwritable, which would have left that arm uninhabitable -- the decoration 4b calls worse than absent. It survives because two reachable states now produce it: a typed OutputObligationsUnreadable, and a resolution bound to a foreign work. Evidence: 29/29 completion and 13/13 commit witnesses. Both new walls are mutation-controlled and each RED is specific -- removing the work-key comparison reddens only a_resolution_for_another_work_cannot_publish_success; dropping kind from the join reddens only the_same_id_under_another_kind_does_not_discharge_it; the module restores byte-identical. Two fixtures initially failed because they judged a resolution derived from a different contract, which is the new wall working rather than a defect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjN9HVXtvsmHak11cPDHSd
…ion/warm-moth-142 # Conflicts: # docs/design-failure-modes.md
Ledger-Repair-Judged: docs/design-failure-modes.md Ledger-Rows-Repaired: docs/design-failure-modes.md subject_and_its_digest_as_independent_parameters Ledger-Repair-Judged: docs/design-rung-drops.md
DESIGN 3c admits a declared frontier only with its trigger stated beside it. publish_after_commit was annotated as "the consumer" while itself having no production consumer: outside the enrolled witnesses, nothing imports either fabric_m0 module. That honest state was inferable only from an import census, not from the module. State it in the module, and state the trigger as the CAPABILITY that consumes it -- a job-publication path returning declared outputs by content through a readable manifest -- rather than as one caller or one job, which would be satisfied while the capability stayed dead. No declarations change; the gate's walls stay enrolled and executing, so this is a consumption frontier and not a 4b(3) rung drop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjN9HVXtvsmHak11cPDHSd
# Conflicts: # dag/gunbc/recurring_failure_mode/subject_and_its_digest_as_independent_parameters.dag # docs/design-failure-modes.md
DESIGN 4b(1) takes a class's rung as the MINIMUM across its in-scope paths, and rung inflation is worse than sitting low because an inflated class never ranks for climbing. Two annotations claimed rung 4 absolutely: the family-pinned digest carrier as "structurally impossible", and the re-homed unreadable-roster state as "unwritable". Both hold on the source-to-dag acceptance path only -- the emitted Rust mirror of these carriers is a public struct with public fields (gunbc.model.population), so a consumer writing Rust against the seed is outside the guarantee. Both now state that scope with the named degradation condition (no such Rust consumer today) instead of an absolute. product.fabric.work already states this for its own seal; the same caveat was owed here. Annotations only -- no declaration changes. 29 completion witnesses discovered and passing, including both discriminating walls. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjN9HVXtvsmHak11cPDHSd
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 688a4a0023
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| resolution: OutputObligationsResolution, | ||
| judged_work: WorkKey, | ||
| committed: List<CommittedOutput>, | ||
| observations: List<OriginObservation>, |
There was a problem hiding this comment.
Carry provenance for every output readback
When a required artifact or receipt exists only in a disposable cache, the caller can still encode that cache lookup as an OriginObservation containing ReadbackConfirmed, because this parameter has no ReadSource equivalent to the manifest path. required_output_verdict then treats a matching digest as satisfied, so a durable manifest plus cache-only outputs can produce PublishSuccess, violating the durable-completion guarantee. Derive or source-tag required-output observations at the same origin boundary as the manifest observation.
Useful? React with 👍 / 👎.
| ReadbackUnknown { cause: "manifest read from disposable cache " + pr + ", not the durable origin; durability unestablished" } | ||
| } | ||
| ManifestReadUnavailable { cause: rc } => ReadbackUnknown { cause: rc } | ||
| ManifestNotFound => |
There was a problem hiding this comment.
Preserve provenance on negative manifest reads
When ManifestNotFound comes from a disposable cache rather than the durable origin, this branch maps it to ReadbackAbsent for every StoreAnswered outcome because, unlike ManifestRead, the not-found arm carries no source. The gate can therefore finalize failure and trigger re-execution even while the manifest remains present at the origin; only an origin-sourced not-found result should establish absence.
Useful? React with 👍 / 👎.
| observations: concat( | ||
| observations, | ||
| [manifest_observation(namespace: namespace, attempt: attempt, transport: transport, observed: observed)] | ||
| ), |
There was a problem hiding this comment.
Exclude caller-authored manifest observations
When the supplied observation collection already contains a CommitManifestSubject—which its public List<OriginObservation> type permits—this unconditional append creates two observations for that subject. readback_verdict_for rejects any duplicate, so even two identical durable confirmations become PublishFinalizationFailed; narrow this input to obligation observations or remove/refuse caller-supplied manifest subjects before adding the derived one.
Useful? React with 👍 / 👎.
CasCommitted/CasPreconditionFailed/CasStoreRefused were imported alongside CasOutcome but never matched on; ObservationSubject likewise. The only remaining textual hit is an annotation sentence, not a reference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T1wPj9dCqfaPT4G2AZyKnS
Establishes the fabric-M0 completion gate — the single authority that may call a job successful — before any cache exists. Dispatched brief reordering: a cache accelerates a completion contract and cannot supply one, so building the fast path first produces something that looks like storage without establishing the property M0 actually needs.
What lands
gunbc.fabric_m0_completion—admit_job_publication, total over four outcomes.gunbc.fabric_m0_program— F0..F6 as gate predicates.test.claim.fabric_m0_completion_witness— 14 discoveredtest fnwitnesses.The ordering is load-bearing
The computation verdict is read first and short-circuits, so a successful storage finalization cannot invert a failed build — unreachable, not merely unwritten. Success is the last arm, reached only when every declared obligation and the manifest itself were read back at the origin.
Two constructions rather than validations
Digests are
Sha256Digest, not theContentHashunion.std.content_hashmakesFnv1a64Structurala structural fingerprint minted for computation identity, and all three existingArtifactRequestvariants are keyed by it — so structural-fingerprint-as-key is the surrounding idiom, not a hypothetical slip. A union-typed field would let a fingerprint stand where byte integrity is required. Pinning the family at the carrier is theGateRosterHash/v2.std.node.Hashprecedent, and leaves the confusion no representation.ArtifactObligationsDeclared{required: []}is a different constructor fromArtifactObligationsUnreadable. With a bare list, an unreadable declaration is satisfiable by producing nothing — the obligation set shrinking to match whatever arrived. Checked in both directions: declared-empty must still succeed, or the refusal would be satisfied by an authority that rejects every zero-length roster and the distinction would carry no information.ReadbackUnknownis a third arm. A lost acknowledgement does not establish that a write failed, and an object existing does not establish it is the intended object. Folding unknown into either decided arm is an absorbing fallback: into failure it discards a possibly-committed result and re-executes; into success it greens over bytes nobody has read.Single authority
The gate roster imports
FabricCiGaterather than minting an M0 twin, and carries no status field — matchinggunbc.fabric_ci_program, where standing comes only from an executed receipt. M0 stays distinct from the FCI-0..6 required-check migration.Evidence, executed through
claim_batchon the discovered path14 witnesses PASS. Mutation controls:
a_lost_acknowledgement_is_pending_not_decideda_failed_build_stays_failed_...The middle row is the one worth attention: collapsing the unknown arm leaves every other conjunct green, because the job still does not publish success. A witness without that specific assertion passes it — and collapsing it looks conservative, which is why it belongs in F2 acceptance explicitly.
Scope honesty
Model grain only. Every
OriginReadbackhere is authored, not observed. This establishes that the authority refuses correctly when handed the facts, and nothing about whether a runner, origin, or teardown path ever hands it those facts. Relatedly,std.materialization_providerrecords its own readback-provenance gap —observed_digestis an assertion that the transport computed it from the stored parts — so F2 inherits known debt at that seam rather than a guarantee.F1 is blocked on origin authorization, spend cap, retention and enrolled scope; F3 needs the runner completion boundary.
std.durable_compare_and_setis the authority F2 should consume for attempt-bound commit identity.🤖 Generated with Claude Code
https://claude.ai/code/session_01CjN9HVXtvsmHak11cPDHSd
Consumption (DESIGN §3c): the consumer and the route, per declaration group
Answered in the vocabulary §3c asks for. Two groups are consumed by execution in this change; one is a declared frontier with its trigger stated. None is dangling.
1.
gunbc.fabric.fabric_m0_completion— the gate (admit_job_publication,OutputObligation,CommittedOutput,OriginObservation,ObligationVerdict,JobPublication).Consumer:
gunbc.fabric.fabric_m0_commitpublish_after_commit, in this diff. Route: a direct call —publish_after_commitbuilds the manifest observation and callsadmit_job_publication, and every one of the 42 enrolled witnesses reaches the gate through a real evaluation, not a declaration probe. Executed on this head asstanding=planned-and-passedinrequired-witnesses-floor.2.
gunbc.fabric.fabric_m0_commit—publish_after_commit.DECLARED FRONTIER. No production route reaches it today; outside the witnesses nothing imports either module, and that is stated in the module itself rather than left to an import census.
TRIGGER, STATED AS THE CAPABILITY: a job-publication path that holds a run's required work and decides its ending — the roadmap's
compute-artifact-return-and-materializationcapability, where a computation's declared outputs come back identified by content through a manifest the caller reads. When that path exists it calls this function to turn a commit attempt into an ending. Naming a smaller artifact (one caller, one job, one emitted script) would be satisfied while the capability stayed dead, which §4b(3) forbids.This is a consumption frontier and NOT a §4b(3) rung drop: no rung is lowered by the wait, and the gate's walls are enrolled and executing over the population they are declared for.
3. The two witness modules.
Consumer: the floor, by execution. They exercise the gate's arms rather than asserting declarations exist — each wall is mutation-controlled, and the controls discriminate: removing the work-key comparison reddens only
a_resolution_for_another_work_cannot_publish_success; dropping kind from the obligation join reddens onlythe_same_id_under_another_kind_does_not_discharge_it; the module restores byte-identical.On +1710 / −0. The zero is correct rather than a tell: this adds a new authority for a question nothing in the corpus previously answered, so there is no predecessor structure to delete. It is not a replacement migration, and no second path is being created beside an existing modelled route.
The frontier is mutual, which is new since this PR opened.
gunbc.product.fabric.workonmaindeclares its own §3c frontier on the sealed roster and names THIS gate as its consumer, with the trigger "that gate takesOutputObligationsResolutioninstead of a caller-supplied coproduct." That condition is met at this head:admit_job_publicationtakesresolution: OutputObligationsResolutionplusjudged_work: WorkKey, and no caller-supplied roster parameter survives — every witness fixture goes throughresolve_output_obligations. Landing this PR closes a frontier on both sides.The reciprocal half: this PR CLOSES a frontier main already declared
Stated here so the join is legible in one place rather than reconstructed from two modules.
gunbc.product.fabric.workonmaincarries, verbatim:The thing that satisfies it is
gunbc.fabric.fabric_m0_completionadmit_job_publication, whose signature takesresolution: OutputObligationsResolutionalongsidejudged_work: WorkKey. The trigger is met at this head. No caller-supplied roster parameter survives anywhere in the module, and every witness fixture reaches the gate throughresolve_output_obligations.Rung honesty on that claim, path-scoped (§4b(1)). An earlier revision of this body said a hand-built resolution is "unwritable" unqualified. That is inflation, and
product.fabric.worknames it as such in its own annotation. The seal is structural on the source→.dag acceptance path only — the sole mint isresolve_output_obligations, and constructing the resolved arm by hand is aSoleConstructorViolationwith a fixture RED on that carrier's side. The emitted Rust mirror of asole_constructortype is a public struct with public fields, so a consumer writing Rust against the seed is OUTSIDE the guarantee. Fabric-M0 has no such Rust consumer today, so the rung holds now with a named degradation condition: the first Rust consumer of the seed against this carrier. A class's rung is the minimum across its in-scope paths, so the honest statement is path-scoped, not absolute.Two consequences a reviewer should not have to derive:
mainstates today, from the sidemainnamed. It is not asking to be trusted about a hypothetical future consumer.work_keyagainst the work being judged, so a resolution correctly sealed for a different work cannot publish success —a_resolution_for_another_work_cannot_publish_successis the discriminating control, and removing the comparison reddens only that witness.