Skip to content

cycle/0006: graft diff — structural git diff#2

Merged
flyingrobots merged 11 commits into
mainfrom
cycle/0006-graft-diff
Apr 3, 2026
Merged

cycle/0006: graft diff — structural git diff#2
flyingrobots merged 11 commits into
mainfrom
cycle/0006-graft-diff

Conversation

@flyingrobots
Copy link
Copy Markdown
Owner

Summary

  • graft_diff MCP tool: structural diff between any two git refs
    (or working tree vs HEAD). Shows added/removed/changed symbols
    with signatures, not line hunks.
  • src/git/diff.ts: git command wrappers (changed files, file at ref)
  • src/operations/graft-diff.ts: orchestrates git + parser + diffOutlines
  • Supports .ts/.js/.tsx/.jsx; other extensions listed with status only
  • 225 tests across 20 files, 10 MCP tools total

Playback witness

graft_diff HEAD~1..HEAD:
  src/git/diff.ts | added
    + interface: ChangedFilesOptions
    + function: getChangedFiles
    + function: getFileAtRef
  src/operations/graft-diff.ts | added
    + function: graftDiff
    + interface: GraftDiffResult, FileDiff, GraftDiffOptions

Test plan

  • Git helpers: changed file list, file at ref, added/deleted, empty diff
  • graft diff operation: modified/added/deleted, multi-file, working tree,
    changed signatures, path filter, non-supported extensions
  • MCP tool registered (cycle/0015: Claude Code hooks enforcement #10), tool count assertions updated
  • Dogfooded on graft's own repo

Cycle 0006: git helpers (7 tests) — changed file list, file content
at ref, added/deleted files. graft diff operation (9 tests) —
modified/added/deleted files, multi-file, working tree vs HEAD,
changed signatures, path filter, non-supported extensions.
Cycle 0006: diff any two git refs structurally.

- src/git/diff.ts: getChangedFiles, getFileAtRef wrappers
- src/operations/graft-diff.ts: orchestrates git + parser + diffOutlines
  for modified/added/deleted files across commits or working tree
- graft_diff registered as MCP tool (#10) with base/head/path params
- Supports .ts/.js/.tsx/.jsx; other extensions listed with empty diff
- 225 tests across 20 files, all passing
Cycle 0006 closed. Structural git diff works across commits and
working tree. Suppressed noisy git stderr on added/deleted files.
225 tests, 10 MCP tools.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 3, 2026

Warning

Rate limit exceeded

@flyingrobots has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 39 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 9 minutes and 39 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 884032ed-4734-4437-b19b-2784577e69de

📥 Commits

Reviewing files that changed from the base of the PR and between f727133 and a5cfab8.

📒 Files selected for processing (8)
  • docs/design/0006-graft-diff/design.md
  • docs/method/backlog/asap/CORE_lang-detection-missing-in-server.md
  • docs/method/backlog/asap/CORE_mcp-tool-descriptions.md
  • docs/method/backlog/asap/CORE_stale-vision-doc.md
  • docs/method/backlog/bad-code/CORE_shape-soup.md
  • docs/method/backlog/up-next/CC_json-codec-port.md
  • src/git/diff.ts
  • src/operations/graft-diff.ts

Walkthrough

Adds a graft-diff feature that computes symbol-level structural diffs between two git points (refs or working tree). Introduces git helper utilities, a graftDiff orchestration, MCP tool registration, tests, and supporting design/retro/backlog docs. Unsupported extensions yield status-only entries with empty structural diffs.

Changes

Cohort / File(s) Summary
Design & Backlog
docs/design/0006-graft-diff/design.md, docs/method/retro/0006-graft-diff/retro.md, docs/method/backlog/asap/CORE_graft-diff.md, docs/method/backlog/*
Adds Cycle 0006 design and retrospective; removes old backlog stub; introduces multiple backlog notes (bad-code, cool-ideas, STYLE/CLEAN_CODE, hexagonal concerns, test-fixture consolidation).
Git Helpers
src/git/diff.ts
New exports: GitError, getChangedFiles(opts) (wraps git diff --name-only) and getFileAtRef(ref,path,cwd) (wraps git show); return safe defaults/null for missing files and throw GitError for other failures.
Graft Diff Operation
src/operations/graft-diff.ts
New graftDiff(opts) and related interfaces (FileDiff, GraftDiffResult, GraftDiffOptions): enumerates changed files, reads base/head (refs or worktree), detects language, uses extractOutline and diffOutlines for supported extensions, classifies `added
MCP Integration
src/mcp/server.ts
Registers graft_diff MCP tool (inputs: optional base, head, path) that invokes graftDiff and returns a receipt-wrapped result.
Tests
test/unit/git/diff.test.ts, test/unit/operations/graft-diff.test.ts, test/unit/mcp/tools.test.ts, test/integration/mcp/server.test.ts
Adds unit tests for git helpers and graftDiff using temp git repos; new graft-diff behavior exercised (added/modified/deleted, multi-file, working-tree vs HEAD, lang detection, path filter). Updates MCP tool count expectations (9→10).
Changelog
CHANGELOG.md
Updates test/file counts in changelog.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MCP_Server
  participant GraftOp as graftDiff
  participant Git as GitCLI
  participant FS as FileSystem
  participant Extract as extractOutline
  participant Diff as diffOutlines

  Client->>MCP_Server: call graft_diff(base?, head?, path?)
  MCP_Server->>GraftOp: invoke graftDiff({cwd, base, head, path})
  GraftOp->>Git: git diff --name-only (base..head or base vs worktree)
  Git-->>GraftOp: list of changed paths
  loop per changed file
    GraftOp->>Git: git show <base>:<path>
    Git-->>GraftOp: baseContent or null
    alt head is ref
      GraftOp->>Git: git show <head>:<path>
      Git-->>GraftOp: headContent or null
    else head is worktree
      GraftOp->>FS: readFileSync(path)
      FS-->>GraftOp: headContent or error
    end
    GraftOp->>Extract: extractOutline(content, lang)
    Extract-->>GraftOp: outline
    GraftOp->>Diff: diffOutlines(baseOutline, headOutline)
    Diff-->>GraftOp: OutlineDiff
  end
  GraftOp-->>MCP_Server: GraftDiffResult
  MCP_Server-->>Client: result (with receipt)
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly Related PRs

Poem

Symbols whisper where hunks once roared,
Functions split and methods soared,
Refs compared, outlines align—
Added, removed, and changed by sign,
Graft diff blooms: precise, not bored. 🌿

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title precisely and concisely captures the main contribution: introducing 'graft diff' as a structural git diff feature in cycle 0006.
Description check ✅ Passed Description is directly related to the changeset, detailing the graft_diff tool, new modules, supported file types, test coverage, and a playback witness example.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cycle/0006-graft-diff

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.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1bed38ef88

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/git/diff.ts Outdated
: `diff --name-only ${base}`;

try {
const output = execSync(`git ${args}`, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Stop executing git refs through the shell

getChangedFiles interpolates base/head directly into execSync("git ..."), and these values come from graft_diff tool input as arbitrary strings. Because execSync string commands run in a shell, a crafted ref like HEAD; <command> can execute arbitrary OS commands with the server’s privileges. Switch to an argument-vector API (for example execFileSync) or strictly validate refs before invocation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

✅ Fixed in 86dc650. Switched to execFileSync (no shell). Further hardened in f727133 with GitError for invalid refs.

Comment thread src/git/diff.ts Outdated
cwd: string,
): string | null {
try {
return execSync(`git show ${ref}:${filePath}`, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Quote blob paths when calling git show

getFileAtRef builds git show ${ref}:${filePath} as a raw shell string, so legitimate filenames containing spaces or shell-special characters are split/misparsed by the shell. In those cases git show fails and the catch path returns null, which makes graftDiff misclassify a modified file as added/deleted and return an incorrect structural diff. Invoke git with explicit argv entries so filePath is passed as one argument.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

✅ Fixed in 86dc650. execFileSync passes args as array — no shell quoting needed.

Comment thread src/git/diff.ts Outdated
Comment on lines +27 to +28
} catch {
return [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Surface git diff failures instead of returning no changes

The broad catch in getChangedFiles converts every git failure (invalid refs, not-a-repo, permission errors) into [], so callers receive a successful-looking empty diff instead of an error. This silently corrupts results for typoed refs (for example HEAD~999) and makes debugging hard because the failure is suppressed. Propagate a structured error or return an error state instead of treating all failures as “no changes.”

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

✅ Fixed in f727133. getChangedFiles now throws GitError for invalid refs instead of returning empty array.

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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
test/unit/mcp/tools.test.ts (1)

5-17: ⚠️ Potential issue | 🟡 Minor

Test asserts length 10 but only verifies 8 tool names explicitly.

You've bumped the count to 10 but the test lacks expect(toolNames).toContain("graft_diff") and expect(toolNames).toContain("changed_since"). This means you could register a tool named "oops_typo" instead, the length check would pass, and you'd never notice.

Add explicit assertions for all registered tools.

🔧 Proposed fix
     expect(toolNames).toContain("doctor");
     expect(toolNames).toContain("stats");
+    expect(toolNames).toContain("changed_since");
+    expect(toolNames).toContain("graft_diff");
     expect(toolNames).toHaveLength(10);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/mcp/tools.test.ts` around lines 5 - 17, The test creates a server
via createGraftServer(), collects registered tools into toolNames, and asserts
total length 10 but only checks 8 specific names; add explicit expects for the
two missing tools by asserting expect(toolNames).toContain("graft_diff") and
expect(toolNames).toContain("changed_since") so the test verifies all registered
tool names rather than relying solely on the length check.
test/integration/mcp/server.test.ts (1)

26-38: ⚠️ Potential issue | 🟡 Minor

Test description says "lists all 8 tools" but asserts toHaveLength(10).

The description is stale and contradicts the assertion. Additionally, same issue as the unit test: you verify only 8 tool names explicitly but expect 10 total.

🔧 Proposed fix
-  it("lists all 8 tools", async () => {
+  it("lists all 10 tools", async () => {
     const tools = await client.listTools();
     const names = tools.tools.map((t) => t.name);
     expect(names).toContain("safe_read");
     expect(names).toContain("file_outline");
     expect(names).toContain("read_range");
     expect(names).toContain("run_capture");
     expect(names).toContain("state_save");
     expect(names).toContain("state_load");
     expect(names).toContain("doctor");
     expect(names).toContain("stats");
+    expect(names).toContain("changed_since");
+    expect(names).toContain("graft_diff");
     expect(names).toHaveLength(10);
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/mcp/server.test.ts` around lines 26 - 38, The test "lists
all 8 tools" is inconsistent: the description says 8, the code asserts 10 and
only checks 8 specific names. Update the test in the it block that calls
client.listTools() so the description, the explicit expects (names checks), and
the length assertion all match the real number of tools; either change the
description to "lists all 10 tools" and add expectations for the two missing
tool names (or remove/change the length assertion to 8), or adjust the length to
8 to match the listed names—modify the it("lists all 8 tools") block, the names
array assertions, and the expect(names).toHaveLength(...) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/design/0006-graft-diff/design.md`:
- Around line 41-43: Add a language identifier to the two fenced code blocks:
the one showing the function signature "graft_diff(base?, head?, path?)" and the
file tree block showing "src/ git/ diff.ts ... operations/ graft-diff.ts". Edit
the markdown to replace the opening triple backticks with ```text for both
blocks (the signature block around "graft_diff(...)" and the code block spanning
the file tree shown around lines ~101-107) so markdownlint MD040 warnings are
resolved.

In `@src/git/diff.ts`:
- Around line 14-30: The getChangedFiles function builds a shell string using
opts.base and opts.head which allows command injection; change it to call
execFileSync (or execFile) with an argument array instead of interpolating into
a single command string: construct a args array like ["diff","--name-only",
base, head?] using the values from ChangedFilesOptions and pass that array to
execFileSync with cwd and encoding, handle empty output and errors the same way;
ensure you no longer call execSync with a backticked command string and remove
any direct string interpolation of user-provided base/head.
- Around line 27-29: The catch that unconditionally returns [] in
src/git/diff.ts silently hides real failures; replace it with an explicit
error-handling path: change the bare catch to catch (err) { /*...*/ } and either
log the diagnostic and rethrow the error or return a discriminated result type
such as DiffResult = { ok: true; files: string[] } | { ok: false; error: Error }
so callers can distinguish "no changes" from an error; update the function in
this module (the try/catch that currently returns []) to emit the error
(logger.error) and/or return the discriminated error variant instead of
returning [].
- Around line 36-50: The getFileAtRef function is vulnerable to command
injection because it interpolates ref and filePath into a shell string passed to
execSync; replace the shell invocation with a non-shell execFileSync call by
invoking git as the executable and passing ['show', `${ref}:${filePath}`] as
args (keep cwd, encoding, stdio options), and ensure you import/use execFileSync
instead of execSync in getFileAtRef to eliminate shell parsing of user input.

In `@src/operations/graft-diff.ts`:
- Around line 83-91: The current status determination treats baseContent ===
null && headContent === null as "modified"; add a defensive check before the
existing if/else that detects if both baseContent and headContent are null and
handle it explicitly (e.g., throw a descriptive Error or skip the file) so we
don't mis-classify files — reference the variables status, baseContent,
headContent and the call site getFileAtRef/getChangedFiles in graft-diff.ts when
crafting the error message or skip logic.
- Around line 61-64: The loop over changedFiles redundantly checks file
extensions after calling detectLang; remove the SUPPORTED_EXTENSIONS.has(ext)
check and rely on detectLang(filePath) returning null for unsupported files
(i.e., keep the detectLang result check and the ext variable only if needed
elsewhere), so update the for (const filePath of changedFiles) block to skip
files when detectLang returns null and eliminate the
SUPPORTED_EXTENSIONS.has(ext) branch (referencing detectLang, changedFiles,
SUPPORTED_EXTENSIONS, and ext to locate the code).

In `@test/unit/operations/graft-diff.test.ts`:
- Around line 8-10: The duplicate git helper function (git(cwd: string, cmd:
string)) is defined in multiple test files; extract it into a single shared test
helper module (e.g., create a new test/helpers/git.ts exporting function git)
and replace the inline definitions in the test suites with imports from that
helper. Update the tests that use git (search for the git function in test
files) to import the shared helper and remove their local duplicate definitions
to avoid repetition.

---

Outside diff comments:
In `@test/integration/mcp/server.test.ts`:
- Around line 26-38: The test "lists all 8 tools" is inconsistent: the
description says 8, the code asserts 10 and only checks 8 specific names. Update
the test in the it block that calls client.listTools() so the description, the
explicit expects (names checks), and the length assertion all match the real
number of tools; either change the description to "lists all 10 tools" and add
expectations for the two missing tool names (or remove/change the length
assertion to 8), or adjust the length to 8 to match the listed names—modify the
it("lists all 8 tools") block, the names array assertions, and the
expect(names).toHaveLength(...) accordingly.

In `@test/unit/mcp/tools.test.ts`:
- Around line 5-17: The test creates a server via createGraftServer(), collects
registered tools into toolNames, and asserts total length 10 but only checks 8
specific names; add explicit expects for the two missing tools by asserting
expect(toolNames).toContain("graft_diff") and
expect(toolNames).toContain("changed_since") so the test verifies all registered
tool names rather than relying solely on the length check.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d1368c2f-40a3-400f-a16c-df70f41171f0

📥 Commits

Reviewing files that changed from the base of the PR and between 2e40b2c and 1bed38e.

📒 Files selected for processing (10)
  • docs/design/0006-graft-diff/design.md
  • docs/method/backlog/asap/CORE_graft-diff.md
  • docs/method/retro/0006-graft-diff/retro.md
  • src/git/diff.ts
  • src/mcp/server.ts
  • src/operations/graft-diff.ts
  • test/integration/mcp/server.test.ts
  • test/unit/git/diff.test.ts
  • test/unit/mcp/tools.test.ts
  • test/unit/operations/graft-diff.test.ts
💤 Files with no reviewable changes (1)
  • docs/method/backlog/asap/CORE_graft-diff.md

Comment thread docs/design/0006-graft-diff/design.md Outdated
Comment thread src/git/diff.ts
Comment thread src/git/diff.ts Outdated
Comment thread src/git/diff.ts
Comment thread src/operations/graft-diff.ts Outdated
Comment thread src/operations/graft-diff.ts
Comment on lines +8 to +10
function git(cwd: string, cmd: string): string {
return execSync(`git ${cmd}`, { cwd, encoding: "utf-8" }).trim();
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 3, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Duplicate git helper function across test files.

This exact helper appears in both test/unit/git/diff.test.ts and here. For test code this is tolerable, but if you add more git-related test suites, consider extracting to test/helpers/git.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/operations/graft-diff.test.ts` around lines 8 - 10, The duplicate
git helper function (git(cwd: string, cmd: string)) is defined in multiple test
files; extract it into a single shared test helper module (e.g., create a new
test/helpers/git.ts exporting function git) and replace the inline definitions
in the test suites with imports from that helper. Update the tests that use git
(search for the git function in test files) to import the shared helper and
remove their local duplicate definitions to avoid repetition.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Acknowledged. Tracked in backlog: docs/method/backlog/bad-code/CORE_duplicate-test-fixtures.md with done criteria added in f727133.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@flyingrobots, understood — the backlog item and done criteria are a clean way to track this. The duplicate createTestRepo fixture setup is already well-documented for extraction into test/helpers/git.ts when the time comes. Carrying on! 🫡


🧠 Learnings used
Learnt from: CR
Repo: flyingrobots/graft PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T13:51:07.896Z
Learning: Applies to **/*.test.{js,ts,tsx} : Use vitest for test execution

- Replaced execSync with execFileSync in git wrappers to prevent
  shell injection via ref/path arguments
- Removed redundant SUPPORTED_EXTENSIONS set (detectLang already
  gates language support)
- Fixed CHANGELOG test count (209 → 225)
Bad code (new):
- server.ts god file (~420 lines, needs extraction)
- Language detection missing in MCP server diff/changed_since paths
- Duplicate git test fixture setup
- VISION.md stale (cycle/test/tool counts wrong)

Cool ideas (new):
- Diff summary line per file (structural --stat)
- MCP tool descriptions for agent UX
- Receipt compression ratio field

Promoted to ASAP:
- run_capture stub (must resolve before 0.1.0)
- STYLE.md: how the standard applies to graft
- CLEAN_CODE legend: structural quality migration domain
- Logged debt: shape soup (plain objects → domain classes),
  hexagonal violations (Node APIs in core)
- Existing code predates the standard; new code follows it,
  migration is incremental
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: 2

♻️ Duplicate comments (2)
src/operations/graft-diff.ts (1)

72-87: ⚠️ Potential issue | 🟠 Major

I/O failures are being silently converted into diff statuses.

Line 72-Line 76 treats every read failure as null, and Line 85-Line 87 then classifies both-null as "modified". That masks filesystem errors and can emit incorrect statuses.

Suggested fix
     } else {
       // Working tree
       const fullPath = path.join(cwd, filePath);
       try {
         headContent = fs.readFileSync(fullPath, "utf-8");
-      } catch {
-        headContent = null;
+      } catch (error) {
+        const err = error as NodeJS.ErrnoException;
+        if (err.code === "ENOENT") {
+          headContent = null;
+        } else {
+          throw new Error(`WORKTREE_READ_FAILED: ${String(error)}`);
+        }
       }
     }

     // Determine status
     let status: "modified" | "added" | "deleted";
     if (baseContent === null && headContent !== null) {
       status = "added";
     } else if (baseContent !== null && headContent === null) {
       status = "deleted";
+    } else if (baseContent === null && headContent === null) {
+      throw new Error("GRAFT_DIFF_BOTH_SIDES_MISSING");
     } else {
       status = "modified";
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/operations/graft-diff.ts` around lines 72 - 87, The code currently treats
any fs.readFileSync failure as null and later marks both-null as "modified",
masking I/O errors; update the read logic around headContent and baseContent in
graft-diff.ts so the catch inspects the thrown error (e.g., err.code) and only
treat ENOENT (file not found) as null, but rethrow or surface any other errors
instead of swallowing them; then compute status ("added" | "deleted" |
"modified") using those explicit nulls—this prevents non-ENOENT I/O failures
from being misclassified and avoids the incorrect both-null => "modified" case.
src/git/diff.ts (1)

27-29: ⚠️ Potential issue | 🟠 Major

Returning [] on failure hides real git errors.

Line 27-Line 29 turns invalid refs/repo failures into “no changed files”, so callers cannot distinguish failure from a true empty diff.

Suggested fix
-export function getChangedFiles(opts: ChangedFilesOptions): string[] {
+export function getChangedFiles(opts: ChangedFilesOptions): string[] {
   const base = opts.base ?? "HEAD";
   const args = opts.head !== undefined
     ? ["diff", "--name-only", base, opts.head]
     : ["diff", "--name-only", base];

   try {
     const output = execFileSync("git", args, {
       cwd: opts.cwd,
       encoding: "utf-8",
     }).trim();
     if (output === "") return [];
     return output.split("\n");
-  } catch {
-    return [];
+  } catch (error) {
+    throw new Error(`GIT_DIFF_FAILED: ${String(error)}`);
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/git/diff.ts` around lines 27 - 29, The catch block in src/git/diff.ts
that currently swallows errors and returns [] should stop hiding real git
failures; replace the empty catch (which returns []) with an error-propagating
implementation (e.g., catch (err) { throw new Error(`git diff failed:
${String(err)}`); } or rethrow the original err) so callers can distinguish
genuine empty diffs from git/ref errors — update the catch surrounding the
git-diff invocation in this file to include the error object and rethrow or wrap
it with contextual text.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/method/backlog/bad-code/CORE_duplicate-test-fixtures.md`:
- Around line 7-10: The backlog item needs explicit completion criteria: add a
shared createTestRepo() factory in test/helpers/git.ts with a clear API (e.g.,
async createTestRepo(options) returning {repoPath, cleanup, initCommit}), update
both test suites (test/unit/git/ and test/unit/operations/) to import and use
that helper, remove duplicated fixture code, and assert no behavior drift by
running the full test suite and ensuring all tests pass; also add a short README
or comment describing the helper API and include a small migration checklist
(helper API shape, both suites migrated, no behavior drift, CI green) to the
backlog entry so ownership and completion are unambiguous.

In `@src/git/diff.ts`:
- Around line 41-49: The try/catch in getFileAtRef currently returns null for
both "file missing at ref" and any git execution error, collapsing two distinct
cases; update getFileAtRef to expose a tri-state result (e.g., return a
discriminated object or throw on execution errors) rather than string | null so
callers can distinguish "file absent at ref" from "git failed". Specifically,
modify getFileAtRef to: 1) detect the git exit/STDERR that indicates a missing
path at the ref and return an explicit missing marker (e.g., { found: false }),
2) propagate or return an error variant for real git failures (or rethrow) so
callers like the code in graft-diff.ts can infer added/deleted/modified
correctly, and 3) update callers to handle the new discriminated shape (or catch
thrown errors) instead of treating null as both cases.

---

Duplicate comments:
In `@src/git/diff.ts`:
- Around line 27-29: The catch block in src/git/diff.ts that currently swallows
errors and returns [] should stop hiding real git failures; replace the empty
catch (which returns []) with an error-propagating implementation (e.g., catch
(err) { throw new Error(`git diff failed: ${String(err)}`); } or rethrow the
original err) so callers can distinguish genuine empty diffs from git/ref errors
— update the catch surrounding the git-diff invocation in this file to include
the error object and rethrow or wrap it with contextual text.

In `@src/operations/graft-diff.ts`:
- Around line 72-87: The code currently treats any fs.readFileSync failure as
null and later marks both-null as "modified", masking I/O errors; update the
read logic around headContent and baseContent in graft-diff.ts so the catch
inspects the thrown error (e.g., err.code) and only treat ENOENT (file not
found) as null, but rethrow or surface any other errors instead of swallowing
them; then compute status ("added" | "deleted" | "modified") using those
explicit nulls—this prevents non-ENOENT I/O failures from being misclassified
and avoids the incorrect both-null => "modified" case.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6971d4fe-71ef-4bf3-b0c4-e0dc759a8c82

📥 Commits

Reviewing files that changed from the base of the PR and between 1bed38e and 4c5bd9f.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • docs/method/backlog/asap/CORE_run-capture-stub.md
  • docs/method/backlog/bad-code/CORE_duplicate-test-fixtures.md
  • docs/method/backlog/bad-code/CORE_lang-detection-missing-in-server.md
  • docs/method/backlog/bad-code/CORE_server-ts-god-file.md
  • docs/method/backlog/bad-code/CORE_stale-vision-doc.md
  • docs/method/backlog/cool-ideas/CORE_diff-summary-line.md
  • docs/method/backlog/cool-ideas/CORE_mcp-tool-descriptions.md
  • docs/method/backlog/cool-ideas/CORE_receipt-compression-ratio.md
  • src/git/diff.ts
  • src/operations/graft-diff.ts

Comment thread docs/method/backlog/bad-code/CORE_duplicate-test-fixtures.md
Comment thread src/git/diff.ts
- getFileAtRef: returns null for clean file absence, throws GitError
  for invalid refs or git failures (tri-state contract)
- getChangedFiles: throws GitError for invalid refs instead of
  silently returning empty array
- graft-diff: handles both-null edge case (skip), catches GitError
  from getFileAtRef gracefully
- Design doc: added language tags to code fences, fixed execSync
  reference to execFileSync
- Backlog: added done criteria to duplicate-test-fixtures item
- 2 new tests for GitError on invalid refs
@flyingrobots
Copy link
Copy Markdown
Owner Author

All 12 review issues addressed

# Issue Severity Fix SHA Status
1 Command injection in getChangedFiles P1/Critical 86dc650 ✅ execFileSync
2 Command injection in getFileAtRef P2/Critical 86dc650 ✅ execFileSync
3 Shell quoting for blob paths P2 86dc650 ✅ array args, no shell
4 Silent error swallowing Minor f727133 ✅ throws GitError
5 Tri-state contract for getFileAtRef Critical f727133 ✅ null vs GitError
6 Both-null edge case Minor f727133 ✅ skip, don't mark modified
7 Code block languages (MD040) Minor f727133 ✅ added text tags
8 Redundant SUPPORTED_EXTENSIONS Nit 86dc650 ✅ removed
9 Duplicate test helpers Nit tracked 📋 backlog with done criteria
10 Backlog item phrasing Nit f727133 ✅ added done criteria
11 Design doc references execSync Nit f727133 ✅ corrected to execFileSync
12 Surface git diff failures P2 f727133 ✅ throws GitError

227 tests passing, lint + typecheck clean.

@coderabbitai review 🥕

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 3, 2026

@flyingrobots Immaculate fix table — every P1/P2 Critical issue patched with surgical precision. The execFileSync migration eliminating shell injection, the GitError tri-state contract separating "file doesn't exist at ref" from "git exploded," and the both-null edge case guard are exactly the right calls. Backlog tracking for the duplicate test helpers is the correct call for a nit that doesn't belong in this diff.

227 passing, lint clean, typecheck clean — kicking off the re-review now. 🥕

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 7

♻️ Duplicate comments (2)
src/git/diff.ts (1)

25-29: ⚠️ Potential issue | 🟠 Major

Empty base and prose-based error classification both violate contract and guidelines.

opts.base === "" produces invalid git diff --name-only "" (exit 128: "ambiguous argument"), breaking index mode. Additionally, getFileAtRef (lines 61–65) classifies errors by substring-matching English prose instead of machine-stable signals, violating the coding guideline requiring enum reason codes. Both require fixes.

Fix 1: Empty-base handling

When opts.base === "" and opts.head === undefined, emit ["diff", "--name-only"] (index vs working tree), not ["diff", "--name-only", ""].

Fix 2: Prose-coupled error classification

Replace msg.includes("does not exist") || msg.includes("exists on disk, but not in") with machine-stable error classification (e.g., parse git exit code, use git rev-parse for validation, or define an enum for error types). Locale-dependent prose is brittle and violates guidelines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/git/diff.ts` around lines 25 - 29, Handle empty base by treating
opts.base === "" as unspecified when opts.head is undefined: in the args
construction for the diff call (where base is set from opts.base ?? "HEAD" and
args is built with ["diff","--name-only", base, opts.head] or
["diff","--name-only", base]) ensure the branch that would append an empty
string instead emits ["diff","--name-only"] (i.e., omit base entirely) when base
=== "" and opts.head === undefined. For getFileAtRef, remove brittle substring
checks of error messages and introduce a machine-stable classification: call git
rev-parse (or inspect git exit codes) to validate refs before diffing and/or
create an enum (e.g., RefErrorReason) and map git command exit codes or
rev-parse results into those enum reasons instead of using msg.includes(...);
update the error handling paths in getFileAtRef to use that enum for downstream
logic.
src/operations/graft-diff.ts (1)

82-86: ⚠️ Potential issue | 🟠 Major

Fix bare error suppression that corrupts diff semantics by mislabeling I/O failures as deletions.

At lines 84–86, the catch block swallows every filesystem error and coerces it to null. This is indefensible given that 10 lines above, the same function correctly discriminates GitError from other exceptions.

When headContent becomes null due to permission denial, disk I/O error, or other non-ENOENT conditions, line 97 incorrectly marks the file as "deleted". This silently corrupts the diff semantics and violates the error handling discipline already established in this codebase.

The suggested fix is correct: discriminate ENOENT and ENOTDIR (genuine absence) from all other errors, and re-throw permission/I/O failures instead of hiding them.

Suggested fix
     } else {
       const fullPath = path.join(cwd, filePath);
       try {
         headContent = fs.readFileSync(fullPath, "utf-8");
-      } catch {
-        headContent = null;
+      } catch (err: unknown) {
+        if (
+          err &&
+          typeof err === "object" &&
+          "code" in err &&
+          ((err as { code?: string }).code === "ENOENT" ||
+            (err as { code?: string }).code === "ENOTDIR")
+        ) {
+          headContent = null;
+        } else {
+          throw err;
+        }
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/operations/graft-diff.ts` around lines 82 - 86, The catch around
fs.readFileSync that sets headContent = null is swallowing all filesystem errors
and causing non-existent-file logic to mislabel I/O/permission failures as
deletions; update the try/catch in the code that reads headContent
(fs.readFileSync(fullPath, "utf-8")) to check the caught error.code and only
treat ENOENT and ENOTDIR as "missing" (set headContent = null), but re-throw any
other errors (e.g., EACCES, EIO) so they surface instead of being converted to a
deletion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/design/0006-graft-diff/design.md`:
- Around line 58-59: Update the extension→language mapping in the design doc
where it currently shows "Detect language from extension (`.ts` → `\"ts\"`,
`.js` → `\"js\"`)" to include JSX/TSX (add `.tsx` → `\"tsx\"`, `.jsx` →
`\"jsx\"`) and update any related mentions (e.g., the step referencing
`extractOutline`) so the supported extensions list and parsing steps
consistently reflect `.ts/.tsx/.js/.jsx` support; ensure the same changes are
applied to the other occurrence noted (around the other mention of the mapping).

