@shipwithai/loopnote 0.0.0-stage → 2.0.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 (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -2
  3. package/THIRD_PARTY_NOTICES.md +580 -0
  4. package/dist/bin/loopnote.mjs +5 -0
  5. package/dist/cli/confirm.js +38 -0
  6. package/dist/cli/picker.js +342 -0
  7. package/dist/cli/request.js +120 -0
  8. package/dist/cli/run.js +970 -0
  9. package/dist/contracts/api.js +640 -0
  10. package/dist/contracts/comparison.js +178 -0
  11. package/dist/contracts/draft.js +606 -0
  12. package/dist/contracts/request.js +124 -0
  13. package/dist/domain/anchor.js +122 -0
  14. package/dist/domain/comparison-key.js +51 -0
  15. package/dist/domain/errors.js +43 -0
  16. package/dist/domain/path-bytes.js +82 -0
  17. package/dist/domain/product-identity.js +66 -0
  18. package/dist/domain/source.js +4 -0
  19. package/dist/draft/mutate-draft.js +87 -0
  20. package/dist/export/render-review-markdown.js +163 -0
  21. package/dist/export/review-export.js +273 -0
  22. package/dist/git/availability.js +92 -0
  23. package/dist/git/candidates.js +413 -0
  24. package/dist/git/comparison.js +263 -0
  25. package/dist/git/exact-patch.js +702 -0
  26. package/dist/git/ignore-status.js +23 -0
  27. package/dist/git/inventory.js +160 -0
  28. package/dist/git/numstat.js +111 -0
  29. package/dist/git/objects.js +95 -0
  30. package/dist/git/raw-diff.js +84 -0
  31. package/dist/git/repository.js +144 -0
  32. package/dist/git/runner.js +145 -0
  33. package/dist/git/selector-drift.js +169 -0
  34. package/dist/git/standalone-resolution.js +174 -0
  35. package/dist/git/worktree-content.js +97 -0
  36. package/dist/git/worktree-inventory.js +391 -0
  37. package/dist/native/directory_exchange.node +0 -0
  38. package/dist/server/app.js +171 -0
  39. package/dist/server/attached-completion.js +85 -0
  40. package/dist/server/capabilities.js +1572 -0
  41. package/dist/server/draft-loader.js +134 -0
  42. package/dist/server/draft-recovery.js +5 -0
  43. package/dist/server/draft-store.js +380 -0
  44. package/dist/server/export-store.js +291 -0
  45. package/dist/server/gitignore-capability.js +197 -0
  46. package/dist/server/image-content.js +101 -0
  47. package/dist/server/lifecycle.js +72 -0
  48. package/dist/server/native-exchange-capability.js +45 -0
  49. package/dist/server/patch-snapshot.js +500 -0
  50. package/dist/server/routes.js +418 -0
  51. package/dist/server/security.js +123 -0
  52. package/dist/server/snapshot-fs.js +58 -0
  53. package/dist/server/support-client.js +76 -0
  54. package/dist/server/support-store.js +194 -0
  55. package/dist/server/worktree-snapshot.js +553 -0
  56. package/dist/web/assets/abap-08VXUWAP.js +1 -0
  57. package/dist/web/assets/apex-BWPQTe0t.js +1 -0
  58. package/dist/web/assets/azcli-Bc_sGQ0U.js +1 -0
  59. package/dist/web/assets/bat-i0X4ZdIN.js +1 -0
  60. package/dist/web/assets/bicep-B5-_aFwp.js +2 -0
  61. package/dist/web/assets/cameligo-DMUM7wLl.js +1 -0
  62. package/dist/web/assets/clojure-Cm7r79vr.js +1 -0
  63. package/dist/web/assets/codicon-ngg6Pgfi.ttf +0 -0
  64. package/dist/web/assets/coffee-Ba7i2nA0.js +1 -0
  65. package/dist/web/assets/cpp-C7h46wYY.js +1 -0
  66. package/dist/web/assets/csharp-BKxtCVv1.js +1 -0
  67. package/dist/web/assets/csp-bTuwJoIa.js +1 -0
  68. package/dist/web/assets/css-DIMkf-bt.js +3 -0
  69. package/dist/web/assets/css.worker-yKcq13f2.js +89 -0
  70. package/dist/web/assets/cssMode-CpBTfhMg.js +1 -0
  71. package/dist/web/assets/cypher-CVaqCwHa.js +1 -0
  72. package/dist/web/assets/dart-onAF5SnQ.js +1 -0
  73. package/dist/web/assets/dockerfile-DZFCIeNp.js +1 -0
  74. package/dist/web/assets/ecl-D05T4iGw.js +1 -0
  75. package/dist/web/assets/editor.worker-tytyn86m.js +26 -0
  76. package/dist/web/assets/elixir-6RTg0lbw.js +1 -0
  77. package/dist/web/assets/flow9-C5_-GSwl.js +1 -0
  78. package/dist/web/assets/freemarker2-skQ63wb7.js +3 -0
  79. package/dist/web/assets/fsharp-C8Ef5oNN.js +1 -0
  80. package/dist/web/assets/go-C-y9NEjX.js +1 -0
  81. package/dist/web/assets/graphql-fmXr3nnJ.js +1 -0
  82. package/dist/web/assets/handlebars-CqvPAZY8.js +1 -0
  83. package/dist/web/assets/hcl-CpzslTdj.js +1 -0
  84. package/dist/web/assets/html-BDAqstpo.js +1 -0
  85. package/dist/web/assets/html.worker-DSJ9zmd7.js +502 -0
  86. package/dist/web/assets/htmlMode-BRYA3pnT.js +1 -0
  87. package/dist/web/assets/index-CCeYBqPo.js +1111 -0
  88. package/dist/web/assets/index-CRj9ojWJ.css +1 -0
  89. package/dist/web/assets/ini-sBoK_t0W.js +1 -0
  90. package/dist/web/assets/java-BEtHBSE6.js +1 -0
  91. package/dist/web/assets/javascript-1Ywytw67.js +1 -0
  92. package/dist/web/assets/json.worker-Oa9UrkKE.js +58 -0
  93. package/dist/web/assets/jsonMode-AO1C_0kX.js +7 -0
  94. package/dist/web/assets/julia-Bri6UV-V.js +1 -0
  95. package/dist/web/assets/kotlin-BOotOW0E.js +1 -0
  96. package/dist/web/assets/less-B9JPFI3C.js +2 -0
  97. package/dist/web/assets/lexon-CfSJPG6W.js +1 -0
  98. package/dist/web/assets/liquid-CkkqyyGc.js +1 -0
  99. package/dist/web/assets/lspLanguageFeatures-CYOFEMZp.js +4 -0
  100. package/dist/web/assets/lua-CsQS60Ue.js +1 -0
  101. package/dist/web/assets/m3-D-oSqn_W.js +1 -0
  102. package/dist/web/assets/markdown-Cimd5fb3.js +1 -0
  103. package/dist/web/assets/mdx-Bd3Wlfqw.js +1 -0
  104. package/dist/web/assets/mips-CIPQ_RoX.js +1 -0
  105. package/dist/web/assets/msdax-DauUninz.js +1 -0
  106. package/dist/web/assets/mysql-SOo6toE5.js +1 -0
  107. package/dist/web/assets/objective-c-FvmIjYaQ.js +1 -0
  108. package/dist/web/assets/pascal-DrH0SRf2.js +1 -0
  109. package/dist/web/assets/pascaligo-D-ptJ9y-.js +1 -0
  110. package/dist/web/assets/perl-oz_6vUea.js +1 -0
  111. package/dist/web/assets/pgsql-DTj74zXo.js +1 -0
  112. package/dist/web/assets/php-nr791fC2.js +1 -0
  113. package/dist/web/assets/pla-CopQ2nXW.js +1 -0
  114. package/dist/web/assets/postiats-43DmfD33.js +1 -0
  115. package/dist/web/assets/powerquery-D3hlyOfw.js +1 -0
  116. package/dist/web/assets/powershell-DmHpPYUd.js +1 -0
  117. package/dist/web/assets/protobuf-C531GsRP.js +2 -0
  118. package/dist/web/assets/pug-Z5eAx3Zn.js +1 -0
  119. package/dist/web/assets/python-CET4IumD.js +1 -0
  120. package/dist/web/assets/qsharp-DkqhCAOL.js +1 -0
  121. package/dist/web/assets/r-BwWrilGY.js +1 -0
  122. package/dist/web/assets/razor-d-reaHeT.js +1 -0
  123. package/dist/web/assets/redis-ClamHrr6.js +1 -0
  124. package/dist/web/assets/redshift-DT7zqm-g.js +1 -0
  125. package/dist/web/assets/restructuredtext-BYgofb2h.js +1 -0
  126. package/dist/web/assets/ruby-DezsRK8O.js +1 -0
  127. package/dist/web/assets/rust-DdL9SqIa.js +1 -0
  128. package/dist/web/assets/sb-CcwsVR0C.js +1 -0
  129. package/dist/web/assets/scala-DHpiXF5c.js +1 -0
  130. package/dist/web/assets/scheme-BeGwcela.js +1 -0
  131. package/dist/web/assets/scss-gp-XZpBa.js +3 -0
  132. package/dist/web/assets/shell-CC2rA5mh.js +1 -0
  133. package/dist/web/assets/solidity-BEEn4gHE.js +1 -0
  134. package/dist/web/assets/sophia-CRfGWb83.js +1 -0
  135. package/dist/web/assets/sparql-D_Lu-MrJ.js +1 -0
  136. package/dist/web/assets/sql-NEE52Syq.js +1 -0
  137. package/dist/web/assets/st-DbInun42.js +1 -0
  138. package/dist/web/assets/swift-Bxkupp3x.js +1 -0
  139. package/dist/web/assets/systemverilog-Bz4Y3fRF.js +1 -0
  140. package/dist/web/assets/tcl-DISqw1ZD.js +1 -0
  141. package/dist/web/assets/ts.worker-BtjBFNHX.js +67734 -0
  142. package/dist/web/assets/tsMode-DLxSAtkD.js +11 -0
  143. package/dist/web/assets/twig-De2hgUGE.js +1 -0
  144. package/dist/web/assets/typescript-DprEVA-S.js +1 -0
  145. package/dist/web/assets/typespec-B8J7ngcE.js +1 -0
  146. package/dist/web/assets/vb-DV3o63ZY.js +1 -0
  147. package/dist/web/assets/wgsl-DpFanUEy.js +298 -0
  148. package/dist/web/assets/xml-B7ZwKOhJ.js +1 -0
  149. package/dist/web/assets/yaml-OQ9Asg7B.js +1 -0
  150. package/dist/web/index.html +14 -0
  151. package/package.json +87 -4
@@ -0,0 +1,606 @@
1
+ import { z } from 'zod';
2
+ import { RENAME_MAP } from '../domain/product-identity.js';
3
+ import { ChangedFileSchema, ExactPatchValidationTargetSchema, ExactPathSchema, GitObjectIdSchema, RangeReviewScopeSchema, } from './comparison.js';
4
+ import { orderExactPaths, decodeBase64url } from '../domain/path-bytes.js';
5
+ export const RevisionSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
6
+ export const CommentIdSchema = z.string().regex(/^comment_[0-9a-f-]{36}$/u);
7
+ export const CommentBodySchema = z
8
+ .string()
9
+ .max(100_000)
10
+ .refine((value) => value.trim().length > 0, 'Comment body must not be blank.');
11
+ export const SummaryMarkdownSchema = z.string().max(100_000);
12
+ const AnchorSideSchema = z.enum(['base', 'head']);
13
+ const AnchorLineSchema = z
14
+ .strictObject({
15
+ line: z.number().int().positive(),
16
+ text: z.string(),
17
+ })
18
+ .readonly();
19
+ export const DurableAnchorV1Schema = z
20
+ .strictObject({
21
+ version: z.literal('durable-anchor-v1'),
22
+ path: ExactPathSchema,
23
+ safeDisplayPath: z.string(),
24
+ side: AnchorSideSchema,
25
+ line: z.number().int().positive(),
26
+ endLine: z.number().int().positive().optional(),
27
+ blobOid: GitObjectIdSchema,
28
+ selectedText: z.string(),
29
+ context: z
30
+ .strictObject({
31
+ before: z.array(AnchorLineSchema).max(3).readonly(),
32
+ target: AnchorLineSchema,
33
+ after: z.array(AnchorLineSchema).max(3).readonly(),
34
+ })
35
+ .readonly(),
36
+ contextHash: z
37
+ .strictObject({
38
+ algorithm: z.literal('sha256-v1'),
39
+ value: z.string().regex(/^[0-9a-f]{64}$/),
40
+ })
41
+ .readonly(),
42
+ uniqueKey: z.string().regex(/^[0-9a-f]{64}$/),
43
+ })
44
+ .superRefine((value, context) => {
45
+ if (value.endLine !== undefined && value.endLine < value.line) {
46
+ context.addIssue({ code: 'custom', message: 'endLine must be >= line.', path: ['endLine'] });
47
+ }
48
+ })
49
+ .readonly();
50
+ export const AnchorVerificationSchema = z
51
+ .strictObject({
52
+ state: z.enum(['verified', 'stale', 'orphaned']),
53
+ reason: z.enum(['exact-match', 'anchor-mismatch', 'anchor-unavailable']),
54
+ })
55
+ .readonly();
56
+ const PinnedDraftComparisonSchema = z
57
+ .strictObject({
58
+ baseCommitOid: GitObjectIdSchema,
59
+ headCommitOid: GitObjectIdSchema,
60
+ mergeBaseOid: GitObjectIdSchema,
61
+ range: RangeReviewScopeSchema.optional(),
62
+ })
63
+ .superRefine((comparison, context) => {
64
+ if (comparison.range !== undefined &&
65
+ (comparison.range.baseOid !== comparison.baseCommitOid ||
66
+ comparison.range.headOid !== comparison.headCommitOid)) {
67
+ context.addIssue({
68
+ code: 'custom',
69
+ message: 'Range provenance must match draft comparison endpoints.',
70
+ path: ['range'],
71
+ });
72
+ }
73
+ })
74
+ .readonly();
75
+ const ExactPatchDraftComparisonSchema = z
76
+ .strictObject({
77
+ kind: z.literal('exact-patch'),
78
+ digest: z.string().regex(/^[0-9a-f]{64}$/u),
79
+ validationTarget: ExactPatchValidationTargetSchema,
80
+ reviewKey: z.string().regex(/^[0-9a-f]{64}$/u),
81
+ })
82
+ .readonly();
83
+ const UncommittedDraftComparisonSchema = z
84
+ .strictObject({
85
+ kind: z.literal('uncommitted'),
86
+ sourceKind: z.enum(['staged', 'unstaged', 'worktree', 'standalone']),
87
+ baseCommitOid: GitObjectIdSchema.optional(),
88
+ reviewKey: z.string().regex(/^[0-9a-f]{64}$/u),
89
+ snapshotChain: z.array(z.string().regex(/^[0-9a-f]{64}$/u)).min(1).readonly(),
90
+ })
91
+ .superRefine((comparison, context) => {
92
+ const hasBase = comparison.baseCommitOid !== undefined;
93
+ const isNoBaseKind = comparison.sourceKind === 'unstaged' || comparison.sourceKind === 'standalone';
94
+ if (isNoBaseKind && hasBase) {
95
+ context.addIssue({
96
+ code: 'custom',
97
+ message: 'Unstaged and Standalone comparisons have no base.',
98
+ path: ['baseCommitOid'],
99
+ });
100
+ }
101
+ if (!isNoBaseKind && !hasBase) {
102
+ context.addIssue({
103
+ code: 'custom',
104
+ message: 'Staged and Working tree comparisons require a base commit.',
105
+ path: ['baseCommitOid'],
106
+ });
107
+ }
108
+ })
109
+ .readonly();
110
+ export const DraftComparisonSchema = z.union([PinnedDraftComparisonSchema, ExactPatchDraftComparisonSchema, UncommittedDraftComparisonSchema]).readonly();
111
+ const DraftCommentSchema = z
112
+ .discriminatedUnion('state', [
113
+ z.strictObject({
114
+ id: CommentIdSchema,
115
+ state: z.literal('open'),
116
+ body: CommentBodySchema,
117
+ anchor: DurableAnchorV1Schema,
118
+ createdAt: z.string().datetime(),
119
+ updatedAt: z.string().datetime(),
120
+ }),
121
+ z.strictObject({
122
+ id: CommentIdSchema,
123
+ state: z.literal('resolved'),
124
+ body: CommentBodySchema,
125
+ anchor: DurableAnchorV1Schema,
126
+ createdAt: z.string().datetime(),
127
+ updatedAt: z.string().datetime(),
128
+ resolvedAt: z.string().datetime(),
129
+ }),
130
+ ])
131
+ .readonly();
132
+ export const CURRENT_DRAFT_SCHEMA_VERSION = 1;
133
+ export const DraftVersionEnvelopeSchema = z
134
+ .object({ schemaVersion: z.number().int().positive() })
135
+ .passthrough()
136
+ .readonly();
137
+ export const ReviewDraftV1Schema = z
138
+ .strictObject({
139
+ schemaVersion: z.literal(CURRENT_DRAFT_SCHEMA_VERSION),
140
+ comparison: DraftComparisonSchema,
141
+ revision: RevisionSchema,
142
+ summary: SummaryMarkdownSchema,
143
+ comments: z.array(DraftCommentSchema).max(10_000).readonly(),
144
+ })
145
+ .superRefine((draft, context) => {
146
+ const ids = new Set();
147
+ const anchors = new Set();
148
+ for (const [index, comment] of draft.comments.entries()) {
149
+ if (ids.has(comment.id)) {
150
+ context.addIssue({ code: 'custom', message: 'Comment IDs must be unique.', path: ['comments', index, 'id'] });
151
+ }
152
+ if (anchors.has(comment.anchor.uniqueKey)) {
153
+ context.addIssue({ code: 'custom', message: 'Comments must have unique anchors.', path: ['comments', index, 'anchor', 'uniqueKey'] });
154
+ }
155
+ ids.add(comment.id);
156
+ anchors.add(comment.anchor.uniqueKey);
157
+ }
158
+ })
159
+ .readonly();
160
+ function containsLoneSurrogate(value) {
161
+ if (typeof value === 'string') {
162
+ return /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value);
163
+ }
164
+ if (Array.isArray(value))
165
+ return value.some(containsLoneSurrogate);
166
+ if (value !== null && typeof value === 'object')
167
+ return Object.values(value).some(containsLoneSurrogate);
168
+ return false;
169
+ }
170
+ const ExportStringSchema = z.string().refine((value) => !containsLoneSurrogate(value), 'Strings must not contain lone UTF-16 surrogate code units.');
171
+ function hasRepositoryRelativePath(path) {
172
+ let bytes;
173
+ try {
174
+ bytes = decodeBase64url(path.bytesBase64url);
175
+ }
176
+ catch {
177
+ return false;
178
+ }
179
+ const values = [path.display, ...(path.utf8 === undefined ? [] : [path.utf8])];
180
+ return (bytes.length > 0 &&
181
+ bytes[0] !== 0x2f &&
182
+ bytes[0] !== 0x5c &&
183
+ values.every((value) => value.length > 0 &&
184
+ !/^(?:[\\/]|[A-Za-z]:[\\/])/u.test(value) &&
185
+ !value.split('/').some((segment) => segment === '.' || segment === '..')));
186
+ }
187
+ const ExportExactPathSchema = ExactPathSchema.superRefine((path, context) => {
188
+ if (!hasRepositoryRelativePath(path)) {
189
+ context.addIssue({ code: 'custom', message: 'Export paths must be repository-relative.' });
190
+ }
191
+ });
192
+ const ExportCommentSchema = z
193
+ .strictObject({
194
+ id: CommentIdSchema,
195
+ state: z.enum(['open', 'resolved']),
196
+ body: CommentBodySchema,
197
+ createdAt: z.string().datetime(),
198
+ updatedAt: z.string().datetime(),
199
+ resolvedAt: z.string().datetime().nullable(),
200
+ verification: AnchorVerificationSchema,
201
+ anchor: DurableAnchorV1Schema,
202
+ })
203
+ .superRefine((comment, context) => {
204
+ const hasResolvedAt = comment.resolvedAt !== null;
205
+ if ((comment.state === 'resolved') !== hasResolvedAt) {
206
+ context.addIssue({
207
+ code: 'custom',
208
+ message: 'Resolved comments require resolvedAt; open comments must not have it.',
209
+ path: ['resolvedAt'],
210
+ });
211
+ }
212
+ if (!hasRepositoryRelativePath(comment.anchor.path)) {
213
+ context.addIssue({ code: 'custom', message: 'Anchor paths must be repository-relative.', path: ['anchor', 'path'] });
214
+ }
215
+ })
216
+ .readonly();
217
+ const ExportFileSchema = z
218
+ .strictObject({
219
+ path: ExportExactPathSchema,
220
+ comments: z.array(ExportCommentSchema).readonly(),
221
+ })
222
+ .readonly();
223
+ export function orderUtf16CodeUnits(left, right) {
224
+ const sharedLength = Math.min(left.length, right.length);
225
+ for (let index = 0; index < sharedLength; index += 1) {
226
+ const difference = left.charCodeAt(index) - right.charCodeAt(index);
227
+ if (difference !== 0)
228
+ return difference;
229
+ }
230
+ return left.length - right.length;
231
+ }
232
+ export function orderReviewExportComments(left, right) {
233
+ const sideDifference = (left.anchor.side === 'base' ? 0 : 1) - (right.anchor.side === 'base' ? 0 : 1);
234
+ if (sideDifference !== 0)
235
+ return sideDifference;
236
+ const lineDifference = left.anchor.line - right.anchor.line;
237
+ if (lineDifference !== 0)
238
+ return lineDifference;
239
+ const blobDifference = orderUtf16CodeUnits(left.anchor.blobOid, right.anchor.blobOid);
240
+ if (blobDifference !== 0)
241
+ return blobDifference;
242
+ const contextDifference = orderUtf16CodeUnits(left.anchor.contextHash.value, right.anchor.contextHash.value);
243
+ if (contextDifference !== 0)
244
+ return contextDifference;
245
+ return orderUtf16CodeUnits(left.id, right.id);
246
+ }
247
+ const ExportCountsSchema = z
248
+ .strictObject({
249
+ all: z.number().int().nonnegative(),
250
+ openActionable: z.number().int().nonnegative(),
251
+ openNeedsAttention: z.number().int().nonnegative(),
252
+ resolved: z.number().int().nonnegative(),
253
+ })
254
+ .readonly();
255
+ const ExportEndpointSchema = z
256
+ .strictObject({
257
+ label: ExportStringSchema.min(1),
258
+ launchOid: GitObjectIdSchema,
259
+ })
260
+ .readonly();
261
+ const ExportDriftEndpointSchema = z
262
+ .strictObject({
263
+ launchOid: GitObjectIdSchema,
264
+ currentOid: GitObjectIdSchema.nullable(),
265
+ status: z.enum(['unchanged', 'moved', 'unavailable']),
266
+ })
267
+ .superRefine((endpoint, context) => {
268
+ if ((endpoint.status === 'unavailable') !== (endpoint.currentOid === null)) {
269
+ context.addIssue({ code: 'custom', message: 'Unavailable drift endpoints have no current OID.', path: ['currentOid'] });
270
+ }
271
+ if (endpoint.status === 'unchanged' && endpoint.currentOid !== endpoint.launchOid) {
272
+ context.addIssue({ code: 'custom', message: 'Unchanged drift endpoints retain their launch OID.', path: ['currentOid'] });
273
+ }
274
+ })
275
+ .readonly();
276
+ export const ReviewExportV1Schema = z
277
+ .strictObject({
278
+ schemaVersion: z.literal(1),
279
+ kind: z.literal(`${RENAME_MAP.kindPrefix}/export`),
280
+ exportedAt: z.string().datetime(),
281
+ acceptedDraftRevision: RevisionSchema,
282
+ comparison: z
283
+ .strictObject({
284
+ selectedBase: ExportEndpointSchema,
285
+ selectedHead: ExportEndpointSchema,
286
+ mergeBaseOid: GitObjectIdSchema,
287
+ comparisonKey: z.string().regex(/^[0-9a-f]{64}$/u),
288
+ })
289
+ .readonly(),
290
+ drift: z
291
+ .strictObject({
292
+ observedAt: z.string().datetime(),
293
+ acknowledged: z.boolean(),
294
+ base: ExportDriftEndpointSchema,
295
+ head: ExportDriftEndpointSchema,
296
+ })
297
+ .readonly(),
298
+ summary: z.strictObject({ markdown: ExportStringSchema.min(1).nullable() }).readonly(),
299
+ files: z.array(ExportFileSchema).readonly(),
300
+ counts: ExportCountsSchema,
301
+ })
302
+ .superRefine((document, context) => {
303
+ if (containsLoneSurrogate(document)) {
304
+ context.addIssue({ code: 'custom', message: 'Export strings must not contain lone UTF-16 surrogate code units.' });
305
+ }
306
+ for (const [fileIndex, file] of document.files.entries()) {
307
+ if (file.comments.length === 0) {
308
+ context.addIssue({ code: 'custom', message: 'Export file groups must not be empty.', path: ['files', fileIndex, 'comments'] });
309
+ }
310
+ if (fileIndex > 0 && orderExactPaths(document.files[fileIndex - 1].path, file.path) >= 0) {
311
+ context.addIssue({ code: 'custom', message: 'Export file groups must have unique exact paths in total order.', path: ['files', fileIndex, 'path'] });
312
+ }
313
+ for (const [commentIndex, comment] of file.comments.entries()) {
314
+ if (orderExactPaths(file.path, comment.anchor.path) !== 0) {
315
+ context.addIssue({ code: 'custom', message: 'Comment anchor path must match its export file group.', path: ['files', fileIndex, 'comments', commentIndex, 'anchor', 'path'] });
316
+ }
317
+ if (commentIndex > 0 && orderReviewExportComments(file.comments[commentIndex - 1], comment) >= 0) {
318
+ context.addIssue({ code: 'custom', message: 'Export comments must have total anchor order.', path: ['files', fileIndex, 'comments', commentIndex] });
319
+ }
320
+ }
321
+ }
322
+ const comments = document.files.flatMap((file) => file.comments);
323
+ const ids = new Set();
324
+ for (const [index, comment] of comments.entries()) {
325
+ if (ids.has(comment.id)) {
326
+ context.addIssue({ code: 'custom', message: 'Export comment IDs must be unique.', path: ['files', index] });
327
+ }
328
+ ids.add(comment.id);
329
+ }
330
+ const counts = {
331
+ all: comments.length,
332
+ openActionable: comments.filter((comment) => comment.state === 'open' && comment.verification.state === 'verified').length,
333
+ openNeedsAttention: comments.filter((comment) => comment.state === 'open' && comment.verification.state !== 'verified').length,
334
+ resolved: comments.filter((comment) => comment.state === 'resolved').length,
335
+ };
336
+ for (const key of Object.keys(counts)) {
337
+ if (document.counts[key] !== counts[key]) {
338
+ context.addIssue({ code: 'custom', message: `Export count ${key} must match comment records.`, path: ['counts', key] });
339
+ }
340
+ }
341
+ })
342
+ .readonly();
343
+ export const ReviewExportV2Schema = z
344
+ .strictObject({
345
+ schemaVersion: z.literal(2),
346
+ kind: z.literal(`${RENAME_MAP.kindPrefix}/export`),
347
+ exportedAt: z.string().datetime(),
348
+ acceptedDraftRevision: RevisionSchema,
349
+ comparison: z
350
+ .strictObject({
351
+ selectedBase: ExportEndpointSchema,
352
+ selectedHead: ExportEndpointSchema,
353
+ mergeBaseOid: GitObjectIdSchema,
354
+ comparisonKey: z.string().regex(/^[0-9a-f]{64}$/u),
355
+ })
356
+ .readonly(),
357
+ drift: z
358
+ .strictObject({
359
+ observedAt: z.string().datetime(),
360
+ acknowledged: z.boolean(),
361
+ base: ExportDriftEndpointSchema,
362
+ head: ExportDriftEndpointSchema,
363
+ })
364
+ .readonly(),
365
+ summary: z.strictObject({ markdown: SummaryMarkdownSchema.nullable() }).readonly(),
366
+ files: z.array(ExportFileSchema).readonly(),
367
+ counts: ExportCountsSchema,
368
+ range: RangeReviewScopeSchema,
369
+ })
370
+ .superRefine((document, context) => {
371
+ const { range, ...versionOne } = document;
372
+ if (!ReviewExportV1Schema.safeParse({ ...versionOne, schemaVersion: 1 }).success) {
373
+ context.addIssue({ code: 'custom', message: 'Version 2 export must retain valid version 1 feedback.' });
374
+ }
375
+ if (range.requestedBase !== document.comparison.selectedBase.label
376
+ || range.requestedHead !== document.comparison.selectedHead.label
377
+ || range.baseOid !== document.comparison.selectedBase.launchOid
378
+ || range.headOid !== document.comparison.selectedHead.launchOid
379
+ || range.baseOid !== document.comparison.mergeBaseOid
380
+ || range.reviewKey !== document.comparison.comparisonKey) {
381
+ context.addIssue({ code: 'custom', message: 'Range provenance must match the frozen export comparison.' });
382
+ }
383
+ })
384
+ .readonly();
385
+ const ExactPatchExportScopeSchema = z
386
+ .strictObject({
387
+ digest: z.string().regex(/^[0-9a-f]{64}$/u),
388
+ validationTarget: ExactPatchValidationTargetSchema,
389
+ reviewKey: z.string().regex(/^[0-9a-f]{64}$/u),
390
+ snapshot: z
391
+ .strictObject({
392
+ status: z.enum(['unchanged', 'drifted']),
393
+ files: z.array(ChangedFileSchema).readonly(),
394
+ })
395
+ .readonly(),
396
+ })
397
+ .readonly();
398
+ export const ReviewExportV3Schema = z
399
+ .strictObject({
400
+ schemaVersion: z.literal(3),
401
+ kind: z.literal(`${RENAME_MAP.kindPrefix}/export`),
402
+ exportedAt: z.string().datetime(),
403
+ acceptedDraftRevision: RevisionSchema,
404
+ patch: ExactPatchExportScopeSchema,
405
+ summary: z.strictObject({ markdown: ExportStringSchema.min(1).nullable() }).readonly(),
406
+ files: z.array(ExportFileSchema).readonly(),
407
+ counts: ExportCountsSchema,
408
+ })
409
+ .superRefine((document, context) => {
410
+ const feedback = ReviewExportV1Schema.safeParse({
411
+ schemaVersion: 1,
412
+ kind: document.kind,
413
+ exportedAt: document.exportedAt,
414
+ acceptedDraftRevision: document.acceptedDraftRevision,
415
+ comparison: {
416
+ selectedBase: { label: 'preimage', launchOid: '0'.repeat(40) },
417
+ selectedHead: { label: 'postimage', launchOid: '0'.repeat(40) },
418
+ mergeBaseOid: '0'.repeat(40),
419
+ comparisonKey: '0'.repeat(64),
420
+ },
421
+ drift: {
422
+ observedAt: document.exportedAt,
423
+ acknowledged: false,
424
+ base: { launchOid: '0'.repeat(40), currentOid: '0'.repeat(40), status: 'unchanged' },
425
+ head: { launchOid: '0'.repeat(40), currentOid: '0'.repeat(40), status: 'unchanged' },
426
+ },
427
+ summary: document.summary,
428
+ files: document.files,
429
+ counts: document.counts,
430
+ });
431
+ if (!feedback.success) {
432
+ context.addIssue({ code: 'custom', message: 'Version 3 export must retain valid review feedback.' });
433
+ }
434
+ })
435
+ .readonly();
436
+ const StandaloneExportScopeSchema = z
437
+ .strictObject({
438
+ originalArguments: z.array(z.string().min(1)).readonly(),
439
+ resolvedPaths: z.array(ExactPathSchema).readonly(),
440
+ fileDigests: z
441
+ .array(z
442
+ .strictObject({
443
+ path: ExactPathSchema,
444
+ sha256: z.string().regex(/^[0-9a-f]{64}$/u),
445
+ })
446
+ .readonly())
447
+ .readonly(),
448
+ snapshotChain: z.array(z.string().regex(/^[0-9a-f]{64}$/u)).min(1).readonly(),
449
+ reviewKey: z.string().regex(/^[0-9a-f]{64}$/u),
450
+ })
451
+ .readonly();
452
+ export const ReviewExportV4Schema = z
453
+ .strictObject({
454
+ schemaVersion: z.literal(4),
455
+ kind: z.literal(`${RENAME_MAP.kindPrefix}/export`),
456
+ exportedAt: z.string().datetime(),
457
+ acceptedDraftRevision: RevisionSchema,
458
+ standalone: StandaloneExportScopeSchema,
459
+ summary: z.strictObject({ markdown: ExportStringSchema.min(1).nullable() }).readonly(),
460
+ files: z.array(ExportFileSchema).readonly(),
461
+ counts: ExportCountsSchema,
462
+ })
463
+ .superRefine((document, context) => {
464
+ const feedback = ReviewExportV1Schema.safeParse({
465
+ schemaVersion: 1,
466
+ kind: document.kind,
467
+ exportedAt: document.exportedAt,
468
+ acceptedDraftRevision: document.acceptedDraftRevision,
469
+ comparison: {
470
+ selectedBase: { label: 'preimage', launchOid: '0'.repeat(40) },
471
+ selectedHead: { label: 'postimage', launchOid: '0'.repeat(40) },
472
+ mergeBaseOid: '0'.repeat(40),
473
+ comparisonKey: '0'.repeat(64),
474
+ },
475
+ drift: {
476
+ observedAt: document.exportedAt,
477
+ acknowledged: false,
478
+ base: { launchOid: '0'.repeat(40), currentOid: '0'.repeat(40), status: 'unchanged' },
479
+ head: { launchOid: '0'.repeat(40), currentOid: '0'.repeat(40), status: 'unchanged' },
480
+ },
481
+ summary: document.summary,
482
+ files: document.files,
483
+ counts: document.counts,
484
+ });
485
+ if (!feedback.success) {
486
+ context.addIssue({ code: 'custom', message: 'Version 4 export must retain valid review feedback.' });
487
+ }
488
+ })
489
+ .readonly();
490
+ const UncommittedExportScopeSchema = z
491
+ .strictObject({
492
+ sourceKind: z.enum(['staged', 'unstaged', 'worktree']),
493
+ baseCommitOid: GitObjectIdSchema.optional(),
494
+ pathspecs: z.array(z.string()).readonly(),
495
+ reviewKey: z.string().regex(/^[0-9a-f]{64}$/u),
496
+ snapshotChain: z.array(z.string().regex(/^[0-9a-f]{64}$/u)).min(1).readonly(),
497
+ })
498
+ .superRefine((scope, context) => {
499
+ const hasBase = scope.baseCommitOid !== undefined;
500
+ if (scope.sourceKind === 'unstaged' && hasBase) {
501
+ context.addIssue({
502
+ code: 'custom',
503
+ message: 'Unstaged comparisons have no base.',
504
+ path: ['baseCommitOid'],
505
+ });
506
+ }
507
+ if (scope.sourceKind !== 'unstaged' && !hasBase) {
508
+ context.addIssue({
509
+ code: 'custom',
510
+ message: 'Staged and Working tree comparisons require a base commit.',
511
+ path: ['baseCommitOid'],
512
+ });
513
+ }
514
+ })
515
+ .readonly();
516
+ const WorkingTreeDriftFlagSchema = z
517
+ .strictObject({
518
+ changed: z.boolean(),
519
+ paths: z.array(ExportExactPathSchema).readonly(),
520
+ })
521
+ .superRefine((flag, context) => {
522
+ if (!flag.changed && flag.paths.length !== 0) {
523
+ context.addIssue({
524
+ code: 'custom',
525
+ message: 'Unchanged working tree drift flag must list no paths.',
526
+ path: ['paths'],
527
+ });
528
+ }
529
+ if (flag.changed && flag.paths.length === 0) {
530
+ context.addIssue({
531
+ code: 'custom',
532
+ message: 'Changed working tree drift flag must list at least one path.',
533
+ path: ['paths'],
534
+ });
535
+ }
536
+ })
537
+ .readonly();
538
+ export const ReviewExportV5Schema = z
539
+ .strictObject({
540
+ schemaVersion: z.literal(5),
541
+ kind: z.literal(`${RENAME_MAP.kindPrefix}/export`),
542
+ exportedAt: z.string().datetime(),
543
+ acceptedDraftRevision: RevisionSchema,
544
+ uncommitted: UncommittedExportScopeSchema,
545
+ workingTreeChangedAfterFinalSnapshot: WorkingTreeDriftFlagSchema,
546
+ summary: z.strictObject({ markdown: ExportStringSchema.min(1).nullable() }).readonly(),
547
+ files: z.array(ExportFileSchema).readonly(),
548
+ counts: ExportCountsSchema,
549
+ })
550
+ .superRefine((document, context) => {
551
+ const feedback = ReviewExportV1Schema.safeParse({
552
+ schemaVersion: 1,
553
+ kind: document.kind,
554
+ exportedAt: document.exportedAt,
555
+ acceptedDraftRevision: document.acceptedDraftRevision,
556
+ comparison: {
557
+ selectedBase: { label: 'preimage', launchOid: '0'.repeat(40) },
558
+ selectedHead: { label: 'postimage', launchOid: '0'.repeat(40) },
559
+ mergeBaseOid: '0'.repeat(40),
560
+ comparisonKey: '0'.repeat(64),
561
+ },
562
+ drift: {
563
+ observedAt: document.exportedAt,
564
+ acknowledged: false,
565
+ base: { launchOid: '0'.repeat(40), currentOid: '0'.repeat(40), status: 'unchanged' },
566
+ head: { launchOid: '0'.repeat(40), currentOid: '0'.repeat(40), status: 'unchanged' },
567
+ },
568
+ summary: document.summary,
569
+ files: document.files,
570
+ counts: document.counts,
571
+ });
572
+ if (!feedback.success) {
573
+ context.addIssue({ code: 'custom', message: 'Version 5 export must retain valid review feedback.' });
574
+ }
575
+ })
576
+ .readonly();
577
+ export const ReviewExportV6Schema = z
578
+ .strictObject({
579
+ schemaVersion: z.literal(6),
580
+ kind: z.literal(`${RENAME_MAP.kindPrefix}/export`),
581
+ exportedAt: z.string().datetime(),
582
+ acceptedDraftRevision: RevisionSchema,
583
+ standalone: StandaloneExportScopeSchema,
584
+ workingTreeChangedAfterFinalSnapshot: WorkingTreeDriftFlagSchema,
585
+ summary: z.strictObject({ markdown: ExportStringSchema.min(1).nullable() }).readonly(),
586
+ files: z.array(ExportFileSchema).readonly(),
587
+ counts: ExportCountsSchema,
588
+ })
589
+ .superRefine((document, context) => {
590
+ const { workingTreeChangedAfterFinalSnapshot: _drift, ...rest } = document;
591
+ const feedback = ReviewExportV4Schema.safeParse({ ...rest, schemaVersion: 4 });
592
+ if (!feedback.success) {
593
+ context.addIssue({ code: 'custom', message: 'Version 6 export must retain a valid version 4 export shape.' });
594
+ }
595
+ })
596
+ .readonly();
597
+ export const DraftMutationSchema = z
598
+ .discriminatedUnion('type', [
599
+ z.strictObject({ type: z.literal('addComment'), commentId: CommentIdSchema, body: CommentBodySchema, anchor: DurableAnchorV1Schema }),
600
+ z.strictObject({ type: z.literal('editComment'), commentId: CommentIdSchema, body: CommentBodySchema }),
601
+ z.strictObject({ type: z.literal('deleteComment'), commentId: CommentIdSchema }),
602
+ z.strictObject({ type: z.literal('resolveComment'), commentId: CommentIdSchema }),
603
+ z.strictObject({ type: z.literal('reopenComment'), commentId: CommentIdSchema }),
604
+ z.strictObject({ type: z.literal('setSummary'), markdown: SummaryMarkdownSchema }),
605
+ ])
606
+ .readonly();