Skip to content

Commit a11242e

Browse files
feat(recipes): evidence chains on high-judgment recipe rows (#174)
* feat(recipes): evidence columns on boundary-violations (slice 1.1) Add reason and evidence_json to boundary-violations SQL; golden + golden-queries contract; agent-enrichment-wave tracer doc for plans 1-4. * feat(recipes): caller evidence on deprecated-symbols (slice 1.2) reason has_callers/no_callers plus bounded evidence_json from calls; golden updated; document name-only callee match caveat. * docs(plans): point agent-enrichment-wave at slice 1.3 * feat(recipes): re-export evidence on unimported-exports (slice 1.3) Rows get reason (no_direct_import | reexport_chain_possible) and evidence_json barrel hops from re_export_chains when a false positive may be explained by barrel re-exports. * docs(agent): evidence columns one-liner in served rule (slice 1.4) Completes Plan 1 evidence chains on recipe rows for PR #B. * harden: AST-only caller evidence, E.3 truncation, doc drift Filter deprecated-symbols callers to AST provenance; append truncated marker when evidence subqueries cap at 3; surface re_export_chains.truncated on unimported-exports hops; align recipe docs and changeset for PR #B. * harden: E.3 boolean truncation, docs parity, golden fixture Use json('true') for list-cap markers; exercise >3 caller branch via ProductCard now() fixture; fix stale unimported-exports action text; add architecture + skill evidence lines; refresh affected goldens.
1 parent d4982c4 commit a11242e

32 files changed

Lines changed: 466 additions & 115 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@stainless-code/codemap": patch
3+
---
4+
5+
Add `reason` and `evidence_json` columns on high-judgment recipe rows (`boundary-violations`, `deprecated-symbols`, `unimported-exports`) so agents can cite detection path before `apply` or manual edits.

docs/architecture.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ Three **mutually exclusive** CLI entry shapes; all converge on `applyDiffPayload
190190

191191
**Show / snippet wiring:** **`src/cli/show-snippet-args.ts`** (shared argv parser) + **`src/cli/show-snippet-render.ts`** (shared terminal/JSON error helpers) + **`src/cli/cmd-show.ts`** + **`src/cli/cmd-snippet.ts`** — sibling CLI verbs sharing the same parser shape (`<name>` or **`--query '<field:value …>'`** + **`--with-fts`** + `--kind` + `--in <path>` + `--json`; show adds **`--print-sql`**) and the pure engines **`src/application/show-engine.ts`** (exact lookup + envelope builders), **`src/application/search-query-parser.ts`** + **`src/application/search-engine.ts`** (field-qualified search → parameterized SQL on `symbols`, optional `source_fts` join), and **`src/application/show-search-mode.ts`** (shared parse/normalize + FTS resolution + **`executeShowLookup`** + **`formatShowSearchSqlForQuery`** for CLI/MCP/HTTP). Exact lookup: `findSymbolsByName({db, name, kind?, inPath?})`. Query lookup: `searchSymbols({db, parsed, withFts?})`. Snippet FS read: `readSymbolSource({match, projectRoot, indexedContentHash?})` + `getIndexedContentHash(db, filePath)`. **`buildShowResult`** + **`buildSnippetResult`** envelope builders — same engines the MCP show/snippet tools call. Both verbs return the same `{matches, disambiguation?, warning?}` envelope — single match → `{matches: [{...}]}`; multi-match adds `{n, by_kind, files, hint}`; optional **`warning`** when FTS was requested but `source_fts` is empty. Snippet matches add `source` / `stale` / `missing` fields (additive — no shape divergence). **`--in <path>`** and **`path:`** inside **`--query`** normalize through `toProjectRelative(projectRoot, p)` (from **`src/application/validate-engine.ts`**). Stale-file behavior on `snippet`: `hashContent` (from **`src/hash.ts`**) compares on-disk content against `files.content_hash`; mismatch sets `stale: true` but source IS still returned. MCP tools `show` and `snippet` register parallel to the CLI surface (see [§ MCP wiring](#cli-usage)).
192192

193+
**Evidence columns (high-judgment recipes):** Some bundled recipes add optional **`reason`** and **`evidence_json`** TEXT columns on each result row — factual detection path for agents, not pass/fail verdicts. Contract: [golden-queries.md § Evidence columns](./golden-queries.md#evidence-columns-high-judgment-recipes).
194+
193195
**Recipes wiring:** **`src/application/recipes-loader.ts`** (pure transport-agnostic loader) + **`src/application/query-recipes.ts`** (cache + public API — `getQueryRecipeSql` / `getQueryRecipeActions` / `getQueryRecipeParams` / `listQueryRecipeIds` / `listQueryRecipeCatalog` / `getQueryRecipeCatalogEntry`, shared by CLI + MCP). Recipes live as file pairs: **`<id>.sql`** + optional **`<id>.md`**. The loader reads `templates/recipes/` (bundled, ships in npm package next to `templates/agents/`) and `<state-dir>/recipes/` (project-local — default `.codemap/recipes/`; honors `--state-dir` / `CODEMAP_STATE_DIR`; root-only resolution per the registry plan, no walk-up). Project recipes win on id collision; entries that override a bundled id carry **`shadows: true`** in the catalog so agents reading `codemap://recipes` at session start see when a recipe behaves differently from the documented bundled version. Per-row **`actions`** templates and recipe **`params`** declarations live in YAML frontmatter on each `<id>.md` — uniform shape across bundled + project. Param types are `string | number | boolean`; CLI passes values via repeatable `--params key=value[,key=value]`, MCP / HTTP pass nested `params: {key: value}` to `query_recipe`. Validation runs before SQL binding; missing / unknown / malformed params return the same `{error}` envelope as query failures. Hand-rolled YAML parser is scoped to block-list `actions:` and `params:` only (no `js-yaml` dep). Load-time validation rejects empty SQL and DML / DDL keywords (`INSERT` / `UPDATE` / `DELETE` / `DROP` / `CREATE` / `ALTER` / `ATTACH` / `DETACH` / `REPLACE` / `TRUNCATE` / `VACUUM` / `PRAGMA`) with recipe-aware error messages — defence in depth alongside the runtime `PRAGMA query_only=1` backstop in `query-engine.ts` (PR #35). `<state-dir>/index.db` is gitignored; `<state-dir>/recipes/` is NOT (verified via `git check-ignore`) — recipes are git-tracked source code authored for human review.
194196

195197
**Tool / resource handlers (transport-agnostic):** **`src/application/tool-handlers.ts`** + **`src/application/resource-handlers.ts`** — pure functions that take the args object an MCP tool / resource URI accepts and return a discriminated **`ToolResult`** (`{ok: true, format: 'json'|'sarif'|'annotations'|'mermaid'|'diff'|'diff-json'|'codeclimate'|'badge', payload}` — badge arm also carries `badgeStyle`; `{ok: false, error}`) or a **`ResourcePayload`** (`{mimeType, text}`). MCP and HTTP both wrap the same handlers — MCP translates to `{content: [{type: "text", text}]}`, HTTP translates to `(status, body)` with the right `Content-Type`. Engine layer untouched; transport changes don't ripple into the SQL.

docs/golden-queries.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ Scenarios live in **`fixtures/golden/scenarios.json`** (Tier A) or optional **`s
6666

6767
**Prompts** in JSON are **intent labels**, not pasted chat logs — pair with queries whose literals come from **fixture-owned** data (see [fixtures/qa/prompts.external.template.md](../fixtures/qa/prompts.external.template.md) for optional chat QA).
6868

69+
### Evidence columns (high-judgment recipes)
70+
71+
Some bundled recipes add optional **`reason`** (TEXT) and **`evidence_json`** (TEXT, JSON array) columns on each row — factual detection path for agents, not engine verdicts. See [plans/evidence-chains-on-recipe-rows.md](./plans/evidence-chains-on-recipe-rows.md). Goldens assert these columns when the recipe ships evidence (`boundary-violations`, `deprecated-symbols`, `unimported-exports`).
72+
6973
---
7074

7175
## Status
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Agent enrichment wave — tracer workflow (plans 1–4)
2+
3+
> **Status:** in-flight · **Scope:** four P2 plans ranked by consumer/agent ROI
4+
>
5+
> **Goal:** Ship tracer bullets that cut agent round-trips, improve answer trust, and sharpen PR/CI deltas — all Moat-A (predicate columns, no verdict primitives).
6+
>
7+
> **Plans (execution order):** [evidence-chains](./evidence-chains-on-recipe-rows.md)[graph-estimated-crap](./graph-estimated-crap.md)[coverage-deletion-confidence](./coverage-deletion-confidence.md)[audit-delta-attribution](./audit-delta-attribution.md)
8+
9+
---
10+
11+
## Shared conventions (locked)
12+
13+
| Convention | Applies to |
14+
| ------------------------------------------------------------------------------------- | ---------- |
15+
| **Moat A** — no `pass`/`fail` engine verdict; extra columns only | All four |
16+
| **`reason` TEXT** — machine code + short clause where useful | #1, #3 |
17+
| **`evidence_json` TEXT** — bounded JSON array (≤3 hops) | #1 |
18+
| **`confidence` / `coverage_source` / `attribution`** — recipe-specific enums | #2, #3, #4 |
19+
| **Golden update per slice**`fixtures/golden/minimal/*.json` + `scenarios.json` | All |
20+
| **`/harden-pr lite`** after each tracer commit; **`/harden-pr full`** before PR merge | All |
21+
22+
**Cross-plan synergy:** #1 `reason` on recipes complements #4 `attribution` on audit `added` rows (optional merge in evidence plan v2). #2 and #3 both touch coverage semantics — ship #2 before #3 so agents have CRAP tiers before deletion-confidence narrows rows.
23+
24+
---
25+
26+
## Plan 1 — Evidence chains (`evidence-chains-on-recipe-rows.md`)
27+
28+
| Slice | Deliverable | Verify |
29+
| ----------------------------- | ------------------------------------------------------------------ | --------------------- |
30+
| **1.0 contract** | `docs/golden-queries.md` § evidence columns; one architecture line | doc review |
31+
| **1.1 `boundary-violations`** | `reason` + `evidence_json` in SQL; `.md` + golden | `bun run test:golden` |
32+
| **1.2 `deprecated-symbols`** | caller hops in `evidence_json` | golden + matrix |
33+
| **1.3 `unimported-exports`** | `re_export_chains` LEFT JOIN; `reason` variants | golden |
34+
| **1.4 agent surface** | `templates/agent-content/rule/00-full.md` one-liner | consumer check |
35+
36+
**Open decisions (locked for v1):** E.2 `evidence_json` only (not typed columns); E.1 SQL-only (no query-engine post-processor).
37+
38+
---
39+
40+
## Plan 2 — Graph-estimated CRAP (`graph-estimated-crap.md`)
41+
42+
| Slice | Deliverable | Verify |
43+
| ------------------------- | --------------------------------------------------------------- | ----------------- |
44+
| **2.0 spike** | Reachability CTE on `fixtures/minimal` (script or ad-hoc query) | manual row counts |
45+
| **2.1 recipe** | `high-crap-score.sql` + `.md`; `scenarios.json` | `test:golden` |
46+
| **2.2 measured override** | golden with `ingest-coverage` setup | golden matrix |
47+
| **2.3 cross-link** | `high-complexity-untested.md` points at CRAP when no ingest | doc |
48+
49+
**Grill before 2.1 if spike ambiguous:** Q1 type-only imports in walk (default: value edges only); Q2 recipe id `high-crap-score`.
50+
51+
---
52+
53+
## Plan 3 — Coverage deletion confidence (`coverage-deletion-confidence.md`)
54+
55+
| Slice | Deliverable | Verify |
56+
| -------------------------- | -------------------------------------------------------------- | ------------- |
57+
| **3.1 recipe fork** | `coverage-confirmed-dead.sql` + `.md` from `untested-and-dead` | query CLI |
58+
| **3.2 golden no-ingest** | `confidence: medium` policy (per D.4) | `test:golden` |
59+
| **3.3 golden with ingest** | fixture coverage → `confidence: high` | `test:golden` |
60+
| **3.4 classifier** | intent keywords if needed | optional |
61+
62+
**Grill before 3.1:** Q3 without ingest — `medium` rows vs empty + stderr (plan D.4 leans medium rows).
63+
64+
---
65+
66+
## Plan 4 — Audit delta attribution (`audit-delta-attribution.md`)
67+
68+
| Slice | Deliverable | Verify |
69+
| -------------------------------- | --------------------------------------- | ---------------------- |
70+
| **4.1 `findingKey()`** | pure helper + unit tests | `audit-engine.test.ts` |
71+
| **4.2 `deprecated` delta** | `attribution` on `added[]` for `--base` | branch fixture test |
72+
| **4.3 `files` + `dependencies`** | generalize key sets from base cache | tests |
73+
| **4.4 transport parity** | MCP/HTTP envelope match CLI | handler test |
74+
| **4.5 docs** | `architecture.md` envelope § | doc |
75+
76+
---
77+
78+
## PR cadence
79+
80+
| PR | Contents | Changeset |
81+
| -------------------------- | ------------------------------------ | --------- |
82+
| **#A Evidence wave 1** | Slices 1.0–1.1 (boundary-violations) | patch |
83+
| **#B Evidence wave 2** | Slices 1.2–1.4 | patch |
84+
| **#C CRAP recipe** | Plan 2 complete | patch |
85+
| **#D Deletion confidence** | Plan 3 complete | patch |
86+
| **#E Audit attribution** | Plan 4 complete | patch |
87+
88+
Each PR: `harden-pr full` → merge. Do not batch plans 1–4 into one PR.
89+
90+
---
91+
92+
## Current slice
93+
94+
**Active:** Plan 1 shipped in [**PR #174**](https://github.com/stainless-code/codemap/pull/174) (awaiting merge) — next: Plan 2 spike **2.0** (`graph-estimated-crap.md`).

docs/plans/evidence-chains-on-recipe-rows.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Evidence is **in-SQL**, not a post-processor — same Moat-A path as the recipe.
3535

3636
### Tracer bullet (slice 1)
3737

38-
`boundary-violations`: add `reason` constant + `evidence_json` with rule tuple; one golden query. Ship before touching `unimported-exports` re-export subquery.
38+
`boundary-violations`: add `reason` constant + `evidence_json` with rule tuple; one golden query. Ship before touching `unimported-exports` re-export subquery. **Orchestration:** [agent-enrichment-wave.md](./agent-enrichment-wave.md) § Plan 1 slice 1.1 — shipped `edeee68`.
3939

4040
### Out of scope (v1)
4141

@@ -98,21 +98,21 @@ Ship one recipe per wave; verify before moving to the next.
9898

9999
## Acceptance
100100

101-
- [ ] `codemap query --recipe unimported-exports --json` rows include `reason`; re-export false-positive class includes non-empty `evidence_json` when chain exists
102-
- [ ] `boundary-violations` rows include stable `reason: boundary_deny_match`
103-
- [ ] `deprecated-symbols` rows with callers include `evidence_json` caller hops
104-
- [ ] Golden queries updated; no new CLI verb
105-
- [ ] SARIF / annotations unchanged (extra columns ignored by formatters unless future mapping added)
101+
- [x] `codemap query --recipe unimported-exports --json` rows include `reason`; re-export false-positive class includes non-empty `evidence_json` when chain exists
102+
- [x] `boundary-violations` rows include stable `reason: boundary_deny_match`
103+
- [x] `deprecated-symbols` rows with callers include `evidence_json` caller hops
104+
- [x] Golden queries updated; no new CLI verb
105+
- [x] SARIF / annotations unchanged (extra columns ignored by formatters unless future mapping added)
106106

107107
---
108108

109109
## Open decisions (impl PR)
110110

111-
| # | Question |
112-
| --- | ------------------------------------------------------------------------------------------------- |
113-
| Q1 | Single `evidence_json` vs separate typed columns (`reexport_hops`, `caller_count`)? |
114-
| Q2 | Post-query enrichment in `query-engine` for recipes that opt in via frontmatter `evidence: true`? |
115-
| Q3 | Include `binding_kind` from `bindings` for rename-preview synergy in v1 or v2? |
111+
| # | Question | Lock (wave 2026-06) |
112+
| --- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
113+
| Q1 | Single `evidence_json` vs separate typed columns (`reexport_hops`, `caller_count`)? | **`evidence_json` only** (E.2) — one JSON contract per recipe row |
114+
| Q2 | Post-query enrichment in `query-engine` for recipes that opt in via frontmatter `evidence: true`? | **SQL-only** (E.1) — no post-processor in v1 |
115+
| Q3 | Include `binding_kind` from `bindings` for rename-preview synergy in v1 or v2? | **v2** |
116116

117117
---
118118

fixtures/golden/minimal/boundary-violations.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"to_path": "src/api/client.ts",
55
"rule_name": "ui-no-api",
66
"rule_from_glob": "src/components/**",
7-
"rule_to_glob": "src/api/**"
7+
"rule_to_glob": "src/api/**",
8+
"reason": "boundary_deny_match",
9+
"evidence_json": "[{\"rule_name\":\"ui-no-api\",\"from_glob\":\"src/components/**\",\"to_glob\":\"src/api/**\"}]"
810
}
911
]

fixtures/golden/minimal/call-resolution-stats.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[
22
{
3-
"total_calls": 47,
4-
"resolved_calls": 26,
3+
"total_calls": 48,
4+
"resolved_calls": 27,
55
"method_calls_deferred": 20,
66
"unresolved_queue": 1,
77
"residual_meta": "1"

fixtures/golden/minimal/components-touching-deprecated.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
[
2+
{
3+
"component": "ProductCard",
4+
"component_file": "src/components/shop/ProductCard.tsx",
5+
"deprecated_symbol": "now",
6+
"deprecated_file": "src/utils/date.ts",
7+
"via": "call"
8+
},
29
{
310
"component": "ShopButton",
411
"component_file": "src/components/shop/ShopButton.tsx",

fixtures/golden/minimal/coverage-rows-after-ingest.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,15 @@
22
{
33
"file_path": "src/components/shop/ProductCard.tsx",
44
"name": "ProductCard",
5-
"hit_statements": 3,
6-
"total_statements": 3,
5+
"hit_statements": 2,
6+
"total_statements": 2,
7+
"coverage_pct": 100
8+
},
9+
{
10+
"file_path": "src/components/shop/ProductCard.tsx",
11+
"name": "spread",
12+
"hit_statements": 1,
13+
"total_statements": 1,
714
"coverage_pct": 100
815
},
916
{

fixtures/golden/minimal/deprecated-symbols.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,28 @@
55
"file_path": "src/api/client.ts",
66
"line_start": 46,
77
"signature": "legacyClient()",
8-
"doc_comment": "@deprecated Use `createClient({ baseUrl })` directly. Kept as a fixture for\n`deprecated-symbols` + `--format sarif` / `--format annotations` recipes."
8+
"doc_comment": "@deprecated Use `createClient({ baseUrl })` directly. Kept as a fixture for\n`deprecated-symbols` + `--format sarif` / `--format annotations` recipes.",
9+
"reason": "no_callers",
10+
"evidence_json": "[]"
911
},
1012
{
1113
"name": "now",
1214
"kind": "function",
1315
"file_path": "src/utils/date.ts",
1416
"line_start": 5,
1517
"signature": "now(): number",
16-
"doc_comment": "@deprecated Use `Date.now()` directly. Kept as a fixture for the\n`deprecated-symbols` recipe golden test."
18+
"doc_comment": "@deprecated Use `Date.now()` directly. Kept as a fixture for the\n`deprecated-symbols` recipe golden test.",
19+
"reason": "has_callers",
20+
"evidence_json": "[{\"kind\":\"caller\",\"name\":\"ProductCard\",\"file_path\":\"src/components/shop/ProductCard.tsx\",\"line_start\":12},{\"kind\":\"caller\",\"name\":\"ShopButton\",\"file_path\":\"src/components/shop/ShopButton.tsx\",\"line_start\":10},{\"kind\":\"caller\",\"name\":\"run\",\"file_path\":\"src/consumer.ts\",\"line_start\":27},{\"truncated\":true}]"
1721
},
1822
{
1923
"name": "epochMs",
2024
"kind": "function",
2125
"file_path": "src/utils/format.ts",
2226
"line_start": 5,
2327
"signature": "epochMs(): number",
24-
"doc_comment": "@deprecated Drift detector for the SARIF / GH-annotations golden output.\nPair with `now()` in `./date.ts` to give recipes >1 row to render."
28+
"doc_comment": "@deprecated Drift detector for the SARIF / GH-annotations golden output.\nPair with `now()` in `./date.ts` to give recipes >1 row to render.",
29+
"reason": "has_callers",
30+
"evidence_json": "[{\"kind\":\"caller\",\"name\":\"run\",\"file_path\":\"src/consumer.ts\",\"line_start\":30}]"
2531
}
2632
]

0 commit comments

Comments
 (0)