In `@docs/method/backlog/bad-code/CORE_shape-soup.md`:
- Around line 28-29: The sentence fragment "**MCP tool args** — cast from
`Record<string, unknown>` with `as`." should be rewritten to include a subject
and form a complete sentence; update it to something like "MCP tool args are
currently cast from `Record<string, unknown>` using `as`, but should be parsed
through zod into domain types." Locate this text in CORE_shape-soup.md (the
phrase "**MCP tool args** — cast from `Record<string, unknown>` with `as`.") and
replace it with the complete sentence that includes the subject and the
suggested wording.

In `@docs/method/backlog/up-next/CC_json-codec-port.md`:
- Around line 24-26: The docs currently leave codec selection implicit around
codec.encode(value), which risks sending CBOR where MCP/NDJSON expect JSON;
explicitly define a codec-selection contract: add/require a selector parameter
or API (e.g., selectCodec(context|surface)) and document the canonical mapping
(MCP and NDJSON surfaces => JSON; storage surfaces => CBOR), update all callers
of codec.encode(value) and codec.decode(...) to pass the selected codec and
validate the I/O type, and add runtime checks in encode/decode to reject
mismatched formats; reference and update the codec.encode/codec.decode usage and
surfaces named MCP, NDJSON, and storage in the docs so callers know who must
choose the codec.
- Around line 16-18: Update the documentation to choose a single canonical JSON
algorithm for CanonicalJsonCodec (used for MCP responses, NDJSON logs,
receipts): replace the ambiguous "RFC 8785 or sorted-keys + no whitespace"
wording with a single statement that the codec uses RFC 8785 exclusively, remove
the alternative ad-hoc approach throughout the doc (including the paragraphs
that currently allow either method), and explicitly state that RFC 8785 is
required to guarantee deterministic byte-for-byte outputs across producers.
- Around line 37-38: Update the backlog doc sentence that estimates migration
scope from “~20 raw JSON.stringify calls” to the accurate current count by
inspecting and referencing the actual JSON.stringify usage sites; replace the
"~20" estimate with "3" (the three current usages in the mcp server, metrics
logger, and operations safe-read modules) and keep mention of CanonicalJsonCodec
and CborCodec as planned follow-ups so the entry accurately reflects current
scope.

