Skip to content

feat(apply): codemap apply <recipe-id> — Q1–Q10 implemented#78

Open
SutuSebastian wants to merge 9 commits intomainfrom
feat/codemap-apply
Open

feat(apply): codemap apply <recipe-id> — Q1–Q10 implemented#78
SutuSebastian wants to merge 9 commits intomainfrom
feat/codemap-apply

Conversation

@SutuSebastian
Copy link
Copy Markdown
Contributor

@SutuSebastian SutuSebastian commented May 6, 2026

Summary

Implements the codemap apply plan (PR #77, merged) across 5 tracer-bullet slices. Each commit is a self-contained vertical slice that ran format/lint/typecheck/tests via the pre-commit hook before landing.

codemap apply <recipe-id> is a substrate-shaped fix executor: the recipe SQL describes the transformation ({file_path, line_start, before_pattern, after_pattern} rows), codemap is the executor. Floor "No fix engine" preserved — codemap doesn't synthesise edits, it only executes the hunks the recipe row described.

Commit-by-commit

Slice Commit What
1 e0677b9 apply-engine.ts phase-1 validation + dry-run envelope. Re-locks Q8 to substring-match (the original "exact byte-match" draft contradicted the existing buildDiffJson formatter contract — rename-preview emits before_pattern = old_name as the bare identifier). 14 unit tests.
2 564ba32 Phase-2 writes via sibling temp + renameSync for POSIX-atomic per-file writes. CRLF round-trip via raw \n split. 6 more unit tests including chmod 0o555 failure mode.
3 a24f715 cmd-apply.ts CLI verb (positional, per Q4) + recipe execution + Q6 TTY/--yes gate matrix. 10 subprocess integration tests including Q7 (a) idempotent re-run after reindex.
4 1fd85ce MCP + HTTP apply tool registration. Same applyDiffPayload engine; yes: true required over non-TTY transports. 4 handler unit tests.
5 30080f6 Docs lockstep — durable design lifted to docs/architecture.md § Apply wiring + boundary SQL kit; glossary.md entry; agent rule + skill + templates updated; roadmap backlog entry removed; plan file deleted per Rule 3.

Q1–Q10 locks (mirroring the merged plan)

Q Lock
Q1 Apply-by-default; --dry-run opts into preview
Q2 Per-recipe-run, all-or-nothing
Q3 Scan-and-collect conflicts in phase 1
Q4 Positional verb (codemap apply <recipe-id>)
Q5 Standalone ApplyJsonPayload, single shape across modes
Q6 TTY prompt + --yes; non-TTY rejects without --yes
Q7 Conflict-only path (no separate already-applied state for v1)
Q8 Substring match (single line) — mirrors buildDiffJson verbatim
Q9 Three-layer tests (unit / subprocess / golden); no TOCTOU / FS-DI / clock-injection
Q10 Boundary SQL kit + delete-on-ship lifecycle

Test plan

  • 30 unit + integration tests across apply-engine.test.ts, tool-handlers.test.ts, cmd-apply.test.ts (140 total in the touched suites; 0 fail).
  • Pre-commit hook (format / lint / typecheck / tests on staged) clean on every slice.
  • Boundary kit: re-runnable SQL at docs/architecture.md § Boundary verification — apply write path returns [] after a fresh reindex.
  • Smoke test: codemap apply rename-preview --params old=foo,new=bar --yes --json against a fresh fixture project renames the definition + import specifiers and reports applied: true with correct summary.rows_applied.
  • CodeRabbit review pass

Cross-references

  • Plan: PR docs(plan): codemap apply <recipe-id> — Q1–Q10 locked #77 (merged).
  • Engine: src/application/apply-engine.ts.
  • CLI: src/cli/cmd-apply.ts.
  • Tool: src/application/tool-handlers.ts handleApply.
  • Architecture: docs/architecture.md § Apply wiring + § Boundary verification — apply write path.
  • Glossary: docs/glossary.md codemap apply / apply tool entry.
  • Changeset: .changeset/codemap-apply.md (minor bump).

Summary by CodeRabbit

  • New Features

    • Add a new apply capability: CLI command plus MCP/HTTP tool to apply recipe-produced diffs with dry-run, interactive prompt gating, explicit --yes for non-TTY, idempotent behavior, conflict detection, and a unified JSON result envelope.
  • Documentation

    • Expanded architecture, glossary, plans, and guides describing the apply workflow, safety boundaries, and tooling.
  • Tests

    • New unit and end-to-end test suites covering apply behavior, conflict cases, and CLI flows.
  • Chore

    • Adjusted recipe-parameter merge behavior (may affect param merging).

Pure transport-agnostic engine implementing phase-1 validation and the
dry-run output shape from the merged plan (Q1–Q10 in
docs/plans/codemap-apply.md). No CLI, no MCP/HTTP wiring, no write
branch yet (Slice 2 lands the latter).

Re-locks Q8 to substring-match (a) — the original "exact byte-match"
draft contradicted the existing buildDiffJson formatter contract and
would have made every shipped rename-preview row conflict (the recipe
emits before_pattern = old_name as the bare identifier, not the full
line). New phase-1 mirrors application/output-formatters.ts buildDiffJson
verbatim: actual.includes(before) for the match check, first-occurrence
substring replace for the transformation (Slice 2), $-pre-escape per
GetSubstitution.

Slice scope:
- src/application/apply-engine.ts — applyDiffPayload({rows, projectRoot,
  dryRun}) returning Q5's ApplyJsonPayload envelope. dryRun=false with a
  clean phase 1 throws NotImplemented (Slice 2 fills in the write).
- src/application/apply-engine.test.ts — 14 unit tests covering happy
  paths, all three conflict reasons, row-shape validation, deterministic
  files[] sort, and the Slice-2 guard semantics.
- docs/plans/codemap-apply.md — Q8 re-lock + edge-case table refresh.

Tests: 14/14 pass. Typecheck / lint / format clean.
Phase 2 lands behind the `!dryRun && conflicts.length === 0` gate per
Q2 (c). Each modified file is written to a sibling temp path then
`rename`d into place — POSIX-atomic per file, so concurrent readers
see either the pre- or post-rename content, never a torn write.

Implementation:
- Phase-1 caches each source's text; phase-2 reuses the cache (one
  read per file across both phases). TOCTOU window collapses to the
  gap between phase-1 read and phase-2 rename — accepted per Q2.
