Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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. Ad-hoc SQL and the `cm.query()` programmatic API stay unchanged.
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 that define per-row action templates append "actions" hints (kebab-case verb +
# description) in --json output; ad-hoc SQL never carries actions. Inspect via --recipes-json.
# List bundled recipes as JSON, or print one recipe's SQL (no DB required)
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