diff --git a/Makefile b/Makefile index a625c37c6..78c0d8d53 100644 --- a/Makefile +++ b/Makefile @@ -310,6 +310,65 @@ prodscale-crossover: ## Measure fold/overlay crossover at production scale. prodscale-topebble: ## Measure SQLite-to-Pebble conversion at production scale. BATON_PROD_SCALE_TOPEBBLE=1 go test -v -count=1 -timeout=180m -run TestProdScaleToPebbleCurve ./pkg/synccompactor +# Formal verification track (formal/). These targets have tool +# prerequisites this repo deliberately does not install: the P checker +# (`p` on PATH — https://p-org.github.io/P/) for the model sweeps, and a +# sibling engine checkout at ../occult (the host go.mod `replace` target) +# plus a Go 1.26 toolchain for the Occult suite. Each target checks its +# prerequisite and says what is missing rather than failing confusingly. +# +# The sweep targets REGENERATE the committed evidence summaries +# (formal/*/PCheckerOutput/*/summary.txt): a clean run reproduces every +# cell's verdict line, and the scripts' exit status carries that +# verdict — any mismatch (drifted cell, untagged red, wrong bake-off +# alarm, checker error) fails the target. The reproduction claim was +# validated AFTER the exit-status gate landed: the 2026-09-01 full +# pass re-ran every cell through the gated scripts and reproduced all +# three committed summaries byte-identically (which is also why the +# summaries carry no post-gate commit — identical bytes leave nothing +# to commit). Two kinds of run-to-run noise +# are possible and treated differently: an alarm-tag difference on a +# multi-shape RED cell (e.g. the walker's tc3a_P1 has two calibrated P1 +# shapes) diffs the summary but still exits 0, while a RED cell that +# MISSES its find fails the gate by design — that has happened once, +# from seed-bimodal search on a narrow target, and the remedy is a +# per-cell strategy pin (see the tcG5dS_W2 note in graph CALIBRATION). +# Walker (56 cells) and graph (66 cells) each take on the order of half +# an hour at the default schedule budget; the bake-off (12 cells) about +# twenty minutes. +P_SCHEDULES ?= 10000 +OCCULT_TEST_TIMEOUT ?= 90m + +.PHONY: p-checker-guard +p-checker-guard: + @command -v p >/dev/null 2>&1 || { \ + echo "formal: the P checker ('p') is not on PATH — install it per https://p-org.github.io/P/ (no install target is provided)" >&2; \ + exit 2; \ + } + +.PHONY: formal-walker-sweep +formal-walker-sweep: p-checker-guard ## Compile and sweep the walker model (56 cells; needs P). + cd formal/walker && p compile -pp walker.pproj && tools/sweep.sh $(P_SCHEDULES) + +.PHONY: formal-graph-sweep +formal-graph-sweep: p-checker-guard ## Compile and sweep the graph model (66 cells; needs P). + cd formal/graph && p compile -pp graph.pproj && tools/sweep.sh $(P_SCHEDULES) + +.PHONY: formal-graph-bakeoff +formal-graph-bakeoff: p-checker-guard ## Run the 12-cell bake-off phase (needs P). + cd formal/graph && p compile -pp graph.pproj && tools/bakeoff.sh $(P_SCHEDULES) + +.PHONY: formal-occult-check +formal-occult-check: ## Run the Occult host suite (needs ../occult and Go 1.26). + @test -d ../occult || { \ + echo "formal-occult-check: sibling engine checkout not found at ../occult (the formal/occult/host go.mod 'replace' target); clone it beside this repo (no install target is provided)" >&2; \ + exit 2; \ + } + cd formal/occult/host && go test -timeout $(OCCULT_TEST_TIMEOUT) ./... + +.PHONY: formal-check +formal-check: formal-walker-sweep formal-graph-sweep formal-graph-bakeoff formal-occult-check ## Run every formal-track sweep and suite. + .PHONY: pkg/sdk/version.go pkg/sdk/version.go: echo $(VERSION) diff --git a/docs/rfcs/0011-demand-graph-sync-runtime.md b/docs/rfcs/0011-demand-graph-sync-runtime.md new file mode 100644 index 000000000..39e6fc950 --- /dev/null +++ b/docs/rfcs/0011-demand-graph-sync-runtime.md @@ -0,0 +1,159 @@ +# RFC 0011: Demand-graph sync runtime — variant S over the shared chassis + +Status: kickoff draft — design mapping and verification plan for review; +no code yet. +Risk routing (REVIEW_CHECKLIST §2): scheduler + checkpoint + storage +semantics — silent/combinatorial subsystem, HIGH; the full BUG_CATCHING +step-up applies to every implementation phase. This RFC implements the +verdict of the formal effort (`formal/REPORT.md`); the design itself was +adversarially reviewed and model-checked before this document existed +(`formal/GRAPH_MODEL_SPEC.md` v4 frozen, `formal/graph/BAKEOFF.md`). + +## 1. Motivation + +The current tiered walker composes per-scope artifacts (fresh rounds, +source-cache replays, overlays) by position in an action queue. +The formal calibration model reproduces the known failure classes of +that design mechanically: the phantom union (individually truthful +responses composing into a false sealed artifact), session laundering +(a reader embedding a dead writer's value), and the artifact-swap +rebind hole (`formal/walker/CALIBRATION.md`, scenarios 1–3). Phase 6b's +mitigations close the calibrated instances; the classes remain +structural, because nothing in the runtime ties an artifact to the +premises it was derived from. + +The demand-graph runtime replaces positional composition with tracked +derivation: work is admitted by demand edges, every output carries its +lineage, and seal-time obligations are checked against a closure +oracle rather than assumed from queue completion. Two lineage variants +were modeled and baked off; the registered decision rule selected +**variant S — observable-causal stamps** (`formal/graph/BAKEOFF.md`). +This RFC maps that verdict onto `pkg/sync`. + +## 2. What is being built (the frozen mechanism inventory) + +From the spec's frozen tally (`formal/GRAPH_MODEL_SPEC.md` §7.5): + +**Shared chassis** — frontier checkpoint carrying the +admitted-derivation set (death semantics, admitted-by edges, cursors); +forced resume checkpoint; generation table with the bump rule, +quiesce-before-bump, and the total mint fence; premise-validated +markers (premise digests, publishBearing, adopt-or-re-derive under +MATCH-only and writer-ineligibility); the supersession matrix and +poison rule; the seal-time sweep with the closure oracle; atomic +units. + +**Variant S adds** — a per-output stamp field with the `eAdopt` +rewrite; stamp merge on read; three observation points (dispatch-time +refusal, session-read read-through with the dead-read count, the +pre-seal pass with an iteration budget); optional bucketed +compression, admissible only under the G9-CAL-1 minting rules. + +Not built: everything on E's bill (durable edge checkpoint rows, the +∀-pending-purge, support rebuild + agreement check, the retraction +queue), and the constrained session primitives as a correctness +requirement — under S they demote to a stamp-width optimization +(BAKEOFF design consequence 1). + +## 3. Model-to-Go mapping (proposed referents) + +The model speaks of nodes, output keys, and generations. The proposed +concrete referents, chosen to coincide with the partition granularity +phase 6b already established: + +| model concept | proposed Go referent | +|---|---| +| node | a scoped derivation task: one (action kind, rowKind, scopeKey) instance — the unit the tiered walker already dispatches | +| output key | the (rowKind, scopeKey) partition — the granularity of `sourcecache` replay, validators, and the trace oracle | +| generation | new durable table in the c1z store: (node, gen) rows with the mint fence; minting rides the existing checkpoint commit | +| frontier checkpoint | extends the existing checkpoint token (`pkg/sync` state marshalling): admitted-derivation set + admitted-by edges + cursors replace positional queue state | +| premise-validated marker | generalizes the source-cache manifest entry (`PutSourceCacheEntry` validators are proto-markers today); adds the premise digest and publishBearing bit | +| atomic unit | generalizes the 6b replay unit (clear + copy + marker + publish as one store transaction) to all derivation commits | +| per-output stamp | new column on partition rows or a per-partition sidecar (open question 2); merged on read, rewritten by adopt | +| dispatch-time refusal | frontier scheduler gate, replacing queue-order dispatch | +| session-read read-through | the session store consults the generation table on read; dead reads counted, not blocked | +| pre-seal pass | `EndSync` extension: bounded staleness chase over the demand closure before seal | +| closure oracle | seal-time check that every demanded output is present and stamped live — the sweep's authority | + +## 4. Strategy: sibling runtime behind a capability, not in-place surgery + +The tiered walker with 6b mitigations is shipping and calibrated. The +proposal is a parallel scheduler path selected per-sync (capability or +config flag), reusing the store, the connector protocol, and the +source-cache annotation surface unchanged — the demand graph changes +WHO dispatches work and WHAT lineage it records, not the wire contract +or row storage. Rationale: the runtime's correctness argument is +holistic (chassis mechanisms interlock; the spec's kill cells show +single-mechanism removals are silently unsafe), so incremental +in-place evolution would transit through states the model says are +broken. Rollout gates on dual-run conformance (§6). + +## 5. Design consequences carried from the bake-off + +Verbatim obligations from `formal/graph/BAKEOFF.md`: + +1. Constrained session primitives are an optimization (stamp width), + not a correctness gate; free-form session reads are safe under S. +2. Upstream validators and node generations get the same + observation-point discipline — consult at observation time. +3. Compression, if shipped, ships only with the G9-CAL-1 minting + rules (as first drafted it livelocked honest histories). +4. The dying-reader race kill needed feedback-PCT and a third worker + to exhibit: implementation tests for that race MUST use perturbed + or priority-based schedules, not uniform random chaos. + +## 6. Verification plan (the bridge stays load-bearing) + +- **Trace oracle first.** Extend the canonical vocabulary + (`formal/occult/src/sync_trace_policies.occult`, + `formal/occult/TRACE_BRIDGE.md`) with generation stamps and + admission events BEFORE the runtime lands; the recorder pattern + (`pkg/sync/sync_trace_audit.go`, nil in production) extends as-is. + Every implementation phase exports fixtures; the oracle judges + them. Note the engine's current trace-length ceiling (ask 8 of the + Occult engine brief) bounds fixture size until fixed. +- **Refimpl as executable spec.** The demand-graph reference + implementation (`formal/occult/host/refimpl/`) already produces + oracle-green traces; production traces must be conformant to the + same policies, and divergences route to whichever side is wrong. +- **Chaos parity.** The 6b chaos scenarios re-run under the new + runtime; crash schedules must include the perturbed-schedule + requirement from §5.4. +- **Dual-run conformance gate.** Same connector dataset, both + runtimes, sealed artifacts compared row-for-row (the `diff` + machinery exists). Ships only behind N green dual-runs on the + chaos corpus plus real connector fixtures. +- **Review routing.** Every phase is HIGH; BUG_CATCHING's step-up is + the floor, and the model's kill cells double as the adversarial + test inventory (each calibrated mutant names a regression test the + implementation must carry). + +## 7. Open questions (to resolve in review before phase 1) + +1. **Node identity vs dynamic fan-out.** The model's nodes are + static; real syncs spawn scoped actions dynamically + (`EnqueuePageTokens`, RFC 0007). Does a spawned child admit under + its parent's demand edge, and is the admitted-derivation set's + death semantics compatible with cursor-spawned siblings? +2. **Stamp storage.** Per-row column, per-partition sidecar, or + in-manifest? Width is unmodeled (BAKEOFF honest limits); measure + before choosing. Compression is the pressure valve, not the plan. +3. **Pass budget at scale.** The 3-iteration pre-seal budget is a + small-scope declaration. Production policy on budget exhaustion + (fail the sync loud vs degrade) needs an explicit decision — + the model says exhaustion with dead stamps is a mechanism bug, + which argues for loud failure. +4. **Generation-table growth.** Mint-per-bump with sync-scoped + lifetime suggests table truncation at seal; confirm no + cross-sync reader (warm consults read markers, not generations). +5. **Coexistence window.** How long do both runtimes ship? The 6b + mitigations stay calibrated in the walker model for as long as + the walker ships. + +## 8. What this RFC is not + +Not a phase plan with estimates (that follows once §7 resolves), not +a storage-schema spec (open question 2 gates it), and not a promise +that the model's guarantees transfer to code — the model arbitrates +the design; the trace oracle and dual-run gates are what tie the +implementation to it (`formal/REPORT.md`, "What is guaranteed"). diff --git a/formal/.gitignore b/formal/.gitignore new file mode 100644 index 000000000..2a37be78b --- /dev/null +++ b/formal/.gitignore @@ -0,0 +1,12 @@ +# Regenerable P toolchain outputs (p compile / p check). The sweep +# summaries the calibration logs cite as evidence are force-added. +PGenerated/ +PCheckerOutput/ +# Machine-replay counterexample archives (~200k lines across the +# calibrated reds). Regenerate any cell's counterexample with +# `p check -tc ` per the run log; the committed evidence is the +# sweep summaries. +trace.json +# Occult engine solver checkpoints (regenerable tool output, ~1MB of +# base64 solver state per probe run). +*.checkpoint.json diff --git a/formal/GLOSSARY.md b/formal/GLOSSARY.md new file mode 100644 index 000000000..8eaf02b6e --- /dev/null +++ b/formal/GLOSSARY.md @@ -0,0 +1,209 @@ +# Glossary — sync scheduling formal model (deliverable 0) + +Pinned vocabulary for the model, its documents, and its P identifiers. +Where a term names something that exists in code today, the anchor is +cited; graph-runtime terms describe CANDIDATE semantics under evaluation +(see `docs/tasks/demand-graph-sync-brief.md`) and are definitions of what +the model checks, not descriptions of shipped behavior. + +## Shared vocabulary + +- **Sync**: the logical unit of work that produces one artifact. Identified + by a sync ID; spans one or more attempts. +- **Attempt**: one process execution of a sync. A crash or interrupt ends + an attempt; a resume starts a new attempt of the same sync from its last + durable checkpoint. The **attempt boundary** is the crash/resume seam. +- **Artifact**: the durable output of a sync (a c1z): row partitions, a + manifest, and — once sealed — a completion verdict. The **previous + artifact** is an immutable, read-only replay base consulted only via the + lookup. +- **Row**: one stored record (resource, entitlement, or grant — the model + abstracts these to one row kind axis). Row identity is its storage + identity within a row kind. +- **Scope / output key**: the stable storage identity a partition is keyed + by. In the walker this is the connector-chosen `(row_kind, scope_key)` + (`annotation_source_cache.proto`); in the graph runtime it is the node's + output key. Deliberately distinct from the derivation hash (scheduling + identity): re-derived work must land in the same partition. +- **Partition**: the set of current-sync rows stamped with one scope. + **Partition invariant**: scopes partition rows — each row identity + belongs to exactly one scope per sync; a cross-scope restamp or + out-of-scope delete is a partition violation and poisons the losing + scope (proto contract). +- **Validator**: the opaque upstream change-detection token persisted per + scope (HTTP ETag, delta token). **Truthful-validator assumption** (trust + boundary): a validator matches iff the scope's upstream content is + unchanged. +- **Manifest**: the artifact's map of scope → manifest entry (validator, + plus a sealed row count). An entry is published only after its page's + rows and tombstones commit. +- **Attestation**: the claim a manifest entry makes: this artifact's + partition for scope S equals replay-of-the-attested-base plus the + declared compositions (overlay upserts, tombstones, or replacement), + produced under validator V. P1 (binding integrity) is the integrity of + this claim. +- **Consult**: resolving a scope's freshness this sync: a lookup against + the previous artifact's manifest, then — on a hit — connector + revalidation of the validator against upstream. A scope was "consulted + against upstream during sync N" if its verdict came from revalidation or + fresh fetch within sync N. +- **Hit**: a warm-lookup consult that found a manifest entry. Recorded + with its validator in the checkpoint-durable hit map + (`state.sourceCacheHits`, CO-6b-004). +- **Record**: fresh-fetched rows stamped into a scope's partition, with a + validator publish when the page carries one (`SourceCacheRecord`). +- **Replay**: copying the previous artifact's partition for a scope into + the current sync (`SourceCacheReplay`). +- **Elision**: what replay skips — the enumeration the connector would + have performed for the scope. Replay copies rows, not side effects: + session writes (and reads) the elided enumeration would have made do + not occur this sync. Elision is sound only if the enumeration is + side-effect-free beyond its rows (model spec scenario 7). +- **Replacement**: the replay copy's semantics — the destination partition + is cleared, then the base is copied + (`clearReplayDestinationScopeLocked`). Contrast **overlay**: upserts and + tombstones applied on top of a replayed base (delta semantics). +- **Composition enum (proposed)**: a `SourceCacheRecord` marker declaring + OVERLAY vs REPLACES semantics for a page's contribution; REPLACES blocks + the artifact as a future replay seed until supersede machinery exists + (deliverable 3 models this staged mitigation). +- **Poison**: a durable per-scope marker recording a partition violation. + A poisoned scope reads as a lookup miss and is refused as a replay + source (preflight). +- **Preflight**: source-side validation before a replay copy: manifest + entry present and not invalidated, scope not poisoned, sealed row count + equals the index cardinality with stamp verification + (`validateReplaySourceScope`). +- **Checkpoint**: the durable snapshot of scheduler state — action stack, + provenance sets, flags — written between dispatch batches + (`state.Marshal` → sync token), and FORCE-written at Init, before seal, + and on graceful stop (`checkpointOnStop`, run-expiry). A stop-forced + checkpoint captures live mid-batch state: mid-chain cursors of + unfinished actions, admitted-but-undrained spawns, and hits recorded + during the aborted batch. Resume restores exactly the checkpoint and + nothing else; all other scheduler state is volatile. +- **Seal**: the transition that completes a sync's artifact (EndSync). + Sealed row counts are stamped before the end stamp; after seal the + artifact is immutable. **Seal obligations** are the checks that must + hold at this point. +- **Warm / cold**: warm = a previous-artifact lookup is installed (every + consume gate G1–G7 passed, this attempt); cold = `NoopLookup`, every + consult misses. Degradation to cold is always safe; it costs caching + value only. +- **Compat key**: the four-field byte-matched record gating warm installs + (connector cache generation, config fingerprint, SDK materialization + generation, selection fingerprint — plan B4). Two artifacts from the + same connector and config share a compat key; this is why validator + binding (CO-6b-004) exists. +- **Smear**: the baseline time-inconsistency of any paginated walk: + different scopes observe upstream at different instants, so every + artifact is a mixture of upstream states that existed during the sync. + Smear is not a defect; P3 bounds which mixtures are acceptable. + +## Walker-specific + +- **Action**: the checkpointed unit of scheduled work: op, page token, + resource keying, spawn/type-scope markers (`state.Action`). The stack is + LIFO by admission order; per-resource phases dispatch batches of up to + 100 consecutive same-op actions to bounded workers. +- **Spawn vs continuation**: `EnqueuePageTokens` ADMITS new sibling cursor + actions — each an independently checkpointed, schedulable identity, + deduplicated per process by identity digest. `NextPageToken` advances + the SAME action's cursor in place. Spawn creates schedulable identity; + continuation does not. +- **Restart-from-root (CO-6b-002)**: resume restores actions as of the + MOST RECENT durable checkpoint. In crash-only histories that is a + loop-top/forced checkpoint holding only root tokens, so an + interrupted paginated action re-executes from its root token; under + a graceful stop the forced stop-checkpoint may capture a mid-chain + cursor, and resume continues from it — including when that + checkpoint survives a LATER hard crash. Restart-from-root is a + property of which checkpoint survives, not of the crash itself. Page + processing is at-least-once across an attempt boundary in all modes. +- **Hit-set / replayed-set**: the checkpoint-durable, within-sync-monotone + provenance maps. The hit-set (row kind → scope → validator) authorizes + same-sync replays; the replayed-set dedups the once-per-sync replacement + copy. Hits recorded in the dispatch batch that crashes are lost with it + (CO-6b-006). +- **Warm gate (attempt-scoped)**: the `sourceCacheWarm` flag — set only + after THIS attempt installed a deliverable warm lookup; consulted before + every replay copy. A restored hit-set does not re-authorize replay in a + cold or drifted resume (CO-6b-003). +- **Scope lock**: the per-(row kind, scope) mutex every scoped page holds + from before its row puts through its validator publish (CO-6b-005/006); + closes the duplicate-copy TOCTOU and the record-page/replacement-copy + interleaving. + +## Graph-runtime (candidate semantics) + +- **Node**: a re-issuable request description (call site + parameters). + Persistent scheduling identity; may be executed any number of times. +- **Execution**: one attempt of one node. Identified by (node, + generation). +- **Generation**: a node's restart counter. An output produced by + execution (n, g) is **dead** once n restarts into generation g' > g. +- **Derivation hash**: the canonical hash of a node's request derivation; + the scheduling identity used for revisit suppression. Distinct from the + output key by design. +- **Spawn lineage**: the single-parent tree of which execution admitted + which node; used for eager purge of a restarted node's spawn-subtree + (variant E). +- **Support / data lineage**: refcounted demand edges recording which + outputs support which rows and nodes; DERIVED, not stored; retraction is + transitive when support drops to zero (variant E). +- **Demand closure**: the set of nodes and outputs transitively demanded + from the sync's roots as of seal time. +- **Sweep**: the seal-time pass that drops every partition outside the + final demand closure and nothing inside it (P5). +- **Fresh-artifact supersession**: a newer execution's outputs replace an + older generation's outputs for the same output key, rather than + composing with them. +- **Causal stamp (variant S)**: a compact causal timestamp over node + generations carried by every output (rows, session values, spawned + tokens). Reads merge the value's stamp into the reader's; writes carry + the writer's merged stamp; validation happens at observation points + (demand derivation, session reads, seal). Lossy compression is + admissible because the error direction is false staleness → redone + work, never wrong data. +- **Consistent cut (variant S)**: the variant-S form of P1/P3: no + published partition mixes outputs from causally incomparable + generations, checkable mechanically from stamps at seal. +- **Sealed cut (P7)**: the checkpointable fact "no pending or reachable + node can emit row-kind K" — the closure precondition grant expansion's + aggregate node requires before condensing. + +## Session store + +- **Variant A (shipped)**: free-form KV, sync-scoped, durable across + attempt boundaries (`pkg/types/sessions`, + `pkg/dotc1z/engine/pebble/session_store.go`). Any execution may read any + key; no lineage is recorded — the dependency channel in calibration + case 2. +- **Variant B (proposed primitives)**: private scratch (readable only by + its own execution, dies with the execution's generation); single-writer + publish (reads are tracked edges; the writer's re-derivation retracts + readers); keyed-contribution merge (per-writer retraction). +- **Session taint (proposed)**: produce-side marking that a kind's + enumeration touched the session store during a replay-capable phase + (replay-capable = the kind is in the declared source-cache flow, + regardless of the recording attempt's warm/cold state), recorded in + the artifact's produce state (checkpoint-cadence durability, + self-healing under at-least-once re-execution); a tainted kind's + scopes read as lookup misses in later syncs — degradation, never a + loud verdict. Write-only taint closes the elision hole (scenario 7a) + but not the stale-read dual (7b); full-traffic taint (session + isolation) closes both. A capability-level opt-out — the connector + attests emission-irrelevance of its session traffic — disables the + detector for those kinds; trust-boundary machinery, and a dishonest + opt-out reproduces 7a/7b exactly. + +## The three-invariant framing + +- **Demand-gating**: nothing outside the final demand closure is + published (consume/sweep side; P5). +- **Validator-gating**: every published row entered via a fetch or a + replay whose validator was consulted against upstream during this sync + (P2). +- **Binding integrity**: every manifest entry attests exactly its + partition's declared composition over the attested base (P1, + produce-time). diff --git a/formal/GRAPH_MODEL_SPEC.md b/formal/GRAPH_MODEL_SPEC.md new file mode 100644 index 000000000..6b0fb8921 --- /dev/null +++ b/formal/GRAPH_MODEL_SPEC.md @@ -0,0 +1,878 @@ +# Model spec — demand-graph runtime model (P), deliverable 4 + +Status: v4, FROZEN. Review history: round 1 +(`formal/reviews/graph-spec-round1.md`) REJECTed v1 with 11 majors, +all dispositioned in v2. Round 2, targeted on the adoption repair +(`formal/reviews/graph-spec-round2-adoption.md`), verified the +adoption core sound and REJECTed on 6 seam majors, all dispositioned +in v3. Round 3, targeted on the v3 repairs +(`formal/reviews/graph-spec-round3-repairs.md`), verified ALL six +round-2 repairs soundly applied and REJECTed with 2 majors + 4 minors ++ 3 notes — every finding fix-without-re-review, no re-review +required ("the fixed spec needs a disposition registration check, not +a fourth adversarial round"). v4 applies the round-3 dispositions +(§12) and freezes: subsequent changes are GS-CO-NNN change orders, +never silent edits. Companion and baseline: `formal/MODEL_SPEC.md` +(v11 FROZEN + MS-CO-001); `formal/GLOSSARY.md`; +`docs/tasks/sync-formal-model-brief.md` (charter, deliverable 4); +`docs/tasks/demand-graph-sync-brief.md`. + +Purpose, stated narrowly: (a) re-check the walker's calibration bug +premises against the graph runtime's candidate semantics, (b) check +the NEW property obligations the graph introduces (P5 sweep soundness, +the laundering oracle and its per-variant mechanisms), and (c) +ARBITRATE THE LINEAGE BAKE-OFF — variant E (eager edges) versus +variant S (observable-causal stamps) — by checker output on property +satisfaction, mechanism count (frozen tally, §7.5), and redo work +under the divergence scripts (§9 G6). The written recommendation is +the artifact the demand-graph RFC cites, assembled under §10.7's +frozen decision rule. Refutation of a candidate invariant is a +success. + +## 1. What is modeled, what is abstracted + +Modeled (the graph runtime's candidate scheduling semantics): + +- A frontier scheduler: nodes as re-issuable requests, executions as + (node, generation), revisit suppression by derivation hash (with + the death semantics of G-RULE-2, round-2 R2-F5), demand derivation + from emissions, frontier checkpoint, resume with generation bumps, + seal-time sweep to the final demand closure, fresh-artifact + supersession with a total interaction matrix incl. marker lifecycle + (§4b). +- BOTH lineage variants as first-class scheduler modes sharing every + other mechanism: E = spawn lineage pending-purge (∀-predicate, §3) + + refcounted support DAG + session reads as tracked edges (requires + session variant B); S = causal stamps merged on read, validated at + the declared observation points (§3), nothing eagerly retracted. +- The session store as a dependency channel, variants A and B. +- Crash/resume at every boundary (armed injection), graceful stop, + the forced resume checkpoint (§5), MID-ATTEMPT generation death + under the quiesce-before-bump rule (§3, round-2 R2-F1), upstream + mutation between attempts and between syncs, bounded workers (2; + the G1d cell scripts 3 so the retraction-forced re-run dispatches + AT the bump — with 2 the dying-reader race needs two consecutive + starvation phases and random search cannot reach the declared + alarm; calibration scripting note, logged). +- Source-cache consult/replay/record inside node executions using + unit-mode materialization (MODEL_SPEC §9.6, settled hand-off), + adapted by §4a's premise-validated adoption with the round-2 + eligibility pins (MATCH-only, writer-ineligible). + +Abstracted, beyond MODEL_SPEC §1's list: + +- Budgets and magnitudes (attempt-level failure and the ladder ARE in + scope, §3/§8; per-node work budgets are not). +- Grant expansion / the aggregate node: de-scoped from this + deliverable (v2; deliverable 5 owns sealed cuts). +- The sessions × replay product (walker scenario-7 class): the + CROSS-SYNC product stays excluded with argument (v2, §12 F17 + entry); cross-sync stamp travel pinned in §4c; the walker cells + own taint. The WITHIN-SYNC product is now governed by the §3 + SESSION-PUBLISH BODY-OP PIN (round-3 R3-F1): in this model, + session publishes are node-body store ops executed by every + non-adopted execution regardless of verdict class — a declared + within-sync deviation from the walker's elision vocabulary, + load-bearing for the writer-ineligibility convergence argument + and for the `writerAdopt` kill's fireability (under the elision + reading a replay-verdict re-derivation would strand a dead + publish on an honest flap-back history and the kill could never + fire). If the runtime design adopts elision-style replay for + publish-bearing nodes, that is a change order with its own cells. +- SESSIONS × DEMAND SHRINK (round-3 R3-M4): a writer legitimately + de-demanded by an epoch shrink never re-publishes, and a + still-demanded reader of its sync-scoped value would carry an + uncleanable dead component under S. No scripted cell composes + sessions with a shrink; the shape is EXCLUDED from the envelope + and stated in §8's inductive bet, and §4a's convergence claim is + scoped to the scripted envelope accordingly. +- PUBLISH-DERIVED DEMAND (round-2 R2-M6(iii) + R2-F2 facet): demand + derivation in this model is a pure function of ROW CONTENT only. + Session publishes are dependency-channel machinery (P6-G's + jurisdiction), never demand sources. This is a declared restriction + of G-RULE-1's vocabulary: a runtime capability to demand work from + a session publish is NOT modeled, and if the design adopts one it + needs its own cells (change order). +- SESSION-TRANSITIVE RETRACTION CHAINS (reader-writer nodes; round-2 + R2-M6(ii)): excluded from the envelope and stated in the inductive + bet (§8) — the retraction rule is keying-uniform, so chains add + length, not mechanism; a reader-writer config is deferred to the + RFC stage if the recommendation's session story needs it. +- The walker itself (mapping in §5; divergences are honest deltas). + +## 2. Ground rules the encoding must obey + +All of MODEL_SPEC §2 carries over verbatim (arrival order a genuine +choice point; crash wipes exactly §5's volatile rows; announce-only +monitors; truthful validators; non-lying connectors; expected +verdicts declared before first run). + +Graph-specific rules: + +- G-RULE-1 (structure rides emissions): demand derivation is a pure + function of announced ROW CONTENT (the child-marker row; §1's + publish-derived-demand exclusion). TIMING PIN: derivation is + per-announce, atomic with that announce's completion-bookkeeping + effects. DERIVED-ANNOUNCE CARRIER PIN (round-2 R2-M2): scheduler + events with monitor significance that arise from processing an + announce (mid-attempt `eAnnGenBump`, retraction re-admissions, + observation-forced re-runs, forced-redo counts) are emitted as + DERIVED ANNOUNCES within the atomic processing of their carrier + announce, so monitors observe them in commit order; resume-time + bumps ride the forced resume checkpoint's commit (unchanged). +- G-RULE-2 (derivation hash is the ONLY suppression key, WITH death + semantics — round-2 R2-F5): the scheduler MUST suppress an + admission iff the same derivation hash's node is currently PENDING + or COMPLETED this sync ("may" is deleted — the checker cannot + legally choose starvation). PURGE AND REFUSAL-DROP REMOVE the + node's derivation hash from the admitted-derivation set, making a + live re-derivation re-admissible. Output keys never suppress; the + distinct-derivation same-key shape is §4b's poison row. RESUME + RULE: completed iff admitted ∧ ¬pending, evaluated AFTER any + purge/refusal removals of the resume. +- G-RULE-3 (generations are per-node monotone counters, durably + fenced): execution (n, g) exists only after every (n, g' < g) is + dead; an output's producing generation is stamped at emission and + never reassigned (`eAdopt` is a recorded transfer). Identity + uniqueness is monitored (P-GEN, §7; rule per R2-N3: no two + attempts contain store-commit announces attributed to the same + (n, g); adoption re-announces attribute to the ADOPTING execution). + (GS-CO-001) The durable fence is TOTAL over every minting path: no + generation dispatches before the table delta that minted it is + durably committed — (a) the attempt-start root mint (rides the + forced-resume-checkpoint discipline; a crash before any checkpoint + otherwise cold-restarts and re-mints attempt 1's identities), (b) + the first-admission mid-attempt mint of a newly demanded node + (rides the mid-bump fence; a crash after the node's first store + commit but before any checkpoint otherwise re-mints it), (c) the + resume bump (F4), (d) the mid-attempt bump (R3-F2). Calibration + found (a) and (b): P-GEN reds an HONEST single-crash history + without them. +- G-RULE-4 (the frontier checkpoint is total): pending nodes (id, + derivation hash, output key, generation, round-boundary cursor), + the admitted-derivation set, generation-qualified admitted-by + edges, closed-cut facts, the session index per variant. Variant + E's support counts are derived; the resume rebuild target is the + CHECKPOINT-CONSISTENT value; the rebuild-agreement monitor checks + that value. + +## 3. Machines + +Reused from MODEL_SPEC §3 (re-declared in `formal/graph/`): MUpstream, +MStore, MCrashInjector, MEnv. MStore's registered op vocabulary: +`eCheckpoint`, `eLookup`, `eGateRead`, `eClearScope`, `eUpsertPage` +(with composition intent REPLACES/OVERLAY), `eTombstones`, +`ePublishEntry`, `eSessionGet/Set` + variant-B primitive ops, `eSeal`, +`eReplayUnit(key)` / `eOverlayUnit(key)`, marker ops +(`eMarkerPut(key, gen, premise, publishBearing)` / marker read — the +marker records whether its round performed session publishes, §4a), +`eAdopt(key, fromGen, toGen)` (PRECONDITIONS, store-side: fromGen is +DEAD (round-2 R2-N1; the one mutant-reachable live-fromGen adoption — +`suppressionOff`'s sequential schedule — is a declared deviation +whose legality alarm still derives) AND the key is NOT POISONED +(round-3 R3-M2: the poison-voids-marker rule is enforced at the +`eAdopt` commit, not only at the worker-side marker check, closing +the check-then-act window against a concurrently landing poison)). CLEAR-PLACEMENT PIN: +`eClearScope` commits only as the first store op of a REPLACES-intent +round; it also DELETES the key's marker (round-2 R2-F4 — marker +lifecycle rides the clear). MARKER SCOPING PIN (round-2 R2-M3): +markers are PER-SYNC scheduling state — the §4a marker check reads +the CURRENT sync's store only (the walker 3-atomic precedent made +explicit), and seal DROPS marker rows from the sealed artifact +(markers never travel cross-sync; consult provenance cross-sync is +the manifest, as everywhere). + +### MGraphScheduler (one per attempt) + +Owns: frontier, admitted-derivation set, admitted-by edges, +generation table, demand derivation, lineage state per variant, +dispatch to 2 workers. + +Dispatch loop: pick a frontier node, issue (n, g), process announces +per-announce (G-RULE-1), checkpoint (placement a choice point), seal +when the frontier drains. + +MID-ATTEMPT DEATH — QUIESCE-BEFORE-BUMP (round-2 R2-F1, the walker +decision-16 precedent): a mid-attempt generation bump (retraction- or +observation-forced) commits only when the dying generation's +execution is NOT in flight; if it is, the bump DEFERS until that +execution's completion announce and is processed atomically with it +(derived announce). Consequence, pinned: a deferred bump orders the +forced re-run AFTER the dead execution's late commits, so the +re-derivation is the last writer and no dead unit can wipe live rows. +The dead-in-flight interleave is a probe cell (G1d, expected +unreachable-after-pin; `quiesceOff` kills it). Non-interference +(round-3 R3-N2, recorded): a deferred bump cannot starve the pass +budget — deferral requires an in-flight dying execution, hence a +non-drained frontier, and the pass only scans a drained frontier, so +every deferral resolves strictly before the pass's first scan; two +workers' deferrals cannot deadlock — a deferral waits on an +execution's completion and no completion ever waits on a bump, so +the wait graph is one-directional. + +MID-BUMP FENCE (round-3 R3-F2 — the F4 durability discipline +extended to mid-attempt minting): a mid-attempt bump's +generation-table delta is durable BEFORE the bumped generation +dispatches — the bump forces an `eCheckpoint` commit (the checkpoint +carries the generation table, G-RULE-4) between the bump's carrier +announce and the new generation's first dispatch. Without it, a +crash landing after the bumped generation's first durable commit +with the elective checkpoint skipped would re-mint the same id from +the restored table (round-1 F4's hazard through the retraction +path). Probe cell G1e; `midBumpFenceOff` kills it (P-GEN RED). + +RETRACTION QUEUE (E; semantics pinned per R2-F1's disposition): +processing a re-publish announce ENQUEUES (as derived effects) a +retraction entry per reader execution of the now-dead value (keyed +per MSessionStore's pin); each entry re-admits the reader node +(bump + re-admit, quiesce-deferred as above) and is removed when the +re-admitted execution completes. The pre-seal condition for E is an +empty retraction queue. + +OBSERVATION POINTS (S): (i) DEMAND-DERIVATION refusal — a pending +node whose admission stamp contains a dead generation is re-validated +at dispatch: run under a live re-derivation if one has re-derived the +hash, else dropped from the frontier (and its hash removed, +G-RULE-2). (ii) SESSION-READ (round-2 R2-M1, registered): a read +returning a dead-stamped value proceeds (read-through) and emits a +derived dead-read announce that feeds the forced-redo count; no +scheduling effect is needed — the dead value's writer is pending by +construction (its death was a bump that re-admitted it; invariant +stated here, checkable). (iii) PRE-SEAL PASS: scan sealed-bound +outputs; any dead-generation stamp forces the producing node's re-run +(bump + re-admit, derived announces); iterate up to the §8 +PASS-ITERATION BUDGET (round-2 R2-M7 — the bound is an explicit +budget row, not "finite by construction"; convergence within budget +is a CHECKED expectation of every honest cell, via the no-dead-stamps +seal form of P6-S, and a budget-exhausted seal is announce-visible). +ITERATION BOUNDARY (round-3 R3-M3): one iteration = one scan over a +DRAINED frontier — a forced re-admission un-drains it, and the next +scan begins only after it re-drains; the budget counts scans, never +mid-flight re-observations. + +SEAL SEQUENCE: frontier drained → pre-seal pass (S) / retraction +queue empty (E) → SWEEP → `eSeal`. + +Resume: restore checkpoint; bump every pending node (prior generation +dead); commit the FORCED RESUME CHECKPOINT before any dispatch (§5); +then dispatch. Variant E purges pending nodes under the ∀-PREDICATE +(round-2 R2-F5: purge only when EVERY admitted-by edge names a dead +generation — the refcount-consistent reading), removing purged +hashes from the admitted-derivation set. Variant S refuses at +dispatch time per observation point (i). + +ATTEMPT FAILURE (walker parity): a loud node failure fails the +attempt; the scheduler checkpoints, announces the failure with its +GENERATION-BLIND fingerprint (failure point, reason, restored state +modulo the generation table), and MEnv resumes or abandons per the +ladder. + +### MNodeExec (worker-side execution body) + +One execution = marker check (§4a) → consult → verdict (ADOPT is a +REGISTERED VERDICT CLASS in the announce vocabulary, round-2 R2-F3 — +alongside replay / changed-with-diff / fetch-fresh) → emissions. +Unit-mode materialization for replay and diff rounds; record rounds +for fetch-fresh (no marker — G8a pin). SESSION-PUBLISH BODY-OP PIN +(round-3 R3-F1): a node's session publishes/contributes are BODY +STORE OPS executed by every NON-ADOPTED execution regardless of +verdict class — a replay-verdict re-derivation re-publishes; only +adoption skips session ops, which is exactly what the +writer-ineligibility bit exists to prevent. This is the reading +round 2's accepted walks used, registered; it is a within-sync +deviation from the walker's elision vocabulary (§1's boundary +sentence), and it is what makes the writer flap-back history +converge (the MATCH-verdict re-derivation re-publishes under its +live generation, the writer-stamp delta re-derives every reader). Under variant S the +execution's stamp initializes {n: g} and merges every session value's +stamp it reads; ADOPTING executions' premise re-reads REGISTER +normally (round-2 R2-M5: tracked read edges under E+B, stamp merge +under S, attributed to the adopting execution). + +### MSessionStore + +Variant A: free-form KV. Variant B: scratch / publish / contribute; +publish-retraction keyed by (session key, value identity, writer +generation); a re-publish retracts every reader execution of a dead +value, including re-runs that read a stale value before the +re-publish landed (witnessed by G2's G-pending leg, round-2 +R2-M6(i)). + +## 4. Node execution and store interaction + +### 4a. Premise-validated adoption (round-1 F1 repair + round-2 eligibility pins; round-3 review target) + +- The marker is a memoization entry: `eMarkerPut` commits (key, + producing generation, premise digest, publishBearing bit) inside + the unit. The premise digest is the canonical hash of the consult + result (previous-artifact entry + revalidation OUTCOME AND MATCHED + VALIDATOR) and the identity+writer-stamp of every session value + read before unit commit. DIGEST CLOSURE (round-2 R2-N2): the + digest is total over the MODEL'S verdict inputs — consult result + and session reads; config/compat and warm/cold state are constants + in every cell of this spec, so their omission is declared vacuity, + not an escape (a drift-modeling extension must extend the digest + by change order). +- A re-execution finding a marker for its output key recomputes the + premises (re-consult; session re-reads). ADOPTION ELIGIBILITY + (round-2 pins, both load-bearing): + - MATCH-ONLY (R2-F3): adoption requires the re-consult's + revalidation to MATCH. A FAILED revalidation re-derives + regardless of the stored digest — FAIL means upstream moved, the + prior FAIL-verdict's fetched content is not re-attested by + anything current, and a FAIL-vs-FAIL digest equality is an + outcome-bit coincidence (the e1→e2→e3 escape, R2-F3's history; + probe cell G1c). With this pin the §7 freshness claim is TRUE: + every adoption's qualifying consult is the adopting MATCH + itself, this attempt. + - WRITER-INELIGIBILITY (R2-F2, the round-2 re-review target): a + marker whose publishBearing bit is set NEVER adopts — a dead + writer re-derives always. Rationale, pinned: adoption re-grounds + ROWS ONLY; a dead generation's session publishes cannot be + re-grounded by any rows-only mechanism, and a stranded dead + publish defeats the observation pass's progress (readers re-adopt + through unchanged premises forever — R2-F2's loop). Under this + pin the dead-publish state is TRANSIENT by construction: the + writer's forced re-derivation re-publishes under its live + generation (same value or not — true for EVERY verdict class + including replay, per the §3 body-op pin, round-3 R3-F1), + retraction (E+B) or the stamp + delta (S: writer stamp g_dead → g_live differs → reader digest + differs → re-derive) clears every reader, and the pre-seal pass + CONVERGES within the iteration budget on every honest history OF + THE SCRIPTED ENVELOPE (round-3 R3-M4's scoping: the sessions × + demand-shrink shape, where a de-demanded writer never + re-publishes, is excluded by declaration — §1/§8) — + the convergence argument the round-2 charge question 5 found + missing, verified mechanically by round 3 (every honest walk + converges in ≤ 2 of the budgeted 3 iterations; the at-least-once + cost claim verified honest, R3-N1: count-legal, + suppression-safe, verdict-neutral). + Cost: writers never amortize across death — recorded as + forced-redo count in the bake-off, safety-neutral. + - Digest EQUAL (and eligible) → ADOPT: one atomic `eAdopt` — + stamps rewritten to the live generation (S), fold/count transfer + (§4d), marker generation updated, stored rows re-announced under + the adopting execution's attribution. Rows only; adoption- + eligible rounds have no session writes by the eligibility bit, + so rows-only re-announce is TOTAL over what the round emitted. + - Digest DIFFERENT or ineligible → RE-DERIVE: the execution + proceeds to its verdict as if unmarked. Its superseding round + clears per §4b — a UNIT round's `eMarkerPut` constituent + overwrites the marker; a RECORD round's `eClearScope` deletes it + (round-2 R2-F4's text correction: fetch-fresh re-derivations are + record rounds and carry no `eMarkerPut`). +- Walker coherence: for MATCH premises with no session inputs, + adopt-on-equal is observationally the walker's clause (iii), + strictly fresher (the adopting MATCH qualifies under the F8 pin) — + round 2 verified this degeneration argument correct for the MATCH + shape; the FAIL shape is now excluded by eligibility rather than + overclaimed. + +### 4b. Supersession and marker lifecycle — the total matrix + +Rows and markers, total over {existing rows dead vs live} × +{incoming unit vs record} × {REPLACES vs OVERLAY intent}, with the +MARKER COLUMN (round-2 R2-F4): + +| existing | incoming | rows | marker | +|---|---|---|---| +| dead rows | unit (replay/overlay) | clear constituent removes them; fold/count contribution removed (§4d) | `eMarkerPut` constituent overwrites (new gen + digest + bit) | +| dead rows | record REPLACES | `eClearScope` removes them; contribution removed | `eClearScope` DELETES the marker (§3 pin) | +| dead rows | record OVERLAY-intent | ILLEGAL under the trust model (base-liveness precondition): overlay composes only over a live base; scripted connector policies never produce it and — for record-over-record keys, which carry NO marker — the precondition rests entirely on scripted policy + the trust model (round-2 R2-M4's honest wording; the `overlayComposeDead` MUTANT is the load-bearing check: expected P1-CONTENT red on epoch divergence + P6-S dead-stamp red under S) | n/a (no marker on record keys) | +| live rows (same derivation) | any | round continuation / at-least-once redo | unit rounds refresh it; record rounds have none | +| live rows (distinct derivation hash, same key) | any | CONNECTOR-CONTRACT VIOLATION: store poisons the scope on the second derivation's first commit; P1 legality EXEMPTS poisoned scopes (the poison is the alarm); seal excludes the scope (SealExpect) | POISON VOIDS the key's marker (round-2 R2-M8): no adoption of poisoned content is ever legal; post-poison rounds for the key commit legally but the scope stays seal-excluded | + +MARKER INVARIANT (round-2 R2-F4, monitored as P-MARK, §7): a marker +present for a key ⟹ the key's current partition equals the marked +round's committed outputs. Every matrix row above preserves it; the +flap-back history that violated it in v2 (record re-derivation +stranding a stale marker, later premise flap-back adopting content +the marker no longer described) is unreachable after the +clear-deletes-marker pin — probe cell G8d, `markerCleanupOff` kill. + +### 4c. Cross-sync stamps + +RESTAMP-ON-REPLAY: REPLAYED rows (cross-sync copies) carry the +replaying execution's stamp; the source artifact's own seal +discharged its internal provenance. (v2's "adopted-across-sync" +wording deleted — adoption is WITHIN-SYNC only; markers are per-sync, +§3.) Cross-sync session-stamp travel remains walker P6-R +jurisdiction. + +### 4d. Fold and count grounding over generations + +Unchanged from v2 (round-2 verified the back-port coherent and the +removal clause correctly DEATH-GATED — a live-rows removal reading +would dissolve walker cell 4's alarm; the gate is load-bearing, +stated here per the round-2 caveat): a generation's complete round +stays in fold and count until ADOPTED (contribution transfers) or +SUPERSEDED (§4b removal). The v2 incoherence (transferring a removed +contribution) is unreachable once §4b's marker column exists. + +## 5. Durability, crash, and resume semantics + +Unchanged from v2 except: the marker row now reads "(key, generation, +premise digest, publishBearing)" and is per-sync (dropped at seal, +§3); the forced-resume-checkpoint row is unchanged (round-2 verified +F4 applied correctly and P-GEN checkable per R2-N3's recorded rule); +mid-attempt bumps' SCHEDULING EFFECT is volatile and its loss is +self-healing (the triggering re-publish or observation re-fires on +the resumed attempt — at-least-once), with their announces carried +per G-RULE-1's derived-announce pin — but the bump's +GENERATION-TABLE DELTA is NOT self-healing against id reuse and is +durably fenced by the §3 MID-BUMP FENCE before the bumped +generation's first dispatch (round-3 R3-F2: "self-healing" is true +for the loss of the bump's scheduling effect, never for the minted +id). + +| state | machine | durability | +|---|---|---| +| partitions, manifest, poison, session KV, seal flag | MStore | durable at op commit | +| unit marker (key, gen, digest, publishBearing) | MStore | durable at op commit; per-sync (seal drops); deleted by REPLACES clear; voided by poison | +| frontier checkpoint (per G-RULE-4) | MStore | durable at eCheckpoint commit | +| forced resume checkpoint | MStore | restore → bump → eCheckpoint commit → dispatch (P-GEN's ground) | +| in-flight execution state | MNodeExec | volatile vs crash; vs mid-attempt death, quiesce-before-bump defers the bump (§3) | +| derived support counts (E) | MGraphScheduler | volatile; checkpoint-consistent rebuild target | +| retraction queue (E), pass state (S) | MGraphScheduler | volatile; self-healing at-least-once (re-publish / re-observation on resume) | +| mid-attempt bump table delta | MStore (via forced eCheckpoint) | durable BEFORE the bumped generation dispatches (§3 MID-BUMP FENCE, R3-F2) | +| causal stamps (S) | ride outputs | durable where the output is; `eAdopt` rewrites atomically | +| generation table | MGraphScheduler | latest-per-pending in checkpoints; death announce-visible (`eAnnGenBump` at carrier commits) | + +Walker→graph mapping: unchanged from v2 (review-verified accurate). + +## 6. Variant axes and mutation toggles + +Axes and defaults unchanged from v2 (lineage per-leg; session A +default, vacuous outside G2/G9; compression off outside G9). + +| toggle | what it removes/injects | kill cell | expected flip | +|---|---|---|---| +| `suppressionOff` | G-RULE-2 admission suppression | G1 mutant leg / G4 | P1-LEGALITY first-find (racing schedules; sequential adopts — live-fromGen deviation per R2-N1) | +| `sweepOff` | seal-time sweep | G5b | P5-UNDER (+ P6-S under S) | +| `sweepOverreach` | sweep drops in-closure partition | G5c | P5-OVER + P1-CONTENT | +| `purgeOff` (E) | pending-purge on death | G5e | execution-count oracle (redo machinery; seal stays green) | +| `stampMergeOff` (S) | read-side stamp merge | G2-S legs | P6-G RED (oracle, mechanism-independent) | +| `retractionOff` (E+B) | re-publish reader retraction | G2 E+B leg | P6-G RED | +| `overlayComposeDead` | §4b base-liveness precondition | G8b mutant leg | P1-CONTENT + P6-S (S) | +| `resumeCkptOff` | forced resume checkpoint | G1b | P-GEN RED | +| `demandDropOff` | drops one derived admission | G5f | closure oracle RED (SealExpect) | +| `adoptOnFail` | removes MATCH-only eligibility | G1c mutant leg | P-ADOPT RED (GS-CO-002: the smuggled rows(e2) sits INSIDE the sync-scoped freshness envelope, so no artifact-level oracle can see it; SealExpect stays GREEN by design — a declared control) | +| `writerAdopt` | removes writer-ineligibility | G2 announce-window legs | S: P6-S RED at seal (pass exhausts its budget against the stranded dead publish — R2-F2's loop, now a kill); E+B: P6-E RED (no re-publish, readers never retracted) | +| `quiesceOff` | quiesce-before-bump | G1d | P6-G RED (dead in-flight unit's late clear wipes the live re-derivation — R2-F1's walk, now a kill) | +| `midBumpFenceOff` | §3 mid-bump fence (checkpoint-at-bump) | G1e | P-GEN RED (two-crash id reuse — R3-F2's walk, now a kill; mirrors G1b/`resumeCkptOff`) | +| `markerCleanupOff` | REPLACES clear deletes marker | G8d | P-MARK RED (stale marker survives a record supersession; the flap-back then adopts mismatched content — P3′ RED under E) | + +## 7. Properties (checkable forms) + +[Post-freeze editorial, two-track seam: the composition algebra the +fold pins assume (L1–L6) and the stamp-lattice laws the S variant +rests on — merge as a join-semilattice, dead-membership +homomorphism, floor-compression staleness soundness (L7–L9) — are +mechanically proved in `formal/occult/LAWS.md`; the model consumes +them as assumptions per the brief's division of labor. No semantic +change.] + +P1/P2/P4 as in v2 (P1 with §4d grounding + poison exemption; P2's +qualifying consult for adopted scopes is the adopting MATCH — true +under MATCH-only eligibility; P4 with the generation-blind +fingerprint). P3′ is the walker form, honestly named (v2 pin). + +- P5 + CLOSURE ORACLE: unchanged from v2 (ghost closure from live + announces; env-side counterfactual SealExpect as the independent + base; round 2 confirmed the independence is real — the oracle is + what catches R2-F5's starvation, now repaired). (GS-CO-002) + SealExpect content is SYNC-SCOPED: the env announces the live + per-key world at EVERY attempt start and the monitor accumulates + the sync's acceptable epoch SET; sealed content must match SOME + attempt-start world. Calibration find G1-CAL-1: a key whose + derivation completed and checkpointed before a crash legitimately + seals the earlier attempt's world (completed-across-crash, + G-RULE-2), so the single-epoch expectation reds an honest history. + Closure (which keys) stays exact both directions. +- P-ADOPT (adopt legality, GS-CO-002): every adoption is justified + by a validated MATCH consult announced by the adopting + (node, generation) BEFORE the adopt commits — the checkable form + of R2-F3's MATCH-only eligibility. This monitor, not SealExpect, + is the `adoptOnFail` kill: FAIL-adopt laundering is + mechanism-visible only (see §6). +- P6-G (laundering oracle, all legs): unchanged from v2; round 2 + confirmed mechanism-independence (both kills flip against it). +- P6-E (mechanism conformance, E+B): retraction liveness — every + reader execution of a dead value re-runs before seal (keying per + §3; adopting re-readers included per R2-M5's pin). +- P6-S (mechanism conformance, S): AT-SEAL form only (round-2 R2-F6): + no sealed output carries a dead-generation stamp; no partition + mixes causally incomparable generations. P6-S red MEANS the + mechanism failed (a dead stamp survived the pass). The + VALUE-BLINDNESS pin is recast (R2-F6's vocabulary fix): + dead-stamp-forced redo on value-identical re-derivation is + intended MECHANISM BEHAVIOR whose signal is the FORCED-REDO COUNT + (derived announces: observation-pass re-runs, dead-read events, + retraction re-admissions) — a bake-off metric, never a property + verdict. A cell expecting mechanism overhead declares an expected + count, not a red. +- P-GEN: no two attempts contain store-commit announces attributed + to the same (node, generation); adoption re-announces attribute to + the adopter (R2-N3's rule, recorded). +- P-MARK (round-2 R2-F4): marker ⟹ partition equals the marked + round's outputs; announce-evidenced (markers, clears, adopts, and + row commits are all announced). +- Compression admissibility: unchanged (G9; safety verdicts + invariant, redo may grow). + +### 7.5. The frozen mechanism tally (amended v3, BEFORE any bake-off run — the round-2 findings changed mechanism counts and the tally must reflect them pre-run; further amendment requires a change order) + +| column | mechanisms | +|---|---| +| shared | frontier checkpoint (+ admitted-derivation set with DEATH SEMANTICS, admitted-by edges, cursors); forced resume checkpoint; generation table + bump rule + QUIESCE-BEFORE-BUMP; markers with premise digests + publishBearing + adopt-or-re-derive (MATCH-only, writer-ineligible) + marker lifecycle column; supersession matrix + poison rule; sweep + closure oracle; unit ops | +| E adds | durable admitted-by edges (checkpoint row); ∀-pending-purge (redo machinery); derived-support rebuild + agreement check; retraction rule + queue with pinned enqueue/drain; REQUIRES session variant B | +| S adds | stamp merge on read; stamp field + `eAdopt` rewrite; three observation points (dispatch-time refusal, session-read read-through + dead-read count, pre-seal pass with iteration budget); optional bucketed compression | + +Counting rule unchanged (durable state classes > scheduler rules > +per-output overhead). + +## 8. Small-scope configuration budgets + +As v2 (safety envelope; widened bake-off envelope: depth 2, one +fan-in edge), plus: PASS-ITERATION BUDGET (round-2 R2-M7): ≤ 3 +pre-seal pass iterations per seal; honest cells must converge within +it (checked — a budget-exhausted seal with dead stamps is P6-S red +and means a mechanism bug). ATTEMPT BUDGET ≤ 3 (unchanged). +INDUCTIVE-BET RESTATEMENT: the envelope excludes session-transitive +(reader-writer) chains, publish-derived demand, and SESSIONS × +DEMAND SHRINK (round-3 R3-M4: a de-demanded writer × a +still-demanded reader of its sync-scoped value — the one shape +where §4a's convergence mechanism has no lever) by declaration +(§1); first-order retraction and row-transitive support are +exercised (G6b/G6c); the bet is that no graph-runtime bug class +requires deeper session topology — stated, not assumed silently. + +## 9. Scenarios and cells (expected verdicts declared before first run) + +- **G1 phantom union**: legs (i) crash-before-commit → fetch-fresh, + GREEN (round-2 walked, derives); (ii) crash-after-commit + + mutation → marker found, revalidation FAILS → RE-DERIVE (MATCH-only + pin; NOT adoption) → fetch-fresh RECORD round — §4b's record + REPLACES row removes the dead rows AND deletes the marker (round-2 + text correction applied) → seal rows(e2)@V2, GREEN both variants; + (iii) crash-after-commit, no mutation → MATCH, digest equal, + writer bit clear → ADOPT → GREEN both variants (round-2: "the + repair working"). Mutant `suppressionOff`: P1-LEGALITY first-find + via the racing schedule (sequential adopts; live-fromGen deviation + noted). +- **G1b generation-reuse probe**: unchanged (round-2 verified + well-formed). `resumeCkptOff` → P-GEN RED. +- **G1c FAIL-adopt probe (round-2 R2-F3)**: e1→e2→e3, crash in the + announce window after attempt 1's CHANGED-WITH-DIFF unit (marker + digest = (V1, FAIL)); attempt 2's re-consult FAILS vs e3. Honest: + MATCH-only pin forces re-derive → fetch reflects e3 → GREEN. + Mutant `adoptOnFail`: adopts rows(e2) after the FAIL consult → + P-ADOPT RED (GS-CO-002; kill reassigned off SealExpect — the + sync-scoped expectation set accepts rows(e2) because e2 is a + legitimate attempt-start world of the sync, and the + completed-across-crash schedule seals it honestly; SealExpect and + P3′ are declared GREEN controls on the mutant cell). +- **G1d dead-in-flight probe (round-2 R2-F1)**: G2's E+B chassis, + schedule forcing the retraction bump while the dying reader + executes. Honest: quiesce defers the bump; the re-derivation is + the last writer; P6-G GREEN. Mutant `quiesceOff`: the dead unit's + late clear+commit wipes the live rows → P6-G RED. +- **G1e mid-bump reuse probe (round-3 R3-F2)**: the G-pending + chassis, two crashes; attempt 2's retraction-forced bump mints + (G, g3), G@g3 commits a durable unit, crash lands with the + elective checkpoint skipped. Honest: the §3 MID-BUMP FENCE forces + the checkpoint at the bump, so resume 2 restores a table already + carrying g3 and mints g4 — no collision; P-GEN GREEN; the reuse + history is unreachable-after-pin. Mutant `midBumpFenceOff`: resume + 2 re-mints (G, g3) from the stale table → attempts 2 and 3 both + carry (G, g3)-attributed store commits → P-GEN RED. +- **G2 session laundering**: legs as v2 with round-2 corrections: + E+A → P6-G RED (derives; round-2 walked). E+B → P6-G GREEN under + the quiesce pin (round-2: conditionally derivable, now + unconditional); `retractionOff` → P6-G RED. S+A and S+B → P6-G + GREEN, P6-S green (derives; the writer re-derives live and the + pass converges); `stampMergeOff` → P6-G RED. SAME-VALUE CONTROL + LEG (round-2 R2-F6 correction): H@g2 re-derives d1 exactly → + P6-G GREEN, P6-S GREEN at seal, FORCED-REDO COUNT ≥ 1 (declared + expected count, not a red — G's re-derivation is forced by the + writer-stamp delta). ANNOUNCE-WINDOW LEG (round-2 R2-F2's + placement, now scripted): checkpoint captures H pending with its + publish-bearing unit committed; crash; resume bumps H → H is + adoption-INELIGIBLE (publishBearing) → re-derives → re-publishes + → readers cleared (retraction under E+B / stamp delta under S) → + P6-G GREEN, P6-S green, pass converges within budget. CHASSIS + REGISTRATION (round-3 R3-F1): the `writerAdopt` kill needs an + adoption-ELIGIBLE publish-bearing marker, which under the §3 + body-op pin is constructible by script — a REPLAY-verdict + publish-bearing unit (MATCH consult, body publish) with a + premise-stable resume (no between-attempt mutation), so the + re-consult MATCHes and the digest is equal; the kill leg schedules + exactly this chassis. Mutant + `writerAdopt` on this leg: S → P6-S RED (stranded dead publish; + pass budget-exhausts — R2-F2's loop as a kill); E+B → P6-E RED (no + re-publish, no retraction). WRITER FLAP-BACK PROBE (round-3 + R3-F1's second registration): H's diff-verdict publish-bearing + unit commits; checkpoint in the announce window; crash; upstream + flaps back (content(e3) = content(e1)). Resume: H ineligible → + re-derives → re-consult MATCHes → REPLAY verdict → the body-op + pin re-publishes d1@g2 anyway → writer-stamp delta re-derives G → + pass converges. Expected: GREEN, forced-redo count ≥ 1 (under the + elision reading this history would strand d1@g1 and red P6-S + honestly — the probe is the body-op pin's load-bearing witness). + G-PENDING LEG (round-2 R2-M6(i)): + checkpoint captures G pending mid-read-window; G's pre-re-publish + re-run reads stale d1, then H re-publishes d2 → the re-retraction + clause fires (G re-runs again) → P6-G GREEN; witnesses the keying + pin's re-retraction clause. +- **G3 artifact swap**: unchanged, GREEN (round-1 walked clean). +- **G4 duplicate admission**: unchanged (suppression GREEN; + `suppressionOff` RED; distinct-derivation shape lives in G8c). +- **G5 sweep family**: (a)–(d) unchanged from v2 (round-1/round-2 + walked); (e) purge-domain cell under the ∀-purge pin — the + paginated parent's child C has ONE admitted-by edge (dead) → purged + → hash removed → count oracle asserts no post-resume C execution; + `purgeOff` → count-oracle RED. (f) `demandDropOff` → closure + oracle RED; HONEST BASELINE re-derived under the R2-F5 pins + (no-shrink epoch: C re-admitted after hash removal via the live + re-derivation — starvation unreachable; round-2's composition + defect repaired and the cell asserts it). +- **G6 redo-work bake-off**: scripts unchanged from v2 [GS-CO-005: + the v2 text is unrecoverable; the control legs are declared in + §12]; G6c's E leg + now derives under the ∀-purge pin (C survives on S2's live edge — + round-2's ∃/∀ divergence resolved); expected counts unchanged; + writers' forced re-derivations (writer-ineligibility) appear in + the counts wherever a session writer dies — declared expected, + charged to BOTH variants equally (the pin is shared machinery). +- **G7 progress under churn**: unchanged (generation-blind + fingerprint; ladder; budget 3). +- **G8 supersession family**: (a) record REPLACES over dead debris — + GREEN (clear wipes rows AND marker); (b) OVERLAY-intent dead base — + honest GREEN by scripted policy, `overlayComposeDead` RED; + (c) same-key distinct-derivation race — poison + SealExpect + exclusion + legality exemption + MARKER VOIDED (R2-M8; a scripted + forced re-run of the first node post-poison must NOT adopt — the + leg asserts re-derive-or-refuse on a poisoned key); (d) MARKER + FLAP-BACK PROBE (round-2 R2-F4): e1→e2→e3 with content(e3) = + content(e1), two crashes; attempt 2 re-derives via record REPLACES + (marker deleted); attempt 3's consult finds NO marker → ordinary + consult → V1 MATCHes vs e3 (truthful validator; content reverted) + → REPLAY verdict (round-3 R3-M1's correction: not fetch-fresh — + replay of the e1 base = rows(e3), SealExpect verdict-equal either + way) → SealExpect rows(e3) GREEN. Mutant `markerCleanupOff`: + the stale marker survives, attempt 3 premise-matches it (V1 MATCH + vs e3) and adopts content the marker no longer describes → P-MARK + RED (+ P3′ RED under E per the round-2 walk). +- **G9 compression admissibility**: unchanged from v2. + +## 10. Adequacy obligations + +1–6 as v2 (kills incl. count-oracle kills; reachability probes; +independent oracle bases; rebuild-agreement active in E resumes; +bake-off table from checker output + frozen tally; G5d meta-analysis +with a which-is-right determination). +7. The decision rule (§10.7, v2) unchanged [GS-CO-005: the v2 text + is unrecoverable; the rule as declared in §12 is the citable + version]; the §7.5 tally as amended + v3 is final for the bake-off (further amendment = change order + BEFORE any bake-off run). +8. NEW (round-2): P-MARK active in every cell with a marker; + convergence of the pre-seal pass within the §8 iteration budget is + asserted in every honest S cell (a budget-exhausted honest seal is + a finding, not noise). + +## 11. Adversarial review record + +Three rounds complete; the spec is FROZEN at v4. Round 1 (full): +REJECT, 11 majors — dispositioned in v2. Round 2 (targeted, +adoption): REJECT, 6 majors — adoption core verified sound; +seams dispositioned in v3. Round 3 (targeted, the v3 repairs; +`formal/reviews/graph-spec-round3-repairs.md`): REJECT, 2 majors + +4 minors + 3 notes — ALL six round-2 repairs verified soundly +applied and composing (writer-ineligibility convergence derives +mechanically on every scripted honest history; quiesce cannot starve +or deadlock; the marker matrix is total; admitted-set death +semantics close the starve/double-admission space; MATCH-only is +classifiable everywhere with SealExpect the right oracle; the G2 +table derives leg by leg), and every round-3 finding was +fix-without-re-review — per the review's own verdict, the fixed +spec needed a disposition registration check, not a fourth round. +v4 is that registration (§12). Checker verdicts against v4 are +citable. + +Standing questions (future change orders): MODEL_SPEC §10's list; +the elision-vocabulary boundary (§1 — if the runtime design elides +publish-bearing replay, re-open R3-F1's product); sessions × demand +shrink (§1/§8) if session scoping ever outlives a sync. + +## 12. Change-order log + +- v1: initial draft. Round-1 review: REJECT (11 majors + 7 minors + + 3 notes). +- v2: round-1 dispositions (see the v2 entry in the git history of + this file and `formal/reviews/graph-spec-round1.md`). Headline: + premise-validated adoption replacing marker suppression; + P6-G laundering oracle; generation-grounded fold pins; forced + resume checkpoint; durable admitted-by edges; total supersession + matrix; closure oracle; G5d meta-analysis reformulation; + divergence bake-off scripts + widened envelope; compression cells; + generation-blind P4 fingerprint; 10 minor/note registrations. + Round-2 targeted review of the adoption mechanism: REJECT (6 + majors + 8 minors + 3 notes; core verified sound, seams found). +- v3: round-2 dispositions + (`formal/reviews/graph-spec-round2-adoption.md`). R2-F1 → + QUIESCE-BEFORE-BUMP pin + retraction-queue semantics + G1d probe + + `quiesceOff` kill. R2-F2 (re-review target) → WRITER-ADOPTION + INELIGIBILITY: publish-bearing markers never adopt; dead publishes + are transient by construction and the pre-seal pass's convergence + is argued and checked (§4a, §8 iteration budget); `writerAdopt` + kill on the new announce-window leg; publish-derived demand + excluded from G-RULE-1 by declaration (§1). R2-F3 → MATCH-ONLY + adoption eligibility + ADOPT registered as a verdict class + G1c + probe + `adoptOnFail` kill + freshness prose corrected. R2-F4 → + marker lifecycle column in §4b (REPLACES clear deletes; unit put + overwrites; poison voids), P-MARK invariant monitor, §4a/G1(ii) + text corrections, G8d flap-back probe + `markerCleanupOff` kill, + per-sync marker scoping (R2-M3). R2-F5 → ∀-purge predicate; + purge/refusal remove the derivation hash; G-RULE-2 MUST-suppress; + G5f baseline and G6c E-leg re-derived. R2-F6 → P6-S at-seal form + only; forced-redo COUNT as the mechanism-overhead signal; G2 + same-value control leg corrected. Minors: session-read observation + registered as read-through + dead-read count with the + writer-pending invariant (R2-M1); derived-announce carrier pin + (R2-M2); marker scoping (R2-M3); dead-base detection wording + (R2-M4); adopting re-reads register normally (R2-M5); G-pending + re-retraction leg added, session-transitive chains and + publish-derived demand excluded with argument (R2-M6); + pass-iteration budget row (R2-M7); poison-voids-marker + G8c + post-poison leg (R2-M8). Notes recorded: eAdopt fromGen-dead + precondition (R2-N1), digest-closure vacuity sentence (R2-N2), + P-GEN monitor rule (R2-N3). Tally amended pre-run (§7.5). + Round-3 targeted review of the v3 repairs: REJECT (2 majors + + 4 minors + 3 notes; all six round-2 repairs verified sound; every + finding fix-without-re-review). +- v4 (FROZEN): round-3 dispositions + (`formal/reviews/graph-spec-round3-repairs.md`). R3-F1 → + SESSION-PUBLISH BODY-OP PIN (§3): session publishes are node-body + store ops executed by every non-adopted execution regardless of + verdict class (the reading round 2's accepted walks used, + registered); declared within-sync deviation from the walker's + elision vocabulary with the §1 F17 boundary sentence extended; + announce-window kill chassis registered (replay-verdict + publish-bearing unit, premise-stable resume); writer flap-back + probe leg added to G2 (expected GREEN, forced-redo ≥ 1). R3-F2 → + MID-BUMP FENCE (§3): a mid-attempt bump's generation-table delta + is durable (forced eCheckpoint) before the bumped generation + dispatches — the F4 discipline extended to the second minting + path; §5's two rows amended ("self-healing" scoped to the + scheduling effect, never the minted id); G1e reuse probe + + `midBumpFenceOff` kill (P-GEN RED). R3-M1 → G8d honest-leg + verdict corrected to REPLAY (SealExpect verdict-equal). R3-M2 → + store-side `eAdopt` poison precondition (closes the + check-then-act window). R3-M3 → pass ITERATION BOUNDARY pin (one + iteration = one scan over a drained frontier). R3-M4 → + convergence claim scoped to the scripted envelope; SESSIONS × + DEMAND SHRINK exclusion declared in §1 and §8's inductive bet. + Notes recorded: at-least-once cost claim verified honest (R3-N1, + into §4a); deferral non-starvation/non-deadlock arguments (R3-N2, + into §3); round-2 registrations sweep-verified applied (R3-N3). + FROZEN — subsequent changes are GS-CO-NNN change orders. +- GS-CO-001 (calibration, G1 build-out): G-RULE-3's durable fence + made TOTAL over all four minting paths (attempt-start root mint, + first-admission mid-attempt mint, resume bump, mid-attempt bump). + Found by P-GEN redding an HONEST single-crash G1 history: attempt + 1 crashed before any checkpoint, attempt 2 cold-restarted and + re-minted (P, 1). Encoding: path (a) rides `resumeCkptOff` + (making the G1b kill fireable with one crash — acceptable, the + kill statement is unchanged), path (b) rides `midBumpFenceOff`. + §2 amended in place, tagged. +- GS-CO-002 (calibration find G1-CAL-1): SealExpect content check + made sync-scoped (acceptable epoch SET accumulated across attempt + starts) — the single-epoch form reds the honest + completed-across-crash schedule, which G-RULE-2 licenses. The + `adoptOnFail` kill is consequently invisible to every + artifact-level oracle and moves to the new P-ADOPT mechanism + monitor (adoption requires a prior validated MATCH consult by the + adopting generation, announced before the adopt commits — the + announce-order pin is part of the monitor's soundness). §6, §7, + §9 amended in place, tagged. Calibrated: G1 sweep 10/10 at 5000 + schedules (greens hold incl. SealExpect+P3′ controls on the + mutant; kills red with first-finds P1-LEGALITY / P-GEN / + P-ADOPT). +- GS-CO-003 (calibration find, G2 build-out): CARRIER-DURABILITY + ATOMICITY. The R2-M2 carrier pin extends to durability: a + completion carrier's derived effects — every admission, mint, and + admitted-by edge — commit as ONE durable delta, and no checkpoint + may separate the carrier's completion from its admissions. The + per-mint fencing GS-CO-001 first encoded leaves a lost-demand + window: a crash between two children's fences restores the parent + COMPLETED with the second child's admission gone; a completed + parent never re-derives, the demand starves, and the closure + oracle reds an honest session-topology history (found on the + first honest G2 run). Edge-only registrations are covered too: a + lost live admitted-by edge mis-arms the ∀-purge predicate + (R2-F5) on resume. Encoding: one forced checkpoint at the end of + the carrier's demand loop, riding `midBumpFenceOff`. §2's + GS-CO-001 sentence is superseded on the (b) path by this rule. +- GS-CO-004 (calibration find, G2 build-out): RETRACTION CATCH-UP + BOUND. The retraction rule is RE-PUBLISH-DRIVEN (R2-M6(i)'s + wording holds as written); the registration side (a read that + registers when its value's death is already knowable, i.e. the + read-note lost the race to the re-publish carrier) is a CATCH-UP + retraction spent ONCE per (reader, dead writer-generation). An + unbounded registration-side rule livelocks the `writerAdopt` + strand — the adopted writer never re-publishes, the reader + re-runs forever, the frontier never drains, and the at-seal + oracles (P6-E/P6-S) are structurally unevaluable, turning the + kill cell GREEN-by-divergence (found: the kill would not fire). + With the bound, the strand costs one re-run, seals, and P6-E + reds the still-dead final read. Session reads register at READ + time (read-through, R2-M1) via a scheduler note; carrier-time + registration makes the R2-F1 dying-reader race structurally + unreachable (also found in this build-out). +- GS-CO-005 (bake-off phase): BAKE-OFF PROTOCOL REGISTRATION — + provenance repair plus pre-run declarations. + (a) REPAIR: §10.6's meta-analysis, §10.7's decision rule, and §9 + G6's "scripts unchanged from v2" cite v2 subsection text + recoverable only from this file's git history, which was never + created (the formal tree is untracked as of this registration). + The rule and both deferred procedures are DECLARED here, BEFORE + any bake-off run; this text is the citable version and supersedes + the §9/§10 references. + (b) DECISION RULE (§10.7 restated): lexicographic, first non-tie + decides. Axis 1 — property satisfaction on the frozen 66-cell + matrix: an honest-cell red or an unfired declared kill eliminates + a variant; a correctness dependency (E REQUIRES session variant + B, evidenced by tcG2ea_P6G) travels with the variant as part of + its Axis-2 mechanism bill, not as an elimination. Axis 2 — + mechanism count per the §7.5 frozen tally under its counting rule + (durable state classes > scheduler rules > per-output overhead). + Axis 3 — redo work under the divergence scripts: minimal green + exec bounds per chassis, the declared-expected redo probes, and + pass-budget consumption. A hybrid recommendation is admissible + only if expressible as the shared column plus exactly ONE + variant's adds. + (c) G6 v1 CONTROLS (metric floor, declared): no-crash G6a/G6c + chassis at bound 1 — tcG6aE_Ctl / tcG6aS_Ctl / tcG6cE_Ctl / + tcG6cS_Ctl declared GREEN (zero-crash redo is zero under BOTH + variants; a red is a variant-overhead find, Axis-3 data). G6b + bound-1 probes — tcG6bE_Redo / tcG6bS_Redo declared RED (the + mutation chassis's redo exists under both variants; a green leg + is a divergence datum). + (d) G5d META-ANALYSIS (reachable-seal-world comparison, + declared): GSEALWORLD existence probe — RED means the announced + target world is sealed for the interrupted sync, where a world is + the manifest restricted to keys sealing non-empty partitions. On + the shrink chassis (cell 24, honest config), both variants: + W1 = {0→2} REACHABLE (RED probe: P re-ran at e2, C swept); + W2 = {0→1, 1→1} REACHABLE (RED probe: P completed-across-crash + at e1, C live — G-RULE-2); W3 = {0→2, 1→1} UNREACHABLE (GREEN + probe: the sweep-failure world; honest reachability would be a + P5-shaped finding). The W1/W2 REDs on the same chassis are the + probe mechanism's positive controls against a vacuous W3 GREEN. + WHICH-IS-RIGHT RULE: every reachable world must lie inside the + sync-scoped SealExpect envelope (asserted in-cell by the honest + G5a greens); a world reachable under exactly ONE variant is a + divergence finding that blocks Axis-3 citation until + dispositioned. diff --git a/formal/MODEL_SPEC.md b/formal/MODEL_SPEC.md new file mode 100644 index 000000000..b6d67339e --- /dev/null +++ b/formal/MODEL_SPEC.md @@ -0,0 +1,1594 @@ +# Model spec — walker + source-cache replay calibration model (P) + +Status: v11, FROZEN. The v10 addendum (overlay-flavor extension of +the atomic-unit variant: §9.6 V-OVERLAY-UNIT, cells 6-overlay and +6-overlay-naive, §5 buffer row) received the targeted round-7 spot +review: REJECT with 3 majors + 2 minors + 2 notes, ALL +fix-without-re-review, applied as v11 +(`formal/reviews/model-spec-round7-overlay.md`); no cell verdict +overturned, no round 8 warranted. v11 also bundles the deliverable-2 +de-scope edits (the `compositionEnum` and `annotationBinding` toggles +stay documented but are NOT BUILT in P — the atomic unit is the +single fix story for the scoping bug family; §10.5 kill obligations +adjusted) and the case-3 re-run extension of V-ATOMIC (§9.6). All +prior content is reviewed through round 6. +The v8 addendum (§9 scenario 7 — sessions × replay elision, P6-R, +session-taint toggles) received the targeted round-6 spot review: +REJECT with 4 majors + 5 minors + 3 notes, ALL fix-without-re-review, +applied as v9 (`formal/reviews/model-spec-round6-scenario7.md`); all +cell verdicts and shipped-system claims verified, no round 7 +warranted. The v6 addenda (§9 cells 1d and 6, §7 fold-totality +clauses) passed the round-5 spot review similarly (2 majors + 6 +minors → v7, `formal/reviews/model-spec-round5-addenda.md`). Review history: round 1 REJECT (15 +findings → `formal/reviews/model-spec-round1.md`); round 2 of v2 REJECT +(2 blockers, both scenario-reachability defects → §9 re-scripted on the +graceful-stop premise generator, standing reachability-walk obligation +in §10.0 — `formal/reviews/model-spec-round2.md`); round 3 of v3 REJECT +(1 blocker: scenario 5b scripted a loud failure where the B1 contract +and code silently ignore annotations; all 18 §9 cells walked, 17 +confirmed — `formal/reviews/model-spec-round3.md`); round 4 of v4 +REJECT with 0 blockers / 0 majors — all seven round-3 dispositions +verified genuine, three fix-without-re-review minors applied as v5, +and per the round-4 verdict no further full round is warranted +(`formal/reviews/model-spec-round4.md`). On signoff this spec freezes +and is committed as the diffable baseline; deviations discovered while +writing P code are appended as change orders (MS-CO-NNN), never +silently absorbed. No green checker run is trusted before the freeze. + +Covers deliverables 2 and 3 of `docs/tasks/sync-formal-model-brief.md`: +the tiered walker + 6b source-cache replay orchestration, its calibration +cases, and the staged composition-enum mitigation. The graph-runtime +model (deliverable 4) gets its own spec addendum, with its own +adversarial review; the machine interfaces below are designed so the +graph scheduler variant slots in without remodeling +Upstream/Store/SessionStore. + +Vocabulary is `formal/GLOSSARY.md`. Behavioral ground truth is +`docs/verification/sync-replay-6b/plan.md` (frozen contract B1–B10, +CO-6b-001..007); code anchors are cited where the contract is silent. + +## 1. What is modeled, what is abstracted + +In the model: + +- The dispatch loop: LIFO action stack, batched same-op dispatch to + bounded workers, loop-top checkpointing, restart-from-root under hard + crash (CO-6b-002), graceful-stop forced checkpoints + (`checkpointOnStop`, run-expiry — `parallel_syncer.go` 195, 204, + 453–493; pre-seal forced site `syncer.go` 1162, Init sites 232/268). +- Source-cache orchestration: warm/cold install, hit recording, the warm + gate, hit-validator binding, once-per-scope replay dedup, per-scope + locks, page-op ordering (copy → upserts → tombstones → publish), + poison, replay-blocked marking at its real durability. +- Multi-sync chains (up to 3 syncs): a sealed artifact becomes the next + sync's previous artifact; gates G4/G7 modeled; G6 (capability) is a + PER-ATTEMPT bit (CO-6b-003's withdrawal-across-attempts case needs + it); G1/G2/G5 collapsed to one "previous artifact usable" boolean. +- Per-attempt compat config k ∈ {K1, K2} (what G7 byte-compares and B4 + cross-attempt-compares); rows carry a ghost config tag (§7 P1c). +- Two interruption modes: hard crash (volatile state lost, last durable + checkpoint wins) and graceful stop (batch aborts, forced checkpoint of + live state — including mid-chain cursors, admitted-but-undrained + spawns, and hits recorded during the aborted batch — attempt ends). +- Upstream mutation between attempts (always available) and mid-attempt + (per scenario); previous-artifact swap between attempts (case 3). +- The session store as shipped (variant A: free-form sync-scoped KV, + durable across attempts). Modeled surface: Get/Set only; the shipped + service also has Delete/DeleteMany/Clear — mutating ops the taint + toggles must cover in production (§6) but the model cannot reach. + +Abstracted away (soundness arguments in §8): + +- Storage internals: Pebble, batching, index synthesis, bytes. The store + is "partitions with atomic page commits and a manifest of scope → + validator entries". Batched clear/copy is abstracted to two atomic + steps; sub-batch partial-commit crash convergence was closed at the + store level in Phase 6a and is registered here as an exclusion. +- Row kinds: one STORAGE row kind (every calibration mechanism is + scope-level). Scenario 7 introduces a declared KIND axis on top: a + kind is an (action op, scope) pair — W and R are distinct ops over + distinct scopes, which is what gives sequential phases under §3's + same-op batching; the storage row-kind axis is unchanged (the + glossary's scope stays (row_kind, scope_key)), and per-kind + produce/consume state (session taint, §6) is scoped by this axis. + Scenario 7 is the only two-kind scenario. +- The G1/G2/G5 decision table (R2 chaos coverage, not scheduling + semantics). +- Grant expansion, external resources, targeted sync, static + entitlements, lookup ask/answer continuation, subprocess transports, + the compactor. +- Produce-side block triggers beyond §3's two: page-arrival shape + guards (child-resource declarations, `InsertResourceGrants`), + ingest-filter drops (B6), unknown-prior-checkpoint conservatism — + unreachable in the model (no child resources, no filter drops, every + modeled token carries the ingest-quality snapshot). +- Connector cross-call memory: scenario policies are pure functions + (§3 MWorker); the proto permits within-sync connector memory, and this + under-approximation is a recorded boundary (§8), not a claim about + connectors. +- Real pagination breadth: at most 2 pages per scope per round. + +Small-scope configuration (every known bug fits; the inductive bet is +stated in the brief's Honest limits): + +| dimension | bound | +|---|---| +| scopes | 1–2 | +| upstream epochs per scope | ≤ 3 | +| row identities per scope | ≤ 2 | +| syncs per scenario | ≤ 3 | +| attempts per sync | ≤ 3 (crash budget ≤ 2, stop budget ≤ 1) | +| workers | 2 | +| session keys | ≤ 1 | +| pages per scope per round | ≤ 2 | +| compat configs | ≤ 2 | +| kinds (action ops) per scenario | 1 (2 in scenario 7 only) | +| batch cap | nondet ∈ {1..2} per dispatch | + +Batch-cap scaling argument: reality's cap is 100 +(`maxPeekActionsCount`). The model's per-dispatch nondet cap {1..2} +explores the split shapes relevant at the model's action counts; +partitions into ≥ 3 batches are unexplored per split, but batch width +beyond the cap is independently reachable because same-op spawns are +admitted to the live batch uncapped, and multi-way splits decompose into +successive two-way splits at loop tops. CO-6b-006's 102-action two-batch +construction shows cap-induced splits are a real premise generator; a +model trace corresponds to a real scenario by inflating action counts, +never by changing the mechanism. + +## 2. Ground rules the encoding must obey + +Properties of the ENCODING (not the system) that the adversarial review +checks: + +1. **Genuine choice points.** Worker interleaving at atomic-step + granularity, crash/stop placement, checkpoint-or-skip at loop tops, + upstream mutation timing, batch cap per dispatch, and store-op + arrival order across senders are all resolved by checker-explored + nondeterminism — never fixed by encoding order. +2. **Crash wipes volatile state and only volatile state.** §5's table is + mechanically enforced: attempt-owned machines die at crash; the Store + survives with exactly its committed state; resume constructs a fresh + MSyncAttempt from the checkpoint token alone. +3. **Mitigations are guards, not behavior.** Every §6 toggle disables a + check/lock that exists in the shipped design; toggles never add + transitions. +4. **Monitors observe, never steer.** Spec machines subscribe to + announce events; ghost state only. Ghost labels attached to events + (§7) must be honest labels of decisions the model already made for + behavioral reasons, never scenario foreknowledge. +5. **One semantics, one place.** Connector policy, page-op ordering, and + gate logic are each encoded once and parameterized per scenario. +6. **Premises must be walked, not asserted.** Every §9 scenario premise + must be mechanically reachable under §3's semantics; the reachability + walk is part of the spec (§9 states each route) and of the review + charge (§10.0). + +## 3. Machines + +### MEnv (test driver, per scenario) + +Owns the scenario configuration (toggles, policy table, crash/stop +budgets, chain length, swap schedule, per-attempt capability bit and +compat config schedule). Runs the sync chain: + +- start attempt → await one of {seal, crash quiesce, stop quiesce, + attempt failure}; +- on crash/stop quiesce: start the resume attempt; +- on ATTEMPT FAILURE (a loud cold verdict — §4): resume-on-failure up to + the attempt budget; with `abandonLadder` on, after k identical + failures abandon the sync and start the next sync cold (6c ladder); + with it off, budget exhaustion with the P4 livelock rule (§7) firing + is the recorded outcome; +- on seal: roll the artifact (it becomes the previous artifact) and + start the next sync; perform the scheduled artifact swap, upstream + mutation, capability withdrawal, or compat-config change between + attempts when the scenario declares one. + +### MUpstream + +Per scope: epoch counter `ep[s]`, content function `rows(s, e)` (fixed +small table per scenario), validator `V(s, e)` truthful by construction +(`V(s,e1) == V(s,e2) ⟺ rows(s,e1) == rows(s,e2)`). Serves +`eValidate(s, v)` (match against current epoch) and `eFetch(s)` (rows, +validator, ghost epoch). `eMutate(s)` from MEnv; mid-attempt mutation +only when the scenario enables it. + +### MSyncAttempt (walker scheduler, one per attempt) + +Volatile state, initialized from the checkpoint token: action stack +(op, page token, scope binding, spawned flag), hit map (scope → +validator), replayed set, ingest-quality snapshot (incl. replay-blocked +reason flags), warm flag — NEVER from the token; recomputed per attempt +(B1, CO-6b-003; `sourceCacheWarm` is a syncer field, +`source_cache_orchestration.go` 363). + +Warm install at attempt start (`installSourceCacheLookup`): usable-prev ∧ +this attempt's G6 capability bit ∧ G4(prev not replay-blocked) ∧ +G7(compat byte-match: this attempt's config vs prev artifact's compat +record) ∧ no drift this attempt → warm; else cold. Produce-side +blocking — exactly two triggers WITHIN THE MODELED FRAGMENT: + +1. compat config recomputed differently across attempts of this sync + (B4); +2. this attempt runs without source-cache handling (G6 bit off) over a + store carrying prior-attempt produce state (CO-6b-003 + capability/shape withdrawal), fail-closed on compat-read error. + +The system has further block triggers the model cannot reach and +excludes (§1): page-arrival shape guards (child-resource declarations, +`InsertResourceGrants` — `ingestQualityReasonSourceCacheShapeUnsupported`), +ingest-filter drops (B6), and unknown-prior-checkpoint conservatism on +restore. "Two triggers" is a claim about the modeled fragment, not the +walker. + +Consume-side degradation alone (previous artifact withdrawn, blocked, or +mismatched) NEVER blocks the current artifact (R13: routine resumes must +not become chain breaks). + +Dispatch loop (mirrors `parallelSync`): + +1. Loop top: stack empty → seal sequence → report seal. +2. Checkpoint decision: nondet {checkpoint, skip}, with forced + checkpoints after Init and before seal (`syncer.go` 1162), and the + graceful-stop forced checkpoint below. `eCheckpoint` snapshots + (stack, hit map, replayed set, ingest-quality) — §5. +3. Batch: take the consecutive same-op prefix of the stack top, bounded + by this iteration's nondet cap; dispatch to free MWorkers; await + batch end (all dispatched actions finished or batch aborted). +4. Batch bookkeeping (mirrors the queue contract, `parallel_syncer.go` + 608–716): same-op spawns are committed to LIVE scheduler state + first, then admitted to the live batch queue and drained within the + batch (`outstanding` gates batch end) — so a loop-top checkpoint can + never contain an unfinished same-op spawn; cross-op spawns are + pushed to the stack. Spawn admission is deduplicated by identity + digest for ALL spawns, same-op included (`transitionAction`'s + spawnedAdmitted guard — re-mentions of finished work are legal + connector behavior and are skipped, not errors), plus a commit-local + duplicate-cursor rejection within one transition (loud failure; + SAME-OP children only — cross-op children bypass it in code); the + dedup index is volatile and rebuilt from the checkpointed stack on + resume. Completed actions pop AT COMPLETION — the pop is a + live-state transition committed mid-batch, not batch-end + bookkeeping (the 2-stop cell depends on a stop checkpoint capturing + one action popped and its batchmate mid-chain). An aborted batch + leaves its unfinished actions on the stack — a + dispatched-but-unfinished action's stack entry retains the token of + its LAST COMMITTED transition (mid-chain), because + `transitionAction` writes page advances into live state as they + commit; admitted-but-undrained spawns likewise remain in live state. + +Graceful stop (`eStop`): the in-flight batch aborts (workers stop at the +next atomic-step boundary; the failing/stopped actions stay unfinished); +MSyncAttempt force-checkpoints the LIVE state — including hits recorded +during the aborted batch, admitted-but-undrained spawns, and mid-chain +cursors of unfinished actions — then the attempt ends +(`checkpointOnStop`, run-expiry force; spawn checkpointability is pinned +by `TestSpawnedActionsSurviveCheckpoint`). This is the one path by which +mid-batch state becomes durable; hard crash never checkpoints. The stop +path is therefore the model's premise generator for "stranded carrier + +recorded hit" states (§9). + +### MWorker (×2) + +Owns one action at a time and runs its ENTIRE page chain to completion +within the batch (mirrors `syncOneAction`): per page — connector call, +page ops, then either continue with the next page token (committing the +advance to live state via the transition, staying inside the same +worker) or finish (the pop commits at completion, mid-batch). A +loop-top checkpoint can +therefore only ever contain root tokens for never-dispatched actions; +mid-chain tokens reach durability only via the stop path. + +Connector call (synchronous request/response): consult = `eLookup` on +the Store's previous-artifact surface, then on hit `eValidate` against +upstream. HIT RECORDING IS PINNED: the hit and its validator are +recorded into the volatile hit map at LOOKUP-HIT time, before and +regardless of the revalidation outcome +(`previousSyncSourceCacheLookup.LookupPreviousSourceCache` fires `onHit` +at lookup time, `source_cache_orchestration.go` 211–214); a later hit +for the same scope OVERWRITES (last-write-wins, +`state.RecordSourceCacheHit`). + +Verdict per the scenario policy table — a pure function of (action, +page position, lookup result, upstream response, session reads). This +purity is a modeling simplification, not a connector contract (§8): + +- unchanged → replay page(s); the annotation carries a validator or not + per scenario cell (both shapes are legal per proto); +- changed-with-diff → replay + overlay record pages; +- changed / miss → fetch → record pages under the new validator; +- planning shape: consult k scopes, spawn same-op carriers via + `EnqueuePageTokens` with (scope, verdict) baked into their page + tokens — the token is the verdict channel across attempts; policies + may also consult on later pages of the same chain. + +Session ops (`eSessionGet/Set`) happen inside page handling when the +policy says so. + +### MStore (durable; one per scenario, holds the whole chain) + +Durable state per sync: partitions (scope → set of (row id, ghost +provenance)), manifest (scope → validator [+ sealed count]), poison set, +replay-blocked flag (via checkpointed ingest quality — see §5), compat +record, session KV, checkpoint token, sealed flag. Plus the +previous-artifact binding, rebindable by MEnv between attempts (case 3). + +Atomic ops (one event each, processed to completion; announce on +commit): `eCheckpoint(token)`, `eLookup(s)` (miss when poisoned/absent, +or when the scope's kind is session-tainted in the previous artifact's +produce state — degradation, never a loud verdict; §6), +`eGateRead`, `eClearScope(s)` (acting-scope semantics: never poisons the +scope being replayed), `eCopyScope(s)` (preflight: entry present ∧ not +poisoned; count consistency is a 6a store guarantee out of scope; ghost +provenance: replay lineage fields (V_base, epoch_base, config_base) are +ADDED to copied rows, and embedded session stamps, when present, are +copied UNCHANGED — the single semantics P6-R's stamp travel refers to), +`eUpsertPage(s, rows)` (a put whose row identity is stamped with a +different scope poisons the losing scope; out-of-scope delete/overwrite +poisons likewise), `eTombstones(s, ids)`, `ePublishEntry(s, v)`, +`eSessionGet/Set`, `eSeal`. SCENARIO-LOCAL ops (§9.6 design variants +only; registered here per MS-CO-001 so §5's crash protocol and §2.1's +arrival-order choice points quantify over the full op vocabulary): +`eReplayUnit(s, v)` (V-ATOMIC's one-op {clear, copy, marker, publish}), +`eOverlayUnit(s, v, pages)` (V-OVERLAY-UNIT's one-op unit), and the +marker ops `eMarkerPut(s)` / marker read (the per-scope marker row all +§9.6 variants share). + +Scheduler-state mutations (hit recording, replayed marking, warm flag, +stack transitions, ingest-quality/replay-blocked flags) are NOT store +ops; they reach durability only inside `eCheckpoint` (CO-6b-006 +semantics: state recorded in the batch that crashes is lost with it). + +### MCrashInjector + +Emits `eCrash` (hard) or `eStop` (graceful) between any two atomic +steps, within the scenario budgets. Crash delivery races in-flight store +ops per §5's prefix rule. + +## 4. Page-op sequence (the replay/record hot path) + +For one scoped page, each named check a separate atomic step. +(Scenario 6's design variants — §9.6 — REPLACE this sequence for +unit-mode scopes: the variant declarations there are the authority; +MS-CO-001.) + +``` +acquire scope lock (s) [toggle: scopeLocks] — record pages too (CO-6b-006) +if replay page: + warm-gate check [toggle: warmGate] + hit check: s ∈ hit map (B5 provenance; loud cold on absence) + [toggle: annotationBinding] annotation validator, when present, == hit validator (case-3 fix, PROPOSED) + if s ∉ replayed set: [toggle: oncePerScope] + base-binding check: current base manifest[s] == hit validator [toggle: hitValidatorBinding] + eClearScope(s) (atomic) + eCopyScope(s) (atomic) + mark replayed (atomic, volatile scheduler state) +eUpsertPage(s, page rows) (atomic) +eTombstones(s, ids) (atomic, when present) +ePublishEntry(s, v) (when the page carries a validator) +release scope lock +``` + +Failure semantics: + +- A COLD verdict (gate/provenance/binding failure) fails the ATTEMPT + loudly; MEnv's resume-on-failure rule applies. Because the offending + cursor is checkpointed, the failure recurs deterministically — the P4 + livelock shape — until `abandonLadder` (when on) abandons the sync. +- A WARM page failure (destination write error) is modeled ONLY in the + lock scenario that needs it: the page fails after `beforeUpserts` + acquired the lock; the WORKER retries the action in-attempt (mirrors + `syncOneAction`'s retry loop), which re-enters the page sequence and + re-acquires the scope lock. With the model's release-on-error edge + removed (mutation check), the retry deadlocks — the CO-6b-007 hang. +- A replay-annotated page arriving in an attempt WITHOUT source-cache + handling (G6 bit off) is SILENTLY IGNORED — no-op page ops, no + failure (B1: absent capability means every source-cache annotation is + ignored for the whole sync; `sourceCachePageOps` returns nil ops when + `sourceCacheEnabled()` is false, `source_cache_orchestration.go` + 463–468). Produce-side blocking trigger 2 fires at ATTEMPT START + (install time) over prior-attempt produce state, fail-closed only on + compat-READ error — never at page arrival. + +Deliverable 3 (staged composition enum) — INPUT ASSUMPTION to confirm +against the PR discussion, at its REAL durability: record pages carry a +wire intent {OVERLAY, REPLACES} (the word "intent" is reserved for this +wire enum; the P1 ghost label is called "verdict class" and exists +regardless of this toggle). Detection is SYNCER-SIDE: when an attempt +observes, in its own (volatile + checkpoint-restored) state, both a +completed replay copy and a REPLACES-intent record for one scope in one +sync, it sets the replay-blocked reason flag — which reaches durability +only at the next checkpoint/seal, like the rest of ingest quality. The +model must therefore exhibit BOTH weaknesses as findings, not +surprises: (a) crash between detection and checkpoint loses the block; +(b) the carrier-less phantom (§9 case 1c) leaves no syncer-visible +evidence a copy ran, so detection never fires. The deliverable-3 claim +("propagation bounded to the one corrupted artifact") is verified ONLY +for the shapes where detection is reachable, and the calibration report +must state that boundary; an op-commit-durable store-side variant may be +modeled ADDITIONALLY for comparison, clearly labeled as stronger than +the proposal. + +## 5. Durability, crash, and resume semantics + +| state | owner | durability | +|---|---|---| +| partitions, manifest, poison, compat, session KV, sealed flags | MStore | durable at op commit | +| checkpoint token: action stack (incl. admitted spawns and, on the stop path, mid-chain cursors), hit map, replayed set, ingest-quality snapshot (incl. replay-blocked reason flags and session-taint marks) | MStore | durable at eCheckpoint commit | +| live stack, hit map, replayed set, ingest-quality/replay-blocked flags, session-taint marks | MSyncAttempt | volatile (checkpoint-cadence durability) | +| warm flag | MSyncAttempt | volatile AND never checkpointed (recomputed per attempt) | +| scope locks, in-flight batch, worker page position | MSyncAttempt/MWorker | volatile | +| spawn-dedup index | MSyncAttempt | volatile; rebuilt from the checkpointed stack on resume | +| overlay collect buffer (V-OVERLAY-UNIT, §9.6 only) | MWorker | volatile; never checkpointed — harmless to lose: page transitions defer to unit commit (§9.6 o-i, round-7 F1), so the stop checkpoint holds the consult-page token and a marker-less resume re-enters at consult | +| replay/overlay unit marker (§9.6 variants only) | MStore | durable at op commit (per-scope store row; rides INSIDE the unit for V-ATOMIC/V-OVERLAY-UNIT, trails as its own op for V-NAIVE/V-OVERLAY-LAST — the placement IS the bake-off variable); MS-CO-001 | +| upstream epochs | MUpstream | environment (unaffected by crash) | + +Crash protocol (pinned): `eCrash` is enqueued to MStore like any op; its +POSITION in MStore's queue partitions the dead attempt's outstanding +ops — ops the store processed before the crash event are committed; ops +behind it are DROPPED, never processed. Per-sender FIFO delivery holds, +so each worker's committed ops are a prefix of its issue order (the WAL +property); interleaving ACROSS senders remains a genuine choice point. +MEnv starts the resume only after quiesce: every attempt-owned machine +halted and the store's queue contains no dead-attempt ops (dropped, not +processed). No dead attempt's op can commit after the resume attempt +starts. + +Resume: fresh MSyncAttempt from the MOST RECENT durable checkpoint, +alone. In crash-only histories that checkpoint is a loop-top/forced +one containing only root tokens (§3 MWorker) — restart-from-root +(CO-6b-002) is the structural consequence for those histories. +Restart-from-root is a property of WHICH checkpoint survives, not of +the crash itself: a stop-forced checkpoint that survives a LATER hard +crash legally carries mid-chain cursors and admitted spawns, and +resume restores them as-is. Under graceful stop the forced checkpoint +may contain mid-chain cursors and admitted-but-undrained spawns; +whether a +replay annotation can be resumed mid-chain WITHOUT a fresh consult is +deliberately left reachable and checked as conformance question C1 (§9) +with a concrete probe script — CO-6b-002 calls that shape "unreachable +in practice", and the model either confirms that or produces the trace +that refutes it. Either outcome is recorded. + +## 6. Mitigation toggles + +OFF never adds behavior; it removes a check or a lock. + +| toggle | guard | contract | default (shipped) | +|---|---|---|---| +| `warmGate` | replay requires this attempt's warm flag | CO-6b-003 | on | +| `hitValidatorBinding` | copy requires base manifest[s] == recorded hit validator | CO-6b-004 | on | +| `scopeLocks` | per-scope mutex incl. record pages, held across the page, released on every path | CO-6b-003/005/006/007 | on | +| `oncePerScope` | replayed-set dedup of the replacement copy | B5 | on | +| `annotationBinding` | replay annotation's validator (when present) must equal the recorded hit | case-3 fix, PROPOSED — DE-SCOPED v11: not built in P, no kill obligation; V-ATOMIC subsumes it (§9.6 case-3 re-run) | off | +| `compositionEnum` | syncer-side REPLACES+replay detection → replay-blocked flag (checkpoint-durable) | deliverable 3, PROPOSED — DE-SCOPED v11: not built in P, no kill obligation; the atomic unit is the single fix story for the scoping family | off | +| `abandonLadder` | after k identical resume failures, abandon and start the next sync cold | 6c ladder | off | +| `sessionTaintWrites` | produce-side: a connector session WRITE during a replay-capable kind's phase marks that kind non-replayable in this artifact's produce state (checkpoint-durable, ingest-quality-style) | sessions×replay fix, PROPOSED (partial) | off | +| `sessionTaintAll` | produce-side: ANY connector session traffic (read or write) during a replay-capable kind's phase marks that kind non-replayable (checkpoint-durable, ingest-quality-style) | sessions×replay fix, PROPOSED (isolation) | off | + +Taint pins (round-6): REPLAY-CAPABLE KIND = a kind in the declared +source-cache flow — its rows are eligible to seed future replays from +this artifact — INDEPENDENT of the recording attempt's warm/cold state +(the taint records in cold attempts too; scenario 7's sync N is cold by +construction and the 7a fix run depends on it). WRITE = any mutating +session op (Set/SetMany/Delete/DeleteMany/Clear; the model reaches Set +only, §1). Durability is checkpoint-cadence (§5) and SELF-HEALING under +at-least-once re-execution: any checkpoint capturing the writer's pop +captures the taint in the same snapshot, and a crash losing the taint +also loses the pop, so the re-run re-records it — a stronger story than +`compositionEnum`'s detection evidence, claimed explicitly. Consume +side: a tainted kind's scopes read as lookup MISSES (degradation, never +a loud verdict — §3 `eLookup`). A capability-level OPT-OUT (the +connector attests emission-irrelevance: "my session traffic during +replay-capable listings does not influence emitted rows") disables the +taint detector for those kinds; it is trust-boundary machinery like the +truthful-validator assumption and needs no new cells — a dishonest +opt-out reproduces 7a/7b exactly, an honest one is green by definition +of emission-irrelevance. + +Kill obligations (§10.5): every BUILT toggle has at least one §9 cell +whose verdict it flips — `warmGate` in 5a, `hitValidatorBinding` in 3B, +`scopeLocks` in 4, `oncePerScope` in 4, `abandonLadder` in the P4 +cells, `sessionTaintWrites` in 7a (its 7b residual is a REQUIRED +finding), `sessionTaintAll` in 7a and 7b. The DE-SCOPED toggles +(`annotationBinding`, `compositionEnum` — v11) carry no kill +obligation: they remain documented as reviewed design records, their +fix duty discharged by the atomic-unit story (§9.6's 1a/1b/1c and +case-3 re-runs under V-ATOMIC). + +## 7. Properties (checkable forms) + +Announce events double as the deliverable-6/7 trace vocabulary: +`consult(s, hit?, v, validated?)`, `replay(s, v_base, e_base, k_base)`, +`record(s, v, e, k, wire_intent?)`, `upsert(s)`, `tombstone(s)`, +`publish(s, v)`, `checkpoint`, `stop`, `seal(n)`, `crash(k)`, +`session_read/write(k, stamp)`, `blocked(reason)`. Ghost fields (epochs, +verdict classes, configs, stamps) are labels of decisions the model +already made for behavioral reasons (the policy verdict, the upstream +response, the attempt's config) — §2.4. + +- **P1 — binding integrity (safety).** Per (sync, scope) the monitor + folds the ROUND LOG. A ROUND is the maximal run of pages of one + action chain for one scope under one verdict; its ghost label is + (verdict class, consult epoch, attempt config); a round is TORN if + its pages committed in more than one attempt; only COMPLETE rounds + enter the fold (torn or INCOMPLETE rounds' debris surfaces as + content divergence, which is the intended alarm — 6-naive's verdict + rests on the incomplete class). Log legality rules: at most + one replacement copy per scope per sync; an overlay round composes + only onto this sync's completed replay of the base its verdict was + computed against. Deterministic fold over complete rounds, ordered by + ROUND COMPLETION — the commit of a round's last page. A page COMMITS + when the last of its prescribed STORE ops commits (announce-visible); + action transitions and pops are scheduler events, NOT fold events; a + round that commits no store ops contributes no fold entry (round-5 + F1 pin — the only reading the §2.4 announce-subscribed monitors can + implement). Rounds enter + the fold when they complete, so completion order IS the fold order; + pages of different rounds may interleave in commit order without + affecting it (pinned so out-of-script counterexample logs fold + identically for every implementer): replacement → rows(s, e_base); overlay(e_from → e_to) — + requires current fold value = rows(s, e_from), yields rows(s, e_to); + a COPY-SKIPPED duplicate overlay round (B5 legal: the replacement + copy is skipped for an already-replayed scope and the page's + upserts/tombstones apply normally) folds as a NO-OP when the fold + value already equals rows(s, e_to); an overlay round whose OWN + replacement copy committed is SELF-GROUNDING — folds as rows(s, + e_to) regardless of prior fold value, its copy counting toward + replacement legality (case 4's locks-on surviving round is this + shape); a COPY-SKIPPED REPLACEMENT round commits no rows and folds + as a NO-OP — its publish, when validator-bearing, participates in + the attestation checks only (1d's stale carrier); anything else is + a legality violation; fresh(e) → rows(s, e) (REPLACES in the fold + even though + the store accumulates — divergence between an accumulating store and + a replacing contract is precisely the union pathology). + [Post-freeze editorial, two-track seam: the fold algebra these + rules implement — REPLACES absorption, OVERLAY composition, + tombstone ordering, replay-copy idempotence — is mechanically + proved in `formal/occult/LAWS.md` (L1–L6 plus negative controls); + the model consumes the laws as assumptions per the brief's + division of labor. No semantic change.] + Replacement-count legality counts committed copies WITHIN COMPLETE + ROUNDS, not verdict labels and not raw copy commits (round-7 F2 + pin): a copy inside a round that never completes is pre-committed- + classification debris and surfaces through content divergence, never + through the count — this keeps cell 4's locks-off run red (two + complete rounds, two counted copies), keeps 6-naive red (its debris + copy is uncounted; the alarm is content), and keeps the benign + cross-attempt at-least-once re-copy green (plan B5's "worst case … + re-runs an idempotent copy": attempt 1's incomplete-round copy plus + attempt 2's completed re-copy count as ONE); the locks-on run + copy-skips the duplicate and folds green. The FOLD'S INITIAL VALUE + IS THE EMPTY PARTITION (round-7 F3 pin): a scope with committed + store ops and no complete round diverges from the empty fold by + construction (never vacuously green), and a published manifest + entry for a scope whose fold result is EMPTY is an ATTESTATION + violation — the entry attests a composition the round log does not + contain. Checks: at SEAL — (a) CONTENT: partition equals the fold + result; (b) ATTESTATION: the manifest entry's epoch (via truthful + validators) equals the fold result's epoch, and an entry over an + empty fold violates outright; (c) CONFIG: every row's + ghost config tag equals the sealing attempt's compat config. At + `publish(s, v)` — ATTESTATION ONLY: v's epoch equals the publishing + round's verdict epoch (plan B5 permits the replay page to publish the + new delta token before overlay pages land, so no content check at + publish). The phantom union fails content or attestation in every §9 +variant; case 4's duplicate copy violates the at-most-one-replacement +rule directly; case 5a's drift copy fails config. BOUNDARY: TORN + completed rounds are outside P1-content's designed domain — under + between-attempt mutation a torn fresh round legally mixes epochs + (pure smear) and the per-round fold would false-alarm. AMENDED per + MS-CO-002 (build-out find; decision 1 of the calibration log's + "Model decisions of record"): this spec originally argued no §9 + config can tear a round (each config's single stop consumed by its + premise), but in the model the stop's PLACEMENT is genuinely + explored, so torn rounds ARE reachable (graceful stop mid-fresh- + round, resumed in attempt 2; crash-based configs still cannot tear — + crash-only histories resume from root-token checkpoints, §3/§5). + The exclusion is therefore enforced MONITOR-SIDE, not config-side: + P1 tracks attempt ghosts per round across every round op + (clear/copy/upsert/tombstones/publish) and excludes torn scopes + from the content and attestation folds. KNOWN NARROWING, registered + here: P3′'s torn tracking observes only overlay writes + (upsert/tombstones), so a replacement-only tear is excluded from + P1-content but not from P3′'s domain; no calibrated cell reaches a + replacement-only tear that survives to a P3′-asserted seal (the + P3′-asserting interrupted cells seal empty or use atomic/overlay + shapes), and widening P3′'s tracking to the P1 op set is the + registered follow-up if one ever does. Widening any config to a + second interruption plus a second mutation still REQUIRES a fold + extension for torn rounds (per-page folding) via change order + first. +- **P2 — bounded staleness (safety, multi-sync).** Ghost provenance per + row: origin epoch and the chain of syncs it traveled by replay. + "Consulted against upstream during sync N" is pinned as: the scope + had, during sync N (any attempt), a verdict that included an upstream + VALIDATION MATCH (`eValidate` == true), a fresh fetch, or a + CHANGED-WITH-DIFF verdict (revalidation occurred and the diff is an + upstream fetch; round-5 F8 pin) — a lookup + hit alone does NOT qualify. At each seal: every row's scope was + consulted-against-upstream this sync. Staleness counter per row; + corollary runs assert staleness ≤ 1 while P1 holds and exhibit + unbounded growth (to the chain bound) in the case-1 corrupted chain. +- **P3′ — per-scope epoch coherence (safety), doubly scoped.** Checked + ONLY (i) in scenarios without mid-attempt upstream mutation, and + (ii) for scopes with no TORN round this sync (a round crossing an + attempt boundary via mid-chain stop-resume observes two epochs and + would false-alarm). At seal, for every in-scope manifest scope: + partition content equals `rows(s, e)` for the epoch e of the scope's + last consulted-against-upstream verdict this sync ("last" by + announce order across attempts). Justification (valid in this class): + with upstream fixed within an attempt and no torn rounds, every page + of a round observes one epoch; truthful overlay composition preserves + coherence. Scenarios outside the class rely on P1/P2; a per-page + refinement is future work, not silently assumed. +- **P4 — progress.** Liveness form: after crash/stop budgets exhaust + and upstream mutation stops, the chain eventually seals (P liveness + monitor, hot while unsealed) — meaningful only with `abandonLadder` + on. Livelock DETECTION is a safety rule checkable in bounded runs: + two consecutive resume attempts that fail from byte-identical + restored checkpoint state with the same verdict at the same step + constitute the deterministic re-failure finding (CO-6b-004's + stuck-resume contract). The leaked-lock hang is checked via §4's + in-attempt retry: with the release edge removed, the retry deadlocks + (the model's own mutation check); with it present, the scenario + seals. +- **P6-A — session laundering witness (safety, variant A).** Ghost: + session values carry (writer action, attempt, derivation id); + committed outputs embed the stamps they read. At seal: violation iff + an embedded stamp's derivation differs from that writer's FINAL + derived value for the key — the artifact holds conclusions from + premises the final sync state does not hold, and no mechanism marked + it. Same-value re-derivation (d1→d2→d1) does not alarm. DOMAIN + (round-6 pin): P6-A quantifies ONLY over stamps embedded by session + reads performed within the sealing sync; traveled (replay-copied) + stamps and ⊥/miss stamps are outside its domain and belong to P6-R — + P6-A is vacuously green on 7a/7b/7c. +- **P6-R — replay-session coherence (safety, scenario 7; signoff + addendum).** Extends variant A's ghost vocabulary two ways: (a) + session-derived stamps TRAVEL WITH COPIED ROWS (replay copies ghost + provenance unchanged); (b) per (sync, key) the model carries a + COUNTERFACTUAL session value — the producer policy's PHASE-FINAL + value under an all-fresh execution at this sync's epoch, its reads + evaluated against the empty per-sync namespace (non-circular: the + namespace starts empty, the key budget is 1, and the producer phase + runs first). The counterfactual is computable, not a second + execution, because policies are deterministic and kinds run in + sequential phases with no cross-op spawns (a scenario-7 config + constraint) — which also makes it independent of reader timing: + every R read happens after W's phase completes (§8 note). It is + defined ONLY for scenarios whose upstream mutations are scheduled + BETWEEN SYNCS (a single epoch per (sync, scope) per sync; + between-attempt mutation within a sync makes "this sync's epoch" + multivalued and is excluded). At seal, for every committed row whose + scripted derivation includes a session input: violation iff the + row's embedded stamp differs from the counterfactual value of that + key this sync. Covers both duals: a fresh reader deriving from a + READ-MISS whose scripted producer was elided (7a — counterfactual + v1, embedded miss), and a replayed row carrying a stamp the producer + re-derived differently this sync (7b — counterfactual v2, embedded + v1). The both-warm control (7c) is green by construction: unchanged + upstream makes the counterfactual equal the carried stamp. P6-A is + unchanged and keeps its final-value form for within-sync laundering; + P6-R is the cross-sync/replay form. + +P5 (sweep) and P7 (sealed cuts) are graph-runtime properties — addendum. + +## 8. Abstraction soundness arguments (for the adversarial review) + +- **Clear/copy as two atomic steps.** Every calibration window is + BETWEEN named steps: gate↔clear (case 4), clear↔copy (record-page + wipe, CO-6b-006 N1), copy↔mark (carrier-less phantom, §9 1c), + stop↔spawn-drain (§9 stop-stranding premise). Sub-batch + partial-commit convergence: 6a store-level exclusion. +- **Checkpoint-or-skip nondeterminism** over-approximates the timer + throttle; forced sites (Init, pre-seal, stop) are preserved exactly. + Every model checkpoint placement is a real placement. +- **Batch cap {1..2}**: §1 scaling argument (two-way splits compose; + in-batch spawn admission makes width > cap reachable). +- **Verdict-as-data**: connector obligations (replay only a this-sync + hit, partition discipline) are deliberately violable by scenario + policy; SDK guards must catch the violations the contract says they + catch, and honest policies must not trip them. Policy PURITY + under-approximates legal connectors (the proto permits within-sync + connector memory, and the CO-6b-006 chaos connector uses it); all §9 + premises route verdicts through page tokens or the session store, so + no premise depends on cross-call memory. A future scenario needing it + is a change order, not a silent extension. +- **Session KV in MStore's durability domain**: the model gives session + writes op-commit durability and the store's crash cut. The production + session store is a separate service whose commits are NOT + prefix-ordered with c1z writes (and noop/in-memory variants are + lossy). The brief pins variant A as durable, so this is recorded as + the trust boundary of P6-A's verdicts, not modeled. +- **Counterfactual session ghost (P6-R)**: scripted-policy determinism + plus sequential kind phases (no cross-op spawns in scenario-7 + configs) make "the session value an all-fresh sync would hold" a + computable ghost label, not a second execution — no ∀∃ obligation — + and reader-timing-independent (every read follows the producer + phase). Pinned as the producer's PHASE-FINAL value (§7). Sound only + while scenario-7 configs schedule upstream mutation between syncs, + never within one (mid-attempt or between attempts); a config + violating that requires a P6-R scoping extension by change order. +- **One STORAGE row kind, 2 row ids**: sufficient to distinguish base + rows from fresh rows so unions and resurrections are content-visible. + Scenario 7's KIND axis ((op, scope) pairs, §1) rides on top without + widening storage row kinds. +- **Session-derived content divergence is under-approximated to ghost + stamps (scenario 7)**: real session-derived enumerations diverge in + CONTENT; §3's policies never choose row content, so the model carries + the divergence in the embedded stamp alone. The finding survives the + abstraction — the real system has no content oracle for fresh rounds + either, so the corruption is invisible to shipped checks for the + same structural reason. +- **Seal as one step**: the real counts-before-ended_at fence is + store-level, closed in 6a/6b. +- **Root-tokens-at-loop-tops is a claim about the modeled population.** + Every annotation-bearing listing (resources/entitlements/grants) runs + inside batches via `syncOneAction`, where it holds. Sequential + non-fanned ops (e.g. `SyncResourceTypesOp`) process one page per loop + iteration and CAN checkpoint mid-chain at loop tops in crash-only + histories; they are outside the model, and future scenarios touching + them must not inherit the batched-population claim. + +## 9. Calibration scenarios + +Expected-fail runs use the shipped design unless stated; every FIND must +be a checker counterexample trace (regenerable on demand from its +CALIBRATION.md cell row; sweep summaries are archived under +`walker/traces/`), rendered per deliverable 6. Every premise below +states its reachability +route; the standard premise generator is the STOP-STRANDING pattern: a +planning page's transition commits (hit recorded, same-op carrier +admitted to live state), the stop lands before the carrier's first +atomic step completes — undequeued or dequeued-but-unstarted both +qualify (genuine interleaving choice) — and the forced stop-checkpoint +makes {parent cursor, pending carrier, hit map} durable together. + +1. **Phantom union** (2 syncs + 1 verification sync, 1 scope, shipped + toggles ON — the residual exists in the shipped design). Premise for + 1a/1b: sync N+1 attempt 1 runs planning action P (2 pages): page 1 + consults S at epoch 1 (lookup hit V1 recorded, validation MATCHES, + verdict replay) and spawns carrier C (same-op, replay annotation V1 + in its token); stop-stranding: forced checkpoint captures P + mid-chain (page-2 cursor), C pending, hit {S: V1}. Between attempts + upstream → epoch 2. Attempt 2 restores both actions; P's page 2 + re-consults per policy: lookup hit V1 (overwrite, same value), + revalidation vs epoch 2 FAILS → verdict fetch-fresh → P's chain + continues with a 2-page fresh round under V2. C and P interleave + (same batch, 2 workers, or cap-1 orderings — genuine choices): + - **1a**: C drains between P's fresh pages → clear wipes the first + fresh page, copy installs base(e1), second fresh page lands on + top; P publishes V2 at round end → partition = base(e1) ∪ partial + fresh(e2) ≠ fold (fresh round replaces) → **P1 content violation** + at seal; chain continues, unchanged upstream → union replays → + **P2 staleness 2** (unbounded branch). + - **1b-i** (C drains after P's complete fresh round; C's annotation + publishes V1): clear wipes the fresh round, copy installs + base(e1), entry V1 → fold (fresh(e2) then replacement(e1)) = + rows(e1); content and attestation coherent → P1 green, **P3′ + violation** (last consulted verdict epoch 2, content epoch 1; no + torn round for S — P's consult pages and the fresh round each + commit within one attempt). + - **1b-ii** (C drains last, defers publish — validator-less page, + legal per proto): partition rows(e1) under entry V2 → **P1 + attestation violation**. + - **1c — carrier-less variant** (priority trace for the chaos + bridge; needs NO stop and NO spawn): attempt 1's single root + action consults S (hit V1, validation matches at epoch 1) and + replays in the same page; `eCopyScope` COMMITS; hard crash before + any post-Init checkpoint — durable base(e1) partition debris, hit + and replayed mark lost. Epoch 2; attempt 2 restarts from root, + re-consults, revalidation fails → fetch-fresh → upserts land over + the debris (fresh never clears) → publish V2 → **P1 content + violation** with ONE crash and NO replay in attempt 2 (warm gate + and binding never evaluated). + - **Fix runs — DE-SCOPED v11, not built** (`compositionEnum` on, + its real checkpoint-durable + syncer-side semantics): 1a/1b — detection fires, `blocked` reaches + the seal, sync N+2 runs cold, P2 restored from N+2. 1c — detection + CANNOT fire (no syncer-visible copy evidence); the propagation + bound fails; this is a REQUIRED finding of the run and bounds the + deliverable-3 claim. Crash-between-detection-and-checkpoint is a + second required finding. Kept as the reviewed design record of + the staged mitigation; the built fix story is §9.6's atomic unit + (1a/1b/1c re-runs green under V-ATOMIC, no detection machinery). + - **1d — overlay-flavor control (signoff addendum; content-green + under shipped toggles)**: same stranding premise as 1a/1b, but + attempt 2's failed revalidation yields CHANGED-WITH-DIFF + (delta-overlay flavor), not fetch-fresh: P's chain continues with + an overlay round — replay page (annotation V1, base e1), then + overlay pages (e1→e2) — while stale carrier C (pure replacement, + base e1) interleaves freely. V2's publish placement is pinned, + and BOTH placements are explored as sub-configs: (i) round-end + (final overlay page carries V2); (ii) B5 early publish (replay + page publishes V2, overlay pages validator-less). Expected: + CONTENT GREEN in EVERY schedule UNDER SHIPPED TOGGLES — both + copies draw the same base(e1) and collapse under `oncePerScope` ∧ + `scopeLocks` (the check-then-mark collapse is atomic only under + the lock; this is case 4's dual-replay shape, whose cell kills + each leg — a `scopeLocks`-off 1d mutant is content-red), and the + overlay composes legally onto either copy; the union cannot form. + The protection is MITIGATION-DEPENDENT, not structural + (V-OVERLAY-UNIT, §9.6, pilots the structural alternative — v10). + ATTESTATION is schedule-dependent by PUBLISH order, not drain + order: when a validator-bearing C's V1 publish is the LAST + publish for S (§4 runs `ePublishEntry` even on a copy-skipped + page), the seal sees entry epoch e1 under content rows(e2) → + expected ATTESTATION-STALE-BEHIND finding. Under sub-config (ii) + a C draining MID-schedule can still publish last (V1 after the + round's early V2); under (i) only C-drains-after-round-end + schedules alarm. Classification: stale-BEHIND is the self-healing + direction (the next sync's V1 consult re-delivers the e1→now + changes) only under idempotent absolute-record overlay + application — a connector-semantics assumption OUTSIDE the pinned + trust boundary — so P1 stays direction-blind and the cell records + the finding rather than weakening the property. A validator-less + C is rowless and legal (B5: a round that never publishes leaves + no entry — the replay itself remains valid); whether its copy + commits is SCHEDULE-dependent, not a property of + validator-lessness: C-first schedules COMMIT C's copy (the + annotation validator plays no part in the hit or base-binding + checks) — a committed replacement folding to rows(e1), counting + toward replacement legality, with P's round then folding as a + copy-skipped overlay to rows(e2); C-later schedules copy-skip C — + the §7 no-op, with no publish. Fully green in all schedules + either way. P2: GREEN (changed-with-diff qualifies as + consulted-against-upstream per §7's round-5 pin; staleness ≤ 1 — + base rows one replay hop, overlay rows fresh). Flavor-coverage + note (EXTERNAL-BOUNDARY commentary, §8-style — a + connector-population claim outside the model): the protection is + FLAVOR-conditional, not connector-conditional — delta connectors + degrade to fetch-fresh on token expiry (Graph 410), so 1a/1b/1c + cover every connector's degraded path. Fold totality over 1d logs + is pinned by §7's self-grounding-overlay and + copy-skipped-replacement clauses (added with this cell). + Provenance: this cell replaced a conversational "every ordering + converges" claim that had checked content only — the attestation + edge was caught while scripting the cell, which is the point of + scripting it. +2. **Session laundering** (1 sync, 2 same-op actions H (session writer) + and G (session reader), sessions variant A). Both cells are expected + findings; the property must alarm on exactly the schedules below and + stay green elsewhere. + - **2-stop** (deterministic script): one batch, two workers; the + explored schedule has H's session write of d1 commit (op-commit + durable), G read d1, emit a row embedding d1, and finish; graceful + stop aborts the batch while H is mid-chain; stop-checkpoint + captures G popped, H on the stack. Resume: H alone re-runs; + upstream mutated between attempts → H derives d2 ≠ d1; G never + re-runs → its committed row embeds d1 ≠ final d2 → **P6-A + violation** at seal. + - **2-crash** (interleaving-dependent, real shipped behavior): hard + crash before any post-batch checkpoint → BOTH re-run + (at-least-once). The schedule where G re-runs BEFORE H's + re-derivation exists (worker interleaving is a genuine choice): + G re-reads the DURABLE stale d1, re-emits embedding d1, H then + derives d2 → **P6-A violation**. The complementary schedule + (H re-derives d2 first, G re-emits embedding d2) stays green — + both outcomes are required calibration results. + - No fix run (variant B is the graph addendum's obligation). +3. **Artifact swap + hit rebind** (2 sealed artifacts A/B, equal compat + records, validators V_A ≠ V_B; upstream unchanged throughout, so + truthful validators give rows(B) ≠ rows(up) = rows(A)). + - **3A** (shipped: `hitValidatorBinding` ON, `annotationBinding` + OFF — the residual hole): premise = stop-stranding with a 2-page + P: page 1 consults base A (hit V_A, validation matches), spawns C + (annotation V_A); stop; checkpoint {P mid-chain, C, hit V_A}; + MEnv swaps the previous artifact to B. Attempt 2: P's page 2 + re-consults — lookup hit V_B OVERWRITES the hit map (lookup-time + recording, last-write-wins) even though revalidation of V_B + FAILS → verdict fetch-fresh (a 2-page round under V_up = V_A + content). C drains: hit check ✓, base-binding compares hit (V_B) + to base B's manifest (V_B) — PASSES — clear+copy installs + rows(B); C's annotation publishes V_A. Interleavings: C last → + partition rows(B) under entry V_A → **P1 attestation violation**; + C first or interleaved → fresh upserts land over rows(B) (fresh + never clears) → **P1 content violation**. **P2 is GREEN** in every + cell — attempt 1's validation match of V_A qualifies the scope as + consulted this sync (corrected expectation; v2 wrongly claimed a + P2 violation). + - **3B** (pre-CO-6b-004: `hitValidatorBinding` OFF): 1-page P + (consult+spawn, pops at the stop checkpoint); no re-consult in + attempt 2, hit map stays V_A; C drains against swapped base B with + NO binding check → copy proceeds, publish V_A → **P1 attestation + violation** on an even weaker premise. Binding ON flips this cell + to loud cold (V_A ≠ V_B) — the CO-6b-004 kill. + - **Fix runs — DE-SCOPED v11, not built** (`annotationBinding` ON, + shipped toggles): 3A — + annotation (V_A) ≠ recorded hit (V_B) → loud cold, no wrong data; + with `abandonLadder` on the chain completes cold (P4). Required + extra cell: a carrier whose annotation validator is EMPTY (legal + per proto) — the run must surface the fix's coverage boundary + (fail cold on absence, or the fix is incomplete for validator-less + connectors) rather than overstate it. Kept as the reviewed design + record; V-ATOMIC subsumes the fix (no carrier, no annotation to + bind — §9.6 case-3 re-run), including the validator-less coverage + boundary, which is structurally absent there. +4. **Once-per-scope TOCTOU** (1 sync, 1 scope, 2 workers, dual replay + carriers spawned same-op — both drain within one batch, no stranding + needed; delta-overlay round). Premise constraint (§3 dedup is + semantics, always on): the carriers carry BYTE-DISTINCT page tokens + encoding the same (scope, verdict) — distinct identity digests, so + both are admitted; this is the realized shape in the CO-6b-003/005 + chaos instruments (pages from different resources targeting one + scope). Literal byte-identical duplicates would be rejected + commit-locally or skipped by the spawned-admitted guard and CANNOT + produce this premise. With `scopeLocks` OFF, both + carriers pass the replayed-set check before either marks → + clear/copy runs twice; the second replacement wipes the first's + overlay upserts → **P1 violation** (two replacement copies violate + log legality; content check catches the resurrection). With locks + ON: single copy, overlays preserved, green under bounded + exploration. Lock-release mutation check per §4/§7 P4. +5. **Warm-drift** (1 sync, 1 scope; the `warmGate` kill and the produce + triggers). Premise = stop-stranding: attempt 1 (warm, config K1) + records hit {S: V1} and strands carrier C; between attempts MEnv + changes the declared drift input: + - **5a — compat drift (trigger 1)**: attempt 2 computes config K2 → + G7 mismatch → COLD, and B4 marks produce-blocked. `warmGate` ON: + C's warm-gate check fails → loud cold; artifact blocked; with + `abandonLadder`, next sync runs cold — no wrong rows. `warmGate` + OFF: C passes hit (restored) and binding (base unchanged, V1) → + copies K1-tagged rows into a K2 attempt → **P1 config violation** + (clause c). This is the warmGate kill (§6). + - **5b — capability withdrawal (trigger 2)**: attempt 2's G6 bit is + off (no source-cache handling). At attempt start, install observes + prior-attempt produce state without handling → produce-side block + (trigger 2) marks the artifact replay-blocked (checkpoint-cadence + durability; the crash-window finding is required here too). C's + replay-annotated page then arrives in the handling-less attempt + and is SILENTLY IGNORED per §4 (B1): no failure, no rows for S, + and the sync SEALS GREEN with partition[S] empty and the artifact + blocked. The green seal, cold consults, compat-record retention, + and blocked marking match the CO-6b-005 capability-withdrawn chaos + cell; the EMPTY-PARTITION DROPOUT is NOT pinned by that cell (its + connector adapts cold on miss — no stranded carrier exists there), + so the model's scripted seal-state expectation is the dropout's + only executable oracle today. + The silent scope dropout is a REQUIRED DESIGN FINDING of this + cell: it is green under P1 — the empty partition equals the empty + fold, and no entry exists to check (round-7 F3 wording pin; NOT + "vacuously green": a scope with no complete round is still + checked, against the empty fold) — and invisible to P2 (which + quantifies only over rows present), so the + cell's oracle is the scripted seal-state expectation itself. + Whether a completeness/coverage oracle should exist is recorded as + a deliverable-6 chaos-bridge question, not invented at freeze + time. + - **C1 probe** (CO-6b-002 conformance question): action A: page 1 + consults S (hit recorded at lookup) and records fresh; page 2 + carries a replay annotation (policy places replay mid-chain); stop + between the pages → checkpoint holds A's mid-chain cursor + the + hit map. Resume: page 2's connector call performs NO fresh + consult; the SDK hit check passes on the RESTORED hit map → the + replay runs on a mid-chain resume without a fresh consult. The + model thus answers C1 "reachable via the stop path" — a + conformance finding against CO-6b-002's "unreachable in practice" + wording, to be confirmed or refuted against the real + implementation through the chaos bridge (deliverable 6), not a + model bug. +6. **Atomic-unit design variant — collect-and-commit (signoff + addendum; bake-off pair, deliverable-4 pilot).** Not a §6 toggle + (it alters commit structure rather than removing a check): a + scenario-local variant of §3/§4/§5. V-ATOMIC pins the discipline + "complete one request's work before any derived work, marker + included": (i) replay executes INLINE on the consulting page — no + carrier spawn — and the page's own transition commits only after + (ii) ONE atomic store op `eReplayUnit(s)` = {clear, copy, marker, + publish} (implementation shape: single WriteBatch under a memory + threshold, grouped SST ingest above it — range-del + rows per key + family + marker row in one manifest edit; the model checks the + unit's CONTENTS, not the mechanism); the marker is a PER-SCOPE + STORE ROW, durable at op commit — AUTHORITATIVE consult provenance + (the marker) leaves checkpoint-cadence durability; (iii) a + re-executed action checks the marker BEFORE consulting and + suppresses re-consult and re-derivation for marked scopes. V-NAIVE + is the internal kill: marker-after-work WITHOUT the unit — shipped + §4 steps, then a separate marker op after `eCopyScope`; clause + (iii) applies to BOTH variants (V-NAIVE's defect is solely the + marker landing outside the unit). Under both variants the §5 + checkpoint token is UNCHANGED — the hit map and replayed set are + still recorded and checkpointed as shipped, but a restored hit + NEVER authorizes replay without a fresh consult: replay is + consult-inline, and marked scopes suppress the consult. A stop + between a lookup-hit and the unit can checkpoint {S: V1} with + nothing materialized; that stranded hit is INERT. Scope of the + pilot: originally FETCH-FRESH flavor only; the v10 addendum extends + it to the changed-with-diff (overlay) flavor via V-OVERLAY-UNIT and + cells 6-overlay / 6-overlay-naive below, which answer the two + obligations this paragraph previously deferred to deliverable 4 + (what the unit publishes for a diff verdict; the stale-AHEAD hazard + of a marker committing before overlay pages land — the latter is + 6-overlay-naive's kill). Boundary note + (round-5 N4): clause (iii)'s marker check precedes the consult + OUTSIDE the scope lock — itself a check-then-act window; two + concurrent consulting actions for one scope would both pass and + commit two units (two committed copies → P1 legality alarm). + Unreachable in this scripted single-consulting-action family; + recorded as a real 2-worker hazard for the deliverable-4 bake-off + boundary notes. + - **6-naive (expected RED — the debris union)**: 1 sync + resume, + 1 scope, fetch-fresh flavor, no post-Init checkpoint before the + crash. Attempt 1: consult S → verdict replay → clear+copy commit; + the hard crash lands in MStore's queue BETWEEN `eCopyScope` and + the marker op (same-sender FIFO prefix, §5 — reachable). One + between-attempt mutation (e1→e2). Attempt 2 restarts from root: + no marker → re-consult → revalidation fails → fetch-fresh; the + fresh round accumulates over the COMPLETE unmarked copy. Fold = + fresh(e2) (the interrupted replay round never completed; its copy + is debris) → **P1 content violation**, rows(e1) ∪ rows(e2) under + V2 — the phantom union rebuilt from debris with no carrier and no + stranding. (Shipped batched commits make PARTIAL-copy debris the + same way through an intra-copy window — 6a store-level + vocabulary, deliberately NOT §7's cross-attempt TORN; the + complete-copy window is the minimal witness and needs no + refinement of §8's copy-as-one-step abstraction.) + - **6-atomic (expected GREEN across this schedule and the 1a/1b/1c + re-runs)**: same schedule under + V-ATOMIC — a crash before the unit leaves nothing durable (resume + re-consults over an empty scope; the fresh round lands clean); a + crash after it leaves {rows(e1), entry V1, marker} together, and + resume suppresses re-derivation → seals coherent rows(e1)@V1, + with the e1→e2 changes arriving next sync via V1's consult. + Round-completion pin (round-5 F1): under V-ATOMIC the replay + round is COMPLETE at `eReplayUnit` commit — even when the + action's transition never commits and the re-execution is + marker-suppressed, the fold sees replacement(e1) and the seal is + green as scripted; under V-NAIVE the marker op is one of the + round's prescribed store ops, which is exactly what leaves the + 6-naive round incomplete. + Additionally re-run the 1a/1b/1c premises under V-ATOMIC with + `compositionEnum` OFF — expected green across those premises: the + stranding premise is unreachable (no carrier; durable consult + PROVENANCE — the marker — is atomic with materialization; a + checkpointed hit-map entry may precede it but is INERT, see + above), and 1c's debris cannot exist (the + marker rides the unit, so attempt 2 suppresses the second verdict + instead of unioning over debris). Also re-run the CASE-3 premise + (v11 extension, part of the de-scope decision): stop after + attempt 1's consult, base swapped to sibling B between attempts, + `annotationBinding` OFF — expected GREEN, because the variant + SUBSUMES the annotation-binding fix: there is no carrier and no + annotation to trust, restored hits are inert, and the marker + lives as a per-scope row in the CURRENT sync's artifact (not the + swapped previous one) — if attempt 1's unit committed, the seal + is that unit's coherent contents; if it did not, attempt 2 + re-consults against whatever base is ACTUALLY current (swapped + B's V1 fails validation against upstream e2 → fetch-fresh). The + 3A residual hole (hit-map rebind) is structurally closed for the + same reason: no restored hit ever authorizes replay. The pair + pins the two + load-bearing lines — the unit's contents (6-naive red shows the + marker must ride inside it) and marker-suppresses-re-execution — + and is the walker-side pilot of the deliverable-4 bake-off: the + graph runtime takes (i) as native scheduling semantics (derived + work is downstream of its premise's commit). + - **V-OVERLAY-UNIT (v10 addendum — the overlay-flavor extension of + V-ATOMIC).** Scenario-local variant of §3/§4/§5, declared here in + full before its cells (round-6 process lesson). The discipline + generalizes clause (i) — BOTH halves of it (round-7 F1): the + "request" whose work must complete is + the CONSULT VERDICT, not one wire page — for a CHANGED-WITH-DIFF + verdict the prescribed work is the base replay PLUS every overlay + page the diff yields. Pins: (o-i) replay-and-overlay executes + INLINE on the consulting chain — no carrier spawn (1d's stale + carrier cannot exist) — AND the transitions of every page in the + verdict's prescribed work commit only AT UNIT COMMIT (clause + (i)'s second half, generalized): intermediate overlay cursors + never enter live state, so a stop-forced checkpoint captures the + chain AT ITS CONSULT-PAGE TOKEN, which is the token resume + re-enters with. The deferral is scoped PER VERDICT, which keeps + multi-scope chains well-defined (MS-CO-001, parallel-review F4): + a page belonging to scope S1's committed unit has its transition + committed WITH that unit, so a chain stopped while consulting S2 + checkpoints at S2's consult token — never inside either scope's + prescribed work; (o-ii) under the variant, `eUpsertPage`/ + `eTombstones` for the scope are BUFFERED in a per-scope volatile + collect buffer (§5 row; bounded by pages-per-round ≤ 2, within + small scope) — no store op commits per page; when the FINAL + overlay page is collected, ONE atomic store op `eOverlayUnit(s)` + = {clear, copy(base e_from), overlay upserts/tombstones in + prescribed page order, marker, publish(V_to)} commits + (implementation shape as V-ATOMIC: WriteBatch or grouped SST + ingest; the model checks contents, not mechanism). Announce + vocabulary is unchanged: the unit announces its constituent ops + at commit, so every page of the round commits simultaneously and + round completion IS unit commit — the round folds as §7's + existing SELF-GROUNDING OVERLAY (own copy committed, folds to + rows(s, e_to) regardless of prior fold value, copy counting + toward replacement legality); no new fold clause. (o-iii) THE + UNIT ANSWERS THE DEFERRED PUBLISH QUESTION: it publishes V_to, + the verdict's post-diff validator (the new delta token), + attesting e_to. The publish constituent is present IFF the round + supplied a non-empty validator (round-7 F4: validator-less diff + rounds are legal per §3/B5) — a publish-less unit commits + {clear, copy, overlays, marker}, leaves NO entry (a miss next + sync, B5-consistent), and its marker still suppresses + re-execution within the sync. Timing (round-7 F5 rewording): the + variant DEFERS THE RUNTIME'S MANIFEST WRITE into the unit — a + deviation from B3/B5's frozen per-page publish timing, change- + order scope if adopted; the wire contract is untouched (the + connector still returns the token on whichever page carries it — + B5 permits either leg) and the deferral is connector-invisible + because the consult surface is the PREVIOUS artifact only + (`previousSyncSourceCacheLookup`). (o-iv) resume rule: the marker + suppresses + re-consult exactly as clause (iii); for a scope with NO marker, + resume restarts the scope's work FROM CONSULT — under (o-i)'s + transition deferral this is DERIVED, not decreed: the stop + checkpoint holds the consult-page token (no mid-chain cursor + exists for a unit-mode scope), the collect buffer is volatile, + and the honest price + is at-least-once re-fetch of the diff, lost work but never + debris. The stranded-hit inertness carries over from V-ATOMIC + unchanged; the marker-check-outside-lock two-worker hazard + carries over IN CLASS but with a MATERIALLY WIDER WINDOW + (round-7 F6): V-ATOMIC's window spans one page's handling, the + overlay variant's spans the whole collect phase (marker check at + consult → unit commit after every overlay page), multiple + connector calls wide. Because each unit is internally atomic, + the racing schedules' final content is the last unit's coherent + rows(e_to) — the alarm is legality-only (two committed copies), + never a wipe-mosaic. Recorded for the deliverable-4 bake-off + boundary notes. + - **6-overlay (expected GREEN across the re-scripted 1d premise + family — with `oncePerScope` AND `scopeLocks` OFF)**: the + structural claim that 1d could not make. 1d's stranding premise + is unreachable under (o-i) — no carrier — so the family is + re-scripted: planning chain consults S (hit V1, revalidation vs + e2 fails → CHANGED-WITH-DIFF), collects the 2-page ROUND inline + (the replay/first-overlay page plus the final overlay page — + NOT consult + 2 overlay pages, which would break §1's + pages-per-scope-per-round ≤ 2 bound; MS-CO-001), + `eOverlayUnit(s)` commits {base(e1) copy, overlay e1→e2, + marker, publish V2}. Sub-cases: (a) uninterrupted — fold = + self-grounding overlay → rows(e2), entry V2, content and + attestation green; (b) stop mid-overlay-chain — stop-checkpoint + captures the chain at its CONSULT-PAGE token (transition + deferral, o-i; round-7 F1 corrected this premise — no mid-chain + cursor exists to capture) and hit {S: V1}; buffer lost; + resume re-enters at the consult (o-iv, derived), re-consults + (restored hit is + inert), re-collects, one unit commits → green; (c) crash before + the unit — nothing durable for S, clean re-consult → green; (d) + crash after the unit — {rows(e2), entry V2, marker} durable + together, marker suppresses re-derivation → seals coherent. Both + mitigation toggles are OFF in every sub-case: `oncePerScope` is + unneeded (the marker inside the unit is the dedup; a re-executed + chain suppresses at the marker check) and `scopeLocks` is + unneeded within the scripted single-consulting-chain family (the + two-worker marker-race boundary note still applies and stays a + recorded hazard, not a scripted cell). 1d's + attestation-stale-behind cannot occur: publish order equals unit + order because no publish exists outside a unit. P2 GREEN + (changed-with-diff qualifies per §7's round-5 pin; staleness ≤ + 1). P3′ applies: no torn round is possible for unit-mode scopes — + every round commits within one attempt by construction. + - **6-overlay-naive (expected RED — the stale-AHEAD kill; the + inherited deliverable-4 obligation made concrete)**: unit + misdrawn at the consult boundary instead of the verdict boundary: + `eOverlayUnit'(s)` = {clear, copy, marker, publish(V2)} commits + at consult time, overlay pages then commit per-page via the + shipped §4 path. 1 sync + resume + 1 verification sync, 1 scope. + Hard crash lands in MStore's queue between the unit' commit and + the final overlay upsert (same-sender FIFO prefix, reachable as + in 6-naive). Attempt 2 restarts from root; the MARKER IS PRESENT + → clause (iii) suppresses re-consult and re-derivation → the + remaining overlay pages NEVER land; seal: partition = base(e1) + + partial overlay under entry V2. Fold: the round is INCOMPLETE + (prescribed overlay store ops never committed) → contributes no + fold entry → the fold for S is EMPTY, and the committed prefix + diverges from it by the §7 empty-fold pin (round-7 F3) → **P1 + content violation** at seal, plus **attestation violation** + outright: entry V2 is published over an empty fold (the entry + attests a composition the log does not contain). The + verification sync makes the direction asymmetry + 1d classified CONCRETE: consult of V2 revalidates CLEAN (upstream + unchanged at e2 — the per-seal consult clause of P2 is GREEN + there) → the mosaic replays warm → the **P2 staleness COUNTER + grows without bound** (round-7 F7 wording: the growth is on the + stale RESIDENT base(e1) rows whose e1→e2 updates never landed — + case 1's "unbounded branch"; the never-landed rows are in no + partition) and P1 content stays RED in the verification sync + itself (the warm mosaic copy folds as rows(e2) via truthful V2; + the partition is the mosaic) — stale-AHEAD is + the NON-self-healing direction (V2 attests changes the artifact + never absorbed; no future consult re-delivers e1→e2), the dual of + 1d's self-healing stale-BEHIND. The marker-before-pages placement + is the sole defect: 6-overlay commits the identical contents one + boundary later and is green. + - **6-overlay-last (round-7 F2 — the third placement, its own + cell)**: per-page commits with marker+publish LAST outside any + unit: clear+copy commit at the replay page, overlay + upserts/tombstones per-page via the shipped §4 path, then marker + and publish(V2) as two trailing separate ops. The round-7 review + REJECTED the v10 dismissal ("reduces to 6-naive's unmarked-debris + class"): the reduction is impossible in the overlay family — a + crash before the marker leaves UNMARKED debris, but attempt 2's + re-consult yields CHANGED-WITH-DIFF by premise, whose prescribed + work BEGINS with clear+copy — the clear WIPES the debris and the + round rebuilds; no schedule unions (6-naive's class requires a + NON-clearing fetch-fresh re-verdict). The placement's real + windows are its own: (w1) crash anywhere before the marker → + converging rebuild, GREEN — and the FIRST history in the spec + where replacement-count legality meets a cross-attempt double + copy (attempt 1's copy in an incomplete round + attempt 2's + completed re-copy), legal under §7's complete-rounds counting + pin (B5's idempotent re-run); (w2) crash BETWEEN marker and + publish → marked, entry-less, content-complete scope whose + re-execution clause (iii) SUPPRESSES → seals correct rows(e2) + with NO entry; the publish was a prescribed round op that never + committed → round incomplete → empty fold → **P1 content + violation** (non-empty partition vs empty fold) — a + suppression-window shape that is NOT 6-naive's union class + either. Expected: P1 RED (w2 is the witness; w1 must NOT alarm — + it exercises the counting pin). +7. **Session elision under replay — the sessions × source-cache + product (signoff addendum; shipped design, pure two-sync scripts — + no interruption machinery).** Two kinds: W (producer, in the + declared replay flow) whose fresh enumeration writes session key K + as a side effect, and R (reader) whose fresh enumeration derives + emitted rows from reading K (ghost stamp). Kinds run in sequential + phases (W's op before R's). Sync N is all-fresh: W writes K=v1, R + emits rows stamped v1, seal green. The shipped design has NO + coupling between sessions and source-cache + (`source_cache_orchestration.go` has no session awareness; + `BatonSessionService` is callable during any listing), so every + cell below runs shipped toggles unless stated. R's connector + violates NO pinned obligation in any cell — its validator + truthfully attests R's upstream scope; the session dependency is + un-attested because no contract clause requires attesting it. That + missing clause is the scenario's design finding. + - **7a — write elision (expected RED)**: upstream unchanged for W; + R runs fresh in sync N+1 (its policy fetches fresh; round-6 F9 + struck the per-kind flow-membership route as unmodeled). W's + consult → validator match → warm + replay; W's enumeration — and its session write — is ELIDED, so + sync N+1's namespace never holds K. R's fresh derivation reads K + → MISS → emits rows stamped ⊥. Row CONTENT stays on the rows + table by construction (§3: policies choose verdicts and session + ops, never row content), so the divergence is carried entirely in + the embedded ghost stamp. Counterfactual: v1 (an + all-fresh sync's W would have written it) → **P6-R violation**. + P1 and P2 are GREEN — every row is individually well-formed and + every scope consulted; the corruption is invisible to + content/attestation checks, which is itself a required finding — + and holds in the real system for the same structural reason + (fresh rounds have no content oracle; §8). + - **7b — stale-read replay (expected RED, the dual)**: between + syncs upstream W-data changes (W fresh in N+1, writes K=v2) while + R's upstream scope is unchanged (validator match → R warm). R's + copied rows carry ghost stamp v1; the counterfactual this sync is + v2 → **P6-R violation**. No elided write anywhere — the writer + ran fresh; the READER was replayed with rows derived from last + sync's session state. This is the cell that kills write-only + bans. + - **7c — both-warm control (expected GREEN)**: nothing changes + upstream; W and R both replay; carried stamps v1 equal the + counterfactual v1. Required so P6-R does not overfit to "replay + near sessions alarms". + - **Fix runs**: fix runs re-execute the FULL two-sync script with + the toggle ON — the toggles are produce-side, so the fix run's + sync-N artifact DIFFERS from the red run's (the first toggle + family that acts a sync earlier than the red verdict). Sync N is + COLD (first sync, no previous artifact) and the taint records + anyway: replay-capable is flow membership, not warm state (§6 + pins). `sessionTaintWrites` ON — sync N's produce observes + W's write during a replay-capable phase and marks W's kind + non-replayable in the artifact; sync N+1 runs W cold (consults + MISS — degradation, not a loud verdict) → 7a GREEN; + 7b stays RED (R's hazard is a READ) — the residual is a REQUIRED + finding: the write-only rule is half a fix. `sessionTaintAll` ON — + R's read during its capable phase (sync N) taints R's kind too → + 7a and 7b both GREEN; 7c runs cold for every session-using kind + (replay forfeited exactly where sessions are used — the toggle's + honest price, recorded not hidden). Taint granularity is per KIND + per artifact (phase attribution — kinds run sequentially, so no + wire change is needed); taint-to-cold vs loud-reject is a + severity choice outside the model — the detector is identical. + Out-of-model enforcement layers of the same detector + (change-order implementation work, recorded for the calibration + report): a static analyzer (call-graph from replay-capable + listing entry points to the session API, suppressible only by + the §6 opt-out attestation), a pre-release conformance assert + (per-response `SessionStoreUsage` attribution), and the runtime + taint itself — one detector, three timings. + +Fix-verification runs are bounded-exploration green runs (schedule +budget recorded in the calibration report), not proofs. + +Pre-committed classification (round-2 residual risk): out-of-script P1 +counterexamples of the cross-attempt fresh-debris shape (fresh pages +commit, crash, mutation, re-fetch unions over debris — no replay +involved) are DESIGN FINDINGS of the shipped walker, not model noise; +P1 must not be weakened to silence them. + +## 10. Adversarial review charge (minimum questions) + +0. Reachability walk: for each §9 premise, walk the route mechanically + under §3's semantics (spawn admission, batch draining, stop/crash + windows, checkpoint contents) and confirm the state is reachable + without hand-placement; confirm each cell's expected verdict follows + from §7's definitions (fold legality, P2's consult pinning, P3′'s + double scoping). +1. Interleaving: did the encoding serialize any calibration-relevant + pair (carrier drain vs fresh pages; record page vs replacement copy; + duplicate carriers; H vs G re-runs in 2-crash; crash vs in-flight + ops across two senders)? +2. Crash/stop: does crash wipe exactly §5's volatile column; is the + warm flag demonstrably not restored; is the prefix rule (§5) + enforced; do stop-checkpoints capture exactly the live state the + stop path captures in code (mid-chain cursors, admitted spawns, + batch-recorded hits)? +3. Emergence: do the §9 findings arise from stack/batch/stop/checkpoint + mechanics rather than scenario hand-placement? (Delete each + finding's trace; check no scenario step writes the corrupt state + directly.) +4. P1/P3′: is the fold deterministic and total over every §9 log; is + the publish-time check attestation-only; is P3′'s double scoping + (mid-attempt mutation, torn rounds) enforced by ghost state rather + than scenario labeling; is the truthful-validator assumption used + only inside the trust boundary? +5. Mutation adequacy: each BUILT §6 toggle's kill cell flips as tabled + (the v11 de-scoped toggles carry no obligation); the + P1 monitor alarms on 1a/1b-ii/1c, 3A/3B, 4, 5a, 6-naive, and 1d's + carrier-publishes-LAST schedules (attestation only; publish order, + not drain order), and stays green on honest delta overlays, 1d's + remaining schedules, the 6-atomic runs, and the case-3 V-ATOMIC + re-run (v11 — the subsumption claim); a `scopeLocks`-off 1d + mutant is content-RED (case 4's dual-replay TOCTOU); 6-overlay is + GREEN across its whole sub-case family with `oncePerScope` AND + `scopeLocks` OFF (the structural claim — the same toggles-off + configuration that turns 1d content-red); 6-overlay-naive is + content-RED and attestation-RED at seal (entry over an empty fold — + round-7 F3) and exhibits unbounded P2-counter growth in the + verification sync with P1 content still red there (stale-AHEAD, + non-self-healing); 6-overlay-last is content-RED via the w2 + suppression window and must NOT alarm legality on w1's + cross-attempt re-copy (the complete-rounds counting pin — round-7 + F2); an o-iv-REMOVAL mutant (resume honors a restored mid-chain + cursor for a unit-mode scope) is content-RED in 6-overlay + sub-case (b)'s schedule — the mutant collects only the final page + into an empty buffer and commits a unit missing the first page's + overlay ops (MS-CO-001, parallel-review F6; the round-5 F2 + precedent); P6-R alarms on + 7a and 7b, stays green on 7c and on fix runs as tabled (7b must + stay RED under `sessionTaintWrites`); the + lock-release edge's removal deadlocks §4's retry. +6. Composition enum: does the encoding match the PR-discussion + semantics, INCLUDING detection-state visibility and mark durability + across attempt boundaries? Are the 1c and detection-crash findings + reported as claim boundaries, not suppressed? + +## 11. Change-order log + +(Append-only after freeze.) + +- (pre-freeze) v2: round-1 adversarial review dispositions — see + `formal/reviews/model-spec-round1.md`. +- (pre-freeze) v3: round-2 adversarial review dispositions — see + `formal/reviews/model-spec-round2.md`. Headline changes: §9 + re-scripted on the stop-stranding premise generator (round-2 blocker + 1); scenario 2's hard-crash cell inverted to an expected finding + (blocker 2); P1 fold fully pinned with publish-time checks reduced to + attestation (finding 3); P3′ torn-round exclusion (finding 4); + scenario 3 expectations re-derived — P1-attestation, P2 green + (finding 5); scenario 5 added for warmGate and both produce triggers + (findings 6, 7); per-attempt G6 bit and compat config with P1 config + clause (findings 6, 7); oncePerScope toggle marker (finding 8); + glossary corrected (finding 9); C1 probe script (finding 10); §8 + notes for session-KV durability domain and connector purity (notes + i–iii). +- (pre-freeze) v4: round-3 adversarial review dispositions — see + `formal/reviews/model-spec-round3.md`. Headline changes: §4/§9-5b + corrected to B1's silent-ignore semantics with trigger 2 at install + time, and 5b's silent scope dropout (green seal, empty partition, + blocked artifact) promoted to a required design finding with a + scripted seal-state oracle (blocker F1 + note F7); P1 fold order + pinned to round completion (F2); action pops commit at completion, + mid-batch (F3); restart-from-root restated as a + which-checkpoint-survives property, §5 and glossary (F4); spawn + dedup extended to all admissions with commit-local duplicate + rejection (F5); stop-stranding window widened to + first-atomic-step-incomplete (F6). +- (pre-freeze) v5: round-4 adversarial review dispositions — see + `formal/reviews/model-spec-round4.md`. Cell 4's premise pinned to + byte-distinct page tokens encoding one (scope, verdict) (finding 1); + P1 fold gains the copy-skipped duplicate-overlay no-op clause and + committed-copies replacement counting (finding 2); produce-blocking + "two triggers" scoped to the modeled fragment with the excluded + triggers registered in §1 (finding 3); 5b's CO-6b-005 attribution + narrowed — the empty-partition dropout is pinned only by the model's + scripted oracle (note 4); root-tokens-at-loop-tops recorded as a + modeled-population claim in §8 (note 5); commit-local duplicate + rejection qualified as same-op-scoped (note 6). +- (pre-freeze) v5 signoff-discussion addendum: P1 torn-round fold + boundary recorded in §7 (torn completed fresh rounds under + between-attempt mutation are legal smear the per-round fold would + false-alarm on; unreachable in all §9 configs; config widening + requires a fold extension by change order). Surfaced while walking + the P3→P3′ narrowing during user signoff. +- (pre-freeze) v6 signoff-discussion addendum: overlay-flavor control + cell 1d added to §9 case 1 (content-green structural protection — + same-base copies collapse under `oncePerScope`; expected + attestation-stale-behind finding when the stale carrier's V1 publish + lands last; flavor-coverage note: token expiry degrades every delta + connector to the fetch-fresh flavor covered by 1a/1b/1c). §7 P1 + fold extended for totality over 1d logs: self-grounding overlay + rounds; copy-skipped replacement rounds fold as attestation-only + no-ops. Scenario 6 added — collect-and-commit design-variant pair + (V-NAIVE marker-after-copy → expected-red debris union; V-ATOMIC + inline replay + one {clear, copy, marker, publish} atomic unit with + the marker as a per-scope store row + marker-suppressed re-execution + → expected green across the 1a/1b/1c re-runs; deliverable-4 pilot). + Provenance: user signoff discussion (smear-vs-union attestation + distinction, copy atomicity, SST-ingest collect-and-commit). The 1d + attestation edge was caught WHILE SCRIPTING the cell, refuting the + conversational "every ordering converges" claim. These addenda have + NOT passed an adversarial round; a targeted spot review before + freeze is the recommended option. +- (pre-freeze) v7: round-5 targeted spot review of the v6 addenda — + see `formal/reviews/model-spec-round5-addenda.md`. REJECT, 2 majors + + 6 minors, all fix-without-re-review per the verdict; reachability, + fold totality, and 1d's content-green-under-shipped-toggles claim + otherwise confirmed, no contradictions with rounds 3–4. Headline + dispositions: page/round COMPLETION pinned to store-op commit + (announce-visible; transitions and pops are not fold events; a + round committing no store ops contributes no fold entry), with + V-ATOMIC's round complete at `eReplayUnit` commit and V-NAIVE's + marker op a prescribed round op (F1); 1d's protection re-attributed + to `oncePerScope` ∧ `scopeLocks` and demoted from "structural" to + mitigation-dependent, `scopeLocks`-off mutant added to §10.5 (F2); + validator-less-C sub-case re-scripted per schedule class — C-first + commits the copy, C-later copy-skips, green either way (F3); 1d + publish placement pinned as two explored sub-configs with + publish-order ≠ drain-order noted (F4); torn-round boundary + justification extended to crash-based configs and incomplete-round + debris named in the fold text (F5); V-ATOMIC's relationship to the + §5 checkpoint token pinned — hit map still checkpointed but never + authorizes without a consult, clause (iii) applies to both variants + (F6); 6-atomic's green claim de-scoped to the 1a/1b/1c re-runs and + the overlay flavor declared an explicit deliverable-4 obligation + (F7); P2's consult pin extended to changed-with-diff verdicts and + 1d's P2 expectation stated (F8). Freeze-record notes: partial-copy + vs torn vocabulary (N1); the self-grounding clause retroactively + closes a latent v5 fold-totality gap on case 4's locks-on surviving + round (N2); the flavor-coverage note labeled external-boundary + commentary (N3); V-ATOMIC's marker-check-outside-lock + check-then-act window recorded as a 2-worker bake-off hazard (N4). +- (pre-freeze) v8 signoff-discussion addendum: scenario 7 added — the + sessions × source-cache PRODUCT, a coverage gap both subsystems + individually modeled but no cell exercised. Shipped design has no + coupling (`source_cache_orchestration.go` session-blind; + `BatonSessionService` callable during any listing; sessions + sync_id-scoped so each sync's namespace starts empty). Cells: 7a + write elision (warm producer's elided enumeration never writes K; + fresh reader derives from the miss), 7b stale-read replay (fresh + producer rewrites K; warm reader's copied rows carry last sync's + stamp — the dual that kills write-only bans), 7c both-warm control + (green). New property P6-R (replay-session coherence): embedded + session stamps vs a counterfactual session ghost (scripted-policy + determinism + sequential kind phases make it computable — §8 note); + ghost stamps travel with copied rows. New PROPOSED toggles + `sessionTaintWrites` (partial — flips 7a only, 7b residual is a + required finding) and `sessionTaintAll` (isolation — flips both; + cost: replay forfeited for session-using kinds, recorded). Glossary + gains Elision and Session taint. Provenance: user signoff + discussion — the elided-session-write question, refined through the + read-side dual to per-kind isolation; taint-to-cold vs loud-reject + recorded as a severity choice outside the model. NOT yet + adversarially reviewed; targeted round-6 spot review required + before freeze. +- (pre-freeze) v9: round-6 targeted spot review of the v8 addendum — + see `formal/reviews/model-spec-round6-scenario7.md`. REJECT, 4 + majors + 5 minors + 3 notes, all fix-without-re-review; every cell + verdict and all six shipped-system claims verified clean. Headline + dispositions: the KIND axis declared in §1 as an (op, scope) pair — + storage row kinds unchanged, small-scope row added, §8 note updated + (F1); P6-A's domain pinned to stamps read within the sealing sync — + traveled and ⊥ stamps belong to P6-R; P6-A vacuously green on + 7a/7b/7c (F2); taint durability pinned checkpoint-cadence in §5/§6 + with the self-healing-under-re-execution argument claimed + explicitly (F3); REPLAY-CAPABLE pinned as flow membership + independent of warm/cold — the taint records in cold attempts, + which the 7a fix run depends on (F4); consume side pinned as + lookup-miss degradation in §3 (F5); P6-R scoping aligned to + between-sync-only mutation (F6); counterfactual pinned to the + producer's phase-final value with empty-namespace evaluation and + reader-timing independence stated (F7); 7a's ghost-only divergence + stated plus the §8 under-approximation note (F8); 7a's unreachable + flow-membership premise route struck (F9); modeled session surface + registered as Get/Set with WRITE covering all mutating ops in + production (F10); two-sync fix-run re-execution stated (F11); §3 + `eCopyScope` ghost tuple extended for stamp travel (F12). Folded in + from the same signoff discussion: the capability-level OPT-OUT + (attested emission-irrelevance; dishonest opt-out ≡ 7a/7b, honest + green by definition) and the out-of-model enforcement layers + (static analyzer, pre-release conformance assert, runtime taint — + one detector, three timings). +- (pre-freeze) v10: overlay-flavor extension of the atomic-unit + variant, per user direction at signoff — the walker-side pilot now + covers the changed-with-diff flavor rather than deferring it wholly + to deliverable 4. §9.6 gains V-OVERLAY-UNIT (unit boundary = the + consult VERDICT's prescribed work: base copy + all overlay pages + + marker + publish(V_to) in one atomic op; per-page ops buffered in a + volatile collect buffer, §5 row added; publish deferred into the + unit — B5 early publish not exercised; marker-absent resume + restarts from consult, mid-chain cursors ignored for unit-mode + scopes), cell 6-overlay (expected GREEN across the re-scripted 1d + premise family with `oncePerScope` AND `scopeLocks` OFF — the + structural claim 1d could not make; folds as §7's existing + self-grounding overlay, no new fold clause), and cell + 6-overlay-naive (expected RED — marker+publish at the consult + boundary, crash before the final overlay page; marker suppression + seals base(e1)+partial-overlay debris under entry V2, P1 content + violation, and the verification sync shows unbounded P2 growth: + stale-AHEAD is the non-self-healing direction, answering the + hazard scenario 6 had named and deferred). 1d cross-referenced; + §10.5 obligations extended. NOT yet adversarially reviewed; + targeted round-7 spot review required before freeze. +- (freeze revision) v11: round-7 targeted spot review of the v10 + addendum — see `formal/reviews/model-spec-round7-overlay.md`. + REJECT, 3 majors + 2 minors + 2 notes, all fix-without-re-review; + no cell verdict overturned; fold treatment, structural claim, + crash-window constructibility, and all shipped-system claims + verified clean. Headline dispositions: (o-i) generalizes BOTH + halves of V-ATOMIC's clause (i) — page transitions in a verdict's + prescribed work commit only at unit commit, so stop checkpoints + hold the consult-page token and o-iv's restart-from-consult is + derived; sub-case (b)'s premise corrected (F1); the third-placement + dismissal replaced with cell 6-overlay-last — the overlay + re-verdict CLEARS unmarked debris so no reduction to 6-naive + exists; its w2 (marker-committed, publish-lost suppression) is the + P1 witness and its w1 pins replacement counting to committed copies + within COMPLETE rounds (F2); the P1 fold's empty case pinned — + initial value is the empty partition, an entry over an empty fold + is an attestation violation outright, 5b's "vacuously green" + reworded (F3); the unit's publish constituent conditional on a + supplied validator — publish-less units leave no entry, + B5-consistent (F4); the publish-timing claim reworded as a + runtime-side B3/B5 deviation (change-order scope if adopted), + connector-invisible via the previous-artifact-only lookup surface, + "whichever page carries it" (F5); the two-worker marker-race note + extended with the widened collect-phase window, legality-only alarm + (F6); 6-overlay-naive's P2 wording corrected to the resident-row + staleness counter with the verification sync's P2 seal clause green + and P1 content still red there (F7). Bundled de-scope edits (user + signoff): `compositionEnum` and `annotationBinding` marked + DE-SCOPED — documented as reviewed design records, not built in P, + kill obligations removed (§6/§10.5); their fix duty is discharged + by the atomic unit as the single fix story, witnessed by the §9.6 + re-runs incl. the NEW case-3 re-run extension (V-ATOMIC subsumes + annotation binding: no carrier, no annotation, markers live in the + current artifact, restored hits inert). FREEZE: this revision is + the diffable baseline; subsequent changes are MS-CO-NNN change + orders, never silent edits. +- MS-CO-001: dispositions for the PARALLEL round-7 review + (`formal/reviews/model-spec-round7-overlay-parallel.md` — a second + independent spot review of the same v10 addendum, surfaced after + the v11 freeze; 1 major + 5 minors + 4 notes). Its major (empty + fold) and two minors (validator-less publish; publish-timing + wording) were independently found by the primary review and were + already fixed in v11 — the double-hit confirms those pins. Newly + applied here: `eReplayUnit`/`eOverlayUnit` and the marker ops + registered in §3's MStore op list as scenario-local, with a §4 + pointer to the variant override (F5) and a §5 durability row for + the marker (N1); o-i's transition deferral explicitly scoped per + verdict, making the resume rule well-defined for multi-scope + chains (F4 — v11's deferral pin already dissolved the ignore-rule + ambiguity the finding targeted); the collect pinned as the 2-page + ROUND (N4); and a NEW §10.5 kill obligation — the o-iv-removal + mutant is content-RED in 6-overlay sub-case (b) (F6), built as a + model cell. DISAGREEMENT OF RECORD: the parallel review judged the + third-placement reduction SOUND where the primary called it false + (round-7 F2); the built cell 6-overlay-last settles it + mechanically for the primary — the w2 marked-entry-less window is + red in a shape that is NOT 6-naive's union class, and w1 exercises + exactly the legality-counting gap the parallel review itself + flagged as its N3 tripwire. Its F3 (degenerate publish-placement + axis) is subsumed by v11's F5 rewording ("whichever page carries + it"); noted here that both B5 token placements collapse into the + single unit publish by construction. diff --git a/formal/README.md b/formal/README.md new file mode 100644 index 000000000..1e69b09a7 --- /dev/null +++ b/formal/README.md @@ -0,0 +1,90 @@ +# Formal model of sync scheduling semantics + +This directory holds the P model of baton-sdk's sync scheduling semantics +and its supporting documents, per +`docs/tasks/sync-formal-model-brief.md`. This is a public repo: no customer +names, tenant IDs, or internal infra in any artifact here, including model +comments and trace renderings. + +## Contents + +- `REPORT.md` — the synthesis report: verdicts, findings register, + and the guarantee/non-guarantee boundary, with pointers into + everything below. Start here for the outcome; start at GLOSSARY.md + to work on the models. +- `GLOSSARY.md` — deliverable 0: pinned vocabulary. Read this first; every + other document and every P identifier uses these meanings. +- `MODEL_SPEC.md` — the frozen model specification (machines, abstraction + decisions, crash semantics, properties, calibration configurations). + Frozen before P code is written; deviations discovered during modeling + are appended as change orders, never silently absorbed. The spec receives + an adversarial review before any green checker run is trusted. +- `GRAPH_MODEL_SPEC.md` — deliverable 4: the demand-graph runtime model + spec (frontier scheduler, generations, sweep, lineage bake-off E vs S). + v4, FROZEN after three adversarial review rounds; inherits MODEL_SPEC's + conventions and adopts its design-variant verdicts (unit-mode + materialization) as settled hand-offs. +- `reviews/` — adversarial review rounds for the specs, one file per + round; dispositions are logged in the specs' change-order sections. +- `walker/` — P project: the tiered walker + source-cache replay (6b) + calibration model (deliverables 2–3). Calibrated and frozen; see + `walker/CALIBRATION.md`. +- `graph/` — P project: the demand-graph runtime model (deliverable 4). + Built after `GRAPH_MODEL_SPEC.md` cleared review. Calibrated and + frozen: the full 66-cell matrix (G1-G9) sweeps clean at 10k + schedules, plus the 12-cell bake-off phase (12/12 on declared + verdicts); see `graph/CALIBRATION.md` for the run log and the + calibration finds (GS-CO-001..005, G8B-CAL-1, G9-CAL-1). +- `graph/BAKEOFF.md` — deliverable 4's written recommendation: the + lineage bake-off verdict (variant S — observable-causal stamps), + assembled under GS-CO-005's registered decision rule. This is the + artifact the demand-graph RFC cites. +- `occult/` — the deductive track (deliverables 6–9): equational laws + of the composition algebra and stamp lattice proved by equality + saturation in the Occult engine (sibling repo `../occult`), the + trace-policy oracle set, the session-typed protocol contract with + per-role projections, and the trace-bridge note. Includes the + broken-vs-good pairing: the phantom union DERIVED deductively from + the broken composition rule, and an executable reference + implementation of the demand-graph runtime validated against the + trace oracle (legacy mode reproduces the broken behavior and is + caught). The trace bridge is closed against the SHIPPED syncer too: + `pkg/sync` chaos tests record real commit-order traces + (`pkg/sync/sync_trace_audit.go`) exported as fixtures and checked by + the same oracle. See `occult/README.md` for status and + `occult/LAWS.md` for the law inventory. + +## Toolchain + +The P checker (https://p-org.github.io/P/) installs as a dotnet global +tool: + +```bash +dotnet tool install --global P # p --version → 3.1.0 at time of writing +``` + +Build and check (from a project directory such as `walker/`): + +```bash +p compile +p check -tc -i 10000 +``` + +The repo root Makefile wraps the standard runs — `make +formal-walker-sweep`, `make formal-graph-sweep`, `make +formal-graph-bakeoff` (override the budget with `P_SCHEDULES=`), `make +formal-occult-check` (needs the sibling `../occult` checkout and a Go +1.26 toolchain), and `make formal-check` for all of them. Each target +reports its missing prerequisite by name; none are installed for you. + +## Standing rules + +- Calibration before trust: the model earns authority by mechanically + rediscovering the known bugs (brief, "Calibration cases") with the + corresponding mitigations toggled off, and verifying the shipped/staged + fixes close them when toggled on. A green checker run means nothing + until the model has found the bugs we already know about. +- The model validates designs, not Go code. The bridge to the + implementation is trace-driven (deliverable 6). +- Candidate invariants are inputs to be checked, not assumptions; + refutation is a success. diff --git a/formal/REPORT.md b/formal/REPORT.md new file mode 100644 index 000000000..10c8780f3 --- /dev/null +++ b/formal/REPORT.md @@ -0,0 +1,309 @@ +# Sync formal verification — synthesis report + +This is the executive summary of the formal verification effort for +baton-sdk's sync scheduling semantics (charter: +`docs/tasks/sync-formal-model-brief.md`). It states what was modeled, +what was found, the verdict, and — as precisely as possible — what is +and is not guaranteed. Detail lives in the documents each section +cites. Public-repo content rules apply throughout. + +## The verdict in three sentences + +The current tiered-walker + source-cache-replay design mechanically +reproduces every calibration bug it was suspected of, including the +phantom union, with each failure caught by a dedicated monitor. The +proposed demand-graph runtime, after three adversarial spec reviews +and a calibration program that found and fixed five design bugs of +its own, is verified at small scope in both lineage variants, and the +registered decision rule selects **variant S (observable-causal +stamps)** — see `graph/BAKEOFF.md`. The shipped syncer's real +commit-order behavior has been witnessed conformant to the ordering +and durability policies on every honest execution we exported (cold, +warm, crash/resume, tombstone-delta, record-flip, external +principals); two exported fixtures are STANDING RED PINS by design — +the session-zombie trace under session-checkpoint consistency +(finding 0) and the SQLite external-principal trace under +external-principal grounding (the accepted degrade) — and the +instrument is sharp enough that it falsified one piece of documented +resume behavior along the way. + +## Track A — calibration model of the current design (P) + +`MODEL_SPEC.md` (v11, frozen after seven review rounds plus +MS-CO-001) and the `walker/` P project. Authority is earned by +rediscovery: with mitigations toggled off, the checker finds the +known bugs; with the shipped/staged fixes toggled on, the same cells +go green. The current sweep is 56 cells, 0 mismatches, 10k schedules +per cell (`walker/CALIBRATION.md`). + +Reproduced reds include: the phantom union in content, staleness, and +epoch-coherence form (scenario 1); session laundering under the +shipped free-form sessions (scenario 2 — an EXPECTED finding, fixed +only by the demand-graph work); session-checkpoint consistency in +both directions (scenario 2's P6-C cells: the shipped zombie +direction and the rejected resume-clear's amnesia direction, with +checkpoint-consistent sessions green — see finding 0); the +artifact-swap hit-rebind residual hole and the CO-6b-004 binding kill +that closes most of it (scenario 3); warm-drift, overlay placement, +and progress cells (scenarios 5–7); and external principals +(scenario 8's P8 monitor — the `deleteStaleExternalPrincipals` +contract: the non-deleting engine's stale-survivor degrade and the +stale-list recency mutant red, the capable-engine crash/stop/carry +schedules green). Every red regenerates its +counterexample on demand (`p check -tc ` — first find is +seconds-to-minutes); the sweep summaries are archived under +`walker/traces/`. + +## Track B — demand-graph runtime model and the bake-off (P) + +`GRAPH_MODEL_SPEC.md` (v4, frozen after three adversarial rounds) and +the `graph/` P project: frontier scheduler, generations with +quiesce-before-bump and the total mint fence, premise-validated +markers, seal-time sweep with the closure oracle, atomic units, and +the two lineage candidates — eager edges (E) and observable-causal +stamps (S). + +The calibration program found five design bugs before any bake-off +run (GS-CO-001..005 in `graph/CALIBRATION.md`), among them generation +identity reuse across crash re-mints, the carrier-durability fence, +and the retraction catch-up bound; plus two mechanism repairs +(G8B-CAL-1, G9-CAL-1 — stamp compression as first drafted livelocked +honest histories). The frozen matrix is 66 cells clean at 10k +schedules, and the 12-cell bake-off phase matched its declared +verdicts 12/12. + +The bake-off (`graph/BAKEOFF.md`, assembled under the GS-CO-005 +registered decision rule) ties on property satisfaction, and S wins +on mechanism count: no new durable state class, versus E's edge +checkpoint rows, retraction queue, two recovery machineries, and — +decisive in kind, not just count — E's correctness DEPENDENCE on the +constrained session primitives. The calibrated laundering cell +(`tcG2ea_P6G`) shows E sealing a dead writer's value under free-form +sessions with every artifact-level oracle green; S is green under +free-form sessions. Redo work corroborates and does not decide: the +reachable seal-world sets are identical under both variants. + +## Track C — the deductive track (Occult) + +`occult/` (deliverables 6–9), proved in the Occult engine (sibling +repo `../occult`) through the Go host in `occult/host/`: + +- **Laws (D9)**: 15 equational laws of the composition algebra and + stamp lattice proved, 5 negative controls correctly refused + (`occult/LAWS.md`). One law (L1, REPLACES absorption) closes in + bounded form pending an induction tactic. +- **Phantom union, derived**: the broken composition rule + (`occult/src/sync_phantom.occult`) DERIVES the phantom union + deductively — the same bug Track A reaches by search, reached by + proof, from an axiomatization of what the broken algorithm does. +- **Protocol (D8)**: the syncer↔connector source-cache lookup + continuation as a global session term with per-role projections — + 6 projection derivations plus a structural expansion, 4 polarity/ + cap/leg controls refused. The bounce cap is structural (no + five-bounce term exists), mirroring + `sourcecache.MaxLookupBouncesPerRequest`. +- **Trace policies (D7)**: seven ordering/durability policies + (consult-before-replay, clear-before-write, once-per-scope, + publish-before-checkpoint quiescence, seal obligations, + session-checkpoint consistency — finding 0's root cause as a + policy — and external-principal grounding, the + `deleteStaleExternalPrincipals` contract) over a canonical event + vocabulary including attempt + boundaries (`ev_resume`), the delta protocol's delete leg + (`ev_delete`), session operations (`ev_swrite`, + `ev_sread_hit`, `ev_sread_miss`), and the external-principal + phase (`ep_list`, `ep_live`, `ep_recon`, `ep_copy`). Verdict + matrix: 140 cells (20 fixtures × 7 policies), each green fixture + satisfies all, each red violates exactly its own. +- **Reference implementation**: an executable Go prototype of the + demand-graph runtime whose traces are judged by the same oracle; + its legacy mode reproduces the broken behavior and is caught. +- **The bridge to shipped code (D6)**: `pkg/sync` carries a + test-only commit-order recorder (`pkg/sync/sync_trace_audit.go`, + nil in production — the field is only ever assigned from test + files, which are not compiled into non-test binaries). Chaos tests + run the real syncer and export JSONL fixtures; the oracle judges + them: 56 policy cells across cold, warm, crash/resume, + tombstone-delta, record-flip, session-zombie, and + external-principal executions — 54 green plus TWO deliberately red + cells: the session-zombie fixture's `session_ckpt_consistency` + verdict, the standing known-defect pin on the shipped session + semantics (finding 0), and the SQLite external-principal fixture's + `external_principal_grounding` verdict, the standing known-degrade + pin on the non-deleting engine's warn-and-continue resume. + Planted-violation tests prove the bridge detects what it claims + to (`occult/TRACE_BRIDGE.md`). + +## Findings register + +Findings that matter beyond the models themselves: + +0. **Phantom union live in shipped code (real defect, FIXED).** The + walker model's scenario-1 red (the phantom union, tc1c flavor) + was reachable in the shipped 6b syncer via the verdict-flip path: + a warm round cut after its replay copy committed but before its + validator published, upstream moving between attempts, and the + resume's consult missing — the connector's fresh RECORD round + composed with the crashed attempt's copied debris and sealed the + union under the fresh validator, which the next sync validates + clean and replays forward (permanently stale "live" data). + Witnessed by `TestChaosSourceCacheRecordFlipOverReplayDebris`, + fixed by record-round grounding (`groundRecordScope` + + `ClearSourceCacheScope`): a record round is a replacement + listing, so a partition holding rows no completed round published + is cleared before the round's first write. The grounding is + trace-visible ("replacement rounds clear first" — the policy + doctrine, previously structural, now witnessed), and the fix's + mechanism and outcome are both pinned by the witness test. Note + the model's verified V-ATOMIC/V-OVERLAY-UNIT fix family maps onto + a code base with durable marker suppression; the shipped code + heals by re-execution (restart-from-root + idempotent re-copy), + so grounding was the missing piece, not unit-mode commit. + + The adjacent flank is the SESSION STORE — with a CORRECTED + provenance note: an earlier revision of this paragraph claimed + the hazard was found by code reading alone and that "none of the + models contain a session store." That second claim was WRONG. The + walker model has always carried the shipped session semantics + (MStore's `sessionKV`: durable at op commit, survives attempts + and crashes, while the checkpoint token is separate state), and + scenario 2's crash cell (`tc2crash_P6A`, an EXPECTED red since + calibration) contains exactly the zombie mechanism — the re-run + reads the dead attempt's durable stale value. The failure was + dispositional, not model-side: that red was filed as a + future-runtime obligation (sessions variant B, the graph + addendum) instead of being routed as a shipped-code change order, + and the checkpoint-relative ROOT CAUSE — post-crash session state + must equal the restored checkpoint's — was never stated as a + property in any system. Both gaps are now closed mechanically: + + - **P**: the P6-C monitor states the constraint in BOTH + directions. `tc2crash_P6C` reds `P6-C-ZOMBIE` under shipped + semantics (a dead attempt's beyond-checkpoint write observed by + the re-run); `tc2clear_P6C` reds `P6-C-AMNESIA` under the + rejected wholesale resume-clear (a checkpoint-committed value + destroyed — data whose producing work will not re-run); + `tc2consistent_P6C` is green under checkpoint-consistent + sessions, the registered fix. The amnesia premise needed a + reversed root order (cell 21) to be reachable — the writer must + pop before the reader for a committed value to precede a + re-run read. + - **Occult**: trace policy 6 (`session_ckpt_consistency`) states + the same constraint over the canonical vocabulary, with four + fixtures — including the correct-rollback green, where a crash + legally erases an UN-checkpointed write and the re-run's miss + is the fix behaving properly. + - **Witnessed**: `warm_replay_sync_session_zombie.jsonl`, + recorded from a REAL syncer crash/resume execution + (`TestChaosSourceCacheSessionPersistsAcrossResume`, which acts + as the session actor), is judged RED by the oracle — a standing + known-defect pin that flips to green when checkpoint-consistent + sessions land. + + On the code side: a mechanical fix (clearing the namespace on a + participating resume) was shipped and then REVERTED as unsound — + resume never re-runs completed actions, so a wholesale clear + destroys session caches whose producing work will not execute + again (now also a model verdict: `P6-C-AMNESIA`). Current stance + is contractual (CO-6b-009 in the 6b plan): session use must + survive at-least-once re-execution with prior state present, + consults must never be answered from session caches, and session + state is silently partial for replayed scopes — pinned in + `pkg/sourcecache` and `pkg/session/README.md`, with + persistence-across-resume pinned by + `TestChaosSourceCacheSessionPersistsAcrossResume`. The correct + mechanical fence (checkpoint-consistent sessions: a volatile + overlay flushed atomically with the checkpoint) is registered as + future work — and is now the variant the models verify green; + the lineage-bearing fix (session reads as stamped observation + points) is variant-S scope. +1. **Resume re-copy (real code, documentation falsified).** The + resume suite documented that a restored replayed-set skips the + replay copy across a mid-batch cut. The real-trace instrument + shows the skip cannot occur: checkpoints commit at batch + boundaries, so a mid-batch crash always forces a re-copy, and the + actual guard is the copy's replacement idempotence. Comments + corrected in `pkg/sync/chaos_source_cache_resume_test.go`; pinned + in `occult/TRACE_BRIDGE.md`. +2. **E-only laundering (design).** Eager-edge lineage is correct + only with constrained session primitives; free-form reads seal a + dead writer's value undetected. This moved the session-primitive + audit from a correctness gate to an optimization under the + adopted variant S. +3. **Generation identity reuse (design, fixed in spec).** Crash + before checkpoint allowed a re-minted generation to collide with + its predecessor; fixed by the total mint fence (GS-CO-002/003 + family), verified by kill cells. +4. **Stamp compression livelock (design, fixed in spec).** The + first-draft compression rules livelocked honest histories into + the pre-seal pass budget; admissible only with the G9-CAL-1 + minting rules. +5. **Scheduler sensitivity (verification methodology).** The + dying-reader race (`tcG1d_P6G`) needed feedback-PCT and a third + worker to exhibit — implementation tests for that race must not + rely on uniform random schedules. +6. **Engine findings (tooling).** Ground-term evaluation cost grows + ~2x per trace event (the gate for longer real traces), and + parameterized protocol definitions do not project. Recorded with + six other asks in `docs/tasks/occult-engine-changes-brief.md` + (local working doc). + +## What is guaranteed, and what is not + +Three grades of code linkage, in decreasing mechanical strength: + +- **Witnessed** (trace bridge): the policy verdicts hold for the + real executions we exported. This catches what happens; it does + not prove what cannot happen. +- **Calibrated** (both P models, the phantom derivation): the models + reproduce the known bugs, which is evidence of fidelity, not proof + of it. +- **Asserted** (laws ↔ named function contracts, protocol ↔ proto + shapes and Go constants): documented correspondence, held by + review. If the Go side drifts, nothing goes red automatically. + +Standing limits: P verdicts are exhaustive only within the stated +schedule budgets and small-scope configurations (each spec's §8 +declares its exclusions — notably sessions × demand shrink and +session-transitive chains in the graph model). Redo figures are +counts at small scope, not throughput. The models arbitrate designs, +not Go code; the trace bridge is the standing instrument that must +stay current as the design becomes an implementation. + +**Asserted-but-unwitnessed monitor clauses.** By this track's own +doctrine, a monitor clause no red cell fires is indistinguishable +from dead code, and the greens say nothing about it. The frozen +matrices leave these alarm strings unwitnessed; a reader must not +credit the green cells with covering them: + +- Walker (3 of its alarms): `P1-ATTEST-EMPTY`, `P1-ATTEST-PUBLISH`, + `P2-CONSULT`. Each is a narrower sibling of a witnessed clause on + the same monitor (`P1-ATTEST-SEAL`, `P1-CONTENT`, and + `P2-STALENESS` all have red cells); no cell targets these + specific legs. (`P8-EXT-MISSING`, formerly on this list, is now + witnessed by tc8overDelete_P8.) +- Graph (8 of its 25 alarms): `P1-CONTENT`, `P1-ATTEST-PUBLISH`, + `P1-ATTEST-SEAL`, `P1-ATTEST-EMPTY`, `P2-CONSULT`, + `P2-STALENESS`, `P3'-COHERENCE`, `PASS-BUDGET`. These are the + walker-inherited content/attestation/staleness oracles restated + over the graph store; the graph matrix's kills target the graph's + OWN mechanisms (generations, markers, adoption, sweep, counts — + all witnessed). The walker leg witnesses the same property + STATEMENTS with 9+ reds, but against the walker's checking code, + not the graph's — the graph implementations of these clauses are + calibrated only by review. + +## What's next + +1. **The variant-S RFC and production implementation** in + `pkg/sync`, carrying the four design consequences listed in + `graph/BAKEOFF.md` (primitives as optimization, observation-point + discipline, compression's minting rules, non-uniform-schedule + race tests). +2. **Keep the oracle current**: as demand-graph code lands, extend + the trace vocabulary with generation stamps and grow fixtures + alongside the implementation — the bridge is only as good as its + coverage. +3. **Occult Ask 8** (tractable ground evaluation) when engine time + exists: it unblocks realistic trace lengths and is the wedge for + the longer-term single-engine unification path. diff --git a/formal/graph/BAKEOFF.md b/formal/graph/BAKEOFF.md new file mode 100644 index 000000000..c6e8eb611 --- /dev/null +++ b/formal/graph/BAKEOFF.md @@ -0,0 +1,151 @@ +# Lineage bake-off verdict: eager edges (E) vs observable-causal stamps (S) + +This is deliverable 4's written recommendation — the artifact the +demand-graph RFC cites. It is assembled under the decision rule +registered in `formal/GRAPH_MODEL_SPEC.md` §12 GS-CO-005 (declared +before the bake-off runs; the rule's v2 text was never committed and +GS-CO-005(a) repairs that provenance), from: + +- the frozen 66-cell calibration matrix (10k schedules per cell, + zero mismatches — `CALIBRATION.md`, freeze sweep 2026-08-30); +- the 12-cell bake-off phase (GS-CO-005(c)/(d) declarations, 12/12 + match — `CALIBRATION.md`, bake-off results); +- the frozen mechanism tally (spec §7.5, amended v3 BEFORE any + bake-off run). + +The decision rule is lexicographic — property satisfaction, then +mechanism count, then redo work — and the first non-tie decides. + +## The candidates + +Both variants ride the same shared chassis (spec §7.5 "shared"): +frontier checkpoint with the admitted-derivation set (death +semantics, admitted-by edges, cursors), generation table with +quiesce-before-bump and the total mint fence, premise-validated +markers (MATCH-only, writer-ineligible), the supersession matrix and +poison rule, the seal-time sweep with the closure oracle, and atomic +units. + +- **Variant E (eager edges)** adds: durable admitted-by edge + checkpoint rows, the resume ∀-pending-purge, derived-support + rebuild plus the agreement check, the retraction rule and queue + with pinned enqueue/drain — and it REQUIRES session variant B + (the three constrained session primitives). +- **Variant S (observable-causal stamps)** adds: stamp merge on + read, the per-output stamp field with the `eAdopt` rewrite, three + observation points (dispatch-time refusal, session-read + read-through with the dead-read count, the pre-seal pass with a + 3-iteration budget), and optional bucketed compression. + +## Axis 1 — property satisfaction: TIE + +Both variants, with their mechanisms on, are GREEN on every honest +cell of the freeze matrix, and every declared kill fires on its +designated monitor (P1-LEGALITY, P-GEN, P-ADOPT, P-MARK, P4-STUCK, +P5-UNDER/OVER, P6-G, SEAL-EXPECT, DEAD-DISPATCH, plus P6-E under E +and P6-S/GPASS under S). Neither variant is eliminated. + +The one structural asymmetry Axis 1 surfaces: **E is correct only +with session variant B.** Under free-form sessions (E+A) the sealed +reader rows embed a dead writer's value with every artifact-level +oracle green — the calibrated laundering find (`tcG2ea_P6G`, the +G2 family's star cell). S is green under free-form sessions +(`tcG2s_All`): stamp merging makes arbitrary reads tracked without +constraining the session surface. Per GS-CO-005(b) this dependency +travels to Axis 2 as part of E's mechanism bill. + +## Axis 2 — mechanism count under the frozen tally: S wins + +The §7.5 counting rule weighs durable state classes over scheduler +rules over per-output overhead. + +- E's bill: one added durable state class (admitted-by edges as + checkpoint rows) plus the retraction queue, two recovery-path + machineries (∀-purge; support rebuild + agreement check), the + retraction rule with its pinned enqueue/drain and the GS-CO-004 + catch-up bound — and the session-B requirement, which is not a + line item but an entire companion surface (three constrained + primitives whose "covers all legitimate uses" claim is a separate + SDK audit obligation). +- S's bill: no new durable state class. Its heaviest item is the + per-output stamp field — the lowest-weight class in the counting + rule — plus merge-on-read and the three observation points as + scheduler rules. Compression is optional (an optimization lever, + not a correctness mechanism). + +First non-tie. **The decision rule terminates here: variant S.** + +## Axis 3 — redo work: corroborates, does not decide + +Citation unblocked by the G5d determination (no divergence; see +below). + +- Metric floor: the zero-crash execution bound is 1 under both + variants (four v1 controls GREEN) — the count oracle measures + crash-caused redo, not variant overhead. +- Worst-case redo across the divergence scripts is SYMMETRIC: + minimal green bound 2 (redo ≤ 1 per node) on the chain, fan-in, + and mutation chassis under both variants; the bound-1 probes red + under both. No chassis separates the variants on redo count. +- Each variant pays the at-least-once cost through its own + mechanism — E's retraction-forced reader re-runs and S's + pass/refusal-forced re-runs both exhibit as REDO-PROBE reds on + their respective legs; where shared machinery forces the redo + (writer-ineligibility), the cost is charged to both equally. +- S-specific costs are bounded and verified: the pre-seal pass + converges within its 3-iteration budget in every honest cell + (GPASS), and stamp compression is admissible only under the + G9-CAL-1 rules (bucket-aligned heal minting + the ambiguity + double-bump), trading bounded extra redo for stamp width. +- G5d (which-is-right): the reachable seal-world sets are IDENTICAL + — {W1, W2} reachable and the sweep-failure world W3 unreachable + under both variants, every reachable world inside the sync-scoped + SealExpect envelope. The variants differ in HOW a world is + reached (E purges the de-demanded child; S refuses its dispatch), + never in WHICH worlds are sealable. + +## Recommendation + +**Adopt variant S — observable-causal stamps over the shared +chassis.** Note that the shared chassis already retains admitted-by +edges and the demand closure for the sweep, so this verdict IS the +hybrid the brief anticipated ("support counting retained solely to +compute the demand closure for the sweep, stamps replacing all eager +retraction") — expressible as shared column + S's adds, which +GS-CO-005(b) admits as a plain variant-S recommendation. + +Design consequences the RFC should carry: + +1. The constrained session primitives demote from correctness + requirement to stamp-width optimization. Free-form session reads + are safe under S (`tcG2s_All`); the primitives remain worth + having to keep stamps narrow, but the SDK audit ("the three + primitives cover all legitimate uses") stops being a correctness + gate. +2. Internal causality gets the same discipline as external: + upstream validators are consulted at observation time, and + stamps apply exactly that observation-point pattern to node + generations (the brief's unification note, now checker-backed). +3. Compression ships only with the G9-CAL-1 minting rules + (re-mints land even, first admissions odd, floor-stale bumps the + owner, parity-ambiguous entries also bump the named node) — + as first drafted it livelocked honest histories into the pass + budget. +4. The quiesce-before-bump pin is shared machinery, but its kill + (`tcG1d_P6G`) needed feedback-PCT and a third worker to exhibit: + implementation tests for the dying-reader race must not rely on + uniform random schedules. + +## Honest limits + +- Small-scope bet as restated in spec §8: sessions × demand shrink + and session-transitive reader-writer chains are excluded by + declaration; first-order retraction and row-transitive support + are exercised. +- Redo figures are checker-verified worst-case COUNTS at small + scope, not throughput; stamp width in bytes is not modeled (G9 + models compression's decision effects, not encodings). +- The model arbitrates the design, not the Go implementation. The + bridge is the trace-policy oracle set (`formal/occult/`, D7) and + the chaos-scenario seam (D6) — keep them current as the RFC turns + into code. diff --git a/formal/graph/CALIBRATION.md b/formal/graph/CALIBRATION.md new file mode 100644 index 000000000..aba9090ef --- /dev/null +++ b/formal/graph/CALIBRATION.md @@ -0,0 +1,463 @@ +# Graph model calibration — run log + +Status: COMPLETE. All scenario families (G1–G9) built and +calibrated; the frozen 66-cell full-matrix sweep PASSED (see +§"Freeze sweep — 2026-08-30" below; run of record +`PCheckerOutput/sweep/summary.txt`, `SWEEP-DONE cells=66 +mismatches=0`; reproduce with `tools/sweep.sh`), and the 12-cell +bake-off phase matched its GS-CO-005 declared verdicts 12/12 (see +§"Bake-off results — 2026-08-30" below; run of record +`PCheckerOutput/bakeoff/summary.txt`; reproduce with +`tools/bakeoff.sh`). The verdict synthesis lives in `BAKEOFF.md`. +FLAP-BACK MACHINERY: the upstream reports CONTENT +epochs everywhere (raw epochs never escape it), so `flapBack` +(raw e>=3 serves content(e1)) keeps validators, manifests, folds, +and SealExpect worlds coherent with no monitor changes. Spec +baseline: +`formal/GRAPH_MODEL_SPEC.md` v4 FROZEN + GS-CO-001..005 (the first +four change orders were driven by calibration finds in this +build-out — see §12 of the spec and the "Model decisions of record" +section below; GS-CO-005 is the pre-registered bake-off decision +rule). + +Toolchain: P 3.1.0 (`p compile` / `p check -tc -s `), +same conventions as `formal/walker/CALIBRATION.md`: verdicts are +audited by counterexample trace-file presence (`tools/sweep.sh`), reds +are the calibration currency, and a green run means nothing except at +the stated budget. + +Project layout mirrors the walker: `PSrc/` (Types, Events, Upstream, +Store, Sched, NodeExec, Env), `PSpec/Monitors.p` (GP1, GP2, GP3prime, +PGEN, PMARK, PADOPT, SealExpectG, GP6G, GP6E, GP6S), +`PTst/ScenarioG1.p`. Cell topology 11: parent P (node 0) → consult +node C (node 1). Sessions topology (cell 21, built, exercised from G2 +on): P → writer W/H + reader G. + +## G1 family — phantom-union premises + generation identity (sweep: 10/10 at 5000 schedules) + +| cell | config | property | expected | observed | budget | +|---|---|---|---|---|---| +| tcG1i_All | leg (i): crash sync 2, fetch-fresh policy, e1→e2 between syncs | all core | GREEN | GREEN | 5000 | +| tcG1ii_All | leg (ii): diff unit, crash, e2→e3 between attempts, MATCH-only re-derive fresh | all core | GREEN | GREEN (after GS-CO-002; see finds) | 5000 | +| tcG1iii_E | leg (iii): crash-after-commit, no mutation → ADOPT, lineage E | all core | GREEN | GREEN | 5000 | +| tcG1iii_S | same, lineage S (+P6-S) | all core + P6-S | GREEN | GREEN | 5000 | +| tcG1b_All | two-crash generation-reuse probe, honest | all core | GREEN | GREEN | 5000 | +| tcG1cMut_P3 | adoptOnFail mutant, P3′ control | P3′ | GREEN | GREEN (FAIL consult does not qualify; last qualifying verdict expects the mutant's own rows) | 5000 | +| tcG1cMut_Seal | adoptOnFail mutant, SealExpect control | SealExpect | GREEN | GREEN (GS-CO-002: rows(e2) is inside the sync-scoped envelope — the declared artifact-blindness control) | 5000 | +| tcG1sup_P1 | suppressionOff on the leg-(iii) chassis | P1 | RED | RED: `P1-LEGALITY` (racing double-admission at one generation; second live complete-round copy) | first find | +| tcG1bMut_PGEN | resumeCkptOff | P-GEN | RED | RED: `P-GEN` (re-minted identity carries store commits in two attempts; fireable with ONE crash post-GS-CO-001) | first find | +| tcG1cMut_Adopt | adoptOnFail | P-ADOPT | RED | RED: `P-ADOPT` (adoption after a FAIL consult — MATCH-only eligibility violated) | first find | + +Sweep summary archived at `PCheckerOutput/sweep/summary.txt` (rerun +`tools/sweep.sh` from `formal/graph`). + +## G2 family — session laundering + G1d/G1e probes (topology 21: P -> writer H + reader G) + +| cell | config | property | expected | observed | budget | +|---|---|---|---|---|---| +| tcG2ea_Core | E + variant A, value change across attempts | artifact-level core | GREEN | GREEN | 5000 | +| tcG2ea_P6G | same cell | P6-G | RED | RED: `P6-G` (THE FINDING: no retraction, no stamps — the sealed reader rows embed the dead value; artifact-level monitors all green, the blindness contrast is tcG2ea_Core) | first find | +| tcG2eb_All | E + variant B (retraction + quiesce) | all + P6-G/P6-E | GREEN | GREEN (after GS-CO-003; see finds) | 5000 | +| tcG2ebRetrOff_P6G | retractionOff | P6-G | RED | RED: `P6-G` | first find | +| tcG2s_All | S (stamps + pre-seal pass) | all + P6-G/P6-S | GREEN | GREEN | 5000 | +| tcG2sStampOff_P6G | stampMergeOff | P6-G | RED | RED: `P6-G` | first find | +| tcG2awE_All | announce-window/flap-back honest, E+B, premise-stable | all + P6-E | GREEN | GREEN | 5000 | +| tcG2awS_All | same, S | all + P6-S | GREEN | GREEN | 5000 | +| tcG2awE_Redo | flap-back existence probe | REDO-PROBE | RED | RED: `REDO-PROBE` (the at-least-once cost is real: retraction-forced reader redo exists) | first find | +| tcG2awS_Redo | same, S (pass-forced redo) | REDO-PROBE | RED | RED: `REDO-PROBE` | first find | +| tcG2awWA_E | writerAdopt, E+B | P6-E | RED | RED: `P6-E` (after GS-CO-004; the stranded dead publish is never retracted) | feedback-PCT(20), first find ~3.8k (random first-find sits at the 5k budget edge) | +| tcG2awWA_S | writerAdopt, S | P6-S | RED | RED: `P6-S` (pass budget exhausts against the stranded dead stamp) | first find | +| tcG2eb2c_All | honest two-crash, E+B | all + P6-E | GREEN | GREEN | 5000 | +| tcG1e_PGEN | two crashes + midBumpFenceOff | P-GEN | RED | RED: `P-GEN` (identity reuse; the first-find may ride the first-admission mint path — same toggle per GS-CO-001, same discipline) | first find | +| tcG2fbE_All | WRITER FLAP-BACK probe (R3-F1 second registration), E+B: attempt-1 DIFF publish-bearing unit (d2), crash, content(e3)=content(e1) | all + P6-G/P6-E | GREEN | GREEN (H ineligible -> re-consult vs prev artifact MATCHes -> REPLAY -> body-op re-publishes d1@g2 -> readers cleared; under an elision reading this history strands d2@g1 — the probe is the body-op pin's load-bearing witness) | 5000 | +| tcG2fbS_All | same, S | all + P6-S | GREEN | GREEN | 5000 | +| tcG2fbE_Redo | flap-back chassis | REDO-PROBE | RED | RED: `REDO-PROBE` (declared expected count >= 1) | first find | +| tcG2ebPend_Redo | honest E+B laundering chassis | REDO-PROBE | RED | RED: `REDO-PROBE` (the G-pending re-retraction clause fires: R2-M6(i) via GS-CO-004's catch-up + re-publish paths) | first find | +| tcG1dProbe_Redo | G1d chassis | REDO-PROBE | RED | RED: `REDO-PROBE` (the race arms: forced reader redo exists) | first find | +| tcG1d_P6G | E+B + quiesceOff, 3 workers | P6-G | RED | RED: `P6-G` (dying reader's stale round survives to seal; confirmed post-handshake — the first feedback-PCT run instead caught the harness deadlock below) | feedback-PCT(20), first find at 188 schedules | + +### G1D-REACH — reachability ladder for the quiesceOff kill + +The dying-reader race is DEEP under uniform random search: 165k +random schedules found nothing. Bisected by temporary existence +probes, each RED = milestone reached: forced reader redo on the +chassis (`tcG1dProbe_Redo`, kept, first find ~3k); same-node +concurrent dispatch (temp diag, common); a second reader round +CLEARING while another is open on the reader's key (temp diag, +reached under 30k); the full kill — the stale round's content +surviving to SEAL — never, at 120k random. Two aids resolved it, +both harness-level: + +1. `nWorkers = 3` for this cell (spec §1 tagged note) so the + retraction-forced re-run dispatches AT the bump instead of + waiting for a completion carrier to free a worker (with 2, the + race additionally needs the scheduler to starve the dying worker + through H's entire completion). +2. Strategy: `--sch-feedbackpct 20` (feedback-mutating PCT). + First find at 188 schedules, 0.53% buggy — the race is + priority-inversion-shaped, exactly PCT's regime; the sweep runs + this cell under that strategy (per-cell strategy column in + `tools/sweep.sh`). + +Diagnostics removed after resolution; the redo probe stays as a +sweep cell (the ladder's first rung documents the chassis arming). + +### Harness find — crash-arm queue race (feedback-PCT bycatch) + +The first feedback-PCT sweep run of `tcG1d_P6G` redded on DEADLOCK, +not P6-G: the env sends `eCrashArm` right after creating the +attempt scheduler, so a schedule that starves the env lets the +ENTIRE attempt (through seal) enter the store's queue ahead of the +arm — the store's at-seal resolution point sees `armed == false`, +seals clean, and the late-landing arm never resolves; the env +blocks on `eCrashAck` forever. Random search never starves one +machine for hundreds of steps, which is why no random sweep (graph +or walker) ever hit it; priority-based PCT does exactly that and +found it at 34 schedules. Fix: synchronous arm handshake +(`eCrashArm` → `eCrashArmed`) BEFORE the attempt is created, so the +arm's queue position precedes every attempt op by construction. +The walker env has the same latent race (arm sent after +`new MSyncAttempt`, random-only sweeps) — noted in the walker's +calibration log; harness-level, no bearing on either model's +verdicts. + +## G5 family — sweep, purge, and the closure oracle (topology 24: paginated P -> consult C, named on page 1 only) + +Cell 24 scripts UPSTREAM DEMAND SHRINK: the parent's own scope (key +0) mutates between attempts, and its page-1 row — the only row that +names C — is the row the e1->e2 mutation deletes. New monitors: GP5 +(artifact demand-closure, both directions, self-contained over the +sealed artifact: CLOSED = every non-root sealed partition has a +living namer, COMPLETE = every named child's partition is sealed), +PURGEPROBE (existence probe on `eAnnPurge`, REDO-PROBE pattern), +GDEADDISPATCH (the G5e count oracle in checkable form: no node ever +dispatches with every admitted-by edge dead), GPASS (§10.8: honest S +cells seal within the pass budget; also asserted retroactively in +tcG1iii_S / tcG2s_All / tcG2awS_All / tcG2fbS_All). + +| cell | config | property | expected | observed | budget | +|---|---|---|---|---|---| +| tcG5aE_All | honest sweep, E, shrink chassis | all core + GP5 + DEAD-DISPATCH | GREEN | GREEN (which parent world seals is schedule-dependent under sync-scoped freshness: P re-ran -> seal {0}; P completed at e1 -> C live, seal {0,1} — C's key EXCLUDED from the attempt-2 expectation, the structural question is GP5's) | 10000 | +| tcG5aS_All | same, S (dispatch-refusal leg) | + GPASS | GREEN | GREEN | 10000 | +| tcG5f_All | honest, NO shrink (R2-F5 no-starvation witness) | all core + GP5 | GREEN | GREEN (resume purges C through the mid-round window; P's live re-derivation re-names it; the purged hash is re-admissible; C re-runs — starvation would red SealExpect) | 10000 | +| tcG5bE_P5 | sweepOff, E | P5 (CLOSED) | RED | RED: `P5-UNDER` (C's dead partition seals with no living namer) | first find | +| tcG5bS_P5 | sweepOff, S | P5 (CLOSED) | RED | RED: `P5-UNDER` | first find | +| tcG5c_P5 | sweepOverreach, no shrink | P5 (COMPLETE) | RED | RED: `P5-OVER` (an in-closure key dropped at seal; the sealed parent still names it) | first find | +| tcG5e_Probe | honest E shrink chassis | PURGE-PROBE | RED | RED: `PURGE-PROBE` (the resume ∀-purge fires — the counterexample exhibits the mid-round C-pending & P-pending checkpoint window) | first find | +| tcG5e_PurgeOff | purgeOff, E | DEAD-DISPATCH | RED | RED: `DEAD-DISPATCH` (the restored C dispatches with its only admitted-by edge dead — dead demand executed) | first find | +| tcG5f_Drop | demandDrop, no shrink | SealExpect / P5 (COMPLETE) | RED | RED: `SEAL-EXPECT` (the dropped child never runs; the expected demand-closure key is missing) | first find | + +### G5 build finds (model-level, no spec change) + +1. **Per-announce demand timing implemented** (G-RULE-1 TIMING PIN). + The G1/G2 build derived demand at the completion carrier only — + observationally equivalent for those families, but the pin is + load-bearing here: the mid-round checkpoint window (C-pending ∧ + parent-pending) that the resume ∀-purge exists for is UNREACHABLE + under carrier-only derivation (exactly round-1 F5(iii)'s + argument). A paginated record round now sends the scheduler a + demand note per committed child-naming page (`eGDemandNote`, + read-note pattern); each note is its own carrier under GS-CO-003's + one-delta fence. The completion carrier still re-derives + (idempotent via G-RULE-2 suppression + edge dedupe) — belt and + braces, and a crashed note costs nothing (the row is durable; a + pending parent re-derives, a completed parent's admissions rode + the fence). +2. **Pass domain = demand closure** (S). The pre-seal pass chased + dead stamps on OUT-OF-CLOSURE keys — debris the sweep is about to + drop — force-bumping their owners into dispatch-refusal loops + until the budget exhausted on an honest history (tcG5aS_All red + before the fix). The scan now skips keys outside the current + closure, recomputed per sealPhase entry. Spec §5's pass text + ("one scan over a drained frontier") does not state the domain; + flagged for an RFC-stage clarification line, not a change order. +3. **demandDrop models a lost PATHWAY, not a lost message.** With + belt-and-braces derivation the original drop-one-admission inject + is always healed by the second derivation of the same emission + (tcG5f_Drop was green). The inject now drops every admission of + the first hash it fires on. +4. **The count oracle is a dispatch-time announce.** "No post-resume + C execution" is not directly assertable (C's post-resume run is + HONEST when the completed parent's naming stays live — + sync-scoped freshness again); the invariant form is + DEAD-DISPATCH: no node dispatches while every admitted-by edge + names a dead generation. Honest mechanisms (E resume purge, S + dispatch refusal) make it unreachable on every cell, so it is + asserted in the honest G5 greens, not just the kill. +5. **Digest session fields are DECLARED VACUITY in this envelope + (SPEC §4a DIGEST CLOSURE discharge).** `tDigest` carries the + session-read fields the spec requires + (`sVal`/`sWriter`/`sWGen`/`hasSess`), but every digest the model + constructs uses the no-session sentinel (`NodeExec.p`): no + adopt-eligible node performs a pre-commit session read in any + calibrated cell — readers always fetch fresh and never consult + markers, writers are writer-ineligible (pubBearing), and the + consult-kind adopt check precedes any session read. Per the + spec's own closure clause this omission is registered vacuity, + not an escape: any extension that gives an adopt-eligible node a + pre-commit session read MUST populate the digest fields (and add + a cell where a session-value change kills adoption) or it + reopens exactly the E-only-laundering class the digest exists to + refuse. + +## G3 / G4 / G6-G9 — built and calibrated + +G4 (duplicate admission) is REGISTERED COVERAGE, not new cells: cell +11's parent names C on EVERY row — the double-token fan-in premise — +so the honest suppression leg IS tcG1i_All and the `suppressionOff` +kill IS tcG1sup_P1 (round-1's walk shape: racing double admission, +P1-LEGALITY). G8a (record REPLACES over dead debris) is likewise G1 +sweep coverage: the crash cells explore every mid-record-round +position and the REPLACES clear wipes debris on re-run. + +New machinery for the remaining families (all announce-only): + +- G3 (cell 25): scripted GRACEFUL STOP (interrupt 1) — the flagged + consult execution stops after its consult announce, the scheduler + checkpoints with the node pending at its cursor, the attempt ends + unsealed with the store intact; `eGSwapPrev` rebinds the PREV + artifact between attempts. nWorkers=1 (no straggler commits). +- G6 (cells 26 chain / 28 fan-in): GEXECBOUND count oracle — + executions per node per sync <= cfg.execBound; minimal GREEN bound + = checker-verified worst case; bound-minus-one RED probes are the + redo existence exhibits (adequacy §10.1 count-oracle kills). + Declared bounds: 2 everywhere (redo <= 1 per node). +- G7 (cell 27): loud deterministic failure (failNode/failSync) with + a GENERATION-BLIND fingerprint (F11); env abandon ladder (give up + after 2 identical fingerprints); GP4STUCK reds 3 in a row. + tcG7_Ladder GREEN / tcG7_Stuck RED declared. +- G8b: `composeDead` on the overlay unit (skip clear + prev-copy). + DECLARED FINDING (pre-run analysis): the mutant is + CONTENT-INVISIBLE in this envelope — with 2 row ids and truthful + TOTAL diffs, every diff from the debris's base overwrites/removes + every debris id. [SUPERSEDED by G8B-CAL-1: content-invisibility + confirmed, but the crash re-run leg is mechanism-visible to P1 — + see calibration results. tcG8bMut_P1 is the kill; tcG8bMut_Ctl + keeps the content oracles GREEN.] +- G8c (cell 29): keyOf maps two distinct derivations to one output + key; the poison path (already store-side: void + adopt-refusal + + SealExpect exclusion + P1 exemption) gets a POISONPROBE existence + probe. GP5 is NOT asserted (its key = hash - 1 convention is what + this cell breaks). +- G8d: marker flap-back (cell 11 + flapBack + interrupt 3 + + between-attempt mutation): honest GREEN via R3-M1's REPLAY-verdict + correction; `markerCleanupOff` -> P-MARK RED declared. +- G9: `stampCompression` — the S pass compares FLOOR-BUCKETED stamps + (buckets of 2): stale-erring, never false-live. Safety verdicts + must be identical to uncompressed legs (any change refutes + admissibility). [Convergence rules and the growth-exhibit pair as + first declared were wrong — see find G9-CAL-1 in the calibration + results.] + +Deferred to the bake-off phase: G6's v1 control scripts (delta ~ 0 +calibration points; metric-only, no kill) and the G5d cross-variant +seal-artifact meta-analysis (harness-level, outside P; §10.6). + +### Calibration results (10k schedules per cell; 28 cells, all +### matching declared verdicts after the finds below) + +Kills, first pass: 7/8 red cells fired under uniform random search — +tcG7_Stuck [P4-STUCK], tcG8c_Poison [POISON-PROBE], and the +count-oracle probes tcG6{a,c}{E,S}_Redo + tcG9c_Redo [EXEC-BOUND]. + +**Find G8D-CAL (search depth, not model).** tcG8dMut_PMARK was 0/10k +under uniform search. The kill needs attempt 1's crash to land in the +[replay-unit commit, completion checkpoint) window — the LAST store +ops of the attempt — and `maybeCrash`'s per-op coin makes P(crash at +op k) = 2^-k: late crash positions decay geometrically, ~1e-4 before +attempt-2 requirements compound it. Same pathology as G1D-REACH; +same remedy: `--sch-feedbackpct=20` finds P-MARK reliably (0.02% +buggy schedules at 20k), pinned for the cell in sweep.sh. Trace +confirms the declared shape: the marker survives attempt 2's REPLACES +clear (mutant), and P-MARK fires on the foreign round mutating the +marked key's partition. + +Honest legs, first pass: 13/17 GREEN; the four reds decomposed into +one harness gap and two genuine finds: + +- Harness gap (not a find): cell 27 was missing from the env's + SealExpect expectation script (key 1 unexpected on an honest + sync-1 seal). One-line fix; tcG7_Ladder GREEN. +- **Find G8B-CAL-1 — the composeDead mutant is content-invisible + but NOT mechanism-invisible.** The declared vacuity (pre-run + analysis, logged below) only considered the no-crash leg. On the + crash re-run the skipped clear leaves the DEAD attempt's copy + round live under the composed diff, and P1's + one-live-replacement-copy legality reds it — the §4b precondition + has a kill after all. Disposition: tcG8bMut_P1 is the kill + [P1-LEGALITY]; tcG8bMut_Ctl now asserts only the content-level + oracles (GP2/GP3'/SealExpect/GP5) and stays GREEN as the + registered content-invisibility evidence. +- **Find G9-CAL-1 — floor-bucketed stamp comparison is admissible + ONLY WITH bucket-aligned heal minting and an ambiguity + double-bump.** As first built (compressed comparison only, all + mints +1), honest S histories redded PASS-BUDGET: every heal + re-created odd generations one level down — the pass's owner bump + left readers merging the writer's unchanged odd generation, and + dispatch-refusal re-admissions re-minted children odd — so + convergence needs O(demand-depth) iterations, not 3. The + admissible rule set, now implemented and green: (i) every + scheduler RE-mint (pass heal, retraction, resume, refusal + re-admission) lands on an even generation; first-admission mints + stay odd, so the mixed-parity stamp population and its redo cost + remain modeled; (ii) a floor-stale entry always bumps the key's + OWNER (the exact rule), and when the entry is parity-AMBIGUOUS + (floor(s) = cur-1, cur odd, named node != owner) it ALSO bumps + the NAMED node — the owner's re-read alone can never prove an + odd live generation. Worst case converges in exactly the budget + (detect/heal, raced-merge re-heal, verify). Exhibit reshaped: a + crash script MASKS the redo growth (resume redo and heal-wave + redo both peak at 2 per node), so the growth pair is the + no-crash fan-in chassis — tcG9cBase_All (uncompressed) + GREEN@bound1, tcG9c_All GREEN@bound2, tcG9c_Redo RED@bound1. + +## Model decisions of record (already registered as spec change orders) + +- GS-CO-001 — MINT-FENCE TOTALITY. First honest run of the leg-(iii) + cell redded P-GEN: attempt 1 crashed before any checkpoint, + attempt 2 cold-restarted and re-minted (P, 1), whose identity + carried commits in both attempts. G-RULE-3's durable fence is + total over all four minting paths: attempt-start root mint and + first-admission mid-attempt mint (found here), resume bump (F4), + mid-attempt bump (R3-F2). Encoded as forced checkpoints before + first dispatch of any newly minted generation; the root-mint fence + rides `resumeCkptOff`, the first-admission fence rides + `midBumpFenceOff`. +- GS-CO-003 — CARRIER-DURABILITY ATOMICITY. First honest G2 run + redded SealExpect (closure key missing): P's carrier admitted H + (fence ckpt), crashed before G's fence — restore shows P + COMPLETED with G's admission lost, and a completed parent never + re-derives, so the demand starves. A carrier's derived effects + (admissions, mints, edges) commit durably as ONE delta at the end + of the demand loop; no checkpoint may separate a carrier's + completion from its admissions. +- GS-CO-004 — RETRACTION CATCH-UP BOUND. Two coupled finds: (a) + carrier-time session-read registration makes the R2-F1 + dying-reader race structurally unreachable (the scheduler can + never see an in-flight reader), so reads register at READ time + via a scheduler note; (b) an unbounded registration-side + retraction rule livelocks the writerAdopt strand (the adopted + writer never re-publishes; the reader re-runs forever; the + frontier never drains; P6-E is structurally unevaluable and the + kill cell goes GREEN-by-divergence). Registration-side retraction + is a once-per-(reader, dead-wgen) CATCH-UP; all repeated + retraction is re-publish-driven, per R2-M6(i) as written. +- GS-CO-002 — SYNC-SCOPED SEALEXPECT + P-ADOPT (find G1-CAL-1). The + scripted single-epoch expectation redded an honest leg-(ii) + schedule: C's diff round completed AND checkpointed in attempt 1, + crash landed before seal, attempt 2 restored an all-completed + frontier and sealed attempt-1 content (e2) while the expectation + demanded attempt-2's live world (e3). That survival is licensed by + G-RULE-2 (completed-across-crash) and the SYNC-scoped staleness + contract, so SealExpect now accumulates the acceptable epoch set + across attempt starts. Consequence: the `adoptOnFail` laundering + (FAIL-consult adoption of e2) is inside the envelope — invisible + to EVERY artifact-level oracle (SealExpect and P3′ both green on + the mutant, kept as declared controls) — so the kill moved to the + new P-ADOPT mechanism monitor: adoption requires a validated MATCH + consult announced by the adopting (node, generation) BEFORE the + adopt commits. The announce-order pin matters: the first build + announced the justifying consult after the store's adopt announce + and P-ADOPT redded three honest adopt cells; the justification + precedes the act. + +## Freeze sweep — 2026-08-30, PASSED + +Full matrix, 66 cells at 10k schedules each (tools/sweep.sh, one +build: the G9-CAL-1 Sched + cell-27 expectation fix + G8b test +split): 66/66 match declared verdicts, zero mismatches. Every kill +fired on its designated monitor (P1-LEGALITY, P-GEN, P-ADOPT, +P-MARK, P4-STUCK, P5-UNDER/OVER, P6-G/E/S, SEAL-EXPECT, EXEC-BOUND, +REDO-PROBE, PURGE-PROBE, POISON-PROBE, DEAD-DISPATCH); every honest +leg is clean. Three cells carry the pinned `--sch-feedbackpct=20` +strategy (tcG1d_P6G, tcG2awWA_E, tcG8dMut_PMARK — deep-crash-window +reach, G1D-REACH/G8D-CAL). The graph model is CALIBRATED and FROZEN. + +COVERAGE LIMIT of the frozen matrix (registered post-freeze; no cell +or expectation changed): eight of Monitors.p's 25 alarm strings fire +in no red cell — P1-CONTENT, P1-ATTEST-PUBLISH, P1-ATTEST-SEAL, +P1-ATTEST-EMPTY, P2-CONSULT, P2-STALENESS, P3'-COHERENCE, +PASS-BUDGET. They are the walker-inherited content/attestation/ +staleness oracles restated over the graph store; the matrix's kills +deliberately target the graph's OWN mechanisms, and the walker leg +witnesses the same property statements with its own reds — but +against the walker's checking code. By this log's own doctrine those +eight clauses are asserted, not calibrated, in THIS model: a +green matrix says nothing about them. Inventory mirrored in +REPORT.md's standing limits so the 66-cell green is not over-read. + +## Bake-off phase — declarations (GS-CO-005, registered BEFORE run) + +The protocol, the restated decision rule, and the provenance repair +(the v2 §10.6/§10.7 text was never committed) are in the spec's §12 +GS-CO-005 entry. Cells and declared verdicts: + +| cell | config | property | declared | +|---|---|---|---| +| tcG6aE_Ctl | cell 26 chain, E, NO crash, execBound 1 | honest stack + EXEC-BOUND | GREEN (zero-crash redo floor is zero) | +| tcG6aS_Ctl | same, S (+GPASS) | honest stack + EXEC-BOUND | GREEN | +| tcG6cE_Ctl | cell 28 fan-in, E, NO crash, execBound 1 | honest stack + EXEC-BOUND | GREEN | +| tcG6cS_Ctl | same, S (+GPASS) | honest stack + EXEC-BOUND | GREEN | +| tcG6bE_Redo | G6b mutation chassis, E, execBound 1 | EXEC-BOUND | RED (mutation-chassis redo exists) | +| tcG6bS_Redo | same, S | EXEC-BOUND | RED | +| tcG5dE_W1 | G5a shrink chassis, E, target {0→2} | SEAL-WORLD | RED (world reachable) | +| tcG5dS_W1 | same, S | SEAL-WORLD | RED | +| tcG5dE_W2 | target {0→1, 1→1} | SEAL-WORLD | RED (completed-across-crash world) | +| tcG5dS_W2 | same, S | SEAL-WORLD | RED | +| tcG5dE_W3 | target {0→2, 1→1} | SEAL-WORLD | GREEN (sweep-failure world unreachable) | +| tcG5dS_W3 | same, S | SEAL-WORLD | GREEN | + +Any variant-asymmetric outcome (a leg deviating from its declared +verdict under exactly one variant) is a divergence finding and +blocks Axis-3 citation until dispositioned (GS-CO-005(d)). + +### Bake-off results — 2026-08-30, 12/12 match declarations + +10k schedules per cell, uniform random except `tcG5dS_W2`, which +carries `--sch-feedbackpct=20` in the script (summary archived at +`PCheckerOutput/bakeoff/summary.txt`; reproduce with +`tools/bakeoff.sh`, which carries exactly these 12 cells — they are +deliberately NOT in `tools/sweep.sh`, so re-running the calibration +sweep cannot overwrite this phase's evidence and vice versa). Every +red records its FIRING MONITOR as an alarm tag, the same audit the +sweep applies: `[EXEC-BOUND]` on the two G6b probes, `[SEAL-WORLD]` +on the four reachable-world probes. The tag is what makes an +expected-RED cell auditable — counterexample presence alone matches +`expected=RED` even for a cell that redded on a deadlock instead of +its calibrated monitor — and in the bake-off it is ENFORCED, not just +recorded: each red cell declares its calibrated monitor in the script +and a red whose tag lacks it is a MISMATCH (sound here because every +bake-off red has exactly one pre-registered monitor; sweep cells can +legitimately red on more than one calibrated shape, so their tags +stay informational with CALIBRATION.md as the comparison surface). +All four v1 controls GREEN +(the zero-crash redo floor is bound 1 under BOTH variants — the +count oracle measures crash-caused redo, not variant overhead); both +G6b bound-1 probes RED on EXEC-BOUND (the mutation chassis's redo is +real and symmetric); all four reachable-world probes RED on +SEAL-WORLD and both W3 probes GREEN under both variants. + +G5d WHICH-IS-RIGHT DETERMINATION: the reachable seal-world sets are +IDENTICAL across variants — {W1, W2} reachable, W3 unreachable — +and every reachable world lies inside the sync-scoped SealExpect +envelope (asserted by the honest G5a greens). No divergence finding; +Axis-3 citation is unblocked. Where the two variants differ is HOW +a world is reached (E purges C / S refuses C's dispatch on the W1 +path), never WHICH worlds are sealable. + +`tcG5dS_W2` STRATEGY PIN (2026-08-31): the cell's find is +seed-BIMODAL under uniform random — one full-10k seed explored 18 +timelines and found nothing while five other seeds (three uniform, +two feedback) all found within ~500 schedules at a 0.23% +buggy-schedule rate, a miss that is ~e^-23 improbable if schedules +were independent draws. Some seeds evidently cannot reach the target +at all, so the cell carries `--sch-feedbackpct=20` in `bakeoff.sh` — +the same remedy the sweep gives its narrow cells (`tcG2awWA_E`, +`tcG1d_P6G`) and the same lesson BAKEOFF.md's methodology note 4 +records: narrow-target kills must not rely on uniform random +schedules. The world's REACHABILITY is unaffected (found under both +strategies); only the gate's reliability needed the pin. + +## Pending +- Nothing. The verdict document is `BAKEOFF.md` (assembled under + GS-CO-005's registered decision rule). diff --git a/formal/graph/PCheckerOutput/bakeoff/summary.txt b/formal/graph/PCheckerOutput/bakeoff/summary.txt new file mode 100644 index 000000000..21e491ad6 --- /dev/null +++ b/formal/graph/PCheckerOutput/bakeoff/summary.txt @@ -0,0 +1,13 @@ +tcG6aE_Ctl expected=GREEN observed=GREEN ok +tcG6aS_Ctl expected=GREEN observed=GREEN ok +tcG6cE_Ctl expected=GREEN observed=GREEN ok +tcG6cS_Ctl expected=GREEN observed=GREEN ok +tcG6bE_Redo expected=RED observed=RED ok [EXEC-BOUND] +tcG6bS_Redo expected=RED observed=RED ok [EXEC-BOUND] +tcG5dE_W1 expected=RED observed=RED ok [SEAL-WORLD] +tcG5dS_W1 expected=RED observed=RED ok [SEAL-WORLD] +tcG5dE_W2 expected=RED observed=RED ok [SEAL-WORLD] +tcG5dS_W2 expected=RED observed=RED ok [SEAL-WORLD] +tcG5dE_W3 expected=GREEN observed=GREEN ok +tcG5dS_W3 expected=GREEN observed=GREEN ok +BAKEOFF-DONE cells=12 mismatches=0 diff --git a/formal/graph/PCheckerOutput/sweep/summary.txt b/formal/graph/PCheckerOutput/sweep/summary.txt new file mode 100644 index 000000000..04a1779a5 --- /dev/null +++ b/formal/graph/PCheckerOutput/sweep/summary.txt @@ -0,0 +1,67 @@ +tcG1i_All expected=GREEN observed=GREEN ok +tcG1ii_All expected=GREEN observed=GREEN ok +tcG1iii_E expected=GREEN observed=GREEN ok +tcG1iii_S expected=GREEN observed=GREEN ok +tcG1b_All expected=GREEN observed=GREEN ok +tcG1cMut_P3 expected=GREEN observed=GREEN ok +tcG1cMut_Seal expected=GREEN observed=GREEN ok +tcG1sup_P1 expected=RED observed=RED ok [P1-LEGALITY] +tcG1bMut_PGEN expected=RED observed=RED ok [P-GEN] +tcG1cMut_Adopt expected=RED observed=RED ok [P-ADOPT] +tcG2ea_Core expected=GREEN observed=GREEN ok +tcG2eb_All expected=GREEN observed=GREEN ok +tcG2s_All expected=GREEN observed=GREEN ok +tcG2awE_All expected=GREEN observed=GREEN ok +tcG2awS_All expected=GREEN observed=GREEN ok +tcG2eb2c_All expected=GREEN observed=GREEN ok +tcG2fbE_All expected=GREEN observed=GREEN ok +tcG2fbS_All expected=GREEN observed=GREEN ok +tcG2ea_P6G expected=RED observed=RED ok [P6-G] +tcG2ebRetrOff_P6G expected=RED observed=RED ok [P6-G] +tcG2sStampOff_P6G expected=RED observed=RED ok [P6-G] +tcG2awE_Redo expected=RED observed=RED ok [REDO-PROBE] +tcG2awS_Redo expected=RED observed=RED ok [REDO-PROBE] +tcG2fbE_Redo expected=RED observed=RED ok [REDO-PROBE] +tcG2ebPend_Redo expected=RED observed=RED ok [REDO-PROBE] +tcG2awWA_E expected=RED observed=RED ok [liveness,P6-E] +tcG2awWA_S expected=RED observed=RED ok [P6-S] +tcG1dProbe_Redo expected=RED observed=RED ok [REDO-PROBE] +tcG1d_P6G expected=RED observed=RED ok [P6-G] +tcG1e_PGEN expected=RED observed=RED ok [P-GEN] +tcG5aE_All expected=GREEN observed=GREEN ok +tcG5aS_All expected=GREEN observed=GREEN ok +tcG5f_All expected=GREEN observed=GREEN ok +tcG5bE_P5 expected=RED observed=RED ok [P5-UNDER] +tcG5bS_P5 expected=RED observed=RED ok [P5-UNDER] +tcG5c_P5 expected=RED observed=RED ok [P5-OVER] +tcG5e_Probe expected=RED observed=RED ok [PURGE-PROBE] +tcG5e_PurgeOff expected=RED observed=RED ok [DEAD-DISPATCH] +tcG5f_Drop expected=RED observed=RED ok [SEAL-EXPECT] +tcG3_E expected=GREEN observed=GREEN ok +tcG3_S expected=GREEN observed=GREEN ok +tcG6aE_All expected=GREEN observed=GREEN ok +tcG6aS_All expected=GREEN observed=GREEN ok +tcG6bE_All expected=GREEN observed=GREEN ok +tcG6bS_All expected=GREEN observed=GREEN ok +tcG6cE_All expected=GREEN observed=GREEN ok +tcG6cS_All expected=GREEN observed=GREEN ok +tcG6aE_Redo expected=RED observed=RED ok [EXEC-BOUND] +tcG6aS_Redo expected=RED observed=RED ok [EXEC-BOUND] +tcG6cE_Redo expected=RED observed=RED ok [EXEC-BOUND] +tcG6cS_Redo expected=RED observed=RED ok [EXEC-BOUND] +tcG7_Ladder expected=GREEN observed=GREEN ok +tcG7_Stuck expected=RED observed=RED ok [P4-STUCK] +tcG8b_All expected=GREEN observed=GREEN ok +tcG8bMut_Ctl expected=GREEN observed=GREEN ok +tcG8bMut_P1 expected=RED observed=RED ok [P1-LEGALITY] +tcG8c_All expected=GREEN observed=GREEN ok +tcG8c_Poison expected=RED observed=RED ok [POISON-PROBE] +tcG8d_All expected=GREEN observed=GREEN ok +tcG8dMut_PMARK expected=RED observed=RED ok [P-MARK] +tcG9s_All expected=GREEN observed=GREEN ok +tcG9awS_All expected=GREEN observed=GREEN ok +tcG9G5aS_All expected=GREEN observed=GREEN ok +tcG9cBase_All expected=GREEN observed=GREEN ok +tcG9c_All expected=GREEN observed=GREEN ok +tcG9c_Redo expected=RED observed=RED ok [EXEC-BOUND] +SWEEP-DONE cells=66 mismatches=0 diff --git a/formal/graph/PSpec/Monitors.p b/formal/graph/PSpec/Monitors.p new file mode 100644 index 000000000..a1b55fd6e --- /dev/null +++ b/formal/graph/PSpec/Monitors.p @@ -0,0 +1,764 @@ +/* Property monitors (SPEC 7). Announce-subscribed; ghost fields are + labels of decisions the model already made. Fold and legality + implement the walker round-5 F1 pin with SPEC 4d generation + grounding: a round completes at the commit of its last prescribed + op; a generation's COMPLETE round stays in fold and count until + ADOPTED (contribution transfers) or SUPERSEDED, and the + supersession removal is DEATH-GATED (a live-rows removal reading + would dissolve the walker cell-4 alarm — the suppressionOff racing + red lives on exactly this gate). Poisoned keys are legality- and + content-EXEMPT (the poison is the alarm; the scope is + seal-excluded by SealExpect). */ + +type tGRound = (key: int, verdict: tGVerdict, consultEpoch: int, vBase: int, hasCopy: bool, completed: bool, node: int, gen: int); + +// Partition key content as id -> epoch (ghost content tag). +fun contentOfG(part: tGPart, key: int): map[int, int] { + var out: map[int, int]; + var ids: seq[int]; + var i: int; + if (!(key in part)) { return out; } + ids = keys(part[key]); + i = 0; + while (i < sizeof(ids)) { + out[ids[i]] = part[key][ids[i]].epoch; + i = i + 1; + } + return out; +} + +spec GP1 observes eAnnSyncStart, eAnnClear, eAnnReplayCopy, eAnnUpsert, eAnnTombstones, eAnnPublish, eAnnMarkerPut, eAnnAdopt, eAnnGenBump, eAnnPoison, eAnnGSeal { + var rounds: map[int, tGRound]; + var foldEpoch: map[int, int]; // key -> folded content epoch + var copyRounds: map[int, seq[int]]; // key -> LIVE copy-contributing roundIds + var latestGen: map[int, int]; // node -> latest generation seen + var poisonedK: map[int, bool]; + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + rounds = default(map[int, tGRound]); + foldEpoch = default(map[int, int]); + copyRounds = default(map[int, seq[int]]); + latestGen = default(map[int, int]); + poisonedK = default(map[int, bool]); + } + on eAnnGenBump do (p: (syncN: int, node: int, newGen: int, reason: int)) { + bumpLatest(p.node, p.newGen); + } + on eAnnPoison do (p: (syncN: int, key: int)) { + poisonedK[p.key] = true; + } + on eAnnMarkerPut do (p: (syncN: int, key: int, node: int, gen: int, roundId: int, pubBearing: bool, contentEpoch: int, ghost: tGGhost)) { + trackOp(p.key, p.ghost, false, -1); + } + on eAnnClear do (p: (syncN: int, key: int, ghost: tGGhost)) { + var live: seq[int]; + var i: int; + var r: tGRound; + trackOp(p.key, p.ghost, false, -1); + // SPEC 4d supersession removal, DEATH-GATED: the clear + // removes the copy contribution of DEAD complete rounds + // only; a live round's contribution stays (and a second + // completed copy then trips legality). + if (p.key in copyRounds) { + i = 0; + while (i < sizeof(copyRounds[p.key])) { + r = rounds[copyRounds[p.key][i]]; + if (r.node in latestGen && r.gen < latestGen[r.node]) { + live = copyRounds[p.key]; + live -= i; + copyRounds[p.key] = live; + } else { + i = i + 1; + } + } + } + } + on eAnnReplayCopy do (p: (syncN: int, key: int, vBase: int, rows: seq[tGRow], ghost: tGGhost)) { + trackOp(p.key, p.ghost, true, p.vBase); + } + on eAnnUpsert do (p: (syncN: int, key: int, rows: seq[tGRow], ghost: tGGhost)) { + trackOp(p.key, p.ghost, false, -1); + } + on eAnnTombstones do (p: (syncN: int, key: int, removes: seq[int], ghost: tGGhost)) { + trackOp(p.key, p.ghost, false, -1); + } + on eAnnPublish do (p: (syncN: int, key: int, v: int, ghost: tGGhost)) { + // Attestation-only at publish (walker parity): truthful + // validators are epoch-valued. + assert p.v == p.ghost.consultEpoch, "P1-ATTEST-PUBLISH: published validator epoch differs from the round's verdict epoch"; + trackOp(p.key, p.ghost, false, -1); + } + on eAnnAdopt do (p: (syncN: int, key: int, node: int, fromGen: int, toGen: int, adoptedRoundId: int, rows: seq[tGRow], ghost: tGGhost)) { + var r: tGRound; + bumpLatest(p.ghost.node, p.ghost.gen); + // Contribution TRANSFER (SPEC 4a/4d): the adopted round is + // re-grounded at the adopting generation; fold and count + // are unchanged. + if (p.adoptedRoundId in rounds) { + r = rounds[p.adoptedRoundId]; + r.gen = p.toGen; + rounds[p.adoptedRoundId] = r; + } + } + on eAnnGSeal do (p: (syncN: int, partition: tGPart, manifest: map[int, int], stamps: map[int, map[int, int]], genTable: map[int, int])) { + var ks: seq[int]; + var i: int; + var k: int; + var have: map[int, int]; + ks = keys(p.partition); + i = 0; + while (i < sizeof(ks)) { + k = ks[i]; + if (!(k in poisonedK) && sizeof(p.partition[k]) > 0) { + have = contentOfG(p.partition, k); + if (k in foldEpoch) { + assert have == rowsAt(foldEpoch[k]), "P1-CONTENT: sealed partition diverges from the round-log fold"; + if (k in p.manifest) { + assert p.manifest[k] == foldEpoch[k], "P1-ATTEST-SEAL: manifest entry epoch differs from the fold epoch"; + } + } else { + assert false, "P1-CONTENT: incomplete-round debris sealed"; + } + } + i = i + 1; + } + // A manifest entry over an empty fold attests a + // composition the round log does not contain (walker + // round-7 F3 pin). + ks = keys(p.manifest); + i = 0; + while (i < sizeof(ks)) { + if (!(ks[i] in poisonedK)) { + assert ks[i] in foldEpoch, "P1-ATTEST-EMPTY: manifest entry published over an empty fold"; + } + i = i + 1; + } + } + } + + fun bumpLatest(node: int, gen: int) { + if (!(node in latestGen) || gen > latestGen[node]) { + latestGen[node] = gen; + } + } + + fun trackOp(key: int, ghost: tGGhost, isCopy: bool, vBase: int) { + var info: tGRound; + var cr: seq[int]; + bumpLatest(ghost.node, ghost.gen); + if (ghost.roundId in rounds) { + info = rounds[ghost.roundId]; + } else { + info = (key = key, verdict = ghost.verdict, consultEpoch = ghost.consultEpoch, vBase = -1, hasCopy = false, completed = false, node = ghost.node, gen = ghost.gen); + } + info.consultEpoch = ghost.consultEpoch; + if (isCopy) { + info.hasCopy = true; + info.vBase = vBase; + } + if (ghost.lastOp) { + info.completed = true; + } + rounds[ghost.roundId] = info; + if (ghost.lastOp) { + // Round completion IS the fold order. Replacement counting + // happens HERE: committed copies within COMPLETE rounds + // only (walker round-7 F2 pin), over LIVE contributions + // only (SPEC 4d grounding). + if (info.hasCopy) { + if (key in copyRounds) { cr = copyRounds[key]; } + cr += (sizeof(cr), ghost.roundId); + copyRounds[key] = cr; + if (!(key in poisonedK)) { + assert sizeof(copyRounds[key]) <= 1, "P1-LEGALITY: second live complete-round replacement copy for one key in one sync"; + } + } + if (ghost.verdict == GV_REPLAY) { + if (info.hasCopy) { + foldEpoch[key] = info.vBase; + } + } else if (ghost.verdict != GV_ADOPT) { + foldEpoch[key] = ghost.consultEpoch; + } + } + } +} + +spec GP2 observes eAnnScenarioInit, eAnnSyncStart, eAnnConsult, eAnnPoison, eAnnGSeal { + var bound: int; + var consulted: map[int, bool]; + var poisonedK: map[int, bool]; + + start state Monitoring { + on eAnnScenarioInit do (p: (maxStaleness: int)) { + bound = p.maxStaleness; + } + on eAnnSyncStart do (p: (syncN: int)) { + consulted = default(map[int, bool]); + poisonedK = default(map[int, bool]); + } + on eAnnPoison do (p: (syncN: int, key: int)) { + poisonedK[p.key] = true; + } + on eAnnConsult do (p: (syncN: int, key: int, hit: bool, v: int, validated: bool, epoch: int, freshFetch: bool, diffVerdict: bool, attempt: int, node: int, gen: int)) { + // Consulted-against-upstream (walker pin): validation + // match, fresh fetch, or CHANGED-WITH-DIFF verdict. The + // adopting MATCH qualifies (MATCH-only eligibility makes + // the freshness claim true, SPEC 4a). + if (p.validated || p.freshFetch || p.diffVerdict) { + consulted[p.key] = true; + } + } + on eAnnGSeal do (p: (syncN: int, partition: tGPart, manifest: map[int, int], stamps: map[int, map[int, int]], genTable: map[int, int])) { + var ks: seq[int]; + var ids: seq[int]; + var i: int; + var j: int; + var k: int; + ks = keys(p.partition); + i = 0; + while (i < sizeof(ks)) { + k = ks[i]; + if (!(k in poisonedK) && sizeof(p.partition[k]) > 0) { + assert k in consulted, "P2-CONSULT: sealed key not consulted against upstream this sync"; + ids = keys(p.partition[k]); + j = 0; + while (j < sizeof(ids)) { + assert p.partition[k][ids[j]].hops <= bound, "P2-STALENESS: row replay-travel exceeds the scenario bound"; + j = j + 1; + } + } + i = i + 1; + } + } + } +} + +spec GP3prime observes eAnnSyncStart, eAnnConsult, eAnnPoison, eAnnGSeal { + var lastEpoch: map[int, int]; // key -> epoch of last qualifying consult + var poisonedK: map[int, bool]; + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + lastEpoch = default(map[int, int]); + poisonedK = default(map[int, bool]); + } + on eAnnPoison do (p: (syncN: int, key: int)) { + poisonedK[p.key] = true; + } + on eAnnConsult do (p: (syncN: int, key: int, hit: bool, v: int, validated: bool, epoch: int, freshFetch: bool, diffVerdict: bool, attempt: int, node: int, gen: int)) { + if (p.validated || p.freshFetch || p.diffVerdict) { + lastEpoch[p.key] = p.epoch; + } + } + on eAnnGSeal do (p: (syncN: int, partition: tGPart, manifest: map[int, int], stamps: map[int, map[int, int]], genTable: map[int, int])) { + var ks: seq[int]; + var i: int; + var k: int; + ks = keys(p.partition); + i = 0; + while (i < sizeof(ks)) { + k = ks[i]; + if (k in lastEpoch && !(k in poisonedK) && sizeof(p.partition[k]) > 0) { + assert contentOfG(p.partition, k) == rowsAt(lastEpoch[k]), "P3'-COHERENCE: sealed content epoch differs from last consulted verdict epoch"; + } + i = i + 1; + } + } + } +} + +// P-GEN (G-RULE-3 / R2-N3): no two attempts contain store-commit +// announces attributed to the same (node, generation); adoption +// re-announces attribute to the ADOPTER. Scoped per sync (the +// generation table is per-sync scheduling state). +spec PGEN observes eAnnSyncStart, eAnnClear, eAnnReplayCopy, eAnnUpsert, eAnnTombstones, eAnnPublish, eAnnMarkerPut, eAnnAdopt, eAnnSessionSet { + var attemptOf: map[int, int]; // node*1000 + gen -> attempt + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + attemptOf = default(map[int, int]); + } + on eAnnClear do (p: (syncN: int, key: int, ghost: tGGhost)) { check(p.ghost); } + on eAnnReplayCopy do (p: (syncN: int, key: int, vBase: int, rows: seq[tGRow], ghost: tGGhost)) { check(p.ghost); } + on eAnnUpsert do (p: (syncN: int, key: int, rows: seq[tGRow], ghost: tGGhost)) { check(p.ghost); } + on eAnnTombstones do (p: (syncN: int, key: int, removes: seq[int], ghost: tGGhost)) { check(p.ghost); } + on eAnnPublish do (p: (syncN: int, key: int, v: int, ghost: tGGhost)) { check(p.ghost); } + on eAnnMarkerPut do (p: (syncN: int, key: int, node: int, gen: int, roundId: int, pubBearing: bool, contentEpoch: int, ghost: tGGhost)) { check(p.ghost); } + on eAnnAdopt do (p: (syncN: int, key: int, node: int, fromGen: int, toGen: int, adoptedRoundId: int, rows: seq[tGRow], ghost: tGGhost)) { check(p.ghost); } + on eAnnSessionSet do (p: (syncN: int, skey: int, val: int, writer: int, wgen: int, ghost: tGGhost)) { check(p.ghost); } + } + + fun check(g: tGGhost) { + var gk: int; + gk = g.node * 1000 + g.gen; + if (gk in attemptOf) { + assert attemptOf[gk] == g.attempt, "P-GEN: store-commit announces attributed to one (node, generation) in two attempts (identity reuse)"; + } else { + attemptOf[gk] = g.attempt; + } + } +} + +// P-MARK (R2-F4): a marker present for a key ⟹ the key's partition +// equals the marked round's committed outputs. Checkable form: no +// FOREIGN store op mutates a marked key's partition — every legal +// transition rides an op that first rebinds or removes the marker +// (unit put overwrites first; REPLACES clear deletes first; poison +// voids; adoption rebinds). +spec PMARK observes eAnnSyncStart, eAnnMarkerPut, eAnnMarkerDel, eAnnAdopt, eAnnPoison, eAnnClear, eAnnReplayCopy, eAnnUpsert, eAnnTombstones { + var markedRound: map[int, int]; // key -> marking roundId + var voidedK: map[int, bool]; + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + markedRound = default(map[int, int]); + voidedK = default(map[int, bool]); + } + on eAnnMarkerPut do (p: (syncN: int, key: int, node: int, gen: int, roundId: int, pubBearing: bool, contentEpoch: int, ghost: tGGhost)) { + markedRound[p.key] = p.roundId; + if (p.key in voidedK) { voidedK -= p.key; } + } + on eAnnMarkerDel do (p: (syncN: int, key: int)) { + if (p.key in markedRound) { markedRound -= p.key; } + } + on eAnnAdopt do (p: (syncN: int, key: int, node: int, fromGen: int, toGen: int, adoptedRoundId: int, rows: seq[tGRow], ghost: tGGhost)) { + markedRound[p.key] = p.ghost.roundId; + } + on eAnnPoison do (p: (syncN: int, key: int)) { + voidedK[p.key] = true; + } + on eAnnClear do (p: (syncN: int, key: int, ghost: tGGhost)) { check(p.key, p.ghost); } + on eAnnReplayCopy do (p: (syncN: int, key: int, vBase: int, rows: seq[tGRow], ghost: tGGhost)) { check(p.key, p.ghost); } + on eAnnUpsert do (p: (syncN: int, key: int, rows: seq[tGRow], ghost: tGGhost)) { check(p.key, p.ghost); } + on eAnnTombstones do (p: (syncN: int, key: int, removes: seq[int], ghost: tGGhost)) { check(p.key, p.ghost); } + } + + fun check(key: int, g: tGGhost) { + if (key in markedRound && !(key in voidedK)) { + assert markedRound[key] == g.roundId, "P-MARK: a foreign round mutated a marked key's partition (marker no longer describes the partition)"; + } + } +} + +// SealExpect: the scripted closure + content oracle (SPEC 7 closure +// oracle / SPEC 9 SealExpect discipline). The env announces the +// expected sealed key set and per-key LIVE content epoch at EVERY +// attempt start; the monitor accumulates the sync's acceptable +// epoch SET per key. Sealed content must match SOME attempt-start +// epoch of this sync — the artifact freshness contract is +// SYNC-scoped (staleness bound 1), and a key whose derivation +// completed and checkpointed before a crash legitimately seals the +// earlier attempt's world (completed-across-crash, G-RULE-2; +// calibration find G1-CAL-1). Closure is exact both directions. +// Poisoned/excluded keys are exempt. +spec SealExpectG observes eAnnSyncStart, eAnnExpectSeal, eAnnPoison, eAnnGSeal { + var active: bool; + var expSync: int; + var acc: map[int, seq[int]]; + var excluded: map[int, bool]; + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + if (active && p.syncN != expSync) { active = false; } + } + on eAnnExpectSeal do (p: (syncN: int, exp: map[int, int], excluded: map[int, bool])) { + var ks: seq[int]; + var es: seq[int]; + var i: int; + if (!active || expSync != p.syncN) { + acc = default(map[int, seq[int]]); + excluded = default(map[int, bool]); + } + active = true; + expSync = p.syncN; + ks = keys(p.exp); + i = 0; + while (i < sizeof(ks)) { + es = default(seq[int]); + if (ks[i] in acc) { es = acc[ks[i]]; } + if (!inIntSeq(es, p.exp[ks[i]])) { + es += (sizeof(es), p.exp[ks[i]]); + } + acc[ks[i]] = es; + i = i + 1; + } + ks = keys(p.excluded); + i = 0; + while (i < sizeof(ks)) { + excluded[ks[i]] = true; + i = i + 1; + } + } + on eAnnPoison do (p: (syncN: int, key: int)) { + excluded[p.key] = true; + } + on eAnnGSeal do (p: (syncN: int, partition: tGPart, manifest: map[int, int], stamps: map[int, map[int, int]], genTable: map[int, int])) { + var ks: seq[int]; + var i: int; + var j: int; + var k: int; + var anyMatch: bool; + var have: map[int, int]; + if (!active || p.syncN != expSync) { return; } + ks = keys(acc); + i = 0; + while (i < sizeof(ks)) { + k = ks[i]; + if (!(k in excluded)) { + assert k in p.partition && sizeof(p.partition[k]) > 0, "SEAL-EXPECT: expected demand-closure key missing from the sealed artifact"; + have = contentOfG(p.partition, k); + anyMatch = false; + j = 0; + while (j < sizeof(acc[k])) { + if (have == rowsAt(acc[k][j])) { anyMatch = true; } + j = j + 1; + } + assert anyMatch, "SEAL-EXPECT: sealed content matches no attempt-start world of this sync"; + } + i = i + 1; + } + ks = keys(p.partition); + i = 0; + while (i < sizeof(ks)) { + k = ks[i]; + if (!(k in excluded) && sizeof(p.partition[k]) > 0) { + assert k in acc, "SEAL-EXPECT: sealed key outside the scripted demand closure"; + } + i = i + 1; + } + active = false; + } + } +} + +// P-ADOPT (adopt legality, R2-F2 MATCH-only eligibility): every +// adoption must be justified by a prior validated MATCH consult by +// the same (node, generation) — adoption and its consult always +// share one execution. A FAIL-consult followed by adoption is the +// G1c laundering mutant: it smuggles a stale world past the seal +// inside the sync-scoped freshness envelope, so no artifact-level +// oracle can see it; the mechanism monitor is the kill. +spec PADOPT observes eAnnSyncStart, eAnnConsult, eAnnAdopt { + var matched: map[int, bool]; // node*1000+gen -> validated MATCH seen + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + matched = default(map[int, bool]); + } + on eAnnConsult do (p: (syncN: int, key: int, hit: bool, v: int, validated: bool, epoch: int, freshFetch: bool, diffVerdict: bool, attempt: int, node: int, gen: int)) { + if (p.hit && p.validated) { + matched[p.node * 1000 + p.gen] = true; + } + } + on eAnnAdopt do (p: (syncN: int, key: int, node: int, fromGen: int, toGen: int, adoptedRoundId: int, rows: seq[tGRow], ghost: tGGhost)) { + assert (p.ghost.node * 1000 + p.ghost.gen) in matched, "P-ADOPT: adoption without a validated MATCH consult by the adopting generation (MATCH-only eligibility violated)"; + } + } +} + +// P6-G (laundering oracle, all legs; SPEC 7): at seal, every row +// derived THIS SYNC (hops 0) embedding a real session value must +// embed the FINAL session value — comparison by VALUE, so same-value +// re-derivation is green (R2-F6). Mechanism-independent. +spec GP6G observes eAnnSyncStart, eAnnSessionSet, eAnnGSeal { + var sess: map[int, int]; + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + sess = default(map[int, int]); + } + on eAnnSessionSet do (p: (syncN: int, skey: int, val: int, writer: int, wgen: int, ghost: tGGhost)) { + sess[p.skey] = p.val; + } + on eAnnGSeal do (p: (syncN: int, partition: tGPart, manifest: map[int, int], stamps: map[int, map[int, int]], genTable: map[int, int])) { + var ks: seq[int]; + var ids: seq[int]; + var i: int; + var j: int; + var k: int; + var r: tGRow; + ks = keys(p.partition); + i = 0; + while (i < sizeof(ks)) { + k = ks[i]; + ids = keys(p.partition[k]); + j = 0; + while (j < sizeof(ids)) { + r = p.partition[k][ids[j]]; + if (r.sVal >= 0 && r.hops == 0) { + assert 0 in sess && sess[0] == r.sVal, "P6-G: sealed row embeds a session value differing from the final derived value (laundered dead read)"; + } + j = j + 1; + } + i = i + 1; + } + } + } +} + +// P6-E (mechanism conformance, E+B; SPEC 7): retraction liveness — +// every reader execution of a value whose writer generation is dead +// at seal re-ran (a later read by the same reader exists). Asserted +// only in E+B cells. +spec GP6E observes eAnnSyncStart, eAnnSessionRead, eAnnGSeal { + var reads: seq[tReadRec]; + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + reads = default(seq[tReadRec]); + } + on eAnnSessionRead do (p: (syncN: int, reader: int, rgen: int, skey: int, found: bool, val: int, writer: int, wgen: int)) { + if (p.found) { + reads += (sizeof(reads), (reader = p.reader, rgen = p.rgen, skey = p.skey, val = p.val, writer = p.writer, wgen = p.wgen)); + } + } + on eAnnGSeal do (p: (syncN: int, partition: tGPart, manifest: map[int, int], stamps: map[int, map[int, int]], genTable: map[int, int])) { + var i: int; + var j: int; + var later: bool; + i = 0; + while (i < sizeof(reads)) { + if (reads[i].writer in p.genTable && reads[i].wgen < p.genTable[reads[i].writer]) { + later = false; + j = 0; + while (j < sizeof(reads)) { + if (reads[j].reader == reads[i].reader && reads[j].rgen > reads[i].rgen) { + later = true; + } + j = j + 1; + } + assert later, "P6-E: a reader execution of a dead session value never re-ran before seal (retraction liveness)"; + } + i = i + 1; + } + } + } +} + +// REDO-PROBE (existence probe, walker C1-probe pattern): asserts NO +// forced re-admission (retraction- or observation-forced) ever +// happens. Asserted in cells DECLARING an expected forced-redo count +// >= 1; RED is the pass verdict — the counterexample is the exhibit +// that the mechanism pays its at-least-once cost (R2-F6: the count +// is a metric, never a property; this probe is how a metric claim +// becomes checkable without asserting it on every schedule). +spec REDOPROBE observes eAnnReadmit { + start state Monitoring { + on eAnnReadmit do (p: (syncN: int, node: int, hash: int, gen: int, reason: int)) { + assert p.reason == 1, "REDO-PROBE: a forced redo occurred (existence exhibit, not a failure)"; + } + } +} + +// P5 (artifact demand-closure, both directions, at seal — the G5 +// family's subject). Self-contained over the sealed artifact itself, +// no environment counterfactual (which attempt's parent world +// survives a crash is schedule-dependent under sync-scoped +// freshness): +// CLOSED (P5-UNDER kill): every non-root sealed key is named by +// some sealed row's childHash — under-sweep debris has no living +// namer. +// COMPLETE (P5-OVER / demand-drop kill): every childHash named by +// a sealed row has a non-empty sealed partition — an overreaching +// sweep or a dropped admission strands a named child. +// Identity conventions: hash = node id + 1, key = node id. +spec GP5 observes eAnnGSeal { + start state Monitoring { + on eAnnGSeal do (p: (syncN: int, partition: tGPart, manifest: map[int, int], stamps: map[int, map[int, int]], genTable: map[int, int])) { + var named: map[int, bool]; + var ks: seq[int]; + var ids: seq[int]; + var r: tGRow; + var i: int; + var j: int; + ks = keys(p.partition); + i = 0; + while (i < sizeof(ks)) { + ids = keys(p.partition[ks[i]]); + j = 0; + while (j < sizeof(ids)) { + r = p.partition[ks[i]][ids[j]]; + if (r.childHash > 0) { named[r.childHash] = true; } + j = j + 1; + } + i = i + 1; + } + i = 0; + while (i < sizeof(ks)) { + if (ks[i] != 0 && sizeof(p.partition[ks[i]]) > 0) { + assert (ks[i] + 1) in named, "P5-UNDER: sealed artifact contains a partition no sealed row names (un-swept debris)"; + } + i = i + 1; + } + ks = keys(named); + i = 0; + while (i < sizeof(ks)) { + assert (ks[i] - 1) in p.partition && sizeof(p.partition[ks[i] - 1]) > 0, "P5-OVER: a sealed row names a child whose partition is missing (overreach or dropped demand)"; + i = i + 1; + } + } + } +} + +// PURGE-PROBE (existence probe, REDO-PROBE pattern): asserts the +// resume-time ∀-purge NEVER fires; RED is the pass verdict — the +// counterexample exhibits the mid-round checkpoint window +// (C-pending & parent-pending) that only per-announce demand timing +// (G-RULE-1 TIMING PIN) makes reachable. +spec PURGEPROBE observes eAnnPurge { + start state Monitoring { + on eAnnPurge do (p: (syncN: int, node: int, hash: int)) { + assert false, "PURGE-PROBE: a resume purge occurred (existence exhibit, not a failure)"; + } + } +} + +// DEAD-DISPATCH (the G5e count oracle in checkable form): no node is +// EVER dispatched while every admitted-by edge names a dead +// generation. Honest mechanisms make this unreachable (E purges at +// resume, S refuses at dispatch); purgeOff executes dead demand. +spec GDEADDISPATCH observes eAnnDeadDispatch { + start state Monitoring { + on eAnnDeadDispatch do (p: (syncN: int, node: int, hash: int)) { + assert false, "DEAD-DISPATCH: a dead-demand node was dispatched (purge/refusal failed)"; + } + } +} + +// PASS-BUDGET (§10.8): the pre-seal pass converges within its budget +// in every honest S cell — a budget-exhausted honest seal is a +// finding, not noise. Kill cells that RELY on exhaustion (writerAdopt +// S) do not assert it. +spec GPASS observes eAnnBudgetExhausted { + start state Monitoring { + on eAnnBudgetExhausted do (p: (syncN: int)) { + assert false, "PASS-BUDGET: the pre-seal pass exhausted its budget on an honest history"; + } + } +} + +// EXEC-BOUND (G6 bake-off count oracle): executions per node per +// sync never exceed the cell's declared bound (announced by the env +// from cfg at scenario init; 0 = unmonitored). The minimal GREEN +// bound is the checker-verified worst-case count for the leg; the +// bound-minus-one RED probe is the existence exhibit that the redo +// is real (adequacy §10.1: count-oracle kills). +spec GEXECBOUND observes eAnnScenarioInit, eAnnExecBound, eAnnSyncStart, eAnnExec { + var bound: int; + var counts: map[int, int]; + + start state Monitoring { + on eAnnScenarioInit do (p: (maxStaleness: int)) { bound = 0; } + on eAnnExecBound do (p: (bound: int)) { bound = p.bound; } + on eAnnSyncStart do (p: (syncN: int)) { counts = default(map[int, int]); } + on eAnnExec do (p: (syncN: int, attempt: int, node: int, gen: int)) { + if (bound <= 0) { return; } + if (p.node in counts) { + counts[p.node] = counts[p.node] + 1; + } else { + counts[p.node] = 1; + } + assert counts[p.node] <= bound, "EXEC-BOUND: a node exceeded the cell's per-sync execution bound"; + } + } +} + +// POISON-PROBE (existence probe, REDO-PROBE pattern): asserts the +// same-key distinct-derivation poison NEVER fires; RED is the pass +// verdict on the G8c chassis (both derivations always commit). +spec POISONPROBE observes eAnnPoison { + start state Monitoring { + on eAnnPoison do (p: (syncN: int, key: int)) { + assert false, "POISON-PROBE: the same-key poison fired (existence exhibit, not a failure)"; + } + } +} + +// P4-STUCK (G7, walker P4 analog): three consecutive attempt +// failures with an identical generation-blind fingerprint and no +// abandon is a stuck sync (attempt budget 3, SPEC 8). The abandon +// ladder gives up after 2, so the honest leg never reaches 3. +spec GP4STUCK observes eAnnSyncStart, eAnnAttemptFail { + var lastFp: int; + var streak: int; + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { lastFp = -1; streak = 0; } + on eAnnAttemptFail do (p: (syncN: int, attempt: int, node: int, fingerprint: int)) { + if (p.fingerprint == lastFp) { + streak = streak + 1; + } else { + lastFp = p.fingerprint; + streak = 1; + } + assert streak < 3, "P4-STUCK: three identical-fingerprint attempt failures without an abandon"; + } + } +} + +// P6-S (mechanism conformance, S; SPEC 7, at-seal form only, R2-F6): +// no sealed output carries a dead-generation stamp. Red MEANS the +// mechanism failed (a dead stamp survived the pre-seal pass). +// Asserted only in S cells. +spec GP6S observes eAnnGSeal { + start state Monitoring { + on eAnnGSeal do (p: (syncN: int, partition: tGPart, manifest: map[int, int], stamps: map[int, map[int, int]], genTable: map[int, int])) { + var ks: seq[int]; + var ns: seq[int]; + var i: int; + var j: int; + ks = keys(p.stamps); + i = 0; + while (i < sizeof(ks)) { + if (ks[i] in p.partition && sizeof(p.partition[ks[i]]) > 0) { + ns = keys(p.stamps[ks[i]]); + j = 0; + while (j < sizeof(ns)) { + assert !(ns[j] in p.genTable) || p.stamps[ks[i]][ns[j]] >= p.genTable[ns[j]], "P6-S: sealed output carries a dead-generation stamp (the pass failed or exhausted its budget)"; + j = j + 1; + } + } + i = i + 1; + } + } + } +} + +// SEAL-WORLD (GS-CO-005(d) existence probe, REDO-PROBE pattern): the +// G5d cross-variant seal-world meta-analysis. The env announces a +// target world (manifest restricted to keys sealing non-empty +// partitions) for the interrupted sync; the probe asserts that world +// is NEVER sealed — RED is the pass verdict on reachable-world +// probes, GREEN on the declared-unreachable sweep-failure world. +// Asserted alone in tcG5d* cells; the honest G5a greens carry the +// SealExpect envelope this probe's reachable worlds must sit inside. +spec GSEALWORLD observes eAnnSealWorld, eAnnGSeal { + var target: map[int, int]; + var targetSync: int; + var armed: bool; + + start state Monitoring { + on eAnnSealWorld do (p: (syncN: int, exp: map[int, int])) { + target = p.exp; + targetSync = p.syncN; + armed = true; + } + on eAnnGSeal do (p: (syncN: int, partition: tGPart, manifest: map[int, int], stamps: map[int, map[int, int]], genTable: map[int, int])) { + var world: map[int, int]; + var ks: seq[int]; + var i: int; + if (!armed || p.syncN != targetSync) { return; } + ks = keys(p.manifest); + i = 0; + while (i < sizeof(ks)) { + if (ks[i] in p.partition && sizeof(p.partition[ks[i]]) > 0) { + world[ks[i]] = p.manifest[ks[i]]; + } + i = i + 1; + } + assert !(world == target), "SEAL-WORLD: the target sealed world was reached"; + } + } +} diff --git a/formal/graph/PSrc/Env.p b/formal/graph/PSrc/Env.p new file mode 100644 index 000000000..263d92645 --- /dev/null +++ b/formal/graph/PSrc/Env.p @@ -0,0 +1,239 @@ +/* MGEnv: the per-scenario test driver (SPEC 3, walker parity). Owns + the sync chain, attempt lifecycle, crash scripting (armed + injection; timing genuinely explored), between-attempt/between-sync + upstream mutation, and the SealExpect counterfactual (the scripted + closure + content expectation, announced from live upstream state + at each attempt start — the env-side independent oracle base). */ + +machine MGEnv { + var store: machine; + var upstream: machine; + var cfg: tGCfg; + var crash1Used: bool; + var crash2Used: bool; + + start state Run { + entry (c: tGCfg) { + var syncN: int; + var attempt: int; + var agen: int; + var ck: tGCkpt; + var hasCk: bool; + var sched: machine; + var syncDone: bool; + var crashedThisAttempt: bool; + var sealedSeen: bool; + var failCount: int; + var attemptFailed: bool; + cfg = c; + announce eAnnScenarioInit, (maxStaleness = 1,); + if (cfg.execBound > 0) { + announce eAnnExecBound, (bound = cfg.execBound,); + } + if (sizeof(cfg.sealWorld) > 0) { + announce eAnnSealWorld, (syncN = cfg.interruptSync, exp = cfg.sealWorld); + } + store = new MGStore(); + upstream = new MGUpstream((flapBack = cfg.flapBack,)); + syncN = 1; + while (syncN <= cfg.nSyncs) { + send store, eGReset, (client = this, syncN = syncN); + receive { case eStoreAck: {} } + announce eAnnSyncStart, (syncN = syncN,); + attempt = 1; + syncDone = false; + failCount = 0; + while (!syncDone) { + assert attempt <= 3, "attempt budget exceeded (SPEC 8)"; + agen = syncN * 10 + attempt; + hasCk = false; + if (attempt > 1) { + send store, eGReadCkptReq, (client = this,); + receive { + case eGReadCkptResp: (r: (ck: tGCkpt, has: bool)) { + ck = r.ck; + hasCk = r.has; + } + } + } + announceExpectation(syncN); + // Arm-and-confirm BEFORE creating the attempt: an + // arm racing the attempt can land after the seal, + // never resolve, and deadlock this machine (the + // store's at-seal resolution point only sees arms + // queued ahead of the seal). + crashedThisAttempt = false; + if (syncN == cfg.interruptSync) { + if ((cfg.interrupt == 2 || cfg.interrupt == 3) && attempt == 1 && !crash1Used) { + crash1Used = true; + crashedThisAttempt = true; + } + if (cfg.interrupt == 3 && attempt == 2 && !crash2Used) { + crash2Used = true; + crashedThisAttempt = true; + } + if (crashedThisAttempt) { + send store, eCrashArm, (client = this, agen = agen); + receive { case eCrashArmed: {} } + } + } + sched = new MGraphSched((env = this, store = store, upstream = upstream, agen = agen, syncN = syncN, attempt = attempt, cfg = cfg, ck = ck, has = hasCk)); + if (crashedThisAttempt) { + receive { case eCrashAck: {} } + send store, eReadSealedReq, (client = this,); + sealedSeen = false; + receive { + case eReadSealedResp: (r: (sealed: bool)) { + sealedSeen = r.sealed; + } + } + if (sealedSeen) { + // The attempt sealed before the crash + // landed; consume its end report. + receive { case eGAttemptEnded: (r: (sealed: bool, failed: bool)) {} } + syncDone = true; + } else { + betweenAttempts(); + attempt = attempt + 1; + } + } else { + attemptFailed = false; + receive { + case eGAttemptEnded: (r: (sealed: bool, failed: bool)) { + sealedSeen = r.sealed; + attemptFailed = r.failed; + } + } + if (attemptFailed) { + // Loud failure (G7): the abandon ladder + // gives up after 2 identical fingerprints + // (the fail script is deterministic, so + // consecutive fingerprints are identical + // by construction); without it the retry + // loop re-fails until P4-STUCK fires. + failCount = failCount + 1; + if (cfg.ladder && failCount >= 2) { + announce eAnnAbandon, (syncN = syncN,); + syncDone = true; + } else { + betweenAttempts(); + attempt = attempt + 1; + } + } else if (cfg.interrupt == 1 && syncN == cfg.interruptSync && attempt == 1) { + // Scripted graceful stop (G3): the flagged + // consult always fires before any seal. + assert !sealedSeen, "stop-scripted attempt sealed"; + betweenAttempts(); + attempt = attempt + 1; + } else { + assert sealedSeen, "attempt ended unsealed without a crash script"; + syncDone = true; + } + } + } + if (cfg.mutateBetweenSyncs && syncN == 1) { + mutateUpstream(mutKey()); + } + syncN = syncN + 1; + } + goto FinishedEnv; + } + } + + state FinishedEnv { + ignore eGAttemptEnded, eCrashAck, eCrashArmed; + } + + // The mutation target: the consult node's scope (cell 11), the + // writer's scope (cell 21), or the PARENT's own scope (cell 24 — + // the G5 demand-shrink family mutates what the parent derives + // demand FROM). + fun mutKey(): int { + if (cfg.cell == 21) { return 2; } + if (cfg.cell == 24) { return 0; } + return 1; + } + + fun betweenAttempts() { + if (cfg.mutateBetweenAttempts) { + mutateUpstream(mutKey()); + } + if (cfg.cell == 25) { + // G3: swap the PREV artifact for C's key to sibling + // content between attempts (epoch 9 never validates). + send store, eGSwapPrev, (client = this, key = 1, epoch = 9); + receive { case eStoreAck: {} } + } + } + + fun mutateUpstream(k: int) { + send upstream, eMutate, (client = this, scope = k); + receive { case eMutateAck: {} } + } + + // SealExpect counterfactual (SPEC 7/9): the expected sealed key + // set is the cell's demand closure; the expected content per key + // is rows at the key's LIVE upstream epoch, read (never executed) + // at attempt start. Later attempts overwrite the expectation, so + // the check always binds the sealing attempt's world. + fun announceExpectation(syncN: int) { + var exp: map[int, int]; + var excluded: map[int, bool]; + exp[0] = epochOf(0); + if (cfg.cell == 11 || cfg.cell == 25 || cfg.cell == 27) { + exp[1] = epochOf(1); + } + if (cfg.cell == 21) { + exp[2] = epochOf(2); + exp[3] = epochOf(3); + } + if (cfg.cell == 24) { + // C is demanded only while P's row 1 exists (epoch 1). + // After the shrink C's key is EXCLUDED, not merely + // unexpected: whether it seals depends on which attempt's + // parent world survives (sync-scoped freshness) — the + // structural question belongs to GP5's artifact closure, + // not the counterfactual. + if (epochOf(0) <= 1) { + exp[1] = epochOf(1); + } else { + excluded[1] = true; + } + } + if (cfg.cell == 26) { + // Chain P -> S1 -> C -> GC; the C/GC tail is demanded + // only while S1's row 1 exists (G6b's shrink excludes it, + // same sync-scoped reasoning as cell 24). + exp[1] = epochOf(1); + if (epochOf(1) <= 1) { + exp[4] = epochOf(4); + exp[5] = epochOf(5); + } else { + excluded[4] = true; + excluded[5] = true; + } + } + if (cfg.cell == 28) { + exp[1] = epochOf(1); + exp[4] = epochOf(4); + exp[5] = epochOf(5); + } + if (cfg.cell == 29) { + // Key 1 is poisoned on every schedule (two distinct + // derivations both commit it); eAnnPoison excludes it in + // the monitor — the expectation still names it so a + // MISSING poison (defense failure) surfaces. + exp[1] = epochOf(1); + } + announce eAnnExpectSeal, (syncN = syncN, exp = exp, excluded = excluded); + } + + fun epochOf(k: int): int { + var e: int; + send upstream, eValidateReq, (client = this, scope = k, v = -1); + receive { + case eValidateResp: (r: (ok: bool, epoch: int)) { e = r.epoch; } + } + return e; + } +} diff --git a/formal/graph/PSrc/Events.p b/formal/graph/PSrc/Events.p new file mode 100644 index 000000000..2cb2c0274 --- /dev/null +++ b/formal/graph/PSrc/Events.p @@ -0,0 +1,165 @@ +/* Events. Store ops are request/ack; every attempt-owned request + carries agen (attempt id) and the crash protocol drops ops from + dead attempts (walker parity: dropped ops are never acked; the + eStoreDead response lets receivers park instead of blocking). */ + +// ---- store ops (worker/scheduler -> MGStore) ---- +event eGReset: (client: machine, syncN: int); +event eGLookupReq: (client: machine, agen: int, key: int); +event eGLookupResp: (hit: bool, v: int); +event eGMarkerReadReq: (client: machine, agen: int, key: int); +event eGMarkerReadResp: (present: bool, marker: tMarker); +// delMarker: the REPLACES clear deletes the key's marker (SPEC 3 +// clear-placement pin; markerCleanupOff removes exactly this). +event eGClearScope: (client: machine, agen: int, key: int, delMarker: bool, ghost: tGGhost); +event eGUpsertPage: (client: machine, agen: int, key: int, rows: seq[tGRow], ghost: tGGhost); +event eGPublishEntry: (client: machine, agen: int, key: int, v: int, stamp: map[int, int], hash: int, ghost: tGGhost); +// V-ATOMIC replay unit (settled hand-off, MODEL_SPEC 9.6): clear + +// copy + marker + publish as ONE atomic store op. Marker announce +// FIRST among constituents (P-MARK convention: every legal partition +// mutation rides an op that first (re)binds the marker). +event eGReplayUnit: (client: machine, agen: int, key: int, v: int, marker: tMarker, stamp: map[int, int], hash: int, ghost: tGGhost); +// V-OVERLAY-UNIT: clear + copy(base) + overlay pages + marker + +// publish(V_to) as ONE atomic store op. +event eGOverlayUnit: (client: machine, agen: int, key: int, v: int, upserts: seq[tGRow], removes: seq[int], marker: tMarker, stamp: map[int, int], hash: int, composeDead: bool, ghost: tGGhost); +// Unit commit response: the key's final rows (copied rows can name +// children, so the carrier report needs them for demand derivation). +event eGUnitResp: (rows: seq[tGRow]); +// Premise-validated adoption (SPEC 4a): one atomic op. Store-side +// preconditions (R2-N1 + R3-M2): fromGen dead per the last durable +// generation table AND the key not poisoned; allowLiveFrom is the +// suppressionOff deviation (declared, R2-N1). Response carries the +// adopted rows for the re-announce (demand re-derivation). +event eGAdoptReq: (client: machine, agen: int, key: int, node: int, fromGen: int, toGen: int, roundId: int, allowLiveFrom: bool, ghost: tGGhost); +event eGAdoptResp: (ok: bool, rows: seq[tGRow]); +// Session ops (SPEC 3 MSessionStore; body ops per the R3-F1 pin). +event eGSessionPub: (client: machine, agen: int, skey: int, val: int, writer: int, wgen: int, ghost: tGGhost); +event eGSessionGetReq: (client: machine, agen: int, reader: int, rgen: int, skey: int); +event eGSessionGetResp: (found: bool, val: int, writer: int, wgen: int); +event eGCheckpointReq: (client: machine, agen: int, ck: tGCkpt, forced: bool); +event eGReadCkptReq: (client: machine); +event eGReadCkptResp: (ck: tGCkpt, has: bool); +event eGReadRowsReq: (client: machine, agen: int, key: int); +event eGReadRowsResp: (rows: seq[tGRow], present: bool); +event eGReadStampsReq: (client: machine, agen: int); +event eGReadStampsResp: (stamps: map[int, map[int, int]], owners: map[int, int]); +// Seal: keep = the final demand closure keys (sweep drops the rest; +// doSweep false = sweepOff). genTable rides the seal for P6-S. +event eGSealReq: (client: machine, agen: int, keep: seq[int], doSweep: bool, genTable: map[int, int]); +event eReadSealedReq: (client: machine); +event eReadSealedResp: (sealed: bool); +event eStoreAck; +event eStoreDead; +event eCrashArm: (client: machine, agen: int); +// Synchronous arm handshake: the store confirms the arm BEFORE the +// env creates the attempt, so the arm can never lose the queue race +// to the entire attempt (an unresolved arm deadlocks the env — found +// by feedback-PCT on the G1d cell; latent in the walker too). +event eCrashArmed; +event eCrashAck; + +// ---- upstream (synchronous request/response; walker parity) ---- +event eValidateReq: (client: machine, scope: int, v: int); +event eValidateResp: (ok: bool, epoch: int); +event eFetchReq: (client: machine, scope: int, page: int); +event eFetchResp: (rows: seq[tGRow], epoch: int, morePages: bool); +event eDiffReq: (client: machine, scope: int, fromEpoch: int, page: int); +event eDiffResp: (upserts: seq[tGRow], removes: seq[int], epoch: int, morePages: bool); +event eMutate: (client: machine, scope: int); +event eMutateAck; + +// ---- scheduler <-> worker ---- +event eGDispatch: (pend: tPendingNode, execId: int, attempt: int, stop: bool); +// Scripted graceful stop (G3, walker case 3): the flagged execution +// stops AFTER its consult announce, before any round commit; the +// scheduler checkpoints (the stopped node stays pending at its +// cursor) and ends the attempt unsealed with the store intact. +event eGStopReq: (node: int, gen: int); +// Between-attempt previous-artifact swap (G3): the env rebinds the +// PREV manifest + partition for one key to sibling content, so the +// resumed consult validates against the actually-current base. +event eGSwapPrev: (client: machine, key: int, epoch: int); +// Loud attempt failure (G7, walker P4 analog): the scripted node +// fails at execution start; the fingerprint is GENERATION-BLIND +// (round-1 F11: a fingerprint that hashes the generation never +// matches across bumped resumes and the stuck detector goes blind). +event eGNodeFail: (node: int, gen: int, fingerprint: int); +event eGNodeDone: (report: tGReport); +// Read-time session-read registration (R2-M1 read-through): the +// scheduler's reader index must see a read WHILE the reading +// execution is in flight, or the retraction/quiesce race (R2-F1) +// is structurally unreachable and an in-flight reader whose read +// races a re-publish is never retracted. +event eGReadNote: (reader: int, rgen: int, skey: int, val: int, writer: int, wgen: int); +event eGAbortWorker; + +// ---- scheduler self-events ---- +event eGLoopTop; + +// ---- env control ---- +event eGAttemptEnded: (sealed: bool, failed: bool); + +// ---- announce vocabulary (SPEC 7) ---- +event eAnnScenarioInit: (maxStaleness: int); +event eAnnSyncStart: (syncN: int); +event eAnnConsult: (syncN: int, key: int, hit: bool, v: int, validated: bool, epoch: int, freshFetch: bool, diffVerdict: bool, attempt: int, node: int, gen: int); +event eAnnClear: (syncN: int, key: int, ghost: tGGhost); +// Replay copy carries the copied rows so P-MARK and the fold can +// ground content without a store readback. +event eAnnReplayCopy: (syncN: int, key: int, vBase: int, rows: seq[tGRow], ghost: tGGhost); +event eAnnUpsert: (syncN: int, key: int, rows: seq[tGRow], ghost: tGGhost); +event eAnnTombstones: (syncN: int, key: int, removes: seq[int], ghost: tGGhost); +event eAnnPublish: (syncN: int, key: int, v: int, ghost: tGGhost); +event eAnnMarkerPut: (syncN: int, key: int, node: int, gen: int, roundId: int, pubBearing: bool, contentEpoch: int, ghost: tGGhost); +event eAnnMarkerDel: (syncN: int, key: int); +// Adoption re-announce: attributes to the ADOPTING execution +// (P-GEN rule R2-N3); adoptedRoundId lets P1 transfer the marked +// round's contribution and P-MARK rebind the marker. +event eAnnAdopt: (syncN: int, key: int, node: int, fromGen: int, toGen: int, adoptedRoundId: int, rows: seq[tGRow], ghost: tGGhost); +event eAnnPoison: (syncN: int, key: int); +event eAnnSessionSet: (syncN: int, skey: int, val: int, writer: int, wgen: int, ghost: tGGhost); +event eAnnSessionRead: (syncN: int, reader: int, rgen: int, skey: int, found: bool, val: int, writer: int, wgen: int); +// Derived announces (G-RULE-1 carrier pin): generation death, forced +// re-admissions, dead reads, pass iterations, budget exhaustion. +// reason: 1 = resume bump, 2 = retraction, 3 = observation pass. +event eAnnGenBump: (syncN: int, node: int, newGen: int, reason: int); +event eAnnReadmit: (syncN: int, node: int, hash: int, gen: int, reason: int); +event eAnnDeadRead: (syncN: int, reader: int, skey: int); +// Resume-time pending purge (E, R2-F5 ∀-predicate) — the G5e +// existence probe's observation point. +event eAnnPurge: (syncN: int, node: int, hash: int); +// A node dispatched while EVERY admitted-by edge names a dead +// generation (dead demand) — unreachable honestly (E purges at +// resume, S refuses at dispatch); the purgeOff kill's alarm. +event eAnnDeadDispatch: (syncN: int, node: int, hash: int); +// Execution-count vocabulary (G6 bake-off): every dispatch announces; +// the env declares the cell's bound once at scenario init. +event eAnnExec: (syncN: int, attempt: int, node: int, gen: int); +event eAnnExecBound: (bound: int); +// Seal-world target (GS-CO-005(d) G5d meta-analysis): the env +// declares the probed world once at scenario init; GSEALWORLD reds +// iff the target sync seals exactly that world. +event eAnnSealWorld: (syncN: int, exp: map[int, int]); +// Attempt-failure vocabulary (G7): the scheduler announces the loud +// failure's generation-blind fingerprint; the env announces abandons. +event eAnnAttemptFail: (syncN: int, attempt: int, node: int, fingerprint: int); +event eAnnAbandon: (syncN: int); +event eAnnPassIter: (syncN: int, iter: int); +event eAnnBudgetExhausted: (syncN: int); +event eAnnCheckpoint: (syncN: int, forced: bool); +event eAnnCrash: (syncN: int); +// Per-announce demand note (G-RULE-1 TIMING PIN): a paginated record +// round's committed page carries its child-naming rows to the +// scheduler AT the page announce, so demand derivation is atomic +// with that announce — the mid-round C-pending & parent-pending +// checkpoint window (G5e) exists only under this timing. Units are +// single atomic commits; their completion carrier IS the announce. +event eGDemandNote: (node: int, gen: int, key: int, rows: seq[tGRow]); + +// Seal announce: partition + manifest + stamps + the final generation +// table (P6-S's dead-set ground). +event eAnnGSeal: (syncN: int, partition: tGPart, manifest: map[int, int], stamps: map[int, map[int, int]], genTable: map[int, int]); +// Scripted seal expectation (SealExpect = closure + content oracle): +// exp maps key -> expected content epoch; excluded keys (poisoned) +// are exempt both directions. +event eAnnExpectSeal: (syncN: int, exp: map[int, int], excluded: map[int, bool]); diff --git a/formal/graph/PSrc/NodeExec.p b/formal/graph/PSrc/NodeExec.p new file mode 100644 index 000000000..14a483cca --- /dev/null +++ b/formal/graph/PSrc/NodeExec.p @@ -0,0 +1,418 @@ +/* MGNodeExec: worker-side execution body (SPEC 3). One execution = + marker check (SPEC 4a) -> consult -> verdict (ADOPT | REPLAY | + CHANGED-WITH-DIFF | FETCH-FRESH) -> emissions. Unit-mode + materialization for replay and diff rounds (settled hand-off); + record rounds for fetch-fresh (no marker, G8a pin). + + SESSION-PUBLISH BODY-OP PIN (R3-F1): a writer's session publish is + a body store op executed by every NON-ADOPTED execution regardless + of verdict class; only adoption skips it. + + Scripted policies (build decisions, CALIBRATION.md): the reader G + always fetches fresh (walker case-7a precedent), so markers arise + on consult-kind and writer-kind nodes only; re-derivations after an + ineligible or refused marker check use fetch-fresh when + cfg.rederiveFresh (the G1(ii)/G1c scripted shape); a writer's + published value is a pure function of its consult epoch (so + same-premise re-derivations re-publish the same value). */ + +machine MGNodeExec { + var sched: machine; + var store: machine; + var upstream: machine; + var agen: int; + var syncN: int; + var attempt: int; + var cfg: tGCfg; + var dead: bool; + var stopAfterConsult: bool; // G3 scripted stop (dispatch flag) + + start state InitW { + entry (p: (sched: machine, store: machine, upstream: machine, agen: int, syncN: int, attempt: int, cfg: tGCfg)) { + sched = p.sched; store = p.store; upstream = p.upstream; + agen = p.agen; syncN = p.syncN; attempt = p.attempt; cfg = p.cfg; + goto IdleW; + } + } + + state IdleW { + on eGDispatch do (p: (pend: tPendingNode, execId: int, attempt: int, stop: bool)) { + stopAfterConsult = p.stop; + runNode(p.pend, p.execId); + } + on eGAbortWorker do { + goto DeadW; + } + } + + state DeadW { + ignore eGDispatch, eGAbortWorker; + } + + fun runNode(pend: tPendingNode, execId: int) { + // Loud deterministic failure (G7): fails at execution start, + // every generation, with a GENERATION-BLIND fingerprint. + if (pend.node == cfg.failNode && syncN == cfg.failSync) { + send sched, eGNodeFail, (node = pend.node, gen = pend.gen, fingerprint = pend.node * 1000 + 7); + return; + } + if (pend.kind == NK_PARENT) { + runParent(pend, execId); + } else if (pend.kind == NK_READER) { + runReader(pend, execId); + } else { + runConsultKind(pend, execId); + } + } + + // ---- parent: fetch-fresh record round, rows name children ---- + fun runParent(pend: tPendingNode, execId: int) { + var rows: seq[tGRow]; + var epoch: int; + var g: tGGhost; + var noPubs: seq[tPub]; + var noReads: seq[tReadRec]; + var stamp: map[int, int]; + g = (roundId = execId, verdict = GV_FRESH, consultEpoch = 0, vBase = -1, lastOp = false, attempt = attempt, node = pend.node, gen = pend.gen); + rows = fetchAll(pend, execId); + if (dead) { abortReport(pend, execId); return; } + epoch = rows[0].epoch; + g.consultEpoch = epoch; + announce eAnnConsult, (syncN = syncN, key = pend.key, hit = false, v = -1, validated = false, epoch = epoch, freshFetch = true, diffVerdict = false, attempt = attempt, node = pend.node, gen = pend.gen); + commitRecordRound(pend, execId, rows, epoch, g); + if (dead) { abortReport(pend, execId); return; } + sendDone(execId, pend, GV_FRESH, rows, noPubs, noReads); + } + + // ---- reader G: always-fresh record round embedding the session + // read; stamp = {G: g} merged with the read value's writer stamp + // (S read-side merge; stampMergeOff is the kill) ---- + fun runReader(pend: tPendingNode, execId: int) { + var rows: seq[tGRow]; + var i: int; + var epoch: int; + var g: tGGhost; + var noPubs: seq[tPub]; + var reads: seq[tReadRec]; + var found: bool; + var sv: int; + var sw: int; + var swg: int; + g = (roundId = execId, verdict = GV_FRESH, consultEpoch = 0, vBase = -1, lastOp = false, attempt = attempt, node = pend.node, gen = pend.gen); + // Session read (observation point (ii): read-through). + found = false; sv = -1; sw = -1; swg = -1; + send store, eGSessionGetReq, (client = this, agen = agen, reader = pend.node, rgen = pend.gen, skey = 0); + receive { + case eGSessionGetResp: (r: (found: bool, val: int, writer: int, wgen: int)) { + found = r.found; sv = r.val; sw = r.writer; swg = r.wgen; + } + case eStoreDead: { dead = true; } + } + if (dead) { abortReport(pend, execId); return; } + if (found) { + reads += (0, (reader = pend.node, rgen = pend.gen, skey = 0, val = sv, writer = sw, wgen = swg)); + // Read-time registration (R2-M1 read-through): the + // scheduler sees the read while this execution flies. + send sched, eGReadNote, (reader = pend.node, rgen = pend.gen, skey = 0, val = sv, writer = sw, wgen = swg); + } + rows = fetchAll(pend, execId); + if (dead) { abortReport(pend, execId); return; } + epoch = rows[0].epoch; + g.consultEpoch = epoch; + i = 0; + while (i < sizeof(rows)) { + if (found) { + rows[i] = (id = rows[i].id, epoch = rows[i].epoch, hops = rows[i].hops, childHash = rows[i].childHash, sVal = sv, sWriter = sw, sWGen = swg); + } + i = i + 1; + } + announce eAnnConsult, (syncN = syncN, key = pend.key, hit = false, v = -1, validated = false, epoch = epoch, freshFetch = true, diffVerdict = false, attempt = attempt, node = pend.node, gen = pend.gen); + commitRecordRoundStamped(pend, execId, rows, epoch, g, readerStamp(pend, sw, swg, found)); + if (dead) { abortReport(pend, execId); return; } + sendDone(execId, pend, GV_FRESH, rows, noPubs, reads); + } + + fun readerStamp(pend: tPendingNode, sw: int, swg: int, found: bool): map[int, int] { + var st: map[int, int]; + st[pend.node] = pend.gen; + if (found && cfg.toggles.stampMerge && sw >= 0) { + st[sw] = swg; + } + return st; + } + + // ---- consult-kind (C) and writer-kind (H) executions ---- + fun runConsultKind(pend: tPendingNode, execId: int) { + var present: bool; + var m: tMarker; + var hit: bool; + var v: int; + var outcome: int; + var epoch: int; + var rederived: bool; + var eligible: bool; + var d2: tDigest; + var g: tGGhost; + var rows: seq[tGRow]; + var ups: seq[tGRow]; + var rms: seq[int]; + var pubs: seq[tPub]; + var noReads: seq[tReadRec]; + var stamp: map[int, int]; + var mk: tMarker; + var isWriter: bool; + var adoptedRows: seq[tGRow]; + var adoptOk: bool; + var verdict: tGVerdict; + isWriter = pend.kind == NK_WRITER; + // Marker check (SPEC 4a). + present = false; + send store, eGMarkerReadReq, (client = this, agen = agen, key = pend.key); + receive { + case eGMarkerReadResp: (r: (present: bool, marker: tMarker)) { + present = r.present; + m = r.marker; + } + case eStoreDead: { dead = true; } + } + if (dead) { abortReport(pend, execId); return; } + // One consult per execution (the marker path's re-consult is + // THE consult when it falls through to re-derivation). + hit = false; v = -1; + send store, eGLookupReq, (client = this, agen = agen, key = pend.key); + receive { + case eGLookupResp: (r: (hit: bool, v: int)) { hit = r.hit; v = r.v; } + case eStoreDead: { dead = true; } + } + if (dead) { abortReport(pend, execId); return; } + send upstream, eValidateReq, (client = this, scope = pend.key, v = v); + receive { + case eValidateResp: (r: (ok: bool, epoch: int)) { + epoch = r.epoch; + if (!hit) { outcome = 0; } + else if (r.ok) { outcome = 1; } + else { outcome = 2; } + } + } + rederived = false; + if (present && !m.voided) { + // ADOPTION ELIGIBILITY (round-2 pins): MATCH-only (R2-F3; + // adoptOnFail waives) and writer-ineligibility (R2-F2; + // writerAdopt waives). Digest EQUAL and eligible -> ADOPT. + d2 = (v = v, outcome = outcome, sVal = -1, sWriter = -1, sWGen = -1, hasSess = false); + eligible = (outcome == 1 || cfg.toggles.adoptOnFail) && d2 == m.digest && (!m.pubBearing || cfg.toggles.writerAdopt); + if (eligible) { + // The consult that justifies the adoption is announced + // BEFORE the adopt commits (P-ADOPT ordering: the + // justification precedes the act). + announce eAnnConsult, (syncN = syncN, key = pend.key, hit = hit, v = v, validated = outcome == 1, epoch = epoch, freshFetch = false, diffVerdict = false, attempt = attempt, node = pend.node, gen = pend.gen); + g = (roundId = execId, verdict = GV_ADOPT, consultEpoch = epoch, vBase = m.contentEpoch, lastOp = true, attempt = attempt, node = pend.node, gen = pend.gen); + adoptOk = false; + send store, eGAdoptReq, (client = this, agen = agen, key = pend.key, node = pend.node, fromGen = m.gen, toGen = pend.gen, roundId = m.roundId, allowLiveFrom = !cfg.toggles.suppression, ghost = g); + receive { + case eGAdoptResp: (r: (ok: bool, rows: seq[tGRow])) { + adoptOk = r.ok; + adoptedRows = r.rows; + } + case eStoreDead: { dead = true; } + } + if (dead) { abortReport(pend, execId); return; } + if (adoptOk) { + sendDone(execId, pend, GV_ADOPT, adoptedRows, pubs, noReads); + return; + } + } + rederived = true; + } + // Verdict as if unmarked (SPEC 4a re-derive clause). + if (outcome == 1) { + verdict = GV_REPLAY; + } else if (outcome == 2 && cfg.diffPolicy && !(cfg.rederiveFresh && rederived)) { + verdict = GV_DIFF; + } else { + verdict = GV_FRESH; + } + announce eAnnConsult, (syncN = syncN, key = pend.key, hit = hit, v = v, validated = outcome == 1, epoch = epoch, freshFetch = verdict == GV_FRESH, diffVerdict = verdict == GV_DIFF, attempt = attempt, node = pend.node, gen = pend.gen); + // Scripted graceful stop (G3, walker case 3): AFTER the + // consult, before any round commit — the node stays pending + // at its consult-granularity cursor. + if (stopAfterConsult) { + stopAfterConsult = false; + send sched, eGStopReq, (node = pend.node, gen = pend.gen); + return; + } + // Writer body op (R3-F1 pin): session publish on every + // non-adopted execution, before the round's store commit. + if (isWriter) { + g = (roundId = execId, verdict = verdict, consultEpoch = epoch, vBase = -1, lastOp = false, attempt = attempt, node = pend.node, gen = pend.gen); + send store, eGSessionPub, (client = this, agen = agen, skey = 0, val = epoch, writer = pend.node, wgen = pend.gen, ghost = g); + receive { + case eStoreAck: {} + case eStoreDead: { dead = true; } + } + if (dead) { abortReport(pend, execId); return; } + pubs += (0, (skey = 0, val = epoch, wgen = pend.gen)); + } + stamp = default(map[int, int]); + stamp[pend.node] = pend.gen; + if (verdict == GV_REPLAY) { + mk = (node = pend.node, gen = pend.gen, roundId = execId, digest = (v = v, outcome = 1, sVal = -1, sWriter = -1, sWGen = -1, hasSess = false), pubBearing = isWriter, voided = false, contentEpoch = v); + g = (roundId = execId, verdict = GV_REPLAY, consultEpoch = epoch, vBase = v, lastOp = true, attempt = attempt, node = pend.node, gen = pend.gen); + send store, eGReplayUnit, (client = this, agen = agen, key = pend.key, v = epoch, marker = mk, stamp = stamp, hash = pend.hash, ghost = g); + receive { + case eGUnitResp: (r: (rows: seq[tGRow])) { rows = r.rows; } + case eStoreDead: { dead = true; } + } + if (dead) { abortReport(pend, execId); return; } + sendDone(execId, pend, GV_REPLAY, rows, pubs, noReads); + return; + } + if (verdict == GV_DIFF) { + send upstream, eDiffReq, (client = this, scope = pend.key, fromEpoch = v, page = 0); + receive { + case eDiffResp: (r: (upserts: seq[tGRow], removes: seq[int], epoch: int, morePages: bool)) { ups = r.upserts; } + } + send upstream, eDiffReq, (client = this, scope = pend.key, fromEpoch = v, page = 1); + receive { + case eDiffResp: (r: (upserts: seq[tGRow], removes: seq[int], epoch: int, morePages: bool)) { rms = r.removes; } + } + mk = (node = pend.node, gen = pend.gen, roundId = execId, digest = (v = v, outcome = 2, sVal = -1, sWriter = -1, sWGen = -1, hasSess = false), pubBearing = isWriter, voided = false, contentEpoch = epoch); + g = (roundId = execId, verdict = GV_DIFF, consultEpoch = epoch, vBase = v, lastOp = true, attempt = attempt, node = pend.node, gen = pend.gen); + send store, eGOverlayUnit, (client = this, agen = agen, key = pend.key, v = epoch, upserts = ups, removes = rms, marker = mk, stamp = stamp, hash = pend.hash, composeDead = cfg.toggles.overlayComposeDead, ghost = g); + receive { + case eGUnitResp: (r: (rows: seq[tGRow])) { rows = r.rows; } + case eStoreDead: { dead = true; } + } + if (dead) { abortReport(pend, execId); return; } + sendDone(execId, pend, GV_DIFF, rows, pubs, noReads); + return; + } + // FETCH-FRESH record round (no marker, G8a pin). + g = (roundId = execId, verdict = GV_FRESH, consultEpoch = epoch, vBase = -1, lastOp = false, attempt = attempt, node = pend.node, gen = pend.gen); + rows = fetchAll(pend, execId); + if (dead) { abortReport(pend, execId); return; } + commitRecordRound(pend, execId, rows, epoch, g); + if (dead) { abortReport(pend, execId); return; } + sendDone(execId, pend, GV_FRESH, rows, pubs, noReads); + } + + // Fetch every page of the node's key at the current epoch, + // decorated with the cell's child-naming script. + fun fetchAll(pend: tPendingNode, execId: int): seq[tGRow] { + var rows: seq[tGRow]; + var page: int; + var more: bool; + var i: int; + var r: tGRow; + var ch: int; + page = 0; + more = true; + while (more) { + send upstream, eFetchReq, (client = this, scope = pend.key, page = page); + receive { + case eFetchResp: (rp: (rows: seq[tGRow], epoch: int, morePages: bool)) { + i = 0; + while (i < sizeof(rp.rows)) { + r = rp.rows[i]; + ch = childHashFor(cfg.cell, pend.node, r.id); + if (ch > 0) { + r = (id = r.id, epoch = r.epoch, hops = r.hops, childHash = ch, sVal = r.sVal, sWriter = r.sWriter, sWGen = r.sWGen); + } + rows += (sizeof(rows), r); + i = i + 1; + } + more = rp.morePages; + } + } + page = page + 1; + } + return rows; + } + + fun commitRecordRound(pend: tPendingNode, execId: int, rows: seq[tGRow], epoch: int, g: tGGhost) { + var stamp: map[int, int]; + stamp[pend.node] = pend.gen; + commitRecordRoundStamped(pend, execId, rows, epoch, g, stamp); + } + + // Record round (REPLACES): clear (deleting the marker — the + // markerCleanup toggle removes exactly that, R2-F4), per-page + // upserts, publish. Per-op commits give the crash protocol its + // windows. + fun commitRecordRoundStamped(pend: tPendingNode, execId: int, rows: seq[tGRow], epoch: int, g: tGGhost, stamp: map[int, int]) { + var i: int; + var pageRows: seq[tGRow]; + send store, eGClearScope, (client = this, agen = agen, key = pend.key, delMarker = cfg.toggles.markerCleanup, ghost = g); + receive { + case eStoreAck: {} + case eStoreDead: { dead = true; } + } + if (dead) { return; } + i = 0; + while (i < sizeof(rows)) { + pageRows = default(seq[tGRow]); + pageRows += (0, rows[i]); + send store, eGUpsertPage, (client = this, agen = agen, key = pend.key, rows = pageRows, ghost = g); + receive { + case eStoreAck: {} + case eStoreDead: { dead = true; } + } + if (dead) { return; } + // Per-announce demand derivation (G-RULE-1 TIMING PIN): + // a committed child-naming page reaches the scheduler AT + // the page announce, not at completion — the mid-round + // checkpoint window (G5e) exists only under this timing. + if (pageRows[0].childHash > 0) { + send sched, eGDemandNote, (node = pend.node, gen = pend.gen, key = pend.key, rows = pageRows); + } + i = i + 1; + } + g.lastOp = true; + send store, eGPublishEntry, (client = this, agen = agen, key = pend.key, v = epoch, stamp = stamp, hash = pend.hash, ghost = g); + receive { + case eStoreAck: {} + case eStoreDead: { dead = true; } + } + } + + fun sendDone(execId: int, pend: tPendingNode, verdict: tGVerdict, rows: seq[tGRow], pubs: seq[tPub], reads: seq[tReadRec]) { + send sched, eGNodeDone, (report = (execId = execId, node = pend.node, gen = pend.gen, hash = pend.hash, key = pend.key, kind = pend.kind, verdict = verdict, rows = rows, pubs = pubs, reads = reads, aborted = false),); + } + + fun abortReport(pend: tPendingNode, execId: int) { + var noRows: seq[tGRow]; + var noPubs: seq[tPub]; + var noReads: seq[tReadRec]; + send sched, eGNodeDone, (report = (execId = execId, node = pend.node, gen = pend.gen, hash = pend.hash, key = pend.key, kind = pend.kind, verdict = GV_FRESH, rows = noRows, pubs = noPubs, reads = noReads, aborted = true),); + goto DeadW; + } +} + +// Child-naming script (G-RULE-1: demand rides row content). +fun childHashFor(cell: int, node: int, rowId: int): int { + if (cell == 11 && node == 0) { return 2; } // P names C on every row + if (cell == 21 && node == 0 && rowId == 0) { return 3; } // P names H + if (cell == 21 && node == 0 && rowId == 1) { return 4; } // P names G + // Cell 24 (G5 family): the PAGINATED parent names C on page 1 + // ONLY — the row the e1->e2 mutation deletes, so the re-execution + // at e2 emits no child naming (upstream demand shrink). + if (cell == 24 && node == 0 && rowId == 1) { return 2; } + if (cell == 25 && node == 0) { return 2; } // G3: cell-11 topology + // Cell 26 (G6a/b chain): P -> S1 -> C -> GC; S1 names C on its + // page-1 row (the row the e1->e2 mutation deletes: G6b's shrink). + if (cell == 26 && node == 0) { return 2; } + if (cell == 26 && node == 1 && rowId == 1) { return 5; } + if (cell == 26 && node == 4 && rowId == 0) { return 6; } + // Cell 27 (G7): cell-11 topology. + if (cell == 27 && node == 0) { return 2; } + // Cell 28 (G6c fan-in): P names S1 + S2; both name C. + if (cell == 28 && node == 0 && rowId == 0) { return 2; } + if (cell == 28 && node == 0 && rowId == 1) { return 5; } + if (cell == 28 && node == 1 && rowId == 0) { return 6; } + if (cell == 28 && node == 4 && rowId == 0) { return 6; } + // Cell 29 (G8c same-key): P names two DISTINCT derivations that + // share output key 1 (keyOf). + if (cell == 29 && node == 0 && rowId == 0) { return 2; } + if (cell == 29 && node == 0 && rowId == 1) { return 5; } + return 0; +} diff --git a/formal/graph/PSrc/Sched.p b/formal/graph/PSrc/Sched.p new file mode 100644 index 000000000..51037e843 --- /dev/null +++ b/formal/graph/PSrc/Sched.p @@ -0,0 +1,818 @@ +/* MGraphSched: the frontier scheduler, one machine per attempt + (SPEC 3). Owns the frontier, admitted-derivation set (G-RULE-2 + death semantics), generation-qualified admitted-by edges, the + generation table (G-RULE-3), demand derivation (G-RULE-1), lineage + state per variant, and dispatch to 2 workers. + + Announce processing is per-carrier atomic: a worker's completion + report (eGNodeDone) hosts all derived effects — demand admissions, + retraction enqueues (E+B), dead-read observations (S), deferred + quiesce bumps — emitted as derived announces inside that handler + (G-RULE-1 derived-announce carrier pin, R2-M2). + + Elective checkpoints commit at loop tops under a genuine choice + point (SPEC 3: placement a choice point — the G1e/R3-F2 crash + window needs skip schedules). The forced resume checkpoint (F4) + and the mid-bump fence (R3-F2) are UNCONDITIONAL commits, removed + only by their kill toggles. */ + +machine MGraphSched { + var env: machine; + var store: machine; + var upstream: machine; + var agen: int; + var syncN: int; + var attempt: int; + var cfg: tGCfg; + var frontier: seq[tPendingNode]; + var admitted: map[int, bool]; + var completedH: map[int, bool]; + var edges: map[int, seq[tEdge]]; + var genTable: map[int, int]; + var readers: seq[tReadRec]; + var retractQ: seq[int]; // reader nodes owing a re-run (E+B) + var deferredBumps: seq[int]; // quiesce-deferred bump targets + var workers: seq[machine]; + var busyW: map[machine, bool]; + var owner: map[int, machine]; // execId -> worker + var outstanding: map[int, tPendingNode]; + var inFlight: map[int, int]; // node -> execId + var execSeq: int; + var storeDead: bool; + var passIters: int; + var droppedHash: int; // demandDrop inject target (0 = unarmed) + var carrierDirty: bool; + var catchupDone: map[int, bool]; // reader*100+wgen -> catch-up retraction spent + + start state Boot { + entry (p: (env: machine, store: machine, upstream: machine, agen: int, syncN: int, attempt: int, cfg: tGCfg, ck: tGCkpt, has: bool)) { + var i: int; + var j: int; + var pend: tPendingNode; + var bumped: seq[int]; + var hs: seq[int]; + var stillPending: map[int, bool]; + env = p.env; store = p.store; upstream = p.upstream; + agen = p.agen; syncN = p.syncN; attempt = p.attempt; cfg = p.cfg; + execSeq = agen * 100; + if (!p.has) { + // Fresh roots (attempt 1, or crash before any durable + // checkpoint: restart-from-root). + rootFrontier(); + // F4 mint fence (R3-F2 extension): the initial minted + // table is durable BEFORE first dispatch, so a later + // attempt always restores past these identities. + if (cfg.toggles.resumeCkpt) { + doCheckpoint(true); + if (storeDead) { goto DeadSched; } + } + } else { + frontier = p.ck.pending; + admitted = p.ck.admitted; + edges = p.ck.edges; + genTable = p.ck.genTable; + readers = p.ck.readers; + // Resume (SPEC 3): bump every pending node (the prior + // generation is dead)... + i = 0; + while (i < sizeof(frontier)) { + pend = frontier[i]; + if (!inIntSeq(bumped, pend.node)) { + genTable[pend.node] = genTable[pend.node] + 1; + // Resume bumps are re-mints: bucket-aligned + // under compression (G9-CAL-1). + if (cfg.stampCompression && genTable[pend.node] % 2 == 1) { + genTable[pend.node] = genTable[pend.node] + 1; + } + bumped += (sizeof(bumped), pend.node); + } + pend.gen = genTable[pend.node]; + frontier[i] = pend; + i = i + 1; + } + // ...variant E purges pending nodes under the + // ∀-PREDICATE (R2-F5: purge only when EVERY admitted-by + // edge names a dead generation), removing purged hashes + // from the admitted-derivation set. Roots (no edges) + // never purge. + if (cfg.lineage == LIN_E && cfg.toggles.purge) { + i = 0; + while (i < sizeof(frontier)) { + pend = frontier[i]; + if (pend.hash in edges && sizeof(edges[pend.hash]) > 0 && allEdgesDead(pend.hash)) { + announce eAnnPurge, (syncN = syncN, node = pend.node, hash = pend.hash); + admitted -= pend.hash; + frontier -= i; + } else { + i = i + 1; + } + } + } + // Completed iff admitted ∧ ¬pending, evaluated AFTER + // removals (G-RULE-2 resume rule). + i = 0; + while (i < sizeof(frontier)) { + stillPending[frontier[i].hash] = true; + i = i + 1; + } + hs = keys(admitted); + i = 0; + while (i < sizeof(hs)) { + if (!(hs[i] in stillPending)) { + completedH[hs[i]] = true; + } + i = i + 1; + } + // FORCED RESUME CHECKPOINT (F4): the bumped table is + // durable before any dispatch; resume bumps ride its + // commit (R2-M2). resumeCkptOff is the G1b kill. + if (cfg.toggles.resumeCkpt) { + doCheckpoint(true); + if (storeDead) { goto DeadSched; } + } + i = 0; + while (i < sizeof(bumped)) { + announce eAnnGenBump, (syncN = syncN, node = bumped[i], newGen = genTable[bumped[i]], reason = 1); + i = i + 1; + } + } + i = 0; + while (i < cfg.nWorkers) { + workers += (sizeof(workers), new MGNodeExec((sched = this, store = store, upstream = upstream, agen = agen, syncN = syncN, attempt = attempt, cfg = cfg))); + i = i + 1; + } + goto Running; + } + } + + state Running { + entry { + loopTop(); + } + + on eGLoopTop do { + loopTop(); + } + + on eGReadNote do (p: (reader: int, rgen: int, skey: int, val: int, writer: int, wgen: int)) { + var isDead: bool; + readers += (sizeof(readers), (reader = p.reader, rgen = p.rgen, skey = p.skey, val = p.val, writer = p.writer, wgen = p.wgen)); + isDead = p.wgen >= 0 && p.writer in genTable && p.wgen < genTable[p.writer]; + if (isDead) { + // S observation point (ii): dead-read count. + announce eAnnDeadRead, (syncN = syncN, reader = p.reader, skey = p.skey); + } + // E+B registration-side CATCH-UP retraction, ONCE per + // (reader, dead wgen): a read that registers after its + // value's death was already knowable may have missed the + // re-publish carrier's retraction (note-vs-carrier race); + // repeated retraction is exclusively RE-PUBLISH-driven + // (R2-M6(i)), so an unretractable strand (writerAdopt: + // the writer adopts and never re-publishes) costs one + // bounded re-run and then SEALS, where P6-E reds the + // still-dead final read. An unbounded registration-side + // rule livelocks that cell instead: the frontier never + // drains and the at-seal oracle never evaluates. + if (isDead && cfg.lineage == LIN_E && cfg.sessVar == SESS_B && cfg.toggles.retraction) { + if (p.reader in genTable && p.rgen == genTable[p.reader] && !((p.reader * 100 + p.wgen) in catchupDone)) { + catchupDone[p.reader * 100 + p.wgen] = true; + if (cfg.toggles.quiesce && p.reader in inFlight) { + if (!inIntSeq(deferredBumps, p.reader)) { + deferredBumps += (sizeof(deferredBumps), p.reader); + } + } else { + bumpAndReadmit(p.reader, 2); + if (storeDead) { goto DeadSched; } + if (sizeof(frontier) > 0) { dispatchFree(); } + } + } + } + } + + on eGDemandNote do (p: (node: int, gen: int, key: int, rows: seq[tGRow])) { + var i: int; + // A dead generation's late page note derives nothing: + // demand rides LIVE emissions (the live re-run re-derives + // its own naming). + if (!(p.node in genTable) || p.gen != genTable[p.node]) { return; } + carrierDirty = false; + i = 0; + while (i < sizeof(p.rows)) { + if (p.rows[i].childHash > 0) { + admitChild(p.rows[i].childHash, p.node, p.gen); + } + i = i + 1; + } + // The note is its own carrier: its derived effects commit + // as ONE durable delta (GS-CO-003 discipline; GS-CO-001(b): + // no minted generation dispatches before its table delta). + if (carrierDirty && cfg.toggles.midBumpFence) { + doCheckpoint(true); + } + if (storeDead) { goto DeadSched; } + if (sizeof(frontier) > 0) { dispatchFree(); } + } + + on eGNodeFail do (p: (node: int, gen: int, fingerprint: int)) { + var i: int; + // Loud attempt failure (G7): announce the generation-blind + // fingerprint, park the workers, end failed. No forced + // checkpoint — the resume restores the last durable one. + announce eAnnAttemptFail, (syncN = syncN, attempt = attempt, node = p.node, fingerprint = p.fingerprint); + i = 0; + while (i < sizeof(workers)) { + send workers[i], eGAbortWorker; + i = i + 1; + } + send env, eGAttemptEnded, (sealed = false, failed = true); + goto DoneSched; + } + + on eGStopReq do (p: (node: int, gen: int)) { + var i: int; + // Graceful stop (G3): checkpoint with the stopped node + // still pending at its cursor (buildCheckpoint includes + // in-flight nodes), park the workers, end unsealed. The + // store lives; resume bumps the stopped generation. + doCheckpoint(false); + if (storeDead) { goto DeadSched; } + i = 0; + while (i < sizeof(workers)) { + send workers[i], eGAbortWorker; + i = i + 1; + } + send env, eGAttemptEnded, (sealed = false, failed = false); + goto DoneSched; + } + + on eGNodeDone do (p: (report: tGReport)) { + var r: tGReport; + var i: int; + var w: machine; + r = p.report; + w = owner[r.execId]; + owner -= r.execId; + busyW -= w; + outstanding -= r.execId; + if (r.node in inFlight && inFlight[r.node] == r.execId) { + inFlight -= r.node; + } + if (r.aborted) { + storeDead = true; + goto DeadSched; + } + completedH[r.hash] = true; + // Retraction-queue drain (E): the re-admitted reader's + // completion removes its entry. + i = 0; + while (i < sizeof(retractQ)) { + if (retractQ[i] == r.node) { + retractQ -= i; + } else { + i = i + 1; + } + } + // Demand derivation from announced row content (G-RULE-1), + // atomic with this carrier. + carrierDirty = false; + i = 0; + while (i < sizeof(r.rows)) { + if (r.rows[i].childHash > 0) { + admitChild(r.rows[i].childHash, r.node, r.gen); + } + i = i + 1; + } + // CARRIER-DURABILITY FENCE (GS-CO-003): the carrier's + // derived effects — every admission, mint, and + // admitted-by edge — commit durably as ONE delta, and no + // checkpoint may separate the carrier's completion from + // its admissions. Per-mint fencing leaves a lost-demand + // window: crash between two children's fences restores + // the parent COMPLETED with the second child's admission + // gone, and a completed parent never re-derives — the + // demand starves and the closure oracle reds an honest + // history. + if (carrierDirty && cfg.toggles.midBumpFence) { + doCheckpoint(true); + } + if (storeDead) { goto DeadSched; } + // Session reads register at READ time (eGReadNote, + // R2-M1 read-through) — NOT here: carrier-time + // registration makes the in-flight retraction race + // (R2-F1) unreachable. + // Re-publish reader retraction (E+B): enqueue a retraction + // entry per reader execution of the now-dead value + // (SPEC 3 retraction queue; keying per MSessionStore). + if (cfg.lineage == LIN_E && cfg.sessVar == SESS_B && cfg.toggles.retraction) { + i = 0; + while (i < sizeof(r.pubs)) { + retractReaders(r.pubs[i].skey, r.node, r.pubs[i].wgen); + i = i + 1; + } + } + // Quiesce-deferred bump lands atomically with the dying + // execution's completion (R2-F1). + i = 0; + while (i < sizeof(deferredBumps)) { + if (deferredBumps[i] == r.node) { + deferredBumps -= i; + bumpAndReadmit(r.node, 2); + } else { + i = i + 1; + } + } + if (storeDead) { goto DeadSched; } + if (sizeof(outstanding) == 0) { + send this, eGLoopTop; + } else if (sizeof(frontier) > 0) { + dispatchFree(); + } + } + } + + state DoneSched { + ignore eGLoopTop, eGNodeDone, eGReadNote, eGDemandNote, eGStopReq, eGNodeFail; + } + + // Crashed attempt: ops from this agen are dropped; workers park; + // MEnv resumes independently from the last durable checkpoint. + state DeadSched { + entry { + var i: int; + i = 0; + while (i < sizeof(workers)) { + send workers[i], eGAbortWorker; + i = i + 1; + } + } + ignore eGLoopTop, eGNodeDone, eGReadNote, eGDemandNote, eGStopReq, eGNodeFail; + } + + fun loopTop() { + if (storeDead) { goto DeadSched; } + // Elective checkpoint: placement is a genuine choice point. + if (choose(2) == 0) { + doCheckpoint(false); + if (storeDead) { goto DeadSched; } + } + if (sizeof(frontier) == 0 && sizeof(outstanding) == 0) { + sealPhase(); + return; + } + dispatchFree(); + } + + fun dispatchFree() { + var pend: tPendingNode; + var w: machine; + var found: bool; + var i: int; + while (sizeof(frontier) > 0 && freeWorkerExists()) { + pend = frontier[0]; + frontier -= 0; + // S observation point (i): dispatch-time refusal — a + // pending node all of whose admission edges name dead + // generations is dropped (hash removed, G-RULE-2) unless + // a live re-derivation re-admitted it. + if (cfg.lineage == LIN_S && pend.hash in edges && sizeof(edges[pend.hash]) > 0 && allEdgesDead(pend.hash)) { + admitted -= pend.hash; + continue; + } + // Dead-demand dispatch (unreachable honestly: E purges at + // resume, S refuses above) — the purgeOff kill's alarm. + if (pend.hash in edges && sizeof(edges[pend.hash]) > 0 && allEdgesDead(pend.hash)) { + announce eAnnDeadDispatch, (syncN = syncN, node = pend.node, hash = pend.hash); + } + found = false; + i = 0; + while (i < sizeof(workers) && !found) { + if (!(workers[i] in busyW)) { + w = workers[i]; + found = true; + } + i = i + 1; + } + execSeq = execSeq + 1; + outstanding[execSeq] = pend; + owner[execSeq] = w; + busyW[w] = true; + inFlight[pend.node] = execSeq; + announce eAnnExec, (syncN = syncN, attempt = attempt, node = pend.node, gen = pend.gen); + send w, eGDispatch, (pend = pend, execId = execSeq, attempt = attempt, stop = cfg.interrupt == 1 && syncN == cfg.interruptSync && attempt == 1 && pend.node == 1); + } + } + + fun freeWorkerExists(): bool { + var i: int; + i = 0; + while (i < sizeof(workers)) { + if (!(workers[i] in busyW)) { return true; } + i = i + 1; + } + return false; + } + + // Demand admission (G-RULE-2): MUST suppress iff the hash is + // pending or completed this sync; purge/refusal removals make a + // live re-derivation re-admissible. suppressionOff duplicates run + // at the SAME generation (identity-duplicate mutant; the racing + // schedule is the P1-LEGALITY first-find, sequential schedules + // adopt with the declared live-fromGen deviation, R2-N1). + fun admitChild(h: int, parent: int, pgen: int) { + var node: int; + var g: int; + var es: seq[tEdge]; + var dup: bool; + var dupGen: int; + var i: int; + dup = false; + i = 0; + while (i < sizeof(frontier)) { + if (frontier[i].hash == h) { dup = true; dupGen = frontier[i].gen; } + i = i + 1; + } + i = 0; + while (i < sizeof(keys(outstanding))) { + if (outstanding[keys(outstanding)[i]].hash == h) { dup = true; dupGen = outstanding[keys(outstanding)[i]].gen; } + i = i + 1; + } + if (cfg.toggles.suppression && (dup || h in completedH)) { + // Edge-only registration still dirties the carrier: a + // LOST live admitted-by edge mis-arms the ∀-purge + // predicate on resume (GS-CO-003 covers edges too). + // Idempotent per (parent, pgen): the page note and the + // completion carrier derive the same emission once. + if (!hasEdge(h, parent, pgen)) { + if (h in edges) { es = edges[h]; } + es += (sizeof(es), (parent = parent, pgen = pgen)); + edges[h] = es; + carrierDirty = true; + } + return; + } + if (cfg.toggles.demandDrop && (droppedHash == 0 || droppedHash == h)) { + // INJECT (G5f kill): silently drop every admission of the + // first hash a derivation names — a lost derivation + // PATHWAY, not a lost message (per-announce notes and the + // completion carrier derive the same emission twice, so a + // single-message drop is always healed; calibration find). + droppedHash = h; + return; + } + node = h - 1; + if (dup) { + g = dupGen; + } else { + if (node in genTable) { + genTable[node] = genTable[node] + 1; + // Refusal re-admissions are re-mints: bucket-aligned + // under compression (G9-CAL-1, see bumpAndReadmit). + if (cfg.stampCompression && genTable[node] % 2 == 1) { + genTable[node] = genTable[node] + 1; + } + } else { + genTable[node] = 1; + } + g = genTable[node]; + } + admitted[h] = true; + if (h in completedH) { completedH -= h; } + if (!hasEdge(h, parent, pgen)) { + if (h in edges) { es = edges[h]; } + es += (sizeof(es), (parent = parent, pgen = pgen)); + edges[h] = es; + } + frontier += (sizeof(frontier), (node = node, kind = kindOf(cfg.cell, node), hash = h, key = keyOf(cfg.cell, node), gen = g)); + carrierDirty = true; + // Durability rides the CARRIER fence (GS-CO-003), committed + // once after the whole demand loop — never per-mint. + } + + fun hasEdge(h: int, parent: int, pgen: int): bool { + var i: int; + var es: seq[tEdge]; + if (!(h in edges)) { return false; } + es = edges[h]; + i = 0; + while (i < sizeof(es)) { + if (es[i].parent == parent && es[i].pgen == pgen) { return true; } + i = i + 1; + } + return false; + } + + // E+B retraction (SPEC 3): a re-publish under wgen retracts every + // reader execution of the same key's earlier (now-dead) value — + // including re-runs that read a stale value before the re-publish + // landed (the G-pending re-retraction clause, R2-M6(i)). + fun retractReaders(skey: int, writer: int, wgen: int) { + var i: int; + var rd: int; + var seen: seq[int]; + i = 0; + while (i < sizeof(readers)) { + if (readers[i].skey == skey && readers[i].writer == writer && readers[i].wgen < wgen) { + rd = readers[i].reader; + if (!inIntSeq(seen, rd) && rd in genTable && readers[i].rgen == genTable[rd]) { + seen += (sizeof(seen), rd); + if (!inIntSeq(retractQ, rd)) { + retractQ += (sizeof(retractQ), rd); + } + // QUIESCE-BEFORE-BUMP (R2-F1): defer while the + // dying generation's execution is in flight; + // quiesceOff is the G1d kill. + if (cfg.toggles.quiesce && rd in inFlight) { + if (!inIntSeq(deferredBumps, rd)) { + deferredBumps += (sizeof(deferredBumps), rd); + } + } else { + bumpAndReadmit(rd, 2); + } + } + } + i = i + 1; + } + } + + // Mid-attempt bump + re-admission (retraction- or observation- + // forced). MID-BUMP FENCE (R3-F2): the bump's generation-table + // delta commits durably (forced checkpoint) between the bump's + // carrier announce and the new generation's first dispatch; + // midBumpFenceOff is the G1e kill. + fun bumpAndReadmit(node: int, reason: int) { + var h: int; + var i: int; + var updated: bool; + var pend: tPendingNode; + genTable[node] = genTable[node] + 1; + // Under compression every scheduler RE-mint is BUCKET-ALIGNED + // (even): floor-bucketed stamps prove liveness only on bucket + // boundaries, and an unaligned heal re-creates the odd + // generation it just chased one demand-level down — the pass + // then needs O(demand-depth) iterations and reds PASS-BUDGET + // on honest histories (G9-CAL-1). First-admission mints stay + // at 1, so the mixed-parity stamp population compression must + // digest — and its redo cost — remain in the model. + if (cfg.stampCompression && genTable[node] % 2 == 1) { + genTable[node] = genTable[node] + 1; + } + announce eAnnGenBump, (syncN = syncN, node = node, newGen = genTable[node], reason = reason); + h = node + 1; + admitted[h] = true; + if (h in completedH) { completedH -= h; } + // If the node is already pending, re-ground that entry at the + // new generation instead of duplicating it (a duplicate would + // dispatch the dead generation a second time). + updated = false; + i = 0; + while (i < sizeof(frontier)) { + if (frontier[i].node == node) { + pend = frontier[i]; + pend.gen = genTable[node]; + frontier[i] = pend; + updated = true; + } + i = i + 1; + } + if (!updated) { + frontier += (sizeof(frontier), (node = node, kind = kindOf(cfg.cell, node), hash = h, key = keyOf(cfg.cell, node), gen = genTable[node])); + } + if (cfg.toggles.midBumpFence) { + doCheckpoint(true); + if (storeDead) { return; } + } + announce eAnnReadmit, (syncN = syncN, node = node, hash = h, gen = genTable[node], reason = reason); + } + + fun allEdgesDead(h: int): bool { + var i: int; + var es: seq[tEdge]; + es = edges[h]; + i = 0; + while (i < sizeof(es)) { + if (!(es[i].parent in genTable) || es[i].pgen >= genTable[es[i].parent]) { + return false; + } + i = i + 1; + } + return true; + } + + // SEAL SEQUENCE (SPEC 3): frontier drained -> pre-seal pass (S) / + // retraction queue empty (E) -> SWEEP -> eSeal. + fun sealPhase() { + var stamps: map[int, map[int, int]]; + var owners: map[int, int]; + var ks: seq[int]; + var ns: seq[int]; + var i: int; + var j: int; + var forced: bool; + var got: bool; + var keep: seq[int]; + // The pass's domain is the DEMAND CLOSURE: a dead stamp on an + // out-of-closure key owes nothing — the sweep drops the key. + // Chasing it burns the pass budget on an honest history (the + // G5 shrink chassis: the refused node's key keeps its stale + // stamp forever). Recomputed on every sealPhase entry, so a + // forced re-run's naming changes are honored next scan. + keep = computeClosure(); + if (storeDead) { goto DeadSched; } + if (cfg.lineage == LIN_S) { + // PRE-SEAL PASS (observation point (iii)): one iteration = + // one scan over a drained frontier (R3-M3); budget <= 3 + // (R2-M7); a budget-exhausted seal is announce-visible and + // P6-S catches surviving dead stamps at seal. + if (passIters < 3) { + got = false; + send store, eGReadStampsReq, (client = this, agen = agen); + receive { + case eGReadStampsResp: (r: (stamps: map[int, map[int, int]], owners: map[int, int])) { + stamps = r.stamps; + owners = r.owners; + got = true; + } + case eStoreDead: { storeDead = true; } + } + if (!got) { goto DeadSched; } + passIters = passIters + 1; + announce eAnnPassIter, (syncN = syncN, iter = passIters); + forced = false; + ks = keys(stamps); + i = 0; + while (i < sizeof(ks)) { + if (!inIntSeq(keep, ks[i])) { i = i + 1; continue; } + ns = keys(stamps[ks[i]]); + j = 0; + while (j < sizeof(ns)) { + if (ns[j] in genTable && staleStamp(stamps[ks[i]][ns[j]], genTable[ns[j]]) ) { + if (!forcedAlready(ks[i], owners)) { + bumpAndReadmit(owners[ks[i]], 3); + if (storeDead) { goto DeadSched; } + forced = true; + } + // Compressed AMBIGUOUS entry (G9-CAL-1): + // floor(s) == cur - 1 with cur ODD means s + // may EQUAL cur — a live entry the owner's + // re-run cannot make provable (the re-read + // merges the named node's unchanged odd + // generation forever). The heal also bumps + // the NAMED node onto an even generation: + // parity ambiguity dies with the odd gen, + // and any raced re-read that merged the + // old gen becomes unambiguously DEAD for + // the next scan's owner-bump rule. + if (cfg.stampCompression && ns[j] != owners[ks[i]] + && genTable[ns[j]] % 2 == 1 + && stamps[ks[i]][ns[j]] - stamps[ks[i]][ns[j]] % 2 == genTable[ns[j]] - 1) { + if (!nodeInFrontier(ns[j])) { + bumpAndReadmit(ns[j], 3); + if (storeDead) { goto DeadSched; } + forced = true; + } + } + } + j = j + 1; + } + i = i + 1; + } + if (forced) { + // Un-drained frontier: resume dispatch; the next + // scan begins only after it re-drains. + dispatchFree(); + return; + } + } else { + announce eAnnBudgetExhausted, (syncN = syncN,); + } + } + if (cfg.toggles.sweepOverreach && sizeof(keep) > 1) { + // INJECT (G5c kill): drop an in-closure key from the keep + // set (the last discovered, never the root). + keep -= (sizeof(keep) - 1); + } + send store, eGSealReq, (client = this, agen = agen, keep = keep, doSweep = cfg.toggles.sweep, genTable = genTable); + receive { + case eStoreAck: {} + case eStoreDead: { storeDead = true; } + } + if (storeDead) { goto DeadSched; } + send env, eGAttemptEnded, (sealed = true, failed = false); + goto DoneSched; + } + + // Pass staleness test. G9 compression admissibility: under + // stampCompression the pass sees FLOOR-BUCKETED stamps (buckets + // of 2) — lossy but STALE-erring, never false-live (safety + // preserved; extra redos are the recorded cost). The forced + // re-run lands on the bumped (even) generation, so the next scan + // converges. Stored stamps and the P6-S judge stay exact. + fun staleStamp(s: int, cur: int): bool { + if (cfg.stampCompression) { + return (s - s % 2) < cur; + } + return s < cur; + } + + fun nodeInFrontier(n: int): bool { + var i: int; + i = 0; + while (i < sizeof(frontier)) { + if (frontier[i].node == n) { return true; } + i = i + 1; + } + return false; + } + + fun forcedAlready(key: int, owners: map[int, int]): bool { + var i: int; + if (!(key in owners)) { return true; } // ownerless key: nothing to force + i = 0; + while (i < sizeof(frontier)) { + if (frontier[i].node == owners[key]) { return true; } + i = i + 1; + } + return false; + } + + // Final demand closure over CURRENT store content (the durable + // truth survives crashes): BFS from the cell's root keys along + // announced childHash references. + fun computeClosure(): seq[int] { + var out: seq[int]; + var queue: seq[int]; + var k: int; + var i: int; + var rows: seq[tGRow]; + var got: bool; + queue += (0, 0); // every scripted cell roots node 0 / key 0 + while (sizeof(queue) > 0) { + k = queue[0]; + queue -= 0; + if (inIntSeq(out, k)) { continue; } + out += (sizeof(out), k); + got = false; + send store, eGReadRowsReq, (client = this, agen = agen, key = k); + receive { + case eGReadRowsResp: (r: (rows: seq[tGRow], present: bool)) { + rows = r.rows; + got = true; + } + case eStoreDead: { storeDead = true; } + } + if (!got) { return out; } + i = 0; + while (i < sizeof(rows)) { + if (rows[i].childHash > 0 && !inIntSeq(out, rows[i].childHash - 1)) { + queue += (sizeof(queue), rows[i].childHash - 1); + } + i = i + 1; + } + } + return out; + } + + fun doCheckpoint(forced: bool) { + var ck: tGCkpt; + ck = buildCheckpoint(); + send store, eGCheckpointReq, (client = this, agen = agen, ck = ck, forced = forced); + receive { + case eStoreAck: {} + case eStoreDead: { storeDead = true; } + } + } + + // Checkpoint contents pinned by G-RULE-4: pending nodes (frontier + // + in-flight at their current generations), the admitted set, + // admitted-by edges, the generation table, the session index. + fun buildCheckpoint(): tGCkpt { + var pend: seq[tPendingNode]; + var ids: seq[int]; + var i: int; + pend = frontier; + ids = keys(outstanding); + i = 0; + while (i < sizeof(ids)) { + pend += (sizeof(pend), outstanding[ids[i]]); + i = i + 1; + } + return (pending = pend, admitted = admitted, edges = edges, genTable = genTable, readers = readers); + } + + fun rootFrontier() { + // Cell scripts: every cell roots the parent P (node 0). + genTable[0] = 1; + admitted[1] = true; + frontier += (0, (node = 0, kind = NK_PARENT, hash = 1, key = 0, gen = 1)); + } +} + +fun inIntSeq(s: seq[int], v: int): bool { + var i: int; + i = 0; + while (i < sizeof(s)) { + if (s[i] == v) { return true; } + i = i + 1; + } + return false; +} diff --git a/formal/graph/PSrc/Store.p b/formal/graph/PSrc/Store.p new file mode 100644 index 000000000..7992f9c94 --- /dev/null +++ b/formal/graph/PSrc/Store.p @@ -0,0 +1,447 @@ +/* MGStore: the durable store, one per scenario (SPEC 3/5). Holds the + artifact chain (partitions + manifest), unit markers (per-sync), + causal stamps (ride outputs), the session KV, poison flags, and the + frontier checkpoint. Crash protocol is walker parity: eCrash's + queue position partitions the dead attempt's outstanding ops; ops + behind it are dropped on arrival (agen check), never acked; + receivers get eStoreDead and park. + + Poison (SPEC 4b): a second DISTINCT derivation hash committing rows + for one key this sync poisons the key on its first hash-carrying + commit, voids the key's marker (R2-M8), and the eGAdopt refusal is + store-side (R3-M2). Post-poison rounds commit legally; the scope is + seal-excluded by the SealExpect exclusion and P1's exemption. */ + +type tSessCell = (val: int, writer: int, wgen: int); + +machine MGStore { + var prevPart: tGPart; + var prevMan: map[int, int]; + var curPart: tGPart; + var curMan: map[int, int]; + var curMarkers: map[int, tMarker]; + var curStamps: map[int, map[int, int]]; + var curOwners: map[int, int]; // key -> producing node (pass target) + var sessKV: map[int, tSessCell]; + var poisoned: map[int, bool]; + var firstHash: map[int, int]; // key -> first committed derivation hash this sync + var ckpt: tGCkpt; + var hasCkpt: bool; + var deadGens: map[int, bool]; + var syncN: int; + var sealed: bool; + var armed: bool; + var armedGen: int; + var armedClient: machine; + + start state Serving { + on eGReset do (p: (client: machine, syncN: int)) { + if (sealed) { + prevPart = curPart; + prevMan = curMan; + } + curPart = default(tGPart); + curMan = default(map[int, int]); + curMarkers = default(map[int, tMarker]); // markers are per-sync (SPEC 3) + curStamps = default(map[int, map[int, int]]); + curOwners = default(map[int, int]); + sessKV = default(map[int, tSessCell]); + poisoned = default(map[int, bool]); + firstHash = default(map[int, int]); + ckpt = default(tGCkpt); + hasCkpt = false; + sealed = false; + syncN = p.syncN; + send p.client, eStoreAck; + } + + on eGSwapPrev do (p: (client: machine, key: int, epoch: int)) { + // G3 between-attempt rebind (env scripting, no agen — no + // attempt is live): the PREV artifact's manifest and + // partition for the key become sibling content, so the + // resumed consult validates against the actually-current + // base and truthfully FAILs. + var rows: map[int, tGRow]; + rows[0] = (id = 0, epoch = p.epoch, hops = 0, childHash = 0, sVal = -1, sWriter = -1, sWGen = -1); + prevMan[p.key] = p.epoch; + prevPart[p.key] = rows; + send p.client, eStoreAck; + } + + on eGLookupReq do (p: (client: machine, agen: int, key: int)) { + maybeCrash(); + if (p.agen in deadGens) { send p.client, eStoreDead; return; } + if (p.key in prevMan) { + send p.client, eGLookupResp, (hit = true, v = prevMan[p.key]); + } else { + send p.client, eGLookupResp, (hit = false, v = -1); + } + } + + on eGMarkerReadReq do (p: (client: machine, agen: int, key: int)) { + maybeCrash(); + if (p.agen in deadGens) { send p.client, eStoreDead; return; } + if (p.key in curMarkers) { + send p.client, eGMarkerReadResp, (present = true, marker = curMarkers[p.key]); + } else { + send p.client, eGMarkerReadResp, (present = false, marker = default(tMarker)); + } + } + + on eGClearScope do (p: (client: machine, agen: int, key: int, delMarker: bool, ghost: tGGhost)) { + maybeCrash(); + if (p.agen in deadGens) { send p.client, eStoreDead; return; } + // Marker lifecycle rides the clear (R2-F4): the delete is + // announced BEFORE the clear so P-MARK sees the unbind + // first (one atomic op; markerCleanupOff removes this). + if (p.delMarker && p.key in curMarkers) { + curMarkers -= p.key; + announce eAnnMarkerDel, (syncN = syncN, key = p.key); + } + curPart[p.key] = default(map[int, tGRow]); + announce eAnnClear, (syncN = syncN, key = p.key, ghost = p.ghost); + send p.client, eStoreAck; + } + + on eGUpsertPage do (p: (client: machine, agen: int, key: int, rows: seq[tGRow], ghost: tGGhost)) { + var i: int; + var dst: map[int, tGRow]; + maybeCrash(); + if (p.agen in deadGens) { send p.client, eStoreDead; return; } + if (p.key in curPart) { dst = curPart[p.key]; } + i = 0; + while (i < sizeof(p.rows)) { + dst[p.rows[i].id] = p.rows[i]; + i = i + 1; + } + curPart[p.key] = dst; + announce eAnnUpsert, (syncN = syncN, key = p.key, rows = p.rows, ghost = p.ghost); + send p.client, eStoreAck; + } + + on eGPublishEntry do (p: (client: machine, agen: int, key: int, v: int, stamp: map[int, int], hash: int, ghost: tGGhost)) { + maybeCrash(); + if (p.agen in deadGens) { send p.client, eStoreDead; return; } + poisonCheck(p.key, p.hash); + curMan[p.key] = p.v; + curStamps[p.key] = p.stamp; + curOwners[p.key] = p.ghost.node; + announce eAnnPublish, (syncN = syncN, key = p.key, v = p.v, ghost = p.ghost); + send p.client, eStoreAck; + } + + on eGReplayUnit do (p: (client: machine, agen: int, key: int, v: int, marker: tMarker, stamp: map[int, int], hash: int, ghost: tGGhost)) { + var ids: seq[int]; + var i: int; + var r: tGRow; + var dst: map[int, tGRow]; + var g: tGGhost; + var copied: seq[tGRow]; + maybeCrash(); + if (p.agen in deadGens) { send p.client, eStoreDead; return; } + // ONE atomic commit: marker (announced first, P-MARK + // convention), clear, copy, publish. Round completion IS + // unit commit; the publish constituent carries lastOp. + poisonCheck(p.key, p.hash); + g = p.ghost; + g.lastOp = false; + curMarkers[p.key] = p.marker; + announce eAnnMarkerPut, (syncN = syncN, key = p.key, node = p.marker.node, gen = p.marker.gen, roundId = p.marker.roundId, pubBearing = p.marker.pubBearing, contentEpoch = p.marker.contentEpoch, ghost = g); + curPart[p.key] = default(map[int, tGRow]); + announce eAnnClear, (syncN = syncN, key = p.key, ghost = g); + dst = curPart[p.key]; + if (p.key in prevPart) { + ids = keys(prevPart[p.key]); + i = 0; + while (i < sizeof(ids)) { + r = prevPart[p.key][ids[i]]; + r.hops = r.hops + 1; + dst[r.id] = r; + copied += (sizeof(copied), r); + i = i + 1; + } + } + curPart[p.key] = dst; + announce eAnnReplayCopy, (syncN = syncN, key = p.key, vBase = p.ghost.vBase, rows = copied, ghost = g); + curStamps[p.key] = p.stamp; + curOwners[p.key] = p.ghost.node; + curMan[p.key] = p.v; + announce eAnnPublish, (syncN = syncN, key = p.key, v = p.v, ghost = p.ghost); + send p.client, eGUnitResp, (rows = rowsOf(curPart, p.key),); + } + + on eGOverlayUnit do (p: (client: machine, agen: int, key: int, v: int, upserts: seq[tGRow], removes: seq[int], marker: tMarker, stamp: map[int, int], hash: int, composeDead: bool, ghost: tGGhost)) { + var ids: seq[int]; + var i: int; + var r: tGRow; + var dst: map[int, tGRow]; + var g: tGGhost; + var copied: seq[tGRow]; + maybeCrash(); + if (p.agen in deadGens) { send p.client, eStoreDead; return; } + poisonCheck(p.key, p.hash); + g = p.ghost; + g.lastOp = false; + curMarkers[p.key] = p.marker; + announce eAnnMarkerPut, (syncN = syncN, key = p.key, node = p.marker.node, gen = p.marker.gen, roundId = p.marker.roundId, pubBearing = p.marker.pubBearing, contentEpoch = p.marker.contentEpoch, ghost = g); + if (p.composeDead && p.key in curPart && sizeof(curPart[p.key]) > 0) { + // INJECT (G8b kill): compose the diff onto whatever the + // partition already holds — possibly a dead + // generation's partial debris — instead of the unit's + // clear + prev-copy base (the §4b precondition waived). + dst = curPart[p.key]; + announce eAnnReplayCopy, (syncN = syncN, key = p.key, vBase = p.ghost.vBase, rows = copied, ghost = g); + } else { + curPart[p.key] = default(map[int, tGRow]); + announce eAnnClear, (syncN = syncN, key = p.key, ghost = g); + dst = curPart[p.key]; + if (p.key in prevPart) { + ids = keys(prevPart[p.key]); + i = 0; + while (i < sizeof(ids)) { + r = prevPart[p.key][ids[i]]; + r.hops = r.hops + 1; + dst[r.id] = r; + copied += (sizeof(copied), r); + i = i + 1; + } + } + announce eAnnReplayCopy, (syncN = syncN, key = p.key, vBase = p.ghost.vBase, rows = copied, ghost = g); + } + i = 0; + while (i < sizeof(p.upserts)) { + dst[p.upserts[i].id] = p.upserts[i]; + i = i + 1; + } + announce eAnnUpsert, (syncN = syncN, key = p.key, rows = p.upserts, ghost = g); + i = 0; + while (i < sizeof(p.removes)) { + if (p.removes[i] in dst) { dst -= p.removes[i]; } + i = i + 1; + } + announce eAnnTombstones, (syncN = syncN, key = p.key, removes = p.removes, ghost = g); + curPart[p.key] = dst; + curStamps[p.key] = p.stamp; + curOwners[p.key] = p.ghost.node; + curMan[p.key] = p.v; + announce eAnnPublish, (syncN = syncN, key = p.key, v = p.v, ghost = p.ghost); + send p.client, eGUnitResp, (rows = rowsOf(curPart, p.key),); + } + + on eGAdoptReq do (p: (client: machine, agen: int, key: int, node: int, fromGen: int, toGen: int, roundId: int, allowLiveFrom: bool, ghost: tGGhost)) { + var m: tMarker; + var st: map[int, int]; + var outRows: seq[tGRow]; + var ids: seq[int]; + var i: int; + var deadFrom: bool; + maybeCrash(); + if (p.agen in deadGens) { send p.client, eStoreDead; return; } + // Store-side preconditions: marker present and not voided; + // key not poisoned (R3-M2); fromGen dead per the last + // durable generation table (R2-N1; allowLiveFrom is the + // suppressionOff declared deviation). + deadFrom = hasCkpt && p.node in ckpt.genTable && p.fromGen < ckpt.genTable[p.node]; + if (!(p.key in curMarkers) || curMarkers[p.key].voided || (p.key in poisoned) || (!deadFrom && !p.allowLiveFrom)) { + send p.client, eGAdoptResp, (ok = false, rows = outRows); + return; + } + m = curMarkers[p.key]; + m.gen = p.toGen; + m.roundId = p.ghost.roundId; + curMarkers[p.key] = m; + if (p.key in curStamps) { st = curStamps[p.key]; } + if (p.node in st) { st -= p.node; } + st[p.node] = p.toGen; + curStamps[p.key] = st; + if (p.key in curPart) { + ids = keys(curPart[p.key]); + i = 0; + while (i < sizeof(ids)) { + outRows += (sizeof(outRows), curPart[p.key][ids[i]]); + i = i + 1; + } + } + announce eAnnAdopt, (syncN = syncN, key = p.key, node = p.node, fromGen = p.fromGen, toGen = p.toGen, adoptedRoundId = p.roundId, rows = outRows, ghost = p.ghost); + send p.client, eGAdoptResp, (ok = true, rows = outRows); + } + + on eGSessionPub do (p: (client: machine, agen: int, skey: int, val: int, writer: int, wgen: int, ghost: tGGhost)) { + maybeCrash(); + if (p.agen in deadGens) { send p.client, eStoreDead; return; } + sessKV[p.skey] = (val = p.val, writer = p.writer, wgen = p.wgen); + announce eAnnSessionSet, (syncN = syncN, skey = p.skey, val = p.val, writer = p.writer, wgen = p.wgen, ghost = p.ghost); + send p.client, eStoreAck; + } + + on eGSessionGetReq do (p: (client: machine, agen: int, reader: int, rgen: int, skey: int)) { + var c: tSessCell; + maybeCrash(); + if (p.agen in deadGens) { send p.client, eStoreDead; return; } + if (p.skey in sessKV) { + c = sessKV[p.skey]; + announce eAnnSessionRead, (syncN = syncN, reader = p.reader, rgen = p.rgen, skey = p.skey, found = true, val = c.val, writer = c.writer, wgen = c.wgen); + send p.client, eGSessionGetResp, (found = true, val = c.val, writer = c.writer, wgen = c.wgen); + } else { + announce eAnnSessionRead, (syncN = syncN, reader = p.reader, rgen = p.rgen, skey = p.skey, found = false, val = -1, writer = -1, wgen = -1); + send p.client, eGSessionGetResp, (found = false, val = -1, writer = -1, wgen = -1); + } + } + + on eGCheckpointReq do (p: (client: machine, agen: int, ck: tGCkpt, forced: bool)) { + maybeCrash(); + if (p.agen in deadGens) { send p.client, eStoreDead; return; } + ckpt = p.ck; + hasCkpt = true; + announce eAnnCheckpoint, (syncN = syncN, forced = p.forced); + send p.client, eStoreAck; + } + + on eGReadCkptReq do (p: (client: machine)) { + send p.client, eGReadCkptResp, (ck = ckpt, has = hasCkpt); + } + + on eGReadRowsReq do (p: (client: machine, agen: int, key: int)) { + var out: seq[tGRow]; + var ids: seq[int]; + var i: int; + maybeCrash(); + if (p.agen in deadGens) { send p.client, eStoreDead; return; } + if (p.key in curPart) { + ids = keys(curPart[p.key]); + i = 0; + while (i < sizeof(ids)) { + out += (sizeof(out), curPart[p.key][ids[i]]); + i = i + 1; + } + send p.client, eGReadRowsResp, (rows = out, present = true); + } else { + send p.client, eGReadRowsResp, (rows = out, present = false); + } + } + + on eGReadStampsReq do (p: (client: machine, agen: int)) { + maybeCrash(); + if (p.agen in deadGens) { send p.client, eStoreDead; return; } + send p.client, eGReadStampsResp, (stamps = curStamps, owners = curOwners); + } + + on eGSealReq do (p: (client: machine, agen: int, keep: seq[int], doSweep: bool, genTable: map[int, int])) { + // Guaranteed resolution point for an armed crash (walker + // parity): fires either just before the seal or right after. + if (armed && armedGen == p.agen) { + if (choose(2) == 0) { + fireCrash(); + } else { + commitSeal(p.keep, p.doSweep, p.genTable); + send p.client, eStoreAck; + fireCrash(); + return; + } + } + if (p.agen in deadGens) { send p.client, eStoreDead; return; } + commitSeal(p.keep, p.doSweep, p.genTable); + send p.client, eStoreAck; + } + + on eReadSealedReq do (p: (client: machine)) { + send p.client, eReadSealedResp, (sealed = sealed,); + } + + on eCrashArm do (p: (client: machine, agen: int)) { + armed = true; + armedGen = p.agen; + armedClient = p.client; + send p.client, eCrashArmed; + } + } + + // Seal-time sweep (SPEC 3 seal sequence): drop partitions, + // manifest entries, and stamps for keys outside the final demand + // closure. Markers never travel into the sealed artifact (per-sync + // scoping pin) — modeled by curMarkers being sync-local state that + // eGReset discards. + fun commitSeal(keep: seq[int], doSweep: bool, genTable: map[int, int]) { + var ks: seq[int]; + var i: int; + if (doSweep) { + ks = keys(curPart); + i = 0; + while (i < sizeof(ks)) { + if (!inKeep(keep, ks[i])) { + curPart -= ks[i]; + if (ks[i] in curMan) { curMan -= ks[i]; } + if (ks[i] in curStamps) { curStamps -= ks[i]; } + } + i = i + 1; + } + ks = keys(curMan); + i = 0; + while (i < sizeof(ks)) { + if (!inKeep(keep, ks[i])) { + curMan -= ks[i]; + } + i = i + 1; + } + } + sealed = true; + announce eAnnGSeal, (syncN = syncN, partition = curPart, manifest = curMan, stamps = curStamps, genTable = genTable); + } + + fun inKeep(keep: seq[int], k: int): bool { + var i: int; + i = 0; + while (i < sizeof(keep)) { + if (keep[i] == k) { return true; } + i = i + 1; + } + return false; + } + + // Same-key distinct-derivation detection (SPEC 4b poison row): + // the second distinct hash's first hash-carrying commit poisons + // the key and VOIDS its marker (R2-M8). + fun poisonCheck(key: int, hash: int) { + var m: tMarker; + if (key in firstHash && firstHash[key] != hash) { + if (!(key in poisoned)) { + poisoned[key] = true; + if (key in curMarkers) { + m = curMarkers[key]; + m.voided = true; + curMarkers[key] = m; + } + announce eAnnPoison, (syncN = syncN, key = key); + } + return; + } + firstHash[key] = hash; + } + + fun maybeCrash() { + if (armed && choose(2) == 0) { + fireCrash(); + } + } + + fun rowsOf(part: tGPart, key: int): seq[tGRow] { + var out: seq[tGRow]; + var ids: seq[int]; + var i: int; + if (!(key in part)) { return out; } + ids = keys(part[key]); + i = 0; + while (i < sizeof(ids)) { + out += (sizeof(out), part[key][ids[i]]); + i = i + 1; + } + return out; + } + + fun fireCrash() { + deadGens[armedGen] = true; + armed = false; + announce eAnnCrash, (syncN = syncN,); + send armedClient, eCrashAck; + } +} diff --git a/formal/graph/PSrc/Types.p b/formal/graph/PSrc/Types.p new file mode 100644 index 000000000..0da353a3d --- /dev/null +++ b/formal/graph/PSrc/Types.p @@ -0,0 +1,241 @@ +/* Types for the demand-graph runtime model (deliverable 4). + Source of truth: formal/GRAPH_MODEL_SPEC.md (v4, FROZEN). + Small scope (SPEC 8): <=4 nodes, epochs 1..3, row ids 0..1, + <=3 syncs, <=3 attempts/sync, 2 workers, <=2 pages/round, + pass-iteration budget <=3. + + Identity conventions (build decision, logged in CALIBRATION.md): + nodes are ints 0..3; a node's output key = its node id; a node's + derivation hash = node id + 1 (so childHash 0 = "no child named"); + the G8c distinct-derivation-same-key shape overrides the key + mapping per cell. Generations are per-node monotone counters in + the scheduler table (G-RULE-3); attempt ids (store crash gating) + are syncN*10 + attempt, disjoint from node generations. */ + +// Node kinds: scripted execution bodies (SPEC 3 MNodeExec). +enum tNodeKind { NK_PARENT, NK_CONSULT, NK_WRITER, NK_READER } + +// Verdict classes (SPEC 3: ADOPT is a registered verdict class, +// round-2 R2-F3, alongside replay / changed-with-diff / fetch-fresh). +enum tGVerdict { GV_ADOPT, GV_REPLAY, GV_DIFF, GV_FRESH } + +// Lineage variants (SPEC 1: both first-class scheduler modes). +enum tLineage { LIN_E, LIN_S } + +// Session store variants (SPEC 3 MSessionStore). +enum tSessVar { SESS_A, SESS_B } + +// Consult/revalidation outcome (digest vocabulary, SPEC 4a). +// 0 = MISS, 1 = MATCH, 2 = FAIL — ints so digests compare with ==. + +// A stored row. epoch is the ghost content tag (truthful upstream), +// hops the P2 replay-travel counter, childHash the demand-derivation +// content (G-RULE-1: structure rides emissions; 0 = none), and the +// sess* triple the embedded session read (sVal -1 = none) for the +// P6-G value comparison. +type tGRow = (id: int, epoch: int, hops: int, childHash: int, sVal: int, sWriter: int, sWGen: int); + +// key -> row id -> row +type tGPart = map[int, map[int, tGRow]]; + +// Premise digest (SPEC 4a): canonical hash of the consult result +// (previous entry + revalidation outcome) and the identity+writer- +// stamp of every session value read before unit commit. One session +// read per node in every scripted cell (build decision). +type tDigest = (v: int, outcome: int, sVal: int, sWriter: int, sWGen: int, hasSess: bool); + +// Unit marker (SPEC 3/4a): per-sync store row. roundId identifies the +// marked round for P-MARK; contentEpoch is the marked round's output +// content tag (replay -> vBase, overlay -> consult epoch). +type tMarker = (node: int, gen: int, roundId: int, digest: tDigest, pubBearing: bool, voided: bool, contentEpoch: int); + +// A pending node on the frontier (G-RULE-4 checkpoint row). +type tPendingNode = (node: int, kind: tNodeKind, hash: int, key: int, gen: int); + +// Generation-qualified admitted-by edge (variant E lineage state, +// durable per G-RULE-4). +type tEdge = (parent: int, pgen: int); + +// Session read record (scheduler session index per variant; in the +// checkpoint per G-RULE-4). +type tReadRec = (reader: int, rgen: int, skey: int, val: int, writer: int, wgen: int); + +// Session publish record (worker completion report). +type tPub = (skey: int, val: int, wgen: int); + +// Frontier checkpoint (G-RULE-4, total): pending nodes, the +// admitted-derivation set, admitted-by edges, the generation table, +// the session index. Variant E support counts are derived (rebuild +// target = checkpoint-consistent value). +type tGCkpt = (pending: seq[tPendingNode], admitted: map[int, bool], edges: map[int, seq[tEdge]], genTable: map[int, int], readers: seq[tReadRec]); + +// Mitigation/mechanism toggles (SPEC 6). Positive names, default ON +// where the toggle REMOVES a mechanism (walker convention); the two +// injections (adoptOnFail, writerAdopt) default OFF. +type tGToggles = ( + suppression: bool, // G-RULE-2 admission suppression + sweep: bool, // seal-time sweep + sweepOverreach: bool, // INJECT: sweep drops an in-closure key + purge: bool, // E pending-purge on death (∀-predicate) + stampMerge: bool, // S read-side stamp merge + retraction: bool, // E+B re-publish reader retraction + overlayComposeDead: bool, // INJECT: overlay over a dead base + resumeCkpt: bool, // forced resume checkpoint (F4) + demandDrop: bool, // INJECT: drop one derived admission + adoptOnFail: bool, // INJECT: waive MATCH-only eligibility + writerAdopt: bool, // INJECT: waive writer-ineligibility + quiesce: bool, // quiesce-before-bump (R2-F1) + midBumpFence: bool, // mid-bump fence (R3-F2) + markerCleanup: bool // REPLACES clear deletes the marker (R2-F4) +); + +// Scenario configuration, built per test cell in PTst via +// defaultGCfg() + field overrides. +type tGCfg = ( + // Cell topology / script id: + // 11 = G1 family: parent P (node 0) -> consult node C (node 1); + // P's fresh rows all name C (double-announce premise). + // 21 = G2 family: P (node 0) -> writer H (node 2) + reader G + // (node 3); H publishes session key 0; G reads it (phase 2). + // 24 = G5 family: PAGINATED parent P (node 0) -> consult node C + // (node 1), named on P's page-1 row ONLY (the row the + // e1->e2 mutation deletes: upstream demand shrink). The + // mutation target is P's OWN scope (key 0). + // 25 = G3 (artifact swap + rebind): cell-11 topology; interrupt 1 + // stops C after its attempt-1 consult; the env swaps C's + // PREV artifact to sibling content between attempts. + // 26 = G6a/G6b (redo bake-off chain): P (node 0) -> S1 (node 1, + // names C on its page-1 row) -> C (node 4) -> GC (node 5). + // G6b mutates S1's scope (key 1) between attempts: the + // descendant chain's demand shrinks. + // 27 = G7 (progress under churn): cell-11 topology; node 1 fails + // LOUD deterministically in failSync (generation-blind + // fingerprint; the abandon ladder is cfg.ladder). + // 28 = G6c (fan-in): P names S1 (node 1) + S2 (node 4); BOTH name + // C (node 5) — two admitted-by edges; a crash killing one + // parent must not purge/refuse C on the survivor's live edge. + // 29 = G8c (same-key distinct-derivation): P names node 1 AND + // node 4; keyOf maps BOTH to output key 1 (distinct hashes, + // one key) — the store poisons on the second derivation's + // first commit. + cell: int, + lineage: tLineage, + sessVar: tSessVar, + // Interruption script: 0 = none; 1 = graceful stop at C's + // attempt-1 consult (G3: stop-forced checkpoint, store intact); + // 2 = hard crash in attempt 1 of interruptSync; 3 = hard crash + // in attempts 1 AND 2 (two-crash cells: G1b, G1e, G8d). + interrupt: int, + interruptSync: int, + nSyncs: int, + mutateBetweenSyncs: bool, // key 1: e1 -> e2 after sync 1 + mutateBetweenAttempts: bool, // key 1: +1 between attempts of interruptSync + // Connector policy (verdicts are connector choice, walker + // precedent): FAIL verdict yields CHANGED-WITH-DIFF unit when + // diffPolicy, else fetch-fresh record round; a re-derivation + // after an ineligible/failed marker check uses fetch-fresh when + // rederiveFresh (the G1(ii)/G1c scripted shape). + diffPolicy: bool, + rederiveFresh: bool, + // Worker pool size (SPEC 8 budget: 2). The G1d cell scripts 3 so + // the retraction-forced re-run can dispatch AT the bump instead + // of waiting for a completion to free a worker — with 2, the + // dying-reader race needs two long starvation phases and random + // search cannot reach it (calibration find, logged). + nWorkers: int, + // Content flap-back (G2 flap-back probe, G8d): raw epoch >= 3 + // serves content identical to epoch 1; the upstream reports + // CONTENT epochs everywhere (raw epochs never escape it), so + // validators, manifests, expectations, and folds stay coherent. + flapBack: bool, + // G6 bake-off count oracle: max executions per node per sync + // (0 = unmonitored). The minimal GREEN bound is the + // checker-verified worst-case redo count for the leg. + execBound: int, + // G7 loud-failure script: failNode fails deterministically at + // execution start in failSync (-1 = none). The abandon ladder + // (abandon after 2 identical generation-blind fingerprints) is + // the proposed machinery; ladder=false is the P4-STUCK kill. + failNode: int, + failSync: int, + ladder: bool, + // G9 compression admissibility: the S pre-seal pass compares + // FLOOR-BUCKETED stamps (buckets of 2) — lossy but stale-erring + // (never false-live); forced re-runs land on the bumped (even) + // generation, so the pass still converges. Stored stamps and + // monitors stay exact: the claim is about mechanism decisions. + stampCompression: bool, + // G5d meta-analysis (GS-CO-005(d)): when non-empty, the env + // announces this map as the GSEALWORLD probe target for + // interruptSync's seal (world = manifest restricted to keys + // sealing non-empty partitions). Empty = probe disarmed. + sealWorld: map[int, int], + toggles: tGToggles +); + +fun defaultGToggles(): tGToggles { + return ( + suppression = true, + sweep = true, + sweepOverreach = false, + purge = true, + stampMerge = true, + retraction = true, + overlayComposeDead = false, + resumeCkpt = true, + demandDrop = false, + adoptOnFail = false, + writerAdopt = false, + quiesce = true, + midBumpFence = true, + markerCleanup = true + ); +} + +fun defaultGCfg(): tGCfg { + return ( + cell = 11, + lineage = LIN_E, + sessVar = SESS_A, + interrupt = 0, + interruptSync = 2, + nSyncs = 2, + mutateBetweenSyncs = false, + mutateBetweenAttempts = false, + diffPolicy = false, + rederiveFresh = false, + nWorkers = 2, + flapBack = false, + execBound = 0, + failNode = -1, + failSync = 0, + ladder = true, + stampCompression = false, + sealWorld = default(map[int, int]), + toggles = defaultGToggles() + ); +} + +// Round ghost carried on store ops for the monitors (walker parity; +// node/gen added for P-GEN attribution, vBase for the fold). +type tGGhost = (roundId: int, verdict: tGVerdict, consultEpoch: int, vBase: int, lastOp: bool, attempt: int, node: int, gen: int); + +// Node kind script (shared by scheduler re-admissions and env roots). +fun kindOf(cell: int, node: int): tNodeKind { + if (node == 0) { return NK_PARENT; } + if (node == 2) { return NK_WRITER; } + if (node == 3) { return NK_READER; } + return NK_CONSULT; +} + +// Output-key script: key = node id everywhere EXCEPT the same-key +// distinct-derivation cell (G8c), where node 4 shares node 1's +// output key — distinct hashes, one key, the poison row's premise. +fun keyOf(cell: int, node: int): int { + if (cell == 29 && node == 4) { return 1; } + return node; +} + +// Worker completion report payload (the carrier announce whose atomic +// processing hosts all derived effects, G-RULE-1). +type tGReport = (execId: int, node: int, gen: int, hash: int, key: int, kind: tNodeKind, verdict: tGVerdict, rows: seq[tGRow], pubs: seq[tPub], reads: seq[tReadRec], aborted: bool); diff --git a/formal/graph/PSrc/Upstream.p b/formal/graph/PSrc/Upstream.p new file mode 100644 index 000000000..781ea80fd --- /dev/null +++ b/formal/graph/PSrc/Upstream.p @@ -0,0 +1,111 @@ +/* MGUpstream: the external system of record (walker parity). + Truthful: a validator (content-epoch-valued) validates iff its + content equals the current content. Row table: epoch 1 has rows + {0,1}; epoch >= 2 has {0} (row 1 deleted at the e1->e2 mutation), + so unions and resurrections are content-visible with 2 row ids. + + FLAP-BACK (G2 flap-back probe, G8d): with cfg.flapBack, raw epoch + >= 3 serves content identical to epoch 1. The upstream reports + CONTENT epochs everywhere — validate responses, fetched rows, diff + pages — so raw epochs never escape it and every downstream consumer + (markers, manifests, folds, SealExpect worlds) stays coherent. */ + +fun upstreamRowIds(epoch: int): seq[int] { + var ids: seq[int]; + ids += (0, 0); + if (epoch <= 1) { + ids += (1, 1); + } + return ids; +} + +// rows(k, e) as id -> content-epoch map (monitor-side fold table). +fun rowsAt(epoch: int): map[int, int] { + var ids: seq[int]; + var m: map[int, int]; + var i: int; + ids = upstreamRowIds(epoch); + i = 0; + while (i < sizeof(ids)) { + m[ids[i]] = epoch; + i = i + 1; + } + return m; +} + +machine MGUpstream { + var epochs: map[int, int]; // key -> current RAW epoch + var flapBack: bool; + + start state Serving { + entry (p: (flapBack: bool)) { + flapBack = p.flapBack; + epochs[0] = 1; + epochs[1] = 1; + epochs[2] = 1; + epochs[3] = 1; + epochs[4] = 1; + epochs[5] = 1; + } + + on eValidateReq do (p: (client: machine, scope: int, v: int)) { + send p.client, eValidateResp, (ok = p.v == contentEpoch(p.scope), epoch = contentEpoch(p.scope)); + } + + on eFetchReq do (p: (client: machine, scope: int, page: int)) { + var ids: seq[int]; + var rows: seq[tGRow]; + var e: int; + e = contentEpoch(p.scope); + ids = upstreamRowIds(e); + if (p.page < sizeof(ids)) { + rows += (0, (id = ids[p.page], epoch = e, hops = 0, childHash = 0, sVal = -1, sWriter = -1, sWGen = -1)); + } + // Fixed 2-page rounds (pages per key per round <= 2). + send p.client, eFetchResp, (rows = rows, epoch = e, morePages = p.page == 0); + } + + on eDiffReq do (p: (client: machine, scope: int, fromEpoch: int, page: int)) { + var e: int; + var ups: seq[tGRow]; + var rms: seq[int]; + var fromIds: seq[int]; + var toIds: seq[int]; + var i: int; + e = contentEpoch(p.scope); + // Truthful total diff from the base content to the current + // content: page 0 upserts (every current row when content + // differs), page 1 removes (base rows absent now). + if (p.page == 0) { + if (p.fromEpoch != e) { + toIds = upstreamRowIds(e); + i = 0; + while (i < sizeof(toIds)) { + ups += (sizeof(ups), (id = toIds[i], epoch = e, hops = 0, childHash = 0, sVal = -1, sWriter = -1, sWGen = -1)); + i = i + 1; + } + } + } else { + fromIds = upstreamRowIds(p.fromEpoch); + i = 0; + while (i < sizeof(fromIds)) { + if (!(fromIds[i] in rowsAt(e))) { + rms += (sizeof(rms), fromIds[i]); + } + i = i + 1; + } + } + send p.client, eDiffResp, (upserts = ups, removes = rms, epoch = e, morePages = p.page == 0); + } + + on eMutate do (p: (client: machine, scope: int)) { + epochs[p.scope] = epochs[p.scope] + 1; + send p.client, eMutateAck; + } + } + + fun contentEpoch(scope: int): int { + if (flapBack && epochs[scope] >= 3) { return 1; } + return epochs[scope]; + } +} diff --git a/formal/graph/PTst/ScenarioG1.p b/formal/graph/PTst/ScenarioG1.p new file mode 100644 index 000000000..9ac5b67f3 --- /dev/null +++ b/formal/graph/PTst/ScenarioG1.p @@ -0,0 +1,167 @@ +/* G1 family — phantom-union premises under the graph runtime + (SPEC 9). Topology cell 11: parent P (node 0, fetch-fresh, every + row names C) -> consult node C (node 1, marker machinery). + Expected verdicts (declared before first run): + - tcG1i_All: leg (i) crash-before/around-commit, fetch-fresh + policy (diffPolicy off), e1->e2 between syncs — GREEN all. + - tcG1ii_All: leg (ii) diff unit + crash + e2->e3 between + attempts, re-derive fetch-fresh (MATCH-only forces re-derive; + record REPLACES deletes the marker) — GREEN all. (Also the G1c + honest chassis.) + - tcG1iii_E / tcG1iii_S: leg (iii) crash-after-commit, no + mutation — MATCH, digest equal, writer bit clear -> ADOPT — + GREEN all, both lineage variants. + - tcG1sup_P1: suppressionOff on the leg-(iii) chassis — + P1-LEGALITY first-find on the racing double-admission schedule + (sequential schedules adopt under the declared live-fromGen + deviation, R2-N1). RED. + - tcG1b_All: two-crash generation-reuse probe, honest — GREEN + (forced resume checkpoint fences resume minting, F4). + - tcG1bMut_PGEN: resumeCkptOff — P-GEN RED (attempts 2 and 3 + re-mint one generation from the stale table). + - tcG1c_All: FAIL-adopt probe honest (same chassis as leg (ii)): + MATCH-only forces re-derive, fetch reflects e3 — GREEN. + - tcG1cMut_Adopt: adoptOnFail — adopts rows(e2) after a FAIL + consult — P-ADOPT RED. (Calibration find G1-CAL-1 moved this + kill off SealExpect: completed-across-crash schedules seal + attempt-1 content honestly under sync-scoped freshness, so the + scripted expectation accepts any attempt-start world and the + FAIL-adopt laundering is only mechanism-visible.) + - tcG1cMut_P3: adoptOnFail — GP3prime stays GREEN (the FAIL + re-consult does not qualify; the last qualifying verdict is + attempt 1's diff at e2, which expects the mutant's own rows — + the adopt-legality monitor is the only oracle that flips). */ + +machine TestG1i { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 11; + c.interrupt = 2; + c.mutateBetweenSyncs = true; + new MGEnv(c); + } + } +} + +machine TestG1ii { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 11; + c.interrupt = 2; + c.mutateBetweenSyncs = true; + c.mutateBetweenAttempts = true; + c.diffPolicy = true; + c.rederiveFresh = true; + new MGEnv(c); + } + } +} + +machine TestG1iiiE { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 11; + c.interrupt = 2; + new MGEnv(c); + } + } +} + +machine TestG1iiiS { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 11; + c.interrupt = 2; + c.lineage = LIN_S; + new MGEnv(c); + } + } +} + +machine TestG1sup { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 11; + c.interrupt = 2; + t = defaultGToggles(); + t.suppression = false; + c.toggles = t; + new MGEnv(c); + } + } +} + +machine TestG1b { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 11; + c.interrupt = 3; + new MGEnv(c); + } + } +} + +machine TestG1bMut { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 11; + c.interrupt = 3; + t = defaultGToggles(); + t.resumeCkpt = false; + c.toggles = t; + new MGEnv(c); + } + } +} + +machine TestG1cMut { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 11; + c.interrupt = 2; + c.mutateBetweenSyncs = true; + c.mutateBetweenAttempts = true; + c.diffPolicy = true; + c.rederiveFresh = true; + t = defaultGToggles(); + t.adoptOnFail = true; + c.toggles = t; + new MGEnv(c); + } + } +} + +module Graph = { MGEnv, MGStore, MGUpstream, MGraphSched, MGNodeExec }; + +// Expected GREEN: +test tcG1i_All [main=TestG1i]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG in (union Graph, { TestG1i }); +test tcG1ii_All [main=TestG1ii]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG in (union Graph, { TestG1ii }); +test tcG1iii_E [main=TestG1iiiE]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG in (union Graph, { TestG1iiiE }); +test tcG1iii_S [main=TestG1iiiS]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP6S, GPASS in (union Graph, { TestG1iiiS }); +test tcG1b_All [main=TestG1b]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG in (union Graph, { TestG1b }); +test tcG1cMut_P3 [main=TestG1cMut]: assert GP3prime in (union Graph, { TestG1cMut }); +test tcG1cMut_Seal [main=TestG1cMut]: assert SealExpectG in (union Graph, { TestG1cMut }); + +// Expected RED (counterexample = the calibration find): +test tcG1sup_P1 [main=TestG1sup]: assert GP1 in (union Graph, { TestG1sup }); +test tcG1bMut_PGEN [main=TestG1bMut]: assert PGEN in (union Graph, { TestG1bMut }); +test tcG1cMut_Adopt [main=TestG1cMut]: assert PADOPT in (union Graph, { TestG1cMut }); diff --git a/formal/graph/PTst/ScenarioG2.p b/formal/graph/PTst/ScenarioG2.p new file mode 100644 index 000000000..40ef2ee2b --- /dev/null +++ b/formal/graph/PTst/ScenarioG2.p @@ -0,0 +1,324 @@ +/* G2 family — session laundering (SPEC 9), plus the G1d/G1e probes + that ride the session chassis. Topology cell 21: parent P (node 0, + row 0 names writer H = node 2, row 1 names reader G = node 3). H + publishes session key 0 (value = its consult epoch: same-premise + re-derivations re-publish the same value); G reads it and embeds + the value in its rows. + + Common chassis: 2 syncs, crash in sync 2 attempt 1. The LAUNDERING + legs mutate H's scope between attempts so H's re-publish value + differs; the ANNOUNCE-WINDOW / flap-back legs are premise-stable + (no mutation) so adoption eligibility and same-value re-publish + are reachable. + + Expected verdicts (declared before first run): + - tcG2ea_P6G: E + session variant A — THE FINDING: no retraction, + no stamps; a schedule where G completed (+ckpt) before the + crash and H re-published a different value seals G's rows + embedding the dead value — P6-G RED. + - tcG2ea_Core: same cell, artifact-level monitors only — GREEN + (the laundering is invisible to P1/P2/P3'/SealExpect: the + blindness contrast is the point). + - tcG2eb_All: E + variant B (retraction + quiesce) — GREEN all + (incl. P6-G, P6-E). + - tcG2ebRetrOff_P6G: retractionOff — P6-G RED. + - tcG2s_All: S (stamps + pre-seal pass) — GREEN all (incl. P6-S). + - tcG2sStampOff_P6G: stampMergeOff — P6-G RED (the pass never + sees the dead writer stamp on G's output). + - tcG2awE_All / tcG2awS_All: announce-window + writer flap-back + honest legs (premise-stable): H's publish-bearing marker is + adoption-INELIGIBLE -> REPLAY re-derive -> re-publish same + value under the new generation -> readers cleared (retraction / + stamp delta) — GREEN all. + - tcG2awE_Redo / tcG2awS_Redo: REDO-PROBE existence exhibits on + the same chassis — RED (the at-least-once cost is real: a + forced reader redo exists). + - tcG2awWA_E: writerAdopt — H adopts, never re-publishes; the + dead-generation publish strands; G is never retracted — P6-E + RED. (P6-G stays green: the value is unchanged — the kill is + mechanism-visible only.) + - tcG2awWA_S: writerAdopt — G's re-reads keep merging the dead + {H: g1} stamp; the pass exhausts its budget — P6-S RED. + - tcG1d_P6G: E+B chassis + quiesceOff (round-2 R2-F1): the + retraction bump lands while the dying reader executes; the dead + execution's late record round overwrites the live re-derivation + — P6-G RED. Honest quiesce covered by tcG2eb_All. + - tcG2eb2c_All: honest two-crash session chassis — GREEN. + - tcG1e_PGEN: two crashes + midBumpFenceOff (round-3 R3-F2) — + P-GEN RED (generation identity reuse; the first-find may fire + on the first-admission mint path, which rides the same toggle + per GS-CO-001 — same discipline, same monitor). + - tcG2fbE_All / tcG2fbS_All: WRITER FLAP-BACK PROBE (round-3 + R3-F1's second registration): sync-2 attempt 1 commits H's + DIFF-verdict publish-bearing unit (d2, marker digest FAIL); + crash in the announce window; upstream flaps back + (content(e3) = content(e1)). Resume: H ineligible + (pubBearing + digest delta) -> re-derives -> re-consult vs the + PREV artifact (V1) MATCHes truthfully -> REPLAY verdict -> the + body-op pin re-publishes d1@g2 anyway -> readers cleared + (retraction / stamp delta) -> GREEN all. Under an elision + reading this history strands d2@g1 and reds P6-G/P6-S honestly + — the probe is the body-op pin's load-bearing witness. + - tcG2fbE_Redo: REDO-PROBE RED on the flap-back chassis (the + forced reader redo exists — the declared expected count >= 1). + - tcG2ebPend_Redo: REDO-PROBE RED on the honest E+B laundering + chassis — the G-pending re-retraction clause (R2-M6(i), + GS-CO-004's catch-up + re-publish paths) actually fires. */ + +machine TestG2ea { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_E; + c.sessVar = SESS_A; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + new MGEnv(c); + } + } +} + +machine TestG2eb { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_E; + c.sessVar = SESS_B; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + new MGEnv(c); + } + } +} + +machine TestG2ebRetrOff { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_E; + c.sessVar = SESS_B; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + t = defaultGToggles(); + t.retraction = false; + c.toggles = t; + new MGEnv(c); + } + } +} + +machine TestG2s { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_S; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + new MGEnv(c); + } + } +} + +machine TestG2sStampOff { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_S; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + t = defaultGToggles(); + t.stampMerge = false; + c.toggles = t; + new MGEnv(c); + } + } +} + +machine TestG2awE { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_E; + c.sessVar = SESS_B; + c.interrupt = 2; + new MGEnv(c); + } + } +} + +machine TestG2awS { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_S; + c.interrupt = 2; + new MGEnv(c); + } + } +} + +machine TestG2awWAE { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_E; + c.sessVar = SESS_B; + c.interrupt = 2; + t = defaultGToggles(); + t.writerAdopt = true; + c.toggles = t; + new MGEnv(c); + } + } +} + +machine TestG2awWAS { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_S; + c.interrupt = 2; + t = defaultGToggles(); + t.writerAdopt = true; + c.toggles = t; + new MGEnv(c); + } + } +} + +machine TestG1d { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_E; + c.sessVar = SESS_B; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + c.nWorkers = 3; + t = defaultGToggles(); + t.quiesce = false; + c.toggles = t; + new MGEnv(c); + } + } +} + +machine TestG2eb2c { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_E; + c.sessVar = SESS_B; + c.interrupt = 3; + c.mutateBetweenAttempts = true; + new MGEnv(c); + } + } +} + +machine TestG2fbE { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_E; + c.sessVar = SESS_B; + c.interrupt = 2; + c.mutateBetweenSyncs = true; + c.mutateBetweenAttempts = true; + c.flapBack = true; + c.diffPolicy = true; + new MGEnv(c); + } + } +} + +machine TestG2fbS { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_S; + c.interrupt = 2; + c.mutateBetweenSyncs = true; + c.mutateBetweenAttempts = true; + c.flapBack = true; + c.diffPolicy = true; + new MGEnv(c); + } + } +} + +machine TestG1e { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_E; + c.sessVar = SESS_B; + c.interrupt = 3; + c.mutateBetweenAttempts = true; + t = defaultGToggles(); + t.midBumpFence = false; + c.toggles = t; + new MGEnv(c); + } + } +} + +// Expected GREEN: +test tcG2ea_Core [main=TestG2ea]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG in (union Graph, { TestG2ea }); +test tcG2eb_All [main=TestG2eb]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP6G, GP6E in (union Graph, { TestG2eb }); +test tcG2s_All [main=TestG2s]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP6G, GP6S, GPASS in (union Graph, { TestG2s }); +test tcG2awE_All [main=TestG2awE]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP6G, GP6E in (union Graph, { TestG2awE }); +test tcG2awS_All [main=TestG2awS]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP6G, GP6S, GPASS in (union Graph, { TestG2awS }); +test tcG2eb2c_All [main=TestG2eb2c]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP6G, GP6E in (union Graph, { TestG2eb2c }); +test tcG2fbE_All [main=TestG2fbE]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP6G, GP6E in (union Graph, { TestG2fbE }); +test tcG2fbS_All [main=TestG2fbS]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP6G, GP6S, GPASS in (union Graph, { TestG2fbS }); + +// Expected RED: +test tcG2ea_P6G [main=TestG2ea]: assert GP6G in (union Graph, { TestG2ea }); +test tcG2ebRetrOff_P6G [main=TestG2ebRetrOff]: assert GP6G in (union Graph, { TestG2ebRetrOff }); +test tcG2sStampOff_P6G [main=TestG2sStampOff]: assert GP6G in (union Graph, { TestG2sStampOff }); +test tcG2awE_Redo [main=TestG2awE]: assert REDOPROBE in (union Graph, { TestG2awE }); +test tcG2awS_Redo [main=TestG2awS]: assert REDOPROBE in (union Graph, { TestG2awS }); +test tcG2fbE_Redo [main=TestG2fbE]: assert REDOPROBE in (union Graph, { TestG2fbE }); +test tcG2ebPend_Redo [main=TestG2eb]: assert REDOPROBE in (union Graph, { TestG2eb }); +test tcG2awWA_E [main=TestG2awWAE]: assert GP6E in (union Graph, { TestG2awWAE }); +test tcG2awWA_S [main=TestG2awWAS]: assert GP6S in (union Graph, { TestG2awWAS }); +test tcG1d_P6G [main=TestG1d]: assert GP6G in (union Graph, { TestG1d }); +// Existence probe: the forced reader redo arms on the G1d chassis +// (the reachability ladder's first rung; see CALIBRATION G1D-REACH). +test tcG1dProbe_Redo [main=TestG1d]: assert REDOPROBE in (union Graph, { TestG1d }); +test tcG1e_PGEN [main=TestG1e]: assert PGEN in (union Graph, { TestG1e }); diff --git a/formal/graph/PTst/ScenarioG3.p b/formal/graph/PTst/ScenarioG3.p new file mode 100644 index 000000000..f1db0a416 --- /dev/null +++ b/formal/graph/PTst/ScenarioG3.p @@ -0,0 +1,54 @@ +/* G3 — artifact swap + rebind (walker case 3 premise re-run; + confirmation cells, round-1 walked clean). Topology cell 25 = + cell-11 shape: parent P (node 0) names consult node C (node 1) on + every row. + + Script: 2 syncs, graceful stop (interrupt 1) in sync 2 — C's + attempt-1 execution stops AFTER its consult announce (MATCH + verdict pending, nothing committed; the stop-forced checkpoint + captures C pending at its consult-granularity cursor). Between + attempts the env swaps C's PREV artifact to sibling content + (epoch 9). Resume bumps the stopped generation (g1 dies with no + commits); the re-consult hits the ACTUALLY current (swapped) base, + truthfully FAILs validation, and falls to fetch-fresh; the + 3-atomic closure carries over from the walker. + + nWorkers = 1: serial execution, so the stop leaves no straggler + worker racing late commits into the stopped attempt's agen. + + Expected GREEN on both variants — the graph adds generation death + of the stopped execution to the walker's case-3 premise, and + nothing reds: no marker was written (stop precedes the round), no + adoption is offered, SealExpect binds the live world. */ + +machine TestG3E { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 25; + c.lineage = LIN_E; + c.interrupt = 1; + c.nWorkers = 1; + new MGEnv(c); + } + } +} + +machine TestG3S { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 25; + c.lineage = LIN_S; + c.interrupt = 1; + c.nWorkers = 1; + new MGEnv(c); + } + } +} + +// Expected GREEN: +test tcG3_E [main=TestG3E]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH in (union Graph, { TestG3E }); +test tcG3_S [main=TestG3S]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH, GP6S, GPASS in (union Graph, { TestG3S }); diff --git a/formal/graph/PTst/ScenarioG5.p b/formal/graph/PTst/ScenarioG5.p new file mode 100644 index 000000000..e974fe582 --- /dev/null +++ b/formal/graph/PTst/ScenarioG5.p @@ -0,0 +1,183 @@ +/* G5 family — sweep, purge, and the closure oracle (SPEC 9, round-1 + F5/F6/F8 re-scripts). Topology cell 24: PAGINATED parent P (node + 0, 2-page record round) names consult node C (node 1) on its + page-1 row ONLY — the row the e1->e2 mutation deletes. The + mutation target is P's OWN scope (key 0), so an attempt-2 + re-execution emits NO child naming: upstream demand shrink. + + The per-announce demand note (G-RULE-1 TIMING PIN) is the family's + load-bearing mechanism: P's page-1 commit admits C mid-round, so a + checkpoint can capture C-pending AND P-pending — the window the + resume ∀-purge (R2-F5) exists for. Carrier-only derivation makes + every shape below unreachable (calibration find; the spec pinned + per-announce timing from the start). + + Shrink chassis (mutateBetweenAttempts on key 0, crash in sync 2): + - tcG5aE_All / tcG5aS_All: honest sweep, both variants — GREEN. + Which parent world survives is schedule-dependent (sync-scoped + freshness): P re-ran at e2 -> C purged (E) or refused (S) or + never admitted, C's debris swept, seal {0}; P completed at e1 -> + C is live demand, seal {0, 1}. C's key is EXCLUDED from the + attempt-2 expectation (the structural question is GP5's). + - tcG5bE_P5 / tcG5bS_P5: sweepOff — the P-re-ran schedule seals + C's partition with no living namer — P5-UNDER RED both variants. + - tcG5e_Probe: PURGE-PROBE existence exhibit on the honest E + chassis — RED (the resume purge fires; the counterexample + exhibits the mid-round checkpoint window). + - tcG5e_PurgeOff: purgeOff (E) — the restored C dispatches with + every admitted-by edge dead — DEAD-DISPATCH RED (the G5e count + oracle: dead demand executed). + + No-shrink chassis (same cell, no mutation, crash in sync 2): + - tcG5f_All: honest — GREEN. The R2-F5 no-starvation witness: the + mid-round window purges C at resume, P's live re-derivation at + the SAME epoch re-names it, the purged hash is re-admissible + (G-RULE-2 removal), C re-runs, seal {0, 1}. Starvation would + red SealExpect (expected key missing). + - tcG5f_Drop: demandDropOff — one derived admission silently + dropped; P's sealed rows name a child that never ran — + P5-OVER / SEAL-EXPECT RED (the closure oracle's kill). + - tcG5c_P5: sweepOverreach — the seal drops an in-closure key; + the sealed parent still names it — P5-OVER RED. */ + +machine TestG5aE { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_E; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + new MGEnv(c); + } + } +} + +machine TestG5aS { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_S; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + new MGEnv(c); + } + } +} + +machine TestG5bE { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_E; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + t = defaultGToggles(); + t.sweep = false; + c.toggles = t; + new MGEnv(c); + } + } +} + +machine TestG5bS { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_S; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + t = defaultGToggles(); + t.sweep = false; + c.toggles = t; + new MGEnv(c); + } + } +} + +machine TestG5eOff { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_E; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + t = defaultGToggles(); + t.purge = false; + c.toggles = t; + new MGEnv(c); + } + } +} + +machine TestG5f { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_E; + c.interrupt = 2; + new MGEnv(c); + } + } +} + +machine TestG5fDrop { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_E; + c.interrupt = 2; + t = defaultGToggles(); + t.demandDrop = true; + c.toggles = t; + new MGEnv(c); + } + } +} + +machine TestG5c { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_E; + c.interrupt = 2; + t = defaultGToggles(); + t.sweepOverreach = true; + c.toggles = t; + new MGEnv(c); + } + } +} + +// Expected GREEN: +test tcG5aE_All [main=TestG5aE]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH in (union Graph, { TestG5aE }); +test tcG5aS_All [main=TestG5aS]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH, GPASS in (union Graph, { TestG5aS }); +test tcG5f_All [main=TestG5f]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH in (union Graph, { TestG5f }); + +// Expected RED: +test tcG5bE_P5 [main=TestG5bE]: assert GP5 in (union Graph, { TestG5bE }); +test tcG5bS_P5 [main=TestG5bS]: assert GP5 in (union Graph, { TestG5bS }); +test tcG5c_P5 [main=TestG5c]: assert GP5 in (union Graph, { TestG5c }); +test tcG5e_Probe [main=TestG5aE]: assert PURGEPROBE in (union Graph, { TestG5aE }); +test tcG5e_PurgeOff [main=TestG5eOff]: assert GDEADDISPATCH in (union Graph, { TestG5eOff }); +test tcG5f_Drop [main=TestG5fDrop]: assert SealExpectG, GP5 in (union Graph, { TestG5fDrop }); diff --git a/formal/graph/PTst/ScenarioG5d.p b/formal/graph/PTst/ScenarioG5d.p new file mode 100644 index 000000000..c3459891e --- /dev/null +++ b/formal/graph/PTst/ScenarioG5d.p @@ -0,0 +1,137 @@ +/* G5d — cross-variant seal-world meta-analysis (GS-CO-005(d)). + The honest shrink chassis (cell 24, crash in sync 2, key-0 + mutation between attempts) admits MULTIPLE legitimate sealed + worlds under sync-scoped freshness; the meta-analysis compares + the REACHABLE world sets across lineage variants via GSEALWORLD + existence probes (RED = the target world is reachable). + + Declared (before run, both variants): + - W1 = {0->2}: P re-ran at e2, C's demand shrank away, C swept + (E: purge / S: refusal / or never admitted) — REACHABLE, RED. + - W2 = {0->1, 1->1}: P completed-across-crash at e1 (G-RULE-2), + C live demand — REACHABLE, RED. + - W3 = {0->2, 1->1}: the sweep-failure world (P's e2 rows name no + C, yet C's partition seals) — UNREACHABLE, GREEN. The W1/W2 + REDs on the same chassis are the positive controls against a + vacuous GREEN here; the sweepOff kill (tcG5bE_P5/tcG5bS_P5, + P5-UNDER) is the registered mechanism-off contrast. + + A world reachable under exactly ONE variant is a divergence + finding and blocks Axis-3 citation (GS-CO-005(d)). */ + +machine TestG5dEW1 { + start state I { + entry { + var c: tGCfg; + var w: map[int, int]; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_E; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + w[0] = 2; + c.sealWorld = w; + new MGEnv(c); + } + } +} + +machine TestG5dSW1 { + start state I { + entry { + var c: tGCfg; + var w: map[int, int]; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_S; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + w[0] = 2; + c.sealWorld = w; + new MGEnv(c); + } + } +} + +machine TestG5dEW2 { + start state I { + entry { + var c: tGCfg; + var w: map[int, int]; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_E; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + w[0] = 1; + w[1] = 1; + c.sealWorld = w; + new MGEnv(c); + } + } +} + +machine TestG5dSW2 { + start state I { + entry { + var c: tGCfg; + var w: map[int, int]; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_S; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + w[0] = 1; + w[1] = 1; + c.sealWorld = w; + new MGEnv(c); + } + } +} + +machine TestG5dEW3 { + start state I { + entry { + var c: tGCfg; + var w: map[int, int]; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_E; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + w[0] = 2; + w[1] = 1; + c.sealWorld = w; + new MGEnv(c); + } + } +} + +machine TestG5dSW3 { + start state I { + entry { + var c: tGCfg; + var w: map[int, int]; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_S; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + w[0] = 2; + w[1] = 1; + c.sealWorld = w; + new MGEnv(c); + } + } +} + +// Declared RED (reachable worlds; the probe mechanism's positive +// controls for the W3 GREEN): +test tcG5dE_W1 [main=TestG5dEW1]: assert GSEALWORLD in (union Graph, { TestG5dEW1 }); +test tcG5dS_W1 [main=TestG5dSW1]: assert GSEALWORLD in (union Graph, { TestG5dSW1 }); +test tcG5dE_W2 [main=TestG5dEW2]: assert GSEALWORLD in (union Graph, { TestG5dEW2 }); +test tcG5dS_W2 [main=TestG5dSW2]: assert GSEALWORLD in (union Graph, { TestG5dSW2 }); + +// Declared GREEN (the sweep-failure world is unreachable honestly): +test tcG5dE_W3 [main=TestG5dEW3]: assert GSEALWORLD in (union Graph, { TestG5dEW3 }); +test tcG5dS_W3 [main=TestG5dSW3]: assert GSEALWORLD in (union Graph, { TestG5dSW3 }); diff --git a/formal/graph/PTst/ScenarioG6.p b/formal/graph/PTst/ScenarioG6.p new file mode 100644 index 000000000..65072bcdf --- /dev/null +++ b/formal/graph/PTst/ScenarioG6.p @@ -0,0 +1,274 @@ +/* G6 — redo-work bake-off (METRIC cells; the verdicts are counts, + not pass/fail; adequacy §10.1 count-oracle kills). The count + oracle is GEXECBOUND: executions per node per sync <= cfg.execBound + (announced at init). The minimal GREEN bound is the + checker-verified worst-case redo for the leg; the bound-minus-one + RED probe is the existence exhibit that the redo is real. The + bake-off table (CALIBRATION.md) is emitted from these verdicts. + + G6a (cell 26 chain, crash sync 2, re-derivation IDENTICAL): P -> + S1 -> C -> GC; per-announce demand notes admit the chain mid-round, + so a checkpoint can capture completed descendants under a pending + S1 — the divergence script from round-1 F9(a). Declared REFUTABLE + expectation: both variants redo <= 1 per node (bound 2): E purges + pending only (completed C/GC stand); S1's re-run re-derives the + same naming and G-RULE-2 suppression keeps completions. + + G6b (same chassis + between-attempt mutation of S1's scope): + re-derivation CHANGED — the C/GC tail's demand shrinks; recorded + counts are arbitration data, no declared winner (round-1 F9(b)). + + G6c (cell 28 fan-in, crash sync 2): C demanded by S1 AND S2; a + crash killing one parent mid-round must not purge (E, ∀-predicate + on the survivor's live edge) or false-refuse (S, live edge) C. + Expected bound 2; the compression divergence lives in G9. */ + +machine TestG6aE { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 26; + c.lineage = LIN_E; + c.interrupt = 2; + c.execBound = 2; + new MGEnv(c); + } + } +} + +machine TestG6aS { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 26; + c.lineage = LIN_S; + c.interrupt = 2; + c.execBound = 2; + new MGEnv(c); + } + } +} + +machine TestG6aEProbe { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 26; + c.lineage = LIN_E; + c.interrupt = 2; + c.execBound = 1; + new MGEnv(c); + } + } +} + +machine TestG6aSProbe { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 26; + c.lineage = LIN_S; + c.interrupt = 2; + c.execBound = 1; + new MGEnv(c); + } + } +} + +/* GS-CO-005(c) v1 CONTROLS: the metric's zero-crash floor. No + interrupt, bound 1 — every node executes exactly once per sync + under BOTH variants; a red is a variant-overhead find (Axis-3 + data), not a kill. */ +machine TestG6aECtl { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 26; + c.lineage = LIN_E; + c.execBound = 1; + new MGEnv(c); + } + } +} + +machine TestG6aSCtl { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 26; + c.lineage = LIN_S; + c.execBound = 1; + new MGEnv(c); + } + } +} + +machine TestG6cECtl { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 28; + c.lineage = LIN_E; + c.execBound = 1; + new MGEnv(c); + } + } +} + +machine TestG6cSCtl { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 28; + c.lineage = LIN_S; + c.execBound = 1; + new MGEnv(c); + } + } +} + +machine TestG6bE { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 26; + c.lineage = LIN_E; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + c.execBound = 2; + new MGEnv(c); + } + } +} + +machine TestG6bEProbe { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 26; + c.lineage = LIN_E; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + c.execBound = 1; + new MGEnv(c); + } + } +} + +machine TestG6bSProbe { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 26; + c.lineage = LIN_S; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + c.execBound = 1; + new MGEnv(c); + } + } +} + +machine TestG6bS { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 26; + c.lineage = LIN_S; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + c.execBound = 2; + new MGEnv(c); + } + } +} + +machine TestG6cE { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 28; + c.lineage = LIN_E; + c.interrupt = 2; + c.execBound = 2; + new MGEnv(c); + } + } +} + +machine TestG6cS { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 28; + c.lineage = LIN_S; + c.interrupt = 2; + c.execBound = 2; + new MGEnv(c); + } + } +} + +machine TestG6cEProbe { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 28; + c.lineage = LIN_E; + c.interrupt = 2; + c.execBound = 1; + new MGEnv(c); + } + } +} + +machine TestG6cSProbe { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 28; + c.lineage = LIN_S; + c.interrupt = 2; + c.execBound = 1; + new MGEnv(c); + } + } +} + +// Expected GREEN (bound 2 holds on every schedule): +test tcG6aE_All [main=TestG6aE]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH, GEXECBOUND in (union Graph, { TestG6aE }); +test tcG6aS_All [main=TestG6aS]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH, GEXECBOUND, GPASS in (union Graph, { TestG6aS }); +test tcG6bE_All [main=TestG6bE]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH, GEXECBOUND in (union Graph, { TestG6bE }); +test tcG6bS_All [main=TestG6bS]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH, GEXECBOUND, GPASS in (union Graph, { TestG6bS }); +test tcG6cE_All [main=TestG6cE]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH, GEXECBOUND in (union Graph, { TestG6cE }); +test tcG6cS_All [main=TestG6cS]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH, GEXECBOUND, GPASS in (union Graph, { TestG6cS }); + +// Expected GREEN (GS-CO-005(c) v1 controls: zero-crash floor at bound 1): +test tcG6aE_Ctl [main=TestG6aECtl]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH, GEXECBOUND in (union Graph, { TestG6aECtl }); +test tcG6aS_Ctl [main=TestG6aSCtl]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH, GEXECBOUND, GPASS in (union Graph, { TestG6aSCtl }); +test tcG6cE_Ctl [main=TestG6cECtl]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH, GEXECBOUND in (union Graph, { TestG6cECtl }); +test tcG6cS_Ctl [main=TestG6cSCtl]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH, GEXECBOUND, GPASS in (union Graph, { TestG6cSCtl }); + +// Expected RED (bound-1 probes: the redo exists): +test tcG6aE_Redo [main=TestG6aEProbe]: assert GEXECBOUND in (union Graph, { TestG6aEProbe }); +test tcG6aS_Redo [main=TestG6aSProbe]: assert GEXECBOUND in (union Graph, { TestG6aSProbe }); +test tcG6bE_Redo [main=TestG6bEProbe]: assert GEXECBOUND in (union Graph, { TestG6bEProbe }); +test tcG6bS_Redo [main=TestG6bSProbe]: assert GEXECBOUND in (union Graph, { TestG6bSProbe }); +test tcG6cE_Redo [main=TestG6cEProbe]: assert GEXECBOUND in (union Graph, { TestG6cEProbe }); +test tcG6cS_Redo [main=TestG6cSProbe]: assert GEXECBOUND in (union Graph, { TestG6cSProbe }); diff --git a/formal/graph/PTst/ScenarioG7.p b/formal/graph/PTst/ScenarioG7.p new file mode 100644 index 000000000..86de0b3e7 --- /dev/null +++ b/formal/graph/PTst/ScenarioG7.p @@ -0,0 +1,50 @@ +/* G7 — progress under churn (walker P4 analog; round-1 F11 pins). + Cell 27 = cell-11 topology; node 1 (C) fails LOUD at execution + start, deterministically, in sync 2 — with a GENERATION-BLIND + fingerprint (a fingerprint hashing the generation never matches + across bumped resumes and the stuck detector goes blind; that was + F11's finding). + + - tcG7_Ladder: the abandon ladder gives up after 2 identical + fingerprints — GREEN (the sync abandons, announce-visible; the + walker's P4 tranche machinery carries over rather than + dissolving). + - tcG7_Stuck: ladderOff — the retry loop re-fails; the third + identical fingerprint is P4-STUCK RED (attempt budget 3, + SPEC 8). */ + +machine TestG7Ladder { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 27; + c.lineage = LIN_E; + c.failNode = 1; + c.failSync = 2; + c.ladder = true; + new MGEnv(c); + } + } +} + +machine TestG7Stuck { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 27; + c.lineage = LIN_E; + c.failNode = 1; + c.failSync = 2; + c.ladder = false; + new MGEnv(c); + } + } +} + +// Expected GREEN: +test tcG7_Ladder [main=TestG7Ladder]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP4STUCK in (union Graph, { TestG7Ladder }); + +// Expected RED: +test tcG7_Stuck [main=TestG7Stuck]: assert GP4STUCK in (union Graph, { TestG7Stuck }); diff --git a/formal/graph/PTst/ScenarioG8.p b/formal/graph/PTst/ScenarioG8.p new file mode 100644 index 000000000..70c6372b5 --- /dev/null +++ b/formal/graph/PTst/ScenarioG8.p @@ -0,0 +1,128 @@ +/* G8 — supersession family (round-1 F6's dedicated cells). + + G8a (record REPLACES over dead debris): NOT a dedicated cell — the + honest G1 crash cells explore every mid-record-round crash + position and the REPLACES clear wipes the debris on re-run; + registered as G1 sweep coverage (CALIBRATION.md). + + G8b (OVERLAY-intent dead base): honest leg — the DIFF unit's + atomic clear + prev-copy re-derives the base; GREEN. The + `overlayComposeDead` mutant is CONTENT-INVISIBLE in this envelope + (2 row ids + truthful TOTAL diffs: every diff from the debris's + base overwrites or removes every debris id) but NOT + mechanism-invisible (calibration find G8B-CAL-1): on the crash + re-run the skipped clear leaves the dead attempt's copy round + LIVE under the composed diff, and P1's one-live-replacement-copy + legality reds it. tcG8bMut_P1 is the kill [P1-LEGALITY]; + tcG8bMut_Ctl keeps the content-level oracles GREEN as the + registered content-invisibility evidence (adequacy §10.1). + + G8c (same-key distinct-derivation race, cell 29): P names two + DISTINCT derivations sharing output key 1 (keyOf); suppression is + correctly silent (different hashes); the store poisons the key on + the second derivation's first commit, VOIDS its marker (R2-M8), + refuses adoption store-side (R3-M2), and the poison announce + excludes the key from SealExpect; P1's legality exemption covers + the double commit. GP5 is NOT asserted here: its key = hash - 1 + identity convention is exactly what this cell breaks. + + G8d (marker flap-back probe, R2-F4): cell 11 + flapBack + two + crashes + between-attempt mutation (raw e1 -> e2 -> e3 with + content(e3) = content(e1)). Attempt 1 commits C's REPLAY unit + (marker v=1); attempt 2 re-derives FETCH-FRESH via record REPLACES + (the clear DELETES the marker); attempt 3 finds NO marker, + consults, v=1 MATCHes content(e3) truthfully, REPLAY verdict + (R3-M1) — GREEN. Mutant `markerCleanupOff`: the stale marker + survives the REPLACES; attempt 3 premise-matches it and adopts + content the marker no longer describes — P-MARK RED. */ + +machine TestG8b { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 11; + c.lineage = LIN_E; + c.interrupt = 2; + c.mutateBetweenSyncs = true; + c.diffPolicy = true; + new MGEnv(c); + } + } +} + +machine TestG8bMut { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 11; + c.lineage = LIN_E; + c.interrupt = 2; + c.mutateBetweenSyncs = true; + c.diffPolicy = true; + t = defaultGToggles(); + t.overlayComposeDead = true; + c.toggles = t; + new MGEnv(c); + } + } +} + +machine TestG8c { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 29; + c.lineage = LIN_E; + new MGEnv(c); + } + } +} + +machine TestG8d { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 11; + c.lineage = LIN_E; + c.interrupt = 3; + c.mutateBetweenAttempts = true; + c.flapBack = true; + new MGEnv(c); + } + } +} + +machine TestG8dMut { + start state I { + entry { + var c: tGCfg; + var t: tGToggles; + c = defaultGCfg(); + c.cell = 11; + c.lineage = LIN_E; + c.interrupt = 3; + c.mutateBetweenAttempts = true; + c.flapBack = true; + t = defaultGToggles(); + t.markerCleanup = false; + c.toggles = t; + new MGEnv(c); + } + } +} + +// Expected GREEN: +test tcG8b_All [main=TestG8b]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5 in (union Graph, { TestG8b }); +test tcG8bMut_Ctl [main=TestG8bMut]: assert GP2, GP3prime, SealExpectG, GP5 in (union Graph, { TestG8bMut }); +test tcG8c_All [main=TestG8c]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG in (union Graph, { TestG8c }); +test tcG8d_All [main=TestG8d]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5 in (union Graph, { TestG8d }); + +// Expected RED: +test tcG8bMut_P1 [main=TestG8bMut]: assert GP1 in (union Graph, { TestG8bMut }); +test tcG8c_Poison [main=TestG8c]: assert POISONPROBE in (union Graph, { TestG8c }); +test tcG8dMut_PMARK [main=TestG8dMut]: assert PMARK in (union Graph, { TestG8dMut }); diff --git a/formal/graph/PTst/ScenarioG9.p b/formal/graph/PTst/ScenarioG9.p new file mode 100644 index 000000000..e3a90b339 --- /dev/null +++ b/formal/graph/PTst/ScenarioG9.p @@ -0,0 +1,114 @@ +/* G9 — compression admissibility (round-1 F10). Re-runs the S legs + under `stampCompression`: the pre-seal pass compares FLOOR-BUCKETED + stamps (buckets of 2) — lossy but STALE-ERRING (never false-live: + safety preserved by construction; the recorded cost is extra + redos, because an odd-generation completion always looks stale + until the forced re-run lands on the bumped even generation). + + ADMISSIBILITY CLAIM (refutable): every safety verdict is identical + to the uncompressed legs; only the redo counts grow. Any safety + change here REFUTES the claim and halts the S recommendation. + + Growth is measured on the NO-CRASH fan-in chassis (cell 28), + where the counts are deterministic (G9-CAL-1: a crash script + masks the growth — the resume redo and the heal-wave redo both + peak at 2 per node, so the per-node bound cannot move): + tcG9cBase_All (uncompressed) is GREEN at bound 1 — every node + executes once; tcG9c_All (compressed) needs bound 2 — the first + pass scan heals every odd first-admission generation with one + forced redo; tcG9c_Redo exhibits the growth at bound 1. */ + +machine TestG9s { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_S; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + c.stampCompression = true; + new MGEnv(c); + } + } +} + +machine TestG9awS { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 21; + c.lineage = LIN_S; + c.interrupt = 2; + c.stampCompression = true; + new MGEnv(c); + } + } +} + +machine TestG9G5aS { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 24; + c.lineage = LIN_S; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + c.stampCompression = true; + new MGEnv(c); + } + } +} + +machine TestG9cBase { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 28; + c.lineage = LIN_S; + c.execBound = 1; + new MGEnv(c); + } + } +} + +machine TestG9c { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 28; + c.lineage = LIN_S; + c.stampCompression = true; + c.execBound = 2; + new MGEnv(c); + } + } +} + +machine TestG9cProbe { + start state I { + entry { + var c: tGCfg; + c = defaultGCfg(); + c.cell = 28; + c.lineage = LIN_S; + c.stampCompression = true; + c.execBound = 1; + new MGEnv(c); + } + } +} + +// Expected GREEN (safety verdicts identical to the uncompressed legs): +test tcG9s_All [main=TestG9s]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP6G, GP6S, GPASS in (union Graph, { TestG9s }); +test tcG9awS_All [main=TestG9awS]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP6G, GP6S, GPASS in (union Graph, { TestG9awS }); +test tcG9G5aS_All [main=TestG9G5aS]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH, GPASS in (union Graph, { TestG9G5aS }); +test tcG9cBase_All [main=TestG9cBase]: assert GEXECBOUND in (union Graph, { TestG9cBase }); +test tcG9c_All [main=TestG9c]: assert GP1, GP2, GP3prime, PGEN, PMARK, PADOPT, SealExpectG, GP5, GDEADDISPATCH, GEXECBOUND, GPASS in (union Graph, { TestG9c }); + +// Expected RED (the bucketing redo-growth exhibit): +test tcG9c_Redo [main=TestG9cProbe]: assert GEXECBOUND in (union Graph, { TestG9cProbe }); diff --git a/formal/graph/graph.pproj b/formal/graph/graph.pproj new file mode 100644 index 000000000..61825b577 --- /dev/null +++ b/formal/graph/graph.pproj @@ -0,0 +1,9 @@ + +graph + + ./PSrc/ + ./PSpec/ + ./PTst/ + +./PGenerated + diff --git a/formal/graph/tools/alarms.sh b/formal/graph/tools/alarms.sh new file mode 100644 index 000000000..3702b3bee --- /dev/null +++ b/formal/graph/tools/alarms.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Shared alarm-tag extraction for the graph model's verdict scripts +# (sweep.sh and bakeoff.sh source this). ONE definition of the +# monitor-name alternation: when the two scripts each carried a copy, +# a monitor added to only one would silently emit an empty tag in the +# other's committed evidence, so there are no copies. SEAL-WORLD is +# asserted only by the bake-off's tcG5d* cells but stays in the shared +# alternation (output-neutral for the frozen 66 sweep cells; a +# G5d-shaped cell landing in the sweep is not silently untagged). +# +# The tag pipeline needs rg. Without this guard a missing rg is +# swallowed into an empty command substitution — the same empty tag a +# red cell with an unrecognized monitor produces — so sourcing this +# file fails loudly instead. +command -v rg >/dev/null 2>&1 || { + echo "graph tools: rg (ripgrep) is required for alarm-tag extraction and is not on PATH" >&2 + exit 2 +} + +MONITOR_ALTERNATION="P-GEN|P-MARK|P-ADOPT|P1-[A-Z-]+[A-Z]|P2-[A-Z]+|P3'-[A-Z]+|P4-STUCK|P5-UNDER|P5-OVER|P6-[GES]|SEAL-EXPECT|SEAL-WORLD|REDO-PROBE|PURGE-PROBE|POISON-PROBE|DEAD-DISPATCH|PASS-BUDGET|EXEC-BOUND|Deadlock detected|liveness" + +# alarm_tag — comma-joined sorted set of the +# firing monitor names found in the trace. Empty when nothing in the +# alternation matches; callers treat an empty tag on a RED cell as a +# MISMATCH (an untagged red is unauditable). +alarm_tag() { + rg -o "($MONITOR_ALTERNATION)" "$1" | sort -u | paste -sd, - +} diff --git a/formal/graph/tools/bakeoff.sh b/formal/graph/tools/bakeoff.sh new file mode 100755 index 000000000..e6d43ef4d --- /dev/null +++ b/formal/graph/tools/bakeoff.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Bake-off phase cells (GS-CO-005 declared verdicts), kept OUT of the +# calibration sweep: sweep.sh reproduces the frozen 66-cell matrix +# (PCheckerOutput/sweep/summary.txt) and this script reproduces the +# 12-cell bake-off run of record (PCheckerOutput/bakeoff/summary.txt) +# without either overwriting the other's evidence. +# +# Usage: tools/bakeoff.sh [schedules] (run from formal/graph) +set -u +. "$(dirname "$0")/alarms.sh" +S="${1:-10000}" +OUT="PCheckerOutput/bakeoff" +SUMMARY="$OUT/summary.txt" +mkdir -p "$OUT" +: > "$SUMMARY" + +# cell:expected[:alarm[:strategy]] (RED = counterexample expected, +# GREEN = none). The third field is the cell's calibrated monitor and +# it is ENFORCED: counterexample presence alone matches expected=RED +# even for a cell that redded on a deadlock instead of its declared +# property, so a RED whose extracted tag does not contain the declared +# alarm is a MISMATCH. Enforcement is sound here because every +# bake-off red has exactly one pre-registered monitor (unlike sweep +# cells, which can legitimately red on more than one calibrated shape +# — see sweep.sh). The optional fourth field is an extra p-check +# strategy flag for cells whose target is too narrow for uniform +# random search to find reliably at the default budget — same +# mechanism as the sweep's third field. +CELLS=" +tcG6aE_Ctl:GREEN +tcG6aS_Ctl:GREEN +tcG6cE_Ctl:GREEN +tcG6cS_Ctl:GREEN +tcG6bE_Redo:RED:EXEC-BOUND +tcG6bS_Redo:RED:EXEC-BOUND +tcG5dE_W1:RED:SEAL-WORLD +tcG5dS_W1:RED:SEAL-WORLD +tcG5dE_W2:RED:SEAL-WORLD +tcG5dS_W2:RED:SEAL-WORLD:--sch-feedbackpct=20 +tcG5dE_W3:GREEN +tcG5dS_W3:GREEN +" + +mismatches=0 +total=0 +for entry in $CELLS; do + rest="${entry#*:}" + cell="${entry%%:*}" + expected="${rest%%:*}" + alarm="" + strategy="" + case "$rest" in *:*) + rest="${rest#*:}" + alarm="${rest%%:*}" + case "$rest" in *:*) strategy="${rest#*:}";; esac + ;; esac + # Grammar guard: the fields are positional and the alarm is only + # consulted on the RED branch, so a strategy flag written in the + # alarm slot (cell:GREEN:--flag) would be dropped SILENTLY — the + # cell would run without its intended search and still report ok. + # A strategy with no alarm takes an explicit empty third field + # (cell:expected::--flag). + case "$alarm" in -*) + echo "bakeoff.sh: $cell: third field must be the calibrated alarm, not '$alarm' — grammar is cell:expected[:alarm[:strategy]]; for strategy-only write $cell:$expected::$alarm" >&2 + exit 2 + ;; esac + total=$((total + 1)) + rm -rf "$OUT/$cell" + # shellcheck disable=SC2086 + p check -tc "$cell" -s "$S" ${strategy/=/ } -o "$OUT/$cell" > "$OUT/$cell.log" 2>&1 + pstatus=$? + ce=$(ls "$OUT/$cell"/BugFinding/graph_[0-9]*_[0-9]*.txt 2>/dev/null | head -1) + # Verdict precedence: a counterexample is RED even if the checker + # then exited nonzero (the find stands); a counterexample-free + # nonzero exit is CHECKER-ERROR, not GREEN — "no bug found" from a + # checker that died is not evidence of anything. + if [ -n "$ce" ]; then observed="RED" + elif [ "$pstatus" -ne 0 ]; then observed="CHECKER-ERROR" + else observed="GREEN"; fi + mark="ok" + [ "$observed" = "$expected" ] || mark="MISMATCH" + detail="" + if [ "$observed" = "RED" ]; then + tag=$(alarm_tag "$ce") + # Empty tag = firing monitor outside the shared alternation + # (tools/alarms.sh): unauditable, so a mismatch even when RED was + # expected. Declared-alarm check is a substring match against the + # comma-joined tag set — sound while no declared alarm is a + # substring of a different monitor's name (EXEC-BOUND and + # SEAL-WORLD are not). + [ -n "$tag" ] || mark="MISMATCH" + if [ -n "$alarm" ]; then + case "$tag" in *"$alarm"*) ;; *) mark="MISMATCH";; esac + fi + detail=" [$tag]" + elif [ "$observed" = "CHECKER-ERROR" ]; then + detail=" (p exit $pstatus, see $OUT/$cell.log)" + fi + [ "$mark" = "ok" ] || mismatches=$((mismatches + 1)) + line="$cell expected=$expected observed=$observed $mark$detail" + echo "$line" | tee -a "$SUMMARY" +done +echo "BAKEOFF-DONE cells=$total mismatches=$mismatches" | tee -a "$SUMMARY" +# The exit status carries the verdict (the Makefile's formal targets +# rely on it): a drifted run must not read as a green make. +[ "$mismatches" -eq 0 ] diff --git a/formal/graph/tools/sweep.sh b/formal/graph/tools/sweep.sh new file mode 100755 index 000000000..edbf9e8e5 --- /dev/null +++ b/formal/graph/tools/sweep.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# Graph-model cell regression sweep (walker tools/sweep.sh parity). +# Verdicts are audited by counterexample trace-file presence, not the +# "Found N bugs" tail; p check's exit status gates only the GREEN side +# (a counterexample-free nonzero exit is CHECKER-ERROR — absence of a +# find from a checker that died proves nothing — while a found +# counterexample stands regardless of exit status). +# +# Usage: tools/sweep.sh [schedules] (run from formal/graph) +set -u +. "$(dirname "$0")/alarms.sh" +S="${1:-10000}" +OUT="PCheckerOutput/sweep" +SUMMARY="$OUT/summary.txt" +mkdir -p "$OUT" +: > "$SUMMARY" + +# cell:expected[:strategy] (RED = counterexample expected, GREEN = +# none). Optional third field = extra p-check strategy flag for cells +# whose target is too deep for uniform random search (G1D-REACH). +CELLS=" +tcG1i_All:GREEN +tcG1ii_All:GREEN +tcG1iii_E:GREEN +tcG1iii_S:GREEN +tcG1b_All:GREEN +tcG1cMut_P3:GREEN +tcG1cMut_Seal:GREEN +tcG1sup_P1:RED +tcG1bMut_PGEN:RED +tcG1cMut_Adopt:RED +tcG2ea_Core:GREEN +tcG2eb_All:GREEN +tcG2s_All:GREEN +tcG2awE_All:GREEN +tcG2awS_All:GREEN +tcG2eb2c_All:GREEN +tcG2fbE_All:GREEN +tcG2fbS_All:GREEN +tcG2ea_P6G:RED +tcG2ebRetrOff_P6G:RED +tcG2sStampOff_P6G:RED +tcG2awE_Redo:RED +tcG2awS_Redo:RED +tcG2fbE_Redo:RED +tcG2ebPend_Redo:RED +tcG2awWA_E:RED:--sch-feedbackpct=20 +tcG2awWA_S:RED +tcG1dProbe_Redo:RED +tcG1d_P6G:RED:--sch-feedbackpct=20 +tcG1e_PGEN:RED +tcG5aE_All:GREEN +tcG5aS_All:GREEN +tcG5f_All:GREEN +tcG5bE_P5:RED +tcG5bS_P5:RED +tcG5c_P5:RED +tcG5e_Probe:RED +tcG5e_PurgeOff:RED +tcG5f_Drop:RED +tcG3_E:GREEN +tcG3_S:GREEN +tcG6aE_All:GREEN +tcG6aS_All:GREEN +tcG6bE_All:GREEN +tcG6bS_All:GREEN +tcG6cE_All:GREEN +tcG6cS_All:GREEN +tcG6aE_Redo:RED +tcG6aS_Redo:RED +tcG6cE_Redo:RED +tcG6cS_Redo:RED +tcG7_Ladder:GREEN +tcG7_Stuck:RED +tcG8b_All:GREEN +tcG8bMut_Ctl:GREEN +tcG8bMut_P1:RED +tcG8c_All:GREEN +tcG8c_Poison:RED +tcG8d_All:GREEN +tcG8dMut_PMARK:RED:--sch-feedbackpct=20 +tcG9s_All:GREEN +tcG9awS_All:GREEN +tcG9G5aS_All:GREEN +tcG9cBase_All:GREEN +tcG9c_All:GREEN +tcG9c_Redo:RED +" + +mismatches=0 +total=0 +for entry in $CELLS; do + rest="${entry#*:}" + cell="${entry%%:*}" + expected="${rest%%:*}" + strategy="" + case "$rest" in *:*) strategy="${rest#*:}";; esac + # Grammar guard, symmetric to bakeoff.sh's: this script's third field + # is a p-check strategy flag, but bakeoff.sh's is the calibrated + # alarm — an entry pasted across the two grammars would hand p check + # an alarm name as a bogus positional argument. Fail loudly at parse + # time instead of with a confusing checker error mid-sweep. + if [ -n "$strategy" ]; then case "$strategy" in -*) ;; *) + echo "sweep.sh: $cell: third field must be a p-check strategy flag, not '$strategy' — grammar is cell:expected[:strategy] (the alarm-carrying 4-field grammar belongs to bakeoff.sh)" >&2 + exit 2 + ;; esac; fi + total=$((total + 1)) + rm -rf "$OUT/$cell" + # shellcheck disable=SC2086 + p check -tc "$cell" -s "$S" ${strategy/=/ } -o "$OUT/$cell" > "$OUT/$cell.log" 2>&1 + pstatus=$? + ce=$(ls "$OUT/$cell"/BugFinding/graph_[0-9]*_[0-9]*.txt 2>/dev/null | head -1) + # Verdict precedence: a counterexample is RED even if the checker + # then exited nonzero (the find stands); a counterexample-free + # nonzero exit is CHECKER-ERROR, not GREEN — "no bug found" from a + # checker that died is not evidence of anything. + if [ -n "$ce" ]; then observed="RED" + elif [ "$pstatus" -ne 0 ]; then observed="CHECKER-ERROR" + else observed="GREEN"; fi + mark="ok" + [ "$observed" = "$expected" ] || mark="MISMATCH" + detail="" + if [ "$observed" = "RED" ]; then + tag=$(alarm_tag "$ce") + # An empty tag means the firing monitor is outside the shared + # alternation (tools/alarms.sh) — an untagged red is unauditable, + # so it is a mismatch even when RED was expected. Per-cell alarm + # ENFORCEMENT is deliberately bake-off-only (see bakeoff.sh): + # sweep cells can legitimately red on more than one calibrated + # shape (e.g. the walker's tc3a_P1), so the sweep's comparison + # surface for WHICH monitor fired is CALIBRATION.md, not this + # script. + [ -n "$tag" ] || mark="MISMATCH" + detail=" [$tag]" + elif [ "$observed" = "CHECKER-ERROR" ]; then + detail=" (p exit $pstatus, see $OUT/$cell.log)" + fi + [ "$mark" = "ok" ] || mismatches=$((mismatches + 1)) + line="$cell expected=$expected observed=$observed $mark$detail" + echo "$line" | tee -a "$SUMMARY" +done +echo "SWEEP-DONE cells=$total mismatches=$mismatches" | tee -a "$SUMMARY" +# The exit status carries the verdict (the Makefile's formal targets +# rely on it): a drifted sweep must not read as a green make. +[ "$mismatches" -eq 0 ] diff --git a/formal/occult/LAWS.md b/formal/occult/LAWS.md new file mode 100644 index 000000000..5c747e8e9 --- /dev/null +++ b/formal/occult/LAWS.md @@ -0,0 +1,86 @@ +# Equational laws — composition algebra and stamp lattice (deliverable 9) + +Status: CHECKED — every law below is mechanically verified against the +engine (defining equations in `src/sync_laws.occult`, checks in +`host/laws_test.go` and `host/compression_test.go`; run with +`go test ./...` in `host/`). L1–L6 and L8 are closed by equality +saturation over free Skolem constants; L7 is an ASSUMPTION (stdlib +`vector_clock` semilattice axioms instantiated on our merge — loaded so +every other check runs in their presence); L9a/L9b are ground-exhaustive +over generations 0..7 (the small-scope envelope, bound stated). Five +saturation negative controls and one ground false-live control are +refused — notably the false-live control is violated at exactly the +predicted pairs (s = cur−1, cur odd): the G9-CAL-1 parity ambiguity the +P model found dynamically, rediscovered deductively. A refuted law is a +finding and goes to the P-model change-order log, not silently +reworded. + +These are the algebraic assumptions the P models consume without +re-deriving them by state exploration (brief: "Prove them in Occult; +let P consume them as assumptions"). Each law cites the spec text that +pins it. The carrier vocabulary is `formal/GLOSSARY.md`'s. + +## Composition algebra (P1 fold, MODEL_SPEC.md §7) + +Carrier: per-(sync, scope) fold values — `empty` (the fold's initial +value, round-7 F3 pin) and `rows(e)` (scope content at upstream epoch +e). Operations are COMPLETE-round contributions (torn/incomplete +rounds never enter the fold): + +- `fresh(e)` — fetch-fresh / REPLACES round +- `repl(e)` — replacement copy of the attested base e (replay verdict) +- `ovl(e_from, e_to)` — overlay round grounded on this sync's + completed replay of `e_from` +- `ovl_sg(e_to)` — self-grounding overlay (its OWN replacement copy + committed inside the round) +- `skip(e_to)` — copy-skipped duplicate (B5-legal) + +| # | Law | Statement | Source | +|---|-----|-----------|--------| +| L1 | REPLACES absorbs | `apply(fresh(e), x) = rows(e)` for every fold value x; hence any composition suffixed by `fresh(e)` equals `rows(e)` — prior history is absorbed. | MODEL_SPEC §7 P1 fold clause "fresh(e) → rows(s, e) (REPLACES in the fold even though the store accumulates)" | +| L2 | Overlay grounds and composes | `apply(ovl(e1, e2), rows(e1)) = rows(e2)`; sequentially, `apply(ovl(e2, e3), apply(ovl(e1, e2), rows(e1))) = apply(ovl(e1, e3), rows(e1))` — truthful-diff transitivity. The truthfulness premise is the pinned trust boundary (validators truthful), NOT proved here. | MODEL_SPEC §7 "overlay(e_from → e_to) requires current fold value = rows(s, e_from), yields rows(s, e_to)"; trust boundary §"Honest limits" of the brief | +| L3 | Self-grounding absorbs | `apply(ovl_sg(e), x) = rows(e)` for every x — same absorption shape as L1; the round's own copy re-grounds the fold. | MODEL_SPEC §7 "SELF-GROUNDING — folds as rows(s, e_to) regardless of prior fold value" | +| L4 | Copy-skip is identity | `apply(skip(e), rows(e)) = rows(e)`; a copy-skipped round on any OTHER fold value is a legality violation, not a law. | MODEL_SPEC §7 "COPY-SKIPPED duplicate overlay round … folds as a NO-OP when the fold value already equals rows(s, e_to)" | +| L5 | Replay-copy idempotence | `copy(e) ∘ copy(e) = copy(e)` — an at-least-once re-run of one round's replacement copy contributes ONE counted copy (attempt-1 incomplete + attempt-2 completed count as one). | MODEL_SPEC §7 round-7 F2 pin; plan B5 "worst case … re-runs an idempotent copy" | +| L6 | Round-local tombstone ordering | Within one round, a page's upserts apply before the page's deletions; under the coalesced-delta precondition (at most one add-or-tombstone per id per round) operations on DISTINCT ids commute, so the round's result is page-order independent: `t(id1) ∘ u(id2) = u(id2) ∘ t(id1)` for `id1 ≠ id2`. Same-id cross-page re-adds are the connector's ordering responsibility (outside the algebra). | annotation_source_cache.proto `SourceCacheRecord.deleted_ids` precondition; MODEL_SPEC §"page-op ordering (copy → upserts → tombstones → publish)" | + +## Stamp lattice (variant S, GRAPH_MODEL_SPEC.md) + +Carrier: causal stamps — finite maps node → generation, merged on +read. Occult's stdlib `vector_clock` already axiomatizes the merge; the +laws below instantiate it and add the baton-specific facts. + +| # | Law | Statement | Source | +|---|-----|-----------|--------| +| L7 | Merge is a join-semilattice | `merge` is commutative, associative, idempotent (stdlib `vc_merge` axioms instantiated by our stamp carrier). | model/stdlib/vector_clock.occult (sibling repo); brief §"Division of labor" | +| L8 | Dead-membership is absorbing | `hasDead(x) → hasDead(merge(x, y))` — a merge never loses a dead generation; staleness is monotone under read-propagation. This is what makes the seal's no-dead-stamps check sufficient over merged stamps. | GRAPH_MODEL_SPEC P6-S ("no sealed output carries a stamp containing a dead generation"); §4c | +| L9a | Compressed definite-stale is sound | `floor2(s) < floor2(cur) → s < cur` (monotonicity of `floor2(g) = g − g mod 2`) — the bucketed pass never declares a LIVE entry definitely-stale. | GRAPH_MODEL_SPEC G9; graph/CALIBRATION.md find G9-CAL-1 | +| L9b | Bucket-aligned liveness is provable | If `cur` is even (bucket-aligned re-mint) and `s ≤ cur`, then `floor2(s) ≥ floor2(cur) → s = cur` — on bucket boundaries the compressed comparison proves liveness exactly; with `cur` odd the case `floor2(s) = cur − 1` is ambiguous, which is WHY the pass double-bumps (G9-CAL-1's admissible rule set). Error direction: false staleness (redo) only, never false-live. | graph/CALIBRATION.md G9-CAL-1; brief §"Variant S" ("lossy stamp compression … error direction is false staleness → redone work, never wrong data") | + +## What is assumed vs proved + +Definitional equations (the fold clauses, the merge definition, +`floor2`) are the axioms; L1–L9 are DERIVED equalities/implications the +engine must close by equality saturation (or refute). The trust-model +premises (truthful validators, coalesced deltas, non-lying connectors) +remain assumptions of the whole effort and are marked as such where a +law depends on one (L2, L6). + +Proved renderings (exact forms the engine closed): + +- L1's "any composition suffixed by fresh(e)" is closed for op-list + suffixes up to length 2 (`foldops` over the round log with a + universal preceding op) — bounded, not inductive; the single-apply + absorption is closed universally over Skolem constants. +- L5 is proved as APPLY-LEVEL idempotence (`app(repl(e)) ∘ + app(repl(e)) = app(repl(e))`); the counts-as-one bookkeeping is the + P side's F2 pin, not an algebraic statement. +- L6 is proved OBSERVATIONALLY over the two-id envelope: `get(i, ·)` + agrees on both op orders for every observer i ∈ {id1, id2}; the + coalesced-delta precondition is what makes two ids sufficient. +- L7 is loaded as an assumption, never proved; all other checks run + with the ACI axioms present, so the controls confirm the assumption + set does not collapse the algebra. +- L9a/L9b are ground-exhaustive over 0..7 × 0..7; the false-live + negative control is violated at exactly {(cur−1, cur) : cur odd}, + the parity ambiguity G9-CAL-1 hit dynamically. diff --git a/formal/occult/README.md b/formal/occult/README.md new file mode 100644 index 000000000..4a0aa1c94 --- /dev/null +++ b/formal/occult/README.md @@ -0,0 +1,188 @@ +# Occult track — deductive verification of sync scheduling semantics + +The parallel track to the P models (brief: +`docs/tasks/sync-formal-model-brief.md`, "Division of labor with +Occult"). P explores reachability; this track proves stated laws and +checks traces deductively with the Occult engine (sibling repo, +`../occult`). The same semantics are never modeled twice — the seams +are (a) equational proofs consumed by the P models as assumptions and +(b) one trace-policy set intended to check both P counterexample +traces and real sync executions. + +## Contents + +- `LAWS.md` — deliverable 9: the equational law inventory + (composition algebra, stamp lattice) with per-law status. The P + model specs cite this document. +- `TRACE_BRIDGE.md` — deliverable 6: the canonical trace vocabulary, + the mappings from P announce traces and real sync executions onto + it, and the runtime-trace-checker evaluation. +- `src/` — pure `.occult` sources: + - `sync_laws.occult` — defining equations of the composition + algebra, row-map observations, and stamp dead-membership. + - `sync_fixtures.occult` — free Skolem constants for law checks. + - `sync_phantom.occult` — the KNOWN-BROKEN composition vs the + premise-validated one: the phantom union stated as an algebra + (stale-attested replay base + last-sync-grounded diff). + - `sync_protocol.occult` — deliverable 8: the syncer↔connector + source-cache session terms (offer→ask→answers, bounce cap 4 + structural) with per-role projections via the engine's + protocol/projection stdlib. + - `sync_trace_policies.occult` — deliverable 7: the seven + ordering/durability policies as recursive verdict functions, with + green/red fixtures. +- `host/` — the verification harness: a standalone Go module (local + `replace` onto `../occult`; NOT part of the baton-sdk public module) + that loads the sources axiomatically and drives the engine's + equality-saturation and evaluation pipelines. Test files: + `laws_test.go`, `compression_test.go`, `phantom_test.go`, + `protocol_test.go`, `trace_policies_test.go`, + `refimpl_oracle_test.go`, `real_trace_oracle_test.go` (real syncer + traces — see below), `raft_probe_test.go` (harness positive + control), `pipeline_test.go` (the shared load/bridge/saturate + sequence). +- `host/testdata/realtraces/` — JSONL trace fixtures recorded from + REAL `pkg/sync` executions by the chaos harness + (`pkg/sync/chaos_trace_oracle_test.go`); regeneration instructions + are in that file's header comment. +- `host/refimpl/` — an executable REFERENCE implementation of the + demand-graph runtime's per-scope loop (the known-good algorithm, + which has a frozen P model but no production implementation), with a + LEGACY mode reproducing the known-broken algorithm's habits. A + modeling artifact, not production code. +- `tests/` — CLI probes (`probe_axiom_fire.occult` documents why plain + CLI file loading is definitional, which is what forced the host). + +## Running + +```bash +cd formal/occult/host && go test -timeout 30m ./... +``` + +(The saturation suites exceed go test's default 10-minute timeout +when run together.) + +Requires the sibling engine checkout at `../occult` (the go.mod +`replace` points there) and a Go 1.26 toolchain: the engine's own +go.mod pins `go 1.26.0`, so the host module pins it too — +deliberately ahead of the SDK root's 1.25.2 (a 1.25 toolchain with +auto-switching, `GOTOOLCHAIN=auto`, fetches 1.26 on its own). The +pure `.occult` sources carry no Go dependency; baton-sdk's public +module does not require the engine repo. + +## Deliverable status + +- 9 (equational proofs): DONE — 15 laws closed by equality saturation + (L1–L6, L8 over free Skolem constants), L7 loaded as the stdlib + semilattice assumption, L9a/L9b ground-exhaustive over generations + 0..7; 5 saturation negative controls + 1 ground false-live control + refused (the false-live control reproduces G9-CAL-1's parity + ambiguity deductively). See `LAWS.md`. +- 7 (trace-policy oracle set): DONE — seven policies, 140-cell + verdict matrix (20 fixtures × 7 policies): green satisfies all, + each red violates exactly its own policy. Multi-attempt traces are + in-domain (ev_resume attempt boundaries: durable facts persist, + once-per-scope resets), and so are the delta protocol's delete leg + (ev_delete is a write: grounding, quiescence, and seal-activity + obligations) and session operations (ev_swrite / ev_sread_hit / + ev_sread_miss). + Policy 6, session_ckpt_consistency, states the CO-6b-009 root cause + — post-crash session state must equal the restored checkpoint's, no + zombie reads and no amnesia — with two directional red fixtures and + a correct-rollback green; it is the trace-policy form of the walker + model's P6-C monitor. Policy 7, external_principal_grounding, + states the deleteStaleExternalPrincipals contract — the current + answer's writes commit only after reconciliation completed, and no + dead attempt's copy survives to seal (nor is a listed principal + dropped) — with two directional reds (recon-before-copy, + stale-survivor) and two greens including the completed-then-crash + carry; it is the trace-policy form of the walker model's P8 + monitor. Generation stamps stay tracked in TRACE_BRIDGE.md. +- 8 (MPST protocol contract): DONE — 7 projection derivations (syncer + = P_leader, connector = P_follower; direct, one-bounce, and maximal + four-bounce sessions; record and replay legs) + 4 polarity/shape + controls. Bounce cap is structural (no five-bounce term exists); + stuck-freedom and cap-violation checking are open engine work. +- 6 (trace bridge): DONE — see `TRACE_BRIDGE.md`. The engine's + runtime trace checker was evaluated and NOT adopted (hardcoded + vocabulary); the deliverable-7 policy set is the oracle. The "real + executions" leg is IMPLEMENTED end to end: the shipped syncer + carries a test-only commit-order recorder + (`pkg/sync/sync_trace_audit.go`), the chaos harness exports cold, + warm, crash/resume multi-attempt, tombstone-delta, record-flip, + session-zombie, and external-principal JSONL fixtures, and + `real_trace_oracle_test.go` checks them against all seven policies + (56 cells: 54 green + TWO deliberately red — the session-zombie + fixture under session_ckpt_consistency, the standing known-defect + pin on the shipped session semantics, which flips to green when + checkpoint-consistent sessions land; and the SQLite + external-principal fixture under external_principal_grounding, the + standing known-degrade pin on the non-deleting engine's + warn-and-continue resume) with planted-violation + validation of the bridge itself (dropped consult, un-regrounded + resume, stripped resume marker, ungrounded delete, stripped + external reconciliation). The instrument + falsified a documented resume mechanism AND witnessed the model's + phantom-union prediction live in the shipped syncer (the + verdict-flip path), now fixed by record-round grounding — see the + findings at the end of `TRACE_BRIDGE.md`. The refimpl leg remains + as the demand-graph instance of the same oracle. + +## Broken vs good, both ways + +Two demonstrations pair the known-broken algorithm against the +known-good one: + +- DEDUCTIVE (`phantom_test.go` + `src/sync_phantom.occult`): the + engine PROVES the broken composition manufactures the phantom union + — a row deleted upstream survives in the sealed artifact — while + every ingredient response is individually truthful, and proves the + premise-validated grounding yields exactly upstream truth from the + same stale-cache ingredients. The controls prove the broken artifact + is NOT the epoch it claims to be. +- EXECUTABLE (`refimpl_oracle_test.go` + `host/refimpl/`): the + demand-graph reference implementation runs the same scenario to seal + — content matches upstream and every attempt trace passes all seven + policies, with and without a crash. In legacy mode the sealed + artifact carries the phantom row: on a clean run the ordering + policies are (correctly) blind to it — the composition class belongs + to the algebra — and on a crash/resume run the legacy + resume-without-regrounding habit is caught by the trace oracle, + firing exactly clear-before-upsert on the resumed attempt. + +## Engine findings (for upstream) + +- Plain CLI file evaluation loads `=` definitionally; axiomatic + loading needs a host (`LoadStdlib`) — no CLI flag exists for it. +- Same-line trailing `#` comments after statements fail to parse in + stdlib-loaded modules; full-line comments only. +- Parameterized protocol definitions (`f(r) = ... send(r) ...` with + `r : Protocol`) gate their rewrite on classifier membership that + free message tags don't carry; closed session terms (the raft shape) + derive fine, and projection equivalences close as CONDITIONAL + equivalences under `Domain:Protocol` membership premises (same as + the engine's own raft test on the egraph backend). +- `pipe` has no associativity axiom: session terms and expected + projections must share the same grouping. +- Ground-term evaluation cost grows steeply (roughly doubling per + list element) when reducing recursive verdict functions over long + cons-lists under axiomatically loaded rules: a 12-element trace + evaluates in seconds, a 25-element trace exceeded 18 minutes. The + real-trace renderer coalesces consecutive checkpoints to stay + tractable; an efficient ground-evaluation path is on the engine + ask list. +- Constrained axiom universals (the `unconstrained-axiom-universal` + lint's prescribed fix, raised by the engine author against our + modules) cannot be adopted for free-constructor algebras yet: + gating a rewrite universal on a userspace sort makes dispatch + require membership evidence that free terms are never given, so + member-only ground terms stop reducing. All four documented + membership forms fail to feed the gate (set extension, extension + + `Unique`, `∃ x ∈ S;` declaration, `∈`-premise with `⇒`); built-in + classifiers (`Number`) gate correctly. Same root cause as the + parameterized-projection footgun above; pinned executable in + `host/constrained_params_probe_test.go`, which fails loudly when + the engine closes the gap (the signal to constrain our modules). + Until then our universals stay deliberately unconstrained, with + head-guard discipline (module-local constructor heads on every + LHS) as the mitigation, documented per module. diff --git a/formal/occult/TRACE_BRIDGE.md b/formal/occult/TRACE_BRIDGE.md new file mode 100644 index 000000000..505e66664 --- /dev/null +++ b/formal/occult/TRACE_BRIDGE.md @@ -0,0 +1,324 @@ +# Trace bridge — P traces and real syncs onto the Occult policy oracle (deliverable 6) + +The policy oracle set (`src/sync_trace_policies.occult`, deliverable 7) +consumes a canonical event list. This note pins the canonical +vocabulary, the two mappings onto it (P counterexample traces; real +sync executions), and the evaluation of the engine's built-in runtime +trace checker. + +## Canonical vocabulary + +One trace = one sync attempt's events, in commit order. Constructors +(two-scope envelope, matching the P models' small scopes): + +| Event | Meaning | +|-------|---------| +| `ev_consult(s)` | source-cache consult for scope s (verdict arrives with it) | +| `ev_clear(s)` | partition clear for scope s | +| `ev_replay(s)` | committed replacement copy for scope s (replay-unit commit) | +| `ev_upsert(s)` | row upsert into scope s's partition | +| `ev_publish(s)` | scope s's rows published to the artifact | +| `ev_checkpoint` | durable watermark commit | +| `ev_seal` | artifact seal | + +| `ev_delete(s)` | committed tombstone application (the delta protocol's delete leg) for scope s | +| `ev_resume` | crash/resume attempt boundary inside one sync's trace | + +`ev_delete` is a WRITE with upsert's obligations: it needs grounding +(clear-before-write — a tombstone against a base this sync never +copied is the un-regrounded class, delete flavor), dirties the +quiescent-checkpoint flag, and marks the scope active for seal +obligations. Naming note: these "tombstones" are deletion entries in +the CONNECTOR RESPONSE (`DeletedIds`/`DeletedPrincipalIds` on the +replay/record annotations), applied synchronously as plain row +deletes — nothing deletion-shaped is durably stored. + +The multi-attempt extension: a trace is ONE SYNC's events, with +`ev_resume` marking attempt boundaries. Durable facts persist across +the marker — consult flags (the checkpoint-durable hit-set), clear +grounding (committed rows), scope activity and publishes — and only +once-per-scope RESETS: an interrupted action restarts from its root +token, so the across-attempt replay re-copy is B5-legal at-least-once +idempotence, while a within-attempt duplicate remains the bug. +Single-attempt traces carry no marker and mean what they always did. + +The external-principal extension (policy 7, the +`deleteStaleExternalPrincipals` contract): `ep_list` marks a phase run +listing the external source's CURRENT answer; `ep_live(p)` declares p +a member of that answer; `ep_recon` marks reconciliation COMPLETED +(stale copies deleted, or nothing stale); `ep_copy(p)` is p's +committed principal write. These live in their own keyspace — +invisible to the artifact policies (1–5) and the session policy (6), +like session events are to both. Committed copies are DURABLE across +`ev_resume` (checkpoint resume deliberately retains completed writes), +which is the debris premise: a between-attempt shrink strands a dead +attempt's copies unless reconciliation deletes them. The +warn-and-continue degrade of a non-deleting engine emits NO `ep_recon` +— the pass ran but did not reconcile — so its first copy is the +recon-before-copy violation. + +Pending extension, tracked not modeled: generation stamps (the graph +model's variant-S lineage). + +## Mapping 1: P model announce events — PROSE ONLY (no renderer) + +STATUS: this mapping is a documented convention, not an implemented +instrument — `host/` carries no P-trace renderer (only +`renderRealTrace` for Mapping 2 and `refimpl.RenderOccult` for the +reference implementation). A P counterexample is judged today by +reading it against this table by hand; anyone automating it must +build the renderer from the ACTUAL announce vocabularies below and +add a planted-seal validation test (a renderer that fails to emit +`ev_seal` silently vacates the seal-anchored policies 4, 5, and 7, +which are green-by-prefix on seal-less traces). + +The walker and graph models announce through the monitor event +vocabulary (`formal/walker/PSrc/Events.p`, `formal/graph/PSrc/Events.p`). +A P counterexample trace renders onto the canonical list by keeping +announce events in schedule order and dropping everything else: + +| P announce (walker / graph where they differ) | Canonical | +|------------|-----------| +| `eAnnConsult` | `ev_consult(s)` | +| `eAnnClear` (incl. the unit's clear constituent) | `ev_clear(s)` | +| `eAnnReplay` / `eAnnReplayCopy` (copy commit) | `ev_replay(s)` | +| `eAnnUpsert` | `ev_upsert(s)` | +| `eAnnTombstones` (one per removed id) | `ev_delete(s)` | +| `eAnnPublish` (artifact-facing publish only) | `ev_publish(s)` | +| `eAnnCheckpoint` | `ev_checkpoint` | +| `eAnnSessionSet` | `ev_swrite(k)` | +| `eAnnSessionGet` / `eAnnSessionRead` (found: hit, else miss) | `ev_sread_hit(k)` / `ev_sread_miss(k)` | +| `eAnnSeal` / `eAnnGSeal` | `ev_seal` | + +Session-write REQUESTS (`eGSessionPub`) do not render: they are the +wire message to the store, and the canonical event is the store's +committed announce (`eAnnSessionSet`), same as every other write. +Session events map to the session keyspace (`ev_swrite`), never to +`ev_publish` — a session write is not an artifact publish. + +The atomic units expand to their ANNOUNCE order, which differs by +model: the walker's `eReplayUnit`/`eOverlayUnit` announce clear, copy +(, upserts, tombstones), publish; the graph's `eGReplayUnit` announces +the marker put FIRST (the P-MARK convention — see +`graph/PSrc/Store.p`'s unit handlers), then clear, copy, publish; and +the graph's `eGOverlayUnit` announces marker, clear, copy, upserts, +tombstones, publish. Marker puts have no canonical event (generation +stamps are the tracked pending extension). ONE deliberate exception to +"every unit contributes a clear": under the G8b `composeDead` INJECT +(`Store.p`'s overlay handler, the `tcG8bMut_P1` kill), the overlay +unit composes onto existing debris and announces the copy with NO +clear at all — that missing clear IS the kill, so a hand-renderer +following this table must not supply a clear the model never +announced, or the injected counterexample's clear-before-write red is +masked into a green. The policies check the leg ordering the honest +units guarantee by construction; running unit-built traces through +the oracle is a consistency check of that guarantee, not new +information. Crash scenarios cut the list at the crash point: +a cut trace must still satisfy the prefix-closed policies (1–3), +while the seal-anchored policies (4–5) are vacuous without `ev_seal` +— exactly the sync-scoped-freshness stance the models take. + +## Mapping 2: real sync executions — IMPLEMENTED + +The runtime emits the same shapes from the syncer: +`ev_consult` = the source-cache lookup round-trip; +`ev_clear`/`ev_upsert` = c1z store writes; `ev_replay` = the completed +replacement copy of an attested base; `ev_publish` = the scope's rows +becoming artifact-visible; `ev_checkpoint` = the sync-state watermark +write; `ev_seal` = `.c1z` finalization. A chaos harness (kill points +between any two events) yields cut traces checked as above. The +chaos-scenario map is: each red fixture in the policy module is the +minimal chaos outcome for its policy — e.g. `trace_red_cbp` is +"killed the watermark write, sealed anyway", `trace_red_seal` is +"sealed with an active unpublished scope" (the phantom-union family's +observable footprint at the artifact boundary). + +This leg now runs against the SHIPPED syncer. `pkg/sync` carries a +test-only commit-order recorder (`sync_trace_audit.go`, +`testSyncTraceAudit` — nil-checked, one pointer test per event, the +`testQueueAudit` pattern) fired at the orchestration seams: lookup +resolution (consult), the replay unit's clear+copy legs in the store's +contractual order, scoped page-row commits (upsert, page granularity), +manifest-entry writes (publish), durable checkpoint tokens, and +EndSync (seal). The chaos test `chaos_trace_oracle_test.go` records +the reference source-cache scenario cold and warm and exports JSONL +fixtures (`host/testdata/realtraces/`); `host/real_trace_oracle_test.go` +renders them onto the canonical vocabulary and checks all seven +policies. + +Two conventions live in the RENDERER, never the recorder (the recorder +is purely observational): scope names map onto s1/s2 in first-seen +order (two-scope envelope), and a NON-resumed attempt's upsert with no +earlier explicit clear gets a structural `ev_clear` inserted — the +partition was born empty at StartNewSync. Resumed attempts get no such +insertion, which is exactly how an un-regrounded resume reds +clear-before-upsert. The bridge itself is validated by planted +violations (`TestRealTraceBridgeCatchesPlantedViolation`): dropping the +warm fixture's consult reds policy 1, and replaying its writes as a +resumed attempt with the replay downgraded to a bare upsert reds +policy 2. + +Multi-attempt traces are in-domain too. The chaos harness cuts a warm +two-page delta round with an `EffectCrash` after the replay unit and +overlay upsert committed, resumes with a new syncer, and exports the +two attempts as one fixture with a `{"kind":"resume"}` marker line +(`warm_replay_sync_interrupted.jsonl`). All seven policies are green +on it, and `TestRealTraceBridgeResumeMarkerLoadBearing` proves the marker +is load-bearing: stripping it turns the two legal across-attempt +replays into a within-attempt duplicate and reds once-per-scope. Two +rendering notes: consecutive checkpoints coalesce to one (verdict +preserving; the engine's evaluation cost grows steeply with event +count), and the structural clear is once per scope per SYNC, attempts +included, granted to the scope's first WRITE (upsert or delete). + +Division-of-labor note for the one-term rendering: clear grounding +persists across `ev_resume` BY DESIGN (committed rows survive the +crash), so in a whole-sync term an attempt-1 clear legitimately +grounds an attempt-2 write and policy 2 cannot red the +resume-without-regrounding class here. That class is covered on the +refimpl leg, which renders each attempt as its own term — a resumed +attempt writing without its own grounding reds clear-before-upsert +(`TestRefImplLegacyCrashResume`) — and its record-flavor incarnation +(the verdict-flip union) is a CONTENT violation owned by the +exporting test's content oracle, per the scope note below. The +policy module carries the same statement at its `ev_resume` +declaration. + +The delete leg is fixtured too: `warm_replay_sync_tombstone.jsonl` +records a warm delta round that replays the base, overlay-upserts one +row, tombstones a departed row, and publishes — B3's within-page +commit order (rows, then tombstones, then the validator) appears +directly in the trace, and the exporting chaos test's content oracle +proves the tombstoned row is absent from the sealed artifact. +`TestRealTraceBridgeCatchesUngroundedDelete` plants the violation: +the same real delete with its grounding stripped reds +clear-before-upsert. + +The instrument also produced a finding about the shipped resume path: +for a mid-chain cut, the resume RE-RUNS the replay copy regardless of +checkpoint cadence — checkpoints commit at batch boundaries and a page +chain runs inside one batch, so `MarkSourceCacheReplayed` from a cut +chain never reaches a checkpoint. The resume suite's prior comment +claimed the restored replayed-set skips the copy; the trace recorder +is the first instrument able to distinguish that skip from an +idempotent re-copy, and it falsified the claimed mechanism (the +corrected comments live in `chaos_source_cache_resume_test.go`; the +convergence conclusion was always right, via B5 idempotence). The +replayed-set's real skip role is within-attempt: a later replay +annotation for an already-copied scope skips. + +The instrument's second finding was a live defect, model-predicted: +the walker calibration's scenario-1 family (the phantom union, tc1c +flavor — `formal/walker/CALIBRATION.md`) was REACHABLE in the shipped +syncer via the verdict-flip path. A warm round cut after its replay +copy committed but before its validator published, upstream moving +between attempts, and the resume's consult missing meant the connector +served a fresh RECORD round — which composed with the crashed +attempt's copied debris and sealed the union under the fresh validator +(the non-self-healing direction: the next sync's consult validates the +entry clean and replays the mosaic forward). Witnessed by +`TestChaosSourceCacheRecordFlipOverReplayDebris` +(`pkg/sync/chaos_source_cache_resume_test.go`); fixed by RECORD-ROUND +GROUNDING: a record round is a replacement listing, so before its +first write to a scope this attempt, a partition holding rows that no +completed round published is cleared (`ClearSourceCacheScope` — the +replay unit's clear leg exposed standalone; `groundRecordScope` in +`source_cache_orchestration.go`). The fix is trace-visible: record +rounds now emit a REAL `ev_clear` before their first write — +"replacement rounds clear first", previously granted structurally by +the renderer, now witnessed in `cold_record_sync.jsonl` — and the flip +scenario is fixtured as `warm_replay_sync_record_flip.jsonl`, where +attempt 2's clear with no replay after it IS the grounding. Scope +note, pinned honestly: the ordering policies do NOT red the un-fixed +flip — attempt 1's real clear grounds the scope durably across resume, +so the union was ordering-legal; it is a CONTENT violation (the +walker model's `P1-CONTENT`), owned by the exporting test's content +oracle. The policies' role in the fix is the positive direction: the +grounded trace joins the green suite. + +The instrument's third finding is a STANDING known-defect pin — the +first fixture committed with a deliberately red expectation. Sessions +commit durably at op time, OUTSIDE the checkpoint mechanism +(`SessionSet` batches commit immediately in the pebble engine), so a +crash rolls the cursor back but not the session namespace: writes from +beyond the restored checkpoint survive into the re-run window, which +can then consume its own dead attempt's "future" (CO-6b-009). The +session vocabulary (`ev_swrite`, `ev_sread_hit`, `ev_sread_miss`, keys +on the one-key `k1` envelope) and policy 6 +(`session_ckpt_consistency`) state the root cause: post-crash session +state must equal the restored checkpoint's, in both directions — +ZOMBIE (a dead attempt's beyond-checkpoint write observed by the +re-run) and AMNESIA (a checkpoint-committed value silently deleted; +its producing work never re-runs — the shape of the reverted +resume-clear fix). The fixture `warm_replay_sync_session_zombie.jsonl` +is recorded from a real crash/resume execution by +`TestChaosSourceCacheSessionPersistsAcrossResume`, which acts as the +session actor (the chaos connector has no session plumbing): the +probe write and the re-run read fire through the same recorder at the +moment of their real store operations. The oracle judges it +`violation: session-zombie-read` — asserted as the EXPECTED verdict in +`realTraceExpected`. When checkpoint-consistent sessions land (the +registered future work), the recorded read becomes a miss and the +expectation flips to "ok". The same constraint is model-side in the +walker as the P6-C monitor, whose three cells red the shipped +semantics (zombie), red the rejected wholesale clear (amnesia), and +green the checkpoint-consistent fix — `formal/walker/CALIBRATION.md`, +decision 25. + +The instrument's fourth leg is the external-principal phase +(`SyncExternalResources` — an outside source's principals copied into +the sync store beside connector rows). The syncer records `ep_list` +after the source's answer is listed, `ep_live` per member, `ep_recon` +when `deleteStaleExternalPrincipals` COMPLETES (deletes applied or +none needed — the warn-and-continue degrade branch of a non-deleting +engine records nothing), and `ep_copy` per committed principal write. +Two fixtures are exported by the existing external-principal chaos +tests: `external_resume_current_answer.jsonl` (five attempts, +capable engine, shrink mid-sync — green on all seven policies: every +attempt that COPIES does so only after a completed `ep_recon`; +mid-phase crash attempts re-list and are cut before reconciling, +which is legal — the recon gate binds copies, not lists) and +`external_resume_sqlite_degrade.jsonl` — a STANDING KNOWN-DEGRADE PIN, +expected `violation: ext-recon-before-copy`: SQLite cannot delete, the +resume warns and copies the current answer over the dead attempt's +unreconciled debris. That is the ACCEPTED degradation (one-artifact +staleness, self-healing at the next cold sync, no replay channel to +launder it further), documented mechanically as an expected red +exactly like the session pin. The renderer maps distinct principal +ids onto the two-principal envelope (p1/p2) in first-seen order and +projects further principals out — sound for the kept ones, since the +policy tracks principals independently and the recon gate is +principal-agnostic. Bridge validation for the other direction: +`TestRealTraceBridgeCatchesStaleExternalSurvivor` strips the final +attempt's reconciliation and copies from the capable-engine fixture's +REAL events and the oracle reds `ext-stale-survivor`. The same +contract is model-side in the walker as the P8 monitor (scenario 8, +`formal/walker/CALIBRATION.md`): two calibrated reds — the +non-deleting engine's stale survivor, the stale-list recency mutant — +and three greens including the completed-then-crash carry +(sync-scoped freshness). + +This leg has an executable instance: `host/refimpl/` (the demand-graph +reference implementation) emits canonical traces from real executions +of the phantom-union scenario, rendered by `RenderOccult` and checked +through the oracle in `host/refimpl_oracle_test.go`. The run also +documents the oracle's DIVISION OF LABOR empirically: the legacy +composition bug (misgrounded diff) seals a phantom artifact with a +policy-clean trace — ordering policies cannot see it, the algebra owns +it (`phantom_test.go`) — while the legacy resume-without-regrounding +habit is an ordering bug and fires clear-before-upsert on the resumed +attempt. + +## Engine runtime trace checker: evaluated, not adopted + +The engine ships a runtime trace checker +(`../occult/runtime_trace_checker.go`) with policies over a HARDCODED +vocabulary (`consume/emit/commit/checkpoint/ack` — +`runtime_trace_policies.go`). Two of its obligations rhyme with ours +(checkpoint-before-progress ~ its commit/checkpoint ordering), but the +vocabulary cannot express scopes, consults, or seals without a mapping +layer that would erase exactly the distinctions our policies check. +Decision: the oracle is the pure-Occult policy set of deliverable 7, +run through the engine host (`host/trace_policies_test.go`); the +runtime checker stays unused for this track. If the engine grows +user-defined trace vocabularies, revisit. diff --git a/formal/occult/host/compression_test.go b/formal/occult/host/compression_test.go new file mode 100644 index 000000000..b3ca2af4c --- /dev/null +++ b/formal/occult/host/compression_test.go @@ -0,0 +1,115 @@ +// L9 — stamp-compression admissibility (formal/occult/LAWS.md; graph +// model find G9-CAL-1). These are arithmetic facts about the floor-2 +// bucketing, checked GROUND-EXHAUSTIVELY over generations 0..7 — the +// same small-scope envelope the P models use (all known behavior fits +// it; the bound is part of the claim). Each instance is evaluated by +// the engine (native arithmetic + classify), not by Go: Go only +// enumerates the finite domain and reads the engine's verdict string. +// +// The negative control is the FALSE-LIVE claim "floor2(s) >= floor2(cur) +// implies s >= cur" — the parity ambiguity that G9-CAL-1 found +// dynamically (P PASS-BUDGET livelock). The engine must exhibit at +// least one violating pair, and exactly the predicted ones: s = cur-1 +// with cur odd. +package host_test + +import ( + "context" + "fmt" + "testing" + + occult "github.com/conductorone/occult" + "github.com/conductorone/occult/state" +) + +const genBound = 8 // generations 0..7: covers first-admission (odd) and bucket-aligned (even) mints + +// evalVerdict evaluates one engine expression that must reduce to the +// string "ok" or "violated". CLI-parity interpreter: the arithmetic and +// boolean stdlibs must be loaded for %, <, ==, classify to reduce. +func evalVerdict(t *testing.T, source string) string { + t.Helper() + interp, pm, err := occult.NewCLIInterpreter("egraph", false, "", "") + if err != nil { + t.Fatalf("NewCLIInterpreter: %v", err) + } + res, err := interp.Eval(context.Background(), "compression-check", source, pm) + if err != nil { + t.Fatalf("Eval %q: %v", source, err) + } + if res == nil || res.Loc == nil { + t.Fatalf("Eval %q: no result location", source) + } + st, ok := interp.State.Resolve(*res.Loc) + if !ok || st.Literal == nil || st.Literal.Kind != state.LitString { + t.Fatalf("Eval %q: result is not a string literal", source) + } + return st.Literal.Str +} + +// implSource renders: premise(s, cur) -> conclusion(s, cur), engine-side, +// as nested classify with string verdicts. +func implSource(premise, conclusion string, s, cur int) string { + p := fmt.Sprintf(premise, s, cur) + c := fmt.Sprintf(conclusion, s, cur) + return fmt.Sprintf( + `classify (%s) { {true,} : classify (%s) { {true,} : "ok", Bool : "violated" }, Bool : "ok" }`, + p, c) +} + +const floor2s = `(%[1]d - %[1]d %% 2)` +const floor2c = `(%[2]d - %[2]d %% 2)` + +func TestL9aDefiniteStaleSound(t *testing.T) { + // floor2(s) < floor2(cur) -> s < cur + for s := 0; s < genBound; s++ { + for cur := 0; cur < genBound; cur++ { + src := implSource(floor2s+` < `+floor2c, `%[1]d < %[2]d`, s, cur) + if v := evalVerdict(t, src); v != "ok" { + t.Errorf("L9a violated at s=%d cur=%d", s, cur) + } + } + } +} + +func TestL9bBucketAlignedLivenessProvable(t *testing.T) { + // cur even AND s <= cur AND floor2(s) >= floor2(cur) -> s = cur + for s := 0; s < genBound; s++ { + for cur := 0; cur < genBound; cur += 2 { + full := fmt.Sprintf( + `classify (%[1]d <= %[2]d) { {true,} : classify ((%[1]d - %[1]d %% 2) >= (%[2]d - %[2]d %% 2)) { {true,} : classify (%[1]d == %[2]d) { {true,} : "ok", Bool : "violated" }, Bool : "ok" }, Bool : "ok" }`, + s, cur) + if v := evalVerdict(t, full); v != "ok" { + t.Errorf("L9b violated at s=%d cur=%d (cur even)", s, cur) + } + } + } +} + +func TestL9NegativeControlFalseLive(t *testing.T) { + // WRONG law: floor2(s) >= floor2(cur) -> s >= cur. Must be violated, + // and exactly at the G9-CAL-1 ambiguity: s = cur-1 with cur odd. + violations := map[[2]int]bool{} + for s := 0; s < genBound; s++ { + for cur := 0; cur < genBound; cur++ { + src := implSource(floor2s+` >= `+floor2c, `%[1]d >= %[2]d`, s, cur) + if v := evalVerdict(t, src); v == "violated" { + violations[[2]int{s, cur}] = true + } + } + } + if len(violations) == 0 { + t.Fatalf("false-live control: no violating pair found — the ground check cannot refute a wrong law") + } + for pair := range violations { + s, cur := pair[0], pair[1] + if !(cur%2 == 1 && s == cur-1) { + t.Errorf("false-live control: unexpected violation shape at s=%d cur=%d (predicted only s=cur-1, cur odd)", s, cur) + } + } + for cur := 1; cur < genBound; cur += 2 { + if !violations[[2]int{cur - 1, cur}] { + t.Errorf("false-live control: predicted ambiguity pair s=%d cur=%d not exhibited", cur-1, cur) + } + } +} diff --git a/formal/occult/host/constrained_params_probe_test.go b/formal/occult/host/constrained_params_probe_test.go new file mode 100644 index 000000000..f72e20729 --- /dev/null +++ b/formal/occult/host/constrained_params_probe_test.go @@ -0,0 +1,220 @@ +// Semantics pin: constrained axiom universals under axiomatic loading. +// +// Context: the engine author's feedback on our modules — "your +// parameters in the examples are largely unconstrained ... Something +// like p(x: Number) instead limits the matching of the callable" (lint +// kind unconstrained-axiom-universal). The hazard is real: an +// unconstrained universal matches e-classes of any shape, including +// cross-model classes merged by module bridges (the engine's +// COMMON_MISTAKES.md §7 names the arithmetic-versus-Peano regression +// this lint was added for). +// +// This probe pins why we have NOT adopted the fix for our free- +// constructor algebras (sync_trace_policies, sync_laws, sync_phantom): +// under LoadStdlib/axiomatic loading, a userspace-sort constraint on an +// axiom universal gates dispatch on classifier membership that free +// constructor terms cannot currently be given. Every documented +// membership form was tried — set extension (Scope = {s1, s2,}), set +// extension plus distinctness (Scope ∈ std.types.Unique), declaration- +// with-membership (∃ s1 ∈ Scope;), and an explicit rewrite premise +// (s ∈ Scope ⇒ rule) — and in all four the member-only green fixture +// stops reducing: membership stays undecided at dispatch, the rule is +// ineligible, the verdict is a stuck term. Built-in classifiers DO +// work (the Number probe below), so the gap is specifically userspace +// free-term membership evidence. This is the same root cause that +// blocked parameterized MPST projection (sync_protocol.occult's +// closed-term rewrite); the engine's own stdlib shows the unresolved +// state — model/stdlib/vector_clock.occult constrains its universals +// to a VectorClock sort that nothing anywhere gives members, which is +// why sync_laws.occult copies the vc_merge ACI axioms instead of +// requiring the module. +// +// The pins below assert TODAY'S behavior. When the engine lands +// membership evidence for free-term classifiers (see the "constrained +// universals" ask in docs/tasks/occult-engine-changes-brief.md), +// TestConstrainedParamsUserspaceMembershipNotConsumed fails — that is +// the signal to constrain the real modules' scope/flag universals and +// delete the pin. +package host_test + +import ( + "context" + "fmt" + "testing" + + occult "github.com/conductorone/occult" + "github.com/conductorone/occult/state" +) + +// probeConstrainedSrc gates the mini-policy's scope and flag universals +// on userspace sorts, with membership stated in the declaration form +// (∃ member ∈ Sort;). Trace tails (t, r) stay unconstrained — traces +// are inductively generated, not enumerable. +const probeConstrainedSrc = `# lint:disable mixed-source-semantics +# lint:disable unguarded-recursion +# lint:disable unconstrained-axiom-universal +syntax("standard"); + +∃ ev_a; +∃ ev_b; +∃ tnil; +∃ tcons; + +∃ Scope; +∃ s1 ∈ Scope; +∃ s2 ∈ Scope; +∃ Flag; +∃ tt ∈ Flag; +∃ ff ∈ Flag; + +s : Scope; +f : Flag; + +∃ probe; +∃ probe_go; +probe(t) = probe_go(t, ff); +probe_go(tnil, f) = "ok"; +probe_go(tcons(ev_a(s), r), f) = probe_go(r, tt); +probe_go(tcons(ev_b(s), r), tt) = probe_go(r, tt); +probe_go(tcons(ev_b(s), r), ff) = "violation: b-before-a"; + +∃ probe_green; +probe_green = tcons(ev_a(s1), tcons(ev_b(s2), tnil)); +` + +// probePremiseSrc states the gate as an explicit rewrite premise +// (s ∈ Scope ⇒ rule) — the docs' other prescribed form. Note the +// implication arrow must be ⇒ (U+21D2); ASCII => is rejected at axiom +// load with "rewrite '=>' requires rewrite LHS". +const probePremiseSrc = `# lint:disable mixed-source-semantics +# lint:disable unguarded-recursion +# lint:disable unconstrained-axiom-universal +syntax("standard"); + +∃ ev_a; +∃ ev_b; +∃ tnil; +∃ tcons; +∃ tt; +∃ ff; + +∃ Scope; +∃ s1 ∈ Scope; +∃ s2 ∈ Scope; + +∃ probe; +∃ probe_go; +probe(t) = probe_go(t, ff); +probe_go(tnil, f) = "ok"; +s ∈ Scope ⇒ probe_go(tcons(ev_a(s), r), f) = probe_go(r, tt); +s ∈ Scope ⇒ probe_go(tcons(ev_b(s), r), tt) = probe_go(r, tt); +s ∈ Scope ⇒ probe_go(tcons(ev_b(s), r), ff) = "violation: b-before-a"; + +∃ probe_green; +probe_green = tcons(ev_a(s1), tcons(ev_b(s2), tnil)); +` + +// probeBuiltinSrc constrains the scope universal to the BUILT-IN Number +// classifier with numeric scope payloads: the control proving the +// dispatch gate itself works when membership is decidable. +const probeBuiltinSrc = `# lint:disable mixed-source-semantics +# lint:disable unguarded-recursion +# lint:disable unconstrained-axiom-universal +syntax("standard"); + +∃ ev_a; +∃ ev_b; +∃ tnil; +∃ tcons; +∃ tt; +∃ ff; + +s : Number; + +∃ probe; +∃ probe_go; +probe(t) = probe_go(t, ff); +probe_go(tnil, f) = "ok"; +probe_go(tcons(ev_a(s), r), f) = probe_go(r, tt); +probe_go(tcons(ev_b(s), r), tt) = probe_go(r, tt); +probe_go(tcons(ev_b(s), r), ff) = "violation: b-before-a"; + +∃ probe_green; +probe_green = tcons(ev_a(1), tcons(ev_b(2), tnil)); + +∃ bogus; +∃ probe_rogue; +probe_rogue = tcons(ev_a(bogus), tnil); +` + +// probeEval loads module src as name and evaluates M.probe(term), +// returning the verdict string and whether it reduced to one. +func probeEval(t *testing.T, name, src, term string) (string, bool) { + t.Helper() + interp, pm, err := occult.NewCLIInterpreter("egraph", false, "", "") + if err != nil { + t.Fatalf("NewCLIInterpreter: %v", err) + } + if _, err := interp.LoadStdlib(name, src, pm); err != nil { + t.Fatalf("LoadStdlib %s: %v", name, err) + } + source := fmt.Sprintf(`M = require(%q); M.probe(%s)`, name, term) + res, err := interp.Eval(context.Background(), "probe", source, pm) + if err != nil { + t.Fatalf("Eval %q: %v", source, err) + } + if res == nil || res.Loc == nil { + return "", false + } + st, ok := interp.State.Resolve(*res.Loc) + if !ok || st.Literal == nil || st.Literal.Kind != state.LitString { + return "", false + } + return st.Literal.Str, true +} + +// TestConstrainedParamsBuiltinClassifierWorks is the positive control: +// with a built-in classifier (Number) the constrained rule fires and +// the verdict reduces. The gate mechanism itself is sound. +func TestConstrainedParamsBuiltinClassifierWorks(t *testing.T) { + if v, ok := probeEval(t, "probe_builtin", probeBuiltinSrc, "M.probe_green"); !ok || v != "ok" { + t.Fatalf("green fixture under built-in Number gating: got (%q, reduced=%v), want (\"ok\", true)", v, ok) + } +} + +// TestConstrainedParamsGateRefusesNonMembers pins that the gate is +// enforced at dispatch, not advisory lint metadata. The refusal is +// asserted against probeBuiltinSrc — the module with a WORKING +// positive (TestConstrainedParamsBuiltinClassifierWorks reduces +// probe_green on it) — so non-reduction of the rogue trace can only +// mean the Number gate refused the non-numeric payload, not that the +// module never reduces anything. (Asserting refusal on +// probeConstrainedSrc would be vacuous: nothing reduces under it — +// that is exactly the gap TestConstrainedParamsUserspaceMembershipNotConsumed +// pins.) +func TestConstrainedParamsGateRefusesNonMembers(t *testing.T) { + if v, ok := probeEval(t, "probe_builtin", probeBuiltinSrc, "M.probe_rogue"); ok { + t.Fatalf("non-member scope reduced to %q: constraints are not gating dispatch", v) + } +} + +// TestConstrainedParamsUserspaceMembershipNotConsumed pins the gap: a +// MEMBER-only trace under userspace-sort gating does not reduce either +// — declared membership (∃ s1 ∈ Scope;) and explicit ∈-premises are +// both invisible to axiom-side dispatch, so the gate treats members +// like non-members and the rule never fires. +// +// WHEN THIS TEST FAILS (a verdict reduces): the engine has learned to +// consume userspace membership evidence. Constrain the scope/flag +// universals in sync_trace_policies.occult and sync_phantom.occult per +// the plan in the engine-changes brief, then delete this pin. +func TestConstrainedParamsUserspaceMembershipNotConsumed(t *testing.T) { + if v, ok := probeEval(t, "probe_constrained", probeConstrainedSrc, "M.probe_green"); ok { + t.Fatalf("member-only trace reduced to %q under declaration-form membership: "+ + "the engine now consumes userspace membership — constrain the real modules and delete this pin", v) + } + if v, ok := probeEval(t, "probe_premise", probePremiseSrc, "M.probe_green"); ok { + t.Fatalf("member-only trace reduced to %q under ∈-premise gating: "+ + "the engine now consumes userspace membership — constrain the real modules and delete this pin", v) + } +} diff --git a/formal/occult/host/go.mod b/formal/occult/host/go.mod new file mode 100644 index 000000000..2b43f6524 --- /dev/null +++ b/formal/occult/host/go.mod @@ -0,0 +1,13 @@ +module github.com/conductorone/baton-sdk/formal/occult/host + +go 1.26.0 + +require github.com/conductorone/occult v0.0.0-00010101000000-000000000000 + +require ( + github.com/tetratelabs/wazero v1.11.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect +) + +replace github.com/conductorone/occult => ../../../../occult diff --git a/formal/occult/host/go.sum b/formal/occult/host/go.sum new file mode 100644 index 000000000..820d1cb2d --- /dev/null +++ b/formal/occult/host/go.sum @@ -0,0 +1,6 @@ +github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= +github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/formal/occult/host/laws_test.go b/formal/occult/host/laws_test.go new file mode 100644 index 000000000..5dd019281 --- /dev/null +++ b/formal/occult/host/laws_test.go @@ -0,0 +1,125 @@ +// Deliverable 9 (docs/tasks/sync-formal-model-brief.md): the equational +// laws of formal/occult/LAWS.md checked by equality saturation against +// the defining equations in ../src/sync_laws.occult. Each check builds a +// fresh interpreter (no cross-check e-graph pollution), loads the axiom +// module into UniversalScope, loads the two query terms as separate +// modules, bridges require() access, saturates, and asks the solver for +// equivalence. Negative controls assert NON-equivalence so a saturation +// bug that merges everything cannot silently green the law set. +// +// This module is a local verification harness: it depends on the sibling +// engine checkout via a replace directive and is not part of the +// baton-sdk public module. +package host_test + +import ( + "os" + "path/filepath" + "testing" + + occult "github.com/conductorone/occult" + "github.com/conductorone/occult/ir" +) + +func readSrc(t *testing.T, name string) string { + t.Helper() + b, err := os.ReadFile(filepath.Join("..", "src", name)) + if err != nil { + t.Fatalf("read %s: %v", name, err) + } + return string(b) +} + +// findQueryNode mirrors the engine's axiom-test helper: the query is the +// last statement of a semicolon-separated module. +func findQueryNode(interp *occult.Interpreter, rootID ir.NodeID) ir.NodeID { + node, ok := interp.Graph.Node(rootID) + if !ok { + return rootID + } + if node.Kind == ir.KindList { + children := interp.Graph.SourcesOfType(rootID, ir.EdgeAst) + if len(children) > 0 { + return children[len(children)-1] + } + } + return rootID +} + +var lawModules = []localModule{ + {"sync_laws", "sync_laws.occult"}, + {"sync_fixtures", "sync_fixtures.occult"}, +} + +// areEquivalent runs the shared pipeline with the law modules loaded. +func areEquivalent(t *testing.T, lhs, rhs string) bool { + t.Helper() + return equivalent(t, nil, lawModules, lhs, rhs) +} + +// prelude prefixes every query with the module imports. +const prelude = `exists L; L = require("sync_laws"); exists F; F = require("sync_fixtures"); ` + +type lawCase struct { + name string + lhs string + rhs string +} + +// Positive laws: MUST be equivalent. Names follow formal/occult/LAWS.md. +var lawCases = []lawCase{ + // L1 — REPLACES absorbs (and the axiom-firing sanity checks for the + // absorption clauses). + {"L1_fresh_absorbs", `L.app(L.fresh(F.e1), F.x0)`, `L.rows(F.e1)`}, + {"L1_fresh_absorbs_suffix1", `L.foldops(L.opscons(L.fresh(F.e1), L.opsnil), F.x0)`, `L.rows(F.e1)`}, + {"L1_fresh_absorbs_suffix2", `L.foldops(L.opscons(F.op0, L.opscons(L.fresh(F.e1), L.opsnil)), F.x0)`, `L.rows(F.e1)`}, + // L2 — overlay grounds and composes. + {"L2_overlay_grounds", `L.app(L.ovl(F.e1, F.e2), L.rows(F.e1))`, `L.rows(F.e2)`}, + {"L2_overlay_transitive", `L.app(L.ovl(F.e2, F.e3), L.app(L.ovl(F.e1, F.e2), L.rows(F.e1)))`, `L.app(L.ovl(F.e1, F.e3), L.rows(F.e1))`}, + // L3 — self-grounding overlay absorbs. + {"L3_selfgrounding_absorbs", `L.app(L.ovlsg(F.e1), F.x0)`, `L.rows(F.e1)`}, + // L4 — copy-skip is identity on the current value. + {"L4_skip_identity", `L.app(L.skip(F.e1), L.rows(F.e1))`, `L.rows(F.e1)`}, + // L5 — replay-copy idempotence (apply-level). + {"L5_repl_idempotent", `L.app(L.repl(F.e1), L.app(L.repl(F.e1), F.x0))`, `L.app(L.repl(F.e1), F.x0)`}, + {"L5_fresh_idempotent", `L.app(L.fresh(F.e1), L.app(L.fresh(F.e1), F.x0))`, `L.app(L.fresh(F.e1), F.x0)`}, + // L6 — distinct-id commutation (observational, both observers) and + // the same-id page-order clause. + {"L6_commute_obs1", `L.get(L.id1, L.del(L.id1, L.put(L.id2, F.v0, F.m0)))`, `L.get(L.id1, L.put(L.id2, F.v0, L.del(L.id1, F.m0)))`}, + {"L6_commute_obs2", `L.get(L.id2, L.del(L.id1, L.put(L.id2, F.v0, F.m0)))`, `L.get(L.id2, L.put(L.id2, F.v0, L.del(L.id1, F.m0)))`}, + {"L6_tombstone_after_upsert", `L.get(L.id1, L.del(L.id1, L.put(L.id1, F.v0, F.m0)))`, `L.absent`}, + // L8 — dead-membership is absorbing under merge. + {"L8_dead_absorbs_left", `L.hasdead(L.merge(L.dead(F.g0), F.y0))`, `L.tt`}, + {"L8_dead_absorbs_right", `L.hasdead(L.merge(F.y0, L.dead(F.g0)))`, `L.tt`}, + {"L8_dead_absorbs_deep", `L.hasdead(L.merge(L.merge(F.x0, L.dead(F.g0)), F.y0))`, `L.tt`}, +} + +// Negative controls: MUST NOT be equivalent. A checker that cannot +// refuse these proves nothing about the positives. +var controlCases = []lawCase{ + {"N1_overlay_not_absorbing", `L.app(L.ovl(F.e1, F.e2), F.x0)`, `L.rows(F.e2)`}, + {"N2_skip_not_absorbing", `L.app(L.skip(F.e1), F.x0)`, `L.rows(F.e1)`}, + {"N3_live_not_dead", `L.hasdead(L.live(F.g0))`, `L.tt`}, + {"N4_distinct_epochs", `L.rows(F.e1)`, `L.rows(F.e2)`}, + {"N5_put_not_invisible", `L.get(L.id1, L.put(L.id1, F.v0, F.m0))`, `L.get(L.id1, F.m0)`}, +} + +func TestLaws(t *testing.T) { + for _, c := range lawCases { + t.Run(c.name, func(t *testing.T) { + if !areEquivalent(t, prelude+c.lhs, prelude+c.rhs) { + t.Errorf("law %s: expected %q equivalent to %q", c.name, c.lhs, c.rhs) + } + }) + } +} + +func TestNegativeControls(t *testing.T) { + for _, c := range controlCases { + t.Run(c.name, func(t *testing.T) { + if areEquivalent(t, prelude+c.lhs, prelude+c.rhs) { + t.Errorf("control %s: %q must NOT be equivalent to %q — the checker cannot refuse a wrong law", c.name, c.lhs, c.rhs) + } + }) + } +} diff --git a/formal/occult/host/phantom_test.go b/formal/occult/host/phantom_test.go new file mode 100644 index 000000000..2ca8984e3 --- /dev/null +++ b/formal/occult/host/phantom_test.go @@ -0,0 +1,72 @@ +// The phantom union derived deductively (../src/sync_phantom.occult): +// the engine PROVES the known-broken composition manufactures a +// phantom row (a row deleted upstream between e0 and e1 survives in +// the sealed artifact), proves every ingredient response is +// individually truthful, and proves the premise-validated composition +// (the demand-graph runtime's grounding rule) yields exactly the +// upstream truth from the same ingredients. The negative controls are +// the finding itself: the broken artifact is provably NOT the epoch it +// claims to be. +package host_test + +import "testing" + +var phantomModules = []localModule{ + {"sync_phantom", "sync_phantom.occult"}, +} + +const phantomPrelude = `exists S; S = require("sync_phantom"); ` + +func phantomEquivalent(t *testing.T, lhs, rhs string) bool { + t.Helper() + return equivalent(t, nil, phantomModules, phantomPrelude+lhs, phantomPrelude+rhs) +} + +var phantomCases = []lawCase{ + // THE BUG, derived: the broken composition retains the deleted row. + {"phantom_row_survives", `S.get(S.id1, S.result_broken)`, `S.found(S.vx)`}, + // Ground truth at the claimed epoch: the row is absent. + {"truth_row_absent", `S.get(S.id1, S.rows_e2)`, `S.absent`}, + // Every ingredient is individually truthful — diff12 on ITS base + // (e1) produces exactly rows_e2's observations. + {"diff12_truthful_id1", `S.get(S.id1, S.diff12(S.rows_e1))`, `S.get(S.id1, S.rows_e2)`}, + {"diff12_truthful_id2", `S.get(S.id2, S.diff12(S.rows_e1))`, `S.get(S.id2, S.rows_e2)`}, + // The insidious part: on the OTHER observer the broken artifact + // looks perfect — only the phantom key betrays it. + {"broken_id2_looks_fine", `S.get(S.id2, S.result_broken)`, `S.found(S.v2)`}, + // The good composition (diff grounded at the replay's attested + // base) matches upstream truth on BOTH observers from the same + // stale-cache ingredients. + {"good_matches_truth_id1", `S.get(S.id1, S.result_good)`, `S.get(S.id1, S.rows_e2)`}, + {"good_matches_truth_id2", `S.get(S.id2, S.result_good)`, `S.get(S.id2, S.rows_e2)`}, +} + +var phantomControls = []lawCase{ + // The finding: the broken artifact provably differs from the epoch + // it claims to be (observed at the phantom key). + {"broken_is_not_e2", `S.get(S.id1, S.result_broken)`, `S.get(S.id1, S.rows_e2)`}, + // And the phantom is not somehow absent. + {"phantom_not_absent", `S.get(S.id1, S.result_broken)`, `S.absent`}, + // Broken and good compositions are observably different artifacts. + {"broken_differs_from_good", `S.get(S.id1, S.result_broken)`, `S.get(S.id1, S.result_good)`}, +} + +func TestPhantomUnionDerived(t *testing.T) { + for _, c := range phantomCases { + t.Run(c.name, func(t *testing.T) { + if !phantomEquivalent(t, c.lhs, c.rhs) { + t.Errorf("phantom case %s: expected %q equivalent to %q", c.name, c.lhs, c.rhs) + } + }) + } +} + +func TestPhantomUnionControls(t *testing.T) { + for _, c := range phantomControls { + t.Run(c.name, func(t *testing.T) { + if phantomEquivalent(t, c.lhs, c.rhs) { + t.Errorf("phantom control %s: %q must NOT be equivalent to %q", c.name, c.lhs, c.rhs) + } + }) + } +} diff --git a/formal/occult/host/pipeline_test.go b/formal/occult/host/pipeline_test.go new file mode 100644 index 000000000..6dc13ae82 --- /dev/null +++ b/formal/occult/host/pipeline_test.go @@ -0,0 +1,143 @@ +// Shared equivalence pipeline: the engine's axiom-test sequence (load +// modules universal-scoped, load lhs/rhs query modules global-scoped, +// load axioms/equalities, bridge require and property access, saturate, +// ask the solver). Each call builds a fresh interpreter so no e-graph +// state leaks between checks. +package host_test + +import ( + "context" + "testing" + + occult "github.com/conductorone/occult" + "github.com/conductorone/occult/ir" + "github.com/conductorone/occult/modelset" + "github.com/conductorone/occult/parse" + "github.com/conductorone/occult/solve" +) + +// localModule is a .occult source from ../src loaded into UniversalScope +// under the given require() name. +type localModule struct { + name string + file string +} + +func equivalent(t *testing.T, requires []string, locals []localModule, lhs, rhs string) bool { + t.Helper() + reg, err := modelset.Registry() + if err != nil { + t.Fatalf("modelset.Registry: %v", err) + } + interp := occult.NewInterpreter(reg, solve.NewEGraphSolver()) + pm := parse.DefaultModel() + ctx := context.Background() + + for _, name := range requires { + if err := interp.RequireModule(name); err != nil { + t.Fatalf("RequireModule %s: %v", name, err) + } + } + for _, lm := range locals { + if _, err := interp.LoadStdlib(lm.name, readSrc(t, lm.file), pm); err != nil { + t.Fatalf("LoadStdlib %s: %v", lm.name, err) + } + } + + lhsRoot, err := interp.LoadModule("lhs", lhs, parse.DefaultModel()) + if err != nil { + t.Fatalf("LoadModule lhs %q: %v", lhs, err) + } + rhsRoot, err := interp.LoadModule("rhs", rhs, parse.DefaultModel()) + if err != nil { + t.Fatalf("LoadModule rhs %q: %v", rhs, err) + } + + for _, root := range []ir.NodeID{lhsRoot, rhsRoot} { + if err := interp.AssignScopes(root, interp.GlobalScope); err != nil { + t.Fatalf("AssignScopes: %v", err) + } + if err := interp.LoadQuantifications(root); err != nil { + t.Fatalf("LoadQuantifications: %v", err) + } + if err := interp.ApplyQuantifications(root); err != nil { + t.Fatalf("ApplyQuantifications: %v", err) + } + } + + moduleRoots := interp.Graph.SourcesOfType(interp.Modules, ir.EdgeAst) + for _, root := range moduleRoots { + if err := interp.LoadAxioms(ctx, root); err != nil { + t.Fatalf("LoadAxioms root %d: %v", root, err) + } + } + allRoots := append(append([]ir.NodeID{}, moduleRoots...), lhsRoot, rhsRoot) + for _, root := range allRoots { + if err := interp.LoadEqualities(ctx, root); err != nil { + t.Fatalf("LoadEqualities root %d: %v", root, err) + } + } + for _, root := range allRoots { + if err := interp.BridgeRequireCalls(ctx, root); err != nil { + t.Fatalf("BridgeRequireCalls root %d: %v", root, err) + } + } + for _, root := range allRoots { + if err := interp.BridgeBareRequiredModuleExports(ctx, root); err != nil { + t.Fatalf("BridgeBareRequiredModuleExports root %d: %v", root, err) + } + } + + lhsQuery := findQueryNode(interp, lhsRoot) + rhsQuery := findQueryNode(interp, rhsRoot) + // A swallowed error here is worse than a flake: the positives would + // fail loudly, but the negative controls assert REFUSAL, and an + // e-graph with no loaded expressions refuses every equivalence — + // they would all pass vacuously. (This module is outside root + // `make lint`, so errcheck does not cover it.) + if err := interp.Solver.LoadExpressions(ctx, interp.Graph, []solve.Expr{ + {RootNodeID: lhsQuery}, + {RootNodeID: rhsQuery}, + }); err != nil { + t.Fatalf("LoadExpressions: %v", err) + } + for _, root := range allRoots { + if err := interp.BridgeModulePropertyAccess(ctx, root); err != nil { + t.Fatalf("BridgeModulePropertyAccess root %d: %v", root, err) + } + } + for _, root := range allRoots { + if err := interp.BridgeMethodPropertyAccess(ctx, root); err != nil { + t.Fatalf("BridgeMethodPropertyAccess root %d: %v", root, err) + } + } + + if err := interp.Saturate(ctx); err != nil { + t.Fatalf("Saturate: %v", err) + } + eq, err := interp.Solver.AreEquivalent(lhsQuery, rhsQuery) + if err != nil { + t.Fatalf("AreEquivalent: %v", err) + } + if eq { + return true + } + // The engine's axiom-test fallback: typed universals (d : Protocol) + // gate rewrites, and the egraph backend discharges them as a + // conditional equivalence with the membership premises as the + // condition. Conditional counts as equivalent — for negative + // controls too, which makes the controls STRICTER. + if conditional, ok := interp.Solver.(solve.ConditionalEquivalenceAware); ok { + condEq, condition, err := conditional.AreEquivalentWithConstraint(lhsQuery, rhsQuery) + if err != nil { + t.Fatalf("AreEquivalentWithConstraint: %v", err) + } + if condEq { + if condition != nil { + t.Logf("conditional equivalence under: %s", condition) + } + return true + } + } + return false +} diff --git a/formal/occult/host/protocol_test.go b/formal/occult/host/protocol_test.go new file mode 100644 index 000000000..5b0120d2f --- /dev/null +++ b/formal/occult/host/protocol_test.go @@ -0,0 +1,89 @@ +// Deliverable 8: projection derivations for the syncer↔connector +// source-cache protocol (../src/sync_protocol.occult) — the engine's +// Raft leader/follower derivation-test pattern applied to our contract. +// SYNCER = P_leader, CONNECTOR = P_follower. Positives prove each +// role's projected IO chain; negatives pin the polarity (a projection +// that confused the roles or erased a bounce must be refused). +// +// Stuck-freedom and bounce-cap VIOLATION checking are open engine work +// (session-types TODO); the cap is structural in the protocol module +// (no five-bounce session term exists) and its Go-side mirror is +// sourcecache.MaxLookupBouncesPerRequest. +package host_test + +import "testing" + +var protocolRequires = []string{"protocol", "projection"} + +var protocolModules = []localModule{ + {"sync_protocol", "sync_protocol.occult"}, +} + +const protoPrelude = `exists P; P = require("projection"); exists S; S = require("sync_protocol"); ` + +func protoEquivalent(t *testing.T, lhs, rhs string) bool { + t.Helper() + return equivalent(t, protocolRequires, protocolModules, protoPrelude+lhs, protoPrelude+rhs) +} + +var protocolCases = []lawCase{ + // Structural expansion of the one-bounce session. + {"expand_ask1", + `S.session_ask1`, + `S.ask_first pipe S.send(S.req_answers) pipe S.recv(S.page_record)`}, + // Direct (no-ask) page service, both roles. + {"syncer_direct", + `P.P_leader(S.session_direct)`, + `P.io_send(S.req_offer) pipe P.io_recv(S.page_record)`}, + {"connector_direct", + `P.P_follower(S.session_direct)`, + `P.io_recv(S.req_offer) pipe P.io_send(S.page_record)`}, + {"connector_direct_replay", + `P.P_follower(S.session_direct_replay)`, + `P.io_recv(S.req_offer) pipe P.io_send(S.page_replay)`}, + // One ask bounce, both roles. + {"syncer_ask1", + `P.P_leader(S.session_ask1)`, + `P.io_send(S.req_offer) pipe P.io_recv(S.lookup_ask) pipe P.local(S.resolve_answers) pipe P.io_send(S.req_answers) pipe P.io_recv(S.page_record)`}, + {"connector_ask1", + `P.P_follower(S.session_ask1)`, + `P.io_recv(S.req_offer) pipe P.io_send(S.lookup_ask) pipe P.local(S.resolve_answers) pipe P.io_recv(S.req_answers) pipe P.io_send(S.page_record)`}, + // The maximal legal session (bounce cap 4), syncer side. pipe has no + // associativity axiom (composition is a raw binary tree), so the + // expected term parenthesizes each bounce to mirror the session's + // grouping exactly. + {"syncer_ask4", + `P.P_leader(S.session_ask4)`, + `(P.io_send(S.req_offer) pipe P.io_recv(S.lookup_ask) pipe P.local(S.resolve_answers)) pipe (P.io_send(S.req_answers) pipe P.io_recv(S.lookup_ask) pipe P.local(S.resolve_answers)) pipe (P.io_send(S.req_answers) pipe P.io_recv(S.lookup_ask) pipe P.local(S.resolve_answers)) pipe (P.io_send(S.req_answers) pipe P.io_recv(S.lookup_ask) pipe P.local(S.resolve_answers)) pipe P.io_send(S.req_answers) pipe P.io_recv(S.page_record)`}, +} + +var protocolControls = []lawCase{ + // Role polarity: the two projections of one session must differ. + {"roles_differ", `P.P_leader(S.session_direct)`, `P.P_follower(S.session_direct)`}, + // A bounce is not erasable. + {"bounce_not_erasable", `S.session_ask1`, `S.session_direct`}, + // Cap shape: four bounces are not one bounce. + {"cap_shape", `P.P_leader(S.session_ask4)`, `P.P_leader(S.session_ask1)`}, + // Annotation exchange: record and replay legs differ. + {"record_not_replay", `S.session_direct`, `S.session_direct_replay`}, +} + +func TestProtocolProjections(t *testing.T) { + for _, c := range protocolCases { + t.Run(c.name, func(t *testing.T) { + if !protoEquivalent(t, c.lhs, c.rhs) { + t.Errorf("projection %s: expected %q equivalent to %q", c.name, c.lhs, c.rhs) + } + }) + } +} + +func TestProtocolControls(t *testing.T) { + for _, c := range protocolControls { + t.Run(c.name, func(t *testing.T) { + if protoEquivalent(t, c.lhs, c.rhs) { + t.Errorf("control %s: %q must NOT be equivalent to %q", c.name, c.lhs, c.rhs) + } + }) + } +} diff --git a/formal/occult/host/raft_probe_test.go b/formal/occult/host/raft_probe_test.go new file mode 100644 index 000000000..7c111e836 --- /dev/null +++ b/formal/occult/host/raft_probe_test.go @@ -0,0 +1,16 @@ +// Harness probe: the engine's own raft projection derivation run +// through OUR pipeline. If this fails while the engine's in-repo axiom +// test passes, the pipeline here is missing a step; if it passes, a +// failing sync_protocol projection is a module problem, not a harness +// problem. Kept permanently as the harness's positive control. +package host_test + +import "testing" + +func TestRaftProbe(t *testing.T) { + lhs := `exists P; P = require("projection"); exists R; R = require("raft"); P.P_leader(R.election)` + rhs := `exists P; P = require("projection"); exists R; R = require("raft"); P.io_recv(R.request_vote) pipe P.local(R.tally_votes) pipe P.io_send(R.announce_leader)` + if !equivalent(t, []string{"raft"}, nil, lhs, rhs) { + t.Fatalf("raft leader projection did not derive through this pipeline — harness gap") + } +} diff --git a/formal/occult/host/real_trace_oracle_test.go b/formal/occult/host/real_trace_oracle_test.go new file mode 100644 index 000000000..3134e6138 --- /dev/null +++ b/formal/occult/host/real_trace_oracle_test.go @@ -0,0 +1,440 @@ +// Real-sync-execution leg of the trace bridge (../TRACE_BRIDGE.md, +// mapping 2): JSONL trace fixtures recorded from REAL syncer executions +// by pkg/sync's chaos harness (chaos_trace_oracle_test.go, the +// testSyncTraceAudit recorder) are rendered onto the canonical event +// vocabulary and checked against all seven deliverable-7 policies. This +// closes the loop the brief asked for: the same oracle that gates the P +// models' traces and the refimpl's traces now gates the shipped +// syncer's commit order. +// +// KNOWN-DEFECT PIN: the session-zombie fixture (recorded by +// pkg/sync's TestChaosSourceCacheSessionPersistsAcrossResume) is +// EXPECTED RED on session_ckpt_consistency — sessions commit durably +// at op time, outside the checkpoint mechanism, so a crashed attempt's +// beyond-checkpoint write survives the cursor rollback and the re-run +// reads it (CO-6b-009). The red verdict on a real execution IS the +// mechanical catch of the shipped defect. When checkpoint-consistent +// sessions land (the registered future work), the recorded trace +// becomes a read-miss and this expectation flips to "ok". +// +// KNOWN-DEGRADE PIN: the SQLite external-principal fixture (recorded +// by pkg/sync's +// TestChaosConnectorSQLiteExternalPrincipalResumeDegradesWithoutFailure) +// is EXPECTED RED on external_principal_grounding — a non-deleting +// engine's resume warns and copies the current answer WITHOUT +// reconciling the dead attempt's stale principals, so the trace's +// resumed segment carries copies with no completed ep_recon. This is +// the ACCEPTED degradation (one-artifact staleness, self-healing at +// the next cold sync, no replay channel to launder it further); the +// red verdict on a real execution documents the acceptance +// mechanically, exactly like the session pin. +// +// Rendering conventions (the recorder is purely observational; the +// conventions live here): +// - Scopes: distinct (row_kind, scope_key) pairs map onto s1/s2 in +// first-seen order — the policies' two-scope envelope. Fixtures +// with more than two scopes are rejected. +// - External principals: distinct ep_live/ep_copy scope keys map +// onto p1/p2 in first-seen order; further principals are PROJECTED +// OUT (their events dropped). Sound for the kept principals: the +// policy tracks each principal independently, and the recon gate +// is principal-agnostic (every attempt in the committed fixtures +// copies the first-seen principal, so a missing ep_recon still +// fires on a kept copy). ORDER DEPENDENCY, stated: the +// stale-survivor direction is only observable if the SHRINKING +// principal lands inside the p1/p2 envelope. The committed +// capable-engine fixture satisfies this (external-user-1 is +// second-seen and shrinks away; external-user-2 is projected +// out and stays live), and the dependency fails LOUDLY, not +// silently: if a regenerated fixture reordered first-seen so the +// shrinking principal were projected out, +// TestRealTraceBridgeCatchesStaleExternalSurvivor's planted +// verdict would come back "ok" and the test would fail. +// - Structural clear: a trace that starts at sync birth +// (header resumed=false) writes into partitions StartNewSync +// created empty, so an upsert with no earlier explicit clear for +// its scope gets an ev_clear inserted before it — once per scope +// for the WHOLE sync trace, attempts included (the partition is +// born empty once). A trace beginning mid-sync (resumed=true) +// gets no insertion — exactly the case clear-before-upsert exists +// to catch. All committed fixtures start at sync birth. +// - Resume markers: multi-attempt fixtures carry {"kind":"resume"} +// lines between attempt segments, rendered as ev_resume. +// - Checkpoint coalescing: a run of consecutive checkpoints renders +// as ONE ev_checkpoint. Verdict-preserving by inspection of all +// seven policies: five pass checkpoints through untouched +// (policies 1-3 and 5 by explicit pass-through rules; policy 7's +// epg_go likewise), checkpoint-before-progress sets an idempotent +// flag, and session_ckpt_consistency's scc_go commits the +// uncommitted-write flag — after the first checkpoint the flag is +// ff, so an immediately following checkpoint is a no-op. Needed +// because the engine's term evaluation cost grows steeply with +// event count (a 25-event trace exceeds 18 minutes per cell; 14 +// events evaluate in tens of seconds). +package host_test + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" +) + +type realTraceHeader struct { + Name string `json:"name"` + Resumed bool `json:"resumed"` +} + +type realTraceEvent struct { + Kind string `json:"kind"` + RowKind string `json:"row_kind"` + ScopeKey string `json:"scope_key"` +} + +// loadRealTrace parses one JSONL fixture: a header line then one event +// per line. +func loadRealTrace(t *testing.T, path string) (realTraceHeader, []realTraceEvent) { + t.Helper() + f, err := os.Open(path) + if err != nil { + t.Fatalf("open %s: %v", path, err) + } + defer f.Close() + scanner := bufio.NewScanner(f) + if !scanner.Scan() { + t.Fatalf("%s: empty fixture", path) + } + var header realTraceHeader + if err := json.Unmarshal(scanner.Bytes(), &header); err != nil { + t.Fatalf("%s: header: %v", path, err) + } + var events []realTraceEvent + for scanner.Scan() { + if len(scanner.Bytes()) == 0 { + continue + } + var ev realTraceEvent + if err := json.Unmarshal(scanner.Bytes(), &ev); err != nil { + t.Fatalf("%s: event: %v", path, err) + } + events = append(events, ev) + } + if err := scanner.Err(); err != nil { + t.Fatalf("%s: scan: %v", path, err) + } + if len(events) == 0 { + t.Fatalf("%s: no events", path) + } + return header, events +} + +// renderRealTrace converts a recorded event list into a canonical trace +// term over the policy module's constructors. +func renderRealTrace(t *testing.T, header realTraceHeader, events []realTraceEvent) string { + t.Helper() + scopes := map[string]string{} + scopeName := func(ev realTraceEvent) string { + key := ev.RowKind + "\x00" + ev.ScopeKey + if name, ok := scopes[key]; ok { + return name + } + name := fmt.Sprintf("s%d", len(scopes)+1) + if len(scopes) >= 2 { + t.Fatalf("fixture %s has more than two scopes (policies' two-scope envelope)", header.Name) + } + scopes[key] = name + return name + } + cleared := map[string]bool{} + sessionKey := "" + principals := map[string]string{} + principalName := func(ev realTraceEvent) string { + if name, ok := principals[ev.ScopeKey]; ok { + return name + } + if len(principals) >= 2 { + // Projection: principals beyond the envelope drop out + // (see the file comment). + return "" + } + name := fmt.Sprintf("p%d", len(principals)+1) + principals[ev.ScopeKey] = name + return name + } + var canonical []string + for _, ev := range events { + switch ev.Kind { + case "checkpoint": + if len(canonical) > 0 && canonical[len(canonical)-1] == "ev_checkpoint" { + continue + } + canonical = append(canonical, "ev_checkpoint") + case "seal": + canonical = append(canonical, "ev_seal") + case "resume": + canonical = append(canonical, "ev_resume") + case "swrite", "sread_hit", "sread_miss": + // Session events map onto the policy module's one-key + // envelope (k1), separate from the artifact scopes. + if sessionKey == "" { + sessionKey = ev.ScopeKey + } + if ev.ScopeKey != sessionKey { + t.Fatalf("fixture %s has more than one session key (policies' one-key envelope)", header.Name) + } + canonical = append(canonical, "ev_"+ev.Kind+"(M.k1)") + case "ep_list", "ep_recon": + canonical = append(canonical, ev.Kind) + case "ep_live", "ep_copy": + p := principalName(ev) + if p == "" { + continue + } + canonical = append(canonical, fmt.Sprintf("%s(M.%s)", ev.Kind, p)) + case "consult", "clear", "replay", "upsert", "delete", "publish": + s := scopeName(ev) + if ev.Kind == "clear" { + cleared[s] = true + } + if (ev.Kind == "upsert" || ev.Kind == "delete") && !cleared[s] && !header.Resumed { + // Structural clear: the partition was born empty this + // sync (see the file comment). Deletes are writes and + // need the same grounding as upserts. + canonical = append(canonical, "ev_clear(M."+s+")") + cleared[s] = true + } + canonical = append(canonical, fmt.Sprintf("ev_%s(M.%s)", ev.Kind, s)) + default: + t.Fatalf("fixture %s: unknown event kind %q", header.Name, ev.Kind) + } + } + term := "M.tnil" + for i := len(canonical) - 1; i >= 0; i-- { + term = fmt.Sprintf("M.tcons(M.%s, %s)", canonical[i], term) + } + return term +} + +// realTraceExpected overrides the default "ok" expectation for +// (fixture, policy) cells: the two standing pins (see the file +// comment) — the shipped session semantics' zombie read, and the +// non-deleting engine's unreconciled external-principal copy. Each is +// RED on a real execution's trace by design. +var realTraceExpected = map[string]map[string]string{ + "warm_replay_sync_session_zombie": { + "session_ckpt_consistency": "violation: session-zombie-read", + }, + "external_resume_sqlite_degrade": { + "external_principal_grounding": "violation: ext-recon-before-copy", + }, +} + +// TestRealSyncTracesSatisfyPolicies checks every committed real-trace +// fixture against all seven policies. +func TestRealSyncTracesSatisfyPolicies(t *testing.T) { + paths, err := filepath.Glob(filepath.Join("testdata", "realtraces", "*.jsonl")) + if err != nil { + t.Fatalf("glob: %v", err) + } + if len(paths) == 0 { + t.Fatal("no real-trace fixtures committed under testdata/realtraces") + } + for _, path := range paths { + header, events := loadRealTrace(t, path) + term := renderRealTrace(t, header, events) + for _, policy := range policies { + t.Run(header.Name+"/"+policy, func(t *testing.T) { + want := "ok" + if overrides, ok := realTraceExpected[header.Name]; ok { + if v, ok := overrides[policy]; ok { + want = v + } + } + verdict := policyVerdictTerm(t, policy, term) + if verdict != want { + t.Errorf("real trace %s under %s: want %q, got %q\nterm: %s", header.Name, policy, want, verdict, term) + } + }) + } + } +} + +// TestRealTraceBridgeCatchesPlantedViolation validates the bridge +// itself (instrument validation: the oracle must fail on a planted +// violation, or a rendering bug that greens everything would pass +// silently). It mutates the warm fixture's REAL events two ways: +// dropping the consult must red consult-before-replay, and replaying +// the same events as a RESUMED attempt with an upsert in place of the +// replay unit must red clear-before-upsert (no structural clear). +func TestRealTraceBridgeCatchesPlantedViolation(t *testing.T) { + path := filepath.Join("testdata", "realtraces", "warm_replay_sync.jsonl") + header, events := loadRealTrace(t, path) + + var noConsult []realTraceEvent + for _, ev := range events { + if ev.Kind == "consult" { + continue + } + noConsult = append(noConsult, ev) + } + term := renderRealTrace(t, header, noConsult) + if verdict := policyVerdictTerm(t, "consult_before_replay", term); verdict != "violation: consult-before-replay" { + t.Errorf("planted consult drop not caught, verdict %q", verdict) + } + + resumed := realTraceHeader{Name: "planted-resume", Resumed: true} + var upsertOnly []realTraceEvent + for _, ev := range events { + switch ev.Kind { + case "clear", "replay", "consult": + if ev.Kind == "replay" { + upsertOnly = append(upsertOnly, realTraceEvent{Kind: "upsert", RowKind: ev.RowKind, ScopeKey: ev.ScopeKey}) + } + default: + upsertOnly = append(upsertOnly, ev) + } + } + term = renderRealTrace(t, resumed, upsertOnly) + if verdict := policyVerdictTerm(t, "clear_before_upsert", term); verdict != "violation: clear-before-upsert" { + t.Errorf("planted un-regrounded resume not caught, verdict %q", verdict) + } +} + +// TestRealTraceBridgeCatchesUngroundedDelete validates the delete leg +// of the bridge: the tombstone fixture's REAL delete, replayed as a +// mid-sync trace with its grounding (clear+replay) and upsert stripped, +// must red clear-before-upsert — a tombstone against a base this trace +// never copied is the un-regrounded-resume class, delete flavor. +func TestRealTraceBridgeCatchesUngroundedDelete(t *testing.T) { + path := filepath.Join("testdata", "realtraces", "warm_replay_sync_tombstone.jsonl") + header, events := loadRealTrace(t, path) + + term := renderRealTrace(t, header, events) + if verdict := policyVerdictTerm(t, "clear_before_upsert", term); verdict != "ok" { + t.Errorf("honest tombstone trace must satisfy clear-before-upsert, got %q", verdict) + } + + hasDelete := false + var ungrounded []realTraceEvent + for _, ev := range events { + switch ev.Kind { + case "clear", "replay", "upsert": + continue + case "delete": + hasDelete = true + } + ungrounded = append(ungrounded, ev) + } + if !hasDelete { + t.Fatal("tombstone fixture carries no delete event; the delete leg is not being exercised") + } + term = renderRealTrace(t, realTraceHeader{Name: "planted-ungrounded-delete", Resumed: true}, ungrounded) + if verdict := policyVerdictTerm(t, "clear_before_upsert", term); verdict != "violation: clear-before-upsert" { + t.Errorf("planted ungrounded delete not caught, verdict %q", verdict) + } +} + +// TestRealTraceBridgeResumeMarkerLoadBearing validates the multi-attempt +// leg of the bridge: the interrupted fixture's two replays are legal +// ONLY because a resume marker separates them (once-per-scope resets at +// the boundary). Deleting the marker must turn the same events into a +// within-attempt duplicate copy and red once-per-scope — proving the +// marker, and therefore the attempt segmentation, is load-bearing. +func TestRealTraceBridgeResumeMarkerLoadBearing(t *testing.T) { + path := filepath.Join("testdata", "realtraces", "warm_replay_sync_interrupted.jsonl") + header, events := loadRealTrace(t, path) + + hasResume := false + var noMarker []realTraceEvent + for _, ev := range events { + if ev.Kind == "resume" { + hasResume = true + continue + } + noMarker = append(noMarker, ev) + } + if !hasResume { + t.Fatal("interrupted fixture carries no resume marker; the multi-attempt leg is not being exercised") + } + + term := renderRealTrace(t, header, events) + if verdict := policyVerdictTerm(t, "once_per_scope", term); verdict != "ok" { + t.Errorf("marked multi-attempt trace must satisfy once-per-scope, got %q", verdict) + } + term = renderRealTrace(t, header, noMarker) + if verdict := policyVerdictTerm(t, "once_per_scope", term); verdict != "violation: once-per-scope" { + t.Errorf("marker-stripped trace must red once-per-scope, got %q", verdict) + } +} + +// TestRealTraceBridgeCatchesStaleExternalSurvivor validates the +// external-principal leg of the bridge (instrument validation for the +// stale-survivor direction; the recon-before-copy direction is already +// witnessed by the SQLite degrade pin). The capable-engine fixture's +// REAL events, with the FINAL attempt's reconciliation and copies +// stripped, describe a history where a dead attempt's principal +// reaches the seal undeleted — the oracle must red ext-stale-survivor. +func TestRealTraceBridgeCatchesStaleExternalSurvivor(t *testing.T) { + path := filepath.Join("testdata", "realtraces", "external_resume_current_answer.jsonl") + header, events := loadRealTrace(t, path) + + term := renderRealTrace(t, header, events) + if verdict := policyVerdictTerm(t, "external_principal_grounding", term); verdict != "ok" { + t.Errorf("honest capable-engine trace must satisfy external_principal_grounding, got %q", verdict) + } + + lastResume := -1 + for i, ev := range events { + if ev.Kind == "resume" { + lastResume = i + } + } + if lastResume < 0 { + t.Fatal("capable-engine fixture carries no resume marker; the multi-attempt leg is not being exercised") + } + var mutated []realTraceEvent + for i, ev := range events { + if i > lastResume && (ev.Kind == "ep_recon" || ev.Kind == "ep_copy") { + continue + } + mutated = append(mutated, ev) + } + term = renderRealTrace(t, header, mutated) + if verdict := policyVerdictTerm(t, "external_principal_grounding", term); verdict != "violation: ext-stale-survivor" { + t.Errorf("planted stale external survivor not caught, verdict %q", verdict) + } +} + +// TestRealTraceBridgeStructuralClearInsertion validates the renderer's +// structural-clear branch — the ONE place the bridge synthesizes +// grounding the recorder never observed (a non-resumed trace's +// partitions are born empty at StartNewSync). Every committed fixture +// now emits an explicit clear before its first write (record-round +// grounding made replacement clears real), so without this test the +// branch is dead across the suite and a regression in it would +// silently green a genuinely un-grounded write. The synthetic event +// list has no explicit clear anywhere and spans a resume: the renderer +// must insert ev_clear exactly ONCE for the scope (per sync, not per +// attempt — the partition is born empty once) and clear_before_upsert +// must answer ok for that reason. +func TestRealTraceBridgeStructuralClearInsertion(t *testing.T) { + header := realTraceHeader{Name: "synthetic_structural_clear", Resumed: false} + events := []realTraceEvent{ + {Kind: "upsert", RowKind: "grant", ScopeKey: "k"}, + {Kind: "resume"}, + {Kind: "upsert", RowKind: "grant", ScopeKey: "k"}, + } + term := renderRealTrace(t, header, events) + if n := strings.Count(term, "ev_clear"); n != 1 { + t.Fatalf("structural clear must be synthesized exactly once per scope per sync, found %d in %s", n, term) + } + if !strings.HasPrefix(term, "M.tcons(M.ev_clear(M.s1)") { + t.Fatalf("structural clear must precede the first write, term %s", term) + } + if verdict := policyVerdictTerm(t, "clear_before_upsert", term); verdict != "ok" { + t.Errorf("structurally grounded sync-birth trace must satisfy clear_before_upsert, got %q", verdict) + } +} diff --git a/formal/occult/host/refimpl/refimpl.go b/formal/occult/host/refimpl/refimpl.go new file mode 100644 index 000000000..662f85552 --- /dev/null +++ b/formal/occult/host/refimpl/refimpl.go @@ -0,0 +1,225 @@ +// Package refimpl is an executable REFERENCE implementation of the +// demand-graph runtime's per-scope sync loop (formal/GRAPH_MODEL_SPEC.md) +// — the "known good" algorithm, which has a frozen P model but no +// production implementation yet. It exists to be tried out: it runs the +// phantom-union scenario end to end, emits canonical traces in the +// TRACE_BRIDGE.md vocabulary, and carries a LEGACY mode reproducing the +// known-broken algorithm's two failure habits: +// +// 1. overlay grounding at the last sync's epoch instead of the replay +// marker's attested base (the phantom-union composition), and +// 2. resume-without-regrounding: after a crash, a non-empty partition +// is treated as already-replayed and overlaid directly, instead of +// re-executing the node under a fresh generation. +// +// This is a modeling artifact, single-threaded and in-memory; it is +// deliberately NOT production code and lives outside baton-sdk's public +// module. The store commits writes durably as they happen (they survive +// a crash); the checkpoint is the scheduler watermark, exactly the +// walker/graph models' crash semantics. +// +// SCOPE: the runtime's premise-validated ADOPTION (marker consult on +// re-execution, GRAPH_MODEL_SPEC §4a) is deliberately NOT modeled — a +// resumed attempt re-executes the full round under the always-honest +// path. Adoption semantics are exercised by the P graph model's +// P-ADOPT cells, not here. +package refimpl + +import ( + "fmt" + "sort" +) + +// Mode selects the algorithm under test. +type Mode int + +const ( + // ModeDemandGraph is the known-good algorithm: premise-validated + // grounding, generation-bump re-execution on resume. + ModeDemandGraph Mode = iota + // ModeLegacy reproduces the known-broken algorithm. + ModeLegacy +) + +// Event is one canonical trace event (TRACE_BRIDGE.md vocabulary). +type Event struct { + Kind string // consult, clear, replay, upsert, delete, publish, checkpoint, seal + Scope string // "" for checkpoint/seal +} + +// Upstream is the truthful system of record: a row set per epoch and +// honest diffs between any two epochs. +type Upstream struct { + epochs []map[string]string +} + +func NewUpstream(epochs ...map[string]string) *Upstream { + return &Upstream{epochs: epochs} +} + +func (u *Upstream) Rows(e int) map[string]string { + out := map[string]string{} + for k, v := range u.epochs[e] { + out[k] = v + } + return out +} + +// Diff returns the truthful delta from one epoch to another: upserts +// for added/changed ids, deletes for removed ids. It never mentions an +// id that did not change — which is exactly what makes the misgrounded +// composition dangerous. +func (u *Upstream) Diff(from, to int) (upserts map[string]string, deletes []string) { + upserts = map[string]string{} + a, b := u.epochs[from], u.epochs[to] + for k, v := range b { + if av, ok := a[k]; !ok || av != v { + upserts[k] = v + } + } + for k := range a { + if _, ok := b[k]; !ok { + deletes = append(deletes, k) + } + } + sort.Strings(deletes) + return upserts, deletes +} + +// Cache is the source-cache offer for the scope: rows attested at Base. +// The consult verdict truthfully reports Base. +type Cache struct { + Base int + Rows map[string]string +} + +// Config describes one sync of one scope. +type Config struct { + Mode Mode + Upstream *Upstream + Head int // the epoch this sync must land on + Cache Cache // the connector's replay offer + LastSyncEpoch int // what the previous completed sync attested (the legacy grounding source) + // CrashAfterReplay kills attempt 1 after the replay unit commits + // and before any checkpoint, then resumes as attempt 2. + CrashAfterReplay bool +} + +// Result is the sealed artifact content plus the canonical trace of +// every attempt (crash-cut traces included). +type Result struct { + Sealed map[string]string + Attempts [][]Event +} + +const scope = "s1" + +// durable is the store state that survives a crash. The checkpoint +// watermark is deliberately NOT part of it: both crash scenarios here +// crash before the (single) checkpoint, so resume logic never has a +// watermark to consult — legacy resumes off partition contents, the +// demand-graph path re-consults unconditionally. The checkpoint exists +// in this implementation only as its trace event. +type durable struct { + partition map[string]string + markerBase int // the replay unit's attested base epoch +} + +// Run executes the sync to seal, crashing and resuming if configured. +func Run(cfg Config) Result { + st := &durable{partition: map[string]string{}} + var attempts [][]Event + for attempt := 1; ; attempt++ { + trace, sealed := runAttempt(cfg, st, attempt) + attempts = append(attempts, trace) + if sealed { + return Result{Sealed: st.partition, Attempts: attempts} + } + if attempt > 2 { + panic("refimpl: more than one resume in a two-attempt scenario") + } + } +} + +func runAttempt(cfg Config, st *durable, attempt int) (trace []Event, sealed bool) { + emit := func(kind, sc string) { trace = append(trace, Event{Kind: kind, Scope: sc}) } + + replayed := false + switch { + case cfg.Mode == ModeLegacy && len(st.partition) > 0 && attempt > 1: + // LEGACY resume habit: a non-empty partition is treated as + // already-replayed. No consult, no clear, no fresh generation — + // straight to the overlay on top of whatever the dead attempt + // left behind. + default: + // Demand-graph path (and legacy attempt 1): consult the source + // cache; the verdict truthfully attests the cache's base epoch. + emit("consult", scope) + // Replay unit — atomic clear + copy + marker (the eGReplayUnit + // shape: the marker commits with the copy or not at all). + st.partition = map[string]string{} + for k, v := range cfg.Cache.Rows { + st.partition[k] = v + } + st.markerBase = cfg.Cache.Base + emit("clear", scope) + emit("replay", scope) + replayed = true + } + + if cfg.CrashAfterReplay && attempt == 1 && replayed { + // Crash: store writes above are durable; nothing was + // checkpointed. The trace is cut here. + return trace, false + } + + // Overlay: bring the partition from the grounded base to Head. + ground := st.markerBase // premise-validated: the marker's attestation + if cfg.Mode == ModeLegacy { + ground = cfg.LastSyncEpoch // the broken habit: last sync's epoch + } + upserts, deletes := cfg.Upstream.Diff(ground, cfg.Head) + for _, k := range sortedKeys(upserts) { + st.partition[k] = upserts[k] + emit("upsert", scope) + } + for _, k := range deletes { + delete(st.partition, k) + emit("delete", scope) + } + + emit("publish", scope) + emit("checkpoint", "") + emit("seal", "") + return trace, true +} + +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// RenderOccult renders one attempt's trace as an Occult term over the +// sync_trace_policies constructors, with every constructor accessed +// through the module handle m (e.g. "M"). +func RenderOccult(m string, trace []Event) string { + term := m + ".tnil" + for i := len(trace) - 1; i >= 0; i-- { + ev := trace[i] + var atom string + switch ev.Kind { + case "checkpoint": + atom = fmt.Sprintf("%s.ev_checkpoint", m) + case "seal": + atom = fmt.Sprintf("%s.ev_seal", m) + default: + atom = fmt.Sprintf("%s.ev_%s(%s.%s)", m, ev.Kind, m, ev.Scope) + } + term = fmt.Sprintf("%s.tcons(%s, %s)", m, atom, term) + } + return term +} diff --git a/formal/occult/host/refimpl_oracle_test.go b/formal/occult/host/refimpl_oracle_test.go new file mode 100644 index 000000000..16c9fcaba --- /dev/null +++ b/formal/occult/host/refimpl_oracle_test.go @@ -0,0 +1,123 @@ +// The demand-graph reference implementation (refimpl/) tried out +// against two oracles on the phantom-union scenario: +// +// - CONTENT oracle (Go): the sealed artifact must equal upstream at +// the head epoch. +// - TRACE oracle (engine): every attempt's canonical trace is checked +// against all seven deliverable-7 policies. +// +// The matrix this asserts: +// +// demand-graph, no crash -> content true, all policies ok +// demand-graph, crash+resume -> content true, all policies ok (both attempts) +// legacy, no crash -> content FALSE (phantom row) but all +// policies ok — the ordering policies are provably blind to the +// composition bug; that class is owned by the algebra +// (phantom_test.go derives it deductively) +// legacy, crash+resume -> content FALSE and attempt 2 violates +// exactly clear-before-upsert (resume-without-regrounding is an +// ordering bug, and the oracle catches it) +package host_test + +import ( + "reflect" + "testing" + + "github.com/conductorone/baton-sdk/formal/occult/host/refimpl" +) + +// The sync_phantom.occult scenario: id1 deleted between e0 and e1, +// id2's value changes between e1 and e2, the source cache is attested +// at e0, the previous sync completed at e1, and this sync targets e2. +func phantomConfig(mode refimpl.Mode, crash bool) refimpl.Config { + up := refimpl.NewUpstream( + map[string]string{"id1": "vx", "id2": "v1"}, // e0 + map[string]string{"id2": "v1"}, // e1 + map[string]string{"id2": "v2"}, // e2 + ) + return refimpl.Config{ + Mode: mode, + Upstream: up, + Head: 2, + Cache: refimpl.Cache{Base: 0, Rows: up.Rows(0)}, + LastSyncEpoch: 1, + CrashAfterReplay: crash, + } +} + +var truthAtHead = map[string]string{"id2": "v2"} +var phantomArtifact = map[string]string{"id1": "vx", "id2": "v2"} + +// checkAttempts runs every attempt trace through all seven policies and +// asserts the expected verdict: expectViolation maps attempt index +// (0-based) to the one policy that must fire; every other cell must be +// "ok". +func checkAttempts(t *testing.T, attempts [][]refimpl.Event, expectViolation map[int]string) { + t.Helper() + for i, trace := range attempts { + term := refimpl.RenderOccult("M", trace) + for _, policy := range policies { + verdict := policyVerdictTerm(t, policy, term) + if expectViolation[i] == policy { + if verdict != "violation: "+policyLabel(policy) { + t.Errorf("attempt %d: expected %s to fire, got %q", i+1, policy, verdict) + } + } else if verdict != "ok" { + t.Errorf("attempt %d: expected %s ok, got %q", i+1, policy, verdict) + } + } + } +} + +func TestRefImplDemandGraph(t *testing.T) { + res := refimpl.Run(phantomConfig(refimpl.ModeDemandGraph, false)) + if !reflect.DeepEqual(res.Sealed, truthAtHead) { + t.Errorf("sealed content %v, want upstream truth %v", res.Sealed, truthAtHead) + } + if len(res.Attempts) != 1 { + t.Fatalf("expected 1 attempt, got %d", len(res.Attempts)) + } + checkAttempts(t, res.Attempts, nil) +} + +func TestRefImplDemandGraphCrashResume(t *testing.T) { + res := refimpl.Run(phantomConfig(refimpl.ModeDemandGraph, true)) + if !reflect.DeepEqual(res.Sealed, truthAtHead) { + t.Errorf("sealed content %v, want upstream truth %v", res.Sealed, truthAtHead) + } + if len(res.Attempts) != 2 { + t.Fatalf("expected 2 attempts, got %d", len(res.Attempts)) + } + // Attempt 1 is a crash-cut prefix; attempt 2 re-executes the node + // under a fresh generation (consult, clear, replay again). Both + // must satisfy every policy. + checkAttempts(t, res.Attempts, nil) +} + +func TestRefImplLegacyPhantom(t *testing.T) { + res := refimpl.Run(phantomConfig(refimpl.ModeLegacy, false)) + if !reflect.DeepEqual(res.Sealed, phantomArtifact) { + t.Errorf("sealed content %v, want the phantom artifact %v", res.Sealed, phantomArtifact) + } + if reflect.DeepEqual(res.Sealed, truthAtHead) { + t.Errorf("legacy mode unexpectedly produced the true artifact") + } + // The ordering policies are BLIND to this one: the event order is + // identical to the honest run. The finding lives in the content + // oracle here and in the algebra deductively (phantom_test.go). + checkAttempts(t, res.Attempts, nil) +} + +func TestRefImplLegacyCrashResume(t *testing.T) { + res := refimpl.Run(phantomConfig(refimpl.ModeLegacy, true)) + if !reflect.DeepEqual(res.Sealed, phantomArtifact) { + t.Errorf("sealed content %v, want the phantom artifact %v", res.Sealed, phantomArtifact) + } + if len(res.Attempts) != 2 { + t.Fatalf("expected 2 attempts, got %d", len(res.Attempts)) + } + // Attempt 2 resumed onto the dead attempt's partition without + // re-grounding: upserts with no clear this attempt. The trace + // oracle must catch exactly clear-before-upsert. + checkAttempts(t, res.Attempts, map[int]string{1: "clear_before_upsert"}) +} diff --git a/formal/occult/host/testdata/realtraces/cold_record_sync.jsonl b/formal/occult/host/testdata/realtraces/cold_record_sync.jsonl new file mode 100644 index 000000000..b6de7067a --- /dev/null +++ b/formal/occult/host/testdata/realtraces/cold_record_sync.jsonl @@ -0,0 +1,8 @@ +{"name":"cold_record_sync","resumed":false} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"clear","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"upsert","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"publish","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"checkpoint"} +{"kind":"seal"} diff --git a/formal/occult/host/testdata/realtraces/external_resume_current_answer.jsonl b/formal/occult/host/testdata/realtraces/external_resume_current_answer.jsonl new file mode 100644 index 000000000..069e01faa --- /dev/null +++ b/formal/occult/host/testdata/realtraces/external_resume_current_answer.jsonl @@ -0,0 +1,36 @@ +{"name":"external_resume_current_answer","resumed":false} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"ep_list"} +{"kind":"ep_live","scope_key":"external-group-1"} +{"kind":"ep_live","scope_key":"external-user-1"} +{"kind":"ep_live","scope_key":"external-user-2"} +{"kind":"ep_recon"} +{"kind":"ep_copy","scope_key":"external-group-1"} +{"kind":"ep_copy","scope_key":"external-user-1"} +{"kind":"ep_copy","scope_key":"external-user-2"} +{"kind":"resume"} +{"kind":"checkpoint"} +{"kind":"ep_list"} +{"kind":"ep_live","scope_key":"external-group-1"} +{"kind":"ep_live","scope_key":"external-user-2"} +{"kind":"resume"} +{"kind":"checkpoint"} +{"kind":"ep_list"} +{"kind":"ep_live","scope_key":"external-group-1"} +{"kind":"ep_live","scope_key":"external-user-2"} +{"kind":"resume"} +{"kind":"checkpoint"} +{"kind":"ep_list"} +{"kind":"ep_live","scope_key":"external-group-1"} +{"kind":"ep_live","scope_key":"external-user-2"} +{"kind":"resume"} +{"kind":"checkpoint"} +{"kind":"ep_list"} +{"kind":"ep_live","scope_key":"external-group-1"} +{"kind":"ep_live","scope_key":"external-user-2"} +{"kind":"ep_recon"} +{"kind":"ep_copy","scope_key":"external-group-1"} +{"kind":"ep_copy","scope_key":"external-user-2"} +{"kind":"checkpoint"} +{"kind":"seal"} diff --git a/formal/occult/host/testdata/realtraces/external_resume_sqlite_degrade.jsonl b/formal/occult/host/testdata/realtraces/external_resume_sqlite_degrade.jsonl new file mode 100644 index 000000000..989792a4e --- /dev/null +++ b/formal/occult/host/testdata/realtraces/external_resume_sqlite_degrade.jsonl @@ -0,0 +1,20 @@ +{"name":"external_resume_sqlite_degrade","resumed":false} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"ep_list"} +{"kind":"ep_live","scope_key":"external-group-1"} +{"kind":"ep_live","scope_key":"external-user-1"} +{"kind":"ep_live","scope_key":"external-user-2"} +{"kind":"ep_recon"} +{"kind":"ep_copy","scope_key":"external-group-1"} +{"kind":"ep_copy","scope_key":"external-user-1"} +{"kind":"ep_copy","scope_key":"external-user-2"} +{"kind":"resume"} +{"kind":"checkpoint"} +{"kind":"ep_list"} +{"kind":"ep_live","scope_key":"external-group-1"} +{"kind":"ep_live","scope_key":"external-user-2"} +{"kind":"ep_copy","scope_key":"external-group-1"} +{"kind":"ep_copy","scope_key":"external-user-2"} +{"kind":"checkpoint"} +{"kind":"seal"} diff --git a/formal/occult/host/testdata/realtraces/warm_replay_sync.jsonl b/formal/occult/host/testdata/realtraces/warm_replay_sync.jsonl new file mode 100644 index 000000000..55120b248 --- /dev/null +++ b/formal/occult/host/testdata/realtraces/warm_replay_sync.jsonl @@ -0,0 +1,9 @@ +{"name":"warm_replay_sync","resumed":false} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"consult","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"clear","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"replay","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"publish","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"checkpoint"} +{"kind":"seal"} diff --git a/formal/occult/host/testdata/realtraces/warm_replay_sync_interrupted.jsonl b/formal/occult/host/testdata/realtraces/warm_replay_sync_interrupted.jsonl new file mode 100644 index 000000000..8a895ad56 --- /dev/null +++ b/formal/occult/host/testdata/realtraces/warm_replay_sync_interrupted.jsonl @@ -0,0 +1,26 @@ +{"name":"warm_replay_sync_interrupted","resumed":false} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"consult","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"clear","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"replay","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"upsert","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"resume"} +{"kind":"checkpoint"} +{"kind":"consult","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"clear","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"replay","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"upsert","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"publish","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"checkpoint"} +{"kind":"seal"} diff --git a/formal/occult/host/testdata/realtraces/warm_replay_sync_record_flip.jsonl b/formal/occult/host/testdata/realtraces/warm_replay_sync_record_flip.jsonl new file mode 100644 index 000000000..6f59a368d --- /dev/null +++ b/formal/occult/host/testdata/realtraces/warm_replay_sync_record_flip.jsonl @@ -0,0 +1,25 @@ +{"name":"warm_replay_sync_record_flip","resumed":false} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"consult","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"clear","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"replay","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"upsert","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"resume"} +{"kind":"checkpoint"} +{"kind":"consult","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"clear","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"upsert","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"publish","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"checkpoint"} +{"kind":"seal"} diff --git a/formal/occult/host/testdata/realtraces/warm_replay_sync_session_zombie.jsonl b/formal/occult/host/testdata/realtraces/warm_replay_sync_session_zombie.jsonl new file mode 100644 index 000000000..d5e212c9f --- /dev/null +++ b/formal/occult/host/testdata/realtraces/warm_replay_sync_session_zombie.jsonl @@ -0,0 +1,11 @@ +{"name":"warm_replay_sync_session_zombie","resumed":false} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"consult","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"clear","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"replay","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"upsert","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"swrite","scope_key":"probe-key"} +{"kind":"resume"} +{"kind":"checkpoint"} +{"kind":"sread_hit","scope_key":"probe-key"} diff --git a/formal/occult/host/testdata/realtraces/warm_replay_sync_tombstone.jsonl b/formal/occult/host/testdata/realtraces/warm_replay_sync_tombstone.jsonl new file mode 100644 index 000000000..584235be2 --- /dev/null +++ b/formal/occult/host/testdata/realtraces/warm_replay_sync_tombstone.jsonl @@ -0,0 +1,11 @@ +{"name":"warm_replay_sync_tombstone","resumed":false} +{"kind":"checkpoint"} +{"kind":"checkpoint"} +{"kind":"consult","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"clear","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"replay","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"upsert","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"delete","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"publish","row_kind":"grants","scope_key":"grants:team-1"} +{"kind":"checkpoint"} +{"kind":"seal"} diff --git a/formal/occult/host/trace_policies_test.go b/formal/occult/host/trace_policies_test.go new file mode 100644 index 000000000..f05efe0b1 --- /dev/null +++ b/formal/occult/host/trace_policies_test.go @@ -0,0 +1,143 @@ +// Deliverable 7: the trace-policy oracle set +// (../src/sync_trace_policies.occult) checked as a full verdict matrix +// — every policy against every fixture. Green fixtures must satisfy +// all seven policies; each red fixture must violate EXACTLY its own +// policy and satisfy the other six, so a policy that silently accepts +// everything (or rejects everything) cannot pass. +package host_test + +import ( + "context" + "fmt" + "testing" + + occult "github.com/conductorone/occult" + "github.com/conductorone/occult/state" +) + +// policyVerdictTerm evaluates one policy applied to a trace TERM +// (any expression over the module's constructors, module handle "M") +// and returns the engine's verdict string. +func policyVerdictTerm(t *testing.T, policy, term string) string { + t.Helper() + interp, pm, err := occult.NewCLIInterpreter("egraph", false, "", "") + if err != nil { + t.Fatalf("NewCLIInterpreter: %v", err) + } + if _, err := interp.LoadStdlib("sync_trace_policies", readSrc(t, "sync_trace_policies.occult"), pm); err != nil { + t.Fatalf("LoadStdlib sync_trace_policies: %v", err) + } + source := fmt.Sprintf(`M = require("sync_trace_policies"); M.%s(%s)`, policy, term) + res, err := interp.Eval(context.Background(), "policy-check", source, pm) + if err != nil { + t.Fatalf("Eval %q: %v", source, err) + } + if res == nil || res.Loc == nil { + t.Fatalf("Eval %q: no result location", source) + } + st, ok := interp.State.Resolve(*res.Loc) + if !ok || st.Literal == nil || st.Literal.Kind != state.LitString { + t.Fatalf("Eval %q: verdict did not reduce to a string", source) + } + return st.Literal.Str +} + +// policyVerdict evaluates one policy applied to one named fixture trace. +func policyVerdict(t *testing.T, policy, trace string) string { + t.Helper() + return policyVerdictTerm(t, policy, "M."+trace) +} + +var policies = []string{ + "consult_before_replay", + "clear_before_upsert", + "once_per_scope", + "checkpoint_before_progress", + "seal_obligations", + "session_ckpt_consistency", + "external_principal_grounding", +} + +// fixtureViolates maps each fixture to the single policy it violates +// ("" = none: the green trace). +var fixtureViolates = map[string]string{ + "trace_green": "", + "trace_red_cbr": "consult_before_replay", + "trace_red_cbu": "clear_before_upsert", + "trace_red_ops": "once_per_scope", + "trace_red_cbp": "checkpoint_before_progress", + "trace_red_seal": "seal_obligations", + "trace_green_resume": "", + "trace_red_ops_resume": "once_per_scope", + "trace_green_delete": "", + "trace_red_cbu_del": "clear_before_upsert", + "trace_red_cbp_del": "checkpoint_before_progress", + "trace_red_seal_del": "seal_obligations", + // Session fixtures (policy 6, session-checkpoint consistency — the + // CO-6b-009 root cause). The two reds violate the SAME policy in + // its two directions; fixtureVerdict carries the exact string. + "trace_green_session": "", + "trace_green_session_rollback": "", + "trace_red_session_zombie": "session_ckpt_consistency", + "trace_red_session_amnesia": "session_ckpt_consistency", + // External-principal fixtures (policy 7, external-principal + // grounding — the deleteStaleExternalPrincipals contract). The two + // reds violate the SAME policy in its two directions; + // fixtureVerdict carries the exact string. + "trace_green_ext": "", + "trace_green_ext_carry": "", + "trace_red_ext_norecon": "external_principal_grounding", + "trace_red_ext_stale": "external_principal_grounding", +} + +// fixtureVerdict overrides the expected violation string for fixtures +// whose policy distinguishes violation directions. +var fixtureVerdict = map[string]string{ + "trace_red_session_zombie": "violation: session-zombie-read", + "trace_red_session_amnesia": "violation: session-amnesia", + "trace_red_ext_norecon": "violation: ext-recon-before-copy", + "trace_red_ext_stale": "violation: ext-stale-survivor", +} + +func TestTracePolicyMatrix(t *testing.T) { + for fixture, violated := range fixtureViolates { + for _, policy := range policies { + t.Run(fixture+"/"+policy, func(t *testing.T) { + verdict := policyVerdict(t, policy, fixture) + if policy == violated { + want := "violation: " + policyLabel(policy) + if v, ok := fixtureVerdict[fixture]; ok { + want = v + } + if verdict != want { + t.Errorf("expected %s to violate %s with %q, got verdict %q", fixture, policy, want, verdict) + } + } else { + if verdict != "ok" { + t.Errorf("expected %s to satisfy %s, got verdict %q", fixture, policy, verdict) + } + } + }) + } + } +} + +func policyLabel(policy string) string { + switch policy { + case "consult_before_replay": + return "consult-before-replay" + case "clear_before_upsert": + return "clear-before-upsert" + case "once_per_scope": + return "once-per-scope" + case "checkpoint_before_progress": + return "checkpoint-before-progress" + case "seal_obligations": + return "seal-obligations" + case "session_ckpt_consistency": + return "session-ckpt-consistency" + case "external_principal_grounding": + return "external-principal-grounding" + } + return policy +} diff --git a/formal/occult/src/sync_fixtures.occult b/formal/occult/src/sync_fixtures.occult new file mode 100644 index 000000000..008b519e0 --- /dev/null +++ b/formal/occult/src/sync_fixtures.occult @@ -0,0 +1,9 @@ +# Free Skolem constants for the law checks: no axiom mentions them, so +# a law verified on them holds for arbitrary values (fresh-constant +# argument). Both sides of every equivalence query reference THE SAME +# module existential, which is what makes the two independently loaded +# query modules talk about one entity. + +∃ e1; ∃ e2; ∃ e3; +∃ x0; ∃ y0; ∃ g0; +∃ v0; ∃ m0; ∃ op0; diff --git a/formal/occult/src/sync_laws.occult b/formal/occult/src/sync_laws.occult new file mode 100644 index 000000000..dd020bc06 --- /dev/null +++ b/formal/occult/src/sync_laws.occult @@ -0,0 +1,87 @@ +# Composition algebra of the P1 fold (formal/MODEL_SPEC.md §7) and the +# variant-S stamp facts (formal/GRAPH_MODEL_SPEC.md). Loaded into +# UniversalScope by the host (LoadStdlib): bare identifiers in equation +# bodies are universal pattern variables; operations and constructors +# are existentials. Every pattern is head-guarded by a constructor. +# +# These equations are the DEFINITIONS (the fold clauses and the +# dead-membership homomorphism). The laws in formal/occult/LAWS.md are +# checked as derived equivalences by the host (host/laws_test.go); +# nothing here states a law as its own assumption. +# +# CONSTRAINT DISCIPLINE (lint: unconstrained-axiom-universal). The +# universals here range over OPEN carriers by design: the laws are +# verified on the free Skolem constants of sync_fixtures.occult, and +# the fresh-constant argument needs those constants free of axioms — +# gating the fold clauses on sorts would demand membership facts about +# the Skolems, and the engine cannot consume userspace free-term +# membership at dispatch anyway (host/constrained_params_probe_test.go +# pins this). This is also why the L7 ACI axioms below COPY the shape +# of the stdlib's vc_merge instead of requiring std vector_clock: that +# module's universals are constrained to a VectorClock sort nothing +# gives members, so its rewrites cannot fire on our stamp terms. + +# --- Fold values and complete-round operations --- + +∃ app; ∃ foldops; +∃ rows; +∃ fresh; ∃ repl; ∃ ovl; ∃ ovlsg; ∃ skip; +∃ opsnil; ∃ opscons; + +# Fold clauses (MODEL_SPEC §7, P1): +# fresh(e): fetch-fresh / REPLACES round — absorbs any prior fold value. +app(fresh(e), x) = rows(e); +# repl(e): completed replacement copy of the attested base e. +app(repl(e), x) = rows(e); +# ovlsg(e): self-grounding overlay (its own copy committed in-round). +app(ovlsg(e), x) = rows(e); +# ovl(a, b): overlay grounded on this sync's completed replay of a. +app(ovl(a, b), rows(a)) = rows(b); +# skip(e): copy-skipped duplicate — a no-op on the already-current value. +app(skip(e), rows(e)) = rows(e); + +# Round-log fold: ops apply in completion order (head first). +foldops(opsnil, x) = x; +foldops(opscons(op, rest), x) = foldops(rest, app(op, x)); + +# --- Row-map observations (two-id envelope, for the tombstone laws) --- +# Distinct ids are distinct constructors: the coalesced-delta +# precondition (at most one op per id per round) makes a two-id +# observational algebra sufficient for the commutation law. + +∃ get; ∃ put; ∃ del; +∃ id1; ∃ id2; +∃ found; ∃ absent; + +get(id1, put(id1, v, m)) = found(v); +get(id2, put(id2, v, m)) = found(v); +get(id1, del(id1, m)) = absent; +get(id2, del(id2, m)) = absent; +get(id1, put(id2, v, m)) = get(id1, m); +get(id2, put(id1, v, m)) = get(id2, m); +get(id1, del(id2, m)) = get(id1, m); +get(id2, del(id1, m)) = get(id2, m); + +# --- Stamps: dead-membership homomorphism (variant S) --- +# Own booleans (tt/ff) keep the algebra free-constructor: natives are +# opaque to saturation. + +∃ merge; ∃ dead; ∃ live; +∃ hasdead; ∃ tt; ∃ ff; ∃ bor; + +bor(tt, b) = tt; +bor(b, tt) = tt; +bor(ff, ff) = ff; + +hasdead(dead(g)) = tt; +hasdead(live(g)) = ff; +hasdead(merge(a, b)) = bor(hasdead(a), hasdead(b)); + +# L7 — the join-semilattice ASSUMPTIONS for merge, instantiating the +# engine stdlib's vector_clock axioms (vc_merge) on our stamp carrier. +# These are assumptions of the design, not theorems here; they are +# loaded so every other law is checked IN THEIR PRESENCE (coherence: +# the controls must stay non-equivalent even with ACI closure active). +merge(a, b) = merge(b, a); +merge(a, merge(b, c)) = merge(merge(a, b), c); +merge(a, a) = a; diff --git a/formal/occult/src/sync_phantom.occult b/formal/occult/src/sync_phantom.occult new file mode 100644 index 000000000..048ce1571 --- /dev/null +++ b/formal/occult/src/sync_phantom.occult @@ -0,0 +1,75 @@ +# The phantom union, derived deductively (the KNOWN-BROKEN composition +# vs the premise-validated one). This is the walker calibration +# family's star bug (formal/MODEL_SPEC.md; brief "Calibration cases"): +# a source-cache replay grounds the partition at the cache's ATTESTED +# epoch (e0), but the broken algorithm requests the overlay diff from +# the LAST SYNC's epoch (e1). Each response is individually truthful — +# the replay really is rows(e0), the diff really is e1→e2 — and their +# composition is false: a row deleted between e0 and e1 survives as a +# phantom. The good algorithm (demand-graph runtime / premise-validated +# adoption) grounds the diff at the marker's attested base, so the same +# ingredients compose truthfully. +# +# Upstream history (two-id observational envelope, as in sync_laws): +# e0: id1 = vx, id2 = v1 +# e1: id1 DELETED, id2 = v1 +# e2: id1 absent, id2 = v2 +# The truthful diff e1→e2 mentions only id2 (id1 did not change between +# e1 and e2 — it was already gone). The truthful diff e0→e2 mentions +# both. Applying diff12 to the e0-grounded base is the bug. +# +# CONSTRAINT DISCIPLINE (lint: unconstrained-axiom-universal): the +# universals v/m/b are deliberately unconstrained — head-guarded by +# module-local constructors (get/put/del), and the engine cannot yet +# consume userspace free-term membership at axiom dispatch (pinned by +# host/constrained_params_probe_test.go; see the engine-changes brief). + +syntax("standard"); + +# Row-map observations (self-contained copy of the two-id algebra). +∃ get; +∃ put; +∃ del; +∃ mt; +∃ id1; +∃ id2; +∃ found; +∃ absent; + +get(id1, mt) = absent; +get(id2, mt) = absent; +get(id1, put(id1, v, m)) = found(v); +get(id2, put(id2, v, m)) = found(v); +get(id1, del(id1, m)) = absent; +get(id2, del(id2, m)) = absent; +get(id1, put(id2, v, m)) = get(id1, m); +get(id2, put(id1, v, m)) = get(id2, m); +get(id1, del(id2, m)) = get(id1, m); +get(id2, del(id1, m)) = get(id2, m); + +# Upstream truth per epoch. +∃ vx; +∃ v1; +∃ v2; +∃ rows_e0; +∃ rows_e1; +∃ rows_e2; +rows_e0 = put(id1, vx, put(id2, v1, mt)); +rows_e1 = put(id2, v1, mt); +rows_e2 = put(id2, v2, mt); + +# Truthful diffs as base transformers. diff12 is truthful FOR BASE e1; +# diff02 is truthful for base e0. +∃ diff12; +∃ diff02; +diff12(b) = put(id2, v2, b); +diff02(b) = put(id2, v2, del(id1, b)); + +# The two compositions under test. broken: replay grounded at e0, diff +# requested from e1 (the composition the current algorithm can +# produce). good: diff grounded at the replay's attested base (what +# premise-validated adoption enforces). +∃ result_broken; +∃ result_good; +result_broken = diff12(rows_e0); +result_good = diff02(rows_e0); diff --git a/formal/occult/src/sync_protocol.occult b/formal/occult/src/sync_protocol.occult new file mode 100644 index 000000000..a63d762e0 --- /dev/null +++ b/formal/occult/src/sync_protocol.occult @@ -0,0 +1,75 @@ +# Deliverable 8: the syncer↔connector source-cache protocol as a global +# session term with per-role projections (brief: "Session-typed protocol +# contract"). Contract source: +# proto/c1/connector/v2/annotation_source_cache.proto (lookup +# continuation: offer→ask→answers with a per-request bounce cap of 4; +# replay/record annotation exchange on page responses). +# +# Role mapping: SYNCER = P_leader, CONNECTOR = P_follower (send(d) is +# syncer→connector; recv(d) is connector→syncer — the projection +# functor's polarity). +# +# The bounce cap is STRUCTURAL: the module defines sessions with at +# most four ask rounds and no five-bounce term exists here. Checking a +# cap VIOLATION (or general stuck-freedom) is the engine's open +# session-types work; until then the cap lives in the protocol shape, +# and the Go-side constant it mirrors is +# sourcecache.MaxLookupBouncesPerRequest = 4. +# +# Fidelity note: local(e) is role-unannotated in the projection functor +# (both projections carry it). resolve_answers is syncer-local in +# reality; the connector's projected local(resolve_answers) is vacuous. + +syntax("standard"); + +∃ proto; +proto = require("protocol"); +∃ send; +send = proto.send; +∃ recv; +recv = proto.recv; + +∃ proj; +proj = require("projection"); +∃ local; +local = proj.local; + +# Message tags: +# req_offer — list request carrying SourceCacheLookupOffer. +# req_answers — the SAME request re-invoked with accumulated +# SourceCacheLookupAnswers (still carrying the offer). +# lookup_ask — response carrying SourceCacheLookupAsk: no rows, no token. +# page_record — page response: fresh rows + SourceCacheRecord. +# page_replay — page response: no rows + SourceCacheReplay (overlay +# pages follow as records). +# resolve_answers — syncer-local: resolve asks against the previous +# artifact. +∃ req_offer; +∃ req_answers; +∃ lookup_ask; +∃ page_record; +∃ page_replay; +∃ resolve_answers; + +# Ask bounces as CLOSED terms (the raft shape: parameterized protocol +# functions gate their rewrite on classifier membership of the +# argument, which free message tags do not carry). ask_first is the +# initial request's bounce; ask_again is a re-invoked request's bounce. +∃ ask_first; +∃ ask_again; +ask_first = send(req_offer) pipe recv(lookup_ask) pipe local(resolve_answers); +ask_again = send(req_answers) pipe recv(lookup_ask) pipe local(resolve_answers); + +# Sessions per request (record leg; the replay legs are symmetric): +# session_direct — no ask: the offer is honored, the page served +# immediately; session_ask1 — one bounce; session_ask4 — the maximal +# legal session (bounce cap 4). +∃ session_direct; +∃ session_ask1; +∃ session_ask4; +∃ session_direct_replay; + +session_direct = send(req_offer) pipe recv(page_record); +session_direct_replay = send(req_offer) pipe recv(page_replay); +session_ask1 = ask_first pipe send(req_answers) pipe recv(page_record); +session_ask4 = ask_first pipe ask_again pipe ask_again pipe ask_again pipe send(req_answers) pipe recv(page_record); diff --git a/formal/occult/src/sync_trace_policies.occult b/formal/occult/src/sync_trace_policies.occult new file mode 100644 index 000000000..926c4a1ee --- /dev/null +++ b/formal/occult/src/sync_trace_policies.occult @@ -0,0 +1,534 @@ +# Deliverable 7: the ordering/durability trace-policy oracle set +# (brief: "Trace-policy oracle set" — consult-before-replay, +# checkpoint-before-progress, clear-before-upsert, once-per-scope, seal +# obligations, policy 6: session-checkpoint consistency (the CO-6b-009 +# root cause), plus policy 7: external-principal grounding, the +# deleteStaleExternalPrincipals contract) over the announce vocabulary +# pinned in formal/MODEL_SPEC.md §7. A trace is ONE SYNC's event list — possibly +# spanning crash/resume attempts, with ev_resume marking each attempt +# boundary (the multi-attempt extension; single-attempt traces carry no +# marker and mean exactly what they did before). Each policy is a +# recursive verdict function returning "ok" or a violation string. Two +# scopes (s1, s2) — the same small-scope envelope as the P models. +# +# Loaded into UniversalScope by the host: equations are rewrite rules, +# bare identifiers (r, a, b, p, q, s, v) are universal pattern +# variables, and every pattern is head-guarded by a constructor. +# +# CONSTRAINT DISCIPLINE (lint: unconstrained-axiom-universal). Every +# universal here is deliberately unconstrained. The prescribed fix — +# gating the scope/flag universals on userspace sorts (s : Scope; with +# Scope = {s1, s2,}) — does not evaluate today: axiom-side dispatch +# cannot consume free-term classifier membership in any documented form, +# so member-only traces stop reducing and every verdict becomes a stuck +# term (pinned by host/constrained_params_probe_test.go; the ask lives +# in docs/tasks/occult-engine-changes-brief.md). Until the engine lands +# that evidence, the mitigation is the head-guard discipline: every LHS +# is guarded by a module-local constructor (tcons/tnil/ev_*), so a +# universal binds only inside applications of this module's own +# existentials — the cross-model bridged-class hazard the lint warns +# about requires someone bridging these heads, which the host never +# does. + +syntax("standard"); + +# Events. ev_replay(s) is the committed replacement copy for scope s +# (the replay-unit commit); ev_clear(s) the partition clear; +# ev_checkpoint the durable watermark commit; ev_seal the artifact +# seal. +∃ ev_consult; +∃ ev_clear; +∃ ev_replay; +∃ ev_upsert; +∃ ev_publish; +∃ ev_checkpoint; +∃ ev_seal; + +# ev_delete(s) is a committed tombstone application for scope s (the +# delta protocol's delete leg: DeletedIds / DeletedPrincipalIds applied +# by the store after the page's rows, before the validator publish — +# B3's within-page order). A delete is a WRITE: it needs grounding like +# an upsert (a tombstone against an un-regrounded base is the same bug +# class — you cannot delete from a base that was never copied), it +# dirties the quiescent-checkpoint flag, and it marks the scope active +# for seal obligations. Consult-before-replay and once-per-scope ignore +# it. +∃ ev_delete; + +# Session-store events (policy 6). ev_swrite(k) is a committed +# connector session write for key k; ev_sread_hit(k) / ev_sread_miss(k) +# a session read returning found / not-found. Sessions commit durably +# at op time, OUTSIDE the checkpoint mechanism — which is exactly the +# hazard policy 6 exists to judge. Session events are invisible to the +# artifact policies (1-5): they are not partition writes, do not need +# grounding, and do not dirty the quiescent-checkpoint watermark. +# One-key envelope (k1), mirroring the P model's single session key. +∃ ev_swrite; +∃ ev_sread_hit; +∃ ev_sread_miss; +∃ k1; + +# External-principal events (policy 7). The external phase copies an +# outside source's principals into the sync store; committed copies +# are durable across crashes (checkpoint resume deliberately retains +# completed writes), so a between-attempt shrink strands a dead +# attempt's copies unless reconciliation deletes them. ep_list marks a +# phase run listing the source's CURRENT answer; ep_live(p) declares p +# a member of that answer; ep_recon marks reconciliation COMPLETED +# (stale copies deleted, or nothing stale — the warn-and-continue +# degrade of a non-deleting engine emits NO ep_recon: the pass ran but +# did not reconcile); ep_copy(p) is p's committed principal write. +# External events live in their own keyspace: invisible to the +# artifact policies (1-5) and the session policy (6), like session +# events are to both. Two-principal envelope (p1, p2), mirroring the +# P model's scenario-8 row ids. +∃ ep_list; +∃ ep_live; +∃ ep_recon; +∃ ep_copy; +∃ p1; +∃ p2; + +# ev_resume marks a crash/resume attempt boundary inside one sync's +# trace. Semantics (matching pkg/sync's checkpoint-durable provenance, +# pinned by chaos_source_cache_resume_test.go): durable facts PERSIST +# across the boundary — consult flags (the hit-set is +# checkpoint-durable), clear grounding (committed rows survive the +# crash), scope activity and publishes (seal obligations are +# sync-scoped). Only once-per-scope RESETS: an interrupted action +# restarts from its root token, so the across-attempt replay re-copy is +# B5-legal at-least-once idempotence, while a within-attempt duplicate +# remains the bug the policy exists to catch. The quiescent-checkpoint +# flag also persists: a resume adds no progress by itself. +# +# CONSEQUENCE, stated so nobody reads more into policy 2 than it says: +# because clear grounding persists across ev_resume, an attempt-1 +# clear legitimately grounds an attempt-2 write in a ONE-TERM +# multi-attempt trace — committed rows really do survive the crash, so +# that history is ordering-legal. Policy 2 is therefore BY DESIGN +# blind to the resume-without-regrounding class when a whole sync +# renders as one term. That class is caught on the other leg of the +# bridge: the refimpl oracle renders EACH ATTEMPT as its own term (no +# ev_resume), where a resumed attempt writing without its own clear +# reds clear-before-upsert (TestRefImplLegacyCrashResume), and the +# real-trace renderer inserts no structural clear for resumed +# fixtures. The record-flip incarnation (fresh RECORD round over a +# durably-grounded partition holding crashed-copy debris) is a CONTENT +# violation, owned by the exporting test's content oracle — see +# TRACE_BRIDGE.md's scope note. +∃ ev_resume; + +# Scopes and trace list constructors. +∃ s1; +∃ s2; +∃ tnil; +∃ tcons; + +# Flag booleans (free constructors; natives are opaque to saturation). +∃ tt; +∃ ff; + +# --- Policy 1: consult-before-replay --- +# Every replay commit for a scope requires an earlier source-cache +# consult for that scope in the same SYNC: the consult flags persist +# across ev_resume (the hit-set is checkpoint-durable, see the +# ev_resume note above), so a resumed attempt replaying under attempt +# 1's consult is legal. A within-sync replay with NO prior consult +# anywhere is the violation. +∃ consult_before_replay; +∃ cbr_go; +consult_before_replay(t) = cbr_go(t, ff, ff); +cbr_go(tnil, a, b) = "ok"; +cbr_go(tcons(ev_consult(s1), r), a, b) = cbr_go(r, tt, b); +cbr_go(tcons(ev_consult(s2), r), a, b) = cbr_go(r, a, tt); +cbr_go(tcons(ev_replay(s1), r), tt, b) = cbr_go(r, tt, b); +cbr_go(tcons(ev_replay(s1), r), ff, b) = "violation: consult-before-replay"; +cbr_go(tcons(ev_replay(s2), r), a, tt) = cbr_go(r, a, tt); +cbr_go(tcons(ev_replay(s2), r), a, ff) = "violation: consult-before-replay"; +cbr_go(tcons(ev_clear(s), r), a, b) = cbr_go(r, a, b); +cbr_go(tcons(ev_upsert(s), r), a, b) = cbr_go(r, a, b); +cbr_go(tcons(ev_publish(s), r), a, b) = cbr_go(r, a, b); +cbr_go(tcons(ev_checkpoint, r), a, b) = cbr_go(r, a, b); +cbr_go(tcons(ev_seal, r), a, b) = cbr_go(r, a, b); +cbr_go(tcons(ev_resume, r), a, b) = cbr_go(r, a, b); +cbr_go(tcons(ev_delete(s), r), a, b) = cbr_go(r, a, b); +cbr_go(tcons(ev_swrite(k), r), a, b) = cbr_go(r, a, b); +cbr_go(tcons(ev_sread_hit(k), r), a, b) = cbr_go(r, a, b); +cbr_go(tcons(ev_sread_miss(k), r), a, b) = cbr_go(r, a, b); +cbr_go(tcons(ep_list, r), a, b) = cbr_go(r, a, b); +cbr_go(tcons(ep_live(p), r), a, b) = cbr_go(r, a, b); +cbr_go(tcons(ep_recon, r), a, b) = cbr_go(r, a, b); +cbr_go(tcons(ep_copy(p), r), a, b) = cbr_go(r, a, b); + +# --- Policy 2: clear-before-upsert (clear-before-WRITE) --- +# Any upsert into — or tombstone delete from — a scope's partition +# requires an earlier clear of that partition (replacement rounds clear +# first; overlay upserts and deletes are grounded on this sync's +# replay, which cleared). The policy name and violation string keep the +# original "upsert" form for continuity; the gate covers both write +# kinds. +∃ clear_before_upsert; +∃ cbu_go; +clear_before_upsert(t) = cbu_go(t, ff, ff); +cbu_go(tnil, a, b) = "ok"; +cbu_go(tcons(ev_clear(s1), r), a, b) = cbu_go(r, tt, b); +cbu_go(tcons(ev_clear(s2), r), a, b) = cbu_go(r, a, tt); +cbu_go(tcons(ev_upsert(s1), r), tt, b) = cbu_go(r, tt, b); +cbu_go(tcons(ev_upsert(s1), r), ff, b) = "violation: clear-before-upsert"; +cbu_go(tcons(ev_upsert(s2), r), a, tt) = cbu_go(r, a, tt); +cbu_go(tcons(ev_upsert(s2), r), a, ff) = "violation: clear-before-upsert"; +cbu_go(tcons(ev_delete(s1), r), tt, b) = cbu_go(r, tt, b); +cbu_go(tcons(ev_delete(s1), r), ff, b) = "violation: clear-before-upsert"; +cbu_go(tcons(ev_delete(s2), r), a, tt) = cbu_go(r, a, tt); +cbu_go(tcons(ev_delete(s2), r), a, ff) = "violation: clear-before-upsert"; +cbu_go(tcons(ev_consult(s), r), a, b) = cbu_go(r, a, b); +cbu_go(tcons(ev_replay(s), r), a, b) = cbu_go(r, a, b); +cbu_go(tcons(ev_publish(s), r), a, b) = cbu_go(r, a, b); +cbu_go(tcons(ev_checkpoint, r), a, b) = cbu_go(r, a, b); +cbu_go(tcons(ev_seal, r), a, b) = cbu_go(r, a, b); +cbu_go(tcons(ev_resume, r), a, b) = cbu_go(r, a, b); +cbu_go(tcons(ev_swrite(k), r), a, b) = cbu_go(r, a, b); +cbu_go(tcons(ev_sread_hit(k), r), a, b) = cbu_go(r, a, b); +cbu_go(tcons(ev_sread_miss(k), r), a, b) = cbu_go(r, a, b); +cbu_go(tcons(ep_list, r), a, b) = cbu_go(r, a, b); +cbu_go(tcons(ep_live(p), r), a, b) = cbu_go(r, a, b); +cbu_go(tcons(ep_recon, r), a, b) = cbu_go(r, a, b); +cbu_go(tcons(ep_copy(p), r), a, b) = cbu_go(r, a, b); + +# --- Policy 3: once-per-scope --- +# At most one committed replacement copy per scope per attempt (the +# across-attempt idempotent re-run is B5-legal and lives outside a +# single-attempt trace). +∃ once_per_scope; +∃ ops_go; +once_per_scope(t) = ops_go(t, ff, ff); +ops_go(tnil, a, b) = "ok"; +ops_go(tcons(ev_replay(s1), r), ff, b) = ops_go(r, tt, b); +ops_go(tcons(ev_replay(s1), r), tt, b) = "violation: once-per-scope"; +ops_go(tcons(ev_replay(s2), r), a, ff) = ops_go(r, a, tt); +ops_go(tcons(ev_replay(s2), r), a, tt) = "violation: once-per-scope"; +ops_go(tcons(ev_consult(s), r), a, b) = ops_go(r, a, b); +ops_go(tcons(ev_clear(s), r), a, b) = ops_go(r, a, b); +ops_go(tcons(ev_upsert(s), r), a, b) = ops_go(r, a, b); +ops_go(tcons(ev_publish(s), r), a, b) = ops_go(r, a, b); +ops_go(tcons(ev_checkpoint, r), a, b) = ops_go(r, a, b); +ops_go(tcons(ev_seal, r), a, b) = ops_go(r, a, b); +ops_go(tcons(ev_resume, r), a, b) = ops_go(r, ff, ff); +ops_go(tcons(ev_delete(s), r), a, b) = ops_go(r, a, b); +ops_go(tcons(ev_swrite(k), r), a, b) = ops_go(r, a, b); +ops_go(tcons(ev_sread_hit(k), r), a, b) = ops_go(r, a, b); +ops_go(tcons(ev_sread_miss(k), r), a, b) = ops_go(r, a, b); +ops_go(tcons(ep_list, r), a, b) = ops_go(r, a, b); +ops_go(tcons(ep_live(p), r), a, b) = ops_go(r, a, b); +ops_go(tcons(ep_recon, r), a, b) = ops_go(r, a, b); +ops_go(tcons(ep_copy(p), r), a, b) = ops_go(r, a, b); + +# --- Policy 4: checkpoint-before-progress (seal form) --- +# The seal requires a QUIESCENT checkpoint: an ev_checkpoint after +# which no write event (clear/replay/upsert/publish) occurred. Reads +# do not dirty the watermark. +∃ checkpoint_before_progress; +∃ cbp_go; +checkpoint_before_progress(t) = cbp_go(t, ff); +cbp_go(tnil, f) = "ok"; +cbp_go(tcons(ev_checkpoint, r), f) = cbp_go(r, tt); +cbp_go(tcons(ev_clear(s), r), f) = cbp_go(r, ff); +cbp_go(tcons(ev_replay(s), r), f) = cbp_go(r, ff); +cbp_go(tcons(ev_upsert(s), r), f) = cbp_go(r, ff); +cbp_go(tcons(ev_delete(s), r), f) = cbp_go(r, ff); +cbp_go(tcons(ev_publish(s), r), f) = cbp_go(r, ff); +cbp_go(tcons(ev_consult(s), r), f) = cbp_go(r, f); +cbp_go(tcons(ev_resume, r), f) = cbp_go(r, f); +cbp_go(tcons(ev_swrite(k), r), f) = cbp_go(r, f); +cbp_go(tcons(ev_sread_hit(k), r), f) = cbp_go(r, f); +cbp_go(tcons(ev_sread_miss(k), r), f) = cbp_go(r, f); +cbp_go(tcons(ep_list, r), f) = cbp_go(r, f); +cbp_go(tcons(ep_live(p), r), f) = cbp_go(r, f); +cbp_go(tcons(ep_recon, r), f) = cbp_go(r, f); +cbp_go(tcons(ep_copy(p), r), f) = cbp_go(r, f); +cbp_go(tcons(ev_seal, r), tt) = "ok"; +cbp_go(tcons(ev_seal, r), ff) = "violation: checkpoint-before-progress"; + +# --- Policy 5: seal obligations --- +# Every scope with any activity this sync (consult/clear/replay/ +# upsert/delete) must be published before the seal. State per scope: +# active flag, published flag. +# +# SCOPE, stated deliberately: the obligation is EXISTENTIAL — some +# publish for the scope precedes the seal. A write AFTER the scope's +# last publish (publish, upsert, seal with no re-publish) still +# satisfies this policy: whether every row is covered by the +# attestation it sealed under is a CONTENT/attestation question, +# owned by the walker model's P1-ATTEST clauses and the exporting +# test's content oracle, not by this ordering policy. Strengthening +# this policy to "last write precedes last publish" is possible +# (reset the published flag on activity) but is registered as a +# deliberate non-goal while the real renderer's fixtures publish +# per-round: the weak form matches what commit order alone can +# witness. +∃ seal_obligations; +∃ so_go; +seal_obligations(t) = so_go(t, ff, ff, ff, ff); +so_go(tnil, a, p, b, q) = "ok"; +so_go(tcons(ev_consult(s1), r), a, p, b, q) = so_go(r, tt, p, b, q); +so_go(tcons(ev_consult(s2), r), a, p, b, q) = so_go(r, a, p, tt, q); +so_go(tcons(ev_clear(s1), r), a, p, b, q) = so_go(r, tt, p, b, q); +so_go(tcons(ev_clear(s2), r), a, p, b, q) = so_go(r, a, p, tt, q); +so_go(tcons(ev_replay(s1), r), a, p, b, q) = so_go(r, tt, p, b, q); +so_go(tcons(ev_replay(s2), r), a, p, b, q) = so_go(r, a, p, tt, q); +so_go(tcons(ev_upsert(s1), r), a, p, b, q) = so_go(r, tt, p, b, q); +so_go(tcons(ev_upsert(s2), r), a, p, b, q) = so_go(r, a, p, tt, q); +so_go(tcons(ev_delete(s1), r), a, p, b, q) = so_go(r, tt, p, b, q); +so_go(tcons(ev_delete(s2), r), a, p, b, q) = so_go(r, a, p, tt, q); +so_go(tcons(ev_publish(s1), r), a, p, b, q) = so_go(r, a, tt, b, q); +so_go(tcons(ev_publish(s2), r), a, p, b, q) = so_go(r, a, p, b, tt); +so_go(tcons(ev_checkpoint, r), a, p, b, q) = so_go(r, a, p, b, q); +so_go(tcons(ev_resume, r), a, p, b, q) = so_go(r, a, p, b, q); +so_go(tcons(ev_swrite(k), r), a, p, b, q) = so_go(r, a, p, b, q); +so_go(tcons(ev_sread_hit(k), r), a, p, b, q) = so_go(r, a, p, b, q); +so_go(tcons(ev_sread_miss(k), r), a, p, b, q) = so_go(r, a, p, b, q); +so_go(tcons(ep_list, r), a, p, b, q) = so_go(r, a, p, b, q); +so_go(tcons(ep_live(x), r), a, p, b, q) = so_go(r, a, p, b, q); +so_go(tcons(ep_recon, r), a, p, b, q) = so_go(r, a, p, b, q); +so_go(tcons(ep_copy(x), r), a, p, b, q) = so_go(r, a, p, b, q); +so_go(tcons(ev_seal, r), tt, ff, b, q) = "violation: seal-obligations"; +so_go(tcons(ev_seal, r), a, p, tt, ff) = "violation: seal-obligations"; +so_go(tcons(ev_seal, r), ff, p, ff, q) = "ok"; +so_go(tcons(ev_seal, r), tt, tt, ff, q) = "ok"; +so_go(tcons(ev_seal, r), ff, p, tt, tt) = "ok"; +so_go(tcons(ev_seal, r), tt, tt, tt, tt) = "ok"; + +# --- Policy 6: session-checkpoint consistency --- +# THE CONSTRAINT (CO-6b-009's root cause; the P model's P6-C monitor +# in trace-policy form): observable session state after a crash/resume +# boundary must equal session state at the restored checkpoint — in +# BOTH directions. +# ZOMBIE: a session write with no later checkpoint before ev_resume +# belongs to the dead attempt's rolled-back window; a read HIT on +# that key afterwards (with no committed value it could be +# shadowing, and no live rewrite) observed the dead attempt's +# future. +# AMNESIA: a checkpoint-committed session value must remain readable +# — the work that produced it will NOT re-run, so a read MISS is +# unrecoverable data loss (the rejected wholesale resume-clear). +# State per key: u = written since the last checkpoint, c = a +# checkpoint committed some value, z = a dead attempt's uncommitted +# write is (potentially) still durable. A live rewrite reclaims the +# key (z := ff). Value-blind conservatism: a HIT with c = tt AND +# z = tt cannot be judged without values (the durable zombie may +# shadow the committed value) and passes here — the P model's +# value-aware P6-C covers that case. +∃ session_ckpt_consistency; +∃ scc_go; +session_ckpt_consistency(t) = scc_go(t, ff, ff, ff); +scc_go(tnil, u, c, z) = "ok"; +scc_go(tcons(ev_swrite(k1), r), u, c, z) = scc_go(r, tt, c, ff); +scc_go(tcons(ev_checkpoint, r), tt, c, z) = scc_go(r, ff, tt, z); +scc_go(tcons(ev_checkpoint, r), ff, c, z) = scc_go(r, ff, c, z); +scc_go(tcons(ev_resume, r), tt, c, z) = scc_go(r, ff, c, tt); +scc_go(tcons(ev_resume, r), ff, c, z) = scc_go(r, ff, c, z); +scc_go(tcons(ev_sread_hit(k1), r), u, c, ff) = scc_go(r, u, c, ff); +scc_go(tcons(ev_sread_hit(k1), r), u, tt, tt) = scc_go(r, u, tt, tt); +scc_go(tcons(ev_sread_hit(k1), r), u, ff, tt) = "violation: session-zombie-read"; +scc_go(tcons(ev_sread_miss(k1), r), u, tt, z) = "violation: session-amnesia"; +scc_go(tcons(ev_sread_miss(k1), r), u, ff, z) = scc_go(r, u, ff, z); +scc_go(tcons(ev_consult(s), r), u, c, z) = scc_go(r, u, c, z); +scc_go(tcons(ev_clear(s), r), u, c, z) = scc_go(r, u, c, z); +scc_go(tcons(ev_replay(s), r), u, c, z) = scc_go(r, u, c, z); +scc_go(tcons(ev_upsert(s), r), u, c, z) = scc_go(r, u, c, z); +scc_go(tcons(ev_delete(s), r), u, c, z) = scc_go(r, u, c, z); +scc_go(tcons(ev_publish(s), r), u, c, z) = scc_go(r, u, c, z); +scc_go(tcons(ev_seal, r), u, c, z) = scc_go(r, u, c, z); +scc_go(tcons(ep_list, r), u, c, z) = scc_go(r, u, c, z); +scc_go(tcons(ep_live(x), r), u, c, z) = scc_go(r, u, c, z); +scc_go(tcons(ep_recon, r), u, c, z) = scc_go(r, u, c, z); +scc_go(tcons(ep_copy(x), r), u, c, z) = scc_go(r, u, c, z); + +# --- Policy 7: external-principal grounding --- +# THE CONSTRAINT (the deleteStaleExternalPrincipals contract; the P +# model's P8 monitor in trace-policy form). Committed principal copies +# are durable across crashes, so a dead attempt's copies are debris +# the moment the source's answer shrinks. Two directions: +# RECON-BEFORE-COPY: within a phase run (ep_list starts one), the +# current answer's writes may commit only after reconciliation +# completed (ep_recon) — the warn-and-continue degrade of a +# non-deleting engine copies over unreconciled debris and emits no +# ep_recon, so its first copy is the violation. +# STALE-SURVIVOR at seal: a principal copied this sync but absent +# from the last-listed answer survived to seal undeleted (and, +# dually, EXT-MISSING: a listed principal never copied). The seal +# clause compares against the last LIST, not truth-at-seal — an +# attempt that completed the phase before a crash seals its own +# answer legitimately (sync-scoped freshness). +# State: n = reconciliation completed this phase run (reset by +# ep_list), c1/c2 = principal copied and not reconciled away (durable +# — never reset by ep_list or ev_resume), l1/l2 = member of the +# current answer (reset by ep_list). ep_recon deletes stale copies: +# c := c AND l. Value-blind like every policy here; the P monitor +# carries the truth ghost this trace form cannot. +∃ band; +band(tt, tt) = tt; +band(tt, ff) = ff; +band(ff, tt) = ff; +band(ff, ff) = ff; +∃ external_principal_grounding; +∃ epg_go; +external_principal_grounding(t) = epg_go(t, ff, ff, ff, ff, ff); +epg_go(tnil, n, c1, c2, l1, l2) = "ok"; +epg_go(tcons(ep_list, r), n, c1, c2, l1, l2) = epg_go(r, ff, c1, c2, ff, ff); +epg_go(tcons(ep_live(p1), r), n, c1, c2, l1, l2) = epg_go(r, n, c1, c2, tt, l2); +epg_go(tcons(ep_live(p2), r), n, c1, c2, l1, l2) = epg_go(r, n, c1, c2, l1, tt); +epg_go(tcons(ep_recon, r), n, c1, c2, l1, l2) = epg_go(r, tt, band(c1, l1), band(c2, l2), l1, l2); +epg_go(tcons(ep_copy(p1), r), tt, c1, c2, l1, l2) = epg_go(r, tt, tt, c2, l1, l2); +epg_go(tcons(ep_copy(p1), r), ff, c1, c2, l1, l2) = "violation: ext-recon-before-copy"; +epg_go(tcons(ep_copy(p2), r), tt, c1, c2, l1, l2) = epg_go(r, tt, c1, tt, l1, l2); +epg_go(tcons(ep_copy(p2), r), ff, c1, c2, l1, l2) = "violation: ext-recon-before-copy"; +# Seal verdict table, disjoint and total over (c1, l1) x (c2, l2): +# stale = copied AND not live; missing = live AND not copied; ok +# otherwise. Stale reports first in mixed cases. +epg_go(tcons(ev_seal, r), n, tt, c2, ff, l2) = "violation: ext-stale-survivor"; +epg_go(tcons(ev_seal, r), n, ff, tt, l1, ff) = "violation: ext-stale-survivor"; +epg_go(tcons(ev_seal, r), n, tt, tt, tt, ff) = "violation: ext-stale-survivor"; +epg_go(tcons(ev_seal, r), n, ff, ff, tt, ff) = "violation: ext-missing"; +epg_go(tcons(ev_seal, r), n, ff, tt, tt, tt) = "violation: ext-missing"; +epg_go(tcons(ev_seal, r), n, ff, ff, tt, tt) = "violation: ext-missing"; +epg_go(tcons(ev_seal, r), n, ff, ff, ff, tt) = "violation: ext-missing"; +epg_go(tcons(ev_seal, r), n, tt, ff, tt, tt) = "violation: ext-missing"; +epg_go(tcons(ev_seal, r), n, ff, ff, ff, ff) = "ok"; +epg_go(tcons(ev_seal, r), n, ff, tt, ff, tt) = "ok"; +epg_go(tcons(ev_seal, r), n, tt, ff, tt, ff) = "ok"; +epg_go(tcons(ev_seal, r), n, tt, tt, tt, tt) = "ok"; +# Pass-throughs: artifact and session machinery is invisible here. +epg_go(tcons(ev_consult(s), r), n, c1, c2, l1, l2) = epg_go(r, n, c1, c2, l1, l2); +epg_go(tcons(ev_clear(s), r), n, c1, c2, l1, l2) = epg_go(r, n, c1, c2, l1, l2); +epg_go(tcons(ev_replay(s), r), n, c1, c2, l1, l2) = epg_go(r, n, c1, c2, l1, l2); +epg_go(tcons(ev_upsert(s), r), n, c1, c2, l1, l2) = epg_go(r, n, c1, c2, l1, l2); +epg_go(tcons(ev_delete(s), r), n, c1, c2, l1, l2) = epg_go(r, n, c1, c2, l1, l2); +epg_go(tcons(ev_publish(s), r), n, c1, c2, l1, l2) = epg_go(r, n, c1, c2, l1, l2); +epg_go(tcons(ev_checkpoint, r), n, c1, c2, l1, l2) = epg_go(r, n, c1, c2, l1, l2); +epg_go(tcons(ev_resume, r), n, c1, c2, l1, l2) = epg_go(r, n, c1, c2, l1, l2); +epg_go(tcons(ev_swrite(k), r), n, c1, c2, l1, l2) = epg_go(r, n, c1, c2, l1, l2); +epg_go(tcons(ev_sread_hit(k), r), n, c1, c2, l1, l2) = epg_go(r, n, c1, c2, l1, l2); +epg_go(tcons(ev_sread_miss(k), r), n, c1, c2, l1, l2) = epg_go(r, n, c1, c2, l1, l2); + +# --- Fixtures --- +# green: one honest replacement round on s1, published, quiescent +# checkpoint, seal. Every policy must answer "ok". +∃ trace_green; +trace_green = tcons(ev_consult(s1), tcons(ev_clear(s1), tcons(ev_replay(s1), tcons(ev_upsert(s1), tcons(ev_publish(s1), tcons(ev_checkpoint, tcons(ev_seal, tnil))))))); + +# Each red fixture violates EXACTLY its own policy and satisfies the +# other six (the isolation matrix is asserted by the host). +∃ trace_red_cbr; +trace_red_cbr = tcons(ev_clear(s1), tcons(ev_replay(s1), tnil)); +∃ trace_red_cbu; +trace_red_cbu = tcons(ev_consult(s1), tcons(ev_upsert(s1), tnil)); +∃ trace_red_ops; +trace_red_ops = tcons(ev_consult(s1), tcons(ev_clear(s1), tcons(ev_replay(s1), tcons(ev_replay(s1), tnil)))); +∃ trace_red_cbp; +trace_red_cbp = tcons(ev_consult(s1), tcons(ev_clear(s1), tcons(ev_replay(s1), tcons(ev_publish(s1), tcons(ev_seal, tnil))))); +∃ trace_red_seal; +trace_red_seal = tcons(ev_consult(s1), tcons(ev_clear(s1), tcons(ev_replay(s1), tcons(ev_checkpoint, tcons(ev_seal, tnil))))); + +# Multi-attempt fixtures. green_resume: the B5 idempotent re-copy — a +# crash after attempt 1's replay unit and overlay upsert, an +# un-checkpointed resume that restarts the action from its root and +# re-runs the whole round (re-consult, re-clear, re-copy, re-upsert), +# publishes, checkpoints, seals. Every policy must answer "ok": the +# across-attempt second replay is exactly what once-per-scope's reset +# legalizes. +∃ trace_green_resume; +trace_green_resume = tcons(ev_consult(s1), tcons(ev_clear(s1), tcons(ev_replay(s1), tcons(ev_upsert(s1), tcons(ev_resume, tcons(ev_consult(s1), tcons(ev_clear(s1), tcons(ev_replay(s1), tcons(ev_upsert(s1), tcons(ev_publish(s1), tcons(ev_checkpoint, tcons(ev_seal, tnil)))))))))))); + +# red_ops_resume: a WITHIN-attempt duplicate copy after a resume must +# still violate once-per-scope — the reset is per boundary, not an +# amnesty. Satisfies the other six (no upserts, no seal). +∃ trace_red_ops_resume; +trace_red_ops_resume = tcons(ev_consult(s1), tcons(ev_replay(s1), tcons(ev_resume, tcons(ev_consult(s1), tcons(ev_replay(s1), tcons(ev_replay(s1), tnil)))))); + +# Tombstone fixtures. green_delete: one honest delta round — replay +# the base, overlay upsert, tombstone a departed row, publish the new +# validator, quiescent checkpoint, seal. Every policy answers "ok". +∃ trace_green_delete; +trace_green_delete = tcons(ev_consult(s1), tcons(ev_clear(s1), tcons(ev_replay(s1), tcons(ev_upsert(s1), tcons(ev_delete(s1), tcons(ev_publish(s1), tcons(ev_checkpoint, tcons(ev_seal, tnil)))))))); + +# red_cbu_del: an UN-GROUNDED tombstone — a delete with no clear this +# attempt (the un-regrounded-resume class, delete flavor). Violates +# exactly clear-before-upsert (no replay, no seal, no writes after a +# checkpoint). +∃ trace_red_cbu_del; +trace_red_cbu_del = tcons(ev_consult(s1), tcons(ev_delete(s1), tnil)); + +# red_cbp_del: a delete AFTER the last checkpoint, then seal — deletes +# are progress and dirty the watermark. Grounded (clear precedes), so +# only checkpoint-before-progress fires. +∃ trace_red_cbp_del; +trace_red_cbp_del = tcons(ev_consult(s1), tcons(ev_clear(s1), tcons(ev_replay(s1), tcons(ev_publish(s1), tcons(ev_checkpoint, tcons(ev_delete(s1), tcons(ev_seal, tnil))))))); + +# red_seal_del: a scope whose only round activity is a grounded delete +# must still publish before seal — a delete-only delta round that never +# publishes leaves the manifest vouching for a base the artifact no +# longer matches. Violates exactly seal-obligations. +∃ trace_red_seal_del; +trace_red_seal_del = tcons(ev_consult(s1), tcons(ev_clear(s1), tcons(ev_delete(s1), tcons(ev_checkpoint, tcons(ev_seal, tnil))))); + +# Session fixtures (policy 6). green_session: an honest round whose +# session write is checkpoint-committed BEFORE the crash — the re-run's +# read hit observes committed state. Every policy answers "ok". +∃ trace_green_session; +trace_green_session = tcons(ev_consult(s1), tcons(ev_clear(s1), tcons(ev_replay(s1), tcons(ev_upsert(s1), tcons(ev_swrite(k1), tcons(ev_publish(s1), tcons(ev_checkpoint, tcons(ev_resume, tcons(ev_sread_hit(k1), tcons(ev_seal, tnil)))))))))); + +# green_session_rollback: the CORRECT crash behavior for an +# UN-checkpointed write — checkpoint-consistent sessions roll it back, +# the re-run's read MISSES, and the at-least-once re-run re-derives +# (the user-stated form: "clearing writes after the last checkpoint, +# on crash — that sounds correct"). Every policy answers "ok"; the +# across-attempt re-copy is B5-legal per once-per-scope's reset. +∃ trace_green_session_rollback; +trace_green_session_rollback = tcons(ev_consult(s1), tcons(ev_clear(s1), tcons(ev_replay(s1), tcons(ev_upsert(s1), tcons(ev_swrite(k1), tcons(ev_resume, tcons(ev_sread_miss(k1), tcons(ev_consult(s1), tcons(ev_clear(s1), tcons(ev_replay(s1), tcons(ev_upsert(s1), tcons(ev_publish(s1), tcons(ev_checkpoint, tcons(ev_seal, tnil)))))))))))))); + +# red_session_zombie: SHIPPED semantics — the session write commits +# durably at op time, the crash rolls the cursor back but not the +# session, and the re-run reads the dead attempt's beyond-checkpoint +# value. Violates exactly session_ckpt_consistency (zombie direction). +∃ trace_red_session_zombie; +trace_red_session_zombie = tcons(ev_consult(s1), tcons(ev_clear(s1), tcons(ev_replay(s1), tcons(ev_upsert(s1), tcons(ev_swrite(k1), tcons(ev_resume, tcons(ev_sread_hit(k1), tnil))))))); + +# red_session_amnesia: the REJECTED wholesale resume-clear — a +# checkpoint-committed value is destroyed at the boundary and the +# re-run's read misses data whose producing work will not re-run. +# Violates exactly session_ckpt_consistency (amnesia direction). +∃ trace_red_session_amnesia; +trace_red_session_amnesia = tcons(ev_consult(s1), tcons(ev_clear(s1), tcons(ev_replay(s1), tcons(ev_upsert(s1), tcons(ev_swrite(k1), tcons(ev_publish(s1), tcons(ev_checkpoint, tcons(ev_resume, tcons(ev_sread_miss(k1), tnil))))))))); + +# External-principal fixtures (policy 7). green_ext: the capable-engine +# crash/shrink history — attempt 1 lists {p1, p2}, reconciles (nothing +# stale), copies both, checkpoints; the source shrinks to {p2} across +# the crash; attempt 2 re-lists, reconciliation deletes the dead +# attempt's p1, copies p2, seals. Every policy answers "ok". +∃ trace_green_ext; +trace_green_ext = tcons(ep_list, tcons(ep_live(p1), tcons(ep_live(p2), tcons(ep_recon, tcons(ep_copy(p1), tcons(ep_copy(p2), tcons(ev_checkpoint, tcons(ev_resume, tcons(ep_list, tcons(ep_live(p2), tcons(ep_recon, tcons(ep_copy(p2), tcons(ev_checkpoint, tcons(ev_seal, tnil)))))))))))))); + +# green_ext_carry: the phase COMPLETED before the crash and the +# resumed attempt seals without re-running it — the sealed answer is +# the completed run's answer, legitimately (sync-scoped freshness; the +# sync is not obligated to chase post-completion source changes). +# Every policy answers "ok". +∃ trace_green_ext_carry; +trace_green_ext_carry = tcons(ep_list, tcons(ep_live(p1), tcons(ep_live(p2), tcons(ep_recon, tcons(ep_copy(p1), tcons(ep_copy(p2), tcons(ev_checkpoint, tcons(ev_resume, tcons(ev_seal, tnil))))))))); + +# red_ext_norecon: the warn-and-continue degrade (non-deleting +# engine) — the resumed attempt lists the shrunk answer and copies it +# WITHOUT a completed reconciliation (no ep_recon after its ep_list), +# writing the current answer over the dead attempt's debris. Violates +# exactly external_principal_grounding (recon-before-copy direction). +∃ trace_red_ext_norecon; +trace_red_ext_norecon = tcons(ep_list, tcons(ep_live(p1), tcons(ep_live(p2), tcons(ep_recon, tcons(ep_copy(p1), tcons(ep_copy(p2), tcons(ev_checkpoint, tcons(ev_resume, tcons(ep_list, tcons(ep_live(p2), tcons(ep_copy(p2), tnil))))))))))); + +# red_ext_stale: a dead attempt's copy reaches the seal — the resumed +# attempt lists the shrunk answer but neither reconciles nor copies, +# and p1 (copied by attempt 1, absent from the last list) seals as +# debris. Violates exactly external_principal_grounding +# (stale-survivor direction). +∃ trace_red_ext_stale; +trace_red_ext_stale = tcons(ep_list, tcons(ep_live(p1), tcons(ep_live(p2), tcons(ep_recon, tcons(ep_copy(p1), tcons(ep_copy(p2), tcons(ev_checkpoint, tcons(ev_resume, tcons(ep_list, tcons(ep_live(p2), tcons(ev_seal, tnil))))))))))); diff --git a/formal/occult/tests/probe_axiom_fire.occult b/formal/occult/tests/probe_axiom_fire.occult new file mode 100644 index 000000000..4a19a3a36 --- /dev/null +++ b/formal/occult/tests/probe_axiom_fire.occult @@ -0,0 +1,20 @@ +# Probe: does a single-file axiom (typed universals + free constructors) +# fire as a rewrite under plain CLI evaluation, and does a ground == check +# reduce? Expected: assert -> true, exit 0. If the equation loads +# definitionally instead, app(fresh(7), rows(3)) stays residual and the +# run exits nonzero — that failure routes us to the axiomatic-require +# loading path instead. +# lint:disable mixed-source-semantics +syntax("standard"); + +∃ app; +∃ fresh; +∃ rows; + +x : FoldVal; +e : Epoch; + +app(fresh(e), x) = rows(e); + +assert(cond) = classify cond { {true,} : true, Bool : fail }; +assert(app(fresh(7), rows(3)) == rows(7)); diff --git a/formal/reviews/graph-spec-round1.md b/formal/reviews/graph-spec-round1.md new file mode 100644 index 000000000..2408fe578 --- /dev/null +++ b/formal/reviews/graph-spec-round1.md @@ -0,0 +1,943 @@ +# Round 1 — adversarial review of GRAPH_MODEL_SPEC v1 (deliverable 4 draft) + +Scope: the entire `formal/GRAPH_MODEL_SPEC.md` v1 DRAFT — ground rules +G-RULE-1..4, machines (§3), the node-execution hot path and supersession +(§4), the durability table (§5), variant axes and mutation toggles (§6), +properties P1–P4 carried plus P5/P6-E/P6-S and the bake-off metrics +(§7), budgets (§8), cells G1–G7 (§9), adequacy obligations (§10), and +the §11 review charge. Baseline anchors: `formal/MODEL_SPEC.md` v11 +FROZEN + MS-CO-001 (conventions, property pins, §9.6 design variants, +durability vocabulary), `formal/GLOSSARY.md` (pinned vocabulary), +`docs/tasks/sync-formal-model-brief.md` (charter, deliverable 4), +`docs/tasks/demand-graph-sync-brief.md` (settled decisions and design +requirements), `formal/walker/CALIBRATION.md` (decisions 19–24, the +6-atomic / 6-overlay / 6-overlay-naive / 6-overlay-last / 3-atomic +hand-off cells). + +Method: mechanical reachability walks of G1 (both crash placements plus +the `suppressionOff` mutant), G2 (all four axis legs plus both kills), +G4 (honest and mutant), G5 (all four sub-cells plus the sweep toggles), +and G7 under the draft's §3–§5 semantics — checking (a) reachability +without hand-placement, (b) that each declared expected verdict derives +from the declared mechanisms and the inherited §7 pins applied by hand, +and (c) that each kill/mutant leg flips for the stated reason; +independent re-derivation of the P1 fold, P2 consult pin, and P3′ +scoping over every walked history; a durability sweep of §5 against +crash/stop placements at each op boundary (two-crash histories +included, per the §8 budget); coherence sweep against MODEL_SPEC §7/§9.6 +pins (complete-rounds replacement counting, empty-fold attestation, +marker-inside-the-unit, clause (iii) suppression, the N4/F6 marker-race +boundary notes) and the glossary; charter-coverage check of deliverable +4's obligations against the §9 cell set. Vocabulary checked against the +glossary term by term. This review does NOT relitigate: P as the +language, the walker model's frozen verdicts, unit-mode +materialization's superiority over the naive/last placements (settled +by cells 6-atomic/6-overlay/6-overlay-naive/6-overlay-last), or the +demand-graph brief's settled decisions. Findings against the unit-mode +HAND-OFF below are findings about the graph-side adaptation of the +settled discipline, not about the walker verdicts. + +Verdict: **REJECT — 11 majors + 7 minors + 3 notes. F1 +re-review-required: its repair introduces new suppression/adoption +semantics that must come back for a targeted round 2 together with the +F2/F3 pins it interlocks with. All other findings are +fix-without-re-review.** No checker verdict from this model is citable +until the round-2 disposition. The draft's skeleton is sound — the +machine decomposition, the axis structure, the inherited discipline +(expected-verdicts-before-first-run, kill obligations, announce-only +monitors) all carry over correctly, and most cells walk. The majors +concentrate in exactly the seam the draft claims as its purpose: what +happens to walker-pinned machinery (the unit marker, the P1 counting +pins, P4's fingerprint) when generations, death, and a frontier +scheduler are placed under it. + +## Findings + +- **F1 (MAJOR, re-review-required) — the unit marker's suppression + scope is per-sync (inherited) but the graph is a generation world; + the collision flips scripted verdicts on G1 and G2 and is never + reconciled.** + - Claim under attack: §4's consult step ("suppressed if the unit + marker for this output key exists this sync"), adopted verbatim + from V-ATOMIC clause (iii); G1's "Expected GREEN on E and S … a + committed attempt-1 unit is superseded per §4"; G2's E+B and S + legs ("H's re-publish retracts G; G re-runs", "G's output + observably stale at seal → G re-run"). + - Evidence, walked. (i) G1, crash-after-unit-commit placement + (reachable in G1's config: crash placement is armed injection, + §1/§2; the scripted before-commit placement is one choice among + several): the unit commits {rows, entry V1, marker} durably; the + checkpoint predates it; resume finds S1 pending → G-RULE-3 bumps + to g2 and marks g1 DEAD; (n, g2) consults, the marker exists this + sync → suppressed; the seal contains g1's rows. Under variant S + those rows carry stamp {S1: g1} with g1 dead → P6-S as written + ("no sealed output carries a stamp containing a dead generation") + fires RED on the leg declared GREEN. The walker's 6-atomic sealed + the identical content green because the walker has no death + concept; the graph's death semantics condemn the exact content + the marker exists to keep. (ii) The same suppression defeats + variant E's retraction: in G2's E+B leg, G@g1's unit committed a + marker for G's output key; H's re-publish retracts G; G's forced + re-execution (n_G, g2) consults, finds the marker → suppressed → + never re-derives → the sealed artifact keeps the d1-embedding + rows → P6-E RED on the leg declared GREEN. Identically for the + S legs: the seal-time observation demands G re-run, the marker + refuses the re-derivation, and the staleness cannot be cleared + before seal. (iii) A marker-suppressed re-execution announces + NOTHING, so its demand (spawn tokens, session ops) is never + re-derived by a live generation — the closure and starvation + consequences are taken up in F7. + - Why it matters: this is the exact hazard the review charge was + told to hunt ("what does 'this sync' mean across generations?"). + The draft adopts unit-mode as settled — correctly — but the + settled pins were proven in a model with at-least-once redo and + NO death/retraction machinery. Marker suppression and generation + death now issue contradictory instructions on the same state + ("do not re-derive" vs "this content's producer is dead; + re-derive or sweep"), and three of the four G2 green legs plus + G1's committed-unit leg are underivable as written. + - Disposition (re-review-required): the repair must pick and pin a + reconciliation — candidates: (a) generation-adoption: a + marker-suppressed re-execution ADOPTS the marked unit's outputs + as its own (stamps rewritten to the live generation, emissions + re-announced from the store), making the dead round's content + live; or (b) generation-scoped markers: the marker suppresses + only within its producing generation's lifetime, and retraction/ + death clears it (at-least-once redo cost returns); or (c) death + redefinition: a generation whose prescribed work fully committed + is not marked dead by a restart (completion inferred from the + marker at resume). Each candidate changes §3/§4/§5 semantics and + the P5/P6 monitors' evidence base; each interacts with F2, F3, + and F7. This is new mechanism, not a wording pin — it requires a + targeted round-2 spot review before any G1/G2/G5d verdict is + citable. + +- **F2 (MAJOR) — the P6 property vocabulary is incomplete and + mechanism-referential: the E+A leg of G2 has no declared property + that can fire, P6-S reads the same stamps the mechanism computes + (so `stampMergeOff` cannot flip), and the S-variant's seal-time + observation step exists in no machine.** + - Claim under attack: G2's "E+A RED (P6-E has no edge to see … + the model exhibits the miss)"; §6's kill row "`stampMergeOff` … + kill cell G2-S"; §7 P6-S; §3's seal sequence ("seal when the + frontier drains: run the SWEEP … then `eSeal`"). + - Evidence: (i) §7 carries P1/P2/P3′/P4 and adds P5/P6-E/P6-S. The + walker's P6-A (ghost session stamps, final-derived-value form) is + NOT carried. P6-E is declared for "variant E + session B" + explicitly. So in the E+A leg no declared monitor can alarm: the + expected RED has no property attached — an implementer building + only the declared monitors seals E+A green and the headline + bake-off fact ("S makes the shipped session store safe; E + requires the session rework") loses its degenerate-leg exhibit. + (ii) `stampMergeOff` removes the read-side stamp merge. P6-S's + evidence IS the stamps: with the merge off, G's rows carry only + {G: g_live}, no dead generation appears in any sealed stamp, the + consistent-cut clause compares only comparable same-node + generations — P6-S is GREEN and the kill cannot flip. A monitor + that reads the mechanism's own bookkeeping cannot detect the + mechanism's mutation; this is a mutation-adequacy failure baked + into the property's definition, and §10.1 says exactly this + halts any recommendation. (iii) P6-S additionally alarms on + same-value re-derivation (H re-derives an identical d1; G's + rows carry the dead g1 stamp; clause 1 fires) — the walker + pinned d1→d2→d1 as non-alarming for P6-A, and P6-E carries the + counterfactual ghost discipline, but P6-S is value-blind with no + stated decision on whether that red is intended (forced-redo + conformance) or a false alarm. (iv) The mechanism half of the S + story — "observably stale at seal → G re-run" — is scheduler + machinery, and §3's dispatch loop has no observation step in the + seal sequence: as declared, seal is drain → sweep → `eSeal`, and + nothing re-runs anybody. Two honest readers diverge on every + S-leg verdict of G2. + - Why it matters: G2 is the calibration case the charter names + (case 2, session laundering) and the bake-off's headline cell; + as drafted its four legs rest on an unnamed property, an + unkillable kill, and an undeclared scheduler step. + - Disposition (fix-without-re-review, interlocks with F1): carry + the walker's P6-A ghost discipline forward as the LAUNDERING + ORACLE for all four legs — ghost dependency labels on emissions + (which session value, which writer generation, value identity), + violation = a sealed output embedding a value that differs from + the key's final live derived value — re-grounded on generations. + Demote lineage edges (E) and stamps (S) to MECHANISM whose job + is to make the oracle green; then `stampMergeOff` and + `retractionOff` both flip against the oracle, not against + themselves. Declare the S-variant's observation-driven re-run + rule in §3 (demand-derivation, session-read, and pre-seal + observation points as scheduler transitions), and pin the + value-blindness decision for P6-S's clause 1. + +- **F3 (MAJOR) — the walker P1 pins do not transfer ungrounded: + fold membership of a DEAD generation's complete round is unpinned + (two readings diverge on G1's committed-unit leg via the + empty-fold attestation pin), and G1's "superseded per §4" claim + is false for unit rounds (marker suppression preempts supersession + — supersession is unreachable for unit-mode keys).** + - Claim under attack: §7 "P1 binding integrity … all v11 pins, + carried unchanged in form … re-grounded on executions"; G1's + "a committed attempt-1 unit is superseded per §4". + - Evidence: take F1's history (unit commits, crash, generation + bump, marker-suppressed re-execution, seal). The only round in + S1's log belongs to the DEAD generation g1. Reading A ("fold + over complete rounds" quantifies over all executions' rounds): + the dead round folds replacement(e1), content matches, entry V1 + attests e1 — green, the 6-atomic analog. Reading B (the fold + quantifies over LIVE generations' rounds — the natural companion + to P5's "announced emissions of LIVE generations only"): the + complete-round set is EMPTY, the fold is the empty partition, + the sealed partition is non-empty → P1-CONTENT red, AND entry V1 + sits over an empty fold → the inherited v11 attestation- + over-empty-fold pin (round-7 F3, CALIBRATION decision 20) fires + outright. Both readings are honest; they diverge on a scripted + cell's verdict — the round-5 F1 / round-7 F3 divergence class + exactly. Separately: for unit-mode keys, dead rows durable ⟺ + the unit committed ⟺ the marker committed ⟺ the re-execution is + suppressed — so §4 supersession (a live commit replacing dead + rows) can NEVER fire for a unit round; G1's cell text attributes + its green to a mechanism that is unreachable in its own config. + Supersession's actual domain is the per-page record path (F6). + - Why it matters: replacement counting and empty-fold attestation + are the two v11 monitor pins the walker's freeze sweep + calibrated (decisions 19–20); importing them "unchanged in + form" into a world where complete rounds can belong to dead + executions leaves the flagship confirmation cell underivable. + - Disposition (fix-without-re-review, contingent on F1's repair): + pin fold membership and replacement-count legality with + generation grounding — recommended: a dead generation's + COMPLETE round remains in the fold and the count until + superseded (its content is what the artifact factually + contains; supersession removes both the rows and the round's + fold/count contribution), which keeps the marker-suppressed + seal green under adoption-style F1 repairs and keeps G1's + mutant-leg legality alarm intact; and correct G1's cell text + (marker suppression, not supersession, is the committed-unit + mechanism). Re-derive G1/G5 verdicts under the chosen pin. + +- **F4 (MAJOR) — generation bookkeeping is unsound across two + crashes: the bump-at-resume rule derives the new generation from + the restored checkpoint alone, so an un-checkpointed resume REUSES + a generation id and the dead set mis-derives — dead debris is + laundered as live.** + - Claim under attack: §5's generation-table row ("the LATEST + generation per pending node is in the frontier checkpoint; the + dead set is derivable (every generation below latest is dead); + no separate durable row"); §3's resume rule; G-RULE-3. + - Evidence, walked within the §8 budget (≤ 2 crashes): checkpoint + cp1 captures S1 pending @ g1. Crash 1 → resume bumps to g2 + (in-memory). Attempt 2 as (S1, g2) commits durable output — + record-path pages, or a session publish stamped {S1: g2} under + variant S. Crash 2 lands before any post-resume checkpoint + (checkpoint-or-skip is a genuine choice point). Resume 2 + restores cp1 AGAIN: latest = g1 → bumps to g2 — the SAME id. + Attempt 3's execution (S1, g2)′ is a different execution with + the same identity; attempt 2's g2-stamped durable debris is now + indistinguishable from attempt 3's live output; "every + generation below latest is dead" classifies attempt-2's debris + as LIVE. G-RULE-3's "an output's producing generation is + stamped at emission time and never reassigned" is violated in + effect (one stamp, two producers), P6-S cannot see the dead + debris (its generation reads as live), and E's supersession + cannot fire on it (no dead rows for the key). Whether §3's + "checkpoint placement is a choice point AS IN THE WALKER" + silently imports the walker's forced Init-checkpoint is exactly + the kind of unstated inheritance §2 forbids relying on — and + even a forced resume checkpoint only closes the hole if it is + pinned to commit BEFORE any resumed execution's first durable + emission. + - Why it matters: every death-keyed mechanism in the spec — the + dead set, purge, supersession, stamp validation, P5's live-only + closure — keys off generation identity; a reusable identity is + a silent-corruption channel of the class the calibration + protocol exists to catch, and it would be hardcoded away by any + encoding that happens to checkpoint eagerly. + - Disposition (fix-without-re-review): pin a forced resume + checkpoint that commits the bumped generation table before the + resumed attempt dispatches anything (ordering stated + explicitly), or make the bump derive from max(restored latest, + highest generation observed in durable stamps/store for the + node) + 1 with the scan rule declared. Add the two-crash + generation-reuse history as a required probe (expected + unreachable after the pin) — the §11 question 2 obligation made + executable. + +- **F5 (MAJOR) — variant E's lineage state does not survive the + crash seam as declared: admitted-by edges are in no durable row + (the purge is unimplementable post-restore), the support + rebuild-agreement check has no well-defined target when the crash + lost announcements the store already reflects, the scripted G5 + family cannot reach a state where the purge does anything, and + therefore the `purgeOff` kill cannot flip.** + - Claim under attack: G-RULE-4 and §5 (checkpoint contents: + pending nodes, completed-derivation set, closed-cut facts, + session index — no spawn/admitted-by edges); §5's + rebuild-agreement row ("rebuilt on resume … the rebuild + agreement check is a monitor obligation"); §3's resume ("Variant + E additionally purges the dead generation's spawn-subtree from + the frontier"); §6's kill row "`purgeOff` … kill cell G5-E crash + window"; G5d's E-leg ("E: purge + no re-demand → swept, GREEN"). + - Evidence: (i) the purge needs, post-restore, the admitted-by + edge (WITH the admitting generation) for every pending node; the + declared checkpoint carries none. G-RULE-4's own rule says + "state the checkpoint does not carry is a FINDING about the + variant" — this review so finds: E requires generation-qualified + admitted-by edges in the durable checkpoint, which is durable + state the mechanism tally must then count (F19). (ii) The + rebuild-agreement check's declared target is "the pre-crash + value"; in the standard window (unit commits, announce processed, + demand derived, THEN crash — no checkpoint between) the restored + frontier predates the derivations the pre-crash support included, + so rebuild ≠ pre-crash value on legal histories and the check + false-alarms; the only implementable target is the + checkpoint-consistent value, which must be said. (iii) + Reachability: for S1's unit-mode single-commit rounds, demand + derivation (C's admission) and S1's completion enter scheduler + state from the SAME announce, so no checkpoint can hold + C-pending ∧ S1-pending; either the checkpoint predates the + announce (C absent from the restored frontier — the purge has + nothing to purge; the SWEEP alone produces G5d's E-leg green) or + it postdates it (S1 completed — no re-execution, no premise). + The purge's real domain is a paginated parent that spawns + mid-round and then restarts — a shape no §9 cell scripts. (iv) + Consequently `purgeOff` cannot flip in the tabled kill cell: in + every reachable G5 history the honest sweep independently drops + the dead-admitted partition, so the mutant leg seals the same + artifact and stays green — a tabled kill that cannot fire, which + §10.1 makes a recommendation-halting defect. + - Why it matters: E's entire mechanism story (purge + derived + support + retraction) is one of the two bake-off contestants; + as drafted its crash behavior is unimplementable from declared + durable state, unprobed by any reachable cell, and its + signature toggle is unkillable — the bake-off would compare S + against an E whose machinery never ran under fire. + - Disposition (fix-without-re-review): add generation-qualified + admitted-by edges to G-RULE-4/§5 (and to the frozen mechanism + tally); pin the rebuild-agreement target as + checkpoint-consistent; re-script the G5-E crash window (and the + `purgeOff` kill) on a paginated record-path parent whose child + admission IS checkpointed while the parent remains pending — + that premise makes the purge load-bearing (with `purgeOff`, the + stale child executes and its redo/closure effects are + observable) — and decide/declare whether the purge also kills + COMPLETED descendants or only pending ones (the two readings + give different redo counts and different G6 outcomes; see F9). + Also correct G5's premise wording — C is fetch-fresh in a + first-sync config and commits a record round, not a "unit". + +- **F6 (MAJOR) — supersession (§4) is not total: the two-live- + derivations-one-output-key interaction that G-RULE-2 explicitly + legalizes is undefined (and alarms P1 legality on legal behavior), + the overlay-intent record path composes dead debris with live rows + (the "structural under E" claim is false to the declared + mechanisms), G4's no-residual-job expectation overreaches, and + G5b's probe is not the strongest premise — with no dedicated + supersession cell anywhere.** + - Claim under attack: G-RULE-2's parenthetical ("two distinct + derivations targeting one output key must both run; their store + interaction is supersession, §4"); §4's supersession definition + ("when execution (n, g′) commits … and a DEAD generation's rows + for K already sit in the current partition, the commit REPLACES + them … under variant E the sweep plus supersession make it + structural"); G4's declared expectation ("NO residual job"); + §1's "fresh-artifact supersession" modeling claim. + - Evidence: (i) §4 defines replacement only against a DEAD + generation's rows. Two distinct derivation hashes with one + output key are both LIVE; G-RULE-2 sends the reader to §4, + which is silent. Compose (union — the partition invariant's + double-stamp poison shape, per the demand-graph brief's silent + partition-poisoning warning) or last-commit-wins (silent loss of + a live derivation's output)? Two honest implementations diverge + — and both commit two complete unit rounds with two copies for + one scope in one sync, so the inherited P1 legality rule alarms + on behavior the spec declares legal. (ii) The 2-worker marker + race (walker N4/round-7 F6, carried over by §4's own boundary + citation) makes this reachable with suppression ON: the two + nodes' consults both pass the absent-marker check and both + commit units. G4's declared expectation — scope locks have NO + residual job — is therefore overreached: it holds for same-hash + duplicates (suppression) but is refuted by the same-key + distinct-hash shape, which no cell scripts. (iii) Record path: + "clear only on REPLACES semantics" means an OVERLAY-intent + fetch-fresh re-run commits over a dead generation's partial + record debris without clearing — dead rows compose with live + rows in one partition, falsifying §4's "never compose … + structural" claim for E. The composition IS detected (the + inherited fresh-replaces fold makes it a P1-CONTENT alarm when + epochs differ — verified by hand), but detected-by-alarm is + precisely not "structural", and when upstream does NOT move + between the attempts the debris is content-identical and the + claim is untested vacuously. Under S the same history is caught + only by P6-S's dead-stamp clause — same-node generations are + causally COMPARABLE, so the consistent-cut clause passes it; + one more reason F2's oracle grounding matters. (iv) G5b probes + only sweep-OFF-with-no-re-demand; the §11 question 5 asks + whether that is the strongest premise — it is not: the + overlay-intent re-demand history above defeats BOTH mechanisms + with the sweep ON. + - Why it matters: supersession is the graph's replacement for the + walker's replay-blocked/supersede machinery and one third of + the three-invariant framing's consume story; an interaction + matrix with undefined and self-contradicting rows would be + silently resolved by whatever the encoding happens to do. + - Disposition (fix-without-re-review): define the supersession + matrix totally over {dead vs live} × {unit vs record} × + {REPLACES vs OVERLAY intent} in §4, including the two-live- + derivations-one-key rule (recommendation: make same-key + distinct-derivation admission a declared connector-contract + violation surfaced as poison, per the partition invariant, OR + pin last-commit-wins with a legality exemption — either way the + P1 counting pin must be updated in the same edit, F3); add a + dedicated supersession cell family: (a) record-path dead-debris + re-run under both intents (expected: REPLACES green, + OVERLAY-intent alarm — the §4 claim corrected to + "detected, not structural"), (b) the same-key distinct-hash + 2-worker race; retitle G4's expectation accordingly. + +- **F7 (MAJOR) — P3′ is cited under the walker pin name but + described as the full walk-equivalence property the walker + explicitly did not build; under the implementable reading, demand + starvation seals green under EVERY declared property — the P5 + ghost closure is circular (monitor and scheduler consume the same + announce stream), no property owns demand-closure completeness, + and no toggle kills under-admission.** + - Claim under attack: §7's "P3′ smear-equivalence (complete + crash-free walk equivalence)"; P5's ghost-closure definition + ("computed by the monitor from announced emissions of LIVE + generations only"); §6's kill table (no under-admission mutant); + the absence of any 5b-style scripted seal-content oracle. + - Evidence: (i) MODEL_SPEC's P3′ is per-scope epoch coherence, + doubly scoped — the full "equal to some crash-free walk" + quantifier was deliberately left as future work because it is an + ∃-over-walks obligation a P monitor cannot discharge. The draft + reuses the name with the stronger description; an implementer + building the walker form checks something materially weaker than + the words. (ii) Under the walker form, walk the starvation + history: S1's spawn announce is processed, C is admitted but no + checkpoint captures it; crash; resume re-runs S1, whose + re-execution is marker-suppressed (F1) and announces nothing; C + is never re-demanded; C never runs. Seal: P1 green for C (no + round, no partition, no entry — empty equals empty), P2 green + (quantifies over rows present), P3′-as-walker green (no manifest + scope), P5 green — the monitor's ghost closure, built from the + same live announces the scheduler consumed, ALSO excludes C. + The artifact silently lost a subtree and every monitor agrees. + P5 as declared can catch a sweep that disobeys the closure, but + never a closure that is wrong — the oracle validates the + scheduler against itself. (iii) Generation death itself is not + an announce event; the monitors' "LIVE generations only" and + "dead generation" predicates are implementable only via an + inference rule (any announce from (n, g′) kills all (n, g < g′)) + that the spec never states — and F1's suppressed restarts emit + no announce, so even the inference goes blind exactly where it + is needed. (iv) The kill table's coverage is asymmetric: + `suppressionOff` kills over-execution, nothing kills + under-admission (a demand-derivation rule that drops tokens), + so §10.2's reachability discipline has no mutation-side + counterpart for the graph's single most load-bearing new + mechanism. + - Why it matters: the walker's 5b taught this exact lesson — a + silent dropout green under every property, whose only executable + oracle was the scripted seal-state expectation — and the graph + spec inherited the lesson's vocabulary but not its oracle. + - Disposition (fix-without-re-review): pin P3′'s actual checkable + form (the walker form, honestly named, with the full-equivalence + form recorded as not built); give P5's ghost closure an + independent evidence base — the MEnv/MUpstream-side + counterfactual closure (which children an honest uninterrupted + walk at the scripted epochs would demand — computable per the + P6-R counterfactual precedent, since policies are deterministic + and content tables are scripted) — or, minimally, a scripted + `SealExpect` oracle per cell in the 5b style; state the + death-inference rule; add a `demandDropOff` (or equivalent + under-admission) mutant with a kill cell. + +- **F8 (MAJOR) — the cross-variant P3′ claim in G5d is not + well-defined as a checker-checkable property: it compares sealed + artifacts ACROSS two runs (two scheduler modes), which no single- + run P monitor can express, and as a general claim it is false- + by-design under smear (the variants may legally seal different, + both-P3′-equivalent artifacts).** + - Claim under attack: G5d's "Both legs must converge to the SAME + sealed artifact (P3′ smear-equivalence across variants — the + strongest cross-variant claim; declare it and let the checker + try to break it)"; §10.6. + - Evidence: the lineage axis is a per-run configuration; a P + monitor observes one run. "E's seal equals S's seal" quantifies + over pairs of runs from different configs — there is no monitor + to fire, so "let the checker try to break it" has no executable + meaning, and §10.6's "if it fails" has no failure event. Deeper: + P3′-equivalence is membership in a SET of legal artifacts + (smear admits many); two correct variants may select different + members whenever their redo behavior differs — which is the + bake-off's own premise (S re-runs what observation finds stale; + E purges and re-derives). In G5d's tight config the legal set + is plausibly a singleton and the claim holds trivially; in any + config where it is not, the claim is false without either + variant being wrong. The strongest cross-variant statement the + charter needs is: each leg satisfies P3′ per-run, and the + enumerated seal-artifact SETS (a meta-comparison over the two + runs' reachable seals, produced by the harness, not a monitor) + either coincide or the recommendation states which artifact is + right — which is what §10.6 already gropes toward. + - Why it matters: the strongest declared cross-variant claim in + the spec is currently unfalsifiable, the precise pathology + ("a badly built model passes its checker") the charter's + execution notes warn against. + - Disposition (fix-without-re-review): reformulate G5d as (a) + per-leg P3′ monitors as usual, plus (b) a harness-level + artifact-set comparison recorded in the bake-off table, with + the divergence rule of §10.6 attached to (b) and honestly + labeled a meta-analysis, not a property. + +- **F9 (MAJOR) — the G6 bake-off scripts cannot distinguish the + variants: both scripted interruptions land where E and S redo + identical work by construction, the §8 envelope contains no + history where purge-vs-stamps can diverge (no depth, no fan-in), + and variant S's refusal rule is operationally unpinned, so the + redo metric is ill-defined — the arbitration deliverable fails as + scripted.** + - Claim under attack: G6's two scripts (stop after S1's unit + before S2 executes; crash during S2's execution) and its + declared falsifiable expectation ("E re-executes MORE under + deep spawn trees"); §8's envelope; §3's variant-S resume rule + ("re-derived or refused at the demand-derivation observation + point"); §11 questions 6 and 10. + - Evidence: (i) the crash script interrupts S2 — a LEAF. Purge of + a leaf's spawn-subtree purges nothing; S re-observes nothing + (no descendants carry S2's dead generation). Both variants + re-execute exactly S2 once. The stop script interrupts before + S2 ever runs — the forced checkpoint is current and total, and + resume re-executes S2 once on both variants. Neither script + produces a between-variants delta; the declared expectation is + not merely undecided but UNDECIDABLE by these cells, and a + bake-off table built from them would report a tie manufactured + by script choice — the fairness failure mode of §11 Q6, in the + opposite direction from the one the question anticipates. (ii) + The divergence cases need a parent restarting OVER completed + descendants (purge discards C's completed work vs stamps keep + or force-redo it) and support fan-in (a child demanded by two + parents; a row supported twice — the refcount and transitive- + retraction machinery of E, and the invalidate-everything + degeneration the session brief worries about, never execute in + a 1-child, depth-1, no-fan-in envelope). §8 has no such + configuration; Q10's answer is therefore NO for the bake-off + cells (the envelope is adequate for the safety cells G1–G5). + (iii) Whether E's purge discards completed descendants (F5's + open reading) and when S "refuses" a dead-admitted pending node + (before or after it executes — the draft's "re-derived or + refused" names no rule and no timing) each swing the + executions-per-node metric; a metric that depends on unpinned + scheduler choices cannot arbitrate anything. + - Why it matters: the charter says this model exists to arbitrate + edges-vs-stamps BY CHECKER OUTPUT; G6 is the redo-work axis of + that arbitration. + - Disposition (fix-without-re-review): add the divergence scripts + — (a) crash while a mid-tree parent with ≥ 1 completed, + checkpointed descendant restarts, identical re-derivation; (b) + the same with a changed re-derivation; (c) a fan-in config (one + child demanded by two live parents; one session publish read by + two readers) — and add the needed envelope rows (depth 2, one + fan-in edge) as an explicit, justified widening of the + inductive bet; pin S's refusal timing and E's purge domain + (F5) first; keep the current two scripts as the + control (expected delta ≈ 0, now an honest calibration point + rather than the whole axis). + +- **F10 (MAJOR) — the stampCompression admissibility obligation has + no cell: the axis is declared in §6, the safety-unchanged claim is + stated in §7, §10 hangs a refutation condition on it — and no §9 + cell declares the axis, exercises `bucketed`, or could refute the + claim; every §9 cell also violates §6's "every cell declares all + three" rule outright.** + - Claim under attack: §6's axis table and its preamble; §7 P6-S's + "Under `stampCompression = bucketed` the SAFETY claim must hold + unchanged and only REDO WORK may grow … a cell where compression + changes any sealed content refutes the admissibility claim"; + §9's cell set. + - Evidence: verified by exhaustive read — G1 through G7 nowhere + mention stampCompression; no cell declares a value for it (nor, + for that matter, for the session axis outside G2 — see F21); + §10.1 covers toggles, and stampCompression is an axis, so it + escapes the kill table too. The admissibility of lossy + compression is not a nicety: it is the charter's stated reason + variant S's mechanism cost is bounded ("error direction is + redone work, never wrong data") and therefore a load-bearing + input to the bake-off's mechanism-count and redo axes. An + unexercised admissibility claim would enter the recommendation + as prose — exactly what §10.5 forbids. + - Why it matters: missing kill/coverage obligation for + load-bearing new machinery, per the severity definition. + - Disposition (fix-without-re-review): add the compression cell + family — re-run G2's S legs and the G5 family under + `stampCompression = bucketed`, expected: every safety verdict + unchanged, redo count recorded (growth permitted and expected — + bucketing makes unrelated nodes' deaths look observable); the + refutation condition of §7 becomes those cells' declared + mismatch criterion. State in §6 which axis values every §9 cell + runs by default. + +- **F11 (MAJOR) — the P4 tranche does not transfer as claimed: + the walker's stuck-detection fingerprint (byte-identical restored + checkpoint state) can NEVER fire under generation bumping, so + G7's stuck leg flips green as written; attempt-failure semantics + for a frontier scheduler are undeclared; and §8 has no attempt + budget for the ladder to consume.** + - Claim under attack: G7's "expected: without an abandon rule the + re-fail loop is P4-STUCK RED (same shape as the walker's)"; §7's + "P4 progress (no stuck re-failure; the walker's ladder analog if + a scenario needs one)"; §8. + - Evidence: the walker's livelock detection (MODEL_SPEC §7 P4, + CALIBRATION decision 17) fires on two consecutive failures from + byte-identical restored checkpoint state. In the graph, every + failure-forced checkpoint captures the failing node at a HIGHER + generation than the last (the bump is the resume rule), so no + two restored states are byte-identical and the detector is + structurally silent — the generation counter is a Zeno counter + under the fingerprint. Reader A (byte-identical, as inherited): + G7's stuck leg seals... never — the monitor simply doesn't fire + and the leg is GREEN, flipping the declared RED. Reader B + (fingerprint modulo generations): RED as declared. Whether a + failure forces a checkpoint at all, and indeed what an "attempt + failure" IS under a frontier scheduler (does one node's loud + failure fail the attempt walker-style, or does the frontier + route around it and the node alone re-fails?), is undeclared in + §3 — the walker's decision-16 machinery has no graph analog in + the draft. And §8's interruption budget ("≤ 2 crashes or 1 stop + per cell") has no attempt-failure/resume-ladder row, while G7 + needs at least three attempts for k = 2 detection. + - Why it matters: G7's whole point is that the P4 findings carry + over "rather than dissolving"; as drafted the detector + dissolves and the cell cannot show it. + - Disposition (fix-without-re-review): pin the stuck fingerprint + as generation-blind (failure point, reason, restored state + modulo the generation table); declare attempt-failure semantics + for MGraphScheduler (recommended: node-loud-failure fails the + attempt, walker-parity, so the ladder analog is meaningful); + add the attempt-budget row (≤ 3 attempts, walker parity). + +- **F12 (MINOR) — demand-derivation timing is unpinned.** §3's + dispatch loop reads batch-shaped ("collect its announced + emissions, derive demand") while §4 announces emissions at commit; + whether the scheduler derives demand per-announce (incrementally, + atomic with completion bookkeeping) or after execution completion + determines which checkpoint contents are reachable — F5(iii)'s + walk turned on it. One sentence pins it (recommended: + per-announce, atomic with the announce's completion effects, which + is what makes the F5 reachability analysis stable). + Fix-without-re-review. + +- **F13 (MINOR) — "completed-derivation set" is admission-keyed in + G-RULE-2 but completion-named everywhere else, and the resume-side + completed-vs-never-admitted distinction is never stated.** G-RULE-2 + suppresses on "already ADMITTED this sync"; §3/G-RULE-4 call the + same state the "completed-derivation set". A resume must + distinguish a node absent-from-pending-because-completed from + never-admitted, presumably via admitted ∧ ¬pending — but that rule + is nowhere written, and it is load-bearing for every restart walk + in this review. Rename to admitted-derivation set (or split the + two sets) and state the resume rule. Fix-without-re-review. + +- **F14 (MINOR) — record-path clearing and the intent enum arrive in + a parenthesis, not a declaration.** §4's "(shipped record path; + clear only on REPLACES semantics)" quietly imports the deliverable-3 + wire-intent enum as BUILT graph semantics and adds a record-path + clear the walker's §4 never had, without §3 op-vocabulary + registration (which §5's crash protocol and the arrival-order choice + points quantify over) and without pinning clear placement + (first page of the round only, presumably). This is the round-6 / + MS-CO-001 registration lesson verbatim. Register the ops and + intents in §3, pin clear placement, cross-reference from §4. + Fix-without-re-review. + +- **F15 (MINOR) — the aggregate node X exists in the budget and in + no cell.** §1 promises X "executes only in configurations where + its precondition is scripted true"; §8 budgets it; no §9 cell + contains such a configuration, so the promise is about an empty + set and Q8's honest-labeling check has nothing to inspect. Either + add the scripted-precondition configuration (X executes last, + P1–P5 checked around it — cheap, and it gives deliverable 5 its + hand-off) or strike X from v1's budget and state the deferral. + Fix-without-re-review. + +- **F16 (MINOR) — cross-sync stamp scoping is undefined for variant + S.** Generations are per-sync restart counters; G1's sync-2 replay + copies rows whose stamps reference sync-1 generations, which the + current sync's generation table cannot classify (ill-typed dead-set + lookups; a refuse-on-unknown reading would false-alarm every warm + replay under S). Pin the rule — recommended: replay re-stamps + copied rows with the replaying execution's stamp (the sealed + source artifact was observation-clean by its own seal), with an + explicit note that cross-sync session-stamp travel remains the + walker P6-R machinery's jurisdiction. Fix-without-re-review. + +- **F17 (MINOR) — the sessions × replay product (the walker's + scenario-7 class) is neither covered nor excluded.** The graph + model replays inside node executions, so replay elision of session + writes is structural here too; the draft's §1 abstraction list is + silent on the product, and silence is how the walker model lost it + the first time (round-6 lesson). Either declare it out of scope + with the argument (taint machinery is verdict-side and orthogonal + to the lineage axis; the walker cells own it) or script the 7a/7b + analogs on the graph runtime. Fix-without-re-review. + +- **F18 (MINOR) — P6-E's retraction-liveness form does not bind the + re-run to the live value.** "Every reader execution that consumed + the dead value must re-run before seal" is satisfied by a re-run + that itself read the stale KV value (reachable in G2's crash + config: G's crash-forced re-run reads d1 from the durable KV + before H re-publishes) — the re-run happened, the sealed output + still embeds d1. The mechanism plausibly closes this (retraction + keyed on the value/key, retracting readers of any dead value on + re-publish), and F2's final-value oracle closes it at the monitor + — but the retraction rule's keying must be pinned so the fan-in + quantification of Q7 is over reader-executions-of-dead-values, + not reader-nodes. Fix-without-re-review, subsumed if F2's oracle + disposition is taken. + +- **F19 (NOTE) — the frozen mechanism tally must absorb this + round's machinery before the cells run.** F5 adds durable + admitted-by edges to E's column; F2 adds the scheduler-side + observation/re-run step to S's column (it was implicitly free); + F6's supersession matrix belongs to the shared column. The tally + is the pre-hoc fairness device — freeze it in the spec revision, + not in the calibration report after the numbers exist. + +- **F20 (NOTE) — the recommendation's decision rule is unpinned.** + §10.5 fixes the table's provenance but not the procedure: what + wins when properties tie, mechanisms differ by definition-sensitive + counts, and redo work splits by script family. Record the + arbitration rule (and where the written recommendation lives — + the CALIBRATION.md analog for `formal/graph/`) before the first + bake-off run, for the same reason the tally is frozen. + +- **F21 (NOTE) — the session axis is vacuous outside G2; say so + once.** Only G2 declares session machinery (H and G exist "scenario + G2 only"), so §6's every-cell-declares-all-three rule is + unsatisfiable as written for the session axis in six of seven + scenarios. One sentence ("cells without session actors run + variant A, axis vacuous") plus F10's default declaration makes §6 + total. + +## Mechanical walks (the §10.2 obligation, executed for this review) + +- **G1.** Crash-before-unit-commit leg: reachable (armed injection at + the pre-commit boundary; nothing durable for S1; resume bumps, + re-consults, revalidation vs epoch 2 fails, fetch-fresh lands + clean); GREEN derives on both variants — confirmed as scripted. + Crash-after-unit-commit placement (same config, different armed + position): verdict underivable — F1 (marker suppresses the live + generation; P6-S fires on the dead stamp under S) and F3 (fold + membership of the dead round; "superseded per §4" false — the + supersession mechanism is unreachable for unit keys). Mutant + (`suppressionOff`): the duplicate-admission race schedule (both + consults pass the absent-marker check before either unit commits — + the inherited N4/F6 window) reaches the double unit; last unit + coherent; legality-only alarm derives as declared; the sequential + schedule is marker-suppressed and green, which is consistent with + a first-find RED. Flip confirmed for the stated reason. +- **G2 (all four legs).** Premise reachable in all legs: H's session + write is a store op committed mid-execution (durable KV), a + nondet checkpoint captures G completed ∧ H pending, crash, H alone + restarts — no hand-placement. E+A: the miss is real, but no + declared property fires — F2; the leg's RED is currently + property-less. E+B: retraction → G re-run is defeated by G's own + unit marker — F1; GREEN underivable as written. `retractionOff` + kill: flips RED via the sealed-d1 witness, PROVIDED the monitor is + grounded per F2 (as drafted the witness form works; confirmed). + S+A and S+B: the merge → dead-stamp → re-run story requires a + seal-time observation step absent from §3 (F2) and the re-run is + marker-suppressed (F1); GREEN underivable as written. + `stampMergeOff` kill: CANNOT flip — the monitor's evidence is the + stamps the mutant removes (F2). The headline bake-off fact + survives IN DIRECTION under the F1+F2 repairs (nothing found + contradicts S-makes-A-safe once the observation step exists and + the marker is generation-aware), but no leg of it is currently + derivable. +- **G4.** Honest leg: root emits S1's token twice in one announce; + second admission suppressed by derivation hash; single unit; + GREEN derives. Mutant: double admission → two workers → both + consults pass the absent-marker window → double unit → P1 + legality alarm; RED derives via the race schedule for the stated + reason (the sequential schedule is suppressed by the first unit's + marker and stays green — worth stating in the cell so the + first-find expectation is explicit). Kill flip confirmed. The + cell's FURTHER claim — no residual job for scope locks — is + refuted by the distinct-derivation same-key shape the cell does + not script: F6. +- **G5a.** Reachable via the checkpoint-predates-S1's-announce + window (F5(iii)): restored frontier holds S1 pending, C absent; C's + committed partition is orphan debris; S1 re-runs at epoch 2, no + child marker; honest sweep drops C's partition (outside the + live-announce closure). GREEN derives on both variants — via the + SWEEP alone; purge contributes nothing in any reachable schedule + (F5). Note C's commit is a record round, not a "unit" (F5 + wording). +- **G5b.** `sweepOff`: C's dead partition seals; P5-UNDER RED + derives on both variants (S additionally alarms P6-S on the dead + stamp — multiple alarms, direction consistent). The E-leg + candidate-hole probe is honest as far as it goes: no re-demand of + K_C means supersession cannot fire and the leg cannot be green. + But it is not the strongest premise — F6(iv)'s overlay-intent + re-demand history defeats sweep and supersession with the sweep + ON. Kill flip confirmed; probe strength finding stands. +- **G5c.** `sweepOverreach`: S2's in-closure partition dropped; + P5-OVER RED and P1-CONTENT (fold has rows the partition lost) + both derive. Confirmed as scripted. +- **G5d.** E-leg premise as scripted (purge removes C from the + restored frontier) is UNREACHABLE — no checkpoint can hold + C-pending ∧ S1-pending for a single-unit parent (F5(iii)); the + green that derives comes from the sweep, so the cell's mechanism + attribution is wrong even though its verdict direction survives. + S-leg: derives as scripted (C's rows carry the dead S1 stamp; + outside the closure; swept) modulo F1's death/announce blindness. + The cross-variant same-artifact claim: not checkable as declared + — F8. +- **G7.** Premise requires attempt-failure machinery (§3 has none — + F11); under the inherited byte-identical fingerprint the stuck + detector never fires across generation-bumped checkpoints, so the + declared RED does not derive (F11). Ladder leg contingent on the + same pins plus the missing attempt-budget row. Direction + plausible after the F11 pins; underivable as written. + +## Answers to the §11 review charge (ten questions) + +1. **G-RULE-1 enforcement**: CLEAN. Every scripted demand source in + G1–G7 is an announced emission (spawn tokens, the child marker + row, session values); mutation timing is env-side state, not + structure; no script routes structure through response-loop + position. Residual: the derivation TIMING ambiguity (F12) is a + §3 gap, not a purity leak. +2. **Generation/death under two crashes**: FINDING — F4 (generation + id reuse from un-checkpointed resumes; dead-set mis-derivation). + The during-checkpoint-commit half is clean (eCheckpoint is one + atomic op; either token wins wholly). +3. **E's derived-support rebuild well-definedness**: FINDING — F5 + (no well-defined target under the announce-lost/store-reflects + gap; admitted-by edges missing from the durable checkpoint + besides). +4. **S stamp durability leaks**: QUALIFIED CLEAN. The §5 row is + internally coherent — no path drops or narrows a stamp while + keeping its output (a lost un-checkpointed spawn token loses the + whole output, which is consistent). The leaks found are + evidentiary, not durability: death is not announce-visible + (F7(iii)) and cross-sync stamps are unscoped (F16). +5. **Supersession sufficiency / two-key interleavings**: FINDING — + F6. The two-live-derivations-one-key row is undefined; the + overlay-intent record path composes dead with live under E with + the sweep on; G5b's probe premise is not the strongest. +6. **G6 script fairness**: FINDING — F9. The scripts do not + pre-decide the bake-off; worse, they cannot decide it (identical + redo by construction on both scripted interruptions). +7. **P6-E under fan-in**: the per-reader quantification is correct + as written (one non-re-run reader among two is a witness), but + the form does not bind re-runs to the live value — F18 (and the + envelope contains no fan-in to exercise the question — F9). +8. **Aggregate node honesty**: no vacuous P7 is smuggled (no cell + claims or approaches P7), but the honesty check is vacuous for a + different reason: no configuration containing X exists — F15. +9. **P1 pins transfer**: FINDING — F3. Complete-rounds counting and + the empty-fold attestation pin need generation grounding before + they are well-defined over logs containing dead generations' + rounds; debris-surfaces-through-content does survive supersession + on the record path (verified by hand in F6's walk — the + fresh-replaces fold alarms on dead∪live composition when epochs + differ), which is the one clean transfer in this family. +10. **Small-scope adequacy**: SPLIT. Adequate for the safety cells + (G1–G5, G7 — every walked premise fits the envelope). NOT + adequate for the bake-off: no depth, no fan-in means + purge-vs-support cannot diverge from stamps anywhere in the + envelope, E's refcount/transitive-retraction machinery never + executes, and the session brief's invalidate-everything + degeneration is unreachable — FINDING, folded into F9. + +## Charter-coverage check (deliverable 4 obligations vs the cell set) + +Present and correctly shaped: frontier/suppression/generations/sweep +(G4, G5), both lineage variants as a shared-mechanism axis (per the +charter's "implement both and compare"), session variants A/B, +calibration case 2 re-run (G2), case 1/3/4 premise re-runs (G1, G3, +G4), P5 in both failure directions, redo/mechanism/property as the +declared comparison axes, refutation-is-success framing. Missing or +defective: the stampCompression admissibility cell (F10), a dedicated +fresh-artifact supersession cell family (F6 — §1 lists supersession as +modeled; no cell exercises it, and for unit keys it is unreachable), +bake-off scripts that can discriminate (F9), the recommendation's +decision rule and destination (F20), and the E-variant's durable-state +honesty in the mechanism tally (F19). The "expect the model to force +design decisions" clause is discharged early: F1's marker/generation +reconciliation IS such a forced decision, surfaced by review rather +than by checker — consistent with the charter's purpose, and the +reason the re-review is targeted rather than resented. + +## Verified clean (for the draft's revision record) + +- The no-relitigation boundary is respected: unit-mode is adopted + without rebuilding the naive/last placements, the walker cells are + cited as settled hand-offs exactly as CALIBRATION.md's pending note + designed, and nothing in the draft re-opens P-as-language or the + frozen verdicts. The findings above concern the graph-side + adaptation, not the hand-off itself. +- §2's inheritance is genuine: arrival order, armed crash injection, + truthful validators, announce-only monitors, and + expected-verdicts-before-first-run all carry with correct graph + additions; G-RULE-1's monitor discipline (ghost closure from + announces only) is the right instinct even where F7 shows it needs + an independent evidence base. +- G3 walks clean end to end: stop-forced checkpoint captures S1 at + its consult-granularity cursor, resume bumps the stopped + generation, the re-consult hits the ACTUALLY current (swapped) base + and fails validation → fetch-fresh; the 3-atomic closure carries + over; cheap confirmation cell as declared. +- G5b and G5c derive exactly as scripted; the P5-UNDER/P5-OVER split + with separately named directions is well-formed and both kill + flips are real (modulo nothing — these two work as written). +- The walker→graph mapping table (§5) is accurate: restart-from-root + ↦ generation bump, hit-map ↦ nothing (in-unit marker as consult + provenance, matching the pilots), EnqueuePageTokens ↦ demand-derived + admission, NextPageToken ↦ same-node cursor advance. +- P2's consult pin transfers cleanly: a marker-suppressed seal's + scope was consulted (validation match) by the dead generation + within the same sync, which qualifies under the any-attempt + wording; staleness hops compute correctly on replayed rows. +- Torn rounds are unreachable model-wide (a round is one execution's + ops; executions never span attempt boundaries), and the draft + keeps the monitor active anyway — the inherited discipline applied + correctly, and stronger than the walker (where only unit-mode + scopes had the by-construction guarantee). +- Vocabulary: glossary conformance is exact everywhere it was + checked (node/execution/generation/derivation hash/output key/ + demand closure/sweep/supersession/causal stamp/consistent cut/ + sealed cut; session variant definitions; the three-invariant + framing). The single drift found is "completed-derivation set" + (F13) — a draft-internal term, not a glossary term. +- Budget shape (§8) inherits the envelope honestly and restates the + inductive bet; the G2-only session actors and the scripted-config- + only aggregate node are correctly fenced (their gaps — F15, F21 — + are registration, not smuggling). +- Public-repo hygiene: clean. No customer names, tenant identifiers, + or internal infrastructure anywhere in the draft. + +## Summary + +| finding | severity | one line | disposition | +|---|---|---|---| +| F1 | MAJOR | per-sync unit-marker suppression vs generation death/retraction is unreconciled; flips G1's committed-unit leg and three G2 legs; the imported walker assumption the charge predicted | pick and pin adoption/scoped-marker/death-redefinition semantics; **re-review-required** (targeted round 2) | +| F2 | MAJOR | P6 vocabulary incomplete: G2 E+A red has no property; P6-S is mechanism-referential so `stampMergeOff` cannot flip; S's seal observation step is in no machine | carry P6-A ghost oracle re-grounded on generations; declare observation machinery in §3; fix-without-re-review | +| F3 | MAJOR | P1 fold/count membership of dead generations' complete rounds unpinned (empty-fold pin fires under one honest reading); G1's "superseded per §4" false — supersession unreachable for unit keys | generation-grounded fold/counting pins; correct G1 text; fix-without-re-review | +| F4 | MAJOR | generation ids reused across un-checkpointed resumes (2 crashes); dead set mis-derives; debris laundered live | forced resume checkpoint ordering pin (or durable-max bump rule) + reuse probe; fix-without-re-review | +| F5 | MAJOR | E's lineage state: admitted-by edges in no durable row; rebuild-agreement target ill-defined; purge unreachable in scripted G5; `purgeOff` kill cannot flip | checkpoint-content + tally edit; checkpoint-consistent rebuild target; re-script the crash window on a paginated parent; fix-without-re-review | +| F6 | MAJOR | supersession not total: live-live same-key undefined (P1 alarms on legal behavior); overlay-intent record path composes dead+live (E "structural" claim false); G4 subsumption overreach; G5b not strongest; no supersession cell | total supersession matrix in §4 + dedicated cells; fix-without-re-review | +| F7 | MAJOR | P3′ name/form drift; P5 ghost closure is circular; demand starvation seals green under every property; no under-admission kill | pin P3′'s form; counterfactual closure or SealExpect oracles; death-inference rule; demand-drop mutant; fix-without-re-review | +| F8 | MAJOR | cross-variant P3′ (G5d) is a cross-run comparison no P monitor can check, and over-strong under smear | per-leg P3′ + harness-level artifact-set comparison with the §10.6 divergence rule; fix-without-re-review | +| F9 | MAJOR | G6 scripts cannot differentiate E/S (leaf crash, pre-execution stop); envelope has no depth/fan-in; S's refusal timing unpinned — the arbitration axis is vacuous | divergence scripts + envelope rows + refusal/purge-domain pins; fix-without-re-review | +| F10 | MAJOR | stampCompression admissibility: §7 obligation, §6 axis, no §9 cell, no §10 entry; all cells violate declare-all-three | add bucketed re-runs of G2-S/G5 with recorded redo; default axis declarations; fix-without-re-review | +| F11 | MAJOR | P4 stuck fingerprint never fires under generation bumps (G7 stuck leg flips green); attempt-failure semantics undeclared; no attempt budget | generation-blind fingerprint; declare attempt failure; budget row; fix-without-re-review | +| F12 | MINOR | demand-derivation timing (per-announce vs post-completion) unpinned; checkpoint reachability depends on it | one-sentence pin; fix-without-re-review | +| F13 | MINOR | "completed-derivation set" is admission-keyed; completed-vs-never-admitted resume rule unstated | rename + state the rule; fix-without-re-review | +| F14 | MINOR | record-path clear + intent enum arrive via parenthesis; §3 registration and clear-placement pin missing (MS-CO-001 lesson) | register ops/intents; pin placement; fix-without-re-review | +| F15 | MINOR | aggregate node X budgeted but appears in no configuration; §1's promise ranges over an empty set | add the scripted-precondition config or de-scope X; fix-without-re-review | +| F16 | MINOR | cross-sync stamp scoping undefined (per-sync generations vs replayed rows' stamps) | pin restamp-on-replay (+ P6-R jurisdiction note); fix-without-re-review | +| F17 | MINOR | sessions × replay product neither covered nor excluded in §1 | explicit exclusion with argument, or 7a/7b analog cells; fix-without-re-review | +| F18 | MINOR | P6-E's "re-run before seal" doesn't bind the re-run to the live value (re-run-before-re-publish schedules) | pin retraction keying / adopt F2's final-value oracle; fix-without-re-review | +| F19 | NOTE | mechanism tally must absorb F2/F5/F6 machinery before any bake-off run | freeze the amended tally in the spec revision | +| F20 | NOTE | recommendation decision rule and destination unpinned | record the arbitration procedure pre-run | +| F21 | NOTE | session axis vacuous outside G2; §6's all-three rule unsatisfiable as written | one-sentence default declaration | diff --git a/formal/reviews/graph-spec-round2-adoption.md b/formal/reviews/graph-spec-round2-adoption.md new file mode 100644 index 000000000..42cd36f68 --- /dev/null +++ b/formal/reviews/graph-spec-round2-adoption.md @@ -0,0 +1,743 @@ +# Round 2 (TARGETED) — premise-validated adoption (§4a) and its interlocks, GRAPH_MODEL_SPEC v2 + +Scope: the targeted round-2 spot review the round-1 disposition of F1 +required. PRIMARY: §4a (premise-validated adoption — the marker as +memoization entry, adopt-on-equal-digest / re-derive-on-differing- +digest, `eAdopt` semantics, §4d fold transfer, row re-announcement), +answering v2's §11 round-2 charge questions 1–7 mechanically. +SECONDARY: composition of the F2 (P6-G + observation pass), F3 (§4d), +F6 (§4b matrix incl. poison exemption), and F7 (closure oracle + +`eAnnGenBump`) dispositions WITH §4a — not a re-review of those +dispositions as applied; and the acceptance walks: G1's three legs and +G2's four legs under v2 semantics. Anchors: `formal/MODEL_SPEC.md` v11 +FROZEN + MS-CO-001 (§7 property pins incl. the round-5 F8 consult +qualification and round-7 F2/F3 counting/attestation pins; §9.6 +V-ATOMIC / V-OVERLAY-UNIT), `formal/GLOSSARY.md`, +`formal/walker/CALIBRATION.md` decisions 19–24, +`formal/reviews/graph-spec-round1.md`. Round-1 dispositions v2 applied +as specified are NOT relitigated; only their composition with §4a is +in scope. + +Method: mechanical walks of every §4a branch (adopt, re-derive, and +their interleavings) under 2 workers, ≤ 2 crashes, ≤ 3 attempts, ≤ 3 +epochs, per the §8 budget; independent re-derivation of P1 (§4d +grounding), P2/P3′ qualification, P6-G/P6-E/P6-S, and the closure +oracle over every walked history; a durability sweep of the marker / +`eAdopt` / checkpoint rows of §5 against crash and announce-in-flight +placements (the G-RULE-1 window between a store commit and its +announce processing is load-bearing in several walks below); a +back-port thought experiment of §4d onto the v11 walker cells +(charge question 4); totality sweep of the G8a no-marker pin against +the §4b matrix and every marker state a walk can construct. + +Verdict: **REJECT — 6 majors + 8 minors + 3 notes. R2-F2 +(rows-only adoption strands dead session publishes) is +re-review-required: every candidate repair is new mechanism touching +the F17 exclusion boundary and the §7.5 tally. The other five majors +are fix-without-re-review, spot-checkable in the same targeted round +3 that R2-F2 forces.** The repair's core is sound in direction: G1's +three legs derive (leg (ii) with a mechanism-attribution correction), +G2's four axis legs derive (E+B conditionally on the R2-F1 pin), both +G2 kills flip against P6-G for the stated reasons, and the round-1 +starvation hole (F7(ii)) is genuinely closed for completed children. +The rejection is for what the new mechanism composes into at its +seams: mid-attempt death with in-flight executions, session publishes +under adoption, the digest's lossy canonicalization of failed +revalidations, marker lifecycle on the re-derive-to-record path, and +the admitted-derivation set's behavior after purge/refusal. + +## Majors + +- **R2-F1 (MAJOR) — the fate of an in-flight execution whose + generation dies MID-ATTEMPT is undeclared; under the + run-to-completion reading a dead execution's late unit commit (its + clear constituent included) wipes a live re-derivation's rows, and + variant E seals stale content on a leg declared GREEN.** + - Claim under attack: G2's E+B leg ("P6-G GREEN, P6-E green"); + §4a's adopt/re-derive dichotomy (implicitly assumes the dead + generation's execution is gone); §3's retraction pin and the + retraction queue (§7.5), whose enqueue/drain timing and effect on + in-flight readers are nowhere stated. + - Evidence, walked. §5 declares in-flight execution state volatile + ONLY against crash ("a crash loses the execution"). Mid-attempt + death — a retraction-forced bump (E+B) — has no such clause: + G-RULE-3 gates dispatch of (n, g+1) on (n, g) being DEAD, and + death is declared at the bump, which is scheduler bookkeeping. + Nothing kills, cancels, or fences the dead execution's worker, + and MStore has no death gate (§5 puts the generation table in + MGraphScheduler; §3's op registration carries no liveness + precondition). Budget-legal history, G2 E+B chassis, 2 workers: + H's re-publish lands while reader G@g2 is in flight having read + the prior value (the §3 keying pin itself contemplates exactly + this reader: "re-runs that themselves read a stale value before + the re-publish landed"). The retraction bumps G to g3; G@g3 + dispatches on the free worker, finds the marker, recomputes + premises (reads the live value), re-derives, and commits its + unit. THEN the dead G@g2 — never cancelled — commits ITS unit: + an atomic {clear, rows, marker} whose clear removes g3's live + rows and installs rows embedding the dead session value, with + marker (g2, stale digest). No further re-publish occurs, so no + further retraction fires; variant E's pre-seal condition is only + that the retraction queue is empty (it is — g3 executed); E has + no observation pass. Seal embeds a value differing from the + key's final live derived value → P6-G RED on the leg declared + GREEN. Under S the same schedule is caught (the late unit's rows + carry the dead g2 stamp; the pre-seal pass forces a re-run) — the + asymmetry is itself bake-off-relevant and currently invisible. + Under the kill-on-death reading, none of this is reachable and + the declared green derives. Two honest readings, one scripted + cell verdict — the severity definition verbatim. + - Why it matters: this is the inherited marker-race window the + charge's question 1 named, relocated by the repair. Adoption + removed the double-unit shape for same-generation races; + the dead-vs-live race survives because §4a decides adopt vs + re-derive per execution but never says the dead execution's + store ops stop counting. + - Disposition (fix-without-re-review, verified in the R2-F2 round + 3): pin ONE of — (a) QUIESCE-BEFORE-BUMP: a mid-attempt + generation bump commits only after the dying execution's worker + has quiesced (walker decision-16 precedent; scheduler rule, no + tally change); or (b) a store-side DEATH FENCE: the bump commits + a fence op and MStore refuses ops from fenced generations — + which amends the §7.5 frozen tally (a new shared store rule) and + incidentally closes R2-M2. Add the dead-in-flight interleave as + a probe cell (expected unreachable-after-pin), and state the + retraction queue's enqueue/drain semantics in §3. + +- **R2-F2 (MAJOR, re-review-required) — adoption re-grounds ROWS + ONLY; the dead generation's SESSION PUBLISHES stay stamped with + the dead writer generation, so downstream readers can never clear + the dead component: the S-variant pre-seal pass loses its progress + guarantee (the "finite fixpoint" is budget exhaustion, not + convergence) and seals dead stamps on an honest history, and + P6-E's quantification alarms on the same honest history under E.** + - Claim under attack: §4a ("Adoption re-announces ROWS ONLY, never + session ops"); §3's seal sequence ("iterate to fixpoint; bounded + by the §8 attempt budget times the node count, so the model's + fixpoint is finite by construction"); G2's same-value control + leg; G6a's "expected redo: both ≈ 1"; G6c's "both keep C with + zero redo"; §11 question 5's premise that the bound is the + question. + - Evidence, walked (budget-legal: 1 crash, no mutation, the + G-RULE-1 announce-in-flight checkpoint window). H's unit — H + publishes d1 during its execution (the publish is its own store + op, durable) — commits durably; the completion announce is in + flight; a checkpoint commits capturing H PENDING (announce + arrival order is a §2 choice point, so a scheduler step fits + between commit and processing); crash loses the announce. G + completed earlier, its rows embedding d1 with merged stamp + {G: g_G, H: g1_H} under S. Resume: H pending → bump, g1_H dead. + H@g2 finds its marker; no upstream mutation, H reads no + sessions → digest EQUAL → ADOPT. Adoption re-grounds H's ROWS + under g2 and re-announces them; d1 remains writer-stamped g1_H — + a DEAD OUTPUT by the glossary's own definition, content-final + but generation-dead, and nothing ever re-grounds it. Now walk + the pre-seal pass (S legs): G's rows carry {H: g1_H} dead → the + pass forces a re-run of the PRODUCING node, G. G's re-execution + finds G's marker; its session re-read returns d1 with identity + AND writer stamp unchanged (g1_H) → digest EQUAL → ADOPT — and + `eAdopt` substitutes only G's OWN from/to generations in the + row stamps; the H-component persists. The pass observes the same + dead stamp, forces G again, G adopts again: NO PROGRESS PER + ITERATION. Re-derivation does not help either — a re-derived G + merges the re-read value's stamp, {H: g1_H}, back in. The + iteration terminates only at the stated bound, and the sealed + rows carry a dead generation → P6-S RED on an honest, no-mutant, + no-mutation history. Under E+B the same placement alarms the + conformance monitor directly: G is a "reader execution of a dead + value" (d1 became dead at H's bump) and nothing re-runs it + (retraction fires only on re-publish; adoption never + re-publishes) → P6-E RED honest. P6-G stays green (content is + final) — these are mechanism-conformance reds on honest legs, + which §10.1's discipline cannot distinguish from real + mutation-adequacy failures. Ripple: G6a's ≈ 1 and G6c's + zero-redo expectations hold only in schedules where the + checkpoint captured the writer's completion; the + announce-in-flight placement makes them placement-dependent. + Note also G-RULE-1 lists SESSION PUBLISHES as demand sources; + adoption's rows-only re-announce therefore also fails to + re-derive publish-derived demand — no scripted cell demands via + a publish, so this facet is latent, but the asymmetry is real. + - Why it matters: this is charge questions 2 and 5 jointly — a + verdict input (the writer-generation liveness of embedded + session values) that adoption changes the truth of but cannot + repair, and a fixpoint claim that is false as a convergence + claim exactly where adoption and sessions meet. It sits directly + on the F17 exclusion boundary ("re-announces ROWS, never session + ops" is §1's exclusion argument made load-bearing). + - Disposition (RE-REVIEW-REQUIRED — every candidate is new + mechanism): (a) publish-bearing units are INELIGIBLE for + adoption (a writer that died re-derives always; its re-publish + re-grounds the value and ordinary retraction/observation clears + readers) — narrowest, keeps rows-only honest, costs adoption + efficiency for writers only; (b) adoption extends to session + publishes (writer-stamp rewrite + re-announce) — re-opens the + F17 exclusion argument and G-RULE-1's demand vocabulary, + heaviest; (c) a generation-alias table: `eAdopt` records + from→to and every stamp/liveness read evaluates through the + alias map — new durable state class, §7.5 tally change. Each + changes §3/§4a/§7 and the G2/G6 expectations; pick, pin, + re-derive the affected cells, and bring it back with the R2-F1 + pin for a targeted round 3. + +- **R2-F3 (MAJOR) — the premise digest canonicalizes a failed + revalidation as a bare outcome, so adopt fires across a genuine + upstream change (fail-vs-e2 ≡ fail-vs-e3); ADOPT is a new verdict + class the inherited P2/P3′ qualification never classifies, two + honest readings diverge on the constructed history, and §4a/§7's + "the adopting re-consult is strictly fresher" claim is false in + the FAIL shape.** + - Claim under attack: §4a's digest definition ("the consult result + (previous-artifact entry + revalidation outcome)") and its + walker-coherence paragraph ("the graph's re-consult is strictly + fresher... strengthens, never weakens"); §7 P2 ("a + marker-adopted scope's consult is the ADOPTING re-consult, this + attempt — strictly fresher than the walker's suppressed case"). + - Evidence, walked (budget-legal: 1 crash, epochs e1→e2→e3, one + scope, no sessions). Attempt 1: (S1, g1) consults — entry V1, + revalidation vs e2 FAILS → CHANGED-WITH-DIFF; the overlay unit + commits {copy base e1, overlay e1→e2, marker(g1, D), publish V2} + with D = H(entry V1, outcome FAIL). Checkpoint predates + completion (announce window); crash; upstream mutates e2→e3 + (between attempts — in scope per §1). Resume: bump g2; marker + found; recompute: entry V1 unchanged, revalidation vs e3 FAILS → + D′ = H(V1, FAIL) = D → ADOPT rows(e2) under upstream e3. The + premise the verdict actually depended on — WHICH upstream state + the diff fetched — changed; the digest cannot see it because a + failed revalidation yields no canonical token for the state it + failed against, and the diff content is fetched inside the + round, after the verdict. Now the readings: the adopting + re-consult performed a revalidation (FAIL) but NO fetch — under + MODEL_SPEC §7's round-5 F8 pin it is neither a validation match, + nor a fresh fetch, nor a changed-with-diff-with-fetch. Reading A + (adopt re-consult does not qualify): P2 green via attempt 1's + qualifying diff (any-attempt wording), P3′'s "last + consulted-against-upstream verdict" is attempt 1's at e2 → + expects rows(e2) → green. Reading B (the spec's own §7 sentence + makes the adopting re-consult THE consult): its epoch is e3 → + P3′ expects rows(e3) against sealed rows(e2) → RED. The sealed + content itself is smear-legal either way (coherent rows(e2)@V2, + staleness ≤ 1, self-healing next sync) — but the verdict + diverges by reading, and the "strictly fresher" justification is + false: in the FAIL shape the adopting re-consult qualifies + nothing, and freshness rests on the DEAD generation's original + fetch, exactly the walker's suppressed case, not stronger. Note + the walker never calibrated this shape: §7's P1 boundary note + confines every walker config to one interruption + one mutation; + suppress-after-diff-unit under a SECOND mutation is outside the + 6-overlay envelope — the adaptation extended the hand-off + without noticing. + - Why it matters: charge question 2's named target ("does any + input to a verdict escape it"). The escape is real, constructed, + and budget-legal; no scripted cell reaches it (G1's original + verdict is a MATCH), so it would be hardcoded away silently by + whatever the encoding does. + - Disposition (fix-without-re-review): pin adopt eligibility to + MATCH-outcome premises — a re-consult whose revalidation FAILS + re-derives regardless of the stored digest (cheap: FAIL means + upstream moved; the re-derivation fetches the current state and + "strictly fresher" becomes true), OR keep FAIL-adoption and pin + ADOPT's P2/P3′ classification explicitly (attempt-anchored + qualification; correct the §4a/§7 freshness prose). Register + ADOPT as a verdict class in the announce vocabulary either way, + and add the e1→e2→e3 FAIL-adopt history as a probe cell. + +- **R2-F4 (MAJOR) — marker lifecycle under supersession is not + total: the re-derive branch's `eMarkerPut` overwrite exists only + for unit-verdict re-derivations, a record-round re-derivation + (fetch-fresh) leaves the stale marker in place, and a later + premise flap-back ADOPTS content the marker no longer describes — + the exact hazard §11 question 6 hypothesized, constructed here + within budget.** + - Claim under attack: §4a's re-derive branch ("its new unit's + clear constituent supersedes the dead rows (§4b), and its + `eMarkerPut` overwrites the marker") — false for fetch-fresh + re-derivations, which are record rounds and commit NO marker + (the G8a pin); §4b's matrix (no marker column: no row says what + any incoming round does to an EXISTING marker); G1 leg (ii)'s + cell text (same misattribution: a failed revalidation yields + fetch-fresh per leg (i), so leg (ii)'s superseding round is a + RECORD round, not "the new unit"). + - Evidence, walked (budget-legal: 2 crashes, 3 attempts, epochs + e1→e2→e3 with content(e3) = content(e1) — value flap-back is in + scope per the walker's d1→d2→d1 precedent and truthful + validators). Attempt 1: (S1, g1) consults (V1, MATCH) → replay + unit + marker(g1, D = H(V1, MATCH)). Checkpoint in the announce + window (S1 pending); crash 1; mutate e1→e2. Attempt 2: bump g2; + marker found; recompute (V1 vs e2: FAIL) → differs → RE-DERIVE → + fetch-fresh → record round: `eClearScope` (removes g1's rows AND + fold contribution, §4b) + pages rows(e2) + publish V2 — and NO + `eMarkerPut` (G8a pin); nothing removes marker(g1, D), which now + describes a unit whose rows are gone. Checkpoint again in the + final-page announce window (S1 pending); crash 2; mutate e2→e3, + content reverting to e1's. Attempt 3: bump g3; marker check + finds marker(g1, D); recompute: entry V1, revalidation vs e3 + MATCHES (truthful validator: content unchanged vs V1's) → digest + EQUAL → ADOPT. Every constituent of `eAdopt` now operates on the + wrong object: the stamp rewrite substitutes g1→g3 and touches + nothing (the partition holds g2's rows, no g1 components — under + S they remain DEAD-stamped g2); the §4d fold transfer transfers + a contribution that was REMOVED at the §4b clear (incoherent + bookkeeping — meanwhile g2's dead complete round persists in the + fold per §4d); the re-announce announces rows(e2) as the adopted + content of a marker attesting replay-of-V1 = rows(e1). Seal, E + leg: partition rows(e2); the adopting re-consult was a MATCH — + which QUALIFIES under the F8 pin — so P3′'s last + consulted-against-upstream epoch is e3 and expects rows(e3) = + rows(e1) → P3′ RED on an honest history. S leg: the pass sees + the dead g2 stamps, forces re-runs that re-ADOPT through the + same stale marker without ever rewriting g2's stamps → the + R2-F2 no-progress loop shape again, from a different cause → + P6-S RED honest. The marker described content the store no + longer held, and adoption believed it. + - Why it matters: totality (charge question 6). The G8a pin is + correct in isolation; its ripple — who clears a marker a record + round strands — was never walked, and §12's own parenthetical + shows the pin was surfaced mid-draft. Adoption's soundness + silently assumes the invariant "marker present ⟹ the key's + partition IS the marked unit's outputs," and no rule maintains + it. + - Disposition (fix-without-re-review): add a MARKER column to + §4b's matrix making marker lifecycle total: any superseding + commit for a key removes-or-overwrites its marker (unit rounds + via their `eMarkerPut` constituent — already true; record + REPLACES rounds: `eClearScope` also deletes the marker, pinned + in §3's clear-placement pin; OVERLAY-intent records: no dead + base is legal, so no marker case arises — state it). Pin the + invariant explicitly and monitor it (marker ⟹ partition equals + the marked unit's outputs; announce-evidenced). Correct §4a's + re-derive branch ("its new ROUND supersedes; a unit round's + `eMarkerPut` overwrites the marker, a record round's clear + removes it") and G1 leg (ii)'s text. Add the flap-back history + as a probe cell (expected unreachable-after-pin). + +- **R2-F5 (MAJOR) — the admitted-derivation set has no death + semantics: a purged (E) or refusal-dropped (S) child's derivation + hash stays in the set, so the live generation's re-announce — by + adoption OR by at-least-once record redo — is suppressed and the + child starves on honest legs; jointly, E's purge predicate is + ∃/∀-ambiguous over fan-in edges and the two readings diverge on + G6c's declared E-leg verdict.** + - Claim under attack: G-RULE-2 ("the scheduler MAY suppress a node + admission iff the same derivation hash is already in the + ADMITTED-DERIVATION SET... this sync" — no removal rule + anywhere, and "may" leaves the load-bearing branch to the + encoding); §3's purge ("purges from the frontier every PENDING + node whose admitted-by edge names a dead generation" — ∃ or ∀ + over a fan-in node's edges?); G6c's E-leg ("E must exercise + refcounted support (C survives on S2's live support — no + retraction)... both keep C with zero redo"); §4a's starvation- + closure claim ("a lost child admission is re-derived from the + adopted rows' content"). + - Evidence, walked. (i) G6c, E leg: C is pending, demanded by S1 + AND S2 (two generation-qualified admitted-by edges); the crash + kills S1 mid-round; resume bumps S1 → C now has one DEAD edge + and one LIVE edge. Under the ∃-reading of §3's purge predicate C + is purged — contradicting the refcounted-support story the cell + asserts; under the ∀-reading C survives. Two honest readings, + one declared cell verdict. The S-side got the equivalent pin + ("S must not false-refuse (C's admission stamp contains a live + parent)"); E's purge predicate did not — asymmetric drafting. + (ii) The starvation composition, either variant: C's hash + entered the admitted-derivation set at ADMISSION (F13 pin) and + the set is checkpoint-durable. C is purged (E) or comes up for + dispatch before any live re-derivation and is refusal-dropped + (S: "otherwise it is dropped from the frontier without + executing"). The live parent's re-execution then re-derives the + SAME hash — S1@g2's at-least-once page-1 re-announce in a + paginated no-shrink config, or an adoption's row re-announce. + Demand derivation consults the set: the hash is present → + suppressed → C is in the frontier of no one, completed by no + one, and never re-admitted. Post-purge, the F13 completion rule + read against LIVE state (admitted ∧ ¬pending) even classifies C + as COMPLETED. The final closure lacks C; the env-side + counterfactual closure oracle fires RED on an honest, + mutant-free run — the oracle works exactly as F7's repair + intended, which is how we know the run is broken, not the + oracle. Adoption's F7(ii) closure claim survives only for + children whose admission was NEVER checkpointed (hash absent + after restore → re-admitted fresh) and for COMPLETED children + (suppression is then correct — G6a's ≈ 1 redo depends on it and + still derives); the purged/refused middle case starves. + - Why it matters: this is the composition of three round-1 + dispositions v2 applied individually and correctly (F5's + pending-only purge, F13's admission-keyed set, F1's re-announce) + — none wrong alone, jointly unsound. It is the graph's + single most load-bearing new mechanism (demand derivation) + failing silent-with-oracle on honest runs. + - Disposition (fix-without-re-review): pin (a) the purge predicate + as ∀ (purge only when EVERY admitted-by edge names a dead + generation — the refcount-consistent reading G6c's text already + assumes); (b) purge and refusal-drop REMOVE the node's + derivation hash from the admitted-derivation set (making + re-derivation re-admissible; G5e's count oracle is unaffected — + its epoch-2 shrink never re-derives the hash); (c) G-RULE-2's + "may" tightened to MUST-suppress iff the hash's node is pending + or completed, so the checker cannot legally choose starvation. + Re-derive G5f's honest baseline and G6c's E leg under the pins. + +- **R2-F6 (MAJOR) — G2's same-value control leg declares "P6-S red + ... recorded as redo," but P6-S's written form is a SEAL check and + the honest observation pass clears the evidence before seal: under + the at-seal reading the declared red is underivable (the leg walks + green), and under the at-observation reading P6-S is not the + property §7 registered — the F2(iii) value-blindness pin as + applied is incoherent with the pass it rides on.** + - Claim under attack: G2's control leg ("P6-S red on the S legs by + the value-blindness pin — recorded as redo"); §7 P6-S ("no + SEALED output carries a stamp containing a dead generation") + plus its value-blindness pin ("P6-S alarming on same-value + re-derivation is INTENDED — it checks the mechanism's + forced-redo promise"). + - Evidence, walked (scripted control premise: checkpoint captures + G completed ∧ H pending; crash; H@g2 re-derives d1 EXACTLY). + H@g2 re-publishes d1 under writer generation g2 (G-RULE-3 stamps + at emission; the value is identical, the writer stamp is not). + Pre-seal pass: G's rows carry {H: g1} dead → forces G's re-run. + G's re-execution finds its marker and recomputes premises: the + session re-read returns d1 with WRITER STAMP g2 ≠ g1 → digest + DIFFERS → RE-DERIVE → new rows carry {G: live, H: g2}, all live. + Seal: no sealed output carries a dead generation → P6-S GREEN as + written. The forced redo happened and is real — but it is a + RECORDED METRIC, not a property violation; if the pass works, + P6-S at seal cannot fire, and if P6-S fires at seal the pass + failed, which is a conformance failure, not "overhead." The two + readings (at-seal vs at-observation) flip a scripted leg's + declared verdict, and §10.1's adequacy discipline cannot tell an + intended red that never derives from a broken kill. + - Why it matters: the control leg is the declared exhibit of the + mechanism-vs-oracle distinction the F2 repair introduced; as + written it exhibits the confusion instead. + - Disposition (fix-without-re-review): recast the same-value + mechanism-conformance signal as what it is — a forced-redo COUNT + (observation-pass re-run events, announce-evidenced), recorded + in the redo column — and reserve P6-S red for sealed dead stamps + (mechanism failure). Correct the control leg's declared verdict + to: P6-G green, P6-S green, forced-redo count ≥ 1 (the + value-blindness pin's content, honestly stated). The + value-blindness PIN itself survives — dead-stamp-forced redo on + value-identical re-derivation remains intended behavior; only + its verdict vocabulary was wrong. + +## Minors + +- **R2-M1 (MINOR)** — the SESSION-READ observation point (variant S) + is named in §7.5's tally and the glossary but has no declared + transition in §3: what an execution does when a session read + returns a dead-stamped value (refuse? proceed and let the pre-seal + pass catch it? force the writer?) is unstated. No scripted leg + turns on it (reads are live at read time in every walked history), + but the tally counts it as S mechanism, so it must be registered + or struck. Fix-without-re-review. +- **R2-M2 (MINOR)** — `eAnnGenBump` at OBSERVATION/RETRACTION-forced + re-admissions has no commit carrier: those re-admissions are + scheduler steps, not store ops, while §2's monitor discipline is + announce-AT-COMMIT. The resume-time bump rides the forced resume + checkpoint's commit (clean); the mid-attempt bump's announce is + evidentially ungrounded. R2-F1's fence option (b) closes this for + free; otherwise pin the carrier. Fix-without-re-review. +- **R2-M3 (MINOR)** — marker store-scoping across syncs is + unregistered: §5 makes the marker durable in the artifact, the + budget runs 2-sync cells, and nothing says the §4a marker check + reads the CURRENT sync's store only (the walker's 3-atomic + precedent implies it; the graph spec never says it). A sealed + artifact also retains stale markers (R2-F4's history seals one + even after the fix, on the no-flap path) — pin that seal or sweep + drops marker rows, or that they are per-sync namespaced; note + §4c's "adopted-across-sync rows" wording invites the wrong + reading. Fix-without-re-review. +- **R2-M4 (MINOR)** — §4b's dead-base OVERLAY row explains detection + "via the marker's dead generation + no adoption," but record-path + keys HAVE no markers (G8a pin): for record-over-record the stated + detection channel does not exist and the precondition rests + entirely on scripted connector policy + the trust model. One + defensible intended meaning (the mutant, not the mechanism, is the + load-bearing part); the wording overclaims. Fix-without-re-review. +- **R2-M5 (MINOR)** — whether the ADOPTING execution's premise + re-reads register as tracked read edges (E+B) / merge into stamps + (S) is undeclared. The walked histories self-heal (the dead + generation's original read still grounds node-level retraction, + and digest equality pins the re-read stamp to the original), but + P6-E's quantification ("every reader execution of a dead value") + silently includes adopting re-readers only under one reading. + State it. Fix-without-re-review. +- **R2-M6 (MINOR)** — coverage gaps in the retraction machinery's + distinctive clauses: (i) the §3 keying pin's re-retraction clause + ("re-runs that themselves read a stale value... retracted again") + has no witness — G2's scripted checkpoint placement (G completed) + never produces a pre-re-publish re-run; add the G-pending + placement as a leg. (ii) Session-TRANSITIVE chains (a + reader-writer: retracted reader re-publishes, retracting + second-order readers) exist in no envelope config — the widened + envelope's readers do not write. Either script one reader-writer + leg or restate the inductive bet to exclude the shape explicitly. + (iii) No cell derives demand from a SESSION PUBLISH though + G-RULE-1 names publishes as demand sources (interacts with + R2-F2's rows-only re-announce). Fix-without-re-review. +- **R2-M7 (MINOR)** — the pre-seal pass's stated bound ("the §8 + attempt budget times the node count") is a category error: the + attempt budget bounds crash/resume attempts, and nothing in §8 + bounds INTRA-attempt observation-forced bumps per node (each bump + is a fresh generation; the state space is unbounded exactly where + R2-F2's loop lives). Add an explicit pass-iteration budget row to + §8 and re-word "finite by construction" to name the cutoff. + Fix-without-re-review. +- **R2-M8 (MINOR)** — poison × marker is unclassified: G8c's second + derivation poisons a scope whose first unit committed a marker; + a later forced re-run of the first node would find the marker, + recompute equal premises, and ADOPT a poisoned scope's rows — + re-announcing them into demand derivation. The seal excludes the + scope (SealExpect), so the direct content hazard is bounded, but + the matrix should say poison VOIDS the key's marker (no adoption + of poisoned content) and classify post-poison rounds for the key. + Fix-without-re-review. + +## Notes + +- **R2-N1 (NOTE)** — `eAdopt`'s precondition (fromGen is DEAD) is + unstated. A live-fromGen adoption is reachable only under + `suppressionOff` (G1's mutant leg: the sequential schedule's + second execution adopts a LIVE generation's unit), where the + declared legality alarm still derives; record the precondition so + the mutant's adopt path is a declared deviation, not an accident. +- **R2-N2 (NOTE)** — digest completeness is relative to the modeled + input set: config/compat drift and warm-install state are + unmodeled in every G cell (no drift scenario), so the digest omits + them VACUOUSLY; a cold re-consult would miss and force re-derive + (safe direction) if cold attempts were ever modeled. Add one + closure sentence to §4a ("the digest is total over the model's + verdict inputs: consult result and session reads; config/compat + are constants in every cell") so the vacuity is declared, per the + F15/F21 registration lesson. +- **R2-N3 (NOTE)** — P-GEN's monitor rule is implicit but derivable: + the checkable form is "no two ATTEMPTS contain store-commit + announces attributed to the same (n, g)" (attempt boundaries are + announce-visible; adoption re-announces attribute to the ADOPTING + execution, so they do not collide). Record the rule so the G1b + probe's evidence base is pinned — this is the affirmative answer + to charge question 7's F4 item. + +## Mechanical walks (the acceptance test for the repair) + +- **G1 leg (i)** (crash before unit commit): nothing durable; resume + bumps; re-consult V1 vs e2 FAILS → fetch-fresh → record round + (REPLACES: clear over empty, rows(e2), publish V2, no marker — + G8a-consistent). Fold = fresh(e2); P1/P2/P6-S green both variants. + **DERIVES as declared.** +- **G1 leg (ii)** (crash after unit commit): marker(g1, (V1, MATCH)) + found; recompute → FAIL vs e2 → digest differs → RE-DERIVE → + fetch-fresh → RECORD round; §4b's dead-rows/record/REPLACES row + removes g1's rows and fold contribution; seal rows(e2)@V2; fold = + fresh(e2); P6-S live. **Verdict GREEN DERIVES on both variants — + but via §4b's record row, NOT "the new unit's clear" (the cell + text and §4a's re-derive branch misattribute, R2-F4), and the + stale marker(g1) survives to seal (latent per R2-F4/R2-M3; + harmless in this cell's remaining history).** +- **G1 leg (iii)** (no mutation): marker found; recompute (V1, + MATCH) = D → ADOPT; `eAdopt` rewrites stamps g1→g2 (S: all live), + §4d transfers replacement(e1) — fold equals content; entry V1 + attests e1; P2's qualifying consult is the adopting re-consult + ITSELF (MATCH qualifies under the F8 pin — the "strictly fresher" + claim is TRUE in the match shape); rows re-announced. **DERIVES + as declared, both variants. This is the repair working.** +- **G1 mutant leg** (`suppressionOff`): racing schedule — both + consults pass the absent-marker check, two units commit, two + complete rounds with two copies → P1-LEGALITY RED first-find; + sequential schedule — second execution finds the first's marker, + premises equal, ADOPTS, seals coherent → green. **Flip confirmed + for the stated reason; first-find expectation correctly stated + in-cell. (Live-fromGen adoption in the sequential schedule: + R2-N1.)** +- **G2 E+A**: nothing forces G's re-run; seal embeds d1 ≠ final live + d2 → P6-G RED. **DERIVES as declared** (the F2 repair works: the + red now has a property). Marker machinery never engages (G never + re-runs) — no §4a interaction. +- **G2 E+B**: re-publish → retraction → G re-runs → marker found → + recompute: session value identity+writer-stamp differ → RE-DERIVE + → unit clears, embeds d2, `eMarkerPut` overwrites → P6-G GREEN. + **DERIVES under the scripted checkpoint placement AND the + kill-on-death reading of mid-attempt bumps; under the + run-to-completion reading the dead-in-flight schedule seals d1 → + R2-F1. Conditionally derivable.** `retractionOff` kill: no + re-run, sealed d1 → P6-G RED — **flips, mechanism-independent + oracle confirmed.** +- **G2 S+A and S+B**: resume bumps H (pending); H re-derives d2, + re-publishes; pre-seal pass sees {H: g1} dead in G's rows → forces + G → recompute differs (identity and writer stamp) → re-derive → + seal embeds d2, all stamps live → P6-G GREEN, P6-S green; the + pass CONVERGES here because the writer re-derived live (contrast + R2-F2's adopt placement). **DERIVE as declared.** `stampMergeOff` + kill: G's rows carry no H component; the pass is blind; sealed d1 + → P6-G RED — **flips against the oracle, not against itself — + the F2 repair's exact design goal, confirmed.** +- **G2 same-value control leg**: walks P6-G green / P6-S GREEN at + seal with forced-redo ≥ 1 — the declared "P6-S red" does not + derive → **R2-F6.** In the announce-window placement (H's unit + committed, H pending) the leg instead walks into R2-F2's adopt + loop → honest P6-S/P6-E reds. **The control leg is the repair's + soft spot, both directions.** + +## Answers to the §11 round-2 charge + +1. **Adoption/re-derivation interleaving under 2 workers**: row-level + MIXED content cannot seal — units and `eAdopt` are single atomic + store ops, and every §4b/§4a path is clear-based, so the store + holds one round's rows at any commit boundary. The race that + survives is WORSE than mixing: under the run-to-completion + reading of mid-attempt death, a dead execution's late atomic unit + (clear included) replaces a live re-derivation wholesale and E + seals it — FINDING R2-F1. Adopt-vs-adopt and adopt-vs-re-derive + orderings are otherwise sound: eAdopt-then-clear ends at the live + unit; clear-then-eAdopt makes the substitution a no-op with stale + marker metadata (subsumed by R2-F4's lifecycle rule). +2. **Premise-digest completeness**: NO for two inputs. (i) The + upstream state behind a FAILED revalidation escapes (outcome-bit + canonicalization) — adopt fires across e2→e3; concrete history + constructed in R2-F3. (ii) The writer-generation liveness of + embedded session values is changed by adoption itself and is + unrepairable by the digest (R2-F2). The previous artifact's + identity is adequately covered by entry + truthful validators; + warm/cold and config/compat are vacuously absent (R2-N2). +3. **eAdopt stamp-rewrite vs eAnnGenBump ordering**: sound at resume + — the forced resume checkpoint's commit carries the bump announce + and precedes any dispatch, so a monitor processes death before + any adoption's re-announce; commit-order delivery + (announce-at-commit) makes the dead-set current when rewritten + stamps arrive. Mid-attempt bumps lack a commit carrier (R2-M2). + A monitor CAN see dead-stamped row announces after a death + announce (the R2-F1 late commit); P6-S being a seal check means + the window itself doesn't false-alarm — the defect is R2-F1's, + not an ordering unsoundness. +4. **§4d back-port coherence**: COHERENT — verified by re-deriving + the v11 cells under §4d. The removal clause (supersession removes + a complete round's fold/count contribution) is DEATH-GATED and + therefore vacuous in the walker (no death): cell 4's locks-off + two-copy legality red survives (both rounds live, nothing + removed); 6-overlay-last w1's ONE-count survives (the incomplete + round was never counted — completion-counting, decision 19, + untouched); 6-naive's content alarm survives (debris is + incomplete either way); empty-fold attestation (decision 20) + fires exactly as pinned — §4d's marker-adopted seal folds the + transferred round, so the fold is non-empty precisely where the + walker's suppressed seal was green. One caveat: keep the removal + clause death-gated verbatim; a live-rows removal reading would + dissolve cell 4's alarm. The transfer-of-a-removed-contribution + incoherence in R2-F4's history is a §4a-composition defect, not + a back-port defect. +5. **Pre-seal fixpoint finiteness**: the model TERMINATES only by + cutoff, and the stated bound is not even the right budget + (R2-M7). Convergence holds when every dead stamp component + belongs to a node that re-derives live (all scripted main legs); + it FAILS on honest histories where the dead component is a + session writer's and the writer ADOPTED (rows-only re-grounding — + R2-F2) or where re-adoption cycles through a stale marker + (R2-F4): the pass forces the rows' producer, which cannot clear + an embedded dead component, and iteration makes no progress. + With `stampMergeOff` the pass is trivially convergent (blind); + `retractionOff` is E-only and does not interact. The claim as + written conceals the non-converging class. +6. **G8a pin + §4b joint totality**: NOT total. Round shapes are + covered (every verdict class maps to unit or record; ADOPT is + pinned as a transfer, not a round; poison-blocked rounds need a + classification sentence — R2-M8). Marker STATES are not: the + matrix has no marker column, the stale-marker-over-record-rows + state is constructible within budget, and the charge's + hypothesized walk — fetch-fresh leaves the stale marker; a later + consult adopts content the marker no longer describes — is + confirmed REACHABLE and misbehaves in both variants (R2-F4). + The pin's same-sync consequences also misfire in §4a's own + re-derive text (unit-only overwrite). Cross-sync, the marker's + store-scoping is unregistered (R2-M3) but the intended walker + reading (current-sync store only) makes the pin total there. +7. **Round-1 leftovers**: F4 — APPLIED CORRECTLY; ordering pin + explicit in §5, G1b probe + `resumeCkptOff` kill well-formed; + P-GEN is checkable via attempt-scoped announce attribution + (rule recorded as R2-N3). F5 — APPLIED CORRECTLY; the + checkpoint-consistent rebuild target is well-defined in G5e's + mid-round premise (a deterministic function of restored frontier + + generation-qualified admitted-by edges + durable store, all + announce-reconstructible; the dead-edge child purges cleanly + after the bump). The COMPOSITION defect found nearby (purge × + admitted-derivation set, purge predicate under fan-in) is new — + R2-F5, not a mis-application of F5. F9 — MOSTLY SUFFICIENT: + depth 2 exercises E's support-transitive retraction (G6b: S1's + changed re-derivation drops C's support, C's death drops GC's — + the machinery executes), fan-in exercises the refcount (G6c, + modulo R2-F5's purge-predicate pin). Still unexercised: + session-transitive (reader-writer) chains and the re-retraction + clause — R2-M6; declare or script. + +## Interlock spot-checks (F2/F3/F6/F7 dispositions × §4a) + +- **F2 × §4a**: the laundering oracle is genuinely + mechanism-independent — both G2 kills flip against P6-G with + adoption active (walked above); adoption cannot defeat a + retraction/observation-forced re-run whose premises changed (the + digest sees the value change) — the round-1 F1 defeat is closed. + Defects at the seam: the observation pass × adoption progress + failure (R2-F2), the value-blindness pin's verdict vocabulary + (R2-F6), the session-read observation point's missing transition + (R2-M1). +- **F3 × §4a**: §4d composes with adopt (transfer) and re-derive + (removal) correctly in every scripted cell; back-port coherent + (charge answer 4). The one incoherence — transferring a removed + contribution — arises only through R2-F4's stale marker. +- **F6 × §4a**: the matrix's four row-classes compose with + adoption's clear-based branches; the poison exemption is coherent + (G8c's alarm is the poison; counting exempted, legality preserved + elsewhere). Gaps: the missing marker column (R2-F4), poison × + marker (R2-M8), the dead-base detection wording (R2-M4). +- **F7 × §4a**: adoption's row re-announce makes the ghost closure + well-defined across deaths as claimed, and the env-side + counterfactual closure is genuinely independent (it is what + catches R2-F5's starvation). The starvation hole is closed for + never-checkpointed and completed children; the purged/refused + middle case re-opens it via the admitted-set composition (R2-F5). + `eAnnGenBump` evidence base: sound at resume, carrier gap + mid-attempt (R2-M2). + +## Verified clean (for the revision record) + +- The adopt-on-equal MATCH path is a faithful, strictly-stronger + adaptation of the walker's clause (iii): same-sync consult + provenance becomes this-attempt re-consult provenance, P2's + qualification holds via the adopting MATCH itself, and the + 6-atomic hand-off content seals identically (G1 leg (iii)). The + walker-coherence paragraph's degeneration argument is correct FOR + MATCH premises; only the FAIL shape overclaims (R2-F3). +- Fold/count transfer and removal (§4d) re-derive P1 correctly in + every scripted G1/G2/G5/G8 history walked; replacement-count + legality never double-counts an adopted copy (adoption commits no + copy) and never loses the mutant legs' alarms. +- `eAdopt` atomicity: single store op, announce-at-commit, rows + re-announced under the adopting execution's attribution — P-GEN, + P5's ghost closure, and G-RULE-1's per-announce timing all remain + well-grounded; no torn adoption state is constructible. +- The no-relitigation boundary is respected in both directions: v2 + did not re-open the walker verdicts, and this review's findings + are all about §4a's own seams or v2-new compositions, not about + round-1 dispositions as applied. +- Public-repo hygiene: clean. No customer names, tenant + identifiers, or internal infrastructure in v2 or in this review. + +## Summary + +| finding | severity | one line | disposition | +|---|---|---|---| +| R2-F1 | MAJOR | mid-attempt death of an in-flight execution unpinned; dead unit's late clear+commit wipes a live re-derivation; G2 E+B red under one honest reading | pin quiesce-before-bump (or store death fence, tally-amended) + retraction-queue semantics + probe cell; fix-without-re-review | +| R2-F2 | MAJOR | rows-only adoption strands dead session publishes; observation pass loses progress (budget-exhaustion seals dead stamps, honest P6-S/P6-E reds); G6 redo expectations placement-dependent | writer-adoption ineligibility vs publish re-grounding vs generation aliasing — new mechanism either way; **re-review-required** | +| R2-F3 | MAJOR | digest's FAIL-outcome canonicalization adopts across a real upstream change; ADOPT unclassified in P2/P3′ qualification (readings diverge); "strictly fresher" false in the FAIL shape | pin adopt-eligibility to MATCH premises (or classify ADOPT explicitly), register the verdict class, probe cell; fix-without-re-review | +| R2-F4 | MAJOR | marker lifecycle not total: record-round re-derive strands the stale marker; flap-back premise ADOPTS content the marker no longer describes (P3′/P6-S reds on honest history); §4a re-derive text false for fetch-fresh | marker column in §4b (supersession removes/overwrites), marker⟺content invariant + monitor, text corrections, probe cell; fix-without-re-review | +| R2-F5 | MAJOR | admitted-derivation set has no death semantics (purged/refused child's hash suppresses its own re-admission → honest starvation); E purge predicate ∃/∀-ambiguous over fan-in, flipping G6c's E leg | ∀-purge pin; purge/refusal remove the hash; suppression scoped MUST over pending∨completed; re-derive G5f/G6c; fix-without-re-review | +| R2-F6 | MAJOR | G2 control leg's declared "P6-S red" underivable at seal (the pass clears the evidence); value-blindness pin's verdict vocabulary conflates property with forced-redo metric | recast as forced-redo count; P6-S red reserved for sealed dead stamps; correct the leg's declaration; fix-without-re-review | +| R2-M1 | MINOR | session-read observation point in tally/glossary but no §3 transition | register or strike; fix-without-re-review | +| R2-M2 | MINOR | mid-attempt eAnnGenBump has no commit carrier (announce-at-commit discipline) | pin the carrier (free under R2-F1's fence option); fix-without-re-review | +| R2-M3 | MINOR | marker store-scoping across syncs unregistered; sealed stale markers as artifact debris; §4c wording | per-sync scoping pin + seal/sweep drops markers; fix-without-re-review | +| R2-M4 | MINOR | §4b dead-base detection "via the marker" overclaims for markerless record keys | wording (scripted policy + trust model); fix-without-re-review | +| R2-M5 | MINOR | adopting execution's premise re-reads: tracked-edge/stamp-merge status undeclared | one-sentence pin; fix-without-re-review | +| R2-M6 | MINOR | re-retraction clause, reader-writer session chains, and publish-derived demand unexercised by any leg | add legs or restate the inductive bet; fix-without-re-review | +| R2-M7 | MINOR | pre-seal pass bound is a category error (attempt budget ≠ intra-attempt bump budget) | §8 iteration-budget row + re-word "finite by construction"; fix-without-re-review | +| R2-M8 | MINOR | poison × marker unclassified (adoption of poisoned rows constructible) | poison voids the marker; classify post-poison rounds; fix-without-re-review | +| R2-N1 | NOTE | eAdopt's fromGen-dead precondition unstated (mutant-only reachable) | record it | +| R2-N2 | NOTE | digest completeness vacuous over unmodeled config/compat inputs | closure sentence in §4a | +| R2-N3 | NOTE | P-GEN's per-(n,g)-per-attempt monitor rule implicit | record it (charge Q7/F4 answer) | diff --git a/formal/reviews/graph-spec-round3-repairs.md b/formal/reviews/graph-spec-round3-repairs.md new file mode 100644 index 000000000..6c5e5d3f8 --- /dev/null +++ b/formal/reviews/graph-spec-round3-repairs.md @@ -0,0 +1,657 @@ +# Round 3 (TARGETED) — the round-2 repairs and their composition, GRAPH_MODEL_SPEC v3 + +Scope: the targeted round-3 spot review the round-2 disposition of +R2-F2 required, executing v3's §11 charge verbatim: (1) +writer-adoption ineligibility and its convergence argument; (2) +quiesce-before-bump composed with the retraction queue and the +pre-seal pass; (3) marker lifecycle totality (§4b marker column + +per-sync scoping + poison voiding + P-MARK); (4) admitted-set death +semantics under G-RULE-2's MUST-suppress; (5) MATCH-only adoption +and the G1c oracle; (6) the corrected G2 table, all legs and all +three new kills; (7) regression spot-check of round-2's +verified-clean results. Secondary (registration-grade): the round-2 +minors/notes as applied. Anchors: `formal/MODEL_SPEC.md` v11 FROZEN ++ MS-CO-001 (§7 pins incl. round-5 F8 and round-7 F2/F3; §9.6 +V-ATOMIC / V-OVERLAY-UNIT and their consult-surface and elision +inheritance), `formal/GLOSSARY.md`, `formal/walker/CALIBRATION.md` +decisions 19–24, `formal/reviews/graph-spec-round1.md`, +`formal/reviews/graph-spec-round2-adoption.md` (the acceptance +baseline — its verified-clean walks and its majors' dispositions as +applied are NOT relitigated; only their v3 composition is in scope). + +Method: mechanical walks of every honest history the charge names — +announce-window placements, two-crash compositions, mid-attempt +deaths under the quiesce pin, the G-pending and same-value G2 legs, +the G8d flap-back and G8c poison probes, the G5f/G6c re-derived +baselines — under 2 workers, ≤ 2 crashes, ≤ 3 attempts, ≤ 3 epochs, +pass-iteration budget ≤ 3, per §8; independent re-derivation of +P1/P2/P3′/P-GEN/P-MARK/P6-G/P6-E/P6-S and the closure oracle over +every walked history; kill-flip derivation for `writerAdopt`, +`quiesceOff`, `markerCleanupOff`, `adoptOnFail`, plus regression +flips for the inherited toggles; a durability sweep of §5's new and +amended rows (marker, mid-attempt bumps, retraction queue / pass +state) against crash and announce-in-flight placements; a totality +sweep of §4b's marker column over every marker state a walk can +construct; a chassis-constructibility check for every leg the charge +names (can the scripted premise exist under the declared semantics — +the round-1 F5(iii) discipline). + +Verdict: **REJECT — 2 majors + 4 minors + 3 notes. Both majors are +fix-without-re-review; NO finding is re-review-required.** All six +round-2 majors are soundly repaired as applied: the +writer-ineligibility core is sound (every scripted dead-writer +history re-derives live and the pass converges within budget — the +convergence argument the round-2 charge found missing now derives +mechanically on every scripted honest history), quiesce-before-bump +closes R2-F1's wipe in every schedule walked and cannot starve the +pass or deadlock, the marker lifecycle matrix is total over every +constructible marker state, the admitted-set death semantics close +the starvation/double-admission space under two workers, MATCH-only +adoption is classifiable in every scripted cell with SealExpect as +the correct oracle, and the corrected G2 table derives leg by leg. +The two majors are v3-composition seams the repairs newly expose, +not defects in the repairs as specified: (R3-F1) the +writer-ineligibility machinery — including its `writerAdopt` kill — +rests on an UNREGISTERED semantics choice about whether a +replay-verdict re-derivation re-performs session publishes, and the +two honest readings diverge on the kill's fireability and on the +convergence claim itself; (R3-F2) mid-attempt-minted generations are +not durably fenced, so a budget-legal two-crash history REUSES a +generation id and P-GEN fires red on an honest run — round-1 F4's +hazard resurrected through the retraction-queue path that v3's own +pins made walkable for the first time. Both repairs are one-pin +registrations of disciplines the spec already practices elsewhere; +the fixed spec needs a disposition registration check, not a fourth +adversarial round. + +## Majors + +- **R3-F1 (MAJOR) — whether a re-derivation whose re-consult MATCHes + (a replay-verdict round) re-performs the node's session publishes + is unregistered; the two honest readings diverge on the + `writerAdopt` kill's fireability (under the glossary's elision + reading the kill is structurally unfireable in every legal + chassis), and under the same reading a budget-legal writer + flap-back history strands a dead publish on an HONEST run — + falsifying §4a's "transient by construction" convergence claim and + breaking the R2-M1 writer-pending invariant the spec declares + checkable.** + - Claim under attack: §4a's convergence rationale ("the writer's + forced re-derivation re-publishes under its live generation + (same value or not)"); §6's `writerAdopt` row ("G2 + announce-window legs ... S: P6-S RED at seal ... E+B: P6-E + RED"); G2's announce-window leg ("publish-bearing unit committed + ... re-derives → re-publishes"); §3's session-read invariant + ("the dead value's writer is pending by construction"); + `formal/GLOSSARY.md`'s elision pin ("session writes (and reads) + the elided enumeration would have made do not occur this sync"). + - Evidence, walked. The model must pick one of two readings of + what a node execution's session publishes are. READING E + (elision, the glossary/walker anchor): publishes ride the + enumeration; a MATCH verdict's replay round elides them, so + publish-bearing markers arise only from CHANGED-WITH-DIFF units + (fetch-fresh rounds carry no marker, G8a). READING B (body-op, + the reading round 2's accepted walks silently used — R2-F2's own + evidence history has H committing a publish-bearing unit under a + no-mutation premise, constructible only if publishes are + node-body store ops independent of the verdict class): + publishes execute on every non-adopted execution. The spec + registers neither. Now the divergences, both within budget: + (i) THE KILL. `writerAdopt` removes writer-ineligibility only; + MATCH-only (R2-F3's pin) still applies, so the mutant fires only + where an adoption is otherwise eligible: marker present, MATCH + re-consult, digest equal. Under READING E every publish-bearing + marker is a diff unit whose digest records a FAIL outcome; an + adoption additionally needs a MATCH re-consult, whose digest + then differs (FAIL ≠ MATCH) → re-derive regardless of the + mutant. Exhaustive over chassis: stable premise (no + between-attempt mutation) → re-consult FAILs again (the consult + surface is the PREVIOUS artifact, §9.6 o-iii carried "as + everywhere", so the current sync's V2 publish never converts the + verdict) → MATCH-only blocks; flap-back mutation → MATCH but + digest differs → blocks. The kill CANNOT fire in any legal + history — a tabled kill for the round-3 re-review target's + load-bearing machinery that cannot flip, the §10.1 + recommendation-halting class (round-1 F5(iv) precedent). Under + READING B a replay-verdict publish-bearing unit is constructible + (MATCH premise, body publish), the announce-window chassis + exists, and the kill fires exactly as declared: H@g2 adopts → + no re-execution → no re-publish → d1 stays writer-stamped + g1-dead → the pass forces G, G's re-read returns (d1, g1) + unchanged → digest EQUAL → G re-adopts → no progress per + iteration → budget exhausts → sealed dead stamps → P6-S RED + (R2-F2's loop as a kill, verbatim); E+B: no re-publish → no + retraction → G never re-runs → P6-E RED. One toggle, one + scripted leg, two honest readings, opposite fireability. + (ii) THE HONEST HOLE. Under READING E, walk the writer + flap-back (budget-legal: 1 crash, 2 mutations — the G8d shape on + a writer): sync 2 warm; upstream e1→e2 before the sync; H's + consult hits V1, revalidation FAILS → CHANGED-WITH-DIFF; the + diff enumeration publishes d1; the overlay unit commits with + marker(g1, (V1, FAIL), publishBearing=1); reader G completes + embedding d1 with {H: g1}; checkpoint in H's announce window; + crash; upstream mutates e2→e3 with content(e3) = content(e1). + Resume: bump; H@g2 is adoption-INELIGIBLE (bit) → re-derives → + re-consult: V1 vs e3 MATCHES (truthful validator; content + reverted) → verdict REPLAY → the replay unit ELIDES the + enumeration → NO re-publish. The dead publish d1@g1 is stranded + exactly as in R2-F2's loop, with writer-ineligibility ON and + honestly obeyed: G's premise re-read returns (d1, g1) unchanged + → digest EQUAL → G adopts through unchanged premises forever → + the pass budget-exhausts → P6-S RED honest; under E+B no + re-publish means no retraction → P6-E RED honest. The §3 + session-read invariant ("the dead value's writer is pending by + construction") is also violated on this history — H completed + without clearing the value — so the model's own declared + checkable invariant fires on an honest run. Under READING B the + same history converges (the replay re-derivation's body + re-publishes d1@g2; digest differs; G re-derives; two pass + iterations). No SCRIPTED cell schedules a flap-back mutation on + a session writer, so no scripted honest verdict flips today — + the divergence that flips a scripted verdict is the kill leg, + (i). + - Why it matters: this is the round-3 charge's question 1 asked + and answered in the negative under one honest reading — the + convergence argument is sound only under a semantics the spec + never states, and the repair's OWN kill (the evidence that the + ineligibility bit is load-bearing) exists only under that same + unstated reading. It sits on the F17 exclusion boundary the + round-2 disposition was warned about: the within-sync + death-and-re-derive machinery constructs a sessions × replay + product (a replay-verdict re-run of a node that published this + sync) that §1's cross-sync exclusion argument does not cover. + - Disposition (fix-without-re-review): REGISTER READING B — one + §3/§4a pin: session publishes are node-body store ops, executed + by every non-adopted execution regardless of verdict class + (adoption alone skips them, which is exactly what the + eligibility bit exists to prevent); declared as a within-sync + deviation from the walker's elision vocabulary, with the + cross-sync sessions × replay product remaining excluded and + walker-owned (extend the §1 F17 sentence). This is a + registration of the reading round 2's accepted walks already + used, not new mechanism — under it, every declared verdict in + this review's walks derives (verified below): the + announce-window honest leg, both `writerAdopt` flips, the + writer flap-back history (converges), and the R2-M1 invariant. + Also register the announce-window leg's chassis (a + replay-verdict publish-bearing unit, MATCH-stable premise) so + the kill's adoption-eligibility is constructible by script, and + add the writer flap-back history as a probe leg (expected: pass + converges, forced-redo count ≥ 1). + +- **R3-F2 (MAJOR) — mid-attempt-minted generations are not durably + fenced: a retraction-forced bump commits (volatile), the new + generation dispatches and commits durable output, a crash lands + before any checkpoint (placement is a genuine choice point), and + the forced resume checkpoint re-mints the SAME generation id from + the restored table — G-RULE-3's "durably fenced" is false on a + budget-legal honest history, P-GEN fires red on it, and attempt-2 + debris reads as live in the window; round-1 F4's hazard + resurrected through the retraction path v3's own queue/quiesce + pins made walkable.** + - Claim under attack: G-RULE-3 ("generations are per-node monotone + counters, DURABLY FENCED: execution (n, g) exists only after + every (n, g' < g) is dead"); §5's generation-table row + ("latest-per-pending in checkpoints; death announce-visible") + and its mid-attempt-bump row ("volatile scheduler state whose + loss is self-healing"); §7 P-GEN ("no two attempts contain + store-commit announces attributed to the same (node, + generation)") as an every-cell monitor. + - Evidence, walked (budget-legal: E+B, the G-pending chassis, 2 + crashes, 1 mutation, 3 attempts, 1 worker suffices). Attempt 1: + H@g1 publishes d1; checkpoint captures G pending ∧ H pending + (mid-read-window); crash 1. Resume 1: forced resume checkpoint + cp1 commits {G@g2, H@g2H} (F4's pin, correctly applied — these + resume-minted ids are durable before dispatch). G@g2 dispatches, + reads stale d1, completes (unit commits, rows@g2). H@g2H + re-derives d2 (upstream moved), re-publishes → retraction entry + for reader G@g2 → G@g2 is quiesced (completed) → the bump + g2→g3 commits as a derived announce on H's completion carrier — + SCHEDULER STATE ONLY; §5 pins mid-attempt bumps volatile and no + rule forces a checkpoint here (placement is a choice point, and + the skip schedule is legal). G@g3 dispatches, re-derives, its + unit COMMITS DURABLY — rows and marker attributed (G, g3). + Crash 2, before any checkpoint. Resume 2: restore cp1 (the last + durable checkpoint: G pending@g2, H pending@g2H); forced resume + checkpoint bumps from the RESTORED table: G g2→g3, H g2H→g3H — + (G, g3) is re-minted. Attempt 3's (G, g3)′ dispatches and + commits (its marker check finds marker(g3) — its OWN id — + recomputes, digest differs on the dead H stamp, re-derives, a + second (G, g3)-attributed unit commits). Consequences, each + checked: (a) P-GEN's recorded rule fires — attempts 2 and 3 + both contain store-commit announces attributed to (G, g3) — an + honest RED from the monitor F4's repair installed; (b) + G-RULE-3's identity uniqueness is violated in effect (one + stamp, two producers — round-1 F4's exact phrasing); (c) in the + window before attempt 3's clear, attempt-2's rows@g3 (embedding + the dead {H: g2H} component) read as LIVE under the + dead-set-derivation rule (every generation below latest is + dead; g3 IS latest) — dead debris laundered live; content is + eventually rescued only because every unit/record path is + clear-based, which is luck of the mechanism, not a fence. + Reachability requires nothing exotic: a mid-attempt bump (any + retraction or observation re-admission), one post-bump durable + commit, and a crash with the checkpoint skipped. The scripted + G2/G1d configs arm one crash, so no CURRENT cell reaches it — + but §8 budgets two crashes, P-GEN is an every-cell monitor, and + the claim "durably fenced" is false as written. This walk was + not constructible in round 2: R2-F1 left the retraction queue's + enqueue/drain semantics unstated, so the post-bump dispatch + path had no pinned behavior to walk; v3's queue and quiesce + pins created the seam they now must fence. + - Why it matters: every death-keyed mechanism keys off generation + identity (round-1 F4's why-it-matters, verbatim applicable). + The forced-resume-checkpoint discipline exists precisely so + that no generation id is used before it is durable; v3 mints + ids on a second path and forgot the discipline there. + - Disposition (fix-without-re-review): extend the F4 discipline + to mid-attempt minting — pin that a mid-attempt bump's + generation-table delta is durable BEFORE the bumped generation + dispatches. Cleanest form, F4-parity, no new state class: the + bump forces an `eCheckpoint` commit (the checkpoint already + carries the generation table per G-RULE-4) between the bump's + carrier announce and the new generation's dispatch; the + alternative durable-max bump rule (resume bumps to + max(restored latest, highest generation in durable stamps for + the node) + 1 — round-1 F4's option (b)) is also sufficient and + touches only the resume rule. Verified under either pin: the + walk above mints g4 at resume 2 (or restores g3 as durable) — + no collision, P-GEN green, the dead-set derivation correct. + Amend §5's two rows ("self-healing" is true for the LOSS of the + bump's scheduling effect, not for id reuse — say so), add the + two-crash reuse history as a probe cell (expected + unreachable-after-pin), and give the pin a kill + (`midBumpFenceOff` → P-GEN RED), mirroring G1b/`resumeCkptOff`. + +## Minors + +- **R3-M1 (MINOR)** — G8d's honest-leg mechanism attribution is + wrong: attempt 3's consult HITS the previous artifact's V1 and + revalidates against e3 whose content equals e1's, so the truthful + validator MATCHES and the verdict is REPLAY, not "ordinary + fetch-fresh" as the cell text says. The cell's own mutant text + concedes this ("attempt 3 premise-matches it (V1 MATCH vs e3)") — + the honest leg's text contradicts its mutant leg. SealExpect + rows(e3) is verdict-equal either way (replay of the e1 base = + rows(e3)); text correction only, the round-2 G1(ii) class. + Fix-without-re-review. +- **R3-M2 (MINOR)** — eAdopt × poison has a check-then-act window + the matrix's "no adoption of poisoned content is EVER legal" + overclaims: the void bit is read at the worker-side marker check, + and a second derivation's poisoning commit can land between that + check and the `eAdopt` commit (2 workers; the N4/round-7-F6 window + class). G8c's scripted leg forces the re-run post-poison, so no + scripted verdict is affected, and the seal-exclusion bounds the + hazard either way — but "ever" needs a mechanism: pin a store-side + `eAdopt` precondition (refuse on a poisoned key — the same shape + as R2-N1's fromGen-dead precondition) or reword to the scripted + guarantee and record the window as a boundary note. + Fix-without-re-review. +- **R3-M3 (MINOR)** — the pre-seal pass's iteration boundary is + unstated: nothing says a scan begins only from a re-drained + frontier. The drain-gated reading is strongly implied (the pass + lives inside the seal sequence, whose precondition is a drained + frontier, and a forced re-admission un-drains it), and under it + every honest walk in this review converges in ≤ 2 iterations; an + eager-re-scan reading would burn the budget observing the same + dead stamp while the forced re-run is still in flight and make + the §10.8 convergence assertion measurement-dependent. One + sentence ("one iteration = one scan over a drained frontier"). + Fix-without-re-review. +- **R3-M4 (MINOR)** — §4a's convergence claim quantifies over "every + honest history" but the mechanism only guarantees convergence + when the dead component's writer is STILL DEMANDED: a writer + legitimately de-demanded by an epoch shrink (its parent's live + re-derivation no longer derives it; ∀-purge removes it) never + re-publishes, and a still-demanded reader of its sync-scoped + session value then carries an unclearable dead component — the + pass budget-exhausts and P6-S reds on an honest history, while + E+B seals green (a variant asymmetry worth recording). No + scripted cell composes sessions with a demand shrink (sessions + exist only in G2/G9, which script none), so no cell verdict is + affected; the CHECKED form ("every honest cell converges") is + true. Scope the prose claim to the scripted envelope and add the + sessions × shrink exclusion to §1/§8's inductive-bet list (the + R2-M6(ii) treatment), or script the leg. Fix-without-re-review. + +## Notes + +- **R3-N1 (NOTE)** — the at-least-once cost claim for + writer-ineligibility is HONEST (charge question 1's second probe, + answered affirmatively): a publish-bearing round's rows are + re-derived, and every consequence is cost-shaped, not + verdict-shaped — §4d's supersession removal keeps + replacement-count legality at one (the dead round's copy leaves + the count at the clear), G-RULE-2's MUST-suppress over + pending∨completed prevents child re-execution (and a purged + child's re-admission via the re-announce is the intended redo), + P2 qualifies via the re-derivation's own consult, P3′/SealExpect + see identical or fresher content, and P-GEN attribution is clean. + No history was found where losing row-adoption changes a verdict. + Recorded for the bake-off's forced-redo bookkeeping. +- **R3-N2 (NOTE)** — the deferral non-interference arguments + (charge question 2) are sound and worth recording in §3 as two + sentences: (a) a deferred bump cannot starve the pass budget + because deferral requires an in-flight dying execution, which + means a non-drained frontier, and the pass only runs and iterates + from a drained frontier — every deferral resolves strictly before + the pass's first scan; (b) two workers' deferrals cannot deadlock + because a deferral waits on an execution's completion and no + execution's completion ever waits on a bump — the wait graph is + one-directional. Verified additionally: a crash that loses a + pending deferral is self-healing exactly as §5 claims (the resume + bump re-admits the reader regardless), MODULO R3-F2's fencing of + bumps that had already COMMITTED and dispatched. +- **R3-N3 (NOTE)** — the round-2 minors and notes are all applied as + dispositioned; registration sweep in its own section below. One + cross-reference: R2-M1's registered invariant is the honest-red + witness in R3-F1's reading-E history — the registration did its + job (it made the hole checkable), which is evidence for the + disposition discipline, not against the registration. + +## Mechanical walks (the acceptance test for the round-2 repairs) + +All walks under READING B where the reading matters (per R3-F1's +disposition — the reading round 2's accepted baseline used); +divergences under reading E are recorded in R3-F1 and not repeated. + +- **Writer convergence, announce-window (G2 new leg, honest)**: + checkpoint captures H pending, publish-bearing unit committed + (d1@g1 durable); G completed embedding {G: gG, H: g1}. Crash. + Resume: forced checkpoint commits H@g2; marker check — + publishBearing set → INELIGIBLE → re-derive → re-publish d1@g2. + E+B: retraction keyed (key, d1, g1) fires on the re-publish → + G re-admitted → re-reads d1@g2 → digest differs (writer stamp) → + re-derives → embeds live → queue drains → seal d2... seal d1@g2 + content, equal to the final live derived value → P6-G GREEN. S: + frontier drains (H completed) → pass scan 1: G's rows carry + {H: g1} dead → force G → re-read d1@g2 → digest differs → + re-derive → scan 2 clean → CONVERGED in 2 ≤ 3. P6-S green. + **Derives as declared.** +- **Writer convergence, two crashes**: the same chassis with crash 2 + in H@g2's announce window (checkpoint captures H pending, g2 unit + + publish committed). Resume 2: bump g3 (resume-minted, durable at + the forced checkpoint — fenced); marker(g2, publishBearing) → + ineligible → re-derive → re-publish d1@g3 → readers cleared as + above → pass converges in 2. Attempts: 3 ≤ 3. **Derives.** (The + UNFENCED two-crash shape — crash 2 after a MID-ATTEMPT bump's + post-dispatch commit — is R3-F2, a different placement.) +- **Writer mid-attempt death**: unreachable in-envelope — a writer + is retraction-bumped only if it reads a re-published value + (reader-writer, excluded §1/§8) and observation-bumped only via + dead components in its own rows (same exclusion). Writer death is + resume-path only; the convergence argument needs exactly the + resume path. **Vacuity confirmed, correctly fenced by the + declared exclusions.** +- **G1d honest (quiesce)**: retraction bump lands while dying reader + G@g2 is in flight → DEFERS → G@g2 completes (late unit commits, + embedding stale d1, marker g2) → bump commits atomically with the + completion announce → G@g3 dispatches → marker(g2) found, reader + bit clear → premises recompute: reads d2@g2H → digest differs → + RE-DERIVES → its clear removes g2's rows → last writer → queue + entry removed at G@g3's completion → queue empty → seal embeds d2 + → P6-G GREEN. **Derives as declared.** +- **G1d mutant (`quiesceOff`)**: bump commits immediately; G@g3 + re-derives d2 on the free worker and commits; the never-cancelled + G@g2 then commits its late atomic unit — clear wipes g3's live + rows, installs d1 — no further re-publish, queue drains, seal + embeds d1 ≠ final live d2 → P6-G RED. **Flips for the stated + reason.** +- **G8d honest (flap-back)**: attempt 1 replay unit + marker(g1, + (V1, MATCH)); crash 1; e1→e2; attempt 2 marker found, FAIL → + MATCH-only → re-derive → fetch-fresh RECORD round — `eClearScope` + removes g1's rows AND DELETES the marker (the v3 pin) → rows(e2) + + V2; crash 2 in the announce window; e2→e3 (content = e1's). + Attempt 3: NO marker → ordinary consult → V1 MATCHES vs e3 → + replay unit (not fetch-fresh — R3-M1) → clear removes g2's dead + rows → seals rows(e3) → SealExpect GREEN, P-MARK vacuous-to-true + throughout, P1 fold = replacement(e1) = rows(e3). **Derives; + verdict as declared, mechanism attribution corrected.** +- **G8d mutant (`markerCleanupOff`)**: the clear leaves marker(g1, + (V1, MATCH)); attempt 3 finds it, recomputes (V1, MATCH) → digest + EQUAL, bit clear → ADOPTS — while the partition holds g2's + rows(e2) that the marker never described → P-MARK RED (marker ⟹ + partition equals the marked round's outputs; announce-evidenced + from attempt 2's record round onward); under E the adopting MATCH + qualifies → P3′ expects rows(e3) = rows(e1) against sealed + rows(e2) → P3′ RED. **Flips for the stated reasons, both + monitors.** +- **G8c post-poison leg**: second distinct derivation's first commit + poisons the scope AND voids the marker; the scripted forced + re-run finds a voided marker → re-derive-or-refuse (no adoption) + → post-poison rounds commit legally → scope seal-excluded + (SealExpect) → P1 legality exempt (the poison is the alarm). + **Derives as declared.** The unscripted marker-check/eAdopt + commit race is R3-M2 (boundary window, no scripted verdict + affected). +- **Marker-state totality hunt**: states {absent, present(g, D, + bit), voided} × transitions {unit put (overwrite), REPLACES clear + (delete), poison (void), eAdopt (generation update, digest and + bit invariant — premises equal by definition), seal (drop — + per-sync), crash (durable, no change), purge (no change; rows + swept at seal, marker dropped at seal — P-MARK holds + throughout)}. Live-rows record rounds for a marked key are + unreachable except through the poison row (a unit round is + single-op, so same-derivation continuation cannot follow it; a + second execution of the node requires death → dead rows; distinct + derivation → poison). OVERLAY-intent records over a marked key: + only via the illegal dead-base row (mutant-only). **No + unclassified marker state found; the matrix is total.** +- **G5f honest baseline (re-derived)**: no-shrink epoch; C admitted + and checkpointed; crash; C's only edge dead → ∀-purge removes C + AND its hash; the parent's live re-derivation (or adoption's row + re-announce) re-derives the hash → not pending, not completed + (hash removed, completion rule evaluated after removals) → MUST + admit → C re-runs → closure complete → GREEN; `demandDropOff` → + closure oracle RED. **Starvation unreachable, as the cell now + asserts; both directions derive.** +- **G6c E leg (re-derived)**: C has edges from S1 (dead after the + crash) and S2 (live) → ∀-purge does NOT fire → C survives, zero + extra redo; S leg: C runs under S1@g2's live re-derivation of the + hash (dispatch-time re-validation, no false refusal). **"Both + keep C with zero redo" derives under the pins.** +- **Two-worker admitted-set composition**: admissions are derived + per-announce, atomic with the carrier's bookkeeping (G-RULE-1), + and the scheduler serializes announce processing — two parents + deriving one hash admit once, suppress once (pending). Purge and + refusal are scheduler steps (resume-time / dispatch-time), never + concurrent with an admission of the same hash. Refusal-then- + re-announce re-admits; re-announce-then-dispatch runs under the + live re-derivation. **No suppress/starve gap, no double-admission + gap.** +- **G1c honest**: marker digest (V1, FAIL, ⊥); attempt 2 re-consult + FAILs vs e3 → MATCH-only forces re-derive → the re-derivation + fetches at e3 → SealExpect rows(e3) GREEN. Mutant `adoptOnFail`: + digest (V1, FAIL) equal → adopts rows(e2) across e2→e3 → + SealExpect RED. P3′ CANNOT catch the mutant (the FAIL re-consult + performs no fetch and does not qualify under the F8 pin, so the + last qualifying verdict is attempt 1's diff at e2, which expects + the mutant's own rows(e2)); P2 stays green via any-attempt + wording. **SealExpect is not just the right oracle — it is the + only oracle that flips; the walker 5b discipline correctly + applied. Both legs derive.** +- **G2 full table**: E+A — nothing re-runs G; seal embeds d1 ≠ + final live d2 → P6-G RED, derives. E+B — retraction → re-run + (quiesce-deferred where needed, G1d) → seal d2 → P6-G GREEN, now + UNCONDITIONAL (the round-2 conditional reading is closed by the + pin); `retractionOff` → P6-G RED. S+A/S+B — resume bumps H; H + re-derives d2 (FAIL vs moved upstream; marker ineligible or + absent), re-publishes; pass scan 1 forces G; digest differs; + re-derive; scan 2 clean → P6-G GREEN, P6-S green, converged; + `stampMergeOff` → the pass is blind, sealed d1 → P6-G RED + (mechanism-independent oracle confirmed again). SAME-VALUE + CONTROL — H@g2 re-derives d1 exactly, re-publishes under g2; + pass forces G on the dead {H: g1}; the re-read's writer stamp + differs → re-derive → all live → P6-G GREEN, P6-S GREEN at seal, + FORCED-REDO COUNT ≥ 1 (the pass-forced re-run's derived + announce) — the R2-F6 vocabulary now derives exactly as + declared. ANNOUNCE-WINDOW — walked above, derives (chassis + registration per R3-F1). `writerAdopt` — flips on both variants + for the stated reasons UNDER READING B (S: budget-exhausted seal + with dead stamps → P6-S RED; E+B: no re-publish → P6-E RED; + P6-G stays green on the same-value content, correctly + distinguishing oracle from mechanism); unfireable under reading + E — R3-F1 is prerequisite to citing this kill. G-PENDING — G's + pre-re-publish re-run reads stale d1 and completes; H re-publishes + d2; the re-retraction clause fires on the completed re-run + (quiesce trivially satisfied) → G re-runs again → embeds d2 → + P6-G GREEN; witnesses the keying pin's clause as intended. + **Every declared verdict derives; three kills flip (one + conditionally on R3-F1's registration).** + +## Regression spot-check (charge question 7) + +- **G1 legs (i)–(iii)**: (i) unchanged, GREEN derives (no marker + machinery engages). (ii) marker found → FAIL → re-derive → + record REPLACES — the clear now also deletes the marker; nothing + downstream in this history reads it; seal rows(e2)@V2 GREEN + derives, and the round-2 latent stale marker is gone. (iii) + MATCH, digest equal, publishBearing CLEAR (S1 neither reads nor + publishes sessions) → ADOPT → GREEN derives — writer-ineligibility + does not perturb the repair's flagship leg. `suppressionOff` + mutant: unchanged, P1-LEGALITY first-find with the R2-N1 declared + deviation. **Preserved.** +- **Fold back-port coherence (§4d)**: text unchanged and still + death-gated verbatim (the walker cell-4 caveat honored); the v2 + transfer-of-a-removed-contribution incoherence is unreachable now + that adoption requires a present marker and P-MARK ties the + marker to the partition's actual outputs; replacement-count + legality re-derived at one across every at-least-once redo walked + (the superseded round's copy leaves the count at its clear). + **Preserved.** +- **eAdopt atomicity**: still one store op; the v3 addition (marker + generation update inside the op) does not tear it; re-announce + attribution to the adopter unchanged; P5 ghost closure and + G-RULE-1 timing unaffected. **Preserved.** +- **P-GEN checkability**: the R2-N3 rule is recorded (G-RULE-3, §7) + and remains checkable; resume-path fencing (F4) intact — G1b and + `resumeCkptOff` well-formed. The MID-ATTEMPT minting path breaks + the rule's truth on an honest two-crash history — R3-F2, a new + v3-composition seam, not a regression of the round-2 verification + (which walked no post-bump-dispatch crash because the queue + semantics did not yet exist to walk). **Preserved on the round-2 + evidence base; extended surface broken — R3-F2.** +- **Inherited kills** (`sweepOff`, `sweepOverreach`, `purgeOff`, + `resumeCkptOff`, `overlayComposeDead`, `demandDropOff`, + `retractionOff`, `stampMergeOff`): none interacts with the v3 + pins in any walked history; G5e's count-oracle premise is + untouched by ∀-purge (its child has one dead edge). **Preserved.** + +## Round-2 minors/notes registration check (lighter touch) + +- **R2-M1** — session-read observation point registered in §3 as + read-through + derived dead-read announce + the writer-pending + invariant. APPLIED. (The invariant is load-bearing evidence in + R3-F1's reading-E history; under the R3-F1 pin it holds on every + honest history — re-verified.) +- **R2-M2** — derived-announce carrier pin registered in G-RULE-1; + resume bumps ride the forced checkpoint. APPLIED. (Durability of + the mid-attempt bump itself is R3-F2 — the carrier pin grounds + the ANNOUNCE, which is what R2-M2 asked.) +- **R2-M3** — per-sync marker scoping pinned in §3 (current-sync + store only; seal drops marker rows) and §4c's cross-sync wording + deleted; §5 row amended. APPLIED. +- **R2-M4** — §4b dead-base OVERLAY row reworded to scripted policy + + trust model, with `overlayComposeDead` as the load-bearing + check. APPLIED. +- **R2-M5** — adopting executions' premise re-reads register + normally, pinned in §3 and quantified into P6-E. APPLIED. +- **R2-M6** — (i) G-pending leg scripted and walked (derives); (ii) + session-transitive chains excluded with the keying-uniform + argument in §1 and restated in §8's inductive bet; (iii) + publish-derived demand excluded from G-RULE-1's vocabulary with a + change-order requirement. APPLIED. +- **R2-M7** — pass-iteration budget row in §8 (≤ 3), convergence a + checked expectation, budget-exhausted seal announce-visible. + APPLIED. (Iteration boundary wording — R3-M3.) +- **R2-M8** — poison voids the marker; post-poison rounds + classified; G8c's post-poison leg added and walked. APPLIED. + (Commit-order window — R3-M2.) +- **R2-N1/N2/N3** — eAdopt fromGen-dead precondition, digest-closure + vacuity sentence, P-GEN monitor rule: all recorded where the + dispositions specified. APPLIED. + +## Answers to the §11 round-3 charge + +1. **Writer-ineligibility convergence**: SOUND on every scripted + honest history under the operative (round-2-baseline) reading — + the dead writer is pending at resume, must complete before the + frontier drains, is barred from adoption by the bit, re-publishes + under its live generation, and retraction (E+B) or the + writer-stamp digest delta (S) clears every reader in one forced + re-run; the pass converges in ≤ 2 iterations in every walk, + including two-crash and announce-window placements; mid-attempt + writer death is unreachable in-envelope. The argument is + UNREGISTERED at its load-bearing step (does a replay-verdict + re-derivation re-publish) — R3-F1; and its "every honest + history" quantifier overreaches the mechanism for de-demanded + writers — R3-M4. Rows-owned-by-writers: cost-only, claim honest + — R3-N1. +2. **Quiesce composition**: no pass-budget starvation (deferrals + resolve strictly pre-drain), no two-worker deadlock (waits are + one-directional), crash-loss of a pending deferral self-heals + via the resume bump; G1d honest and mutant legs derive — R3-N2. + The COMMITTED-bump durability seam is R3-F2. +3. **Marker lifecycle totality**: total — every constructible + marker state and transition is classified by the §4b column plus + the per-sync and poison pins; P-MARK is preserved by every row + and fires exactly on the mutant; G8d honest and mutant derive + (one text misattribution, R3-M1); the eAdopt×poison window is + boundary-grade, R3-M2. No missing state found. +4. **Admitted-set death semantics**: compose cleanly — MUST-suppress + over pending∨completed with purge/refusal hash removal and the + after-removals completion rule closes both the starve and + double-admission gaps under two workers; G5f and G6c re-derive + as declared. +5. **MATCH-only adoption**: classifiable in every scripted cell — + every legal adoption's consult is its own MATCH (P2), P3′ + coheres through truthful validators, ADOPT is a registered + verdict class; G1c derives both legs and SealExpect is the only + oracle that can flip the mutant — the right oracle, confirmed. +6. **The corrected G2 table**: all seven legs derive; `quiesceOff` + and `markerCleanupOff` flip unconditionally for the stated + reasons; `writerAdopt` flips for the stated reasons ONLY under + the reading R3-F1 requires the spec to register — as written it + is unfireable under the equally honest elision reading. +7. **Regression**: round-2's verified-clean results preserved (G1 + legs, G2 axis legs, fold back-port, eAdopt atomicity, resume-path + P-GEN); the one break (R3-F2) is on a surface round 2 could not + walk, introduced by v3's own — otherwise correct — queue/quiesce + pins. + +## Verified clean (for the revision record) + +- The six round-2 majors are repaired as dispositioned and their + repairs compose in every scripted history walked; nothing in this + review re-opens a round-1 or round-2 disposition as applied, and + neither major is a defect in a repair's specified mechanism — + both are seams BETWEEN the repairs and pre-existing semantics + (elision vocabulary; generation-fencing discipline). +- The pass-iteration budget (R2-M7) is honest: every honest walk + converges in ≤ 2 of the budgeted 3 iterations, and the one + constructed budget-exhaustion (reading-E writer flap-back) is + exactly the class the budget-exhausted-seal red exists to catch. +- The §7.5 tally as amended reflects the v3 mechanisms accurately + (quiesce, queue semantics, eligibility pins, marker column, death + semantics, observation points + budget) and writer-ineligibility + is correctly charged to both variants as shared machinery in G6. +- G3, G4, G7, G9 are untouched by the v3 pins in every walked + history; G7's attempt-failure machinery composes with quiesce + (a loud failure's checkpoint captures pending state; a + never-committed deferral mints nothing and self-heals). +- Public-repo hygiene: clean. No customer names, tenant + identifiers, or internal infrastructure in v3 or in this review. + +## Summary + +| finding | severity | one line | disposition | +|---|---|---|---| +| R3-F1 | MAJOR | session-publish semantics under replay-verdict re-derivation unregistered; `writerAdopt` kill unfireable under the elision reading and the convergence claim fails on a budget-legal writer flap-back; all declared verdicts derive under the round-2-baseline body-op reading | register the body-op pin (+ F17 boundary sentence), register the announce-window chassis, add the writer flap-back probe; fix-without-re-review | +| R3-F2 | MAJOR | mid-attempt-minted generations not durably fenced: bump → dispatch → durable commit → crash-skip-checkpoint reuses the id; P-GEN red on honest two-crash history; G-RULE-3's "durably fenced" false | force durability of the bump's table delta before dispatch (checkpoint-at-bump or durable-max resume rule), amend §5, add reuse probe + fence kill; fix-without-re-review | +| R3-M1 | MINOR | G8d honest leg says "fetch-fresh" where truthful validators derive REPLAY (its own mutant text concedes MATCH); verdict unchanged | correct the cell text; fix-without-re-review | +| R3-M2 | MINOR | eAdopt × poison check-then-act window; "never legal" overclaims relative to worker-side check | store-side eAdopt poison precondition or boundary-note wording; fix-without-re-review | +| R3-M3 | MINOR | pass iteration boundary (scan from a drained frontier) implied but unstated | one-sentence pin; fix-without-re-review | +| R3-M4 | MINOR | convergence prose quantifies over histories the mechanism doesn't cover (de-demanded writer × sync-scoped session value); unreachable in scripted cells | scope the claim; declare the sessions × shrink exclusion in the inductive bet; fix-without-re-review | +| R3-N1 | NOTE | at-least-once cost claim for writer row re-derivation verified honest (count-legal, suppression-safe, verdict-neutral) | record | +| R3-N2 | NOTE | deferral non-starvation / non-deadlock arguments verified; record the two-line argument in §3 | record | +| R3-N3 | NOTE | round-2 minors/notes all applied as dispositioned (sweep above) | record | diff --git a/formal/reviews/model-spec-round1.md b/formal/reviews/model-spec-round1.md new file mode 100644 index 000000000..fea995cc8 --- /dev/null +++ b/formal/reviews/model-spec-round1.md @@ -0,0 +1,34 @@ +# Model spec adversarial review — round 1 + +Reviewer: independent agent (decorrelated from the spec author's session +context; formed its view from the brief, the frozen 6b contract, and the +code). Verdict: **REJECT** — 2 blockers, 8 majors, 3 minors, 2 notes. +Spec revision v2 addresses every finding; dispositions below. The full +review text is preserved in the review agent's transcript; this file +records the findings and their dispositions for the freeze record. + +| # | severity | finding (condensed) | disposition in spec v2 | +|---|---|---|---| +| 1 | BLOCKER | Restart-from-root does not fall out: the spec had workers return per-page continuations applied to the stack, so loop-top checkpoints could capture mid-chain cursors — a resume granularity hard crashes don't have (CO-6b-002). | §3 MWorker now owns an action's entire page chain (mirrors `syncOneAction`); stack entries keep root tokens until finish; loop-top checkpoints therefore contain only root tokens. Graceful-stop checkpoints (new, finding 3) are the one place mid-chain state becomes durable, and CO-6b-002's "unreachable in practice" claim becomes an explicit conformance question (§9 C1). | +| 2 | BLOCKER | P1 monitor vacuous: the "declared composition log" was the announced store ops, whose fold trivially equals the partition; the attestation clause was undefined and cannot be defined from the wire vocabulary (legit delta overlay and phantom union are wire-identical). | §7 P1 rewritten: declared composition = connector-intent ghost provenance per page (verdict class + consult epoch, an honest label of the policy decision that already drives emissions); contractual fold (one replacement per scope per sync, overlays compose only with this-sync replay); attestation = published validator's epoch equals the fold result's epoch. Honesty argument added; §10.5 mutation-adequacy check extended. | +| 3 | MAJOR | Graceful-stop forced checkpoints missing (run-expiry / external cancel force-checkpoint an aborted batch's mid-flight state — hits, spawns, mid-chain cursors); spawn admission timing wrong (same-op spawns drain in the live batch, so pending carriers arise from batch-cap splitting, not batch-end admission). | §3: eStop interruption mode added with forced checkpoint of live state; same-op spawns admitted to the live batch queue (drained in-batch), cross-op spawns to the stack; §9 case 1's pending-carrier premise re-scripted on batch-cap splitting (CO-6b-006's construction). | +| 4 | MAJOR | Replay-blocked flag given op-commit durability the real mechanism lacks (rides ingest-quality state, checkpoint-cadence durable); composition-enum detection modeled store-side/omniscient masks two real windows. | §5 table corrected (checkpoint-cadence durability); §4 deliverable-3 semantics remodeled as syncer-side detection over volatile/checkpointed state (weak variant is the default fix-run configuration); §10.6 obligation extended to detection-state visibility and mark durability. | +| 5 | MAJOR | Produce-side blocking condition broader than contract ("any gate-outcome difference" vs the two real triggers) — over-blocking masks multi-sync propagation. | §3 restated to exactly: (i) compat key recomputed differently across attempts (B4); (ii) attempt without source-cache handling over prior-attempt produce state (CO-6b-003), fail-closed on read error. Consume-side degradation alone never blocks. | +| 6 | MAJOR | P3′ strengthening false for multi-page fetches under mid-attempt mutation (crash-free walks observe multiple instants per scope). | §7 P3′ scoped: checked only in scenarios without mid-attempt mutation; mid-attempt-mutation scenarios rely on P1/P2; per-page refinement noted as future work. | +| 7 | MAJOR | MEnv had no attempt-failure behavior; neither P4 hang shape (deterministic cold re-failure; leaked-lock deadlock) was representable; livelock undetectable in a bounded scenario via raw liveness. | §3 MEnv resume-on-failure rule added; §4 warm-verdict page failure + in-attempt retry added (lock scenario); §7 P4 livelock detection restated as a safety rule (two consecutive resumes failing from identical restored state with identical verdict). | +| 8 | MAJOR | Crash-vs-in-flight race underspecified for multiple outstanding ops (non-prefix-closed committed sets possible); "queue drained" ambiguous. | §5 pinned: eCrash's position in MStore's queue partitions dead-attempt ops — before = committed, after = dropped; per-sender FIFO preserves per-worker prefix closure; quiesce = drop, never process. | +| 9 | MAJOR | Hit-recording semantics unpinned (record-at-lookup-hit regardless of revalidation outcome; last-write-wins; P2's "validated" = upstream match, not lookup hit) — case 3 depends on all three. | §3/§7 pinned with code citations; §9 case 3's "also exhibits with no re-consult" scoped to `hitValidatorBinding` off. | +| 10 | MAJOR | Carrier-less phantom variant missing: copy commits durably mid-batch, crash before checkpoint → durable debris with no durable replayed mark; attempt-2 fresh fetch upserts over it. More reachable than the scripted variant; defeats syncer-side composition detection. | §9 case 1 gains the carrier-less variant as a first-class cell; flagged as the priority trace to compile into a chaos scenario (deliverable 6). | +| 11 | MINOR | §2.1 vs §3.3 disagreement on batch composition as a choice point; cap-shrinking argument implicit. | §3: batch cap nondeterministic ∈ {1..capMax} per iteration; small-scope scaling argument stated (CO-6b-006's 102-action construction is the existence proof). | +| 12 | MINOR | P6-A fired on any later-attempt divergence (d1→d2→d1 false positive). | §7 P6-A compares embedded stamps against the writer's FINAL derived value at seal. | +| 13 | MINOR | Case 1 interleaving B's expected verdict depends on unpinned carrier publish behavior. | §9 splits interleaving B into two cells: carrier publishes V1 → P3′ violation (P1 green); deferred publish → P1 attestation violation. | +| 14 | NOTE | annotationBinding fix run silent on empty-validator replay pages (legal per proto). | §9 case 3 fix run gains an empty-annotation cell; the fix's coverage boundary is a required output of the run. | +| 15 | NOTE | §5 checkpoint-token row understated (ingest quality incl. replay-blocked reason flags is load-bearing for deliverable 3). | §5 table row added. | + +Reviewer's stated residual risk (recorded verbatim in substance): the +composition enum exists only in PR discussion; the model must choose its +durability/detection semantics, and an eventual implementation is not +bound by that choice. The carrier-less phantom trace must be compiled to +a chaos scenario and kept green against the real implementation, or a +model-green "propagation bounded" claim can be true of the model and +false of the shipped mitigation. diff --git a/formal/reviews/model-spec-round2.md b/formal/reviews/model-spec-round2.md new file mode 100644 index 000000000..914c6e46d --- /dev/null +++ b/formal/reviews/model-spec-round2.md @@ -0,0 +1,46 @@ +# Model spec adversarial review — round 2 (of spec v2) + +Reviewer: independent agent (fresh, decorrelated; built its own model +from the 6b contract and code before reading the spec). Verdict: +**REJECT** — 2 blockers, 4 majors, 4 minors, 3 notes. It verified 12 of +the 15 round-1 dispositions as genuinely resolved (1, 4, 5, 7–15) and +confirmed all of v2's code citations and the durability table. Spec +revision v3 addresses every finding; dispositions below. Both blockers +were scenario-REACHABILITY defects — the machine semantics were right, +and the scripts contradicted them — which is why v3 adds the standing +reachability-walk obligation (§2.6, §10.0). + +| # | severity | finding (condensed) | disposition in spec v3 | +|---|---|---|---| +| 1 | BLOCKER | Scenarios 1a/1b and 3 unreachable as scripted: a same-op spawn can never be stranded at a loop-top checkpoint (spawns drain in-batch, `parallel_syncer.go` 656–697), a mid-batch spawn admission is volatile under hard crash, and the pre-pushed-carrier alternative needs connector cross-call state the spec forbids. The reachable premise generator is the graceful-stop forced checkpoint. | §9 re-scripted on the STOP-STRANDING pattern: the stop-forced checkpoint durably captures {parent mid-chain cursor, admitted-but-undrained carrier, hit map} together (checkpointability pinned by `TestSpawnedActionsSurviveCheckpoint`); the attempt-2 re-consult comes from the parent's next page. Verdicts route through page tokens (no cross-call memory); the purity-vs-proto boundary is recorded in §8. Verified against code before adoption. | +| 2 | BLOCKER | Scenario 2's hard-crash cell claimed expected-green; false — session KV is durable, pops are volatile, so both actions re-run, and the G-before-H schedule (a genuine choice point) re-embeds stale d1 → P6-A alarms. Real shipped behavior; freezing the green expectation would force the P code to mask it. | §9 case 2 split into 2-stop (deterministic script) and 2-crash (interleaving-dependent expected FINDING, with the complementary H-first schedule required green). The false parenthetical claim removed. | +| 3 | MAJOR | P1 fold under-specified where it decides alarm-vs-miss: per-page vs per-round granularity, undefined epoch for partially applied delta rounds at publish (plan B5 legally publishes the new token before overlay pages land), deterministic-fold vs existential formulations mixed. | §7 P1 fully pinned: round definition (maximal same-verdict run of one chain's pages), complete-rounds-only deterministic fold in commit order (replacement/overlay/fresh rules; fresh REPLACES in the fold), log-legality rules, content+attestation+config checks at seal, ATTESTATION-ONLY at publish. | +| 4 | MAJOR | P3′ scoping incomplete: a fresh round torn mid-chain by a stop and resumed after between-attempt mutation observes two epochs inside the allowed scenario class → spurious counterexamples contaminating calibration attribution. | §7 P3′ doubly scoped: no mid-attempt mutation AND no torn round for the scope (ghost torn-round flag). 1b-i's P3′ verdict re-checked under the new scoping (its rounds are not torn). | +| 5 | MAJOR | Scenario 3's claimed P2 violation contradicts P2's own pinning: attempt 1's validation match qualifies the scope as consulted this sync, so P2 passes; the actual catch is P1-attestation. Interleaving with attempt 2's fetch-fresh round was underived. | §9 case 3 re-derived per cell: P1 attestation (carrier last) or P1 content (carrier first/interleaved); P2 explicitly GREEN in every cell; 3B (binding-off) premise simplified to the no-re-consult route. | +| 6 | MAJOR | `warmGate` had no killing scenario (all scripted attempts were warm), so §10.5's mutation-adequacy obligation was unsatisfiable; produce trigger 1 also unexercised. | §9 scenario 5 added: 5a compat-drift cell (trigger 1) kills warmGate via the new P1 config clause; 5b capability-withdrawal cell exercises trigger 2 fail-closed. §6 gains an explicit kill-obligation table. | +| 7 | MINOR | G6 (capability) folded into a static "previous artifact usable" boolean, making produce trigger 2 unreachable. | §1/§3: G6 is a per-attempt bit on MEnv's schedule; warm install and trigger 2 consume it; scenario 5b exercises it. | +| 8 | MINOR | §4's replayed-set check missing its `[toggle: oncePerScope]` marker. | Marker added. | +| 9 | MINOR | Glossary "Restart-from-root" and "Checkpoint" entries state the hard-crash rule as universal, contradicting the spec's (correct) stop-path semantics; the glossary is read first. | Both `GLOSSARY.md` entries corrected: forced sites enumerated, stop-checkpoint contents named, restart-from-root scoped to hard crash. | +| 10 | MINOR | C1 had no carrying scenario (no policy places a replay annotation on page ≥ 2), so it would resolve vacuously. | §9 scenario 5 gains the C1 probe: replay annotation on page 2, stop between pages, resume without fresh consult — model answers C1 "reachable via the stop path", recorded as a conformance finding to test through the chaos bridge. | +| 11 | NOTE | Session KV shares MStore's durability domain/crash cut in the model; prod session store is a separate, non-prefix-ordered service. | §8 records the trust boundary of P6-A's verdicts (brief pins variant A durable). | +| 12 | NOTE | "Intent" named both the P1 ghost label and the deliverable-3 wire enum; the ghost exists with `compositionEnum` off. | Ghost renamed "verdict class" throughout; "intent" reserved for the wire enum (§4 states the reservation). | +| 13 | NOTE | Batch-cap argument overclaimed "every split shape"; parts ≥ 3 unexplored; the true justification (in-batch spawn admission is uncapped; multi-way splits decompose into two-way splits) was unstated. | §1 argument rewritten with the honest justification. | + +Residual risks recorded by the reviewer (carried into the calibration +report's obligations): + +1. The composition enum exists only in PR discussion; the 1c trace must + be compiled to a chaos scenario and kept green against the real + implementation, or a model-green "propagation bounded" claim can be + true of the model and false of the shipped mitigation. (Carried + from round 1; §4 and §10.6 keep it explicit.) +2. Cross-attempt fresh-debris unions (no replay involved) are real + shipped behavior adjacent to scenario 1's config; §9 now + pre-commits to classifying out-of-script counterexamples of that + shape as design findings, not model noise. +3. Connector purity under-approximates legal connector behavior + (within-sync memory); extensions are change orders (§8). +4. The pre-seal forced checkpoint site, which the reviewer accepted + from the spec's assertion, was verified directly during disposition: + `syncer.go` 1162 (post-expansion, pre-seal, force=true), alongside + Init (232/268/1115) and stop/expiry (195/469/490). diff --git a/formal/reviews/model-spec-round3.md b/formal/reviews/model-spec-round3.md new file mode 100644 index 000000000..6d19e138a --- /dev/null +++ b/formal/reviews/model-spec-round3.md @@ -0,0 +1,47 @@ +# Model spec adversarial review — round 3 (of spec v3) + +Reviewer: independent agent (fresh, decorrelated; built its own model +from the 6b contract and code before reading the spec). Verdict: +**REJECT** — 1 blocker, 0 majors, 4 minors, 2 notes. Substantial +convergence from round 2: the reviewer mechanically walked ALL 18 §9 +scenario cells (reachability + verdict derivation, applying the P1 fold +by hand) and confirmed 17; it verified 12 of 13 round-2 dispositions as +genuine (the 13th — finding 6 — was half-resolved: the warmGate leg +held, the trigger-2 leg is this round's blocker). Spec revision v4 +addresses every finding; dispositions below. + +| # | severity | finding (condensed) | disposition in spec v4 | +|---|---|---|---| +| F1 | BLOCKER | §4/§9-5b scripted "fail closed: loud attempt failure" for a replay annotation arriving in a capability-withdrawn attempt. Ground truth: B1 ignores every source-cache annotation wholesale when capability is absent (`sourceCachePageOps` returns nil ops when `sourceCacheEnabled()` is false, `source_cache_orchestration.go` 463–468 — verified directly during disposition); the CO-6b-005 capability-withdrawn chaos cell completes GREEN with the artifact replay-blocked; the block fires at install time (CO-6b-003), not page arrival. §3 (install-time) and §4 (page-arrival) also contradicted each other. Frozen as-is: a machine-checked falsehood, and the cell masks the real shipped hazard (silent annotation drop under a green sync). | §4 bullet rewritten to B1 silent-ignore with trigger 2 pinned to install time; §9 5b re-scripted: green seal, cold consults, empty partition for S, artifact blocked — with the SILENT SCOPE DROPOUT promoted to a required design finding whose oracle is the scripted seal-state expectation (see F7). | +| F2 | MINOR | P1 fold order unanchored for page-interleaved rounds (first- vs last-page commit); all scripted logs agree under either anchor, but out-of-script counterexample logs — exactly what §9's pre-committed classification covers — need not. | §7: fold order pinned to ROUND COMPLETION (commit of a round's last page), with the rationale stated. | +| F3 | MINOR | §3 said completed actions pop "at batch end"; in code the pop commits at completion, mid-batch (state commit ~656 precedes queue admission ~667) — and the spec's own 2-stop cell depends on the mid-batch pop. §2.5 violation (correct semantics lived only in the eStop paragraph and the script). | §3 bullet 4 and MWorker corrected: pops commit at completion, mid-batch; the 2-stop dependency is named in place. | +| F4 | MINOR | §5 and the glossary overclaimed "under hard crash the surviving checkpoint contains only root tokens" — false for stop-then-crash, where a stop-forced checkpoint with mid-chain cursors/spawns survives a later hard crash. | §5 resume paragraph and the glossary entry restated: restart-from-root is a property of WHICH checkpoint survives (crash-only histories), not of the crash itself. | +| F5 | MINOR | Spawn dedup modeled only on cross-op stack pushes; code applies the spawnedAdmitted guard to ALL spawned admissions plus commit-local duplicate-cursor rejection. Without it, legal connector re-mentions produce spurious P4 livelock counterexamples. | §3 bullet 4: dedup extended to all admissions (re-mentions skipped, not errors) plus the commit-local loud rejection; dedup index volatility unchanged. | +| F6 | NOTE | Stop-stranding window stated too narrowly ("before a worker dequeues"); any stop before the carrier's first atomic step completes qualifies. Documentation-only (the scripted route was reachable). | §9 preamble widened. | +| F7 | NOTE | Even corrected, 5b has a shape invisible to P1 (vacuously green — no legal round) and P2 (quantifies over rows present): the carrier's scope rows silently missing from a green seal. | Folded into the 5b re-script: the dropout is the cell's required design finding, checked by the scripted seal-state oracle; a general completeness/coverage oracle is recorded as a deliverable-6 chaos-bridge question. | + +§9 cells walked and confirmed by the reviewer (reachability + verdict): +1a, 1b-i, 1b-ii, 1c, the 1-fix cells including the crash-window +variant, 2-stop, 2-crash (both schedules — the G-first alarm and the +required-green H-first), 3A (both interleavings), 3B, the 3-fix cells +including empty-validator, 4 (locks off and on), 5a (including the +warmGate-off mutant kill via the P1 config clause), the C1 probe +(checkpoint token carries `sourceCacheHits` per `state.go` Marshal, so +the restored hit map passes the SDK hit check — C1 is genuinely +reachable via the stop path). 5b was walked and refuted (F1). + +Residual risks recorded by the reviewer (carried into the calibration +report's obligations): + +1. The 1c compositionEnum trace must be compiled to a chaos scenario + and kept green against the real implementation (carried from rounds + 1–2). +2. The fold-anchor pin (F2) matters precisely on out-of-script + counterexample logs; keep it in view when classifying them. +3. Connector purity under-approximates legal within-sync connector + memory (carried; §8 records it). +4. The silent-missing-rows shape behind 5b is invisible to P1/P2 by + construction; the scripted seal-state oracle covers the calibration + cell, and the general question is a deliverable-6 obligation. +5. The empty-validator vacuous-binding residual from the scenario-3 fix + runs remains open by design. diff --git a/formal/reviews/model-spec-round4.md b/formal/reviews/model-spec-round4.md new file mode 100644 index 000000000..257c68b15 --- /dev/null +++ b/formal/reviews/model-spec-round4.md @@ -0,0 +1,56 @@ +# Model spec adversarial review — round 4 (of spec v4) + +Reviewer: independent agent (fresh, decorrelated; built its own model +from the 6b contract and code before reading the spec). Verdict: +**REJECT — 0 blockers, 0 majors, 3 minors, 3 notes**, with the explicit +disposition that all three minors are fix-without-re-review: "land the +three edits and freeze; no further full round is warranted." All seven +round-3 dispositions were verified as genuinely resolved against ground +truth (not just present); the corrected 5b cell was walked end to end +against code and the CO-6b-005 chaos cell; the fold was re-applied by +hand across 1a/1b-i/1b-ii/1c/3A/3B/4; every cited line number and test +pin was confirmed to exist. Spec revision v5 applies all six findings; +dispositions below. + +| # | severity | finding (condensed) | disposition in spec v5 | +|---|---|---|---| +| 1 | MINOR | v4's own dedup extension (F5) made cell 4's "duplicate replay carriers" premise unreachable as written: byte-identical duplicates are rejected commit-locally within one transition or skipped by the spawnedAdmitted guard across transitions. The realized ground-truth shape (CO-6b-003/005 chaos instruments) uses pages from DIFFERENT resources targeting one scope — distinct identity digests. | §9 cell 4 premise pinned: byte-distinct page tokens encoding the same (scope, verdict); literal duplicates explicitly noted as unable to produce the premise. | +| 2 | MINOR | P1's strict overlay precondition (fold value must equal rows(e_from)) alarms on cell 4's scripted GREEN locks-on run: B5 legally copy-skips the duplicate replay and applies its upserts normally, making the second overlay round's precondition unsatisfiable — the fold ambiguity F2 was meant to kill, surviving in the passage F2 edited. | §7 fold gains the duplicate-tolerance clause: a copy-skipped overlay round folds as a NO-OP when the fold value already equals rows(e_to); replacement-count legality counts COMMITTED copies, not verdict labels (locks-off's two committed copies alarm; locks-on's copy-skip folds green). | +| 3 | MINOR | "Produce-side blocking — exactly two triggers, nothing broader" is false of the system: page-arrival shape guards (child-resource declarations, `InsertResourceGrants`), ingest-filter drops (B6), and unknown-prior-checkpoint conservatism also block. None are reachable in the model, so no cell verdict was wrong — but frozen as a fidelity statement it is a falsehood readers and model extensions inherit. | §3 scoped to "within the modeled fragment" with the excluded triggers named; §1's abstraction list registers them with the reason each is unreachable in the model. | +| 4 | NOTE | 5b's "matching the CO-6b-005 chaos cell" over-attributed: the chaos cell pins green completion, cold consults, compat retention, and the blocked mark — NOT the empty-partition dropout (its connector adapts cold on miss; no stranded carrier exists there). | §9 5b narrowed: the four pinned outcomes attributed to the chaos cell; the dropout attributed solely to the model's scripted seal-state oracle. | +| 5 | NOTE | "Loop-top checkpoints contain only root tokens" is true of the modeled (batched) population only; sequential non-fanned ops (e.g. `SyncResourceTypesOp`) can checkpoint mid-chain at loop tops in crash-only histories. | §8 boundary note added; future scenarios touching sequential ops must not inherit the claim. | +| 6 | NOTE | The commit-local duplicate-cursor rejection is same-op-scoped in code (`queue.transition` checks only children with the batch op); §3 stated it unqualified. | §3 qualifier added. | + +Verification coverage reported by the reviewer: round-3 dispositions +F1–F6 + §11 all verified genuine (F1 against `sourceCachePageOps` +463–468, the proto's capability wording, `installSourceCacheLookup`'s +install-time block, and `Checkpoint`'s `SetIngestQuality` durability; +F4 against the derived stop-then-crash ground truth). Cells re-walked +by hand: 1a, 1b-i, 1b-ii, 1c (+ fix-run claim boundaries), 2-stop, +2-crash (both schedules), 3A (both interleavings), 3B, 4 (all +togglings — findings 1 and 2 live here), 5a (both togglings), 5b (end +to end), C1 probe. Freeze sweep: §5 table vs `state.go` +Marshal/Unmarshal (warm flag genuinely absent from the token), §6 kill +table vs cells, §7 vocabulary vs events, §11 vs all round logs, +glossary vs spec and code. + +Residual risks recorded by the reviewer (carried into the freeze +record and the calibration report's obligations): + +1. No v3/v4 baseline exists to diff — `formal/` is untracked, so the + "surgical delta" claims were verified by re-walking, not + mechanically. COMMIT THE SPEC AT FREEZE so change orders have a + diffable baseline. +2. The composition enum's semantics exist only in PR discussion and + could not be verified against any repo artifact; the spec labels + this an input assumption (§4). Confirm before deliverable 3's runs; + keep the 1c trace compiled to a chaos scenario (carried from rounds + 1–3). +3. The silent-dropout shape behind 5b has no chaos-suite analogue; the + deliverable-6 completeness-oracle question is the only path to an + executable check. +4. Carried: fold-anchor discipline on out-of-script logs (now with + finding 2's clause), connector-purity under-approximation, the + empty-validator vacuous-binding residual in the scenario-3 fix runs. +5. Finding 5's boundary: future scenarios touching sequential ops must + not inherit the batched-population checkpoint claim. diff --git a/formal/reviews/model-spec-round5-addenda.md b/formal/reviews/model-spec-round5-addenda.md new file mode 100644 index 000000000..42007b591 --- /dev/null +++ b/formal/reviews/model-spec-round5-addenda.md @@ -0,0 +1,84 @@ +# Round 5 — targeted spot review of the v6 signoff-discussion addenda + +Scope: §9 cell 1d, §9 scenario 6 (collect-and-commit variant pair), +§7 fold-totality clauses (self-grounding overlay rounds, copy-skipped +replacement rounds), §10.5 update — per the §11 v6 change-order entry. +Not a fifth full round; v1–v5 material was out of scope except where +the addenda destabilize it. + +Method: mechanical reachability walk of each new premise under +§3/§4/§5; hand-application of the extended §7 fold over every schedule +class each cell can produce; coherence sweep against rounds 3–4; +verification of the copy-skip publish and validator-less replay +citations against `pkg/sync/source_cache_orchestration.go` and plan B5. + +Verdict: **REJECT — 2 majors (F1, F2) + 6 minors, ALL +fix-without-re-review** per the round-4 convention; no further round +warranted. Positive results: all three premises reachable without +hand-placement; the extended fold is total and deterministic over +every addenda log once F1's pin lands; 1d's +content-green-every-schedule claim survived adversarial schedule +construction under shipped toggles; no contradictions with the +round-3/round-4 dispositions. + +Findings and dispositions (all applied as v7): + +- **F1 (MAJOR)**: "round completion — the commit of a round's last + page" never defined page COMMIT; store-op vs scheduler-transition + readings diverge for the first time on 6-atomic's verdict (unit + committed, transition lost, re-execution marker-suppressed → + ops-reading green as scripted, transition-reading false-alarms). + → §7 pins: a page commits when the last of its prescribed STORE ops + commits (announce-visible); transitions/pops are not fold events; + no-store-op rounds contribute no fold entry. Scenario 6 pins the + variant-specific consequences (V-ATOMIC round complete at + `eReplayUnit` commit; V-NAIVE's marker op is a prescribed round op). +- **F2 (MAJOR)**: 1d attributed the same-base-copy collapse to + `oncePerScope` alone and called it "structural"; the check-then-mark + collapse is atomic only under `scopeLocks` (case 4's dual-replay + TOCTOU — a locks-off 1d schedule is content-red, walked concretely). + → re-attributed to `oncePerScope` ∧ `scopeLocks`, "structural" + demoted to mitigation-dependent, locks-off 1d mutant added to §10.5. +- **F3 (minor)**: validator-less-C sub-case mis-scripted — whether C's + copy commits is schedule-dependent, not a property of + validator-lessness (C-first commits; verdict green either way). + → re-scripted per schedule class, aligned with §7's no-op vocabulary. +- **F4 (minor)**: 1d left V2's publish placement unpinned, so the + green/alarm schedule partition wasn't enumerable (B5 early publish + makes publish order ≠ drain order). → both placements pinned as + explored sub-configs; partition stated per sub-config. +- **F5 (minor)**: the §7 torn-round boundary justification argued only + from stop-consumption; scenario 6 is crash-based. → extended: + crash-only histories resume from root-token checkpoints, cannot + tear; incomplete-round debris named in the fold text (6-naive rests + on it). +- **F6 (minor)**: V-ATOMIC under-specified its relationship to the §5 + checkpoint token; "hit durability can never precede materialization" + was false for the still-checkpointed hit map (true of the marker). + → pinned: token unchanged, restored hits never authorize without a + consult, stranded hits inert, clause (iii) applies to both variants; + wording corrected to durable consult PROVENANCE (the marker). +- **F7 (minor)**: §11 over-scoped 6-atomic's green claim to "the + case-1 family", which now contains 1d, whose overlay flavor V-ATOMIC + doesn't define (stale-AHEAD hazard of marker-before-overlay). + → de-scoped to the 1a/1b/1c re-runs; overlay-flavor unit semantics + declared an explicit deliverable-4 obligation. +- **F8 (minor)**: §7 P2's consult pin (validation match or fresh + fetch) excluded 1d's changed-with-diff verdict, making P2 alarm + every 1d schedule unscripted. → pin extended (revalidation occurred + and the diff is an upstream fetch); 1d's P2 expectation stated + (green, staleness ≤ 1). + +Notes to the freeze record: **N1** partial-copy debris renamed to +avoid colliding with §7's cross-attempt TORN. **N2** the +self-grounding clause retroactively closes a latent v5 fold-totality +gap (case 4's locks-on surviving round satisfied no v5 clause); +round-4's fold-walk coherence is now documented rather than +accidental. **N3** the 1d flavor-coverage note (delta connectors +degrade to fetch-fresh on token expiry) labeled external-boundary +commentary — a connector-population claim outside the model. **N4** +V-ATOMIC's marker-check-before-consult sits outside the scope lock — +a check-then-act window that two concurrent consulting actions for +one scope would race (two committed copies → P1 legality alarm); +unreachable in the scripted family, recorded as a 2-worker hazard for +the deliverable-4 bake-off boundary notes. diff --git a/formal/reviews/model-spec-round6-scenario7.md b/formal/reviews/model-spec-round6-scenario7.md new file mode 100644 index 000000000..518882e3d --- /dev/null +++ b/formal/reviews/model-spec-round6-scenario7.md @@ -0,0 +1,91 @@ +# Round 6 — targeted spot review of the v8 addendum (scenario 7) + +Scope: §9 scenario 7 (cells 7a/7b/7c + fix runs), §7 P6-R, §8 +counterfactual-ghost note, §6 `sessionTaintWrites`/`sessionTaintAll` + +kill obligations, §10.5 additions, glossary entries "Elision" and +"Session taint" — per the §11 v8 change-order entry. Not a full round; +v1–v7 material out of scope except where the addendum destabilizes it. + +Method: mechanical reachability walk of all cells and fix runs under +§3/§4/§5; hand-application of the fold (round-5 F1 pin), P2, P3′, +P6-A, and P6-R over every schedule the cells produce; coherence sweep +against rounds 3–5; verification of every shipped-system claim against +`source_cache_orchestration.go`, `session.proto`, `sessions.go`, +`pebble/session_store.go`, `session_server.go`, and the 6b plan. + +Verdict: **REJECT — 4 majors + 5 minors + 3 notes, ALL +fix-without-re-review**; no round 7 warranted. Verified clean: all six +shipped-system claims true (no sessions × source-cache coupling; +sync_id-scoped namespaces; ungated session RPC during listings); every +cell and fix-run verdict re-derives as scripted under the recoverable +readings; 7c's green is entailed by policy purity, not assumed; +round-5 F1 and the torn-round boundary undisturbed; kill tables +internally consistent; "R violates no pinned obligation" consistent +with the verdict-as-data and trust-boundary framing. + +Findings and dispositions (all applied as v9): + +- **F1 (MAJOR)**: scenario 7 silently widened §1's one-row-kind + abstraction — two kinds require two OPS for the sequential-phase + premise (same-op W/R would batch together and interleave), plus + per-kind produce/consume state, none declared. → KIND axis declared + in §1 as an (op, scope) pair riding on an unchanged storage row-kind + axis; small-scope table row added (2 kinds in scenario 7 only); §8 + note updated; taint state scoped by the axis. +- **F2 (MAJOR)**: P6-R's stamp-travel extension put traveled stamps + inside P6-A's quantification domain, where two readings diverge on + 7b and the ⊥ stamp is ill-typed. → P6-A domain pinned: only stamps + embedded by reads within the sealing sync; traveled and ⊥ stamps + belong to P6-R; P6-A vacuously green on 7a/7b/7c. +- **F3 (MAJOR)**: taint durability unstated (5b/CO-6b-003 precedent + requires pinning); the volatile-until-seal reading silently breaks + the 7a kill obligation in interrupted histories. → pinned + checkpoint-cadence (ingest-quality-style) in §5 and §6, with the + self-healing argument (re-execution re-records) claimed explicitly — + stronger than `compositionEnum`'s detection-evidence story. +- **F4 (MAJOR)**: "replay-capable kind's phase" ambiguous; the + warm-install reading makes the 7a fix run unsatisfiable (sync N is + cold, so no taint would record). → pinned: flow membership, + independent of the recording attempt's warm/cold state; fix-run text + states the taint records in a cold attempt by design. +- **F5 (minor)**: consume-side enforcement unspecified and "runs W + cold" collides with §4's loud COLD verdict. → §3 `eLookup` extended + (tainted kind → miss; degradation, never loud); §9.7 wording + clarified. +- **F6 (minor)**: §7's P6-R scoping ("no mid-attempt mutation") + admitted between-attempt mutation §8 declares unsound. → aligned to + between-sync-only (single epoch per (sync, scope) per sync). +- **F7 (minor)**: counterfactual unpinned for multi-write producers; + reader-timing independence implied, not stated. → pinned as the + producer's phase-final value under empty-namespace evaluation; + no-cross-op-spawn config constraint and timing independence stated. +- **F8 (minor)**: "divergent rows" cannot diverge in content under + §3 (policies never choose content); ghost-only divergence forced but + unstated. → stated in 7a plus a §8 under-approximation note (the + real system has no content oracle for fresh rounds either — the + finding survives the abstraction). +- **F9 (minor)**: 7a's "kind outside the replay flow" premise route + is unmodeled. → struck; route pinned to "policy fetches fresh." +- **F10 (NOTE)**: shipped session surface includes + Delete/DeleteMany/Clear; model reaches Get/Set only, newly + load-bearing for the taint definition. → registered in §1; §6 WRITE + defined as any mutating op. +- **F11 (NOTE)**: fix runs re-execute the full two-sync script with + the toggle ON (first produce-side toggle family — acts a sync before + the red verdict); was implied only. → stated in §9.7. +- **F12 (NOTE)**: §3's `eCopyScope` ghost tuple lacked the embedded + session stamp P6-R requires to travel. → extended: lineage fields + added, embedded stamps copied unchanged. + +Also applied in the same v9 revision (signoff-discussion items, not +review findings): the capability-level OPT-OUT (attested +emission-irrelevance; a dishonest opt-out reproduces 7a/7b exactly) +and the out-of-model enforcement layers (static analyzer, pre-release +`SessionStoreUsage` conformance assert, runtime taint — one detector, +three timings). + +Process observation (from the review, for the freeze record): v6 and +v8 share a pattern — new machinery arriving through scenario scripts +rather than through §1/§3/§5 first. Scenario cells were sound both +times; the undeclared machinery beneath them was where all four majors +lived. diff --git a/formal/reviews/model-spec-round7-overlay-parallel.md b/formal/reviews/model-spec-round7-overlay-parallel.md new file mode 100644 index 000000000..2d807dd93 --- /dev/null +++ b/formal/reviews/model-spec-round7-overlay-parallel.md @@ -0,0 +1,332 @@ +# Round 7 (parallel) — second independent spot review of the v10 addendum + +PROVENANCE NOTE: two independent round-7 spot reviews of the v10 +addendum were run in parallel; this is the second, surfaced after the +primary (`model-spec-round7-overlay.md`) had been dispositioned as +v11 and the spec frozen. Its findings are dispositioned as MS-CO-001 +(see MODEL_SPEC §11): the overlapping findings (its F1 ≈ primary F3, +F2 ≈ primary F4, F3 ⊂ primary F5) were already fixed in v11; the +non-overlapping items (F4, F5, F6, N1, N4) are applied by the change +order. Its "third-placement reduction: sound" verdict DISAGREES with +the primary's F2 (false reduction); the built cell `tc6overlayLast_P1` +settles the disagreement mechanically in the primary's favor — see the +MS-CO-001 entry. Text below is the review verbatim. + +--- + +Scope: §9.6 V-OVERLAY-UNIT declaration (pins o-i..o-iv), cells +6-overlay and 6-overlay-naive, the §5 overlay-collect-buffer row, the +rewritten scenario-6 pilot-scope paragraph, the 1d cross-reference +sentence, the §10.5 obligation extensions, and the §11 v10 entry — per +the §11 v10 change-order entry. Not a full round; v1–v9 material out of +scope except where the addendum destabilizes it. + +Method: mechanical reachability walk of 6-overlay sub-cases (a)–(d) and +6-overlay-naive under §3/§4/§5 as modified by o-i..o-iv (crash position +in MStore's queue per the §5 prefix rule, stop-checkpoint contents, +o-iv resume behavior, clause-(iii) suppression); hand-application of +§7's fold (round-5 F1 completion pin, self-grounding-overlay and +copy-skipped clauses), P2 (changed-with-diff consult pin), and P3′ over +every schedule the cells produce; adversarial schedule construction +against the toggles-off green claim; coherence sweep against rounds 3–6 +and against 6-atomic, 1d, 5b, and the round-5 F7 disposition; +verification of the wire-contract and walker-mechanics claims against +`docs/verification/sync-replay-6b/plan.md` (B1–B10, CO-6b-001..007), +`proto/c1/connector/v2/annotation_source_cache.proto`, +`pkg/sync/source_cache_orchestration.go`, `pkg/sync/state.go`, +`pkg/sync/parallel_syncer.go`, and +`pkg/dotc1z/engine/pebble/source_cache.go`. + +Verdict: **REJECT — 1 major (F1) + 5 minors + 4 notes, ALL +fix-without-re-review** per the rounds 4–6 convention (every +disposition is a pin or declaration; no scripted cell verdict changes +under the intended readings; no re-script). No round 8 warranted. + +## Verified clean (for the freeze record) + +- **Reachability.** All 6-overlay sub-cases (a)–(d) and + 6-overlay-naive's premise walk mechanically without hand-placement. + 6-overlay-naive's crash lands between `eOverlayUnit'` and the final + page's `eUpsertPage` in one sender's FIFO stream — a genuine + queue-position choice under §5's prefix rule, the same mechanism + 6-naive uses; restart-from-root holds (crash-only history, Init + checkpoint survives, checkpoint-skip nondet at the sole loop top). + Sub-case (b)'s stop-checkpoint contents (mid-chain cursor + hit + {S: V1}, buffer lost) follow §3's stop semantics exactly; the + restored hit is inert per the carried-over V-ATOMIC pin; + clause-(iii) suppression in naive's attempt 2 is the declared marker + semantics, not scenario hand-placement. Budgets hold: ≤ 2 syncs, + ≤ 2 attempts per sync, 1 crash or 1 stop per sub-case, buffer + bounded by the 2-page round. +- **Fold coherence for 6-overlay.** The unit's round folds as §7's + EXISTING self-grounding-overlay clause with no new fold clause — own + copy committed inside the unit, folds to rows(s, e_to), copy counts + toward replacement legality (exactly one unit per scope per sync is + constructible in the family: one chain, marker suppresses + re-execution, crash cannot split the atomic op). Round completion = + unit commit is consistent with the round-5 F1 pin (every prescribed + store op of every page commits at the unit's queue position; + announce in prescribed page order keeps the fold order + well-defined). Sub-case (b)'s attempt-1 fragment commits no store + ops and contributes no fold entry (existing pin); no torn round is + constructible — P3′'s by-construction claim holds, and its (i) + scoping holds (mutation is between syncs only). +- **6-overlay-naive's incompleteness.** Under the F1 pin, the round's + prescribed store ops are the unit' constituents PLUS each overlay + page's upserts/tombstones; the final page's ops never commit, the + round is INCOMPLETE (not torn — all committed pages sit in attempt + 1), and marker suppression prevents any attempt-2 round. The + committed prefix is debris. The publish-time check is + attestation-only and green (V2's epoch equals the verdict epoch e2), + so the alarm correctly lands at seal — modulo finding F1 below on + what the seal checks are defined to compare against. +- **Toggles-off claim.** With `oncePerScope` AND `scopeLocks` both + OFF, every schedule the scripted family allows was walked: the + single consulting chain is the only actor on S (o-i removes the + carrier; whole-chain worker ownership per §3), so no interleaving + exists for a lock to serialize — the family does not secretly depend + on one. `oncePerScope`'s function is genuinely subsumed by the + in-unit marker (at most one unit per scope per sync in every + constructible history). The two-worker marker-race boundary note is + honestly scoped: the dual-consult shape is unreachable in the + single-chain family and stays a recorded hazard, correctly carried + over from round-5 N4. +- **P2/P3′.** 6-overlay's P2 green follows from the round-5 F8 + changed-with-diff pin (staleness ≤ 1: base rows one hop, overlay + rows fresh). 6-overlay-naive's verification-sync claim follows from + P2's pinned definitions: the mosaic's V2 entry revalidates clean + (truthful validators, upstream fixed at e2), the per-seal consult + check stays green, and the staleness corollary exhibits growth to + the chain bound on the replayed stale rows — the same "unbounded + branch" convention as case 1 (wording caveat in N2). The stale-AHEAD + non-self-healing classification is correct: a future consult of V2 + delivers diffs from e2 onward only; the e1→e2 changes are never + re-delivered — the genuine dual of 1d's stale-BEHIND. +- **Wire-contract claim (o-iii).** TRUE as stated, verified against + the proto and the orchestration code: + `SourceCacheReplay.cache_validator` documents both token placements + (replay page, or deferred to the final record page's + `cache_validator`); the manifest write timing is runtime-internal + and unobservable on the wire within a sync — the warm lookup and the + ask/answer continuation resolve against the PREVIOUS artifact only + (`previousSyncSourceCacheLookup.prev`; the proto's continuation + diagram). B5's early publish is a permission, not an obligation, so + declining it is a runtime design restriction, exactly as the pin + says. (Two adjacent looseness items: F2, F3.) +- **Third-placement reduction.** "Per-page commits with marker+publish + LAST outside any unit reduces to 6-naive's unmarked-debris class" is + sound: under a changed-with-diff re-verdict the next attempt's own + clear/copy replaces the debris; the debris is harmful only under a + fetch-fresh follow-up, which is exactly 6-naive's scripted kill. No + separate cell needed. (An adjacent legality-counting boundary is + recorded as N3.) + [MS-CO-001 NOTE: overturned mechanically — see the provenance note.] +- **No destabilization.** 6-atomic's green claim stays de-scoped to + the 1a/1b/1c re-runs; 1d's verdicts are untouched and its new + cross-reference sentence is accurate; the round-5 F7 disposition is + correctly historicized (the §11 v7 entry is unmodified, append-only + discipline kept; the v10 entry states the supersession); the + rewritten pilot-scope paragraph names exactly the two obligations + round 5 F7 deferred (the unit's publish for a diff verdict; the + marker-before-overlay stale-AHEAD hazard) — verified against the + round-5 record. §5's crash-wipe column stays consistent with the new + buffer row; the announce-at-commit story keeps the §2.4 monitor + subscription implementable; the o-iv/§5 relationship is coherent + (restore-as-is governs checkpoint restoration, o-iv governs + dispatch, and the §5 buffer row cross-references the rule); + conformance question C1 is a shipped-design probe and is untouched. + +## Findings and dispositions + +- **F1 (MAJOR) — the P1 seal checks are undefined over a log with ZERO + complete rounds for a non-empty, attested scope, and the two live + readings flip 6-overlay-naive's verdict.** Spec text at issue: §7's + content check ("partition equals the fold result") and attestation + check ("the manifest entry's epoch … equals the fold result's + epoch"), versus the 6-overlay-naive script ("the round is INCOMPLETE + … contributes no fold entry; the committed prefix is debris → P1 + content violation at seal (and attestation: entry e2 over an + e1-mosaic partition)"). Reasoning: every prior cell's seal log + contains at least one complete round per attested scope — 6-naive + and 1c have the attempt-2 fresh round; the fold's value over an + EMPTY round set is never pinned, and neither is the attestation + comparison when the fold result carries no epoch. Worse, the spec + itself contains the opposing precedent: 5b pins "invisible to P1 (no + legal round for S — vacuously green)." An implementer following 5b's + vacuous-domain reading returns GREEN on 6-overlay-naive's content + check (no complete round → scope outside the check's domain), + contradicting the scripted RED; the empty-fold reading (fold(∅ + rounds) = empty partition → mosaic ≠ ∅ → RED) gives the scripted + verdict. This is precisely the round-5 F1 divergence class: a + definitional gap two honest readings disagree on, surfacing for the + first time on the new cell. The attestation parenthetical is doubly + underdetermined (no fold epoch to compare V2 against). Disposition: + pin in §7 — (a) the fold over zero complete rounds yields the EMPTY + partition (initial fold value = ∅, stated once); (b) a + validator-bearing manifest entry for a scope whose fold result + carries no epoch is an ATTESTATION violation (the entry attests an + epoch the fold cannot ground). Note that this pin RECONCILES 5b + rather than disturbing it: 5b's partition is empty and it publishes + no entry, so it is green by empty-equality and attestation vacuity — + adjust 5b's "vacuously green" parenthetical to say so. State + explicitly that 6-naive and 1c are unaffected (complete rounds + exist). **Fix-without-re-review**: both scripted verdicts hold as + written under the pin; the disposition is a fold pin, not a + re-script. [MS-CO-001: already applied in v11 — primary F3.] + +- **F2 (MINOR) — `eOverlayUnit`'s contents are not total over + wire-legal validator-less rounds.** Spec text at issue: o-ii/o-iii + declare the unit as "{clear, copy(base e_from), overlay + upserts/tombstones …, marker, publish(V_to)}" with publish an + unconditional constituent. Reasoning: B5 legalizes a replayed round + that never supplies a validator ("no entry … a miss next sync — the + replay itself remains valid"), and 1d's validator-less-C sub-case + (round-5 F3) leaned on exactly this legality. Under the variant, a + validator-less changed-with-diff round has no V_to; the declaration + does not say what the unit then contains, so the variant's semantics + are undefined over a legal wire shape. No scripted cell reaches it + (the family always carries V2). Disposition: pin publish-when-present + — the unit omits the publish constituent when the round supplies no + validator; the scope gets no entry and is a miss next sync, the + replay/overlay contents remain valid (B5's own language) — or + explicitly declare validator-less rounds outside the pilot's scope. + **Fix-without-re-review** (declaration pin, no verdict changes). + [MS-CO-001: already applied in v11 — primary F4.] + +- **F3 (MINOR) — the 1d publish-placement sub-config axis silently + collapses in the re-scripted family, and o-iii's parenthetical + asserts one wire shape where the proto permits two.** Spec text at + issue: o-iii's "the connector still returns the token on the replay + page"; the 6-overlay family text, which never mentions the placement + axis that round-5 F4 required 1d to pin as two explored sub-configs. + Reasoning: the proto permits the new token on the replay page OR + deferred to the final overlay page's + `SourceCacheRecord.cache_validator`. Under the variant both + placements feed the same buffered collection and the same unit + publish, so the axis is degenerate — but a family presented as "the + re-scripted 1d premise family" that silently drops a pinned explored + axis invites the round-5 F4 question all over again, and the + parenthetical as written is factually over-narrow. Disposition: one + sentence in o-iii or the 6-overlay cell — both B5 token placements + are wire-legal and collapse to the single unit publish under + o-ii/o-iii; the 1d sub-config axis is degenerate by construction. + **Fix-without-re-review.** [MS-CO-001: subsumed by v11's F5 + rewording; degeneracy noted in the change-order entry.] + +- **F4 (MINOR) — o-iv's cursor-ignoring rule is phrased per-scope but + the restartable unit is the ACTION; multi-scope chains leave the + operational rule undefined.** Spec text at issue: "mid-chain cursors + for unit-mode scopes are restored but IGNORED … resume restarts the + scope's work FROM CONSULT." Reasoning: an action has one cursor, not + one per scope. The model's state space contains planning chains that + consult k scopes (§3 MWorker) and 2-scope configs; a stop-checkpoint + of a chain whose page 1 committed scope S1's unit and whose page 2 + consults S2 restores ONE cursor — "ignore it for unit-mode scopes" + does not say what the scheduler dispatches. The scripted cells are + single-scope, so no verdict diverges, but the pin as declared is not + implementable as written. Disposition: pin o-iv operationally — + under the variant, a restored mid-chain cursor for an action + carrying unit-mode scope work is discarded and the action restarts + from its ROOT token; clause-(iii) marker suppression provides + per-scope idempotence for already-committed units (the at-least-once + re-fetch price already stated). Alternatively restrict the variant + to single-scope chains by declaration. **Fix-without-re-review.** + [MS-CO-001: v11's transition-deferral pin (primary F1) dissolved the + ignore-rule; deferral now explicitly scoped per verdict.] + +- **F5 (MINOR) — the variant's atomic store op is not registered in + §3's MStore op list, and §4 carries no pointer to the variant + override; the round-6 process lesson is only partially complied + with.** Spec text at issue: §3's MStore atomic-op enumeration + (eCheckpoint … eSeal), which contains neither `eOverlayUnit` nor + V-ATOMIC's `eReplayUnit`; §4, whose page-op sequence the variants + supersede for unit-mode scopes with no cross-reference. Reasoning: + the round-6 lesson — new machinery arrives through §1/§3/§5 + declarations, not scenario scripts — is the standing hunt pattern + for this review. v10 complied at §5 (buffer row) and by declaring + pins before cells, but a new MStore atomic op is §3 machinery: §5's + crash protocol ("eCrash is enqueued … like any op") and §2.1's + cross-sender arrival-order choice point quantify over the store-op + vocabulary, which a reader assembles from §3. The semantics are + fully pinned in §9.6, so no two readings diverge — hence minor, not + major — but the registration gap is real, and `eReplayUnit` was + grandfathered only because round 5 predates the lesson. Disposition: + add both variant ops to §3's MStore list, marked scenario-local + (§9.6), and add one line to §4 noting that scenario 6's design + variants replace this sequence for unit-mode scopes. + **Fix-without-re-review.** [MS-CO-001: applied.] + +- **F6 (MINOR) — §10.5 carries no mutation obligation for o-iv, the + one load-bearing line the overlay flavor adds beyond V-ATOMIC's + two.** Spec text at issue: §10.5's new 6-overlay/6-overlay-naive + obligations, which pin the unit boundary (naive red) and the + toggles-off green, but nothing kills a resume that honors the + restored mid-chain cursor. Reasoning: the pair pins "unit contents" + and "marker suppresses re-execution" (scenario 6's own framing); the + overlay flavor newly introduces the buffer-loss resume rule, and its + kill is constructible — in sub-case (b)'s schedule, a + continuation-without-buffer mutant collects only page 2 and commits + a unit missing page 1's overlay ops → partition ≠ rows(e2) under the + self-grounding fold → content-RED. Round-5 F2 set the precedent (the + locks-off 1d mutant was added to §10.5 for exactly this reason). + Disposition: add to §10.5 — an o-iv-removal mutant (resume continues + the restored cursor for a unit-mode scope) is content-RED in + 6-overlay sub-case (b)'s schedule. **Fix-without-re-review.** + [MS-CO-001: applied, and built as a model cell.] + +## Notes to the freeze record + +- **N1.** The marker's durability (per-scope store row, durable at op + commit) is declared only inside §9.6's V-ATOMIC paragraph, while + §2.2 keys mechanical crash-wipe enforcement to §5's table. v10 added + a §5 row for the buffer but not for the marker, which + 6-overlay-naive's premise newly leans on (the marker must survive + the exact crash that drops the overlay upserts). Grandfathered from + v6, but a variant-scoped §5 line ("replay/overlay unit marker | + MStore | durable at op commit — §9.6 variants only") would make the + table total. Cosmetic-plus; fold into the F5 edit. [MS-CO-001: + applied.] +- **N2.** "P2 staleness grows without bound on the never-landed rows" + is imprecise: staleness grows on the PRESENT mosaic rows whose + e1→e2 updates never landed; rows the lost final page would have + ADDED are absent and outside P2's row quantification entirely (the + 5b dropout class — their only oracle is the content check). Reword + so the two halves of the non-self-healing story (present-stale rows + with growing counters; absent rows invisible to P2) are stated + separately. [MS-CO-001: already applied in v11 — primary F7.] +- **N3.** §7's replacement legality "counts COMMITTED copies, not + verdict labels" does not say whether copies belonging to INCOMPLETE + rounds count. No §9 config reaches the shipped-benign shape (B5: a + checkpoint cut between lookup and page commit "re-runs an idempotent + copy" — copy commits, crash loses the replayed set, resume replays + the same scope again), so no scripted verdict is affected; but under + the all-committed reading that shipped-legal schedule would + false-alarm. Record as a config-widening tripwire in the + torn-round-boundary style: any future config scheduling a replay + verdict on both sides of a crash for one scope requires a + legality-count scoping decision (complete-rounds-only vs + all-committed) by change order first. [MS-CO-001: superseded — v11 + pinned complete-rounds-only counting (primary F2) and the + 6-overlay-last cell reaches the shape this note called unreached.] +- **N4.** "Collects the 2-page overlay inline" should be pinned as the + 2-page ROUND (the replay/first-overlay page plus the final overlay + page). The 3-page reading (consult page + 2 overlay pages) violates + §1's pages-per-scope-per-round ≤ 2 bound; 6-overlay-naive's own + premise confirms the 2-page reading (unit' at page 1's consult + boundary, page 1's upserts committed, page 2's dropped). One clause + fixes it. [MS-CO-001: applied.] + +## Process observation + +v10 is the first addendum that visibly internalized the round-6 lesson +— the pins precede the cells, the buffer reached §5, and the cells +cite the fold clauses they rely on — and it shows: the cells +themselves walked clean, and the one major lives in §7's check +definitions rather than in undeclared scenario machinery. The residual +pattern is narrower than round 6's: machinery declared in the right +ORDER but not in all the right PLACES (F5, N1), and a property-layer +totality gap that only a new log class could expose (F1). The +5b-vs-6-naive precedent collision behind F1 is worth remembering at +freeze time: two cells can each pin a locally-correct reading whose +union is contradictory, and only a cell that sits in the intersection +detects it. diff --git a/formal/reviews/model-spec-round7-overlay.md b/formal/reviews/model-spec-round7-overlay.md new file mode 100644 index 000000000..a725b1a2a --- /dev/null +++ b/formal/reviews/model-spec-round7-overlay.md @@ -0,0 +1,353 @@ +# Round 7 — targeted spot review of the v10 addendum (V-OVERLAY-UNIT) + +Scope: §9 scenario 6 V-OVERLAY-UNIT declaration (pins o-i..o-iv), +cells 6-overlay and 6-overlay-naive (including the "third placement" +dismissal), the §5 overlay-collect-buffer durability row, the 1d +cross-reference, and the §10.5 obligation extensions — per the §11 v10 +change-order entry. Not a full round; v1–v9 material out of scope +except where the addendum destabilizes or newly depends on it. + +Method: mechanical reachability walk of 6-overlay sub-cases (a)–(d) +and 6-overlay-naive's crash window under §3/§4/§5 (prefix rule, +stop/crash checkpoint contents, batch semantics); independent +re-derivation of every P1/P2/P3′ verdict from the §7 fold as written +(round-5 F1 pin applied by hand), including the dismissed third +placement, walked window by window; coherence sweep against the +round-5 V-ATOMIC pins (clause (i)–(iii), F1, F6, N4) and the round-6 +process conventions; shipped-system claims verified against +`docs/verification/sync-replay-6b/plan.md` (B3/B5), +`pkg/sync/source_cache_orchestration.go` (lookup surface 163–215, +`beforeUpserts` 563+, `afterUpserts` publish 718–771), and +`pkg/synccompactor/pebble/overlay.go` (atomic range-del + rows batch +precedent, 353–385). + +Verdict: **REJECT — 3 majors + 2 minors + 2 notes, ALL +fix-without-re-review**; no round 8 warranted. No cell verdict is +overturned under the recoverable readings, but two majors leave a +scripted verdict underivable from the text as written and one +dismisses a placement on a false equivalence. Verified clean (see +list after the findings): the unit-commit fold treatment is genuinely +F1-pin-consistent with no new fold clause; the toggles-off structural +green derives within the scripted family; the naive crash window is +constructible; all shipped-system claims are true in code; the §5 +buffer row is coherent; the 1d cross-reference and §10.5 extensions +are consistent with round-5 F2. + +## Findings + +- **F1 (MAJOR) — pin o-iv is not realizable from the pinned durable + state; 6-overlay sub-case (b) rests on the gap.** + - Claim under attack: "(o-iv) … for a scope with NO marker, resume + restarts the scope's work FROM CONSULT even when a stop-forced + checkpoint restored a mid-chain cursor for it … mid-chain cursors + for unit-mode scopes are restored but IGNORED"; sub-case (b) + scripts exactly this ("stop-checkpoint captures the mid-chain + cursor … resume ignores the cursor (o-iv), re-consults"). + - Evidence: §3 pins that a stack entry holds ONE page token, + advanced in place — "a dispatched-but-unfinished action's stack + entry retains the token of its LAST COMMITTED transition + (mid-chain)" — and the glossary pins `NextPageToken` as in-place + cursor advance. §5 pins "Resume: fresh MSyncAttempt from the MOST + RECENT durable checkpoint, alone." In sub-case (b) the most + recent checkpoint is the stop-forced one whose entry for the + consulting chain holds the mid-overlay cursor; the consult-page + token was destroyed by the in-place advance and exists in no + restored state. After "ignoring" the cursor, the resume has NO + re-entry token: it cannot construct the consult from the + checkpoint alone, and dispatching the mid-chain cursor is the + exact thing o-iv forbids ("cursor continuation without the buffer + is undefined"). A third reading — the worker fails loudly on the + forbidden cursor — produces a P4-shape livelock, turning (b) RED. + Root cause: V-ATOMIC's clause (i) had TWO halves — inline + execution AND "the page's own transition commits only after the + unit". (o-i) generalizes the first half and silently drops the + second; sub-case (b)'s "captures the mid-chain cursor" premise + shows the dropped half was load-bearing. + - Why it matters: the variant's marker-absent resume rule — the + §5 buffer row cites it as the thing buffer loss "forces" — has no + executable semantics, and (b)'s green is the sub-case that makes + the structural claim non-vacuous under graceful stop. This is + not a contradiction-in-principle with the glossary's + stop-resume pin or CO-6b-002 (a declared scenario-local variant + may deviate); it is a joint-unsatisfiability of o-iv with §3's + in-place advance and §5's checkpoint-alone resume. + - Disposition (fix-without-re-review; pick one and pin it): + (1) generalize clause (i) faithfully — transitions of the pages + in a consult verdict's prescribed work commit only at unit + commit, so intermediate overlay cursors never enter live state, + the stop checkpoint captures the chain AT its consult-page + token, o-iv's ignore-rule becomes derivable/vacuous, and + sub-case (b)'s premise text is corrected ("captures the + consult-page cursor", not "the mid-chain cursor"); or + (2) keep per-page transitions and declare that unit-mode stack + entries additionally retain their consult-page token — an + explicit checkpoint-content extension in §5 (and a stated + departure from V-ATOMIC's "token UNCHANGED" pin). Option (1) is + the smaller change and matches V-ATOMIC's discipline. Verdict of + (b) survives either repair. + +- **F2 (MAJOR) — the "third placement" dismissal is unsound: within + the overlay family the reduction to 6-naive's unmarked-debris + class cannot occur, and the placement's real windows are not + covered by any existing cell.** + - Claim under attack: "(The third placement — per-page commits + with marker+publish LAST outside any unit — reduces to 6-naive's + unmarked-debris class and needs no separate cell.)" + - Evidence: 6-naive's class requires a NON-clearing re-verdict + unioning over unmarked debris — its attempt 2 goes FETCH-FRESH, + and "fresh never clears" (1c). In the re-scripted overlay family + the re-consult's failed revalidation yields CHANGED-WITH-DIFF by + premise, whose prescribed work BEGINS with + `eClearScope`+`eCopyScope` (§4; the replayed set is volatile and + lost in the crash, and the family runs `oncePerScope` OFF + besides). Walking the placement's windows: (w1) crash anywhere + before marker+publish → attempt 2 re-consults, the replay page's + clear WIPES the debris, base+overlay rebuild, marker+publish + commit → partition rows(e2)@V2 — no union in any schedule; the + residual is attempt 1's committed copy PLUS attempt 2's committed + copy in one sync — a replacement-count legality question, not a + debris union. (w2) marker-before-publish ordering, crash between + them → marked, entry-less, content-complete scope whose + re-execution is SUPPRESSED by clause (iii) — seals correct + rows(e2) with no entry, and P1-content alarms against the empty + fold (publish was a prescribed round op that never committed) — + a suppression-window shape that is not 6-naive's class either. + Neither window reproduces the union; both are uncovered. + - Why it matters: "needs no separate cell" is a coverage decision + resting on a false equivalence — the flavor of the re-verdict is + exactly what makes the debris classes differ, which is the + addendum's own central insight (stale-AHEAD vs stale-BEHIND). + Worse, (w1) is the FIRST history in the spec whose class depends + on whether "at most one replacement copy per scope per sync" + counts committed copies inside INCOMPLETE rounds: §7's + "replacement-count legality counts COMMITTED copies" was pinned + on cell 4's two-complete-rounds shape, while plan B5 pins the + cross-attempt re-execution copy as legal ("the worst case … + re-runs an idempotent copy"). Under the all-committed-copies + reading (w1) alarms on a converging, B5-legal history; under a + complete-rounds-only reading it is green. No scripted cell + reaches cross-attempt double-copy today, so the pin is genuinely + missing, and out-of-script counterexample logs must fold + identically (round-5 F1's own motivation). + - Disposition (fix-without-re-review): replace the dismissal with + a correct argument or a cheap cell (same premise, shifted op + placement — both windows above are the interesting rows), and + pin the replacement-count rule's treatment of copies in + incomplete rounds. Recommended pin: legality counts committed + copies within COMPLETE rounds, plus the pre-committed- + classification rule that incomplete-round copy debris surfaces + through content divergence (which keeps cell 4 red, keeps + 6-naive red, and keeps the benign at-least-once re-copy green). + +- **F3 (MAJOR) — the P1 fold value for a scope with ZERO complete + rounds is unpinned; two readings exist in the spec's own text and + they diverge on 6-overlay-naive's headline content verdict, while + the cell's attestation claim is underivable under either.** + - Claim under attack: 6-overlay-naive — "the round is INCOMPLETE + … contributes no fold entry; the committed prefix is debris → + P1 content violation at seal (and attestation: entry e2 over an + e1-mosaic partition)"; §10.5 — "6-overlay-naive is content-RED + at seal". + - Evidence: §7 defines the fold "over complete rounds" and never + pins its value when the complete-round set is EMPTY ("current + fold value" is likewise presupposed by the overlay and + copy-skipped clauses, never seeded). 6-overlay-naive is the + first cell whose content verdict depends on the empty case: + every prior red cell had at least one complete round (6-naive + folds fresh(e2); 1b-ii folds rows(e1)). The spec's own 5b text + supplies the competing reading — "invisible to P1 (no legal + round for S — vacuously green)" — under which a scope with no + complete round is simply not checked, and 6-overlay-naive seals + content-GREEN. The intended reading (empty fold = empty + partition; debris diverges) is recoverable from "torn or + INCOMPLETE rounds' debris surfaces as content divergence", but + recoverable-not-pinned with a divergent cell verdict is exactly + the round-5 F1 shape. Separately, the attestation check is + pinned as "the manifest entry's epoch … equals the fold + result's epoch" — an empty fold HAS no epoch, so the comparison + is undefined, not violated; the cell's parenthetical attestation + claim does not follow from the check as written (again a first: + 1b-ii's entry-vs-fold mismatch compared two defined epochs). + - Why it matters: the kill cell of the whole addendum — the one + that answers scenario 6's deferred stale-AHEAD obligation — must + be RED because the property text forces it, not because the + scenario intends it; §10.5's obligation is unverifiable as + written and a checker implementing the vacuous reading passes + the naive variant. + - Disposition (fix-without-re-review), three pins in §7: (a) the + fold's initial value is the EMPTY partition, so a scope with + committed store ops and no complete round diverges by + construction; (b) a published manifest entry for a scope whose + fold result is empty is an ATTESTATION violation (the entry + attests a composition the log does not contain); (c) reword + 5b's "vacuously green" to "green — empty partition equals the + empty fold, and no entry exists to check" so the vacuous reading + loses its textual foothold. Collateral swept: 5b stays green + under (a)+(b) (empty partition, no entry); no other cell has a + non-empty partition or a published entry over an empty fold; + 1d's copy-skipped C publishes over a NON-empty fold + (self-grounding rows(e2)) and keeps its scripted + stale-BEHIND alarm. + +- **F4 (MINOR) — (o-iii) pins the unit's contents unconditionally to + include publish(V_to), but validator-less diff rounds are legal in + the modeled population and the declaration is silent on them.** + - Evidence: §3 MWorker pins "the annotation carries a validator or + not per scenario cell (both shapes are legal per proto)"; plan + B5 pins that a round which never supplies a non-empty validator + gets no entry and the replay remains valid; 1d scripts a + validator-less carrier as a first-class sub-case. V-OVERLAY-UNIT + declares `eOverlayUnit(s)` = {…, publish(V_to)} with V_to "the + verdict's post-diff validator (the new delta token)" — + presupposing the connector supplied one. The round-6 process + lesson ("declared here in full before its cells") makes the + declaration, not the cells, the place where the legal input + space must be total; the v10 §11 entry claims the variant + answers "what the unit publishes for a diff verdict", and the + answer is currently partial. + - Why it matters: an implementer must know whether a + validator-less diff round's unit omits the publish constituent, + synthesizes a validator, or rejects the shape; the three differ + observably (miss next sync vs forged attestation vs loud cold). + - Disposition: one sentence in the declaration — the publish + constituent is present iff the round supplied a non-empty + validator; a publish-less unit commits {clear, copy, overlays, + marker}, leaves no entry (miss next sync, B5-consistent), and + the marker still suppresses re-execution within the sync. No + scripted sub-case changes. + +- **F5 (MINOR) — "B5's early-publish permission is NOT exercised by + the variant" conflates the connector-side permission with the + runtime's frozen publish timing; the deferral is a frozen-contract + deviation on the runtime axis, though verified connector-invisible.** + - Evidence: B5's publish rule is a runtime MANDATE with pinned + timing — "if `SourceCacheReplay.cache_validator` is non-empty, + the manifest entry is published after that page's operations + complete" — and the code publishes per-page in `afterUpserts` + (`source_cache_orchestration.go` 758–771). The "permission" is + the CONNECTOR's option of which page carries the token (1d's + reading: "plan B5 permits the replay page to publish … before + overlay pages land"). Under V-OVERLAY-UNIT the connector still + exercises that option — (o-iii) itself says "the connector still + returns the token on the replay page" — so the permission IS + exercised on the wire; what the variant changes is the runtime's + manifest-write timing, a deviation from B3/B5's frozen per-page + publish that would need a change order if adopted. The + load-bearing half of the claim verifies clean in code: the + consult surface is the PREVIOUS artifact only + (`previousSyncSourceCacheLookup` wraps the previous store's + entry reader, 163–215), so the current sync's manifest is never + connector-readable mid-sync and the deferral is + connector-invisible — "not a wire-contract change" is true. + - Why it matters: the bake-off comparison must not book the + deferral as free; it is a runtime-contract change with a CO + obligation, and "permission not exercised" misattributes whose + behavior changed. Also (o-iii)'s "on the replay page" + over-narrows: B5's other leg (token on a later record/overlay + page) is equally legal and equally buffered into the unit. + - Disposition: reword — the variant defers the runtime's manifest + write into the unit (a B3/B5 timing deviation, change-order + scope if adopted; wire contract untouched, connector-invisible + because the lookup surface is the previous artifact only), and + replace "on the replay page" with "on whichever page carries + it". + +- **F6 (NOTE) — the two-worker marker-race carry-over is the same + hazard CLASS but a materially wider WINDOW; the boundary note + should say so for the deliverable-4 bake-off.** + - V-ATOMIC's N4 window spans one page's handling (marker check → + `eReplayUnit`). V-OVERLAY-UNIT's spans the whole collect phase — + marker check at consult → unit commit after every overlay page + is collected, multiple connector calls and atomic steps. The + class carries over as claimed (two consulting actions both pass + the absent-marker check, two units commit, two committed copies + → P1 legality alarm; unreachable in the scripted + single-consulting-chain family), and one difference from + shipped case 4 is worth recording: because each unit is + internally atomic, the racing schedules' final CONTENT is the + last unit's coherent rows(e_to) — the alarm is legality-only, + never a wipe-mosaic. Stranded-hit inertness carries over + unchanged as claimed (consult-inline + o-iv make a restored hit + inert). Disposition: extend the boundary-note sentence with the + widened-window observation; no cell change. + +- **F7 (NOTE) — 6-overlay-naive's P2 wording misplaces the staleness + counter, and the verification-sync expectations are stated + narrower than what re-derives.** + - "P2 staleness grows without bound on the never-landed rows": + staleness attaches to rows PRESENT in the partition — the growth + is on the stale base(e1) rows whose e1→e2 updates never landed; + the never-landed rows are in no partition. Recomputing P2 in the + verification sync: the per-seal consult clause is GREEN (S is + consulted via V2's validation match, which qualifies under §7's + round-5 pin) — the unbounded growth is the staleness-counter + corollary, case 1's "unbounded branch" vocabulary, and should be + labeled as such. Also true but unclaimed: P1 content stays RED + in the verification sync itself (the replacement fold of the + warm mosaic copy is rows(e2) via truthful V2; the partition is + the mosaic), so an implementer checking only P2 there would + under-report. Disposition: two wording fixes; verdict direction + unchanged. + +## Verified clean (positive results, for the freeze record) + +- Fold treatment of 6-overlay: genuinely consistent with §7 as + written and the round-5 F1 pin — each page's prescribed store ops + commit at unit commit, so round completion IS unit commit; the + round is an overlay round whose own copy committed → + self-grounding, folds rows(e2), copy counts once; no new fold + clause needed. Sub-cases (a), (c), (d) re-derive green exactly as + scripted ((b) pending F1's mechanism pin, after which it + re-derives green too). +- The structural claim is non-vacuous and derivable within the + scripted family with both toggles OFF: the marker-inside-the-unit + is the dedup (at most one unit per scope per sync — a committed + unit implies a marker that suppresses every later execution; an + uncommitted unit implies no copy), and no interleaving partner + exists for `scopeLocks` to matter (single consulting chain, no + carriers by o-i, no record pages). +- 6-overlay-naive's crash window is constructible under §5's prefix + rule (unit', overlay-p1 upsert committed; final upsert dropped; + same-sender FIFO), attempt 2's marker suppression follows clause + (iii) as declared, and the verification sync's warm mosaic replay + is reachable (nothing marks the artifact blocked; V2 revalidates + clean). Stale-AHEAD as the non-self-healing direction re-derives: + V2 attests e2, so no future consult ever re-delivers e1→e2 — the + exact dual of 1d's stale-BEHIND, correctly classified. +- Shipped-system claims: per-page publish at `afterUpserts` when the + page carries a validator (so B5 early publish is real shipped + behavior for validator-bearing replay pages); hit recording at + lookup time via `onHit`; the consult surface is the previous + artifact only. The implementation-shape sentence (WriteBatch / + grouped ingest with range-del + rows in one commit) has an in-repo + precedent (`overlay.go` fold batches combine `DeleteRange` with + row writes in one atomic commit); the model checks contents, not + mechanism, as declared. +- §5 buffer row: coherent — MWorker ownership matches §3 (one worker + runs the entire chain, so the buffer never crosses workers); + volatile-never-checkpointed is consistent with the stop + checkpoint's pinned contents; bound (pages ≤ 2) inside small + scope. +- 1d cross-reference and §10.5: consistent with round-5 F2's + mitigation-dependent demotion; the "same toggles-off configuration + that turns 1d content-red" claim checks out (locks-off 1d is + content-red per round-5 F2; both-off is red a fortiori). +- P3′ claim in 6-overlay: correct — the family's mutation is between + syncs, unit-mode rounds cannot tear, and rows(e2) matches the last + consulted verdict epoch. +- o-iv vs the glossary's stop-resume pin and CO-6b-002: deviation is + declared and legitimate for a scenario-local variant (precedent: + V-ATOMIC's marker suppression, round 5); the defect is F1's + realizability, not contradiction-in-principle. + +## Summary + +| finding | severity | one line | disposition | +|---|---|---|---| +| F1 | MAJOR | o-iv's restart-from-consult has no re-entry token under §3's in-place cursor advance + §5's checkpoint-alone resume; sub-case (b) rests on it | pin transition deferral to unit commit (or explicit token retention); correct (b)'s premise text; fix-without-re-review | +| F2 | MAJOR | third-placement dismissal reduces to 6-naive falsely — the overlay re-verdict CLEARS debris; real windows (cross-attempt double copy, marked-entry-less suppression) are uncovered and expose an unpinned legality-counting case | correct argument or cheap cell; pin replacement counting for incomplete-round copies; fix-without-re-review | +| F3 | MAJOR | empty-fold value unpinned; 5b's "vacuously green" wording supplies a reading that flips 6-overlay-naive content-GREEN; attestation-over-empty-fold undefined | pin empty-fold = empty partition, entry-over-empty-fold = attestation violation, reword 5b; fix-without-re-review | +| F4 | MINOR | unit contents pinned unconditionally with publish(V_to); validator-less diff rounds legal and unhandled | pin publish constituent conditional on a supplied validator; publish-less unit leaves no entry | +| F5 | MINOR | "B5 early-publish permission not exercised" conflates connector permission with the runtime's frozen B3/B5 publish timing (a CO-scope deviation, though verified connector-invisible) | reword; widen "on the replay page" to "whichever page carries it" | +| F6 | NOTE | marker-race carries over in class but with a materially wider window (whole collect phase); racing content is coherent, alarm legality-only | extend the boundary note | +| F7 | NOTE | P2 growth wording misplaces the counter (stale resident base rows, not "never-landed rows"); P2's seal clause is green in the verification sync; P1 also stays red there | wording fixes | diff --git a/formal/walker/CALIBRATION.md b/formal/walker/CALIBRATION.md new file mode 100644 index 000000000..c9d44a39c --- /dev/null +++ b/formal/walker/CALIBRATION.md @@ -0,0 +1,518 @@ +# Walker calibration — run log + +Status: COMPLETE. CURRENT GATE: the 56-cell full-matrix sweep, 0 +mismatches, every red on its calibrated alarm (`tools/sweep.sh`, 10k +schedules per cell; the run of record is +`PCheckerOutput/sweep/summary.txt`, ending +`SWEEP-DONE cells=56 mismatches=0`). Scenarios 1, 2 (including the +P6-C session-checkpoint-consistency tranche, decision 25 — the +CO-6b-009 root cause made executable), 3, 4, 5 (both triggers + crash +window + C1 probe), 6 (both flavors + the round-7 third-placement +cell + the MS-CO-001 o-iv mutant), 7, 8 (external principals, +decision 26), and the P4 progress cells built and calibrated. Spec +baseline: `formal/MODEL_SPEC.md` v11, FROZEN +(round-7 dispositions + de-scope edits applied; see the §11 v11 +entry), plus change order MS-CO-001 (dispositions for the PARALLEL +round-7 review — `reviews/model-spec-round7-overlay-parallel.md` — +surfaced post-freeze; §3/§4/§5 registration edits, the per-verdict +deferral scoping, and the o-iv-removal kill cell) and MS-CO-002 (the +§7 torn-round amendment: tearing is stop-reachable, exclusion is +monitor-side — discharging decision 1 below). The v11 monitor +pins (F2 complete-rounds replacement counting, F3 +attestation-over-empty-fold) and the two v11 cells (6-overlay-last, +3-atomic) are built and verified. Sweep history (superseded gates, +summaries archived under `traces/`): the 46-cell v11 freeze sweep +(`traces/freeze-sweep-v11-summary.txt`; the pre-v11 44-cell sweep was +also clean, isolating the P4 merge from the v11 monitor change), the +47-cell post-MS-CO-001 sweep +(`traces/msco001-sweep-summary.txt`), then +3 P6-C cells (50), ++5 scenario-8 cells (55), and the tc8overDelete_P8 over-deletion +kill (the current 56). + +COVERAGE LIMIT of the 56-cell matrix (the graph log carries the +mirror block for its leg; shared doctrine and both inventories in +REPORT.md's standing limits): three of this model's alarm strings +fire in no red cell — P1-ATTEST-EMPTY, P1-ATTEST-PUBLISH, and +P2-CONSULT. Each is a narrower sibling of a witnessed clause on the +same monitor (P1-ATTEST-SEAL, P1-CONTENT, and P2-STALENESS all have +reds); by this log's own doctrine the three are asserted, not +calibrated, and a green matrix says nothing about them. + +Toolchain: P 3.1.0 (`p compile` / `p check -tc -s `). +Counterexample traces are NOT committed (neither the human-readable +schedule nor the machine-replay `trace.json`): archived traces rot +silently as the model evolves, while every red REGENERATES on demand +— re-run the cell's `p check` line and a counterexample lands in +`PCheckerOutput/BugFinding/` at the stated find rate (first find is +seconds-to-minutes on every red below). Only the sweep summaries are +archived under `traces/`. GREEN rows state the explored budget. A +green run means nothing except at the stated budget — the reds are the +calibration currency. + +NOTE on verdict reading: `p check` runs a small portfolio of search +strategies; the aggregate verdict is the "Checker found a bug." line. +A run can print a strategy block reporting 0 bugs AFTER another +strategy found one (observed on tc1c_P2 — red, found by the first +strategy at ~60 schedules, missed by the 100k random pass). + +## Scenario 1 — phantom union (shipped toggles ON in every cell) + +| cell | config | property | expected | observed | budget | +|---|---|---|---|---|---| +| tc1a1b_P1 | stop-stranding, carrier publishes, 2 syncs | P1 | RED | RED: `P1-CONTENT` (carrier clear+copy composes with the fresh round; union sealed) | first find; 3000 explored | +| tc1a1b_P3 | same config | P3′ | RED | RED: `P3'-COHERENCE` (1b-i shape: carrier drains after the complete fresh round; content green, epoch incoherent) | first find | +| tc1a1b_P2 | 3 syncs, corollary-run scoping | P2 | RED | RED: `P2-STALENESS` (union replays warm in the verification sync; hops reach 2 — the unbounded branch) | first find | +| tc1bii_P1 | carrier validator-less | P1 | RED | RED: `P1-CONTENT` (the attestation-only 1b-ii edge is also live in this config; the checker surfaces the content shape first) | first find | +| tc1c_P1 | carrier-less hard crash | P1 | RED | RED: `P1-CONTENT` (copy debris survives restart-from-root; fresh round unions over it) | first find | +| tc1c_P2 | crash + verification sync | P2 | RED | RED: `P2-STALENESS` | first find | +| tc1c_P1_probe | tc1c's verification config (3 syncs), P1 asserted | P1 | RED | RED: `P1-CONTENT` — premise-liveness probe: the verify config still contains the sync-2 union, so tc1c_P2's red is fired by the staleness mechanism, not by the premise having drifted out of the config | first find | +| tcGreen_All | no interruption, no mutation, honest replay | P1+P2+P3′ | GREEN | GREEN | 10000 schedules | +| tc1c_P2_honest | crash config, P2 only, 2 syncs | P2 | GREEN | GREEN (the corrupted seal itself is staleness-legal; the alarm belongs to the verification sync) | 3000 schedules | + +## Scenario 2 — session laundering (P6-A; sessions variant A, shipped) + +One sync, root stack [H writer, G reader] (same op, one batch on the +2-cap schedules). H derives-and-writes the session key on EACH of its +two pages; G reads once and emits a row embedding the read value (a +read-miss emits nothing). Both reds are EXPECTED FINDINGS — no fix run +(variant B is the graph addendum's obligation): + +| cell | config | property | expected | observed | budget | +|---|---|---|---|---|---| +| tc2stop_P6A | graceful stop, mutate between attempts | P6-A | RED | RED: `P6-A` — H's d1 commits, G embeds d1 and pops; stop strands H mid-chain; H alone re-derives d2 on resume | first find (0.5% of 10k) | +| tc2crash_P6A | hard crash (at-least-once, both re-run) | P6-A | RED | RED: `P6-A` — the G-before-H interleaving re-embeds the durable stale d1 under H's re-derived d2; the complementary H-first schedule is green in the same config | first find (11% of 10k) | +| tc2green_P6A | no interruption, no mutation | P6-A | GREEN | GREEN | 10000 schedules | + +### P6-C — session-checkpoint consistency (CO-6b-009 root cause) + +The constraint P6-A never stated: post-crash session state must equal +the session state at the restored checkpoint, in BOTH directions — +no ZOMBIE (a dead attempt's beyond-checkpoint write observed by the +re-run: the cursor rolled back, the session did not) and no AMNESIA +(a checkpoint-committed value destroyed: its producing work will not +re-run, so deletion is unrecoverable). The axis is `cfg.sessVariant`, +the store's session semantics at the crash boundary: 0 = shipped +(durable at op commit), 1 = the rejected wholesale resume-clear, +2 = checkpoint-consistent sessions (state latched with each +checkpoint, restored at crash — the registered fix). The amnesia +cells run cell 21 (cell 2 with the ROOT ORDER REVERSED so the writer +pops first): under cell 2's LIFO order the reader pops before the +writer, so every checkpoint that still contains the reader predates +the writes and a committed-value-plus-re-run-read history is +structurally unreachable — verified green at 10k before the chassis +was added, which is a reachability fact, not evidence the rejected +fix is sound. + +| cell | config | property | expected | observed | budget | +|---|---|---|---|---|---| +| tc2crash_P6C | cell 2, hard crash, sessVariant 0 (shipped) | P6-C | RED | RED: `P6-C-ZOMBIE` — H's un-checkpointed d1 survives the crash; the re-run G reads it before H's re-derivation lands. The SHIPPED defect, now model-caught | first find (3.7% of explored) | +| tc2clear_P6C | cell 21, hard crash, sessVariant 1 (rejected resume-clear) | P6-C | RED | RED: `P6-C-AMNESIA` — H's batch completes, the loop-top checkpoint commits d1, the crash clears the namespace wholesale, G's re-run read misses committed data. The rejected fix, now model-killed | first find (20% of explored) | +| tc2consistent_P6C | cell 21, hard crash, sessVariant 2 (checkpoint-consistent) | P6-C | GREEN | GREEN — rollback to the checkpoint snapshot closes both directions; an un-checkpointed write legally vanishes (the re-run re-derives), a committed value survives | 10000 schedules | + +## Scenario 3 — artifact swap + hit rebind + +Upstream at e2 throughout (preMutate, then never moves); sync 1 seals +A = rows(e2) @ V2; the stop strands the carrier in sync 2 and MEnv +swaps the base to sibling B = rows(e1) @ V1 (equal compat, truthful +validators): + +| cell | config | property | expected | observed | budget | +|---|---|---|---|---|---| +| tc3a_P1 | shipped (`hitValidatorBinding` ON) — the residual hole | P1 | RED | RED: TWO calibrated shapes are live in this config and the run of record may show EITHER as its first find — `P1-CONTENT` (carrier-first/interleaved content shape) or `P1-ATTEST-SEAL` (carrier-last: rows(B) sealed under entry V_A); which trips first is seed luck (the Makefile's formal-section note on multi-shape RED tag noise names this cell). The conformance contract for a differ: either of those two alarms is calibrated; any OTHER alarm on this cell is a real drift. Both shapes require the re-consult's lookup-hit V_B to OVERWRITE the hit map before the carrier's LIVE hit read (last-write-wins rebind) | first find (0.34% of 10k — narrow: needs C dispatched after P's re-consult transition) | +| tc3a_P2 | same config | P2 | GREEN | GREEN — attempt 1's validation match qualifies the scope as consulted; copied rows carry hops 1 (corrected expectation; spec v2 wrongly claimed a P2 red) | 10000 schedules | +| tc3b_P1 | pre-CO-6b-004 (`hitValidatorBinding` OFF), 1-page planning (cell 31) | P1 | RED | RED: `P1-ATTEST-SEAL` — no re-consult, hit stays V_A, NO binding check; carrier copies swapped B and publishes V_A | first find (100% of explored) | +| tc3bBindingOn_All | same premise, binding ON — the CO-6b-004 kill | P1+P2+P3′ | GREEN | GREEN — binding gate compares hit V_A to base V_B, fails LOUD-COLD (behavior, not assert); scope seals empty in premise schedules, no wrong data | 10000 schedules | +| tc3atomic_All | v11: same stop+swap premise under V-ATOMIC — the `annotationBinding` de-scope's subsumption witness | P1+P2+P3′ | GREEN | GREEN — no carrier and no annotation exist; either the unit committed (marker in the CURRENT artifact suppresses attempt 2; seal is the unit's coherent contents) or attempt 2 re-consults the actually-current swapped base (V1 fails validation vs e2 → fetch-fresh). The 3A rebind hole is structurally closed: no restored hit authorizes replay | 20000 schedules | + +## Scenario 4 — duplicate replay carriers (MODEL_SPEC §9 case 4) + +(Section added post-freeze: the four cells have been in every sweep +since the v11 freeze — see `traces/freeze-sweep-v11-summary.txt` — +but this log never carried their section; the run log is completed +here with the frozen verdicts, no cell or expectation changed.) + +Two carriers with byte-distinct page tokens (distinct aids) encoding +the same (scope, verdict); no interruption, no mutation; 2 syncs. The +dedup obligation is split across two shipped guards — `oncePerScope` +(the replayed-set mark) and `scopeLocks` (serialization) — and the +cells kill each guard separately: + +| cell | config | property | expected | observed | budget | +|---|---|---|---|---|---| +| tc4shipped_All | `oncePerScope` + `scopeLocks` ON (shipped) | P1+P2+P3′ | GREEN | GREEN — the second carrier's copy is deduped under the lock (grant carries the replayed status; the mark commits at release); its B5-legal copy-skipped round folds as a no-op re-publish | 10000 schedules | +| tc4noOnce_P1 | `oncePerScope` OFF, locks ON | P1 | RED | RED: `P1-LEGALITY` — both copies commit; the lock serializes but does not dedup | first find | +| tc4noLocks_P1 | `oncePerScope` ON, locks OFF | P1 | RED | RED: `P1-LEGALITY` — check-then-mark TOCTOU: both carriers read the replayed set before either transition commits the mark | first find | +| tc4atomic_All | V-ATOMIC re-run (v11 scope) | P1+P2+P3′ | GREEN | GREEN — carriers do not exist under the variant (replay is inline at the consult); the duplicate-carrier premise is structurally unreachable | 10000 schedules | + +## Scenario 5 — warm-drift (both produce triggers + the crash window) + +Upstream never moves (drift is config-side). Sync 1 seeds A = rows(e1) +@ V1 with compat record K1 (`baseConfig = 1`); sync 2 is the premise +sync: attempt 1 (warm, K1) consults, records hit {S: V1}, spawns +carrier C (cell-31 planning shape) and the stop strands C. The +scripted drift input (compat recompute / G6 withdrawal) applies to +attempt 2 ONLY in premise histories — see decision 13. Warm install +and both produce-block triggers run at attempt start +(`installProduceState`, MODEL_SPEC 3/4); the blocked flag is volatile +at checkpoint-cadence durability (§5): + +| cell | config | property | expected | observed | budget | +|---|---|---|---|---|---| +| tc5a_P1 | cell 51, compat drift K1→K2, `warmGate` OFF — the kill | P1 | RED | RED: `P1-CONFIG` — attempt 2 cold + B4-blocked, but C passes hit (restored) and binding (base unchanged V1) and copies K1-tagged rows into the K2 attempt; sealed rows carry config 1 under seal config 2 | first find (100% of explored) | +| tc5a_Gate_All | cell 51, shipped toggles (`warmGate` ON) | P1+P2+SealExpect | GREEN | GREEN — C fails LOUD-COLD at the warm gate (reason 2), no ops; seal blocked with partition[S] empty | 30000 schedules | +| tc5b_Dropout_All | cell 52, G6 withdrawn in attempt 2, stop script | P1+P2+SealExpect | GREEN | GREEN, and the green IS the required design finding (MODEL_SPEC 9.5b): trigger 2 blocks at install, C's replay-annotated page is SILENTLY IGNORED (B1 — no failure, no announce, no rows), the sync seals green with partition[S] EMPTY and the artifact blocked. P1/P2 are structurally blind (no rows, no round); the scripted `SealExpect` expectation (wantBlocked + wantScopeEmpty) is the dropout's only executable oracle | 30000 schedules | +| tc5b_CrashWindow | cell 52, interrupt 3 (stop attempt 1, crash attempt 2) | SealExpect | RED | RED: `SEAL-EXPECT sealed unblocked` — the crash-window finding: trigger 2's block lives only in attempt 2's volatile flag; the crash lands before any checkpoint carries it; attempt 3 (handling restored, withdrawal is attempt-2-exact) re-detects nothing, runs warm, and seals UNBLOCKED with replayed rows. Schedules where the crash lands after a flag-carrying checkpoint seal blocked and stay green | first find (100% of explored) | +| tc5c_C1Probe | cell 53: ONE action, replay annotation MID-CHAIN (page 0 consults, page 1 replays), plain stop | C1Probe (+P1, P2 clean) | RED | RED: `C1-PROBE` — the CO-6b-002 conformance answer, REACHABLE VIA THE STOP PATH: attempt 21 consults (hit V1 recorded at lookup), the stop strands the action between pages, the checkpoint holds the mid-chain cursor + hit map, and attempt 22's resumed replay page commits its copy with NO fresh consult (hit check passed on the RESTORED map). A finding to confirm against the real implementation via the chaos bridge (deliverable 6), not a model bug — P1/P2 stay green in the same cell (the replayed content is truthful) | first find | + +## Scenario 6 — atomic-unit bake-off + +Fetch-fresh flavor (shipped toggles ON): + +| cell | config | property | expected | observed | budget | +|---|---|---|---|---|---| +| tc6naive_P1 | V-NAIVE (marker a separate op after copy), crash script | P1 | RED | RED: `P1-CONTENT` — crash fires at the marker op's queue position; unmarked copy debris; resumed attempt re-consults, revalidation fails, fresh round unions over debris | first find | +| tc6atomic_All | V-ATOMIC (one `eReplayUnit`), same crash script | P1+P2+P3′ | GREEN | GREEN — every crash placement leaves nothing or the complete unit; the marker suppresses re-derivation | 10000 schedules | +| tc6atomicStop_All | V-ATOMIC, stop-stranding script (1a/1b premises) | P1+P2+P3′ | GREEN | GREEN — replay executes inline; the stranded-carrier premise is structurally unreachable | 10000 schedules | + +Changed-with-diff (overlay) flavor, v10 addendum. 6-overlay runs with +`oncePerScope` AND `scopeLocks` OFF (the structural claim); +6-overlay-naive runs shipped toggles — its defect is the unit boundary, +which no toggle repairs: + +| cell | config | property | expected | observed | budget | +|---|---|---|---|---|---| +| tc6overlayNaive_P1 | unit misdrawn at the consult boundary ({clear, copy, marker, publish V2} at consult; overlay pages per-page), crash script | P1 | RED | RED: `P1-CONTENT (incomplete-round debris sealed)` — marker suppression seals base(e1)+partial-overlay under entry V2 | first find | +| tc6overlayNaive_P2 | same + verification sync | P2 | RED | RED: `P2-STALENESS` — V2 revalidates clean, the mosaic replays warm; stale-AHEAD is the non-self-healing direction | first find | +| tc6overlayLast_P1 | v11 (round-7 F2): the THIRD placement — no unit; clear+copy per-page at the replay boundary, marker+publish LAST as two trailing ops; crash script | P1 | RED | RED: `P1-CONTENT (incomplete-round debris sealed)` — the w2 window exactly: the trace shows the marker committing and the very next op (publish) dropped by the crash; attempt 2 suppresses on the marker; the scope seals CONTENT-COMPLETE but entry-less, diverging from the EMPTY fold (2.17% of schedules). NO `P1-LEGALITY` appears in any strategy's counterexample — w1's much wider crash window (cross-attempt double copy after the re-verdict's clear wipes the debris) runs green under the complete-rounds counting pin, where the pre-pin monitor would have alarmed on the converging B5-legal history | first find | +| tc6overlayMutO4_P1 | MS-CO-001 (parallel-review F6): the o-iv-REMOVAL mutant — stop script, `o4Mutant` removes the consult reset | P1 | RED | RED: `P1-CONTENT (sealed partition diverges from the round-log fold)` — the resume honors the restored mid-chain cursor with an EMPTY collect buffer, collects only the final overlay page, and commits a unit missing page 1's ops; the self-grounding fold says rows(e2), the partition disagrees (100% of explored). Kills the one load-bearing line the overlay flavor adds beyond V-ATOMIC; the non-mutant sibling stays green in the same config | first find | +| tc6overlay_All | V-OVERLAY-UNIT, crash script | P1+P2+P3′ | GREEN | GREEN | 10000 schedules | +| tc6overlayStop_All | V-OVERLAY-UNIT, stop script (mid-collect aborts; buffer loss; o-iv resume) | P1+P2+P3′ | GREEN | GREEN (re-verified post-MS-CO-001) | 10000 schedules | + +## Scenario 7 — session elision under replay (P6-R; signoff addendum) + +Pure two-sync scripts, no interruption machinery. Kinds W (producer, +scope 0) and R (reader, scope 1) in sequential phases (different ops — +the batch prefix never spans both). W's FRESH enumeration writes K as a +side effect; warm replay is the inline carrier-less path, so ELISION IS +STRUCTURAL. R stamps its fresh rows from its session read (0 = miss); +copied rows carry stamps unchanged. The counterfactual is announced by +MEnv as a computed ghost (= epoch of W's scope this sync). All reds are +deterministic (100% of schedules) — the premises are phase-ordered, not +interleaving-dependent: + +| cell | config | property | expected | observed | budget | +|---|---|---|---|---|---| +| tc7a_P6R | write elision: upstream unchanged, R policy always-fresh | P6-R | RED | RED: `P6-R` — W warm, session write elided; R reads MISS, stamps 0; counterfactual v1 | first find | +| tc7a_P1P2 | same config | P1+P2 | GREEN | GREEN — REQUIRED FINDING: the corruption is invisible to content/attestation/staleness checks | 10000 schedules | +| tc7b_P6R | stale-read replay: W's upstream moves between syncs, R warm | P6-R | RED | RED: `P6-R` — R's copied rows carry stamp v1 under counterfactual v2; no elided write anywhere (kills write-only bans) | first find | +| tc7c_All | both-warm control | P1+P2+P3′+P6-A+P6-R | GREEN | GREEN — carried stamps equal the counterfactual; P6-R does not overfit to "replay near sessions" | 10000 schedules | +| tc7aTaintW_P6R | fix run: `sessionTaintWrites` ON | P6-R | GREEN | GREEN — sync N taints W; sync N+1 consults W MISS, re-runs fresh, K present | 10000 schedules | +| tc7bTaintW_P6R | fix run: `sessionTaintWrites` ON, 7b premise | P6-R | RED | RED: `P6-R` — REQUIRED RESIDUAL: R's hazard is a READ; the write-only rule is half a fix | first find | +| tc7aTaintAll_P6R / tc7bTaintAll_P6R | fix run: `sessionTaintAll` ON | P6-R | GREEN | GREEN — replay forfeited exactly where sessions are used (honest price) | 10000 schedules each | + +## Scenario 8 — external principals (P8; the deleteStaleExternalPrincipals contract) + +One sync, one external-phase action (cell 8): page 0 LISTs the +source's current answer and commits the reconciliation op +(`eExtReconReq`); page 1 COPYs the answer's principals (`eExtCopy`). +The ext keyspace is separate from scope partitions (BatonID-annotated +rows beside connector rows). Committed copies are durable across +crashes — the debris premise — and a redispatched external phase +restarts from its root token. MEnv announces a truth ghost +(`eAnnExtTruth`) at sync start and after every between-attempt +mutation; the shrink drops principal 1 (e1 = {0,1} → e2 = {0}). P8 +asserts two clauses: CURRENT — every round's listed answer equals the +source's answer at that moment (a resume must RE-LIST; the +`ResumeUsesCurrentExternalAnswer` chaos pin) — and SEAL — the sealed +ext keyspace equals the last-RUN round's answer exactly (STALE: a dead +attempt's copy survived reconciliation; MISSING: a listed principal +dropped). The seal clause deliberately compares against the last LIST, +not truth-at-seal: a completed-then-crash schedule seals attempt 1's +answer legitimately (sync-scoped freshness). The axes are `extRecon` +(TRUE = the shipped capable-engine path; FALSE = the warn-and-continue +degrade of a non-deleting engine), `extStaleList` (the recency +mutant: attempts ≥ 2 consume the sync-start answer), and +`extOverDelete` (the late over-deleting sweep — a seal-prep deletion +whose predicate mistakes a live principal for stale): + +| cell | config | property | expected | observed | budget | +|---|---|---|---|---|---| +| tc8green_P8 | no interruption, no mutation | P8 | GREEN | GREEN — cold baseline | 10000 schedules | +| tc8crash_P8 | hard crash sync 1, shrink between attempts, capable engine | P8 | GREEN | GREEN — every crash placement heals: the resumed attempt re-lists the current answer and reconciliation deletes the dead attempt's stale copies before the fresh writes; completed-then-crash schedules seal attempt 1's answer without re-running the phase (deliberately green — sync-scoped freshness) | 10000 schedules | +| tc8stop_P8 | graceful stop + shrink, capable engine | P8 | GREEN | GREEN — restart-from-root re-lists; no mid-phase cursor can copy a fresh answer over a stale reconciliation | 10000 schedules | +| tc8reconOff_P8 | crash + shrink, `extRecon` OFF (non-deleting engine) | P8 | RED | RED: `P8-EXT-STALE` — the warn-and-continue degrade seals the dead attempt's principal 1 (the SQLite degradation pinned by `SQLiteExternalPrincipalResumeDegradesWithoutFailure`, now model-caught) | first find (20% of explored) | +| tc8staleList_P8 | crash + shrink, `extStaleList` ON (resume consumes the dead attempt's answer) | P8 | RED | RED: `P8-EXT-CURRENT` — the recency mutant the `ResumeUsesCurrentExternalAnswer` chaos pin forbids | first find (100% of explored) | +| tc8overDelete_P8 | no interruption, `extOverDelete` ON (late over-deleting sweep) | P8 | RED | RED: `P8-EXT-MISSING` — the over-deletion direction of the seal clause, witnessed so P8 is calibrated in both directions (the P6-C ZOMBIE/AMNESIA pattern). MODEL FACT the kill's placement records: an over-deleting EARLY pass — the engine order, delete-stale before copy — cannot produce this shape in ANY schedule, because the page-1 copy rewrites every listed id (structural self-heal); the mutant therefore models a late sweep committing at seal prep, where nothing re-writes the row (`Store.p`'s `extSweepMutant`) | first find (100% of explored) | + +## P4 tranche — progress properties (stuck-resume, ladder, leaked lock) + +Scenario-5 chassis (cell 51 premise: stop strands carrier C in sync 2, +compat drifts K1→K2 for the remainder of the sync). Attempt-level loud +failure is ON (`loudColdFailsAttempt`): a warm-gate/binding cold +verdict fails the ATTEMPT (checkpoint forced, `eAnnAttemptFailed`, +resume ladder) instead of completing the chain cold — the deviation +recorded in decision 10 is repaid here. The drift latch makes the +re-failure deterministic: every resume restores the same checkpoint +and meets the same drifted config: + +| cell | config | property | expected | observed | budget | +|---|---|---|---|---|---| +| tcP4stuck_P4 | ladder OFF, 3 attempts | P4Stuck | RED | RED: `P4-STUCK` — attempts 2 and 3 fail at the same scope/cursor/reason from byte-identical restored checkpoint state (CO-6b-004 stuck-resume made executable) | first find | +| tcP4ladder_All | `abandonLadder` ON (k = 2), 2 syncs | P4Live+P1+P2 | GREEN | GREEN — after 2 identical failures the sync is abandoned (sealed=false, resume ladder ends), sync 3 starts COLD from root and seals; the liveness monitor confirms the eventual seal. P4Stuck is deliberately NOT asserted: k = 2 IS the detection event — the ladder's claim is the recovery, not the absence of re-failure | 20000 schedules | +| tcP4leak_P1 | `scopeLocks` ON, warm page fails once, `lockReleaseOnError` OFF | P1 (deadlock) | RED | RED: DEADLOCK — the failed page's retry re-requests the scope lock the dead first try never released; the retry parks forever on `eScopeLockGrant` (CO-6b-007 leaked-lock hang; surfaces as P's deadlock detector, not an assert) | first find | +| tcP4release_All | same premise, `lockReleaseOnError` ON | P1+P2 | GREEN | GREEN — the error path releases before retry; the retry acquires, replays, seals | 20000 schedules | + +## Model decisions of record (change-order candidates for the spec) + +1. **Torn-round exclusion is monitor-side, not config-side + (DISCHARGED as MS-CO-002).** The spec (§7 boundary note) argued no + §9 config can tear a round because each config's single stop is + consumed by its premise. In the model the stop's placement is + genuinely explored, so torn rounds ARE reachable (stop + mid-fresh-round, resumed in attempt 2). The monitors track attempt + ghosts per round and exclude torn scopes from P1-content and P3′ + rather than trusting configs. Spec §7 now carries the amendment + (change order MS-CO-002), including the registered narrowing that + P3′'s torn tracking observes only overlay writes — a + replacement-only tear is P1-excluded but inside P3′'s domain; no + calibrated cell reaches one at a P3′-asserted seal. +2. **P2 corollary-run scoping** is realized as a config flag + (`verificationOnlyIfInterrupted`): the verification sync runs only in + histories where the scripted interruption landed. Without it, honest + double-replay chains reach hops 2 legally and the ≤1 bound + false-alarms — the flag is the "corollary runs" language of §7 made + operational. +3. **Crash injection is store-armed.** `eCrashArm` lets MStore fire the + crash nondeterministically at any op boundary of the armed gen + (resolution guaranteed at seal). Equivalent to the pinned + queue-position semantics of §5, but every window is explored with + useful probability — pure env-side racing missed the copy→marker + window entirely at 3000 schedules. +4. **Op granularity** per §1: upsert pages are single atomic store ops + ("partitions with atomic page commits"); clear and copy are two + separate atomic steps; V-NAIVE's marker is a third; V-ATOMIC's unit + and V-OVERLAY-UNIT's unit are single ops by design. +5. **Dead-machine parking**: ops from a crashed gen receive `eStoreDead` + and the machines park in terminal states instead of blocking — model + hygiene only (P would otherwise flag the pinned block-forever + semantics as deadlocks). +6. **tc1bii surfaces the content shape first.** The validator-less + config contains both the 1a content schedules and the 1b-ii + attestation schedules; the checker reports the first violation per + run. The attestation edge (`P1-ATTEST-SEAL`) remains an obligation to + surface explicitly — either via a schedule-restricted sub-config or + by inspecting further counterexamples. +7. **`scopeLocks` was inert in scenarios 1/6; load-bearing in 4.** The + case-4 kills (check-then-mark TOCTOU) landed as scripted; the + leaked-lock retry (CO-6b-007 hang) belongs to the P4 tranche. +8. **The hit map is read LIVE, not from the dispatch snapshot.** The + carrier's hit check and binding compare round-trip to the scheduler + at drain time (`eHitReadReq`), matching the shipped one-sync-level- + map, lookup-time-recording, last-write-wins semantics. This is + LOAD-BEARING for 3A: with dispatch-snapshot reads the rebind hole is + structurally unreachable (the carrier can never observe the + re-consult's V_B overwrite) and tc3a_P1 stays green. The replayed + set was already live (lock grant / `eReplayedCheckReq`). +9. **Replacement rounds fold by the base they ACTUALLY copied.** The + P1 fold uses the announce-side `vBase` (the copied base's manifest + entry), not the carrier's believed validator, and a copy-skipped + replacement folds as a NO-OP (round-4 F2). Equal in every scenario- + 1/4/6 cell (belief == base there); the distinction is exactly what + scenario 3 tests — a swapped base folds as its own content and the + belief mismatch surfaces as `P1-ATTEST-SEAL`. +10. **Loud cold is behavior, not an assert.** Binding-gate mismatch + (and, when scenario 5 lands, warm-gate failure) makes the chain + fail cold: no copy, no publish, `eAnnLoudCold` announced. Scenario-3 + schedules reach the gate legitimately in green histories (carrier + drains before the re-consult), so an assert would report fake bugs. + DEVIATION OF RECORD: the real system fails the ATTEMPT (resume + ladder); the model completes the chain cold with no ops. Equivalent + for P1/P2/P3′ (no wrong data either way); the attempt-failure form + becomes load-bearing only in the P4/abandonLadder cells. +11. **P6-A is scoped to this-sync stamps.** Its assert covers rows with + stamp >= 1 AND hops == 0: copied rows carry LAST sync's stamps + (P6-R's domain — cross-sync), and the miss marker 0 is P6-R's 7a + evidence. This realizes the spec's "P6-A is vacuously green on + 7a/7b/7c" without weakening the scenario-2 cells (whose reader + emits hops-0 rows with stamps >= 1, or nothing on a miss). +12. **Produce-side taint is store-recorded, worker-attributed.** The + session ops carry the acting kind's scope and the config's taint + verdict (so MStore stays config-free); taint rotates with the + artifact and a prev-tainted kind's consult MISSES. Checkpoint- + cadence durability of taint marks is not modeled (op-commit) — + accepted for scenario 7 (no interruption in its configs); revisit + if a taint-crash cell is ever scripted. +13. **Scenario-5 drift inputs are premise-scoped.** MEnv applies the + compat recompute / G6 withdrawal to attempt 2 only when the + restored checkpoint holds a stranded (replay-annotated) carrier — + the same spirit as `verificationOnlyIfInterrupted` (decision 2). + Without this, a legal non-premise history (stop lands AFTER C + drains; attempt 1's K1 rows are already sealed-bound) meets the + drifted seal and P1's clause (c) false-alarms on the shipped + design's own mitigated behavior (the artifact seals blocked). The + premise witness (checkpoint introspection) is env-level test + scripting, not modeled machinery. +14. **The seal carries the sealing attempt's blocked flag and compat + config.** `eSealReq`/`eAnnSeal` gained `(blocked, config)`; P1's + clause (c) compares every sealed row's ghost config tag to the seal + config (NOT torn-scope-excluded: config drift is between-attempt by + construction, so a mixed-config scope is exactly the alarm). The + `SealExpect` monitor consumes the scripted `eAnnExpectSeal` + expectation — MODEL_SPEC 9.5b's "scripted seal-state expectation" + made executable. G6 silent-ignore (B1) is a worker-side early + return BEFORE any gate: no ops, no announce, no marks. +15. **Warm install is computed only in the config-modeled cells.** + `installProduceState` (produce read + gates G4/G6/G7 + triggers 1/2 + + compat record write) runs for cells 51/52; every other cell keeps + the calibrated warm=true boot and op stream byte-identical. The + full-sweep regression after the scenario-5 merge confirmed all 35 + prior verdicts unchanged. +16. **Attempt-level loud failure is opt-in per cell.** With + `loudColdFailsAttempt` ON, a cold verdict at the warm/binding gate + sends `eChainFailed`; the scheduler quiesces in-flight workers, + forces a checkpoint, announces the failure (scope, cursor, reason, + restored-state fingerprint), and ends the attempt failed. The + scenario-3/5 gate cells keep the complete-cold-chain form (decision + 10) — their properties are seal-side and both forms are equivalent + there; the P4 cells need the resume ladder itself. +17. **Stuck-detection and the abandon ladder are the same event.** + `P4Stuck` fires on the SECOND identical failure (same restored + checkpoint fingerprint, same failure point); the ladder abandons at + k = 2 — i.e. exactly when detection fires. The ladder cell + therefore asserts recovery (P4Live liveness: abandoned sync's + successor seals) and NOT P4Stuck. `eAttemptEnded` carries `failed` + so MEnv can count identical failures without store introspection. +18. **The leaked-lock hang surfaces as a deadlock, not an assert.** + The failed warm page's retry (same worker, recursion in + `replayPage`) re-requests its scope lock; with `lockReleaseOnError` + OFF the grant never arrives and P's deadlock detector reports the + parked worker — matching CO-6b-007's hang phenomenology (no wrong + data, no progress). +19. **Replacement counting moved to round completion (v11, round-7 + F2 pin).** The P1 monitor counts a scope's committed copies when + their round COMPLETES (same-attempt rounds only), not at copy + commit. Verdict-equal in every pre-v11 cell (cells 4's rounds both + complete, 6-naive's debris copy alarms via content either way); + load-bearing exactly in 6-overlay-last's w1, where the + cross-attempt at-least-once re-copy is B5-legal and must not + alarm. +20. **Attestation over an empty fold is checked at seal (v11, round-7 + F3 pin).** New assert `P1-ATTEST-EMPTY`: a manifest entry for a + non-torn scope with no fold epoch fires outright — covering the + sealed-empty-partition case the content loop never visits. No + pre-v11 cell reaches the shape (copy-skipped publishes always ride + scopes another round grounded); in 6-overlay-naive the content + assert fires first in observed traces, and the direction is red + either way. +21. **o-iv is realized as worker-side cursor reset, equivalent to the + spec's transition deferral (v11 deviation of record).** The spec + pins option (1) of round-7 F1: unit-mode page transitions commit + only at unit commit, so the checkpoint holds the consult token. + The model keeps per-page transitions and instead RESETS a restored + non-zero cursor to the consult page at dispatch — executable here + because model cursors are page indices (the consult re-entry is + always constructible), which is precisely what the real system's + in-place opaque tokens lack (the review's F1 point). Equivalent + for every property: no store op commits before unit commit in + either mechanism, the buffer is volatile in both, and resume + behavior is identical (restart at consult, marker suppression + unchanged). +22. **The third placement commits shipped ops, no new store + machinery.** VAR_OVERLAY_LAST reuses `eClearScope`/`eCopyScope` + (replay boundary), per-page `eUpsertPage`/`eTombstonePage` + (tombstones NOT lastOp), then `eMarkerPut` and `ePublishEntry` + (lastOp) as two trailing queue positions — six crash windows, all + explored by the armed crash. +23. **Two round-7 reviews ran; the model arbitrated their + disagreement (MS-CO-001).** The parallel review + (`reviews/model-spec-round7-overlay-parallel.md`) independently + re-found the empty-fold major and two v11 minors (double-hit + confirmation), judged the third-placement reduction SOUND where + the primary called it false — and the built cell 6-overlay-last + settled it for the primary (the w2 window is red in a non-union + shape; w1 is the legality-counting case the parallel review + itself flagged as a tripwire). Its genuinely new item is the + o-iv-removal kill (decision 24); its registration/wording items + are §3/§4/§5 spec edits with no model impact. +24. **The o-iv mutant is one flag on the realization line.** + `o4Mutant` disables the worker's consult reset (decision 21's + realization of the spec's transition deferral), which is exactly + "resume honors the restored mid-chain cursor". The kill firing at + 100% of explored schedules in the stop config shows the reset is + load-bearing, not vestigial — the strongest possible answer to + "does the model actually depend on o-iv". +25. **Session durability variants are store-side, one field, crash- + boundary-applied (P6-C).** `cfg.sessVariant` rides `eStoreReset` + so MStore stays config-free in spirit (decision 12); variant + semantics apply in `fireCrash` (variant 1 clears `sessionKV`, + variant 2 restores the `sessCkpt` snapshot latched in + `eCheckpointReq`). Applying at the crash commit is equivalent to + acting at resume start: dead-gen ops arriving after the crash are + dropped by the gen gate and can never observe the adjusted map. + The P6-C monitor tracks write provenance from the announce stream + (uncommitted → committed at `eAnnCheckpoint`; uncommitted → + zombie at `eAnnCrash`, unless value-identical to the committed + state, where survival is unobservable; a live rewrite reclaims + the key) and judges reads via the new `eAnnSessionGet` announce. + DISPOSITION LESSON recorded with the cells: scenario 2's original + reds carried the zombie mechanism since calibration but were + filed as a future-runtime obligation; the shipped-code defect + (CO-6b-009) sat unrouted until re-found by hand. Expected-red + findings against SHIPPED semantics must be routed as change + orders on the shipped code, not only as obligations on the + replacement design. +26. **External principals are their own keyspace, phase-shaped, with + an env truth ghost (scenario 8 / P8).** The spec's announce + vocabulary (§7) predates the external phase, so scenario 8 adds + `eAnnExtTruth`/`eAnnExtRound`/`eAnnExtSeal` beside it rather than + inside a scope partition: external rows carry no validator, no + consult, no replay — the only mechanism is LIST (current answer), + RECONCILE (delete what the answer no longer contains), COPY. The + truth ghost is announced by MEnv at sync start and after every + between-attempt mutation, so P8's CURRENT clause always compares a + round's list against the answer live when the attempt listed — + which is what makes the completed-then-crash carry legitimately + green (the seal clause anchors on the last-RUN list, sync-scoped + freshness). The reconciliation op is ONE atomic store pass + (`eExtReconReq`); partial-delete debris reduces to the same + stale-survivor class the crash-between-ops windows already cover, + so no finer granularity is modeled. `extRecon` FALSE is a store + CAPABILITY (the non-deleting engine), not a worker toggle — + the round still announces, because the LIST truly happened; only + reconciliation didn't. + +## Known latent harness race (no bearing on verdicts) + +The env sends `eCrashArm` AFTER `new MSyncAttempt`: a schedule that +starves the env for the entire attempt lets the seal enter the +store's queue ahead of the arm, the at-seal resolution point sees +`armed == false`, and the late arm never resolves — the env +deadlocks on `eCrashAck`. Unreachable in practice under this +project's random-strategy sweeps (needs hundreds of consecutive +starvation steps); found by feedback-PCT on the graph model's G1d +cell, which inherited the same env shape. The graph model fixes it +with a synchronous arm handshake (`eCrashArmed`) before attempt +creation — apply here if this project ever runs priority-based +strategies. Harness-level: no walker verdict depends on schedules +in that regime. + +## Pending + +- Nothing for deliverables 2–3. The 47-cell post-MS-CO-001 sweep ran + clean (0 mismatches, every red on its calibrated alarm; summary + archived at `traces/msco001-sweep-summary.txt`). Deliverable 4's + spec is FROZEN: `formal/GRAPH_MODEL_SPEC.md` v4 after three review + rounds (round 1: REJECT, 11 majors, dispositioned in v2; round 2, + targeted on the adoption repair: core verified sound, REJECT on 6 + seam majors, dispositioned in v3; round 3, targeted on the v3 + repairs: all six round-2 repairs verified sound, REJECT on 2 + fix-without-re-review majors — session-publish body-op pin, + mid-bump fence — dispositioned in v4, no fourth round required — + see the spec's §12 log and `formal/reviews/`). The P project in + `formal/graph/` is BUILT against the frozen v4 spec and its G1 + family is calibrated (10/10 sweep; two calibration-driven change + orders GS-CO-001/GS-CO-002 — see `formal/graph/CALIBRATION.md`). + The walker-side + V-ATOMIC / V-OVERLAY-UNIT pilots (6-atomic, 6-overlay, 3-atomic + re-runs) are its designed hand-off points and their verdicts are + adopted there as settled. diff --git a/formal/walker/PCheckerOutput/sweep/summary.txt b/formal/walker/PCheckerOutput/sweep/summary.txt new file mode 100644 index 000000000..3281d4b19 --- /dev/null +++ b/formal/walker/PCheckerOutput/sweep/summary.txt @@ -0,0 +1,57 @@ +tc1a1b_P1 expected=RED observed=RED ok [P1-CONTENT] +tc1a1b_P2 expected=RED observed=RED ok [P2-STALENESS] +tc1a1b_P3 expected=RED observed=RED ok [P3'-COHERENCE] +tc1bii_P1 expected=RED observed=RED ok [P1-CONTENT] +tc1c_P1 expected=RED observed=RED ok [P1-CONTENT] +tc1c_P1_probe expected=RED observed=RED ok [P1-CONTENT] +tc1c_P2 expected=RED observed=RED ok [P2-STALENESS] +tc1c_P2_honest expected=GREEN observed=GREEN ok +tcGreen_All expected=GREEN observed=GREEN ok +tc2stop_P6A expected=RED observed=RED ok [P6-A,P6A] +tc2crash_P6A expected=RED observed=RED ok [P6-A,P6A] +tc2green_P6A expected=GREEN observed=GREEN ok +tc2crash_P6C expected=RED observed=RED ok [P6-C-ZOMBIE,P6C] +tc2clear_P6C expected=RED observed=RED ok [P6-C-AMNESIA,P6C] +tc2consistent_P6C expected=GREEN observed=GREEN ok +tc3a_P1 expected=RED observed=RED ok [P1-ATTEST-SEAL] +tc3a_P2 expected=GREEN observed=GREEN ok +tc3b_P1 expected=RED observed=RED ok [P1-ATTEST-SEAL] +tc3bBindingOn_All expected=GREEN observed=GREEN ok +tc3atomic_All expected=GREEN observed=GREEN ok +tc4shipped_All expected=GREEN observed=GREEN ok +tc4atomic_All expected=GREEN observed=GREEN ok +tc4noOnce_P1 expected=RED observed=RED ok [P1-LEGALITY] +tc4noLocks_P1 expected=RED observed=RED ok [P1-LEGALITY] +tc5a_P1 expected=RED observed=RED ok [P1-CONFIG] +tc5a_Gate_All expected=GREEN observed=GREEN ok +tc5b_Dropout_All expected=GREEN observed=GREEN ok +tc5b_CrashWindow expected=RED observed=RED ok [SEAL-EXPECT] +tc5c_C1Probe expected=RED observed=RED ok [C1-PROBE] +tc6naive_P1 expected=RED observed=RED ok [P1-CONTENT] +tc6atomic_All expected=GREEN observed=GREEN ok +tc6atomicStop_All expected=GREEN observed=GREEN ok +tc6overlayNaive_P1 expected=RED observed=RED ok [P1-CONTENT] +tc6overlayNaive_P2 expected=RED observed=RED ok [P2-STALENESS] +tc6overlayLast_P1 expected=RED observed=RED ok [P1-CONTENT] +tc6overlayMutO4_P1 expected=RED observed=RED ok [P1-CONTENT] +tc6overlay_All expected=GREEN observed=GREEN ok +tc6overlayStop_All expected=GREEN observed=GREEN ok +tc7a_P6R expected=RED observed=RED ok [P6-R,P6R] +tc7a_P1P2 expected=GREEN observed=GREEN ok +tc7b_P6R expected=RED observed=RED ok [P6-R,P6R] +tc7c_All expected=GREEN observed=GREEN ok +tc7aTaintW_P6R expected=GREEN observed=GREEN ok +tc7bTaintW_P6R expected=RED observed=RED ok [P6-R,P6R] +tc7aTaintAll_P6R expected=GREEN observed=GREEN ok +tc7bTaintAll_P6R expected=GREEN observed=GREEN ok +tcP4stuck_P4 expected=RED observed=RED ok [P4-STUCK,P4S] +tcP4ladder_All expected=GREEN observed=GREEN ok +tcP4leak_P1 expected=RED observed=RED ok [Deadlock detected,P4L] +tcP4release_All expected=GREEN observed=GREEN ok +tc8green_P8 expected=GREEN observed=GREEN ok +tc8crash_P8 expected=GREEN observed=GREEN ok +tc8stop_P8 expected=GREEN observed=GREEN ok +tc8reconOff_P8 expected=RED observed=RED ok [P8-EXT-STALE] +tc8staleList_P8 expected=RED observed=RED ok [P8-EXT-CURRENT] +tc8overDelete_P8 expected=RED observed=RED ok [P8-EXT-MISSING] +SWEEP-DONE cells=56 mismatches=0 diff --git a/formal/walker/PSpec/Monitors.p b/formal/walker/PSpec/Monitors.p new file mode 100644 index 000000000..ef2966c77 --- /dev/null +++ b/formal/walker/PSpec/Monitors.p @@ -0,0 +1,636 @@ +/* Property monitors (MODEL_SPEC 7). Announce-subscribed; ghost fields + are labels of decisions the model already made (2.4). The fold and + its legality rules implement the round-5 F1 pin: a round completes + at the commit of its last prescribed store op; scheduler transitions + are not fold events; incomplete rounds contribute no fold entry and + their debris surfaces as content divergence. + + Torn rounds (pages committed under more than one attempt) are outside + P1-content's and P3's designed domain (MODEL_SPEC 7 BOUNDARY); the + monitors track attempt ghosts and exclude torn scopes rather than + trusting the configs to never tear — placement of the single stop is + explored, so a torn fresh round is reachable and must not false-alarm. */ + +// Round bookkeeping shared shape: roundId -> info. vBase/hasCopy track +// the replacement copy actually committed (announce-side truth): a +// replacement round folds by the base it COPIED, not by the carrier's +// belief — the distinction scenario 3 (artifact swap) exists to test. +type tRoundInfo = (scope: int, verdict: tVerdict, consultEpoch: int, vBase: int, hasCopy: bool, attempts: map[int, bool], completed: bool); + +spec P1 observes eAnnSyncStart, eAnnClear, eAnnReplay, eAnnUpsert, eAnnTombstones, eAnnPublish, eAnnSeal { + var rounds: map[int, tRoundInfo]; + var foldEpoch: map[int, int]; // scope -> folded content epoch + var copies: map[int, int]; // scope -> committed replacement copies + var tornScopes: map[int, bool]; + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + rounds = default(map[int, tRoundInfo]); + foldEpoch = default(map[int, int]); + copies = default(map[int, int]); + tornScopes = default(map[int, bool]); + } + on eAnnClear do (p: (syncN: int, scope: int, ghost: tRoundGhost)) { + trackOp(p.scope, p.ghost, false, -1); + } + on eAnnReplay do (p: (syncN: int, scope: int, vBase: int, cBase: int, ghost: tRoundGhost)) { + // Counting/legality moved to round completion (round-7 F2 + // pin): a copy inside a round that never completes is + // pre-committed-classification debris — it surfaces through + // content divergence, never through the count. See trackOp. + trackOp(p.scope, p.ghost, true, p.vBase); + } + on eAnnUpsert do (p: (syncN: int, scope: int, rows: seq[tRow], ghost: tRoundGhost)) { + trackOp(p.scope, p.ghost, false, -1); + } + on eAnnTombstones do (p: (syncN: int, scope: int, removes: seq[int], ghost: tRoundGhost)) { + trackOp(p.scope, p.ghost, false, -1); + } + on eAnnPublish do (p: (syncN: int, scope: int, v: int, ghost: tRoundGhost)) { + // Publish-time check is ATTESTATION ONLY (B5 permits early + // token publish; no content check here). Truthful + // validators are epoch-valued. + assert p.v == p.ghost.consultEpoch, "P1-ATTEST-PUBLISH: published validator epoch differs from the publishing round's verdict epoch"; + trackOp(p.scope, p.ghost, false, -1); + } + on eAnnSeal do (p: (syncN: int, partition: tPartition, manifest: map[int, int], blocked: bool, config: int)) { + var scopes: seq[int]; + var ids: seq[int]; + var i: int; + var j: int; + var s: int; + var have: map[int, int]; + scopes = keys(p.partition); + i = 0; + while (i < sizeof(scopes)) { + s = scopes[i]; + if (!(s in tornScopes)) { + have = contentOf(p.partition, s); + if (s in foldEpoch) { + assert have == rowsAt(foldEpoch[s]), "P1-CONTENT: sealed partition diverges from the round-log fold"; + if (s in p.manifest) { + assert p.manifest[s] == foldEpoch[s], "P1-ATTEST-SEAL: manifest entry epoch differs from the fold epoch"; + } + } else { + // No completed round for this scope: any + // content is incomplete-round debris. + assert sizeof(have) == 0, "P1-CONTENT: incomplete-round debris sealed"; + } + } + // Clause (c) — CONFIG (MODEL_SPEC 7): every sealed + // row's ghost config tag equals the sealing attempt's + // compat config (5a's warmGate kill: K1-tagged rows + // copied into a K2 attempt). Vacuous when configs are + // unmodeled (both sides 0). Torn scopes are NOT + // excluded: config drift is between-attempt by + // construction, so a mixed-config scope is exactly the + // alarm, never a legal smear. + ids = keys(p.partition[s]); + j = 0; + while (j < sizeof(ids)) { + assert p.partition[s][ids[j]].config == p.config, "P1-CONFIG: sealed row's compat config tag differs from the sealing attempt's config"; + j = j + 1; + } + i = i + 1; + } + // Round-7 F3 pin (b): a published manifest entry for a + // scope whose fold result is EMPTY attests a composition + // the round log does not contain — attestation violation + // outright, even when the partition is empty for the scope + // (the sealed-empty case the partition loop never visits). + scopes = keys(p.manifest); + i = 0; + while (i < sizeof(scopes)) { + s = scopes[i]; + if (!(s in tornScopes)) { + assert s in foldEpoch, "P1-ATTEST-EMPTY: manifest entry published over an empty fold"; + } + i = i + 1; + } + } + } + + fun trackOp(scope: int, ghost: tRoundGhost, isCopy: bool, vBase: int) { + var info: tRoundInfo; + if (ghost.roundId in rounds) { + info = rounds[ghost.roundId]; + } else { + info = (scope = scope, verdict = ghost.verdict, consultEpoch = ghost.consultEpoch, vBase = -1, hasCopy = false, attempts = default(map[int, bool]), completed = false); + } + info.attempts[ghost.attempt] = true; + info.consultEpoch = ghost.consultEpoch; + if (isCopy) { + info.hasCopy = true; + info.vBase = vBase; + } + if (ghost.lastOp) { + info.completed = true; + } + rounds[ghost.roundId] = info; + if (sizeof(info.attempts) > 1) { + tornScopes[scope] = true; + } + if (ghost.lastOp && sizeof(info.attempts) == 1) { + // Round completion IS the fold order (completion = commit + // of the last prescribed op; this announce). Fold rule + // (round-5 F1 + round-4 F2): a completed REPLACEMENT round + // folds by the base it ACTUALLY copied (announce-side + // vBase), not the carrier's belief — a swapped base folds + // as its own content, and the mismatch with the published + // entry surfaces as P1-ATTEST-SEAL. A copy-skipped + // replacement (B5-legal duplicate) folds as a NO-OP. Fresh + // and overlay rounds fold their verdict epoch. + // Legality (round-7 F2 pin): replacement counting happens + // HERE — committed copies within COMPLETE rounds only. + // Cross-attempt at-least-once re-copies (the incomplete + // first try) are legal B5 idempotence and stay uncounted. + if (info.hasCopy) { + if (scope in copies) { + copies[scope] = copies[scope] + 1; + } else { + copies[scope] = 1; + } + assert copies[scope] <= 1, "P1-LEGALITY: second complete-round replacement copy for one scope in one sync"; + } + if (ghost.verdict == V_REPLAY) { + if (info.hasCopy) { + foldEpoch[scope] = info.vBase; + } + } else { + foldEpoch[scope] = ghost.consultEpoch; + } + } + } +} + +// Partition scope content as id -> epoch (ghost content tag). +fun contentOf(part: tPartition, scope: int): map[int, int] { + var out: map[int, int]; + var ids: seq[int]; + var i: int; + if (!(scope in part)) { return out; } + ids = keys(part[scope]); + i = 0; + while (i < sizeof(ids)) { + out[ids[i]] = part[scope][ids[i]].epoch; + i = i + 1; + } + return out; +} + +spec P2 observes eAnnScenarioInit, eAnnSyncStart, eAnnConsult, eAnnSeal { + var bound: int; + var consulted: map[int, bool]; + + start state Monitoring { + on eAnnScenarioInit do (p: (maxStaleness: int)) { + bound = p.maxStaleness; + } + on eAnnSyncStart do (p: (syncN: int)) { + consulted = default(map[int, bool]); + } + on eAnnConsult do (p: (syncN: int, scope: int, hit: bool, v: int, validated: bool, epoch: int, freshFetch: bool, diffVerdict: bool, attempt: int)) { + // Consulted-against-upstream (MODEL_SPEC 7 pin): validation + // match, fresh fetch, or CHANGED-WITH-DIFF verdict; a lookup + // hit alone does not qualify. + if (p.validated || p.freshFetch || p.diffVerdict) { + consulted[p.scope] = true; + } + } + on eAnnSeal do (p: (syncN: int, partition: tPartition, manifest: map[int, int], blocked: bool, config: int)) { + var scopes: seq[int]; + var ids: seq[int]; + var i: int; + var j: int; + var s: int; + scopes = keys(p.partition); + i = 0; + while (i < sizeof(scopes)) { + s = scopes[i]; + if (sizeof(p.partition[s]) > 0) { + assert s in consulted, "P2-CONSULT: sealed scope not consulted against upstream this sync"; + ids = keys(p.partition[s]); + j = 0; + while (j < sizeof(ids)) { + assert p.partition[s][ids[j]].hops <= bound, "P2-STALENESS: row replay-travel exceeds the scenario bound"; + j = j + 1; + } + } + i = i + 1; + } + } + } +} + +spec P3prime observes eAnnSyncStart, eAnnConsult, eAnnUpsert, eAnnTombstones, eAnnSeal { + var lastEpoch: map[int, int]; // scope -> epoch of last consulted-against-upstream verdict + var attemptsSeen: map[int, map[int, bool]]; // roundId -> attempts (torn tracking) + var tornScopes: map[int, bool]; + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + lastEpoch = default(map[int, int]); + attemptsSeen = default(map[int, map[int, bool]]); + tornScopes = default(map[int, bool]); + } + on eAnnConsult do (p: (syncN: int, scope: int, hit: bool, v: int, validated: bool, epoch: int, freshFetch: bool, diffVerdict: bool, attempt: int)) { + if (p.validated || p.freshFetch || p.diffVerdict) { + lastEpoch[p.scope] = p.epoch; + } + } + // Torn tracking here observes OVERLAY writes only — narrower + // than P1's every-round-op tracking. Replacement-only tears + // are P1-excluded but inside this monitor's domain; registered + // as the MS-CO-002 narrowing (no calibrated cell reaches one + // at a P3'-asserted seal). Widen to the P1 op set before + // adding any cell that could. + on eAnnUpsert do (p: (syncN: int, scope: int, rows: seq[tRow], ghost: tRoundGhost)) { + trackTorn(p.scope, p.ghost); + } + on eAnnTombstones do (p: (syncN: int, scope: int, removes: seq[int], ghost: tRoundGhost)) { + trackTorn(p.scope, p.ghost); + } + on eAnnSeal do (p: (syncN: int, partition: tPartition, manifest: map[int, int], blocked: bool, config: int)) { + var scopes: seq[int]; + var i: int; + var s: int; + scopes = keys(p.partition); + i = 0; + while (i < sizeof(scopes)) { + s = scopes[i]; + // Doubly scoped (MODEL_SPEC 7): configs schedule no + // mid-attempt mutation (env only mutates between + // attempts/syncs), and torn scopes are excluded here. + if (s in lastEpoch && !(s in tornScopes) && sizeof(p.partition[s]) > 0) { + assert contentOf(p.partition, s) == rowsAt(lastEpoch[s]), "P3'-COHERENCE: sealed content epoch differs from last consulted verdict epoch"; + } + i = i + 1; + } + } + } + + fun trackTorn(scope: int, ghost: tRoundGhost) { + var att: map[int, bool]; + if (ghost.roundId in attemptsSeen) { + att = attemptsSeen[ghost.roundId]; + } + att[ghost.attempt] = true; + attemptsSeen[ghost.roundId] = att; + if (sizeof(att) > 1) { + tornScopes[scope] = true; + } + } +} + +// P6-A (MODEL_SPEC 7, case 2): session-embed agreement, WITHIN-SYNC +// form. At seal, every row whose stamp was derived THIS SYNC (hops 0 — +// copied rows carry last sync's stamps and belong to P6-R) and embeds +// a real value (stamp >= 1; the miss marker 0 is P6-R's domain) must +// embed the FINAL session value — comparison is by VALUE, so +// same-value re-derivation is green. Rows with stamp -1 carry no +// session data. The session KV is sync-scoped: tracking resets at +// sync start. P6-A is vacuously green on 7a/7b/7c by this scoping. +spec P6A observes eAnnSyncStart, eAnnSessionSet, eAnnSeal { + var sess: map[int, int]; + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + sess = default(map[int, int]); + } + on eAnnSessionSet do (p: (syncN: int, key: int, val: int)) { + sess[p.key] = p.val; + } + on eAnnSeal do (p: (syncN: int, partition: tPartition, manifest: map[int, int], blocked: bool, config: int)) { + var scopes: seq[int]; + var ids: seq[int]; + var i: int; + var j: int; + var s: int; + var st: int; + scopes = keys(p.partition); + i = 0; + while (i < sizeof(scopes)) { + s = scopes[i]; + ids = keys(p.partition[s]); + j = 0; + while (j < sizeof(ids)) { + st = p.partition[s][ids[j]].stamp; + if (st >= 1 && p.partition[s][ids[j]].hops == 0) { + assert 0 in sess && sess[0] == st, "P6-A: sealed row embeds a session stamp differing from the final session value"; + } + j = j + 1; + } + i = i + 1; + } + } + } +} + +// P6-C (session-checkpoint consistency; the CO-6b-009 root cause made +// executable). THE CONSTRAINT: observable session state after a crash +// must equal session state at the restored checkpoint — in BOTH +// directions. Direction 1 (ZOMBIE): a value a dead attempt wrote +// AFTER its last checkpoint must not be observable by the re-run — +// the cursor rolled back, the work that produced the value will run +// again, and the re-run window would otherwise consume its own +// future. Direction 2 (AMNESIA): a value observable at the restored +// checkpoint must REMAIN observable — the work that produced it will +// NOT re-run, so deleting it is unrecoverable data loss. Provenance +// is tracked monitor-side from the announce stream: writes are +// uncommitted until an eAnnCheckpoint folds them; a crash turns the +// still-uncommitted residue into zombies (unless value-identical to +// the committed state, where survival is unobservable); a live +// rewrite reclaims the key. Variant 0 (shipped, durable-at-op-commit) +// violates direction 1; the rejected wholesale resume-clear +// (variant 1) violates direction 2; checkpoint-consistent sessions +// (variant 2) satisfy both. +spec P6C observes eAnnSyncStart, eAnnSessionSet, eAnnSessionGet, eAnnCheckpoint, eAnnCrash { + var committed: map[int, int]; // key -> value at the last checkpoint + var uncommitted: map[int, int]; // writes since the last checkpoint + var zombies: map[int, int]; // dead attempts' beyond-checkpoint values + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + committed = default(map[int, int]); + uncommitted = default(map[int, int]); + zombies = default(map[int, int]); + } + on eAnnSessionSet do (p: (syncN: int, key: int, val: int)) { + // A live write takes over the key: whatever is durable now + // is attributable to the current attempt. + uncommitted[p.key] = p.val; + if (p.key in zombies) { zombies -= p.key; } + } + on eAnnCheckpoint do (p: (syncN: int)) { + var ks: seq[int]; + var i: int; + ks = keys(uncommitted); + i = 0; + while (i < sizeof(ks)) { + committed[ks[i]] = uncommitted[ks[i]]; + i = i + 1; + } + uncommitted = default(map[int, int]); + } + on eAnnCrash do (p: (syncN: int)) { + var ks: seq[int]; + var i: int; + var k: int; + ks = keys(uncommitted); + i = 0; + while (i < sizeof(ks)) { + k = ks[i]; + if (!(k in committed && committed[k] == uncommitted[k])) { + zombies[k] = uncommitted[k]; + } + i = i + 1; + } + uncommitted = default(map[int, int]); + } + on eAnnSessionGet do (p: (syncN: int, key: int, found: bool, val: int)) { + if (p.key in zombies) { + assert !(p.found && p.val == zombies[p.key]), "P6-C-ZOMBIE: session read observed a dead attempt's beyond-checkpoint write (the cursor rolled back; the session state did not)"; + } + if (p.key in committed) { + assert p.found, "P6-C-AMNESIA: session read missed a checkpoint-committed value (session data silently deleted; the work that produced it will not re-run)"; + } + } + } +} + +// P8 (external principals, scenario 8 — the deleteStaleExternalPrincipals +// contract made executable). Two clauses. CURRENT: every external +// round's listed answer equals the source's answer at that moment +// (the env truth ghost) — a resumed attempt must RE-LIST, never +// consume a dead attempt's answer (the ResumeUsesCurrentExternalAnswer +// chaos pin). SEAL: the sealed ext keyspace equals the LAST-RUN +// round's listed answer exactly — no dead attempt's copied principal +// survives reconciliation (STALE direction: the non-deleting-engine +// degrade's debris), and nothing listed is dropped (MISSING direction: +// the over-deletion guard). The seal clause compares against the last +// LIST, not the truth at seal time: an attempt that completed the +// phase before a crash seals its own answer legitimately even if the +// source moved afterwards (sync-scoped freshness — the sync is not +// obligated to chase post-completion changes). +spec P8 observes eAnnSyncStart, eAnnExtTruth, eAnnExtRound, eAnnExtSeal { + var truth: map[int, bool]; + var lastLive: map[int, bool]; + var hasRound: bool; + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + truth = default(map[int, bool]); + lastLive = default(map[int, bool]); + hasRound = false; + } + on eAnnExtTruth do (p: (syncN: int, ids: seq[int])) { + truth = toSet(p.ids); + } + on eAnnExtRound do (p: (syncN: int, live: seq[int], supported: bool, deleted: seq[int])) { + assert toSet(p.live) == truth, "P8-EXT-CURRENT: external round listed an answer differing from the source's current answer (resume consumed a dead attempt's list)"; + lastLive = toSet(p.live); + hasRound = true; + } + on eAnnExtSeal do (p: (syncN: int, ids: seq[int])) { + var i: int; + var ks: seq[int]; + var sealedSet: map[int, bool]; + if (!hasRound) { return; } + sealedSet = toSet(p.ids); + i = 0; + while (i < sizeof(p.ids)) { + assert p.ids[i] in lastLive, "P8-EXT-STALE: sealed external principal absent from the last-run round's answer (dead attempt's copy survived reconciliation)"; + i = i + 1; + } + ks = keys(lastLive); + i = 0; + while (i < sizeof(ks)) { + assert ks[i] in sealedSet, "P8-EXT-MISSING: external principal listed by the last-run round missing at seal"; + i = i + 1; + } + } + } +} + +fun toSet(ids: seq[int]): map[int, bool] { + var m: map[int, bool]; + var i: int; + i = 0; + while (i < sizeof(ids)) { + m[ids[i]] = true; + i = i + 1; + } + return m; +} + +// P6-R (MODEL_SPEC 7, case 7): replay-session coherence. Per (sync, +// key) the model carries a COUNTERFACTUAL session value — the producer +// policy's phase-final value under an all-fresh execution at this +// sync's epoch (announced by MEnv as a computed ghost). At seal, every +// committed row whose scripted derivation includes a session input +// (stamp >= 0: real values AND the miss marker 0) must match the +// counterfactual. Covers both duals: the fresh reader deriving from a +// read-miss whose producer was elided (7a: counterfactual v1, embedded +// 0) and the replayed row carrying a stamp the producer re-derived +// differently this sync (7b: counterfactual v2, embedded v1 travels +// with the copy). Defined only for configs that mutate upstream +// BETWEEN syncs (single epoch per (sync, scope)). +spec P6R observes eAnnSyncStart, eAnnCounterfactual, eAnnSeal { + var cf: map[int, int]; + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + cf = default(map[int, int]); + } + on eAnnCounterfactual do (p: (syncN: int, key: int, val: int)) { + cf[p.key] = p.val; + } + on eAnnSeal do (p: (syncN: int, partition: tPartition, manifest: map[int, int], blocked: bool, config: int)) { + var scopes: seq[int]; + var ids: seq[int]; + var i: int; + var j: int; + var s: int; + var st: int; + scopes = keys(p.partition); + i = 0; + while (i < sizeof(scopes)) { + s = scopes[i]; + ids = keys(p.partition[s]); + j = 0; + while (j < sizeof(ids)) { + st = p.partition[s][ids[j]].stamp; + if (st >= 0 && 0 in cf) { + assert st == cf[0], "P6-R: sealed session-derived row diverges from the all-fresh counterfactual value"; + } + j = j + 1; + } + i = i + 1; + } + } + } +} + +// C1Probe (MODEL_SPEC 9.5 C1, CO-6b-002 conformance question): a +// REACHABILITY probe, not a safety property — its red is the witness +// that a replay copy can commit in an attempt that never freshly +// consulted the scope (the hit check passed on the checkpoint-RESTORED +// hit map after a mid-chain stop-resume). Asserted only in the cell-53 +// probe test; the counterexample trace is the C1 answer ("reachable +// via the stop path"), to be confirmed against the real implementation +// through the chaos bridge (deliverable 6), not treated as a model bug. +spec C1Probe observes eAnnSyncStart, eAnnConsult, eAnnReplay { + var consultedBy: map[int, map[int, bool]]; // scope -> attempt gens + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + consultedBy = default(map[int, map[int, bool]]); + } + on eAnnConsult do (p: (syncN: int, scope: int, hit: bool, v: int, validated: bool, epoch: int, freshFetch: bool, diffVerdict: bool, attempt: int)) { + var att: map[int, bool]; + if (p.scope in consultedBy) { att = consultedBy[p.scope]; } + att[p.attempt] = true; + consultedBy[p.scope] = att; + } + on eAnnReplay do (p: (syncN: int, scope: int, vBase: int, cBase: int, ghost: tRoundGhost)) { + assert p.scope in consultedBy && p.ghost.attempt in consultedBy[p.scope], "C1-PROBE: replay copy committed in an attempt with no fresh consult of the scope (restored-hit mid-chain resume) — CO-6b-002 reachable via the stop path"; + } + } +} + +// P4Stuck (MODEL_SPEC 7 P4, safety form): livelock DETECTION checkable +// in bounded runs — two CONSECUTIVE resume attempts that fail from +// identical restored checkpoint state (scheduler-progress fields: +// stack, hits, replayed) with the same verdict (reason) at the same +// step (scope, cursor) constitute the deterministic re-failure finding +// (CO-6b-004's stuck-resume contract). The offending cursor is IN the +// failure checkpoint, so the recurrence is by construction, not luck. +spec P4Stuck observes eAnnSyncStart, eAnnAttemptFailed { + var have: bool; + var lastGen: int; + var lastStack: seq[tAction]; + var lastHits: map[int, int]; + var lastReplayed: map[int, bool]; + var lastScope: int; + var lastReason: int; + var lastCursor: int; + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + have = false; + } + on eAnnAttemptFailed do (p: (syncN: int, gen: int, stack: seq[tAction], hits: map[int, int], replayed: map[int, bool], scope: int, reason: int, cursor: int)) { + if (have && p.gen == lastGen + 1) { + assert !(p.stack == lastStack && p.hits == lastHits && p.replayed == lastReplayed && p.scope == lastScope && p.reason == lastReason && p.cursor == lastCursor), "P4-STUCK: consecutive attempts failed from identical restored checkpoint state at the same step (deterministic re-failure, CO-6b-004 stuck-resume)"; + } + have = true; + lastGen = p.gen; + lastStack = p.stack; + lastHits = p.hits; + lastReplayed = p.replayed; + lastScope = p.scope; + lastReason = p.reason; + lastCursor = p.cursor; + } + } +} + +// P4Live (MODEL_SPEC 7 P4, liveness form): hot while a started sync is +// unsealed. An ABANDONED sync (the ladder) stays hot until a LATER +// sync's seal cools it — "after budgets exhaust the chain eventually +// seals" in the bounded scenario shape. Meaningful with abandonLadder +// on; the ladder cell ends cold because the post-abandon sync runs +// cold against the drifted config and seals fresh. +spec P4Live observes eAnnSyncStart, eAnnSeal { + start state Sealed { + on eAnnSyncStart do (p: (syncN: int)) { goto Unsealed; } + ignore eAnnSeal; + } + hot state Unsealed { + on eAnnSeal do (p: (syncN: int, partition: tPartition, manifest: map[int, int], blocked: bool, config: int)) { + goto Sealed; + } + ignore eAnnSyncStart; + } +} + +// SealExpect (MODEL_SPEC 9.5): the scripted seal-state expectation — +// scenario 5's only executable oracle for the produce-blocked marking +// and the 5b silent scope dropout (both invisible to P1/P2: no rows, +// no illegal round). MEnv announces the expectation when the +// stop-stranding premise lands; this sync's seal must satisfy it. +// In the 5b crash cell the wantBlocked check failing IS the +// crash-window finding: the trigger fired volatile in attempt 2 and +// died with it before any checkpoint carried it. +spec SealExpect observes eAnnSyncStart, eAnnExpectSeal, eAnnSeal { + var active: bool; + var expSync: int; + var expScope: int; + var wantBlocked: bool; + var wantScopeEmpty: bool; + + start state Monitoring { + on eAnnSyncStart do (p: (syncN: int)) { + // Expectations are per-sync; a new sync clears any (already + // checked) prior expectation. + if (active && p.syncN != expSync) { active = false; } + } + on eAnnExpectSeal do (p: (syncN: int, scope: int, wantBlocked: bool, wantScopeEmpty: bool)) { + active = true; + expSync = p.syncN; + expScope = p.scope; + wantBlocked = p.wantBlocked; + wantScopeEmpty = p.wantScopeEmpty; + } + on eAnnSeal do (p: (syncN: int, partition: tPartition, manifest: map[int, int], blocked: bool, config: int)) { + if (!active || p.syncN != expSync) { return; } + if (wantBlocked) { + assert p.blocked, "SEAL-EXPECT: artifact sealed unblocked though a produce trigger fired this sync (5b: the crash-window loss)"; + } + if (wantScopeEmpty) { + assert !(expScope in p.partition) || sizeof(p.partition[expScope]) == 0, "SEAL-EXPECT: rows sealed for a scope whose only work was a silently-ignored carrier (5b dropout pin)"; + } + active = false; + } + } +} diff --git a/formal/walker/PSrc/Env.p b/formal/walker/PSrc/Env.p new file mode 100644 index 000000000..a116cae8f --- /dev/null +++ b/formal/walker/PSrc/Env.p @@ -0,0 +1,347 @@ +/* MEnv: the per-scenario test driver (MODEL_SPEC 3). Owns the sync + chain, attempt lifecycle, interruption scripting (one stop OR one + crash per scenario-1 config), and between-attempt upstream mutation. + + Interruption timing is genuinely explored, not hand-placed: the stop + (or crash) event is sent when the attempt starts and the P scheduler + explores every delivery point in the attempt's lifetime. Schedules + where it lands too late (after seal) are vacuous and legal. + + Crash quiesce (MODEL_SPEC 5): eCrash's queue position at MStore + partitions the dead attempt's ops; dropped ops are never acked, so + dead machines block forever and are abandoned. After eCrashAck no + dead-attempt op can commit (its position was behind the crash). The + sealed-read disambiguates "attempt beat the crash" schedules. */ + +machine MEnv { + var store: machine; + var upstream: machine; + var cfg: tScenarioCfg; + var stopUsed: bool; + var crashUsed: bool; + // Scenario-5 premise flag, re-evaluated per attempt from the + // restored checkpoint: the stop-stranding premise landed iff the + // checkpoint stack holds a replay-annotated carrier. The scripted + // drift inputs (compat recompute, G6 withdrawal) apply only in + // premise histories — non-premise schedules run undrifted and + // green, so exploration never manufactures out-of-premise alarms. + var premise: bool; + // Once compat drift lands it PERSISTS (the recomputed config is + // the new reality): later attempts AND later syncs run K2 — the + // 6c-ladder cell's abandoned-sync successor is cold against the + // K1 base exactly because of this latch. + var drifted: bool; + // Consecutive loud attempt failures this sync (resume-on-failure + // bookkeeping; abandonLadder abandons after k = 2). + var failCount: int; + + start state Run { + entry (c: tScenarioCfg) { + var syncN: int; + var attempt: int; + var gen: int; + var ckpt: tCheckpoint; + var sched: machine; + var syncDone: bool; + var crashedThisAttempt: bool; + var sealedSeen: bool; + var stoppedSeen: bool; + var failedSeen: bool; + var interrupted: bool; + cfg = c; + interrupted = false; + announce eAnnScenarioInit, (maxStaleness = 1,); + store = new MStore(); + upstream = new MUpstream(); + if (cfg.preMutate) { + // Case-3 world: upstream sits at e2 from the start (and + // never moves), so the fabricated sibling artifact B + // (rows at e1) is content-distinct from rows(up). + mutateUpstream(); + } + syncN = 1; + while (syncN <= cfg.nSyncs) { + // P2 corollary-run scoping: the verification sync is + // meaningful only in histories where the scripted + // interruption landed (MODEL_SPEC 7, "corollary runs"). + if (cfg.verificationOnlyIfInterrupted && syncN == cfg.nSyncs && !interrupted) { + break; + } + send store, eStoreReset, (client = this, syncN = syncN, sessVariant = cfg.sessVariant, extOverDelete = cfg.extOverDelete); + receive { case eStoreAck: {} } + announce eAnnSyncStart, (syncN = syncN,); + if (cfg.cell == 7) { + announceCounterfactual(syncN); + } + if (cfg.cell == 8) { + announceExtTruth(syncN); + } + attempt = 1; + syncDone = false; + failCount = 0; + while (!syncDone) { + assert attempt <= 3, "attempt budget exceeded (MODEL_SPEC small scope)"; + gen = syncN * 10 + attempt; + ckpt = attemptCheckpoint(attempt); + premise = hasStrandedCarrier(ckpt); + if (cfg.driftCompat && premise && syncN == cfg.interruptSync && attempt >= 2) { + drifted = true; + } + if ((cfg.cell == 51 || cfg.cell == 52) && syncN == cfg.interruptSync && attempt == 2 && premise) { + announceSealExpectation(syncN); + } + sched = new MSyncAttempt((env = this, store = store, upstream = upstream, gen = gen, syncN = syncN, cfg = cfg, aconfig = attemptConfig(syncN, attempt), g6 = attemptG6(syncN, attempt), ckpt = ckpt)); + crashedThisAttempt = false; + if ((cfg.interrupt == 1 || cfg.interrupt == 3) && syncN == cfg.interruptSync && attempt == 1 && !stopUsed) { + stopUsed = true; + send sched, eStopAttempt; + } + if ((cfg.interrupt == 2 && attempt == 1 || cfg.interrupt == 3 && attempt == 2) && syncN == cfg.interruptSync && !crashUsed) { + crashUsed = true; + crashedThisAttempt = true; + send store, eCrashArm, (client = this, gen = gen); + } + if (crashedThisAttempt) { + receive { case eCrashAck: {} } + send store, eReadSealedReq, (client = this,); + sealedSeen = false; + receive { + case eReadSealedResp: (r: (sealed: bool)) { + sealedSeen = r.sealed; + } + } + if (sealedSeen) { + // The attempt sealed before the crash landed; + // consume its end report and finish the sync. + receive { case eAttemptEnded: (r: (stopped: bool, sealed: bool, failed: bool)) {} } + syncDone = true; + } else { + // Dead attempt (quiesced by ack); apply the + // between-attempt script and resume. + interrupted = true; + betweenAttempts(syncN); + attempt = attempt + 1; + } + } else { + sealedSeen = false; + stoppedSeen = false; + failedSeen = false; + receive { + case eAttemptEnded: (r: (stopped: bool, sealed: bool, failed: bool)) { + sealedSeen = r.sealed; + stoppedSeen = r.stopped; + failedSeen = r.failed; + } + } + if (sealedSeen) { + syncDone = true; + } else { + assert stoppedSeen || failedSeen, "attempt ended neither sealed, stopped, nor failed"; + if (failedSeen) { + // Resume-on-failure (MODEL_SPEC 3): the + // ladder abandons the sync UNSEALED + // after k = 2 identical failures; the + // next sync starts against the last + // sealed artifact (6c ladder). + failCount = failCount + 1; + if (cfg.toggles.abandonLadder && failCount >= 2) { + syncDone = true; + } else { + betweenAttempts(syncN); + attempt = attempt + 1; + } + } else { + interrupted = true; + betweenAttempts(syncN); + attempt = attempt + 1; + } + } + } + } + // Overlay-family premise: one upstream mutation between + // sync 1 and sync 2 (e1 -> e2), none afterwards. + if (cfg.mutateBetweenSyncs && syncN == 1) { + mutateUpstream(); + } + syncN = syncN + 1; + } + goto Finished; + } + } + + state Finished { + ignore eAttemptEnded, eCrashAck; + } + + fun attemptCheckpoint(attempt: int): tCheckpoint { + var root: seq[tAction]; + var ck: tCheckpoint; + var got: bool; + if (attempt == 1) { + return rootCheckpoint(); + } + send store, eReadCheckpointReq, (client = this,); + receive { + case eReadCheckpointResp: (r: (ckpt: tCheckpoint, hasCkpt: bool)) { + ck = r.ckpt; + got = r.hasCkpt; + } + } + if (!got) { + // Crash before any post-Init checkpoint: restart-from-root. + return rootCheckpoint(); + } + return ck; + } + + // Fresh sync root: empty hit map and replayed set (per-sync volatile + // scheduler state). Cell 2 roots TWO same-op actions — H the session + // writer (aid 1) and G the session reader (aid 2) — one batch when + // the nondet cap allows; every other cell roots one planning action. + fun rootCheckpoint(): tCheckpoint { + var root: seq[tAction]; + if (cfg.cell == 2) { + root += (0, (aid = 1, op = OP_PLANNING, scope = 0, cursor = 0, hasAnnotation = false, annotationV = -1, publishes = false)); + root += (1, (aid = 2, op = OP_PLANNING, scope = 0, cursor = 0, hasAnnotation = false, annotationV = -1, publishes = false)); + return (stack = root, hits = default(map[int, int]), replayed = default(map[int, bool]), blocked = false); + } + if (cfg.cell == 21) { + // P6-C chassis: cell 2's actors with the ROOT ORDER + // REVERSED — LIFO pops the writer H first, so a loop-top + // checkpoint can commit H's session write while the reader + // G still has its run ahead of it. That is the amnesia + // premise (checkpoint-committed value + a re-run read), + // structurally unreachable in cell 2 where G pops first. + root += (0, (aid = 2, op = OP_PLANNING, scope = 0, cursor = 0, hasAnnotation = false, annotationV = -1, publishes = false)); + root += (1, (aid = 1, op = OP_PLANNING, scope = 0, cursor = 0, hasAnnotation = false, annotationV = -1, publishes = false)); + return (stack = root, hits = default(map[int, int]), replayed = default(map[int, bool]), blocked = false); + } + if (cfg.cell == 7) { + // Kinds in sequential phases: W (top, popped first) and R + // carry DIFFERENT ops, so the same-op batch prefix never + // spans both — R's phase starts only after W's completes. + root += (0, (aid = 2, op = OP_CARRIER, scope = 1, cursor = 0, hasAnnotation = false, annotationV = -1, publishes = false)); + root += (1, (aid = 1, op = OP_PLANNING, scope = 0, cursor = 0, hasAnnotation = false, annotationV = -1, publishes = false)); + return (stack = root, hits = default(map[int, int]), replayed = default(map[int, bool]), blocked = false); + } + if (cfg.cell == 53) { + // C1-probe root: ONE action carrying the mid-chain replay + // annotation in its token from the start (the policy + // places replay at page 1; the annotation's validator is + // truthful V1). Sync 1 never reaches it (consult misses -> + // fresh chain at cursor 2). + root += (0, (aid = 1, op = OP_PLANNING, scope = 0, cursor = 0, hasAnnotation = true, annotationV = 1, publishes = true)); + return (stack = root, hits = default(map[int, int]), replayed = default(map[int, bool]), blocked = false); + } + root += (0, (aid = 1, op = OP_PLANNING, scope = 0, cursor = 0, hasAnnotation = false, annotationV = -1, publishes = false)); + return (stack = root, hits = default(map[int, int]), replayed = default(map[int, bool]), blocked = false); + } + + // The stop-stranding premise witness: a replay-annotated carrier + // sits in the restored checkpoint stack (spawned by the planning + // consult, undrained when the interruption checkpointed). + fun hasStrandedCarrier(ck: tCheckpoint): bool { + var i: int; + i = 0; + while (i < sizeof(ck.stack)) { + if (ck.stack[i].hasAnnotation) { return true; } + i = i + 1; + } + return false; + } + + // Per-attempt compat config (0 = configs unmodeled). 5a drift + // (trigger 1): attempts >= 2 of the drift sync recompute K2 = + // K1 + 1 — only in premise histories (see the premise var note), + // PERSISTING once landed (the drifted latch). + fun attemptConfig(n: int, attempt: int): int { + if (cfg.baseConfig == 0) { return 0; } + if (drifted) { return cfg.baseConfig + 1; } + return cfg.baseConfig; + } + + // Per-attempt G6 capability bit. 5b withdrawal (trigger 2): attempt + // 2 of the drift sync EXACTLY runs handling-less — attempt 3 (the + // crash cell's resume) has handling back, which is what makes the + // lost block observable rather than re-detected. + fun attemptG6(n: int, attempt: int): bool { + if (cfg.withdrawG6 && premise && n == cfg.interruptSync && attempt == 2) { + return false; + } + return true; + } + + // Scenario-5 seal-state oracle (MODEL_SPEC 9.5b): announced when + // the premise lands, checked by the SealExpect monitor at this + // sync's seal. Both 5a and 5b expect a BLOCKED seal (trigger 1 / + // trigger 2 fired at attempt-2 install); the 5b stop cell also + // pins the silent dropout (partition[S] empty). The 5b crash cell + // wants blocked but may legally seal rows (attempt 3 is warm), and + // its red verdict is the crash-window: a schedule sealing + // UNBLOCKED because the trigger's flag died with attempt 2. + fun announceSealExpectation(n: int) { + announce eAnnExpectSeal, (syncN = n, scope = 0, wantBlocked = true, wantScopeEmpty = cfg.cell == 52 && cfg.interrupt == 1); + } + + // Between-attempt premise script: base swap (case 3), upstream + // mutation (drift premises). Order: swap first — both are env-level + // initial-condition surgery for the NEXT attempt. + fun betweenAttempts(n: int) { + if (cfg.swapBase) { + swapBaseArtifact(); + } + if (cfg.mutateBetweenAttempts) { + mutateUpstream(); + } + // Scenario 8: the truth ghost follows every mutation, so the + // P8 CURRENT clause always compares an attempt's list against + // the answer that was live when the attempt listed. + if (cfg.cell == 8 && cfg.mutateBetweenAttempts) { + announceExtTruth(n); + } + } + + // Case-3 sibling artifact B: truthful rows at e1 under validator 1; + // equal compat record (compat is not modeled as a distinguishing + // axis here — both artifacts are warm-installable); content-distinct + // from rows(up) = rows(e2) because id 1 exists only at e1. + fun swapBaseArtifact() { + var rowsB: seq[tRow]; + rowsB += (0, (id = 0, epoch = 1, hops = 0, config = 0, stamp = -1)); + rowsB += (1, (id = 1, epoch = 1, hops = 0, config = 0, stamp = -1)); + send store, eSwapBase, (client = this, scope = 0, vB = 1, rowsB = rowsB); + receive { case eStoreAck: {} } + } + + fun mutateUpstream() { + send upstream, eMutate, (client = this, scope = 0); + receive { case eMutateAck: {} } + } + + // P6-R counterfactual ghost (MODEL_SPEC 7): W's policy writes + // K = f(epoch of W's scope) as its phase-final value, so the + // all-fresh counterfactual at this sync is exactly that epoch — + // computed by a read, never by a second execution. Sound only + // because case-7 configs mutate upstream BETWEEN syncs (single + // epoch per (sync, scope)). + fun announceCounterfactual(n: int) { + var e: int; + send upstream, eValidateReq, (client = this, scope = 0, v = -1); + receive { + case eValidateResp: (r: (ok: bool, epoch: int)) { e = r.epoch; } + } + announce eAnnCounterfactual, (syncN = n, key = 0, val = e); + } + + // Scenario-8 truth ghost: the external source's CURRENT answer + // (the truthful epoch table at the current epoch) — computed by a + // read, never by an execution, like the P6-R counterfactual. + fun announceExtTruth(n: int) { + var e: int; + send upstream, eValidateReq, (client = this, scope = 0, v = -1); + receive { + case eValidateResp: (r: (ok: bool, epoch: int)) { e = r.epoch; } + } + announce eAnnExtTruth, (syncN = n, ids = upstreamRowIds(e)); + } +} diff --git a/formal/walker/PSrc/Events.p b/formal/walker/PSrc/Events.p new file mode 100644 index 000000000..20d096059 --- /dev/null +++ b/formal/walker/PSrc/Events.p @@ -0,0 +1,214 @@ +/* Events. Store ops are request/ack: a page's store ops commit (and + announce) before the worker reports its action transition, matching + the shipped synchronous store API (MODEL_SPEC 3: announce on commit; + a page COMMITS when the last of its prescribed store ops commits). + Every attempt-owned request carries gen; the crash protocol drops + ops from dead gens (MODEL_SPEC 5: ops behind eCrash in MStore's + queue are DROPPED — a dropped op is never acked, which also gives + quiesce: dead senders block forever awaiting acks). */ + +// ---- store ops (worker/scheduler -> MStore; eStoreAck on commit) ---- +event eLookupReq: (client: machine, gen: int, scope: int); +event eLookupResp: (hit: bool, v: int); +event eBaseReadReq: (client: machine, gen: int, scope: int); // base-binding check read +event eBaseReadResp: (v: int, present: bool); +event eClearScope: (client: machine, gen: int, scope: int, ghost: tRoundGhost); +event eCopyScope: (client: machine, gen: int, scope: int, ghost: tRoundGhost); +event eUpsertPage: (client: machine, gen: int, scope: int, rows: seq[tRow], ghost: tRoundGhost); +event ePublishEntry: (client: machine, gen: int, scope: int, v: int, ghost: tRoundGhost); +// Scenario-6 marker machinery: the marker is a PER-SCOPE STORE ROW in +// the current artifact — authoritative consult provenance, durable at +// op commit, reset at sync rotation (a new sync always re-consults). +event eMarkerPut: (client: machine, gen: int, scope: int); +event eMarkerReadReq: (client: machine, gen: int, scope: int); +event eMarkerReadResp: (marked: bool); +// V-ATOMIC unit: {clear, copy, marker, publish} committed as ONE atomic +// store op (single queue position — a crash cannot split it). Announces +// its constituent ops so the monitor vocabulary is unchanged; the round +// completes at unit commit (round-5 F1 pin). +event eReplayUnit: (client: machine, gen: int, scope: int, v: int, ghost: tRoundGhost); +// V-OVERLAY-UNIT: {clear, copy(base), overlay upserts/tombstones in +// prescribed page order, marker, publish(V_to)} as ONE atomic store op. +event eOverlayUnit: (client: machine, gen: int, scope: int, v: int, upserts: seq[tRow], removes: seq[int], ghost: tRoundGhost); +// Per-page tombstone commit (shipped path, used by the naive misdraw). +event eTombstonePage: (client: machine, gen: int, scope: int, removes: seq[int], ghost: tRoundGhost); +event eCheckpointReq: (client: machine, gen: int, ckpt: tCheckpoint); +// Seal carries the sealing attempt's produce-blocked flag and compat +// config (blocked last made durable by the forced pre-seal checkpoint; +// the seal is its final resting place on the artifact; config feeds +// P1's clause (c) at seal). +event eSealReq: (client: machine, gen: int, blocked: bool, config: int); +// Case-5 produce state: the artifact's compat record (written by each +// handling attempt at install) and the produce-state read at attempt +// start (install-time gates G4/G7 + block triggers 1/2 read exactly +// this). hasPrev = a previous sealed artifact exists (usable-prev). +event eCompatPut: (client: machine, gen: int, k: int); +event eProduceReadReq: (client: machine, gen: int); +event eProduceReadResp: (prevCompat: int, prevBlocked: bool, curCompat: int, hasCur: bool, hasPrev: bool); +event eStoreAck; +// Response to any op from a dead (crashed) gen: the op was DROPPED, not +// committed. Receivers park in a dead state instead of blocking, so the +// pinned drop semantics don't register as P deadlocks. +event eStoreDead; +// Crash injection (MODEL_SPEC 3 MCrashInjector / 5 crash protocol): +// arming lets MStore fire the crash nondeterministically at any op +// boundary of the armed gen — every queue position is explored with +// per-op granularity. Resolution is guaranteed by the seal op (crash +// fires before or immediately after it), so the env's ack wait always +// terminates. +event eCrashArm: (client: machine, gen: int); +event eCrashAck; +event eStoreReset: (client: machine, syncN: int, sessVariant: int, extOverDelete: bool); // begin-of-sync rotation (env, not gen-gated) +event eReadCheckpointReq: (client: machine); +event eReadCheckpointResp: (ckpt: tCheckpoint, hasCkpt: bool); +// Session store (MODEL_SPEC 3/9 cases 2 and 7): sync-scoped KV, durable +// at op commit, survives attempts and crashes within the sync, reset at +// sync rotation. Gen-gated like every attempt-owned op. scope = the +// acting kind's scope (phase attribution for produce-side taint); +// taint = the acting config's toggle verdict for THIS op (computed by +// the worker so the store stays config-free). +event eSessionSet: (client: machine, gen: int, scope: int, key: int, val: int, taint: bool); +event eSessionGetReq: (client: machine, gen: int, scope: int, key: int, taint: bool); +event eSessionGetResp: (found: bool, val: int); +// Case-3 premise: env swaps the previous sealed artifact for a +// fabricated-but-legal sibling B (equal compat record, validator +// vB, content = truthful rows at vB's epoch). Env-level, not gen-gated. +event eSwapBase: (client: machine, scope: int, vB: int, rowsB: seq[tRow]); +// Scenario-8 external-principal ops (MODEL_SPEC abstraction of +// SyncExternalResources). The ext keyspace is separate from scope +// partitions (external principals carry the BatonID annotation and +// live beside connector rows; the model keeps them in their own map). +// eExtReconReq = deleteStaleExternalPrincipals: with supported TRUE +// it deletes every ext row absent from live (the current listed +// answer) as ONE atomic op; with supported FALSE it is the +// warn-and-continue degrade — a no-op that still announces the round +// (the LIST happened; reconciliation did not). +event eExtReconReq: (client: machine, gen: int, live: seq[int], supported: bool); +// eExtCopy = the current answer's principal writes (page-atomic per +// the MODEL_SPEC 1 store abstraction; partial-write debris reduces to +// the same stale-survivor class the crash-between-ops windows cover). +event eExtCopy: (client: machine, gen: int, ids: seq[int]); + +// ---- upstream (synchronous request/response) ---- +event eValidateReq: (client: machine, scope: int, v: int); +event eValidateResp: (ok: bool, epoch: int); +event eFetchReq: (client: machine, scope: int, page: int); +event eFetchResp: (rows: seq[tRow], epoch: int, morePages: bool); +// Overlay diff (changed-with-diff verdict): pages of upserts/removes +// from a base epoch to the current epoch. +event eDiffReq: (client: machine, scope: int, fromEpoch: int, page: int); +event eDiffResp: (upserts: seq[tRow], removes: seq[int], epoch: int, morePages: bool); +event eMutate: (client: machine, scope: int); +event eMutateAck; + +event eReadSealedReq: (client: machine); +event eReadSealedResp: (sealed: bool); + +// ---- scheduler <-> worker ---- +// hits/replayed here are dispatch-time snapshots kept for reference; +// the load-bearing reads are LIVE: the replayed set via lock grant or +// eReplayedCheckReq (case-4 TOCTOU), the hit map via eHitReadReq +// (case-3A rebind). +event eDispatch: (action: tAction, hits: map[int, int], replayed: map[int, bool]); +event eActionTransition: (aid: int, nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int); +event eContinuePage; +event eAbortWorker; +event eWorkerAborted: (aid: int, cursor: int); + +// ---- scope locks + replayed-set access (CO-6b-007 / MODEL_SPEC 4) ---- +// With scopeLocks ON the oncePerScope check-and-mark is lock-mediated: +// the grant carries the replayed status and the release commits the +// mark atomically before the next grant. With locks OFF the worker +// reads the status lock-free and the mark lands later, at the action +// transition — the case-4 check-then-mark TOCTOU window. +event eScopeLockAcquire: (worker: machine, scope: int); +event eScopeLockGrant: (replayed: bool); +event eScopeLockRelease: (scope: int, mark: bool); +event eReplayedCheckReq: (worker: machine, scope: int); +event eReplayedCheckResp: (replayed: bool); +// Live hit-map read (MODEL_SPEC 3: hits live in ONE sync-level map, +// recorded at lookup time, last-write-wins). The carrier's hit check +// and binding compare read this map at DRAIN time — the case-3A +// rebind hole lives in exactly this read-after-overwrite. +event eHitReadReq: (worker: machine, scope: int); +event eHitReadResp: (has: bool, v: int); + +// ---- scheduler self-events (commit/reply/dispatch split so the stop +// can interleave between a transition's state commit and the worker's +// continuation — the stop-stranding premise generator, MODEL_SPEC 9) ---- +event eReplyWorker: (aid: int, worker: machine); +event eDispatchPending; +event eLoopTop; + +// ---- attempt-level loud failure (MODEL_SPEC 4 failure semantics; +// P4 cells only — cfg.loudColdFailsAttempt) ---- +// Worker -> scheduler: a cold verdict failed the chain loudly at the +// offending cursor; the scheduler restores the action at that cursor +// (so the failure recurs deterministically from the checkpoint), +// quiesces, force-checkpoints, and reports the attempt failed. +event eChainFailed: (aid: int, cursor: int, scope: int, reason: int); + +// ---- env control ---- +event eStopAttempt; +event eAttemptEnded: (stopped: bool, sealed: bool, failed: bool); + +// ---- announce vocabulary (MODEL_SPEC 7) ---- +event eAnnScenarioInit: (maxStaleness: int); +event eAnnSyncStart: (syncN: int); +// attempt = the consulting attempt's gen: the C1 probe's oracle +// compares it to the replaying round's attempt ghost (a replay whose +// scope was consulted only in an EARLIER attempt ran on a restored +// hit — the CO-6b-002 conformance question). +event eAnnConsult: (syncN: int, scope: int, hit: bool, v: int, validated: bool, epoch: int, freshFetch: bool, diffVerdict: bool, attempt: int); +// cBase = the copied base content's compat config ghost (first row's +// tag; -1 when the base is empty or configs are unmodeled). P1's +// config clause (c) compares it to the round's attempt config. +event eAnnReplay: (syncN: int, scope: int, vBase: int, cBase: int, ghost: tRoundGhost); +event eAnnClear: (syncN: int, scope: int, ghost: tRoundGhost); +event eAnnUpsert: (syncN: int, scope: int, rows: seq[tRow], ghost: tRoundGhost); +event eAnnTombstones: (syncN: int, scope: int, removes: seq[int], ghost: tRoundGhost); +event eAnnPublish: (syncN: int, scope: int, v: int, ghost: tRoundGhost); +event eAnnCheckpoint: (syncN: int); +event eAnnStop: (syncN: int); +event eAnnCrash: (syncN: int); +// config = the sealing attempt's compat config (0 when unmodeled); +// P1 clause (c) compares every sealed row's ghost tag to it. +event eAnnSeal: (syncN: int, partition: tPartition, manifest: map[int, int], blocked: bool, config: int); +// Scripted seal-state expectation (scenario 5 oracle): announced by +// MEnv when the stop-stranding premise lands; the SealExpect monitor +// checks this sync's seal. wantBlocked: the artifact must seal +// produce-blocked (its violation in the 5b crash cell IS the +// crash-window finding). wantScopeEmpty: partition[scope] must seal +// empty — the 5b silent-dropout pin (the B1-ignored carrier leaves no +// rows and no failure; this scripted expectation is its only +// executable oracle, MODEL_SPEC 9.5b). +event eAnnExpectSeal: (syncN: int, scope: int, wantBlocked: bool, wantScopeEmpty: bool); +// Session announce (P6-A vocabulary): committed session writes and the +// final session KV at seal ride the announce channel like row ops. +event eAnnSessionSet: (syncN: int, key: int, val: int); +event eAnnSessionGet: (syncN: int, key: int, found: bool, val: int); +// P6-R counterfactual ghost (MODEL_SPEC 7): the producer policy's +// phase-final session value under an all-fresh execution at this +// sync's epoch — computable (deterministic policies, sequential +// phases), announced by MEnv at sync start, never executed. +event eAnnCounterfactual: (syncN: int, key: int, val: int); +// Loud-cold: a gate (binding / warm-gate) detected a mismatch and the +// chain failed cold instead of copying — the mitigation's success path. +event eAnnLoudCold: (syncN: int, scope: int, reason: int); // 1=binding, 2=warmGate +// Scenario-8 announce vocabulary (P8). eAnnExtTruth: the env's ghost +// of the external source's CURRENT answer, announced at sync start +// and after every between-attempt mutation — the truth the CURRENT +// clause compares each attempt's list against. eAnnExtRound: the +// store's commit-side record of one external round (the answer the +// attempt listed, whether reconciliation ran, what it deleted). +// eAnnExtSeal: the ext keyspace as sealed, announced with the seal. +event eAnnExtTruth: (syncN: int, ids: seq[int]); +event eAnnExtRound: (syncN: int, live: seq[int], supported: bool, deleted: seq[int]); +event eAnnExtSeal: (syncN: int, ids: seq[int]); +// Attempt-level loud failure record (P4): the RESTORED checkpoint's +// scheduler-progress state (stack/hits/replayed — the ingest-quality +// blocked flag is deliberately excluded: it does not influence the +// verdict) plus the failing step (scope, reason, offending cursor). +// P4Stuck's red = two CONSECUTIVE attempts failing from identical +// restored state at the same step — CO-6b-004's stuck-resume contract. +event eAnnAttemptFailed: (syncN: int, gen: int, stack: seq[tAction], hits: map[int, int], replayed: map[int, bool], scope: int, reason: int, cursor: int); diff --git a/formal/walker/PSrc/Scheduler.p b/formal/walker/PSrc/Scheduler.p new file mode 100644 index 000000000..0961f77c3 --- /dev/null +++ b/formal/walker/PSrc/Scheduler.p @@ -0,0 +1,496 @@ +/* MSyncAttempt: the walker scheduler, one machine per attempt + (MODEL_SPEC 3). LIFO action stack; batch = consecutive same-op + prefix, cap nondet in {1,2}; spawned same-op actions are admitted to + the live batch uncapped. Checkpoints at loop tops (between dispatch + batches) and force-written on graceful stop, capturing live mid-batch + state: mid-chain cursors, admitted-but-undrained spawns, hits + recorded during the aborted batch (GLOSSARY "Checkpoint"). + + The transition-commit / worker-reply / spawn-dispatch split into + separate self-events mirrors the shipped ordering (state commit + precedes queue admission) and is what makes the stop-stranding + premise reachable as a genuine interleaving rather than by + hand-placement (MODEL_SPEC 9, premise generator). */ + +machine MSyncAttempt { + var env: machine; + var store: machine; + var upstream: machine; + var gen: int; + var syncN: int; + var cfg: tScenarioCfg; + var warm: bool; + var aconfig: int; // this attempt's compat config (0 = unmodeled) + var g6: bool; // this attempt's source-cache capability bit + // Produce-blocked flag: ingest-quality state, volatile at + // checkpoint-cadence durability (MODEL_SPEC 5) — restored from the + // checkpoint token, OR'd with this attempt's install triggers, + // durable again only at the next checkpoint and at seal. The 5b + // crash window is a crash landing between the install-time trigger + // and the first checkpoint carrying it. + var blocked: bool; + var stack: seq[tAction]; + var hits: map[int, int]; + var replayed: map[int, bool]; + var workers: seq[machine]; + var busyWith: map[machine, int]; // worker -> aid (absent = free) + var outstanding: map[int, tAction]; // aid -> action at last committed cursor + var owner: map[int, machine]; + var pendingSpawns: seq[tAction]; + var restoreList: seq[tAction]; + var stopping: bool; + var storeDead: bool; // this gen crashed; ops dropped (MODEL_SPEC 5) + var lockHeld: map[int, machine]; // scope -> holding worker + var lockWait: map[int, seq[machine]]; // scope -> waiting workers + // Attempt-level loud failure (P4 cells): the failing step, plus + // the RESTORED checkpoint's progress state for the P4Stuck record. + var failing: bool; + var failScope: int; + var failReason: int; + var failCursor: int; + var restoredStack: seq[tAction]; + var restoredHits: map[int, int]; + var restoredReplayed: map[int, bool]; + + start state Boot { + entry (p: (env: machine, store: machine, upstream: machine, gen: int, syncN: int, cfg: tScenarioCfg, aconfig: int, g6: bool, ckpt: tCheckpoint)) { + env = p.env; store = p.store; upstream = p.upstream; + gen = p.gen; syncN = p.syncN; cfg = p.cfg; + aconfig = p.aconfig; g6 = p.g6; + stack = p.ckpt.stack; + hits = p.ckpt.hits; + replayed = p.ckpt.replayed; + blocked = p.ckpt.blocked; + restoredStack = p.ckpt.stack; + restoredHits = p.ckpt.hits; + restoredReplayed = p.ckpt.replayed; + warm = true; + if (cfg.cell == 51 || cfg.cell == 52) { + installProduceState(); + if (storeDead) { goto DeadState; } + } + workers += (0, new MWorker((scheduler = this, store = store, upstream = upstream, gen = gen, syncN = syncN, cfg = cfg, warm = warm, g6 = g6, aconfig = aconfig))); + workers += (1, new MWorker((scheduler = this, store = store, upstream = upstream, gen = gen, syncN = syncN, cfg = cfg, warm = warm, g6 = g6, aconfig = aconfig))); + goto Running; + } + } + + // Warm install at attempt start (MODEL_SPEC 3, + // installSourceCacheLookup): warm = usable-prev ∧ this attempt's G6 + // bit ∧ G4(prev not replay-blocked) ∧ G7(compat byte-match vs the + // prev artifact's compat record) ∧ no drift this attempt (vs this + // sync's earlier-attempt record). Produce-side block triggers fire + // HERE (install time, MODEL_SPEC 4): trigger 1 = compat recomputed + // differently across attempts of this sync (B4); trigger 2 = this + // attempt runs handling-less (G6 off) over prior-attempt produce + // state. The trigger lands in the VOLATILE blocked flag only. + // A handling attempt then records its own compat config. Scoped to + // the cells that model configs so calibrated op streams elsewhere + // are undisturbed. + fun installProduceState() { + var prevCompat: int; + var prevBlocked: bool; + var curCompat: int; + var hasCur: bool; + var hasPrev: bool; + send store, eProduceReadReq, (client = this, gen = gen); + receive { + case eProduceReadResp: (r: (prevCompat: int, prevBlocked: bool, curCompat: int, hasCur: bool, hasPrev: bool)) { + prevCompat = r.prevCompat; + prevBlocked = r.prevBlocked; + curCompat = r.curCompat; + hasCur = r.hasCur; + hasPrev = r.hasPrev; + } + case eStoreDead: { storeDead = true; } + } + if (storeDead) { return; } + warm = hasPrev && g6 && !prevBlocked && prevCompat == aconfig && (!hasCur || curCompat == aconfig); + if (hasCur && curCompat != aconfig) { blocked = true; } // trigger 1 (B4) + if (!g6 && hasCur) { blocked = true; } // trigger 2 (CO-6b-003) + if (g6) { + send store, eCompatPut, (client = this, gen = gen, k = aconfig); + receive { + case eStoreAck: {} + case eStoreDead: { storeDead = true; } + } + } + } + + state Running { + entry { + loopTop(); + } + + on eLoopTop do { + loopTop(); + } + + on eActionTransition do (p: (aid: int, nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int)) { + var act: tAction; + var w: machine; + // COMMIT the transition into live scheduler state first + // (hit recording pinned at lookup time is carried here; the + // reply to the worker is a separate self-event so a stop can + // land between commit and continuation). + if (p.hasHit) { hits[p.hitScope] = p.hitV; } + if (p.markReplayed) { replayed[p.replayedScope] = true; } + if (p.hasSpawn) { pendingSpawns += (sizeof(pendingSpawns), p.spawn); } + if (p.done) { + w = owner[p.aid]; + outstanding -= p.aid; + owner -= p.aid; + busyWith -= w; + if (p.hasSpawn && !stopping && !failing) { send this, eDispatchPending; } + checkBatchEnd(); + } else { + act = outstanding[p.aid]; + act.cursor = p.nextCursor; + outstanding[p.aid] = act; + send this, eReplyWorker, (aid = p.aid, worker = owner[p.aid]); + if (p.hasSpawn && !stopping && !failing) { send this, eDispatchPending; } + } + } + + on eReplyWorker do (p: (aid: int, worker: machine)) { + if (stopping || failing) { + send p.worker, eAbortWorker; + } else { + send p.worker, eContinuePage; + } + } + + on eDispatchPending do { + dispatchPending(); + } + + on eScopeLockAcquire do (p: (worker: machine, scope: int)) { + var q: seq[machine]; + if (stopping) { + send p.worker, eAbortWorker; + return; + } + if (p.scope in lockHeld) { + if (p.scope in lockWait) { q = lockWait[p.scope]; } + q += (sizeof(q), p.worker); + lockWait[p.scope] = q; + return; + } + lockHeld[p.scope] = p.worker; + send p.worker, eScopeLockGrant, (replayed = p.scope in replayed,); + } + + on eScopeLockRelease do (p: (scope: int, mark: bool)) { + var q: seq[machine]; + var w: machine; + if (p.mark) { replayed[p.scope] = true; } + lockHeld -= p.scope; + if (p.scope in lockWait && sizeof(lockWait[p.scope]) > 0) { + q = lockWait[p.scope]; + w = q[0]; + q -= (0); + lockWait[p.scope] = q; + if (stopping) { + send w, eAbortWorker; + } else { + lockHeld[p.scope] = w; + send w, eScopeLockGrant, (replayed = p.scope in replayed,); + } + } + } + + on eReplayedCheckReq do (p: (worker: machine, scope: int)) { + // Lock-free read (scopeLocks OFF): the mark lands later at + // the transition — the TOCTOU window is real here. + send p.worker, eReplayedCheckResp, (replayed = p.scope in replayed,); + } + + on eHitReadReq do (p: (worker: machine, scope: int)) { + if (stopping) { + send p.worker, eAbortWorker; + return; + } + if (p.scope in hits) { + send p.worker, eHitReadResp, (has = true, v = hits[p.scope]); + } else { + send p.worker, eHitReadResp, (has = false, v = -1); + } + } + + on eStopAttempt do { + var scopes: seq[int]; + var q: seq[machine]; + var i: int; + var j: int; + stopping = true; + announce eAnnStop, (syncN = syncN,); + // Grant-waiters abort at the wait point (their pages restart + // from the current cursor on resume). + scopes = keys(lockWait); + i = 0; + while (i < sizeof(scopes)) { + q = lockWait[scopes[i]]; + j = 0; + while (j < sizeof(q)) { + send q[j], eAbortWorker; + j = j + 1; + } + lockWait -= scopes[i]; + i = i + 1; + } + checkBatchEnd(); + } + + on eWorkerAborted do (p: (aid: int, cursor: int)) { + var act: tAction; + var w: machine; + act = outstanding[p.aid]; + act.cursor = p.cursor; + restoreList += (sizeof(restoreList), act); + w = owner[p.aid]; + outstanding -= p.aid; + owner -= p.aid; + busyWith -= w; + checkBatchEnd(); + } + + on eChainFailed do (p: (aid: int, cursor: int, scope: int, reason: int)) { + // Attempt-level loud failure: the offending action is + // restored AT ITS FAILING CURSOR (so the forced failure + // checkpoint reproduces the premise and the failure recurs + // deterministically on resume — MODEL_SPEC 4). + var act: tAction; + var w: machine; + act = outstanding[p.aid]; + act.cursor = p.cursor; + restoreList += (sizeof(restoreList), act); + w = owner[p.aid]; + outstanding -= p.aid; + owner -= p.aid; + busyWith -= w; + failing = true; + failScope = p.scope; + failReason = p.reason; + failCursor = p.cursor; + checkBatchEnd(); + } + } + + state Done { + ignore eStopAttempt, eActionTransition, eReplyWorker, eDispatchPending, eLoopTop, eWorkerAborted, eScopeLockRelease, eReplayedCheckReq, eChainFailed; + on eScopeLockAcquire do (p: (worker: machine, scope: int)) { + send p.worker, eAbortWorker; + } + on eHitReadReq do (p: (worker: machine, scope: int)) { + send p.worker, eAbortWorker; + } + } + + // Crashed attempt: every op from this gen is dropped. Workers are + // released (aborts) so nothing blocks; the machine parks forever. + // MEnv resumes from the last durable checkpoint independently. + state DeadState { + entry { + var i: int; + i = 0; + while (i < sizeof(workers)) { + send workers[i], eAbortWorker; + i = i + 1; + } + } + on eActionTransition do (p: (aid: int, nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int)) { + if (p.aid in owner) { + send owner[p.aid], eAbortWorker; + } + } + ignore eStopAttempt, eReplyWorker, eDispatchPending, eLoopTop, eWorkerAborted, eScopeLockRelease, eReplayedCheckReq, eChainFailed; + on eScopeLockAcquire do (p: (worker: machine, scope: int)) { + send p.worker, eAbortWorker; + } + on eHitReadReq do (p: (worker: machine, scope: int)) { + send p.worker, eAbortWorker; + } + } + + fun loopTop() { + if (stopping) { + finalizeStopIfQuiesced(); + return; + } + if (failing) { + finalizeFailIfQuiesced(); + return; + } + doCheckpoint(); + if (storeDead) { + goto DeadState; + } + if (sizeof(stack) == 0 && sizeof(outstanding) == 0 && sizeof(pendingSpawns) == 0) { + doSeal(); + return; + } + popBatchAndDispatch(); + } + + fun doCheckpoint() { + var ck: tCheckpoint; + ck = buildCheckpoint(); + send store, eCheckpointReq, (client = this, gen = gen, ckpt = ck); + receive { + case eStoreAck: {} + case eStoreDead: { storeDead = true; } + } + } + + // Checkpoint token contents pinned by MODEL_SPEC 5: action stack + // (with cursors as currently committed, incl. admitted-but-undrained + // spawns and outstanding mid-chain actions), hit map, replayed set. + fun buildCheckpoint(): tCheckpoint { + var st: seq[tAction]; + var i: int; + var aids: seq[int]; + st = stack; + aids = keys(outstanding); + i = 0; + while (i < sizeof(aids)) { + st += (sizeof(st), outstanding[aids[i]]); + i = i + 1; + } + i = 0; + while (i < sizeof(restoreList)) { + st += (sizeof(st), restoreList[i]); + i = i + 1; + } + i = 0; + while (i < sizeof(pendingSpawns)) { + st += (sizeof(st), pendingSpawns[i]); + i = i + 1; + } + return (stack = st, hits = hits, replayed = replayed, blocked = blocked); + } + + fun doSeal() { + send store, eSealReq, (client = this, gen = gen, blocked = blocked, config = aconfig); + receive { + case eStoreAck: {} + case eStoreDead: { storeDead = true; } + } + if (storeDead) { + goto DeadState; + } + send env, eAttemptEnded, (stopped = false, sealed = true, failed = false); + goto Done; + } + + fun popBatchAndDispatch() { + var cap: int; + var batch: seq[tAction]; + var act: tAction; + var op0: tOp; + cap = 1 + choose(2); // nondet in {1, 2} per dispatch + act = stack[sizeof(stack) - 1]; + stack -= (sizeof(stack) - 1); + op0 = act.op; + batch += (sizeof(batch), act); + while (sizeof(stack) > 0 && sizeof(batch) < cap && stack[sizeof(stack) - 1].op == op0) { + act = stack[sizeof(stack) - 1]; + stack -= (sizeof(stack) - 1); + batch += (sizeof(batch), act); + } + while (sizeof(batch) > 0) { + act = batch[0]; + batch -= (0); + dispatchAction(act); + } + } + + fun dispatchAction(act: tAction) { + var w: machine; + var found: bool; + var i: int; + found = false; + i = 0; + while (i < sizeof(workers) && !found) { + if (!(workers[i] in busyWith)) { + w = workers[i]; + found = true; + } + i = i + 1; + } + // At loop tops cap <= worker count; spawns beyond free workers + // wait in pendingSpawns until a worker frees up. + if (!found) { + pendingSpawns += (sizeof(pendingSpawns), act); + return; + } + outstanding[act.aid] = act; + owner[act.aid] = w; + busyWith[w] = act.aid; + send w, eDispatch, (action = act, hits = hits, replayed = replayed); + } + + fun dispatchPending() { + var act: tAction; + if (stopping || failing) { return; } + while (sizeof(pendingSpawns) > 0 && freeWorkerExists()) { + act = pendingSpawns[0]; + pendingSpawns -= (0); + dispatchAction(act); + } + } + + fun freeWorkerExists(): bool { + var i: int; + i = 0; + while (i < sizeof(workers)) { + if (!(workers[i] in busyWith)) { return true; } + i = i + 1; + } + return false; + } + + fun checkBatchEnd() { + if (sizeof(outstanding) > 0) { return; } + if (stopping) { + finalizeStopIfQuiesced(); + return; + } + if (failing) { + finalizeFailIfQuiesced(); + return; + } + if (sizeof(pendingSpawns) > 0) { + dispatchPending(); + return; + } + send this, eLoopTop; + } + + fun finalizeStopIfQuiesced() { + if (sizeof(outstanding) > 0) { return; } + // Force stop-checkpoint of live state: remaining stack, aborted + // actions at their committed cursors, undrained spawns, and the + // hits/replayed recorded during the aborted batch. + doCheckpoint(); + if (storeDead) { + goto DeadState; + } + send env, eAttemptEnded, (stopped = true, sealed = false, failed = false); + goto Done; + } + + // Attempt-level loud failure (P4): quiesce in-flight pages, force + // the failure checkpoint (the offending cursor is IN it — the + // deterministic-recurrence pin), announce the failure record for + // P4Stuck, and report the failed attempt to the env. + fun finalizeFailIfQuiesced() { + if (sizeof(outstanding) > 0) { return; } + doCheckpoint(); + if (storeDead) { + goto DeadState; + } + announce eAnnAttemptFailed, (syncN = syncN, gen = gen, stack = restoredStack, hits = restoredHits, replayed = restoredReplayed, scope = failScope, reason = failReason, cursor = failCursor); + send env, eAttemptEnded, (stopped = false, sealed = false, failed = true); + goto Done; + } +} diff --git a/formal/walker/PSrc/Store.p b/formal/walker/PSrc/Store.p new file mode 100644 index 000000000..032e38ecf --- /dev/null +++ b/formal/walker/PSrc/Store.p @@ -0,0 +1,489 @@ +/* MStore: the durable store, one per scenario, holding the artifact + chain (MODEL_SPEC 3). Abstraction per MODEL_SPEC 1: partitions with + atomic page commits and a manifest of scope -> validator entries; + batched clear/copy abstracted to TWO atomic steps (separate ops). + + Crash protocol (MODEL_SPEC 5, pinned): eCrash's position in this + machine's queue partitions the dead attempt's outstanding ops — ops + processed before it committed; ops behind it are dropped when they + arrive (gen check), never processed, never acked. Per-sender FIFO is + P's own delivery guarantee (the WAL property). */ + +machine MStore { + var prevPart: tPartition; // previous artifact (replay source) + var prevMan: map[int, int]; // previous manifest: scope -> validator + var curPart: tPartition; // this sync's artifact + var curMan: map[int, int]; + var ckpt: tCheckpoint; + var hasCkpt: bool; + var deadGens: map[int, bool]; + var syncN: int; + var sealed: bool; + var armed: bool; + var armedGen: int; + var armedClient: machine; + var curMarkers: map[int, bool]; // scenario-6 markers, per current sync + // Session store (cases 2, 7): sync-scoped KV, durable at op commit. + // Survives attempts and crashes within the sync (committed writes + // are durable; the crash protocol only drops UNPROCESSED ops); + // reset at sync rotation like the checkpoint token. + var sessionKV: map[int, int]; + // Session durability variant (P6-C, cfg.sessVariant): sessCkpt is + // the session state latched with the last committed checkpoint — + // consumed only by variant 2 (checkpoint-consistent rollback). + var sessVariant: int; + var sessCkpt: map[int, int]; + // Produce-side session taint (case-7 fix runs): kinds (scopes here) + // marked non-replayable in the artifact being produced. Rotates + // with the artifact; the NEXT sync's consult on a prev-tainted + // kind returns MISS (degradation, not a loud verdict). + var curTaint: map[int, bool]; + var prevTaint: map[int, bool]; + // Case-5 produce state: compat records (per artifact; -1 = none) + // and the sealed artifact's produce-blocked flag. + var curCompat: int; + var prevCompat: int; + var sealedBlocked: bool; + var prevBlocked: bool; + // Scenario-8 external-principal keyspace (BatonID-annotated rows, + // separate from scope partitions). Durable at op commit like every + // row write: a dead attempt's committed copies SURVIVE the crash — + // which is exactly why eExtReconReq exists. + var extRows: map[int, bool]; + // Scenario-8 over-deletion mutant (cfg.extOverDelete; the + // P8-EXT-MISSING kill). See extSweepMutant below. + var extOverDelete: bool; + + start state Serving { + on eStoreReset do (p: (client: machine, syncN: int, sessVariant: int, extOverDelete: bool)) { + // Begin-of-sync rotation: the sealed artifact becomes the + // replay source; the new artifact starts empty; the + // checkpoint token belongs to a sync and does not survive it. + if (sealed) { + prevPart = curPart; + prevMan = curMan; + prevTaint = curTaint; + prevCompat = curCompat; + prevBlocked = sealedBlocked; + } + curPart = default(tPartition); + curMan = default(map[int, int]); + curMarkers = default(map[int, bool]); + curTaint = default(map[int, bool]); + curCompat = -1; + sealedBlocked = false; + sessionKV = default(map[int, int]); + sessVariant = p.sessVariant; + sessCkpt = default(map[int, int]); + extRows = default(map[int, bool]); + extOverDelete = p.extOverDelete; + hasCkpt = false; + ckpt = default(tCheckpoint); + sealed = false; + syncN = p.syncN; + send p.client, eStoreAck; + } + + on eSessionSet do (p: (client: machine, gen: int, scope: int, key: int, val: int, taint: bool)) { + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + sessionKV[p.key] = p.val; + if (p.taint) { curTaint[p.scope] = true; } + announce eAnnSessionSet, (syncN = syncN, key = p.key, val = p.val); + send p.client, eStoreAck; + } + + on eSessionGetReq do (p: (client: machine, gen: int, scope: int, key: int, taint: bool)) { + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + if (p.taint) { curTaint[p.scope] = true; } + if (p.key in sessionKV) { + announce eAnnSessionGet, (syncN = syncN, key = p.key, found = true, val = sessionKV[p.key]); + send p.client, eSessionGetResp, (found = true, val = sessionKV[p.key]); + } else { + announce eAnnSessionGet, (syncN = syncN, key = p.key, found = false, val = 0); + send p.client, eSessionGetResp, (found = false, val = 0); + } + } + + on eExtReconReq do (p: (client: machine, gen: int, live: seq[int], supported: bool)) { + var ids: seq[int]; + var liveSet: map[int, bool]; + var deleted: seq[int]; + var i: int; + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + i = 0; + while (i < sizeof(p.live)) { + liveSet[p.live[i]] = true; + i = i + 1; + } + if (p.supported) { + // The capable path: delete every ext row the current + // answer no longer contains (one atomic pass — + // deleteStaleExternalPrincipals runs before the + // current answer's writes). + ids = keys(extRows); + i = 0; + while (i < sizeof(ids)) { + if (!(ids[i] in liveSet)) { + deleted += (sizeof(deleted), ids[i]); + extRows -= ids[i]; + } + i = i + 1; + } + } + // supported FALSE: warn-and-continue — the round still + // announces (the list happened), nothing is deleted. + announce eAnnExtRound, (syncN = syncN, live = p.live, supported = p.supported, deleted = deleted); + send p.client, eStoreAck; + } + + on eExtCopy do (p: (client: machine, gen: int, ids: seq[int])) { + var i: int; + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + i = 0; + while (i < sizeof(p.ids)) { + extRows[p.ids[i]] = true; + i = i + 1; + } + send p.client, eStoreAck; + } + + on eCompatPut do (p: (client: machine, gen: int, k: int)) { + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + curCompat = p.k; + send p.client, eStoreAck; + } + + on eProduceReadReq do (p: (client: machine, gen: int)) { + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + send p.client, eProduceReadResp, (prevCompat = prevCompat, prevBlocked = prevBlocked, curCompat = curCompat, hasCur = curCompat > 0, hasPrev = sizeof(prevMan) > 0); + } + + on eSwapBase do (p: (client: machine, scope: int, vB: int, rowsB: seq[tRow])) { + // Case-3 premise: replace the previous sealed artifact for + // this scope with sibling B. Env-level initial-condition + // surgery (B is a legal artifact from another history), not + // an attempt op — no gen gate, no crash point. + var i: int; + var m: map[int, tRow]; + i = 0; + while (i < sizeof(p.rowsB)) { + m[p.rowsB[i].id] = p.rowsB[i]; + i = i + 1; + } + prevPart[p.scope] = m; + prevMan[p.scope] = p.vB; + send p.client, eStoreAck; + } + + on eLookupReq do (p: (client: machine, gen: int, scope: int)) { + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + // A kind tainted in the previous artifact's produce state + // consults MISS (case-7 fix runs): replay is forfeited. + if (p.scope in prevMan && !(p.scope in prevTaint)) { + send p.client, eLookupResp, (hit = true, v = prevMan[p.scope]); + } else { + send p.client, eLookupResp, (hit = false, v = -1); + } + } + + on eBaseReadReq do (p: (client: machine, gen: int, scope: int)) { + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + if (p.scope in prevMan) { + send p.client, eBaseReadResp, (v = prevMan[p.scope], present = true); + } else { + send p.client, eBaseReadResp, (v = -1, present = false); + } + } + + on eClearScope do (p: (client: machine, gen: int, scope: int, ghost: tRoundGhost)) { + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + curPart[p.scope] = default(map[int, tRow]); + announce eAnnClear, (syncN = syncN, scope = p.scope, ghost = p.ghost); + send p.client, eStoreAck; + } + + on eCopyScope do (p: (client: machine, gen: int, scope: int, ghost: tRoundGhost)) { + var ids: seq[int]; + var i: int; + var r: tRow; + var dst: map[int, tRow]; + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + if (p.scope in curPart) { dst = curPart[p.scope]; } + if (p.scope in prevPart) { + ids = keys(prevPart[p.scope]); + i = 0; + while (i < sizeof(ids)) { + r = prevPart[p.scope][ids[i]]; + // P2 ghost: replay travel increments the hop counter. + r.hops = r.hops + 1; + dst[r.id] = r; + i = i + 1; + } + } + curPart[p.scope] = dst; + announce eAnnReplay, (syncN = syncN, scope = p.scope, vBase = prevManValue(prevMan, p.scope), cBase = baseConfigOf(prevPart, p.scope), ghost = p.ghost); + send p.client, eStoreAck; + } + + on eUpsertPage do (p: (client: machine, gen: int, scope: int, rows: seq[tRow], ghost: tRoundGhost)) { + var i: int; + var dst: map[int, tRow]; + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + if (p.scope in curPart) { dst = curPart[p.scope]; } + i = 0; + while (i < sizeof(p.rows)) { + dst[p.rows[i].id] = p.rows[i]; + i = i + 1; + } + curPart[p.scope] = dst; + announce eAnnUpsert, (syncN = syncN, scope = p.scope, rows = p.rows, ghost = p.ghost); + send p.client, eStoreAck; + } + + on ePublishEntry do (p: (client: machine, gen: int, scope: int, v: int, ghost: tRoundGhost)) { + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + curMan[p.scope] = p.v; + announce eAnnPublish, (syncN = syncN, scope = p.scope, v = p.v, ghost = p.ghost); + send p.client, eStoreAck; + } + + on eMarkerPut do (p: (client: machine, gen: int, scope: int)) { + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + curMarkers[p.scope] = true; + send p.client, eStoreAck; + } + + on eMarkerReadReq do (p: (client: machine, gen: int, scope: int)) { + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + send p.client, eMarkerReadResp, (marked = p.scope in curMarkers,); + } + + on eReplayUnit do (p: (client: machine, gen: int, scope: int, v: int, ghost: tRoundGhost)) { + var ids: seq[int]; + var i: int; + var r: tRow; + var dst: map[int, tRow]; + var g: tRoundGhost; + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + // One atomic commit: clear, copy, marker, publish. The + // constituent announces fire together at commit; the last + // (publish) carries lastOp — round completion IS unit commit. + curPart[p.scope] = default(map[int, tRow]); + g = p.ghost; + g.lastOp = false; + announce eAnnClear, (syncN = syncN, scope = p.scope, ghost = g); + dst = curPart[p.scope]; + if (p.scope in prevPart) { + ids = keys(prevPart[p.scope]); + i = 0; + while (i < sizeof(ids)) { + r = prevPart[p.scope][ids[i]]; + r.hops = r.hops + 1; + dst[r.id] = r; + i = i + 1; + } + } + curPart[p.scope] = dst; + announce eAnnReplay, (syncN = syncN, scope = p.scope, vBase = prevManValue(prevMan, p.scope), cBase = baseConfigOf(prevPart, p.scope), ghost = g); + curMarkers[p.scope] = true; + curMan[p.scope] = p.v; + announce eAnnPublish, (syncN = syncN, scope = p.scope, v = p.v, ghost = p.ghost); + send p.client, eStoreAck; + } + + on eTombstonePage do (p: (client: machine, gen: int, scope: int, removes: seq[int], ghost: tRoundGhost)) { + var i: int; + var dst: map[int, tRow]; + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + if (p.scope in curPart) { dst = curPart[p.scope]; } + i = 0; + while (i < sizeof(p.removes)) { + if (p.removes[i] in dst) { dst -= p.removes[i]; } + i = i + 1; + } + curPart[p.scope] = dst; + announce eAnnTombstones, (syncN = syncN, scope = p.scope, removes = p.removes, ghost = p.ghost); + send p.client, eStoreAck; + } + + on eOverlayUnit do (p: (client: machine, gen: int, scope: int, v: int, upserts: seq[tRow], removes: seq[int], ghost: tRoundGhost)) { + var ids: seq[int]; + var i: int; + var r: tRow; + var dst: map[int, tRow]; + var g: tRoundGhost; + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + // ONE atomic commit: clear, copy(base), overlay upserts and + // tombstones in prescribed page order, marker, publish(V_to). + // Constituent announces fire together; publish carries + // lastOp — round completion IS unit commit, and the round + // folds as the self-grounding overlay (own copy committed). + g = p.ghost; + g.lastOp = false; + curPart[p.scope] = default(map[int, tRow]); + announce eAnnClear, (syncN = syncN, scope = p.scope, ghost = g); + dst = curPart[p.scope]; + if (p.scope in prevPart) { + ids = keys(prevPart[p.scope]); + i = 0; + while (i < sizeof(ids)) { + r = prevPart[p.scope][ids[i]]; + r.hops = r.hops + 1; + dst[r.id] = r; + i = i + 1; + } + } + i = 0; + while (i < sizeof(p.upserts)) { + dst[p.upserts[i].id] = p.upserts[i]; + i = i + 1; + } + announce eAnnReplay, (syncN = syncN, scope = p.scope, vBase = prevManValue(prevMan, p.scope), cBase = baseConfigOf(prevPart, p.scope), ghost = g); + announce eAnnUpsert, (syncN = syncN, scope = p.scope, rows = p.upserts, ghost = g); + i = 0; + while (i < sizeof(p.removes)) { + if (p.removes[i] in dst) { dst -= p.removes[i]; } + i = i + 1; + } + announce eAnnTombstones, (syncN = syncN, scope = p.scope, removes = p.removes, ghost = g); + curPart[p.scope] = dst; + curMarkers[p.scope] = true; + curMan[p.scope] = p.v; + announce eAnnPublish, (syncN = syncN, scope = p.scope, v = p.v, ghost = p.ghost); + send p.client, eStoreAck; + } + + on eCheckpointReq do (p: (client: machine, gen: int, ckpt: tCheckpoint)) { + maybeCrash(); + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + ckpt = p.ckpt; + hasCkpt = true; + // Checkpoint-consistent sessions (variant 2): the session + // overlay flushes atomically with the checkpoint token. + if (sessVariant == 2) { sessCkpt = sessionKV; } + announce eAnnCheckpoint, (syncN = syncN,); + send p.client, eStoreAck; + } + + on eSealReq do (p: (client: machine, gen: int, blocked: bool, config: int)) { + // Guaranteed resolution point for an armed crash: it fires + // either just before the seal (dropping it) or right after. + if (armed && armedGen == p.gen) { + if (choose(2) == 0) { + fireCrash(); + } else { + sealed = true; + sealedBlocked = p.blocked; + extSweepMutant(); + announce eAnnExtSeal, (syncN = syncN, ids = keys(extRows)); + announce eAnnSeal, (syncN = syncN, partition = curPart, manifest = curMan, blocked = p.blocked, config = p.config); + send p.client, eStoreAck; + fireCrash(); + return; + } + } + if (p.gen in deadGens) { send p.client, eStoreDead; return; } + sealed = true; + sealedBlocked = p.blocked; + extSweepMutant(); + announce eAnnExtSeal, (syncN = syncN, ids = keys(extRows)); + announce eAnnSeal, (syncN = syncN, partition = curPart, manifest = curMan, blocked = p.blocked, config = p.config); + send p.client, eStoreAck; + } + + on eReadCheckpointReq do (p: (client: machine)) { + send p.client, eReadCheckpointResp, (ckpt = ckpt, hasCkpt = hasCkpt); + } + + on eReadSealedReq do (p: (client: machine)) { + send p.client, eReadSealedResp, (sealed = sealed,); + } + + on eCrashArm do (p: (client: machine, gen: int)) { + armed = true; + armedGen = p.gen; + armedClient = p.client; + } + } + + fun maybeCrash() { + if (armed && choose(2) == 0) { + fireCrash(); + } + } + + fun fireCrash() { + deadGens[armedGen] = true; + armed = false; + // Session state at the crash boundary (P6-C). Variant 0 + // (shipped) keeps sessionKV untouched: writes beyond the last + // checkpoint survive the cursor rollback (the zombie + // direction). Variant 1 models the rejected wholesale + // resume-clear: checkpoint-committed data is destroyed too + // (the amnesia direction). Variant 2 rolls sessions back to + // the state latched with the last checkpoint — both + // directions closed. Equivalent to acting at resume start: + // dead-gen ops arriving after the crash are dropped by the + // gen gate and can never observe the adjusted map. + if (sessVariant == 1) { + sessionKV = default(map[int, int]); + } + if (sessVariant == 2) { + sessionKV = sessCkpt; + } + announce eAnnCrash, (syncN = syncN,); + send armedClient, eCrashAck; + } + + // The over-deletion inject (tc8overDelete_P8's kill, the + // P8-EXT-MISSING witness): a LATE deleteStaleExternalPrincipals + // sweep whose predicate mistakes a live principal for stale, + // committing at seal prep where nothing re-writes the row. + // Injected at the seal and not in eExtReconReq because the + // engine-ordered early pass is structurally self-healing for + // over-deletion: it runs before the page-1 copy, which rewrites + // every listed id — an early over-delete cannot survive to seal + // in any schedule (recorded in the scenario-8 calibration). + fun extSweepMutant() { + var ks: seq[int]; + if (!extOverDelete) { return; } + if (sizeof(extRows) == 0) { return; } + ks = keys(extRows); + extRows -= (ks[0]); + } +} + +fun prevManValue(m: map[int, int], scope: int): int { + if (scope in m) { return m[scope]; } + return -1; +} + +// Config ghost of the copied base content: rows in a scope share one +// attempt config, so the first row's tag stands for the base. -1 when +// the base is empty or configs are unmodeled (all rows tag 0 -> the +// caller treats 0 as unmodeled too). +fun baseConfigOf(part: tPartition, scope: int): int { + var ids: seq[int]; + if (scope in part) { + ids = keys(part[scope]); + if (sizeof(ids) > 0) { return part[scope][ids[0]].config; } + } + return -1; +} diff --git a/formal/walker/PSrc/Types.p b/formal/walker/PSrc/Types.p new file mode 100644 index 000000000..ebdb5ca75 --- /dev/null +++ b/formal/walker/PSrc/Types.p @@ -0,0 +1,227 @@ +/* Types for the walker + source-cache replay calibration model. + Source of truth: formal/MODEL_SPEC.md (v10). Identifiers follow + formal/GLOSSARY.md. Small scope: 1-2 scopes, epochs 1..3, row ids + 0..1, <=3 syncs, <=3 attempts/sync, 2 workers, <=2 pages/round. */ + +// A stored row. epoch is the ghost content tag (truthful upstream), +// hops is the P2 replay-travel counter (0 = fresh this sync), +// config is the P1 config ghost, stamp the session ghost (-1 none, 0 miss). +type tRow = (id: int, epoch: int, hops: int, config: int, stamp: int); + +// scope -> row id -> row +type tPartition = map[int, map[int, tRow]]; + +// Mitigation toggles (MODEL_SPEC 6). OFF removes a check, never adds. +// The sessionTaint pair is produce-side (scenario 7 fix runs): session +// traffic during a replay-capable kind's phase marks that kind +// non-replayable in the artifact being produced; the NEXT sync's +// consult on a tainted kind misses (degradation, not a loud verdict). +type tToggles = ( + warmGate: bool, + hitValidatorBinding: bool, + scopeLocks: bool, + oncePerScope: bool, + annotationBinding: bool, // de-scoped from build; kept for spec parity + abandonLadder: bool, + sessionTaintWrites: bool, + sessionTaintAll: bool +); + +// Verdict classes (P1 ghost "verdict class"; GLOSSARY "verdict"). +enum tVerdict { V_REPLAY, V_FRESH, V_OVERLAY } + +// Action ops. PLANNING consults and may spawn; CARRIER carries a replay +// annotation; FRESH_PAGES is the continuation of a fetch-fresh chain. +// One op per kind (round-6 F1 pin: kind = (op, scope)). +enum tOp { OP_PLANNING, OP_CARRIER } + +// An action on the scheduler stack. cursor is the page index the chain +// resumes from (0 = root token; restart-from-root restores 0 unless a +// stop-forced checkpoint captured a mid-chain cursor). +type tAction = ( + aid: int, // action id, unique per sync + op: tOp, + scope: int, + cursor: int, + hasAnnotation: bool, // carrier: replay annotation present in token + annotationV: int, // the annotation's validator (epoch-valued) + publishes: bool // carrier publishes its validator (1b-i vs 1b-ii) +); + +// Checkpoint token (MODEL_SPEC 5): action stack (with cursors as +// captured), hit map, replayed set, and the ingest-quality fragment +// the build models: the produce-blocked flag (checkpoint-cadence +// durability — the scenario-5b crash-window finding lives in exactly +// this field). compositionEnum remains de-scoped (v11). +type tCheckpoint = ( + stack: seq[tAction], + hits: map[int, int], // scope -> hit validator + replayed: map[int, bool], // replayed set (map as set) + blocked: bool // produce-blocked reason flag +); + +// Design variant (MODEL_SPEC 9 scenario 6). Not a mitigation toggle: +// it alters commit structure. 0 = shipped; 1 = V-NAIVE (marker as a +// separate op after eCopyScope, outside any unit); 2 = V-ATOMIC +// (eReplayUnit: clear+copy+marker+publish as ONE atomic store op; +// replay executes inline on the consulting page — no carrier spawns; +// the marker check precedes the consult and suppresses re-consult and +// re-derivation for marked scopes — clause (iii) applies to both). +// v10 addendum: VAR_OVERLAY_UNIT = V-OVERLAY-UNIT (unit boundary at the +// consult VERDICT: base copy + all overlay pages + marker + publish in +// ONE atomic op; per-page ops buffered volatile; publish deferred into +// the unit; marker-absent resume restarts from consult, mid-chain +// cursors ignored — pin o-iv). VAR_OVERLAY_NAIVE = the misdraw: the +// unit {clear, copy, marker, publish(V_to)} commits at the CONSULT +// boundary; overlay pages then commit per-page (6-overlay-naive kill). +// VAR_OVERLAY_LAST = the third placement (round-7 F2): NO unit at all — +// clear+copy commit per-page at the replay boundary, overlay pages +// per-page via the shipped path, then marker and publish(V_to) trail +// LAST as two separate ops (6-overlay-last: w2 marker-committed/ +// publish-lost suppression is the P1 witness; w1's cross-attempt +// re-copy exercises the complete-rounds replacement-counting pin). +enum tVariant { VAR_SHIPPED, VAR_NAIVE, VAR_ATOMIC, VAR_OVERLAY_NAIVE, VAR_OVERLAY_UNIT, VAR_OVERLAY_LAST } + +// Scenario configuration, built per test cell in PTst. Constructed via +// defaultCfg() + field overrides (P named tuples are order-sensitive). +type tScenarioCfg = ( + scenario: int, + variant: tVariant, + // Premise SHAPE of the planning chain / root stack: + // 1 = 2-page planning, spawn at page 1, re-consult at page 2 (1a/1b, 3A) + // 2 = session laundering: root stack [H writer, G reader] (case 2) + // 3 = 1-page planning, consult+replay inline (1c, 6-family) + // 4 = 2-page planning spawning dual carriers (case 4) + // 7 = sessions x replay: kinds W (scope 0, producer) then R + // (scope 1, reader) in sequential phases (case 7) + // 31 = 1-page planning, consult+spawn then done (3B) + // 51 = 5a shape: cell-31 planning + warm/produce machinery, + // compat drift between attempts (trigger 1) + // 52 = 5b shape: cell-31 planning + warm/produce machinery, + // G6 capability withdrawal in attempt 2 (trigger 2) + // 53 = C1 probe: one action, replay annotation MID-CHAIN (page 0 + // consults, page 1 replays); stop between the pages resumes + // the replay on the restored hit map without a fresh consult + cell: int, + // Interruption script: 0 = none, 1 = graceful stop, 2 = hard + // crash; 3 = stop in attempt 1 THEN hard crash in attempt 2 (the + // 5b crash-window premise). Fired in sync interruptSync only. + interrupt: int, + interruptSync: int, + toggles: tToggles, + carrierPublishes: bool, // 1b-i vs 1b-ii sub-cells + nSyncs: int, // total syncs incl. verification + mutateBetweenAttempts: bool, + mutateBetweenSyncs: bool, + preMutate: bool, // advance upstream to e2 before sync 1 (case 3) + swapBase: bool, // between-attempt base swap to artifact B (case 3) + // Case-7a reader policy: R's connector fetches fresh regardless of + // a valid hit (verdicts are connector choice; replay is opt-in). + readerAlwaysFresh: bool, + // Case-5 warm/produce axes. baseConfig: the compat config K of + // attempt 1 (0 = configs unmodeled in this cell). driftCompat: + // attempts >= 2 compute K2 != K1 (trigger 1 / B4). withdrawG6: + // attempt 2 runs without source-cache handling (trigger 2). + baseConfig: int, + driftCompat: bool, + withdrawG6: bool, + // P2 corollary-run scoping (MODEL_SPEC 7: "corollary runs assert + // staleness <= 1"): the verification sync runs only in histories + // where the scripted interruption actually landed; otherwise the + // honest double-replay chain reaches hops 2 legally and the <=1 + // bound is outside its run class. + verificationOnlyIfInterrupted: bool, + // P4 tranche axes (MODEL_SPEC 4 failure semantics / 7 P4): + // loudColdFailsAttempt = a cold verdict fails the ATTEMPT loudly + // (resume-on-failure; the offending cursor is checkpointed and the + // failure recurs deterministically). Default false = the chain-cold + // simplification of decision 10, load-bearing only in P4 cells. + loudColdFailsAttempt: bool, + // warmPageFails = inject ONE destination-write failure on the + // replay page after the scope lock is acquired (CO-6b-007 premise); + // the worker retries in-attempt, re-entering the page sequence. + warmPageFails: bool, + // lockReleaseOnError = the release-on-error edge (shipped). The + // mutation check REMOVES it: the in-attempt retry then deadlocks + // on its own leaked lock — the CO-6b-007 hang. + lockReleaseOnError: bool, + // o4Mutant = the o-iv-REMOVAL mutant (MS-CO-001 / parallel-review + // F6): a V-OVERLAY-UNIT resume HONORS a restored mid-chain cursor + // instead of restarting at the consult — the retry collects only + // the final page into an empty buffer and commits a unit missing + // the first page's overlay ops (content-RED in sub-case (b)). + o4Mutant: bool, + // Session durability variant at the crash boundary (P6-C, the + // CO-6b-009 root cause): 0 = shipped (durable at op commit — + // beyond-checkpoint writes survive the cursor rollback); + // 1 = the rejected wholesale resume-clear (checkpoint-committed + // data destroyed); 2 = checkpoint-consistent sessions (session + // state latched with each checkpoint, restored at crash). + sessVariant: int, + // Scenario-8 axes (external principals; cell 8). extRecon models + // the storage engine's delete capability: TRUE = the shipped + // capable path (deleteStaleExternalPrincipals reconciles a dead + // attempt's copied principals before the current answer is + // written); FALSE = the warn-and-continue degrade (non-deleting + // engine) — stale principals survive to seal. extStaleList is the + // resume-recency mutant: attempts >= 2 list the SYNC-START answer + // instead of the current one (the behavior the + // ResumeUsesCurrentExternalAnswer chaos pin forbids). + extRecon: bool, + extStaleList: bool, + // extOverDelete is the over-deletion mutant (P8-EXT-MISSING kill): + // a LATE stale-principal sweep whose predicate mistakes a live + // principal for stale, running at seal prep where nothing re-writes + // the row. It is injected at the seal (Store.p) and NOT in + // eExtReconReq because the engine-ordered early pass is + // structurally self-healing for over-deletion — the page-1 copy + // rewrites every listed id (see the scenario-8 calibration notes). + extOverDelete: bool +); + +// Base config: shipped design, no interruption, no mutation, 2 syncs. +fun defaultCfg(): tScenarioCfg { + return ( + scenario = 0, + variant = VAR_SHIPPED, + cell = 1, + interrupt = 0, + interruptSync = 2, + toggles = (warmGate = true, hitValidatorBinding = true, scopeLocks = true, oncePerScope = true, annotationBinding = false, abandonLadder = false, sessionTaintWrites = false, sessionTaintAll = false), + carrierPublishes = true, + nSyncs = 2, + mutateBetweenAttempts = false, + mutateBetweenSyncs = false, + preMutate = false, + swapBase = false, + readerAlwaysFresh = false, + baseConfig = 0, + driftCompat = false, + withdrawG6 = false, + verificationOnlyIfInterrupted = false, + loudColdFailsAttempt = false, + warmPageFails = false, + lockReleaseOnError = true, + o4Mutant = false, + sessVariant = 0, + extRecon = true, + extStaleList = false, + extOverDelete = false + ); +} + +// Consult outcome handed from store+upstream to the worker. +type tConsult = (hit: bool, v: int, validated: bool); + +// Round ghost label carried on store ops for the P1 fold: +// (verdict class, consult epoch, attempt config) + round identity and +// a lastOp marker (round completion = commit of last prescribed op, +// round-5 F1 pin). +type tRoundGhost = ( + roundId: int, + verdict: tVerdict, + consultEpoch: int, + config: int, + lastOp: bool, + attempt: int // gen; a round whose pages commit under >1 attempt is TORN +); diff --git a/formal/walker/PSrc/Upstream.p b/formal/walker/PSrc/Upstream.p new file mode 100644 index 000000000..790430e6e --- /dev/null +++ b/formal/walker/PSrc/Upstream.p @@ -0,0 +1,91 @@ +/* MUpstream: the external system of record (MODEL_SPEC 3). + Truthful: a validator (epoch-valued) validates iff it equals the + current epoch. Row table: epoch 1 has rows {0,1}; epoch >= 2 has {0} + (row 1 deleted upstream at the e1->e2 mutation), so unions and + resurrections are content-visible with 2 row ids (MODEL_SPEC 8). */ + +// Row ids present at an epoch. Shared by MUpstream (fetch) and the +// monitors (the rows(s,e) fold table) so the oracle and the system +// draw from one table. +fun upstreamRowIds(epoch: int): seq[int] { + var ids: seq[int]; + ids += (0, 0); + if (epoch <= 1) { + ids += (1, 1); + } + return ids; +} + +// rows(s, e) as id -> content-epoch map (monitor-side fold table). +fun rowsAt(epoch: int): map[int, int] { + var ids: seq[int]; + var m: map[int, int]; + var i: int; + ids = upstreamRowIds(epoch); + i = 0; + while (i < sizeof(ids)) { + m[ids[i]] = epoch; + i = i + 1; + } + return m; +} + +machine MUpstream { + var epochs: map[int, int]; // scope -> current epoch + + start state Serving { + entry { + epochs[0] = 1; + epochs[1] = 1; + } + + on eValidateReq do (p: (client: machine, scope: int, v: int)) { + send p.client, eValidateResp, (ok = p.v == epochs[p.scope], epoch = epochs[p.scope]); + } + + on eFetchReq do (p: (client: machine, scope: int, page: int)) { + var ids: seq[int]; + var rows: seq[tRow]; + var e: int; + e = epochs[p.scope]; + ids = upstreamRowIds(e); + if (p.page < sizeof(ids)) { + rows += (0, (id = ids[p.page], epoch = e, hops = 0, config = 0, stamp = -1)); + } + // Fixed 2-page rounds (pages per scope per round <= 2). + send p.client, eFetchResp, (rows = rows, epoch = e, morePages = p.page == 0); + } + + on eDiffReq do (p: (client: machine, scope: int, fromEpoch: int, page: int)) { + var e: int; + var ups: seq[tRow]; + var rms: seq[int]; + var fromIds: seq[int]; + var i: int; + e = epochs[p.scope]; + // Diff from base epoch to current: page 0 carries upserts + // (rows whose content changed), page 1 carries removes + // (ids present at base, absent now). Truthful and total. + if (p.page == 0) { + if (p.fromEpoch != e) { + ups += (0, (id = 0, epoch = e, hops = 0, config = 0, stamp = -1)); + } + } else { + fromIds = upstreamRowIds(p.fromEpoch); + i = 0; + while (i < sizeof(fromIds)) { + if (!(fromIds[i] in rowsAt(e))) { + rms += (sizeof(rms), fromIds[i]); + } + i = i + 1; + } + } + send p.client, eDiffResp, (upserts = ups, removes = rms, epoch = e, morePages = p.page == 0); + } + + on eMutate do (p: (client: machine, scope: int)) { + epochs[p.scope] = epochs[p.scope] + 1; + send p.client, eMutateAck; + } + } +} diff --git a/formal/walker/PSrc/Worker.p b/formal/walker/PSrc/Worker.p new file mode 100644 index 000000000..f2b9f0811 --- /dev/null +++ b/formal/walker/PSrc/Worker.p @@ -0,0 +1,782 @@ +/* MWorker: executes whole action chains page by page (syncOneAction's + whole-chain ownership, MODEL_SPEC 3). Each page's store ops commit + (acked) before the transition is reported; the worker then awaits + continue/abort, so a graceful stop aborts at page boundaries with + the current page's work committed (MODEL_SPEC 5). + + Scenario-1 connector policy (scripted, pure — MODEL_SPEC 9 case 1): + planning chain cursors: 0 = consult page (may spawn carrier [cell 1] + or replay inline [cell 1c]); 1 = re-consult page; 2,3 = fresh round + pages (fetch + upsert; publish on the last). Carrier actions + (hasAnnotation) run the MODEL_SPEC 4 replay page sequence. + + scopeLocks note: upsert pages are single atomic store ops (the + MODEL_SPEC 1 store abstraction), and no scenario-1 cell's verdict + depends on lock-excluded interleavings, so the lock itself is not + yet contended in this build; the toggle becomes load-bearing in the + case-4/7 cells (check-then-mark TOCTOU, leaked-lock retry). */ + +machine MWorker { + var scheduler: machine; + var store: machine; + var upstream: machine; + var gen: int; + var syncN: int; + var cfg: tScenarioCfg; + var warm: bool; + var g6: bool; // this attempt's source-cache capability bit (B1) + var aconfig: int; // this attempt's compat config (0 = unmodeled) + var action: tAction; + var hits: map[int, int]; + var replayedSnap: map[int, bool]; + var dead: bool; // this gen crashed; park (ops dropped, MODEL_SPEC 5) + var abortedMid: bool; // aborted at an in-page wait (lock grant) + var failedChain: bool; // attempt-level loud failure reported (P4 cells) + var warmFailUsed: bool; // the one-shot CO-6b-007 write-failure injection + // V-OVERLAY-UNIT volatile collect buffer (MODEL_SPEC 5 row): worker + // memory only, never checkpointed; buffer loss forces re-consult + // via the marker-absent resume rule (pin o-iv). + var ovFrom: int; + var ovTo: int; + var bufUp: seq[tRow]; + var bufRm: seq[int]; + // Case-7 reader: the session-derived stamp applied to this chain's + // fresh rows (-1 = the chain has no session derivation; 0 = miss). + var pageStamp: int; + + state Idle { + on eDispatch do (p: (action: tAction, hits: map[int, int], replayed: map[int, bool])) { + action = p.action; + hits = p.hits; + replayedSnap = p.replayed; + dead = false; + abortedMid = false; + failedChain = false; + warmFailUsed = false; + // Pin o-iv (V-OVERLAY-UNIT): the collect buffer is volatile, + // so cursor continuation without it is undefined; the model + // realizes the spec's transition-deferral pin (round-7 F1, + // CALIBRATION decision 21) as a reset to the consult page. + // Honest price: at-least-once re-fetch, lost work but never + // debris. The o4Mutant (MS-CO-001 F6 kill) REMOVES the + // reset: the resume honors the mid-chain cursor with an + // empty buffer and commits a unit missing the earlier + // pages' ops. + if (cfg.variant == VAR_OVERLAY_UNIT && action.cursor != 0 && !cfg.o4Mutant) { + action.cursor = 0; + } + // Scenario 8: the external op restarts from its root token + // on every (re)dispatch — a resumed external phase re-lists + // and re-reconciles rather than resuming a mid-phase + // cursor (restart-from-root is the healing mechanism + // deleteStaleExternalPrincipals is built for; a mid-phase + // cursor resume would copy a fresh answer over a stale + // reconciliation). + if (cfg.cell == 8 && action.cursor != 0) { + action.cursor = 0; + } + bufUp = default(seq[tRow]); + bufRm = default(seq[int]); + pageStamp = -1; + runChain(); + } + // A stale abort can reach an idle worker when its last + // transition raced the stop; nothing to roll back. + ignore eAbortWorker; + } + + fun runChain() { + var res: (nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int); + var aborted: bool; + aborted = false; + while (!aborted) { + res = execPage(); + if (dead) { return; } + // Attempt-level loud failure: eChainFailed already sent; + // no transition — the scheduler restores the action at the + // offending cursor and quiesces the attempt. + if (failedChain) { return; } + if (abortedMid) { + send scheduler, eWorkerAborted, (aid = action.aid, cursor = action.cursor); + return; + } + send scheduler, eActionTransition, (aid = action.aid, nextCursor = res.nextCursor, done = res.done, spawn = res.spawn, hasSpawn = res.hasSpawn, hitScope = res.hitScope, hitV = res.hitV, hasHit = res.hasHit, markReplayed = res.markReplayed, replayedScope = res.replayedScope); + if (res.done) { return; } + receive { + case eContinuePage: { + action.cursor = res.nextCursor; + } + case eAbortWorker: { + send scheduler, eWorkerAborted, (aid = action.aid, cursor = res.nextCursor); + aborted = true; + } + } + } + } + + // Executes the page at action.cursor; returns the transition. + fun execPage(): (nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int) { + // C1-probe shape (MODEL_SPEC 9.5 C1, cell 53): ONE action whose + // policy places the replay annotation MID-CHAIN — page 0 + // consults (hit recorded at lookup), page 1 is the annotated + // replay page, pages 2+ are the fresh chain (consult miss / + // revalidation failure). A stop between pages 0 and 1 + // checkpoints the mid-chain cursor + the hit map; the resumed + // page 1 performs NO fresh consult and the hit check passes on + // the RESTORED map — the C1Probe monitor's red is exactly that + // witness. + if (cfg.cell == 53) { + if (action.cursor == 0) { + return c1ConsultPage(); + } + if (action.cursor == 1) { + return replayPage(action.annotationV, action.publishes, action.aid * 100, false, -1); + } + return freshPage(); + } + if (action.hasAnnotation) { + return replayPage(action.annotationV, action.publishes, action.aid * 100, false, -1); + } + if (cfg.cell == 2 || cfg.cell == 21) { + return sessionPage(); + } + if (cfg.cell == 8) { + return extPage(); + } + if (cfg.cell == 7 && (action.cursor == 0 || action.cursor == 1)) { + return kindPage7(); + } + if (action.cursor >= 4) { + return overlayPage(); + } + if (action.cursor == 0 || action.cursor == 1) { + return consultPage(); + } + return freshPage(); + } + + // Case-7 kind chains (sessions x replay, MODEL_SPEC 9.7). Kind = + // (op, scope): W = scope 0 (producer), R = scope 1 (reader); the + // env roots them with DIFFERENT ops so batching runs the phases + // sequentially. Page 0 consults the kind's own scope: + // - valid hit (and, for R, an opted-in policy): warm replay INLINE + // (shipped carrier-less path). ELISION IS STRUCTURAL: the fresh + // enumeration — and W's session write inside it — never runs. + // - otherwise fetch-fresh: W writes K = f(this sync's epoch) as the + // enumeration side effect; R reads K and stamps its fresh rows + // with the read value (0 = read-miss); chains continue on the + // shared fresh pages (cursors 2, 3). + fun kindPage7(): (nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int) { + var hit: bool; + var v: int; + var ok: bool; + var e: int; + var found: bool; + var val: int; + send store, eLookupReq, (client = this, gen = gen, scope = action.scope); + receive { + case eLookupResp: (r: (hit: bool, v: int)) { + hit = r.hit; + v = r.v; + } + case eStoreDead: { dead = true; } + } + if (dead) { return mkTransition(0, true, false, -1, false); } + ok = false; + e = -1; + if (hit) { + hits[action.scope] = v; + send upstream, eValidateReq, (client = this, scope = action.scope, v = v); + receive { + case eValidateResp: (r: (ok: bool, epoch: int)) { + ok = r.ok; + e = r.epoch; + } + } + } + announce eAnnConsult, (syncN = syncN, scope = action.scope, hit = hit, v = v, validated = ok, epoch = e, freshFetch = false, diffVerdict = false, attempt = gen); + if (hit && ok && !(action.scope == 1 && cfg.readerAlwaysFresh)) { + return replayPage(v, true, action.aid * 100, true, v); + } + if (action.scope == 0) { + // W fresh: the session write is part of the producer's + // enumeration phase (side effect, op-commit durable). + send upstream, eFetchReq, (client = this, scope = action.scope, page = 0); + receive { + case eFetchResp: (r: (rows: seq[tRow], epoch: int, morePages: bool)) { e = r.epoch; } + } + send store, eSessionSet, (client = this, gen = gen, scope = action.scope, key = 0, val = e, taint = cfg.toggles.sessionTaintWrites || cfg.toggles.sessionTaintAll); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + if (dead) { return mkTransition(0, true, false, -1, false); } + return mkTransition(2, false, hit, v, false); + } + // R fresh: derive the row stamp from the session read (0 = miss). + send store, eSessionGetReq, (client = this, gen = gen, scope = action.scope, key = 0, taint = cfg.toggles.sessionTaintAll); + receive { + case eSessionGetResp: (r: (found: bool, val: int)) { + found = r.found; + val = r.val; + } + case eStoreDead: { dead = true; } + } + if (dead) { return mkTransition(0, true, false, -1, false); } + if (found) { + pageStamp = val; + } else { + pageStamp = 0; + } + return mkTransition(2, false, hit, v, false); + } + + // Case-2 connector policies (session laundering, MODEL_SPEC 9.2). + // H (aid 1): 2-page writer; EACH page derives from upstream and + // writes the session key (op-commit durable) — a mid-chain resume + // re-derives on its remaining pages. G (aid 2): 1-page reader; + // reads the session key and emits a row EMBEDDING the read value. + // A read-miss emits nothing (the reader consumes a value the writer + // produced; absent key = nothing to embed), so read-before-write + // races don't manufacture alarms outside the laundering premise. + fun sessionPage(): (nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int) { + var e: int; + var found: bool; + var val: int; + var rows: seq[tRow]; + var ghost: tRoundGhost; + if (action.aid == 1) { + send upstream, eFetchReq, (client = this, scope = action.scope, page = 0); + receive { + case eFetchResp: (r: (rows: seq[tRow], epoch: int, morePages: bool)) { e = r.epoch; } + } + send store, eSessionSet, (client = this, gen = gen, scope = action.scope, key = 0, val = e, taint = cfg.toggles.sessionTaintWrites || cfg.toggles.sessionTaintAll); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + if (dead) { return mkTransition(0, true, false, -1, false); } + if (action.cursor == 0) { + return mkTransition(1, false, false, -1, false); + } + return mkTransition(0, true, false, -1, false); + } + send store, eSessionGetReq, (client = this, gen = gen, scope = action.scope, key = 0, taint = cfg.toggles.sessionTaintAll); + receive { + case eSessionGetResp: (r: (found: bool, val: int)) { + found = r.found; + val = r.val; + } + case eStoreDead: { dead = true; } + } + if (dead) { return mkTransition(0, true, false, -1, false); } + if (!found) { + return mkTransition(0, true, false, -1, false); + } + ghost = (roundId = action.aid * 100, verdict = V_FRESH, consultEpoch = 0, config = 0, lastOp = true, attempt = gen); + rows += (0, (id = 1, epoch = 0, hops = 0, config = 0, stamp = val)); + send store, eUpsertPage, (client = this, gen = gen, scope = action.scope, rows = rows, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + return mkTransition(0, true, false, -1, false); + } + + // Scenario-8 external phase (MODEL_SPEC abstraction of + // SyncExternalResources; cell 8). Page 0 = LIST + RECONCILE: read + // the external source's current answer (the truthful epoch table — + // the extStaleList mutant lists the sync-start epoch on attempts + // >= 2 instead, the recency bug ResumeUsesCurrentExternalAnswer + // pins) and commit the reconciliation op (delete-stale under + // cfg.extRecon; warn-and-continue degrade otherwise). Page 1 = + // COPY: commit the listed answer's principal writes. Crash windows + // exist at every op boundary; a resumed phase restarts from page 0 + // (the eDispatch reset above). + fun extPage(): (nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int) { + var e: int; + var live: seq[int]; + send upstream, eValidateReq, (client = this, scope = action.scope, v = -1); + receive { + case eValidateResp: (r: (ok: bool, epoch: int)) { e = r.epoch; } + } + if (cfg.extStaleList && gen % 10 >= 2) { + // The recency mutant: the resumed attempt consumes the dead + // attempt's answer instead of re-listing. + e = 1; + } + live = upstreamRowIds(e); + if (action.cursor == 0) { + send store, eExtReconReq, (client = this, gen = gen, live = live, supported = cfg.extRecon); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + if (dead) { return mkTransition(0, true, false, -1, false); } + return mkTransition(1, false, false, -1, false); + } + send store, eExtCopy, (client = this, gen = gen, ids = live); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + return mkTransition(0, true, false, -1, false); + } + + // C1-probe page 0: consult and CONTINUE mid-chain (no spawn, no + // inline replay). Hit + valid -> the annotated replay page (cursor + // 1); miss or revalidation failure -> the fresh chain (cursor 2). + // Hit recording is pinned at lookup time as everywhere. + fun c1ConsultPage(): (nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int) { + var hit: bool; + var v: int; + var ok: bool; + var e: int; + send store, eLookupReq, (client = this, gen = gen, scope = action.scope); + receive { + case eLookupResp: (r: (hit: bool, v: int)) { + hit = r.hit; + v = r.v; + } + case eStoreDead: { dead = true; } + } + if (dead) { return mkTransition(0, true, false, -1, false); } + ok = false; + e = -1; + if (hit) { + send upstream, eValidateReq, (client = this, scope = action.scope, v = v); + receive { + case eValidateResp: (r: (ok: bool, epoch: int)) { + ok = r.ok; + e = r.epoch; + } + } + } + announce eAnnConsult, (syncN = syncN, scope = action.scope, hit = hit, v = v, validated = ok, epoch = e, freshFetch = false, diffVerdict = false, attempt = gen); + if (hit && ok) { + return mkTransition(1, false, true, v, false); + } + return mkTransition(2, false, hit, v, false); + } + + fun consultPage(): (nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int) { + var hit: bool; + var v: int; + var ok: bool; + var e: int; + var carrier: tAction; + var marked: bool; + var willDiff: bool; + var ghost: tRoundGhost; + var rp: (nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int); + // Cold attempt (warm install failed at attempt start): no + // source-cache lookup exists — the connector adapts cold and + // fetches fresh (CO-6b-005 shape). No hit is recorded. + if (!warm && (cfg.cell == 51 || cfg.cell == 52)) { + return mkTransition(2, false, false, -1, false); + } + // Scenario-6 variants: the marker check precedes the consult + // (clause iii). A marked scope's work completed this sync; + // re-consult and re-derivation are suppressed. + if (cfg.variant != VAR_SHIPPED && action.cursor == 0) { + send store, eMarkerReadReq, (client = this, gen = gen, scope = action.scope); + receive { + case eMarkerReadResp: (r: (marked: bool)) { marked = r.marked; } + case eStoreDead: { dead = true; } + } + if (dead) { return mkTransition(0, true, false, -1, false); } + if (marked) { + return mkTransition(0, true, false, -1, false); + } + } + send store, eLookupReq, (client = this, gen = gen, scope = action.scope); + receive { + case eLookupResp: (r: (hit: bool, v: int)) { + hit = r.hit; + v = r.v; + } + case eStoreDead: { dead = true; } + } + if (dead) { return mkTransition(0, true, false, -1, false); } + ok = false; + e = -1; + if (hit) { + // Hit recording is pinned at lookup-hit time, before and + // regardless of the revalidation outcome (MODEL_SPEC 3). + hits[action.scope] = v; + send upstream, eValidateReq, (client = this, scope = action.scope, v = v); + receive { + case eValidateResp: (r: (ok: bool, epoch: int)) { + ok = r.ok; + e = r.epoch; + } + } + } + // Changed-with-diff verdict (overlay flavors): the diff-based + // consult counts as consulted-against-upstream (round-5 P2 pin). + willDiff = hit && !ok && (cfg.variant == VAR_OVERLAY_NAIVE || cfg.variant == VAR_OVERLAY_UNIT || cfg.variant == VAR_OVERLAY_LAST); + announce eAnnConsult, (syncN = syncN, scope = action.scope, hit = hit, v = v, validated = ok, epoch = e, freshFetch = false, diffVerdict = willDiff, attempt = gen); + if (!hit) { + // No previous artifact surface: fetch-fresh. + return mkTransition(2, false, false, -1, false); + } + if (ok) { + if (cfg.variant != VAR_SHIPPED) { + // Variants replay INLINE on the consulting page — no + // carrier spawn exists (V-ATOMIC clause i; V-NAIVE + // shares the inline shape, differing only in commit + // structure). + return variantReplay(v); + } + if (action.cursor == 0 && cfg.cell == 1) { + // Verdict replay: spawn a same-op carrier with the + // replay annotation in its token; chain continues. + carrier = (aid = action.aid + 100, op = action.op, scope = action.scope, cursor = 0, hasAnnotation = true, annotationV = v, publishes = cfg.carrierPublishes); + return (nextCursor = 1, done = false, spawn = carrier, hasSpawn = true, hitScope = action.scope, hitV = v, hasHit = true, markReplayed = false, replayedScope = -1); + } + if (action.cursor == 0 && (cfg.cell == 31 || cfg.cell == 51 || cfg.cell == 52)) { + // Case-3B/5 shape: 1-page planning — consult, spawn the + // carrier, and POP (done at the stop checkpoint). No + // re-consult page exists; the hit map keeps V_A. + carrier = (aid = action.aid + 100, op = action.op, scope = action.scope, cursor = 0, hasAnnotation = true, annotationV = v, publishes = cfg.carrierPublishes); + return (nextCursor = 0, done = true, spawn = carrier, hasSpawn = true, hitScope = action.scope, hitV = v, hasHit = true, markReplayed = false, replayedScope = -1); + } + if (action.cursor == 0 && cfg.cell == 4) { + // Scenario 4: first of two duplicate carriers — + // byte-distinct tokens (distinct aids) encoding the + // same (scope, verdict), dodging spawn dedup. + carrier = (aid = action.aid + 100, op = action.op, scope = action.scope, cursor = 0, hasAnnotation = true, annotationV = v, publishes = cfg.carrierPublishes); + return (nextCursor = 1, done = false, spawn = carrier, hasSpawn = true, hitScope = action.scope, hitV = v, hasHit = true, markReplayed = false, replayedScope = -1); + } + if (action.cursor == 1 && cfg.cell == 4) { + // Second duplicate carrier; chain ends. + carrier = (aid = action.aid + 200, op = action.op, scope = action.scope, cursor = 0, hasAnnotation = true, annotationV = v, publishes = cfg.carrierPublishes); + return (nextCursor = 0, done = true, spawn = carrier, hasSpawn = true, hitScope = action.scope, hitV = v, hasHit = true, markReplayed = false, replayedScope = -1); + } + if (action.cursor == 0 && cfg.cell == 3) { + // Carrier-less variant (1c): replay inline on the + // consulting page itself; no spawn, no stop needed. + rp = replayPage(v, true, action.aid * 100, true, v); + return rp; + } + // Re-consult page (cursor 1): still valid, nothing new. + return mkTransition(0, true, true, v, false); + } + if (willDiff) { + ovFrom = v; + ovTo = e; + if (cfg.variant == VAR_OVERLAY_NAIVE) { + // THE MISDRAW (6-overlay-naive): the unit {clear, copy, + // marker, publish(V_to)} commits at the CONSULT + // boundary; overlay pages follow per-page. The round is + // NOT complete here (lastOp waits for the final overlay + // page), but the marker and post-diff validator are + // already durable. + ghost = (roundId = action.aid * 100, verdict = V_OVERLAY, consultEpoch = e, config = 0, lastOp = false, attempt = gen); + send store, eReplayUnit, (client = this, gen = gen, scope = action.scope, v = e, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + if (dead) { return mkTransition(0, true, false, -1, false); } + } + if (cfg.variant == VAR_OVERLAY_LAST) { + // THE THIRD PLACEMENT (6-overlay-last, round-7 F2): no + // unit anywhere — clear and copy commit as two separate + // per-page ops at the replay boundary; marker+publish + // trail LAST (see overlayPage). A crash before the + // marker leaves unmarked debris that the re-verdict's + // own clear WIPES (no reduction to 6-naive's union). + ghost = (roundId = action.aid * 100, verdict = V_OVERLAY, consultEpoch = e, config = 0, lastOp = false, attempt = gen); + send store, eClearScope, (client = this, gen = gen, scope = action.scope, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + if (dead) { return mkTransition(0, true, false, -1, false); } + send store, eCopyScope, (client = this, gen = gen, scope = action.scope, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + if (dead) { return mkTransition(0, true, false, -1, false); } + } + // V-OVERLAY-UNIT: no store op commits at consult; the + // collect starts (pin o-ii). + return mkTransition(4, false, true, v, false); + } + // Revalidation failed: verdict fetch-fresh; chain continues + // with the fresh round (hit stays recorded — last-write-wins). + return mkTransition(2, false, true, v, false); + } + + // Overlay pages (cursors 4, 5): diff collection. V-OVERLAY-UNIT + // buffers pages and commits ONE eOverlayUnit at the final page + // (pins o-i/o-ii/o-iii); the naive misdraw commits per-page via the + // shipped path (upserts, then tombstones with the round's lastOp). + fun overlayPage(): (nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int) { + var page: int; + var ups: seq[tRow]; + var rms: seq[int]; + var more: bool; + var i: int; + var ghost: tRoundGhost; + page = action.cursor - 4; + send upstream, eDiffReq, (client = this, scope = action.scope, fromEpoch = ovFrom, page = page); + receive { + case eDiffResp: (r: (upserts: seq[tRow], removes: seq[int], epoch: int, morePages: bool)) { + ups = r.upserts; + rms = r.removes; + more = r.morePages; + } + } + if (cfg.variant == VAR_OVERLAY_UNIT) { + i = 0; + while (i < sizeof(ups)) { bufUp += (sizeof(bufUp), ups[i]); i = i + 1; } + i = 0; + while (i < sizeof(rms)) { bufRm += (sizeof(bufRm), rms[i]); i = i + 1; } + if (more) { + return mkTransition(action.cursor + 1, false, false, -1, false); + } + ghost = (roundId = action.aid * 100, verdict = V_OVERLAY, consultEpoch = ovTo, config = 0, lastOp = true, attempt = gen); + send store, eOverlayUnit, (client = this, gen = gen, scope = action.scope, v = ovTo, upserts = bufUp, removes = bufRm, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + return mkTransition(0, true, false, -1, false); + } + // Naive and third-placement: per-page commits on the shipped path. + if (more) { + ghost = (roundId = action.aid * 100, verdict = V_OVERLAY, consultEpoch = ovTo, config = 0, lastOp = false, attempt = gen); + send store, eUpsertPage, (client = this, gen = gen, scope = action.scope, rows = ups, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + return mkTransition(action.cursor + 1, false, false, -1, false); + } + if (cfg.variant == VAR_OVERLAY_LAST) { + // Third placement's tail: tombstones are NOT the round's + // last prescribed op — marker and publish(V_to) trail as + // two separate queue positions. w2 = a crash between them: + // marked, entry-less, content-complete; clause (iii) + // suppresses the re-execution and the scope seals with an + // EMPTY fold (publish never committed) — the P1 witness. + ghost = (roundId = action.aid * 100, verdict = V_OVERLAY, consultEpoch = ovTo, config = 0, lastOp = false, attempt = gen); + send store, eTombstonePage, (client = this, gen = gen, scope = action.scope, removes = rms, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + if (dead) { return mkTransition(0, true, false, -1, false); } + send store, eMarkerPut, (client = this, gen = gen, scope = action.scope); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + if (dead) { return mkTransition(0, true, false, -1, false); } + ghost = (roundId = action.aid * 100, verdict = V_OVERLAY, consultEpoch = ovTo, config = 0, lastOp = true, attempt = gen); + send store, ePublishEntry, (client = this, gen = gen, scope = action.scope, v = ovTo, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + return mkTransition(0, true, false, -1, false); + } + ghost = (roundId = action.aid * 100, verdict = V_OVERLAY, consultEpoch = ovTo, config = 0, lastOp = true, attempt = gen); + send store, eTombstonePage, (client = this, gen = gen, scope = action.scope, removes = rms, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + return mkTransition(0, true, false, -1, false); + } + + fun freshPage(): (nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int) { + var rows: seq[tRow]; + var e: int; + var more: bool; + var page: int; + var i: int; + var r2: tRow; + var ghost: tRoundGhost; + page = action.cursor - 2; + send upstream, eFetchReq, (client = this, scope = action.scope, page = page); + receive { + case eFetchResp: (r: (rows: seq[tRow], epoch: int, morePages: bool)) { + rows = r.rows; + e = r.epoch; + more = r.morePages; + } + } + // Session-derived chains (case-7 reader) stamp their fresh rows + // with the value read in the derivation phase; config-modeled + // cells (case 5) tag fresh rows with this attempt's compat + // config (the P1 clause-c ghost). + if (pageStamp != -1 || aconfig != 0) { + i = 0; + while (i < sizeof(rows)) { + r2 = rows[i]; + if (pageStamp != -1) { r2.stamp = pageStamp; } + r2.config = aconfig; + rows[i] = r2; + i = i + 1; + } + } + announce eAnnConsult, (syncN = syncN, scope = action.scope, hit = false, v = -1, validated = false, epoch = e, freshFetch = true, diffVerdict = false, attempt = gen); + ghost = (roundId = action.aid * 100 + 2, verdict = V_FRESH, consultEpoch = e, config = aconfig, lastOp = false, attempt = gen); + send store, eUpsertPage, (client = this, gen = gen, scope = action.scope, rows = rows, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + if (more) { + return mkTransition(action.cursor + 1, false, false, -1, false); + } + // Last fresh page: publish the new validator (epoch-valued, + // truthful); publish is the round's last prescribed op. + ghost = (roundId = action.aid * 100 + 2, verdict = V_FRESH, consultEpoch = e, config = aconfig, lastOp = true, attempt = gen); + send store, ePublishEntry, (client = this, gen = gen, scope = action.scope, v = e, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + return mkTransition(0, true, false, -1, false); + } + + // MODEL_SPEC 4 replay page sequence. inline = the 1c shape (consult + // and replay on one page; the hit was recorded this page). + fun replayPage(vAnn: int, publishes: bool, roundId: int, inline: bool, inlineHitV: int): (nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int) { + var ghost: tRoundGhost; + var baseV: int; + var basePresent: bool; + var doCopy: bool; + var hitHas: bool; + var hitNow: int; + // B1 (MODEL_SPEC 4): a replay-annotated page arriving in an + // attempt WITHOUT source-cache handling is SILENTLY IGNORED — + // no-op page ops, no failure, no announce, no marks. Precedes + // every gate (the page ops are nil, so no gate ever evaluates). + // The 5b scripted seal-state expectation is this path's oracle. + if (!g6) { + return mkTransition(0, true, false, -1, false); + } + // warm-gate check (toggle: warmGate): a cold attempt's replay + // page fails LOUD — announced, chain ends cold, no ops (the 5a + // mitigation's success path; behavior, not an assert, because + // scripted drift configs reach this gate legitimately). In P4 + // cells the loud failure fails the ATTEMPT (MODEL_SPEC 4). + if (cfg.toggles.warmGate && !warm) { + announce eAnnLoudCold, (syncN = syncN, scope = action.scope, reason = 2); + if (cfg.loudColdFailsAttempt) { + failedChain = true; + send scheduler, eChainFailed, (aid = action.aid, cursor = action.cursor, scope = action.scope, reason = 2); + } + return mkTransition(0, true, false, -1, false); + } + // hit check (B5 provenance; loud cold on absence). The hit map + // is ONE sync-level structure, recorded at lookup time, + // last-write-wins — so the carrier reads it LIVE at drain time + // (a re-consult in the same attempt may have overwritten it; + // the case-3A rebind hole is this read seeing the overwrite). + if (inline) { + hitHas = true; + hitNow = inlineHitV; + } else { + send scheduler, eHitReadReq, (worker = this, scope = action.scope); + receive { + case eHitReadResp: (r: (has: bool, v: int)) { + hitHas = r.has; + hitNow = r.v; + } + case eAbortWorker: { abortedMid = true; } + } + if (abortedMid) { return mkTransition(0, true, false, -1, false); } + } + assert hitHas, "unmodeled loud-cold: replay without recorded hit"; + // oncePerScope check-and-mark. Locks ON: the grant carries the + // replayed status and the release commits the mark — atomic + // check-then-mark. Locks OFF: lock-free read here, mark lands + // at the action transition — the case-4 TOCTOU window. + doCopy = true; + if (cfg.toggles.scopeLocks) { + send scheduler, eScopeLockAcquire, (worker = this, scope = action.scope); + receive { + case eScopeLockGrant: (g: (replayed: bool)) { + if (cfg.toggles.oncePerScope && g.replayed) { doCopy = false; } + } + case eAbortWorker: { abortedMid = true; } + } + if (abortedMid) { return mkTransition(0, true, false, -1, false); } + } else { + send scheduler, eReplayedCheckReq, (worker = this, scope = action.scope); + receive { + case eReplayedCheckResp: (r: (replayed: bool)) { + if (cfg.toggles.oncePerScope && r.replayed) { doCopy = false; } + } + } + } + // CO-6b-007 premise: ONE injected destination-write failure + // after the scope lock is acquired; the worker retries the + // action IN-ATTEMPT (mirrors syncOneAction's retry loop), + // re-entering the page sequence from the top. With the + // release-on-error edge present (shipped) the retry re-acquires + // cleanly and the page completes; with the edge REMOVED (the + // model's mutation check) the retry blocks forever on its own + // leaked lock — the hang surfaces as a checker deadlock. + if (cfg.warmPageFails && !warmFailUsed) { + warmFailUsed = true; + if (cfg.lockReleaseOnError && cfg.toggles.scopeLocks) { + send scheduler, eScopeLockRelease, (scope = action.scope, mark = false); + } + return replayPage(vAnn, publishes, roundId, inline, inlineHitV); + } + if (doCopy) { + if (cfg.toggles.hitValidatorBinding) { + send store, eBaseReadReq, (client = this, gen = gen, scope = action.scope); + receive { + case eBaseReadResp: (r: (v: int, present: bool)) { + baseV = r.v; + basePresent = r.present; + } + case eStoreDead: { dead = true; } + } + if (dead) { return mkTransition(0, true, false, -1, false); } + if (!basePresent || baseV != hitNow) { + // Loud cold (CO-6b-004 kill): the binding gate + // detects a base the recorded hit did not attest; + // the chain fails cold — no copy, no publish, no + // wrong data. Behavior, not an assert: scenario-3 + // schedules reach this gate legitimately and green. + // In P4 cells the loud failure fails the ATTEMPT. + announce eAnnLoudCold, (syncN = syncN, scope = action.scope, reason = 1); + if (cfg.toggles.scopeLocks) { + send scheduler, eScopeLockRelease, (scope = action.scope, mark = false); + } + if (cfg.loudColdFailsAttempt) { + failedChain = true; + send scheduler, eChainFailed, (aid = action.aid, cursor = action.cursor, scope = action.scope, reason = 1); + } + return mkTransition(0, true, false, -1, false); + } + } + ghost = (roundId = roundId, verdict = V_REPLAY, consultEpoch = vAnn, config = aconfig, lastOp = false, attempt = gen); + send store, eClearScope, (client = this, gen = gen, scope = action.scope, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + ghost = (roundId = roundId, verdict = V_REPLAY, consultEpoch = vAnn, config = aconfig, lastOp = !publishes, attempt = gen); + send store, eCopyScope, (client = this, gen = gen, scope = action.scope, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + } + if (cfg.toggles.scopeLocks) { + send scheduler, eScopeLockRelease, (scope = action.scope, mark = doCopy); + } + if (publishes) { + // A copy-skipped replacement round's publish still runs + // (MODEL_SPEC 4: ePublishEntry even on a copy-skipped page). + ghost = (roundId = roundId, verdict = V_REPLAY, consultEpoch = vAnn, config = aconfig, lastOp = true, attempt = gen); + send store, ePublishEntry, (client = this, gen = gen, scope = action.scope, v = vAnn, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + } + return (nextCursor = 0, done = true, spawn = default(tAction), hasSpawn = false, hitScope = action.scope, hitV = hitNow, hasHit = inline, markReplayed = doCopy, replayedScope = action.scope); + } + + // Scenario-6 replay-with-marker, inline on the consulting page. + // V-NAIVE: shipped steps with the marker as a SEPARATE op after + // eCopyScope (clear, copy, marker, publish — four queue positions; + // a crash can land between any two). V-ATOMIC: one eReplayUnit op. + fun variantReplay(v: int): (nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int) { + var ghost: tRoundGhost; + if (cfg.variant == VAR_ATOMIC || cfg.variant == VAR_OVERLAY_NAIVE || cfg.variant == VAR_OVERLAY_UNIT) { + ghost = (roundId = action.aid * 100, verdict = V_REPLAY, consultEpoch = v, config = 0, lastOp = true, attempt = gen); + send store, eReplayUnit, (client = this, gen = gen, scope = action.scope, v = v, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + return (nextCursor = 0, done = true, spawn = default(tAction), hasSpawn = false, hitScope = action.scope, hitV = v, hasHit = true, markReplayed = true, replayedScope = action.scope); + } + ghost = (roundId = action.aid * 100, verdict = V_REPLAY, consultEpoch = v, config = 0, lastOp = false, attempt = gen); + send store, eClearScope, (client = this, gen = gen, scope = action.scope, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + if (dead) { return mkTransition(0, true, false, -1, false); } + send store, eCopyScope, (client = this, gen = gen, scope = action.scope, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + if (dead) { return mkTransition(0, true, false, -1, false); } + send store, eMarkerPut, (client = this, gen = gen, scope = action.scope); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + if (dead) { return mkTransition(0, true, false, -1, false); } + ghost = (roundId = action.aid * 100, verdict = V_REPLAY, consultEpoch = v, config = 0, lastOp = true, attempt = gen); + send store, ePublishEntry, (client = this, gen = gen, scope = action.scope, v = v, ghost = ghost); + receive { case eStoreAck: {} case eStoreDead: { dead = true; } } + return (nextCursor = 0, done = true, spawn = default(tAction), hasSpawn = false, hitScope = action.scope, hitV = v, hasHit = true, markReplayed = true, replayedScope = action.scope); + } + + fun mkTransition(next: int, done: bool, hasHit: bool, hitV: int, markReplayed: bool): (nextCursor: int, done: bool, spawn: tAction, hasSpawn: bool, hitScope: int, hitV: int, hasHit: bool, markReplayed: bool, replayedScope: int) { + return (nextCursor = next, done = done, spawn = default(tAction), hasSpawn = false, hitScope = action.scope, hitV = hitV, hasHit = hasHit, markReplayed = markReplayed, replayedScope = action.scope); + } + + start state Boot { + entry (p: (scheduler: machine, store: machine, upstream: machine, gen: int, syncN: int, cfg: tScenarioCfg, warm: bool, g6: bool, aconfig: int)) { + scheduler = p.scheduler; + store = p.store; + upstream = p.upstream; + gen = p.gen; + syncN = p.syncN; + cfg = p.cfg; + warm = p.warm; + g6 = p.g6; + aconfig = p.aconfig; + goto Idle; + } + } +} diff --git a/formal/walker/PTst/Scenario1.p b/formal/walker/PTst/Scenario1.p new file mode 100644 index 000000000..f148cbd89 --- /dev/null +++ b/formal/walker/PTst/Scenario1.p @@ -0,0 +1,122 @@ +/* Scenario 1 — phantom union (MODEL_SPEC 9 case 1). + Shipped toggles ON in every cell: the residual exists in the shipped + design. Expected verdicts: + - tc1a1b_P1 (stop-stranding, carrier publishes, 2 syncs): P1-CONTENT + red on 1a schedules (carrier drains between/before fresh pages). + - tc1a1b_P3: P3'-COHERENCE red on 1b-i schedules (carrier drains + after the complete fresh round; content green, epoch incoherent). + - tc1a1b_P2 (3 syncs, corollary-run scoping): P2-STALENESS red on + the verification sync — the union replays warm, hops reach 2 + (the unbounded branch). + - tc1bii_P1 (carrier validator-less): P1 red; the attestation-only + edge (entry V2 over folded rows(e1)) is among the violations. + - tc1c_P1/tc1c_P2 (carrier-less hard crash): P1-CONTENT red with one + crash and no replay in attempt 2; P2 red on the verification sync. + - tcGreen_All (no interruption, no mutation, 2 syncs incl. one + honest replay): all monitors green — the abstraction sanity + control. */ + +fun shippedToggles(): tToggles { + return (warmGate = true, hitValidatorBinding = true, scopeLocks = true, oncePerScope = true, annotationBinding = false, abandonLadder = false, sessionTaintWrites = false, sessionTaintAll = false); +} + +machine Test1a { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 1; + c.interrupt = 1; + c.mutateBetweenAttempts = true; + new MEnv(c); + } + } +} + +machine Test1aVerify { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 1; + c.interrupt = 1; + c.mutateBetweenAttempts = true; + c.nSyncs = 3; + c.verificationOnlyIfInterrupted = true; + new MEnv(c); + } + } +} + +machine Test1bii { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 1; + c.interrupt = 1; + c.mutateBetweenAttempts = true; + c.carrierPublishes = false; + new MEnv(c); + } + } +} + +machine Test1c { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 1; + c.cell = 3; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + new MEnv(c); + } + } +} + +machine Test1cVerify { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 1; + c.cell = 3; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + c.nSyncs = 3; + c.verificationOnlyIfInterrupted = true; + new MEnv(c); + } + } +} + +machine TestGreen { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 1; + new MEnv(c); + } + } +} + +module Walker = { MEnv, MStore, MUpstream, MSyncAttempt, MWorker }; + +// Expected RED (counterexample = the calibration find): +test tc1a1b_P1 [main=Test1a]: assert P1 in (union Walker, { Test1a }); +test tc1a1b_P3 [main=Test1a]: assert P3prime in (union Walker, { Test1a }); +test tc1a1b_P2 [main=Test1aVerify]: assert P2 in (union Walker, { Test1aVerify }); +test tc1bii_P1 [main=Test1bii]: assert P1 in (union Walker, { Test1bii }); +test tc1c_P1 [main=Test1c]: assert P1 in (union Walker, { Test1c }); +test tc1c_P2 [main=Test1cVerify]: assert P2 in (union Walker, { Test1cVerify }); + +// Expected GREEN (sanity controls): +test tcGreen_All [main=TestGreen]: assert P1, P2, P3prime in (union Walker, { TestGreen }); +// The P1/P3 cells must also hold P1 green where their premise implies +// no alarm; P2's corollary config must stay green in honest histories. +test tc1c_P2_honest [main=Test1c]: assert P2 in (union Walker, { Test1c }); +// Probe: the verify config must still contain the sync-2 union premise. +test tc1c_P1_probe [main=Test1cVerify]: assert P1 in (union Walker, { Test1cVerify }); diff --git a/formal/walker/PTst/Scenario2.p b/formal/walker/PTst/Scenario2.p new file mode 100644 index 000000000..8a2d7fd89 --- /dev/null +++ b/formal/walker/PTst/Scenario2.p @@ -0,0 +1,149 @@ +/* Scenario 2 — session laundering (MODEL_SPEC 9 case 2, P6-A). + One sync, two same-op actions on the root stack: H (aid 1) writes + the session key with a value derived from upstream on EACH of its + two pages; G (aid 2) reads the key and emits a row embedding the + read value. Sessions variant A (shipped): the session KV is durable + at op commit and survives attempts; committed rows are never + invalidated by later session writes. Both red cells are EXPECTED + FINDINGS (no fix run — variant B is the graph addendum's + obligation). Expected verdicts: + - tc2stop_P6A (graceful stop, upstream mutated between attempts): + RED — the schedule where H's d1 write commits, G reads d1, emits, + and finishes, then the stop strands H mid-chain; H alone re-runs + and derives d2; G's committed row embeds d1 != final d2. + - tc2crash_P6A (hard crash, at-least-once): RED — both re-run from + root; the interleaving where G re-reads the DURABLE stale d1 + before H's re-derivation lands re-commits the d1 embed under a + final d2. The complementary interleaving (H re-derives first, G + embeds d2) is green — both outcomes live in this one config. + - tc2green_P6A (no interruption, no mutation): GREEN — the reader + embeds what the writer wrote; a read-miss emits nothing. */ + +machine Test2Stop { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 2; + c.cell = 2; + c.interrupt = 1; + c.interruptSync = 1; + c.nSyncs = 1; + c.mutateBetweenAttempts = true; + new MEnv(c); + } + } +} + +machine Test2Crash { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 2; + c.cell = 2; + c.interrupt = 2; + c.interruptSync = 1; + c.nSyncs = 1; + c.mutateBetweenAttempts = true; + new MEnv(c); + } + } +} + +machine Test2Green { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 2; + c.cell = 2; + c.nSyncs = 1; + new MEnv(c); + } + } +} + +// P6-C cells (session-checkpoint consistency — the CO-6b-009 root +// cause). Same crash chassis as Test2Crash; the axis is +// cfg.sessVariant (the store's session semantics at the crash +// boundary). All three variants of the conversation are cells: +// - variant 0 (shipped, durable-at-op-commit): expected RED on +// P6-C-ZOMBIE — H's beyond-checkpoint d1 survives the crash and +// the re-run G reads it before H's re-derivation lands. +// - variant 1 (the REJECTED wholesale resume-clear): expected RED +// on P6-C-AMNESIA — a checkpoint-committed d1 is destroyed and +// G's re-read misses data whose producing work will not re-run. +// - variant 2 (checkpoint-consistent sessions — the correct fix, +// future work per CO-6b-009): expected GREEN, both directions. + +machine Test2CrashP6C { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 2; + c.cell = 2; + c.interrupt = 2; + c.interruptSync = 1; + c.nSyncs = 1; + c.mutateBetweenAttempts = true; + new MEnv(c); + } + } +} + +// The amnesia and fix cells run cell 21 (root order REVERSED: the +// writer pops first). In cell 2 the reader G pops first under LIFO, +// so every checkpoint that still contains G predates H's writes — a +// checkpoint-COMMITTED session value with a reader re-run ahead of it +// (the amnesia premise) is structurally unreachable there. Verified: +// tc2clear_P6C on cell 2 is green at 10k schedules for exactly this +// reason, not because the rejected fix is sound. +machine Test2ClearP6C { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 2; + c.cell = 21; + c.interrupt = 2; + c.interruptSync = 1; + c.nSyncs = 1; + c.mutateBetweenAttempts = true; + c.sessVariant = 1; + new MEnv(c); + } + } +} + +machine Test2ConsistentP6C { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 2; + c.cell = 21; + c.interrupt = 2; + c.interruptSync = 1; + c.nSyncs = 1; + c.mutateBetweenAttempts = true; + c.sessVariant = 2; + new MEnv(c); + } + } +} + +// Expected RED (both are design findings of sessions variant A): +test tc2stop_P6A [main=Test2Stop]: assert P6A in (union Walker, { Test2Stop }); +test tc2crash_P6A [main=Test2Crash]: assert P6A in (union Walker, { Test2Crash }); + +// Expected GREEN: +test tc2green_P6A [main=Test2Green]: assert P6A in (union Walker, { Test2Green }); + +// P6-C cells. Expected RED (shipped zombie; rejected-fix amnesia): +test tc2crash_P6C [main=Test2CrashP6C]: assert P6C in (union Walker, { Test2CrashP6C }); +test tc2clear_P6C [main=Test2ClearP6C]: assert P6C in (union Walker, { Test2ClearP6C }); + +// Expected GREEN (checkpoint-consistent sessions close both directions): +test tc2consistent_P6C [main=Test2ConsistentP6C]: assert P6C in (union Walker, { Test2ConsistentP6C }); diff --git a/formal/walker/PTst/Scenario3.p b/formal/walker/PTst/Scenario3.p new file mode 100644 index 000000000..5ebe9f83a --- /dev/null +++ b/formal/walker/PTst/Scenario3.p @@ -0,0 +1,115 @@ +/* Scenario 3 — artifact swap + hit rebind (MODEL_SPEC 9 case 3). + World: upstream sits at e2 throughout (preMutate) and never moves. + Sync 1 seals artifact A = rows(e2) under V_A = 2. Sync 2 is the + premise sync: the stop strands the carrier and MEnv swaps the + previous artifact to sibling B = rows(e1) under V_B = 1 (equal + compat; truthful validators — V_B does not validate against e2, and + rows(B) != rows(up) = rows(A)). Expected verdicts: + - tc3a_P1 (shipped: hitValidatorBinding ON — the residual hole): + RED. Attempt 2's re-consult overwrites the hit map with V_B + (lookup-time recording, last-write-wins); revalidation fails; + verdict fetch-fresh. The carrier then passes the binding check + (hit V_B == base B's manifest V_B) and installs rows(B) while + publishing its annotation's V_A. Carrier-last schedules seal + rows(B) under entry V_A -> P1-ATTEST-SEAL; carrier-first or + interleaved schedules leave B's id-1 debris under the fresh round + -> P1-CONTENT. Schedules where the carrier drains BEFORE the + re-consult hit the binding gate honestly (hit still V_A != base + V_B) -> loud cold, green: both faces of the same toggle. + - tc3a_P2: GREEN in every cell — attempt 1's validation match + qualifies the scope as consulted this sync, and copied rows carry + hops 1 (corrected expectation; spec v2 wrongly claimed a P2 red). + - tc3b_P1 (pre-CO-6b-004: hitValidatorBinding OFF, 1-page planning + that pops at the stop checkpoint): RED on a weaker premise — no + re-consult exists, the hit map keeps V_A, and NO binding check + runs: the carrier copies swapped base B and publishes V_A -> + P1-ATTEST-SEAL. + - tc3bBindingOn_All (same premise, binding ON — the CO-6b-004 + kill): GREEN — the binding gate compares hit V_A to base V_B, + fails loud-cold, and no wrong data lands (the scope seals empty + in the premise schedules). + The annotationBinding fix runs (3A fix + empty-validator coverage + cell) are de-scoped from the build with the toggle itself (v11). */ + +fun noBindingToggles(): tToggles { + return (warmGate = true, hitValidatorBinding = false, scopeLocks = true, oncePerScope = true, annotationBinding = false, abandonLadder = false, sessionTaintWrites = false, sessionTaintAll = false); +} + +machine Test3A { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 3; + c.interrupt = 1; + c.preMutate = true; + c.swapBase = true; + new MEnv(c); + } + } +} + +machine Test3B { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 3; + c.cell = 31; + c.interrupt = 1; + c.preMutate = true; + c.swapBase = true; + c.toggles = noBindingToggles(); + new MEnv(c); + } + } +} + +machine Test3BBindOn { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 3; + c.cell = 31; + c.interrupt = 1; + c.preMutate = true; + c.swapBase = true; + new MEnv(c); + } + } +} + +/* v11 — the case-3 re-run under V-ATOMIC (MODEL_SPEC 9.6): the + subsumption witness for the annotationBinding de-scope. Same stop + + base-swap premise as 3A, but replay is consult-inline with the + atomic unit: no carrier and no annotation exist to trust; the marker + lives in the CURRENT sync's artifact (untouched by the swap); + restored hits are inert. Either the unit committed before the stop + (attempt 2 marker-suppresses — the seal is the unit's coherent + contents) or nothing did (attempt 2 re-consults the ACTUALLY current + base: swapped B's V1 fails validation against upstream e2 → + fetch-fresh). Expected GREEN across P1/P2/P3'. */ +machine Test3Atomic { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 3; + c.variant = VAR_ATOMIC; + c.interrupt = 1; + c.preMutate = true; + c.swapBase = true; + new MEnv(c); + } + } +} + +// Expected RED (the residual hole, then the weaker pre-fix premise): +test tc3a_P1 [main=Test3A]: assert P1 in (union Walker, { Test3A }); +test tc3b_P1 [main=Test3B]: assert P1 in (union Walker, { Test3B }); + +// Expected GREEN: +test tc3a_P2 [main=Test3A]: assert P2 in (union Walker, { Test3A }); +test tc3bBindingOn_All [main=Test3BBindOn]: assert P1, P2, P3prime in (union Walker, { Test3BBindOn }); +test tc3atomic_All [main=Test3Atomic]: assert P1, P2, P3prime in (union Walker, { Test3Atomic }); diff --git a/formal/walker/PTst/Scenario4.p b/formal/walker/PTst/Scenario4.p new file mode 100644 index 000000000..bbb9fd295 --- /dev/null +++ b/formal/walker/PTst/Scenario4.p @@ -0,0 +1,83 @@ +/* Scenario 4 — duplicate replay carriers (MODEL_SPEC 9 case 4). + Two carriers with byte-distinct page tokens (distinct aids) encoding + the same (scope, verdict); no interruption, no mutation; 2 syncs. + Expected verdicts: + - tc4shipped_All (oncePerScope + scopeLocks ON): GREEN — the second + carrier's copy is deduped under the lock (grant carries the + replayed status; the mark commits at release); its B5-legal + copy-skipped round folds as a no-op re-publish. + - tc4noOnce_P1 (oncePerScope OFF, locks ON): RED — P1-LEGALITY, + both copies commit (the lock serializes but does not dedup). + - tc4noLocks_P1 (oncePerScope ON, locks OFF): RED — P1-LEGALITY via + the check-then-mark TOCTOU: both carriers read the replayed set + before either transition commits the mark. + - tc4atomic_All (V-ATOMIC re-run, v11 scope): GREEN — carriers do + not exist under the variant (replay is inline at the consult); + the duplicate-carrier premise is structurally unreachable. */ + +fun noOnceToggles(): tToggles { + return (warmGate = true, hitValidatorBinding = true, scopeLocks = true, oncePerScope = false, annotationBinding = false, abandonLadder = false, sessionTaintWrites = false, sessionTaintAll = false); +} + +fun noLocksToggles(): tToggles { + return (warmGate = true, hitValidatorBinding = true, scopeLocks = false, oncePerScope = true, annotationBinding = false, abandonLadder = false, sessionTaintWrites = false, sessionTaintAll = false); +} + +machine Test4Shipped { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 4; + c.cell = 4; + new MEnv(c); + } + } +} + +machine Test4NoOnce { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 4; + c.cell = 4; + c.toggles = noOnceToggles(); + new MEnv(c); + } + } +} + +machine Test4NoLocks { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 4; + c.cell = 4; + c.toggles = noLocksToggles(); + new MEnv(c); + } + } +} + +machine Test4Atomic { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 4; + c.variant = VAR_ATOMIC; + c.cell = 4; + new MEnv(c); + } + } +} + +// Expected RED (mitigation kill runs): +test tc4noOnce_P1 [main=Test4NoOnce]: assert P1 in (union Walker, { Test4NoOnce }); +test tc4noLocks_P1 [main=Test4NoLocks]: assert P1 in (union Walker, { Test4NoLocks }); + +// Expected GREEN: +test tc4shipped_All [main=Test4Shipped]: assert P1, P2, P3prime in (union Walker, { Test4Shipped }); +test tc4atomic_All [main=Test4Atomic]: assert P1, P2, P3prime in (union Walker, { Test4Atomic }); diff --git a/formal/walker/PTst/Scenario5.p b/formal/walker/PTst/Scenario5.p new file mode 100644 index 000000000..4f197884f --- /dev/null +++ b/formal/walker/PTst/Scenario5.p @@ -0,0 +1,131 @@ +/* Scenario 5 — warm-drift (MODEL_SPEC 9 case 5): the warmGate kill and + both produce-side block triggers. World: upstream never moves (drift + is config-side). Sync 1 seeds artifact A = rows(e1) under V1 with + compat record K1 (baseConfig = 1). Sync 2 is the premise sync: + attempt 1 (warm, K1) consults, records hit {S: V1}, spawns carrier C + (cell-31 planning shape: consult+spawn+pop) and the stop strands C; + MEnv applies the scripted drift input to attempt 2 ONLY in premise + histories (checkpoint holds a stranded carrier), so non-premise + schedules run undrifted and green. Expected verdicts: + - tc5a_P1 (cell 51, warmGate OFF — the kill): RED. Attempt 2 + computes K2; install: G7 mismatch -> COLD, trigger 1 (B4) marks + produce-blocked. With the gate off, C passes the hit check + (restored {S: V1}) and binding (base unchanged, V1) and copies + K1-tagged rows into the K2 attempt -> P1-CONFIG (clause c). + - tc5a_Gate_All (cell 51, shipped toggles — warmGate ON): GREEN. + C's warm-gate check fails LOUD (eAnnLoudCold, chain cold, no + ops); the artifact seals blocked with partition[S] empty. The + attempt-failure/abandonLadder ladder is P4's tranche; here loud + cold ends the chain and the sync seals (scenario-3 precedent). + - tc5b_Dropout_All (cell 52, shipped toggles, G6 withdrawn in + attempt 2): GREEN, and the green IS the required design finding: + trigger 2 blocks the artifact at install; C's replay-annotated + page arrives in the handling-less attempt and is SILENTLY + IGNORED (B1) — no failure, no rows; the sync seals green with + partition[S] EMPTY and the artifact blocked. P1/P2 are blind + here (no rows, no illegal round); SealExpect's scripted + wantBlocked + wantScopeEmpty expectation is the dropout's only + executable oracle (MODEL_SPEC 9.5b). + - tc5b_CrashWindow (cell 52, interrupt 3 = stop attempt 1, crash + attempt 2): RED on SealExpect — the crash-window finding. + Trigger 2's block lives ONLY in attempt 2's volatile flag until + a checkpoint carries it; a crash landing before that first + checkpoint kills the flag with the attempt. Attempt 3 has + handling back (withdrawal is attempt-2-exact), re-detects + nothing (no compat mismatch), runs warm, and seals UNBLOCKED — + the expectation's wantBlocked assert fires. Schedules where the + crash lands after a checkpoint carrying the flag seal blocked + and stay green: the red is exactly the window. + - tc5c_C1Probe (cell 53, shipped toggles, plain stop): RED on the + C1Probe reachability monitor — the CO-6b-002 conformance answer. + One action, replay annotation MID-CHAIN: page 0 consults (hit + recorded at lookup), page 1 replays. The stop lands between the + pages; the checkpoint holds the mid-chain cursor + the hit map; + the resumed page 1 performs NO fresh consult and its hit check + passes on the RESTORED map. The counterexample trace is the + witness (finding, not a bug); confirm against the real + implementation via the chaos bridge (deliverable 6). */ + +machine Test5aGateOff { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 5; + c.cell = 51; + c.interrupt = 1; + c.baseConfig = 1; + c.driftCompat = true; + c.toggles = (warmGate = false, hitValidatorBinding = true, scopeLocks = true, oncePerScope = true, annotationBinding = false, abandonLadder = false, sessionTaintWrites = false, sessionTaintAll = false); + new MEnv(c); + } + } +} + +machine Test5aGateOn { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 5; + c.cell = 51; + c.interrupt = 1; + c.baseConfig = 1; + c.driftCompat = true; + new MEnv(c); + } + } +} + +machine Test5bDropout { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 5; + c.cell = 52; + c.interrupt = 1; + c.baseConfig = 1; + c.withdrawG6 = true; + new MEnv(c); + } + } +} + +machine Test5bCrash { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 5; + c.cell = 52; + c.interrupt = 3; + c.baseConfig = 1; + c.withdrawG6 = true; + new MEnv(c); + } + } +} + +machine Test5cC1 { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 5; + c.cell = 53; + c.interrupt = 1; + new MEnv(c); + } + } +} + +// Expected RED (the warmGate kill, the crash-window finding, then the +// C1 reachability witness): +test tc5a_P1 [main=Test5aGateOff]: assert P1 in (union Walker, { Test5aGateOff }); +test tc5b_CrashWindow [main=Test5bCrash]: assert SealExpect in (union Walker, { Test5bCrash }); +test tc5c_C1Probe [main=Test5cC1]: assert C1Probe, P1, P2 in (union Walker, { Test5cC1 }); + +// Expected GREEN: +test tc5a_Gate_All [main=Test5aGateOn]: assert P1, P2, SealExpect in (union Walker, { Test5aGateOn }); +test tc5b_Dropout_All [main=Test5bDropout]: assert P1, P2, SealExpect in (union Walker, { Test5bDropout }); diff --git a/formal/walker/PTst/Scenario6.p b/formal/walker/PTst/Scenario6.p new file mode 100644 index 000000000..aaf6fecda --- /dev/null +++ b/formal/walker/PTst/Scenario6.p @@ -0,0 +1,201 @@ +/* Scenario 6 — atomic-unit design variant, fetch-fresh flavor + (MODEL_SPEC 9 case 6). The bake-off pair plus the V-ATOMIC re-runs + of the scenario-1 premise family. Expected verdicts: + - tc6naive_P1 (V-NAIVE: marker as a separate op outside any unit, + crash script): P1-CONTENT red — a crash between eCopyScope and the + marker leaves unmarked debris; the resumed attempt re-consults, + revalidation fails, and the fresh round unions over the debris. + - tc6atomic_All (V-ATOMIC, same crash script): GREEN — the unit + {clear, copy, marker, publish} holds one queue position; every + crash placement leaves either nothing or the complete unit, and + the marker suppresses re-derivation on resume. + - tc6atomicStop_All (V-ATOMIC, stop-stranding script of 1a/1b): + GREEN — replay executes inline on the consulting page, so the + stranded-carrier premise is structurally unreachable. + Both variants run with the shipped mitigation toggles ON; the + variant is a commit-structure change, not a toggle (MODEL_SPEC 9.6). */ + +machine Test6Naive { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 6; + c.variant = VAR_NAIVE; + c.cell = 3; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + new MEnv(c); + } + } +} + +machine Test6Atomic { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 6; + c.variant = VAR_ATOMIC; + c.cell = 3; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + new MEnv(c); + } + } +} + +machine Test6AtomicStop { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 6; + c.variant = VAR_ATOMIC; + c.interrupt = 1; + c.mutateBetweenAttempts = true; + new MEnv(c); + } + } +} + +/* v10 addendum — overlay flavor. 6-overlay runs with oncePerScope AND + scopeLocks OFF (the structural claim 1d could not make); the marker + inside the unit is the dedup. 6-overlay-naive runs shipped toggles; + its defect is the unit boundary, which no toggle repairs. + mutateBetweenAttempts stays ON in the crash cells (the pre-refactor + env mutated unconditionally on crash; the green claims were made + under that broader drift and are preserved as-is). */ + +fun overlayToggles(): tToggles { + return (warmGate = true, hitValidatorBinding = true, scopeLocks = false, oncePerScope = false, annotationBinding = false, abandonLadder = false, sessionTaintWrites = false, sessionTaintAll = false); +} + +machine Test6OverlayCrash { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 6; + c.variant = VAR_OVERLAY_UNIT; + c.cell = 3; + c.interrupt = 2; + c.toggles = overlayToggles(); + c.mutateBetweenAttempts = true; + c.mutateBetweenSyncs = true; + new MEnv(c); + } + } +} + +machine Test6OverlayStop { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 6; + c.variant = VAR_OVERLAY_UNIT; + c.interrupt = 1; + c.toggles = overlayToggles(); + c.mutateBetweenSyncs = true; + new MEnv(c); + } + } +} + +/* MS-CO-001 (parallel-review F6) — the o-iv-removal mutant: the + 6-overlay STOP config with the consult-reset removed (o4Mutant). + In sub-case (b)'s schedule the resume honors the restored mid-chain + cursor with an EMPTY collect buffer, collects only the final overlay + page, and commits a unit missing the first page's ops → partition + diverges from the self-grounding fold's rows(e2). Expected RED + (P1-CONTENT); kills the one load-bearing line the overlay flavor + adds beyond V-ATOMIC. */ +machine Test6OverlayMutO4 { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 6; + c.variant = VAR_OVERLAY_UNIT; + c.interrupt = 1; + c.toggles = overlayToggles(); + c.mutateBetweenSyncs = true; + c.o4Mutant = true; + new MEnv(c); + } + } +} + +machine Test6OverlayNaive { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 6; + c.variant = VAR_OVERLAY_NAIVE; + c.cell = 3; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + c.mutateBetweenSyncs = true; + new MEnv(c); + } + } +} + +/* v11 (round-7 F2) — the third placement gets its own cell. Same crash + premise as 6-overlay-naive; the placement differs: NO unit — clear+ + copy per-page at the replay boundary, marker+publish LAST as two + separate trailing ops. Expected RED via w2 (crash between marker and + publish: marked, entry-less, content-complete scope suppressed on + resume; non-empty partition vs EMPTY fold). w1 (crash before the + marker) must NOT alarm: the re-verdict's clear wipes the debris and + the cross-attempt double copy is legal under the complete-rounds + replacement-counting pin — the pre-pin monitor would have raised + P1-LEGALITY on this converging history. */ +machine Test6OverlayLast { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 6; + c.variant = VAR_OVERLAY_LAST; + c.cell = 3; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + c.mutateBetweenSyncs = true; + new MEnv(c); + } + } +} + +machine Test6OverlayNaiveVerify { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 6; + c.variant = VAR_OVERLAY_NAIVE; + c.cell = 3; + c.interrupt = 2; + c.mutateBetweenAttempts = true; + c.mutateBetweenSyncs = true; + c.nSyncs = 3; + c.verificationOnlyIfInterrupted = true; + new MEnv(c); + } + } +} + +// Expected RED: +test tc6naive_P1 [main=Test6Naive]: assert P1 in (union Walker, { Test6Naive }); +test tc6overlayNaive_P1 [main=Test6OverlayNaive]: assert P1 in (union Walker, { Test6OverlayNaive }); +test tc6overlayNaive_P2 [main=Test6OverlayNaiveVerify]: assert P2 in (union Walker, { Test6OverlayNaiveVerify }); +test tc6overlayLast_P1 [main=Test6OverlayLast]: assert P1 in (union Walker, { Test6OverlayLast }); +test tc6overlayMutO4_P1 [main=Test6OverlayMutO4]: assert P1 in (union Walker, { Test6OverlayMutO4 }); + +// Expected GREEN (the structural claim): +test tc6atomic_All [main=Test6Atomic]: assert P1, P2, P3prime in (union Walker, { Test6Atomic }); +test tc6atomicStop_All [main=Test6AtomicStop]: assert P1, P2, P3prime in (union Walker, { Test6AtomicStop }); +test tc6overlay_All [main=Test6OverlayCrash]: assert P1, P2, P3prime in (union Walker, { Test6OverlayCrash }); +test tc6overlayStop_All [main=Test6OverlayStop]: assert P1, P2, P3prime in (union Walker, { Test6OverlayStop }); diff --git a/formal/walker/PTst/Scenario7.p b/formal/walker/PTst/Scenario7.p new file mode 100644 index 000000000..03ee64955 --- /dev/null +++ b/formal/walker/PTst/Scenario7.p @@ -0,0 +1,144 @@ +/* Scenario 7 — session elision under replay (MODEL_SPEC 9 case 7, + signoff addendum). Pure two-sync scripts, no interruption machinery. + Kinds in sequential phases: W (scope 0) writes session key K as a + side effect of its FRESH enumeration; R (scope 1) derives its rows' + ghost stamps from reading K. The shipped design has no coupling + between sessions and the source cache, and R's connector violates + no pinned obligation in any cell — the missing contract clause IS + the design finding. Expected verdicts: + - tc7a_P6R (write elision): RED — W warm-replays, its session write + is structurally elided, R (policy: always fresh) reads MISS and + stamps 0; counterfactual v1. + - tc7a_P1P2 GREEN — required finding: every row individually + well-formed, every scope consulted; the corruption is invisible + to content/attestation/staleness checks. + - tc7b_P6R (stale-read replay, the dual): RED — W's upstream moves + between syncs (W fresh, writes v2) while R's scope is unchanged + (R warm; copied rows carry stamp v1); counterfactual v2. No + elided write anywhere — this cell kills write-only bans. + - tc7c_All (both-warm control): GREEN — carried stamps v1 equal the + counterfactual v1; required so P6-R does not overfit to "replay + near sessions alarms". + Fix runs (produce-side taint; the full two-sync script re-executes + with the toggle ON — sync N's artifact differs from the red run's): + - tc7aTaintW_P6R: GREEN — sync N taints W (write during a capable + phase); sync N+1 consults W MISS, W re-runs fresh, K present. + - tc7bTaintW_P6R: RED — REQUIRED residual: R's hazard is a READ; + the write-only rule is half a fix. + - tc7aTaintAll_P6R, tc7bTaintAll_P6R: GREEN — R's read taints R's + kind too; replay is forfeited exactly where sessions are used + (the toggle's honest price, recorded not hidden). */ + +fun taintWritesToggles(): tToggles { + return (warmGate = true, hitValidatorBinding = true, scopeLocks = true, oncePerScope = true, annotationBinding = false, abandonLadder = false, sessionTaintWrites = true, sessionTaintAll = false); +} + +fun taintAllToggles(): tToggles { + return (warmGate = true, hitValidatorBinding = true, scopeLocks = true, oncePerScope = true, annotationBinding = false, abandonLadder = false, sessionTaintWrites = false, sessionTaintAll = true); +} + +machine Test7a { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 7; + c.cell = 7; + c.readerAlwaysFresh = true; + new MEnv(c); + } + } +} + +machine Test7b { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 7; + c.cell = 7; + c.mutateBetweenSyncs = true; + new MEnv(c); + } + } +} + +machine Test7c { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 7; + c.cell = 7; + new MEnv(c); + } + } +} + +machine Test7aTaintW { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 7; + c.cell = 7; + c.readerAlwaysFresh = true; + c.toggles = taintWritesToggles(); + new MEnv(c); + } + } +} + +machine Test7bTaintW { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 7; + c.cell = 7; + c.mutateBetweenSyncs = true; + c.toggles = taintWritesToggles(); + new MEnv(c); + } + } +} + +machine Test7aTaintAll { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 7; + c.cell = 7; + c.readerAlwaysFresh = true; + c.toggles = taintAllToggles(); + new MEnv(c); + } + } +} + +machine Test7bTaintAll { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 7; + c.cell = 7; + c.mutateBetweenSyncs = true; + c.toggles = taintAllToggles(); + new MEnv(c); + } + } +} + +// Expected RED (design findings, then the required fix residual): +test tc7a_P6R [main=Test7a]: assert P6R in (union Walker, { Test7a }); +test tc7b_P6R [main=Test7b]: assert P6R in (union Walker, { Test7b }); +test tc7bTaintW_P6R [main=Test7bTaintW]: assert P6R in (union Walker, { Test7bTaintW }); + +// Expected GREEN: +test tc7a_P1P2 [main=Test7a]: assert P1, P2 in (union Walker, { Test7a }); +test tc7c_All [main=Test7c]: assert P1, P2, P3prime, P6A, P6R in (union Walker, { Test7c }); +test tc7aTaintW_P6R [main=Test7aTaintW]: assert P6R in (union Walker, { Test7aTaintW }); +test tc7aTaintAll_P6R [main=Test7aTaintAll]: assert P6R in (union Walker, { Test7aTaintAll }); +test tc7bTaintAll_P6R [main=Test7bTaintAll]: assert P6R in (union Walker, { Test7bTaintAll }); diff --git a/formal/walker/PTst/Scenario8.p b/formal/walker/PTst/Scenario8.p new file mode 100644 index 000000000..2f55df099 --- /dev/null +++ b/formal/walker/PTst/Scenario8.p @@ -0,0 +1,142 @@ +/* Scenario 8 — external principals (SyncExternalResources x + crash-resume; the deleteStaleExternalPrincipals contract). One + sync, one external-phase action (cell 8): page 0 LISTs the source's + current answer and commits the reconciliation op; page 1 COPYs the + answer's principals. The external answer is the truthful epoch + table (e1 = {0,1}; e2 = {0} — the between-attempt shrink drops + principal 1). Committed copies are durable across crashes — the + debris premise — and a resumed phase restarts from its root token. + Expected verdicts: + - tc8green_P8 (no interruption, no mutation): GREEN — cold baseline. + - tc8crash_P8 (hard crash in attempt 1, shrink between attempts, + capable engine): GREEN — every crash placement heals: the resumed + attempt re-lists the current answer and reconciliation deletes + the dead attempt's stale copies before the fresh writes. Includes + the completed-then-crash schedules where attempt 2 seals attempt + 1's answer without re-running the phase — deliberately green + (sync-scoped freshness; the P8 seal clause compares against the + last-RUN list, not truth-at-seal). + - tc8stop_P8 (graceful stop + shrink, capable engine): GREEN — the + restart-from-root reset re-lists; no mid-phase cursor can copy a + fresh answer over a stale reconciliation. + - tc8reconOff_P8 (crash + shrink, NON-DELETING ENGINE): RED on + P8-EXT-STALE — the warn-and-continue degrade ships the dead + attempt's principal 1 in the sealed artifact (the SQLite + degradation pinned by + SQLiteExternalPrincipalResumeDegradesWithoutFailure). + - tc8staleList_P8 (crash + shrink, resume consumes the dead + attempt's answer): RED on P8-EXT-CURRENT — the recency mutant the + ResumeUsesCurrentExternalAnswer chaos pin forbids. + - tc8overDelete_P8 (no interruption; LATE over-deleting sweep): + RED on P8-EXT-MISSING — the over-deletion direction of the seal + clause, witnessed so the clause is calibrated in both directions + like P6-C and the Occult ext policy. The mutant deletes a live + principal at seal prep; it CANNOT be modeled in eExtReconReq + because the engine-ordered early pass runs before the page-1 + copy, which rewrites every listed id — early over-deletion is + structurally self-healing (see Store.p's extSweepMutant). */ + +machine Test8Green { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 8; + c.cell = 8; + c.nSyncs = 1; + new MEnv(c); + } + } +} + +machine Test8Crash { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 8; + c.cell = 8; + c.interrupt = 2; + c.interruptSync = 1; + c.nSyncs = 1; + c.mutateBetweenAttempts = true; + new MEnv(c); + } + } +} + +machine Test8Stop { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 8; + c.cell = 8; + c.interrupt = 1; + c.interruptSync = 1; + c.nSyncs = 1; + c.mutateBetweenAttempts = true; + new MEnv(c); + } + } +} + +machine Test8ReconOff { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 8; + c.cell = 8; + c.interrupt = 2; + c.interruptSync = 1; + c.nSyncs = 1; + c.mutateBetweenAttempts = true; + c.extRecon = false; + new MEnv(c); + } + } +} + +machine Test8OverDelete { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 8; + c.cell = 8; + c.nSyncs = 1; + c.extOverDelete = true; + new MEnv(c); + } + } +} + +machine Test8StaleList { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 8; + c.cell = 8; + c.interrupt = 2; + c.interruptSync = 1; + c.nSyncs = 1; + c.mutateBetweenAttempts = true; + c.extStaleList = true; + new MEnv(c); + } + } +} + +// Expected GREEN (the shipped capable-engine path heals every +// interruption placement): +test tc8green_P8 [main=Test8Green]: assert P8 in (union Walker, { Test8Green }); +test tc8crash_P8 [main=Test8Crash]: assert P8 in (union Walker, { Test8Crash }); +test tc8stop_P8 [main=Test8Stop]: assert P8 in (union Walker, { Test8Stop }); + +// Expected RED (the degrade path's debris; the recency mutant; the +// over-deleting late sweep): +test tc8reconOff_P8 [main=Test8ReconOff]: assert P8 in (union Walker, { Test8ReconOff }); +test tc8staleList_P8 [main=Test8StaleList]: assert P8 in (union Walker, { Test8StaleList }); +test tc8overDelete_P8 [main=Test8OverDelete]: assert P8 in (union Walker, { Test8OverDelete }); diff --git a/formal/walker/PTst/ScenarioP4.p b/formal/walker/PTst/ScenarioP4.p new file mode 100644 index 000000000..f690dc08c --- /dev/null +++ b/formal/walker/PTst/ScenarioP4.p @@ -0,0 +1,106 @@ +/* P4 tranche — progress (MODEL_SPEC 7 P4, 4 failure semantics). + Attempt-level loud failure is ON in these cells only + (cfg.loudColdFailsAttempt — decision 10's deviation closes here): + a cold verdict fails the ATTEMPT; the offending cursor is + checkpointed, so the failure recurs deterministically on resume. + + Stuck/ladder premise = the 5a drift script (cell 51, warmGate ON): + sync 2 attempt 1 strands carrier C; drift lands K2 and PERSISTS + (env latch); every resume dispatches C cold and fails loud at the + warm gate from byte-identical restored state. + - tcP4stuck_P4 (abandonLadder OFF): RED on P4-STUCK — attempts 2 + and 3 fail consecutively from identical restored checkpoint + state {[C@0], hits {S:V1}} at the same step (warm gate, cursor + 0) — CO-6b-004's deterministic re-failure / stuck-resume + finding. Budget exhaustion is the recorded outcome shape. + - tcP4ladder_All (abandonLadder ON, k = 2, 3 syncs): GREEN incl. + the P4Live liveness monitor — after 2 identical failures the env + abandons sync 2 UNSEALED; sync 3 starts against the last sealed + artifact (sync 1's), runs COLD (persisted K2 vs the K1 compat + record — the 6c ladder's "next sync runs cold"), fetches fresh, + and SEALS: the chain eventually seals, no wrong rows (P1's + config clause holds: K2 rows under a K2 seal). + + Leaked-lock premise = CO-6b-007 (cell 31, no interruption): the + carrier's replay page suffers ONE injected destination-write + failure after acquiring the scope lock; the worker retries + IN-ATTEMPT, re-entering the page sequence (syncOneAction's retry + loop). + - tcP4leak_P1 (release-on-error edge REMOVED — the mutation + check): RED as a checker DEADLOCK — the retry re-requests the + scope lock it still holds and waits forever on its own leak; the + attempt never seals and the env never unblocks. + - tcP4release_All (edge present, shipped): GREEN — the retry + re-acquires cleanly, the page completes, the sync seals. */ + +machine TestP4Stuck { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 8; + c.cell = 51; + c.interrupt = 1; + c.baseConfig = 1; + c.driftCompat = true; + c.loudColdFailsAttempt = true; + new MEnv(c); + } + } +} + +machine TestP4Ladder { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 8; + c.cell = 51; + c.interrupt = 1; + c.baseConfig = 1; + c.driftCompat = true; + c.loudColdFailsAttempt = true; + c.nSyncs = 3; + c.toggles = (warmGate = true, hitValidatorBinding = true, scopeLocks = true, oncePerScope = true, annotationBinding = false, abandonLadder = true, sessionTaintWrites = false, sessionTaintAll = false); + new MEnv(c); + } + } +} + +machine TestP4Leak { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 8; + c.cell = 31; + c.warmPageFails = true; + c.lockReleaseOnError = false; + new MEnv(c); + } + } +} + +machine TestP4Release { + start state I { + entry { + var c: tScenarioCfg; + c = defaultCfg(); + c.scenario = 8; + c.cell = 31; + c.warmPageFails = true; + new MEnv(c); + } + } +} + +// Expected RED (stuck-resume detection; then the leaked-lock DEADLOCK — +// the checker's deadlock report, no monitor involved): +test tcP4stuck_P4 [main=TestP4Stuck]: assert P4Stuck in (union Walker, { TestP4Stuck }); +test tcP4leak_P1 [main=TestP4Leak]: assert P1 in (union Walker, { TestP4Leak }); + +// Expected GREEN (P4Stuck is deliberately NOT asserted in the ladder +// cell: the ladder abandons exactly when detection fires — k = 2 IS +// the deterministic re-failure; the cell's claim is the recovery): +test tcP4ladder_All [main=TestP4Ladder]: assert P4Live, P1, P2 in (union Walker, { TestP4Ladder }); +test tcP4release_All [main=TestP4Release]: assert P1, P2 in (union Walker, { TestP4Release }); diff --git a/formal/walker/tools/sweep.sh b/formal/walker/tools/sweep.sh new file mode 100755 index 000000000..3c1cd76af --- /dev/null +++ b/formal/walker/tools/sweep.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# Full-cell regression sweep. Verdicts are audited by counterexample +# trace-file presence (NOT the "Found N bugs" tail — see the NOTE in +# CALIBRATION.md: the strategy portfolio's last block can report 0 +# bugs after an earlier block found one). p check's exit status gates +# only the GREEN side (a counterexample-free nonzero exit is +# CHECKER-ERROR — absence of a find from a checker that died proves +# nothing — while a found counterexample stands regardless of exit +# status). +# +# Usage: tools/sweep.sh [schedules] (run from formal/walker) +set -u +# The alarm-tag pipeline needs rg; without this guard a missing rg is +# swallowed into an empty tag, indistinguishable from an unrecognized +# monitor. (The graph scripts share tools/alarms.sh; this alternation +# is walker-specific and single-consumer, so it stays inline.) +command -v rg >/dev/null 2>&1 || { + echo "walker tools: rg (ripgrep) is required for alarm-tag extraction and is not on PATH" >&2 + exit 2 +} +MONITOR_ALTERNATION="P[0-9][0-9A-Z'-]*[A-Z]|SEAL-EXPECT|C1-PROBE|P4-STUCK|Deadlock detected|liveness" +S="${1:-10000}" +OUT="PCheckerOutput/sweep" +SUMMARY="$OUT/summary.txt" +mkdir -p "$OUT" +: > "$SUMMARY" + +# cell:expected (RED = counterexample expected, GREEN = none) +CELLS=" +tc1a1b_P1:RED +tc1a1b_P2:RED +tc1a1b_P3:RED +tc1bii_P1:RED +tc1c_P1:RED +tc1c_P1_probe:RED +tc1c_P2:RED +tc1c_P2_honest:GREEN +tcGreen_All:GREEN +tc2stop_P6A:RED +tc2crash_P6A:RED +tc2green_P6A:GREEN +tc2crash_P6C:RED +tc2clear_P6C:RED +tc2consistent_P6C:GREEN +tc3a_P1:RED +tc3a_P2:GREEN +tc3b_P1:RED +tc3bBindingOn_All:GREEN +tc3atomic_All:GREEN +tc4shipped_All:GREEN +tc4atomic_All:GREEN +tc4noOnce_P1:RED +tc4noLocks_P1:RED +tc5a_P1:RED +tc5a_Gate_All:GREEN +tc5b_Dropout_All:GREEN +tc5b_CrashWindow:RED +tc5c_C1Probe:RED +tc6naive_P1:RED +tc6atomic_All:GREEN +tc6atomicStop_All:GREEN +tc6overlayNaive_P1:RED +tc6overlayNaive_P2:RED +tc6overlayLast_P1:RED +tc6overlayMutO4_P1:RED +tc6overlay_All:GREEN +tc6overlayStop_All:GREEN +tc7a_P6R:RED +tc7a_P1P2:GREEN +tc7b_P6R:RED +tc7c_All:GREEN +tc7aTaintW_P6R:GREEN +tc7bTaintW_P6R:RED +tc7aTaintAll_P6R:GREEN +tc7bTaintAll_P6R:GREEN +tcP4stuck_P4:RED +tcP4ladder_All:GREEN +tcP4leak_P1:RED +tcP4release_All:GREEN +tc8green_P8:GREEN +tc8crash_P8:GREEN +tc8stop_P8:GREEN +tc8reconOff_P8:RED +tc8staleList_P8:RED +tc8overDelete_P8:RED +" + +mismatches=0 +total=0 +for entry in $CELLS; do + cell="${entry%%:*}" + expected="${entry##*:}" + total=$((total + 1)) + rm -rf "$OUT/$cell" + p check -tc "$cell" -s "$S" -o "$OUT/$cell" > "$OUT/$cell.log" 2>&1 + pstatus=$? + ce=$(ls "$OUT/$cell"/BugFinding/walker_[0-9]*_[0-9]*.txt 2>/dev/null | head -1) + # Verdict precedence: a counterexample is RED even if the checker + # then exited nonzero (the find stands); a counterexample-free + # nonzero exit is CHECKER-ERROR, not GREEN — "no bug found" from a + # checker that died is not evidence of anything. + if [ -n "$ce" ]; then observed="RED" + elif [ "$pstatus" -ne 0 ]; then observed="CHECKER-ERROR" + else observed="GREEN"; fi + mark="ok" + [ "$observed" = "$expected" ] || mark="MISMATCH" + detail="" + if [ "$observed" = "RED" ]; then + tag=$(rg -o "($MONITOR_ALTERNATION)" "$ce" | sort -u | paste -sd, -) + # An empty tag means the firing monitor is outside the alternation + # above — an untagged red is unauditable, so it is a mismatch even + # when RED was expected. Per-cell alarm enforcement stays with the + # graph bake-off (bakeoff.sh): walker sweep cells can legitimately + # red on more than one calibrated shape (tc3a_P1's two P1 clauses), + # so the comparison surface for WHICH monitor fired is + # CALIBRATION.md, not this script. + [ -n "$tag" ] || mark="MISMATCH" + detail=" [$tag]" + elif [ "$observed" = "CHECKER-ERROR" ]; then + detail=" (p exit $pstatus, see $OUT/$cell.log)" + fi + [ "$mark" = "ok" ] || mismatches=$((mismatches + 1)) + line="$cell expected=$expected observed=$observed $mark$detail" + echo "$line" | tee -a "$SUMMARY" +done +echo "SWEEP-DONE cells=$total mismatches=$mismatches" | tee -a "$SUMMARY" +# The exit status carries the verdict (the Makefile's formal targets +# rely on it): a drifted sweep must not read as a green make. +[ "$mismatches" -eq 0 ] diff --git a/formal/walker/traces/freeze-sweep-v11-summary.txt b/formal/walker/traces/freeze-sweep-v11-summary.txt new file mode 100644 index 000000000..987f9056d --- /dev/null +++ b/formal/walker/traces/freeze-sweep-v11-summary.txt @@ -0,0 +1,47 @@ +tc1a1b_P1 expected=RED observed=RED ok [P1-CONTENT] +tc1a1b_P2 expected=RED observed=RED ok [P2-STALENESS] +tc1a1b_P3 expected=RED observed=RED ok [P3'-COHERENCE] +tc1bii_P1 expected=RED observed=RED ok [P1-CONTENT] +tc1c_P1 expected=RED observed=RED ok [P1-CONTENT] +tc1c_P1_probe expected=RED observed=RED ok [P1-CONTENT] +tc1c_P2 expected=RED observed=RED ok [P2-STALENESS] +tc1c_P2_honest expected=GREEN observed=GREEN ok +tcGreen_All expected=GREEN observed=GREEN ok +tc2stop_P6A expected=RED observed=RED ok [P6-A,P6A] +tc2crash_P6A expected=RED observed=RED ok [P6-A,P6A] +tc2green_P6A expected=GREEN observed=GREEN ok +tc3a_P1 expected=RED observed=RED ok [P1-CONTENT] +tc3a_P2 expected=GREEN observed=GREEN ok +tc3b_P1 expected=RED observed=RED ok [P1-ATTEST-SEAL] +tc3bBindingOn_All expected=GREEN observed=GREEN ok +tc3atomic_All expected=GREEN observed=GREEN ok +tc4shipped_All expected=GREEN observed=GREEN ok +tc4atomic_All expected=GREEN observed=GREEN ok +tc4noOnce_P1 expected=RED observed=RED ok [P1-LEGALITY] +tc4noLocks_P1 expected=RED observed=RED ok [P1-LEGALITY] +tc5a_P1 expected=RED observed=RED ok [P1-CONFIG] +tc5a_Gate_All expected=GREEN observed=GREEN ok +tc5b_Dropout_All expected=GREEN observed=GREEN ok +tc5b_CrashWindow expected=RED observed=RED ok [SEAL-EXPECT] +tc5c_C1Probe expected=RED observed=RED ok [C1-PROBE] +tc6naive_P1 expected=RED observed=RED ok [P1-CONTENT] +tc6atomic_All expected=GREEN observed=GREEN ok +tc6atomicStop_All expected=GREEN observed=GREEN ok +tc6overlayNaive_P1 expected=RED observed=RED ok [P1-CONTENT] +tc6overlayNaive_P2 expected=RED observed=RED ok [P2-STALENESS] +tc6overlayLast_P1 expected=RED observed=RED ok [P1-CONTENT] +tc6overlay_All expected=GREEN observed=GREEN ok +tc6overlayStop_All expected=GREEN observed=GREEN ok +tc7a_P6R expected=RED observed=RED ok [P6-R,P6R] +tc7a_P1P2 expected=GREEN observed=GREEN ok +tc7b_P6R expected=RED observed=RED ok [P6-R,P6R] +tc7c_All expected=GREEN observed=GREEN ok +tc7aTaintW_P6R expected=GREEN observed=GREEN ok +tc7bTaintW_P6R expected=RED observed=RED ok [P6-R,P6R] +tc7aTaintAll_P6R expected=GREEN observed=GREEN ok +tc7bTaintAll_P6R expected=GREEN observed=GREEN ok +tcP4stuck_P4 expected=RED observed=RED ok [P4-STUCK,P4S] +tcP4ladder_All expected=GREEN observed=GREEN ok +tcP4leak_P1 expected=RED observed=RED ok [Deadlock detected,P4L] +tcP4release_All expected=GREEN observed=GREEN ok +SWEEP-DONE cells=46 mismatches=0 diff --git a/formal/walker/traces/msco001-sweep-summary.txt b/formal/walker/traces/msco001-sweep-summary.txt new file mode 100644 index 000000000..7f27e8e12 --- /dev/null +++ b/formal/walker/traces/msco001-sweep-summary.txt @@ -0,0 +1,48 @@ +tc1a1b_P1 expected=RED observed=RED ok [P1-CONTENT] +tc1a1b_P2 expected=RED observed=RED ok [P2-STALENESS] +tc1a1b_P3 expected=RED observed=RED ok [P3'-COHERENCE] +tc1bii_P1 expected=RED observed=RED ok [P1-CONTENT] +tc1c_P1 expected=RED observed=RED ok [P1-CONTENT] +tc1c_P1_probe expected=RED observed=RED ok [P1-CONTENT] +tc1c_P2 expected=RED observed=RED ok [P2-STALENESS] +tc1c_P2_honest expected=GREEN observed=GREEN ok +tcGreen_All expected=GREEN observed=GREEN ok +tc2stop_P6A expected=RED observed=RED ok [P6-A,P6A] +tc2crash_P6A expected=RED observed=RED ok [P6-A,P6A] +tc2green_P6A expected=GREEN observed=GREEN ok +tc3a_P1 expected=RED observed=RED ok [P1-CONTENT] +tc3a_P2 expected=GREEN observed=GREEN ok +tc3b_P1 expected=RED observed=RED ok [P1-ATTEST-SEAL] +tc3bBindingOn_All expected=GREEN observed=GREEN ok +tc3atomic_All expected=GREEN observed=GREEN ok +tc4shipped_All expected=GREEN observed=GREEN ok +tc4atomic_All expected=GREEN observed=GREEN ok +tc4noOnce_P1 expected=RED observed=RED ok [P1-LEGALITY] +tc4noLocks_P1 expected=RED observed=RED ok [P1-LEGALITY] +tc5a_P1 expected=RED observed=RED ok [P1-CONFIG] +tc5a_Gate_All expected=GREEN observed=GREEN ok +tc5b_Dropout_All expected=GREEN observed=GREEN ok +tc5b_CrashWindow expected=RED observed=RED ok [SEAL-EXPECT] +tc5c_C1Probe expected=RED observed=RED ok [C1-PROBE] +tc6naive_P1 expected=RED observed=RED ok [P1-CONTENT] +tc6atomic_All expected=GREEN observed=GREEN ok +tc6atomicStop_All expected=GREEN observed=GREEN ok +tc6overlayNaive_P1 expected=RED observed=RED ok [P1-CONTENT] +tc6overlayNaive_P2 expected=RED observed=RED ok [P2-STALENESS] +tc6overlayLast_P1 expected=RED observed=RED ok [P1-CONTENT] +tc6overlayMutO4_P1 expected=RED observed=RED ok [P1-CONTENT] +tc6overlay_All expected=GREEN observed=GREEN ok +tc6overlayStop_All expected=GREEN observed=GREEN ok +tc7a_P6R expected=RED observed=RED ok [P6-R,P6R] +tc7a_P1P2 expected=GREEN observed=GREEN ok +tc7b_P6R expected=RED observed=RED ok [P6-R,P6R] +tc7c_All expected=GREEN observed=GREEN ok +tc7aTaintW_P6R expected=GREEN observed=GREEN ok +tc7bTaintW_P6R expected=RED observed=RED ok [P6-R,P6R] +tc7aTaintAll_P6R expected=GREEN observed=GREEN ok +tc7bTaintAll_P6R expected=GREEN observed=GREEN ok +tcP4stuck_P4 expected=RED observed=RED ok [P4-STUCK,P4S] +tcP4ladder_All expected=GREEN observed=GREEN ok +tcP4leak_P1 expected=RED observed=RED ok [Deadlock detected,P4L] +tcP4release_All expected=GREEN observed=GREEN ok +SWEEP-DONE cells=47 mismatches=0 diff --git a/formal/walker/walker.pproj b/formal/walker/walker.pproj new file mode 100644 index 000000000..321cb3dab --- /dev/null +++ b/formal/walker/walker.pproj @@ -0,0 +1,9 @@ + +walker + + ./PSrc/ + ./PSpec/ + ./PTst/ + +./PGenerated +