- Phase-2 splits on raw "\n" (not /\r?\n/) so CRLF lines retain their
  trailing \r and round-trip when joined back with "\n". Phase-1
  conflict reporting still strips the \r so `actual_at_line` is clean.
- Edits applied per-file in descending line order — defensive default
  for when multi-line transforms land (today's single-line rows are
  order-independent).
- `$`-pre-escape on `after_pattern` per GetSubstitution rule (mirrors
  buildDiffJson) so identifiers like `$inject` round-trip safely.
- Temp paths use `crypto.randomBytes(6)` so concurrent applies don't
  collide; cleanup on success is implicit (rename atomically removes
  the source name).

Tests: 20/20 pass. Failure-mode coverage: chmod 0o555 on the project
dir to force the temp-write to fail; dry-run no-op-on-disk; no temp
siblings left behind on success; conflict short-circuits before any
writes (good.ts untouched when bad.ts is missing).
Adds `codemap apply <recipe-id>` as a positional verb (per Q4) wired
through the same dispatch as every other CLI command. Recipe execution
reuses `queryRows` + the existing `--params` plumbing (`parseParamsCli`
+ `resolveRecipeParams`); rows feed straight into `applyDiffPayload`.

Q6 gating matrix implemented:
- TTY no `--yes` → phase-1 dry-run preview, prompt `Proceed? [y/N]`
  on stderr, default-N, phase-2 only on `y` (uses node:readline).
- TTY `--yes` → no prompt; proceed if validation clean.
- Non-TTY no `--yes` (no `--dry-run`) → reject with stderr message
  ("Pass --yes for non-interactive runs, or --dry-run for preview.").
- `--dry-run` + `--yes` → mutually exclusive, parse-time error.
- `--json` everywhere routes errors as `{"error":"..."}` envelopes.

Files:
- src/cli/cmd-apply.ts — argv parser + run loop. Mirrors cmd-impact's
  shape (positional + flags + JSON envelope).
- src/cli/cmd-apply.test.ts — 10 subprocess integration tests:
  dry-run no-op, --yes happy path (with cross-file import rename via
  rename-preview), Q7 (a) idempotent re-run after reindex, Q6 non-TTY
  rejection (text + JSON), unknown recipe id, missing positional, mut-
  ex check, --help prints without bootstrap.
- src/cli/main.ts + bootstrap.ts — register the verb.

realpath note: tests `realpathSync` the temp project root so oxc-
resolver's symlink-dereferenced `resolved_path` aligns with the
indexed file paths (without it the import-rename rows in rename-
preview return empty on macOS where /tmp → /private/tmp).

Tests: 10/10 integration + 20/20 engine. Typecheck / lint / format clean.
Registers `apply` as the 13th tool over both MCP (stdio) and HTTP
transports. Dispatches the same `applyDiffPayload` engine the CLI uses;
output envelope is identical to the CLI's --json output (Q5).

- src/application/tool-handlers.ts — `handleApply(args, root)` + Zod
  schema (`applyArgsSchema`). Q6 gate enforced: non-TTY transports
  always require `yes: true` (no prompt to fall back on). dry_run + yes
  rejected as mutually exclusive. Unknown recipe returns 404.
- src/application/mcp-server.ts — `registerApplyTool` mirrors the
  impact tool's shape; description encodes the Q5 envelope + Q2 (c)
  all-or-nothing semantics so agents can reason about the tool without
  reading docs.
- src/application/http-server.ts — adds `apply` to TOOL_NAMES + the
  POST /tool/{name} dispatcher case.
- src/application/tool-handlers.test.ts — 4 handleApply tests (404,
  yes-required, mutex, dry-run envelope shape). 104 mcp/http server
  tests still green; tool catalogs are inferred from TOOL_NAMES so
  the new tool surfaces automatically in /tools listings.

Per the plan's Slice 4 lock: `query_batch` does NOT get an apply
analogue (Moat-A: batched writes are verdict-shaped; consumers
compose multiple apply calls if they need cross-recipe writes).
Final slice — lifts the durable design from the plan into reference
docs and retires the plan file per docs/README.md Rule 3.

- docs/architecture.md — new "Apply wiring" section (engine + phase-1
  algorithm + phase-2 atomic temp-rename + Q6 gate + Q5 envelope + Q7
  idempotency) plus "Boundary verification — apply write path" SQL
  kit. Layering table mentions `apply-engine.ts`.
- docs/glossary.md — `codemap apply` / apply tool entry.
- docs/roadmap.md — backlog entry removed (shipped).
- docs/plans/codemap-apply.md — DELETED (closing-state lifecycle per
  docs-governance skill: delete + lift, never "Slim & keep in plans/").
- .agents/rules/codemap.md + .agents/skills/codemap/SKILL.md — Apply
  row in CLI table, "Apply (`bun src/index.ts apply <recipe-id>`)"
  paragraph, MCP `apply` tool listed alongside `impact`.
- templates/agents/rules/codemap.md + templates/agents/skills/codemap/
  SKILL.md — same updates in the published-package mirror (uses
  `codemap` instead of `bun src/index.ts`).
- .changeset/codemap-apply.md — minor bump; summarises Q1–Q10 locks
  + boundary discipline anchor.

Boundary kit verified empty after a fresh reindex of the apply files;
140/140 tests pass across apply-engine + tool-handlers + cmd-apply +
mcp/http-server suites.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 6, 2026

🦋 Changeset detected

Latest commit: b9448bd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@stainless-code/codemap Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

📝 Walkthrough

Walkthrough

Adds a transport-agnostic "apply" feature: a two-phase apply engine that validates recipe-produced diff rows and, when consented, performs atomic per-file writes. Includes CLI, MCP, and HTTP wiring, tests, docs, and a minor recipe-params merge tweak.

Changes

Codemap Apply Feature

