@vintasoftware/pr-review-canvas 0.2.0 → 0.3.0

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.
Files changed (90) hide show
  1. package/README.md +9 -3
  2. package/docs/reference.md +99 -99
  3. package/package.json +12 -3
  4. package/pr-review.config.example.yml +4 -4
  5. package/skills/pr-review-canvas/SKILL.md +8 -1
  6. package/src/acpx/acpx.ts +21 -4
  7. package/src/acpx/events.ts +5 -1
  8. package/src/acpx/ndjson.ts +3 -1
  9. package/src/acpx/preflight.ts +5 -1
  10. package/src/canvas/export.ts +11 -2
  11. package/src/canvas/zip.ts +4 -1
  12. package/src/chat/chat-manager.ts +16 -3
  13. package/src/chat/context.ts +10 -3
  14. package/src/chat/seed.ts +3 -1
  15. package/src/cli.ts +12 -2
  16. package/src/commands.ts +41 -9
  17. package/src/config.ts +15 -3
  18. package/src/contract/comments.ts +3 -1
  19. package/src/contract/generation-context.ts +3 -1
  20. package/src/contract/review-artifact.ts +14 -8
  21. package/src/contract/state.ts +3 -1
  22. package/src/git/diff-collector.ts +2 -1
  23. package/src/git/git.ts +9 -4
  24. package/src/github/capabilities.ts +5 -1
  25. package/src/github/comments.ts +6 -3
  26. package/src/github/gh.ts +18 -3
  27. package/src/github/post-comment.ts +4 -1
  28. package/src/github/pr.ts +4 -1
  29. package/src/github/threads.ts +5 -1
  30. package/src/project-config.ts +12 -9
  31. package/src/prompt-files.ts +4 -3
  32. package/src/review/doctor.ts +9 -2
  33. package/src/review/normalize.ts +3 -1
  34. package/src/review/prepare.ts +24 -8
  35. package/src/review/prompt.ts +8 -2
  36. package/src/review/publish.ts +15 -3
  37. package/src/review/trim-caps.ts +10 -6
  38. package/src/review/validate.ts +65 -13
  39. package/src/server/app.ts +12 -4
  40. package/src/server/bundle.ts +17 -4
  41. package/src/server/context.ts +14 -2
  42. package/src/server/errors.ts +26 -2
  43. package/src/server/html.ts +2 -2
  44. package/src/server/node-server.ts +4 -2
  45. package/src/server/routes/api.ts +29 -5
  46. package/src/server/routes/chat-routes.ts +30 -9
  47. package/src/server/routes/pages.ts +3 -1
  48. package/src/server/routes/review-routes.ts +19 -4
  49. package/src/server/sse.ts +3 -1
  50. package/src/store/atomic-json.ts +5 -1
  51. package/src/store/canvas-store.ts +20 -6
  52. package/src/store/derived-store.ts +5 -1
  53. package/src/store/pr-store.ts +2 -1
  54. package/src/store/state-store.ts +2 -1
  55. package/static/brand.svg +19 -0
  56. package/static/js/api.js +15 -3
  57. package/static/js/app.js +8 -2
  58. package/static/js/chat-panel.js +93 -0
  59. package/static/js/chat.js +38 -15
  60. package/static/js/composer.js +6 -3
  61. package/static/js/diagram.js +2 -1
  62. package/static/js/diff-decorations.js +5 -3
  63. package/static/js/diff-renderer.js +6 -1
  64. package/static/js/dom.js +5 -7
  65. package/static/js/empty-state.js +3 -1
  66. package/static/js/header.js +1 -1
  67. package/static/js/interactions.js +55 -15
  68. package/static/js/keyboard.js +7 -2
  69. package/static/js/layers.js +23 -11
  70. package/static/js/links.js +8 -2
  71. package/static/js/nav.js +5 -2
  72. package/static/js/overview.js +32 -4
  73. package/static/js/points.js +3 -2
  74. package/static/js/progress.js +2 -1
  75. package/static/js/proposed-comment.js +4 -1
  76. package/static/js/quick-questions.js +2 -1
  77. package/static/js/regenerate.js +4 -1
  78. package/static/js/signoff.js +6 -2
  79. package/static/styles/base.css +16 -6
  80. package/static/styles/chat-panel.css +81 -0
  81. package/static/styles/chat-tools.css +28 -0
  82. package/static/styles/chat.css +1 -1
  83. package/static/styles/commands.css +4 -4
  84. package/static/styles/diff.css +1 -1
  85. package/static/styles/header.css +18 -4
  86. package/static/styles/layout.css +4 -4
  87. package/static/styles/responsive.css +1 -15
  88. package/static/styles/review.css +24 -3
  89. package/static/styles/skin-github.css +93 -100
  90. package/static/styles.css +13 -12
package/README.md CHANGED
@@ -138,9 +138,9 @@ Customize generation and AI Chat prompts with the `prompts` map in your project'
138
138
 
139
139
  ```yaml
140
140
  prompts:
141
- generation-format.md: review-prompts/generation-format.md
142
- generation-surfacing.md: review-prompts/generation-surfacing.md
143
- chat-seed.md: review-prompts/chat-seed.md
141
+ generation-format.md: review-prompts/generation-format.md
142
+ generation-surfacing.md: review-prompts/generation-surfacing.md
143
+ chat-seed.md: review-prompts/chat-seed.md
144
144
  ```
145
145
 
146
146
  Copy the installed templates to start editing (for an npm global install):
