Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .claude/board/entries/2026-11-11-cg-beh-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 2026-11-11 — CG-BEH-2: ON, WHEN and DO read as separate registered facts

**Status:** MEASURED (lab). `crates/cg-beh-probe`, `.claude/probes/2026-11-11-cg-beh-2/`. Owner EXO-155; board `P-EXO-18`.

A read-only lab crate reads a method's ON positions, WHEN guard and ordered DO steps from a
`BindStore` (`method_ref`, `admit`, `resolve`), for its own class and for Foundry's `CanCook`, and
compares two such readings. 23 tests; 15 of 15 disable runs red.

**Findings.**
- A guard change does not move the binding digest; only `admit`'s witness comparison catches it.
- `admit` takes no `ClassView`. Over a reordered basis of the same count `resolve` refuses and
`admit` still admits the old reference.
- Registration does not check guard positions against the basis or the reads.
- `on` is registered and never evaluated by dt64, r2il, xlog or the Foundry lab.
- `CanCook` (16 methods) has no DO step, so no polyfill word implementation reaches it through DO.

**OPEN.** Executed parity per word. Source attribution for ON and WHEN. Whether the guard belongs in
the binding digest and whether `admit` should see the basis are owner questions for EXO-155, not
decided here. `.mermaid/05` and `/06` still describe PR #1996 as open (it is merged).
3 changes: 2 additions & 1 deletion .claude/board/entries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ 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.

598 entries, 2026-08-06 .. 2026-11-11.
600 entries, 2026-08-06 .. 2026-11-11.

| date | entry id | finding | file |
|---|---|---|---|
| 2026-11-11 | `codegraph-recon-2` | | [2026-11-11-codegraph-recon-2.md](2026-11-11-codegraph-recon-2.md) |
| 2026-11-11 | `codegraph-recon-1` | | [2026-11-11-codegraph-recon-1.md](2026-11-11-codegraph-recon-1.md) |
| 2026-11-11 | `cg-beh-2` | | [2026-11-11-cg-beh-2.md](2026-11-11-cg-beh-2.md) |
| 2026-11-11 | `cg-beh-1` | | [2026-11-11-cg-beh-1.md](2026-11-11-cg-beh-1.md) |
| 2026-11-10 | `lab-resolve-borrow-all` | | [2026-11-10-lab-resolve-borrow-all.md](2026-11-10-lab-resolve-borrow-all.md) |
| 2026-11-10 | `int8-direct-packed-b` | | [2026-11-10-int8-direct-packed-b.md](2026-11-10-int8-direct-packed-b.md) |
Expand Down
93 changes: 93 additions & 0 deletions .claude/probes/2026-11-11-cg-beh-2/REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# D-CG-BEH-2: the ON × WHEN × DO contract as separate registered facts

Source `317c4ffc0`. Crate `crates/cg-beh-probe` (lab, own workspace). Read-only: no production
registration is edited, no word is added, nothing is executed. Owner EXO-155.

## Gate A: registered facts

`lab.facts.tsv` holds this crate's own class (three methods; method 1 has ON, WHEN and DO).
`foundry.facts.tsv` holds Foundry's `CanCook` class as `World::new()` registers it: 16 methods over
a 211-field basis. Format: `crates/cg-beh-probe/README.md`.

`CanCook(item 0, q 1)`:

| part | registered |
|---|---|
| ON | 9 positions: `ops:stock/0..2`, `ops:stockKnown/0..2`, `ops:staff/0`, `ops:equipment/0`, `ops:offers/0` |
| reads | the same 9 positions |
| WHEN | two-valued conjunction of 9 leaves; every leaf number is one of the reads |
| DO | none (`DO_NONE`), for all 16 methods |
| binding | `2fb0b32d08e66e2cd2ad070eaf572c82`, signature revision 1 |

ON equals reads because `can_cook_method` builds both from the guard's fields. The store keeps them
as separate masks.

## Gate B: four kinds of change

Each prediction was compared with what the stores answer.

