Skip to content

Commit b34af5e

Browse files
committed
docs(skills): config_use vocabulary and bridge analyses
PY_USES_CONFIG / PY_READS_CONFIG_UNRESOLVED join the vocabulary tables, the level table gains the literal tier at -a 2, and analyses.md gains the bridge section: read-to-key edges, key-rename blast radius, the ambient-environment contract (undefined-key reads), dynamic-read triage, and the config-to-exit-point join. Discharges the skill DoD item from the config_use spec.
1 parent 63d8937 commit b34af5e

3 files changed

Lines changed: 49 additions & 3 deletions

File tree

docs/skills/analyzing-canpy-graphs/SKILL.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ minimum `-a` level.
1919
| `-a` | tree | edges |
2020
| --- | --- | --- |
2121
| 1 | callables + `call` body nodes + entrypoints + artifacts/dependencies | `HAS_ARTIFACT`, `DECLARES_DEPENDENCY`, `LOCKS`, `PY_PROVIDES`, `PY_UNRESOLVED_IMPORT` |
22-
| 2 | `callee` resolved | `PY_CALLS` (prov `jedi`/`defuse`), `PY_RESOLVES_TO` |
22+
| 2 | `callee` resolved | `PY_CALLS` (prov `jedi`/`defuse`), `PY_RESOLVES_TO`, `PY_USES_CONFIG` (literal tier) + `PY_READS_CONFIG_UNRESOLVED` |
2323
| 3 | full statement `body`, `@entry`/`@exit` | `PY_CFG_NEXT`, `PY_CDG`, `PY_DDG` (prov `ssa`, `reaching-defs`) |
2424
| 4 | `formal_in/out`, `actual_in/out` vertices | `PY_PARAM_IN`, `PY_PARAM_OUT`, `PY_SUMMARY`, ddg widened `points-to` |
2525

@@ -54,7 +54,9 @@ Neo4j is always projected full-depth for the level analyzed.
5454
`entrypoint_frameworks`) of `PyCallable` AND `PyClass` — there is no
5555
`:Entrypoint` label.
5656

57-
Taint is composed, not stored: reachability over
57+
`PY_USES_CONFIG` widens with the analysis level (literal at `-a 2`, def-use
58+
closure at `-a 3`, cross-call closure at `-a 4`) and never guesses — unresolved
59+
reads carry a reason instead. Taint is composed, not stored: reachability over
5860
`PY_DDG ∪ PY_PARAM_IN ∪ PY_PARAM_OUT ∪ PY_SUMMARY` (provider/client boundary —
5961
source/sink packs are the SDK's job). Exit points = returns + external calls +
6062
caller-visible writes; both have full recipes in analyses.md.

docs/skills/analyzing-canpy-graphs/references/analyses.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,45 @@ MATCH (f:Artifact) WHERE f.source CONTAINS "POSTGRES_PASSWORD" RETURN f.path
224224
MATCH (p:Package) WHERE p.id STARTS WITH "pkg:" RETURN p.ecosystem, count(*)
225225
```
226226

227-
## 12. Health metrics (any level)
227+
## 12. Config-use bridge (L2 literal; L3/L4 widen)
228+
229+
```cypher
230+
// which statements read which config keys, with tier evidence
231+
MATCH (s:PyBodyNode)-[u:PY_USES_CONFIG]->(k:ConfigKey)
232+
RETURN s.id, k.key, k.namespace, u.prov
233+
234+
// blast radius of renaming a key: every reading statement + its callable
235+
MATCH (k:ConfigKey {key: "DB_HOST"})<-[:PY_USES_CONFIG]-(s:PyBodyNode)
236+
MATCH (c:PyCallable)-[:PY_HAS_BODY_NODE]->(s)
237+
RETURN c.id, s.start_line
238+
239+
// the service's ambient-environment contract: reads nobody defines
240+
MATCH (:PyApplication)-[r:PY_READS_CONFIG_UNRESOLVED]->(callee:PyExternal)
241+
WHERE r.reason = "undefined-key"
242+
RETURN r.key, callee.module + "." + callee.name AS via, count(*) AS sites
243+
ORDER BY sites DESC
244+
245+
// dynamic config reads needing human eyes (key statically unknown)
246+
MATCH (:PyApplication)-[r:PY_READS_CONFIG_UNRESOLVED]->(callee:PyExternal)
247+
WHERE r.reason = "non-literal"
248+
RETURN callee.module, count(*)
249+
250+
// config-to-exit-point join: keys whose values look like backend URLs,
251+
// beside the external libraries the code exits through
252+
MATCH (a:Artifact)-[:DEFINES_CONFIG]->(k:ConfigKey)
253+
WHERE k.value =~ ".*(postgres|redis|amqp|https?)://.*"
254+
WITH collect({file: a.path, key: k.key}) AS declared_backends
255+
MATCH (c:PyCallable)-[:PY_CALLS]->(x:PyExternal)
256+
WHERE x.module IN ["psycopg2", "redis", "requests"]
257+
RETURN declared_backends, x.module, count(DISTINCT c)
258+
```
259+
260+
Edges never guess: literal tier (`-a 2`) needs a string-literal key at a
261+
detector-listed call; dataflow tier (`-a 3` intra, `-a 4` interprocedural)
262+
resolves only chains closing over exactly one literal. Everything else lands
263+
in the unresolved rel with a reason.
264+
265+
## 13. Health metrics (any level)
228266

229267
```cypher
230268
// external surface per module

docs/skills/analyzing-canpy-graphs/references/vocabulary.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,13 @@
5151
| `LOCKS` | lock artifact → package | version | locks never create packages alone |
5252
| `PY_PROVIDES` | package → external (module-level ghost) || joins dependencies into the call graph |
5353
| `PY_UNRESOLVED_IMPORT` | application → external (module-level ghost) | prov[] | undeclared-import hygiene |
54+
| `PY_USES_CONFIG` | body node → ConfigKey | prov[] | which statement reads which key; prov ⊆ {literal, dataflow}; superset-monotonic `-a 2 ⊆ 3 ⊆ 4` |
55+
| `PY_READS_CONFIG_UNRESOLVED` | application → external (callee ghost) | key, reason, prov[], `_k` | config reads that never resolved; reason ∈ {non-literal, undefined-key}; the unresolved list is the edge set's sanctioned complement (shrinks as levels rise) |
5456

5557
All dataflow relationships are stored src→dst in the forward direction.
58+
59+
Call arguments carry literal evidence: `PyCallArgument.value` (JSON-encoded
60+
constant — `json.loads` it) and `.name` (bare identifier) back the config_use
61+
tiers and are queryable via `PyBodyNode.arguments_json`.
5662
Dependency `prov` vocabulary: `declared`, `lockfile`, `installed-metadata`
5763
(only with `--resolve-installed`), `heuristic`.

0 commit comments

Comments
 (0)