@@ -168,11 +168,17 @@ In a clone of this tool, use pnpm for the shared lockfile and development checks
168
168
  ```bash
169
169
  corepack pnpm --version
170
170
  corepack pnpm install --frozen-lockfile
171
+ corepack pnpm hooks:install
171
172
  corepack pnpm exec playwright install --with-deps chromium
172
173
  corepack pnpm verify
173
174
  corepack pnpm start --repo /path/to/your-project
174
175
  ```
175
176
 
177
+ The pre-commit hook runs `pnpm precommit`: lint, formatting, strict type checks, and tests.
178
+ Any failure blocks the commit. Run `pnpm hooks:install`
179
+ once per clone to enable it. Use `pnpm lint:fix` and `pnpm format` to apply automatic fixes.
180
+ CI runs the same checks through `pnpm verify`, with coverage executing the unit tests once.
181
+
176
182
  Run the full `pnpm verify` before pushing. Keep branch coverage at least 96% when adding or
177
183
  changing behavior, leaving a margin above CI's 95% minimum. Cover meaningful failure and boundary
178
184
  cases rather than lowering thresholds. Each CI job uploads `coverage-node-<version>` with branch
package/docs/reference.md CHANGED
@@ -15,14 +15,14 @@ For setup and the basic review workflow, see the [README](../README.md).
15
15
 
16
16
  ### Repository and runtime options
17
17
 
18
- | Option | Applies to | Default and behavior |
19
- |---|---|---|
20
- | `--repo <dir>` | All commands | Uses the current directory when omitted; resolves the repository root from there |
21
- | `--data-dir <dir>` | All except `install-skill` | Overrides `PR_REVIEW_DATA_DIR`, then the default `<main checkout>/.pr-review` |
22
- | `--port <n>` | `serve` | Overrides `PR_REVIEW_PORT`, then `3010`; accepts 1–65535 |
23
- | `--agent claude\|codex` | `serve` | Overrides the saved chat agent for this run |
24
- | `--model <id>` | `serve` | Overrides the saved chat model for this run |
25
- | `--fixture-canvas <review.json>` | `serve` | Development preview: uses the supplied canvas for every requested PR, with its head replaced by the live PR head |
18
+ | Option | Applies to | Default and behavior |
19
+ | -------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
20
+ | `--repo <dir>` | All commands | Uses the current directory when omitted; resolves the repository root from there |
21
+ | `--data-dir <dir>` | All except `install-skill` | Overrides `PR_REVIEW_DATA_DIR`, then the default `<main checkout>/.pr-review` |
22
+ | `--port <n>` | `serve` | Overrides `PR_REVIEW_PORT`, then `3010`; accepts 1–65535 |
23
+ | `--agent claude\|codex` | `serve` | Overrides the saved chat agent for this run |
24
+ | `--model <id>` | `serve` | Overrides the saved chat model for this run |
25
+ | `--fixture-canvas <review.json>` | `serve` | Development preview: uses the supplied canvas for every requested PR, with its head replaced by the live PR head |
26
26
 
27
27
  Repository operations require an `origin` remote on **github.com**. GitHub Enterprise Server
28
28
  hosts are not supported. Fetching a PR does not check out its branch.
@@ -129,13 +129,13 @@ JSON error. `serve` stays running and writes its startup message to stderr.
129
129
  Command failures use `{ "error": { "code", "message", "hint" } }`, with `hint` optional.
130
130
  Validation failures from `validate` use its report format instead.
131
131
 
132
- | Exit code | Meaning |
133
- |---|---|
134
- | `0` | Success |
135
- | `1` | Error, including a failed `doctor` check |
136
- | `2` | Command usage error, such as an unknown command or missing required flag |
137
- | `4` | GitHub CLI missing or unauthenticated |
138
- | `5` | Validation failed in `validate` or `publish` |
132
+ | Exit code | Meaning |
133
+ | --------- | ------------------------------------------------------------------------ |
134
+ | `0` | Success |
135
+ | `1` | Error, including a failed `doctor` check |
136
+ | `2` | Command usage error, such as an unknown command or missing required flag |
137
+ | `4` | GitHub CLI missing or unauthenticated |
138
+ | `5` | Validation failed in `validate` or `publish` |
139
139
 
140
140
  `doctor` reports failed checks with exit `1`, including authentication failures.
141
141
 
@@ -148,20 +148,20 @@ Lists you supply replace their defaults.
148
148
  Path patterns match repository-relative paths. `**` crosses directories; `*` and `?` match
149
149
  within one path segment.
150
150
 
