@vintasoftware/pr-review-canvas 0.1.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/LICENSE +202 -0
- package/README.md +192 -0
- package/bin/pr-review.mjs +5 -0
- package/docs/reference.md +340 -0
- package/package.json +74 -0
- package/pr-review.config.example.yml +68 -0
- package/prompts/chat-seed.md +64 -0
- package/prompts/generation-format.md +255 -0
- package/prompts/generation-strict.md +34 -0
- package/prompts/generation-surfacing.md +67 -0
- package/prompts/layers-default.md +13 -0
- package/prompts/quality-standards.md +32 -0
- package/skills/pr-review-canvas/SKILL.md +177 -0
- package/src/acpx/acpx.ts +530 -0
- package/src/acpx/agents.ts +85 -0
- package/src/acpx/events.ts +216 -0
- package/src/acpx/ndjson.ts +69 -0
- package/src/acpx/preflight.ts +44 -0
- package/src/canvas/export.ts +95 -0
- package/src/canvas/import.ts +138 -0
- package/src/canvas/name.ts +55 -0
- package/src/canvas/zip.ts +123 -0
- package/src/chat/chat-manager.ts +389 -0
- package/src/chat/context.ts +160 -0
- package/src/chat/seed.ts +71 -0
- package/src/chat/threads.ts +114 -0
- package/src/cli.ts +199 -0
- package/src/commands.ts +424 -0
- package/src/config.ts +142 -0
- package/src/contract/api.ts +190 -0
- package/src/contract/canvas-manifest.ts +29 -0
- package/src/contract/chat.ts +76 -0
- package/src/contract/comments.ts +96 -0
- package/src/contract/discovery.ts +20 -0
- package/src/contract/generation-context.ts +77 -0
- package/src/contract/keys.ts +14 -0
- package/src/contract/links.ts +5 -0
- package/src/contract/mermaid-fences.ts +4 -0
- package/src/contract/review-artifact.ts +324 -0
- package/src/contract/settings.ts +144 -0
- package/src/contract/state.ts +46 -0
- package/src/contract/validation.ts +43 -0
- package/src/git/diff-collector.ts +151 -0
- package/src/git/git.ts +115 -0
- package/src/git/lang.ts +1 -0
- package/src/git/materialize.ts +79 -0
- package/src/git/patch-lines.ts +60 -0
- package/src/github/attachments.ts +288 -0
- package/src/github/capabilities.ts +112 -0
- package/src/github/comments.ts +132 -0
- package/src/github/gh.ts +196 -0
- package/src/github/post-comment.ts +104 -0
- package/src/github/post-review.ts +44 -0
- package/src/github/pr.ts +133 -0
- package/src/github/review-body.ts +72 -0
- package/src/github/threads.ts +63 -0
- package/src/paths.ts +10 -0
- package/src/project-config.ts +219 -0
- package/src/prompt-files.ts +26 -0
- package/src/review/diagram-nodes.ts +227 -0
- package/src/review/doctor.ts +139 -0
- package/src/review/glob.ts +33 -0
- package/src/review/install-skill.ts +107 -0
- package/src/review/normalize.ts +209 -0
- package/src/review/prepare.ts +165 -0
- package/src/review/prompt.ts +233 -0
- package/src/review/publish.ts +209 -0
- package/src/review/skill-command.ts +4 -0
- package/src/review/test-paths.ts +32 -0
- package/src/review/text-length.ts +15 -0
- package/src/review/trim-caps.ts +114 -0
- package/src/review/validate-folds.ts +110 -0
- package/src/review/validate.ts +520 -0
- package/src/server/app.ts +46 -0
- package/src/server/bundle.ts +266 -0
- package/src/server/capped-body.ts +62 -0
- package/src/server/context.ts +174 -0
- package/src/server/env.ts +7 -0
- package/src/server/errors.ts +65 -0
- package/src/server/html.ts +140 -0
- package/src/server/node-server.ts +42 -0
- package/src/server/routes/api.ts +256 -0
- package/src/server/routes/chat-routes.ts +221 -0
- package/src/server/routes/pages.ts +64 -0
- package/src/server/routes/review-routes.ts +245 -0
- package/src/server/routes/static.ts +114 -0
- package/src/server/security.ts +104 -0
- package/src/server/sse.ts +67 -0
- package/src/store/atomic-json.ts +68 -0
- package/src/store/canvas-store.ts +120 -0
- package/src/store/data-dir.ts +29 -0
- package/src/store/derived-store.ts +93 -0
- package/src/store/pr-store.ts +69 -0
- package/src/store/settings-store.ts +152 -0
- package/src/store/state-store.ts +121 -0
- package/static/js/anchors.js +141 -0
- package/static/js/api.js +542 -0
- package/static/js/app.js +418 -0
- package/static/js/ask.js +35 -0
- package/static/js/chat-context.js +137 -0
- package/static/js/chat-scroll.js +114 -0
- package/static/js/chat.js +843 -0
- package/static/js/code-folds.js +200 -0
- package/static/js/commands.js +110 -0
- package/static/js/comment-link.js +37 -0
- package/static/js/composer.js +241 -0
- package/static/js/contract-types.d.ts +59 -0
- package/static/js/deep-link.js +160 -0
- package/static/js/diagram.js +582 -0
- package/static/js/diff-decorations.js +204 -0
- package/static/js/diff-renderer.js +860 -0
- package/static/js/dom.js +145 -0
- package/static/js/download.js +52 -0
- package/static/js/empty-state.js +161 -0
- package/static/js/errors.js +135 -0
- package/static/js/fences.js +90 -0
- package/static/js/header.js +134 -0
- package/static/js/hunks.js +62 -0
- package/static/js/import-zone.js +95 -0
- package/static/js/interactions.js +952 -0
- package/static/js/keyboard.js +131 -0
- package/static/js/keys.js +97 -0
- package/static/js/lang.js +54 -0
- package/static/js/layers.js +596 -0
- package/static/js/links.js +150 -0
- package/static/js/markdown.js +232 -0
- package/static/js/mermaid-fences.js +55 -0
- package/static/js/nav.js +91 -0
- package/static/js/overview.js +85 -0
- package/static/js/points.js +247 -0
- package/static/js/progress.js +49 -0
- package/static/js/proposed-comment.js +133 -0
- package/static/js/quick-questions.js +216 -0
- package/static/js/regenerate.js +69 -0
- package/static/js/review-session.js +257 -0
- package/static/js/scroll-spy.js +66 -0
- package/static/js/selection.js +193 -0
- package/static/js/settings.js +206 -0
- package/static/js/signoff.js +171 -0
- package/static/js/skin.js +56 -0
- package/static/js/store.js +35 -0
- package/static/js/theme.js +56 -0
- package/static/js/threads.js +78 -0
- package/static/js/vendor.d.ts +15 -0
- package/static/styles/base.css +223 -0
- package/static/styles/chat-tools.css +130 -0
- package/static/styles/chat.css +140 -0
- package/static/styles/commands.css +156 -0
- package/static/styles/diff.css +258 -0
- package/static/styles/header.css +114 -0
- package/static/styles/layout.css +123 -0
- package/static/styles/panels.css +152 -0
- package/static/styles/responsive.css +80 -0
- package/static/styles/review-actions.css +124 -0
- package/static/styles/review.css +473 -0
- package/static/styles/skin-github.css +356 -0
- package/static/styles.css +14 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/** Character caps applied to model output. Numbers, so the prompt can print them. */
|
|
4
|
+
export const TEXT_CAPS = {
|
|
5
|
+
summary: 1200,
|
|
6
|
+
layerTitle: 60,
|
|
7
|
+
rationale: 300,
|
|
8
|
+
decisions: 600,
|
|
9
|
+
checkByHand: 400,
|
|
10
|
+
annotation: 240,
|
|
11
|
+
pointTitle: 90,
|
|
12
|
+
pointBody: 600,
|
|
13
|
+
testBehavior: 120,
|
|
14
|
+
/** Mermaid source. Measured raw, since it is code and not text a reader reads. */
|
|
15
|
+
diagram: 1500,
|
|
16
|
+
} as const
|
|
17
|
+
export type TextCaps = { -readonly [K in keyof typeof TEXT_CAPS]: number }
|
|
18
|
+
|
|
19
|
+
export const LIMITS = { maxPoints: 12, maxDiagramsPerLayer: 1, maxDiagramLinks: 12 } as const
|
|
20
|
+
export type Limits = { -readonly [K in keyof typeof LIMITS]: number }
|
|
21
|
+
|
|
22
|
+
export const TEXT_CAP_KEYS = [
|
|
23
|
+
'summary',
|
|
24
|
+
'layerTitle',
|
|
25
|
+
'rationale',
|
|
26
|
+
'decisions',
|
|
27
|
+
'checkByHand',
|
|
28
|
+
'annotation',
|
|
29
|
+
'pointTitle',
|
|
30
|
+
'pointBody',
|
|
31
|
+
'testBehavior',
|
|
32
|
+
'diagram',
|
|
33
|
+
] as const satisfies ReadonlyArray<keyof TextCaps>
|
|
34
|
+
|
|
35
|
+
function mapCaps(fn: (key: keyof TextCaps) => number): TextCaps {
|
|
36
|
+
return {
|
|
37
|
+
summary: fn('summary'),
|
|
38
|
+
layerTitle: fn('layerTitle'),
|
|
39
|
+
rationale: fn('rationale'),
|
|
40
|
+
decisions: fn('decisions'),
|
|
41
|
+
checkByHand: fn('checkByHand'),
|
|
42
|
+
annotation: fn('annotation'),
|
|
43
|
+
pointTitle: fn('pointTitle'),
|
|
44
|
+
pointBody: fn('pointBody'),
|
|
45
|
+
testBehavior: fn('testBehavior'),
|
|
46
|
+
diagram: fn('diagram'),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The project config may raise or lower a cap; the result is what the validator and the prompt use. */
|
|
51
|
+
export function effectiveCaps(overrides: { [K in keyof TextCaps]?: number | undefined } | undefined): TextCaps {
|
|
52
|
+
return mapCaps(key => overrides?.[key] ?? TEXT_CAPS[key])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The raw-text ceiling behind a cap: four times the cap. The cap itself is measured on the visible
|
|
57
|
+
* text (review/text-length.ts) by the validator; the schema only refuses pathological input.
|
|
58
|
+
*/
|
|
59
|
+
export const HARD_CAP_FACTOR = 4
|
|
60
|
+
export function hardCaps(caps: TextCaps): TextCaps {
|
|
61
|
+
return mapCaps(key => caps[key] * HARD_CAP_FACTOR)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Ceilings on a stored or imported artifact: four times the default caps and point limit. A project
|
|
66
|
+
* may raise its caps, but a zip from elsewhere must not hand the browser unbounded text.
|
|
67
|
+
*/
|
|
68
|
+
export const ARTIFACT_HARD_CAPS: TextCaps = hardCaps(TEXT_CAPS)
|
|
69
|
+
export const ARTIFACT_MAX_POINTS = LIMITS.maxPoints * HARD_CAP_FACTOR
|
|
70
|
+
|
|
71
|
+
export const FILE_STATUSES = ['added', 'modified', 'deleted', 'renamed', 'binary'] as const
|
|
72
|
+
export const POINT_KINDS = ['decision', 'risk', 'drift', 'tests', 'debt', 'question'] as const
|
|
73
|
+
export const POINT_LEVELS = ['decide', 'check', 'fyi'] as const
|
|
74
|
+
export const TEST_STATUSES = ['covered', 'missing', 'not-needed'] as const
|
|
75
|
+
export const HARNESSES = ['claude-code', 'codex', 'other'] as const
|
|
76
|
+
|
|
77
|
+
export const SideSchema = z.enum(['new', 'old'])
|
|
78
|
+
export type Side = z.infer<typeof SideSchema>
|
|
79
|
+
|
|
80
|
+
export const HunkSchema = z.object({
|
|
81
|
+
id: z.string().min(1),
|
|
82
|
+
header: z.string(),
|
|
83
|
+
oldStart: z.number().int().nonnegative(),
|
|
84
|
+
oldLines: z.number().int().nonnegative(),
|
|
85
|
+
newStart: z.number().int().nonnegative(),
|
|
86
|
+
newLines: z.number().int().nonnegative(),
|
|
87
|
+
})
|
|
88
|
+
export type Hunk = z.infer<typeof HunkSchema>
|
|
89
|
+
|
|
90
|
+
export const FileEntrySchema = z.object({
|
|
91
|
+
path: z.string().min(1),
|
|
92
|
+
oldPath: z.string().min(1).optional(),
|
|
93
|
+
key: z.string().min(1),
|
|
94
|
+
status: z.enum(FILE_STATUSES),
|
|
95
|
+
additions: z.number().int().nonnegative(),
|
|
96
|
+
deletions: z.number().int().nonnegative(),
|
|
97
|
+
lang: z.string().optional(),
|
|
98
|
+
hunks: z.array(HunkSchema),
|
|
99
|
+
})
|
|
100
|
+
export type FileEntry = z.infer<typeof FileEntrySchema>
|
|
101
|
+
|
|
102
|
+
export const RepoSchema = z.object({ owner: z.string().min(1), name: z.string().min(1) })
|
|
103
|
+
export type Repo = z.infer<typeof RepoSchema>
|
|
104
|
+
|
|
105
|
+
export const PrSchema = z.object({
|
|
106
|
+
number: z.number().int().positive().nullable(),
|
|
107
|
+
title: z.string(),
|
|
108
|
+
body: z.string(),
|
|
109
|
+
author: z.string(),
|
|
110
|
+
url: z.string(),
|
|
111
|
+
state: z.string(),
|
|
112
|
+
draft: z.boolean(),
|
|
113
|
+
/**
|
|
114
|
+
* When the pull request was last edited; the body's attachment links are ranked by it. Absent
|
|
115
|
+
* in a canvas written before this field existed, so an older zip still imports.
|
|
116
|
+
*/
|
|
117
|
+
updatedAt: z.string().optional(),
|
|
118
|
+
baseRef: z.string(),
|
|
119
|
+
headRef: z.string(),
|
|
120
|
+
headSha: z.string().regex(/^[0-9a-f]{40}$/),
|
|
121
|
+
mergeBaseSha: z.string().regex(/^[0-9a-f]{40}$/),
|
|
122
|
+
additions: z.number().int().nonnegative(),
|
|
123
|
+
deletions: z.number().int().nonnegative(),
|
|
124
|
+
changedFiles: z.number().int().nonnegative(),
|
|
125
|
+
repo: RepoSchema,
|
|
126
|
+
})
|
|
127
|
+
export type Pr = z.infer<typeof PrSchema>
|
|
128
|
+
|
|
129
|
+
export const RiskTagSchema = z.object({
|
|
130
|
+
label: z.string().min(1),
|
|
131
|
+
source: z.enum(['config', 'model']),
|
|
132
|
+
reason: z.string().optional(),
|
|
133
|
+
})
|
|
134
|
+
export type RiskTag = z.infer<typeof RiskTagSchema>
|
|
135
|
+
|
|
136
|
+
/** A link in one of the four canvas forms. Parsed and resolved by contract/links. */
|
|
137
|
+
export const LinkSchema = z.string().regex(/^#(layer|file|hunk|line):.+/)
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* One diagram of a layer. `links` maps a node id of the mermaid source to a canvas link, so a
|
|
141
|
+
* reader can click a node and land on the code it stands for. The validator checks that every key
|
|
142
|
+
* names a node of the source and that every value resolves.
|
|
143
|
+
*/
|
|
144
|
+
export function diagramSchema(caps: Caps) {
|
|
145
|
+
return z.object({
|
|
146
|
+
mermaid: z.string().min(1).max(caps.diagram),
|
|
147
|
+
links: z.record(z.string(), LinkSchema),
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
export const DiagramSchema = diagramSchema(ARTIFACT_HARD_CAPS)
|
|
151
|
+
export type Diagram = z.infer<typeof DiagramSchema>
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The model output is checked against the caps in force; the artifact is the published result of
|
|
155
|
+
* a validated output, so it reads back under the generous hard ceilings instead.
|
|
156
|
+
*/
|
|
157
|
+
type Caps = TextCaps
|
|
158
|
+
|
|
159
|
+
function text(caps: Caps, key: keyof TextCaps): z.ZodString {
|
|
160
|
+
return textOrEmpty(caps, key).min(1)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function textOrEmpty(caps: Caps, key: keyof TextCaps): z.ZodString {
|
|
164
|
+
const visibleCap = caps[key] / HARD_CAP_FACTOR
|
|
165
|
+
const diagrams = key === 'summary' || key === 'rationale'
|
|
166
|
+
? ' Mermaid fences count toward the separate diagram cap, not this prose cap.'
|
|
167
|
+
: ''
|
|
168
|
+
return z.string().max(caps[key]).meta({
|
|
169
|
+
description: `At most ${visibleCap} visible characters. Link targets, backticks, and code-fence lines do not count. maxLength is only the raw Markdown ceiling.${diagrams}`,
|
|
170
|
+
'x-visibleMaxLength': visibleCap,
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function testEntrySchema(caps: Caps) {
|
|
175
|
+
return z.object({
|
|
176
|
+
behavior: text(caps, 'testBehavior'),
|
|
177
|
+
status: z.enum(TEST_STATUSES),
|
|
178
|
+
testPath: z.string().min(1).optional(),
|
|
179
|
+
note: z.string().optional(),
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
export const TestEntrySchema = testEntrySchema(ARTIFACT_HARD_CAPS)
|
|
183
|
+
export type TestEntry = z.infer<typeof TestEntrySchema>
|
|
184
|
+
|
|
185
|
+
export function annotationSchema(caps: Caps) {
|
|
186
|
+
return z.object({
|
|
187
|
+
side: SideSchema,
|
|
188
|
+
startLine: z.number().int().positive(),
|
|
189
|
+
endLine: z.number().int().positive(),
|
|
190
|
+
text: text(caps, 'annotation'),
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
export const AnnotationSchema = annotationSchema(ARTIFACT_HARD_CAPS)
|
|
194
|
+
export type Annotation = z.infer<typeof AnnotationSchema>
|
|
195
|
+
|
|
196
|
+
function codeFoldSchema(caps: Caps) {
|
|
197
|
+
return z.object({
|
|
198
|
+
title: z.string().max(caps.pointTitle).regex(/\S/, 'A fold title must contain visible text'),
|
|
199
|
+
side: SideSchema,
|
|
200
|
+
startLine: z.number().int().positive(),
|
|
201
|
+
endLine: z.number().int().positive(),
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
export const CodeFoldSchema = codeFoldSchema(ARTIFACT_HARD_CAPS)
|
|
205
|
+
export type CodeFold = z.infer<typeof CodeFoldSchema>
|
|
206
|
+
|
|
207
|
+
function layerFileBase(caps: Caps, foldTitleCap = caps.pointTitle) {
|
|
208
|
+
return {
|
|
209
|
+
path: z.string().min(1),
|
|
210
|
+
hunks: z.array(z.string().min(1)).min(1),
|
|
211
|
+
note: textOrEmpty(caps, 'annotation').optional(),
|
|
212
|
+
annotations: z.array(annotationSchema(caps)),
|
|
213
|
+
collapsed: z.boolean().optional(),
|
|
214
|
+
folds: z.array(codeFoldSchema({ ...caps, pointTitle: foldTitleCap })).optional(),
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const LayerFileSchema = z.object({ ...layerFileBase(ARTIFACT_HARD_CAPS), isTest: z.boolean() })
|
|
219
|
+
export type LayerFile = z.infer<typeof LayerFileSchema>
|
|
220
|
+
|
|
221
|
+
export const LayerKeySchema = z
|
|
222
|
+
.string()
|
|
223
|
+
.min(1)
|
|
224
|
+
.max(40)
|
|
225
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/)
|
|
226
|
+
|
|
227
|
+
function layerBase(caps: Caps) {
|
|
228
|
+
return {
|
|
229
|
+
key: LayerKeySchema,
|
|
230
|
+
title: text(caps, 'layerTitle'),
|
|
231
|
+
rationale: textOrEmpty(caps, 'rationale'),
|
|
232
|
+
/** Markdown: decisions and trade-offs a human should agree with. */
|
|
233
|
+
decisions: text(caps, 'decisions').optional(),
|
|
234
|
+
/** Markdown: what tests cannot verify (UI feel, migrations on real data, ...). */
|
|
235
|
+
checkByHand: text(caps, 'checkByHand').optional(),
|
|
236
|
+
kind: z.enum(['layer', 'other']),
|
|
237
|
+
defaultLayerId: z.string().optional(),
|
|
238
|
+
diagram: diagramSchema(caps).optional(),
|
|
239
|
+
tests: z.array(testEntrySchema(caps)),
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export const LayerSchema = z.object({
|
|
244
|
+
...layerBase(ARTIFACT_HARD_CAPS),
|
|
245
|
+
id: z.string().min(1),
|
|
246
|
+
risk: z.array(RiskTagSchema),
|
|
247
|
+
files: z.array(LayerFileSchema).min(1),
|
|
248
|
+
})
|
|
249
|
+
export type Layer = z.infer<typeof LayerSchema>
|
|
250
|
+
|
|
251
|
+
function pointBase(caps: Caps) {
|
|
252
|
+
return {
|
|
253
|
+
kind: z.enum(POINT_KINDS),
|
|
254
|
+
level: z.enum(POINT_LEVELS),
|
|
255
|
+
title: text(caps, 'pointTitle'),
|
|
256
|
+
path: z.string().min(1),
|
|
257
|
+
line: z.number().int().positive(),
|
|
258
|
+
endLine: z.number().int().positive().optional(),
|
|
259
|
+
side: SideSchema.optional(),
|
|
260
|
+
body: text(caps, 'pointBody'),
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export const PointSchema = z.object({
|
|
265
|
+
...pointBase(ARTIFACT_HARD_CAPS),
|
|
266
|
+
id: z.string().min(1),
|
|
267
|
+
fingerprint: z.string().min(1),
|
|
268
|
+
origin: z.enum(['model', 'tests']),
|
|
269
|
+
/** The layer that owns the point's hunk; publish fills it from the file's layer. */
|
|
270
|
+
layerId: z.string().optional(),
|
|
271
|
+
})
|
|
272
|
+
export type Point = z.infer<typeof PointSchema>
|
|
273
|
+
|
|
274
|
+
export const GeneratorSchema = z.object({
|
|
275
|
+
agent: z.string().min(1),
|
|
276
|
+
model: z.string().optional(),
|
|
277
|
+
harness: z.enum(HARNESSES),
|
|
278
|
+
attempts: z.number().int().positive(),
|
|
279
|
+
})
|
|
280
|
+
export type Generator = z.infer<typeof GeneratorSchema>
|
|
281
|
+
|
|
282
|
+
export const ReviewArtifactSchema = z.object({
|
|
283
|
+
version: z.literal(1),
|
|
284
|
+
pr: PrSchema,
|
|
285
|
+
files: z.array(FileEntrySchema),
|
|
286
|
+
/** Markdown, PR-wide "what changes": behavior before and after, no required sections. */
|
|
287
|
+
summary: textOrEmpty(ARTIFACT_HARD_CAPS, 'summary'),
|
|
288
|
+
risk: z.array(RiskTagSchema),
|
|
289
|
+
layers: z.array(LayerSchema).min(1),
|
|
290
|
+
points: z.array(PointSchema).max(ARTIFACT_MAX_POINTS),
|
|
291
|
+
generatedAt: z.string(),
|
|
292
|
+
generator: GeneratorSchema,
|
|
293
|
+
source: z.enum(['local', 'import']),
|
|
294
|
+
importedAt: z.string().optional(),
|
|
295
|
+
})
|
|
296
|
+
export type ReviewArtifact = z.infer<typeof ReviewArtifactSchema>
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* What the agent writes to model.json. The schema bounds raw text at the hard limit (4x the cap);
|
|
300
|
+
* the validator measures the visible text against the cap itself. Ids, fingerprints, layerId,
|
|
301
|
+
* isTest, and config risk are added by publish. A layer may list no files here so the validator
|
|
302
|
+
* can name the problem (`LAYER_EMPTY`) instead of a schema failure; the point count is checked by
|
|
303
|
+
* the validator too, together with the points that missing tests add.
|
|
304
|
+
*/
|
|
305
|
+
export function modelOutputSchema(textCaps: TextCaps) {
|
|
306
|
+
const caps = hardCaps(textCaps)
|
|
307
|
+
return z.object({
|
|
308
|
+
summary: textOrEmpty(caps, 'summary'),
|
|
309
|
+
layers: z
|
|
310
|
+
.array(
|
|
311
|
+
z.object({
|
|
312
|
+
...layerBase(caps),
|
|
313
|
+
risk: z.array(z.object({ label: z.string().min(1), reason: z.string().min(1) })).optional(),
|
|
314
|
+
files: z.array(z.object(layerFileBase(caps, textCaps.pointTitle))),
|
|
315
|
+
})
|
|
316
|
+
)
|
|
317
|
+
.min(1),
|
|
318
|
+
points: z.array(z.object(pointBase(caps))),
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
export const ModelOutputSchema = modelOutputSchema(TEXT_CAPS)
|
|
322
|
+
export type ModelOutput = z.infer<typeof ModelOutputSchema>
|
|
323
|
+
export type ModelLayer = ModelOutput['layers'][number]
|
|
324
|
+
export type ModelPoint = ModelOutput['points'][number]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { DEFAULT_SKIN, isSkin, SKINS, type Skin } from '../../static/js/skin.js'
|
|
3
|
+
import { DEFAULT_THEME, isTheme, THEMES, type Theme } from '../../static/js/theme.js'
|
|
4
|
+
|
|
5
|
+
export type { Skin } from '../../static/js/skin.js'
|
|
6
|
+
// The skin and theme names live with the browser modules that apply them, because the page needs
|
|
7
|
+
// them before any server type could reach it.
|
|
8
|
+
export { DEFAULT_SKIN, SKINS } from '../../static/js/skin.js'
|
|
9
|
+
export type { Theme } from '../../static/js/theme.js'
|
|
10
|
+
export { DEFAULT_THEME, THEMES } from '../../static/js/theme.js'
|
|
11
|
+
|
|
12
|
+
/** The agents the chat knows how to reach through acpx. */
|
|
13
|
+
export const CHAT_AGENTS = ['claude', 'codex'] as const
|
|
14
|
+
export type ChatAgent = (typeof CHAT_AGENTS)[number]
|
|
15
|
+
|
|
16
|
+
export function isChatAgent(value: string): value is ChatAgent {
|
|
17
|
+
return (CHAT_AGENTS as readonly string[]).includes(value)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const CHAT_TIMEOUT_MIN_SEC = 30
|
|
21
|
+
export const CHAT_TIMEOUT_MAX_SEC = 3600
|
|
22
|
+
|
|
23
|
+
export const SettingsSchema = z.object({
|
|
24
|
+
version: z.literal(1),
|
|
25
|
+
skin: z.enum(SKINS),
|
|
26
|
+
theme: z.enum(THEMES),
|
|
27
|
+
agent: z.enum(CHAT_AGENTS),
|
|
28
|
+
model: z.string().min(1).nullable(),
|
|
29
|
+
chatTimeoutSec: z.number().int().min(CHAT_TIMEOUT_MIN_SEC).max(CHAT_TIMEOUT_MAX_SEC),
|
|
30
|
+
maxTurns: z.number().int().positive().max(100).nullable(),
|
|
31
|
+
})
|
|
32
|
+
export type Settings = z.infer<typeof SettingsSchema>
|
|
33
|
+
|
|
34
|
+
export const DEFAULT_SETTINGS: Settings = {
|
|
35
|
+
version: 1,
|
|
36
|
+
skin: DEFAULT_SKIN,
|
|
37
|
+
theme: DEFAULT_THEME,
|
|
38
|
+
agent: 'claude',
|
|
39
|
+
model: null,
|
|
40
|
+
chatTimeoutSec: 600,
|
|
41
|
+
maxTurns: null,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** What `PUT /api/settings` accepts: every field optional, the rest stays as it was. */
|
|
45
|
+
export const SettingsInputSchema = z.object({
|
|
46
|
+
skin: z.enum(SKINS).optional(),
|
|
47
|
+
theme: z.enum(THEMES).optional(),
|
|
48
|
+
agent: z.enum(CHAT_AGENTS).optional(),
|
|
49
|
+
model: z.string().max(200).nullable().optional(),
|
|
50
|
+
chatTimeoutSec: z.number().int().min(CHAT_TIMEOUT_MIN_SEC).max(CHAT_TIMEOUT_MAX_SEC).optional(),
|
|
51
|
+
maxTurns: z.number().int().positive().max(100).nullable().optional(),
|
|
52
|
+
})
|
|
53
|
+
export type SettingsInput = z.infer<typeof SettingsInputSchema>
|
|
54
|
+
|
|
55
|
+
/** How the page is painted: the skin, and light or dark. Both live in the settings file. */
|
|
56
|
+
export interface Appearance {
|
|
57
|
+
skin: Skin
|
|
58
|
+
theme: Theme
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The `?skin` and `?theme` of one request, either of which may be absent. */
|
|
62
|
+
export interface AppearanceQuery {
|
|
63
|
+
skin?: string | undefined
|
|
64
|
+
theme?: string | undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The appearance for one request: `?skin=` and `?theme=` win over the saved ones, for that load. */
|
|
68
|
+
export function appearanceForRequest(saved: Appearance, query: AppearanceQuery): Appearance {
|
|
69
|
+
return {
|
|
70
|
+
skin: isSkin(query.skin) ? query.skin : saved.skin,
|
|
71
|
+
theme: isTheme(query.theme) ? query.theme : saved.theme,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* What `PUT /api/appearance` accepts. It is its own route, apart from the chat settings, because a
|
|
77
|
+
* repository with chat turned off still has a page to paint. A body that names neither is refused
|
|
78
|
+
* rather than saved as a no-op.
|
|
79
|
+
*/
|
|
80
|
+
export const AppearanceInputSchema = z
|
|
81
|
+
.object({ skin: z.enum(SKINS).optional(), theme: z.enum(THEMES).optional() })
|
|
82
|
+
.refine(input => input.skin !== undefined || input.theme !== undefined, {
|
|
83
|
+
message: 'name a skin, a theme, or both',
|
|
84
|
+
})
|
|
85
|
+
export type AppearanceInput = z.infer<typeof AppearanceInputSchema>
|
|
86
|
+
|
|
87
|
+
/** How the page is painted right now, as the server has it saved. */
|
|
88
|
+
export type AppearanceResponse = Appearance
|
|
89
|
+
|
|
90
|
+
/** A `serve --agent/--model` flag wins over the file, and the dialog says so. */
|
|
91
|
+
export interface SettingsOverrides {
|
|
92
|
+
agent?: ChatAgent
|
|
93
|
+
model?: string
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface SettingsResponse {
|
|
97
|
+
settings: Settings
|
|
98
|
+
/** The flags that win over the file for this run. */
|
|
99
|
+
overrides: SettingsOverrides
|
|
100
|
+
/** Absolute path of `.pr-review/settings.yml`, shown in the dialog. */
|
|
101
|
+
file: string
|
|
102
|
+
/** The committed project config, read-only in the UI. */
|
|
103
|
+
project: {
|
|
104
|
+
file: string | null
|
|
105
|
+
chatEnabled: boolean
|
|
106
|
+
rulebook: string | null
|
|
107
|
+
maxRepairRounds: number
|
|
108
|
+
inlineDiffMaxLines: number
|
|
109
|
+
smallPrHunks: number
|
|
110
|
+
layers: number
|
|
111
|
+
highRisk: number
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Whether an agent can run right now, and why not when it cannot. */
|
|
116
|
+
export interface AgentAvailability {
|
|
117
|
+
id: ChatAgent
|
|
118
|
+
/** True when the binary is on PATH and its auth check passed. */
|
|
119
|
+
available: boolean
|
|
120
|
+
installed: boolean
|
|
121
|
+
authenticated: boolean
|
|
122
|
+
reason?: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface AgentsResponse {
|
|
126
|
+
/** False when `acpx` itself is missing: the whole pane is unusable then. */
|
|
127
|
+
acpx: { installed: boolean; version: string | null }
|
|
128
|
+
agents: AgentAvailability[]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The answer of `POST /api/settings/agents/:id/probe`: one real one-shot round trip. */
|
|
132
|
+
export interface AgentProbeResult {
|
|
133
|
+
id: ChatAgent
|
|
134
|
+
ok: boolean
|
|
135
|
+
/** How long the round trip took, in milliseconds. */
|
|
136
|
+
ms: number
|
|
137
|
+
/** The agent's reply, cut to one short line; empty when it failed. */
|
|
138
|
+
reply: string
|
|
139
|
+
code?: string
|
|
140
|
+
message?: string
|
|
141
|
+
/** When this result was produced; results are reused for ten minutes. */
|
|
142
|
+
at: string
|
|
143
|
+
cached: boolean
|
|
144
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const ChatThreadSchema = z.object({
|
|
4
|
+
name: z.string(),
|
|
5
|
+
agent: z.string(),
|
|
6
|
+
rev: z.number().int(),
|
|
7
|
+
title: z.string(),
|
|
8
|
+
createdAt: z.string(),
|
|
9
|
+
seededHeadSha: z.string(),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export type ChatThread = z.infer<typeof ChatThreadSchema>
|
|
13
|
+
|
|
14
|
+
export const PrStateSchema = z.object({
|
|
15
|
+
version: z.literal(1),
|
|
16
|
+
/**
|
|
17
|
+
* Counts the writes to this file. The page uses it to tell a newer answer from an older one
|
|
18
|
+
* when two of its requests overlap. Absent in state files of older tool versions.
|
|
19
|
+
*/
|
|
20
|
+
rev: z.number().int().nonnegative().optional(),
|
|
21
|
+
reviewed: z.record(z.string(), z.literal(true)),
|
|
22
|
+
/**
|
|
23
|
+
* The commit the reviewed marks describe. A canvas for another commit describes other code,
|
|
24
|
+
* so the marks start again when the head moves. Absent in state files of older tool versions.
|
|
25
|
+
*/
|
|
26
|
+
reviewedHeadSha: z.string().optional(),
|
|
27
|
+
hiddenThreads: z.record(z.string(), z.object({ at: z.string() })),
|
|
28
|
+
posted: z.array(z.object({ commentId: z.number().int(), pointFingerprint: z.string().optional(), at: z.string() })),
|
|
29
|
+
dismissed: z.record(z.string(), z.object({ at: z.string(), reason: z.string().optional() })),
|
|
30
|
+
chat: z.object({ threads: z.array(ChatThreadSchema), activeThread: z.string().optional() }),
|
|
31
|
+
updatedAt: z.string(),
|
|
32
|
+
})
|
|
33
|
+
export type PrState = z.infer<typeof PrStateSchema>
|
|
34
|
+
|
|
35
|
+
export function emptyState(updatedAt: string): PrState {
|
|
36
|
+
return {
|
|
37
|
+
version: 1,
|
|
38
|
+
rev: 0,
|
|
39
|
+
reviewed: {},
|
|
40
|
+
hiddenThreads: {},
|
|
41
|
+
posted: [],
|
|
42
|
+
dismissed: {},
|
|
43
|
+
chat: { threads: [] },
|
|
44
|
+
updatedAt,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** The problems `pr-review validate` and `publish` report, one line each. */
|
|
2
|
+
export const VALIDATION_CODES = [
|
|
3
|
+
'SCHEMA',
|
|
4
|
+
'TEXT_TOO_LONG',
|
|
5
|
+
'HUNK_UNASSIGNED',
|
|
6
|
+
'HUNK_DUPLICATE',
|
|
7
|
+
'HUNK_UNKNOWN',
|
|
8
|
+
'PATH_UNKNOWN',
|
|
9
|
+
'LAYER_EMPTY',
|
|
10
|
+
'LAYER_KEY_DUPLICATE',
|
|
11
|
+
'OTHER_DUPLICATE',
|
|
12
|
+
'OTHER_NOT_LAST',
|
|
13
|
+
'TEST_NOT_LAST',
|
|
14
|
+
'TEST_IN_OTHER',
|
|
15
|
+
'RISK_IN_OTHER',
|
|
16
|
+
'ANNOTATION_OUTSIDE_HUNK',
|
|
17
|
+
'FOLD_INVALID',
|
|
18
|
+
'POINT_OUTSIDE_DIFF',
|
|
19
|
+
'TOO_MANY_POINTS',
|
|
20
|
+
'TEST_PATH_UNKNOWN',
|
|
21
|
+
'LINK_UNRESOLVED',
|
|
22
|
+
'DIAGRAM_NODE_UNKNOWN',
|
|
23
|
+
'DIAGRAM_LIMIT',
|
|
24
|
+
] as const
|
|
25
|
+
export type ValidationCode = (typeof VALIDATION_CODES)[number]
|
|
26
|
+
|
|
27
|
+
export interface ValidationError {
|
|
28
|
+
code: ValidationCode
|
|
29
|
+
/** One line that stands on its own, without the code. */
|
|
30
|
+
message: string
|
|
31
|
+
/** Where in the output the problem sits, for tools: `summary`, `layer:<key>`, `point:<n>`. */
|
|
32
|
+
where?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ValidationReport {
|
|
36
|
+
ok: boolean
|
|
37
|
+
errors: ValidationError[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The line a human reads: the code, then the message. */
|
|
41
|
+
export function formatValidationError(error: ValidationError): string {
|
|
42
|
+
return `${error.code} ${error.message}`
|
|
43
|
+
}
|