In `@src/git/diff.ts`:
- Around line 61-65: The catch block in src/git/diff.ts that inspects
err.message after the git "show" call is brittle; replace that approach by
probing the ref and object existence explicitly: before calling git show, run
`git rev-parse --verify ${ref}` and if it exits non-zero treat the ref as
invalid/absent and return null; if rev-parse succeeds then run `git cat-file -e
${ref}:${filePath}` and if that exits non-zero treat the file-as-of-ref as
missing and return null; only call the existing git show logic when both probes
succeed, and otherwise rethrow unexpected errors — update the error-handling
around the git show call (the catch block in src/git/diff.ts) to use these
probes instead of matching human-readable messages.

In `@src/operations/graft-diff.ts`:
- Around line 64-69: The try/catch blocks in graft-diff that catch GitError and
coerce it to null must be removed: directly call getFileAtRef(...) to assign
baseContent and headContent (do not catch GitError), so invalid refs/ repo
errors propagate per getFileAtRef's tri-state contract; specifically remove the
try/catch around the getFileAtRef call that sets baseContent and the analogous
one that sets headContent (references: getFileAtRef, GitError, baseContent,
headContent, filePath, cwd).

---

Duplicate comments:
In `@src/git/diff.ts`:
- Around line 25-29: Handle empty base by treating opts.base === "" as
unspecified when opts.head is undefined: in the args construction for the diff
call (where base is set from opts.base ?? "HEAD" and args is built with
["diff","--name-only", base, opts.head] or ["diff","--name-only", base]) ensure
the branch that would append an empty string instead emits
["diff","--name-only"] (i.e., omit base entirely) when base === "" and opts.head
=== undefined. For getFileAtRef, remove brittle substring checks of error
messages and introduce a machine-stable classification: call git rev-parse (or
inspect git exit codes) to validate refs before diffing and/or create an enum
(e.g., RefErrorReason) and map git command exit codes or rev-parse results into
those enum reasons instead of using msg.includes(...); update the error handling
paths in getFileAtRef to use that enum for downstream logic.

