# Lessons

Persistent log of corrections and insights from past sessions. Read when relevant; **encode durable lessons in rules/skills** — this file is a staging area, not an archive.

## Rules

1. **Append only durable, non-obvious corrections** — not session trivia already captured in a rule, skill, or reference doc.
2. **Prefer lifting** — when a lesson becomes policy, move it to `.agents/rules/` or the relevant skill and **delete** the bullet here (or supersede with one line pointing at the rule).
3. **Keep entries atomic** — one lesson per bullet. One sentence.
4. **No duplicates** — check before appending; merge or supersede instead.
5. **Supersede, don't accumulate** — outdated lesson → replace with a new bullet citing what changed; don't leave both.

## Lessons

- **changesets bump policy (pre-v1)** — while in `0.x`, default to **patch** for everything (additive features, fixes, docs, internal refactors); reserve **minor** for schema-breaking changes that force a `.codemap/index.db` rebuild (matches 0.2.0 precedent: new tables/columns/`SCHEMA_VERSION` bump). Strict SemVer kicks in only after `1.0.0`. Don't propose `minor` just because new CLI commands or public types were added.
- **agent-content maintenance (pointer protocol)** — when shipping a CLI flag, recipe id, recipe `actions` template, schema column, or any agent-queryable surface, follow [docs/README.md Rule 10](../docs/README.md) in the **same PR**: (1) **recipes** → `templates/recipes/*.{sql,md}` (auto-flows via `20-recipes.gen.md`); (2) **schema** → `src/db.ts` (auto-flows via `30-schema.gen.md`); (3) **narrative** (flags, MCP/HTTP tools, output shapes) → hand-written `templates/agent-content/skill/*.md`; (4) **pointer shape** only → `templates/agents/{rules/codemap,skills/codemap/SKILL}.md` + bump `EXPECTED_POINTER_VERSION` in `agent-content.ts`. Consumer pointers in `.agents/` regenerate via `bun src/index.ts agents init --force` when shape changes — no fat dual-copy sync. Pre-pointer mistake: editing only `.agents/` and leaving served content stale.
- **backticks inside SQL or help-text template literals** — never put a literal backtick inside a `` `...` `` template-literal string. `db.ts` SQL DDL strings (multi-line CREATE TABLE templates) and `printQueryCmdHelp()` (multi-line help text) are both `` `...` `` template literals; an inner backtick — typically a Markdown-style code-fence around a flag like `` `--full` `` — terminates the literal early and the parser blows up several lines later with cryptic "expected `,` or `)`" errors. **Use plain prose in those strings** (`--full` not `` `--full` ``), or escape (`` \` ``) if you really need the character. Hit twice (B.7 + B.6 PR #30); the lesson is general — applies to any TS template literal that gets pasted prose later, not just SQL / help text.
- **STOP-before-Grep applies to symbol lookups too** — `Grep` for symbol names like `printQueryResult`, `getCurrentCommit`, `dropAll` violates the [`codemap` rule](rules/codemap.md). The codemap query `SELECT file_path, line_start FROM symbols WHERE name = '<X>'` answers it faster and without scanning. Reach for `Grep` only when the question is content-shaped (regex over file bodies, finding pattern usages inside function bodies, etc.) — not when it's "where is X defined / who calls X / what does file Y export." This was a PR #30 self-correction.
- **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.
- **Never fabricate quantitative explanations to rationalize surprising data** — when a CI / perf / benchmark number is unexpected (e.g. went up after a "performance" PR), the failure mode is to invent a plausible-sounding cause without verification ("+8 doc files were added since the baseline"). This is intellectual dishonesty that compounds: the fabricated cause gets baked into PR bodies, commit messages, follow-up reasoning, and other agents inherit it as fact. **Pattern:** when a number surprises you, say "I don't know yet" and either (a) instrument / re-run to find the real cause, or (b) accept the unknown and surface it explicitly to the user. Verifiable claim formats: `git diff --name-status A..B | grep '^A'` for file-add counts, `gh run view <id> --log` for CI numbers, repeated CI runs on the same commit for variance characterisation. **Never** invent a numeric explanation that "sounds right". Hit on PR #104 (perf-baseline refresh) — claimed "8 new doc files indexed since cc8daed" explaining a +71ms CI regression; actual delta was 1 file. User caught it; PR was closed.
- **PR titles: no `SCHEMA N` / `SCHEMA_VERSION` suffix** — schema bumps belong in the PR body, changeset, and `db.ts`; putting `(SCHEMA 37)` in the title adds noise and duplicates what reviewers see in the diff. Title = what shipped (`feat(index): callback dispatch synthesis`), not the integer.
- **Perf-baseline flakes are often GHA runner tiers, not code regressions** — `ubuntu-latest` jobs on the same commit can land on fast (~630 ms total) or slow (~1117 ms) runners; cross-job spread dwarfs the +25% gate. **Pattern:** compare pass vs fail CI logs on the same SHA before re-baselining or blaming a PR; capture baseline medians from slow-tier CI logs, use `CODEMAP_PERF_RUNS=5` in CI, and honour `CODEMAP_PERF_REGRESSION_PCT` env during compare (not just `--update`). Hit on main CI May 2026 — #136 merge blocked by `index_create_ms` +31.7% while `total_ms` was only +15%.
