Skip to content

Commit efcb010

Browse files
committed
feat(cli): codemap validate, codemap context, --performance, friendlier no-DB error; JSDoc the public type surface
CLI commands and flags - codemap validate [--json] [paths...] — diffs disk SHA-256 against files.content_hash; statuses: stale | missing | unindexed; exits 1 on any drift (git-status semantics) so agents can branch on $? - codemap context [--compact] [--for "<intent>"] — stable JSON envelope (project metadata, top hubs, recent markers, recipe catalog). --for runs lightweight intent classification (refactor / debug / test / feature / explore / other) and returns matched recipe ids plus a hint - codemap --performance — per-phase breakdown (collect / parse / insert / index_create) plus top-10 slowest files by parse time during full rebuild; surfaces pathological inputs - Friendlier no-.codemap.db error — `no such table: <X>` rewrites to an actionable hint pointing at `codemap` / `codemap --full`, on both the JSON and human paths - bootstrap.ts whitelists the new subcommands and flag Public type surface (visible in dist/index.d.mts via the existing index re-exports) - New IndexPerformanceReport; IndexRunStats.performance? field - Per-field JSDoc on IndexResult, IndexRunStats, IndexPerformanceReport, IndexTableStats (snake_case key note), ResolvedCodemapConfig - Interface-level JSDoc on every db.ts row interface (FileRow, SymbolRow, ImportRow, ExportRow, ComponentRow, DependencyRow, MarkerRow, CssVariableRow, CssClassRow, CssKeyframeRow, CallRow, TypeMemberRow) with cross-links to docs/architecture.md § Schema as the single source - Per-field JSDoc on ParsedFile (clarifies error vs parseError, category semantics, CSS-only fields, cssImportSources main-thread conversion) - @param/@returns on getAdapterForExtension Implementation plumbing - ParsedFile gains optional parseMs?, populated by parse-worker-core.ts - RunIndexOptions.performance threads through index-engine.ts and run-index.ts - index-engine.ts times collect / parse / insert / createIndexes phases when the flag is on; bottom-of-summary block prints the breakdown plus the slowest_files list Auto-fixes from the new lint baseline - import type hoisting (consistent-type-specifier-style: prefer-top-level) - type X = {...} → interface X {...} for object shapes (ValidateRow, ValidateOpts, ContextEnvelope, ContextOpts) - switch case braces in src/parser.ts (12 keyword-mapping cases) and src/agents-init.ts Tests - 9 new tests for cmd-validate (parse + computeValidateRows: stale, missing, unindexed, dedup, sort) - 14 new tests for cmd-context (parse + classifyIntent: 6 categories + fallback) - 2 new cases on cmd-query for -r Changeset entered as patch (matches 0.x precedent for additive changes; schema unchanged).
1 parent ab32f87 commit efcb010

24 files changed