In `@src/operations/graft-diff.ts`:
- Around line 82-86: The catch around fs.readFileSync that sets headContent =
null is swallowing all filesystem errors and causing non-existent-file logic to
mislabel I/O/permission failures as deletions; update the try/catch in the code
that reads headContent (fs.readFileSync(fullPath, "utf-8")) to check the caught
error.code and only treat ENOENT and ENOTDIR as "missing" (set headContent =
null), but re-throw any other errors (e.g., EACCES, EIO) so they surface instead
of being converted to a deletion.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9e319376-1cb0-42f0-9ccb-f85cb7e18146

📥 Commits

Reviewing files that changed from the base of the PR and between 4c5bd9f and f727133.

📒 Files selected for processing (10)
  • STYLE.md
  • docs/design/0006-graft-diff/design.md
  • docs/method/backlog/bad-code/CORE_duplicate-test-fixtures.md
  • docs/method/backlog/bad-code/CORE_hexagonal-violation.md
  • docs/method/backlog/bad-code/CORE_shape-soup.md
  • docs/method/backlog/up-next/CC_json-codec-port.md
  • docs/method/legends/CLEAN_CODE.md
  • src/git/diff.ts
  • src/operations/graft-diff.ts
  • test/unit/git/diff.test.ts

Comment thread docs/design/0006-graft-diff/design.md Outdated
Comment thread docs/method/backlog/bad-code/CORE_shape-soup.md Outdated
Comment thread docs/method/backlog/up-next/CC_json-codec-port.md Outdated
Comment thread docs/method/backlog/up-next/CC_json-codec-port.md Outdated
Comment thread docs/method/backlog/up-next/CC_json-codec-port.md Outdated
Comment thread src/git/diff.ts Outdated
Comment thread src/operations/graft-diff.ts Outdated
- getFileAtRef uses git rev-parse --verify + git cat-file -e for
  ref/object existence checks. No error message parsing.
