@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.
Files changed (157) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +192 -0
  3. package/bin/pr-review.mjs +5 -0
  4. package/docs/reference.md +340 -0
  5. package/package.json +74 -0
  6. package/pr-review.config.example.yml +68 -0
  7. package/prompts/chat-seed.md +64 -0
  8. package/prompts/generation-format.md +255 -0
  9. package/prompts/generation-strict.md +34 -0
  10. package/prompts/generation-surfacing.md +67 -0
  11. package/prompts/layers-default.md +13 -0
  12. package/prompts/quality-standards.md +32 -0
  13. package/skills/pr-review-canvas/SKILL.md +177 -0
  14. package/src/acpx/acpx.ts +530 -0
  15. package/src/acpx/agents.ts +85 -0
  16. package/src/acpx/events.ts +216 -0
  17. package/src/acpx/ndjson.ts +69 -0
  18. package/src/acpx/preflight.ts +44 -0
  19. package/src/canvas/export.ts +95 -0
  20. package/src/canvas/import.ts +138 -0
  21. package/src/canvas/name.ts +55 -0
  22. package/src/canvas/zip.ts +123 -0
  23. package/src/chat/chat-manager.ts +389 -0
  24. package/src/chat/context.ts +160 -0
  25. package/src/chat/seed.ts +71 -0
  26. package/src/chat/threads.ts +114 -0
  27. package/src/cli.ts +199 -0
  28. package/src/commands.ts +424 -0
  29. package/src/config.ts +142 -0
  30. package/src/contract/api.ts +190 -0
  31. package/src/contract/canvas-manifest.ts +29 -0
  32. package/src/contract/chat.ts +76 -0
  33. package/src/contract/comments.ts +96 -0
  34. package/src/contract/discovery.ts +20 -0
  35. package/src/contract/generation-context.ts +77 -0
  36. package/src/contract/keys.ts +14 -0
  37. package/src/contract/links.ts +5 -0
  38. package/src/contract/mermaid-fences.ts +4 -0
  39. package/src/contract/review-artifact.ts +324 -0
  40. package/src/contract/settings.ts +144 -0
  41. package/src/contract/state.ts +46 -0
  42. package/src/contract/validation.ts +43 -0
  43. package/src/git/diff-collector.ts +151 -0
  44. package/src/git/git.ts +115 -0
  45. package/src/git/lang.ts +1 -0
  46. package/src/git/materialize.ts +79 -0
  47. package/src/git/patch-lines.ts +60 -0
  48. package/src/github/attachments.ts +288 -0
  49. package/src/github/capabilities.ts +112 -0
  50. package/src/github/comments.ts +132 -0
  51. package/src/github/gh.ts +196 -0
  52. package/src/github/post-comment.ts +104 -0
  53. package/src/github/post-review.ts +44 -0
  54. package/src/github/pr.ts +133 -0
  55. package/src/github/review-body.ts +72 -0
  56. package/src/github/threads.ts +63 -0
  57. package/src/paths.ts +10 -0
  58. package/src/project-config.ts +219 -0
  59. package/src/prompt-files.ts +26 -0
  60. package/src/review/diagram-nodes.ts +227 -0
  61. package/src/review/doctor.ts +139 -0
  62. package/src/review/glob.ts +33 -0
  63. package/src/review/install-skill.ts +107 -0
  64. package/src/review/normalize.ts +209 -0
  65. package/src/review/prepare.ts +165 -0
  66. package/src/review/prompt.ts +233 -0
  67. package/src/review/publish.ts +209 -0
  68. package/src/review/skill-command.ts +4 -0
  69. package/src/review/test-paths.ts +32 -0
  70. package/src/review/text-length.ts +15 -0
  71. package/src/review/trim-caps.ts +114 -0
  72. package/src/review/validate-folds.ts +110 -0
  73. package/src/review/validate.ts +520 -0
  74. package/src/server/app.ts +46 -0
  75. package/src/server/bundle.ts +266 -0
  76. package/src/server/capped-body.ts +62 -0
  77. package/src/server/context.ts +174 -0
  78. package/src/server/env.ts +7 -0
  79. package/src/server/errors.ts +65 -0
  80. package/src/server/html.ts +140 -0
  81. package/src/server/node-server.ts +42 -0
  82. package/src/server/routes/api.ts +256 -0
  83. package/src/server/routes/chat-routes.ts +221 -0
  84. package/src/server/routes/pages.ts +64 -0
  85. package/src/server/routes/review-routes.ts +245 -0
  86. package/src/server/routes/static.ts +114 -0
  87. package/src/server/security.ts +104 -0
  88. package/src/server/sse.ts +67 -0
  89. package/src/store/atomic-json.ts +68 -0
  90. package/src/store/canvas-store.ts +120 -0
  91. package/src/store/data-dir.ts +29 -0
  92. package/src/store/derived-store.ts +93 -0
  93. package/src/store/pr-store.ts +69 -0
  94. package/src/store/settings-store.ts +152 -0
  95. package/src/store/state-store.ts +121 -0
  96. package/static/js/anchors.js +141 -0
  97. package/static/js/api.js +542 -0
  98. package/static/js/app.js +418 -0
  99. package/static/js/ask.js +35 -0
  100. package/static/js/chat-context.js +137 -0
  101. package/static/js/chat-scroll.js +114 -0
  102. package/static/js/chat.js +843 -0
  103. package/static/js/code-folds.js +200 -0
  104. package/static/js/commands.js +110 -0
  105. package/static/js/comment-link.js +37 -0
  106. package/static/js/composer.js +241 -0
  107. package/static/js/contract-types.d.ts +59 -0
  108. package/static/js/deep-link.js +160 -0
  109. package/static/js/diagram.js +582 -0
  110. package/static/js/diff-decorations.js +204 -0
  111. package/static/js/diff-renderer.js +860 -0
  112. package/static/js/dom.js +145 -0
  113. package/static/js/download.js +52 -0
  114. package/static/js/empty-state.js +161 -0
  115. package/static/js/errors.js +135 -0
  116. package/static/js/fences.js +90 -0
  117. package/static/js/header.js +134 -0
  118. package/static/js/hunks.js +62 -0
  119. package/static/js/import-zone.js +95 -0
  120. package/static/js/interactions.js +952 -0
  121. package/static/js/keyboard.js +131 -0
  122. package/static/js/keys.js +97 -0
  123. package/static/js/lang.js +54 -0
  124. package/static/js/layers.js +596 -0
  125. package/static/js/links.js +150 -0
  126. package/static/js/markdown.js +232 -0
  127. package/static/js/mermaid-fences.js +55 -0
  128. package/static/js/nav.js +91 -0
  129. package/static/js/overview.js +85 -0
  130. package/static/js/points.js +247 -0
  131. package/static/js/progress.js +49 -0
  132. package/static/js/proposed-comment.js +133 -0
  133. package/static/js/quick-questions.js +216 -0
  134. package/static/js/regenerate.js +69 -0
  135. package/static/js/review-session.js +257 -0
  136. package/static/js/scroll-spy.js +66 -0
  137. package/static/js/selection.js +193 -0
  138. package/static/js/settings.js +206 -0
  139. package/static/js/signoff.js +171 -0
  140. package/static/js/skin.js +56 -0
  141. package/static/js/store.js +35 -0
  142. package/static/js/theme.js +56 -0
  143. package/static/js/threads.js +78 -0
  144. package/static/js/vendor.d.ts +15 -0
  145. package/static/styles/base.css +223 -0
  146. package/static/styles/chat-tools.css +130 -0
  147. package/static/styles/chat.css +140 -0
  148. package/static/styles/commands.css +156 -0
  149. package/static/styles/diff.css +258 -0
  150. package/static/styles/header.css +114 -0
  151. package/static/styles/layout.css +123 -0
  152. package/static/styles/panels.css +152 -0
  153. package/static/styles/responsive.css +80 -0
  154. package/static/styles/review-actions.css +124 -0
  155. package/static/styles/review.css +473 -0
  156. package/static/styles/skin-github.css +356 -0
  157. 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'