docs: ADR-0033 Slice 2 acceptance contract — transactional apply (no implementation) - #184
Conversation
…onal apply Open Slice 2 as a docs-only, separately reviewable acceptance contract (ADR-0033 Decision 10): exact Apply inputs/outputs and per-fact authority, application-index v1 and apply-report v1 schemas, the fail-closed verification order, chain equations, already-applied semantics, the replacement-authority law, staging/commit publication semantics with an enumerated failure model, a bounded evidence-backed preservation law (the historical byte-for-byte claim audited against migrate's re-rendering and core's tolerant parsing), holdout-readiness law, a closed refusal taxonomy, explicit dispositions of historical-proposal rules, prior-art deltas, and the preregistered RED→GREEN acceptance matrix. No implementation: Slice 2 stays prohibited until this contract receives independent acceptance. No corpus files, labels, runtime behaviour, dependencies, or Swang files change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kc6r6PhKQCNqArwkijUnzn
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…s addressed 1. Filesystem names decoupled from plan content: fixed staging name (<output_parent>/.<output_name>.apply-staging) and index temp/lock names derived only from the index path, so step 1 needs nothing parsed and an unrestricted batch_id String can never influence a path (adversarial matrix case F7). 2. Step 9/10 ordering fixed: stage corpus + curated manifest first, then the single song_holdout_preflight and recomputation self-check, and only then build, digest, and write the report — its fields now exist before it is written. 3. Single-writer index lock: exclusive create of <index_dir>/.<index_name>.lock held from before the first index read to after the commit rename; pre-existing lock refuses ApplicationIndexLocked (no waiting, no lock-breaking); stale-lock crash state and recovery added to the §8.2 table; matrix cases C11/C12. Closes the read-to-commit lost-update window that could silently drop a committed record. 4. Report partition corrected to four ways: applied / unchanged / reviewed-unassigned / untouched, so a reject_suggestion source (referenced yet unassigned) is representable; sum equals coverage.unique_sources. 5. Preservation guard made honest about duplicate keys: a distinct duplicate-rejecting JSON pass is required (Value comparison cannot prove duplicates absent — serde_json keeps the last one silently); matrix case K10. 6. Supersession-evidence consistency law: redundant supersedes_song_ids on accept/merge/split must agree with the action's authority set (empty / from_song_ids / [from_song_id]); violation is the new typed SupersessionEvidenceContradiction, checked per event before any per-assignment authority decision; matrix cases L8/L9. Docs-only; still awaiting independent acceptance; implementation remains prohibited. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kc6r6PhKQCNqArwkijUnzn
|
Revision r2 ( An independent hostile review of
Totals: refusal taxonomy now 14 reused + 19 new typed refusals; preregistered matrix now 50 cases. Still docs-only; Slice 2 remains PROPOSED / IMPLEMENTATION BLOCKED, awaiting a repeat independent hostile review at Generated by Claude Code |
|
r2 hostile re-review on exact head
Minor but worth fixing while there: §8.1 says a second applier after the first completes is refused because its input fingerprint no longer matches the head. That is not universally true for the contract's own fingerprint-neutral/reject-only batch (C6); refusal may instead come from duplicate batch or predecessor-report/chain mismatch. Phrase it in terms of the actual chain law, not fingerprint change. No implementation/RED work should start. After these docs-only corrections, I will re-review the next exact head. |
1. The §6 no-mutation invariant is narrowed honestly: the transient index lockfile is its one named exception; lock create/release failures other than pre-existence are typed ApplyIoError (extended to step-1 lock I/O), and a failed best-effort release is surfaced explicitly without changing the run's primary outcome. 2. The canonical resolved index file is declared the one and only index identity: lock name, read, temp, fsync, commit rename, and release all act on the canonicalized path, so symlink aliases contend on one lock and the rename replaces the real file, never an alias (matrix case F8). 3. The step-12 failure row now includes the already-published output tree; recovery removes both the temp file and the orphan. R3 is restricted to pre-publication refusals, deferring post-publication states to F4/R4 and the §8.2 table. 4. Step 10 re-runs the §4.2 tree-agreement law over the staged tree, so a write that updates the manifests but misses a chunk file (or vice versa) aborts as OutputPreflightInconsistent before publication (fault-injected matrix case K11). 5. The Slice-1 refusal surface is split honestly: eleven members reachable through verify_plan, three ledger-side members (UnsupportedDecisionsLedgerSchema, DuplicateDecisionBatchId, BatchNotInLedger) declared intentionally unreachable in Apply and excluded from the coverage claim; new case A8 exercises the reachable inventory/replay refusals at the Apply boundary. 6. Minor: the post-completion second-applier refusal is phrased via the actual chain law (already-applied or a broken §7.2 relation), not a fingerprint change, which a fingerprint-neutral batch would falsify. Docs-only; still awaiting independent acceptance; implementation remains prohibited. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kc6r6PhKQCNqArwkijUnzn
|
Revision r3 (
Totals: matrix now 53 preregistered cases; refusal surface = 11 reachable Slice-1 refusals + 19 new typed refusals (+ 3 ledger-side members documented as unreachable). Still docs-only; Slice 2 remains PROPOSED / IMPLEMENTATION BLOCKED, awaiting re-review at exact head Generated by Claude Code |
|
r3 hostile re-review on exact head
Everything else from r3's intended repairs checks out: staged tree agreement is real, post-publication step-12 recovery is complete, symlink aliases converge on the canonical index path, reachable Slice-1 refusals are separated correctly, and the fingerprint-neutral post-completion wording is fixed. No RED/implementation work should start. After these docs-only corrections, re-review the next exact head. |
1. The missing-index refusal is made reachable again: step 1 first proves the supplied index path resolves to an existing regular file (MalformedApplicationIndex raised there, before canonicalization and the lock, because canonicalize needs an existing target); C7 and the taxonomy row now name both raise points. 2. One unambiguous observable result shape: Apply returns a single primary outcome (applied, or one typed refusal) plus an optional orthogonal lock-release warning. A release failure is never a refusal and never changes the primary outcome; every refusal, ApplyIoError included, is returned only by a run that did not reach the commit point, so a refusal always proves pre-commit. Fault-injection case F9 preregisters both shapes (success+warning, refusal+warning). 3. Reserved-area carry-forward law: the input's song-curation/ proof artifacts describe the input's own application and are superseded, never raw-copied; a §4.2 shape law admits only the two tool-owned artifacts in the reserved area (anything else is CorpusTreeDisagreement); §10.1 excludes the area from the raw-copy law; C2 now chains from the first Apply's real published output and asserts the second reserved area. 4. Coordination-path collisions refused deterministically: new typed OutputCollidesWithIndexArtifacts when output or staging equals the canonical index file, its lockfile, or its temp path — checked at step 1 before lock acquisition, so the lock can never be created at a declared output path; adversarial case F10. ApplicationIndexInsideTree tightened to equals-or-inside. Docs-only; still awaiting independent acceptance; implementation remains prohibited. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kc6r6PhKQCNqArwkijUnzn
|
Revision r4 (
Totals: matrix now 55 preregistered cases; refusal surface = 11 reachable Slice-1 refusals + 20 new typed refusals (+ 3 ledger-side members documented as unreachable). Still docs-only; Slice 2 remains PROPOSED / IMPLEMENTATION BLOCKED, awaiting re-review at exact head Generated by Claude Code |
|
r4 hostile re-review on exact head
Separate integration note, not a Slice-2 semantic blocker: GitHub currently reports PR #184 as Everything r4 intended to repair checks out: missing index now has a reachable pre-lock outcome; release failure is orthogonal to the single primary outcome and no longer masquerades as No RED or implementation work should start. Next revision should remain docs-only. |
1 similar comment
|
r4 hostile re-review on exact head
Separate integration note, not a Slice-2 semantic blocker: GitHub currently reports PR #184 as Everything r4 intended to repair checks out: missing index now has a reachable pre-lock outcome; release failure is orthogonal to the single primary outcome and no longer masquerades as No RED or implementation work should start. Next revision should remain docs-only. |
Base moved by Swang merges (#185–#187). No contract-file changes in this merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kc6r6PhKQCNqArwkijUnzn
1. Hardlink aliases of the index are refused outright: step 1 requires the canonical index file's link count to be exactly one, else the new typed ApplicationIndexHardLinked. canonicalize collapses only symlinks; two hardlinked entries would derive distinct sidecar locks (reviving the lost update) and commit-by-rename would split the aliases into divergent registries. The unqualified aliases-contend-on-one-lock claim in §8.1 is corrected to its symlink-only truth, with the hardlink mechanism spelled out and the flock alternative rejected for needing a non-std dependency (§15). Matrix case F11 (both appliers through different hardlinks refuse). 2. A pre-existing file at the temp coordination path is never silently unlinked: the lock proves no live writer, not ownership of bytes already at that name (which could be an unrelated file or another index whose name equals this index's temp name). Step 1 refuses the new typed ApplicationIndexTempExists; crash debris is removed only by the explicit §8.2 operator recovery. Matrix case C13 (two-index filename collision: the second index is never unlinked). 3. The failure model now enumerates failure of the step-11 publication rename itself: staging present, output absent, index unchanged — or, under an external race, an output path occupied by an entity this run did not create, which is never deleted. F4 extended with the injected publication-rename failure. 4. The reserved-area shape law is wired through the executable surface: step 3 enforces it explicitly with an unambiguous recursive meaning (exactly at most the two tool-owned proof artifacts as regular files at the reserved root; any nested entry or subdirectory is foreign), the CorpusTreeDisagreement taxonomy row names the condition and carries the foreign path, and matrix case K12 exercises both a root-level and a nested foreign entry. Also merges origin/main (Swang #185–#187) in the preceding merge commit to restore mergeability; the merge itself does not touch the contract. Docs-only; still awaiting independent acceptance; implementation remains prohibited. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kc6r6PhKQCNqArwkijUnzn
|
Revision r5 (
Mergeability: Totals: matrix now 58 preregistered cases; refusal surface = 11 reachable Slice-1 refusals + 22 new typed refusals (+ 3 ledger-side members documented as unreachable). Still docs-only; Slice 2 remains PROPOSED / IMPLEMENTATION BLOCKED, awaiting re-review at exact head Generated by Claude Code |
|
r5 hostile re-review on exact head
Two non-blocking corrections while touching the text:
Everything r5 intended to close otherwise checks out: initial hardlink aliases refuse before side effects; a pre-existing temp is no longer silently unlinked; the publication-rename error state is represented; reserved-area shape is now executable through step 3/taxonomy/K12; PR #184 is mergeable again; and No RED or implementation work should start. Next revision should remain contract/docs-only. |
1. Temp inspection moved under the held lock: step 1 now acquires the lock
first, so a live writer's step-12 temp is unreachable for a non-holder —
a second applier always loses at the lock boundary
(ApplicationIndexLocked) and can never misclassify a live commit's temp
as debris. Concurrency case C14 preregisters exactly the step-12 temp
lifetime window.
2. The lockfile gains a validated ownership marker (one line,
song-curation.lock.v1). On contention the occupant is classified by
content: marker or empty file -> ApplicationIndexLocked; anything else
-> the new typed ApplicationIndexLockPathOccupied, and the §8.2
stale-lock recovery validates content before any deletion, so a real
second index named .foo.lock can never be deleted by recovery (case
C15, symmetric to C13).
3. The step-12 temp write is an atomic no-clobber creation
(OpenOptions::create_new — std's answer to exactly this TOCTOU); the
step-1 absence check is a fail-fast courtesy, not the safety argument;
AlreadyExists at creation is a pre-commit ApplyIoError with the late
occupant untouched (case F12).
4. Publication is stated honestly as non-no-clobber: compliant-applier
overlap is made impossible by construction — the .name.apply-staging
pattern is a reserved namespace refused for outputs (OutputNameReserved,
case F13) and staging creation is an atomic create_dir — while the
residual (an external actor's empty directory at the output path being
replaced by POSIX rename) is documented and accepted with rationale
(std has no RENAME_NOREPLACE); a foreign file or non-empty tree makes
the rename fail with the occupant untouched.
Non-blocking corrections: the step-11 failure row and F4 now say staging
MAY remain (best-effort cleanup can succeed); the stale flock claim is
replaced — std::fs::File::{lock,try_lock} is stable since Rust 1.89, and
nlink == 1 stays the chosen design because file-identity locking would not
repair commit-by-rename alias splitting.
Docs-only; still awaiting independent acceptance; implementation remains
prohibited.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kc6r6PhKQCNqArwkijUnzn
|
Revision r6 (
Non-blocking corrections: the §8.2 step-11 failure row and F4 now say staging may remain (best-effort cleanup can succeed), with retry refusal conditional on actual residue; the stale Totals: matrix now 62 preregistered cases; refusal surface = 11 reachable Slice-1 refusals + 24 new typed refusals (+ 3 ledger-side members documented as unreachable). Still docs-only (the r6 delta is one commit touching only the contract file); Slice 2 remains PROPOSED / IMPLEMENTATION BLOCKED, awaiting re-review at exact head Generated by Claude Code |
|
r6 hostile re-review on exact head Blocker — the lock ownership marker is itself published non-atomicallyThe protocol is now That leaves two states the contract itself can produce:
The zero-byte special case does not close this. It also weakens the ownership claim in the opposite direction: a pre-existing unrelated empty file is not actually proven to be Griff-owned merely because deleting it loses no payload bytes; pathname existence/metadata can itself be meaningful. This directly conflicts with the PR body's stronger statement that unproven coordination-path occupants are never deleted. Please make lock acquisition/recovery semantics closed over all prefixes / partial-marker states, not just
One possible design direction is to separate contention classification from recovery authority: conservative prefix/empty states may safely block as Everything else in r6 held under adversarial review:
No RED or implementation work should start yet. If the lock-marker publication/recovery state machine is closed without changing other accepted laws, I currently see no further contract-level blocker. |
…e machine
The r6 ownership marker was published non-atomically (create_new is
atomic, write_all is not), so a compliant applier could produce a
non-empty marker prefix at the lock path — live during a concurrent
observation, or permanently after a crash — and the {empty, exact}
classification would misread it as foreign occupancy, while recovery
forbade deleting the contract's own debris.
The state machine is now closed over all prefixes, with contention
classification and recovery authority deliberately separated:
- Contention classification (live-safe): any byte-prefix of the canonical
marker — empty, partial, or complete — classifies ApplicationIndexLocked,
so live compliant ownership can never be misclassified as foreign; only
non-prefix content is ApplicationIndexLockPathOccupied.
- Recovery authority (provenance-gated): only the exact complete marker is
automatically deletable by stale-lock recovery; an empty or partial
prefix is ambiguous (an unrelated file can resemble one, and pathname
existence/metadata can themselves be meaningful) and takes the explicit
operator-proven path — provenance-established deletion or
non-destructive relocation out of the coordination namespace; non-prefix
occupants are never deleted or relocated by any mandated recovery.
The atomic-visibility alternative (private file + std::fs::hard_link
publication, the mail-spool lockfile protocol) is recorded as considered
and not chosen: it adds a second transient coordination namespace with its
own debris states, while prefix-closed classification needs no new names
and is mechanically testable.
Matrix: new fault-injection case C16 (pause between lock creation and
completed marker publication — live observation and permanent crash
variants, both with a non-empty partial marker); C12/C15 aligned with the
exact-marker auto-delete rule.
Docs-only; still awaiting independent acceptance; implementation remains
prohibited.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kc6r6PhKQCNqArwkijUnzn
|
Revision r7 ( The chosen design is the review's first direction — contention classification separated from recovery authority — because it needs no new coordination names and every rule is mechanically testable:
Both required properties now hold mechanically: (1) live compliant ownership is never misclassified as foreign — every state a live writer can present is a prefix, and every prefix classifies Preregistered: new fault-injection case C16 — a pause/fault between lock creation and completed marker publication with a non-empty partial marker, in both variants: (a) a concurrent applier observing it live refuses Totals: matrix now 63 preregistered cases; refusal surface unchanged (11 reachable Slice-1 + 24 new typed + 3 documented unreachable). The r6→r7 delta is one docs commit touching only the contract file (+43/−16). Slice 2 remains PROPOSED / IMPLEMENTATION BLOCKED, awaiting re-review at exact head Generated by Claude Code |
|
Final hostile re-review on exact head I re-checked the r6→r7 delta rather than accepting the summary. It is exactly one docs commit ( The last blocker is genuinely closed. The lock-marker protocol now distinguishes two different questions that r6 had conflated:
That closes both failure modes from round 6: a live partial marker cannot be misclassified as foreign occupancy, and a crash-created partial marker no longer produces an unrecoverable self-state. The zero-byte weakness is also fixed: emptiness is no longer treated as ownership proof. C16 mechanically pins both sides of the state machine: live observation of a non-empty partial marker must yield I also checked the secondary edges introduced across r5/r6: temp inspection remains under the held lock; late temp creation remains atomic no-clobber; hardlink aliases remain fail-closed; reserved staging namespace still closes compliant-applier publication overlap; the external empty-directory Verdict at this exact SHA: the Slice-2 transactional Apply contract is content-acceptable and has passed hostile falsification review. I found no reason to request an r8. One governance boundary remains separate from the content verdict: §16 requires independent acceptance by someone other than the PR author. The GitHub connector identity here is |
…ntract ADR-0033 Slice 2 transactional-Apply acceptance contract is independently ACCEPTED at exact reviewed head 47e734c after six hostile review rounds (PR #184; 24 contract blockers found and resolved across r2–r7; final re-review found none; CI 14/14 green). Acceptance is recorded here per repository governance (AGENTS.md routing; S7 Slice C precedent at 7d0c0cb) — the PR merge itself was not the acceptance act. Authorizes only Slice 2 implementation under the frozen §14 RED→GREEN matrix and closed §12 refusal surface; Slice 3, the controlled pilot, and real-/full-corpus labeling remain separately gated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kc6r6PhKQCNqArwkijUnzn
…only Update the stale Status lines in the Slice 2 contract and the proposals index to reflect the acceptance recorded in docs/decisions.log.md @ bad7b44. Lifecycle metadata only: the normative reviewed contract remains the exact artifact at 47e734c (PR #184), no contract law changes, and acceptance is not reopened by this edit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kc6r6PhKQCNqArwkijUnzn
This PR specifies ADR-0033 Slice 2 only. It does not implement transactional Apply. Implementation remains prohibited until independent acceptance is formally recorded against the exact head SHA.
Status: CONTENT REVIEW PASS at
47e734cfbf1a6bd90c1bd2a035cdc68692378e96(see the final hostile re-review comment in this PR's thread, 2026-08-19) — six hostile review rounds, no remaining contract-level blockers, no r8 requested. The content verdict is deliberately not the acceptance record: §16 requires independent acceptance by someone other than the PR author, and the reviewing GitHub identity here is the PR author. Implementation stays blocked until that acceptance is recorded against this exact SHA; any head movement that changes the contract reopens review.Base / head
main(branch carries a clean merge ofmain@d7012f5; the contract was authored against6188cc2)47e734cfbf1a6bd90c1bd2a035cdc68692378e96(r7 — the content-pass SHA)main:docs/proposals/song-curation-slice-2-transactional-apply.md(new),docs/proposals/README.md(index entry only)Revision history
473377a) — initial contract.f9e8a67) — six blockers from review round 1 (r2 audit): filesystem names decoupled from plan content; report after the single staged preflight; single-writer index lock; four-way report partition; honest duplicate-key guard; supersession-evidence consistency law. (Reviews arrive as PR comments: the reviewing identity is the PR author, so GitHub rejects a formal REQUEST_CHANGES with a 422.)975550f) — five blockers + one minor from round 2 (r3 audit): §6 invariant narrowed, lock I/O typed; canonical resolved index identity (F8); step-12 failure state completed, R3 restricted; staged tree-agreement re-check (K11); Slice-1 surface split 11 reachable + 3 unreachable (A8).dca507f) — four blockers from round 3 (r4 audit): missing-index refusal reachable at step 1; one observable result shape (F9); reserved-area carry-forward law; coordination-path collisions refused pre-lock (F10).49a7750) — four blockers from round 4 (r5 audit): hardlinked index refused (F11); pre-existing temp refused, never silently unlinked (C13); step-11 publication-rename failure enumerated; reserved-area shape law wired through step 3 / taxonomy / K12. Mergeability restored by merge commitd24cc1b(contract untouched).d441331) — four blockers + two corrections from round 5 (r6 audit): temp inspection under the held lock (C14); lock ownership marker +ApplicationIndexLockPathOccupied(C15); step-12 temp writecreate_newno-clobber (F12); reserved.apply-stagingnamespace (OutputNameReserved, F13) + honestly documented empty-directory rename residual; staging-may-remain wording;flockclaim corrected.47e734c) — the one remaining round-6 blocker (r7 audit): the lock-marker state machine closed over all byte-prefixes — contention classification (any marker prefix refusesApplicationIndexLocked, so live ownership is never misclassified) separated from recovery authority (only the exact complete marker auto-deletes; ambiguous prefixes take an operator-proven path; non-prefix occupants are never deleted or relocated by mandate);hard_linkalternative recorded as considered; case C16. The crash-between-publish-and-commit model survived all six review rounds unchanged.Authority
Normative: ADR-0033 (Decisions 5–10), ADR-0031, ADR-0032, the accepted frozen Slice 1 (
song-curation/), core contracts (song_holdout_preflight, corpus schema), themigrate-v9preflight precedent. The historical proposal is historical context only — every detailed rule not fixed by ADR-0033 is explicitly adopted, modified, or rejected in §13; nothing is inherited by silence.Decisions resolved
song-curation/area with recursive shape law and supersession semantics; tree agreement; no-root-songslaw.report_digestcoverage.nlink == 1, coordination collisions, reserved namespace) → lock acquisition with prefix-closed content classification → under-lock temp inspection; nothing else touches the filesystem before step 9; step 10 re-runs tree agreement on the staged tree, runs the single preflight, then writes the report;verify_planliterally reused; already-applied precedes chain equations.create_dir;.apply-stagingreserved) → one publication rename (honestly non-no-clobber; accepted empty-directory residual documented) →create_newtemp + rename as the single commit point, under a marker-bearing single-writer lock bound to the canonical resolved index (symlink aliases converge; hardlinks refused; marker state machine closed over all prefixes; unproven occupants of coordination paths never deleted — by Apply or by recovery); one observable result shape; complete enumerated failure model; durability caveat; no ACID vocabulary.holdout_ready: falsesnapshot.Unresolved blockers
None at content level (six-round review record above). Outstanding: the formal independent-acceptance record itself.
Non-goals
No Apply implementation; no corpus writes or labels; no suggestions/normalization; no
SongIdissuance; no Slice 3 / pilot / full-corpus labeling; no changes to ADR-0031/0032/0033, S7/S8/S9/S15/S16, or Swang; no CLI/cockpit integration; nogriff manifestchanges; no similarity/embeddings/MIR;song-curation/stays a non-workspace-member; no production dependency.Validation performed
maincontains no executable-code changes: two Markdown files only; themainmerge commit (d24cc1b) leavesdocs/proposals/untouched; each review round's semantic delta is one dedicated docs commit.Implementation acceptance criteria
After independent acceptance is recorded against
47e734c, the implementation is acceptable only if: strict RED→GREEN per the §14 matrix (tests-only RED commits first, per commit); frozen Slice-1 API, semantics, and tests untouched and green; the §12 taxonomy implemented closed; the §6 ordering and §8 publication semantics observable in the enumerated failure states; any needed deviation reopens this contract for re-acceptance.🤖 Generated with Claude Code
https://claude.ai/code/session_01Kc6r6PhKQCNqArwkijUnzn