- Removed redundant GitError catch blocks in graft-diff.ts —
  getFileAtRef's contract is now clean (null = absent, throw = error)
- Design doc: updated language support to include .tsx/.jsx
- Backlog fixes: sentence fragment, canonical JSON algorithm choice,
  codec selection rule, corrected migration scope
@flyingrobots
Copy link
Copy Markdown
Owner Author

Round 2: All 7 new review issues addressed

# Issue Severity Fix SHA Status
13 Design doc language support (.tsx/.jsx missing) Minor 31d4bce
14 Sentence fragment in backlog Minor 31d4bce
15 JSON codec: pick one algorithm Major 31d4bce ✅ sorted keys + no WS
16 JSON codec: explicit selection rule Major 31d4bce ✅ surface owns codec
17 JSON codec: wrong migration scope Major 31d4bce ✅ corrected to 3 sites
18 Brittle error message parsing Major 31d4bce ✅ git rev-parse + cat-file
19 Redundant GitError coercion Major 31d4bce ✅ removed try/catch

Cumulative: 19 issues across 2 rounds, 19 resolved. 227 tests, lint + typecheck clean.

@coderabbitai review 🥕

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 3, 2026

Rate Limit Exceeded

@flyingrobots have exceeded the limit for the number of chat messages per hour. Please wait 0 minutes and 56 seconds before sending another message.

Promoted to ASAP (blocks 0.1.0):
- lang detection bug (.js parsed as TS in server diff paths)
- stale VISION.md (public-facing doc has wrong numbers)
- MCP tool descriptions (empty schemas = bad agent UX)

ASAP now has 5 pre-release items + 1 post-release (WARP).
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