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
2 changes: 1 addition & 1 deletion .claude/board/STATUS_BOARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ Plan: `.claude/plans/codegraph-expansion-v1.md`. Coordination: Linear `P-EXO-18`
| **CG-5** | co-research: body semantics through R2IL and egglog | Queued (needs go-ahead): EXO-237 |
| **CG-6** | review report follow-ups (mask-risc extraction, re-grade, placement, advisory CI) | Queued: EXO-238 |
| **CG-7** | report-rs #33 transfer case | Other session: EXO-239 / EXO-185 |
| **CG-8** | effects, target kind and cfg as Codegraph rows | Queued: EXO-241 |
| **CG-8** | effects, target kind and cfg as Codegraph rows | In PR (effects reader and reach; target kind and cfg still queued): EXO-241 |

## D-CODEGRAPH-0 — resident Codegraph: Charon → ruff → lance-graph + crai-rs (2026-11-02)

Expand Down
82 changes: 82 additions & 0 deletions .claude/probes/2026-11-11-codegraph-effects/REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# CG-8: the Codegraph reads the effects rows (EXO-241)

Ruling: D-CODEGRAPH-WAVE0-ACCEPTANCE, priority 2. Stacked on PR 2004.

## What it is

`crates/lance-graph-codegraph/src/effects.rs`, read-only:

- `Effects::open(dir)` reads every `<crate>.effects.tsv` (ruff_charon effects version 1,
`subject effect kind file start end`). A file that does not match the format is refused whole.
- `Effects::of(symbol)`: `Witnessed(rows)`, `NoneWitnessed` (not a purity verdict) or
`CrateNotRead`.
- `EffectClass::{Division, Remainder, Shift, Write}` groups the kinds a review asks about.
- `Codegraph::effects_reached(symbol, &effects, keep)`: the effect rows in bodies the symbol is
or reaches, each graded `Own`, `Confirmed`, `Possible` or `Unknown` from the call rows, plus
every known `ReachLimit`.
- `Effects::identity(krate)`: FNV-1a 64 of the file's bytes and its row count.

Nothing is written into the call masks. `Codegraph::impact` and the ruff_charon format are
unchanged.

## Acceptance

"Which witnessed division, remainder, shift or write effects are reachable from a named symbol":

```
$ cg_query tests/data/effects <foundry-parity-lab> effects \
--reach "foundry_parity_lab::arm_b::{ItemQuery<'s>}::max_quantities" \
--class division,remainder,shift,write
reach Confirmed effect ASSERT DivisionByZero …::cook_ops src/arm_b.rs 245 7745..7780
reach Confirmed effect ASSERT Overflow:Div …::cook_ops src/arm_b.rs 245 7745..7780
(each twice: the store has two rows for the site)
LIMIT CalleesOutsideStore { crates: ["alloc", "core", "lance_graph_mask_risc"] }
effects-file foundry_parity_lab fnv64=05ca96551de2c6a0 rows=213
reached 4 PARTIAL (see LIMIT lines)
```

"A missing effects artifact must produce UNKNOWN/PARTIAL, never an empty clean answer":

```
$ cg_query tests/data/rev1 <src> effects --reach cg_app::run --class division
LIMIT EffectsNotRead { krate: "cg_app" }
LIMIT EffectsNotRead { krate: "cg_lib" }
LIMIT DynamicCalls { store_dynamic_calls: 1 }
reached 0 UNKNOWN (partial: see LIMIT lines)
```

| asked to preserve | how |
|---|---|
| source spans | each row keeps `file start end`; the CLI adds the 1-based line |
| coverage | `EffectsNotRead` per stored crate without a file; `CalleesOutsideStore` names the crates reached but not stored; `DynamicCalls` when the symbol reaches a dynamic call; `Unplaced` for an effect whose subject has no position |
| residuals | `HarvestIncomplete` for a crate that is partial, has residuals, or has no residuals file |
| revision identity | the file's digest and row count. **The effects file carries no source revision**, so this identifies the bytes read, not the commit. Binding it to a revision is CG-3 (EXO-235). |

