@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,190 @@
|
|
|
1
|
+
import type { z } from 'zod'
|
|
2
|
+
import type { CanvasManifest } from './canvas-manifest.js'
|
|
3
|
+
import type { CommentsPayload, IssueComment, ReviewComment } from './comments.js'
|
|
4
|
+
import type { SharedCanvasInfoSchema } from './discovery.js'
|
|
5
|
+
import type { FileEntry, Pr, ReviewArtifact } from './review-artifact.js'
|
|
6
|
+
import type { PrState } from './state.js'
|
|
7
|
+
|
|
8
|
+
export const ERROR_CODES = [
|
|
9
|
+
'BAD_REQUEST',
|
|
10
|
+
'NOT_FOUND',
|
|
11
|
+
'FORBIDDEN_HOST',
|
|
12
|
+
'CROSS_ORIGIN',
|
|
13
|
+
'NOT_A_REPO',
|
|
14
|
+
'NO_ORIGIN',
|
|
15
|
+
'GIT_ERROR',
|
|
16
|
+
'GH_MISSING',
|
|
17
|
+
'GH_UNAUTHENTICATED',
|
|
18
|
+
'GITHUB_API_ERROR',
|
|
19
|
+
'PR_NOT_FOUND',
|
|
20
|
+
'CANVAS_NOT_FOUND',
|
|
21
|
+
'CANVAS_INVALID',
|
|
22
|
+
'CANVAS_REPO_MISMATCH',
|
|
23
|
+
'CANVAS_TOO_LARGE',
|
|
24
|
+
'CANVAS_STALE',
|
|
25
|
+
'MODEL_INVALID',
|
|
26
|
+
'SKILL_DIR_EXISTS',
|
|
27
|
+
'COMMENT_FORBIDDEN',
|
|
28
|
+
'COMMENT_LINE_NOT_IN_DIFF',
|
|
29
|
+
'SIGNOFF_INCOMPLETE',
|
|
30
|
+
'CHAT_BUSY',
|
|
31
|
+
'NOT_IMPLEMENTED',
|
|
32
|
+
'INTERNAL',
|
|
33
|
+
] as const
|
|
34
|
+
export type ErrorCode = (typeof ERROR_CODES)[number]
|
|
35
|
+
|
|
36
|
+
export interface ErrorEnvelope {
|
|
37
|
+
/** `issues` names what a rejected file got wrong, one line each, never its content. */
|
|
38
|
+
error: { code: ErrorCode; message: string; hint?: string; issues?: string[] }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type BundleStatus = 'missing' | 'ready' | 'stale' | 'error'
|
|
42
|
+
|
|
43
|
+
export interface Capabilities {
|
|
44
|
+
canComment: boolean | 'unknown'
|
|
45
|
+
tokenKind: string
|
|
46
|
+
login: string | null
|
|
47
|
+
reason?: string
|
|
48
|
+
hint?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CanvasInfo {
|
|
52
|
+
headSha: string
|
|
53
|
+
source: 'local' | 'import' | 'fixture'
|
|
54
|
+
manifest: CanvasManifest | null
|
|
55
|
+
importedAt?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** How a canvas for another commit relates to the PR head: behind it, or off a discarded branch. */
|
|
59
|
+
export type CanvasRelation = 'ancestor' | 'unrelated'
|
|
60
|
+
|
|
61
|
+
export interface StaleInfo {
|
|
62
|
+
canvasHeadSha: string
|
|
63
|
+
currentHeadSha: string
|
|
64
|
+
relation: CanvasRelation
|
|
65
|
+
commitsBehind?: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The answer of `POST /import`, of `pr-review import`, and of an imported shared canvas. */
|
|
69
|
+
export interface ImportResult {
|
|
70
|
+
status: 'ready' | 'stale' | 'exists'
|
|
71
|
+
headSha: string
|
|
72
|
+
currentHeadSha: string
|
|
73
|
+
relation?: CanvasRelation
|
|
74
|
+
commitsBehind?: number
|
|
75
|
+
/** True when the diffs of this canvas can be rebuilt from the local clone. */
|
|
76
|
+
derivable: boolean
|
|
77
|
+
warnings: string[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The answer of `POST /shared-canvas/fetch`. */
|
|
81
|
+
export interface SharedCanvasFetchResponse {
|
|
82
|
+
imported: boolean
|
|
83
|
+
status: 'ready' | 'stale' | 'missing'
|
|
84
|
+
sharedCanvas: SharedCanvasInfo | null
|
|
85
|
+
warnings: string[]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A canvas zip found on the pull request. Shaped by the schema the discovery cache stores. */
|
|
89
|
+
export type SharedCanvasInfo = z.infer<typeof SharedCanvasInfoSchema>
|
|
90
|
+
|
|
91
|
+
export interface PrBundle {
|
|
92
|
+
status: BundleStatus
|
|
93
|
+
pr: Pr
|
|
94
|
+
files: FileEntry[]
|
|
95
|
+
derivable: boolean
|
|
96
|
+
artifact?: ReviewArtifact
|
|
97
|
+
canvas?: CanvasInfo
|
|
98
|
+
stale?: StaleInfo
|
|
99
|
+
sharedCanvas?: SharedCanvasInfo
|
|
100
|
+
skillCommand: string
|
|
101
|
+
comments: CommentsPayload
|
|
102
|
+
state: PrState
|
|
103
|
+
capabilities: Capabilities
|
|
104
|
+
/** From the project config plus the acpx check; the client hides the pane when either says no. */
|
|
105
|
+
chat: ChatStatus
|
|
106
|
+
/** More than 400 files or 50 000 changed lines: the page says the canvas was capped. */
|
|
107
|
+
largePr: boolean
|
|
108
|
+
error?: ErrorEnvelope['error']
|
|
109
|
+
warnings: string[]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Every answer of a state route, so the page can replace its copy of the state in one step. */
|
|
113
|
+
export interface StateResponse {
|
|
114
|
+
prNumber: number
|
|
115
|
+
state: PrState
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type PostCommentResponse = (
|
|
119
|
+
| { kind: 'review'; comment: ReviewComment }
|
|
120
|
+
| { kind: 'issue'; comment: IssueComment }
|
|
121
|
+
) & { state: PrState }
|
|
122
|
+
|
|
123
|
+
/** The review GitHub created, as the page shows it: a link and its state. */
|
|
124
|
+
export interface ReviewSummary {
|
|
125
|
+
id: number
|
|
126
|
+
state: string
|
|
127
|
+
url: string
|
|
128
|
+
submittedAt: string | null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** What the sign-off dialog shows before anything is posted. */
|
|
132
|
+
export interface ReviewBodyResponse {
|
|
133
|
+
headSha: string
|
|
134
|
+
body: string
|
|
135
|
+
/** The titles of the layers that still need a look; approve is refused while this is not empty. */
|
|
136
|
+
unreviewed: string[]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface PostReviewResponse {
|
|
140
|
+
review: ReviewSummary
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface PatchesResponse {
|
|
144
|
+
headSha: string
|
|
145
|
+
patches: Record<string, string>
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface ContextResponse {
|
|
149
|
+
path: string
|
|
150
|
+
side: 'new' | 'old'
|
|
151
|
+
from: number
|
|
152
|
+
to: number
|
|
153
|
+
lines: string[]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface HealthCheck {
|
|
157
|
+
ok: boolean
|
|
158
|
+
detail?: string
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface HealthResponse {
|
|
162
|
+
ok: boolean
|
|
163
|
+
version: string
|
|
164
|
+
checks: {
|
|
165
|
+
git: HealthCheck
|
|
166
|
+
origin: HealthCheck
|
|
167
|
+
gh: HealthCheck
|
|
168
|
+
ghAuth: HealthCheck
|
|
169
|
+
/** Chat only; absent when the project config turns chat off. */
|
|
170
|
+
acpx?: HealthCheck
|
|
171
|
+
agentInstalled?: HealthCheck
|
|
172
|
+
agentAuth?: HealthCheck
|
|
173
|
+
}
|
|
174
|
+
repo: { owner: string; name: string } | null
|
|
175
|
+
dataDir: string
|
|
176
|
+
chat: ChatStatus
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** What the page needs to decide whether to draw the AI Chat pane, and what to say when it cannot. */
|
|
180
|
+
export interface ChatStatus {
|
|
181
|
+
enabled: boolean
|
|
182
|
+
/** False when acpx is not on PATH: the pane is hidden and a banner says why. */
|
|
183
|
+
acpx: boolean
|
|
184
|
+
agent?: string
|
|
185
|
+
model?: string | null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface HomeData {
|
|
189
|
+
recentPrs: Array<{ number: number; title: string; updatedAt: string }>
|
|
190
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { GeneratorSchema, RepoSchema } from './review-artifact.js'
|
|
3
|
+
|
|
4
|
+
export const CanvasManifestSchema = z.object({
|
|
5
|
+
formatVersion: z.literal(1),
|
|
6
|
+
tool: z.object({ name: z.string(), version: z.string() }),
|
|
7
|
+
repo: RepoSchema,
|
|
8
|
+
prNumber: z.number().int().positive().optional(),
|
|
9
|
+
headSha: z.string().regex(/^[0-9a-f]{40}$/),
|
|
10
|
+
mergeBaseSha: z.string().regex(/^[0-9a-f]{40}$/),
|
|
11
|
+
baseRef: z.string(),
|
|
12
|
+
headRef: z.string(),
|
|
13
|
+
generatedAt: z.string(),
|
|
14
|
+
generator: GeneratorSchema,
|
|
15
|
+
})
|
|
16
|
+
export type CanvasManifest = z.infer<typeof CanvasManifestSchema>
|
|
17
|
+
|
|
18
|
+
export const CanvasIndexSchema = z.object({
|
|
19
|
+
canvases: z.record(
|
|
20
|
+
z.string(),
|
|
21
|
+
z.object({
|
|
22
|
+
prNumber: z.number().int().positive().optional(),
|
|
23
|
+
generatedAt: z.string(),
|
|
24
|
+
source: z.enum(['local', 'import']),
|
|
25
|
+
importedAt: z.string().optional(),
|
|
26
|
+
})
|
|
27
|
+
),
|
|
28
|
+
})
|
|
29
|
+
export type CanvasIndex = z.infer<typeof CanvasIndexSchema>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { SideSchema } from './review-artifact.js'
|
|
3
|
+
|
|
4
|
+
// The context helpers live in static/js/chat-context.js so the browser can load them without a
|
|
5
|
+
// bundler; the server imports the same code here.
|
|
6
|
+
export type { ChatContext } from '../../static/js/chat-context.js'
|
|
7
|
+
export {
|
|
8
|
+
chatContextAttrs,
|
|
9
|
+
chatContextFromElement,
|
|
10
|
+
chatContextLabel,
|
|
11
|
+
sameChatContext,
|
|
12
|
+
WHOLE_PR,
|
|
13
|
+
} from '../../static/js/chat-context.js'
|
|
14
|
+
|
|
15
|
+
export const ChatContextSchema = z.discriminatedUnion('kind', [
|
|
16
|
+
z.object({ kind: z.literal('pr') }),
|
|
17
|
+
z.object({ kind: z.literal('layer'), layerId: z.string().min(1).max(200) }),
|
|
18
|
+
z.object({ kind: z.literal('file'), path: z.string().min(1).max(1024) }),
|
|
19
|
+
z.object({
|
|
20
|
+
kind: z.literal('lines'),
|
|
21
|
+
path: z.string().min(1).max(1024),
|
|
22
|
+
side: SideSchema,
|
|
23
|
+
start: z.number().int().positive(),
|
|
24
|
+
end: z.number().int().positive(),
|
|
25
|
+
}),
|
|
26
|
+
z.object({ kind: z.literal('point'), fingerprint: z.string().min(1).max(200) }),
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
/** A message longer than this is a paste, not a question. */
|
|
30
|
+
export const CHAT_MESSAGE_MAX = 8000
|
|
31
|
+
|
|
32
|
+
export const ChatSendSchema = z.object({
|
|
33
|
+
message: z.string().min(1).max(CHAT_MESSAGE_MAX),
|
|
34
|
+
context: ChatContextSchema,
|
|
35
|
+
/** The thread to answer in; the active one when absent. */
|
|
36
|
+
thread: z.string().min(1).max(200).optional(),
|
|
37
|
+
})
|
|
38
|
+
export type ChatSendInput = z.infer<typeof ChatSendSchema>
|
|
39
|
+
|
|
40
|
+
export const ChatThreadInputSchema = z.object({ agent: z.string().min(1).max(40).optional() })
|
|
41
|
+
|
|
42
|
+
/** One saved turn of a thread, as `chat/<name>.jsonl` stores it. */
|
|
43
|
+
export const ChatTurnSchema = z.object({
|
|
44
|
+
role: z.enum(['user', 'assistant']),
|
|
45
|
+
text: z.string(),
|
|
46
|
+
at: z.string(),
|
|
47
|
+
context: ChatContextSchema.optional(),
|
|
48
|
+
/** Why the turn ended, on an assistant turn that did not finish. */
|
|
49
|
+
incomplete: z.string().optional(),
|
|
50
|
+
})
|
|
51
|
+
export type ChatTurn = z.infer<typeof ChatTurnSchema>
|
|
52
|
+
|
|
53
|
+
export interface ChatThreadsResponse {
|
|
54
|
+
threads: Array<{ name: string; agent: string; title: string; createdAt: string }>
|
|
55
|
+
activeThread: string | null
|
|
56
|
+
/** The agent the next thread would use, from the settings plus any flag override. */
|
|
57
|
+
agent: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ChatHistoryResponse {
|
|
61
|
+
name: string
|
|
62
|
+
turns: ChatTurn[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The SSE frames the chat route writes, named the same on both sides. */
|
|
66
|
+
export const CHAT_EVENTS = ['turn', 'chunk', 'thought', 'tool', 'done', 'error', 'cancelled'] as const
|
|
67
|
+
export type ChatEventName = (typeof CHAT_EVENTS)[number]
|
|
68
|
+
|
|
69
|
+
export type ChatEvent =
|
|
70
|
+
| { event: 'turn'; thread: string; agent: string; seeded: boolean }
|
|
71
|
+
| { event: 'chunk'; text: string }
|
|
72
|
+
| { event: 'thought'; text: string }
|
|
73
|
+
| { event: 'tool'; id: string; title: string; status: string }
|
|
74
|
+
| { event: 'done'; stopReason: string }
|
|
75
|
+
| { event: 'error'; code: string; message: string; hint?: string }
|
|
76
|
+
| { event: 'cancelled' }
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { SideSchema } from './review-artifact.js'
|
|
3
|
+
|
|
4
|
+
export const ReviewCommentSchema = z.object({
|
|
5
|
+
id: z.number().int(),
|
|
6
|
+
author: z.string(),
|
|
7
|
+
avatarUrl: z.string().optional(),
|
|
8
|
+
body: z.string(),
|
|
9
|
+
path: z.string(),
|
|
10
|
+
line: z.number().int().nullable(),
|
|
11
|
+
originalLine: z.number().int().nullable(),
|
|
12
|
+
side: SideSchema,
|
|
13
|
+
startLine: z.number().int().optional(),
|
|
14
|
+
outdated: z.boolean(),
|
|
15
|
+
commitId: z.string(),
|
|
16
|
+
inReplyToId: z.number().int().optional(),
|
|
17
|
+
createdAt: z.string(),
|
|
18
|
+
/** When the comment was last edited, which is its creation time until someone edits it. */
|
|
19
|
+
updatedAt: z.string(),
|
|
20
|
+
url: z.string(),
|
|
21
|
+
resolved: z.boolean(),
|
|
22
|
+
})
|
|
23
|
+
export type ReviewComment = z.infer<typeof ReviewCommentSchema>
|
|
24
|
+
|
|
25
|
+
export const IssueCommentSchema = z.object({
|
|
26
|
+
id: z.number().int(),
|
|
27
|
+
author: z.string(),
|
|
28
|
+
avatarUrl: z.string().optional(),
|
|
29
|
+
body: z.string(),
|
|
30
|
+
createdAt: z.string(),
|
|
31
|
+
/** When the comment was last edited, which is its creation time until someone edits it. */
|
|
32
|
+
updatedAt: z.string(),
|
|
33
|
+
url: z.string(),
|
|
34
|
+
})
|
|
35
|
+
export type IssueComment = z.infer<typeof IssueCommentSchema>
|
|
36
|
+
|
|
37
|
+
export const ReviewSummarySchema = IssueCommentSchema.extend({ state: z.string() })
|
|
38
|
+
|
|
39
|
+
export const CommentsPayloadSchema = z.object({
|
|
40
|
+
fetchedAt: z.string(),
|
|
41
|
+
headSha: z.string(),
|
|
42
|
+
reviewComments: z.array(ReviewCommentSchema),
|
|
43
|
+
issueComments: z.array(IssueCommentSchema),
|
|
44
|
+
reviews: z.array(ReviewSummarySchema).optional(),
|
|
45
|
+
})
|
|
46
|
+
export type CommentsPayload = z.infer<typeof CommentsPayloadSchema>
|
|
47
|
+
|
|
48
|
+
/** GitHub rejects a comment body over 65 536 characters, so the tool rejects it first. */
|
|
49
|
+
export const COMMENT_BODY_MAX = 65536
|
|
50
|
+
|
|
51
|
+
const BodySchema = z.string().min(1).max(COMMENT_BODY_MAX)
|
|
52
|
+
const HeadShaSchema = z
|
|
53
|
+
.string()
|
|
54
|
+
.regex(/^[0-9a-f]{40}$/)
|
|
55
|
+
.optional()
|
|
56
|
+
|
|
57
|
+
/** What the page sends to `POST /api/prs/:n/comments`. */
|
|
58
|
+
export const PostCommentInputSchema = z.discriminatedUnion('kind', [
|
|
59
|
+
z.object({
|
|
60
|
+
kind: z.literal('inline'),
|
|
61
|
+
path: z.string().min(1),
|
|
62
|
+
line: z.number().int().positive(),
|
|
63
|
+
side: SideSchema,
|
|
64
|
+
startLine: z.number().int().positive().optional(),
|
|
65
|
+
body: BodySchema,
|
|
66
|
+
/** Set when the comment comes from an attention point, so the sign-off body can name it. */
|
|
67
|
+
pointFingerprint: z.string().min(1).optional(),
|
|
68
|
+
/** The commit the page was showing. The server refuses the post when the head moved on. */
|
|
69
|
+
headSha: HeadShaSchema,
|
|
70
|
+
}),
|
|
71
|
+
z.object({
|
|
72
|
+
kind: z.literal('reply'),
|
|
73
|
+
inReplyToId: z.number().int().positive(),
|
|
74
|
+
body: BodySchema,
|
|
75
|
+
headSha: HeadShaSchema,
|
|
76
|
+
}),
|
|
77
|
+
z.object({ kind: z.literal('issue'), body: BodySchema, headSha: HeadShaSchema }),
|
|
78
|
+
])
|
|
79
|
+
export type PostCommentInput = z.infer<typeof PostCommentInputSchema>
|
|
80
|
+
|
|
81
|
+
/** The posted comment: a review comment for inline and reply, an issue comment for PR-level. */
|
|
82
|
+
export type PostCommentResult = { kind: 'review'; comment: ReviewComment } | { kind: 'issue'; comment: IssueComment }
|
|
83
|
+
|
|
84
|
+
// The proposed-comment block a chat answer can carry. The parser lives in
|
|
85
|
+
// static/js/proposed-comment.js so the browser loads the same code without a bundler.
|
|
86
|
+
export type { ChatSegment, CommentTargets, ProposedComment } from '../../static/js/proposed-comment.js'
|
|
87
|
+
export {
|
|
88
|
+
PROPOSED_BODY_MAX,
|
|
89
|
+
parseProposedComment,
|
|
90
|
+
splitChatAnswer,
|
|
91
|
+
targetsFromFiles,
|
|
92
|
+
} from '../../static/js/proposed-comment.js'
|
|
93
|
+
|
|
94
|
+
export function emptyComments(headSha: string, fetchedAt: string): CommentsPayload {
|
|
95
|
+
return { fetchedAt, headSha, reviewComments: [], issueComments: [] }
|
|
96
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const SharedCanvasInfoSchema = z.object({
|
|
4
|
+
url: z.string(),
|
|
5
|
+
name: z.string(),
|
|
6
|
+
matchesHead: z.boolean(),
|
|
7
|
+
downloadable: z.boolean(),
|
|
8
|
+
reason: z.enum(['auth-required', 'not-zip', 'too-large', 'network', 'name-mismatch']).optional(),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The result of one attachment scan, kept next to the PR. `fingerprint` covers the PR body and
|
|
13
|
+
* its comments: the scan runs again when one of them changes, or when the user asks for it.
|
|
14
|
+
*/
|
|
15
|
+
export const DiscoveryCacheSchema = z.object({
|
|
16
|
+
fingerprint: z.string(),
|
|
17
|
+
checkedAt: z.string(),
|
|
18
|
+
sharedCanvas: SharedCanvasInfoSchema.nullable(),
|
|
19
|
+
})
|
|
20
|
+
export type DiscoveryCache = z.infer<typeof DiscoveryCacheSchema>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { DefaultLayerSchema, GenerationModeSchema, HighRiskRuleSchema } from '../project-config.js'
|
|
3
|
+
import { DEFAULT_TEST_PATTERNS } from '../review/test-paths.js'
|
|
4
|
+
import { FileEntrySchema, LIMITS, PrSchema, RepoSchema, type TextCaps } from './review-artifact.js'
|
|
5
|
+
|
|
6
|
+
/** What `prepare` was asked to describe: a pull request, or two refs before a PR exists. */
|
|
7
|
+
export const PrepareTargetSchema = z.discriminatedUnion('kind', [
|
|
8
|
+
z.object({ kind: z.literal('pr'), number: z.number().int().positive() }),
|
|
9
|
+
z.object({ kind: z.literal('refs'), base: z.string().min(1), head: z.string().min(1) }),
|
|
10
|
+
])
|
|
11
|
+
export type PrepareTarget = z.infer<typeof PrepareTargetSchema>
|
|
12
|
+
|
|
13
|
+
const capsShape = {
|
|
14
|
+
summary: z.number().int().positive(),
|
|
15
|
+
layerTitle: z.number().int().positive(),
|
|
16
|
+
rationale: z.number().int().positive(),
|
|
17
|
+
decisions: z.number().int().positive(),
|
|
18
|
+
checkByHand: z.number().int().positive(),
|
|
19
|
+
annotation: z.number().int().positive(),
|
|
20
|
+
pointTitle: z.number().int().positive(),
|
|
21
|
+
pointBody: z.number().int().positive(),
|
|
22
|
+
testBehavior: z.number().int().positive(),
|
|
23
|
+
diagram: z.number().int().positive(),
|
|
24
|
+
} satisfies Record<keyof TextCaps, z.ZodNumber>
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `context.json`: everything the agent and `publish` need about one prepared canvas. Written by
|
|
28
|
+
* `prepare` next to `prompt.md`; `publish` validates `model.json` against the hunk index and the
|
|
29
|
+
* caps recorded here, so a config change between the two commands does not change the rules.
|
|
30
|
+
*/
|
|
31
|
+
export const GenerationContextSchema = z.object({
|
|
32
|
+
version: z.literal(1),
|
|
33
|
+
target: PrepareTargetSchema,
|
|
34
|
+
repo: RepoSchema,
|
|
35
|
+
pr: PrSchema,
|
|
36
|
+
headSha: z.string().regex(/^[0-9a-f]{40}$/),
|
|
37
|
+
mergeBaseSha: z.string().regex(/^[0-9a-f]{40}$/),
|
|
38
|
+
canvasDir: z.string().min(1),
|
|
39
|
+
/** Absolute paths: the PR head files, the merge-base files, and one labeled patch per file. */
|
|
40
|
+
paths: z.object({ head: z.string(), base: z.string(), patches: z.string(), model: z.string() }),
|
|
41
|
+
/** The manifest: every changed file with its hunk ids and headers. */
|
|
42
|
+
files: z.array(FileEntrySchema),
|
|
43
|
+
defaultLayers: z.array(DefaultLayerSchema),
|
|
44
|
+
rulebook: z.object({ path: z.string().nullable(), text: z.string().nullable() }),
|
|
45
|
+
highRisk: z.array(HighRiskRuleSchema),
|
|
46
|
+
caps: z.object(capsShape),
|
|
47
|
+
limits: z.object({
|
|
48
|
+
maxPoints: z.number().int().positive(),
|
|
49
|
+
maxDiagramsPerLayer: z.number().int().positive(),
|
|
50
|
+
maxDiagramLinks: z.number().int().positive().default(LIMITS.maxDiagramLinks),
|
|
51
|
+
}),
|
|
52
|
+
generation: z.object({
|
|
53
|
+
mode: GenerationModeSchema.default('strict'),
|
|
54
|
+
maxRepairRounds: z.number().int().positive(),
|
|
55
|
+
inlineDiffMaxLines: z.number().int().positive(),
|
|
56
|
+
smallPrHunks: z.number().int().positive(),
|
|
57
|
+
}),
|
|
58
|
+
/** The globs that make a file a test here. Defaulted, so a context written before this
|
|
59
|
+
* field existed still reads. */
|
|
60
|
+
tests: z.object({ patterns: z.array(z.string().min(1)) }).default(() => ({ patterns: [...DEFAULT_TEST_PATTERNS] })),
|
|
61
|
+
/** At most `generation.smallPrHunks` hunks: one layer unless concerns differ, fewer annotations. */
|
|
62
|
+
smallPr: z.boolean(),
|
|
63
|
+
/** More than 400 files or 50 000 changed lines: the prompt inlines nothing and tightens the caps. */
|
|
64
|
+
largePr: z.boolean(),
|
|
65
|
+
preparedAt: z.string(),
|
|
66
|
+
})
|
|
67
|
+
export type GenerationContext = z.infer<typeof GenerationContextSchema>
|
|
68
|
+
|
|
69
|
+
export const LARGE_PR = { files: 400, lines: 50_000 } as const
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A change set big enough that the prompt stops inlining diffs, the model keeps to 6 annotations
|
|
73
|
+
* per file, and the page says so in its header.
|
|
74
|
+
*/
|
|
75
|
+
export function isLargePr(counts: { files: number; additions: number; deletions: number }): boolean {
|
|
76
|
+
return counts.files > LARGE_PR.files || counts.additions + counts.deletions > LARGE_PR.lines
|
|
77
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// One implementation for both sides: the browser loads static/js/keys.js as a module, and
|
|
2
|
+
// the server imports the same file through this re-export.
|
|
3
|
+
export {
|
|
4
|
+
buildLineId,
|
|
5
|
+
fileAnchorId,
|
|
6
|
+
hunkAnchorId,
|
|
7
|
+
hunkId,
|
|
8
|
+
layerAnchorId,
|
|
9
|
+
parseHunkId,
|
|
10
|
+
parseLineId,
|
|
11
|
+
pointAnchorId,
|
|
12
|
+
sanitizeKey,
|
|
13
|
+
uniqueKey,
|
|
14
|
+
} from '../../static/js/keys.js'
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The link parser lives in static/js/links.js so the browser can load it without a bundler;
|
|
2
|
+
// the server imports the same code here.
|
|
3
|
+
|
|
4
|
+
export type { LinkTargets, ParsedLink } from '../../static/js/links.js'
|
|
5
|
+
export { extractLinks, linkLabel, linkTargetId, parseLink, resolveLink } from '../../static/js/links.js'
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// One implementation for both sides: the browser loads static/js/mermaid-fences.js as a module,
|
|
2
|
+
// and the validator imports the same file through this re-export.
|
|
3
|
+
export type { MermaidSegment } from '../../static/js/mermaid-fences.js'
|
|
4
|
+
export { diagramKind, mermaidBlocks, splitMermaid, withoutMermaid } from '../../static/js/mermaid-fences.js'
|