| change | registered fact that moves | binding digest | old reference at the new store | stale |
|---|---|---|---|---|
| ON declaration | ON positions | moves | `BindingMismatch` | reference stamp; `methods_on` answers |
| WHEN guard, other rows | guard witness | does not move | `GuardMismatch` | guard witness |
| WHEN guard, same rows written differently | expression only | does not move | admitted but for the issuer | nothing |
| DO registration (step order) | ordered steps | moves | `BindingMismatch` | reference stamp |
| DO implementation (a dispatch arm) | none | does not move | admitted | executed parity of each word that has a user |
| field basis, same count | ordered predicates | moves for every method of the class | `BindingMismatch` | witnessed `resolve` / lane binding; reference stamp |
| class schema revision, same basis | schema revision | does not move | admitted but for the issuer | `resolve` at the old revision (`SchemaRevisionMismatch`) |

Evidence paths: the first four and the last are `DT64_REGISTRATION` only. The DO implementation row
is `SOURCE_STRUCTURE` (line in arm, arm names opcode, opcode at table position) joined to
`DT64_REGISTRATION` (DO steps on that position). No row is `EXECUTED_PARITY`.

Measured on the real dispatch source: line 977 is in the `INT_S_LESS` arm (976 to 1018), word 5, used
at ordinal 0 by lab method 1. The `INT_ADD | INT_MULT | INT_S_DIV` arm (1145 to 1162) reaches three
words; one has a user in the lab snapshot.

## Gate C: falsifiers

24 tests. `disable_runs.py` removes 16 guards one at a time; each named test fails (16 of 16).

| falsifier | result |
|---|---|
| basis altered, same field count | `resolve` refuses with `PredicateBasisChanged`; no facts are read |
| guard changed, DO unchanged | only `When`; DO rows and binding digest equal |
| independent DO steps reversed | only `Do` |
| dependent DO steps reversed | the store refuses the registration (`UndeclaredRead`) |
| dispatch mapping removed | that word is refused as unknown; the other 16 answer as before |
| reference from a cloned store | `ForeignStore`; no facts |
| reference with its guard witness dropped | `GuardDropped`; no facts |
| unrelated method | `Unchanged` under every variant, facts byte-equal |
| facts altered against the digest | refused as inconsistent |
| dispatch reading from other text | refused as stale |

## Findings

1. **The guard is outside the binding digest.** A guard change leaves the digest equal and is caught
only by the witness comparison in `admit`.
2. **`admit` does not see the basis.** It takes no `ClassView`. Over a reordered basis of the same
count, `resolve` refuses and `admit` still admits the old reference. A caller that admits without
resolving does not learn that the basis moved.
3. **Guard positions are not checked at registration.** A guard may name a lane the method does not
read. Foundry's convention (guard number = field position) is the consumer's; the facts mark it.
4. **ON is registered and read, never evaluated.** In dt64, r2il, xlog and the Foundry lab `on` is
used by `methods_on`, `footprint`, the digest and the registration check. No executor consults it.
5. **`CanCook` has no DO edge.** A changed word implementation reaches no `CanCook` registration
through DO. Its guard is lowered by relalg, which this gate does not map.

## Gate D

`gate-d-diagram-check.md`. Checked statements: `03` 37 (4 stale), `05` 35 (5 stale), `06` 32 (4 stale).
The stale group that matters here: `05` and `06` describe PR #1996 (`PreparedMethod`, `rel_from_set`)
as open; it is merged. Relationships the diagrams omit: findings 1 to 4 above, the polyfill lens in
`03`, and that `resolve_algebra` checks revision and count but not the lens predicates.

## Not shown

- No executed parity. Nothing here says a word computes what it is declared to mean.
- The native lowering path (`binding_native`) and relalg's guard lowering are not mapped to source.
- Source-level attribution exists for DO implementations only. Which source line builds ON or WHEN
is not derived; ON and WHEN changes are found by comparing facts.
- The lens revision and a word's arity are not in the facts. A change of either moves the digest and
is refused as inconsistent rather than explained.
- One production-shaped binding (`CanCook`) and one lab class.
111 changes: 111 additions & 0 deletions .claude/probes/2026-11-11-cg-beh-2/disable_runs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Disable runs for crates/cg-beh-probe: remove one guard, the named test must fail.

