Skip to content

Commit 0fafbff

Browse files
committed
fix(cli): address PR #23 review — recent_markers misnomer, validate TOCTOU, --compact, --for guard, README
Adopts the 7 valid items from CodeRabbit review. A1 — `recent_markers` was alphabetical-first-20, not recency-ordered (the markers table has no timestamp column). Rename to `sample_markers` with a JSDoc note pointing at the proper time-ordered query (join `files.last_modified`). A2 — `cmd-validate.ts` had a TOCTOU between existsSync and readFileSync. Replace with a single guarded read; also drops one syscall in the common path (open instead of stat + open). N2 — Rename `performance` local in cmd-index.ts to `reportPerformance` to avoid shadowing the global `performance` API. N4 — Route the `files-hashes` golden through the bundled recipe (was inline SQL); now exercises the recipe SQL itself and prevents drift. Snapshot refreshed. N5 — Add `validate` and `context` to the README "Daily commands" stripe; they were the headline additions in this PR but weren't on the showcase. N7 — `--for ""` was accepted as a valid intent and silently classified as "other". Reject empty-string values with the same error as missing. N8 — `--compact` now actually compacts: `JSON.stringify` is called without indent in compact mode (was unconditional 2-space pretty-print). Tests: +1 case for `--for ""` rejection. 19/19 goldens green, 24/24 cmd-context + cmd-validate tests pass.
1 parent efcb010 commit 0fafbff

7 files changed

Lines changed: 54 additions & 19 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@ bun add @stainless-code/codemap
4646

4747
```bash
4848
codemap # incremental index (run once per session)
49-
codemap query --json --recipe fan-out # bundled SQL via recipe id
49+
codemap query --json --recipe fan-out # bundled SQL via recipe id (alias: -r)
5050
codemap query --json "SELECT name, file_path FROM symbols WHERE name = 'foo'" # ad-hoc SQL
5151
codemap --files src/a.ts src/b.tsx # targeted re-index after edits
52+
codemap validate --json # detect stale / missing / unindexed files
53+
codemap context --compact --for "refactor auth" # JSON envelope + intent-matched recipes
5254
codemap agents init # scaffold .agents/ rules + skills
5355
```
5456