151
- | Key | Default | Details |
152
- |---|---|---|
153
- | `version` | `1` | The only supported configuration version |
154
- | `rulebook` | Unset | Path to a Markdown file of project code standards, resolved from the repository root; these standards take precedence over bundled standards |
155
- | `layers` | `[]` | Optional review guidance; each entry has `id`, `title`, `description`, and optional `paths` patterns. The agent may combine, split, or reorder groups. When omitted or empty, it chooses semantic sections from the change |
156
- | `highRisk` | `[]` | Entries with a `pattern` glob and `label`; matching changes receive risk labels and cannot go in the Other layer |
157
- | `generation.mode` | `strict` | See [generation modes](#generation-modes) |
158
- | `generation.maxRepairRounds` | `3` | Failed validation rounds allowed by the generation skill |
159
- | `generation.inlineDiffMaxLines` | `1500` | Maximum diff length to include directly in the generation prompt |
160
- | `generation.smallPrHunks` | `10` | At or below this hunk count, the prompt asks for one layer unless concerns differ |
161
- | `generation.caps` | See below | Overrides individual text limits |
162
- | `tests.patterns` | `['**/*.test.*', '**/*.spec.*', '**/__tests__/**']` | Paths treated as tests for review ordering and labels |
163
- | `chat.enabled` | `true` | Set to `false` to disable AI Chat |
164
- | `prompts` | Bundled templates | See [prompt templates](#prompt-templates) for supported keys and behavior |
151
+ | Key | Default | Details |
152
+ | ------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
153
+ | `version` | `1` | The only supported configuration version |
154
+ | `rulebook` | Unset | Path to a Markdown file of project code standards, resolved from the repository root; these standards take precedence over bundled standards |
155
+ | `layers` | `[]` | Optional review guidance; each entry has `id`, `title`, `description`, and optional `paths` patterns. The agent may combine, split, or reorder groups. When omitted or empty, it chooses semantic sections from the change |
156
+ | `highRisk` | `[]` | Entries with a `pattern` glob and `label`; matching changes receive risk labels and cannot go in the Other layer |
157
+ | `generation.mode` | `strict` | See [generation modes](#generation-modes) |
158
+ | `generation.maxRepairRounds` | `3` | Failed validation rounds allowed by the generation skill |
159
+ | `generation.inlineDiffMaxLines` | `1500` | Maximum diff length to include directly in the generation prompt |
160
+ | `generation.smallPrHunks` | `10` | At or below this hunk count, the prompt asks for one layer unless concerns differ |
161
+ | `generation.caps` | See below | Overrides individual text limits |
162
+ | `tests.patterns` | `['**/*.test.*', '**/*.spec.*', '**/__tests__/**']` | Paths treated as tests for review ordering and labels |
163
+ | `chat.enabled` | `true` | Set to `false` to disable AI Chat |
164
+ | `prompts` | Bundled templates | See [prompt templates](#prompt-templates) for supported keys and behavior |
165
165
 
166
166
  Generation's numeric options and text caps must be positive integers. An empty `layers` list
167
167
  provides no suggested groups; an empty `tests.patterns` list recognizes no files as tests.
@@ -182,14 +182,14 @@ affects canvas generation; chat answers the reviewer's selected question.
182
182
 
183
183
  The `prompts` map in `pr-review.config.yml` accepts these keys:
184
184
 
185
- | Key | Purpose |
186
- |---|---|
187
- | `generation-format.md` | Schema and output rules |
188
- | `generation-strict.md` | Instructions for strict mode |
189
- | `generation-surfacing.md` | Instructions for surfacing mode |
190
- | `quality-standards.md` | Bundled code standards |
191
- | `layering-guidance.md` | Guidance for grouping related changes |
192
- | `chat-seed.md` | Opening AI Chat instructions |
185
+ | Key | Purpose |
186
+ | ------------------------- | ------------------------------------- |
187
+ | `generation-format.md` | Schema and output rules |
188
+ | `generation-strict.md` | Instructions for strict mode |
189
+ | `generation-surfacing.md` | Instructions for surfacing mode |
190
+ | `quality-standards.md` | Bundled code standards |
191
+ | `layering-guidance.md` | Guidance for grouping related changes |
192
+ | `chat-seed.md` | Opening AI Chat instructions |
193
193
 
194
194
  Each configured file replaces a whole template. Paths resolve from the project root,
195
195
  including when running from a subdirectory or using `--repo`. Absolute paths work for
@@ -214,18 +214,18 @@ when upgrading.
214
214
  Set any of these keys under `generation.caps`. Prose limits count visible characters, so Markdown
215
215
  link targets do not count. `diagram` counts raw Mermaid source characters.
216
216
 
217
- | Key | Default |
218
- |---|---|
219
- | `summary` | 1200 |
220
- | `layerTitle` | 60 |
221
- | `rationale` | 300 |
222
- | `decisions` | 600 |
223
- | `checkByHand` | 400 |
224
- | `annotation` | 240 |
225
- | `pointTitle` | 90 |
226
- | `pointBody` | 600 |
227
- | `testBehavior` | 120 |
228
- | `diagram` | 1500 |
217
+ | Key | Default |
218
+ | -------------- | ------- |
219
+ | `summary` | 1200 |
220
+ | `layerTitle` | 60 |
221
+ | `rationale` | 300 |
222
+ | `decisions` | 600 |
223
+ | `checkByHand` | 400 |
224
+ | `annotation` | 240 |
225
+ | `pointTitle` | 90 |
226
+ | `pointBody` | 600 |
227
+ | `testBehavior` | 120 |
228
+ | `diagram` | 1500 |
229
229
 
230
230
  The canvas has a separate limit of 12 attention points, including entries generated from missing
231
231
  tests. Increasing text caps does not increase that limit.
@@ -236,7 +236,7 @@ Custom patterns replace the JavaScript/TypeScript defaults. For example, a Pytho
236
236
 
237
237
  ```yaml
238
238
  tests:
239
- patterns: ['**/test_*.py', '**/tests/**']
239
+ patterns: ['**/test_*.py', '**/tests/**']
240
240
  ```
241
241
 
242
242
  Test recognition controls ordering, but matching a test to its source file currently follows
@@ -247,15 +247,15 @@ placed in Other while its source is in a regular layer.
247
247
 
248
248
  The data directory's `settings.yml` accepts these keys and values:
249
249
 
250
- | Key | Default | Accepted values |
251
- |---|---|---|
252
- | `version` | `1` | `1` |
253
- | `skin` | `terminal` | `terminal`, `github` |
254
- | `theme` | `auto` | `auto`, `light`, `dark` |
255
- | `agent` | `claude` | `claude`, `codex` |
256
- | `model` | `null` | A model ID, or `null` for the agent's default |
257
- | `chatTimeoutSec` | `600` | Integer seconds, 30–3600 |
258
- | `maxTurns` | `null` | Integer 1–100, or `null` for the agent's default |
250
+ | Key | Default | Accepted values |
251
+ | ---------------- | ---------- | ------------------------------------------------ |
252
+ | `version` | `1` | `1` |
253
+ | `skin` | `terminal` | `terminal`, `github` |
254
+ | `theme` | `auto` | `auto`, `light`, `dark` |
255
+ | `agent` | `claude` | `claude`, `codex` |
256
+ | `model` | `null` | A model ID, or `null` for the agent's default |
257
+ | `chatTimeoutSec` | `600` | Integer seconds, 30–3600 |
258
+ | `maxTurns` | `null` | Integer 1–100, or `null` for the agent's default |
259
259
 
260
260
  Invalid settings fall back to defaults. URL parameters `?skin=github&theme=light` can override
261
261
  appearance for one page load without saving it.
@@ -274,13 +274,13 @@ Deleting the directory loses saved preferences, canvases, progress, and chat his
274
274
 
275
275
  Append a fragment to `/review/<pr-number>`:
276
276
 
277
- | Target | Fragment example |
278
- |---|---|
279
- | Layer | `#layer:data-access` |
280
- | File | `#file:src/store.ts` |
281
- | Hunk | `#hunk:src/store.ts#2` |
282
- | New-side line range | `#line:src/store.ts:40-52` |
283
- | Old-side line | `#line:src/store.ts:40:old` |
277
+ | Target | Fragment example |
278
+ | ------------------- | --------------------------- |
279
+ | Layer | `#layer:data-access` |
280
+ | File | `#file:src/store.ts` |
281
+ | Hunk | `#hunk:src/store.ts#2` |
282
+ | New-side line range | `#line:src/store.ts:40-52` |
283
+ | Old-side line | `#line:src/store.ts:40:old` |
284
284
 
285
285
  Use the layer key and file path from the canvas. Links open the target file when needed; if a
286
286
  line is unavailable, navigation uses the nearest visible row. Recipients need their own running
@@ -344,41 +344,41 @@ sandbox for the agent. Its access also depends on the agent's own permissions. D
344
344
 
345
345
  ## Troubleshooting
346
346
 
347
- | Symptom or code | Next step |
348
- |---|---|
349
- | `NOT_A_REPO` | Run inside a Git clone or pass `--repo <dir>` |
350
- | `NO_ORIGIN` | Check that `origin` points to a repository on github.com |
351
- | `GH_MISSING` / `GH_UNAUTHENTICATED` | Install [GitHub CLI](https://cli.github.com), run `gh auth login`, and check authentication in the same environment that runs the server |
352
- | `GITHUB_API_ERROR` | Read the underlying error for permissions, rate limits, connectivity, or GitHub service problems |
353
- | `PR_NOT_FOUND` | Check the PR number, repository, and your access |
354
- | `CANVAS_NOT_FOUND` | Generate or import a canvas for the requested commit |
355
- | `CANVAS_INVALID` | Read the format errors; re-export or regenerate the canvas |
356
- | `CANVAS_REPO_MISMATCH` | Check which clone is open; use `import --force` only when importing from the other repository is intentional |
357
- | `CANVAS_TOO_LARGE` | The archive exceeds the 20 MiB import limit |
358
- | `CANVAS_STALE` | The PR head moved; prepare again for the current commit |
359
- | `MODEL_INVALID` | Fix the reported problems in `model.json`, validate, then publish again |
360
- | `SKILL_DIR_EXISTS` | The destination contains a customized directory; preserve it elsewhere before replacing it with `--force` |
361
- | `CHAT_BUSY` | Wait for the running reply or press **stop** |
362
- | `AGENT_AUTH_REQUIRED` | Sign in through the selected agent's CLI, then retry |
363
- | `AGENT_MISSING` or missing chat pane | Check `chat.enabled` and confirm the server can find `acpx` and the selected agent; run `pr-review doctor --all-checks` |
364
- | `AGENT_INCOMPLETE` | Retry the message or increase the chat timeout |
365
- | `COMMENT_FORBIDDEN` | Check the GitHub account's repository access and token permissions |
366
- | `COMMENT_LINE_NOT_IN_DIFF` | Choose a line shown in the current diff |
367
- | `SIGNOFF_INCOMPLETE` | Mark every layer except Other reviewed for this head |
368
- | `FORBIDDEN_HOST` / `CROSS_ORIGIN` | Open the local server using `localhost` or `127.0.0.1` and submit actions from that page |
347
+ | Symptom or code | Next step |
348
+ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
349
+ | `NOT_A_REPO` | Run inside a Git clone or pass `--repo <dir>` |
350
+ | `NO_ORIGIN` | Check that `origin` points to a repository on github.com |
351
+ | `GH_MISSING` / `GH_UNAUTHENTICATED` | Install [GitHub CLI](https://cli.github.com), run `gh auth login`, and check authentication in the same environment that runs the server |
352
+ | `GITHUB_API_ERROR` | Read the underlying error for permissions, rate limits, connectivity, or GitHub service problems |
353
+ | `PR_NOT_FOUND` | Check the PR number, repository, and your access |
354
+ | `CANVAS_NOT_FOUND` | Generate or import a canvas for the requested commit |
355
+ | `CANVAS_INVALID` | Read the format errors; re-export or regenerate the canvas |
356
+ | `CANVAS_REPO_MISMATCH` | Check which clone is open; use `import --force` only when importing from the other repository is intentional |
357
+ | `CANVAS_TOO_LARGE` | The archive exceeds the 20 MiB import limit |
358
+ | `CANVAS_STALE` | The PR head moved; prepare again for the current commit |
359
+ | `MODEL_INVALID` | Fix the reported problems in `model.json`, validate, then publish again |
360
+ | `SKILL_DIR_EXISTS` | The destination contains a customized directory; preserve it elsewhere before replacing it with `--force` |
361
+ | `CHAT_BUSY` | Wait for the running reply or press **stop** |
362
+ | `AGENT_AUTH_REQUIRED` | Sign in through the selected agent's CLI, then retry |
363
+ | `AGENT_MISSING` or missing chat pane | Check `chat.enabled` and confirm the server can find `acpx` and the selected agent; run `pr-review doctor --all-checks` |
364
+ | `AGENT_INCOMPLETE` | Retry the message or increase the chat timeout |
365
+ | `COMMENT_FORBIDDEN` | Check the GitHub account's repository access and token permissions |
366
+ | `COMMENT_LINE_NOT_IN_DIFF` | Choose a line shown in the current diff |
367
+ | `SIGNOFF_INCOMPLETE` | Mark every layer except Other reviewed for this head |
368
+ | `FORBIDDEN_HOST` / `CROSS_ORIGIN` | Open the local server using `localhost` or `127.0.0.1` and submit actions from that page |
369
369
 
370
370
  ### Validation diagnostics
371
371
 
372
372
  Validation reports name the field, file, hunk, or line to fix. Common groups are:
373
373
 
374
- | Codes | What to check |
375
- |---|---|
376
- | `SCHEMA`, `TEXT_TOO_LONG` | Required fields, types, and text limits |
377
- | `HUNK_UNASSIGNED`, `HUNK_DUPLICATE`, `HUNK_UNKNOWN` | Each known hunk belongs to exactly one layer |
378
- | `PATH_UNKNOWN`, `TEST_PATH_UNKNOWN` | Referenced files exist in the relevant diff or PR head |
379
- | `LAYER_EMPTY`, `LAYER_KEY_DUPLICATE` | Layers contain hunks and have unique keys |
380
- | `OTHER_DUPLICATE`, `OTHER_NOT_LAST`, `RISK_IN_OTHER` | At most one Other layer, last, without risk-tagged changes |
381
- | `TEST_NOT_LAST`, `TEST_IN_OTHER` | Tests follow the code they cover and use the appropriate layer |
382
- | `ANNOTATION_OUTSIDE_HUNK`, `POINT_OUTSIDE_DIFF`, `FOLD_INVALID` | Locations and fold ranges fit the assigned diff |
383
- | `TOO_MANY_POINTS` | Count explicit points and missing-test entries together |
384
- | `LINK_UNRESOLVED`, `DIAGRAM_NODE_UNKNOWN`, `DIAGRAM_LIMIT` | Link targets, diagram node IDs, and diagram counts |
374
+ | Codes | What to check |
375
+ | --------------------------------------------------------------- | -------------------------------------------------------------- |
376
+ | `SCHEMA`, `TEXT_TOO_LONG` | Required fields, types, and text limits |
377
+ | `HUNK_UNASSIGNED`, `HUNK_DUPLICATE`, `HUNK_UNKNOWN` | Each known hunk belongs to exactly one layer |
378
+ | `PATH_UNKNOWN`, `TEST_PATH_UNKNOWN` | Referenced files exist in the relevant diff or PR head |
379
+ | `LAYER_EMPTY`, `LAYER_KEY_DUPLICATE` | Layers contain hunks and have unique keys |
380
+ | `OTHER_DUPLICATE`, `OTHER_NOT_LAST`, `RISK_IN_OTHER` | At most one Other layer, last, without risk-tagged changes |
381
+ | `TEST_NOT_LAST`, `TEST_IN_OTHER` | Tests follow the code they cover and use the appropriate layer |
382
+ | `ANNOTATION_OUTSIDE_HUNK`, `POINT_OUTSIDE_DIFF`, `FOLD_INVALID` | Locations and fold ranges fit the assigned diff |
383
+ | `TOO_MANY_POINTS` | Count explicit points and missing-test entries together |
384
+ | `LINK_UNRESOLVED`, `DIAGRAM_NODE_UNKNOWN`, `DIAGRAM_LIMIT` | Link targets, diagram node IDs, and diagram counts |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vintasoftware/pr-review-canvas",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Localhost PR review canvas: layered diffs, attention points, and GitHub comments for one PR",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,13 +10,20 @@
10
10
  "dev": "tsx watch --clear-screen=false --ignore '**/.pr-review/**' src/cli.ts serve",
11
11
  "start": "tsx src/cli.ts serve",
12
12
  "test": "vitest run",
13
+ "hooks:install": "git config core.hooksPath .githooks",
13
14
  "test:watch": "vitest",
14
15
  "test:browser": "playwright test",
15
16
  "coverage": "vitest run --coverage",
16
17
  "typecheck": "tsc --noEmit",
17
18
  "test:package": "node scripts/test-package.mjs",
18
- "verify": "pnpm typecheck && pnpm coverage && pnpm test:browser --workers=2 && pnpm test:package",
19
- "prepublishOnly": "pnpm verify"
19
+ "verify": "pnpm check && pnpm coverage && pnpm test:browser --workers=2 && pnpm test:package",
20
+ "prepublishOnly": "pnpm verify",
21
+ "lint": "oxlint --deny-warnings",
22
+ "lint:fix": "oxlint --fix --deny-warnings",
23
+ "format": "oxfmt --write",
24
+ "format:check": "oxfmt --check",
25
+ "check": "pnpm lint && pnpm format:check && pnpm typecheck",
26
+ "precommit": "pnpm check && pnpm test"
20
27
  },
21
28
  "dependencies": {
22
29
  "@highlightjs/cdn-assets": "^11.12.0",
@@ -36,6 +43,8 @@
36
43
  "@types/node": "^24.13.3",
37
44
  "@vitest/coverage-v8": "^4.1.10",
38
45
  "happy-dom": "20.11.2",
46
+ "oxfmt": "^0.68.0",
47
+ "oxlint": "^1.83.0",
39
48
  "typescript": "^5.9.3",
40
49
  "vite": "~8.0.16",
41
50
  "vitest": "^4.1.10"
@@ -30,10 +30,10 @@ highRisk:
30
30
  label: auth
31
31
 
32
32
  generation:
33
- mode: strict # strict: quality review; surfacing: walkthrough that audits as it reads
34
- maxRepairRounds: 3 # publish retries the skill may make after a failed validation
33
+ mode: strict # strict: quality review; surfacing: walkthrough that audits as it reads
34
+ maxRepairRounds: 3 # publish retries the skill may make after a failed validation
35
35
  inlineDiffMaxLines: 1500 # diffs longer than this are read per file instead of inlined in the prompt
36
- smallPrHunks: 10 # at most this many hunks: one layer unless concerns differ, fewer annotations
36
+ smallPrHunks: 10 # at most this many hunks: one layer unless concerns differ, fewer annotations
37
37
  # caps: # override the text caps (characters)
38
38
  # summary: 1800
39
39
  # rationale: 300
@@ -46,4 +46,4 @@ tests:
46
46
  patterns: ['**/*.test.*', '**/*.spec.*', '**/__tests__/**']
47
47
 
48
48
  chat:
49
- enabled: true # false removes the chat pane and its routes
49
+ enabled: true # false removes the chat pane and its routes
@@ -39,7 +39,14 @@ pr-review prepare --base <ref> --head <ref> [--force]
39
39
  Progress goes to stderr. The last stdout line is JSON:
40
40
 
41
41
  ```json
42
- { "canvasDir": "...", "headSha": "...", "mergeBaseSha": "...", "promptPath": "...", "contextPath": "...", "status": "prepared" }
42
+ {
43
+ "canvasDir": "...",
44
+ "headSha": "...",
45
+ "mergeBaseSha": "...",
46
+ "promptPath": "...",
47
+ "contextPath": "...",
48
+ "status": "prepared"
49
+ }
43
50
  ```
44
51
 
45
52
  - `status: "exists"` means a canvas already exists for this head. Stop and tell the user:
package/src/acpx/acpx.ts CHANGED
@@ -231,7 +231,11 @@ export function createAgentRunner(opts: CreateAgentRunnerOptions = {}): AgentRun
231
231
  // The cancel call gets its own timeout, and SIGKILL when it elapses: a cancel that hangs
232
232
  // must neither hold the kill back nor stay behind as a process of its own.
233
233
  agentArgs =>
234
- execQuiet(bin, agentArgs, { cwd: options.cwd, timeoutSec: CANCEL_TIMEOUT_SEC, killSignal: 'SIGKILL' }),
234
+ execQuiet(bin, agentArgs, {
235
+ cwd: options.cwd,
236
+ timeoutSec: CANCEL_TIMEOUT_SEC,
237
+ killSignal: 'SIGKILL',
238
+ }),
235
239
  slackMs,
236
240
  cancelGraceMs
237
241
  )
@@ -268,7 +272,12 @@ export function createAgentRunner(opts: CreateAgentRunnerOptions = {}): AgentRun
268
272
  }
269
273
  }
270
274
  if (!ended) {
271
- return { ok: false, text: '', code: 'AGENT_INCOMPLETE', message: 'the agent stopped before finishing' }
275
+ return {
276
+ ok: false,
277
+ text: '',
278
+ code: 'AGENT_INCOMPLETE',
279
+ message: 'the agent stopped before finishing',
280
+ }
272
281
  }
273
282
  return text === ''
274
283
  ? { ok: false, text: '', code: 'AGENT_INCOMPLETE', message: 'the agent replied nothing' }
@@ -516,11 +525,19 @@ function startRun(
516
525
  const exit = code ?? 1
517
526
  const mapped = exitCodeToAgentCode(exit)
518
527
  if (mapped !== null) {
519
- finish({ type: 'error', code: mapped, message: stderr.trim() === '' ? exitCodeMessage(exit) : stderr.trim() })
528
+ finish({
529
+ type: 'error',
530
+ code: mapped,
531
+ message: stderr.trim() === '' ? exitCodeMessage(exit) : stderr.trim(),
532
+ })
520
533
  return
521
534
  }
522
535
  // acpx 0.13.2 exits 0 when its own --timeout elapses, without a terminal event.
523
- finish({ type: 'error', code: 'AGENT_INCOMPLETE', message: 'the agent stopped before finishing its answer' })
536
+ finish({
537
+ type: 'error',
538
+ code: 'AGENT_INCOMPLETE',
539
+ message: 'the agent stopped before finishing its answer',
540
+ })
524
541
  })
525
542
 
526
543
  child.stdin?.on('error', () => undefined)
@@ -64,7 +64,11 @@ function contentText(value: unknown): string {
64
64
  */
65
65
  export function mapAcpxMessage(message: unknown): AgentEvent | null {
66
66
  if (!isRecord(message)) {
67
- return { type: 'error', code: 'AGENT_PROTOCOL_INVALID', message: 'the agent wrote a line that is not a message' }
67
+ return {
68
+ type: 'error',
69
+ code: 'AGENT_PROTOCOL_INVALID',
70
+ message: 'the agent wrote a line that is not a message',
71
+ }
68
72
  }
69
73
  const error = read(message, 'error')
70
74
  if (isRecord(error)) {
@@ -50,7 +50,9 @@ export function createNdjsonSplitter(lineMax: number = NDJSON_LINE_MAX): NdjsonS
50
50
  const raw = buffer.slice(0, at)
51
51
  buffer = buffer.slice(at + 1)
52
52
  if (raw.length > lineMax) {
53
- throw new NdjsonError(`the agent wrote a line of ${raw.length} characters, over the ${lineMax} limit`)
53
+ throw new NdjsonError(
54
+ `the agent wrote a line of ${raw.length} characters, over the ${lineMax} limit`
55
+ )
54
56
  }
55
57
  out.push(...take(raw))
56
58
  at = buffer.indexOf('\n')
@@ -16,7 +16,11 @@ export interface PreflightProbe {
16
16
  * Is acpx on PATH? The answer barely changes while the server runs, so it is asked once and
17
17
  * reused; the page shows a banner and hides the chat when it says no.
18
18
  */
19
- export function createPreflightProbe(runner: AgentRunner, now: () => Date, ttlMs = PREFLIGHT_TTL_MS): PreflightProbe {
19
+ export function createPreflightProbe(
20
+ runner: AgentRunner,
21
+ now: () => Date,
22
+ ttlMs = PREFLIGHT_TTL_MS
23
+ ): PreflightProbe {
20
24
  let cached: { at: number; value: AcpxPreflight } | null = null
21
25
  let inFlight: Promise<AcpxPreflight> | null = null
22
26
  return {
@@ -37,7 +37,12 @@ export async function buildCanvasZipFor(
37
37
  const number = prNumber ?? stored.prNumber
38
38
  const manifest: CanvasManifest = number === undefined ? stored : { ...stored, prNumber: number }
39
39
  const zip: CanvasZip = {
40
- name: buildCanvasZipName({ repo: manifest.repo, headSha, prNumber: number, generatedAt: manifest.generatedAt }),
40
+ name: buildCanvasZipName({
41
+ repo: manifest.repo,
42
+ headSha,
43
+ prNumber: number,
44
+ generatedAt: manifest.generatedAt,
45
+ }),
41
46
  bytes: buildCanvasZip(manifest, artifact),
42
47
  headSha,
43
48
  }
@@ -64,7 +69,11 @@ async function isDirectory(target: string): Promise<boolean> {
64
69
  }
65
70
 
66
71
  /** `--out` names a file when it ends in `.zip` or is not an existing directory. */
67
- export async function resolveOutPath(out: string | undefined, defaultDir: string, name: string): Promise<string> {
72
+ export async function resolveOutPath(
73
+ out: string | undefined,
74
+ defaultDir: string,
75
+ name: string
76
+ ): Promise<string> {
68
77
  if (out === undefined) {
69
78
  return path.join(defaultDir, name)
70
79
  }
package/src/canvas/zip.ts CHANGED
@@ -66,7 +66,10 @@ function decode(entry: Uint8Array | undefined, name: string, issues: string[]):
66
66
  */
67
67
  export function readCanvasZip(bytes: Uint8Array): CanvasZipContents {
68
68
  if (bytes.length > CANVAS_ZIP_MAX_BYTES) {
69
- throw new CanvasZipError('CANVAS_TOO_LARGE', `the canvas zip is larger than ${CANVAS_ZIP_MAX_BYTES} bytes`)
69
+ throw new CanvasZipError(
70
+ 'CANVAS_TOO_LARGE',
71
+ `the canvas zip is larger than ${CANVAS_ZIP_MAX_BYTES} bytes`
72
+ )
70
73
  }
71
74
  if (!hasZipMagic(bytes)) {
72
75
  throw new CanvasZipError('CANVAS_INVALID', 'this file is not a zip', [
@@ -10,7 +10,13 @@ import type { SettingsStore } from '../store/settings-store.js'
10
10
  import type { StateStore } from '../store/state-store.js'
11
11
  import { type ContextSources, renderChatContext } from './context.js'
12
12
  import { renderSeed, type SeedPaths } from './seed.js'
13
- import { NEW_THREAD_TITLE, nextThreadIndex, type TranscriptStore, threadName, threadTitle } from './threads.js'
13
+ import {
14
+ NEW_THREAD_TITLE,
15
+ nextThreadIndex,
16
+ type TranscriptStore,
17
+ threadName,
18
+ threadTitle,
19
+ } from './threads.js'
14
20
 
15
21
  export class ChatBusyError extends Error {
16
22
  constructor() {
@@ -188,7 +194,11 @@ export function createChatManager(deps: ChatManagerDeps): ChatManager {
188
194
  }
189
195
  }
190
196
 
191
- async function* runTurn(target: ChatTarget, input: ChatSendInput, slot: RunningTurn): AsyncIterable<ChatEvent> {
197
+ async function* runTurn(
198
+ target: ChatTarget,
199
+ input: ChatSendInput,
200
+ slot: RunningTurn
201
+ ): AsyncIterable<ChatEvent> {
192
202
  const settings = await effectiveSettings()
193
203
  const thread = await resolveThread(target.prNumber, settings.agent, input.thread)
194
204
  const seeded = thread.seededHeadSha !== target.headSha
@@ -350,7 +360,10 @@ export function createChatManager(deps: ChatManagerDeps): ChatManager {
350
360
  if (thread === undefined) {
351
361
  return null
352
362
  }
353
- await deps.state.update(prNumber, current => ({ ...current, chat: { ...current.chat, activeThread: name } }))
363
+ await deps.state.update(prNumber, current => ({
364
+ ...current,
365
+ chat: { ...current.chat, activeThread: name },
366
+ }))
354
367
  return thread
355
368
  },
356
369
  send,
@@ -106,7 +106,10 @@ export async function renderChatContext(context: ChatContext, sources: ContextSo
106
106
  * An attention point: what it says, plus the lines it sits on. The agent needs the text to know
107
107
  * what the reader is reacting to; the lines alone leave it guessing. One context, one heading.
108
108
  */
109
- async function renderPoint(context: Extract<ChatContext, { kind: 'point' }>, sources: ContextSources): Promise<string> {
109
+ async function renderPoint(
110
+ context: Extract<ChatContext, { kind: 'point' }>,
111
+ sources: ContextSources
112
+ ): Promise<string> {
110
113
  const point = sources.artifact.points.find(p => p.fingerprint === context.fingerprint)
111
114
  if (point === undefined) {
112
115
  throw new ChatContextError('this canvas has no such attention point')
@@ -140,7 +143,10 @@ async function renderLines(
140
143
  }
141
144
 
142
145
  /** The quoted lines and the hunk around them, with no heading of their own. */
143
- async function linesBody(context: Extract<ChatContext, { kind: 'lines' }>, sources: ContextSources): Promise<string> {
146
+ async function linesBody(
147
+ context: Extract<ChatContext, { kind: 'lines' }>,
148
+ sources: ContextSources
149
+ ): Promise<string> {
144
150
  if (context.end < context.start) {
145
151
  throw new ChatContextError('the last line of a selection comes after the first')
146
152
  }
@@ -155,6 +161,7 @@ async function linesBody(context: Extract<ChatContext, { kind: 'lines' }>, sourc
155
161
  lines === null || lines.length === 0
156
162
  ? '_these lines are not available locally_'
157
163
  : fence(lines.map((text, i) => `${context.start + i}: ${text}`).join('\n'))
158
- const hunkBlock = hunk === undefined ? '' : `\n\nThe hunk around them is \`${hunk.id}\` (\`${hunk.header}\`).`
164
+ const hunkBlock =
165
+ hunk === undefined ? '' : `\n\nThe hunk around them is \`${hunk.id}\` (\`${hunk.header}\`).`
159
166
  return `${numbered}${hunkBlock}`
160
167
  }
package/src/chat/seed.ts CHANGED
@@ -42,7 +42,9 @@ export function pointsMarkdown(artifact: ReviewArtifact): string {
42
42
  if (artifact.points.length === 0) {
43
43
  return '_none_'
44
44
  }
45
- return artifact.points.map(p => `- ${p.level} · ${p.kind} · ${p.title} (\`${p.path}:${p.line}\`)`).join('\n')
45
+ return artifact.points
46
+ .map(p => `- ${p.level} · ${p.kind} · ${p.title} (\`${p.path}:${p.line}\`)`)
47
+ .join('\n')
46
48
  }
47
49
 
48
50
  export function testsMarkdown(artifact: ReviewArtifact): string {
package/src/cli.ts CHANGED
@@ -26,7 +26,16 @@ import { startServer } from './server/node-server.js'
26
26
  import { readJson } from './store/atomic-json.js'
27
27
  import { ensureDataDir } from './store/data-dir.js'
28
28
 
29
- const SUBCOMMANDS = ['serve', 'prepare', 'validate', 'publish', 'export', 'import', 'install-skill', 'doctor'] as const
29
+ const SUBCOMMANDS = [
30
+ 'serve',
31
+ 'prepare',
32
+ 'validate',
33
+ 'publish',
34
+ 'export',
35
+ 'import',
36
+ 'install-skill',
37
+ 'doctor',
38
+ ] as const
30
39
 
31
40
  const USAGE = `usage: pr-review <command> [flags]
32
41
 
@@ -78,7 +87,8 @@ async function buildContext(
78
87
  )
79
88
  const projectConfig = await loadProjectConfig(config.repoRoot)
80
89
  await ensureDataDir(config.dataDir)
81
- const fixtureArtifact = config.fixtureCanvasPath === null ? null : await loadFixture(config.fixtureCanvasPath)
90
+ const fixtureArtifact =
91
+ config.fixtureCanvasPath === null ? null : await loadFixture(config.fixtureCanvasPath)
82
92
  return createAppContext({ config, projectConfig, fixtureArtifact })
83
93
  }
84
94