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/fix-high-bugs-batch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"codemap": patch
---

Fix high-severity bugs (describe.each parent stack, git porcelain -z paths, CLI symlink entry, pr-comment TTY stdin), medium bugs (changed-since -z paths, perf baseline RUNS guard, qualified typeof, decorator args_text, for-of binding refs, HTTP body drain, benchmark regex validation, jsx INSERT RETURNING id, sqlite stmt cache on close), and low bugs (CLI parse guards, incremental delete transaction, apply summary.files, extension case, pnpm # paths, coverage db.transaction, impact walk delimiter, worker errors, pointer dedup, benchmark readAll visibility).
9 changes: 8 additions & 1 deletion scripts/check-perf-baseline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import type { IndexPerformanceReport } from "../src/application/types";
// 25% threshold + 10ms noise floor gate real regressions, not jitter; docs/benchmark.md § Perf baseline.
const NOISE_FLOOR_MS = Number(process.env.CODEMAP_PERF_NOISE_FLOOR_MS ?? 10);
const REGRESSION_PCT = Number(process.env.CODEMAP_PERF_REGRESSION_PCT ?? 25);
const RUNS = Number(process.env.CODEMAP_PERF_RUNS ?? 3);
const RUNS_RAW = Number(process.env.CODEMAP_PERF_RUNS ?? 3);
if (!Number.isInteger(RUNS_RAW) || RUNS_RAW < 1) {
console.error(
`CODEMAP_PERF_RUNS must be a positive integer (got ${process.env.CODEMAP_PERF_RUNS ?? RUNS_RAW})`,
);
process.exit(2);
}
const RUNS = RUNS_RAW;

const REPO_ROOT = resolve(import.meta.dirname, "..");
const INDEXER = join(REPO_ROOT, "src/index.ts");
Expand Down
1 change: 1 addition & 0 deletions src/adapters/builtin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BUILTIN_ADAPTERS, getAdapterForExtension } from "./builtin";
describe("getAdapterForExtension", () => {
it("resolves TS/JS extensions to builtin.ts-js", () => {
expect(getAdapterForExtension(".ts")?.id).toBe("builtin.ts-js");
expect(getAdapterForExtension(".TS")?.id).toBe("builtin.ts-js");
expect(getAdapterForExtension(".tsx")?.id).toBe("builtin.ts-js");
});

Expand Down
5 changes: 3 additions & 2 deletions src/adapters/builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ function buildAdapterIndex(
// First-match-wins: skip ext if an earlier adapter already claimed it.
for (const a of adapters) {
for (const ext of a.extensions) {
if (!index.has(ext)) index.set(ext, a);
const key = ext.toLowerCase();
if (!index.has(key)) index.set(key, a);
}
}
return index;
Expand All @@ -129,5 +130,5 @@ export function getAdapterForExtension(
index = buildAdapterIndex(adapters);
adapterIndexCache.set(adapters, index);
}
return index.get(ext);
return index.get(ext.toLowerCase());
}
25 changes: 25 additions & 0 deletions src/agents-init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,31 @@ describe("upsertCodemapPointerFile", () => {
}
});

it("replaces ALL managed sections when file has two blocks", () => {
const dir = mkdtempSync(join(tmpdir(), "codemap-pointer-"));
const p = join(dir, "NOTE.md");
try {
writeFileSync(
p,
`${wrapPointerTest("FIRST")}\nOther content\n${wrapPointerTest("SECOND")}`,
"utf-8",
);
upsertCodemapPointerFile(
p,
"UPDATED\n\nstill https://github.com/stainless-code/codemap\n`.agents/skills/codemap`\n`codemap query`",
"NOTE.md",
false,
);
const out = readFileSync(p, "utf-8");
expect(out).toContain("UPDATED");
expect(out).not.toContain("FIRST");
expect(out).not.toContain("SECOND");
expect(out.match(new RegExp(CODMAP_POINTER_BEGIN, "g"))?.length).toBe(1);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("--force replaces entire file with managed section", () => {
const dir = mkdtempSync(join(tmpdir(), "codemap-pointer-"));
const p = join(dir, "AGENTS.md");
Expand Down
15 changes: 10 additions & 5 deletions src/agents-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,10 @@ function escapeRegexChars(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function codemapPointerBlockRegex(): RegExp {
function codemapPointerBlockRegex(flags = "m"): RegExp {
return new RegExp(
`${escapeRegexChars(CODMAP_POINTER_BEGIN)}\\s*[\\s\\S]*?${escapeRegexChars(CODMAP_POINTER_END)}`,
"m",
flags,
);
}

Expand Down Expand Up @@ -247,10 +247,15 @@ export function upsertCodemapPointerFile(
}

const content = readFileSync(path, "utf-8");
const re = codemapPointerBlockRegex();

if (content.match(re)) {
const next = content.replace(re, wrapped);
if (content.match(codemapPointerBlockRegex())) {
const stripped = content
.replace(codemapPointerBlockRegex("gm"), "")
.replace(/\n{3,}/g, "\n\n")
.trimEnd();
const sep = stripped.length === 0 || stripped.endsWith("\n") ? "" : "\n\n";
const next =
stripped.length === 0 ? wrapped : `${stripped}${sep}${wrapped}`;
if (next === content) {
console.log(` Codemap section in ${label} already up to date`);
return;
Expand Down
1 change: 1 addition & 0 deletions src/application/apply-engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,7 @@ describe("applyDiffPayload", () => {
});

expect(result.summary.rows).toBe(0); // row silently dropped
expect(result.summary.files).toBe(0);
expect(result.files).toEqual([]);
});
});
Expand Down
20 changes: 6 additions & 14 deletions src/application/apply-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export function applyDiffPayload(opts: ApplyDiffPayloadOpts): ApplyJsonPayload {
// would split phase 2 mid-loop and leak Q2 (c) all-or-nothing.
const seenLines = new Map<string, Set<number>>();
let validRows = 0;
const distinctInputFiles = new Set<string>();

for (const row of rows) {
const filePath = readString(row, "file_path");
Expand All @@ -151,6 +152,11 @@ export function applyDiffPayload(opts: ApplyDiffPayloadOpts): ApplyJsonPayload {
continue;
}
validRows++;
distinctInputFiles.add(
isAbsolute(filePath) || !isWithinProjectRoot(resolvedRoot, filePath)
? filePath
: canonicalizeProjectFilePath(resolvedRoot, filePath),
);

// Path-containment guard — without it `file_path: "../escape.ts"` would
// write sibling-of-root files (CLI + MCP + HTTP all share this engine).
Expand Down Expand Up @@ -261,20 +267,6 @@ export function applyDiffPayload(opts: ApplyDiffPayloadOpts): ApplyJsonPayload {
}

const filesWithConflicts = new Set(conflicts.map((c) => c.file_path)).size;
const distinctInputFiles = new Set<string>();
for (const row of rows) {
const filePath = readString(row, "file_path");
if (filePath === undefined) continue;
// Count distinct disk targets, not distinct spellings — same as the
// dedup applied to the cache + pending keys.
if (isAbsolute(filePath) || !isWithinProjectRoot(resolvedRoot, filePath)) {
distinctInputFiles.add(filePath);
} else {
distinctInputFiles.add(
canonicalizeProjectFilePath(resolvedRoot, filePath),
);
}
}

// Q2 (c) — any conflict aborts the run; dry-run never writes. Same Q5 envelope.
if (dryRun || conflicts.length > 0) {
Expand Down
11 changes: 3 additions & 8 deletions src/application/coverage-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,7 @@ export function upsertCoverageRows(opts: UpsertOpts): IngestResult {
// we're ingesting (a re-ingest is a full replace per file, not a merge),
// then bulk-insert the new aggregates. Idempotent across re-runs.
let pruned = 0;
db.run("BEGIN");
try {
const persist = db.transaction(() => {
for (const file_path of filesSeen) {
db.run("DELETE FROM coverage WHERE file_path = ?", [file_path]);
}
Expand Down Expand Up @@ -203,12 +202,8 @@ export function upsertCoverageRows(opts: UpsertOpts): IngestResult {
"coverage_last_ingested_format",
format,
]);

db.run("COMMIT");
} catch (err) {
db.run("ROLLBACK");
throw err;
}
});
persist();

return {
ingested: { symbols: buckets.size, files: filesSeen.size },
Expand Down
115 changes: 115 additions & 0 deletions src/application/get-changed-files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,119 @@ describe("getChangedFiles", () => {
closeDb(db);
}
});

it("indexes modified files whose paths contain spaces (porcelain -z)", () => {
mkdirSync(join(projectRoot, "src"), { recursive: true });
writeFileSync(
join(projectRoot, "src/my module.ts"),
"export const x = 1;\n",
);
const base = commitAll("add spaced file");

writeFileSync(
join(projectRoot, "src/my module.ts"),
"export const x = 2;\n",
);

const db = openDb();
try {
createTables(db);
db.run(
"INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at) VALUES ('src/my module.ts', 'old', 1, 1, 'typescript', 1, 1)",
);
setMeta(db, "last_indexed_commit", base);

const delta = getChangedFiles(db);
expect(delta).not.toBeNull();
expect(delta!.changed).toContain("src/my module.ts");
expect(delta!.deleted).not.toContain("src/my module.ts");
} finally {
closeDb(db);
}
});

it("detects committed changes to paths with spaces (diff --name-status -z)", () => {
mkdirSync(join(projectRoot, "src"), { recursive: true });
writeFileSync(
join(projectRoot, "src/my module.ts"),
"export const x = 1;\n",
);
writeFileSync(join(projectRoot, "src/other.ts"), "export const o = 1;\n");
const base = commitAll("add spaced and other");

writeFileSync(
join(projectRoot, "src/my module.ts"),
"export const x = 2;\n",
);
commitAll("commit spaced change");

const db = openDb();
try {
createTables(db);
db.run(
"INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at) VALUES ('src/my module.ts', 'old', 1, 1, 'typescript', 1, 1)",
);
db.run(
"INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at) VALUES ('src/other.ts', 'old', 1, 1, 'typescript', 1, 1)",
);
setMeta(db, "last_indexed_commit", base);

const delta = getChangedFiles(db);
expect(delta).not.toBeNull();
expect(delta!.changed).toContain("src/my module.ts");
expect(delta!.deleted).not.toContain("src/my module.ts");
} finally {
closeDb(db);
}
});

it("includes uppercase extensions in incremental candidates", () => {
mkdirSync(join(projectRoot, "src"), { recursive: true });
writeFileSync(join(projectRoot, "src/File.TS"), "export const x = 1;\n");
const base = commitAll("add uppercase ext");

writeFileSync(join(projectRoot, "src/File.TS"), "export const x = 2;\n");

const db = openDb();
try {
createTables(db);
setMeta(db, "last_indexed_commit", base);

const delta = getChangedFiles(db);
expect(delta).not.toBeNull();
expect(delta!.changed).toContain("src/File.TS");
} finally {
closeDb(db);
}
});

it("returns deleted and changed paths in the same delta", () => {
mkdirSync(join(projectRoot, "src"), { recursive: true });
writeFileSync(join(projectRoot, "src/a.ts"), "export const a = 1;\n");
writeFileSync(join(projectRoot, "src/b.ts"), "export const b = 1;\n");
const base = commitAll("add a and b");

git(["rm", "src/a.ts"]);
commitAll("delete a");
writeFileSync(join(projectRoot, "src/b.ts"), "export const b = 2;\n");

const db = openDb();
try {
createTables(db);
db.run(
"INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at) VALUES ('src/a.ts', 'old', 1, 1, 'typescript', 1, 1)",
);
db.run(
"INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at) VALUES ('src/b.ts', 'old', 1, 1, 'typescript', 1, 1)",
);
setMeta(db, "last_indexed_commit", base);

const delta = getChangedFiles(db);
expect(delta).not.toBeNull();
expect(delta!.deleted).toContain("src/a.ts");
expect(delta!.changed).toContain("src/b.ts");
} finally {
closeDb(db);
}
});
});
1 change: 1 addition & 0 deletions src/application/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ async function readJsonBody(
const buf = chunk as Buffer;
total += buf.length;
if (total > MAX_BYTES) {
req.resume();
return {
ok: false,
error: `codemap serve: request body exceeds ${MAX_BYTES} bytes.`,
Expand Down
14 changes: 14 additions & 0 deletions src/application/impact-engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,20 @@ describe("findImpact — cycle detection", () => {
const names = r.matches.map((m) => m.name).sort();
expect(names).toEqual(["b", "c"]);
});

it("does not treat comma-containing symbol names as prefix matches", () => {
seedFile("src/a.ts");
seedFile("src/ab.ts");
seedFile("src/c.ts");
seedSymbol("a", "src/a.ts");
seedSymbol("a,b", "src/ab.ts");
seedSymbol("c", "src/c.ts");
seedCall("src/ab.ts", "a,b", "c");
seedCall("src/a.ts", "a", "c");
const r = findImpact(db, { target: "c", direction: "up", depth: 3 });
const names = r.matches.map((m) => m.name).sort();
expect(names).toEqual(["a", "a,b"]);
});
});

describe("findImpact — limit termination", () => {
Expand Down
12 changes: 6 additions & 6 deletions src/application/impact-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,14 +290,14 @@ function walkCalls(db: CodemapDatabase, opts: WalkOpts): ImpactNode[] {
// Seed depth = 0; `WHERE depth > 0` filters seed; `< depthLimit` is the cap.
const sql = `
WITH RECURSIVE walk(node, depth, path, file_path) AS (
SELECT ?, 0, ',' || ? || ',', NULL
SELECT ?, 0, char(30) || ? || char(30), NULL
UNION ALL
SELECT c.${joinToCol}, walk.depth + 1,
walk.path || c.${joinToCol} || ',', c.file_path
walk.path || c.${joinToCol} || char(30), c.file_path
FROM calls c
JOIN walk ON c.${joinFromCol} = walk.node
WHERE walk.depth < ?
AND instr(walk.path, ',' || c.${joinToCol} || ',') = 0
AND instr(walk.path, char(30) || c.${joinToCol} || char(30)) = 0
)
SELECT node, depth, file_path
FROM (
Expand Down Expand Up @@ -359,14 +359,14 @@ function walkFileGraph(db: CodemapDatabase, opts: WalkOpts): ImpactNode[] {

const sql = `
WITH RECURSIVE walk(node, depth, path) AS (
SELECT ?, 0, ',' || ? || ','
SELECT ?, 0, char(30) || ? || char(30)
UNION ALL
SELECT c.${joinToCol}, walk.depth + 1,
walk.path || c.${joinToCol} || ','
walk.path || c.${joinToCol} || char(30)
FROM ${table} c
JOIN walk ON c.${joinFromCol} = walk.node
WHERE walk.depth < ?
AND instr(walk.path, ',' || c.${joinToCol} || ',') = 0
AND instr(walk.path, char(30) || c.${joinToCol} || char(30)) = 0
${filterNonNull}
)
SELECT node, MIN(depth) AS depth
Expand Down
Loading
Loading