Run from the repository root on a clean tree (the restore is `git checkout`):
python3 -I .claude/probes/2026-11-11-cg-beh-2/disable_runs.py
A case counts only when the patch applied exactly once, the test binary was
built and ran, and the named test is in its failure list.
"""
import os
import subprocess
import sys

CRATE = "crates/cg-beh-probe"
FACTS, QUERY, DISPATCH = (f"{CRATE}/src/{n}.rs" for n in ("facts", "query", "dispatch"))

CASES = [
("issuer check", FACTS,
"let binding = store.admit(r).map_err(Missing::Admit)?;",
"let binding = match store.admit(r) { Ok(b) => b, Err(e) => store.resolve(view, r.class, read.schema_rev).ok().and_then(|x| x.methods.iter().find(|m| m.id == r.method)).ok_or(Missing::Admit(e))? };",
"gate_c::a_reference_from_another_issuer_yields_no_facts"),
("guard dependency must be a registered read", FACTS,
".filter(|p| binding.reads.has(*p))", "",
"gate_a::a_guard_dependency_outside_the_registered_reads_stays_unknown"),
("source revision required", FACTS,
'if rev.is_empty() || rev.eq_ignore_ascii_case("unknown") {', "if false {",
"gate_a::a_snapshot_without_a_source_revision_is_refused"),
("lens revision required", FACTS,
"read.lens_rev.ok_or(Missing::LensRevision { lens })?", "read.lens_rev.unwrap_or(SchemaRev(1))",
"gate_a::do_steps_are_not_read_without_the_lens_they_were_bound_at"),
("basis read through the witnessed resolve", FACTS,
" let resolved = store\n .resolve(view, class, schema_rev)\n .map_err(Missing::Basis)?;\n let mut h",
" let resolved = store\n .resolve_algebra(view, class, schema_rev)\n .or_else(|_| Ok::<_, Missing>(lance_graph_dt64::Resolved { class, schema: Box::leak(Box::new(lance_graph_dt64::ClassSchema { schema_rev, field_count: 0, extent: lance_graph_dt64::dr::Extent::DR64_CUBE })), fields: view.fields(lance_graph_dt64::view_class(class)), methods: &[], derived_from: None }))?;\n let mut h",
"gate_c::a_basis_altered_at_the_same_field_count_is_refused_not_read"),
("witness, not spelling, decides WHEN", QUERY,
"if witness(b) != witness(a) {", "if spelling(b) != spelling(a) {",
"gate_b::a_respelled_guard_keeps_its_witness_and_makes_nothing_stale"),
("basis component", QUERY,
"if basis_b != basis_a {", "if false {",
"gate_b::a_changed_field_basis_reaches_every_method_of_the_class"),
("schema revision component", QUERY,
"if rev_b != rev_a {", "if false {",
"gate_b::a_changed_schema_revision_is_reported_though_the_binding_does_not_move"),
("DO order", QUERY,
" m.steps\n .iter()\n .map(|s| {",
" let mut steps: Vec<_> = m.steps.iter().collect();\n steps.sort_by_key(|s| s.word);\n steps\n .into_iter()\n .map(|s| {",
"gate_c::reversing_independent_do_steps_is_a_do_change_and_nothing_else"),
("digest moved alone", QUERY,
"if binding_changed && !covered {", "if false {",
"gate_c::facts_that_contradict_the_binding_digest_are_refused"),
("covered component moved alone", QUERY,
"if covered && !binding_changed {", "if false {",
"gate_c::facts_that_contradict_the_binding_digest_are_refused"),
("dispatch witness", QUERY,
"if current != dispatch.file_fnv {", "if false {",
"gate_c::a_dispatch_reading_from_other_source_text_is_refused"),
("arm that names no word", QUERY,
"if words.is_empty() {", "if false {",
"gate_c::a_line_outside_the_word_arms_has_no_behaviour_edge"),
("no stale parity without a user", QUERY,
"let stale = if users.is_empty() {", "let stale = if false {",
"gate_b::an_arm_shared_by_three_opcodes_reaches_all_three_words"),
("word without an arm", DISPATCH,
"self.arms.iter().find(|a| a.names.iter().any(|n| n == name))",
"self.arms.iter().find(|a| a.names.iter().any(|n| n == name)).or(self.arms.first())",
"gate_c::a_removed_dispatch_mapping_makes_its_word_unknown_and_no_other"),
("ON read from the binding", FACTS,
'let on = positions(&binding.on)', 'let on = positions(&binding.reads)',
"gate_a::the_lab_method_is_read_part_by_part"),
]


def run_tests():
env = dict(os.environ, CARGO_PROFILE_DEV_DEBUG="0", CARGO_PROFILE_TEST_DEBUG="0", CARGO_INCREMENTAL="0")
env.pop("RUSTFLAGS", None)
p = subprocess.run(
["cargo", "test", "--release", "--manifest-path", f"{CRATE}/Cargo.toml", "--test", "integration"],
capture_output=True, text=True, env=env)
return p.returncode, p.stdout + p.stderr


def main():
if subprocess.run(["git", "status", "--porcelain", CRATE], capture_output=True, text=True).stdout.strip():
sys.exit("the crate has uncommitted changes; commit before disabling")
code, out = run_tests()
if code != 0:
sys.exit("baseline is not green")
bad = 0
for name, path, old, new, test in CASES:
src = open(path).read()
if src.count(old) != 1:
print(f"ANCHOR {name}: found {src.count(old)} times")
bad += 1
continue
open(path, "w").write(src.replace(old, new))
try:
code, out = run_tests()
finally:
subprocess.run(["git", "checkout", "--", path], check=True)
ran = "test result:" in out
failed = f"test {test} ... FAILED" in out
verdict = "RED" if (code != 0 and ran and failed) else ("NOT-BUILT" if not ran else "GREEN")
if verdict != "RED":
bad += 1
print(out[-1500:])
print(f"{verdict:9} {name} -> {test}")
print(f"{len(CASES) - bad}/{len(CASES)} disable runs red")
sys.exit(1 if bad else 0)


if __name__ == "__main__":
main()
27 changes: 27 additions & 0 deletions .claude/probes/2026-11-11-cg-beh-2/dispatch.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
H cg-beh.dispatch.v1 b60ac87550156f8b
ARM FnIndex::NUMBER NOT_A_WORD 860 863
ARM LOAD NOT_A_WORD 864 886
ARM VIA NOT_A_WORD 887 903
ARM INT_EQUAL 4 904 974
ARM INT_LESS NOT_A_WORD 975 975
ARM INT_LESS_EQUAL NOT_A_WORD 975 975
ARM INT_S_LESS 5 976 1018
ARM INT_AND 6 1019 1085
ARM INT_OR 7 1086 1113
ARM INT_XOR 8 1114 1144
ARM INT_ADD 0 1145 1162
ARM INT_MULT 2 1145 1162
ARM INT_S_DIV 3 1145 1162
ARM INT_SUB 1 1163 1175
ARM POP_COUNT 10 1176 1194
ARM KEY_RUNS 16 1195 1213
ARM ANY 14 1214 1226
ARM ALL 15 1214 1226
ARM SUM 11 1227 1246
ARM MIN 12 1247 1274
ARM MAX 13 1247 1274
ARM GROUP_SUM NOT_A_WORD 1275 1314
ARM INT_NOT 9 1315 1347
ARM RANGE NOT_A_WORD 1348 1359
ARM KEEP NOT_A_WORD 1360 1368
ARM other NOT_A_WORD 1369 1369
Loading
Loading