## What it found on real data

- All 62 effect subjects of the `foundry_parity_lab` store resolve to Codegraph positions.
- `--kind ASSERT:DivisionByZero` lists five rows at four sites: `method.rs:58`, `arm_a.rs:75`,
`arm_b.rs:245` (two rows) and `oracle.rs:62`.
- `method::execute_can_cook` reaches no division, although `can_cook_guard` (`method.rs:58`) is
registered from it through a closure. That is the closure-ownership gap of CG-1 (EXO-233): the
call rows have no edge into a closure body. The answer is `PARTIAL`, not clean, only because
of the other limits; nothing here reports the missing closure edge itself.
- Almost every function reaches `core` or `alloc`, so almost every answer is `PARTIAL`. That is
accurate: those bodies were not extracted.

## Tests and controls

- `tests/effects.rs`: 20 tests. Real fixture: `tests/data/effects/` (213 effect rows and the
matching call rows of `foundry_parity_lab`). The four reach grades use the `rev1` call rows
with one synthetic effect row.
- `python3 .claude/probes/2026-11-11-codegraph-effects/disable_runs.py`: 30 guards, each patched
out through an asserted anchor. Receipt beside it: 30 red, 0 not red.

## Not done

- Target-kind and cfg rows, the other half of CG-8's plan entry. They need a decision on the
cfg source and are a separate slice.
- No join with `impact_report`'s `Scope`: declared targets and excluded regions do not narrow a
reach answer yet.
- The reach is computed by one `impact` per candidate symbol (cold path). It is not timed here
and makes no latency claim.
84 changes: 84 additions & 0 deletions .claude/probes/2026-11-11-codegraph-effects/disable_runs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Disable runs for crates/lance-graph-codegraph/src/effects.rs.

Each guard is patched out (the anchor must occur exactly once, or the run is reported as
ANCHOR-MISSING and counts as a failure), `cargo test --release --test effects` is run, and
the file is restored. A guard is load-bearing when its run is RED.

