-Substrate-shaped fix executor — reads the same row contract `--format diff-json` emits and applies hunks to disk. Recipe SQL is the synthesis surface; codemap is the executor (Moat-A — verdict-shape "should we fix this?" stays on the recipe author). **Three CLI input modes** (mutually exclusive): (1) **recipe** — `codemap apply <recipe-id> [--params k=v[,k=v]]`; MCP/HTTP `apply` `{recipe, params?, dry_run?, yes?, force?, until_empty?, max_passes?, commit_message?}`. (2) **rows** — `codemap apply --rows -|<file.json>`; MCP/HTTP [`apply_rows`](#apply_rows-mcp-tool--cli-mode). (3) **diff** — `codemap apply --diff-input <unified-diff>`; MCP/HTTP [`apply_diff_input`](#apply_diff_input-mcp-tool--cli-mode). Shared flags: `--dry-run` (phase-1 only), `--yes` (skip TTY prompt; required for non-TTY writes), `--json`. **Recipe-only flags:** `--force` / MCP `force` (bypass `auto_fixable` + allowlist); `--until-empty` / `until_empty` + `--max-passes` / `max_passes` (fixpoint on recipe `apply` only). **Git commit:** `--commit` / `commit_message` on recipe `apply` and `apply_diff_input`. **diff-json preview:** each hunk includes `ambiguity_count` (extra `before_pattern` matches on the line; apply rewrites first match only). **Policy (recipe mode only):** recipes with `auto_fixable: false` in `<id>.md` frontmatter reject writes unless `--force` / MCP `force: true`; `apply.autoApplyRecipes` in [user config](./architecture.md#user-config) allowlists recipe ids for non-interactive CLI without `--yes` (MCP/HTTP writes still need `yes: true`). Bundled diff-shape recipe ids: `rename-preview`, `migrate-import-source`, `replace-marker-kind` (`auto_fixable: true`); `stale-imports`, `migrate-deprecated`, `deprecated-usages`, `add-jsdoc-deprecated`, `migrate-jsx-prop` (force-gated unless allowlisted). **`rename-preview` homonyms:** pass `define_in=<symbols.file_path>` to scope the rename to one definition; omit to union all homonyms (use `find-symbol-references` to pick the anchor first). CLI shorthand: `codemap rename <old> <new> [--define-in <file_path>] [--in-file <prefix>] [--kind <k>]` → `apply rename-preview` (thin alias — same recipe + policy gates). **`--commit` / `commit_message`:** recipe `apply` and `apply_diff_input` only (not `apply_rows`); with `--until-empty`, commit only when `terminated_by` is `empty`. **Phase 1** validates every row via `actual.includes(before_pattern)` (substring match); seven conflict reasons (`file missing` / `line out of range` / `line content drifted` / `path escapes project root` / `path is a symlink` / `duplicate edit on same line`). **Phase 2** (gated on `!dryRun && zero conflicts`) writes via sibling temp + `renameSync` per file; **all-or-nothing** across files on conflicts (no cross-file rollback on crash mid-phase-2). **Q6 gate** — TTY without `--yes` prompts `Proceed? [y/N]`; MCP/HTTP have no prompt path. Result envelope: `{mode, applied, files, conflicts, summary}` (+ optional `passes`, `terminated_by`). Re-apply on stale disk → `line content drifted`; re-index then vacuous zero-row pass (Q7). Engine: `application/apply-engine.ts` (`applyDiffPayload`); orchestration: `application/apply-run.ts`. Full transport matrix: [`architecture.md` § Apply — input modes](./architecture.md#apply--input-modes-transport-and-policy). Boundary kit: [§ Boundary verification — apply write path](./architecture.md#boundary-verification--apply-write-path).
0 commit comments