Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/tier-a-query-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stainless-code/codemap": patch
---

`codemap query` Tier A flags — `--summary`, `--changed-since <ref>`, `--group-by owner|directory|package`, plus per-row `actions` templates on bundled recipes. All output filters; the SQL still executes against the index. Recipes, ad-hoc SQL, and the `cm.query()` programmatic API stay unchanged.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ codemap query "SELECT name, file_path FROM symbols LIMIT 10"
# Bundled SQL (same as skill examples): fan-out rankings
codemap query --json --recipe fan-out
codemap query --json --recipe fan-out-sample
# Counts only (skip the rows) — pairs well with --recipe for dashboards / agent context windows
codemap query --json --summary -r deprecated-symbols
# PR-scoped: filter result rows to those touching files changed since <ref>
codemap query --json --changed-since origin/main -r fan-out
codemap query --json --summary --changed-since HEAD~5 "SELECT file_path FROM symbols"
# Group rows by directory, CODEOWNERS owner, or workspace package
codemap query --json --summary --group-by directory -r fan-in
codemap query --json --group-by owner -r deprecated-symbols
codemap query --json --summary --group-by package "SELECT file_path FROM symbols"
# Recipes append per-row "actions" hints (kebab-case verb + description) in --json output;
# ad-hoc SQL never carries actions. Inspect a recipe's actions via --recipes-json.
# List bundled recipes as JSON, or print one recipe's SQL (no DB required)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
codemap query --recipes-json
codemap query --print-sql fan-out
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ A local SQLite database (`.codemap.db`) indexes the project tree and stores stru

**Commands and flags** (index, query, **`codemap agents init`**, **`--root`**, **`--config`**, environment): [../README.md § CLI](../README.md#cli) — **do not duplicate** flag lists here; this section only adds implementation notes. From this repository: **`bun run dev`** or **`bun src/index.ts`** (same flags).

**Query wiring:** **`src/cli/cmd-query.ts`** (argv, **`printQueryResult`**, `--recipe` / `-r` alias), **`src/cli/query-recipes.ts`** (**`QUERY_RECIPES`** — bundled SQL only source), **`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`. 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)).
**Query wiring:** **`src/cli/cmd-query.ts`** (argv, **`printQueryResult`**, `--recipe` / `-r` alias, **`--summary`**, **`--changed-since`**, **`--group-by`**), **`src/cli/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:`. **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. 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)).

**Validate wiring:** **`src/cli/cmd-validate.ts`** — **`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`.

Expand Down
55 changes: 51 additions & 4 deletions src/application/index-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
SCHEMA_VERSION,
} from "../db";
import type { CodemapDatabase, FileRow } from "../db";
import { filterRowsByChangedFiles } from "../git-changed";
import { globSync } from "../glob-sync";
import { hashContent } from "../hash";
import { extractMarkers } from "../markers";
Expand Down Expand Up @@ -527,19 +528,52 @@ export async function targetedReindex(
/**
* Run read-only SQL and print results to stdout (`console.table`, or JSON when `opts.json`).
* Does not throw on invalid SQL: prints an error and returns **1** (CLI-style). With **`json`**, errors are printed as **`{"error":"<message>"}`** on stdout.
*
* When `opts.summary` is true, only the row count is emitted — `{"count": N}` with `--json`,
* `count: N` otherwise. The SQL still executes against the index; `--summary` filters output, not work.
*
* When `opts.changedFiles` is provided, rows are post-filtered to those whose path columns
* (`path`, `file_path`, `from_path`, `to_path`, `resolved_path`) match at least one entry.
* Rows with no recognised path column pass through (the filter cannot decide; pair with `--summary`
* if the count of changed-touching rows is what's wanted).
*
* When `opts.recipeActions` is provided AND `opts.json` is true, each row gets an `actions`
* key set to the same template (recipe-only feature; ad-hoc SQL never carries actions).
* Rows that already define their own `actions` column are not overwritten.
* @returns **0** on success, **1** on SQL/runtime error.
*/
export function printQueryResult(
sql: string,
opts?: { json?: boolean },
opts?: {
json?: boolean;
summary?: boolean;
changedFiles?: Set<string> | undefined;
recipeActions?: ReadonlyArray<unknown> | undefined;
},
): number {
const json = opts?.json === true;
const summary = opts?.summary === true;
const changedFiles = opts?.changedFiles;
const recipeActions = opts?.recipeActions;
let db: CodemapDatabase | undefined;
try {
db = openDb();
const rows = db.query(sql).all();
if (json) {
console.log(JSON.stringify(rows));
let rows = db.query(sql).all();
if (changedFiles !== undefined) {
rows = filterRowsByChangedFiles(rows, changedFiles);
}
if (summary) {
if (json) {
console.log(JSON.stringify({ count: rows.length }));
} else {
console.log(`count: ${rows.length}`);
}
} else if (json) {
const enriched =
recipeActions !== undefined && recipeActions.length > 0
? rows.map((row) => attachRecipeActions(row, recipeActions))
: rows;
console.log(JSON.stringify(enriched));
} else if (rows.length === 0) {
console.log("(no results)");
} else {
Expand All @@ -561,6 +595,19 @@ export function printQueryResult(
}
}

// Append the recipe's action template to a row without overwriting a pre-existing
// `actions` column from the SQL itself (recipe authors should never collide, but
// defensive: keep the SQL output authoritative).
function attachRecipeActions(
row: unknown,
actions: ReadonlyArray<unknown>,
): unknown {
if (typeof row !== "object" || row === null) return row;
const obj = row as Record<string, unknown>;
if ("actions" in obj) return obj;
return { ...obj, actions };
}

/**
* Rewrites raw SQLite errors that almost always indicate a missing or empty
* `.codemap.db` into an actionable hint. Other errors are returned unchanged.
Expand Down
5 changes: 4 additions & 1 deletion src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ describe("CLI --help", () => {
expect(err).toBe("");
});

test("query --help exits 0 and documents --json", async () => {
test("query --help exits 0 and documents --json + --summary + --group-by", async () => {
const { exitCode, out, err } = await runCli(["query", "--help"]);
expect(exitCode).toBe(0);
expect(out).toContain("--json");
expect(out).toContain("--summary");
expect(out).toContain("--changed-since");
expect(out).toContain("--group-by");
expect(out).toContain("--recipe");
expect(out).toContain("fan-out");
expect(out).toContain("codemap query");
Expand Down
Loading
Loading