Run from the repository root on a clean tree: python3 <this file>
Exit 0 only when every guard is RED. Writes nothing but the temporary patch.
"""
import re, subprocess, sys
from pathlib import Path

CRATE = Path(__file__).resolve().parents[3] / "crates/lance-graph-codegraph"
P = CRATE / "src/effects.rs"
TEST = "effects"
G = [
("version", " if found != 1 {", " if false {"),
("schema", " if lines.next() != Some(SCHEMA) {", " if lines.next().is_none() {"),
("arity", "let [subject, effect, kind, file, start, end] = f[..] else {", "let [subject, effect, kind, file, start, end, ..] = f[..] else {"),
("assert-empty-kind", '("ASSERT", k) if !k.is_empty() =>', '("ASSERT", k) =>'),
("site-order", 'if file != "-" && start <= end =>', 'if file != "-" =>'),
("site-dash-file", 'if file != "-" && start <= end =>', 'if start <= end =>'),
("site-partial-dash", '("-", _, _) if start == "-" && end == "-" => None,', '("-", _, _) => None,'),
("crate-not-read", " if !self.crates.contains(krate) {\n return EffectAnswer::CrateNotRead;\n }\n", ""),
("none-witnessed", " if hits.is_empty() {\n EffectAnswer::NoneWitnessed\n } else {\n EffectAnswer::Witnessed(hits)\n }", " EffectAnswer::Witnessed(hits)"),
("subject-filter", ".filter(|e| e.subject == subject)", ".filter(|e| !e.subject.is_empty() || e.subject == subject)"),
("kind-filter", ".filter(move |e| e.kind == *kind)", ".filter(move |e| e.kind == *kind || true)"),
("class-division", '"DivisionByZero" | "Overflow:Div" => Some(EffectClass::Division),', '"DivisionByZero" => Some(EffectClass::Division),'),
("class-remainder", '"RemainderByZero" | "Overflow:Rem" => Some(EffectClass::Remainder),', '"RemainderByZero" => Some(EffectClass::Remainder),'),
("class-shift", '"Overflow:Shl" | "Overflow:Shr" => Some(EffectClass::Shift),', '"Overflow:Shl" => Some(EffectClass::Shift),'),
("class-write", "EffectKind::DerefWriteArg | EffectKind::DerefWriteLocal => Some(EffectClass::Write),", "EffectKind::DerefWriteArg => Some(EffectClass::Write),\n EffectKind::DerefWriteLocal => None,"),
("identity-digest", "fnv64: fnv1a(text.as_bytes()),", "fnv64: fnv1a(krate.as_bytes()),"),
("identity-rows", "rows: (rows.len() - before) as u32,", "rows: rows.len() as u32 + 1,"),
("reach-own", " Some(p) if p == me => Some(ReachGrade::Own),\n", ""),
("reach-confirmed", " if has(&i.confirmed) {\n Some(ReachGrade::Confirmed)", " if false {\n Some(ReachGrade::Confirmed)"),
("reach-possible", "} else if has(&i.possible) {\n Some(ReachGrade::Possible)", "} else if false {\n Some(ReachGrade::Possible)"),
("reach-unknown", "} else if has(&i.unknown) {\n Some(ReachGrade::Unknown)", "} else if false {\n Some(ReachGrade::Unknown)"),
("reach-keep", ".filter(|e| keep(&e.kind)) {", ".filter(|e| keep(&e.kind) || true) {"),
("reach-no-position", " let Some(me) = self.pos(symbol) else {\n return Ok(None);\n };", " let me = self.pos(symbol).unwrap_or(0);"),
("limit-effects-not-read", " if effects.identity(&krate).is_none() {", " if false {"),
("limit-harvest", " if status == RunStatus::Partial || !clean_harvest {", " if status == RunStatus::Partial {"),
("limit-queried-symbol-outside", " if !me_stored {\n", " if false {\n"),
("limit-outside", " if !outside.is_empty() {", " if false {"),
("limit-dynamic", " if reaches_dynamic {", " if false {"),
("limit-dynamic-per-symbol", " reaches_dynamic = true;", " reaches_dynamic = true;\n }\n if true {\n reaches_dynamic = true;"),
("limit-unplaced", " unplaced.insert(subject);\n", " unplaced.insert(subject);\n unplaced.clear();\n"),
]

def main():
if subprocess.run(["git", "diff", "--quiet", "--", str(P)]).returncode != 0:
sys.exit(f"{P} has local changes; commit first (the restore would not be exact)")
orig = P.read_text()
bad = 0
try:
for name, old, new in G:
if orig.count(old) != 1:
print(f"{name}\tANCHOR-MISSING({orig.count(old)})")
bad += 1
continue
P.write_text(orig.replace(old, new))
out = subprocess.run(
["cargo", "test", "--release", "--test", TEST],
cwd=CRATE, capture_output=True, text=True,
)
text = out.stdout + out.stderr
m = re.search(r"test result: (\w+)\. (\d+) passed; (\d+) failed", text)
failed = sorted(set(re.findall(r"^test (\S+) \.\.\. FAILED", text, re.M)))
if not m:
print(f"{name}\tBUILD-ERROR")
bad += 1
elif m.group(1) == "FAILED":
print(f"{name}\tRED\t{','.join(f.split('::')[-1] for f in failed)}")
else:
print(f"{name}\tGREEN\tno test depends on this guard")
bad += 1
finally:
P.write_text(orig)
print(f"guards\t{len(G)}\tnot-red\t{bad}")
sys.exit(1 if bad else 0)


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
version RED a_wrong_version_is_refused
schema RED a_wrong_schema_is_refused
arity RED a_short_or_long_row_is_refused
assert-empty-kind RED an_unknown_effect_is_refused
site-order RED a_malformed_site_is_refused
site-dash-file RED a_malformed_site_is_refused
site-partial-dash RED a_malformed_site_is_refused
crate-not-read RED an_unread_crate_is_not_looked_at
none-witnessed RED no_row_is_not_a_purity_verdict
subject-filter RED a_function_with_rows_is_witnessed,no_row_is_not_a_purity_verdict
kind-filter RED division_sites_are_found_by_kind
class-division RED classes_select_division_remainder_shift_and_write
class-remainder RED classes_select_division_remainder_shift_and_write
class-shift RED classes_select_division_remainder_shift_and_write
class-write RED classes_select_division_remainder_shift_and_write
identity-digest RED the_effects_file_is_identified_by_its_bytes
identity-rows RED the_effects_file_is_identified_by_its_bytes
reach-own RED a_caller_reaches_its_callees_division,the_reach_grade_follows_the_call_rows
reach-confirmed RED a_caller_reaches_its_callees_division,classes_select_division_remainder_shift_and_write,the_reach_grade_follows_the_call_rows
reach-possible RED the_reach_grade_follows_the_call_rows
reach-unknown RED the_reach_grade_follows_the_call_rows
reach-keep RED a_caller_reaches_its_callees_division,classes_select_division_remainder_shift_and_write
reach-no-position RED a_caller_reaches_its_callees_division
limit-effects-not-read RED a_missing_effects_file_makes_the_answer_partial_never_clean
limit-harvest RED dynamic_calls_and_an_incomplete_harvest_are_limits
limit-queried-symbol-outside RED a_queried_symbol_outside_the_store_is_a_limit
limit-outside RED a_queried_symbol_outside_the_store_is_a_limit,callees_outside_the_store_are_a_limit
limit-dynamic RED dynamic_calls_and_an_incomplete_harvest_are_limits
limit-dynamic-per-symbol RED callees_outside_the_store_are_a_limit
limit-unplaced RED an_effect_subject_without_a_position_is_a_limit
guards 30 not-red 0
2 changes: 1 addition & 1 deletion .codegraph/coverage.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ lance-graph-benches 0 10 0 macro-generated items invisible;fn-local use not scan
lance-graph-callcenter 35 6 3 macro-generated items invisible;fn-local use not scanned
lance-graph-catalog 9 1 0 macro-generated items invisible;fn-local use not scanned
lance-graph-codec-research 10 0 0 macro-generated items invisible;fn-local use not scanned
lance-graph-codegraph 9 12 0 macro-generated items invisible;fn-local use not scanned
lance-graph-codegraph 10 13 0 macro-generated items invisible;fn-local use not scanned
lance-graph-cognitive 60 0 0 macro-generated items invisible;fn-local use not scanned
lance-graph-consumer-conformance 2 0 0 macro-generated items invisible;fn-local use not scanned
lance-graph-contract 175 35 2 macro-generated items invisible;fn-local use not scanned
Expand Down
2 changes: 1 addition & 1 deletion .codegraph/crates.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ lance-graph-benches crates/lance-graph-benches member 2f2fb296a08260fb
lance-graph-callcenter crates/lance-graph-callcenter member 41a65d38ac70bc87
lance-graph-catalog crates/lance-graph-catalog member 9d0c34f70c28a465
lance-graph-codec-research crates/lance-graph-codec-research excluded 8b8afbd9a6d0f359
lance-graph-codegraph crates/lance-graph-codegraph excluded 434f8b1c7314d3b0
lance-graph-codegraph crates/lance-graph-codegraph excluded 8314ca9d65242c9c
lance-graph-cognitive crates/lance-graph-cognitive excluded 93f956ac0d69ca66
lance-graph-consumer-conformance crates/lance-graph-consumer-conformance member 82e980465d770990
lance-graph-contract crates/lance-graph-contract member 6b4e13ff1d0afadb
Expand Down
Loading
Loading