Layer / File(s) Summary
Data Shape / Types
src/application/apply-engine.ts
Defines ApplyInputRow, ApplyDiffPayloadOpts, ApplyFile, ConflictRow, ConflictReason, ApplyJsonPayload.
Core Engine
src/application/apply-engine.ts
Implements applyDiffPayload(): Phase 1 validation (path containment, existence, line-range, content drift, duplicate detection) and Phase 2 atomic per-file apply via temp file + rename; dry-run support and conflict collection.
Engine Tests
src/application/apply-engine.test.ts
Comprehensive tests covering dry-run, batching, deterministic sorting, substring matching, conflict collection, CRLF handling, overlap detection, cleanup, and failure modes.
Tool Handler / API
src/application/tool-handlers.ts
Adds ApplyArgs, applyArgsSchema, and handleApply() that resolves recipe params, gates non-interactive writes (yes/dry_run), executes recipe SQL, and delegates to applyDiffPayload.
Tool Handler Tests
src/application/tool-handlers.test.ts
Tests for handleApply: unknown recipe rejection, write gating, mutual-exclusivity of flags, and dry-run envelope checks (real fs usage).
Transport Integration
src/application/mcp-server.ts, src/application/http-server.ts
Registers "apply" tool in MCP and HTTP servers; validates inputs and routes to handleApply.
CLI Command Layer
src/cli/cmd-apply.ts, src/cli/main.ts
Adds codemap apply subcommand: help, argument parsing, interactive prompting, non-TTY gating, and output rendering.
CLI Bootstrap
src/cli/bootstrap.ts
Registers "apply" as a valid top-level command and updates usage help.
CLI Tests
src/cli/cmd-apply.test.ts
End-to-end tests exercising dry-run and apply flows, non-TTY gating, JSON envelopes, idempotence, error paths, and help output.
Docs & Templates
.agents/rules/codemap.md, .agents/skills/codemap/SKILL.md, docs/architecture.md, docs/glossary.md, .changeset/codemap-apply.md
Adds Apply documentation (two-phase workflow, gating, envelope), architecture wiring and boundary verification notes, glossary entry, agent templates, and a changeset.
Plan / Roadmap
docs/plans/codemap-apply.md (deleted), docs/roadmap.md
Removes the dedicated plan file and consolidates decision notes into the changeset; adjusts roadmap backlog.
Small Behavior Change
src/application/recipe-params.ts
mergeParams now uses { ...base, ...next } (removed base ?? {} fallback).

Sequence Diagram

sequenceDiagram
    participant CLI as CLI
    participant Handler as ToolHandler
    participant Engine as ApplyEngine
    participant Disk as DiskIO

    CLI->>Handler: parse args and call handleApply(args, root)
    Handler->>Handler: gate: require yes or dry_run (non-TTY requires yes)
    Handler->>Handler: resolve recipe params and run recipe SQL -> rows
    Handler->>Engine: applyDiffPayload({ rows, projectRoot, dryRun })
    Engine->>Disk: read files (per-path cache) for validation
    Engine->>Engine: Phase 1: validate rows (containment, existence, line ranges, content drift, duplicates)
    alt conflicts or dry_run
        Engine-->>Handler: return ApplyJsonPayload (applied=false, conflicts, summary)
    else no conflicts and apply mode
        Engine->>Disk: write per-file to temp file
        Engine->>Disk: rename temp -> target (atomic)
        Engine-->>Handler: return ApplyJsonPayload (applied=true, files, summary)
    end
    Handler-->>CLI: tool result (payload)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 I hopped through rows and patches fine,

Checked each line and guarded line,
Two phases first, then swaps so neat,
Temp to target — atomic and fleet.
Conflicts flagged; I left a note,
Now recipes can fix what they wrote.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: implementation of the codemap apply feature with Q1-Q10 requirements fulfilled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/codemap-apply

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Lands four fixes from a triangulated review of three independent agent
audits (Composer, GPT-5.5, Codex). Two HIGH-severity correctness bugs
were each reproducible against the prior `apply-engine.ts` in 30 seconds:

F1 (HIGH) — Path traversal. Pre-fix:
  applyDiffPayload({ rows: [{ file_path: "../outside.ts", ... }],
                     projectRoot: "/tmp/proj/", dryRun: false })
returned `applied: true` and mutated a sibling-of-root file. Now phase 1
resolves the project root once and rejects (a) absolute `file_path`
inputs and (b) any candidate whose `path.resolve(resolvedRoot, file_path)`
lands outside it. New conflict reason: `path escapes project root`.

F2 (HIGH) — Phase-2 partial cross-file write. Pre-fix: two rows on the
same `(file_path, line_start)` both passed phase-1 (substring check
against original source); phase-2 applied the first replace, the
second's substring assertion failed, the function threw — AFTER earlier
files in alphabetical order had already been `renameSync`d. The "Q2 (c)
all-or-nothing" guarantee was demonstrably broken. Now phase 1
maintains a per-file Set<line_start>; the second hit at the same line
emits a `duplicate edit on same line` conflict before any write.

F3 (MEDIUM, doc-first) — Same-line `before_pattern` ambiguity. The
formatter precedent (`buildDiffJson`) uses `actual.replace(before, after)`,
which rewrites only the leftmost occurrence. `const foo = foo();` with
`before = "foo"` becomes `const bar = foo();` — variable renamed,
recursive call broken, `applied: true` reported. This mirrors the
formatter exactly and the `--format diff` preview shows the same shape,
so the audit's recommendation of an engine-level fix would diverge
preview from execution. Documented as a deliberate limitation in the
engine docstring + `architecture.md § Apply wiring` caveat instead;
test pins the current behaviour so a future engine change lands as a
deliberate breaking change rather than silent drift.

F4 + F6 (LOW) — `apply-engine.ts` docstring no longer points at the
deleted plan (now links to `docs/architecture.md` for durable design);
`apply-engine` added to the `application/` row of the Key Files table
in architecture.md (it was meant to be in that enumeration alongside
the other 14 engines).

Tests: 25 unit tests (8 new — three F1 paths, one F2 repro, one F3
limitation pin, plus existing happy-paths / failure-modes); 41 pass
across the apply path. Boundary kit returns []. Changeset entry
amended with the path-containment + overlap-detection bullets so the
release notes carry the security-relevant fixes.

Triangulated audit doc + the three source agent reviews are NOT
checked in — they served their purpose for this fix-up commit and
removing them avoids stale "review backlog" cruft per docs-governance.

Follow-up (separate PR): the audit also surfaced that
`DEFAULT_EXCLUDE_DIR_NAMES` in src/config.ts doesn't include `.codemap`,
so `audit --base` followed by `--full` walks the audit-cache subtree.
Tracked separately because the gap predates this PR.
@SutuSebastian
Copy link
Copy Markdown
Contributor Author

