Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 237 additions & 0 deletions plans/005-pass-comments-into-unified-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# Plan 005: Pass reviewer comments into unified AI review export

> **Executor instructions**: Follow this plan step by step. Run every
> verification command and confirm the expected result before moving to the
> next step. If anything in the "STOP conditions" section occurs, stop and
> report β€” do not improvise. When done, update the status row for this plan
> in `plans/README.md` β€” unless a reviewer dispatched you and told you they
> maintain the index.
>
> **Drift check (run first)**: `git diff --stat 37ac800..HEAD -- services/ReviewGenerator.js server.js test/server.test.js`
> If any in-scope file changed since this plan was written, compare the
> "Current state" excerpts against the live code before proceeding; on a
> mismatch, treat it as a STOP condition.

## Status

- **Priority**: P1
- **Effort**: M
- **Risk**: MED
- **Depends on**: none
- **Category**: bug
- **Planned at**: commit `37ac800`, 2026-08-29

## Why this matters

The visual review UI collects file comments and line comments, then `POST /api/export-for-ai` validates them and reports `commentsIncluded` in the JSON response. `ReviewGenerator.generateUnifiedReview` never accepts those fields, so `AI_REVIEW.md` is diffs-only. Split export (`generateSplitReviews` β†’ `generateFileContent`) already embeds comments. Unified export is the default button in `public/index.html` (`exportForAI`). Reviewer notes are the product; they currently stop at the HTTP seam.

## Current state

- `services/ReviewGenerator.js` β€” `generateUnifiedReview` writes `AI_REVIEW.md`. Signature at the time of this plan:

```100:110:services/ReviewGenerator.js
static async generateUnifiedReview({ includedFiles, excludedFiles = [], largeFiles = [] }) {
const timestamp = new Date().toLocaleString();
let content = `# πŸ” Code Review - ${timestamp}\n\n`;
content += '**Project:** AI Visual Code Review\n';
content += '**Generated by:** AI Visual Code Review v2.0\n\n';
```

- The per-file loop builds `fileContent` from status + diff only (starts ~line 136). It never reads `comments` or `lineComments`.
- Split path already embeds comments. Copy that shape, do not invent a new markdown dialect. Excerpt:

```254:269:services/ReviewGenerator.js
// File Comment
if (fileComment) {
content += `## πŸ’­ Review Comment\n\n${fileComment}\n\n`;
}

// Line Comments
const fileLineComments = Object.entries(lineComments || {})
.filter(([lineId]) => lineId.includes(file.replace(/[^a-zA-Z0-9]/g, '_')));

if (fileLineComments.length > 0) {
content += '## πŸ” Line Comments\n\n';
fileLineComments.forEach(([lineId, comment]) => {
content += `- **${lineId}:** ${comment}\n`;
});
content += '\n';
}
```

- `server.js` destructures comments then drops them:

```556:624:server.js
const { comments = {}, lineComments = {}, excludedFiles = [] } = req.body;
// ...
const result = await ReviewGenerator.generateUnifiedReview({
includedFiles,
excludedFiles,
// Pass other options if needed by generateUnifiedReview,
// though currently it handles content generation primarily.
});

res.json({
success: true,
file: 'AI_REVIEW.md',
// ...
commentsIncluded: Object.keys(comments).length,
```

- `scripts/export-ai-review.js` also calls `generateUnifiedReview` with files only. After this plan it can pass `{}` for comments; CLI has no comment UI. That is fine.
- There is **no** `test/reviewGenerator.test.js`. `test/server.test.js` hits export with supertest but only checks status codes, not markdown body.
- `GitService` is a singleton (`module.exports = new GitService()`). Tests must mock that module **before** requiring `ReviewGenerator`.
- Conventions: CommonJS `require`, Jest. Model new tests after `test/diffService.test.js` (describe/test, no TypeScript). Do not add TypeScript.

## Commands you will need

| Purpose | Command | Expected on success |
|---------|---------|---------------------|
| Tests | `npm test` | exit 0 |
| ReviewGenerator tests | `npx jest test/reviewGenerator.test.js --verbose` | exit 0, new cases pass |
| Lint | `npm run lint` | exit 0 (warnings allowed) |
| Prove comments in signature | `rg -n "comments" services/ReviewGenerator.js` | `generateUnifiedReview` destructure includes `comments` |

## Scope

**In scope**:

- `services/ReviewGenerator.js`
- `server.js` (only the `generateUnifiedReview({...})` call in `POST /api/export-for-ai`)
- `test/reviewGenerator.test.js` (create)
- `test/server.test.js` (one assertion that is still HTTP-level and does not require reading `AI_REVIEW.md`)

**Out of scope**:

- `src/` wiring (plan 001)
- `public/index.html` markup (plan 006)
- Git listing refactor (plan 007)
- Cache middleware (plan 008)
- `scripts/export-ai-review.js` beyond remaining compatible with extra optional keys
- Changing the HTTP JSON field names
- Writing a real `AI_REVIEW.md` into the repo during tests

## Git workflow

- Branch: stay on the current working branch if you were dispatched onto one. Otherwise `prax/unified-export-comments-3d82`.
- Commit message example: `fix(export): include comments in unified AI_REVIEW.md`
- Do not push or open a PR unless the operator instructed it.

## Steps

### Step 1: Characterization test that fails on current code

Create `test/reviewGenerator.test.js`. Mock git and disk **before** requiring the module:

```js
jest.mock('../services/GitService', () => ({
getDiffStats: jest.fn().mockResolvedValue('1 file changed'),
getFileStatuses: jest.fn().mockResolvedValue({ 'foo.js': 'M' }),
getDiffForFile: jest.fn().mockResolvedValue('@@ -1,1 +1,1 @@\n-old\n+new\n')
}));

const fs = require('fs');
const ReviewGenerator = require('../services/ReviewGenerator');

describe('generateUnifiedReview comments', () => {
let writeSpy;

beforeEach(() => {
writeSpy = jest.spyOn(fs.promises, 'writeFile').mockResolvedValue();
});

afterEach(() => {
writeSpy.mockRestore();
});

test('embeds file comments in the markdown written to disk', async () => {
await ReviewGenerator.generateUnifiedReview({
includedFiles: ['foo.js'],
excludedFiles: [],
comments: { 'foo.js': 'UNIQUE_REVIEW_NOTE_ABC' },
lineComments: { 'foo_js_1_1': 'LINE_NOTE_XYZ' }
});

expect(writeSpy).toHaveBeenCalled();
const content = writeSpy.mock.calls[0][1];
expect(content).toContain('UNIQUE_REVIEW_NOTE_ABC');
expect(content).toContain('LINE_NOTE_XYZ');
});
});
```

On current `main` this test **must fail** (comments ignored). If it passes before you edit `ReviewGenerator.js`, STOP β€” the bug is already gone or the mock is wrong.

**Verify**: `npx jest test/reviewGenerator.test.js --verbose` β†’ the new test fails because the unique strings are absent.

### Step 2: Extend `generateUnifiedReview` interface

In `services/ReviewGenerator.js`:

1. Destructure `comments = {}` and `lineComments = {}` next to `includedFiles`.
2. Inside the per-file `includedFiles.map` callback, after status/type markdown and **before** the diff block, insert the same comment + line-comment markdown as `generateFileContent` (same headings: `## πŸ’­ Review Comment` and `## πŸ” Line Comments`). Use `comments[file]` and the same `lineId.includes(file.replace(/[^a-zA-Z0-9]/g, '_'))` filter.

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 Match each line comment to its exact file

When staged filenames overlap after normalization, this includes filter assigns comments to the wrong file. For example, the UI builds a foo.js.map line ID such as foo_js_map_0_0, which also includes normalized foo.js (foo_js), so the unified export duplicates that note under both files. Carry or compare the exact owning filename and add an overlapping-filename regression test rather than copying the split-export bug.

Useful? React with πŸ‘Β / πŸ‘Ž.

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 Preserve comments when a file diff fails

When getDiffForFile rejects for a non-deleted file because of a Git timeout, command failure, or index race, the existing inner catch rethrows and the outer per-file catch returns a failure result; the final builder then emits only the generic diff error and discards the accumulated fileContent, including the newly inserted reviewer comments. Split export catches the diff failure in place and preserves those notes. Add a rejection-path test and retain the comment markdown when emitting the per-file error.

Useful? React with πŸ‘Β / πŸ‘Ž.

3. Do not add a second checklist per file. Keep `getChecklistTemplate()` once at the end as today.

**Verify**: `npx jest test/reviewGenerator.test.js --verbose` β†’ the characterization test now passes.

### Step 3: Pass comments from `server.js`

Change only the `generateUnifiedReview` call in `POST /api/export-for-ai` to:

```js
const result = await ReviewGenerator.generateUnifiedReview({
includedFiles,
excludedFiles,
comments,
lineComments
});
```

Leave `POST /api/export-individual-reviews` alone (it already passes comments into `generateSplitReviews`).

**Verify**: `rg -n "generateUnifiedReview" server.js` β†’ the object includes `comments` and `lineComments`.

### Step 4: HTTP smoke in existing server tests

In `test/server.test.js`, in the existing `POST /api/export-for-ai should accept valid request` case, keep status `[200, 400]`. Add a **new** test that invalid comments still 400 (already covered). Add one comment: no new HTTP markdown assertion (file is written to `process.cwd()`). The unit test in step 1 is the regression lock.

**Verify**: `npx jest test/server.test.js --verbose` β†’ all pass.

### Step 5: Full suite

**Verify**: `npm test` β†’ exit 0. Confirm no leftover `AI_REVIEW.md` from tests (`git status` clean of that file). If a test wrote one, delete it and fix the mock.

## Test plan

- New file `test/reviewGenerator.test.js`:
- Happy path: file comment + line comment appear in `writeFile` content.
- Empty comments: still writes markdown, no throw (add this second test).
- Pattern: `test/diffService.test.js` (plain Jest, no supertest).
- HTTP: existing export tests still pass.
- Verification: `npm test` β†’ all pass, including the new file.

## Done criteria

- [ ] `npm test` exits 0
- [ ] `npx jest test/reviewGenerator.test.js` exits 0
- [ ] `rg -n "UNIQUE_REVIEW_NOTE"` is only in the test file, not in `services/`
- [ ] `generateUnifiedReview` destructure includes `comments` and `lineComments`
- [ ] `server.js` passes `comments` and `lineComments` into that call
- [ ] No files outside the in-scope list are modified (`git status`)
- [ ] `plans/README.md` status row for 005 is DONE
Comment on lines +220 to +221

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 Permit mandatory plan-index bookkeeping

For the default executor, these adjacent completion criteria are mutually exclusive: the plan requires changing the plans/README.md status row, but that file is absent from the in-scope list and all out-of-scope modifications are forbidden. Plans 006–009 repeat the same contradiction, so an executor following the strict stop/do-not-improvise instructions cannot declare any of them complete. Add plans/README.md as an explicit bookkeeping exception or in-scope file in each plan.

Useful? React with πŸ‘Β / πŸ‘Ž.


## STOP conditions

- The code at the locations in "Current state" doesn't match the excerpts.
- Step 1's test passes **before** you change `ReviewGenerator.js`.
- Making the test pass appears to require mocking `child_process` instead of `GitService` (wrong mock β€” fix the mock, don't rewrite Git).
- A step's verification fails twice after a reasonable fix attempt.
- The fix appears to require touching `public/index.html` or `src/`.
- You discover `generateUnifiedReview` already takes and renders comments (bug already fixed).

## Maintenance notes

- Any new unified-export field (large-file skips, extra metadata) belongs on this same interface so HTTP and CLI stay aligned.
- Reviewers should confirm comments are interpolated as markdown text, not HTML, matching `generateFileContent`.
- Split export already had comments; do not "simplify" by deleting that path.
- Follow-up deferred: plan 007 (Git listing) and plan 008 (dead cache).
Loading
Loading