All verification criteria, fixture-design rules, and the per-level gate commands for the analyzer
backend (codeanalyzer-<lang>). Each level's gate is what the SKILL.md <HARD-GATE> refers to: no
level advance while the current level's conformance gate is red. The SDK-side testing (mocked +
E2E against the facade, and the slice/taint query gates) lives in the companion cldk-sdk-frontend
skill; keep the two in sync but scoped to their own surface.
Never fake verification. The toolchain is confirmed installed at the start (the tooling menu), so every gate below should actually run. If a required tool goes missing mid-build, stop and instruct the user to install it — don't scaffold-and-leave-unverified, and don't claim a gate passed without running it.
"Conformance" means the output validates against the SDK models generated from the keystone
(skills/designing-cldk-changes/references/canonical-schema.md) — in practice,
Application(**json.load(open("analysis.json"))) must not raise. An analyzer that runs but emits
non-conformant JSON has failed the real job: the SDK can't load it and the Neo4j graph won't match.
Validate at every level, in both projections. Mirror the schema comprehensively — a thin
shape that "looks right" but drops fields is a silent failure.
Fixture location: testdata/fixture/ (or testdata/realistic/) in the analyzer repo. A field with no
test is a silent regression point — assert a specific value, never just len > 0.
L1/L2 minimum coverage:
- Every field added beyond the shared spine, with a concrete-value assertion.
- At least one multi-file compilation unit (two+ files in one package/module) — the cross-file method-attachment bug surfaces only here.
- Both exported and unexported symbols; assert the unexported one's visibility encoding.
- The idiomatic compound-return / error pattern into
error_channel. - At least one variadic / spread parameter (if the language has them), asserted.
- At least one named expected call-graph edge — assert the exact
(src, dst), not "graph non-empty". - At least one cross-package / cross-module call, so cross-package edges appear.
- A call site with a language-specific flag set true (goroutine/async/unsafe/constructor), asserted.
L3/L4 additional coverage (each with a named expected result in the test):
- an
if/elseand a loop (control dependence + a loop-carriedddgedge); - an early return and a throw/panic/raise with a handler (exceptional CFG + multi-exit);
- a closure / nested function capturing a local (capture edges);
- aliasing: two names for one object, a write through one, a read through the other (the L3
syntactic
ddgmisses it; the L4points-toddgcatches it); - a call chain
a → b → cwhere a value flows froma's argument tocand back (summary+param_in/param_out); - mutual recursion (SCC fixpoint termination);
- a module-level/global variable written in one function, read in another;
- a multi-file flow (cross-module SDG edges);
- each language-specific lowering construct from the L3 checklist
(
references/level-3-intraprocedural-dataflow.md).
Run each level's gate on the fixture; do not build the next level until the current is green.
- Output validates against
Application—Application(**json.load(...))does not raise. symbol_tablenon-empty and keyed by stable relative paths — assert no key starts with/(absolute) or... Both are common bugs.- A known file's
modulehas the expectedtypes/functions, asourceblob, and callables carryingcallnodes withcallee == null; theget_method_bodyslice matchesmodule.source[span.bytes]. - Re-running reuses cache —
analysis.json(oranalysis_cache.json) mtime unchanged on a second non-eager run.
- Every edge endpoint matches a real
can://id —for e in call_graph: assert e.src in all_ids and e.dst in all_ids(no dangling). - Every edge has a non-empty
provnaming the resolver. calleebackfilled (non-null id) on resolved sites; stillnullon honest-unresolved ones.- A named expected edge present (exact
(src, dst)), plus at least one cross-package edge. - Output still validates.
- CFG: every body node maps to a real span; single
@entry/@exit; every node reachable from@entryand reaching@exit; each fixture construct emits its documentedcfgedges (includingexceptionedges). - Dominance: post-dominator tree rooted at
@exit; hand-computed control dependences for the fixture'sif/loop/early-return callables match thecdgedges exactly. - PDG slice (the L3 gate): a backward intraprocedural slice — reverse reachability over
cdg ∪ ddg— of a named variable at a named line equals the hand-computed node set exactly. Write the expected set into the test.
- No dangling
param_in/param_out/summaryendpoints. param_in/param_outarity matches each callable's parameters.- A
summaryedge exists for a known transitive flow. - The semantic
ddg(prov:["points-to"]) edges are present and added to, not replacing, the L3prov:["ssa"]edges. - Output validates. (Slice/taint are frontend gates, in
cldk-sdk-frontend— the backend proves the graph is correct; those prove the SDK's queries over it are.)
| Test | Assert |
|---|---|
CacheFileWritten |
After a run with --cache-dir set, analysis_cache.json exists there. |
CacheContentsRoundTrip |
It deserializes to a valid Application with the same symbol_table key count as the in-memory result. |
SecondRunReuses |
A second non-eager run returns the same key count; analysis.json mtime unchanged. |
EagerForcesRebuild |
After seeding the cache, --eager rewrites analysis_cache.json (mtime advances; sleep before the eager run so the timestamp differs). |
--format <unsupported> (e.g. msgpack before it's implemented) must exit non-zero with a clear
message, never silently fall back to JSON. Assert both the non-zero exit and the message
(references/cli-contract.md § Flag validation).
Run -a 1, -a 2, -a 3, -a 4 on the fixture and assert
json(-a 1) ⊆ json(-a 2) ⊆ json(-a 3) ⊆ json(-a 4) — every node and edge present at a lower level
is present, unchanged, at every higher level. The only sanctioned differences are additions
(new body nodes, new edge-list entries) and the single callee: null → id refinement. A diff that
changes an existing fact (a rewritten span, a re-anchored call_graph edge, a removed syntactic
ddg edge) fails the gate — a level rewrote instead of added. This is a CI-checkable superset gate.
Assert the two projections agree: the Neo4j node/edge counts at full depth match the JSON at
max_level (modulo the explicit HAS_* containment edges Neo4j makes explicit —
references/neo4j-projection.md).
can:// ids (≥ callable) are stable across two runs on unchanged source; …@line:col ids carry
a column (assert no id is a bare line); every edge endpoint resolves to a real node (no dangling, at
every level and in both projections).
-j N output must be byte-identical to -j 1 at every level. Implement sequentially first, pass
every gate at -j 1, then parallelize using the -j 1 output as the differential oracle — never
assign ids or emit during parallel execution (collect, then sort by node id). -j 1 stays the debug
mode forever.
- The level test suite passes (
go test ./...or equivalent) — L1, L2, and, if in scope, L3/L4. - Output on the fixture validates against
Applicationat every implemented level, in both projections. -
symbol_tablekeys are relative; none absolute or..-prefixed. - Every language-specific field has a concrete-value assertion.
- Named expected call-graph edge asserted (not just "non-empty").
-
--eagerrewrites cache; a non-eager second run reuses it. -
--format <unsupported>returns an explicit error. - The monotonicity, cross-projection, identity, and determinism gates all pass.
Release-artifact validation (wheel, Release binaries, brew formula build cleanly) is not an
analyzer-surface gate — it belongs to finishing-cldk-work. Both this surface and the SDK surface
(cldk-sdk-frontend) must be green before the language is considered complete.