@coderabbitai help

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai rate limit to show your current review rate limit status.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
  • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
  • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve merge conflict to automatically resolve merge conflicts.
  • @coderabbitai autofix to automatically fix issues identified in unresolved review comments.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai generate configuration to create a PR that adds the current resolved configuration as .coderabbit.yaml (or show it if already present).
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit configuration file (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

Concise-comments sweep on the apply surface — module docstring goes
from a six-section narrative to three named call-outs (same-line
ambiguity / TOCTOU / EOL); inline comments drop redundant prose where
the next line of code already says it. Net 65 lines removed across
src/ with no behavioural change.

Docs sync: post-fix the engine collects FIVE conflict reasons (added
`path escapes project root` + `duplicate edit on same line` in commit
bdf7ef3), but the agent rule, the published-package agent rule, and
the glossary all still said "three." Updated all three to enumerate
the full set + briefly describe what each new guard rejects.

Touched:
- src/application/apply-engine.ts — slim docstring + 6 inline blocks.
- src/application/apply-engine.test.ts — slim test rationale where
  the assertion already conveyed it.
- src/cli/cmd-apply.ts — collapse two same-branch returns into one
  union; slim Q6/path-derivation comments.
- src/application/tool-handlers.ts — slim handleApply schema/header
  doc to one sentence each.
- .agents/rules/codemap.md + templates/agents/rules/codemap.md +
  docs/glossary.md — three → five conflict reasons + new-guard one-liners.

Tests + typecheck + format + boundary kit all green.
@SutuSebastian
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.agents/rules/codemap.md:
- Around line 77-80: The "Impact" and "Apply" sections in the codemap doc are
accidentally merged causing a sentence break and stale conflict-reason list;
split the stitched paragraph so the Impact description (starting with "Impact
(`bun src/index.ts impact <target>`)") ends cleanly before the Apply section,
then restore the Apply header and its correct description for "Apply (`bun
src/index.ts apply <recipe-id>`)": update the conflict reasons list to include
the two newer reasons (so Phase 1 reports all current conflict reasons) and
ensure references to applyDiffPayload and findImpact /
application/apply-engine.ts / application/impact-engine.ts remain accurate; keep
the output envelope and behavior notes intact and ensure the two sections are
separated into distinct paragraphs with no crossover text.

In `@docs/glossary.md`:
- Around line 36-37: Update the Phase-1 conflict reasons list in
docs/glossary.md to match the current apply engine: include the two additional
reasons now emitted by application/apply-engine.ts (applyDiffPayload) — namely
"path escapes project root" and "duplicate edit on same line" — alongside the
existing "file missing", "line out of range", and "line content drifted"; ensure
the glossary wording and ordering reflect the engine's exact reason strings and
that any references to the Phase-1 validation behavior (e.g.,
actual.includes(before_pattern) and the Q2 all-or-nothing rule) remain accurate.

In `@src/application/apply-engine.ts`:
- Around line 155-184: The path containment check and cache keys must use a
canonical on-disk path (resolved + realpath + normalized) so symlinks and
lexical duplicates (e.g. "a.ts" vs "./a.ts") can't escape or be double-booked:
replace uses of the raw filePath in isWithinRoot, sourceCache.get/set,
readFileSync(resolve(..., filePath)), and any pending-writes map keys with a
single canonical variable computed via path.resolve + fs.realpathSync (or async
realpath) and path.normalize (e.g., compute canonicalPath =
normalize(realpathSync(resolve(resolvedRoot, filePath)))) and then use
canonicalPath for the containment check, file reads, cache keys, conflict
objects (file_path), and any write/pending maps; apply the same change to the
other similar blocks noted (around the other ranges) so all cache/write lookups
share the same canonical target.
- Around line 283-330: The loop in apply-engine.ts can leave earlier files
permanently changed if writeFileSync/renameSync fails mid-loop; to make the
multi-file apply transactional, create per-file backups and a rollback on error:
before overwriting each absPath, move the original to a backup (e.g., backupPath
= `${absPath}.codemap-backup-${rand}.tmp`) or copy it, then rename the tempPath
into place; push {absPath, backupPath} into a backups list; surround the
write/rename sequence in a try/catch that on any error iterates the backups in
reverse and restores each backup (rename backupPath back to absPath) and cleans
up temps/backups, then rethrow the error; update references to
writeFileSync/renameSync/tempPath/absPath and the surrounding loop (the code
that builds writtenFiles and appliedRows) so you only commit
writtenFiles/appliedRows after successful completion and ensure all temp/backup
files are removed on success.
- Around line 141-149: The check is incorrectly skipping valid rows because
readString currently treats an explicitly empty after_pattern as missing; update
the code so empty-string after patterns are allowed: either modify readString to
accept an allowEmpty boolean (e.g., readString(row, "after_pattern",
{allowEmpty: true}) and return "" for present-but-empty cells) or add a new
helper (readAllowEmptyString/readNullableString) and use it when reading
after_pattern, then keep the validation only rejecting undefined (not empty
string) for the variables filePath, lineStart, before and allow after === "" as
valid; apply the same change to the other analogous read/validation block that
handles recipe rows.

In `@src/cli/cmd-apply.ts`:
- Around line 240-245: The abort branch currently calls emitResult(preview,
opts) while proceed is false, causing downstream logic to treat it as a real
apply and print "apply <id>: no rows applicable."; update that call to indicate
a dry run so the terminal path treats it as a user-cancelled preview — e.g.,
call emitResult(preview, { ...opts, dryRun: true }) or otherwise set opts.dryRun
= true before calling emitResult in the block where proceed is false (the
variables/functions to change are the proceed check and the emitResult(preview,
opts) invocation).
- Around line 195-205: The gate currently checks process.stdout.isTTY but
promptYesNo() uses process.stdin and process.stderr, so replace the isTTY check
with TTY checks on the exact streams the prompt uses: require
process.stdin.isTTY === true and process.stderr.isTTY === true (or check each
separately as your policy prefers) before allowing interactive prompting; keep
the existing opts.yes and opts.dryRun allowances and ensure promptYesNo
(referenced at promptYesNo) will only be reachable when stdin/stderr are TTYs so
it won't attempt to prompt on non-TTY input.

In `@templates/agents/rules/codemap.md`:
- Around line 86-87: Update the Phase 1 conflict-reason list in the Apply
documentation to match the current engine: add "path escapes project root" and
"duplicate edit on same line" to the three existing reasons so the list now
reflects all five conflict reasons used by the apply logic; reference the
runtime behavior implemented by application/apply-engine.ts and the public apply
entry applyDiffPayload so readers can trace the source of these conflict
reasons.

In `@templates/agents/skills/codemap/SKILL.md`:
- Line 82: The Phase 1 conflict list in the SKILL.md description for the apply
tool is missing two conflict reasons; update the prose under the "Phase 1
validates every row" paragraph (the description of the apply executor / `apply`
signature) to include the additional conflict reasons `path escapes project
root` and `duplicate edit on same line` so the documented Phase-1 validation
list matches the runtime engine contract and the `conflicts` entries described
in the result envelope.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9a27c1ad-fa9d-4e83-b52f-7937c2da1487

📥 Commits

Reviewing files that changed from the base of the PR and between 2b7eea8 and bdf7ef3.

📒 Files selected for processing (19)
  • .agents/rules/codemap.md
  • .agents/skills/codemap/SKILL.md
  • .changeset/codemap-apply.md
  • docs/architecture.md
  • docs/glossary.md
  • docs/plans/codemap-apply.md
  • docs/roadmap.md
  • src/application/apply-engine.test.ts
  • src/application/apply-engine.ts
  • src/application/http-server.ts
  • src/application/mcp-server.ts
  • src/application/tool-handlers.test.ts
  • src/application/tool-handlers.ts
  • src/cli/bootstrap.ts
  • src/cli/cmd-apply.test.ts
  • src/cli/cmd-apply.ts
  • src/cli/main.ts
  • templates/agents/rules/codemap.md
  • templates/agents/skills/codemap/SKILL.md
💤 Files with no reviewable changes (2)
  • docs/plans/codemap-apply.md
  • docs/roadmap.md

Comment thread .agents/rules/codemap.md Outdated
Comment thread docs/glossary.md Outdated
Comment thread src/application/apply-engine.ts
Comment thread src/application/apply-engine.ts Outdated
Comment thread src/application/apply-engine.ts Outdated
Comment thread src/cli/cmd-apply.ts
Comment thread src/cli/cmd-apply.ts
Comment thread templates/agents/rules/codemap.md Outdated
Comment thread templates/agents/skills/codemap/SKILL.md Outdated
Triaged 9 actionable comments via pr-comment-fact-check. Each finding
verified against the source on aaabc13; 2 were already addressed by
the prior commit (CodeRabbit auto-tagged with "✅ Addressed in
aaabc13"); 7 are new fixes here:

F1 (paragraph merge in .agents/rules/codemap.md, partial earlier-fix):
  CodeRabbit's auto-tag was optimistic — the conflict-reason count
  was synced in aaabc13 but the Impact section's tail (`...
  --summary trims …`jq '.summary.nodes'``) was still stitched onto
  the END of the Apply paragraph. Restored the section break.

F3 (after_pattern: "" silently dropped):
  `readString` rejected empty strings, so a deletion-shaped row got
  silently skipped by phase-1's required-keys check. New
  `readStringAllowEmpty` helper for `after_pattern` only — empty
  `before_pattern` still rejected (would match anywhere on the line).
  Regression test deletes a `// FIXME(team): ` prefix.

F4 (cache-key dedup `a.ts` vs `./a.ts`):
  Pre-fix, the cache + pending + seenLines maps used the raw
  `file_path` as their key. Two rows naming the same disk file via
  different spellings created two cache entries → second write
  clobbered the first edit. New `canonicalizeFilePath` collapses
  every spelling to a project-relative form. Symlink-realpath
  defense remains documented as a separate (heavier) follow-up.

F5 (Q2 (c) over-promised on I/O failures):
  CodeRabbit's "🔴 Heavy lift" — a writeFileSync/renameSync mid-loop
  failure leaves files 1..N-1 already renamed with no rollback. Full
  transactional rollback (per-file backups + restore-on-throw) is
  deferred. Honest fix: weakened the Q2 (c) claim in
  `architecture.md § Apply wiring` to "all-or-nothing (semantic) —
  phase-1 conflicts abort phase 2 entirely; phase-2 I/O failures are
  NOT transactional across files." Engine docstring carries the same
  caveat as a fourth call-out.

F6 (TTY check used wrong stream):
  Gate checked `process.stdout.isTTY` but `promptYesNo()` reads from
  `process.stdin` and writes to `process.stderr`. So
  `codemap apply foo | tee log.txt` (interactive stdin, piped stdout)
  was rejected as non-TTY. Now gates on `stdin.isTTY && stderr.isTTY`.

F7 (user-cancel rendered "no rows applicable"):
  Abort path called `emitResult(preview, opts)` with `opts.dryRun ===
  false`, so `renderTerminal` fell through to "no rows applicable" —
  contradicting the user's explicit cancel. Terminal mode now prints
  `apply <id>: aborted by user; no files written.`; JSON consumers
  still get the full preview envelope.

F9 (skill files missed two conflict reasons):
  `.agents/skills/codemap/SKILL.md` + `templates/agents/skills/
  codemap/SKILL.md` apply tool description didn't enumerate the 5
  conflict reasons. Synced.

Tests: 44/44 (3 new — `./a.ts` dedup, deletion via empty
after_pattern, empty-before-still-rejected). Typecheck / lint /
format clean.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/glossary.md`:
- Around line 36-37: Update the Q6 gate wording so the non-TTY `--yes` / `yes:
true` requirement only applies when performing a write (i.e., when `!dryRun` /
`dry_run: false`) rather than to all non-TTY runs; explicitly state that non-TTY
dry-run is allowed without `--yes`; use MCP-style flag names `yes` and `dry_run`
in the transport/HTTP examples and ensure the CLI example shows `--dry-run`
mapping to `dry_run`. Locate and edit the paragraph describing the TTY/`--yes`
behavior (the Q6 gate sentence around the `Proceed? [y/N]` prompt) and any
transport/HTTP examples in the same glossary block so they reflect the scoped
requirement.

In `@src/application/apply-engine.test.ts`:
- Around line 530-531: The test currently writes to a fixed shared path by using
join(root, "..", "outside.ts") which can collide across runs; change the test so
outsidePath is unique per run (e.g. derive it from tmpdir or mkdtemp/mkdirSync
or append a random/pid/timestamp/uuid) before calling writeFileSync, and clean
up after the test; update the code that constructs outsidePath (the join(root,
"..", ... ) invocation) and keep using writeFileSync/outsidePath as before.

In `@src/cli/cmd-apply.ts`:
- Around line 110-118: The "--params" branch currently only checks for undefined
and accepts the next token even if it's another flag (e.g., "--params
--dry-run"); update the check in the handler that inspects variable a for
"--params" so that after reading const next = rest[i + 1] you also reject any
next that looks like a flag (e.g., startsWith("-")) and return the same error
shape (kind: "error", message: `codemap apply: "--params" requires a value
(k=v[,k=v]).`) instead of calling mergeParams/parseParamsCli; keep using
mergeParams and parseParamsCli when next is valid so the rest of the logic is
unchanged.

In `@templates/agents/rules/codemap.md`:
- Around line 86-87: Update the Q6 gating sentence to explicitly state that
non-TTY runs require --yes (or yes: true) except when --dry-run is used; mention
that --dry-run + --yes remain mutually exclusive and that CLI/agents/HTTP/CI
behavior follows the same applyDiffPayload gating (TTY prompts on stderr with
default N). Locate the Q6 paragraph referencing TTY prompts and the non-TTY
requirement and reword it to: non-TTY requires explicit --yes unless running
--dry-run, which is allowed without --yes, and preserve the mutual-exclusion
rule and consistent terminology for --dry-run/--yes across the document.

In `@templates/agents/skills/codemap/SKILL.md`:
- Line 82: Update the SKILL.md description of the apply action (symbol: apply)
to remove the absolute claim “partial writes never ship” and instead state that
phase-1 (conflict detection via substring-match /
actual.includes(before_pattern)) gates whether phase-2 runs, but phase-2
per-file writes (sibling temp + rename) are not cross-file transactional — I/O
failures during phase-2 may leave some files written and others not; keep the
all-or-nothing semantics as “abort phase-2 entirely if any conflicts detected in
phase-1” and explicitly note the caveat that phase-2 is best-effort per-file
atomic via rename but does not provide a multi-file atomic transaction; preserve
descriptions of yes/dry_run semantics and re-run behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1de750c0-3942-4e8c-83f8-273beb681c12

📥 Commits

Reviewing files that changed from the base of the PR and between bdf7ef3 and b9448bd.

📒 Files selected for processing (12)
  • .agents/rules/codemap.md
  • .agents/skills/codemap/SKILL.md
  • docs/architecture.md
  • docs/glossary.md
  • src/application/apply-engine.test.ts
  • src/application/apply-engine.ts
  • src/application/recipe-params.ts
  • src/application/tool-handlers.ts
  • src/cli/cmd-apply.test.ts
  • src/cli/cmd-apply.ts
  • templates/agents/rules/codemap.md
  • templates/agents/skills/codemap/SKILL.md

Comment thread docs/glossary.md
Comment on lines +36 to +37
Substrate-shaped fix executor — reads the same `{file_path, line_start, before_pattern, after_pattern}` row contract `--format diff-json` emits and applies the hunks to disk. Recipe SQL is the synthesis surface; codemap is the executor (Moat-A clean — verdict-shape "should we fix this?" stays on the recipe author). CLI: `codemap apply <recipe-id> [--params k=v[,k=v]] [--dry-run] [--yes] [--json]`. MCP: `apply` tool. HTTP: `POST /tool/apply`. **Phase 1** validates every row against current disk via `actual.includes(before_pattern)` (substring match — mirrors `buildDiffJson`'s contract); collects five conflict reasons (`file missing` / `line out of range` / `line content drifted` / `path escapes project root` / `duplicate edit on same line`). The path-containment guard rejects absolute `file_path` inputs and `../`-traversal that resolves outside `projectRoot`; the overlap guard rejects two-or-more rows targeting the same `(file_path, line_start)`. **Phase 2** (gated on `!dryRun && conflicts.length === 0`) writes each modified file via sibling temp + `renameSync` for POSIX-atomic per-file writes, with `$`-pre-escape on `after_pattern` per `String.prototype.replace` GetSubstitution rule. **Q2 (c) all-or-nothing** — any conflict in any file aborts phase 2 entirely; partial writes never ship. **Q6 gate** — TTY no `--yes` triggers a `Proceed? [y/N]` prompt (default-N) on stderr; non-TTY contexts (CI / agents / MCP / HTTP) require `--yes` (or `yes: true`) explicitly. Result envelope (Q5; identical across modes): `{mode, applied, files, conflicts, summary}`. Re-running on already-applied code reports a `line content drifted` conflict whose `actual_at_line` shows the post-rename content — user re-runs `codemap` to refresh the index, then re-runs apply for a vacuous clean pass (Q7). Engine: `application/apply-engine.ts` (pure; `applyDiffPayload`). Boundary: only `cli/cmd-apply.ts` + `application/tool-handlers.ts` may import the engine — re-runnable kit at [`docs/architecture.md` § Boundary verification — apply write path](./architecture.md#boundary-verification--apply-write-path). Floor "No fix engine" preserved — codemap doesn't synthesise edits, it only executes the hunks the recipe row described.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Narrow the non-TTY yes requirement to write mode only.

Line 36 currently implies non-TTY always needs --yes / yes: true; that should be scoped to non-dry-run apply. Non-TTY dry-run is allowed and should be stated explicitly to keep docs accurate.

Based on learnings: “For non-TTY transports, require yes: true unconditionally since there is no interactive prompt fallback” (for write path), and use MCP-style key naming (yes, dry_run) in transport docs.

🧰 Tools
🪛 LanguageTool

[grammar] ~36-~36: Ensure spelling is correct
Context: ...patternperString.prototype.replace` GetSubstitution rule. Q2 (c) all-or-nothing — any c...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/glossary.md` around lines 36 - 37, Update the Q6 gate wording so the
non-TTY `--yes` / `yes: true` requirement only applies when performing a write
(i.e., when `!dryRun` / `dry_run: false`) rather than to all non-TTY runs;
explicitly state that non-TTY dry-run is allowed without `--yes`; use MCP-style
flag names `yes` and `dry_run` in the transport/HTTP examples and ensure the CLI
example shows `--dry-run` mapping to `dry_run`. Locate and edit the paragraph
describing the TTY/`--yes` behavior (the Q6 gate sentence around the `Proceed?
[y/N]` prompt) and any transport/HTTP examples in the same glossary block so
they reflect the scoped requirement.

Comment on lines +530 to +531
const outsidePath = join(root, "..", "outside.ts");
writeFileSync(outsidePath, "const foo = 1;\n", "utf8");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a unique out-of-root target in the traversal test.

join(root, "..", "outside.ts") resolves to a shared /tmp/outside.ts, which can race across tests and leak state between runs.

Suggested fix
-      const outsidePath = join(root, "..", "outside.ts");
+      const outsideRoot = mkdtempSync(join(tmpdir(), "codemap-apply-outside-"));
+      const outsidePath = join(outsideRoot, "outside.ts");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/apply-engine.test.ts` around lines 530 - 531, The test
currently writes to a fixed shared path by using join(root, "..", "outside.ts")
which can collide across runs; change the test so outsidePath is unique per run
(e.g. derive it from tmpdir or mkdtemp/mkdirSync or append a
random/pid/timestamp/uuid) before calling writeFileSync, and clean up after the
test; update the code that constructs outsidePath (the join(root, "..", ... )
invocation) and keep using writeFileSync/outsidePath as before.

Comment thread src/cli/cmd-apply.ts
Comment on lines +110 to +118
if (a === "--params") {
const next = rest[i + 1];
if (next === undefined) {
return {
kind: "error",
message: `codemap apply: "--params" requires a value (k=v[,k=v]).`,
};
}
params = mergeParams(params, parseParamsCli(next));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject flag tokens as --params values.

Line 112 only checks undefined; --params --dry-run is currently parsed as params input instead of a direct CLI error.

Suggested fix
-      if (next === undefined) {
+      if (next === undefined || next.startsWith("-")) {
         return {
           kind: "error",
           message: `codemap apply: "--params" requires a value (k=v[,k=v]).`,
         };
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/cmd-apply.ts` around lines 110 - 118, The "--params" branch currently
only checks for undefined and accepts the next token even if it's another flag
(e.g., "--params --dry-run"); update the check in the handler that inspects
variable a for "--params" so that after reading const next = rest[i + 1] you
also reject any next that looks like a flag (e.g., startsWith("-")) and return
the same error shape (kind: "error", message: `codemap apply: "--params"
requires a value (k=v[,k=v]).`) instead of calling mergeParams/parseParamsCli;
keep using mergeParams and parseParamsCli when next is valid so the rest of the
logic is unchanged.

Comment on lines +86 to +87
**Apply (`codemap apply <recipe-id>`)**: substrate-shaped fix executor over the existing `--format diff-json` row contract — recipe SQL is the synthesis surface, codemap executes. Phase 1 validates every `{file_path, line_start, before_pattern, after_pattern}` row against current disk via `actual.includes(before_pattern)` (substring match — same contract `buildDiffJson` uses); collects five conflict reasons (`file missing` / `line out of range` / `line content drifted` / `path escapes project root` / `duplicate edit on same line`). The `path escapes project root` guard rejects absolute `file_path` inputs and any candidate whose resolved form lands outside `projectRoot`; the `duplicate edit on same line` guard rejects two-or-more rows targeting the same `(file_path, line_start)` so phase 2 doesn't split mid-loop and leak Q2 (c). Phase 2 (gated on `!dryRun && conflicts.length === 0`) writes via sibling temp + `rename` for POSIX-atomic per-file writes. **Q2 (c) all-or-nothing** — any conflict aborts the whole run before any file is touched. **Q6 gate** — TTY prompts `Proceed? [y/N]` (default-N) on stderr; non-TTY (CI / agents / MCP / HTTP) requires `--yes` (or `yes: true`) explicitly; `--dry-run` + `--yes` mutually exclusive. Q7 idempotency: re-running on already-applied code reports `line content drifted` with `actual_at_line` showing the post-rename content — re-run `codemap` to refresh the index, then re-run apply (vacuous clean pass). Output envelope (identical across modes): `{mode: 'dry-run'|'apply', applied: bool, files: [{file_path, rows_applied, warnings?}], conflicts: [{file_path, line_start, before_pattern, actual_at_line, reason}], summary: {files, files_modified, rows, rows_applied, conflicts, files_with_conflicts}}`. Pure transport-agnostic engine in `application/apply-engine.ts`; CLI / MCP / HTTP all dispatch the same `applyDiffPayload` function.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify non-TTY gating to include the dry-run exception.

Line 86 currently reads as if non-TTY always requires --yes, but Line 45 (and runtime behavior) allows non-TTY with --dry-run and no --yes. Please make this sentence explicit to avoid conflicting guidance.

As per coding guidelines: “Documentation files must be kept up-to-date with code changes” and “Use consistent terminology across all documentation.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@templates/agents/rules/codemap.md` around lines 86 - 87, Update the Q6 gating
sentence to explicitly state that non-TTY runs require --yes (or yes: true)
except when --dry-run is used; mention that --dry-run + --yes remain mutually
exclusive and that CLI/agents/HTTP/CI behavior follows the same applyDiffPayload
gating (TTY prompts on stderr with default N). Locate the Q6 paragraph
referencing TTY prompts and the non-TTY requirement and reword it to: non-TTY
requires explicit --yes unless running --dry-run, which is allowed without
--yes, and preserve the mutual-exclusion rule and consistent terminology for
--dry-run/--yes across the document.

- **`show`** — `{name, kind?, in?}`. Exact, case-sensitive symbol name lookup. Returns `{matches: [{name, kind, file_path, line_start, line_end, signature, ...}], disambiguation?: {n, by_kind, files, hint}}`. Single match → `{matches: [{...}]}`; multi-match adds the disambiguation envelope so you narrow without re-scanning. Fuzzy lookup belongs in `query` with `LIKE`.
- **`snippet`** — `{name, kind?, in?}`. Same lookup as `show` but each match also carries `source` (file lines from disk at `line_start..line_end`), `stale` (true when content_hash drifted since indexing — line range may have shifted), `missing` (true when file is gone). `source` is always returned when the file exists; agent decides whether to act on stale content or run `codemap` / `codemap --files <path>` to re-index first. No auto-reindex side-effects from this read tool.
- **`impact`** — `{target, direction?, via?, depth?, limit?, summary?}`. Symbol/file blast-radius walker — replaces hand-composed `WITH RECURSIVE` queries that agents struggle to write reliably. `target` is a symbol name (case-sensitive, exact) OR a project-relative file path (auto-detected by `/` or by matching `files.path`). `direction`: `up` (callers / dependents), `down` (callees / dependencies), `both` (default). `via`: `dependencies`, `calls`, `imports`, `all` (default — every backend compatible with the resolved target kind: symbol → `calls`; file → `dependencies` + `imports`; mismatched explicit choices land in `skipped_backends`, no error). `depth` default 3, `0` = unbounded (still cycle-detected and limit-capped). `limit` default 500. `summary: true` trims `matches` for cheap CI-gate consumption (`jq '.summary.nodes'`) but preserves the count. Result: `{target, direction, via, depth_limit, matches: [{depth, direction, edge, kind, name?, file_path}], summary: {nodes, max_depth_reached, by_kind, terminated_by: 'depth'|'limit'|'exhausted'}}`. Cycle detection is approximate-but-bounded — bounded depth + `LIMIT` keep cyclic graphs cheap; `terminated_by` reports the dominant stop reason. SARIF / annotations not supported (impact rows are graph traversals, not findings).
- **`apply`** — `{recipe, params?, dry_run?, yes?}`. Substrate-shaped fix executor — runs the same recipe `query_recipe` runs, then applies the diff hunks each row describes (`{file_path, line_start, before_pattern, after_pattern}`) to disk. Recipe SQL is the synthesis surface; codemap is the executor. Phase 1 validates every row against current disk via substring-match (`actual.includes(before_pattern)`) — the same contract `--format diff-json` uses; collects five conflict reasons (`file missing` / `line out of range` / `line content drifted` / `path escapes project root` / `duplicate edit on same line`). Phase 2 (gated on no conflicts) writes via sibling temp + `rename` for POSIX-atomic per-file writes. **Q2 (c) all-or-nothing** — any conflict in any file aborts phase 2 entirely; partial writes never ship. Over MCP/HTTP `yes: true` is required for the write path (no TTY prompt to fall back on); `dry_run: true` previews without writing; the two are mutually exclusive. Re-running on already-applied code reports a `line content drifted` conflict whose `actual_at_line` shows the post-rename content — re-run `codemap` to refresh the index, then re-run `apply` for a clean vacuous pass. Result envelope (identical across modes): `{mode: 'dry-run'|'apply', applied, files: [{file_path, rows_applied, warnings?}], conflicts: [{file_path, line_start, before_pattern, actual_at_line, reason}], summary: {files, files_modified, rows, rows_applied, conflicts, files_with_conflicts}}`. Floor "No fix engine" preserved — codemap doesn't synthesise edits, it only executes the hunks the recipe row described.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply docs overstate cross-file transactional safety.

Line 82 currently says “partial writes never ship,” but phase-2 I/O failures are not cross-file transactional. Please scope the guarantee to phase-1 conflict gating and explicitly note the phase-2 caveat.

Suggested wording
- **Q2 (c) all-or-nothing** — any conflict in any file aborts phase 2 entirely; partial writes never ship.
+ **Q2 (c) all-or-nothing (phase-1 semantic gate)** — any conflict in any file aborts phase 2 entirely before any file is touched.
+ If phase 2 starts, writes are per-file atomic (`temp` + `rename`), but cross-file rollback is not guaranteed on I/O failure.

As per coding guidelines: “Documentation files must be kept up-to-date with code changes.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **`apply`** — `{recipe, params?, dry_run?, yes?}`. Substrate-shaped fix executor — runs the same recipe `query_recipe` runs, then applies the diff hunks each row describes (`{file_path, line_start, before_pattern, after_pattern}`) to disk. Recipe SQL is the synthesis surface; codemap is the executor. Phase 1 validates every row against current disk via substring-match (`actual.includes(before_pattern)`) — the same contract `--format diff-json` uses; collects five conflict reasons (`file missing` / `line out of range` / `line content drifted` / `path escapes project root` / `duplicate edit on same line`). Phase 2 (gated on no conflicts) writes via sibling temp + `rename` for POSIX-atomic per-file writes. **Q2 (c) all-or-nothing** — any conflict in any file aborts phase 2 entirely; partial writes never ship. Over MCP/HTTP `yes: true` is required for the write path (no TTY prompt to fall back on); `dry_run: true` previews without writing; the two are mutually exclusive. Re-running on already-applied code reports a `line content drifted` conflict whose `actual_at_line` shows the post-rename content — re-run `codemap` to refresh the index, then re-run `apply` for a clean vacuous pass. Result envelope (identical across modes): `{mode: 'dry-run'|'apply', applied, files: [{file_path, rows_applied, warnings?}], conflicts: [{file_path, line_start, before_pattern, actual_at_line, reason}], summary: {files, files_modified, rows, rows_applied, conflicts, files_with_conflicts}}`. Floor "No fix engine" preserved — codemap doesn't synthesise edits, it only executes the hunks the recipe row described.
- **`apply`** — `{recipe, params?, dry_run?, yes?}`. Substrate-shaped fix executor — runs the same recipe `query_recipe` runs, then applies the diff hunks each row describes (`{file_path, line_start, before_pattern, after_pattern}`) to disk. Recipe SQL is the synthesis surface; codemap is the executor. Phase 1 validates every row against current disk via substring-match (`actual.includes(before_pattern)`) — the same contract `--format diff-json` uses; collects five conflict reasons (`file missing` / `line out of range` / `line content drifted` / `path escapes project root` / `duplicate edit on same line`). Phase 2 (gated on no conflicts) writes via sibling temp + `rename` for POSIX-atomic per-file writes. **Q2 (c) all-or-nothing (phase-1 semantic gate)** — any conflict in any file aborts phase 2 entirely before any file is touched. If phase 2 starts, writes are per-file atomic (`temp` + `rename`), but cross-file rollback is not guaranteed on I/O failure. Over MCP/HTTP `yes: true` is required for the write path (no TTY prompt to fall back on); `dry_run: true` previews without writing; the two are mutually exclusive. Re-running on already-applied code reports a `line content drifted` conflict whose `actual_at_line` shows the post-rename content — re-run `codemap` to refresh the index, then re-run `apply` for a clean vacuous pass. Result envelope (identical across modes): `{mode: 'dry-run'|'apply', applied, files: [{file_path, rows_applied, warnings?}], conflicts: [{file_path, line_start, before_pattern, actual_at_line, reason}], summary: {files, files_modified, rows, rows_applied, conflicts, files_with_conflicts}}`. Floor "No fix engine" preserved — codemap doesn't synthesise edits, it only executes the hunks the recipe row described.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@templates/agents/skills/codemap/SKILL.md` at line 82, Update the SKILL.md
description of the apply action (symbol: apply) to remove the absolute claim
“partial writes never ship” and instead state that phase-1 (conflict detection
via substring-match / actual.includes(before_pattern)) gates whether phase-2
runs, but phase-2 per-file writes (sibling temp + rename) are not cross-file
transactional — I/O failures during phase-2 may leave some files written and
others not; keep the all-or-nothing semantics as “abort phase-2 entirely if any
conflicts detected in phase-1” and explicitly note the caveat that phase-2 is
best-effort per-file atomic via rename but does not provide a multi-file atomic
transaction; preserve descriptions of yes/dry_run semantics and re-run behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant