@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.
- package/README.md +9 -3
- package/docs/reference.md +99 -99
- package/package.json +12 -3
- package/pr-review.config.example.yml +4 -4
- package/skills/pr-review-canvas/SKILL.md +8 -1
- package/src/acpx/acpx.ts +21 -4
- package/src/acpx/events.ts +5 -1
- package/src/acpx/ndjson.ts +3 -1
- package/src/acpx/preflight.ts +5 -1
- package/src/canvas/export.ts +11 -2
- package/src/canvas/zip.ts +4 -1
- package/src/chat/chat-manager.ts +16 -3
- package/src/chat/context.ts +10 -3
- package/src/chat/seed.ts +3 -1
- package/src/cli.ts +12 -2
- package/src/commands.ts +41 -9
- package/src/config.ts +15 -3
- package/src/contract/comments.ts +3 -1
- package/src/contract/generation-context.ts +3 -1
- package/src/contract/review-artifact.ts +14 -8
- package/src/contract/state.ts +3 -1
- package/src/git/diff-collector.ts +2 -1
- package/src/git/git.ts +9 -4
- package/src/github/capabilities.ts +5 -1
- package/src/github/comments.ts +6 -3
- package/src/github/gh.ts +18 -3
- package/src/github/post-comment.ts +4 -1
- package/src/github/pr.ts +4 -1
- package/src/github/threads.ts +5 -1
- package/src/project-config.ts +12 -9
- package/src/prompt-files.ts +4 -3
- package/src/review/doctor.ts +9 -2
- package/src/review/normalize.ts +3 -1
- package/src/review/prepare.ts +24 -8
- package/src/review/prompt.ts +8 -2
- package/src/review/publish.ts +15 -3
- package/src/review/trim-caps.ts +10 -6
- package/src/review/validate.ts +65 -13
- package/src/server/app.ts +12 -4
- package/src/server/bundle.ts +17 -4
- package/src/server/context.ts +14 -2
- package/src/server/errors.ts +26 -2
- package/src/server/html.ts +2 -2
- package/src/server/node-server.ts +4 -2
- package/src/server/routes/api.ts +29 -5
- package/src/server/routes/chat-routes.ts +30 -9
- package/src/server/routes/pages.ts +3 -1
- package/src/server/routes/review-routes.ts +19 -4
- package/src/server/sse.ts +3 -1
- package/src/store/atomic-json.ts +5 -1
- package/src/store/canvas-store.ts +20 -6
- package/src/store/derived-store.ts +5 -1
- package/src/store/pr-store.ts +2 -1
- package/src/store/state-store.ts +2 -1
- package/static/brand.svg +19 -0
- package/static/js/api.js +15 -3
- package/static/js/app.js +8 -2
- package/static/js/chat-panel.js +93 -0
- package/static/js/chat.js +38 -15
- package/static/js/composer.js +6 -3
- package/static/js/diagram.js +2 -1
- package/static/js/diff-decorations.js +5 -3
- package/static/js/diff-renderer.js +6 -1
- package/static/js/dom.js +5 -7
- package/static/js/empty-state.js +3 -1
- package/static/js/header.js +1 -1
- package/static/js/interactions.js +55 -15
- package/static/js/keyboard.js +7 -2
- package/static/js/layers.js +23 -11
- package/static/js/links.js +8 -2
- package/static/js/nav.js +5 -2
- package/static/js/overview.js +32 -4
- package/static/js/points.js +3 -2
- package/static/js/progress.js +2 -1
- package/static/js/proposed-comment.js +4 -1
- package/static/js/quick-questions.js +2 -1
- package/static/js/regenerate.js +4 -1
- package/static/js/signoff.js +6 -2
- package/static/styles/base.css +16 -6
- package/static/styles/chat-panel.css +81 -0
- package/static/styles/chat-tools.css +28 -0
- package/static/styles/chat.css +1 -1
- package/static/styles/commands.css +4 -4
- package/static/styles/diff.css +1 -1
- package/static/styles/header.css +18 -4
- package/static/styles/layout.css +4 -4
- package/static/styles/responsive.css +1 -15
- package/static/styles/review.css +24 -3
- package/static/styles/skin-github.css +93 -100
- 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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
| `--repo <dir>`
|
|
21
|
-
| `--data-dir <dir>`
|
|
22
|
-
| `--port <n>`
|
|
23
|
-
| `--agent claude\|codex`
|
|
24
|
-
| `--model <id>`
|
|
25
|
-
| `--fixture-canvas <review.json>` | `serve`
|
|
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`
|
|
135
|
-
| `1`
|
|
136
|
-
| `2`
|
|
137
|
-
| `4`
|
|
138
|
-
| `5`
|
|
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
|
|
152
|
-
|
|
153
|
-
| `version`
|
|
154
|
-
| `rulebook`
|
|
155
|
-
| `layers`
|
|
156
|
-
| `highRisk`
|
|
157
|
-
| `generation.mode`
|
|
158
|
-
| `generation.maxRepairRounds`
|
|
159
|
-
| `generation.inlineDiffMaxLines` | `1500`
|
|
160
|
-
| `generation.smallPrHunks`
|
|
161
|
-
| `generation.caps`
|
|
162
|
-
| `tests.patterns`
|
|
163
|
-
| `chat.enabled`
|
|
164
|
-
| `prompts`
|
|
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
|
|
186
|
-
|
|
187
|
-
| `generation-format.md`
|
|
188
|
-
| `generation-strict.md`
|
|
189
|
-
| `generation-surfacing.md` | Instructions for surfacing mode
|
|
190
|
-
| `quality-standards.md`
|
|
191
|
-
| `layering-guidance.md`
|
|
192
|
-
| `chat-seed.md`
|
|
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
|
|
218
|
-
|
|
219
|
-
| `summary`
|
|
220
|
-
| `layerTitle`
|
|
221
|
-
| `rationale`
|
|
222
|
-
| `decisions`
|
|
223
|
-
| `checkByHand`
|
|
224
|
-
| `annotation`
|
|
225
|
-
| `pointTitle`
|
|
226
|
-
| `pointBody`
|
|
227
|
-
| `testBehavior` | 120
|
|
228
|
-
| `diagram`
|
|
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
|
-
|
|
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
|
|
251
|
-
|
|
252
|
-
| `version`
|
|
253
|
-
| `skin`
|
|
254
|
-
| `theme`
|
|
255
|
-
| `agent`
|
|
256
|
-
| `model`
|
|
257
|
-
| `chatTimeoutSec` | `600`
|
|
258
|
-
| `maxTurns`
|
|
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
|
|
278
|
-
|
|
279
|
-
| Layer
|
|
280
|
-
| File
|
|
281
|
-
| Hunk
|
|
282
|
-
| New-side line range | `#line:src/store.ts:40-52`
|
|
283
|
-
| Old-side line
|
|
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
|
|
348
|
-
|
|
349
|
-
| `NOT_A_REPO`
|
|
350
|
-
| `NO_ORIGIN`
|
|
351
|
-
| `GH_MISSING` / `GH_UNAUTHENTICATED`
|
|
352
|
-
| `GITHUB_API_ERROR`
|
|
353
|
-
| `PR_NOT_FOUND`
|
|
354
|
-
| `CANVAS_NOT_FOUND`
|
|
355
|
-
| `CANVAS_INVALID`
|
|
356
|
-
| `CANVAS_REPO_MISMATCH`
|
|
357
|
-
| `CANVAS_TOO_LARGE`
|
|
358
|
-
| `CANVAS_STALE`
|
|
359
|
-
| `MODEL_INVALID`
|
|
360
|
-
| `SKILL_DIR_EXISTS`
|
|
361
|
-
| `CHAT_BUSY`
|
|
362
|
-
| `AGENT_AUTH_REQUIRED`
|
|
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`
|
|
365
|
-
| `COMMENT_FORBIDDEN`
|
|
366
|
-
| `COMMENT_LINE_NOT_IN_DIFF`
|
|
367
|
-
| `SIGNOFF_INCOMPLETE`
|
|
368
|
-
| `FORBIDDEN_HOST` / `CROSS_ORIGIN`
|
|
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
|
|
375
|
-
|
|
376
|
-
| `SCHEMA`, `TEXT_TOO_LONG`
|
|
377
|
-
| `HUNK_UNASSIGNED`, `HUNK_DUPLICATE`, `HUNK_UNKNOWN`
|
|
378
|
-
| `PATH_UNKNOWN`, `TEST_PATH_UNKNOWN`
|
|
379
|
-
| `LAYER_EMPTY`, `LAYER_KEY_DUPLICATE`
|
|
380
|
-
| `OTHER_DUPLICATE`, `OTHER_NOT_LAST`, `RISK_IN_OTHER`
|
|
381
|
-
| `TEST_NOT_LAST`, `TEST_IN_OTHER`
|
|
382
|
-
| `ANNOTATION_OUTSIDE_HUNK`, `POINT_OUTSIDE_DIFF`, `FOLD_INVALID` | Locations and fold ranges fit the assigned diff
|
|
383
|
-
| `TOO_MANY_POINTS`
|
|
384
|
-
| `LINK_UNRESOLVED`, `DIAGRAM_NODE_UNKNOWN`, `DIAGRAM_LIMIT`
|
|
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.
|
|
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
|
|
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
|
|
34
|
-
maxRepairRounds: 3
|
|
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
|
|
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
|
|
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
|
-
{
|
|
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, {
|
|
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 {
|
|
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({
|
|
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({
|
|
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)
|
package/src/acpx/events.ts
CHANGED
|
@@ -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 {
|
|
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)) {
|
package/src/acpx/ndjson.ts
CHANGED
|
@@ -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(
|
|
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')
|
package/src/acpx/preflight.ts
CHANGED
|
@@ -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(
|
|
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 {
|
package/src/canvas/export.ts
CHANGED
|
@@ -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({
|
|
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(
|
|
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(
|
|
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', [
|
package/src/chat/chat-manager.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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 => ({
|
|
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,
|
package/src/chat/context.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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 =
|
|
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
|
|
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 = [
|
|
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 =
|
|
90
|
+
const fixtureArtifact =
|
|
91
|
+
config.fixtureCanvasPath === null ? null : await loadFixture(config.fixtureCanvasPath)
|
|
82
92
|
return createAppContext({ config, projectConfig, fixtureArtifact })
|
|
83
93
|
}
|
|
84
94
|
|