diff --git a/.claude/board/ISSUES.md b/.claude/board/ISSUES.md index 435ba8e4e..7f7d4e27e 100644 --- a/.claude/board/ISSUES.md +++ b/.claude/board/ISSUES.md @@ -1,3 +1,19 @@ +## ISS-STREAMDTO-INGEST-WRITES-THE-SINGLETON-ONLY — perturbation ingress has no MailboxSoA arm (2026-10-06) + +**Status:** RESOLVED 2026-10-06 — `engine_bridge::ingest_codebook_indices_soa` writes a `StreamDto`'s fields into the owner's `MailboxSoA` through `write_row`, sharing the per-index encoding with the singleton arm (BindSpace is superseded by the SoA); the probe no longer mirrors. Residue: `CognitiveShaderBuilder` still requires a BindSpace even on the mailbox arm (W7). + +**Was:** OPEN. **Basis:** VERIFIED-IN-CODE + TEST-PINNED in `cognitive-shader-driver/examples/streamdto_circuit_probe.rs` (D-STREAMDTO-0). + +- `engine_bridge::ingest_codebook_indices` (fed by the lab serve/grpc wire DTOs; `StreamDto` itself has no production constructor) writes only the BindSpace singleton, which is RETIRE (W7). The driver's mailbox arm can only be fed by a manual mirror (the W2 differential pattern), which the probe uses through `write_row`. The write-back half of the circuit is closed (`ShaderDriver::mailbox_mut`); the ingress half is not. + +## ISS-NO-EVIDENCE-WRITER-FOR-EPISTEMIC-STATE — accumulated evidence never reaches what the next cycle may do (2026-10-06) + +**Status:** OPEN. **Basis:** TEST-PINNED in `cognitive-shader-driver/examples/ce64_cycle_survival_probe.rs` (D-CE64-TIME-0). + +- `CausalEdge64::learn` (the shipped fold) revises F/C and plasticity only; it never writes bits 59..63. The #1370 eligibility law reads only bits 59..63 and Pearl. So F/C accumulation, however long it survives, cannot change `EligibleRecipes`. The probe declares the missing step locally (code 3 ↔ 7); no production writer of bits 59..63 from evidence exists (the band writers live in probes only), and `CausalEdge64::learn` itself has no production caller. +- Cross-cycle source identity is not carried: the `SupportLedger` lives one cycle, and the register holds no source set, so two different sources seen in two different cycles never count as two (`sources_split_across_cycles_do_not_combine`). Carrying it would need state the register does not have. +- **What closes it:** a declared, versioned transition from evidence to EpistemicState5 in production (and a decision whether cross-cycle corroboration is wanted, and where its state would live). Not decided by the probe. + ## ISS-MAILBOX-ROUTES-WITNESS-WITHOUT-GRAPH — `MailboxSoA` accepts same-slot edges from any graph (2026-10-06) **Status:** RESOLVED 2026-10-06 as not a `MailboxSoA` defect (D-ALPHA-G-0, `alpha_world_provenance_probe`): the cross-graph delivery is constructed by bypassing the Alpha split tunnel. The world is selected upstream by `SpogTenants` (route to `graph_of(addr)`, `NoTenant` for an undeclared world); `MailboxSoA` works inside an already-selected world and stays graph-blind; `apply_edges` has no production caller. Neither closing option below is taken, and no G check goes into `apply_edges`. The residue moves upstream: the attention-to-edge join does not exist yet, and when it is built it must feed mailboxes from the tunnel's per-world output, as the probe does (OPEN in the D-ALPHA-G-0 entry). The bullets below record the issue as filed. **Basis:** TEST-PINNED in `cognitive-shader-driver/examples/spog_witness_probe.rs` diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index ea167735d..9a8f0cae1 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -37,8 +37,12 @@ Plan: `.claude/plans/2026-10-06-global-sudoku-replayable-orchestration-v1.md`. O | **D-GSO-7a** | P7a: the reasoning band as a relational certification (supersedes the meaning of D-GSO-7's rungs; #1360 unchanged) | Shipped (#1369) | `cognitive-shader-driver/examples/relational_certification_probe.rs`: a reading declared per class over bits 61..63 (`0 Open, 1 Associated, 2 Related, 3 Contributes, 4 CausalCandidate, 5 Causes`, 6..7 refuse; a class declared under the historical reading refuses the same bits); contracts are integer folds over unit masks + `causal_audit::SupportLedger` (2 distinct sources; the robustness mask only refutes; `Causes` = 2 distinct `InterventionBacked` sources + executed randomized arms); `entails` is an explicit table equal to `code >=` on 0..=5 only; repetition, observational effect size, the inference mantissa and confounded observational data never raise or demote past their contract; exhaustive 2,500-model family: 0 chain violations with scoped association, 10 with marginal (Simpson); sibling specificity and removal (overdetermination) are shown not to be rungs; only receipts recorded at or before the model's seal count, so a later receipt cannot change an earlier seal's replay; 13 tests, 12 disable runs red | | **D-GSO-AFF-0** | Affordance measurement: `CausalEdge64 × RecipeLaw[0..63] → EligibleRecipes: u64` | Shipped (#1370) | `cognitive-shader-driver/examples/affordance_measurement_probe.rs`: bits 59..63 read as one EpistemicState5 code (`spare() << 2 \| truth_raw()`, 10 declared codes, non-monotone, 22 refuse) + Pearl3; 7 recipes as `requires`/`forbids` facts + Pearl planes, compiled at build time to `[u64; 32]` × `[u64; 8]`; measure = 2 lookups + AND, 0 allocations; F1 determinism and law-generation dependence, F2 relevant factor flips its bit, F3 S/P/O/F/C/43..45/mantissa/plasticity/witness swept with no change, F4/F5 tables agree bit-for-bit with an independent rule re-derivation that names every refusal, F6/F7 preference only clears, half-field writes to 59..60 yield refusing codes; v1 / unknown provenance refuses (a v1 row with temporal 128 would read as `Causes`); no scheduler/task/capability object; 9 tests, 8 disable runs red | | **D-SPOG-W-0** | Witness slot as a SPOG sub-context under classid-G | Shipped (#1371) | `cognitive-shader-driver/examples/spog_witness_probe.rs`: SPOG view = (S/P/O blind bytes, G = `graph_of(classid)`, sub = `w_slot()` under a per-class declared reading, slot 0 = no anchor); undeclared class, another slot reading and v1 / unknown provenance refuse; G never moves with the slot; real `MailboxSoA::apply_edges` routing agrees with the reading on all 64 × 64 slot pairs within one graph and diverges across graphs (routing reads no classid; `ISS-MAILBOX-ROUTES-WITNESS-WITHOUT-GRAPH`, since resolved as a false positive by D-ALPHA-G-0: the cross-graph case bypasses the Alpha split tunnel); 0 allocations; 7 tests, 6 disable runs red | -| **D-ALPHA-G-0** | WorldG as Alpha-route provenance, not a CE64 field | In PR | `cognitive-shader-driver/examples/alpha_world_provenance_probe.rs`: event = (Alpha-attended address, CE64); WorldG = `graph_of(addr)`, order = `SpogTenants` route (`merge_in_claim_order`); same CE64 bits in two worlds = two events; replay of the same claims recovers the world sequence with all-zero edges; changing CE64 fields never moves the world while the events change; fed from the tunnel's per-world shadows each `MailboxSoA` holds only its own deliveries through the unchanged `apply_edges`; an undeclared world is refused at the tunnel (`NoTenant`). No production caller of `apply_edges` exists, so "no mixing" is absence of a path; 5 tests, 5 disable runs red | -| **D-WA-0** | Witness × Angle as a declared sparse local source coordinate | In PR | `cognitive-shader-driver/examples/witness_angle_probe.rs`: `(class, generation, w_slot, direction bits 43..45) → SourceCoord` under a probe-local sparse law (6 entries); same bits mean different sources in another class of the same world, in another world and in another generation; undeclared class, a class reading the bits as pathology triad or cohort `WitnessTable`, an undeclared generation, Witness 0 and v1/unknown provenance refuse; other CE64 fields never move the coordinate; world from the attended key, never the edge; 0 allocations; 7 tests, 8 disable runs red | +| **D-ALPHA-G-0** | WorldG as Alpha-route provenance, not a CE64 field | Shipped (#1372) | `cognitive-shader-driver/examples/alpha_world_provenance_probe.rs`: event = (Alpha-attended address, CE64); WorldG = `graph_of(addr)`, order = `SpogTenants` route (`merge_in_claim_order`); same CE64 bits in two worlds = two events; replay of the same claims recovers the world sequence with all-zero edges; changing CE64 fields never moves the world while the events change; fed from the tunnel's per-world shadows each `MailboxSoA` holds only its own deliveries through the unchanged `apply_edges`; an undeclared world is refused at the tunnel (`NoTenant`). No production caller of `apply_edges` exists, so "no mixing" is absence of a path; 5 tests, 5 disable runs red | +| **D-WA-0** | Witness × Angle as a declared sparse local source coordinate | Shipped (#1372) | `cognitive-shader-driver/examples/witness_angle_probe.rs`: `(class, generation, w_slot, direction bits 43..45) → SourceCoord` under a probe-local sparse law (6 entries); same bits mean different sources in another class of the same world, in another world and in another generation; undeclared class, a class reading the bits as pathology triad or cohort `WitnessTable`, an undeclared generation, Witness 0 and v1/unknown provenance refuse; other CE64 fields never move the coordinate; world from the attended key, never the edge; 0 allocations; 7 tests, 8 disable runs red | +| **D-CE64-TIME-0** | `CausalEdge64` as the register that survives a cycle | In PR | `cognitive-shader-driver/examples/ce64_cycle_survival_probe.rs`: one declared (`set_populated`) `MailboxSoA` `edges` row (cycle-gated `write_row` + `tick`, read back through `MailboxSoaView`) is the only cross-cycle state; folds = shipped `CausalEdge64::learn` (NARS revision of F/C) over a per-cycle `SupportLedger`; next-cycle reading = the #1370 law, moved unchanged to `examples/shared/affordance_law.rs`. Trajectory over cycles 0..3: eligible `OBS, OBS, OBS\|STRATIFY, OBS` (accumulation, certification read next cycle, contradiction read next cycle). The EpistemicState5 step (3 ↔ 7) is probe-declared: no production writer of bits 59..63 from evidence exists (`ISS-NO-EVIDENCE-WRITER-FOR-EPISTEMIC-STATE`). F1–F7 tests; 1000 folds from one source never certify; sources split across cycles never combine (missing state, pinned); an undeclared row is not a register. 10 tests, 9 disable runs red | +| **D-CE64-NEXTSTATE-0** | EpistemicState5 as a sparse next-state mutation of the CE64 register | In PR | `cognitive-shader-driver/examples/ce64_nextstate_probe.rs`: N1 only bits 59..63 move, all 32 codes on an edge with every field set; N2 written in cycle k through `MailboxSoA::write_row`, read in k+1 through `MailboxSoaView`; N3 the mailbox does not double-buffer (a same-cycle read sees the write), and next-cycle authority is a read discipline: start-of-cycle copy plus `last_write_cycle == cycle` treated as pending; N4 canonical LE round trip (bits 59..63 in byte 7, bits 3..7); N5 a fresh mailbox from the persisted word continues identically; N6 the shipped `with_spare` + `with_truth` compile to `bzhi; shl; or`, identical to the masked insert (LLVM folded the two), so no 5-bit writer is added. Production change: `CausalEdge64::to_le_bytes` / `from_le_bytes` (mirroring `EpisodicEdges64`) and the register contract on the type. 6 tests, 7 disable runs red | +| **D-ANCESTRY-K1-0** | StreamDto / PerturbationDto / BusDto / p64 / shader-driver ancestry vs code | Shipped (this PR) | `entries/2026-10-06-streamdto-circuit-ancestry.md`: v3 docs first, then code (git history is shallow at #1327). The k → k+1 loop was intended (`EmitMode::Persist`, `persist_cycle`, `mailbox-thoughtspace` read shim, W4a cast pairing) and left open: the driver owned its mailbox read-only. Path B | +| **D-STREAMDTO-0** | A real `StreamDto` through the shipped path to next-cycle #1370 eligibility | In PR | `cognitive-shader-driver/examples/streamdto_circuit_probe.rs`: `StreamDto` → the new `ingest_codebook_indices_soa` (straight into the SoA, cycle-gated `write_row`, encoding shared with the superseded BindSpace arm) → `dispatch` k → `persisted_row` gets `emitted_edges[0]` via the new `ShaderDriver::mailbox_mut` → `tick` → `dispatch` k+1 reads it and changes → `measure`. The edge steers k+1 only through `s_idx / 4`; the shader writes code 0, so eligibility is unchanged by the circuit (`0x20`, same as an unwritten word; code 7 gives `0x24`). Timestamp ≠ cycle; restart from LE bytes reproduces k+1. CI step added (feature-gated targets incl. `w2_differential` were never run). 6 tests + 1 lib test, 9 disable runs red | | **D-GSO-8** | P8: seal boundary | Shipped (#1363) | `lance-graph-planner/examples/seal_boundary_probe.rs`: internal events change only working rows; a declared `Boundary` event freezes the changed rows with the shipped `DetachedCycleBatch::freeze` into a local ledger (no `WalSink`); an unchanged boundary writes nothing; replaying the persisted events from each prior seal reproduces the next seal exactly (frame, landings, image, hash); wrong base, dropped event and swapped order each change the seal; tie limit handled by plan option 1: the key is the event's own durable unique `seq`, a tied batch is refused, and a test shows on the real `freeze` that tied keys make the seal arrival-dependent; 6 tests, 5 disable runs red. Follow-up: `apply` refuses a `seq` not greater than the last (a reuse on one row or across seals escaped `first_tie`), `resume_after` restores the top persisted key; 7 tests, 2 more disable runs red | ## D-PLX — Population-law cross-check (2026-10-03) diff --git a/.claude/board/SUPERSESSION-INDEX.md b/.claude/board/SUPERSESSION-INDEX.md index 6e599dcd4..d366af1ba 100644 --- a/.claude/board/SUPERSESSION-INDEX.md +++ b/.claude/board/SUPERSESSION-INDEX.md @@ -14,7 +14,7 @@ ## What this table says **`BindSpace` is the shape of the problem.** Marked RETIRE, and simultaneously the -most-referenced symbol here: **68 crate files, 48 plans, 42 of them blind.** +most-referenced symbol here: **69 crate files, 48 plans, 42 of them blind.** That is a programme, not a cleanup. **`GateState` is the sharpest case: 1 plan names it and *every one* is blind.** @@ -53,14 +53,14 @@ coverage instead. | `StepMask` | BLOCKED | — | 3 | 10 | 4 | | `commit_to_l4` | BLOCKED | — | 2 | 2 | 0 | | `dispatch_busdto` | BLOCKED | — | 3 | 8 | 5 | -| `persist_cycle` | BLOCKED | — | 11 | 8 | 5 | +| `persist_cycle` | BLOCKED | — | 12 | 8 | 5 | | `CognitiveMarkers` | REPURPOSE | `Commit` | 1 | 0 | 0 | | `DominoCascade` | REPURPOSE | `Commit` | 7 | 1 | 0 | | `GateDecision` | REPURPOSE | — | 25 | 28 | 25 | | `GateState` | REPURPOSE | — | 14 | 1 | 1 | | `MergeMode` | REPURPOSE | — | 8 | 14 | 13 | | `ResonanceDto` | REPURPOSE | `PerturbationDto` | 2 | 12 | 7 | -| `BindSpace` | RETIRE | — | 68 | 48 | 42 | +| `BindSpace` | RETIRE | — | 69 | 48 | 42 | | `CollapseGateEmission` | RETIRE | — | 5 | 14 | 12 | | `ThinkingStyle` | RETIRE-toward-contract | — | 53 | 28 | 24 | @@ -102,13 +102,13 @@ a licence to act on it. | **RESCOPE** | `soa-migration-diff-resolution-2026-06-13` | `BindSpace`, `CollapseGateEmission`, `GateDecision`, `MergeMode` … | — | 1/5 | | **RESCOPE** | `cognitive-substrate-convergence-v1` | `BindSpace`, `CollapseGateEmission`, `GateDecision`, `MergeMode` … | PROPOSAL (sprint-10 architectural decisions | 3/13 | | **RESCOPE** | `cognitive-substrate-convergence-v2` | `BindSpace`, `CollapseGateEmission`, `GateDecision`, `MergeMode` … | ACTIVE — sprint-11 Phase A/B COMPLETE (pendi | 4/15 | -| **RESCOPE** | `bindspace-singleton-to-mailbox-soa-v1` | `BindSpace`, `CollapseGateEmission`, `ResonanceDto`, `ThinkingStyle` | CONJECTURE / design (migration spec). NOT ye | 3/19 | +| **RESCOPE** | `bindspace-singleton-to-mailbox-soa-v1` | `BindSpace`, `CollapseGateEmission`, `ResonanceDto`, `ThinkingStyle` | CONJECTURE / design (migration spec). NOT ye | 4/19 | | **RESCOPE** | `callcenter-membrane-v1` | `BindSpace`, `GateDecision`, `MergeMode`, `ThinkingStyle` | Active | 0/0 | | **RESCOPE** | `causaledge64-mailbox-rename-soa-v1` | `BindSpace`, `GateDecision`, `MergeMode`, `ThinkingStyle` | Active (draft, 2026-05-14) | 1/10 | -| **RESCOPE** | `integrated-cognitive-planner-v1` | `BindSpace`, `GateDecision`, `ResonanceDto`, `dispatch_busdto` | — | 0/3 | +| **RESCOPE** | `integrated-cognitive-planner-v1` | `BindSpace`, `GateDecision`, `ResonanceDto`, `dispatch_busdto` | — | 1/3 | | **RESCOPE** | `palantir-parity-cascade-v2` | `BindSpace`, `MergeMode`, `ResonanceDto`, `ThinkingStyle` | plan, not implementation. | 0/17 | | **RESCOPE** | `temporal-markov-and-style-classes-v1` | `BindSpace`, `MergeMode`, `StepMask`, `ThinkingStyle` | ACTIVE (operator-ratified 2026-07-10: "other | 1/19 | -| **RESCOPE** | `unified-soa-convergence-v1` | `BindSpace`, `CollapseGateEmission`, `ResonanceDto`, `ThinkingStyle` | PROPOSAL / integration plan. Design-spec onl | 4/25 | +| **RESCOPE** | `unified-soa-convergence-v1` | `BindSpace`, `CollapseGateEmission`, `ResonanceDto`, `ThinkingStyle` | PROPOSAL / integration plan. Design-spec onl | 5/25 | | **RESCOPE** | `alpha-reason-witness-shader-field-archaeology-pass-1` | `BindSpace`, `MergeMode`, `ResonanceDto` | SOURCE AUDIT / PLAN ONLY. No production wiri | 0/1 | | **RESCOPE** | `bindspace-mailbox-soa-dependency-map-v1` | `BindSpace`, `dispatch_busdto`, `persist_cycle` | MAP / preflight. No source wired yet. Read-b | 0/2 | | **RESCOPE** | `bindspace-mailbox-soa-w3-w4a-impl-v1` | `BindSpace`, `dispatch_busdto`, `persist_cycle` | v2 — 5-consolidation + 3-brutal-critic pass | 0/1 | @@ -146,7 +146,7 @@ a licence to act on it. | **RESCOPE** | `lite-unified-surrealql-lance-v1` | `BindSpace` | CONJECTURE / design. **Test via feature gate | 0/0 | | **RESCOPE** | `ogit-cascade-supabase-callcenter-v1` | `BindSpace` | plan, not implementation. | 0/16 | | **RESCOPE** | `q2-foundry-integration-v1` | `BindSpace` | Proposed (2026-04-24) | 0/0 | -| **RESCOPE** | `reliability-checklist-arc-v1` | `ThinkingStyle` | PROPOSAL / possibility menu (2026-05-30). NO | 0/3 | +| **RESCOPE** | `reliability-checklist-arc-v1` | `ThinkingStyle` | PROPOSAL / possibility menu (2026-05-30). NO | 1/3 | | **RESCOPE** | `singleton-to-snapshot-nudge-v1` | `BindSpace` | PROPOSAL | 0/12 | | **RESCOPE** | `soa-value-tenant-migration-v1` | `BindSpace` | BRIEF (2026-06-24). This is NOT the migratio | 0/0 | | **RESCOPE** | `splat-native-ultrasound-v1` | `BindSpace` | PROPOSAL / integration plan. Design-spec onl | 0/17 | @@ -154,7 +154,7 @@ a licence to act on it. | **RESCOPE** | `super-domain-rbac-tenancy-v1` | `BindSpace` | Active | 0/41 | | **RESCOPE** | `thought-cycle-soa-awareness-integration-v1` | `BindSpace` | integration plan. No implementation claimed | 0/0 | | **RESCOPE** | `unified-ogit-architecture-v1` | `ThinkingStyle` | — | 0/0 | -| **RESCOPE** | `unified-soa-rubikon-integration-v1` | `BindSpace` | — | 0/8 | +| **RESCOPE** | `unified-soa-rubikon-integration-v1` | `BindSpace` | — | 1/8 | - **ARCHIVE?**: 0 - **RESCOPE**: 56 diff --git a/.claude/board/entries/2026-10-06-ce64-cycle-survival.md b/.claude/board/entries/2026-10-06-ce64-cycle-survival.md new file mode 100644 index 000000000..425459eec --- /dev/null +++ b/.claude/board/entries/2026-10-06-ce64-cycle-survival.md @@ -0,0 +1,56 @@ +# 2026-10-06 — D-CE64-TIME-0: `CausalEdge64` as the register that survives a cycle + +## VERIFIED-IN-CODE — inventory (main @ 7a2b0ef1) + +1. **Where cycle state survives.** `MailboxSoA` columns (`edges`, energy, + plasticity counters, …) across `tick()`; writes are cycle-gated by + `write_row(row, cycle, cell)` (`Accepted` / `Stale` / `Future`). Durable + persistence is Lance versions; the Alpha overlay is discardable per cycle. +2. **`StreamDto`** (`thinking-engine/src/dto.rs`) is sensor input: + `source`, `codebook_indices`, `timestamp`. It has no `now` field and carries + no `CausalEdge64`. The nearest "current surviving register image" is a + `MailboxSoA` `edges` row. +3. **Types carrying CE64 across cycles:** `MailboxSoA::edges` (`edge` / + `set_edge` / `write_row`); `bindspace::EdgeColumn` (`Box<[u64]>`). +4. **Created** everywhere via `pack` / `pack_v2`; **revised** by + `CausalEdge64::learn` and `forward` (`Revision` arm), and as u8 truths by + `NarsTables::revise`; **copied** through `edge` / `set_edge`; + **discarded:** every probe-local edge (`reasoning_band_probe`, + `relational_certification_probe` write local edges, not rows). +5. **Transient fold outputs:** #1370 `EligibleRecipes` (transient by design), + `SpogTenants` merges, `apply_edges` energy (reset by `consume_firing`). +6. **Folds that could feed F/C or EpistemicState5:** `learn` (F/C, plasticity), + `NarsTables::revise` / `deduce`, `ontology_warrant::Quorum`, + `causal_audit::SupportLedger` (distinct sources). +7. **Transition seam:** `edge(row)` → `learn` → `write_row(row, cycle, edge)` + → `tick()` → `edge(row)`. +8. **Edge-as-message vs register:** `apply_edges` reads an incoming CE64's + mantissa and confidence into energy and never revises the stored edge; the + `edges` column is register-shaped. +9. **Replay:** yes, from the seed register, the folds in order, and the law + generation (f32 revision is deterministic on one platform). +10. **Smallest real path:** one `MailboxSoA` row through the seam in 7. + `CausalEdge64::learn` has no production caller, and no production code + writes bits 59..63 from evidence. + +## MEASURED + +`ce64_cycle_survival_probe.rs`: 10 tests, 9 disable runs red (no write-back, +no `learn`, distinct-source condition removed, confidence bar removed, +downgrade removed, an irrelevant field leaking into revision, a ledger that +survives the cycle, no `set_populated`, no `n_rows` guard on the read). The +first ledger disable came back green because it recorded into a clone; redone +so the ledger really persists, it fails. After review (#1373) the register row +is declared (`set_populated`) and read through the production `MailboxSoaView` +lens, so the survival is of a live mailbox row, not of backing storage. +Trajectory: `f/c` 128/0 → 230/191 (code 3) → 230/213 (code 7) → 172/223 +(code 3); eligibility `OBS`, `OBS`, `OBS|STRATIFY`, `OBS`. + +The #1370 law moved unchanged to `examples/shared/affordance_law.rs`; its own +probe still passes 9 tests, and breaking the shared `measure` turns 2 of them +red. + +## OPEN + +`ISS-NO-EVIDENCE-WRITER-FOR-EPISTEMIC-STATE`: no production writer of bits +59..63 from evidence; cross-cycle source identity is not carried. diff --git a/.claude/board/entries/2026-10-06-ce64-nextstate-register.md b/.claude/board/entries/2026-10-06-ce64-nextstate-register.md new file mode 100644 index 000000000..bdb2fee1a --- /dev/null +++ b/.claude/board/entries/2026-10-06-ce64-nextstate-register.md @@ -0,0 +1,58 @@ +# 2026-10-06 — D-CE64-NEXTSTATE-0: the CE64 register has clean sparse next-state semantics + +## VERIFIED-IN-CODE — inventory + +1. **Physical representation.** `CausalEdge64(pub u64)`, `#[repr(transparent)]`; + `MailboxSoA::edges` is `[CausalEdge64; N]`, read zero-copy as `&[u64]` by + `MailboxSoaView::edges_raw`. A native `u64` from `pack_v2` through + `write_row`, `tick` and the view. +2. **Layout.** `causal_edge::layout`: S 0..7, P 8..15, O 16..23, F 24..31, + C 32..39, Pearl 40..42, direction 43..45, mantissa 46..49, plasticity + 50..52, W 53..58, truth 59..60, spare/band 61..63; a const assert covers all + 64 bits exactly once. Bit 0 = LSB. +3. **Endian contract.** None stated for the v2 edge before this change: it had + no `to_le_bytes` (only `CausalEdgeV3`, 12 bytes, and the sibling + `EpisodicEdges64` did), though `soa_envelope.rs` said it had. No production + path persists a v2 edge as bytes; no code reads it in host byte order + (`facet.rs` is the only `target_endian` guard, for facets). +4. **Masked update helpers.** `with_truth`, `with_topology`, `with_spare`, + `with_reasoning_band`, `with_routing` and the `set_*` twins are each one + mask and insert. No joint 5-bit writer exists; the #1370 law reads + `spare() << 2 | truth_raw()` and writes with `with_spare` + `with_truth`. +5. **Temporal contract.** `write_row` is cycle-gated (`Accepted` only when + `cycle == current_cycle`) and stamps `last_write_cycle[row]`; the write + lands in place. There is no double buffer, so "k vs k+1" is implicit, not + enforced. + +## MEASURED + +`ce64_nextstate_probe.rs`: 6 tests, 7 disable runs red (no clear, clearing W +too, no write, no stamp check, big-endian persist, lossy restore, big-endian +`CausalEdge64::to_le_bytes`). + +- **Codegen** (x86-64, release, `--emit asm`): the shipped two-writer path + is `bzhi rax, rdi, 59; shl rsi, 59; or rax, rsi; ret`, and the masked + insert compiled to the identical body; LLVM folded the two functions. +- **Same-cycle authority:** a plain read in cycle k already sees the write. A + committed-only read (refuse the row while `last_write_cycle == cycle`) + gives `None` until `tick`, but the pre-write value is gone; cycle k keeps + its start-of-cycle copy. + +## DECISION + +- **SCOPE:** no `with_epistemic_state`. The machine code is already the + minimal sparse update, and a fifth name over bits 59..63 (already read as + `truth`, `topology`, `spare` and `reasoning_band`) would add a lens, not + correctness. +- **Added:** `CausalEdge64::to_le_bytes` / `from_le_bytes`, mirroring + `EpisodicEdges64`, and the register contract (bit numbering, LE form, + sparse update, cycle semantics) on the type. +- **REVISIT WHEN:** a production writer of bits 59..63 lands (the gap in + `ISS-NO-EVIDENCE-WRITER-FOR-EPISTEMIC-STATE`) and needs a named joint + field, or a reader needs enforced next-cycle authority (double buffering). + +## OPEN + +- The evidence → EpistemicState5 certification step still has no production + writer (`ISS-NO-EVIDENCE-WRITER-FOR-EPISTEMIC-STATE`, unchanged). +- Next-cycle authority is not enforced by the mailbox. diff --git a/.claude/board/entries/2026-10-06-streamdto-circuit-ancestry.md b/.claude/board/entries/2026-10-06-streamdto-circuit-ancestry.md new file mode 100644 index 000000000..1ba2f3c0e --- /dev/null +++ b/.claude/board/entries/2026-10-06-streamdto-circuit-ancestry.md @@ -0,0 +1,67 @@ +# 2026-10-06 — D-ANCESTRY-K1-0 + D-STREAMDTO-0: the k → k+1 circuit was intended and left open at one seam + +## BASIS + +`.claude/v3/` first (`COMPONENT-MAP.md` :55-61, :80, :106-110; +`knowledge/v3-substrate-primer.md` §3; `MODULE-TABLE.md` 2026-07-10(2) +ancestry census; `soa_layout/tenants.md`, `routing.md`), then the code. +Git archaeology is not available: the clone is shallow and every symbol's +`git log -S` stops at the #1327 merge. Code and board only. + +## VERIFIED-IN-CODE — ancestry map (old → current) + +| v3 name | current carrier | status | +|---|---|---| +| Φ `StreamDto` (perturbation ingress) | `thinking_engine::dto::StreamDto`; its fields reach `engine_bridge::ingest_codebook_indices` from the lab serve/grpc wire DTOs | RETAIN. The struct has no production constructor; ingest writes the BindSpace singleton only | +| Ψ `PerturbationDto` | `dispatch_from_top_k` consumes `top_k` only | REUSE semantics; `energy` never reaches the driver (E-THE-PERTURBATION-FIELD-NEVER-REACHED-THE-MASK-ALU-1) | +| B `BusDto` (commit, `converged`/`cycle_count` = D-MBX-A6 outcome) | `dispatch_busdto` / `unbind_busdto` (singleton); `MailboxSoA::cast_on_behalf` (W4a, `with-planner`) | RETAIN | +| Γ `ThoughtStruct` | no consumer on the circuit | not evaluated | +| shader cycle | `ShaderDriver::run` (`&self`): reads each row's `CausalEdge64`, `s_idx` as cascade query; emits up to 8 CE64 in `ShaderBus` | RETAIN | +| commit row | `EmitMode::Persist` → `persisted_row = top_k[0].row`; contract doc still says "Commit to BindSpace via CollapseGate" | RETAIN; stale prose | +| write-back | `engine_bridge::persist_cycle` writes `emitted_edges[0]` into the singleton | SUPERSEDED for the mailbox arm (BindSpace = RETIRE W7) | +| substrate | `MailboxSoA<1024>` via the `mailbox-thoughtspace` read shim (`backing.rs`) | RETAIN | +| commit boundary | `MailboxSoA::write_row` (cycle-gated) + `tick` | RETAIN | +| emitted edge → mailbox row | none: the driver owned the mailbox read-only (`mailbox(&self)`), `run` is `&self` | MISSING → completed | +| `StreamDto` → SoA ingress | none (ingest wrote the BindSpace singleton, superseded by the SoA) | MISSING → completed: `ingest_codebook_indices_soa` (cycle-gated `write_row`, shared encoding) | + +## DECISION — Path B + +The circuit was intended (`EmitMode::Persist`, `persist_cycle`, the read +shim, W4a cast pairing) and never closed. Smallest completion, both +ends at the SoA: `ingest_codebook_indices_soa` (ingress) and +`ShaderDriver::mailbox_mut(&mut self, id)` (write-back). No new DTO, scheduler, or +temporal type; `StreamDto` untouched; #1370 law untouched. +REVISIT WHEN: W5 multi-mailbox routing or W7 BindSpace retirement moves +the persist call site. + +## MEASURED — `streamdto_circuit_probe.rs` + +6 tests, 9 disable runs red (no write, no tick, wrong row, BE restore, +temporal from cycle, eligibility from the uncommitted word, SoA encoding +drift, SoA ingest bypassing `write_row`, SoA ingest not growing +`populated`); plus a default-feature lib test of the SoA arm. + +- Cycle k: 8 hits, 8 emitted, row 4 persisted (`s_idx` 4). Cycle k+1 reads + it and its output changes; without the write k+1 repeats k. +- The edge's only read in `run` is `s_idx`, keyed on `s_idx / 4`. F/C are + revised and discarded (TD-INT-10, "observed only"); bits 59..63 unread. + A write-back to block 0, or to a block whose distance-0 self-hit carries + the same predicates, is invisible to k+1 (both hit while building the + probe). +- #1370 eligibility at k+1 reads the committed row and is unchanged by the + circuit: the shader stamps code 0, which under Pearl S grants `0x20`, the + same as an unwritten word. Code 7 in the same row gives `0x24`. +- `StreamDto::timestamp` lands in `temporal` as the caller's number; `tick` + never touches it; the dispatch's `cycle_index` is passed as `pack`'s v1 + `temporal`, which v2 drops. +- Restart: the committed word as LE bytes, restored into a fresh mailbox 7 + cycles later, gives the same k+1. +- Visibility does not wait for `tick` (no double buffer, see + D-CE64-NEXTSTATE-0); k/k+1 separation is the owner's `&mut` plus the + cycle gate. + +## OPEN + +- No production writer of bits 59..63 (`ISS-NO-EVIDENCE-WRITER-FOR-EPISTEMIC-STATE`), so the circuit cannot move eligibility. +- `CognitiveShaderBuilder` still requires a BindSpace on the mailbox arm (W7). +- `run` reads only `s_idx` from the edge; whether F/C/state should steer the cascade is undecided. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index ac581e4f4..1e33af0c5 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,11 +25,12 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -233 entries, 2026-08-06 .. 2026-10-06. +236 entries, 2026-08-06 .. 2026-10-06. | date | entry id | finding | file | |---|---|---|---| | 2026-10-06 | `v3-v4-dual-reading-round3` | | [2026-10-06-v3-v4-dual-reading-round3.md](2026-10-06-v3-v4-dual-reading-round3.md) | +| 2026-10-06 | `D-ANCESTRY-K1-0` | | [2026-10-06-streamdto-circuit-ancestry.md](2026-10-06-streamdto-circuit-ancestry.md) | | 2026-10-06 | `D-SPOG-W-0` | | [2026-10-06-spog-witness-sub-context.md](2026-10-06-spog-witness-sub-context.md) | | 2026-10-06 | `spofc-do-learn-as-compile-round6` | | [2026-10-06-spofc-do-learn-as-compile-round6.md](2026-10-06-spofc-do-learn-as-compile-round6.md) | | 2026-10-06 | `D-GSO-6` | | [2026-10-06-selector-revision-wired.md](2026-10-06-selector-revision-wired.md) | @@ -41,6 +42,8 @@ exactly one of them, never both. | 2026-10-06 | `diff-plateau-as-expression-round5` | | [2026-10-06-diff-plateau-as-expression-round5.md](2026-10-06-diff-plateau-as-expression-round5.md) | | 2026-10-06 | `dav-resident-reading-round2` | | [2026-10-06-dav-resident-reading-round2.md](2026-10-06-dav-resident-reading-round2.md) | | 2026-10-06 | `dav-carrier-invariance-quack` | | [2026-10-06-dav-carrier-invariance-quack.md](2026-10-06-dav-carrier-invariance-quack.md) | +| 2026-10-06 | `D-CE64-NEXTSTATE-0` | | [2026-10-06-ce64-nextstate-register.md](2026-10-06-ce64-nextstate-register.md) | +| 2026-10-06 | `D-CE64-TIME-0` | | [2026-10-06-ce64-cycle-survival.md](2026-10-06-ce64-cycle-survival.md) | | 2026-10-06 | `D-ALPHA-G-0` | | [2026-10-06-alpha-world-provenance-and-witness-angle.md](2026-10-06-alpha-world-provenance-and-witness-angle.md) | | 2026-10-06 | `D-GSO-AFF-0` | | [2026-10-06-affordance-measurement.md](2026-10-06-affordance-measurement.md) | | 2026-10-05 | `text-to-numeric-boundary-inventory` | | [2026-10-05-text-to-numeric-boundary-inventory.md](2026-10-05-text-to-numeric-boundary-inventory.md) | diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index cf08afe27..040dbdf29 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -438,6 +438,14 @@ jobs: run: cargo test --manifest-path crates/neural-debug/Cargo.toml - name: Run shader-driver tests (previously ungated) # 107 + 2 green run: cargo test --manifest-path crates/cognitive-shader-driver/Cargo.toml + # The default run above skips both targets: `w2_differential` is + # `cfg(feature = "mailbox-thoughtspace")` and the StreamDto circuit probe + # needs `with-engine` for `thinking_engine::dto::StreamDto`. + - name: Run shader-driver mailbox circuit (D-STREAMDTO-0) # 6 + 6 green + run: >- + cargo test --manifest-path crates/cognitive-shader-driver/Cargo.toml + --features with-engine,mailbox-thoughtspace + --example streamdto_circuit_probe --test w2_differential # lance-graph-benches carries NO tests — it is a benches-only crate with # one `harness = false` target (`graph_execution`). `cargo test` on it # runs zero tests and would be a gate that cannot fail; the meaningful diff --git a/crates/causal-edge/src/edge.rs b/crates/causal-edge/src/edge.rs index 4f2cfb924..7d9084f09 100644 --- a/crates/causal-edge/src/edge.rs +++ b/crates/causal-edge/src/edge.rs @@ -156,6 +156,24 @@ impl InferenceType { /// was already a bare `u64`; this only documents and compiler-enforces it for /// the reinterpret (a future second field becomes a compile error). Mirrors /// the ndarray twin `ndarray::hpc::causal_diff::CausalEdge64`. +/// +/// # Register contract (D-CE64-NEXTSTATE-0) +/// +/// - **Bit numbering:** bit 0 is the least significant bit of the `u64`; the +/// v2 fields are the `*_SHIFT` / `*_MASK` constants in [`crate::layout`]. +/// - **Byte order:** in memory the word is a native `u64`. Its canonical byte +/// form is [`CausalEdge64::to_le_bytes`] (bits 59..63 land in byte 7, bits +/// 3..7). Nothing in-tree persists a v2 edge as bytes today; a writer that +/// does must use this form. +/// - **Sparse update:** every `with_*` / `set_*` v2 writer is one mask and +/// insert on the word, and leaves every bit outside its field unchanged. +/// Writing bits 59..63 with `with_spare` + `with_truth` compiles to +/// `bzhi; shl; or` on x86-64 (no decode/repack). +/// - **Cycle semantics:** a write through `MailboxSoA::write_row` lands in +/// place and is visible at once; the mailbox does not double-buffer. A +/// state certified in cycle k is authoritative from cycle k+1 only by read +/// discipline: read the register once at cycle start, and treat a row +/// whose `last_write_cycle` equals the current cycle as pending. #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[repr(transparent)] pub struct CausalEdge64(pub u64); @@ -180,6 +198,22 @@ const BITS3_MASK: u64 = 0b111; const BITS12_MASK: u64 = 0xFFF; impl CausalEdge64 { + /// Canonical little-endian bytes of the word (the form a persisted or + /// transported edge uses; see the register contract on the type). + /// Mirrors `EpisodicEdges64::to_le_bytes`. + #[inline] + #[must_use] + pub const fn to_le_bytes(self) -> [u8; 8] { + self.0.to_le_bytes() + } + + /// Reconstruct from canonical little-endian bytes. + #[inline] + #[must_use] + pub const fn from_le_bytes(bytes: [u8; 8]) -> Self { + Self(u64::from_le_bytes(bytes)) + } + /// Zero edge: unknown, no evidence, no time. pub const ZERO: Self = Self(0); @@ -1215,6 +1249,27 @@ impl std::fmt::Debug for CausalEdge64 { mod tests { use super::*; + /// The canonical byte form is little-endian and round-trips every bit, + /// including bits 59..63 (byte 7, bits 3..7). + #[test] + fn le_bytes_round_trip_and_byte_order() { + for w in [ + 0x0123_4567_89AB_CDEF_u64, + 0xF800_0000_0000_0001, + 0x0800_0000_0000_0000, + u64::MAX ^ 1, + ] { + let b = CausalEdge64(w).to_le_bytes(); + assert_eq!(CausalEdge64::from_le_bytes(b).0, w); + assert_eq!(b[0], (w & 0xFF) as u8, "byte 0 holds bits 0..7"); + assert_eq!( + b[7] >> 3, + (w >> 59) as u8, + "byte 7 bits 3..7 hold bits 59..63" + ); + } + } + #[test] #[cfg(not(feature = "causal-edge-v2-layout"))] fn test_roundtrip() { diff --git a/crates/cognitive-shader-driver/Cargo.toml b/crates/cognitive-shader-driver/Cargo.toml index e17400d24..3e2bfe532 100644 --- a/crates/cognitive-shader-driver/Cargo.toml +++ b/crates/cognitive-shader-driver/Cargo.toml @@ -122,6 +122,21 @@ test = true name = "witness_angle_probe" test = true +# D-CE64-TIME-0 CausalEdge64 cycle-survival probe; tests run under `cargo test`. +[[example]] +name = "ce64_cycle_survival_probe" +test = true + +# D-CE64-NEXTSTATE-0 sparse next-state register probe; tests run under `cargo test`. +[[example]] +name = "ce64_nextstate_probe" +test = true + +[[example]] +name = "streamdto_circuit_probe" +test = true +required-features = ["with-engine", "mailbox-thoughtspace"] + [dependencies] lance-graph-contract = { path = "../lance-graph-contract" } # Read-only ontology registry (Phase 7, v4 plan). default-features = false so the diff --git a/crates/cognitive-shader-driver/examples/README.md b/crates/cognitive-shader-driver/examples/README.md index c782bcaa1..fb92a607d 100644 --- a/crates/cognitive-shader-driver/examples/README.md +++ b/crates/cognitive-shader-driver/examples/README.md @@ -258,6 +258,47 @@ The world comes from the attended key, never from the edge. cargo run -p cognitive-shader-driver --example witness_angle_probe ``` +## ce64_cycle_survival_probe.rs + +D-CE64-TIME-0. `CausalEdge64` as the register that survives a cycle. One +`MailboxSoA` `edges` row is the only cross-cycle state; each cycle folds its +observations with the shipped `CausalEdge64::learn`, counts distinct sources in +a per-cycle `SupportLedger`, settles the EpistemicState5 code (probe-declared +step 3 ↔ 7) and writes the register back. The next cycle measures eligibility +with the #1370 law (`shared/affordance_law.rs`). Eligibility over four cycles: +`OBSERVE`, `OBSERVE`, `OBSERVE|STRATIFY`, `OBSERVE`. + +```bash +cargo run -p cognitive-shader-driver --example ce64_cycle_survival_probe +``` + +## ce64_nextstate_probe.rs + +D-CE64-NEXTSTATE-0. EpistemicState5 (bits 59..63) as a sparse next-state +mutation of the `CausalEdge64` register: only those five bits move, the write +crosses the real `MailboxSoA` cycle seam and is read in cycle k+1, the word +survives the canonical little-endian round trip, and a fresh mailbox built +from the persisted word continues identically. Also pins that the mailbox +does not double-buffer: next-cycle authority is a read discipline. + +```bash +cargo run -p cognitive-shader-driver --example ce64_nextstate_probe +``` + +## streamdto_circuit_probe.rs + +D-STREAMDTO-0. A real `StreamDto` straight into the SoA (`ingest_codebook_indices_soa`), +the mailbox read shim, `dispatch` at cycle k, the persisted row's emitted edge +written through `ShaderDriver::mailbox_mut`, `tick`, and `dispatch` at k+1, +which reads it. Then the #1370 law on the committed row. Pins that the edge +steers the next cycle only through `s_idx / 4`, that the shader's code 0 +leaves eligibility where an unwritten word leaves it, and that the +`StreamDto` timestamp is not the cycle. + +```bash +cargo run -p cognitive-shader-driver --features with-engine,mailbox-thoughtspace --example streamdto_circuit_probe +``` + ## ewa_anisotropic_probe.rs D-CTX-3. Anisotropic EWA: the footprint is each virtual surfel's normalised diff --git a/crates/cognitive-shader-driver/examples/affordance_measurement_probe.rs b/crates/cognitive-shader-driver/examples/affordance_measurement_probe.rs index 20c0ba0c5..724e5a81a 100644 --- a/crates/cognitive-shader-driver/examples/affordance_measurement_probe.rs +++ b/crates/cognitive-shader-driver/examples/affordance_measurement_probe.rs @@ -111,18 +111,9 @@ fn counting(f: impl FnOnce() -> T) -> (T, usize) { (out, ALLOCATIONS.with(Cell::get) - before) } -// ── facts (the EpistemicState5 reading) ──────────────────────────────────── - -const DIRECT: u32 = 1 << 0; -const INDIRECT: u32 = 1 << 1; -const INTERMEDIATE_PRESENT: u32 = 1 << 2; -const INTERMEDIATE_UNKNOWN: u32 = 1 << 3; -const INTERMEDIATE_KNOWN: u32 = 1 << 4; -const OBSERVED: u32 = 1 << 5; -const ASSOCIATED: u32 = 1 << 6; -const RELATED: u32 = 1 << 7; -const SUPPORTS: u32 = 1 << 8; -const CAUSES: u32 = 1 << 9; +#[path = "shared/affordance_law.rs"] +mod affordance_law; +use affordance_law::*; const FACT_NAMES: [&str; 10] = [ "DIRECT", @@ -137,63 +128,6 @@ const FACT_NAMES: [&str; 10] = [ "CAUSES", ]; -const IND_UNKNOWN: u32 = INDIRECT | INTERMEDIATE_PRESENT | INTERMEDIATE_UNKNOWN; -const IND_KNOWN: u32 = INDIRECT | INTERMEDIATE_PRESENT | INTERMEDIATE_KNOWN; -const UP_TO_RELATED: u32 = ASSOCIATED | RELATED; -const UP_TO_SUPPORTS: u32 = UP_TO_RELATED | SUPPORTS; - -/// EpistemicState5 law: code → facts. Ten valid codes, deliberately not ordered -/// by strength (`Causes` sits at 1, `Open` at 0, `Related` at 20, …). -const EPI_LAW: [Option; 32] = { - let mut t = [None; 32]; - t[0] = Some(0); // Open - t[3] = Some(DIRECT | OBSERVED); - t[7] = Some(DIRECT | OBSERVED | ASSOCIATED); - t[12] = Some(DIRECT | ASSOCIATED); - t[20] = Some(DIRECT | UP_TO_RELATED); - t[25] = Some(IND_UNKNOWN | UP_TO_RELATED); - t[5] = Some(IND_KNOWN | UP_TO_RELATED); - t[30] = Some(IND_KNOWN | UP_TO_SUPPORTS); - t[1] = Some(DIRECT | UP_TO_SUPPORTS | CAUSES); - t[17] = Some(IND_KNOWN | UP_TO_SUPPORTS | CAUSES); - t -}; - -// ── the recipe law ────────────────────────────────────────────────────────── - -/// One recipe's static obligations. `pearl` names the planes that must -/// participate (S = 0b100, P = 0b010, O = 0b001). -#[derive(Clone, Copy)] -struct Rule { - active: bool, - requires: u32, - forbids: u32, - pearl: u8, -} - -const NOT_IN_LAW: Rule = Rule { - active: false, - requires: 0, - forbids: 0, - pearl: 0, -}; - -const fn rule(requires: u32, forbids: u32, pearl: u8) -> Rule { - Rule { - active: true, - requires, - forbids, - pearl, - } -} - -const HYDRATE_INTERMEDIATE: u8 = 0; -const MECHANISM_FOLD: u8 = 1; -const STRATIFY: u8 = 2; -const ROBUSTNESS_TEST: u8 = 3; -const CAUSAL_IDENTIFICATION: u8 = 4; -const OBSERVE_FOLD: u8 = 5; -const COUNTERFACTUAL_PROBE: u8 = 6; const RECIPE_NAMES: [&str; 7] = [ "HYDRATE_INTERMEDIATE", "MECHANISM_FOLD", @@ -204,129 +138,6 @@ const RECIPE_NAMES: [&str; 7] = [ "COUNTERFACTUAL_PROBE", ]; -const fn law(stratify_forbids: u32) -> [Rule; 64] { - let mut t = [NOT_IN_LAW; 64]; - t[HYDRATE_INTERMEDIATE as usize] = rule(INDIRECT | INTERMEDIATE_UNKNOWN, INTERMEDIATE_KNOWN, 0); - t[MECHANISM_FOLD as usize] = rule(INDIRECT | INTERMEDIATE_KNOWN, 0, 0); - t[STRATIFY as usize] = rule(ASSOCIATED, stratify_forbids, 0); - t[ROBUSTNESS_TEST as usize] = rule(RELATED, 0, 0); - t[CAUSAL_IDENTIFICATION as usize] = rule(SUPPORTS, INTERMEDIATE_UNKNOWN | CAUSES, 0); - t[OBSERVE_FOLD as usize] = rule(0, 0, 0); - t[COUNTERFACTUAL_PROBE as usize] = rule(CAUSES, 0, 0b111); - t -} - -/// The recipe law in force. A generation names the whole declared reading: -/// the EpistemicState5 codebook and the recipe rules together. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum LawGen { - V1, - /// V2 forbids `STRATIFY` once `CAUSES` is certified. - V2, -} - -const LAW_V1: [Rule; 64] = law(0); -const LAW_V2: [Rule; 64] = law(CAUSES); - -/// Compiled static affordances for one law generation. -struct Tables { - /// Per EpistemicState5 code: recipes whose fact obligations hold. Invalid - /// codes have no entry here; they refuse before lookup. - state: [u64; 32], - /// Per Pearl projection: recipes whose plane obligations hold. - pearl: [u64; 8], -} - -const fn compile(law: &[Rule; 64]) -> Tables { - let mut state = [0u64; 32]; - let mut code = 0; - while code < 32 { - if let Some(facts) = EPI_LAW[code] { - let mut r = 0; - while r < 64 { - let rl = law[r]; - if rl.active && facts & rl.requires == rl.requires && facts & rl.forbids == 0 { - state[code] |= 1 << r; - } - r += 1; - } - } - code += 1; - } - let mut pearl = [0u64; 8]; - let mut p = 0; - while p < 8 { - let mut r = 0; - while r < 64 { - let rl = law[r]; - if rl.active && (p as u8) & rl.pearl == rl.pearl { - pearl[p] |= 1 << r; - } - r += 1; - } - p += 1; - } - Tables { state, pearl } -} - -static TABLES_V1: Tables = compile(&LAW_V1); -static TABLES_V2: Tables = compile(&LAW_V2); - -impl LawGen { - fn tables(self) -> &'static Tables { - match self { - LawGen::V1 => &TABLES_V1, - LawGen::V2 => &TABLES_V2, - } - } - fn rules(self) -> &'static [Rule; 64] { - match self { - LawGen::V1 => &LAW_V1, - LawGen::V2 => &LAW_V2, - } - } -} - -// ── the measurement ───────────────────────────────────────────────────────── - -/// Why an edge cannot be measured under this law. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Refusal { - /// The caller did not assert that bits 59..63 were written under the v2 - /// layout. On a v1 row they are old `temporal` bits: `temporal = 128` - /// sets bit 59 and would read as code 1 (`Causes`). Same rule as - /// `band_reading`: v1 and unknown provenance refuse. - Provenance(EdgeProvenance), - /// Bits 59..63 do not form a code this law declares. - Undeclared(u8), -} - -/// Only an asserted v2-stamped edge or a clean V3 register is readable. -fn admitted(provenance: EdgeProvenance) -> bool { - matches!( - provenance, - EdgeProvenance::V2Stamped | EdgeProvenance::V3Register - ) -} - -/// Bits 59..63 as one code, through the shipped accessors. -fn raw5(edge: CausalEdge64) -> u8 { - (edge.spare() << 2) | edge.truth_raw() -} - -/// `CausalEdge64 × RecipeLaw → EligibleRecipes`. Two lookups and one AND. -fn measure(law: LawGen, edge: CausalEdge64, provenance: EdgeProvenance) -> Result { - if !admitted(provenance) { - return Err(Refusal::Provenance(provenance)); - } - let code = raw5(edge); - if EPI_LAW[code as usize].is_none() { - return Err(Refusal::Undeclared(code)); - } - let t = law.tables(); - Ok(t.state[code as usize] & t.pearl[edge.causal_mask() as usize]) -} - /// Preference may only remove legal recipes. fn prefer(eligible: u64, preference: u64) -> u64 { eligible & preference diff --git a/crates/cognitive-shader-driver/examples/ce64_cycle_survival_probe.rs b/crates/cognitive-shader-driver/examples/ce64_cycle_survival_probe.rs new file mode 100644 index 000000000..0ab6fbadb --- /dev/null +++ b/crates/cognitive-shader-driver/examples/ce64_cycle_survival_probe.rs @@ -0,0 +1,375 @@ +//! D-CE64-TIME-0: `CausalEdge64` as the register that survives a cycle. +//! +//! Claim under test: +//! +//! ```text +//! CE64_k ──folds of cycle k──▶ CE64_{k+1} ──▶ read by cycle k+1 +//! ``` +//! +//! A cycle's fold population is transient; only what the existing revision +//! writes into the register survives, and the next cycle measures its +//! affordances from that register alone. +//! +//! # Existing machinery used (unchanged) +//! +//! - **The surviving store:** one declared (`set_populated`) row of +//! `MailboxSoA`'s `edges` column, written through the cycle-gated +//! `write_row(row, cycle, cell)`, carried across `tick()`, and read back +//! through the production `MailboxSoaView` lens (`n_rows` + `edges_raw`). +//! Nothing else crosses a cycle boundary. +//! - **The fold:** `CausalEdge64::learn(observation, t)`, the shipped in-place +//! NARS revision: `f`/`c` merge by evidence weight, plasticity follows +//! confidence. Under the v2 layout it never touches bits 59..63. +//! - **The fold population:** `causal_audit::SupportLedger`, built fresh in +//! each cycle and dropped at its end; `distinct_sources_for` counts sources, +//! not receipts. +//! - **The next-cycle reading:** the D-GSO-AFF-0 recipe law (#1370), shared +//! unchanged through `shared/affordance_law.rs`: +//! `EligibleRecipes = STATE[raw5] & PEARL[pearl3]`. +//! +//! # The one probe-declared piece: the EpistemicState5 transition +//! +//! No production code writes bits 59..63 from evidence: `learn` moves only +//! F/C and plasticity, and the #1370 law reads only bits 59..63 and Pearl, so +//! F/C alone can never change eligibility. The missing step is declared here, +//! as small as possible, over the AFF-0 codebook: +//! +//! | from | to | when (after the cycle's folds) | +//! |---|---|---| +//! | 3 `DIRECT·OBSERVED` | 7 `+ASSOCIATED` | ≥ 2 distinct `DirectlyObserved` sources this cycle, `c ≥ 200`, `f ≥ 200` | +//! | 7 | 3 | `f < 180` (contradiction revised the carried evidence down) | +//! +//! Accumulation and certification stay separate: any number of folds from +//! one source moves F/C but never the code. Thresholds are policy pins. +//! +//! # What it does NOT prove +//! +//! - That this transition belongs in production; there is no production +//! writer of bits 59..63 from evidence. That gap is the finding. +//! - Cross-cycle source identity: the ledger dies with its cycle, so two +//! different sources seen in two different cycles never count as two. The +//! register cannot carry a source set; a test pins this as missing state. +//! - Scale: the population here is a handful of folds. `learn` is a +//! fixed-size, O(1) update per fold, so a larger population changes the +//! number of updates, not what crosses the boundary (one 64-bit word). +//! +//! Run: `cargo run -p cognitive-shader-driver --example ce64_cycle_survival_probe` +//! Tests: `cargo test -p cognitive-shader-driver --example ce64_cycle_survival_probe` + +#[path = "shared/affordance_law.rs"] +mod affordance_law; + +use affordance_law::{measure, raw5, LawGen}; +use causal_edge::edge::CausalEdge64; +use causal_edge::layout::TrustTexture; +use causal_edge::pearl::CausalMask; +use causal_edge::PlasticityState; +use cognitive_shader_driver::mailbox_soa::{MailboxSoA, WriteCell, WriteOutcome}; +use lance_graph_contract::band_reading::EdgeProvenance; +use lance_graph_contract::causal_audit::{ + EvidenceSourceId, SupportBasis, SupportLedger, SupportReceipt, +}; +use lance_graph_contract::scheduler::DatasetVersion; +use lance_graph_contract::soa_view::MailboxSoaView; + +/// The row of the mailbox that holds the register. +const ROW: usize = 0; + +/// EpistemicState5 codes of the AFF-0 law used here. +const CODE_OBSERVED: u8 = 3; +const CODE_ASSOCIATED: u8 = 7; + +/// Policy pins of the probe-declared transition. +const C_MIN: u8 = 200; +const F_HI: u8 = 200; +const F_LO: u8 = 180; + +/// One local contribution: a source reports `(f, c)` about the tracked +/// relation. `strength` and `at` are carried on the receipt but read by +/// nothing below, so they serve as irrelevant inputs. +#[derive(Debug, Clone, Copy)] +struct Fold { + source: u64, + f: u8, + c: u8, + strength: u8, + at: u64, +} + +const fn fold(source: u64, f: u8) -> Fold { + Fold { + source, + f, + c: 128, + strength: 1, + at: 1, + } +} + +/// Stamp an EpistemicState5 code through the shipped writers (bits 59..63). +fn with_code(edge: CausalEdge64, code: u8) -> CausalEdge64 { + edge.with_spare(code >> 2) + .with_truth(TrustTexture::from_bits_2(code & 0b11)) +} + +/// The tracked relation as an observation carrying `(f, c)`. S/P/O match the +/// register, so `learn` never re-targets it. +fn observation(f: u8, c: u8) -> CausalEdge64 { + CausalEdge64::pack_v2(1, 2, 3, f, c, CausalMask::SO, 0, PlasticityState::ALL_HOT) +} + +/// The register before any evidence: code `OBSERVED`, no confidence. +fn initial() -> CausalEdge64 { + with_code( + CausalEdge64::pack_v2(1, 2, 3, 128, 0, CausalMask::SO, 0, PlasticityState::ALL_HOT), + CODE_OBSERVED, + ) +} + +/// One cycle: fold the population into the register, then settle the code. +/// Takes the surviving register and this cycle's folds; nothing else. +fn cycle(register: CausalEdge64, folds: &[Fold]) -> CausalEdge64 { + let mut reg = register; + let mut ledger = SupportLedger::new(); + for x in folds { + reg.learn(observation(x.f, x.c), 0); + ledger.record(SupportReceipt { + basis: SupportBasis::DirectlyObserved, + source: EvidenceSourceId(x.source), + at: DatasetVersion(x.at), + strength: x.strength, + }); + } + let sources = ledger.distinct_sources_for(SupportBasis::DirectlyObserved); + let code = match raw5(reg) { + CODE_OBSERVED + if sources >= 2 && reg.confidence_u8() >= C_MIN && reg.frequency_u8() >= F_HI => + { + CODE_ASSOCIATED + } + CODE_ASSOCIATED if reg.frequency_u8() < F_LO => CODE_OBSERVED, + other => other, + }; + with_code(reg, code) + // `ledger` is dropped here: the population does not survive the cycle. +} + +/// What a cycle can legally do, measured from the register alone. +fn eligible(register: CausalEdge64) -> u64 { + measure(LawGen::V1, register, EdgeProvenance::V2Stamped).expect("declared code") +} + +/// The live register, read through the production `MailboxSoaView` lens. +/// `None` when the row is not a declared (populated) row of the mailbox, so +/// an undeclared row can never pass for surviving state. +fn read_register(view: &dyn MailboxSoaView) -> Option { + (ROW < view.n_rows()).then(|| CausalEdge64(view.edges_raw()[ROW])) +} + +/// A mailbox with the register row declared and seeded with `seed`. +fn mailbox_with(id: u32, seed: CausalEdge64) -> MailboxSoA<4> { + let mut mb: MailboxSoA<4> = MailboxSoA::new(id, 0, 0.5); + mb.set_populated(ROW + 1); + let cell = WriteCell { + edge: Some(seed), + ..WriteCell::default() + }; + assert_eq!(mb.write_row(ROW, mb.cycle(), &cell), WriteOutcome::Accepted); + mb +} + +/// Run a schedule of cycles through the mailbox row. With `persist == false` +/// the cycle's result is not written back (F1). +fn run(schedule: &[&[Fold]], persist: bool) -> (Vec, Vec) { + let mut mb = mailbox_with(1, initial()); + let mut registers = Vec::new(); + let mut eligibility = Vec::new(); + for folds in schedule { + let now = read_register(&mb).expect("declared register row"); + registers.push(now); + eligibility.push(eligible(now)); + let next = cycle(now, folds); + if persist { + let cell = WriteCell { + edge: Some(next), + ..WriteCell::default() + }; + assert_eq!(mb.write_row(ROW, mb.cycle(), &cell), WriteOutcome::Accepted); + } + mb.tick(); + } + let last = read_register(&mb).expect("declared register row"); + registers.push(last); + eligibility.push(eligible(last)); + (registers, eligibility) +} + +/// Cycle 0: one source, three times. Cycle 1: two new sources agree. +/// Cycle 2: two sources contradict. +const K0: &[Fold] = &[fold(1, 230), fold(1, 230), fold(1, 230)]; +const K1: &[Fold] = &[fold(2, 230), fold(3, 230)]; +const K2: &[Fold] = &[fold(4, 25), fold(5, 25)]; +const SCHEDULE: [&[Fold]; 3] = [K0, K1, K2]; + +fn names(e: u64) -> String { + let n = [ + "HYDRATE", + "MECHANISM", + "STRATIFY", + "ROBUSTNESS", + "CAUSAL_ID", + "OBSERVE", + "COUNTERFACTUAL", + ]; + (0..7) + .filter(|i| e & (1 << i) != 0) + .map(|i| n[i]) + .collect::>() + .join("|") +} + +fn main() { + let (regs, elig) = run(&SCHEDULE, true); + println!("D-CE64-TIME-0: the register survives the cycle"); + for (k, (r, e)) in regs.iter().zip(&elig).enumerate() { + println!( + " now_{k}: f={:>3} c={:>3} code={:>2} eligible={}", + r.frequency_u8(), + r.confidence_u8(), + raw5(*r), + names(*e) + ); + } +} + +#[cfg(test)] +mod tests { + use super::affordance_law::{OBSERVE_FOLD, STRATIFY}; + use super::*; + + const OBS: u64 = 1 << OBSERVE_FOLD; + const STRAT: u64 = 1 << STRATIFY; + + /// The trajectory: accumulation in cycle 0 changes nothing legal, the + /// certification in cycle 1 is read by cycle 2, and the contradiction in + /// cycle 2 is read by cycle 3. + #[test] + fn the_register_carries_the_result_across_three_boundaries() { + let (regs, elig) = run(&SCHEDULE, true); + assert_eq!(elig, vec![OBS, OBS, OBS | STRAT, OBS]); + // Cycle 0 moved F/C without moving the code. + assert!(regs[1].confidence_u8() > regs[0].confidence_u8()); + assert_eq!(raw5(regs[1]), CODE_OBSERVED); + // Cycle 1 certified association; cycle 2 revised it away. + assert_eq!(raw5(regs[2]), CODE_ASSOCIATED); + assert_eq!(raw5(regs[3]), CODE_OBSERVED); + assert!(regs[3].frequency_u8() < regs[2].frequency_u8()); + } + + /// F1: without the write-back each cycle starts from the seed, and the + /// accumulated result never reaches a later cycle. + #[test] + fn f1_no_survival_no_memory() { + let (_, kept) = run(&SCHEDULE, true); + let (regs, lost) = run(&SCHEDULE, false); + assert!( + regs.iter().all(|r| *r == initial()), + "every cycle saw the seed" + ); + assert_eq!(lost, vec![OBS; 4]); + assert_ne!(lost, kept); + } + + /// F2: the evidence carried out of cycle 0 is load-bearing for cycle 1. + /// Without it, cycle 1's two sources do not reach the confidence bar. + #[test] + fn f2_a_carried_contribution_matters() { + let (_, with) = run(&SCHEDULE, true); + let (_, without) = run(&[&[], K1, K2], true); + assert_eq!(with[2], OBS | STRAT); + assert_eq!(without[2], OBS, "cycle 2 never gains STRATIFY"); + // And one source standing in for another breaks the certification. + let (_, same_source) = run(&[K0, &[fold(2, 230), fold(2, 230)], K2], true); + assert_eq!(same_source[2], OBS); + } + + /// F3: inputs the transition does not read leave the surviving register + /// bit-identical. + #[test] + fn f3_irrelevant_contributions_stay_silent() { + let (base, _) = run(&SCHEDULE, true); + let noisy = |fs: &[Fold]| -> Vec { + fs.iter() + .enumerate() + .map(|(i, x)| Fold { + strength: 200 - i as u8, + at: 99 + i as u64, + ..*x + }) + .collect() + }; + let (a, b, c) = (noisy(K0), noisy(K1), noisy(K2)); + let (other, _) = run(&[&a, &b, &c], true); + assert_eq!(base, other); + } + + /// F4: a change in cycle k alters what cycle k+1 may legally do. + #[test] + fn f4_the_next_cycle_reads_the_change() { + let (_, elig) = run(&SCHEDULE, true); + assert_ne!(elig[1], elig[2], "cycle 1's certification reaches cycle 2"); + assert_ne!(elig[2], elig[3], "cycle 2's contradiction reaches cycle 3"); + } + + /// F5: the same seed, folds, order and law replay to the same registers. + #[test] + fn f5_replay_is_deterministic() { + assert_eq!(run(&SCHEDULE, true), run(&SCHEDULE, true)); + } + + /// F6 and F7: a fresh mailbox seeded with nothing but the surviving + /// register continues exactly like the full run. No earlier population, + /// event log or second object is needed. + #[test] + fn f6_f7_the_register_alone_continues_the_run() { + let (regs, _) = run(&SCHEDULE, true); + let mut fresh = mailbox_with(9, regs[2]); + fresh.tick(); + let now = read_register(&fresh).expect("declared register row"); + assert_eq!(cycle(now, K2), regs[3]); + } + + /// The register is only readable as a declared mailbox row: the same + /// bytes in an undeclared row are not surviving state. + #[test] + fn an_undeclared_row_is_not_a_register() { + let mut mb: MailboxSoA<4> = MailboxSoA::new(3, 0, 0.5); + let cell = WriteCell { + edge: Some(initial()), + ..WriteCell::default() + }; + assert_eq!(mb.write_row(ROW, mb.cycle(), &cell), WriteOutcome::Accepted); + assert_eq!(read_register(&mb), None, "written but not declared"); + mb.set_populated(ROW + 1); + assert_eq!(read_register(&mb), Some(initial())); + } + + /// Accumulation is not certification: a thousand folds from one source + /// saturate the evidence and leave the code where it was. + #[test] + fn repetition_from_one_source_never_certifies() { + let many: Vec = (0..1000).map(|_| fold(1, 230)).collect(); + let reg = cycle(initial(), &many); + assert!(reg.confidence_u8() >= C_MIN && reg.frequency_u8() >= F_HI); + assert_eq!(raw5(reg), CODE_OBSERVED); + assert_eq!(eligible(reg), OBS); + } + + /// Missing state, pinned: two different sources in two different cycles + /// never count as two, because the register carries no source set. + #[test] + fn sources_split_across_cycles_do_not_combine() { + let (_, elig) = run(&[K0, &[fold(2, 230)], &[fold(3, 230)]], true); + assert!(elig.iter().all(|&e| e == OBS), "{elig:?}"); + } +} diff --git a/crates/cognitive-shader-driver/examples/ce64_nextstate_probe.rs b/crates/cognitive-shader-driver/examples/ce64_nextstate_probe.rs new file mode 100644 index 000000000..5848d49e6 --- /dev/null +++ b/crates/cognitive-shader-driver/examples/ce64_nextstate_probe.rs @@ -0,0 +1,308 @@ +//! D-CE64-NEXTSTATE-0: EpistemicState5 as a sparse next-state mutation of the +//! `CausalEdge64` register. +//! +//! Claim under test: +//! +//! ```text +//! next = (current & !EPISTEMIC_MASK) | (code << 59) bits 59..63 +//! ``` +//! +//! Every other bit of the register is carried into cycle k+1 bit-identically, +//! the write crosses the real `MailboxSoA` cycle seam, and the word survives +//! a canonical little-endian round trip. +//! +//! # Bit numbering and byte order +//! +//! Logical bit 0 is the least significant bit of the `u64`. The canonical +//! persisted form is `u64::to_le_bytes`, so bits 59..63 sit in byte 7, bits +//! 3..7. Nothing here writes a byte: the update is a `u64` mask and insert. +//! +//! # Tests +//! +//! | test | what it pins | +//! |---|---| +//! | N1 | only bits 59..63 move, for every code on an edge with every field set | +//! | N2 | the certified state is read by the next cycle through the real mailbox | +//! | N3 | what the mailbox does and does not enforce about same-cycle authority | +//! | N4 | asymmetric words round-trip through canonical LE bytes | +//! | N5 | a fresh mailbox built from the persisted word continues identically | +//! | N6 | the existing two-writer path and the masked insert agree on all inputs | +//! +//! # Codegen (N6, measured, not a test) +//! +//! `cargo rustc --release --example ce64_nextstate_probe -- --emit asm` on +//! x86-64: `via_two_writers` (the shipped `with_spare` + `with_truth`) is +//! `bzhi rax, rdi, 59; shl rsi, 59; or rax, rsi; ret`, i.e. clear bits +//! 59..63 and insert. `via_mask` compiled to the identical body and LLVM +//! folded the two into one symbol. The shipped writers do not force a +//! decode/repack, so this probe adds no production writer for the 5-bit +//! field: it would be a fourth name over bits that already have four lenses +//! (`truth`, `topology`, `spare`, `reasoning_band`) and would not change the +//! machine code. +//! +//! Run: `cargo run -p cognitive-shader-driver --example ce64_nextstate_probe` +//! Tests: `cargo test -p cognitive-shader-driver --example ce64_nextstate_probe` + +#[path = "shared/affordance_law.rs"] +mod affordance_law; + +use affordance_law::{measure, raw5, LawGen}; +use causal_edge::edge::CausalEdge64; +use causal_edge::layout::{TrustTexture, SPARE_MASK, TRUTH_MASK, TRUTH_SHIFT}; +use causal_edge::pearl::CausalMask; +use causal_edge::PlasticityState; +use cognitive_shader_driver::mailbox_soa::{MailboxSoA, WriteCell, WriteOutcome}; +use lance_graph_contract::band_reading::EdgeProvenance; +use lance_graph_contract::soa_view::MailboxSoaView; + +/// Bits 59..63. +const EPISTEMIC_MASK: u64 = TRUTH_MASK | SPARE_MASK; + +/// The register row. +const ROW: usize = 0; + +const CODE_OBSERVED: u8 = 3; +const CODE_ASSOCIATED: u8 = 7; + +/// The existing field-update path: two shipped writers, one per sub-field. +#[inline(never)] +fn via_two_writers(edge: CausalEdge64, code: u8) -> CausalEdge64 { + edge.with_spare(code >> 2) + .with_truth(TrustTexture::from_bits_2(code & 0b11)) +} + +/// The minimal sparse update: one mask and one insert on the `u64`. +#[inline(never)] +fn via_mask(edge: CausalEdge64, code: u8) -> CausalEdge64 { + CausalEdge64((edge.0 & !EPISTEMIC_MASK) | ((u64::from(code) & 0x1F) << TRUTH_SHIFT)) +} + +/// An edge with every field non-zero, including bits 59..63. +fn busy() -> CausalEdge64 { + CausalEdge64::pack_v2( + 0xA5, + 0x3C, + 0x7E, + 201, + 149, + CausalMask::SO, + 0b101, + PlasticityState::from_bits(0b011), + ) + .with_inference_mantissa(-6) + .with_w_slot(41) + .with_spare(0b101) + .with_truth(TrustTexture::from_bits_2(0b10)) +} + +/// The register read through the production view; `None` outside the +/// declared rows. +fn read(view: &dyn MailboxSoaView) -> Option { + (ROW < view.n_rows()).then(|| CausalEdge64(view.edges_raw()[ROW])) +} + +/// The register as committed by an earlier cycle: `None` while the row was +/// written in the current cycle (its `last_write_cycle` equals `cycle`). +fn committed(mb: &MailboxSoA<4>) -> Option { + (mb.last_write_cycle_at(ROW) != mb.cycle()) + .then(|| read(mb)) + .flatten() +} + +/// A mailbox with the register row declared and seeded, already ticked so +/// the seed is committed. +fn mailbox(seed: CausalEdge64) -> MailboxSoA<4> { + let mut mb: MailboxSoA<4> = MailboxSoA::new(1, 0, 0.5); + mb.set_populated(ROW + 1); + write(&mut mb, seed); + mb.tick(); + mb +} + +fn write(mb: &mut MailboxSoA<4>, edge: CausalEdge64) { + let cell = WriteCell { + edge: Some(edge), + ..WriteCell::default() + }; + assert_eq!(mb.write_row(ROW, mb.cycle(), &cell), WriteOutcome::Accepted); +} + +fn eligible(edge: CausalEdge64) -> u64 { + measure(LawGen::V1, edge, EdgeProvenance::V2Stamped).expect("declared code") +} + +/// The canonical persisted form of one register (the shipped +/// `CausalEdge64::to_le_bytes`). +fn persist(edge: CausalEdge64) -> [u8; 8] { + edge.to_le_bytes() +} + +fn restore(bytes: [u8; 8]) -> CausalEdge64 { + CausalEdge64::from_le_bytes(bytes) +} + +fn main() { + let start = via_mask(busy(), CODE_OBSERVED); + let mut mb = mailbox(start); + let now = committed(&mb).expect("seed committed"); + let next = via_mask(now, CODE_ASSOCIATED); + write(&mut mb, next); + println!("D-CE64-NEXTSTATE-0"); + println!( + " cycle k word {:016x} code {} eligible {:07b}", + now.0, + raw5(now), + eligible(now) + ); + println!( + " written word {:016x} changed bits {:016x}", + next.0, + now.0 ^ next.0 + ); + println!( + " pending in cycle k: committed() = {:?}", + committed(&mb).map(|e| e.0) + ); + mb.tick(); + let k1 = committed(&mb).expect("committed after tick"); + println!(" cycle k+1 word {:016x} code {}", k1.0, raw5(k1)); + println!(" persisted LE {:02x?}", persist(k1)); + assert_eq!(restore(persist(k1)), k1); + // Both update paths, kept out of the optimiser's reach so their code can + // be compared (`--emit asm`). + let (e, c) = ( + std::hint::black_box(k1), + std::hint::black_box(CODE_OBSERVED), + ); + println!( + " two writers == mask: {}", + via_two_writers(e, c) == via_mask(e, c) + ); +} + +#[cfg(test)] +mod tests { + use super::affordance_law::{EPI_LAW, OBSERVE_FOLD, STRATIFY}; + use super::*; + + const OBS: u64 = 1 << OBSERVE_FOLD; + const STRAT: u64 = 1 << STRATIFY; + + /// N1: setting any code moves no bit outside 59..63. + #[test] + fn n1_only_bits_59_to_63_move() { + let cur = busy(); + assert!(cur.0 & !EPISTEMIC_MASK != 0 && cur.0 & EPISTEMIC_MASK != 0); + let mut moved = 0; + for code in 0u8..32 { + let next = via_mask(cur, code); + assert_eq!((cur.0 ^ next.0) & !EPISTEMIC_MASK, 0, "code {code}"); + assert_eq!(raw5(next), code); + if next != cur { + moved += 1; + } + } + assert_eq!(moved, 31, "every other code changes the word"); + } + + /// N2: written in cycle k through the real seam, read in cycle k+1. + #[test] + fn n2_the_next_cycle_reads_the_certified_state() { + let mut mb = mailbox(via_mask(busy(), CODE_OBSERVED)); + let now = committed(&mb).unwrap(); + assert_eq!(eligible(now), OBS); + let next = via_mask(now, CODE_ASSOCIATED); + write(&mut mb, next); + mb.tick(); + let k1 = committed(&mb).unwrap(); + assert_eq!(k1, next); + assert_eq!(eligible(k1), OBS | STRAT); + assert_eq!((now.0 ^ k1.0) & !EPISTEMIC_MASK, 0); + } + + /// N3: the mailbox does not double-buffer. A write in cycle k lands in + /// place and a plain read sees it at once; the only distinction the + /// mailbox keeps is the write stamp, which marks the row as written this + /// cycle. A committed-only read therefore refuses the row until `tick`, + /// but the pre-write value is gone; a cycle that needs it must keep the + /// copy it read at cycle start. + #[test] + fn n3_same_cycle_authority_is_a_read_discipline() { + let mut mb = mailbox(via_mask(busy(), CODE_OBSERVED)); + let at_start = committed(&mb).unwrap(); + let eligible_k = eligible(at_start); + write(&mut mb, via_mask(at_start, CODE_ASSOCIATED)); + // Not enforced: the plain read already sees the new state. + assert_eq!(raw5(read(&mb).unwrap()), CODE_ASSOCIATED); + // The stamp marks it pending; a committed-only read refuses it. + assert_eq!(committed(&mb), None); + // Cycle k's eligibility came from the start-of-cycle copy and stays. + assert_eq!(eligible(at_start), eligible_k); + assert_eq!(eligible_k, OBS); + mb.tick(); + assert_eq!(raw5(committed(&mb).unwrap()), CODE_ASSOCIATED); + } + + /// N4: asymmetric words survive the canonical LE round trip, and bits + /// 59..63 sit in byte 7, bits 3..7. + #[test] + fn n4_canonical_le_round_trip() { + let words = [ + 0x0123_4567_89AB_CDEF_u64, + 0xF800_0000_0000_0001, + 0x0800_0000_0000_0000, + 0x8000_0000_0000_0000, + 0xA5C3_0F1E_2D3C_4B5A, + u64::MAX ^ 1, + busy().0, + ]; + for w in words { + let bytes = persist(CausalEdge64(w)); + assert_eq!(restore(bytes).0, w, "{w:016x}"); + assert_eq!(bytes[0], (w & 0xFF) as u8, "byte 0 is the low byte"); + assert_eq!(bytes[7] >> 3, ((w & EPISTEMIC_MASK) >> TRUTH_SHIFT) as u8); + } + } + + /// N5: a fresh mailbox seeded only from the persisted word continues + /// exactly like the original; no evidence is replayed. + #[test] + fn n5_restart_from_the_persisted_word() { + let mut mb = mailbox(via_mask(busy(), CODE_OBSERVED)); + let now = committed(&mb).unwrap(); + write(&mut mb, via_mask(now, CODE_ASSOCIATED)); + mb.tick(); + let original = committed(&mb).unwrap(); + let fresh = mailbox(restore(persist(original))); + let restarted = committed(&fresh).unwrap(); + assert_eq!(restarted, original); + assert_eq!(eligible(restarted), eligible(original)); + assert_eq!(raw5(restarted), CODE_ASSOCIATED); + } + + /// N6: the existing two-writer path and the masked insert produce the + /// same word for every code on varied edges, so the shipped API does not + /// force a decode/repack to get the sparse update. + #[test] + fn n6_existing_path_equals_the_masked_insert() { + let edges = [ + CausalEdge64::ZERO, + busy(), + CausalEdge64(u64::MAX), + CausalEdge64(0xA5C3_0F1E_2D3C_4B5A), + ]; + for e in edges { + for code in 0u8..32 { + assert_eq!( + via_two_writers(e, code), + via_mask(e, code), + "{:016x} {code}", + e.0 + ); + } + } + // Every declared code of the #1370 law is reachable this way. + let declared = (0u8..32).filter(|c| EPI_LAW[*c as usize].is_some()).count(); + assert_eq!(declared, 10); + } +} diff --git a/crates/cognitive-shader-driver/examples/shared/affordance_law.rs b/crates/cognitive-shader-driver/examples/shared/affordance_law.rs new file mode 100644 index 000000000..938ade46f --- /dev/null +++ b/crates/cognitive-shader-driver/examples/shared/affordance_law.rs @@ -0,0 +1,208 @@ +//! The D-GSO-AFF-0 recipe law (merged in #1370), shared by the examples that +//! measure eligibility: `affordance_measurement_probe` (where it is tested) +//! and `ce64_cycle_survival_probe` (which measures eligibility across cycles +//! with the identical law instead of a copy). +//! +//! Moved here unchanged. Each including example uses a subset, hence the +//! `dead_code` allowance. +#![allow(dead_code)] + +use causal_edge::edge::CausalEdge64; +use lance_graph_contract::band_reading::EdgeProvenance; + +// ── facts (the EpistemicState5 reading) ──────────────────────────────────── + +pub const DIRECT: u32 = 1 << 0; +pub const INDIRECT: u32 = 1 << 1; +pub const INTERMEDIATE_PRESENT: u32 = 1 << 2; +pub const INTERMEDIATE_UNKNOWN: u32 = 1 << 3; +pub const INTERMEDIATE_KNOWN: u32 = 1 << 4; +pub const OBSERVED: u32 = 1 << 5; +pub const ASSOCIATED: u32 = 1 << 6; +pub const RELATED: u32 = 1 << 7; +pub const SUPPORTS: u32 = 1 << 8; +pub const CAUSES: u32 = 1 << 9; + +pub const IND_UNKNOWN: u32 = INDIRECT | INTERMEDIATE_PRESENT | INTERMEDIATE_UNKNOWN; +pub const IND_KNOWN: u32 = INDIRECT | INTERMEDIATE_PRESENT | INTERMEDIATE_KNOWN; +pub const UP_TO_RELATED: u32 = ASSOCIATED | RELATED; +pub const UP_TO_SUPPORTS: u32 = UP_TO_RELATED | SUPPORTS; + +/// EpistemicState5 law: code → facts. Ten valid codes, deliberately not ordered +/// by strength (`Causes` sits at 1, `Open` at 0, `Related` at 20, …). +pub const EPI_LAW: [Option; 32] = { + let mut t = [None; 32]; + t[0] = Some(0); // Open + t[3] = Some(DIRECT | OBSERVED); + t[7] = Some(DIRECT | OBSERVED | ASSOCIATED); + t[12] = Some(DIRECT | ASSOCIATED); + t[20] = Some(DIRECT | UP_TO_RELATED); + t[25] = Some(IND_UNKNOWN | UP_TO_RELATED); + t[5] = Some(IND_KNOWN | UP_TO_RELATED); + t[30] = Some(IND_KNOWN | UP_TO_SUPPORTS); + t[1] = Some(DIRECT | UP_TO_SUPPORTS | CAUSES); + t[17] = Some(IND_KNOWN | UP_TO_SUPPORTS | CAUSES); + t +}; + +// ── the recipe law ────────────────────────────────────────────────────────── + +/// One recipe's static obligations. `pearl` names the planes that must +/// participate (S = 0b100, P = 0b010, O = 0b001). +#[derive(Clone, Copy)] +pub struct Rule { + pub active: bool, + pub requires: u32, + pub forbids: u32, + pub pearl: u8, +} + +pub const NOT_IN_LAW: Rule = Rule { + active: false, + requires: 0, + forbids: 0, + pearl: 0, +}; + +pub const fn rule(requires: u32, forbids: u32, pearl: u8) -> Rule { + Rule { + active: true, + requires, + forbids, + pearl, + } +} + +pub const HYDRATE_INTERMEDIATE: u8 = 0; +pub const MECHANISM_FOLD: u8 = 1; +pub const STRATIFY: u8 = 2; +pub const ROBUSTNESS_TEST: u8 = 3; +pub const CAUSAL_IDENTIFICATION: u8 = 4; +pub const OBSERVE_FOLD: u8 = 5; +pub const COUNTERFACTUAL_PROBE: u8 = 6; +pub const fn law(stratify_forbids: u32) -> [Rule; 64] { + let mut t = [NOT_IN_LAW; 64]; + t[HYDRATE_INTERMEDIATE as usize] = rule(INDIRECT | INTERMEDIATE_UNKNOWN, INTERMEDIATE_KNOWN, 0); + t[MECHANISM_FOLD as usize] = rule(INDIRECT | INTERMEDIATE_KNOWN, 0, 0); + t[STRATIFY as usize] = rule(ASSOCIATED, stratify_forbids, 0); + t[ROBUSTNESS_TEST as usize] = rule(RELATED, 0, 0); + t[CAUSAL_IDENTIFICATION as usize] = rule(SUPPORTS, INTERMEDIATE_UNKNOWN | CAUSES, 0); + t[OBSERVE_FOLD as usize] = rule(0, 0, 0); + t[COUNTERFACTUAL_PROBE as usize] = rule(CAUSES, 0, 0b111); + t +} + +/// The recipe law in force. A generation names the whole declared reading: +/// the EpistemicState5 codebook and the recipe rules together. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LawGen { + V1, + /// V2 forbids `STRATIFY` once `CAUSES` is certified. + V2, +} + +pub const LAW_V1: [Rule; 64] = law(0); +pub const LAW_V2: [Rule; 64] = law(CAUSES); + +/// Compiled static affordances for one law generation. +pub struct Tables { + /// Per EpistemicState5 code: recipes whose fact obligations hold. Invalid + /// codes have no entry here; they refuse before lookup. + pub state: [u64; 32], + /// Per Pearl projection: recipes whose plane obligations hold. + pub pearl: [u64; 8], +} + +pub const fn compile(law: &[Rule; 64]) -> Tables { + let mut state = [0u64; 32]; + let mut code = 0; + while code < 32 { + if let Some(facts) = EPI_LAW[code] { + let mut r = 0; + while r < 64 { + let rl = law[r]; + if rl.active && facts & rl.requires == rl.requires && facts & rl.forbids == 0 { + state[code] |= 1 << r; + } + r += 1; + } + } + code += 1; + } + let mut pearl = [0u64; 8]; + let mut p = 0; + while p < 8 { + let mut r = 0; + while r < 64 { + let rl = law[r]; + if rl.active && (p as u8) & rl.pearl == rl.pearl { + pearl[p] |= 1 << r; + } + r += 1; + } + p += 1; + } + Tables { state, pearl } +} + +pub static TABLES_V1: Tables = compile(&LAW_V1); +pub static TABLES_V2: Tables = compile(&LAW_V2); + +impl LawGen { + pub fn tables(self) -> &'static Tables { + match self { + LawGen::V1 => &TABLES_V1, + LawGen::V2 => &TABLES_V2, + } + } + pub fn rules(self) -> &'static [Rule; 64] { + match self { + LawGen::V1 => &LAW_V1, + LawGen::V2 => &LAW_V2, + } + } +} + +// ── the measurement ───────────────────────────────────────────────────────── + +/// Why an edge cannot be measured under this law. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Refusal { + /// The caller did not assert that bits 59..63 were written under the v2 + /// layout. On a v1 row they are old `temporal` bits: `temporal = 128` + /// sets bit 59 and would read as code 1 (`Causes`). Same rule as + /// `band_reading`: v1 and unknown provenance refuse. + Provenance(EdgeProvenance), + /// Bits 59..63 do not form a code this law declares. + Undeclared(u8), +} + +/// Only an asserted v2-stamped edge or a clean V3 register is readable. +pub fn admitted(provenance: EdgeProvenance) -> bool { + matches!( + provenance, + EdgeProvenance::V2Stamped | EdgeProvenance::V3Register + ) +} + +/// Bits 59..63 as one code, through the shipped accessors. +pub fn raw5(edge: CausalEdge64) -> u8 { + (edge.spare() << 2) | edge.truth_raw() +} + +/// `CausalEdge64 × RecipeLaw → EligibleRecipes`. Two lookups and one AND. +pub fn measure( + law: LawGen, + edge: CausalEdge64, + provenance: EdgeProvenance, +) -> Result { + if !admitted(provenance) { + return Err(Refusal::Provenance(provenance)); + } + let code = raw5(edge); + if EPI_LAW[code as usize].is_none() { + return Err(Refusal::Undeclared(code)); + } + let t = law.tables(); + Ok(t.state[code as usize] & t.pearl[edge.causal_mask() as usize]) +} diff --git a/crates/cognitive-shader-driver/examples/streamdto_circuit_probe.rs b/crates/cognitive-shader-driver/examples/streamdto_circuit_probe.rs new file mode 100644 index 000000000..495c84b81 --- /dev/null +++ b/crates/cognitive-shader-driver/examples/streamdto_circuit_probe.rs @@ -0,0 +1,414 @@ +//! D-STREAMDTO-0 — a real `StreamDto` through the v3 ladder to next-cycle +//! eligibility, on the shipped pieces only. +//! +//! The v3 ladder (`.claude/v3/COMPONENT-MAP.md`, `v3-substrate-primer.md` §3): +//! Φ `StreamDto` (perturbation ingress) → Ψ `PerturbationDto` → B `BusDto` +//! (commit) → Γ `ThoughtStruct`. In `cognitive-shader-driver` the same loop is +//! `ingest_codebook_indices` → `ShaderDriver::dispatch` (reads each row's +//! `CausalEdge64`, emits up to 8) → `EmitMode::Persist` names the row → +//! `persist_cycle` writes `emitted_edges[0]` back → the next dispatch reads it. +//! +//! What this probe found, link by link: +//! +//! | link | status | +//! |---|---| +//! | `StreamDto` → SoA | `ingest_codebook_indices` wrote only the BindSpace singleton (superseded by the SoA). **Missing** → completed by `ingest_codebook_indices_soa`, cycle-gated `write_row`, same per-index encoding | +//! | `dispatch` reads the mailbox | shipped (`mailbox-thoughtspace` read shim) | +//! | emitted edge → mailbox row | **missing**: the driver owned the mailbox read-only and `run()` is `&self`. Completed by `ShaderDriver::mailbox_mut` | +//! | commit | `MailboxSoA::tick` (shipped) | +//! | next cycle reads it | shipped; the edge's `s_idx` is the cascade query | +//! | #1370 eligibility | reads the committed word; the shader writes code 0 (`Open`), see `ISS-NO-EVIDENCE-WRITER-FOR-EPISTEMIC-STATE` | +//! +//! Two limits, measured here. (1) The edge's only read in `run()` is `s_idx` +//! as the cascade query, keyed on `s_idx / 4`; F/C are revised and discarded, +//! bits 59..63 are not read. A write-back is visible only when it moves the +//! query to another block whose hits differ. (2) Visibility does not wait for +//! `tick`: there is no double buffer, so a dispatch would see the write at +//! once. What separates k from k+1 is the owner's `&mut` (no dispatch runs +//! during the write) and the cycle gate (`tick` makes late writes `Stale`). +//! +//! `StreamDto::timestamp` lands in the temporal column as the caller's number. +//! It is not the cycle, and the cycle never reaches the edge: the driver passes +//! `cycle_index` as `pack`'s v1 `temporal` argument, which v2 drops. + +#[path = "shared/affordance_law.rs"] +mod affordance_law; + +use std::sync::Arc; + +use affordance_law::{measure, raw5, LawGen}; +use bgz17::base17::Base17; +use bgz17::palette::Palette; +use bgz17::palette_semiring::PaletteSemiring; +use causal_edge::edge::CausalEdge64; +#[cfg(test)] +use causal_edge::layout::{SPARE_MASK, TRUTH_MASK, TRUTH_SHIFT}; +use cognitive_shader_driver::bindspace::BindSpace; +#[cfg(test)] +use cognitive_shader_driver::engine_bridge::ingest_codebook_indices; +use cognitive_shader_driver::engine_bridge::ingest_codebook_indices_soa; +use cognitive_shader_driver::mailbox_soa::{MailboxSoA, WriteCell, WriteOutcome}; +use cognitive_shader_driver::{ + auto_style, CognitiveShaderBuilder, CognitiveShaderDriver, ColumnWindow, EmitMode, MetaFilter, + ShaderCrystal, ShaderDispatch, ShaderDriver, StyleSelector, +}; +use lance_graph_contract::band_reading::EdgeProvenance; +use thinking_engine::dto::{SourceType, StreamDto}; + +const MAILBOX: u32 = 0; +const ROWS: usize = 12; + +fn semiring() -> PaletteSemiring { + let entries: Vec = (0..16) + .map(|i| { + let mut dims = [0i16; 17]; + dims[0] = (i as i16) * 100; + dims[1] = ((i as i16) * 37) % 200; + Base17 { dims } + }) + .collect(); + PaletteSemiring::build(&Palette { entries }) +} + +/// A CAUSES chain over the 16 palettes; only palette block 0 SUPPORTS itself. +/// +/// The edge's only read in `run()` is `s_idx`, as the cascade query. The +/// cascade keys on the query's block (`s_idx / 4`), and a hit records the +/// scanned row, not the target. So if the query's block holds a distance-0 +/// self-hit with the same predicates as block 0 (true for every block in the +/// W2 demo topology, where each block SUPPORTS itself), the persisted edge +/// leaves the output unchanged. Here block 1 only CAUSES block 2: no self-hit. +fn planes() -> [[u64; 64]; 8] { + let mut p = [[0u64; 64]; 8]; + for (i, causes) in p[0].iter_mut().take(16).enumerate() { + *causes |= 1u64 << (i + 1); + } + p[2][0] |= 1; + p +} + +fn stream(timestamp: u64) -> StreamDto { + StreamDto { + source: SourceType::DeepNsm, + codebook_indices: (0..ROWS as u16).map(|i| i * 3 + 1).collect(), + timestamp, + } +} + +/// Φ ingress into the SoA: the `StreamDto`'s fields go straight into the +/// owner's mailbox through `ingest_codebook_indices_soa` (cycle-gated +/// `write_row`). BindSpace is not on the path. +fn ingest(dto: &StreamDto) -> MailboxSoA<1024> { + let mut mb: MailboxSoA<1024> = MailboxSoA::new(MAILBOX, 0, 1.0); + ingest_codebook_indices_soa( + &mut mb, + &dto.codebook_indices, + dto.source as u8, + dto.timestamp, + 0, + ); + mb +} + +/// The builder still requires a BindSpace even when the mailbox arm is +/// selected; an empty one stands in and is never read (W7 residue). +fn driver(mb: MailboxSoA<1024>) -> ShaderDriver { + CognitiveShaderBuilder::new() + .bindspace(Arc::new(BindSpace::zeros(0))) + .semiring(Arc::new(semiring())) + .planes(planes()) + .with_mailbox(MAILBOX, mb) + .build() +} + +fn request() -> ShaderDispatch { + ShaderDispatch { + // Start at row 4. The driver emits `s_palette = row % 256`; the + // cascade keys on `s_idx / 4`, so a persisted query in block 0 (rows + // 0..4) is the block every unwritten row (edge 0) already uses, and + // k+1 cannot see it. The wire paths already pass a non-zero + // `row_start`. + rows: ColumnWindow::new(4, ROWS as u32), + meta_prefilter: MetaFilter::ALL, + layer_mask: 0xFF, + radius: u16::MAX, + style: StyleSelector::Ordinal(auto_style::CREATIVE), + emit: EmitMode::Persist, + ..Default::default() + } +} + +/// B → state: the row `EmitMode::Persist` names receives `emitted_edges[0]` +/// (the same choice `engine_bridge::persist_cycle` makes for the singleton), +/// written by the owner at the current cycle. Returns `(row, edge)`. +fn persist(d: &mut ShaderDriver, c: &ShaderCrystal) -> (usize, CausalEdge64) { + let row = c.persisted_row.expect("EmitMode::Persist names a row") as usize; + assert!(c.bus.emitted_edge_count > 0, "no edge was emitted"); + let edge = CausalEdge64(c.bus.emitted_edges[0]); + let mb = d.mailbox_mut(MAILBOX).expect("the designated mailbox"); + let cell = WriteCell { + edge: Some(edge), + ..Default::default() + }; + assert_eq!(mb.write_row(row, mb.cycle(), &cell), WriteOutcome::Accepted); + (row, edge) +} + +/// Commit: the existing cycle boundary. +fn commit(d: &mut ShaderDriver) { + d.mailbox_mut(MAILBOX) + .expect("the designated mailbox") + .tick(); +} + +/// What a dispatch produced, comparable across runs. +fn signature(c: &ShaderCrystal) -> (Vec<(u32, u32)>, [u64; 8], u8) { + let hits = c + .bus + .resonance + .top_k + .iter() + .map(|h| (h.row, h.resonance.to_bits())) + .collect(); + (hits, c.bus.emitted_edges, c.bus.emitted_edge_count) +} + +/// Cycle k then cycle k+1 on one owned driver. `write` disables the persist. +fn run( + write: bool, +) -> ( + ShaderDriver, + ShaderCrystal, + ShaderCrystal, + usize, + CausalEdge64, +) { + let mut d = driver(ingest(&stream(1_000))); + let k = d.dispatch(&request()); + let (row, edge) = if write { + persist(&mut d, &k) + } else { + (k.persisted_row.unwrap_or(0) as usize, CausalEdge64(0)) + }; + commit(&mut d); + let k1 = d.dispatch(&request()); + (d, k, k1, row, edge) +} + +fn main() { + let (d, k, k1, row, edge) = run(true); + let mb = d.mailbox(MAILBOX).unwrap(); + println!("D-STREAMDTO-0"); + println!( + "cycle k: hits {} emitted {} persisted_row {:?}", + k.bus.resonance.hit_count, k.bus.emitted_edge_count, k.persisted_row + ); + println!( + "persisted: row {row} edge {:#018x} s_idx {} raw5 {} pearl {:03b}", + edge.0, + edge.s_idx(), + raw5(edge), + edge.causal_mask() as u8 + ); + println!( + "cycle k+1: mailbox cycle {} hits {} changed {}", + mb.cycle(), + k1.bus.resonance.hit_count, + signature(&k) != signature(&k1) + ); + println!( + "eligibility at k+1: {:?}", + measure(LawGen::V1, mb.edge(row), EdgeProvenance::V2Stamped) + ); + println!( + "temporal[{row}] = {} (StreamDto.timestamp)", + mb.temporal_at(row) + ); + let t = LawGen::V1.tables(); + let pearl = t.pearl[edge.causal_mask() as usize]; + for code in [0usize, 1, 3, 5, 7, 12, 17, 20, 25, 30] { + println!( + " code {code:2} under this Pearl: {:#x}", + t.state[code] & pearl + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// S1 — every link produces something. The SoA ingest writes one bit per + /// index and encodes each row exactly as the retiring singleton arm does + /// (that arm is only the oracle here); cycle k hits, emits, names a row. + #[test] + fn every_link_is_live() { + let dto = stream(1_000); + let mb = ingest(&dto); + assert_eq!(mb.populated(), ROWS); + let mut oracle = BindSpace::zeros(ROWS); + ingest_codebook_indices( + &mut oracle, + &dto.codebook_indices, + dto.source as u8, + dto.timestamp, + 0, + ); + for row in 0..ROWS { + let ones: u32 = mb.content_row(row).iter().map(|w| w.count_ones()).sum(); + assert_eq!(ones, 1, "row {row}"); + assert_eq!(mb.content_row(row), oracle.fingerprints.content_row(row)); + assert_eq!(mb.meta_at(row), oracle.meta.get(row)); + assert_eq!(mb.temporal_at(row), oracle.temporal[row]); + assert_eq!( + mb.last_write_cycle_at(row), + mb.cycle(), + "ingress went through write_row" + ); + } + let d = driver(mb); + let k = d.dispatch(&request()); + assert!(k.bus.resonance.hit_count > 0); + assert!(k.bus.emitted_edge_count > 0); + assert!(k.persisted_row.is_some()); + } + + /// S2 — the circuit closes: cycle k+1 reads the edge cycle k emitted, and + /// reading it changes what k+1 computes. Without the write, k+1 repeats k. + #[test] + fn the_next_cycle_reads_what_this_cycle_emitted() { + let (d, k, k1, row, edge) = run(true); + assert_ne!( + edge.s_idx(), + 0, + "fixture: the persisted edge must move the query" + ); + assert_eq!(d.mailbox(MAILBOX).unwrap().edge(row), edge); + assert_ne!( + signature(&k), + signature(&k1), + "k+1 ignored the persisted edge" + ); + + let (_, k_off, k1_off, _, _) = run(false); + assert_eq!( + signature(&k_off), + signature(&k1_off), + "nothing else changes k+1" + ); + assert_eq!( + signature(&k), + signature(&k_off), + "cycle k does not depend on the write" + ); + } + + /// S3 — the commit boundary is `tick`. Before it the write is this + /// cycle's; after it a late write for the old cycle is refused. + #[test] + fn tick_is_the_commit_and_the_cycle_gate_holds() { + let mut d = driver(ingest(&stream(1_000))); + let k = d.dispatch(&request()); + let cycle_k = d.mailbox(MAILBOX).unwrap().cycle(); + let (row, edge) = persist(&mut d, &k); + let mb = d.mailbox(MAILBOX).unwrap(); + assert_eq!( + mb.last_write_cycle_at(row), + mb.cycle(), + "uncommitted: written this cycle" + ); + + commit(&mut d); + let mb = d.mailbox_mut(MAILBOX).unwrap(); + assert_eq!(mb.cycle(), cycle_k.wrapping_add(1)); + assert!(mb.last_write_cycle_at(row) != mb.cycle(), "committed"); + let late = WriteCell { + edge: Some(CausalEdge64(0)), + ..Default::default() + }; + assert_eq!(mb.write_row(row, cycle_k, &late), WriteOutcome::Stale); + assert_eq!(mb.edge(row), edge); + } + + /// S4 — #1370 eligibility at k+1 is measured on the committed row, and + /// the circuit does not move it: the shader stamps code 0 (`Open`), which + /// under the edge's Pearl planes grants exactly what an unwritten word + /// grants. The second half proves the measurement reads the row: an + /// evidence code committed into the same row changes it. + #[test] + fn eligibility_at_k1_reads_the_committed_row_and_the_circuit_leaves_it_open() { + let (mut d, _, _, row, edge) = run(true); + let committed = d.mailbox(MAILBOX).unwrap().edge(row); + assert_eq!(raw5(committed), 0, "the shader writes no epistemic state"); + let t = LawGen::V1.tables(); + let open = measure(LawGen::V1, committed, EdgeProvenance::V2Stamped); + assert_eq!(open, Ok(t.state[0] & t.pearl[edge.causal_mask() as usize])); + assert_eq!( + open, + measure(LawGen::V1, CausalEdge64(0), EdgeProvenance::V2Stamped), + "the circuit moved eligibility" + ); + + // Code 7 (DIRECT | OBSERVED | ASSOCIATED) into the same row, committed + // by tick. Not code 3: under Pearl S it grants the same 0x20 as code 0 + // (the table `main` prints). + let observed = + CausalEdge64((committed.0 & !(TRUTH_MASK | SPARE_MASK)) | (7u64 << TRUTH_SHIFT)); + let mb = d.mailbox_mut(MAILBOX).unwrap(); + let cell = WriteCell { + edge: Some(observed), + ..Default::default() + }; + assert_eq!(mb.write_row(row, mb.cycle(), &cell), WriteOutcome::Accepted); + mb.tick(); + let reread = measure(LawGen::V1, mb.edge(row), EdgeProvenance::V2Stamped); + assert_eq!( + reread, + Ok(t.state[7] & t.pearl[edge.causal_mask() as usize]) + ); + assert_ne!(reread, open, "the measurement does not read the row"); + } + + /// S5 — timestamp is the caller's number in the temporal column; the cycle + /// is the mailbox's counter. Neither moves the other, and the dispatch's + /// `cycle_index` never reaches the edge (v2 drops `pack`'s temporal). + #[test] + fn timestamp_is_not_the_cycle() { + let early = ingest(&stream(5)); + let late = ingest(&stream(9_999_999)); + assert_eq!(early.cycle(), late.cycle()); + assert_eq!(early.temporal_at(0), 5); + assert_eq!(late.temporal_at(0), 9_999_999); + + let (d, k, _, row, _) = run(true); + assert_eq!( + d.mailbox(MAILBOX).unwrap().temporal_at(row), + 1_000, + "tick left temporal" + ); + for &e in &k.bus.emitted_edges[..k.bus.emitted_edge_count as usize] { + assert_eq!(e >> 59, 0, "cycle_index leaked into bits 59..63"); + } + } + + /// S6 — restart: the committed word persisted as LE bytes and restored into + /// a fresh mailbox at a different cycle gives the same cycle k+1. + #[test] + fn restart_from_the_persisted_word_gives_the_same_next_cycle() { + let (d, _, k1, row, _) = run(true); + let bytes = d.mailbox(MAILBOX).unwrap().edge(row).to_le_bytes(); + + let mut mb = ingest(&stream(1_000)); + for _ in 0..7 { + mb.tick(); + } + let cell = WriteCell { + edge: Some(CausalEdge64::from_le_bytes(bytes)), + ..Default::default() + }; + assert_eq!(mb.write_row(row, mb.cycle(), &cell), WriteOutcome::Accepted); + mb.tick(); + let restored = driver(mb).dispatch(&request()); + assert_eq!(signature(&restored), signature(&k1)); + } +} diff --git a/crates/cognitive-shader-driver/src/driver.rs b/crates/cognitive-shader-driver/src/driver.rs index 1590b2578..9d46c9d81 100644 --- a/crates/cognitive-shader-driver/src/driver.rs +++ b/crates/cognitive-shader-driver/src/driver.rs @@ -160,6 +160,20 @@ impl ShaderDriver { self.mailboxes.get(&id) } + /// The owner's `&mut` to the `MailboxSoA<1024>` registered under `id`. + /// + /// `run()` is `&self` and writes nothing, so without this a cycle's + /// emitted edges had no route back into the row the next cycle reads: + /// the driver owns the mailbox and only lent it out read-only. Holding + /// `&mut ShaderDriver` is the ownership; this lends it to the owner's own + /// write (`MailboxSoA::write_row`, which accepts only the current cycle) + /// and commit (`MailboxSoA::tick`). No dispatch can run while the borrow + /// is live. D-STREAMDTO-0. + #[inline] + pub fn mailbox_mut(&mut self, id: MailboxId) -> Option<&mut MailboxSoA<1024>> { + self.mailboxes.get_mut(&id) + } + /// Borrow the underlying BindSpace (read-only). #[inline] pub fn bindspace(&self) -> &BindSpace { diff --git a/crates/cognitive-shader-driver/src/engine_bridge.rs b/crates/cognitive-shader-driver/src/engine_bridge.rs index b92c6631b..88c0ade32 100644 --- a/crates/cognitive-shader-driver/src/engine_bridge.rs +++ b/crates/cognitive-shader-driver/src/engine_bridge.rs @@ -31,6 +31,7 @@ use lance_graph_contract::cognitive_shader::{ }; use crate::bindspace::{BindSpace, WORDS_PER_FP}; +use crate::mailbox_soa::{MailboxSoA, WriteCell, WriteOutcome}; use lance_graph_contract::qualia::QualiaI4_16D; // QUALIA_DIMS is referenced only inside `dispatch_busdto` (which is // `#[cfg(feature = "with-engine")]`); gate the import so default builds don't @@ -78,9 +79,7 @@ pub fn ingest_codebook_indices( // caller-supplied indices — a hard assert here would let a network // client panic the lab server. Documented instead of asserted; // canonical inputs are codebook indices < 4096 and never wrap. - let mut content = [0u64; WORDS_PER_FP]; - let bit = idx as usize % (WORDS_PER_FP * 64); - content[bit / 64] |= 1u64 << (bit % 64); + let content = codebook_index_fingerprint(idx); bs.fingerprints.set_content(cursor, &content); // Meta: source_ordinal as thinking style, no NARS yet. @@ -94,6 +93,61 @@ pub fn ingest_codebook_indices( (start as u32, cursor as u32) } +/// The content fingerprint one codebook index produces: the bit at `idx` +/// (mod 16384). Shared by both ingest arms so the singleton and the SoA can +/// never encode a `StreamDto` differently. +#[inline] +fn codebook_index_fingerprint(idx: u16) -> [u64; WORDS_PER_FP] { + let mut content = [0u64; WORDS_PER_FP]; + let bit = idx as usize % (WORDS_PER_FP * 64); + content[bit / 64] |= 1u64 << (bit % 64); + content +} + +/// Ingest a `StreamDto`'s fields into the owner's `MailboxSoA` — the SoA arm +/// of [`ingest_codebook_indices`] (BindSpace is superseded by the SoA). +/// +/// One row per index from `start_row`, written through +/// [`MailboxSoA::write_row`] at the mailbox's current cycle, so ingress obeys +/// the same cycle gate as every other write: content = the shared +/// per-index fingerprint, meta = `source_ordinal` as thinking style, temporal +/// = the caller's `timestamp` (never the cycle). The populated count grows to +/// cover the written rows. Rows past `N` are not written; the returned range +/// says how far ingress got. D-STREAMDTO-0. +pub fn ingest_codebook_indices_soa( + mb: &mut MailboxSoA, + indices: &[u16], + source_ordinal: u8, + timestamp: u64, + start_row: usize, +) -> (u32, u32) { + let start = start_row.min(N); + let mut row = start; + for &idx in indices { + if row >= N { + break; + } + let content = codebook_index_fingerprint(idx); + let cell = WriteCell { + content: Some(&content), + meta: Some(MetaWord::new(source_ordinal, 0, 0, 0, 0)), + temporal: Some(timestamp), + ..Default::default() + }; + let outcome = mb.write_row(row, mb.cycle(), &cell); + debug_assert_eq!( + outcome, + WriteOutcome::Accepted, + "current-cycle write refused" + ); + row += 1; + } + if row > mb.populated() { + mb.set_populated(row); + } + (start as u32, row as u32) +} + // ═══════════════════════════════════════════════════════════════════════════ // PerturbationDto → ShaderDispatch (top-k seeds the scan window) // ═══════════════════════════════════════════════════════════════════════════ @@ -828,6 +882,34 @@ mod tests { } } + /// The SoA arm encodes each row exactly as the singleton arm, writes + /// through the cycle gate (stamped with the current cycle), grows + /// `populated`, and stops at `N`. + #[test] + fn soa_ingest_matches_the_singleton_encoding_through_the_cycle_gate() { + let idx = [42u16, 100, 200]; + let mut bs = BindSpace::zeros(4); + ingest_codebook_indices(&mut bs, &idx, 3, 777, 0); + let mut mb: MailboxSoA<4> = MailboxSoA::new(1, 0, 1.0); + mb.tick(); + assert_eq!( + ingest_codebook_indices_soa(&mut mb, &idx, 3, 777, 0), + (0, 3) + ); + assert_eq!(mb.populated(), 3); + for row in 0..3 { + assert_eq!(mb.content_row(row), bs.fingerprints.content_row(row)); + assert_eq!(mb.meta_at(row), bs.meta.get(row)); + assert_eq!(mb.temporal_at(row), 777); + assert_eq!(mb.last_write_cycle_at(row), 1); + } + assert_eq!( + ingest_codebook_indices_soa(&mut mb, &idx, 3, 777, 2), + (2, 4) + ); + assert_eq!(mb.populated(), 4); + } + #[test] fn ingest_sets_bits() { let mut bs = BindSpace::zeros(10); diff --git a/crates/cognitive-shader-driver/src/lib.rs b/crates/cognitive-shader-driver/src/lib.rs index 84574ab0b..29018fb43 100644 --- a/crates/cognitive-shader-driver/src/lib.rs +++ b/crates/cognitive-shader-driver/src/lib.rs @@ -213,8 +213,9 @@ pub use bindspace::{ }; // deprecated — use QualiaI4Column pub use driver::{CognitiveShaderBuilder, ShaderDriver}; pub use engine_bridge::{ - dispatch_from_top_k, ingest_codebook_indices, persist_cycle, read_qualia_17d, unified_style, - write_qualia_17d, EngineBusBridge, UnifiedStyle, UNIFIED_STYLES, + dispatch_from_top_k, ingest_codebook_indices, ingest_codebook_indices_soa, persist_cycle, + read_qualia_17d, unified_style, write_qualia_17d, EngineBusBridge, UnifiedStyle, + UNIFIED_STYLES, }; pub use mailbox_soa::{DefaultMailboxSoA, MailboxSoA}; pub use palette_perturbation::{