Skip to content

Commit 3568c5f

Browse files
feat(runtime): guard root switches and isolate test global state (#182)
* feat(runtime): guard root switches and isolate test global state Codify one-root-per-process: initCodemap/configureResolver throw on mid-process root changes except audit worktree reindex (runtime-swap bracket). Validate user config at loadUserConfig; roll beforeEach/afterEach teardown across initCodemap suites. * harden: swap finally safety, tests, retire runtime-test-isolation plan Pair exitRuntimeSwap in outer finally; reset swap depth in test teardown. Add createCodemap/configureResolver/makeWorktreeReindex coverage; consumer-clean docs/changeset; delete plan and repoint orchestrator + roadmap. * harden: cover loadUserConfig invalid .ts paths at load * chore(changeset): patch bump for runtime root guard tighten
1 parent aae172f commit 3568c5f

42 files changed

Lines changed: 373 additions & 115 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@stainless-code/codemap": patch
3+
---
4+
5+
`createCodemap()` and the CLI now reject invalid project config at load time. A second `createCodemap()` with a different project root in the same process throws (audit `--base` worktree reindex is exempt).

docs/architecture.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,13 +234,13 @@ The npm package exports **`createCodemap`**, **`Codemap`** (`query`, `index`), *
234234
2. **`await cm.index({ mode, files?, quiet? })`** — same pipeline as the CLI (incremental / full / targeted).
235235
3. **`cm.query(sql)`** — read-only SQL against `.codemap/index.db` (opens the DB per call).
236236

237-
**Constraint:** `initCodemap` is global to the process; only one active indexed project at a time.
237+
**Constraint:** One project root per process — a second `initCodemap` / `createCodemap` with a different `root` throws. Audit `--base` worktree reindex brackets temporary root swaps (internal swap guard). Re-init on the same root is allowed.
238238

239239
### User config
240240

241241
Optional **`<state-dir>/config.{ts,js,json}`** (default `.codemap/config.*`; default export: object or async factory). **`--config <path>`** overrides with an explicit file (absolute or relative to cwd). Example shape: [`codemap.config.example.json`](../codemap.config.example.json). **Self-healing (D11):** `<state-dir>/.gitignore` is reconciled to canonical on every codemap boot via **`ensureStateGitignore`** (`src/application/state-dir.ts`); JSON config is reconciled via **`ensureStateConfig`** (`src/application/state-config.ts` — prunes unknown keys with a warning, sorts alphabetically, write-only-on-drift). TS/JS configs are validate-only at load time. Bumping the canonical `STATE_GITIGNORE_BODY` constant or the Zod schema IS the migration — every consumer's project repairs itself on next boot. Single attachment point: **`src/cli/bootstrap-codemap.ts`** runs the reconcilers before `loadUserConfig`.
242242

243-
**Validation:** **`codemapUserConfigSchema`** ([Zod](https://zod.dev)) — strict object (unknown keys are rejected). **`defineConfig({ ... })`**, **`parseCodemapUserConfig`**, and **`resolveCodemapConfig`** (CLI and merged `createCodemap({ config })`) all go through the same schema. Invalid config throws **`TypeError`** with a short path/message list.
243+
**Validation:** **`codemapUserConfigSchema`** ([Zod](https://zod.dev)) — strict object (unknown keys are rejected). **`defineConfig({ ... })`**, **`parseCodemapUserConfig`**, and **`resolveCodemapConfig`** (CLI and merged `createCodemap({ config })`) all go through the same schema; **`createCodemap`** and the CLI load path validate config files at load time. Invalid config throws **`TypeError`** with a short path/message list.
244244

245245
**Exports:** `codemapUserConfigSchema`, `parseCodemapUserConfig`, `defineConfig`, and **`CodemapUserConfig`** (inferred type) from the package entry — see **`src/config.ts`** / **`dist/index.d.mts`**.
246246

docs/plans/runtime-test-isolation.md

Lines changed: 0 additions & 75 deletions
This file was deleted.

docs/plans/security-hardening-orchestrator.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121

2222
## PR schedule
2323

24-
| PR | Plan | Status | Blocks |
25-
| ----- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------- |
26-
| **1** | lifted → [`architecture.md`](../architecture.md) (plan retired) | **merged** ([#180](https://github.com/stainless-code/codemap/pull/180) · `a5caca8`) | |
27-
| **2** | lifted → [`architecture.md`](../architecture.md) (plan retired) | **PR open** (`fix/impact-inpath-homonyms`) | |
28-
| **3** | [`runtime-test-isolation.md`](./runtime-test-isolation.md) | **pending** | PR **1** merged (PR **2** optional) |
24+
| PR | Plan | Status | Blocks |
25+
| ----- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------ |
26+
| **1** | lifted → [`architecture.md`](../architecture.md) (plan retired) | **merged** ([#180](https://github.com/stainless-code/codemap/pull/180) · `a5caca8`) | |
27+
| **2** | lifted → [`architecture.md`](../architecture.md) (plan retired) | **merged** ([#181](https://github.com/stainless-code/codemap/pull/181) · `aae172f`) ||
28+
| **3** | lifted → [`architecture.md`](../architecture.md) (plan retired) | **PR open** ([#182](https://github.com/stainless-code/codemap/pull/182) · `fix/runtime-test-isolation`) | |
2929

3030
||| **deferred** | golden `schema.test.ts` + path guards |
3131
||| **skip** | atomic `ensureStateConfig` writes |
@@ -64,17 +64,18 @@ Evaluated 2026-06 against [roadmap § Floors](../roadmap.md#floors-v1-product-sh
6464

6565
## Session log
6666

67-
| Date | Event | Notes |
68-
| ---------- | ----------- | ---------------------------------------------------------------------------- |
69-
| 2026-06-10 | Triage | ROI on 7 slices; 3-PR program adopted. |
70-
| 2026-06-10 | PR 1 impl | PR **1** committed on `fix/security-hardening-wave1`; harden pass in flight. |
71-
| 2026-06-05 | PR 1 harden | `/harden-pr full` — plan retired; contracts in architecture/glossary. |
72-
| 2026-06-05 | PR 1 merge | [#180](https://github.com/stainless-code/codemap/pull/180)`a5caca8`. |
73-
| 2026-06-05 | PR 2 start | `fix/impact-inpath-homonyms``inPath` + homonym walks in impact-engine. |
74-
| 2026-06-05 | PR 2 harden | `/harden-pr full` — plan retired; CLI/MCP/docs parity. |
75-
|| PR 2 merge | _PR URL · merge SHA_ |
76-
|| PR 3 start | _from `main`_ |
77-
|| PR 3 merge | _fill · close orchestrator_ |
67+
| Date | Event | Notes |
68+
| ---------- | ----------- | ----------------------------------------------------------------------------- |
69+
| 2026-06-10 | Triage | ROI on 7 slices; 3-PR program adopted. |
70+
| 2026-06-10 | PR 1 impl | PR **1** committed on `fix/security-hardening-wave1`; harden pass in flight. |
71+
| 2026-06-05 | PR 1 harden | `/harden-pr full` — plan retired; contracts in architecture/glossary. |
72+
| 2026-06-05 | PR 1 merge | [#180](https://github.com/stainless-code/codemap/pull/180)`a5caca8`. |
73+
| 2026-06-05 | PR 2 start | `fix/impact-inpath-homonyms``inPath` + homonym walks in impact-engine. |
74+
| 2026-06-05 | PR 2 harden | `/harden-pr full` — plan retired; CLI/MCP/docs parity. |
75+
| 2026-06-05 | PR 2 merge | [#181](https://github.com/stainless-code/codemap/pull/181)`aae172f`. |
76+
| 2026-06-05 | PR 3 start | `fix/runtime-test-isolation` — root guards + test teardown + config validate. |
77+
| 2026-06-05 | PR 3 harden | `/harden-pr full` — plan retired; swap finally fix + API/config tests. |
78+
|| PR 3 merge | _fill · close orchestrator_ |
7879

7980
---
8081

docs/roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ Predicate-as-API only — enrich row shape and audit deltas; no standalone pass/
108108
- [ ] **`organize-imports` diff-shape recipe** — deterministic single-file import sort/group; `imports.line_number` + `source` substrate sufficient. Review-first (`auto_fixable: false`). Effort: S.
109109
- [ ] **`codemap-to-tsmorph` Path B adapter** — separate package experiment: `query_recipe` discovery → `ts-morph` / `jscodeshift` transforms for AST-shape edits codemap's substring executor defers (see [architecture § Rejected apply-path alternatives](./architecture.md#apply--input-modes-transport-and-policy)). Not an in-tree AST writer (Path A rejected). Effort: M.
110110
- [ ] **Apply write-safety hardening** — close apply TOCTOU: SHA-256 `hashContent` at phase-1 read, recheck disk hash immediately before phase-2 write (`file content changed` conflict); `fsync` temp file before `rename`; skip files with mixed CRLF/LF (`mixed line endings`). Preserves all-or-nothing on any conflict. Plan: [`plans/apply-write-safety.md`](./plans/apply-write-safety.md). Effort: L.
111-
- [ ] **Read-surface hardening (3 PRs)** — query/HTTP/validate safety, `impact` `inPath` homonyms, runtime guards + test teardown. **Orchestrator:** [`plans/security-hardening-orchestrator.md`](./plans/security-hardening-orchestrator.md). PR1 ([#180](https://github.com/stainless-code/codemap/pull/180), lifted to [architecture](./architecture.md)) · PR2 (`impact` homonyms, lifted to [architecture](./architecture.md)) · Plans: [PR3](./plans/runtime-test-isolation.md). Effort: S–M.
111+
- [ ] **Read-surface hardening (3 PRs)** — query/HTTP/validate safety, `impact` `inPath` homonyms, runtime guards + test teardown. **Orchestrator:** [`plans/security-hardening-orchestrator.md`](./plans/security-hardening-orchestrator.md). PR1 ([#180](https://github.com/stainless-code/codemap/pull/180), lifted to [architecture](./architecture.md)) · PR2 ([#181](https://github.com/stainless-code/codemap/pull/181), lifted to [architecture](./architecture.md)) · PR3 ([#182](https://github.com/stainless-code/codemap/pull/182), lifted to [architecture](./architecture.md)). Effort: S–M.
112112
- [ ] **`history` table** (deferred — revisit-triggered) — temporal queries: "when did symbol X get `@deprecated`?", "coverage trend over last 50 commits", "files that became dead this week". `audit --base <ref>` covers the most-common temporal question (PR-scoped diff) without schema growth, so the table earns its place only when bigger questions emerge. Two shapes (per-commit snapshots ~N × DB size; append-only event log heavier CTE walks); both pay an N-reindexes backfill cost (~30s per reindex). **Revisit triggers:** two consumers ship `jq`-based "audit-runs-over-time" workflows, OR `query_baselines` evolution becomes a recurring agent need.
113113
- [ ] **`codemap audit` verdict + thresholds** (v1.x) — `verdict: "pass" | "warn" | "fail"` driven by an `audit.deltas[<key>].{added_max, action}` field on the config object (`.codemap/config.{ts,js,json}`). Triggers: two consumers ship `jq`-based threshold scripts with similar shapes, OR one consumer asks with a concrete config sketch. Until then, raw deltas + consumer-side `jq` is the CI exit-code idiom. **Likely accelerant:** the Marketplace Action (next item) shipping is the most plausible path to firing the trigger — once `- uses: stainless-code/codemap@v1` is the dominant CI path, real `jq` threshold scripts will surface.
114114
- [ ] **GitHub Marketplace Action — publish + listing finish** — core Action implementation is in-tree: root `action.yml`, `query --ci`, `audit --format sarif` / `--ci`, package-manager detection, dogfood smoke, and opt-in `pr-comment` summary renderer have shipped. Remaining work is the release/listing slice: `MARKETPLACE.md`, `v1.0.0` / floating `v1` tags, Marketplace setup, sacrificial-repo smoke, and making `action-smoke` blocking once the Action tag exists. Action version stream is independent of CLI version (`package.json` currently drives CLI/npm version; Action publishes at its own `v1.0.0`). Plan: [`plans/github-marketplace-action.md`](./plans/github-marketplace-action.md). Effort: S.

scripts/agent-eval/parse-agent-log.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { join } from "node:path";
1111

1212
import { resolveCodemapConfig } from "../../src/config";
1313
import { initCodemap } from "../../src/runtime";
14+
import { installCodemapTestTeardown } from "../../src/test-helpers/runtime-reset";
1415
import { resolveGoldenQuery } from "../query-golden/resolve-golden-query";
1516
import { compareLogArms, summarizeLogComparison } from "./compare-live-logs";
1617
import { runLiveMcpArm } from "./live-mcp-arm";
@@ -43,6 +44,8 @@ import {
4344
traditionalToolSequence,
4445
} from "./traditional-probe";
4546

47+
installCodemapTestTeardown();
48+
4649
const sampleLog = join(
4750
import.meta.dir,
4851
"../../fixtures/agent-eval/sample-cursor-log.json",

src/api.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { tmpdir } from "node:os";
44
import { join } from "node:path";
55

66
import { createCodemap } from "./api";
7+
import { installCodemapTestTeardown } from "./test-helpers/runtime-reset";
8+
9+
installCodemapTestTeardown();
710

811
describe("createCodemap", () => {
912
test("query runs against the index database", async () => {
@@ -13,4 +16,24 @@ describe("createCodemap", () => {
1316
const rows = cm.query("SELECT 1 as ok") as { ok: number }[];
1417
expect(rows[0]?.ok).toBe(1);
1518
});
19+
20+
test("throws when switching to a different root in the same process", async () => {
21+
const rootA = mkdtempSync(join(tmpdir(), "codemap-api-a-"));
22+
const rootB = mkdtempSync(join(tmpdir(), "codemap-api-b-"));
23+
writeFileSync(join(rootA, "package.json"), "{}");
24+
writeFileSync(join(rootB, "package.json"), "{}");
25+
await createCodemap({ root: rootA });
26+
await expect(createCodemap({ root: rootB })).rejects.toThrow(
27+
/cannot switch project root/,
28+
);
29+
});
30+
31+
test("throws when config file is invalid at load", async () => {
32+
const root = mkdtempSync(join(tmpdir(), "codemap-api-bad-"));
33+
const configPath = join(root, "bad.json");
34+
writeFileSync(configPath, JSON.stringify({ include: [1, 2] }));
35+
await expect(
36+
createCodemap({ root, configFile: configPath }),
37+
).rejects.toThrow(/include/);
38+
});
1639
});

src/api.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ export interface CodemapInitOptions {
4545
* and returns a {@link Codemap} handle.
4646
*
4747
* @remarks
48-
* Only one Codemap project per process: `initCodemap` is global; the last `createCodemap()` wins.
48+
* One project root per process: a second `createCodemap()` with a different `root` throws.
49+
* Re-initializing the same root is allowed. Audit `--base` worktree reindex is the only
50+
* production path that may temporarily switch roots.
51+
*
52+
* Invalid project config (unknown keys, wrong types) throws at load time via the same
53+
* schema as {@link parseCodemapUserConfig}.
4954
*/
5055
export async function createCodemap(
5156
options: CodemapInitOptions = {},

src/application/affected-engine.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
44
import { tmpdir } from "node:os";
55
import { join } from "node:path";
66

7+
import { installCodemapTestTeardown } from "../test-helpers/runtime-reset";
8+
9+
installCodemapTestTeardown();
10+
711
import { resolveCodemapConfig } from "../config";
812
import { closeDb, createTables, openDb } from "../db";
913
import { initCodemap } from "../runtime";

0 commit comments

Comments
 (0)