Skip to content

Commit 4061ac3

Browse files
feat(query): --format sarif | annotations (B.8 — pipe rows into GitHub Code Scanning + PR annotations) (#43)
* feat(query): --format <text|json|sarif|annotations> flag parser (Tracer 1 of 6) Adds the parser slice + tests for the new --format flag. No formatter wired yet — sarif/annotations values parse and propagate but render via the existing text/json paths until Tracers 2 + 3 land the actual formatters. Per docs/plans/sarif-formatter.md § D9, --format overrides --json when both passed; --json alone implies --format json (back-compat); default = text. Plan doc committed alongside (created on commit, deleted on ship per docs-governance Rule 3). * feat(query): formatSarif end-to-end + parser combo guards (Tracer 2 of 6) - New application/output-formatters.ts: pure transport-agnostic formatter; SARIF 2.1.0 doc with auto-detected location columns (file_path / path / to_path / from_path priority) and optional region.startLine + .endLine. Recipes without locations emit results: [] + stderr warning (per plan § D6 + § D8). - Wired into runQueryCmd: --format sarif short-circuits to printFormattedQuery before printQueryResult; recipe description / body pulled from getQueryRecipeCatalogEntry to populate rule.shortDescription / fullDescription. - Parser combo guard: --format sarif|annotations cannot be combined with --summary / --group-by / --save-baseline / --baseline (different output shapes — sarif/annotations require flat rows). Tested. - 22 unit tests on the formatter cover location detection, message construction, region emission, ad-hoc rule id (codemap.adhoc), recipe-body fullDescription. - Annotations branch is a stub that prints "not yet implemented" + returns 1 — Tracer 3 lands the actual formatter. Verified end-to-end: bun src/index.ts query --recipe deprecated-symbols --format sarif emits a valid SARIF 2.1.0 doc with one result for the @deprecated fixture symbol. * feat(query): formatAnnotations end-to-end (Tracer 3 of 6) GitHub Actions ::notice file=…,line=…::msg per row. One line per locatable row; rows without a location column are silently skipped (caller decides whether to print a stderr warning); empty input → empty string. Newlines in the message are collapsed to single spaces because the GH parser stops at the first newline. Default level 'notice'; 'warning' / 'error' overrides supported for future per-recipe severity (sarifLevel frontmatter, deferred to v1.x). Verified end-to-end: bun src/index.ts query --recipe deprecated-symbols --format annotations emits the expected ::notice line for the @deprecated fixture symbol. * feat(mcp): format=sarif|annotations on query + query_recipe tools (Tracer 5 of 6) Adds the same --format CLI surface to the MCP query / query_recipe tools so agents can request a formatted text payload directly without piping through codemap query. - New formatEnum on the inputSchema (json | sarif | annotations); 'text' is omitted because terminal-table output is useless to an agent. - formatToolIncompatibility mirrors the CLI parser's incompatibility check (sarif/annotations + summary/group_by → error). - formattedQueryResult shared helper between query and query_recipe — query_recipe passes recipeId so the SARIF rule.id derives to codemap.<recipe>; query (ad-hoc) leaves it undefined → codemap.adhoc. - 4 new MCP server tests cover SARIF on a recipe, annotations on a recipe, sarif+summary rejection, and sarif on ad-hoc SQL. query_batch deliberately not wired — annotation/sarif on a heterogeneous batch is awkward (every statement could ask for a different format) and no real consumer has asked. Defer to v1.x. * docs: sync README + glossary + architecture + agents (Rule 10) + delete plan + changeset (Tracer 6 of 6) - README.md "Daily commands" stripe: add --format <text|json|sarif|annotations> example pair (sarif > findings.sarif and annotations). - docs/glossary.md: new 'SARIF' and 'GH annotations' entries (per Rule 9 — new domain nouns). - docs/architecture.md: new 'Output formatters' wiring paragraph above 'Validate wiring'; covers location auto-detection, rule-id taxonomy, MCP integration, deferred-to-v1.x overrides. - .agents/rules/codemap.md + templates/agents/rules/codemap.md (Rule 10): new 'SARIF / GH annotations' row in the CLI table. - .agents/skills/codemap/SKILL.md + templates/agents/skills/codemap/SKILL.md: query / query_recipe tool descriptions extended with format arg semantics; query_batch deferral noted. - .changeset/sarif-formatter.md: minor changeset (new flag). - docs/plans/sarif-formatter.md: deleted on ship per docs-governance Rule 3. - src/{cli/cmd-query.ts, application/output-formatters.ts}: replaced dangling cross-refs to the deleted plan with cross-refs to architecture.md § Output formatters. * fix(query): respect resolved --format + escape annotation fields + doc --format in CLI help (CodeRabbit on #43) Four CodeRabbit threads, all verified ✅ correct: 1. **(major) Text/JSON path was ignoring --format.** runQueryCmd resolved opts.format in the parser then passed opts.json (raw) to printQueryResult, breaking my own design D9: --format json (no --json) printed a terminal table; --json --format text printed JSON. Fixed by computing effectiveFormat once at the top of runQueryCmd and threading the resulting isJson boolean through every branch (saveBaseline / baselineDiff / groupedQuery / printQueryResult / emitErrorMaybeJson). Two new parser tests lock the precedence (--format text wins over --json; --format json with no flag still emits JSON). 2. **(major) formatAnnotations emitted unescaped fields.** Per actions/toolkit (https://github.com/actions/toolkit/blob/master/packages/core/src/command.ts), property values must escape % \r \n : , and message payloads must escape % \r \n; otherwise file paths with : (Windows drive letters) or , break the annotation, and messages with % get parsed as malformed escape sequences. Added escapeAnnotationData + escapeAnnotationProperty helpers (exported + 5 unit tests covering order-of-operations, empty strings, idempotence). Whitespace collapse still runs first so messages stay single-line by GH spec; the CR/LF escape paths are exercised by property values. 3. **(minor) printQueryCmdHelp didn't advertise --format.** Added the full enum + precedence + sarif/annotations incompatibility note. Both "missing SQL or recipe" usage strings updated to mention --format <fmt>. 4. **(minor) docs/glossary.md alphabetical order.** GH annotations was under ## S (next to SARIF since they shipped together); moved to a new ## G section between F and H per glossary's per-letter structure. Cross-references unchanged. Smoke verified post-fix: `codemap query --format json` emits JSON; `--json --format text` emits text. 37 new + updated unit tests pass; bun run check green. * test(query): lock --format sarif|annotations combo-guard rejection (CodeRabbit nitpick on #43) 7 new parser tests covering every incompatible combo CodeRabbit flagged: --format sarif|annotations × --summary | --group-by | --baseline | --save-baseline (both =name and default-name forms) on ad-hoc SQL + on recipes. Plus 2 negative tests confirming --format text/json compose freely with summary/group-by (text/json don't trip the guard). The guard logic existed since Tracer 2 but only had end-to-end coverage via the SARIF/annotations smoke + the MCP-side mirror tests; these direct parse-time tests prevent regressions when the parser grows new flags.
1 parent e47d1f5 commit 4061ac3

15 files changed

Lines changed: 1237 additions & 19 deletions

File tree

.agents/rules/codemap.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ A local database (default **`.codemap.db`**) indexes structure: symbols, imports
2828
| MCP server (for agent hosts) || `bun src/index.ts mcp` — JSON-RPC on stdio; one tool per CLI verb. See **MCP** section below. |
2929
| Targeted read (metadata) || `bun src/index.ts show <name> [--kind <k>] [--in <path>] [--json]` — file:line + signature |
3030
| Targeted read (source text) || `bun src/index.ts snippet <name> [--kind <k>] [--in <path>] [--json]` — same lookup + source from disk + stale flag |
31+
| SARIF / GH annotations || `bun src/index.ts query --recipe deprecated-symbols --format sarif` · `… --format annotations` |
3132

3233
**Recipe `actions`:** with **`--json`**, recipes that define an `actions` template append it to every row (kebab-case verb + description — e.g. `fan-out``review-coupling`). Under `--baseline`, actions attach to the **`added`** rows only. Inspect via **`--recipes-json`**. Ad-hoc SQL never carries actions.
3334

.agents/skills/codemap/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ Each emitted delta carries its own `base` metadata so mixed-baseline audits are
5858

5959
**Tools (snake_case keys — Codemap convention matching MCP spec examples + reference servers; spec is convention-agnostic. CLI stays kebab; translation lives at the MCP-arg layer.):**
6060

61-
- **`query`** — one SQL statement. Args: `{sql, summary?, changed_since?, group_by?}`. Same envelope as `codemap query --json`.
62-
- **`query_batch`** — MCP-only, no CLI counterpart. Args: `{statements: (string | {sql, summary?, changed_since?, group_by?})[], summary?, changed_since?, group_by?}`. Items are bare SQL strings (inherit batch-wide flag defaults) or objects (override on a per-key basis). Output is N-element array; per-element shape mirrors single-`query`'s output for that statement's effective flag set. Per-statement errors are isolated — failed statements return `{error}` in their slot; siblings still execute. SQL-only (no `recipe` polymorphism in items).
63-
- **`query_recipe`**`{recipe, summary?, changed_since?, group_by?}`. Resolves the recipe id to SQL + per-row actions, then executes like `query`. Unknown recipe id returns a structured `{error}` pointing at the `codemap://recipes` resource.
61+
- **`query`** — one SQL statement. Args: `{sql, summary?, changed_since?, group_by?, format?}`. Same envelope as `codemap query --json`. Pass `format: "sarif"` or `"annotations"` to receive a formatted text payload (SARIF 2.1.0 doc / `::notice` lines); ad-hoc SQL gets `rule.id = codemap.adhoc`. Format is incompatible with `summary` / `group_by` (parser rejects with a structured `{error}`).
62+
- **`query_batch`** — MCP-only, no CLI counterpart. Args: `{statements: (string | {sql, summary?, changed_since?, group_by?})[], summary?, changed_since?, group_by?}`. Items are bare SQL strings (inherit batch-wide flag defaults) or objects (override on a per-key basis). Output is N-element array; per-element shape mirrors single-`query`'s output for that statement's effective flag set. Per-statement errors are isolated — failed statements return `{error}` in their slot; siblings still execute. SQL-only (no `recipe` polymorphism in items). `format` deferred to v1.x — annotation/sarif on a heterogeneous batch is awkward; call `query` per recipe instead.
63+
- **`query_recipe`**`{recipe, summary?, changed_since?, group_by?, format?}`. Resolves the recipe id to SQL + per-row actions, then executes like `query`. Unknown recipe id returns a structured `{error}` pointing at the `codemap://recipes` resource. With `format: "sarif"`, `rule.id = codemap.<recipe>`, `rule.shortDescription` = recipe description, `rule.fullDescription` = the recipe's `<id>.md` body.
6464
- **`audit`**`{baseline_prefix?, baselines?: {files?, dependencies?, deprecated?}, summary?, no_index?}`. Composes per-delta baselines into the `{head, deltas}` envelope. Auto-runs incremental index unless `no_index: true`.
6565
- **`save_baseline`** — polymorphic `{name, sql? | recipe?}` with runtime exclusivity check (mirrors the CLI's single `--save-baseline=<name>` verb). Pass exactly one of `sql` or `recipe`.
6666
- **`list_baselines`** — no args; returns the array `codemap query --baselines --json` would print.

.changeset/sarif-formatter.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@stainless-code/codemap": minor
3+
---
4+
5+
`codemap query --format <text|json|sarif|annotations>` — pipe any recipe row-set into GitHub Code Scanning (SARIF 2.1.0) or surface findings inline on PRs (GH Actions `::notice file=…,line=…::msg`). Pure output-formatter additions on top of the existing JSON pipeline; no schema impact.
6+
7+
Auto-detects file-path columns (`file_path` / `path` / `to_path` / `from_path` priority) and `line_start` (+ optional `line_end`) for SARIF region. Aggregate recipes without locations (`index-summary`, `markers-by-kind`) emit `results: []` + a stderr warning. Rule id is `codemap.<recipe-id>` for `--recipe`, `codemap.adhoc` for ad-hoc SQL. Default `result.level` is `"note"`; per-recipe overrides via `<id>.md` frontmatter (`sarifLevel`, `sarifMessage`, `sarifRuleId`) deferred to v1.x.
8+
9+
`--format` overrides `--json` when both passed; `--json` stays as the alias for `--format json`. Incompatible with `--summary` / `--group-by` / baseline (different output shapes — sarif/annotations only support flat row lists).
10+
11+
MCP `query` and `query_recipe` tools accept the same `format: "sarif" | "annotations"` argument; `query_batch` deferred to v1.x.

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ codemap audit --baseline base --files-baseline hotfix-files # mixed — auto
105105
codemap audit --baseline base --no-index # skip the auto-incremental-index prelude (frozen-DB CI)
106106
# Recipes that define per-row action templates append "actions" hints (kebab-case verb +
107107
# description) in --json output; ad-hoc SQL never carries actions. Inspect via --recipes-json.
108+
# --format <text|json|sarif|annotations> — pipe results into GitHub Code Scanning (SARIF
109+
# 2.1.0) or surface findings inline on PRs (GH Actions ::notice file=…,line=…::msg). Both
110+
# require a flat row list (no --summary / --group-by / baseline). Auto-detects file_path /
111+
# path / to_path / from_path; rule.id is codemap.<recipe-id> (or codemap.adhoc for ad-hoc).
112+
codemap query --recipe deprecated-symbols --format sarif > findings.sarif
113+
codemap query --recipe deprecated-symbols --format annotations # one ::notice per row
108114
# List bundled recipes as JSON, or print one recipe's SQL (no DB required)
109115
codemap query --recipes-json
110116
codemap query --print-sql fan-out

docs/architecture.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ A local SQLite database (`.codemap.db`) indexes the project tree and stores stru
119119

120120
**Query wiring:** **`src/cli/cmd-query.ts`** (argv, **`printQueryResult`**, `--recipe` / `-r` alias, **`--summary`**, **`--changed-since`**, **`--group-by`**, **`--save-baseline`** / **`--baseline`** / **`--baselines`** / **`--drop-baseline`**), **`src/application/query-recipes.ts`** (**`QUERY_RECIPES`** — bundled SQL only source; optional **`actions: RecipeAction[]`** per recipe), **`src/cli/main.ts`** (**`--recipes-json`** / **`--print-sql`** exit before config/DB). With **`--json`**, errors use **`{"error":"…"}`** on stdout for SQL failures, DB open, and bootstrap (same shape); **`runQueryCmd`** sets **`process.exitCode`** instead of **`process.exit`**. Friendlier "no `.codemap.db`" — `no such table: <X>` and `no such column: <X>` errors are rewritten in **`enrichQueryError`** to point at `codemap` / `codemap --full`. **`--summary`** filters output only — the SQL still executes against the index; output collapses to `{"count": N}` (with `--json`) or `count: N`. **`--changed-since <ref>`** post-filters result rows by `path` / `file_path` / `from_path` / `to_path` / `resolved_path` against `git diff --name-only <ref>...HEAD ∪ git status --porcelain` (helper: **`src/git-changed.ts`** — `getFilesChangedSince`, `filterRowsByChangedFiles`, `PATH_COLUMNS`); rows with no recognised path column pass through. **`--group-by <mode>`** (`owner` | `directory` | `package`) routes through **`runGroupedQuery`** in `cmd-query.ts` and emits `{"group_by": "<mode>", "groups": [{key, count, rows}]}` (or `[{key, count}]` with `--summary`); helpers in **`src/group-by.ts`** (`groupRowsBy`, `firstDirectory`, `loadCodeowners`, `discoverWorkspaceRoots`, `makePackageBucketizer`, `codeownersGlobToRegex`). CODEOWNERS lookup is last-match-wins (GitHub semantics); workspace discovery reads `package.json` `workspaces` and `pnpm-workspace.yaml` `packages:`. **`--save-baseline[=<name>]`** snapshots the result to the **`query_baselines`** table inside `.codemap.db` (no parallel JSON files; survives `--full` / SCHEMA bumps because the table is intentionally absent from `dropAll()`); name defaults to `--recipe` id, ad-hoc SQL needs an explicit name. **`--baseline[=<name>]`** replays the SQL, fetches the saved row set, and emits `{baseline:{...}, current_row_count, added: [...], removed: [...]}` (or `{baseline:{...}, current_row_count, added: N, removed: N}` with `--summary`); identity is per-row multiset equality (canonical `JSON.stringify` keyed frequency map — duplicate rows are tracked, not collapsed). No fuzzy "changed" category in v1. **`--group-by` is mutually exclusive** with both `--save-baseline` and `--baseline` (different output shapes). **`--baselines`** (read-only list) and **`--drop-baseline <name>`** complete the surface; helpers in **`src/db.ts`** (`upsertQueryBaseline`, `getQueryBaseline`, `listQueryBaselines`, `deleteQueryBaseline`). **Per-row recipe `actions`** are appended only when the user runs **`--recipe <id>`** with **`--json`** AND the recipe defines an `actions` template — programmatic `cm.query(sql)` and ad-hoc CLI SQL never carry actions; under `--baseline`, actions attach to `added` rows only (the rows the agent should act on). The **`components-by-hooks`** recipe ranks by hook count with a **comma-based tally** on **`hooks_used`** (no SQLite JSON1). Shipped **`templates/agents/`** documents **`codemap query --json`** as the primary agent example ([README § CLI](../README.md#cli)).
121121

122+
**Output formatters:** **`src/application/output-formatters.ts`** — pure transport-agnostic; **`formatSarif`** emits SARIF 2.1.0 (auto-detected location columns: `file_path` / `path` / `to_path` / `from_path` priority + optional `line_start` / `line_end` region; `rule.id = codemap.<recipe-id>` for `--recipe`, `codemap.adhoc` for ad-hoc SQL; aggregate recipes without locations → `results: []` + stderr warning); **`formatAnnotations`** emits `::notice file=…,line=…::msg` GitHub Actions workflow commands (one line per locatable row; messages collapsed to a single line because the GH parser stops at the first newline). Wired into both **`src/cli/cmd-query.ts`** (`--format <text|json|sarif|annotations>`; `--format` overrides `--json`; `sarif` / `annotations` reject `--summary` / `--group-by` / baseline at parse time) and the MCP **`query`** / **`query_recipe`** tools (`format: "sarif" | "annotations"` with the same incompatibility guard). Per-recipe `sarifLevel` / `sarifMessage` / `sarifRuleId` overrides via frontmatter on `<id>.md` deferred to v1.x.
123+
122124
**Validate wiring:** **`src/cli/cmd-validate.ts`** (argv + render) + **`src/application/validate-engine.ts`** (engine — **`computeValidateRows`** + **`toProjectRelative`**). `computeValidateRows` is a pure function over `(db, projectRoot, paths)` returning `{path, status}` rows where `status ∈ stale | missing | unindexed`. CLI wraps it with read-once-and-print + exits **1** on any drift (git-status semantics). Path normalization: **`toProjectRelative`** converts CLI input to POSIX-style relative keys matching the `files.path` storage format (Windows backslash → forward slash); same convention as `lint-staged.config.js`. Also reused by `cmd-show.ts` / `cmd-snippet.ts` and the MCP show/snippet handlers — single canonical implementation.
123125

124126
**Audit wiring:** **`src/cli/cmd-audit.ts`** (argv, `--baseline <prefix>` auto-resolve sugar, `--<key>-baseline <name>` per-delta explicit overrides, `--json`, `--summary`, `--no-index`) + **`src/application/audit-engine.ts`** (delta registry + diff). Mirrors the `cmd-index.ts ↔ application/index-engine.ts` seam — CLI parses + dispatches; engine does the diff. **`runAudit({db, baselines})`** iterates the per-delta baseline map; deltas absent from the map don't run. Each entry in **`V1_DELTAS`** pins a canonical SQL projection (`files`: `SELECT path FROM files`; `dependencies`: `SELECT from_path, to_path FROM dependencies`; `deprecated`: `SELECT name, kind, file_path FROM symbols WHERE doc_comment LIKE '%@deprecated%'`) plus a `requiredColumns` list. **`computeDelta`** validates baseline column-set membership, projects baseline rows down to the canonical column subset (extras dropped — schema-drift-resilient), runs the canonical SQL via the caller's DB connection, and set-diffs via the existing **`src/diff-rows.ts`** multiset helper (shared with `query --baseline`). Each emitted delta carries its own **`base`** metadata so mixed-baseline audits (e.g. `--baseline base --dependencies-baseline override`) are first-class. **`runAuditCmd`** runs an auto-incremental-index prelude (`runCodemapIndex({mode: "incremental", quiet: true})`) before the diff so `head` reflects the current source — `--no-index` opts out for frozen-DB CI scenarios. **`resolveAuditBaselines({db, baselinePrefix, perDelta})`** composes the baseline map: auto-resolves `<prefix>-<delta-key>` for slots that exist (silently absent otherwise) and lets per-delta flags override individual slots. v1 ships no `verdict` / threshold config / non-zero exit codes — consumers compose `--json` + `jq` for CI exit codes; v1.x adds `verdict` + `codemap.config.audit` thresholds + `--base <ref>` (worktree+reindex snapshot strategy).

docs/glossary.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ A `bun scripts/query-golden.ts` regression that compares a query/recipe's output
189189

190190
---
191191

192+
## G
193+
194+
### GH annotations
195+
196+
`::notice file=<path>,line=<n>::<message>`[GitHub Actions workflow command](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message) that surfaces a finding inline on the PR diff without writing a custom Action wrapper. Codemap emits annotations via `codemap query --format annotations` (or `format: "annotations"` on the MCP query tools). One line per locatable row; rows without a location are skipped. Property values + message payload are percent-encoded per [actions/toolkit](https://github.com/actions/toolkit/blob/master/packages/core/src/command.ts) so paths with `:` / `,` and messages with `%` round-trip safely. Default level `notice`; `warning` and `error` overrides supported via the `level` parameter (CLI exposes only the default for v1; per-recipe override comes with the same v1.x frontmatter that grants per-recipe SARIF severity).
197+
198+
---
199+
192200
## H
193201

194202
### hash
@@ -376,6 +384,10 @@ Integer constant in `src/db.ts`. Bumped whenever the DDL changes. `createSchema(
376384

377385
`codemap snippet <name>` — 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 last index — line range may have shifted), and `missing` (true when file is gone). Per-execution shape mirrors `show`'s envelope; source/stale/missing are additive fields. Stale-file behavior: `source` is ALWAYS returned when the file exists; `stale: true` is metadata the agent reads (no refusal, no auto-reindex side-effects from a read tool — agent decides whether to act on possibly-shifted lines or run `codemap` first). See [`architecture.md` § Show / snippet wiring](./architecture.md#cli-usage).
378386

387+
### SARIF
388+
389+
[SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) — Static Analysis Results Interchange Format. JSON envelope GitHub Code Scanning consumes natively. Codemap emits SARIF via `codemap query --format sarif` (or `format: "sarif"` on the MCP `query` / `query_recipe` tools). Rule id is `codemap.<recipe-id>` for `--recipe`; `codemap.adhoc` for ad-hoc SQL. Location columns auto-detected (`file_path` / `path` / `to_path` / `from_path` priority; `line_start` + optional `line_end` for region). Aggregate recipes (`index-summary`, `markers-by-kind`) emit `results: []` + a stderr warning. Incompatible with `--summary` / `--group-by` / baseline (different output shapes). Default `result.level` is `"note"`; per-recipe override deferred to v1.x. See [`architecture.md` § Output formatters](./architecture.md#cli-usage).
390+
379391
### skill
380392

381393
A `.agents/skills/<name>/SKILL.md` file with YAML frontmatter. Longer than a rule; describes a complete agent workflow. Distinct from a **rule** (shorter, normative).

src/application/mcp-server.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,24 @@ describe("MCP server — query tool", () => {
110110
await server.close();
111111
}
112112
});
113+
114+
it("query format=sarif on ad-hoc SQL uses codemap.adhoc rule id", async () => {
115+
const { client, server } = await makeClient();
116+
try {
117+
const r = await client.callTool({
118+
name: "query",
119+
arguments: {
120+
sql: "SELECT path AS file_path FROM files",
121+
format: "sarif",
122+
},
123+
});
124+
const doc = readJson(r);
125+
expect(doc.runs[0].tool.driver.rules[0].id).toBe("codemap.adhoc");
126+
expect(doc.runs[0].results.length).toBeGreaterThan(0);
127+
} finally {
128+
await server.close();
129+
}
130+
});
113131
});
114132

115133
describe("MCP server — query_batch tool", () => {
@@ -291,6 +309,77 @@ describe("MCP server — query_recipe tool", () => {
291309
await server.close();
292310
}
293311
});
312+
313+
it("returns a SARIF doc with format=sarif", async () => {
314+
const db = openDb();
315+
try {
316+
db.run(
317+
`INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, doc_comment)
318+
VALUES ('src/a.ts', 'oldFn', 'function', 1, 5, 'function oldFn()', '/** @deprecated */')`,
319+
);
320+
} finally {
321+
closeDb(db);
322+
}
323+
const { client, server } = await makeClient();
324+
try {
325+
const r = await client.callTool({
326+
name: "query_recipe",
327+
arguments: { recipe: "deprecated-symbols", format: "sarif" },
328+
});
329+
const doc = readJson(r);
330+
expect(doc.version).toBe("2.1.0");
331+
expect(doc.runs[0].tool.driver.rules[0].id).toBe(
332+
"codemap.deprecated-symbols",
333+
);
334+
expect(doc.runs[0].results).toHaveLength(1);
335+
expect(doc.runs[0].results[0].ruleId).toBe("codemap.deprecated-symbols");
336+
} finally {
337+
await server.close();
338+
}
339+
});
340+
341+
it("returns annotation lines with format=annotations", async () => {
342+
const db = openDb();
343+
try {
344+
db.run(
345+
`INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, doc_comment)
346+
VALUES ('src/a.ts', 'oldFn', 'function', 7, 10, 'function oldFn()', '/** @deprecated */')`,
347+
);
348+
} finally {
349+
closeDb(db);
350+
}
351+
const { client, server } = await makeClient();
352+
try {
353+
const r = await client.callTool({
354+
name: "query_recipe",
355+
arguments: { recipe: "deprecated-symbols", format: "annotations" },
356+
});
357+
const text = (r as { content: { text: string }[] }).content[0]!.text;
358+
expect(text).toMatch(/^::notice file=src\/a\.ts,line=7::oldFn/);
359+
} finally {
360+
await server.close();
361+
}
362+
});
363+
364+
it("rejects format=sarif combined with summary", async () => {
365+
const { client, server } = await makeClient();
366+
try {
367+
const r = await client.callTool({
368+
name: "query_recipe",
369+
arguments: {
370+
recipe: "deprecated-symbols",
371+
format: "sarif",
372+
summary: true,
373+
},
374+
});
375+
expect((r as { isError?: boolean }).isError).toBe(true);
376+
expect(readJson(r)).toMatchObject({
377+
error: expect.stringContaining("summary"),
378+
});
379+
} finally {
380+
await server.close();
381+
}
382+
});
294383
});
295384

296385
describe("MCP server — audit / context / validate tools", () => {

0 commit comments

Comments
 (0)