diff --git a/.claude/board/entries/2026-11-03-d-xlog-meta-wire-p0.md b/.claude/board/entries/2026-11-03-d-xlog-meta-wire-p0.md new file mode 100644 index 000000000..5cec1b8bd --- /dev/null +++ b/.claude/board/entries/2026-11-03-d-xlog-meta-wire-p0.md @@ -0,0 +1,32 @@ +# 2026-11-03 — D-XLOG-META-WIRE-P0: the first break between M1's result and M2 + +**Status:** MEASURED, lab only (`.claude/probes/2026-11-03-d-xlog-meta-wire/`, `WIRING-MAP.md`, probe 18/18). No production change; dt64, r2il, mask-risc, Quack and ndarray are unchanged. + +- **First break: Receipt → M2.** `r2il::binding::run` has no input for another method's result. + - `Arg::Field` reads the same run's value or a resident I32/U32/U64 lane, else `FieldUnbound` / `LaneUnusable`. + - A mask never crosses: + - the receipt keeps only its field id; + - `discard_pending` makes the slot `StaleSlot`; + - `KEEP` is not executable. +- **Scope does not narrow M2.** `Scope { id }` is an equality-checked id with no population behind it, and M2 cannot be guarded by M1's field: WHEN → `GuardNotExecutable`. +- **The virtual XY hop is refused as a method effect:** `VIA` → `NotExecutable`, and Gather has no R2IL word (EXO-140). +- **`methods_on` (ON) and `methods_touching` (footprint) differ as documented.** Both have test-only callers, and no change event reaches them. +- **The only lawful route:** M1's mask feeding M2's fold as an in-program register inside ONE binding. + - Oracle 177; disable 435; one support deleted 176; unrelated lane unchanged; scope and frame refused. + - It needs a derived classid, because behaviour is immutable. + +**OPEN, owner decisions:** +- WHEN evaluation as an in-program guard register (D-DT64-6); +- `VIA` / `EqU32Via` executability (EXO-140); +- a write path for change events (v3); +- composite identity via `register_derived`. + +**P0b (same day, `COMPOSE-RULING.md`, `probe/src/bin/v2_compose.rs` 25/25):** +- No method-body composition exists in D-DT64-6 or OGAR. Composition today goes through a written field or stays inside one method, and `ogar-loco::Program` was rejected. +- The missing edge is a guard leaf that names another method's predicate `(MethodRef, field)`. `GuardWitness` cannot tell `Sym(1)`=M1 from `Sym(1)`=M1B. +- A lab composer emulating the proposed ruling builds one program from M1 and M2: + - oracle 177; disabling M1's predicate gives 435; + - wrong permit, class, sig rev, schema rev, scope, route and frame are refused; + - cycle refused; `register_derived` + reroute succeeds. +- Corrected chain (Global WHERE → DR view → MethodRef): `where_dr3::LocalSelection` binds one selection to ONE method (`SchemaChanged`, pinned by a test). It is lab only and not lifted into dt64. +- Proposed D-DT64-6 ruling: PROPOSED / NOT IN FORCE. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index c21b20143..e75c83f56 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,10 +25,11 @@ 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. -561 entries, 2026-08-06 .. 2026-11-03. +562 entries, 2026-08-06 .. 2026-11-03. | date | entry id | finding | file | |---|---|---|---| +| 2026-11-03 | `D-XLOG-META-WIRE-P0` | | [2026-11-03-d-xlog-meta-wire-p0.md](2026-11-03-d-xlog-meta-wire-p0.md) | | 2026-11-03 | `D-BAG-FOLD` | | [2026-11-03-d-bag-fold-witnessed-existential.md](2026-11-03-d-bag-fold-witnessed-existential.md) | | 2026-11-03 | `D-BAG-FOLD-V4` | | [2026-11-03-d-bag-fold-v4-attested-lens-gated-exec.md](2026-11-03-d-bag-fold-v4-attested-lens-gated-exec.md) | | 2026-11-03 | `D-BAG-FOLD-V3` | | [2026-11-03-d-bag-fold-v3-production-witnesses.md](2026-11-03-d-bag-fold-v3-production-witnesses.md) | diff --git a/.claude/probes/2026-11-03-d-xlog-meta-wire/COMPOSE-RULING.md b/.claude/probes/2026-11-03-d-xlog-meta-wire/COMPOSE-RULING.md new file mode 100644 index 000000000..9268729ca --- /dev/null +++ b/.claude/probes/2026-11-03-d-xlog-meta-wire/COMPOSE-RULING.md @@ -0,0 +1,98 @@ +# D-XLOG-META-WIRE P0b: composing two MethodRefs at cold preparation, and the WHERE-scope chain + +**Status:** MEASURED, lab only. +- Probe `probe/src/bin/v2_compose.rs`: 25/25 checks. +- Sections 1–2 are source-read; section 5 is source-read and tests already in the tree, nothing new run. +- No production change: `GuardNotExecutable` is untouched (check F0). +- The survival law is preserved: no mask crosses a program boundary, nothing is written to the WORLD, no Gather. + +**Proposed ruling: PROPOSED / NOT IN FORCE, for operator review.** + +```sh +cd .claude/probes/2026-11-03-d-xlog-meta-wire/probe +CARGO_PROFILE_DEV_DEBUG=0 cargo +1.99.0 run --release -q --bin v2_compose +``` + +## 1. Does D-DT64-6 / OGAR already provide method-body composition? + +**No.** What exists: + +| mechanism | where | what it composes | class | +|---|---|---|---| +| ordered `MethodBinding.effects`; `register` refuses `UndeclaredRead` | `dt64/src/lib.rs`; plan `d-dt64-6-routing-bindstore-behavior-v1.md` A.4 | steps inside ONE method | PRODUCTION | +| DO and ON share the property domain, so chaining is DO → field → ON | plan §3, citing `odoo-rs:docs/DT64-ON-WHEN-DO-REGISTRY.md:61-65` | methods, through a written field (the WORLD) | PROPOSED | +| `dep = do_ ∘ read_by` via `relcomp::compose`; schedule from `dep` | `tract-harvest-probe` (plan A.4 table) | a method dependency graph and a schedule | LAB | +| OGAR `KausalSpec::{Depends, StateGuard, Constrains, Onchange}` | `OGAR:crates/ogar-vocab/src/lib.rs:610-652` | triggers by field path; chaining through stored fields | other repo, PRODUCTION vocabulary | +| `ogar-loco::Program` as the method body | rejected by operator decision, 2026-10-31 (entries `2026-10-31-coresearch-mandatory-codegraph.md`, `…-d-tuple-polyfill-0-architecture.md`) | — | REJECTED | +| `register_derived` + `route` (D5) | `dt64/src/lib.rs:570, 739` | a new derived classid for new behaviour | PRODUCTION | + +Every existing composition goes through a **written field** (WORLD) or stays **inside one method**. Nothing resolves two MethodRefs into one program. + +## 2. The exact missing semantic edge + +A WHEN leaf is `SetExpr::Sym(u32)` ("an opaque two-valued set … What carries it is the consumer's business", `contract/src/set_algebra.rs:139-142`) or `Res`, a predicate on one lane. Nothing lets a guard leaf name **another method's predicate**. + +`GuardWitness` is the guard's normal form. It is identical whether `Sym(1)` stands for M1 (`a < 5`) or M1B (`a < 7`) (check **GAP**). So a reference carried as a `Sym` is invisible to `admit` and to `plan_id`: +- the referenced method's identity and signature revision cannot be checked from the contract; +- the lab composer has to carry them itself (`Composed.deps`). + +**Missing edge:** a guard leaf, or a binding-side table checked by `admit`, that names `(MethodRef incl. sig_rev, predicate field)`, and a prepare step that inlines that predicate into the referencing method's program. + +## 3. Bounded falsifier (lab composer emulating the proposed ruling) + +The composer: +- resolves M2's WHEN `Sym(1)` through a lab table to (M1, field `m1`); +- prepends M1's predicate effects; +- gates each of M2's folds by the guard register (`u = m1 AND t; s = POP_COUNT(u)`); +- registers the composite with `register_derived` and re-routes the position; +- runs it through unchanged r2il `prepare` / `run`. + +| # | requirement | result | +|---|---|---| +| F0 | production unchanged | `prepare(M2 with WHEN)` = `GuardNotExecutable` | +| 1 | M1, M2 independently registered | both admitted; distinct MethodIds, same class | +| 2 | WHEN refers to M1's predicate, not a slot | `Sym(1)` → (M1, `m1`) | +| 3 | one native fold program | `programs_run = 1`; deps `[(M1, rev 1), (M2, rev 1)]` | +| 4 | independent oracle | 177 = \|a<5 ∧ b<10\| | +| 5 | M1's predicate disabled → divergence | 435 = \|b<10\|; `Sym(1)` → M1B composes a different program | +| 6 | refusals | permit lacks M1 → `NotPermitted(M1)`; M1 of another class → `OtherClass`; M1 at another sig rev → `SignatureRevisionMismatch`; another schema rev → `SchemaRevisionMismatch`; another scope → `ScopeChanged`; permits for M1+M2 do not run the composite → `NotPermitted`; plan from before the re-route → `RouteChanged`; source moved → stale frame | +| 7 | cyclic dependency | M3 ↔ M4 → `Cycle([M3, M4, M3])`. Neighbours: a predicate computed from a fold → refused (the `FoldResultFeedsFold` rule, kept); Kleene WHEN → refused | +| 8 | `register_derived` + re-route **succeeds** | `Ok`, route → version 2; the base still resolves; adding the composite to the base → `BehaviourImmutable` | +| 9 | in-program narrowing ≠ DT64 population Scope | the narrowing lives in the program (intermediates `[m1, t, u]`); the same plan under another `Scope` id is refused. Scope is policy, not population | +| 10 | real DR-coordinate virtual relation | **not done.** Separately gated; the `a < 5 ∧ b < 10` fixture is not that test | + +## 4. Proposed D-DT64-6 ruling (for operator review, not in force) + +1. **Reference, not result.** + - A WHEN may reference another registered method's *predicate*, as `(MethodRef incl. sig_rev, predicate field)`. + - It never references a result, slot or Receipt. + - Only two-valued guards over Distinct populations at first; Kleene stays refused. +2. **Resolved cold, executed as one program.** Preparation inlines the referenced predicate's effects ahead of the referencing method's effects, and gates its folds by the guard register. One native program; no mask leaves it. +3. **Identity.** The composite is a derived classid (`register_derived` + `route`, D5). Its plan identity covers every referenced MethodRef and signature revision. A changed reference is a new derived classid, never an in-place edit. +4. **Checks.** `admit` per referenced MethodRef; same class (or a declared derived chain); one Frame/Focus/Scope for the program; route checked at prepare and run. +5. **Permission.** Open owner question. The probe requires a permit for the composite itself and does not derive it from the permits for M1 and M2 (F6f). +6. **Acyclic, fold-free predicates.** A reference cycle refuses. A referenced predicate computed from a fold result refuses (keeps `FoldResultFeedsFold`). +7. **Not Scope.** This is in-program predicate narrowing. A DT64 population Scope bound to a WHERE view is a separate question (section 5). +8. **Unchanged laws.** No cross-program mask hand-off, no persisted intermediate mask, no silent WORLD write, no generalized Gather (EXO-140 still gates `VIA`). + +## 5. The corrected chain: Global WHERE → DR view → MethodRef → Fold (operator correction, same day) + +Source read only; no new run. This is the chain the operator re-prioritised to P0/P1. + +| step | where | class | carries | +|---|---|---|---| +| Global WHERE | Quack typed WHERE → mask-risc `Keep`: one borrowed 8 KiB mask over 65,536 addresses ("the only materialization") | PRODUCTION (Quack) | the rows where the criteria are TRUE | +| DR addressing | `dt64::dr::{LocalMap, MapTable, FrameStore}` | PRODUCTION | global → local positions, versioned | +| projection into a view | `dr-math-binding-probe/src/where_dr3.rs` `project`: `map.len()` probes, never a pass over 65,536 rows; a frame must carry `map.digest()` | LAB, not lifted into dt64 | `LocalSelection { truth, axis, method, global frame, local frame, focus, scope, criteria }` | +| admission | `LocalSelection::admit` (`where_dr3.rs:225`) | LAB | re-checks global seal, local frame, focus, scope, axis, **method**, criteria | +| fold | `selected_count`, `selected_column`, `selected_column_k3` | LAB | Boolean folds refuse K3/Bag registers | +| reuse of one selection across programs | `drf-focus-probe/examples/p2_quack_where.rs` (R kept once, then `Plane(R) ∧ pres_p` for P properties) | LAB | works, but "Quack has no frame": stale codebook, scope and selection run silently stale (measured there) | + +**First break of the corrected chain (P1):** +- The admitted selection is bound to **one** method signature: `if register.method != self.method { SchemaChanged }` (`where_dr3.rs:244`). +- The test `foreign_owner_codebook_criteria_and_schema_refuse` pins it: "Another DR method over the same axis" → `SchemaChanged`. +- Two methods can read one WHERE only by projecting it twice (O(map.len()) each, lawful: the WHERE mask is the demanded result). There is no shared, admitted scope object. +- That method type is the probe's `MethodSignature { owner_class, method_ordinal, … }`, not dt64's `MethodRef`. +- Production dt64 has `Scope { id }` and nothing that binds a scope to a WHERE view. + +**Consequence:** the common case the operator describes (two methods over one global WHERE view, no `Receipt → M2`) needs no new algebra. It needs the WHERE-view binding (`LocalSelection` + `Criteria`) lifted from the lab and tied to `MethodRef`, plus a ruling on whether one admitted selection may serve several MethodRefs. diff --git a/.claude/probes/2026-11-03-d-xlog-meta-wire/WIRING-MAP.md b/.claude/probes/2026-11-03-d-xlog-meta-wire/WIRING-MAP.md new file mode 100644 index 000000000..b77860942 --- /dev/null +++ b/.claude/probes/2026-11-03-d-xlog-meta-wire/WIRING-MAP.md @@ -0,0 +1,126 @@ +# D-XLOG-META-WIRE P0: virtual XY → M1 → result → scoped M2 + +**Status:** +- MEASURED, lab only. A read-only source census plus one isolated falsifier (`probe/`, own workspace, 18/18 checks). +- No production change. dt64, r2il, mask-risc, Quack and ndarray are used unchanged. +- No Gather admission, no new relation, scope store, scheduler or evaluator. + +**Sources:** +- lance-graph `fb5e2d35b10fc4940f5c78285bae246f90c7faad` (`origin/main`). +- OGAR `ogar-r2il` / `ogar-loco` from `main` (cargo checkout `5b82ae0`). +- ndarray `b3d7919662a3`. +- rustc 1.99.0. + +```sh +cd .claude/probes/2026-11-03-d-xlog-meta-wire/probe +CARGO_PROFILE_DEV_DEBUG=0 cargo +1.99.0 run --release -q # exits 1 on any failed check +``` + +## Answer + +The chain `virtual XY → M1 → result → scoped M2` is **not expressible as two methods under the current contracts**. + +**The first actual break is Receipt → M2.** +- `run` has no input that names another method's result. +- A field that the same run has not written is read from a resident **value** lane, or refused. +- A mask result cannot cross at all: + 1. the receipt keeps only its field position; + 2. the slot is stale once the run ends; + 3. `FieldLanes` cannot name a mask; + 4. `KEEP` is not executable. + +This is the existing survival law working as written, not a missing cable. + +**The one lawful route is inside one binding.** M1's mask can feed M2's fold *inside one binding* (an in-program register, Data origin, no `FoldResultFeedsFold`). +- That is not M1 → M2. It is a new method, and a registered class's behaviour is immutable. +- It needs a derived classid and a re-route (`register_derived`), which exist. + +## Transitions + +| # | transition | producer | consumer | contract actually carried | first missing edge or typed refusal | reusable mechanism | owner gate | falsifier (probe) | +|---|---|---|---|---|---|---|---|---| +| 1 | DR position → class | `BindStore::route` (`dt64/src/lib.rs:739`) | `resolve_at` (`:816`), `resolve` (`:794`) | classid per position at a `RouteVersion`; class schema; fields through `ClassView` | none: CONNECTED | `RouteLog`, `MapTable` | — | existing dt64 tests | +| 2 | virtual XY as a method body | `Effect { algebra, args }` (`lib.rs:128`) | r2il `prepare` (`binding.rs:276`) | words over field positions of ONE class; `Arg::Field` / `Arg::Const` | a cross-row hop has no executable word: `VIA` → `NotExecutable { opcode: VIA }`. `Gather`/`Semijoin` have no R2IL word | `EXECUTABLE` (`binding.rs:98`): single-row compares, `And/Or/Xor/Not`, folds | **EXO-140** (`VIA` lowers to `EqU32Via`, an unclassified ADMISSION B site) | T6 | +| 3 | change → eligible method | none: no change event is emitted. `run` computes effects but writes nothing back; `Cube::DeltaLog` (`xlog/src/cube.rs`) never reaches dt64 | `Resolved::methods_on` (`lib.rs:454`), whose only callers are dt64 tests | `on: WideFieldMask` | EXISTING BUT UNWIRED: no producer | `methods_on` is the trigger; `methods_touching` (`:523`) is the dependency footprint (ON ∪ reads ∪ writes) | v3 write path / mailbox owner (who writes M1's effects) | T1a–c: `methods_on(m1)` = {M2, M2_WHEN}; `methods_touching(m1)` adds M1 (it writes m1, it is not triggered by it) | +| 4 | MethodRef → native fold | `MethodRef::of` (`lib.rs:272`), `admit` (`:832`) | `prepare` / `prepare_native` (`binding_native.rs:202`) | method, sig rev, guard witness | `GuardNotExecutable` (`binding.rs:291`); `ThreeValuedNotExecutable`; sink words (`KEEP`, `GROUP_SUM`) `NotExecutable` | `ExecPermit` (lab, `binding.rs:126`) | D-DT64-6 owner (WHEN evaluation); permit issuer undecided | T5, T6 | +| 5 | fold → Receipt | `run_steps` (`binding.rs:354`) | `Receipt` (`:178`), `Executed` (`:169`) | `values: (field, i64)`, `intermediates: field positions`, `plan_id`, `MethodRef`, `FrameRevisions`; native: at most 8 outputs (`MAX_NATIVE_OUTPUTS`) | by design: a non-scalar `Val` (`Slot`, `Address`) keeps only its field id. `run` then calls `discard_pending` (`r2il/src/lib.rs:533`, epoch += 1), so a kept slot is `StaleSlot` | `Val::Slot { epoch }` (`lib.rs:200`) | the survival law (inter-program computed-mask handoff forbidden) | T2a–b, T4a–b | +| 6 | **Receipt → M2** | `Receipt.output` | `run(plan, store, frames, now, permit, fields, dialect)` (`binding.rs:330`) | — | **FIRST BREAK.** No parameter takes a receipt or a value. `Arg::Field(p)` is the same run's earlier value, else `FieldLanes` → `lane()` (`binding.rs:579`), which accepts only I32/U32/U64 lanes → `FieldUnbound` / `LaneUnusable`. `Arg::Const` is fixed at registration (immutable, D5) | none for a mask. For a scalar, only a caller-written resident lane: a WORLD write, which no seam performs | v3 write-on-behalf owner (scalar route); survival law (mask route) | T3a–b | +| 7 | result → narrower Scope | `Guards { frame, focus, scope }` (`dr.rs:509`) | `Guards::check` (`dr.rs:526`) | `Scope { id: u64 }`, `Focus { revision: u64 }`: opaque ids compared for equality | no type relates a scope to a population. Another scope id is `ScopeChanged` | `Frame`/`FrameStore` issuer identity | EXO-46 (binding membrane) | T7 | +| 8 | M1's meaning as M2's WHEN | `Guard { expr: SetExpr, logic }` (`lib.rs:152`), witness in `MethodRef` | `prepare` | the guard expression, stored and never evaluated ("The store keeps it; it evaluates nothing", `lib.rs` header) | `GuardNotExecutable` | D-DT64-6 §3: WHEN is a Quack WHERE into a guard mask G, which would be an in-program register in M2's own program (meaning crosses, no mask hand-off) | D-DT64-6 owner | T5 | +| 9 | M1 ∘ M2 fused | one binding with M1's and M2's effects | `prepare` / `run` | in-program register: Data-origin mask into a fold | lawful. Breaks only identity: behaviour is immutable (`BehaviourImmutable`), so the composite is a new derived classid | `register_derived` (`lib.rs:570`) + `route` | DT64 owner (composite identity) | T8a–e, T9 | +| 10 | consequence retention | `Receipt` (in memory) | none | — | no persistence or replay path in r2il; xlog `bag_count_admitted` (`xlog/src/lib.rs:384`) returns `Vec` outside `Receipt`, `Guards` and the permit | `FrameStore` versions; THOUGHT probes (lab only) | v3 / EXO-108 (Bag) | — | + +## The lawful partial trace (T8) + +FUSED = `m1 = a < 5; t = b < 10; u = m1 AND t; s = POP_COUNT(u)` in one binding: + +| check | result | +|---|---| +| matches the scalar oracle | 177 = \|a<5 ∧ b<10\| | +| M1 operand removed (`s = POP_COUNT(t)`) | 435 = \|b<10\|, so the binding is load-bearing | +| one supporting row of M1 removed | 176 | +| unrelated lane `k` changed | unchanged | +| another scope | `ScopeChanged` | +| a stale frame | refused | +| tuples / stored intermediates | none; the mask is an in-program register | + +This is the v4 register-gated pattern at the method level. It shows the algebra can scope M2 by M1 *inside one program*. It does not show M1's result reaching a separately registered M2. + +## Crosswalk: local wiring vs upstream + +Upstream facts marked † are quoted from the 2026-11-02 archaeology entry (`.claude/board/entries/2026-11-02-coresearch-xlog-souffle-crepe-dr3-archaeology.md`), not re-verified here. + +| capability | class | evidence | +|---|---|---| +| position → class → fields | EXISTING AND CONNECTED | transition 1 | +| ON-trigger lookup (`methods_on`) | EXISTING BUT UNWIRED | no change producer; test-only callers | +| dependency footprint (`methods_touching`) | EXISTING BUT UNWIRED | test-only callers | +| method execution (single-row predicates + folds) | PARTIALLY IMPLEMENTED | `EXECUTABLE` subset, lab `ExecPermit` | +| cross-row hop as a method word | BLOCKED BY OWNER RULING | `VIA`/`EqU32Via` (EXO-140); no Gather word | +| WHEN evaluation | BLOCKED BY OWNER RULING | `GuardNotExecutable`; D-DT64-6 §3 names the intended shape | +| result → second method | MISSING (by law for masks; no write path for scalars) | transitions 5–6 | +| scope as a population | NOT A CONTRACT TODAY | `Scope { id }` | +| XLOG F100 lens execution | EXISTING BUT UNWIRED† | `prepare` refuses any lens that is neither POLYFILL nor the R2IL fold concept (`NotR2ilReading`, `binding.rs:297`) | +| XLOG MagicSetReport | dropped in the fork's compile.rs† (`:1496/:1511`) | owned by the D-XLOG-WIRE W1 session, not here | +| XLOG edge polarity | dropped at stratify → lower† | D-XLOG-WIRE (PR #1936) | +| XLOG recursion (`RirNode::Fixpoint`) | dormant upstream IR node†; semi-naive is runtime- and CUDA-only upstream† | EXO-73 | +| Soufflé magic sets | upstream, positive programs only† | semantic reference only | +| Crepe / `crepe_plan!` | upstream; `crepe_plan!` drifts on `let` and unsafe-rule labels (D1/D2)† | lab | +| Scallop provenance semirings | upstream capability; nothing imported | OUTSIDE scope of this chain | +| DialEgg / egglog-MLIR | offline compiler rewrites; e-graph discarded per call† | OUTSIDE scope (no runtime role) | + +## Backlog (planning estimates, not deadlines) + +Each item: producer → missing edge → consumer → falsifier → owner → PR scope. + +**P0** +- **WHEN as in-program register.** + - Path: M2's `Guard` (SetExpr over a field M1 *defines*) → evaluated as a Quack WHERE inside M2's own program → `prepare`/`run`. + - Falsifier: M2_WHEN = FUSED oracle; remove the guard → 435. + - Owner: D-DT64-6 (lift `GuardNotExecutable`). + - Scope: one seam change, gated. **OWNER DECISION.** +- **`methods_on` / `methods_touching` negative controls.** Done here (T1); 0.5 d to move into dt64 tests. **WIRE NOW.** + +**P1** +- **Change → `methods_on`.** Needs a producer of field-change events, which needs a write path (v3 mailbox, write-on-behalf). Falsifier: change a; M2 is offered, UNRELATED is not. 2–4 d after the write-path owner rules. **OWNER DECISION.** +- **Composite identity for M1∘M2.** `register_derived` + route; the falsifier is T8/T9 under a derived classid. 1–2 d. **PROBE.** +- **`VIA` executability.** EXO-140 classification first. **OWNER DECISION.** + +**P2** +- XLOG compile records (MagicSetReport, polarity): already owned by D-XLOG-WIRE (session `01X6y3drw…`). **PARK** here. +- Caller-API polyfill. **PARK.** + +**P3** +- Recursion (EXO-73), Bag ℕ-compose (EXO-108), cross-tile compose. **PARK.** + +**SKIP** +- Carrying a `Slot` or mask across runs. +- Storing M1's mask as a plane or relation for M2. +- A scope store, scheduler or second evaluator. + +## Recommendation + +**OWNER DECISION** on transition 8 (WHEN evaluation as an in-program guard register). +- It is the shortest lawful route found. M2 states M1's *meaning* as its guard, and both evaluate in one program, so nothing is handed off and nothing is stored. +- Everything it needs exists except the evaluation of WHEN, which `prepare` refuses by name. +- T8 shows the same algebra already works when the effects are written into one binding. diff --git a/.claude/probes/2026-11-03-d-xlog-meta-wire/probe/Cargo.toml b/.claude/probes/2026-11-03-d-xlog-meta-wire/probe/Cargo.toml new file mode 100644 index 000000000..d2391a3c1 --- /dev/null +++ b/.claude/probes/2026-11-03-d-xlog-meta-wire/probe/Cargo.toml @@ -0,0 +1,27 @@ +# D-XLOG-META-WIRE P0 falsifier: where does M1's result stop on its way to M2? +# Lab only. Own [workspace]; the root workspace never builds it. Consumes +# dt64, r2il and mask-risc unchanged. +[package] +name = "d-xlog-meta-wire-probe" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +lance-graph-contract = { path = "../../../../crates/lance-graph-contract" } +lance-graph-dt64 = { path = "../../../../crates/lance-graph-dt64" } +lance-graph-mask-risc = { path = "../../../../crates/lance-graph-mask-risc" } +lance-graph-r2il = { path = "../../../../crates/lance-graph-r2il" } +ogar-loco = { git = "https://github.com/AdaWorldAPI/OGAR", branch = "main" } +ogar-r2il = { git = "https://github.com/AdaWorldAPI/OGAR", branch = "main" } + +[patch."https://github.com/AdaWorldAPI/lance-graph"] +lance-graph-contract = { path = "../../../../crates/lance-graph-contract" } + +[patch."https://github.com/AdaWorldAPI/ndarray"] +ndarray = { path = "../../../../../ndarray" } + +[profile.release] +debug = 0 + +[workspace] diff --git a/.claude/probes/2026-11-03-d-xlog-meta-wire/probe/src/bin/v2_compose.rs b/.claude/probes/2026-11-03-d-xlog-meta-wire/probe/src/bin/v2_compose.rs new file mode 100644 index 000000000..9e986fa7c --- /dev/null +++ b/.claude/probes/2026-11-03-d-xlog-meta-wire/probe/src/bin/v2_compose.rs @@ -0,0 +1,396 @@ +//! D-XLOG-META-WIRE P0b: can two registered MethodRefs be resolved at cold +//! preparation into ONE executable program, with identity, revision, +//! permission, guard semantics and Frame/Focus/Scope still checkable? +//! +//! LAB ONLY. Production `GuardNotExecutable` is untouched: the guarded M2 is +//! still refused by `prepare` (F0). A lab composer emulates the PROPOSED +//! D-DT64-6 ruling with existing contracts only: +//! +//! - the IR is DT64's own `MethodBinding` / `Effect` (no new IR); +//! - M2's WHEN is `SetExpr::Sym(k)`. The contract leaves a `Sym`'s meaning to +//! the consumer ("What carries it is the consumer's business"), so the lab +//! binds `k` to (M1's `MethodRef`, the predicate field M1 writes) in a lab +//! table. That table is exactly the semantic edge the contract lacks; +//! - the composite is registered with `register_derived` and the position +//! re-routed to it (D5), then prepared and run by r2il unchanged. +//! +//! No mask crosses a program boundary; nothing is written to the WORLD. + +use lance_graph_contract::class_view::{ClassId, ClassView, WideFieldMask}; +use lance_graph_contract::ontology::{DisplayTemplate, FieldRef}; +use lance_graph_contract::set_algebra::{Logic, SetExpr}; +use lance_graph_contract::set_rules::{Population, Truth}; +use lance_graph_dt64::dr::{Focus, FrameRevisions, FrameStore, GuardRefusal, Guards, Scope}; +use lance_graph_dt64::{ + AlgebraRef, Arg, BindStore, ClassSchema, Effect, Extent, Guard, MethodBinding, MethodId, + MethodRef, Refusal, RouteVersion, SchemaRev, SigRev, ValueDemand, +}; +use lance_graph_mask_risc::{Foreign, LaneRef, Planes}; +use lance_graph_r2il::binding::{ + prepare, run, ExecPermit, ExecRefusal, FieldLanes, Prepared, R2ilLens, Receipt, Resolution, +}; +use lance_graph_r2il::FoldDialect; +use ogar_loco::{FnIndex, Vocabulary}; +use ogar_r2il::{R2ILVocabulary, CONCEPT_R2IL_FOLD}; +use std::collections::BTreeSet; + +const CLASS: u32 = 0x7F0B_0001; +const DERIVED: u32 = 0x7F0B_0002; // same concept, another app half: same ClassView +const OTHER: u32 = 0x7F0C_0001; +const CLASS_VIEW: ClassId = 0x7F0B; +const OTHER_VIEW: ClassId = 0x7F0C; +const LENS: u32 = (CONCEPT_R2IL_FOLD as u32) << 16; +const REV: SchemaRev = SchemaRev(1); +const R2IL: R2ilLens = R2ilLens { lens: LENS, schema_rev: REV }; + +const FIELDS: [&str; 8] = ["a", "b", "k", "m1", "s", "t", "u", "x"]; +const LANES: [Option; 8] = [Some(0), Some(1), Some(2), None, None, None, None, None]; +const F_A: u8 = 0; +const F_B: u8 = 1; +const F_K: u8 = 2; +const F_M1: u8 = 3; +const F_S: u8 = 4; +const F_T: u8 = 5; +const F_U: u8 = 6; +const F_X: u8 = 7; + +const WORDS: [&str; 3] = ["IntSLess", "IntAnd", "PopCount"]; +const W_LT: u8 = 0; +const W_AND: u8 = 1; +const W_POP: u8 = 2; +/// Words that end a program (fold opcodes) in this lens. +const FOLD_WORDS: [u8; 1] = [W_POP]; + +const NOW: (Focus, Scope) = (Focus { revision: 1 }, Scope { id: 1 }); +const POSITION: u16 = 3; +const REVS: FrameRevisions = FrameRevisions { source: 1, relation_map: 1, coordinates: 1 }; + +const M1: u32 = 1; // m1 = a < 5 (the semantic predicate) +const M1B: u32 = 2; // m1 = a < 7 (a second predicate on the same field) +const M2: u32 = 3; // WHEN Sym(1): t = b < 10; s = POP_COUNT(t) +const M3: u32 = 4; // WHEN Sym(3): x = k < 2; s = POP_COUNT(x) (cycle with M4) +const M4: u32 = 5; // WHEN Sym(4): x = k < 3; s = POP_COUNT(x) +const M_FOLDPRED: u32 = 6; // m1 = POP_COUNT(...) — a fold result as "predicate" +const M2_K3: u32 = 7; // M2 under Kleene logic +const COMPOSITE: u32 = 100; + +/// The lab guard-reference table: Sym k → (referenced method, the predicate +/// field it writes). The contract has no slot for this; see the module doc. +struct SymTable(Vec<(u32, MethodRef, u8)>); +impl SymTable { + fn get(&self, k: u32) -> Option<(&MethodRef, u8)> { + self.0.iter().find(|(s, _, _)| *s == k).map(|(_, r, f)| (r, *f)) + } +} + +struct Views(Vec, Vec); +fn refs(labels: &[&str], ns: &str) -> Vec { + labels.iter().map(|l| FieldRef::new(format!("lab:{ns}/{l}"), (*l).to_string())).collect() +} +impl ClassView for Views { + fn fields(&self, class: ClassId) -> &[FieldRef] { + match class { + CLASS_VIEW | OTHER_VIEW => &self.0, + c if c == CONCEPT_R2IL_FOLD => &self.1, + _ => &[], + } + } + fn template(&self, _c: ClassId) -> DisplayTemplate { DisplayTemplate::Card } + fn dolce_category_id(&self, _c: ClassId) -> u8 { 0 } +} +fn arities() -> Vec { + WORDS.iter().map(|w| { + (0..=u8::MAX).map(FnIndex).find(|&f| R2ILVocabulary.name(f) == Some(*w)) + .and_then(|f| R2ILVocabulary.stack_arity(f)).unwrap_or(1) + }).collect() +} +fn step(target: u8, word: u8, args: Vec) -> Effect { + Effect { target, algebra: AlgebraRef { lens: LENS, word }, args } +} +fn method(id: u32, on: &[u8], reads: &[u8], when: Option, effects: Vec) -> MethodBinding { + MethodBinding { + id: MethodId(id), sig_rev: SigRev(1), name: format!("m{id}"), + on: WideFieldMask::from_positions(on), when, + reads: WideFieldMask::from_positions(reads), effects, + population: Population::Distinct, truth: Truth::TwoValued, demand: ValueDemand::Values, + } +} +fn sym(k: u32) -> Option { Some(Guard::new(SetExpr::Sym(k), Logic::TwoValued)) } +fn schema() -> ClassSchema { ClassSchema { schema_rev: REV, field_count: FIELDS.len(), extent: Extent::DR64_CUBE } } + +fn base_methods() -> Vec { + use Arg::{Const, Field}; + let mut k3 = method(M2_K3, &[F_B], &[F_B], None, vec![ + step(F_T, W_LT, vec![Field(F_B), Const(10)]), + step(F_S, W_POP, vec![Field(F_T)]), + ]); + k3.when = Some(Guard::new(SetExpr::Sym(1), Logic::Kleene)); + vec![ + method(M1, &[F_A], &[F_A], None, vec![step(F_M1, W_LT, vec![Field(F_A), Const(5)])]), + method(M1B, &[F_A], &[F_A], None, vec![step(F_M1, W_LT, vec![Field(F_A), Const(7)])]), + method(M2, &[F_B], &[F_B], sym(1), vec![ + step(F_T, W_LT, vec![Field(F_B), Const(10)]), + step(F_S, W_POP, vec![Field(F_T)]), + ]), + method(M3, &[F_K], &[F_K], sym(3), vec![ + step(F_X, W_LT, vec![Field(F_K), Const(2)]), + step(F_S, W_POP, vec![Field(F_X)]), + ]), + method(M4, &[F_K], &[F_K], sym(4), vec![ + step(F_X, W_LT, vec![Field(F_K), Const(3)]), + step(F_S, W_POP, vec![Field(F_X)]), + ]), + method(M_FOLDPRED, &[F_A], &[F_A], None, vec![ + step(F_T, W_LT, vec![Field(F_A), Const(5)]), + step(F_M1, W_POP, vec![Field(F_T)]), + ]), + k3, + ] +} +fn mref(class: u32, b: &MethodBinding) -> MethodRef { MethodRef::of(class, b) } +fn find(id: u32) -> MethodBinding { base_methods().into_iter().find(|m| m.id == MethodId(id)).expect("known") } + +#[derive(Debug, PartialEq)] +enum ComposeRefusal { + Store(Refusal), + NotPermitted(MethodRef), + UnboundSym(u32), + OtherClass { referenced: u32, class: u32 }, + Cycle(Vec), + GuardShape, + ThreeValued, + PredicateIsFoldResult { method: u32 }, + PredicateNotWritten { method: u32, field: u8 }, + FieldClash(u8), +} + +/// What the composite was resolved from: every MethodRef (with its signature +/// revision), in dependency order. The composite's identity rides on these. +#[derive(Debug, Clone, PartialEq)] +struct Composed { binding: MethodBinding, deps: Vec } + +/// LAB composer (the proposed ruling, emulated). Resolves `m2`'s WHEN leaves +/// through `syms` into the referenced predicate's effects, prepends them, +/// and gates each of `m2`'s folds by the guard register. Refuses by name. +fn compose(store: &BindStore, m2: &MethodRef, syms: &SymTable, permit: &ExecPermit, id: u32) -> Result { + let mut deps = vec![]; + let mut stack = vec![]; + let prefix = resolve(store, m2, syms, permit, &mut stack, &mut deps)?; + let b2 = store.admit(m2).map_err(ComposeRefusal::Store)?; + let (pre_effects, guard_field, pre_reads) = prefix.expect("m2 is guarded"); + let written: BTreeSet = pre_effects.iter().map(|e| e.target).collect(); + let mut effects = pre_effects.clone(); + for e in &b2.effects { + if written.contains(&e.target) || e.target == F_U { return Err(ComposeRefusal::FieldClash(e.target)); } + if FOLD_WORDS.contains(&e.algebra.word) { + // Gate the fold's mask operand by the guard register (in-program). + let Some(Arg::Field(m)) = e.args.first().copied() else { return Err(ComposeRefusal::GuardShape) }; + effects.push(step(F_U, W_AND, vec![Arg::Field(guard_field), Arg::Field(m)])); + let mut e2 = e.clone(); + e2.args[0] = Arg::Field(F_U); + effects.push(e2); + } else { + effects.push(e.clone()); + } + } + deps.push(m2.clone()); + let binding = MethodBinding { + id: MethodId(id), sig_rev: SigRev(1), name: format!("compose{id}"), + on: b2.on.clone(), when: None, + reads: b2.reads.union(&pre_reads), effects, + population: b2.population, truth: Truth::TwoValued, demand: b2.demand, + }; + Ok(Composed { binding, deps }) +} + +type Prefix = Option<(Vec, u8, WideFieldMask)>; +fn resolve(store: &BindStore, r: &MethodRef, syms: &SymTable, permit: &ExecPermit, stack: &mut Vec, deps: &mut Vec) -> Result { + if stack.contains(&r.method.0) { + let mut c = stack.clone(); + c.push(r.method.0); + return Err(ComposeRefusal::Cycle(c)); + } + let b = store.admit(r).map_err(ComposeRefusal::Store)?; + if !permit.methods.contains(r) { return Err(ComposeRefusal::NotPermitted(r.clone())); } + let Some(g) = &b.when else { return Ok(None) }; + if g.logic != Logic::TwoValued || b.truth != Truth::TwoValued { return Err(ComposeRefusal::ThreeValued); } + let SetExpr::Sym(k) = g.expr else { return Err(ComposeRefusal::GuardShape) }; + let (m1, field) = syms.get(k).ok_or(ComposeRefusal::UnboundSym(k))?; + if m1.class != r.class { return Err(ComposeRefusal::OtherClass { referenced: m1.class, class: r.class }); } + stack.push(r.method.0); + let nested = resolve(store, m1, syms, permit, stack, deps)?; + stack.pop(); + let b1 = store.admit(m1).map_err(ComposeRefusal::Store)?; + let last = b1.effects.iter().rposition(|e| e.target == field) + .ok_or(ComposeRefusal::PredicateNotWritten { method: m1.method.0, field })?; + let pre: Vec = b1.effects[..=last].to_vec(); + if pre.iter().any(|e| FOLD_WORDS.contains(&e.algebra.word)) { + return Err(ComposeRefusal::PredicateIsFoldResult { method: m1.method.0 }); + } + if nested.is_some() { return Err(ComposeRefusal::GuardShape); } // one level in this probe + deps.push(m1.clone()); + Ok(Some((pre, field, b1.reads.clone()))) +} + +struct Data { a: Vec, b: Vec, k: Vec } +impl Data { + fn new(n: usize) -> Data { + Data { + a: (0..n).map(|i| (i % 37) as i32 - 10).collect(), + b: (0..n).map(|i| ((7 * i) % 23) as i32).collect(), + k: (0..n).map(|i| (i % 5) as u32).collect(), + } + } + fn both(&self, ta: i32) -> i64 { self.a.iter().zip(&self.b).filter(|(&a, &b)| a < ta && b < 10).count() as i64 } + fn b_only(&self) -> i64 { self.b.iter().filter(|&&b| b < 10).count() as i64 } +} + +struct World { views: Views, store: BindStore, frames: FrameStore, version: RouteVersion } +impl World { + fn new() -> World { + let mut store = BindStore::new(); + store.declare_algebra(LENS, REV, Extent::DR64_CUBE, arities()).expect("lens"); + store.register(CLASS, schema(), base_methods()).expect("register"); + store.register(OTHER, schema(), vec![method(M1, &[F_A], &[F_A], None, vec![step(F_M1, W_LT, vec![Arg::Field(F_A), Arg::Const(5)])])]).expect("other"); + let version = store.route(POSITION, CLASS).expect("route"); + World { views: Views(refs(&FIELDS, "c"), refs(&WORDS, "r2il")), store, frames: FrameStore::new(REVS), version } + } + fn prepare(&self, r: &MethodRef, rev: SchemaRev) -> Result { + let g = Guards { frame: self.frames.seal(), focus: NOW.0, scope: NOW.1 }; + prepare(&self.store, &self.views, R2IL, + &Resolution { method: r, schema_rev: rev, route: (self.version, POSITION), guards: &g }, + &self.frames, NOW) + } +} +fn exec(w: &World, plan: &Prepared, d: &Data, permit: &ExecPermit, now: (Focus, Scope)) -> Result { + let lanes = [LaneRef::I32(&d.a), LaneRef::I32(&d.b), LaneRef::U32(&d.k)]; + let planes = Planes { n_rows: d.a.len(), masks: &[], lanes: &lanes }; + let foreign = Foreign::NONE; + let mut dl = FoldDialect::new(&planes, &foreign); + run(plan, &w.store, &w.frames, now, permit, FieldLanes { lanes: &LANES }, &mut dl) +} + +struct Tally(u32, u32); +impl Tally { + fn check(&mut self, id: &str, ok: bool, detail: String) { + if ok { self.0 += 1 } else { self.1 += 1 } + println!("{} {:<5} {}", if ok { "PASS" } else { "FAIL" }, id, detail); + } +} + +fn main() { + let mut t = Tally(0, 0); + let mut w = World::new(); + let d = Data::new(1_000); + let (r1, r1b, r2) = (mref(CLASS, &find(M1)), mref(CLASS, &find(M1B)), mref(CLASS, &find(M2))); + let syms = SymTable(vec![(1, r1.clone(), F_M1), (3, mref(CLASS, &find(M4)), F_X), (4, mref(CLASS, &find(M3)), F_X)]); + let permit_both = ExecPermit { methods: vec![r1.clone(), r2.clone()] }; + + // F0 — production unchanged: the guarded M2 is still refused by prepare. + t.check("F0", matches!(w.prepare(&r2, REV), Err(ExecRefusal::GuardNotExecutable)), "prepare(M2 with WHEN) = GuardNotExecutable (unchanged)".into()); + + // F1 — M1 and M2 are independently registered and admitted. + t.check("F1", w.store.admit(&r1).is_ok() && w.store.admit(&r2).is_ok() && r1.class == r2.class && r1.method != r2.method, + format!("M1 {:?}/{:?}, M2 {:?}/{:?} (guard witness carried: {})", r1.method, r1.sig_rev, r2.method, r2.sig_rev, r2.guard.is_some())); + + // F2 — M2's WHEN names a symbol bound to M1's semantic predicate, not a slot. + t.check("F2", matches!(find(M2).when, Some(Guard { expr: SetExpr::Sym(1), .. })) && syms.get(1) == Some((&r1, F_M1)), + "WHEN = Sym(1); lab table: 1 → (M1, field m1)".into()); + + // F3/F4 — cold preparation resolves both into one binding → one program. + let c = compose(&w.store, &r2, &syms, &permit_both, COMPOSITE).expect("composes"); + t.check("F3a", c.deps == vec![r1.clone(), r2.clone()], format!("composite deps (identity + sig rev): {:?}", c.deps.iter().map(|r| (r.method.0, r.sig_rev.0)).collect::>())); + // F8 — register_derived + re-route succeeds; the base stays readable. + let mut derived_methods = base_methods(); + derived_methods.push(c.binding.clone()); + let reg = w.store.register_derived(CLASS, DERIVED, schema(), derived_methods); + let v2 = w.store.route(POSITION, DERIVED); + t.check("F8a", reg.is_ok() && v2.is_ok(), format!("register_derived {reg:?}, route {v2:?}")); + t.check("F8b", w.store.resolve(&w.views, CLASS, REV).is_ok() + && w.store.register(CLASS, schema(), { let mut m = base_methods(); m.push(c.binding.clone()); m }) == Err(Refusal::BehaviourImmutable(CLASS)), + "base class still resolves; adding the composite to the base is BehaviourImmutable".into()); + let old_version = w.version; + w.version = v2.expect("routed"); + let rc = mref(DERIVED, &c.binding); + let permit_c = ExecPermit { methods: vec![rc.clone()] }; + let plan = w.prepare(&rc, REV).expect("composite prepares"); + let rec = exec(&w, &plan, &d, &permit_c, NOW).expect("composite runs"); + t.check("F3b", rec.programs_run == 1, format!("one native fold program: programs_run = {}", rec.programs_run)); + t.check("F4", rec.output.values == vec![(F_S, d.both(5))], format!("{:?} = oracle |a<5 ∧ b<10| = {}", rec.output.values, d.both(5))); + + // F5 — disable M1's predicate: compose with the guard ignored → diverges. + let b2 = find(M2); + let unguarded = MethodBinding { id: MethodId(101), when: None, ..b2 }; + let mut dm = base_methods(); + dm.push(unguarded.clone()); + let mut w5 = World::new(); // its own world: re-routing here must not touch `w` + w5.store.register_derived(CLASS, 0x7F0B_0003, schema(), dm).expect("derived 3"); + w5.version = w5.store.route(POSITION, 0x7F0B_0003).expect("route 3"); + let ru = mref(0x7F0B_0003, &unguarded); + let pu = w5.prepare(&ru, REV).expect("prepares"); + let reu = exec(&w5, &pu, &d, &ExecPermit { methods: vec![ru] }, NOW).expect("runs"); + t.check("F5", reu.output.values == vec![(F_S, d.b_only())] && d.b_only() != d.both(5), + format!("M1's predicate removed: {:?} = |b<10| {} ≠ {}", reu.output.values, d.b_only(), d.both(5))); + // The referenced predicate is load-bearing: re-binding Sym(1) to M1B changes the answer. + let syms_b = SymTable(vec![(1, r1b.clone(), F_M1)]); + let cb = compose(&w.store, &r2, &syms_b, &ExecPermit { methods: vec![r1b.clone(), r2.clone()] }, 102).expect("composes"); + t.check("F5b", cb.binding.effects != c.binding.effects && cb.deps[0] == r1b, "Sym(1) bound to M1B composes a different program".into()); + + // F6 — refusals. + t.check("F6a", compose(&w.store, &r2, &syms, &ExecPermit { methods: vec![r2.clone()] }, 103) == Err(ComposeRefusal::NotPermitted(r1.clone())), + "permit lacks M1 → NotPermitted(M1)".into()); + let other_m1 = MethodRef { class: OTHER, ..r1.clone() }; + let syms_o = SymTable(vec![(1, other_m1.clone(), F_M1)]); + t.check("F6b", compose(&w.store, &r2, &syms_o, &ExecPermit { methods: vec![other_m1, r2.clone()] }, 104) == Err(ComposeRefusal::OtherClass { referenced: OTHER, class: CLASS }), + "Sym bound to a method of another class → OtherClass".into()); + let stale_m1 = MethodRef { sig_rev: SigRev(2), ..r1.clone() }; + let syms_s = SymTable(vec![(1, stale_m1.clone(), F_M1)]); + t.check("F6c", matches!(compose(&w.store, &r2, &syms_s, &ExecPermit { methods: vec![stale_m1, r2.clone()] }, 105), Err(ComposeRefusal::Store(Refusal::SignatureRevisionMismatch { .. }))), + "referenced M1 at another signature revision → SignatureRevisionMismatch".into()); + let f6d = w.prepare(&rc, SchemaRev(2)); + t.check("F6d", matches!(f6d, Err(ExecRefusal::Binding(_))), format!("composite at another schema revision → {:?}", f6d.map(|_| ()))); + let rs = exec(&w, &plan, &d, &permit_c, (NOW.0, Scope { id: 2 })); + t.check("F6e", rs == Err(ExecRefusal::Guards(GuardRefusal::ScopeChanged)), format!("another scope: {rs:?}")); + let f6f = exec(&w, &plan, &d, &ExecPermit { methods: vec![r1.clone(), r2.clone()] }, NOW); + t.check("F6f", f6f == Err(ExecRefusal::NotPermitted), format!("permits for M1 and M2 do not run the composite: {f6f:?}")); + // A plan prepared before the re-route is refused after it (route moved). + let g = Guards { frame: w.frames.seal(), focus: NOW.0, scope: NOW.1 }; + let old = prepare(&w.store, &w.views, R2IL, &Resolution { method: &mref(CLASS, &find(M1)), schema_rev: REV, route: (old_version, POSITION), guards: &g }, &w.frames, NOW); + t.check("F6g", matches!(old, Err(ExecRefusal::RouteChanged { .. })), "a base-class plan after the re-route → RouteChanged".into()); + w.frames.advance(FrameRevisions { source: 2, ..REVS }); + t.check("F6h", matches!(exec(&w, &plan, &d, &permit_c, NOW), Err(ExecRefusal::Guards(GuardRefusal::Frame(_)))), "after the source moved → stale frame refused".into()); + + // F7 — cyclic method dependency. + let r3 = mref(CLASS, &find(M3)); + let r4 = mref(CLASS, &find(M4)); + let cy = compose(&w.store, &r3, &syms, &ExecPermit { methods: vec![r3.clone(), r4] }, 106); + t.check("F7", matches!(cy, Err(ComposeRefusal::Cycle(ref p)) if p == &vec![M3, M4, M3]), format!("{cy:?}")); + // Neighbours of F7: a fold result as "predicate", and a three-valued guard. + let syms_f = SymTable(vec![(1, mref(CLASS, &find(M_FOLDPRED)), F_M1)]); + t.check("F7b", compose(&w.store, &r2, &syms_f, &ExecPermit { methods: vec![mref(CLASS, &find(M_FOLDPRED)), r2.clone()] }, 107) == Err(ComposeRefusal::PredicateIsFoldResult { method: M_FOLDPRED }), + "a predicate computed from a fold result → refused (FoldResultFeedsFold, kept)".into()); + let rk = mref(CLASS, &find(M2_K3)); + t.check("F7c", compose(&w.store, &rk, &syms, &ExecPermit { methods: vec![rk.clone(), r1.clone()] }, 108) == Err(ComposeRefusal::ThreeValued), "Kleene WHEN → refused".into()); + + // F9 — in-program narrowing is not a DT64 population Scope. + let s1 = exec(&w, &plan, &d, &permit_c, NOW).map(|_| ()); + t.check("F9a", s1.is_err(), "(frame already advanced; F9 restated below on a fresh world)".into()); + let mut w2 = World::new(); + w2.store.register_derived(CLASS, DERIVED, schema(), { let mut m = base_methods(); m.push(c.binding.clone()); m }).expect("derived"); + w2.version = w2.store.route(POSITION, DERIVED).expect("route"); + let p2 = w2.prepare(&rc, REV).expect("prepares"); + let a = exec(&w2, &p2, &d, &permit_c, NOW).expect("runs"); + let receipt_has_no_population = a.output.intermediates.iter().all(|f| [F_M1, F_T, F_U].contains(f)); + t.check("F9b", receipt_has_no_population && a.output.values == vec![(F_S, d.both(5))], + format!("narrowing lives in the program (intermediates {:?}); the scope stays id {}", a.output.intermediates, NOW.1.id)); + t.check("F9c", exec(&w2, &p2, &d, &permit_c, (NOW.0, Scope { id: 7 })).is_err(), "the same narrowing under another Scope id is still refused: Scope is policy, not population".into()); + + // The witness gap (the missing semantic edge, measured). + let wit_m2 = find(M2).when.unwrap().witness(); + let wit_same = Guard::new(SetExpr::Sym(1), Logic::TwoValued).witness(); + t.check("GAP", wit_m2 == wit_same && c.deps[0] != cb.deps[0], + "M2's GuardWitness is identical whether Sym(1) means M1 (a<5) or M1B (a<7): the witness cannot name the referenced method".into()); + + println!("== checks: {} passed, {} failed", t.0, t.1); + if t.1 > 0 { std::process::exit(1); } +} diff --git a/.claude/probes/2026-11-03-d-xlog-meta-wire/probe/src/main.rs b/.claude/probes/2026-11-03-d-xlog-meta-wire/probe/src/main.rs new file mode 100644 index 000000000..64a4eec61 --- /dev/null +++ b/.claude/probes/2026-11-03-d-xlog-meta-wire/probe/src/main.rs @@ -0,0 +1,311 @@ +//! D-XLOG-META-WIRE P0: where does M1's result stop on its way to M2? +//! +//! Lab only, own workspace. Every call is a production API, used unchanged: +//! `lance_graph_dt64::{BindStore, Resolved::methods_on, methods_touching, +//! MethodRef}`, `lance_graph_r2il::binding::{prepare, run}`, `FoldDialect`. +//! Nothing here adds a relation, a scope store, a scheduler or an evaluator; +//! the probe only shows which of the existing contracts carry M1's result and +//! where the chain stops. +//! +//! Class fields: `a`, `b`, `k` (resident lanes 0, 1, 2), then `m1`, `s`, `t`, +//! `u`, `x` (computed, no lane). Oracle: scalar loops over the lane slices. + +use lance_graph_contract::class_view::{ClassId, ClassView, WideFieldMask}; +use lance_graph_contract::ontology::{DisplayTemplate, FieldRef}; +use lance_graph_contract::set_algebra::{Logic, SetExpr}; +use lance_graph_contract::set_rules::{Population, Truth}; +use lance_graph_dt64::dr::{Focus, FrameRevisions, FrameStore, GuardRefusal, Guards, Scope}; +use lance_graph_dt64::{ + AlgebraRef, Arg, BindStore, ClassSchema, Effect, Extent, Guard, MethodBinding, MethodId, + MethodRef, RouteVersion, SchemaRev, SigRev, ValueDemand, +}; +use lance_graph_mask_risc::{Foreign, LaneRef, Planes}; +use lance_graph_r2il::binding::{ + prepare, run, ExecPermit, ExecRefusal, FieldLanes, Prepared, R2ilLens, Receipt, Resolution, +}; +use lance_graph_r2il::{FoldDialect, FoldError, Val, INT_S_LESS, LOAD, POP_COUNT}; +use ogar_loco::{FnIndex, Vocabulary}; +use ogar_r2il::{R2ILVocabulary, CONCEPT_R2IL_FOLD, VIA}; + +const CLASS: u32 = 0x7F0B_0001; +const CLASS_VIEW: ClassId = 0x7F0B; +const LENS: u32 = (CONCEPT_R2IL_FOLD as u32) << 16; +const REV: SchemaRev = SchemaRev(1); +const R2IL: R2ilLens = R2ilLens { lens: LENS, schema_rev: REV }; + +const FIELDS: [&str; 8] = ["a", "b", "k", "m1", "s", "t", "u", "x"]; +const LANES: [Option; 8] = [Some(0), Some(1), Some(2), None, None, None, None, None]; +const F_A: u8 = 0; +const F_B: u8 = 1; +const F_K: u8 = 2; +const F_M1: u8 = 3; +const F_S: u8 = 4; +const F_T: u8 = 5; +const F_U: u8 = 6; +const F_X: u8 = 7; + +const WORDS: [&str; 5] = ["IntSLess", "IntAnd", "PopCount", "VIA", "IntEqual"]; +const W_LT: u8 = 0; +const W_AND: u8 = 1; +const W_POP: u8 = 2; +const W_VIA: u8 = 3; +const W_EQ: u8 = 4; + +const NOW: (Focus, Scope) = (Focus { revision: 1 }, Scope { id: 1 }); +const POSITION: u16 = 3; +const REVS: FrameRevisions = FrameRevisions { source: 1, relation_map: 1, coordinates: 1 }; + +// Methods. +const M1: u32 = 1; // ON a: m1 = a < 5 (a masked result, no fold) +const M1_COUNT: u32 = 2; // ON a: m1 = a < 5; s = POP_COUNT(m1) (M1 with a scalar answer) +const M2: u32 = 3; // ON m1: t = b < 10; u = m1 AND t; s = POP_COUNT(u) +const M2_WHEN: u32 = 4; // ON m1, WHEN over m1: t = b < 10; s = POP_COUNT(t) +const FUSED: u32 = 5; // M1's and M2's effects in ONE binding +const FUSED_CUT: u32 = 6; // FUSED with the M1 → M2 operand removed (u = t) +const XY_VIA: u32 = 7; // a cross-row hop as a method effect (VIA) +const UNRELATED: u32 = 8; // ON k: x = k == 1; s = POP_COUNT(x) + +struct Views { class: Vec, lens: Vec } +fn refs(labels: &[&str], ns: &str) -> Vec { + labels.iter().map(|l| FieldRef::new(format!("lab:{ns}/{l}"), (*l).to_string())).collect() +} +impl ClassView for Views { + fn fields(&self, class: ClassId) -> &[FieldRef] { + match class { + CLASS_VIEW => &self.class, + c if c == CONCEPT_R2IL_FOLD => &self.lens, + _ => &[], + } + } + fn template(&self, _class: ClassId) -> DisplayTemplate { DisplayTemplate::Card } + fn dolce_category_id(&self, _class: ClassId) -> u8 { 0 } +} + +fn arities() -> Vec { + WORDS.iter().map(|w| { + (0..=u8::MAX).map(FnIndex).find(|&f| R2ILVocabulary.name(f) == Some(*w)) + .and_then(|f| R2ILVocabulary.stack_arity(f)).unwrap_or(1) + }).collect() +} +fn step(target: u8, word: u8, args: Vec) -> Effect { + Effect { target, algebra: AlgebraRef { lens: LENS, word }, args } +} +fn method(id: u32, on: &[u8], reads: &[u8], effects: Vec) -> MethodBinding { + MethodBinding { + id: MethodId(id), sig_rev: SigRev(1), name: format!("m{id}"), + on: WideFieldMask::from_positions(on), when: None, + reads: WideFieldMask::from_positions(reads), effects, + population: Population::Distinct, truth: Truth::TwoValued, demand: ValueDemand::Values, + } +} +fn methods() -> Vec { + use Arg::{Const, Field}; + let m1 = || step(F_M1, W_LT, vec![Field(F_A), Const(5)]); + let mut m2_when = method(M2_WHEN, &[F_M1], &[F_M1, F_B], vec![ + step(F_T, W_LT, vec![Field(F_B), Const(10)]), + step(F_S, W_POP, vec![Field(F_T)]), + ]); + m2_when.when = Some(Guard::new(SetExpr::Sym(u32::from(F_M1)), Logic::TwoValued)); + vec![ + method(M1, &[F_A], &[F_A], vec![m1()]), + method(M1_COUNT, &[F_A], &[F_A], vec![m1(), step(F_S, W_POP, vec![Field(F_M1)])]), + method(M2, &[F_M1], &[F_M1, F_B], vec![ + step(F_T, W_LT, vec![Field(F_B), Const(10)]), + step(F_U, W_AND, vec![Field(F_M1), Field(F_T)]), + step(F_S, W_POP, vec![Field(F_U)]), + ]), + m2_when, + method(FUSED, &[F_A], &[F_A, F_B], vec![ + m1(), + step(F_T, W_LT, vec![Field(F_B), Const(10)]), + step(F_U, W_AND, vec![Field(F_M1), Field(F_T)]), + step(F_S, W_POP, vec![Field(F_U)]), + ]), + method(FUSED_CUT, &[F_A], &[F_A, F_B], vec![ + m1(), + step(F_T, W_LT, vec![Field(F_B), Const(10)]), + step(F_S, W_POP, vec![Field(F_T)]), + ]), + method(XY_VIA, &[F_A], &[F_A], vec![step(F_X, W_VIA, vec![Const(1), Const(0)])]), + method(UNRELATED, &[F_K], &[F_K], vec![ + step(F_X, W_EQ, vec![Field(F_K), Const(1)]), + step(F_S, W_POP, vec![Field(F_X)]), + ]), + ] +} + +struct Fx { views: Views, store: BindStore, version: RouteVersion, frames: FrameStore } +impl Fx { + fn new() -> Fx { + let mut store = BindStore::new(); + store.declare_algebra(LENS, REV, Extent::DR64_CUBE, arities()).expect("lens"); + let schema = ClassSchema { schema_rev: REV, field_count: FIELDS.len(), extent: Extent::DR64_CUBE }; + store.register(CLASS, schema, methods()).expect("register"); + let version = store.route(POSITION, CLASS).expect("route"); + Fx { + views: Views { class: refs(&FIELDS, "class"), lens: refs(&WORDS, "r2il") }, + store, version, frames: FrameStore::new(REVS), + } + } + /// The faithful reference (carries the guard witness when there is one). + fn mref(&self, id: u32) -> MethodRef { + let all = methods(); + MethodRef::of(CLASS, all.iter().find(|m| m.id == MethodId(id)).expect("known")) + } + fn prepare(&self, id: u32) -> Result { + let r = self.mref(id); + let g = Guards { frame: self.frames.seal(), focus: NOW.0, scope: NOW.1 }; + prepare(&self.store, &self.views, R2IL, + &Resolution { method: &r, schema_rev: REV, route: (self.version, POSITION), guards: &g }, + &self.frames, NOW) + } +} + +struct Data { a: Vec, b: Vec, k: Vec } +impl Data { + fn new(n: usize) -> Data { + Data { + a: (0..n).map(|i| (i % 37) as i32 - 10).collect(), + b: (0..n).map(|i| ((7 * i) % 23) as i32).collect(), + k: (0..n).map(|i| (i % 5) as u32).collect(), + } + } + fn oracle_m1(&self) -> i64 { self.a.iter().filter(|&&a| a < 5).count() as i64 } + fn oracle_both(&self) -> i64 { + self.a.iter().zip(&self.b).filter(|(&a, &b)| a < 5 && b < 10).count() as i64 + } + fn oracle_b(&self) -> i64 { self.b.iter().filter(|&&b| b < 10).count() as i64 } +} + +fn exec(fx: &Fx, plan: &Prepared, data: &Data, ids: &[u32], now: (Focus, Scope), lanes_map: &[Option]) -> Result { + let lanes = [LaneRef::I32(&data.a), LaneRef::I32(&data.b), LaneRef::U32(&data.k)]; + let planes = Planes { n_rows: data.a.len(), masks: &[], lanes: &lanes }; + let foreign = Foreign::NONE; + let mut d = FoldDialect::new(&planes, &foreign); + let permit = ExecPermit { methods: ids.iter().map(|&i| fx.mref(i)).collect() }; + run(plan, &fx.store, &fx.frames, now, &permit, FieldLanes { lanes: lanes_map }, &mut d) +} + +struct Tally(u32, u32); +impl Tally { + fn check(&mut self, id: &str, ok: bool, detail: String) { + if ok { self.0 += 1 } else { self.1 += 1 } + println!("{} {:<6} {}", if ok { "PASS" } else { "FAIL" }, id, detail); + } +} + +fn ids<'a>(it: impl Iterator) -> Vec { + it.map(|m| m.id.0).collect() +} + +fn main() { + let mut t = Tally(0, 0); + let fx = Fx::new(); + let data = Data::new(1_000); + + // T1: trigger (ON) vs dependency footprint, on the change of field m1. + let res = fx.store.resolve(&fx.views, CLASS, REV).expect("resolve"); + let on = ids(res.methods_on(F_M1)); + let mask = WideFieldMask::from_positions(&[F_M1]); + let touching = ids(res.methods_touching(&mask)); + t.check("T1a", on == vec![M2, M2_WHEN], format!("methods_on(m1) = {on:?}: only methods whose ON names m1")); + t.check("T1b", touching.contains(&M1) && !on.contains(&M1), + format!("methods_touching(m1) = {touching:?}: includes M1, which WRITES m1 but is not triggered by it")); + t.check("T1c", !on.contains(&UNRELATED) && !touching.contains(&UNRELATED), "UNRELATED (ON k) in neither".into()); + + // T2: the receipt boundary. M1's masked result leaves only its field position. + let p1 = fx.prepare(M1).expect("M1 prepares"); + let r1 = exec(&fx, &p1, &data, &[M1], NOW, &LANES).expect("M1 runs"); + t.check("T2a", r1.output.values.is_empty() && r1.output.intermediates == vec![F_M1] && r1.programs_run == 0, + format!("M1 receipt: values {:?}, intermediates {:?}, programs {}", r1.output.values, r1.output.intermediates, r1.programs_run)); + let p1c = fx.prepare(M1_COUNT).expect("M1_COUNT prepares"); + let r1c = exec(&fx, &p1c, &data, &[M1_COUNT], NOW, &LANES).expect("runs"); + t.check("T2b", r1c.output.values == vec![(F_S, data.oracle_m1())] && r1c.output.intermediates == vec![F_M1], + format!("M1_COUNT receipt: values {:?} (oracle {}), mask still only {:?}", r1c.output.values, data.oracle_m1(), r1c.output.intermediates)); + + // T3: M2 cannot be handed M1's result. `run` takes no receipt; a field + // with no earlier value in the SAME run is read from a resident lane. + let p2 = fx.prepare(M2).expect("M2 prepares (preparation runs nothing)"); + let r2 = exec(&fx, &p2, &data, &[M2], NOW, &LANES); + t.check("T3a", r2 == Err(ExecRefusal::FieldUnbound { effect: 1, field: F_M1 }), format!("M2 alone: {r2:?}")); + // Binding m1 to a lane index that names no I32/U32/U64 lane: FieldLanes + // addresses value lanes only, never a mask. + let mut lm = LANES; + lm[F_M1 as usize] = Some(3); + let r2b = exec(&fx, &p2, &data, &[M2], NOW, &lm); + t.check("T3b", r2b == Err(ExecRefusal::LaneUnusable { effect: 1, field: F_M1, lane: 3 }), format!("M2 with m1 bound to a non-lane: {r2b:?}")); + + // T4: a mask slot does not outlive the run that minted it. + let lanes = [LaneRef::I32(&data.a), LaneRef::I32(&data.b), LaneRef::U32(&data.k)]; + let planes = Planes { n_rows: data.a.len(), masks: &[], lanes: &lanes }; + let foreign = Foreign::NONE; + let mut d = FoldDialect::new(&planes, &foreign); + let mut st = vec![Val::Scalar(0)]; + d.call(LOAD, [1, 0, 0], &mut st).expect("load a"); + st.push(Val::Scalar(5)); + d.call(INT_S_LESS, [0, 0, 0], &mut st).expect("a < 5"); + let slot = st.pop().expect("slot"); + let mut live = vec![slot]; + let ok = d.call(POP_COUNT, [0, 0, 0], &mut live).map(|_| live.clone()); + t.check("T4a", ok == Ok(vec![Val::Scalar(data.oracle_m1())]), format!("same epoch: {ok:?}")); + let mut st = vec![Val::Scalar(0)]; + d.call(LOAD, [1, 0, 0], &mut st).expect("load a"); + st.push(Val::Scalar(5)); + d.call(INT_S_LESS, [0, 0, 0], &mut st).expect("a < 5"); + let slot = st.pop().expect("slot"); + d.discard_pending(); // what `run` does after every run + let mut stale = vec![slot]; + let e = d.call(POP_COUNT, [0, 0, 0], &mut stale); + t.check("T4b", e == Err(FoldError::StaleSlot), format!("after discard_pending (end of a run): {e:?}")); + + // T5: WHEN cannot carry M1's result as M2's scope. + let pw = fx.prepare(M2_WHEN); + t.check("T5", pw.as_ref().map(|_| ()) == Err(&ExecRefusal::GuardNotExecutable), format!("M2 with WHEN over m1: {:?}", pw.map(|_| ()))); + + // T6: the cross-row XY hop is not an executable method effect. + let pv = fx.prepare(XY_VIA); + t.check("T6", matches!(pv, Err(ExecRefusal::NotExecutable { effect: 0, opcode }) if opcode == VIA), format!("VIA as an effect: {:?}", pv.map(|_| ()))); + + // T7: Scope is an id compared for equality; a different scope refuses. + let pf = fx.prepare(FUSED).expect("FUSED prepares"); + let narrower = (NOW.0, Scope { id: 2 }); + let rs = exec(&fx, &pf, &data, &[FUSED], narrower, &LANES); + t.check("T7", rs == Err(ExecRefusal::Guards(GuardRefusal::ScopeChanged)), format!("FUSED under another scope: {rs:?}")); + + // T8: the furthest lawful route: M1's mask feeds M2's fold INSIDE one + // binding (a Data-origin mask into a fold; no FoldResultFeedsFold). + let rf = exec(&fx, &pf, &data, &[FUSED], NOW, &LANES).expect("FUSED runs"); + t.check("T8a", rf.output.values == vec![(F_S, data.oracle_both())], + format!("FUSED = {:?}, oracle |a<5 ∧ b<10| = {}", rf.output.values, data.oracle_both())); + let pc = fx.prepare(FUSED_CUT).expect("prepares"); + let rc = exec(&fx, &pc, &data, &[FUSED_CUT], NOW, &LANES).expect("runs"); + t.check("T8b", rc.output.values == vec![(F_S, data.oracle_b())] && data.oracle_b() != data.oracle_both(), + format!("M1 operand removed: {:?} = |b<10| {} ≠ {}", rc.output.values, data.oracle_b(), data.oracle_both())); + let mut d2 = Data::new(1_000); + let i = (0..d2.a.len()).find(|&i| d2.a[i] < 5 && d2.b[i] < 10).expect("a supporting row"); + d2.a[i] = 30; // delete one support of M1 + let rd = exec(&fx, &pf, &d2, &[FUSED], NOW, &LANES).expect("runs"); + t.check("T8c", rd.output.values == vec![(F_S, data.oracle_both() - 1)], format!("one M1 support removed: {:?}", rd.output.values)); + let mut d3 = Data::new(1_000); + for k in d3.k.iter_mut() { *k = (*k + 1) % 5; } + let ru = exec(&fx, &pf, &d3, &[FUSED], NOW, &LANES).expect("runs"); + t.check("T8d", ru.output.values == rf.output.values, "unrelated lane k changed: answer unchanged".into()); + let mut frames2 = FrameStore::new(REVS); + let g_old = frames2.seal(); + frames2.advance(FrameRevisions { source: 2, ..REVS }); + t.check("T8e", Guards { frame: g_old, focus: NOW.0, scope: NOW.1 }.check(&frames2, NOW).is_err(), + "a frame sealed before the source moved is refused".into()); + + // T9: fusing is not M1 → M2: the class's behaviour is immutable, so the + // fused binding cannot be added beside M1 and M2 later. + let mut s2 = Fx::new().store; + let schema = ClassSchema { schema_rev: REV, field_count: FIELDS.len(), extent: Extent::DR64_CUBE }; + let mut more = methods(); + more.push(method(99, &[F_A], &[F_A], vec![step(F_M1, W_LT, vec![Arg::Field(F_A), Arg::Const(6)])])); + let reg = s2.register(CLASS, schema, more); + t.check("T9", matches!(reg, Err(lance_graph_dt64::Refusal::BehaviourImmutable(c)) if c == CLASS), + format!("adding a composed method to a registered class: {reg:?}")); + + println!("== checks: {} passed, {} failed", t.0, t.1); + if t.1 > 0 { std::process::exit(1); } +}