Lines changed: 1070 additions & 70 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@stainless-code/codemap": patch
3+
---
4+
5+
Agent-friendly CLI surface: `codemap validate`, `codemap context`, `--performance`, `-r` recipe alias, and four new bundled query recipes.
6+
7+
- **New: `codemap validate [--json] [paths...]`** — diffs the on-disk SHA-256 of indexed files against `files.content_hash` and prints stale / missing / unindexed rows. Lets agents skip re-reads they don't need; exits `1` on any drift (git-status semantics)
8+
- **New: `codemap context [--compact] [--for "<intent>"]`** — emits a stable JSON envelope (project metadata, top hubs, recent markers, recipe catalog) for any agent or editor that wants the index in one cheap shot. `--for` runs lightweight intent classification (refactor / debug / test / feature / explore / other) and returns matched recipe ids plus a hint
9+
- **New: `codemap --performance`** flag — prints a per-phase timing breakdown (collect / parse / insert / index_create) and the top-10 slowest files by parse time during full rebuilds, for triaging giant or pathological inputs
10+
- **New: `-r` short alias for `codemap query --recipe`** + cleaner organized `codemap query --help` (sectioned flags, dynamic recipe-id padding, examples for both forms)
11+
- **New recipes**: `deprecated-symbols` (`@deprecated` JSDoc tag scan), `visibility-tags` (`@internal` / `@private` / `@alpha` / `@beta`), `files-hashes` (powers `validate`), `barrel-files` (top files by export count)
12+
- **Friendlier no-`.codemap.db` error**: `no such table: <X>` now rewrites to an actionable hint pointing at `codemap` / `codemap --full`, on both the JSON and human paths
13+
- **Public type surface**: new `IndexPerformanceReport`; `IndexRunStats.performance?` field; per-field JSDoc coverage on `IndexResult`, `IndexRunStats`, `ResolvedCodemapConfig`, all `db.ts` row interfaces (`FileRow`, `SymbolRow`, `ImportRow`, `ExportRow`, `ComponentRow`, `DependencyRow`, `MarkerRow`, `CssVariableRow`, `CssClassRow`, `CssKeyframeRow`, `CallRow`, `TypeMemberRow`), and `ParsedFile`
14+
- **Documentation**: README now leads with a "What you get" Grep/Read vs Codemap capability table and a "Daily commands" stripe; `docs/why-codemap.md` adds a "What Codemap is **not**" anti-pitch section and a scenario-keyed token-savings table (single lookup → 50-turn session) replacing the earlier hand-wave
15+
- **Stricter lint baseline**: enabled `prefer-const`, `consistent-type-specifier-style`, `consistent-type-definitions`, `no-confusing-non-null-assertion`, `no-unnecessary-{boolean-literal-compare,template-expression,type-assertion}`, `prefer-{includes,nullish-coalescing,optional-chain}`, and `unicorn/switch-case-braces`

scripts/benchmark-query-output.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import { fileURLToPath } from "node:url";
1313
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
1414
const INDEX_TS = join(REPO_ROOT, "src/index.ts");
1515

16-
type Scenario = { label: string; args: string[] };
16+
interface Scenario {
17+
label: string;
18+
args: string[];
19+
}
1720

