@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,163 @@
1
+ import { controlSafeDisplay } from '../domain/path-bytes.js';
2
+ import { RENAME_MAP } from '../domain/product-identity.js';
3
+ import { parseCanonicalReviewExport } from './review-export.js';
4
+ function fenceFor(value) {
5
+ const runs = value.match(/`+/gu) ?? [];
6
+ const longest = runs.reduce((length, run) => Math.max(length, run.length), 2);
7
+ return '`'.repeat(longest + 1);
8
+ }
9
+ function appendFencedData(lines, label, value) {
10
+ const fence = fenceFor(value);
11
+ lines.push(label, `${fence}text`, value, fence);
12
+ }
13
+ function appendAnchor(lines, comment, exactPatch) {
14
+ const { anchor } = comment;
15
+ lines.push(`Side: \`${exactPatch ? (anchor.side === 'base' ? 'preimage' : 'postimage') : anchor.side}\``, `Recorded line: ${anchor.line} (hint only)`, `Blob: \`${anchor.blobOid}\``, `Context hash: \`${anchor.contextHash.algorithm}:${anchor.contextHash.value}\``);
16
+ appendFencedData(lines, 'Repository-relative path:', anchor.path.utf8 ?? anchor.path.bytesBase64url);
17
+ appendFencedData(lines, 'Selected text:', anchor.selectedText);
18
+ const context = [];
19
+ for (const line of anchor.context.before)
20
+ context.push(`${line.line}: ${line.text}`);
21
+ context.push(`${anchor.context.target.line}: ${anchor.context.target.text}`);
22
+ for (const line of anchor.context.after)
23
+ context.push(`${line.line}: ${line.text}`);
24
+ appendFencedData(lines, 'Nearby context:', context.join('\n'));
25
+ }
26
+ function appendComment(lines, comment, exactPatch) {
27
+ lines.push(`### ${comment.id}`);
28
+ appendAnchor(lines, comment, exactPatch);
29
+ appendFencedData(lines, 'Review feedback:', comment.body);
30
+ }
31
+ export function renderReviewMarkdown(canonicalBytes) {
32
+ const document = parseCanonicalReviewExport(canonicalBytes);
33
+ const exactPatch = document.schemaVersion === 3;
34
+ const standalone = document.schemaVersion === 4 || document.schemaVersion === 6;
35
+ const uncommitted = document.schemaVersion === 5;
36
+ const lines = [`# ${RENAME_MAP.displayName} Export`, ''];
37
+ if (exactPatch) {
38
+ lines.push('## Exact patch', `Accepted draft revision: ${document.acceptedDraftRevision}`, `Exported at: ${document.exportedAt}`, `Patch digest: \`${document.patch.digest}\``, `Validation target: \`${document.patch.validationTarget.kind}\``, `Review key: \`${document.patch.reviewKey}\``, 'Anchor sides: `Preimage` and `Postimage`.', '', '## Frozen patch files');
39
+ if (document.patch.snapshot.files.length === 0) {
40
+ lines.push('No changed files were recorded.');
41
+ }
42
+ else {
43
+ for (const file of document.patch.snapshot.files) {
44
+ lines.push('', `### ${file.id}`, `Status: \`${file.status.kind}\``, `Old mode: \`${file.oldMode}\``, `New mode: \`${file.newMode}\``, `Availability: \`${file.availability.kind}\``);
45
+ if (file.oldPath !== undefined) {
46
+ appendFencedData(lines, 'Raw preimage path:', file.oldPath.utf8 ?? file.oldPath.bytesBase64url);
47
+ }
48
+ if (file.newPath !== undefined) {
49
+ appendFencedData(lines, 'Raw postimage path:', file.newPath.utf8 ?? file.newPath.bytesBase64url);
50
+ }
51
+ }
52
+ }
53
+ }
54
+ else if (document.schemaVersion === 4 || document.schemaVersion === 6) {
55
+ lines.push('## Standalone files', `Accepted draft revision: ${document.acceptedDraftRevision}`, `Exported at: ${document.exportedAt}`, 'Original arguments:');
56
+ if (document.standalone.originalArguments.length === 0) {
57
+ lines.push('No arguments were recorded.');
58
+ }
59
+ else {
60
+ let index = 0;
61
+ for (const argument of document.standalone.originalArguments) {
62
+ index += 1;
63
+ appendFencedData(lines, `Argument ${index}:`, controlSafeDisplay(argument));
64
+ }
65
+ }
66
+ lines.push(`Resolved paths: ${document.standalone.resolvedPaths.length}`, `Snapshot chain: ${document.standalone.snapshotChain.join(' → ')}`, 'Anchor side: `Head` (current content).', '', '## Standalone file digests');
67
+ if (document.standalone.fileDigests.length === 0) {
68
+ lines.push('No file digests were recorded.');
69
+ }
70
+ else {
71
+ for (const digest of document.standalone.fileDigests) {
72
+ appendFencedData(lines, `File digest (sha256 ${digest.sha256}):`, digest.path.utf8 ?? digest.path.bytesBase64url);
73
+ }
74
+ }
75
+ if (document.schemaVersion === 6) {
76
+ lines.push('', '## Working tree drift', `Changed since final snapshot: ${document.workingTreeChangedAfterFinalSnapshot.changed ? 'yes' : 'no'}`);
77
+ for (const driftPath of document.workingTreeChangedAfterFinalSnapshot.paths) {
78
+ appendFencedData(lines, 'Changed path:', driftPath.utf8 ?? driftPath.bytesBase64url);
79
+ }
80
+ }
81
+ }
82
+ else if (document.schemaVersion === 5) {
83
+ lines.push('## Uncommitted scope', `Accepted draft revision: ${document.acceptedDraftRevision}`, `Exported at: ${document.exportedAt}`, `Source kind: \`${document.uncommitted.sourceKind}\``);
84
+ if (document.uncommitted.baseCommitOid !== undefined) {
85
+ lines.push(`Base commit: \`${document.uncommitted.baseCommitOid}\``);
86
+ }
87
+ lines.push(`Review key: \`${document.uncommitted.reviewKey}\``, `Snapshot chain: ${document.uncommitted.snapshotChain.join(' → ')}`, 'Anchor side: `Head` (current content).', '', '## Working tree drift', `Changed since final snapshot: ${document.workingTreeChangedAfterFinalSnapshot.changed ? 'yes' : 'no'}`);
88
+ for (const driftPath of document.workingTreeChangedAfterFinalSnapshot.paths) {
89
+ appendFencedData(lines, 'Changed path:', driftPath.utf8 ?? driftPath.bytesBase64url);
90
+ }
91
+ }
92
+ else {
93
+ lines.push('## Pinned comparison', `Accepted draft revision: ${document.acceptedDraftRevision}`, `Exported at: ${document.exportedAt}`, `Base commit: \`${document.comparison.selectedBase.launchOid}\``, `Head commit: \`${document.comparison.selectedHead.launchOid}\``, `Merge base: \`${document.comparison.mergeBaseOid}\``, `Comparison key: \`${document.comparison.comparisonKey}\``);
94
+ if (document.schemaVersion === 2) {
95
+ lines.push('## Review scope', `Base commit: \`${document.range.baseOid}\``, `Head commit: \`${document.range.headOid}\``, `Review key: \`${document.range.reviewKey}\``);
96
+ appendFencedData(lines, 'Requested base:', controlSafeDisplay(document.range.requestedBase));
97
+ appendFencedData(lines, 'Requested head:', controlSafeDisplay(document.range.requestedHead));
98
+ lines.push('Ordered Git pathspecs:');
99
+ let index = 0;
100
+ for (const pathspec of document.range.pathspecs) {
101
+ index += 1;
102
+ appendFencedData(lines, `Pathspec ${index}:`, controlSafeDisplay(pathspec));
103
+ }
104
+ lines.push('');
105
+ }
106
+ appendFencedData(lines, 'Base label:', document.comparison.selectedBase.label);
107
+ appendFencedData(lines, 'Head label:', document.comparison.selectedHead.label);
108
+ lines.push('', '## Drift observation', `Observed at: ${document.drift.observedAt}`, `Acknowledged: ${document.drift.acknowledged ? 'yes' : 'no'}`, `Base selector: ${document.drift.base.status}; launch \`${document.drift.base.launchOid}\`; current \`${document.drift.base.currentOid ?? 'unavailable'}\``, `Head selector: ${document.drift.head.status}; launch \`${document.drift.head.launchOid}\`; current \`${document.drift.head.currentOid ?? 'unavailable'}\``);
109
+ }
110
+ lines.push('', '## Summary');
111
+ if (document.summary.markdown === null) {
112
+ lines.push('No summary provided');
113
+ }
114
+ else {
115
+ appendFencedData(lines, 'Review summary:', document.summary.markdown);
116
+ }
117
+ lines.push('', '## Open actionable requests');
118
+ let actionable = 0;
119
+ for (const file of document.files) {
120
+ const verified = [];
121
+ for (const comment of file.comments) {
122
+ if (comment.state === 'open' && comment.verification.state === 'verified')
123
+ verified.push(comment);
124
+ }
125
+ if (verified.length === 0)
126
+ continue;
127
+ actionable += verified.length;
128
+ lines.push('', '### Path');
129
+ appendFencedData(lines, 'Repository-relative path:', file.path.utf8 ?? file.path.bytesBase64url);
130
+ for (const comment of verified) {
131
+ lines.push('');
132
+ appendComment(lines, comment, exactPatch);
133
+ }
134
+ }
135
+ if (actionable === 0)
136
+ lines.push('No open actionable requests.');
137
+ lines.push('', '## Needs reviewer attention');
138
+ const attention = [];
139
+ for (const file of document.files) {
140
+ for (const comment of file.comments) {
141
+ if (comment.state === 'open' && comment.verification.state !== 'verified')
142
+ attention.push(comment);
143
+ }
144
+ }
145
+ if (attention.length === 0) {
146
+ lines.push('No open comments need reviewer attention.');
147
+ }
148
+ else {
149
+ for (const comment of attention) {
150
+ lines.push('', `### ${comment.id}`, `Anchor state: \`${comment.verification.state}\` (${comment.verification.reason})`);
151
+ appendAnchor(lines, comment, exactPatch);
152
+ appendFencedData(lines, 'Review feedback:', comment.body);
153
+ }
154
+ }
155
+ lines.push('', `Resolved comments: ${document.counts.resolved}. Full history remains in canonical JSON.`, '', '## Applying-agent instructions', exactPatch
156
+ ? 'Before editing, verify the exact patch digest, frozen snapshot facts, relevant blob, repository-relative path, preimage/postimage side, exact selected text, and context hash.'
157
+ : standalone
158
+ ? 'Before editing, verify the standalone review key, snapshot chain, relevant blob, repository-relative path, head side, exact selected text, and context hash.'
159
+ : uncommitted
160
+ ? 'Before editing, verify the uncommitted review key, snapshot chain, relevant blob, repository-relative path, head side, exact selected text, and context hash.'
161
+ : 'Before editing, verify the pinned comparison commit, relevant blob, repository-relative path, base/head side, exact selected text, and context hash.', 'Line number is a navigation hint, never editing authority.', 'Do not guess, fuzzy-match, silently relocate, or apply resolved feedback.', 'Do not apply stale, orphaned, missing, or ambiguous anchors.', 'Report ambiguous, missing, stale, orphaned, blob, path, side, selected-text, and context-hash mismatches.', 'Include the recorded comment ID and identity in every mismatch report.');
162
+ return `${lines.join('\n')}\n`;
163
+ }
@@ -0,0 +1,273 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { RENAME_MAP } from '../domain/product-identity.js';
3
+ import { AnchorVerificationSchema, ReviewDraftV1Schema, ReviewExportV1Schema, ReviewExportV2Schema, ReviewExportV3Schema, ReviewExportV4Schema, ReviewExportV5Schema, ReviewExportV6Schema, orderReviewExportComments, orderUtf16CodeUnits, } from '../contracts/draft.js';
4
+ import { orderExactPaths } from '../domain/path-bytes.js';
5
+ function assertCanonicalString(value) {
6
+ if (/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value)) {
7
+ throw new TypeError('Canonical JSON rejects strings with lone UTF-16 surrogate code units.');
8
+ }
9
+ }
10
+ function serializeCanonicalJson(value) {
11
+ if (value === null)
12
+ return 'null';
13
+ if (typeof value === 'string') {
14
+ assertCanonicalString(value);
15
+ return JSON.stringify(value);
16
+ }
17
+ if (typeof value === 'boolean')
18
+ return value ? 'true' : 'false';
19
+ if (typeof value === 'number') {
20
+ if (!Number.isFinite(value))
21
+ throw new TypeError('Canonical JSON rejects non-finite numbers.');
22
+ return JSON.stringify(value);
23
+ }
24
+ if (Array.isArray(value))
25
+ return `[${value.map(serializeCanonicalJson).join(',')}]`;
26
+ if (typeof value !== 'object' || value === undefined) {
27
+ throw new TypeError('Canonical JSON accepts only I-JSON primitives, arrays, and objects.');
28
+ }
29
+ const prototype = Object.getPrototypeOf(value);
30
+ if (prototype !== Object.prototype && prototype !== null) {
31
+ throw new TypeError('Canonical JSON accepts only plain objects.');
32
+ }
33
+ const object = value;
34
+ return `{${Object.keys(object)
35
+ .sort(orderUtf16CodeUnits)
36
+ .map((key) => `${serializeCanonicalJson(key)}:${serializeCanonicalJson(object[key])}`)
37
+ .join(',')}}`;
38
+ }
39
+ export function buildReviewExportV1(snapshot, exportedAt) {
40
+ const draft = ReviewDraftV1Schema.parse(snapshot.acceptedDraft);
41
+ const comments = draft.comments.map((comment) => {
42
+ const verification = snapshot.commentVerification[comment.id];
43
+ if (verification === undefined) {
44
+ throw new TypeError(`Missing anchor verification for accepted comment ${comment.id}.`);
45
+ }
46
+ const verified = AnchorVerificationSchema.parse(verification);
47
+ return {
48
+ id: comment.id,
49
+ state: comment.state,
50
+ body: comment.body,
51
+ createdAt: comment.createdAt,
52
+ updatedAt: comment.updatedAt,
53
+ resolvedAt: comment.state === 'resolved' ? comment.resolvedAt : null,
54
+ verification: verified,
55
+ anchor: comment.anchor,
56
+ };
57
+ });
58
+ const groups = new Map();
59
+ for (const comment of comments) {
60
+ const key = comment.anchor.path.bytesBase64url;
61
+ const group = groups.get(key);
62
+ if (group === undefined) {
63
+ groups.set(key, { path: comment.anchor.path, comments: [comment] });
64
+ }
65
+ else {
66
+ group.comments.push(comment);
67
+ }
68
+ }
69
+ const files = [...groups.values()]
70
+ .sort((left, right) => orderExactPaths(left.path, right.path))
71
+ .map((group) => ({ path: group.path, comments: group.comments.sort(orderReviewExportComments) }));
72
+ const allComments = files.flatMap((file) => file.comments);
73
+ const document = {
74
+ schemaVersion: 1,
75
+ kind: `${RENAME_MAP.kindPrefix}/export`,
76
+ exportedAt,
77
+ acceptedDraftRevision: draft.revision,
78
+ comparison: snapshot.comparison,
79
+ drift: snapshot.drift,
80
+ summary: { markdown: draft.summary === '' ? null : draft.summary },
81
+ files,
82
+ counts: {
83
+ all: allComments.length,
84
+ openActionable: allComments.filter((comment) => comment.state === 'open' && comment.verification.state === 'verified').length,
85
+ openNeedsAttention: allComments.filter((comment) => comment.state === 'open' && comment.verification.state !== 'verified').length,
86
+ resolved: allComments.filter((comment) => comment.state === 'resolved').length,
87
+ },
88
+ };
89
+ return ReviewExportV1Schema.parse(document);
90
+ }
91
+ function hasMatchingRangeScope(left, right) {
92
+ return left.requestedBase === right.requestedBase
93
+ && left.requestedHead === right.requestedHead
94
+ && left.baseOid === right.baseOid
95
+ && left.headOid === right.headOid
96
+ && left.reviewKey === right.reviewKey
97
+ && left.pathspecs.length === right.pathspecs.length
98
+ && left.pathspecs.every((pathspec, index) => pathspec === right.pathspecs[index]);
99
+ }
100
+ export function buildReviewExportV2(snapshot, range, exportedAt) {
101
+ const draft = ReviewDraftV1Schema.parse(snapshot.acceptedDraft);
102
+ if ('kind' in draft.comparison
103
+ ||
104
+ draft.comparison.range === undefined
105
+ || !hasMatchingRangeScope(draft.comparison.range, range)
106
+ || snapshot.comparison.selectedBase.label !== range.requestedBase
107
+ || snapshot.comparison.selectedHead.label !== range.requestedHead
108
+ || snapshot.comparison.selectedBase.launchOid !== range.baseOid
109
+ || snapshot.comparison.selectedHead.launchOid !== range.headOid
110
+ || snapshot.comparison.mergeBaseOid !== range.baseOid
111
+ || snapshot.comparison.comparisonKey !== range.reviewKey) {
112
+ throw new TypeError('Range export provenance does not match the frozen comparison.');
113
+ }
114
+ const versionOne = buildReviewExportV1(snapshot, exportedAt);
115
+ return ReviewExportV2Schema.parse({ ...versionOne, schemaVersion: 2, range });
116
+ }
117
+ export function buildReviewExportV3(snapshot, patch, exportedAt) {
118
+ const draft = ReviewDraftV1Schema.parse(snapshot.acceptedDraft);
119
+ if (!('kind' in draft.comparison)
120
+ || draft.comparison.kind !== 'exact-patch'
121
+ || draft.comparison.digest !== patch.digest
122
+ || draft.comparison.validationTarget.kind !== patch.validationTarget.kind
123
+ || draft.comparison.reviewKey !== patch.reviewKey) {
124
+ throw new TypeError('Exact patch export provenance does not match accepted draft.');
125
+ }
126
+ const feedback = buildReviewExportV1({
127
+ acceptedDraft: draft,
128
+ commentVerification: snapshot.commentVerification,
129
+ comparison: {
130
+ selectedBase: { label: 'preimage', launchOid: '0'.repeat(40) },
131
+ selectedHead: { label: 'postimage', launchOid: '0'.repeat(40) },
132
+ mergeBaseOid: '0'.repeat(40),
133
+ comparisonKey: '0'.repeat(64),
134
+ },
135
+ drift: {
136
+ observedAt: exportedAt,
137
+ acknowledged: false,
138
+ base: { launchOid: '0'.repeat(40), currentOid: '0'.repeat(40), status: 'unchanged' },
139
+ head: { launchOid: '0'.repeat(40), currentOid: '0'.repeat(40), status: 'unchanged' },
140
+ },
141
+ }, exportedAt);
142
+ const { comparison: _comparison, drift: _drift, ...document } = feedback;
143
+ return ReviewExportV3Schema.parse({ ...document, schemaVersion: 3, patch });
144
+ }
145
+ export function buildReviewExportV4(snapshot, standalone, exportedAt) {
146
+ const draft = ReviewDraftV1Schema.parse(snapshot.acceptedDraft);
147
+ if (!('kind' in draft.comparison)
148
+ || draft.comparison.kind !== 'uncommitted'
149
+ || draft.comparison.sourceKind !== 'standalone'
150
+ || draft.comparison.reviewKey !== standalone.reviewKey) {
151
+ throw new TypeError('Standalone export provenance does not match accepted draft.');
152
+ }
153
+ const feedback = buildReviewExportV1({
154
+ acceptedDraft: draft,
155
+ commentVerification: snapshot.commentVerification,
156
+ comparison: {
157
+ selectedBase: { label: 'preimage', launchOid: '0'.repeat(40) },
158
+ selectedHead: { label: 'postimage', launchOid: '0'.repeat(40) },
159
+ mergeBaseOid: '0'.repeat(40),
160
+ comparisonKey: '0'.repeat(64),
161
+ },
162
+ drift: {
163
+ observedAt: exportedAt,
164
+ acknowledged: false,
165
+ base: { launchOid: '0'.repeat(40), currentOid: '0'.repeat(40), status: 'unchanged' },
166
+ head: { launchOid: '0'.repeat(40), currentOid: '0'.repeat(40), status: 'unchanged' },
167
+ },
168
+ }, exportedAt);
169
+ const { comparison: _comparison, drift: _drift, ...document } = feedback;
170
+ return ReviewExportV4Schema.parse({ ...document, schemaVersion: 4, standalone });
171
+ }
172
+ export function buildReviewExportV5(snapshot, uncommitted, drift, exportedAt) {
173
+ const draft = ReviewDraftV1Schema.parse(snapshot.acceptedDraft);
174
+ if (!('kind' in draft.comparison)
175
+ || draft.comparison.kind !== 'uncommitted'
176
+ || draft.comparison.sourceKind !== uncommitted.sourceKind
177
+ || draft.comparison.reviewKey !== uncommitted.reviewKey) {
178
+ throw new TypeError('Uncommitted export provenance does not match accepted draft.');
179
+ }
180
+ const feedback = buildReviewExportV1({
181
+ acceptedDraft: draft,
182
+ commentVerification: snapshot.commentVerification,
183
+ comparison: {
184
+ selectedBase: { label: 'preimage', launchOid: '0'.repeat(40) },
185
+ selectedHead: { label: 'postimage', launchOid: '0'.repeat(40) },
186
+ mergeBaseOid: '0'.repeat(40),
187
+ comparisonKey: '0'.repeat(64),
188
+ },
189
+ drift: {
190
+ observedAt: exportedAt,
191
+ acknowledged: false,
192
+ base: { launchOid: '0'.repeat(40), currentOid: '0'.repeat(40), status: 'unchanged' },
193
+ head: { launchOid: '0'.repeat(40), currentOid: '0'.repeat(40), status: 'unchanged' },
194
+ },
195
+ }, exportedAt);
196
+ const { comparison: _comparison, drift: _drift, ...document } = feedback;
197
+ return ReviewExportV5Schema.parse({
198
+ ...document,
199
+ schemaVersion: 5,
200
+ uncommitted,
201
+ workingTreeChangedAfterFinalSnapshot: drift,
202
+ });
203
+ }
204
+ export function buildReviewExportV6(snapshot, standalone, drift, exportedAt) {
205
+ const draft = ReviewDraftV1Schema.parse(snapshot.acceptedDraft);
206
+ if (!('kind' in draft.comparison)
207
+ || draft.comparison.kind !== 'uncommitted'
208
+ || draft.comparison.sourceKind !== 'standalone'
209
+ || draft.comparison.reviewKey !== standalone.reviewKey) {
210
+ throw new TypeError('Standalone export provenance does not match accepted draft.');
211
+ }
212
+ const feedback = buildReviewExportV1({
213
+ acceptedDraft: draft,
214
+ commentVerification: snapshot.commentVerification,
215
+ comparison: {
216
+ selectedBase: { label: 'preimage', launchOid: '0'.repeat(40) },
217
+ selectedHead: { label: 'postimage', launchOid: '0'.repeat(40) },
218
+ mergeBaseOid: '0'.repeat(40),
219
+ comparisonKey: '0'.repeat(64),
220
+ },
221
+ drift: {
222
+ observedAt: exportedAt,
223
+ acknowledged: false,
224
+ base: { launchOid: '0'.repeat(40), currentOid: '0'.repeat(40), status: 'unchanged' },
225
+ head: { launchOid: '0'.repeat(40), currentOid: '0'.repeat(40), status: 'unchanged' },
226
+ },
227
+ }, exportedAt);
228
+ const { comparison: _comparison, drift: _drift, ...document } = feedback;
229
+ return ReviewExportV6Schema.parse({
230
+ ...document,
231
+ schemaVersion: 6,
232
+ standalone,
233
+ workingTreeChangedAfterFinalSnapshot: drift,
234
+ });
235
+ }
236
+ export function canonicalizeReviewExport(document) {
237
+ return new TextEncoder().encode(serializeCanonicalJson(document));
238
+ }
239
+ export function parseCanonicalReviewExport(bytes) {
240
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
241
+ const parsed = JSON.parse(text);
242
+ const document = zodParseReviewExport(parsed);
243
+ const canonicalBytes = canonicalizeReviewExport(document);
244
+ if (bytes.byteLength !== canonicalBytes.byteLength || bytes.some((byte, index) => byte !== canonicalBytes[index])) {
245
+ throw new TypeError('Export bytes are not the canonical representation of the validated document.');
246
+ }
247
+ return document;
248
+ }
249
+ function zodParseReviewExport(document) {
250
+ if (document === null || typeof document !== 'object' || !('schemaVersion' in document)) {
251
+ throw new TypeError('Export schema version is unsupported.');
252
+ }
253
+ if (document.schemaVersion === 1)
254
+ return ReviewExportV1Schema.parse(document);
255
+ if (document.schemaVersion === 2)
256
+ return ReviewExportV2Schema.parse(document);
257
+ if (document.schemaVersion === 3)
258
+ return ReviewExportV3Schema.parse(document);
259
+ if (document.schemaVersion === 4)
260
+ return ReviewExportV4Schema.parse(document);
261
+ if (document.schemaVersion === 5)
262
+ return ReviewExportV5Schema.parse(document);
263
+ if (document.schemaVersion === 6)
264
+ return ReviewExportV6Schema.parse(document);
265
+ throw new TypeError('Export schema version unsupported.');
266
+ }
267
+ export function hashExportBytes(bytes) {
268
+ return Object.freeze({
269
+ algorithm: 'sha256',
270
+ sha256: createHash('sha256').update(bytes).digest('hex'),
271
+ bytes: bytes.byteLength,
272
+ });
273
+ }
@@ -0,0 +1,92 @@
1
+ export const MAX_INLINE_TEXT_BYTES = 1_048_576;
2
+ const regularFileModes = Object.freeze({
3
+ '100644': true,
4
+ '100755': true,
5
+ });
6
+ const textAvailability = Object.freeze({ kind: 'text' });
7
+ const missingAvailability = Object.freeze({
8
+ kind: 'unavailable',
9
+ reason: 'missing-object',
10
+ });
11
+ const unsupportedAvailability = Object.freeze({
12
+ binary: Object.freeze({ kind: 'unsupported', reason: 'binary' }),
13
+ 'mode-or-type': Object.freeze({
14
+ kind: 'unsupported',
15
+ reason: 'mode-or-type',
16
+ }),
17
+ 'non-utf8': Object.freeze({ kind: 'unsupported', reason: 'non-utf8' }),
18
+ oversized: Object.freeze({ kind: 'unsupported', reason: 'oversized' }),
19
+ submodule: Object.freeze({ kind: 'unsupported', reason: 'submodule' }),
20
+ symlink: Object.freeze({ kind: 'unsupported', reason: 'symlink' }),
21
+ 'changed-during-capture': Object.freeze({
22
+ kind: 'unsupported',
23
+ reason: 'changed-during-capture',
24
+ }),
25
+ });
26
+ const fatalUtf8Decoder = new TextDecoder('utf-8', { fatal: true });
27
+ export async function classifyAvailability(file, objectReader, signal) {
28
+ if (file.oldMode === '160000' || file.newMode === '160000') {
29
+ return unsupportedAvailability.submodule;
30
+ }
31
+ if (file.oldMode === '120000' || file.newMode === '120000') {
32
+ return unsupportedAvailability.symlink;
33
+ }
34
+ if (file.status.kind === 'unsupported') {
35
+ return unsupportedAvailability['mode-or-type'];
36
+ }
37
+ const sides = [];
38
+ if (file.oldMode !== '000000') {
39
+ if (regularFileModes[file.oldMode] !== true) {
40
+ return unsupportedAvailability['mode-or-type'];
41
+ }
42
+ sides.push({ mode: file.oldMode, oid: file.oldBlobOid });
43
+ }
44
+ if (file.newMode !== '000000') {
45
+ if (regularFileModes[file.newMode] !== true) {
46
+ return unsupportedAvailability['mode-or-type'];
47
+ }
48
+ sides.push({ mode: file.newMode, oid: file.newBlobOid });
49
+ }
50
+ if (sides.length === 0) {
51
+ return unsupportedAvailability['mode-or-type'];
52
+ }
53
+ const sizes = [];
54
+ for (const side of sides) {
55
+ const metadata = await objectReader.inspect(side.oid, signal);
56
+ if (metadata.kind === 'missing' || metadata.objectType !== 'blob') {
57
+ return missingAvailability;
58
+ }
59
+ sizes.push(metadata.size);
60
+ }
61
+ if (sizes.some((size) => size > MAX_INLINE_TEXT_BYTES)) {
62
+ return unsupportedAvailability.oversized;
63
+ }
64
+ if (file.additions === null || file.deletions === null) {
65
+ return unsupportedAvailability.binary;
66
+ }
67
+ const contents = [];
68
+ for (let index = 0; index < sides.length; index += 1) {
69
+ const side = sides[index];
70
+ const content = await objectReader.read(side.oid, {
71
+ maxBytes: MAX_INLINE_TEXT_BYTES,
72
+ signal,
73
+ });
74
+ if (content.kind === 'missing' ||
75
+ content.bytes.byteLength !== sizes[index]) {
76
+ return missingAvailability;
77
+ }
78
+ contents.push(content.bytes);
79
+ }
80
+ if (contents.some((content) => content.includes(0))) {
81
+ return unsupportedAvailability.binary;
82
+ }
83
+ try {
84
+ for (const content of contents) {
85
+ fatalUtf8Decoder.decode(content);
86
+ }
87
+ }
88
+ catch {
89
+ return unsupportedAvailability['non-utf8'];
90
+ }
91
+ return textAvailability;
92
+ }