fixtures/golden/minimal/files-hashes.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,67 @@
11
[
22
{
33
"path": "README.md",
4+
"content_hash": "428564b4ac5e4864ca2292a4195b726be3d4c7c5cac3d19dd01f17714d026c66",
45
"language": "md",
56
"line_count": 11
67
},
78
{
89
"path": "package.json",
10+
"content_hash": "a8e79efc943697cee4d4435360de9150bd7261376abe9e2c34c0c7238177c7de",
911
"language": "json",
1012
"line_count": 11
1113
},
1214
{
1315
"path": "src/api/client.ts",
16+
"content_hash": "4d0ec5edad3f758e7f84898dd602dbacaf3ea65ad87614ae9f179bd2383bbf56",
1417
"language": "ts",
1518
"line_count": 9
1619
},
1720
{
1821
"path": "src/components/shop/ShopButton.tsx",
22+
"content_hash": "481b9ff2e493fd69b36b67d2738e298999ce55040bce64c4c38a2e736c0573fd",
1923
"language": "tsx",
2024
"line_count": 8
2125
},
2226
{
2327
"path": "src/consumer.ts",
28+
"content_hash": "420ad98f7fedaf07ec07d1a64ef6ade100be4a7973883718a3942c68b1350903",
2429
"language": "ts",
2530
"line_count": 11
2631
},
2732
{
2833
"path": "src/notes.md",
34+
"content_hash": "1a2bff433137690132077b15032ba3f07f5ed1b576b894816890342155ab640a",
2935
"language": "md",
3036
"line_count": 4
3137
},
3238
{
3339
"path": "src/styles/button.module.css",
40+
"content_hash": "1c385277a506b2cb1c70c2d2128bbd280bf080796c83ab37f6f3434655c17348",
3441
"language": "css",
3542
"line_count": 16
3643
},
3744
{
3845
"path": "src/theme.css",
46+
"content_hash": "19ff92a0acce67e5a385e8f1b1607f787462626669d3b941ee0fc91030672b67",
3947
"language": "css",
4048
"line_count": 9
4149
},
4250
{
4351
"path": "src/usePermissions.ts",
52+
"content_hash": "8758cc9ba2756e08198f11fca9ff58a0612fc9219173d06784e0b7e2f2ca1e1f",
4453
"language": "ts",
4554
"line_count": 4
4655
},
4756
{
4857
"path": "src/utils/date.ts",
58+
"content_hash": "89e6301076caeb9f4c13cee8ea06f7f25a5e1aa239daf921f7ff984236ffa177",
4959
"language": "ts",
5060
"line_count": 15
5161
},
5262
{
5363
"path": "tsconfig.json",
64+
"content_hash": "6b8ead4b09e6885483c805937a35fa0df3cbc47de346abb2d0ed8b73dc7f9e92",
5465
"language": "json",
5566
"line_count": 17
5667
}

fixtures/golden/scenarios.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@
8686
},
8787
{
8888
"id": "files-hashes",
89-
"prompt": "All indexed file paths with language and line count (powers `codemap validate`)",
90-
"sql": "SELECT path, language, line_count FROM files ORDER BY path"
89+
"prompt": "All indexed files with content_hash (recipe powers `codemap validate`)",
90+
"recipe": "files-hashes"
9191
},
9292
{
9393
"id": "barrel-files",

src/cli/cmd-context.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ describe("parseContextRest", () => {
4848
expect(r.kind).toBe("error");
4949
});
5050

51+
it("errors when --for value is an empty string", () => {
52+
const r = parseContextRest(["context", "--for", ""]);
53+
expect(r.kind).toBe("error");
54+
});
55+
5156
it("rejects unknown options", () => {
5257
expect(parseContextRest(["context", "--nope"]).kind).toBe("error");
5358
});

src/cli/cmd-context.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ export interface ContextEnvelope {
2222
languages: { language: string; files: number }[];
2323
};
2424
hubs?: { to_path: string; fan_in: number }[];
25-
recent_markers?: {
25+
/**
26+
* A flavor sample of TODO/FIXME/HACK/NOTE markers — the alphabetically-first
27+
* 20 across the repo, ordered by `(file_path, line_number)`. Not a recency
28+
* signal; for time-ordered output query `markers` directly, joining
29+
* `files.last_modified`.
30+
*/
31+
sample_markers?: {
2632
file_path: string;
2733
line_number: number;
2834
kind: string;
@@ -51,11 +57,12 @@ export function printContextCmdHelp(): void {
5157
console.log(`Usage: codemap context [--compact] [--for "<intent>"]
5258
5359
Emit a JSON envelope describing the current index — project metadata, top
54-
hubs (fan-in), recent markers, and the bundled recipe catalog. Designed for
55-
agents and editors that want a single-command "give me everything cheap".
60+
hubs (fan-in), a sample of markers, and the bundled recipe catalog. Designed
61+
for agents and editors that want a single-command "give me everything cheap".
5662
5763
Flags:
58-
--compact Drop hubs and recent_markers (smaller payload).
64+
--compact Drop hubs and sample_markers; emit JSON without
65+
pretty-print (smaller payload).
5966
--for "<intent>" Pre-classify a free-text intent (refactor, debug, test,
6067
feature, explore) and recommend recipes that match.
6168
--help, -h Show this help.
@@ -90,7 +97,7 @@ export function parseContextRest(
9097
}
9198
if (a === "--for") {
9299
const v = rest[i + 1];
93-
if (v === undefined || v.startsWith("--")) {
100+
if (v === undefined || v === "" || v.startsWith("--")) {
94101
return {
95102
kind: "error",
96103
message: 'codemap: "--for" requires an intent string in quotes.',
@@ -210,11 +217,11 @@ export function buildContextEnvelope(
210217
envelope.hubs = db
211218
.query(QUERY_RECIPES["fan-in"]!.sql)
212219
.all() as ContextEnvelope["hubs"];
213-
envelope.recent_markers = db
220+
envelope.sample_markers = db
214221
.query(
215222
"SELECT file_path, line_number, kind, content FROM markers ORDER BY file_path ASC, line_number ASC LIMIT 20",
216223
)
217-
.all() as ContextEnvelope["recent_markers"];
224+
.all() as ContextEnvelope["sample_markers"];
218225
}
219226

220227
if (opts.intent !== null) {
@@ -248,7 +255,11 @@ export async function runContextCmd(opts: ContextOpts): Promise<void> {
248255
} finally {
249256
closeDb(db, { readonly: true });
250257
}
251-
console.log(JSON.stringify(envelope, null, 2));
258+
console.log(
259+
opts.compact
260+
? JSON.stringify(envelope)
261+
: JSON.stringify(envelope, null, 2),
262+
);
252263
} catch (err) {
253264
const msg = err instanceof Error ? err.message : String(err);
254265
console.log(JSON.stringify({ error: msg }));

src/cli/cmd-index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ export async function runIndexCmd(opts: {
3434
}
3535
} else {
3636
const fullRebuild = args.includes("--full");
37-
const performance = args.includes("--performance");
37+
const reportPerformance = args.includes("--performance");
3838
await runCodemapIndex(db, {
3939
mode: fullRebuild ? "full" : "incremental",
40-
performance,
40+
performance: reportPerformance,
4141
});
4242
}
4343
} finally {

src/cli/cmd-validate.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, readFileSync } from "node:fs";
1+
import { readFileSync } from "node:fs";
22
import { isAbsolute, relative, resolve } from "node:path";
33

44
import { loadUserConfig, resolveCodemapConfig } from "../config";
@@ -116,18 +116,24 @@ export function computeValidateRows(
116116

117117
const indexedHash = indexByPath.get(rel);
118118
const abs = resolve(projectRoot, rel);
119-
const onDisk = existsSync(abs);
119+
let source: string | undefined;
120+
try {
121+
source = readFileSync(abs, "utf8");
122+
} catch {
123+
source = undefined;
124+
}
120125

121126
if (indexedHash === undefined) {
122-
if (onDisk) rows.push({ path: rel, status: "unindexed" });
127+
if (source !== undefined) rows.push({ path: rel, status: "unindexed" });
123128
continue;
124129
}
125-
if (!onDisk) {
130+
if (source === undefined) {
126131
rows.push({ path: rel, status: "missing" });
127132
continue;
128133
}
129-
const diskHash = hashContent(readFileSync(abs, "utf8"));
130-
if (diskHash !== indexedHash) rows.push({ path: rel, status: "stale" });
134+
if (hashContent(source) !== indexedHash) {
135+
rows.push({ path: rel, status: "stale" });
136+
}
131137
}
132138
rows.sort((a, b) => a.path.localeCompare(b.path));
133139
return rows;

0 commit comments

Comments
 (0)