1821
const SCENARIOS: Scenario[] = [
1922
{

scripts/query-golden.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@ import { fileURLToPath } from "node:url";
55

66
import { createCodemap } from "../src/api";
77
import { getQueryRecipeSql } from "../src/cli/query-recipes";
8-
import {
9-
type GoldenMatch,
10-
type GoldenScenario,
11-
parseScenariosJson,
12-
} from "./query-golden/schema";
8+
import { parseScenariosJson } from "./query-golden/schema";
9+
import type { GoldenMatch, GoldenScenario } from "./query-golden/schema";
1310

1411
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
1512

src/adapters/builtin.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ export const BUILTIN_ADAPTERS: readonly LanguageAdapter[] = [
7777
},
7878
];
7979

80+
/**
81+
* First-match lookup of a {@link LanguageAdapter} by file extension.
82+
*
83+
* @param ext - File extension **with leading dot**, e.g. `".tsx"`. Compared
84+
* verbatim against each adapter's `extensions` array.
85+
* @param adapters - Adapter list to search; defaults to {@link BUILTIN_ADAPTERS}.
86+
* Pass a custom list to support project-local adapters once a registration
87+
* API lands (see [docs/roadmap.md](../../docs/roadmap.md)).
88+
* @returns The first adapter whose `extensions` contains `ext`, or `undefined`
89+
* when no adapter matches (the indexer then falls back to markers-only text).
90+
*/
8091
export function getAdapterForExtension(
8192
ext: string,
8293
adapters: readonly LanguageAdapter[] = BUILTIN_ADAPTERS,

src/agents-init.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -366,10 +366,11 @@ export function applyAgentsInitTargets(
366366

367367
for (const t of targets) {
368368
switch (t) {
369-
case "cursor":
369+
case "cursor": {
370370
applyCursorIntegration(projectRoot, linkMode, force);
371371
break;
372-
case "windsurf":
372+
}
373+
case "windsurf": {
373374
wireAgentsRulesTo(
374375
projectRoot,
375376
join(projectRoot, ".windsurf", "rules"),
@@ -378,7 +379,8 @@ export function applyAgentsInitTargets(
378379
force,
379380
);
380381
break;
381-
case "continue":
382+
}
383+
case "continue": {
382384
wireAgentsRulesTo(
383385
projectRoot,
384386
join(projectRoot, ".continue", "rules"),
@@ -387,7 +389,8 @@ export function applyAgentsInitTargets(
387389
force,
388390
);
389391
break;
390-
case "cline":
392+
}
393+
case "cline": {
391394
wireAgentsRulesTo(
392395
projectRoot,
393396
join(projectRoot, ".clinerules"),
@@ -396,7 +399,8 @@ export function applyAgentsInitTargets(
396399
force,
397400
);
398401
break;
399-
case "amazon-q":
402+
}
403+
case "amazon-q": {
400404
wireAgentsRulesTo(
401405
projectRoot,
402406
join(projectRoot, ".amazonq", "rules"),
@@ -405,15 +409,17 @@ export function applyAgentsInitTargets(
405409
force,
406410
);
407411
break;
408-
case "claude-md":
412+
}
413+
case "claude-md": {
409414
upsertCodemapPointerFile(
410415
join(projectRoot, "CLAUDE.md"),
411416
CLAUDE_MD_TEMPLATE,
412417
"CLAUDE.md",
413418
force,
414419
);
415420
break;
416-
case "copilot":
421+
}
422+
case "copilot": {
417423
mkdirSync(join(projectRoot, ".github"), { recursive: true });
418424
upsertCodemapPointerFile(
419425
join(projectRoot, ".github", "copilot-instructions.md"),
@@ -422,22 +428,25 @@ export function applyAgentsInitTargets(
422428
force,
423429
);
424430
break;
425-
case "agents-md":
431+
}
432+
case "agents-md": {
426433
upsertCodemapPointerFile(
427434
join(projectRoot, "AGENTS.md"),
428435
AGENTS_MD_TEMPLATE,
429436
"AGENTS.md",
430437
force,
431438
);
432439
break;
433-
case "gemini-md":
440+
}
441+
case "gemini-md": {
434442
upsertCodemapPointerFile(
435443
join(projectRoot, "GEMINI.md"),
436444
GEMINI_MD_TEMPLATE,
437445
"GEMINI.md",
438446
force,
439447
);
440448
break;
449+
}
441450
}
442451
}
443452
}

src/api.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { resolve } from "node:path";
22

33
import { queryRows } from "./application/index-engine";
4-
import { runCodemapIndex, type RunIndexOptions } from "./application/run-index";
4+
import { runCodemapIndex } from "./application/run-index";
5+
import type { RunIndexOptions } from "./application/run-index";
56
import type { IndexResult } from "./application/types";
6-
import {
7-
type CodemapUserConfig,
8-
loadUserConfig,
9-
resolveCodemapConfig,
10-
} from "./config";
7+
import { loadUserConfig, resolveCodemapConfig } from "./config";
8+
import type { CodemapUserConfig } from "./config";
119
import { closeDb, openDb } from "./db";
1210
import { configureResolver } from "./resolver";
1311
import {

src/application/index-engine.ts

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ import {
2828
insertCalls,
2929
getAllFileHashes,
3030
SCHEMA_VERSION,
31-
type CodemapDatabase,
32-
type FileRow,
3331
} from "../db";
32+
import type { CodemapDatabase, FileRow } from "../db";
3433
import { globSync } from "../glob-sync";
3534
import { hashContent } from "../hash";
3635
import { extractMarkers } from "../markers";
@@ -39,7 +38,11 @@ import { extractFileData } from "../parser";
3938
import { resolveImports } from "../resolver";
4039
import { getIncludePatterns, getProjectRoot, isPathExcluded } from "../runtime";
4140
import { parseFilesParallel } from "../worker-pool";
42-
import type { IndexRunStats, IndexTableStats } from "./types";
41+
import type {
42+
IndexPerformanceReport,
43+
IndexRunStats,
44+
IndexTableStats,
45+
} from "./types";
4346

4447
export const VALID_EXTENSIONS = new Set(Object.keys(LANG_MAP));
4548

@@ -270,10 +273,15 @@ export async function indexFiles(
270273
filePaths: string[],
271274
fullRebuild: boolean,
272275
knownIndexedPaths?: Set<string>,
273-
options?: { quiet?: boolean },
276+
options?: { quiet?: boolean; performance?: boolean; collectMs?: number },
274277
): Promise<IndexRunStats> {
275278
const quiet = options?.quiet ?? false;
279+
const wantPerformance = options?.performance === true;
276280
const startTime = performance.now();
281+
let parseMs = 0;
282+
let insertMs = 0;
283+
let indexCreateMs = 0;
284+
let slowest: { path: string; parse_ms: number }[] = [];
277285

278286
if (fullRebuild) {
279287
dropAll(db);
@@ -290,9 +298,20 @@ export async function indexFiles(
290298
let skipped = 0;
291299

292300
if (fullRebuild) {
301+
const parseStart = performance.now();
293302
const results = await parseFilesParallel(filePaths);
303+
parseMs = performance.now() - parseStart;
294304
results.sort((a, b) => a.relPath.localeCompare(b.relPath));
305+
if (wantPerformance) {
306+
slowest = results
307+
.filter((r) => typeof r.parseMs === "number")
308+
.map((r) => ({ path: r.relPath, parse_ms: Math.round(r.parseMs!) }))
309+
.sort((a, b) => b.parse_ms - a.parse_ms)
310+
.slice(0, 10);
311+
}
312+
const insertStart = performance.now();
295313
indexed = insertParsedResults(db, results, indexedPaths);
314+
insertMs = performance.now() - insertStart;
296315
} else {
297316
const existingHashes = getAllFileHashes(db);
298317
const root = getProjectRoot();
@@ -390,7 +409,9 @@ export async function indexFiles(
390409
}
391410

392411
if (fullRebuild) {
412+
const idxStart = performance.now();
393413
createIndexes(db);
414+
indexCreateMs = performance.now() - idxStart;
394415
db.run("PRAGMA synchronous = NORMAL");
395416
db.run("PRAGMA foreign_keys = ON");
396417
setMeta(db, "schema_version", String(SCHEMA_VERSION));
@@ -407,6 +428,19 @@ export async function indexFiles(
407428
const elapsed = Math.round(performance.now() - startTime);
408429
const stats = fetchTableStats(db);
409430

431+
let perf: IndexPerformanceReport | undefined;
432+
if (wantPerformance) {
433+
const collectMs = Math.round(options?.collectMs ?? 0);
434+
perf = {
435+
collect_ms: collectMs,
436+
parse_ms: Math.round(parseMs),
437+
insert_ms: Math.round(insertMs),
438+
index_create_ms: Math.round(indexCreateMs),
439+
total_ms: elapsed,
440+
slowest_files: slowest,
441+
};
442+
}
443+
410444
if (!quiet) {
411445
console.log(
412446
`\n Codemap ${fullRebuild ? "(full rebuild)" : "(incremental)"}`,
@@ -418,6 +452,27 @@ export async function indexFiles(
418452
for (const [key, value] of Object.entries(stats)) {
419453
console.log(` ${(key + ":").padEnd(14)}${value}`);
420454
}
455+
if (perf) {
456+
console.log(` ───────────────────────────────────`);
457+
console.log(` Performance breakdown (ms)`);
458+
console.log(` collect: ${perf.collect_ms} (file glob)`);
459+
console.log(` parse: ${perf.parse_ms} (workers)`);
460+
console.log(` insert: ${perf.insert_ms} (bulk SQL)`);
461+
console.log(
462+
` index_create: ${perf.index_create_ms} (B-tree build)`,
463+
);
464+
console.log(
465+
` index_run: ${perf.total_ms} (parse + insert + index_create + DDL)`,
466+
);
467+
if (perf.slowest_files.length > 0) {
468+
console.log(
469+
` Top ${perf.slowest_files.length} slowest files (parse ms)`,
470+
);
471+
for (const f of perf.slowest_files) {
472+
console.log(` ${String(f.parse_ms).padStart(5)} ${f.path}`);
473+
}
474+
}
475+
}
421476
console.log();
422477
}
423478

@@ -427,6 +482,7 @@ export async function indexFiles(
427482
elapsedMs: elapsed,
428483
fullRebuild,
429484
stats,
485+
performance: perf,
430486
};
431487
}
432488

@@ -491,7 +547,9 @@ export function printQueryResult(
491547
}
492548
return 0;
493549
} catch (err) {
494-
const msg = err instanceof Error ? err.message : String(err);
550+
const msg = enrichQueryError(
551+
err instanceof Error ? err.message : String(err),
552+
);
495553
if (json) {
496554
console.log(JSON.stringify({ error: msg }));
497555
} else {
@@ -503,6 +561,20 @@ export function printQueryResult(
503561
}
504562
}
505563

564+
/**
565+
* Rewrites raw SQLite errors that almost always indicate a missing or empty
566+
* `.codemap.db` into an actionable hint. Other errors are returned unchanged.
567+
*/
568+
function enrichQueryError(message: string): string {
569+
if (
570+
/^no such table:\s*\w+/i.test(message) ||
571+
/^no such column:\s*\w+/i.test(message)
572+
) {
573+
return `${message} — run \`codemap\` (or \`codemap --full\`) first to build the index, then re-run your query.`;
574+
}
575+
return message;
576+
}
577+
506578
/**
507579
* Open the index, run SQL, return all rows, then close. Used by the public **`Codemap.query`** method.
508580
* @throws On invalid SQL or database errors (same as `better-sqlite3`-style `.all()`).

src/application/run-index.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ export interface RunIndexOptions {
4949
* Suppresses progress logs; parse failures may still be printed. Defaults to `false`.
5050
*/
5151
quiet?: boolean;
52+
/**
53+
* Emits a per-phase timing breakdown and the top-10 slowest files (full
54+
* rebuild only). Off by default — wired by the CLI's `--performance` flag.
55+
*/
56+
performance?: boolean;
5257
}
5358

5459
/**
@@ -68,10 +73,18 @@ export async function runCodemapIndex(
6873
const quiet = options.quiet ?? false;
6974
const mode: IndexMode = options.mode ?? "incremental";
7075

76+
const wantPerformance = options.performance === true;
77+
7178
if (mode === "full") {
7279
if (!quiet) console.log(" Full rebuild requested...");
80+
const collectStart = performance.now();
7381
const files = collectFiles();
74-
const run = await indexFiles(db, files, true, undefined, { quiet });
82+
const collectMs = performance.now() - collectStart;
83+
const run = await indexFiles(db, files, true, undefined, {
84+
quiet,
85+
performance: wantPerformance,
86+
collectMs,
87+
});
7588
return {
7689
mode: "full",
7790
indexed: run.indexed,
@@ -156,8 +169,14 @@ export async function runCodemapIndex(
156169
" No previous index or incompatible history, doing full rebuild...",
157170
);
158171
}
172+
const fallbackCollectStart = performance.now();
159173
const files = collectFiles();
160-
const run = await indexFiles(db, files, true, undefined, { quiet });
174+
const fallbackCollectMs = performance.now() - fallbackCollectStart;
175+
const run = await indexFiles(db, files, true, undefined, {
176+
quiet,
177+
performance: wantPerformance,
178+
collectMs: fallbackCollectMs,
179+
});
161180
return {
162181
mode: "full",
163182
indexed: run.indexed,

0 commit comments

Comments
 (0)