Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
59e9ba4
docs(plans): recipe-recency plan with Q1–Q12 resolved
SutuSebastian May 6, 2026
bcda3a4
feat(recency): recipe_recency schema + engine (Slice 1 of recipe-rece…
SutuSebastian May 6, 2026
6ed1c36
feat(recency): wire recordRecipeRun into both write sites (Slice 2)
SutuSebastian May 6, 2026
8b8ea45
feat(recency): inline last_run_at + run_count on --recipes-json (Slic…
SutuSebastian May 6, 2026
d13e170
feat(recency): opt-out config (recipe_recency: false) — Slice 4
SutuSebastian May 6, 2026
448e94f
docs(recency): lift to architecture.md + glossary.md, retire plan (Sl…
SutuSebastian May 6, 2026
6fcbb18
chore(recency): slim comments + close lifted-to research entry
SutuSebastian May 6, 2026
26272fd
chore(changeset): patch bump for recipe-recency tracking
SutuSebastian May 6, 2026
649f7d2
fix(db): strip semicolon from `--` comment in createTables (Node CI)
SutuSebastian May 6, 2026
5bde603
chore(deps): pin ip-address >=10.2.0 via overrides (CI audit)
SutuSebastian May 6, 2026
dcf8b08
fix(recency): apply triangulated PR audit (3-agent review)
SutuSebastian May 6, 2026
b698a4f
docs(recency): apply low-severity audit findings + sweep comments
SutuSebastian May 6, 2026
df06bca
docs(recency): apply CodeRabbit review — fix stale lazy-on-read prose
SutuSebastian May 6, 2026
4a82bf7
refactor(recency): apply CodeRabbit nitpicks (6 of 9)
SutuSebastian May 6, 2026
976a223
fix(db): strip `;` from `--` comment + add regression guard
SutuSebastian May 6, 2026
5c4485e
chore(recency): final concise-comments sweep before merge
SutuSebastian May 6, 2026
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
3 changes: 3 additions & 0 deletions .agents/lessons.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ Each entry is a single bullet: `- **<topic>** — <lesson>`. Newest entries at t
- **PR / issue / comment bodies always go through a temp file** — never pass markdown bodies via shell heredoc to `gh pr create --body "$(cat <<'EOF'…)"` / `gh pr edit --body …` / `gh pr comment --body …` / `gh issue create --body …` / `gh api` `--field body=…`. Backticks inside the heredoc (every code span and code fence) get shell-escaped to `\`` and render literally on GitHub — every recipe id, file path, flag, SQL fragment, and code fence in the rendered body comes out as `\`coverage\``instead of`coverage`. Pattern: write the body to a temp file (`Write`to`/tmp/pr-<n>-body.md`), pass `--body-file /tmp/pr-<n>-body.md`, then delete the temp file. Cost is one extra tool call; saves redoing every PR body that has more than a few backticks. Hit on PR #57 — final body was a wall of `\`` artifacts until rewritten via temp file.
- **Never commit absolute local user paths** — no `/Users/<name>/…`, `/home/<name>/…`, `~/…`, or `file:///` URIs in any tracked doc, code, comment, or PR body. Reasons: (1) leaks the maintainer's directory structure / username to public mirrors; (2) every other contributor's paths differ — the reference is dead on their machine; (3) a `git clone` of someone else's machine isn't a fact we can cite as a "source for deep-dives" — public upstream URLs are. Pattern: cite `https://github.com/<org>/<repo>` (with optional `/tree/<sha>/<path>`) for upstream sources; use repo-relative paths (`docs/foo.md`, `src/bar.ts`) for in-tree references. Hit on PR #58 first draft — referenced a local peer-repo clone path in a research note before the user caught it.
- **Prescriptive research notes pin every concrete claim before recommending a ship sequence** — when a research/plan-shape doc proposes work (effort estimates, capability inventories, "we already do X" framing), every concrete claim needs a `file:line` / `codemap query` / `rg` / `--recipes-json` reference a reviewer can re-run. Reasoning-from-substrate intuition without pinning ships errors: "the AST walker already counts nodes" / "fan-in detects orphans" / "the `re_export_source` column doesn't exist" — all real errors caught on PR #58 by triangulating against the codebase. Don't ship a peer / parallel "descriptive baseline" doc to triangulate against (Rule 1 violation — it duplicates `architecture.md` / `db.ts` / `--recipes-json`); instead, either (a) pin claims in the prescriptive doc itself, or (b) self-audit by re-running every claim against the canonical home before committing. Either path beats the "dual descriptive + prescriptive doc" pattern on docs-governance grounds.
- **Semicolons inside `--` line comments in `db.ts` DDL strings break Node CI** — `architecture.md` notes this but the lesson wasn't in this file. `runSql()` on Node splits multi-statement SQL on `;` (because `better-sqlite3` allows one statement per `prepare()`); a `;` inside an inner `--` comment (e.g. `-- recipe_id is loose (matches a or b; no FK)`) creates a comment-only fragment, which `prepare()` rejects with `RangeError: The supplied SQL string contains no statements`. **Bun tests don't catch this** — `bun:sqlite` accepts multi-statement SQL natively, so `bun test` + `bun run check` are both green; the failure surfaces only when CI runs `node dist/index.mjs --full`. **Pattern:** rewrite comment-side `;` to a comma or em-dash; sanity-check with a tiny harness that splits the createTables template on `;` and prints any fragment that's all-`--`-comments. Hit on PR #76. The validator can be a one-liner — adding it as `bun run smoke:node` is a candidate roadmap item.
- **`process.exitCode` is NOT a safe success oracle inside CLI verbs** — when a CLI handler in `runQueryCmd` (or any sibling that supports `--ci`) needs to know "did the work succeed?" for a side-effect decision (recency tracking, telemetry, follow-up writes), do NOT key off `process.exitCode !== 1`. Two failure modes: (1) `--ci` deliberately sets `exitCode = 1` on findings as the CI gating signal even though the recipe ran cleanly — keying off exitCode then treats success as failure (PR #76 audit Finding 1: `--ci + --recipe X --format sarif` exits 1 → recency NOT recorded → undercounts every CI run). (2) The exit code is process-global and survives across calls when an exported helper is invoked multiple times in one process (tests, programmatic use); a prior failure poisons later success. **Pattern:** track an explicit `recipeQuerySucceeded` (or domain-named) local flag, set true at each successful exit point inside the try block; check it in the finally. For helpers that conflate "ran cleanly" with "exit 1 because findings" (like `printFormattedQuery`), refactor to a discriminated-union return shape (`{ ok: true, exitCode: 0 | 1 } | { ok: false }`) so callers can disambiguate. Hit on PR #76 — caught by 2/3 PR-review agents (Codex + gpt-5.5) with concrete repros.
- **Read-side substrate must be pure even when "lazy on read" looks cheap** — when a write-side cleanup (DELETE-on-prune, GC sweep, cache invalidation) seems cheaper to run lazily on the read path, weigh that against the documented contract of the read site. If the read surface promises "no side effects" / "no DB required" / "read resource" semantics (per CLI help text, README, MCP resource conventions), an in-place DELETE breaks the contract — even under WAL where the write doesn't block readers. **Pattern:** filter at SELECT time (`WHERE last_run_at >= cutoff`) for read paths, do eager prune on the write path (typically cheap on tiny domain tables — index-bounded scan is microseconds). The "hot path" cost concern that motivates lazy-on-read usually doesn't hold for tiny tables, but the read-purity invariant is durable across N agents reading the resource. Hit on PR #76 — Q3 of the recipe-recency plan locked lazy-on-read with reasoning that didn't model the "No DB required" `--recipes-json` contract; gpt-5.5 audit caught it.
2 changes: 1 addition & 1 deletion .agents/rules/codemap.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ A local database (default **`.codemap/index.db`**) indexes structure: symbols, i
| Parametrised recipe | — | `bun src/index.ts query --json --recipe find-symbol-by-kind --params kind=function,name_pattern=%Query%` — params declared in recipe `.md` frontmatter and validated before SQL binding. |
| Boundary violations | — | `bun src/index.ts query --json --recipe boundary-violations` — joins `dependencies` × `boundary_rules` (config-driven) via SQLite `GLOB`. `.codemap/config.ts` `boundaries: [{name, from_glob, to_glob, action?}]`; default `action: "deny"`. SARIF / annotations work via the `file_path` alias. |
| Rename preview | — | `bun src/index.ts query --recipe rename-preview --params old=usePermissions,new=useAccess,kind=function --format diff` — read-only unified diff; codemap never writes files. |
| Recipe catalog / SQL | — | `bun src/index.ts query --recipes-json` · `bun src/index.ts query --print-sql fan-out` |
| Recipe catalog / SQL | — | `bun src/index.ts query --recipes-json` (every entry includes `last_run_at: number \| null` + `run_count: number` recency fields; rank with `jq 'sort_by(.last_run_at // 0) \| reverse'`; opt-out via `.codemap/config` `recipe_recency: false`) · `bun src/index.ts query --print-sql fan-out` |
| Counts only | — | `bun src/index.ts query --json --summary -r deprecated-symbols` |
| PR-scoped rows | — | `bun src/index.ts query --json --changed-since origin/main -r fan-out` |
| Bucket by owner / dir / pkg | — | `bun src/index.ts query --json --group-by directory -r fan-in` |
Expand Down
8 changes: 4 additions & 4 deletions .agents/skills/codemap/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Replace placeholders (`'...'`) with your module path, file glob, or symbol name.

**Suppressions (opt-in):** `// codemap-ignore-next-line <recipe-id>` and `// codemap-ignore-file <recipe-id>` (also `#`, `--`, `<!--`, `/*` leaders) get parsed into the `suppressions(file_path, line_number, recipe_id)` table. Recipe authors opt in by `LEFT JOIN`-ing on `(file_path, recipe_id)` with `line_number = 0` for file scope or `line_number = <row's line>` for next-line. Ad-hoc SQL is unaffected. No severity, no suppression-by-default, no universal-honor — consumer-chosen substrate. Today's opt-in recipes: `untested-and-dead` (line + file), `unimported-exports` (file only — exports has no `line_number`).

**CLI shortcuts:** **`bun src/index.ts query --json --recipe <id>`** runs bundled SQL (preferred for agents). **`bun src/index.ts query --recipe <id>`** without **`--json`** prints a table. **`bun src/index.ts query --recipes-json`** prints every bundled recipe (**`id`**, **`description`**, **`sql`**, optional **`actions`**) as JSON (no index / DB required). **`bun src/index.ts query --print-sql <id>`** prints one recipe’s SQL only. Ids include **`fan-out`**, **`fan-out-sample`** (**`GROUP_CONCAT`** samples), **`fan-out-sample-json`** (same, but **`json_group_array`** — needs SQLite JSON1), **`fan-in`**, **`index-summary`**, **`files-largest`**, **`components-by-hooks`**, **`components-touching-deprecated`** (UNION of hook + call paths to `@deprecated` symbols), **`markers-by-kind`**, **`deprecated-symbols`**, **`refactor-risk-ranking`** (per-file `(fan_in + 1) × (100 - avg_coverage_pct)`), **`high-complexity-untested`** (cyclomatic complexity ≥ 10 + coverage < 50%; per-function), **`text-in-deprecated-functions`** (FTS5 ⨯ symbols ⨯ coverage demo — needs `--with-fts` enabled), **`unimported-exports`** (exports with no detectable importer; v1 doesn't follow re-export chains — see recipe `.md` for caveats), **`unused-type-members`** (advisory; field-level enumeration of `unimported-exports` joining `type_members`; codemap can't see indexed access / `keyof T` / mapped types / destructuring — see recipe `.md` for caveats), **`visibility-tags`**, **`barrel-files`**, **`files-hashes`**, **`untested-and-dead`** (exported AND uncalled AND uncovered), **`files-by-coverage`** (per-file rollup of statement coverage), **`worst-covered-exports`** (lowest-covered exported symbols) — see **`bun src/index.ts query --help`**.
**CLI shortcuts:** **`bun src/index.ts query --json --recipe <id>`** runs bundled SQL (preferred for agents). **`bun src/index.ts query --recipe <id>`** without **`--json`** prints a table. **`bun src/index.ts query --recipes-json`** prints every bundled recipe (**`id`**, **`description`**, **`sql`**, optional **`actions`**, plus **`last_run_at`** + **`run_count`** recency fields) as JSON (no DB required for the catalog itself; recency populates when an indexed DB exists, otherwise null/0). **`bun src/index.ts query --print-sql <id>`** prints one recipe’s SQL only. Ids include **`fan-out`**, **`fan-out-sample`** (**`GROUP_CONCAT`** samples), **`fan-out-sample-json`** (same, but **`json_group_array`** — needs SQLite JSON1), **`fan-in`**, **`index-summary`**, **`files-largest`**, **`components-by-hooks`**, **`components-touching-deprecated`** (UNION of hook + call paths to `@deprecated` symbols), **`markers-by-kind`**, **`deprecated-symbols`**, **`refactor-risk-ranking`** (per-file `(fan_in + 1) × (100 - avg_coverage_pct)`), **`high-complexity-untested`** (cyclomatic complexity ≥ 10 + coverage < 50%; per-function), **`text-in-deprecated-functions`** (FTS5 ⨯ symbols ⨯ coverage demo — needs `--with-fts` enabled), **`unimported-exports`** (exports with no detectable importer; v1 doesn't follow re-export chains — see recipe `.md` for caveats), **`unused-type-members`** (advisory; field-level enumeration of `unimported-exports` joining `type_members`; codemap can't see indexed access / `keyof T` / mapped types / destructuring — see recipe `.md` for caveats), **`visibility-tags`**, **`barrel-files`**, **`files-hashes`**, **`untested-and-dead`** (exported AND uncalled AND uncovered), **`files-by-coverage`** (per-file rollup of statement coverage), **`worst-covered-exports`** (lowest-covered exported symbols) — see **`bun src/index.ts query --help`**.

**Output flags** (compose with **`--recipe`** or ad-hoc SQL):

Expand Down Expand Up @@ -80,10 +80,10 @@ Each emitted delta carries its own `base` metadata so mixed-baseline audits are
- **`snippet`** — `{name, kind?, in?}`. Same lookup as `show` but each match also carries `source` (file lines from disk at `line_start..line_end`), `stale` (true when content_hash drifted since indexing — line range may have shifted), `missing` (true when file is gone). Per Q-6 (settled): `source` is always returned when the file exists; agent decides whether to act on stale content or run `codemap` / `codemap --files <path>` to re-index first. No auto-reindex side-effects from this read tool.
- **`impact`** — `{target, direction?, via?, depth?, limit?, summary?}`. Symbol/file blast-radius walker — replaces hand-composed `WITH RECURSIVE` queries that agents struggle to write reliably. `target` is a symbol name (case-sensitive, exact) OR a project-relative file path (auto-detected by `/` or by matching `files.path`). `direction`: `up` (callers / dependents), `down` (callees / dependencies), `both` (default). `via`: `dependencies`, `calls`, `imports`, `all` (default — every backend compatible with the resolved target kind: symbol → `calls`; file → `dependencies` + `imports`; mismatched explicit choices land in `skipped_backends`, no error). `depth` default 3, `0` = unbounded (still cycle-detected and limit-capped). `limit` default 500. `summary: true` trims `matches` for cheap CI-gate consumption (`jq '.summary.nodes'`) but preserves the count. Result: `{target, direction, via, depth_limit, matches: [{depth, direction, edge, kind, name?, file_path}], summary: {nodes, max_depth_reached, by_kind, terminated_by: 'depth'|'limit'|'exhausted'}}`. Cycle detection is approximate-but-bounded — bounded depth + `LIMIT` keep cyclic graphs cheap; `terminated_by` reports the dominant stop reason. SARIF / annotations not supported (impact rows are graph traversals, not findings).

**Resources (lazy-cached on first `read_resource`; constant for server-process lifetime):**
**Resources (mostly lazy-cached on first `read_resource`; recipes / one-recipe live-read every call so the inline recency fields stay fresh):**

- **`codemap://recipes`** — full catalog JSON (same as `--recipes-json`). Each entry carries `source: "bundled" | "project"` and `shadows: true` on project entries that override a bundled recipe id. Read this at session start so you know when a `--recipe foo` call will run a project override instead of the documented bundled version.
- **`codemap://recipes/{id}`** — single recipe `{id, description, body?, sql, actions?, source, shadows?}`. Replaces `--print-sql <id>`.
- **`codemap://recipes`** — full catalog JSON (same as `--recipes-json`). Each entry carries `source: "bundled" | "project"`, optional `shadows: true` on project entries that override a bundled recipe id, plus `last_run_at: number | null` and `run_count: number` recency fields (rank with `jq 'sort_by(.last_run_at // 0) | reverse'`). Read this at session start so you know when a `--recipe foo` call will run a project override instead of the documented bundled version.
- **`codemap://recipes/{id}`** — single recipe `{id, description, body?, sql, actions?, source, shadows?, last_run_at, run_count}`. Replaces `--print-sql <id>`.
- **`codemap://schema`** — DDL of every table in `.codemap/index.db` (queried live from `sqlite_schema`).
- **`codemap://skill`** — full text of bundled `templates/agents/skills/codemap/SKILL.md`. Agents that don't preload the skill at session start can fetch it here.
- **`codemap://files/{path}`** — per-file roll-up. Returns `{path, language, line_count, symbols, imports, exports, coverage}` where `imports.specifiers` is parsed JSON and `coverage` is `{measured_symbols, avg_coverage_pct, per_symbol}` or `null` when the file has no measured coverage. URI-encode the path. Reads live (no caching).
Expand Down
13 changes: 13 additions & 0 deletions .changeset/recipe-recency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@stainless-code/codemap": patch
---

**Recipe-recency tracking** — every successful `--recipe` call now writes to a new `recipe_recency(recipe_id PK, last_run_at, run_count)` table. `--recipes-json` and the matching `codemap://recipes` / `codemap://recipes/{id}` MCP resources gain inline `last_run_at: number | null` + `run_count: number` fields per entry, so agent hosts can rank live recipes ahead of historic ones via `jq 'sort_by(.last_run_at // 0) | reverse'`. Default ON; opt-out via `.codemap/config` `recipe_recency: false` (short-circuits before any DB write — no rows ever land).

Two write sites both call a shared `recordRecipeRun` helper from `application/recipe-recency.ts`: `handleQueryRecipe` in `application/tool-handlers.ts` (covers MCP + HTTP — both flow through it) and `runQueryCmd` in `cli/cmd-query.ts` (CLI — finally-block observes `process.exitCode` as the unified success signal). Counts only successful runs; recency-write failures are swallowed with a stderr `[recency] write failed: <reason>` warning so they NEVER block the recipe response. The 90-day rolling window is enforced lazily on `--recipes-json` reads (no DELETE on the write path).
Comment thread
SutuSebastian marked this conversation as resolved.
Outdated

The MCP/HTTP catalog cache was dropped — caching the JSON.stringify result alongside recency would freeze `last_run_at` at first-read forever per long-running `codemap mcp` / `codemap serve` lifetime. The underlying `listQueryRecipeCatalog()` is itself module-cached upstream, so the extra cost is one DB-read + one JSON.stringify per call. Schema / skill resources stay cached.

**Local-only — no upload primitive ever ships.** The Floor exists to resist accumulation pressure. Sibling to `query_baselines` / `coverage`: intentionally absent from `dropAll()` so `--full` and `SCHEMA_VERSION` rebuilds preserve user-activity history. **No `SCHEMA_VERSION` bump** — the new table is purely additive and lands on existing DBs via `CREATE TABLE IF NOT EXISTS` on next boot.

Schema docs: `architecture.md` § `recipe_recency`. Term entry: `glossary.md`. Bundled agent rule + skill (`templates/agents/`) + dev-side mirror (`.agents/`) updated in lockstep per Rule 10.
Loading
Loading