git-reviewer 2.3.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![CI](https://github.com/dheerajjha/reviewer/actions/workflows/ci.yml/badge.svg)](https://github.com/dheerajjha/reviewer/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
5
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](package.json)
6
- [![Tests](https://img.shields.io/badge/tests-261-brightgreen.svg)](test/)
6
+ [![Tests](https://img.shields.io/badge/tests-264-brightgreen.svg)](test/)
7
7
  [![Dependencies](https://img.shields.io/badge/dependencies-3-brightgreen.svg)](package.json)
8
8
 
9
9
  When an agent writes the code, reading it becomes the bottleneck — and the tool
@@ -271,7 +271,7 @@ rather than a public issue.
271
271
 
272
272
  ```bash
273
273
  npm install # 3 dependencies, no build step, ~6MB
274
- npm test # 261 tests
274
+ npm test # 264 tests
275
275
  npm run test:watch
276
276
  npm run test:coverage
277
277
  ```
package/lib/comments.js CHANGED
@@ -30,10 +30,31 @@
30
30
  * Optional fields are omitted rather than stored as `null` so that an absent
31
31
  * selection and an empty one are not two different states on disk.
32
32
  *
33
+ * A comment missing its file, line or text is rejected here rather than stored:
34
+ * `JSON.stringify` would silently drop the missing fields, and the record would
35
+ * only surface later, in `reviewer export`, as a comment on a file called
36
+ * `undefined` for an agent to act on.
37
+ *
33
38
  * @param {object} comment
39
+ * @param {number} index position in the batch, for the error message
34
40
  * @returns {Comment}
41
+ * @throws {TypeError} when a required field is missing or malformed
35
42
  */
36
- function normalizeComment(comment) {
43
+ function normalizeComment(comment, index) {
44
+ const where = `comments[${index}]`;
45
+ if (comment === null || typeof comment !== 'object' || Array.isArray(comment)) {
46
+ throw new TypeError(`${where} must be an object`);
47
+ }
48
+ if (typeof comment.file !== 'string' || comment.file.trim() === '') {
49
+ throw new TypeError(`${where}.file must be a non-empty string`);
50
+ }
51
+ if (!Number.isInteger(comment.line) || comment.line < 1) {
52
+ throw new TypeError(`${where}.line must be a positive integer`);
53
+ }
54
+ if (typeof comment.text !== 'string' || comment.text.trim() === '') {
55
+ throw new TypeError(`${where}.text must be a non-empty string`);
56
+ }
57
+
37
58
  const normalized = {
38
59
  file: comment.file,
39
60
  line: comment.line,
@@ -61,13 +82,13 @@ function normalizeComment(comment) {
61
82
  *
62
83
  * @param {object[]} comments
63
84
  * @returns {Comment[]}
64
- * @throws {TypeError} when `comments` is not an array
85
+ * @throws {TypeError} when `comments` is not an array or a comment is malformed
65
86
  */
66
87
  function normalizeComments(comments) {
67
88
  if (!Array.isArray(comments)) {
68
89
  throw new TypeError('comments must be an array');
69
90
  }
70
- return comments.map(normalizeComment);
91
+ return comments.map((comment, index) => normalizeComment(comment, index));
71
92
  }
72
93
 
73
94
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-reviewer",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Review your coding agent's changes like a pull request — locally, with inline comments and threaded follow-ups, then hand the marked-up review back to the agent",
5
5
  "author": "Dheeraj Jha",
6
6
  "license": "MIT",
package/public/app.js CHANGED
@@ -180,8 +180,10 @@ async function loadRepo() {
180
180
  // Load saved comments
181
181
  const savedComments = await loadCommentsFromBackend();
182
182
  if (savedComments.length > 0) {
183
- // Store saved comments in a global variable for matching later
184
- window.savedComments = savedComments;
183
+ // These went into `window.savedComments` as well, as a second copy for
184
+ // the matcher to read. Nothing reads it now, and leaving a stale copy of
185
+ // the review lying around is how the resurrection happened in the first
186
+ // place -- so there is one list, and it is this one.
185
187
 
186
188
  // Add all saved comments to the comments array immediately
187
189
  comments = savedComments.map(c => ({
@@ -308,9 +310,20 @@ async function loadFile(filePath, index) {
308
310
  throw new Error(data.error || 'Failed to load file');
309
311
  }
310
312
 
311
- // Match saved comments to current diff if available
312
- if (window.savedComments && window.savedComments.length > 0) {
313
- const matchedComments = matchCommentsToDiff(window.savedComments, filePath, data.diffLines);
313
+ // Give this file's comments their position in the diff just rendered.
314
+ //
315
+ // The source here used to be `window.savedComments`, a snapshot taken when
316
+ // the repository was opened and never updated again. Everything else works
317
+ // from `comments`, the live list -- so a comment you deleted was still in
318
+ // the snapshot, was not found in the live list, and the branch below added
319
+ // it straight back. It vanished from disk and reappeared on screen, and
320
+ // the next save wrote the resurrected copy back over the deletion. Editing
321
+ // one did the same thing by a different route: the text no longer matched,
322
+ // so the pre-edit copy was added alongside the edited one (#52).
323
+ //
324
+ // `comments` is what exists. A comment that is not in it does not exist.
325
+ if (comments.length > 0) {
326
+ const matchedComments = matchCommentsToDiff(comments, filePath, data.diffLines);
314
327
 
315
328
  // Update existing comments in the array with matched diff indices
316
329
  matchedComments.forEach(mc => {
@@ -330,6 +343,10 @@ async function loadFile(filePath, index) {
330
343
  existing.followUps = mc.followUps;
331
344
  }
332
345
  } else {
346
+ // Unreachable now that the matcher is fed the live list: everything
347
+ // it returns came from there. Kept as a guard rather than removed,
348
+ // because silently dropping a comment is the worse failure of the
349
+ // two, and this is the line that would do it.
333
350
  // Add new comment if it doesn't exist
334
351
  comments.push({
335
352
  file: mc.file,