Skip to content

feat(graph): L3/L4 program-slice engine core (Tasks 1–6 of #270)#271

Merged
rahlk merged 19 commits into
release/2.0from
feat/issue-270-slice-engine
Jul 15, 2026
Merged

feat(graph): L3/L4 program-slice engine core (Tasks 1–6 of #270)#271
rahlk merged 19 commits into
release/2.0from
feat/issue-270-slice-engine

Conversation

@rahlk

@rahlk rahlk commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

L3/L4 program-slice engine — core (Tasks 1–6 of #270)

Adds cldk/graph/: a shared, language-neutral program-slice engine that turns schema-v2 L3/L4 graphs (CFG/CDG/DDG + interprocedural SDG) into citable slice/flow evidence. This is the engine core; the facade/backend wiring (Tasks 7–10 of the plan) is blocked on F2 (#240) and lands separately. Part of epic #238 (2.0.0-rc.1). Design/plan: docs/superpowers/specs/2026-07-14-l3l4-consumption-api-design.md, docs/superpowers/plans/2026-07-14-l3l4-slice-engine.md (local).

What's here

  • result.pySliceResult / FlowResult / FlowPath (walkable subgraph + citable evidence + explain()), confidence from prov (ssa→structural, points-to→resolved).
  • provider.py — the ProgramGraphProvider ABC data-seam (the only per-backend surface) + polymorphic resolve_vertex (location string ∪ can:// id ∪ node).
  • capability.py — honest-degrade / strict gating on analysis level.
  • engine.py — the single shared traversal: slice_backward, slice_forward, flows_to, def_use, control_deps.

Traversal is written once over the seam (parity by construction); the graph is an nx.MultiDiGraph (coincident cfg/cdg/ddg edges stay distinct); the uris() == subgraph.nodes() invariant holds across every verb. This engine is also the executor the 2.1.0 fluent chain (#155) will reuse.

Scope / safety

Purely additive — a new package with no imports into any existing facade; the frozen get_* API is untouched. No production caller yet (the facade delegates arrive in Tasks 7–10), so nothing is wired until then.

Tests & review

36 tests, all green. Built via subagent-driven TDD with a two-stage review (per-task + whole-branch on the most capable model). The review gate caught and closed 9 defects latent in the plan's own code that passing tests did not reveal — including flows_to gated at L3 not L4, flows_to ignoring the sink's callable (false "no flow"), and a family-blind SDG overlay contaminating control-flow slices. Final whole-branch verdict: ready for scope-complete.

Carry-forward for the wiring tasks (7–10, on #270)

  • RISK-A (contract): flow-hop kind must stay family-first for intra edges — when the Task-7 provider materializes cpg Edge.kind onto intra ddg edges, the hop-kind fallback would otherwise silently change intra hops from "ddg", breaking facade parity.
  • Deferred (needs real L4 analysis.json): def_use interprocedural reach; multi-hop (3+ callable) flows_to; multi-seed union (resolve_vertex[0]); _ddg_tier prov-shape verification.

Part of #270. Do not merge as "F7 done" — this is the core; F7 completes when Tasks 7–10 wire it.

rahlk added 19 commits July 14, 2026 16:18
_intra applied the sdg (dataflow) overlay whenever the backend was L4 and
interprocedural was wanted, ignoring the edges family filter — so a cfg- or
cdg-only slice picked up dataflow vertices from foreign callables. Fold the
family gate into want_inter so the overlay predicate and
explain()["interprocedural"] stay one source of truth: no dataflow family
requested means no boundary crossing. Subsumes the redundant max_level()>=4
check on the overlay branch; control_deps' explicit interprocedural=False
stays as belt-and-suspenders.
…270)

flows_to's full semantics are ddg + summary/param_in/param_out — an
interprocedural (L4) capability — but it gated on require(3), so an L3
backend silently returned intra-only results as if they were complete.
Raise the requirement to L4: non-strict now attaches the degraded note
(absence is UNKNOWN, not safety) while still returning the intra ddg
witnesses it can compute; strict=True raises CapabilityError.
flows_to built its dataflow graph from the source's callable only, so a
sink inside a different callable (reachable via param_in into the callee
interior) was unreachable and reported a false "no flow". _dataflow_graph
now unions the intra ddg graphs of the given callables before adding the
sdg overlay, and flows_to passes both endpoint callables. def_use keeps
its single-callable scope with a NOTE — interprocedural completeness
lands with the whole-program dataflow graph (deferred to Task 7).
…or (#270)

resolve_location legitimately returns [] when no vertex sits at the given
line, but every engine verb indexes resolve_vertex(...)[0], turning an
ordinary user miss into an IndexError. Raise a descriptive ValueError at
the source in resolve_vertex instead, converting all [0] call sites into
a clean failure mode.
…#270)

The graph is an nx.MultiDiGraph everywhere, but three annotations still
said nx.DiGraph: ProgramGraphProvider.program_graph, GraphResult.subgraph
(and engine._dataflow_graph, already corrected in the C2 commit). Fix the
first two and document the provider contract: parallel cfg/cdg/ddg edges
between the same vertex pair must stay distinct edges with their own
family/var/prov/kind, and sub-L3 providers still answer the structural
methods — the engine handles level gating via require(...).
The sdg overlay dropped e.kind (param_in/param_out/summary) on the floor
in both _intra and _dataflow_graph, so a FlowPath hop crossing a callable
boundary could only say "sdg" — not which boundary edge carried the
flow. Carry kind on the overlay edges and have the hop dict prefer the
edge kind over the family: intra hops still report cfg/cdg/ddg, boundary
hops now report the concrete sdg kind.
…#270)

_evidence stamped every non-seed vertex "def", which is wrong for
control_deps (the guard CONTROLS the seed) and def_use (downstream
vertices are USES of the definition). Thread a default_role through
_evidence and _intra: slices and flows keep "def", control_deps passes
"control", def_use passes "use"; seeds are always "seed".
flows_to enumerated simple paths with a hard-coded cutoff=64 and no path
cap, and never told the caller when witnesses were dropped. Hoist the
bounds to module constants (_PATH_CUTOFF=64, _MAX_PATHS=1000 — provisional,
to be tuned on real graphs), stop collecting at _MAX_PATHS, and surface
explain()["truncated"] so a partial witness set is never silently
presented as complete.
…270)

prov is a provenance set in list form; the exact-list comparisons
(prov == ["points-to"] / == ["ssa"]) made the STRONGER combined
provenance ["ssa", "points-to"] fall through to "unresolved".
Rank by membership: points-to anywhere means resolved, else ssa means
structural, else unresolved.
@rahlk rahlk added the kind/feature New feature(s) label Jul 14, 2026
@rahlk
rahlk merged commit 74510fb into release/2.0 Jul 15, 2026
@rahlk
rahlk deleted the feat/issue-270-slice-engine branch July 15, 2026 04:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature New feature(s)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant