feat(diff): implement LCS-based line diff for prompt versions (closes #7)#9
Conversation
|
Please address this PR . |
Hi, thanks for your contribution and for opening this PR. I appreciate your effort! Apologies for the delay in getting back to you. I’ll review the changes shortly and share feedback if anything needs adjustment. |
Ali7040
left a comment
There was a problem hiding this comment.
Thanks for the contribution — the formatting cleanup and comment corrections are appreciated. However there are two correctness bugs and a missing acceptance criterion that need to be addressed before this can merge.
Must fix
1. equal lines get the wrong lineNumber (see inline comment on line 93)
Deleted lines report the old-file line number; inserted lines report the new-file line number. Equal lines now hardcode ai + 1 (old-file), but after any insertion above that point the new-file position is bi + 1, which is different. The frontend renderer will display equal lines at the wrong position in the right-hand column.
2. Empty-string guard silently changes diff semantics (see inline comment on line 58)
a ? a.split('\n') : [] treats '' as falsy and returns [], but ''.split('\n') returns [''] (one empty line). Diffing an empty version against a non-empty one now produces different removed counts than before.
3. Acceptance criterion not met — no HTTP endpoint
Issue #7 requires:
GET /prompts/:id/diff?from=v1.0.0&to=v1.0.2
This PR only modifies DiffService. There is no controller method wired up to call diffVersions(), so the feature is unreachable from the HTTP layer.
Should fix
4. Missing tests
For a diff algorithm, unit tests are essential. At minimum please cover:
- identical content → all
equal - completely replaced content → all
delete+insert - empty
from→ allinsert - empty
to→ alldelete - single-line content (no newline)
statstotals matchlines.length
5. Missing trailing newline
The diff ends with No newline at end of file. Every source file in this repo ends with a newline — please add one.
What is good
- Correcting the comment from "Myers diff" to "LCS-based" is accurate — they are different algorithms.
- Inline section comments (
// deletions (from old),// equal line, etc.) improve readability. - Expanding
aLines[i++]toaLines[i]; i++is clearer and removes the subtle evaluation-order dependency.
| private computeDiff(a: string, b: string): DiffLine[] { | ||
| const aLines = a.split('\n'); | ||
| const bLines = b.split('\n'); | ||
| const aLines = a ? a.split('\n') : []; |
There was a problem hiding this comment.
Bug: empty-string guard changes diff semantics.
'' is falsy in JavaScript, so a prompt version with empty content now produces [] instead of ['']. This silently changes the diff result:
| scenario | old behaviour | PR behaviour |
|---|---|---|
a = '', b = 'hello' |
removed: 1, added: 1 |
removed: 0, added: 1 |
The guard is also unnecessary — content is a non-nullable String Prisma column and will never be null at runtime.
Fix: remove the guard, or use an explicit length check:
const aLines = a.length > 0 ? a.split('\n') : [];
const bLines = b.length > 0 ? b.split('\n') : [];
Issue #7 Requirements — Gap AnalysisMapping every requirement from the issue against what this PR delivers. Requirement 1 —
|
|
Thanks for the detailed review, this is really helpful . I’m currently working on addressing the issues:
Will push an updated commit shortly. |
…dd endpoint & tests)
|
Addressed all review feedback:
Ready for another review , happy to make further adjustments if needed. |
Ali7040
left a comment
There was a problem hiding this comment.
Really solid work here — the LCS implementation is correct, the toLineNumber field is a nice addition that makes side-by-side rendering easier on the frontend, and adding tests from the start is exactly the right instinct. The algorithm itself is production-ready.
Two quick blockers before this can land, and one subtle test gap to tighten up. All small fixes — nothing structural needs to change. 🙌
| class DiffQueryDto { | ||
| from: string; | ||
| to: string; | ||
| } |
There was a problem hiding this comment.
Blocker — DTO validation will break every request.
The global ValidationPipe in main.ts is configured with whitelist: true + forbidNonWhitelisted: true. Without class-validator decorators, from and to are treated as non-whitelisted and get stripped — so query.from and query.to arrive as undefined, and Prisma throws a 500 on every call.
Quick fix:
import { IsString, IsNotEmpty } from 'class-validator';
class DiffQueryDto {
@IsString()
@IsNotEmpty()
from: string;
@IsString()
@IsNotEmpty()
to: string;
}This will also give callers a proper 400 Bad Request with a clear message when from or to are missing, which is much friendlier than a 500.
|
|
||
| it('empty from → insert', () => { | ||
| const res = service['computeDiff']('', 'a\nb'); | ||
| expect(res.filter(l => l.type === 'insert').length).toBe(2); |
There was a problem hiding this comment.
Subtle edge case — test assertion is too loose.
''.split(' ') returns [''] (a 1-element array with an empty string), not []. So computeDiff('', 'a b') currently produces 3 lines — a spurious delete of the empty string, plus the 2 inserts. The test passes here because it only counts inserts, but the output is wrong.
Tighten the assertion:
it('empty from → all inserts', () => {
const res = service['computeDiff']('', 'a
b');
expect(res.length).toBe(2); // not 3
expect(res.every(l => l.type === 'insert')).toBe(true);
});And add guards at the top of computeDiff to handle the empty-string case cleanly:
if (!a) return b.split('
').filter(Boolean).map((c, i) => ({
type: 'insert' as const, content: c, lineNumber: i + 1, toLineNumber: i + 1
}));
if (!b) return a.split('
').filter(Boolean).map((c, i) => ({
type: 'delete' as const, content: c, lineNumber: i + 1
}));|
Updated pnpm-lock.yaml to match package.json — CI should pass now. |
Resolve the conflicts |
|
Addressed final review points:
Should be ready for merge, happy to refine further if needed. |
|
Fixed broken pnpm-lock.yaml by regenerating it with pnpm install. CI should pass now. |
Ali7040
left a comment
There was a problem hiding this comment.
Well done! Really appreciate the effort you put into this.
|
@anasahhm could you please take a look at why the CI tests are failing? Everything else looks great. once this is resolved, we should be good to go. |
|
The CI failure was due to incorrect newline handling in the test file, which caused a parsing error during Jest/Babel execution (the multiline string wasn’t properly escaped). I’ve fixed this by using proper \n formatting and tightening the test assertions for empty-string edge cases. Also aligned the implementation with the tests to ensure consistent behavior for: empty from → all inserts All tests are now deterministic and should pass in CI. |
|
The CI failure was due to an invalid multiline string in the test file ('a\nb' was accidentally written as a raw line break), which caused Babel to fail parsing before tests could run. Fixed by correcting newline usage and tightening assertions for empty-string edge cases. All tests should now execute and pass correctly in CI. |
Well done! Great catch on the issue. |
|
CI fix needed — Jest isn't configured to use The test suite is failing with a Babel parse error on Add this file to module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: 'src',
testRegex: '.*\.spec\.ts$',
transform: {
'^.+\.(t|j)s$': 'ts-jest',
},
collectCoverageFrom: ['**/*.(t|j)s'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
};( |
|
Good catch - thanks for pointing that out. The failure was due to Jest falling back to Babel without a config, which doesn't support TypeScript syntax like CI should now run the test suite correctly. |
Closes #7 by implementing an LCS-based line-level diff for prompt version comparison.
What’s included
insert,delete,equal)lineNumberhandlingNotes