Skip to content

Commit 2e4cdfc

Browse files
committed
harden: read-surface PR parity, tests, and wave1 plan retirement
Align validate reason docs across consumer surfaces, extend formatted-query DML coverage to diff formats, retire security-hardening-wave1 plan with orchestrator/roadmap updates.
1 parent c83ec5b commit 2e4cdfc

10 files changed

Lines changed: 40 additions & 106 deletions

File tree

.changeset/read-surface-hardening.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@stainless-code/codemap": patch
33
---
44

5-
Harden read surfaces: `codemap query --format …` blocks index mutations via the same read-only guard as `--json`; `codemap serve` requires `--token` when `--host` is not loopback (any `127.0.0.0/8` address counts as loopback, so `--token` stays optional on `127.0.0.2` and similar); `codemap validate` (and MCP/HTTP `validate`) can return `rejected` rows with a `reason` when a path escapes the project root, resolves outside via symlink, or is a broken symlink — output `path` keys are always project-relative POSIX paths.
5+
Harden read surfaces: `codemap query --format …` blocks index mutations via the same read-only guard as `--json`; `codemap serve` requires `--token` when `--host` is not loopback (any `127.0.0.0/8` address counts as loopback, so `--token` stays optional on `127.0.0.2` and similar); `codemap validate` (and MCP/HTTP `validate`) can return `rejected` rows with optional `reason` (`path escapes project root` | `path escapes via symlink` | `path resolves outside project root`) — output `path` keys are always project-relative POSIX paths.

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ A local SQLite database (`.codemap/index.db`) indexes the project tree and store
124124

125125
**Output formatters:** **`src/application/output-formatters.ts`** — pure transport-agnostic; **`formatSarif`** emits SARIF 2.1.0 (auto-detected location columns: `file_path` / `path` / `to_path` / `from_path` priority + optional `line_start` / `line_end` region; `rule.id = codemap.<recipe-id>` for `--recipe`, `codemap.adhoc` for ad-hoc SQL; aggregate recipes without locations → `results: []` + stderr warning); **`formatAuditSarif`** emits the audit-shaped variant — one rule per delta key (`codemap.audit.<key>-added`), one result per `added` row at severity `warning`; `removed` rows excluded (SARIF surfaces findings, not cleanups); location-only rows fall back to `"new <key>: <uri>"` messages; **`formatAnnotations`** emits `::notice file=…,line=…::msg` GitHub Actions workflow commands (one line per locatable row; messages collapsed to a single line because the GH parser stops at the first newline); **`formatCodeClimate`** emits a GitLab Code Quality JSON array (`severity: minor` flat in v1; stable SHA-256 fingerprints from recipe id + path + line + check name + row message (`lines.begin` falls back to `1` when `line_start` absent)); **`formatBadge`** / **`formatBadgeJson`** emit a single-line markdown summary (`codemap: N issues` / `codemap: clean`) or `codemap-badge/v1` JSON (`--badge-style json` / MCP `badge_style`) from locatable-row count — agents triage via JSON rows, not badge severity; **`formatMermaid`** emits a `flowchart LR` from `{from, to, label?, kind?}` rows with a hard `MERMAID_MAX_EDGES = 50` ceiling — unbounded inputs reject with a scope-suggestion error naming the recipe + count + `LIMIT` / `--via` / `WHERE` knobs (auto-truncation deliberately out of scope; would be a verdict masquerading as output mode); **`formatDiff`** emits read-only unified diff text from `{file_path, line_start, before_pattern, after_pattern}` rows; **`formatDiffJson`** emits structured `{files, warnings, summary}` hunks for agents. Diff formatters read source files at format time and surface `stale` / `missing` flags when the indexed line no longer matches. Wired into both **`src/cli/cmd-query.ts`** (`--format <text|json|sarif|annotations|mermaid|diff|diff-json|codeclimate|badge>`; `--format` overrides `--json`; formatted outputs reject `--summary` / `--group-by` / baseline at parse time) and the MCP **`query`** / **`query_recipe`** tools (`format: "sarif" | "annotations" | "mermaid" | "diff" | "diff-json" | "codeclimate" | "badge"` with the same incompatibility guard). Per-recipe `sarifLevel` / `sarifMessage` / `sarifRuleId` overrides via frontmatter on `<id>.md` deferred to v1.x.
126126

127-
**Validate wiring:** **`src/cli/cmd-validate.ts`** (argv + render) + **`src/application/validate-engine.ts`** (engine — **`computeValidateRows`** + **`toProjectRelative`**). `computeValidateRows` is a pure function over `(db, projectRoot, paths)` returning `{path, status}` rows where `status ∈ stale | missing | unindexed | rejected` (`rejected` + optional `reason` when a path escapes the project root, resolves outside via symlink, or has a broken symlink — `readUtf8WithinProjectRoot` re-checks via `realpath` immediately before read; hardlinks to outside files keep an in-root pathname and are a documented local-trust boundary). Path keys are always project-relative POSIX paths (`toProjectRelative`). 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`. Also reused by `cmd-show.ts` / `cmd-snippet.ts` and the MCP show/snippet handlers — single canonical implementation.
127+
**Validate wiring:** **`src/cli/cmd-validate.ts`** (argv + render) + **`src/application/validate-engine.ts`** (engine — **`computeValidateRows`** + **`toProjectRelative`**). `computeValidateRows` is a pure function over `(db, projectRoot, paths)` returning `{path, status}` rows where `status ∈ stale | missing | unindexed | rejected` (`rejected` + optional `reason`: `path escapes project root` | `path escapes via symlink` | `path resolves outside project root` — `readUtf8WithinProjectRoot` re-checks via `realpath` immediately before read; hardlinks to outside files keep an in-root pathname and are a documented local-trust boundary). Path keys are always project-relative POSIX paths (`toProjectRelative`). 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`. Also reused by `cmd-show.ts` / `cmd-snippet.ts` and the MCP show/snippet handlers — single canonical implementation.
128128

129129
#### Audit wiring
130130

docs/glossary.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ CI-aggregate flag on `codemap query` and `codemap audit`. Aliases `--format sari
125125

126126
### `codemap validate`
127127

128-
CLI subcommand comparing on-disk SHA-256 against `files.content_hash`. Statuses: `stale | missing | unindexed | rejected` (`rejected` carries optional `reason` when a path escapes the project root, resolves outside via symlink, or has a broken symlink; output `path` keys are always project-relative POSIX paths). Exits `1` on any drift.
128+
CLI subcommand comparing on-disk SHA-256 against `files.content_hash`. Statuses: `stale | missing | unindexed | rejected` (`rejected` carries optional `reason`: `path escapes project root` | `path escapes via symlink` | `path resolves outside project root`; output `path` keys are always project-relative POSIX paths). Exits `1` on any drift.
129129

130130
### `module_cycles` (table) / circular imports
131131

docs/plans/security-hardening-orchestrator.md

Lines changed: 15 additions & 14 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** | [`security-hardening-wave1.md`](./security-hardening-wave1.md) | **committed, PR pending** ||
27-
| **2** | [`impact-inpath-homonyms.md`](./impact-inpath-homonyms.md) | **pending** | PR **1** merged |
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) | **PR [#180](https://github.com/stainless-code/codemap/pull/180) open** ||
27+
| **2** | [`impact-inpath-homonyms.md`](./impact-inpath-homonyms.md) | **pending** | PR **1** merged |
28+
| **3** | [`runtime-test-isolation.md`](./runtime-test-isolation.md) | **pending** | PR **1** merged (PR **2** optional) |
2929

3030
||| **deferred** | golden `schema.test.ts` + path guards |
3131
||| **skip** | atomic `ensureStateConfig` writes |
@@ -64,15 +64,16 @@ 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-
|| PR 1 merge | _PR URL · merge SHA · update plan status → closed_ |
72-
|| PR 2 start | _from `main`_ |
73-
|| PR 2 merge | _fill_ |
74-
|| PR 3 start | _from `main`_ |
75-
|| 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+
|| PR 1 merge | _merge SHA · update status → merged_ |
73+
|| PR 2 start | _from `main`_ |
74+
|| PR 2 merge | _fill_ |
75+
|| PR 3 start | _from `main`_ |
76+
|| PR 3 merge | _fill · close orchestrator_ |
7677

7778
---
7879

docs/plans/security-hardening-wave1.md

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

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). Plans: [PR1](./plans/security-hardening-wave1.md) · [PR2](./plans/impact-inpath-homonyms.md) · [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)) · Plans: [PR2](./plans/impact-inpath-homonyms.md) · [PR3](./plans/runtime-test-isolation.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.

src/cli/cmd-mcp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Tools (21; snake_case — mirrors CLI verbs where a shell twin exists):
9999
ingest_coverage Load Istanbul/LCOV/V8 coverage into the index.
100100
ingest_churn Load precomputed file_churn JSON into the index.
101101
context Project bootstrap envelope.
102-
validate On-disk hash vs indexed hash.
102+
validate Hash drift rows (stale/missing/unindexed/rejected + reason).
103103
show Symbol metadata: file:line + signature.
104104
snippet Same lookup + source text from disk.
105105
impact Symbol/file blast-radius walker (callers, callees,

0 commit comments

Comments
 (0)