@dev.fast/whiteboard 0.0.0-stage → 0.2.1-preview.20261005.98

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 (247) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -2
  3. package/THIRD_PARTY_NOTICES.md +93 -0
  4. package/dist/account-alias-DOHN1RSH.js +973 -0
  5. package/dist/agent-cli-ChOq0ZuO.js +117 -0
  6. package/dist/agent-cli-DzWfZr6G.js +2 -0
  7. package/dist/agent-client-CPQo7iTI.js +305 -0
  8. package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
  9. package/dist/build-info.json +1 -0
  10. package/dist/cli-hVgHcdsv.js +47 -0
  11. package/dist/cli-runner-D8y2_luU.js +5095 -0
  12. package/dist/cli.d.ts +1 -0
  13. package/dist/cli.js +128 -0
  14. package/dist/client-DFoh50W3.js +950 -0
  15. package/dist/desktop-discovery-DBO-PM2V.js +164 -0
  16. package/dist/error-message-OtiDonty.js +6 -0
  17. package/dist/fs-utils-BMPLt0cr.js +22 -0
  18. package/dist/fuzzy-match-BoAmcyak.js +30 -0
  19. package/dist/headless-host-BJxf4dUU.js +94 -0
  20. package/dist/input-error-OLgB_h31.js +11 -0
  21. package/dist/local-data-CxLk75rx.js +4491 -0
  22. package/dist/mcp-QeF8vkdf.js +140 -0
  23. package/dist/package-paths-B6-zxvIO.js +47 -0
  24. package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
  25. package/dist/profile-4ry2f3AM.js +76 -0
  26. package/dist/profile-Bh5FIrmf.js +2 -0
  27. package/dist/request-origin-D_4QIdoN.js +811 -0
  28. package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
  29. package/dist/review-home-paths-6zZH-1c9.js +2560 -0
  30. package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
  31. package/dist/runtime-CKiWhGrf.js +52 -0
  32. package/dist/runtime.d.ts +9 -0
  33. package/dist/runtime.js +2 -0
  34. package/dist/s3-SzLpeCG6.js +345 -0
  35. package/dist/s3-config-BgxkSoOy.js +2 -0
  36. package/dist/s3-config-CAxhO9u_.js +676 -0
  37. package/dist/server/desktop-host.d.ts +4 -0
  38. package/dist/server/desktop-host.js +1702 -0
  39. package/dist/server-discovery-Dbzn3w6Z.js +66 -0
  40. package/dist/sharing/index.d.ts +1977 -0
  41. package/dist/sharing/index.js +2 -0
  42. package/dist/src-CGP5ytbV.js +283 -0
  43. package/dist/src-CmdiBL20.js +3676 -0
  44. package/dist/src-DnwdaQ2r.js +865 -0
  45. package/dist/src-X9phtB2j.js +68 -0
  46. package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
  47. package/dist/tool-failure-C8zB73HV.js +98 -0
  48. package/dist/tutorial-trace-DrBvkaeL.js +43 -0
  49. package/instructions/authoring.md +36 -0
  50. package/instructions/file-lenses.md +13 -0
  51. package/instructions/scratchpad.md +23 -0
  52. package/instructions/trace-archaeology.md +113 -0
  53. package/onboarding.md +8 -0
  54. package/package.json +106 -3
  55. package/src/agent-selection.ts +100 -0
  56. package/src/agent-session-ref.ts +80 -0
  57. package/src/ask/agents.ts +351 -0
  58. package/src/ask/checkout-files.ts +47 -0
  59. package/src/ask/file-refs.ts +111 -0
  60. package/src/ask/pi-mcp.ts +154 -0
  61. package/src/ask/protocol.ts +244 -0
  62. package/src/ask/thread-state.ts +350 -0
  63. package/src/ask/thread.ts +1400 -0
  64. package/src/ask/threads.ts +219 -0
  65. package/src/ask/watch.ts +79 -0
  66. package/src/cli-install.ts +1041 -0
  67. package/src/cli-runner.ts +1461 -0
  68. package/src/cli-runtime-info.ts +35 -0
  69. package/src/cli.ts +222 -0
  70. package/src/connect-prompts.ts +248 -0
  71. package/src/cursor-deeplink.ts +11 -0
  72. package/src/desktop-discovery.ts +355 -0
  73. package/src/diff-selection-migration.ts +63 -0
  74. package/src/embedded-posthog-key.ts +6 -0
  75. package/src/error-telemetry.ts +247 -0
  76. package/src/evidence.ts +14 -0
  77. package/src/exception-telemetry.ts +126 -0
  78. package/src/fixtures/blocks/call_stack_diff.json +27 -0
  79. package/src/fixtures/blocks/callout.json +11 -0
  80. package/src/fixtures/blocks/code.json +9 -0
  81. package/src/fixtures/blocks/code_peek.json +8 -0
  82. package/src/fixtures/blocks/database_lens.json +51 -0
  83. package/src/fixtures/blocks/divider.json +1 -0
  84. package/src/fixtures/blocks/fixtures.ts +33 -0
  85. package/src/fixtures/blocks/flow_diagram.json +36 -0
  86. package/src/fixtures/blocks/ids.ts +8 -0
  87. package/src/fixtures/blocks/image.json +9 -0
  88. package/src/fixtures/blocks/markdown.json +7 -0
  89. package/src/fixtures/blocks/section.json +11 -0
  90. package/src/fixtures/blocks/sequence.json +31 -0
  91. package/src/fixtures/blocks/software_map.json +7 -0
  92. package/src/fixtures/blocks/trace_quote.json +9 -0
  93. package/src/fixtures/blocks/tutorial.json +51 -0
  94. package/src/fs-utils.ts +32 -0
  95. package/src/fuzzy-match.ts +94 -0
  96. package/src/install.ts +29 -0
  97. package/src/legacy-skills.ts +133 -0
  98. package/src/lens-selection.ts +230 -0
  99. package/src/markdown-latex-math.ts +230 -0
  100. package/src/markdown.ts +81 -0
  101. package/src/package-paths.ts +53 -0
  102. package/src/posthog-capture-client.ts +610 -0
  103. package/src/review-api/README.md +295 -0
  104. package/src/review-api/activity.ts +340 -0
  105. package/src/review-api/agent-cli.ts +219 -0
  106. package/src/review-api/agent-client.ts +188 -0
  107. package/src/review-api/anchor-quotes.ts +88 -0
  108. package/src/review-api/ask-history.ts +242 -0
  109. package/src/review-api/authoring-tools.ts +211 -0
  110. package/src/review-api/blocks/call_stack_diff.ts +75 -0
  111. package/src/review-api/blocks/callout.ts +24 -0
  112. package/src/review-api/blocks/code.ts +9 -0
  113. package/src/review-api/blocks/code_peek.ts +13 -0
  114. package/src/review-api/blocks/database_lens.ts +150 -0
  115. package/src/review-api/blocks/definition.ts +40 -0
  116. package/src/review-api/blocks/divider.ts +6 -0
  117. package/src/review-api/blocks/flow_diagram.ts +111 -0
  118. package/src/review-api/blocks/image.ts +10 -0
  119. package/src/review-api/blocks/index.ts +90 -0
  120. package/src/review-api/blocks/markdown.ts +17 -0
  121. package/src/review-api/blocks/section.ts +24 -0
  122. package/src/review-api/blocks/sequence.ts +56 -0
  123. package/src/review-api/blocks/software_map.ts +9 -0
  124. package/src/review-api/blocks/trace_quote.ts +10 -0
  125. package/src/review-api/blocks/tutorial.ts +39 -0
  126. package/src/review-api/checkout-fs.ts +14 -0
  127. package/src/review-api/client.ts +1 -0
  128. package/src/review-api/comparison-coverage.ts +304 -0
  129. package/src/review-api/component-reference.ts +30 -0
  130. package/src/review-api/diff-lenses.ts +175 -0
  131. package/src/review-api/document-headings.ts +51 -0
  132. package/src/review-api/document-text.ts +200 -0
  133. package/src/review-api/document.ts +906 -0
  134. package/src/review-api/file-lenses.ts +100 -0
  135. package/src/review-api/http.ts +1877 -0
  136. package/src/review-api/image-decode.ts +29 -0
  137. package/src/review-api/input-error.ts +9 -0
  138. package/src/review-api/instructions.ts +89 -0
  139. package/src/review-api/lens-alignment.ts +52 -0
  140. package/src/review-api/local-data.ts +1837 -0
  141. package/src/review-api/map-input.ts +154 -0
  142. package/src/review-api/mcp-client-agent.ts +32 -0
  143. package/src/review-api/mcp.ts +240 -0
  144. package/src/review-api/origin.ts +43 -0
  145. package/src/review-api/profile.ts +146 -0
  146. package/src/review-api/public-tools.ts +102 -0
  147. package/src/review-api/pull-request.ts +389 -0
  148. package/src/review-api/read-schemas.ts +85 -0
  149. package/src/review-api/recovery.ts +2 -0
  150. package/src/review-api/request-origin.ts +33 -0
  151. package/src/review-api/review-progress.ts +382 -0
  152. package/src/review-api/status-tool.ts +10 -0
  153. package/src/review-api/store-schema.ts +22 -0
  154. package/src/review-api/store.ts +1647 -0
  155. package/src/review-api/tool-failure.ts +64 -0
  156. package/src/review-api/trace-schema.ts +13 -0
  157. package/src/review-api/traces.ts +119 -0
  158. package/src/review-api/unsupported-files.integration.ts +91 -0
  159. package/src/review-api/workspaces.ts +692 -0
  160. package/src/review-api/worktree-source.ts +170 -0
  161. package/src/review-api/worktree-structural.integration.ts +320 -0
  162. package/src/review-app-launcher.ts +432 -0
  163. package/src/review-app-picker.ts +168 -0
  164. package/src/review-app.ts +134 -0
  165. package/src/review-bundled-tools.ts +249 -0
  166. package/src/review-checkout-paths.ts +37 -0
  167. package/src/review-diff-files.ts +89 -0
  168. package/src/review-head-checkout.ts +300 -0
  169. package/src/review-home-paths.ts +73 -0
  170. package/src/review-info.ts +59 -0
  171. package/src/review-instances.ts +113 -0
  172. package/src/review-logger.ts +183 -0
  173. package/src/review-preferences.ts +88 -0
  174. package/src/review-prepare.ts +291 -0
  175. package/src/review-stack.ts +112 -0
  176. package/src/review-telemetry.ts +1066 -0
  177. package/src/runtime.ts +74 -0
  178. package/src/server/account-alias.ts +30 -0
  179. package/src/server/bounded-stream.ts +34 -0
  180. package/src/server/bug-report.ts +301 -0
  181. package/src/server/client-error-budget.ts +45 -0
  182. package/src/server/crash-report.ts +273 -0
  183. package/src/server/desktop-host-shutdown.ts +64 -0
  184. package/src/server/desktop-host.ts +190 -0
  185. package/src/server/desktop-server.ts +703 -0
  186. package/src/server/diffr-config.ts +509 -0
  187. package/src/server/diffr-languages.ts +97 -0
  188. package/src/server/global-verb-relay.ts +190 -0
  189. package/src/server/headless-host.ts +149 -0
  190. package/src/server/hono-http.ts +162 -0
  191. package/src/server/http-json.ts +24 -0
  192. package/src/server/json-review-reporting.ts +174 -0
  193. package/src/server/process-error-telemetry.ts +174 -0
  194. package/src/server/review-api-parsers.ts +90 -0
  195. package/src/server/review-lifecycle-telemetry.ts +73 -0
  196. package/src/server/review-open-watchdog.ts +46 -0
  197. package/src/server/review-server-core.ts +290 -0
  198. package/src/server/structural-comparisons.ts +153 -0
  199. package/src/server/structural-diff.ts +186 -0
  200. package/src/server/tutorial-service.ts +241 -0
  201. package/src/server/ui-telemetry.ts +198 -0
  202. package/src/server-discovery.ts +95 -0
  203. package/src/session-markers.ts +132 -0
  204. package/src/sharing/auth.ts +41 -0
  205. package/src/sharing/cli.ts +83 -0
  206. package/src/sharing/client.ts +334 -0
  207. package/src/sharing/export.ts +195 -0
  208. package/src/sharing/host.ts +316 -0
  209. package/src/sharing/import.ts +787 -0
  210. package/src/sharing/index.ts +16 -0
  211. package/src/sharing/repository.ts +145 -0
  212. package/src/sharing/routes.ts +34 -0
  213. package/src/slug.ts +23 -0
  214. package/src/software-map-diff-counts.ts +517 -0
  215. package/src/software-map-model.ts +1147 -0
  216. package/src/software-map-topology-diff.ts +260 -0
  217. package/src/source.ts +92 -0
  218. package/src/startup-trace.ts +232 -0
  219. package/src/stored-document-migration.ts +151 -0
  220. package/src/telemetry-clean-text.ts +257 -0
  221. package/src/telemetry-config.ts +298 -0
  222. package/src/telemetry-debug-sink.ts +38 -0
  223. package/src/telemetry.ts +13 -0
  224. package/src/trace-cli.ts +156 -0
  225. package/src/trace-storage-cli.ts +509 -0
  226. package/src/tutorial-conversation.ts +18 -0
  227. package/src/ui-telemetry-events.ts +765 -0
  228. package/src/unified-diff.ts +71 -0
  229. package/src/viewed-coverage.ts +259 -0
  230. package/src/windows-cli.ts +125 -0
  231. package/tutorial/document.json +275 -0
  232. package/tutorial/runtime-manifest.json +12 -0
  233. package/tutorial/sample-service/package.json +9 -0
  234. package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
  235. package/tutorial/sample-service/src/app.ts +26 -0
  236. package/tutorial/sample-service/src/database/schema.ts +10 -0
  237. package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
  238. package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
  239. package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
  240. package/tutorial/sample-service/src/orders/order-service.ts +34 -0
  241. package/tutorial/sample-service/src/orders/order.ts +21 -0
  242. package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
  243. package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
  244. package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
  245. package/tutorial/sample-service/tsconfig.json +12 -0
  246. package/tutorial/software-map.json +139 -0
  247. package/tutorial/trace.json +20 -0
@@ -0,0 +1,2671 @@
1
+ import { f as isNumberValue, l as jsonValueSchema, m as isStringValue, n as isJsonObject } from "./src-X9phtB2j.js";
2
+ import "./src-DnwdaQ2r.js";
3
+ import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
4
+ import { z } from "zod";
5
+ import { fromMarkdown } from "mdast-util-from-markdown";
6
+ import { gfmFromMarkdown } from "mdast-util-gfm";
7
+ import { mathFromMarkdown } from "mdast-util-math";
8
+ import { gfm } from "micromark-extension-gfm";
9
+ //#region src/viewed-coverage.ts
10
+ const coverageSchema = z.object({
11
+ base: z.array(z.tuple([z.number().int().nonnegative(), z.number().int().nonnegative()])),
12
+ head: z.array(z.tuple([z.number().int().nonnegative(), z.number().int().nonnegative()]))
13
+ });
14
+ const emptyCoverage = () => ({
15
+ base: [],
16
+ head: []
17
+ });
18
+ function unionIntervals(ranges) {
19
+ const result = [];
20
+ for (const [start, end] of [...ranges].sort((a, b) => a[0] - b[0])) {
21
+ if (end <= start) continue;
22
+ const last = result.at(-1);
23
+ if (last && start <= last[1]) last[1] = Math.max(last[1], end);
24
+ else result.push([start, end]);
25
+ }
26
+ return result;
27
+ }
28
+ function intersectIntervals(left, right) {
29
+ const result = [];
30
+ const a = unionIntervals(left), b = unionIntervals(right);
31
+ let i = 0, j = 0;
32
+ while (i < a.length && j < b.length) {
33
+ const start = Math.max(a[i][0], b[j][0]), end = Math.min(a[i][1], b[j][1]);
34
+ if (start < end) result.push([start, end]);
35
+ if (a[i][1] < b[j][1]) i++;
36
+ else j++;
37
+ }
38
+ return result;
39
+ }
40
+ function subtractIntervals(left, right) {
41
+ const result = [];
42
+ const removed = unionIntervals(right);
43
+ for (const [start, end] of unionIntervals(left)) {
44
+ let cursor = start;
45
+ for (const [from, to] of removed) {
46
+ if (to <= cursor) continue;
47
+ if (from >= end) break;
48
+ if (from > cursor) result.push([cursor, from]);
49
+ cursor = Math.min(end, Math.max(cursor, to));
50
+ }
51
+ if (cursor < end) result.push([cursor, end]);
52
+ }
53
+ return result;
54
+ }
55
+ function updateCoverage(current, scope, viewed) {
56
+ const apply = (side) => viewed ? unionIntervals([...current[side], ...scope[side]]) : subtractIntervals(current[side], scope[side]);
57
+ return {
58
+ base: apply("base"),
59
+ head: apply("head")
60
+ };
61
+ }
62
+ function scopedCoverage(file, sources) {
63
+ if (!sources) return file.changed;
64
+ const side = (side) => intersectIntervals(file.changed[side], sources.flatMap((source) => source.side === side && source.file === (side === "base" ? file.previousPath ?? file.path : file.path) ? [[source.fromLine - 1, source.toLine]] : []));
65
+ return {
66
+ base: side("base"),
67
+ head: side("head")
68
+ };
69
+ }
70
+ function coverageSources(file, coverage = file.viewed) {
71
+ return ["base", "head"].flatMap((side) => coverage[side].map(([start, end]) => ({
72
+ side,
73
+ file: side === "base" ? file.previousPath ?? file.path : file.path,
74
+ fromLine: start + 1,
75
+ toLine: end
76
+ })));
77
+ }
78
+ //#endregion
79
+ //#region src/lens-selection.ts
80
+ const ANCHOR_FORMS = "\"head/path#L10-L24\" or \"base/path#L7\" (one side, lines 10–24 or line 7); across sides, GitHub diff style \"diff/path#L84-R90\" (L = base line, R = head line)";
81
+ const sameSideAnchor = /^(head|base)\/(.+)#L(\d+)(?:-L(\d+))?$/;
82
+ const crossSideAnchor = /^diff\/(.+)#([LR])(\d+)-([LR])(\d+)$/;
83
+ /** An anchor's selection, as markdown source links write it too (without
84
+ * their `review-source:` scheme). Undefined when it isn't one. */
85
+ function parseAnchor(text) {
86
+ const same = sameSideAnchor.exec(text);
87
+ if (same) {
88
+ const side = same[1] === "base" ? "base" : "head";
89
+ return {
90
+ file: same[2],
91
+ start: {
92
+ side,
93
+ line: Number(same[3])
94
+ },
95
+ end: {
96
+ side,
97
+ line: Number(same[4] ?? same[3])
98
+ }
99
+ };
100
+ }
101
+ const cross = crossSideAnchor.exec(text);
102
+ if (!cross || cross[2] === cross[4]) return void 0;
103
+ const side = (letter) => letter === "L" ? "base" : "head";
104
+ return {
105
+ file: cross[1],
106
+ start: {
107
+ side: side(cross[2]),
108
+ line: Number(cross[3])
109
+ },
110
+ end: {
111
+ side: side(cross[4]),
112
+ line: Number(cross[5])
113
+ }
114
+ };
115
+ }
116
+ /** Why a parsed selection can't be quoted, or undefined when it can. */
117
+ function selectionProblem(selection) {
118
+ if (selection.start.line < 1 || selection.end.line < 1) return "Source lines count from 1.";
119
+ if (selection.start.side === selection.end.side && selection.start.line > selection.end.line) return "Source range ends before it starts.";
120
+ if (selection.pins && selection.pins.base === void 0 && (selection.start.side === "base" || selection.end.side === "base")) return "A base-side anchor needs base pins.";
121
+ }
122
+ const anchorSchema = z.string({ error: `Expected a source anchor: ${ANCHOR_FORMS}.` }).superRefine((text, context) => {
123
+ const selection = parseAnchor(text);
124
+ const problem = selection ? selectionProblem(selection) : `Use ${ANCHOR_FORMS} for a source anchor.`;
125
+ if (problem) context.addIssue({
126
+ code: "custom",
127
+ message: problem
128
+ });
129
+ }).describe("head/path#L10-L24, base/path#L7, or diff/path#L84-R90");
130
+ /** A stored anchor's selection, read at the pins of whatever holds it. */
131
+ function anchorSelection(anchor, pins) {
132
+ const selection = parseAnchor(anchor);
133
+ if (!selection) throw new Error(`Not a source anchor: ${anchor}`);
134
+ return pins ? {
135
+ ...selection,
136
+ pins
137
+ } : selection;
138
+ }
139
+ /** The anchor that writes a selection; its pins belong to its holder. */
140
+ function formatAnchor(selection) {
141
+ const { file, start, end } = selection;
142
+ if (start.side === end.side) return start.line === end.line ? `${start.side}/${file}#L${start.line}` : `${start.side}/${file}#L${start.line}-L${end.line}`;
143
+ const letter = (side) => side === "base" ? "L" : "R";
144
+ return `diff/${file}#${letter(start.side)}${start.line}-${letter(end.side)}${end.line}`;
145
+ }
146
+ /** Pins-less selections keep the key they always had. */
147
+ function selectionKey(source) {
148
+ return JSON.stringify([
149
+ source.file,
150
+ source.start.side,
151
+ source.start.line,
152
+ source.end.side,
153
+ source.end.line,
154
+ ...source.pins ? [sourcePinsKey(source.pins)] : []
155
+ ]);
156
+ }
157
+ function sourcePinsKey(pins) {
158
+ return `${pins.repositoryId}:${pins.base ?? ""}:${pins.head}`;
159
+ }
160
+ /** The comparison a source's pins name: head alone means head against
161
+ * itself. Reference coverage is grouped under this key on both sides. */
162
+ function comparisonKey(pins) {
163
+ return `${pins.repositoryId}:${pins.base ?? pins.head}:${pins.head}`;
164
+ }
165
+ /** Navigation/quote anchors only; never use these as lens coverage. */
166
+ function sourceAnchors(source) {
167
+ return ["head", "base"].flatMap((side) => {
168
+ const lines = [source.start, source.end].flatMap((endpoint) => endpoint.side === side ? [endpoint.line] : []);
169
+ if (!lines.length) return [];
170
+ const anchor = {
171
+ file: source.file,
172
+ side,
173
+ fromLine: Math.min(...lines),
174
+ toLine: Math.max(...lines)
175
+ };
176
+ if (source.pins) anchor.pins = source.pins;
177
+ return [anchor];
178
+ });
179
+ }
180
+ /** Resolve once against the renderer's rows. No matching or context expansion. */
181
+ function resolveDiffSelection(source, rows, file) {
182
+ const locate = (endpoint) => rows.findIndex((row) => row[endpoint.side === "base" ? 0 : 1] === endpoint.line - 1);
183
+ const start = locate(source.start);
184
+ const end = locate(source.end);
185
+ if (start < 0 || end < start) throw new Error("Lens endpoints do not identify an ordered interval in this diff.");
186
+ const selected = rows.slice(start, end + 1);
187
+ return ["base", "head"].flatMap((side, index) => unionIntervals(selected.flatMap((row) => row[index] === null ? [] : [[row[index], row[index] + 1]])).map(([from, to]) => ({
188
+ file: side === "base" ? file.previousPath ?? file.path : file.path,
189
+ side,
190
+ fromLine: from + 1,
191
+ toLine: to
192
+ })));
193
+ }
194
+ /**
195
+ * Check whether a character code is a markdown line ending.
196
+ *
197
+ * A **markdown line ending** is the virtual characters M-0003 CARRIAGE RETURN
198
+ * LINE FEED (CRLF), M-0004 LINE FEED (LF) and M-0005 CARRIAGE RETURN (CR).
199
+ *
200
+ * In micromark, the actual character U+000A LINE FEED (LF) and U+000D CARRIAGE
201
+ * RETURN (CR) are replaced by these virtual characters depending on whether
202
+ * they occurred together.
203
+ *
204
+ * @param {Code} code
205
+ * Code.
206
+ * @returns {boolean}
207
+ * Whether it matches.
208
+ */
209
+ function markdownLineEnding(code) {
210
+ return code !== null && code < -2;
211
+ }
212
+ /**
213
+ * Check whether a character code is a markdown space.
214
+ *
215
+ * A **markdown space** is the concrete character U+0020 SPACE (SP) and the
216
+ * virtual characters M-0001 VIRTUAL SPACE (VS) and M-0002 HORIZONTAL TAB (HT).
217
+ *
218
+ * In micromark, the actual character U+0009 CHARACTER TABULATION (HT) is
219
+ * replaced by one M-0002 HORIZONTAL TAB (HT) and between 0 and 3 M-0001 VIRTUAL
220
+ * SPACE (VS) characters, depending on the column at which the tab occurred.
221
+ *
222
+ * @param {Code} code
223
+ * Code.
224
+ * @returns {boolean}
225
+ * Whether it matches.
226
+ */
227
+ function markdownSpace(code) {
228
+ return code === -2 || code === -1 || code === 32;
229
+ }
230
+ //#endregion
231
+ //#region ../../node_modules/micromark-factory-space/index.js
232
+ /**
233
+ * @import {Effects, State, TokenType} from 'micromark-util-types'
234
+ */
235
+ /**
236
+ * Parse spaces and tabs.
237
+ *
238
+ * There is no `nok` parameter:
239
+ *
240
+ * * spaces in markdown are often optional, in which case this factory can be
241
+ * used and `ok` will be switched to whether spaces were found or not
242
+ * * one line ending or space can be detected with `markdownSpace(code)` right
243
+ * before using `factorySpace`
244
+ *
245
+ * ###### Examples
246
+ *
247
+ * Where `␉` represents a tab (plus how much it expands) and `␠` represents a
248
+ * single space.
249
+ *
250
+ * ```markdown
251
+ * ␉
252
+ * ␠␠␠␠
253
+ * ␉␠
254
+ * ```
255
+ *
256
+ * @param {Effects} effects
257
+ * Context.
258
+ * @param {State} ok
259
+ * State switched to when successful.
260
+ * @param {TokenType} type
261
+ * Type (`' \t'`).
262
+ * @param {number | undefined} [max=Infinity]
263
+ * Max (exclusive).
264
+ * @returns {State}
265
+ * Start state.
266
+ */
267
+ function factorySpace(effects, ok, type, max) {
268
+ const limit = max ? max - 1 : Number.POSITIVE_INFINITY;
269
+ let size = 0;
270
+ return start;
271
+ /** @type {State} */
272
+ function start(code) {
273
+ if (markdownSpace(code)) {
274
+ effects.enter(type);
275
+ return prefix(code);
276
+ }
277
+ return ok(code);
278
+ }
279
+ /** @type {State} */
280
+ function prefix(code) {
281
+ if (markdownSpace(code) && size++ < limit) {
282
+ effects.consume(code);
283
+ return prefix;
284
+ }
285
+ effects.exit(type);
286
+ return ok(code);
287
+ }
288
+ }
289
+ //#endregion
290
+ //#region ../../node_modules/micromark-extension-math/lib/math-flow.js
291
+ /**
292
+ * @import {Construct, State, TokenizeContext, Tokenizer} from 'micromark-util-types'
293
+ */
294
+ /** @type {Construct} */
295
+ const mathFlow = {
296
+ tokenize: tokenizeMathFenced,
297
+ concrete: true,
298
+ name: "mathFlow"
299
+ };
300
+ /** @type {Construct} */
301
+ const nonLazyContinuation = {
302
+ tokenize: tokenizeNonLazyContinuation,
303
+ partial: true
304
+ };
305
+ /**
306
+ * @this {TokenizeContext}
307
+ * @type {Tokenizer}
308
+ */
309
+ function tokenizeMathFenced(effects, ok, nok) {
310
+ const self = this;
311
+ const tail = self.events[self.events.length - 1];
312
+ const initialSize = tail && tail[1].type === "linePrefix" ? tail[2].sliceSerialize(tail[1], true).length : 0;
313
+ let sizeOpen = 0;
314
+ return start;
315
+ /**
316
+ * Start of math.
317
+ *
318
+ * ```markdown
319
+ * > | $$
320
+ * ^
321
+ * | \frac{1}{2}
322
+ * | $$
323
+ * ```
324
+ *
325
+ * @type {State}
326
+ */
327
+ function start(code) {
328
+ effects.enter("mathFlow");
329
+ effects.enter("mathFlowFence");
330
+ effects.enter("mathFlowFenceSequence");
331
+ return sequenceOpen(code);
332
+ }
333
+ /**
334
+ * In opening fence sequence.
335
+ *
336
+ * ```markdown
337
+ * > | $$
338
+ * ^
339
+ * | \frac{1}{2}
340
+ * | $$
341
+ * ```
342
+ *
343
+ * @type {State}
344
+ */
345
+ function sequenceOpen(code) {
346
+ if (code === 36) {
347
+ effects.consume(code);
348
+ sizeOpen++;
349
+ return sequenceOpen;
350
+ }
351
+ if (sizeOpen < 2) return nok(code);
352
+ effects.exit("mathFlowFenceSequence");
353
+ return factorySpace(effects, metaBefore, "whitespace")(code);
354
+ }
355
+ /**
356
+ * In opening fence, before meta.
357
+ *
358
+ * ```markdown
359
+ * > | $$asciimath
360
+ * ^
361
+ * | x < y
362
+ * | $$
363
+ * ```
364
+ *
365
+ * @type {State}
366
+ */
367
+ function metaBefore(code) {
368
+ if (code === null || markdownLineEnding(code)) return metaAfter(code);
369
+ effects.enter("mathFlowFenceMeta");
370
+ effects.enter("chunkString", { contentType: "string" });
371
+ return meta(code);
372
+ }
373
+ /**
374
+ * In meta.
375
+ *
376
+ * ```markdown
377
+ * > | $$asciimath
378
+ * ^
379
+ * | x < y
380
+ * | $$
381
+ * ```
382
+ *
383
+ * @type {State}
384
+ */
385
+ function meta(code) {
386
+ if (code === null || markdownLineEnding(code)) {
387
+ effects.exit("chunkString");
388
+ effects.exit("mathFlowFenceMeta");
389
+ return metaAfter(code);
390
+ }
391
+ if (code === 36) return nok(code);
392
+ effects.consume(code);
393
+ return meta;
394
+ }
395
+ /**
396
+ * After meta.
397
+ *
398
+ * ```markdown
399
+ * > | $$
400
+ * ^
401
+ * | \frac{1}{2}
402
+ * | $$
403
+ * ```
404
+ *
405
+ * @type {State}
406
+ */
407
+ function metaAfter(code) {
408
+ effects.exit("mathFlowFence");
409
+ if (self.interrupt) return ok(code);
410
+ return effects.attempt(nonLazyContinuation, beforeNonLazyContinuation, after)(code);
411
+ }
412
+ /**
413
+ * After eol/eof in math, at a non-lazy closing fence or content.
414
+ *
415
+ * ```markdown
416
+ * | $$
417
+ * > | \frac{1}{2}
418
+ * ^
419
+ * > | $$
420
+ * ^
421
+ * ```
422
+ *
423
+ * @type {State}
424
+ */
425
+ function beforeNonLazyContinuation(code) {
426
+ return effects.attempt({
427
+ tokenize: tokenizeClosingFence,
428
+ partial: true
429
+ }, after, contentStart)(code);
430
+ }
431
+ /**
432
+ * Before math content, definitely not before a closing fence.
433
+ *
434
+ * ```markdown
435
+ * | $$
436
+ * > | \frac{1}{2}
437
+ * ^
438
+ * | $$
439
+ * ```
440
+ *
441
+ * @type {State}
442
+ */
443
+ function contentStart(code) {
444
+ return (initialSize ? factorySpace(effects, beforeContentChunk, "linePrefix", initialSize + 1) : beforeContentChunk)(code);
445
+ }
446
+ /**
447
+ * Before math content, after optional prefix.
448
+ *
449
+ * ```markdown
450
+ * | $$
451
+ * > | \frac{1}{2}
452
+ * ^
453
+ * | $$
454
+ * ```
455
+ *
456
+ * @type {State}
457
+ */
458
+ function beforeContentChunk(code) {
459
+ if (code === null) return after(code);
460
+ if (markdownLineEnding(code)) return effects.attempt(nonLazyContinuation, beforeNonLazyContinuation, after)(code);
461
+ effects.enter("mathFlowValue");
462
+ return contentChunk(code);
463
+ }
464
+ /**
465
+ * In math content.
466
+ *
467
+ * ```markdown
468
+ * | $$
469
+ * > | \frac{1}{2}
470
+ * ^
471
+ * | $$
472
+ * ```
473
+ *
474
+ * @type {State}
475
+ */
476
+ function contentChunk(code) {
477
+ if (code === null || markdownLineEnding(code)) {
478
+ effects.exit("mathFlowValue");
479
+ return beforeContentChunk(code);
480
+ }
481
+ effects.consume(code);
482
+ return contentChunk;
483
+ }
484
+ /**
485
+ * After math (ha!).
486
+ *
487
+ * ```markdown
488
+ * | $$
489
+ * | \frac{1}{2}
490
+ * > | $$
491
+ * ^
492
+ * ```
493
+ *
494
+ * @type {State}
495
+ */
496
+ function after(code) {
497
+ effects.exit("mathFlow");
498
+ return ok(code);
499
+ }
500
+ /** @type {Tokenizer} */
501
+ function tokenizeClosingFence(effects, ok, nok) {
502
+ let size = 0;
503
+ /**
504
+ * Before closing fence, at optional whitespace.
505
+ *
506
+ * ```markdown
507
+ * | $$
508
+ * | \frac{1}{2}
509
+ * > | $$
510
+ * ^
511
+ * ```
512
+ */
513
+ return factorySpace(effects, beforeSequenceClose, "linePrefix", self.parser.constructs.disable.null.includes("codeIndented") ? void 0 : 4);
514
+ /**
515
+ * In closing fence, after optional whitespace, at sequence.
516
+ *
517
+ * ```markdown
518
+ * | $$
519
+ * | \frac{1}{2}
520
+ * > | $$
521
+ * ^
522
+ * ```
523
+ *
524
+ * @type {State}
525
+ */
526
+ function beforeSequenceClose(code) {
527
+ effects.enter("mathFlowFence");
528
+ effects.enter("mathFlowFenceSequence");
529
+ return sequenceClose(code);
530
+ }
531
+ /**
532
+ * In closing fence sequence.
533
+ *
534
+ * ```markdown
535
+ * | $$
536
+ * | \frac{1}{2}
537
+ * > | $$
538
+ * ^
539
+ * ```
540
+ *
541
+ * @type {State}
542
+ */
543
+ function sequenceClose(code) {
544
+ if (code === 36) {
545
+ size++;
546
+ effects.consume(code);
547
+ return sequenceClose;
548
+ }
549
+ if (size < sizeOpen) return nok(code);
550
+ effects.exit("mathFlowFenceSequence");
551
+ return factorySpace(effects, afterSequenceClose, "whitespace")(code);
552
+ }
553
+ /**
554
+ * After closing fence sequence, after optional whitespace.
555
+ *
556
+ * ```markdown
557
+ * | $$
558
+ * | \frac{1}{2}
559
+ * > | $$
560
+ * ^
561
+ * ```
562
+ *
563
+ * @type {State}
564
+ */
565
+ function afterSequenceClose(code) {
566
+ if (code === null || markdownLineEnding(code)) {
567
+ effects.exit("mathFlowFence");
568
+ return ok(code);
569
+ }
570
+ return nok(code);
571
+ }
572
+ }
573
+ }
574
+ /**
575
+ * @this {TokenizeContext}
576
+ * @type {Tokenizer}
577
+ */
578
+ function tokenizeNonLazyContinuation(effects, ok, nok) {
579
+ const self = this;
580
+ return start;
581
+ /** @type {State} */
582
+ function start(code) {
583
+ if (code === null) return ok(code);
584
+ effects.enter("lineEnding");
585
+ effects.consume(code);
586
+ effects.exit("lineEnding");
587
+ return lineStart;
588
+ }
589
+ /** @type {State} */
590
+ function lineStart(code) {
591
+ return self.parser.lazy[self.now().line] ? nok(code) : ok(code);
592
+ }
593
+ }
594
+ //#endregion
595
+ //#region ../../node_modules/micromark-extension-math/lib/math-text.js
596
+ /**
597
+ * @import {Options} from 'micromark-extension-math'
598
+ * @import {Construct, Previous, Resolver, State, Token, TokenizeContext, Tokenizer} from 'micromark-util-types'
599
+ */
600
+ /**
601
+ * @param {Options | null | undefined} [options={}]
602
+ * Configuration (default: `{}`).
603
+ * @returns {Construct}
604
+ * Construct.
605
+ */
606
+ function mathText(options) {
607
+ let single = (options || {}).singleDollarTextMath;
608
+ if (single === null || single === void 0) single = true;
609
+ return {
610
+ tokenize: tokenizeMathText,
611
+ resolve: resolveMathText,
612
+ previous,
613
+ name: "mathText"
614
+ };
615
+ /**
616
+ * @this {TokenizeContext}
617
+ * @type {Tokenizer}
618
+ */
619
+ function tokenizeMathText(effects, ok, nok) {
620
+ let sizeOpen = 0;
621
+ /** @type {number} */
622
+ let size;
623
+ /** @type {Token} */
624
+ let token;
625
+ return start;
626
+ /**
627
+ * Start of math (text).
628
+ *
629
+ * ```markdown
630
+ * > | $a$
631
+ * ^
632
+ * > | \$a$
633
+ * ^
634
+ * ```
635
+ *
636
+ * @type {State}
637
+ */
638
+ function start(code) {
639
+ effects.enter("mathText");
640
+ effects.enter("mathTextSequence");
641
+ return sequenceOpen(code);
642
+ }
643
+ /**
644
+ * In opening sequence.
645
+ *
646
+ * ```markdown
647
+ * > | $a$
648
+ * ^
649
+ * ```
650
+ *
651
+ * @type {State}
652
+ */
653
+ function sequenceOpen(code) {
654
+ if (code === 36) {
655
+ effects.consume(code);
656
+ sizeOpen++;
657
+ return sequenceOpen;
658
+ }
659
+ if (sizeOpen < 2 && !single) return nok(code);
660
+ effects.exit("mathTextSequence");
661
+ return between(code);
662
+ }
663
+ /**
664
+ * Between something and something else.
665
+ *
666
+ * ```markdown
667
+ * > | $a$
668
+ * ^^
669
+ * ```
670
+ *
671
+ * @type {State}
672
+ */
673
+ function between(code) {
674
+ if (code === null) return nok(code);
675
+ if (code === 36) {
676
+ token = effects.enter("mathTextSequence");
677
+ size = 0;
678
+ return sequenceClose(code);
679
+ }
680
+ if (code === 32) {
681
+ effects.enter("space");
682
+ effects.consume(code);
683
+ effects.exit("space");
684
+ return between;
685
+ }
686
+ if (markdownLineEnding(code)) {
687
+ effects.enter("lineEnding");
688
+ effects.consume(code);
689
+ effects.exit("lineEnding");
690
+ return between;
691
+ }
692
+ effects.enter("mathTextData");
693
+ return data(code);
694
+ }
695
+ /**
696
+ * In data.
697
+ *
698
+ * ```markdown
699
+ * > | $a$
700
+ * ^
701
+ * ```
702
+ *
703
+ * @type {State}
704
+ */
705
+ function data(code) {
706
+ if (code === null || code === 32 || code === 36 || markdownLineEnding(code)) {
707
+ effects.exit("mathTextData");
708
+ return between(code);
709
+ }
710
+ effects.consume(code);
711
+ return data;
712
+ }
713
+ /**
714
+ * In closing sequence.
715
+ *
716
+ * ```markdown
717
+ * > | `a`
718
+ * ^
719
+ * ```
720
+ *
721
+ * @type {State}
722
+ */
723
+ function sequenceClose(code) {
724
+ if (code === 36) {
725
+ effects.consume(code);
726
+ size++;
727
+ return sequenceClose;
728
+ }
729
+ if (size === sizeOpen) {
730
+ effects.exit("mathTextSequence");
731
+ effects.exit("mathText");
732
+ return ok(code);
733
+ }
734
+ token.type = "mathTextData";
735
+ return data(code);
736
+ }
737
+ }
738
+ }
739
+ /** @type {Resolver} */
740
+ function resolveMathText(events) {
741
+ let tailExitIndex = events.length - 4;
742
+ let headEnterIndex = 3;
743
+ /** @type {number} */
744
+ let index;
745
+ /** @type {number | undefined} */
746
+ let enter;
747
+ if ((events[headEnterIndex][1].type === "lineEnding" || events[headEnterIndex][1].type === "space") && (events[tailExitIndex][1].type === "lineEnding" || events[tailExitIndex][1].type === "space")) {
748
+ index = headEnterIndex;
749
+ while (++index < tailExitIndex) if (events[index][1].type === "mathTextData") {
750
+ events[tailExitIndex][1].type = "mathTextPadding";
751
+ events[headEnterIndex][1].type = "mathTextPadding";
752
+ headEnterIndex += 2;
753
+ tailExitIndex -= 2;
754
+ break;
755
+ }
756
+ }
757
+ index = headEnterIndex - 1;
758
+ tailExitIndex++;
759
+ while (++index <= tailExitIndex) if (enter === void 0) {
760
+ if (index !== tailExitIndex && events[index][1].type !== "lineEnding") enter = index;
761
+ } else if (index === tailExitIndex || events[index][1].type === "lineEnding") {
762
+ events[enter][1].type = "mathTextData";
763
+ if (index !== enter + 2) {
764
+ events[enter][1].end = events[index - 1][1].end;
765
+ events.splice(enter + 2, index - enter - 2);
766
+ tailExitIndex -= index - enter - 2;
767
+ index = enter + 2;
768
+ }
769
+ enter = void 0;
770
+ }
771
+ return events;
772
+ }
773
+ /**
774
+ * @this {TokenizeContext}
775
+ * @type {Previous}
776
+ */
777
+ function previous(code) {
778
+ return code !== 36 || this.events[this.events.length - 1][1].type === "characterEscape";
779
+ }
780
+ //#endregion
781
+ //#region ../../node_modules/micromark-extension-math/lib/syntax.js
782
+ /**
783
+ * @import {Options} from 'micromark-extension-math'
784
+ * @import {Extension} from 'micromark-util-types'
785
+ */
786
+ /**
787
+ * Create an extension for `micromark` to enable math syntax.
788
+ *
789
+ * @param {Options | null | undefined} [options={}]
790
+ * Configuration (default: `{}`).
791
+ * @returns {Extension}
792
+ * Extension for `micromark` that can be passed in `extensions`, to
793
+ * enable math syntax.
794
+ */
795
+ function math(options) {
796
+ return {
797
+ flow: { [36]: mathFlow },
798
+ text: { [36]: mathText(options) }
799
+ };
800
+ }
801
+ //#endregion
802
+ //#region src/markdown-latex-math.ts
803
+ const backslash = 92;
804
+ /**
805
+ * `\(…\)` inline and `\[…\]` display math, read ahead of character escapes.
806
+ * `\[` and `\]` count only on their own lines, so an escaped `\[1\]` stays prose.
807
+ */
808
+ function latexMath() {
809
+ return { text: { [backslash]: [delimitedMath("latexInlineMath", 40, 41, false), delimitedMath("latexDisplayMath", 91, 93, true)] } };
810
+ }
811
+ /** Builds the nodes `$` and `$$` produce. */
812
+ function latexMathFromMarkdown() {
813
+ return {
814
+ enter: {
815
+ latexInlineMath(token) {
816
+ this.enter({
817
+ type: "inlineMath",
818
+ value: ""
819
+ }, token);
820
+ this.buffer();
821
+ },
822
+ latexDisplayMath(token) {
823
+ this.enter({
824
+ type: "math",
825
+ value: ""
826
+ }, token);
827
+ this.buffer();
828
+ }
829
+ },
830
+ exit: {
831
+ latexInlineMath: exitMath,
832
+ latexDisplayMath: exitMath,
833
+ latexMathData(token) {
834
+ this.config.enter.data.call(this, token);
835
+ this.config.exit.data.call(this, token);
836
+ }
837
+ }
838
+ };
839
+ }
840
+ const exitMath = function(token) {
841
+ const value = this.resume().trim();
842
+ const node = this.stack.at(-1);
843
+ if (node?.type !== "inlineMath" && node?.type !== "math") throw new Error(`Expected a math node, found ${node?.type}.`);
844
+ node.value = value;
845
+ this.exit(token);
846
+ };
847
+ const lineEnding = (code) => code !== null && code < -2;
848
+ const lineSpace = (code) => code === 32 || code === -2 || code === -1;
849
+ function delimitedMath(type, open, close, ownLine) {
850
+ const closing = {
851
+ tokenize: tokenizeClosing,
852
+ partial: true
853
+ };
854
+ return {
855
+ name: type,
856
+ tokenize: tokenizeMath
857
+ };
858
+ function tokenizeMath(effects, ok, nok) {
859
+ let lineStart = false;
860
+ return start;
861
+ function start(code) {
862
+ effects.enter(type);
863
+ effects.enter("latexMathSequence");
864
+ effects.consume(code);
865
+ return opening;
866
+ }
867
+ function opening(code) {
868
+ if (code !== open) return nok(code);
869
+ effects.consume(code);
870
+ return ownLine ? openingLine : openingEnd;
871
+ }
872
+ function openingLine(code) {
873
+ if (lineSpace(code)) {
874
+ effects.consume(code);
875
+ return openingLine;
876
+ }
877
+ return lineEnding(code) ? openingEnd(code) : nok(code);
878
+ }
879
+ function openingEnd(code) {
880
+ effects.exit("latexMathSequence");
881
+ return between(code);
882
+ }
883
+ function between(code) {
884
+ if (code === null) return nok(code);
885
+ if (code === backslash) return ownLine && !lineStart ? escape(code) : effects.attempt(closing, after, escape)(code);
886
+ if (lineEnding(code)) {
887
+ effects.enter("lineEnding");
888
+ effects.consume(code);
889
+ effects.exit("lineEnding");
890
+ lineStart = true;
891
+ return between;
892
+ }
893
+ effects.enter("latexMathData");
894
+ return data(code);
895
+ }
896
+ function escape(code) {
897
+ effects.enter("latexMathData");
898
+ effects.consume(code);
899
+ lineStart = false;
900
+ return escaped;
901
+ }
902
+ function escaped(code) {
903
+ if (code === null || lineEnding(code)) {
904
+ effects.exit("latexMathData");
905
+ return between(code);
906
+ }
907
+ effects.consume(code);
908
+ return data;
909
+ }
910
+ function data(code) {
911
+ if (code === null || code === backslash || lineEnding(code)) {
912
+ effects.exit("latexMathData");
913
+ return between(code);
914
+ }
915
+ if (!lineSpace(code)) lineStart = false;
916
+ effects.consume(code);
917
+ return data;
918
+ }
919
+ function after(code) {
920
+ effects.exit(type);
921
+ return ok(code);
922
+ }
923
+ }
924
+ function tokenizeClosing(effects, ok, nok) {
925
+ return start;
926
+ function start(code) {
927
+ effects.enter("latexMathSequence");
928
+ effects.consume(code);
929
+ return closer;
930
+ }
931
+ function closer(code) {
932
+ if (code !== close) return nok(code);
933
+ effects.consume(code);
934
+ return ownLine ? closingLine : closingEnd;
935
+ }
936
+ function closingLine(code) {
937
+ if (lineSpace(code)) {
938
+ effects.consume(code);
939
+ return closingLine;
940
+ }
941
+ return code === null || lineEnding(code) ? closingEnd(code) : nok(code);
942
+ }
943
+ function closingEnd(code) {
944
+ effects.exit("latexMathSequence");
945
+ return ok(code);
946
+ }
947
+ }
948
+ }
949
+ //#endregion
950
+ //#region src/markdown.ts
951
+ function* markdownNodes(node) {
952
+ yield node;
953
+ for (const child of node.children ?? []) yield* markdownNodes(child);
954
+ }
955
+ /** The text a node carries, its own and every descendant's. */
956
+ function markdownText(node) {
957
+ return [...markdownNodes(node)].map((child) => child.value ?? "").join("");
958
+ }
959
+ /** Shared syntax for submit-time source checks and safe Markdown rendering. */
960
+ function parseMarkdown(source) {
961
+ const tree = fromMarkdown(source, {
962
+ extensions: [
963
+ gfm(),
964
+ math(),
965
+ latexMath()
966
+ ],
967
+ mdastExtensions: [
968
+ gfmFromMarkdown(),
969
+ mathFromMarkdown(),
970
+ latexMathFromMarkdown()
971
+ ]
972
+ });
973
+ for (const node of markdownNodes(tree)) {
974
+ const [only, ...others] = node.children ?? [];
975
+ if (node.type === "paragraph" && only?.type === "inlineMath" && others.length === 0 && source.startsWith("$$", only.position?.start.offset)) only.type = "math";
976
+ }
977
+ const definitions = /* @__PURE__ */ new Map();
978
+ for (const node of markdownNodes(tree)) if (node.type === "definition" && !definitions.has(node.identifier)) definitions.set(node.identifier, node);
979
+ for (const node of markdownNodes(tree)) {
980
+ if (node.type !== "linkReference") continue;
981
+ const definition = definitions.get(node.identifier);
982
+ if (definition) Object.assign(node, {
983
+ type: "link",
984
+ url: definition.url,
985
+ title: definition.title
986
+ });
987
+ }
988
+ return tree;
989
+ }
990
+ //#endregion
991
+ //#region src/source.ts
992
+ const label$2 = z.string().trim().min(1);
993
+ /** The repository and commits one source reference was read from. A
994
+ * reference without pins resolves against its document's pins; one with
995
+ * pins names them itself, so a document can quote several repositories or
996
+ * commits. `base` is only needed by references to the base side. */
997
+ const sourcePinsSchema = z.strictObject({
998
+ repositoryId: label$2,
999
+ head: label$2,
1000
+ base: label$2.optional()
1001
+ });
1002
+ /** Internal coordinates for reading one revision or storing resolved coverage.
1003
+ * Authored document attachments use DiffSelection instead. */
1004
+ const fileLineRangeSchema = z.strictObject({
1005
+ side: z.enum(["base", "head"]).default("head"),
1006
+ file: label$2,
1007
+ fromLine: z.number().int().positive(),
1008
+ toLine: z.number().int().positive(),
1009
+ pins: sourcePinsSchema.optional()
1010
+ }).refine((s) => s.toLine >= s.fromLine, "Source range ends before it starts.").refine((s) => s.side === "head" || !s.pins || s.pins.base !== void 0, "A base-side source needs base pins.");
1011
+ /** Thrown by the pure checks; each boundary translates it for its clients. */
1012
+ var SourceRangeError = class extends Error {};
1013
+ /** Git and jj read committed objects, so a lexical check suffices: nothing
1014
+ * here follows a working-copy symlink. An empty string names the root. */
1015
+ function checkSourcePath(file) {
1016
+ if (file.startsWith("/") || file.includes("\\") || file.split("/").some((part) => part === ".." || part === ".") || /[\u0000-\u001f]/.test(file)) throw new SourceRangeError("Source file must be a repository-relative path.");
1017
+ }
1018
+ /** The range's text from a whole file. A trailing newline is not a line. */
1019
+ function sliceSourceRange(text, range) {
1020
+ const lines = text.split(/\r?\n/);
1021
+ if (text.endsWith("\n")) lines.pop();
1022
+ if (text === "" || range.toLine > lines.length) throw new SourceRangeError(`Source range ${range.file}:${range.fromLine}-${range.toLine} exceeds the pinned file (${lines.length} lines).`);
1023
+ return lines.slice(range.fromLine - 1, range.toLine).join("\n");
1024
+ }
1025
+ /** A source shown as a peek must contain visible text; a whitespace-only
1026
+ * range is an authoring mistake. Prose links may point at blank lines, so
1027
+ * only peek-rendering references call this. */
1028
+ function requireVisibleSource(text, range) {
1029
+ if (text.trim() === "") throw new SourceRangeError(`Source range ${range.file}:${range.fromLine}-${range.toLine} contains only whitespace.`);
1030
+ }
1031
+ //#endregion
1032
+ //#region src/review-api/blocks/definition.ts
1033
+ const text$1 = z.string();
1034
+ const label$1 = text$1.trim().min(1);
1035
+ const identity = { id: text$1.optional() };
1036
+ /** The repository and commits an element's anchors quote. A step, frame,
1037
+ * attachment or operation without them reads its block's; a block without
1038
+ * them reads its document's. */
1039
+ const elementPins = sourcePinsSchema.optional().describe("Pins for the anchors here; needed on the scratchpad.");
1040
+ /** The strict schema for a block's own fields; id and type are added here. */
1041
+ function defineBlock(type, props) {
1042
+ return z.strictObject({
1043
+ ...identity,
1044
+ type: z.literal(type),
1045
+ ...props
1046
+ });
1047
+ }
1048
+ /** Shared by every check that resolves a component-local name. */
1049
+ function requireKey(record, name) {
1050
+ if (!Object.hasOwn(record, name)) throw new ReviewInputError(`Unknown component name: ${name}`);
1051
+ }
1052
+ //#endregion
1053
+ //#region src/review-api/diff-lenses.ts
1054
+ /**
1055
+ * Lenses partition a review's change for the Diff view. They live on the
1056
+ * snapshot beside the document, are versioned with it, and are written one
1057
+ * at a time with the lens command; the document never holds them.
1058
+ */
1059
+ const patternsSchema = z.array(label$1).min(1).max(1e3);
1060
+ const lensTargetSchema = z.discriminatedUnion("kind", [z.strictObject({
1061
+ kind: z.literal("files"),
1062
+ patterns: patternsSchema
1063
+ }), z.strictObject({
1064
+ kind: z.literal("ranges"),
1065
+ sources: z.array(anchorSchema).min(1).max(1e4)
1066
+ })]);
1067
+ const lensTargetsSchema = z.array(lensTargetSchema).min(1).max(1e3).describe("Select changed lines using repository-relative paths/globs or source ranges. Counts and viewed actions apply only to those lines.");
1068
+ const lensSchema = z.strictObject({
1069
+ id: label$1,
1070
+ title: label$1,
1071
+ targets: lensTargetsSchema
1072
+ });
1073
+ /** One lens edit. Ids are host-assigned (`lens-N`); an update patches only
1074
+ * the fields it names. */
1075
+ const lensEditSchema = z.discriminatedUnion("type", [
1076
+ z.strictObject({
1077
+ type: z.literal("insert"),
1078
+ title: label$1,
1079
+ targets: lensTargetsSchema,
1080
+ afterId: label$1.optional().describe("The lens this one follows. Omitted appends.")
1081
+ }),
1082
+ z.strictObject({
1083
+ type: z.literal("update"),
1084
+ targetId: label$1,
1085
+ title: label$1.optional(),
1086
+ targets: lensTargetsSchema.optional()
1087
+ }),
1088
+ z.strictObject({
1089
+ type: z.literal("remove"),
1090
+ targetId: label$1
1091
+ })
1092
+ ]);
1093
+ /** Patterns are evaluated against the changed-file list, never the
1094
+ * filesystem, but must still read as repository-relative globs. */
1095
+ function checkLensTargets(targets) {
1096
+ for (const pattern of targets.flatMap((target) => target.kind === "files" ? target.patterns : [])) if (pattern.startsWith("/") || pattern.split("/").includes("..") || pattern.includes("\\") || pattern.includes("\0")) throw new ReviewInputError("File lens patterns must be repository-relative paths or globs using forward slashes.");
1097
+ }
1098
+ /** Apply one edit in place and return the lens it landed on. */
1099
+ function applyLensEdit(lenses, edit, allocate) {
1100
+ const index = (id) => {
1101
+ const at = lenses.findIndex((lens) => lens.id === id);
1102
+ if (at < 0) throw new ReviewInputError(`Lens ${id} does not exist.`);
1103
+ return at;
1104
+ };
1105
+ if (edit.type === "insert") {
1106
+ if (lenses.length >= 200) throw new ReviewInputError(`A review holds at most 200 lenses.`);
1107
+ checkLensTargets(edit.targets);
1108
+ const lens = {
1109
+ id: allocate(),
1110
+ title: edit.title,
1111
+ targets: edit.targets
1112
+ };
1113
+ lenses.splice(edit.afterId === void 0 ? lenses.length : index(edit.afterId) + 1, 0, lens);
1114
+ return lens;
1115
+ }
1116
+ const at = index(edit.targetId);
1117
+ const lens = lenses[at];
1118
+ if (edit.type === "remove") {
1119
+ lenses.splice(at, 1);
1120
+ return lens;
1121
+ }
1122
+ if (edit.title === void 0 && edit.targets === void 0) throw new ReviewInputError("A lens update names a title or targets.");
1123
+ if (edit.targets) checkLensTargets(edit.targets);
1124
+ const next = {
1125
+ ...lens,
1126
+ ...edit.title !== void 0 && { title: edit.title },
1127
+ ...edit.targets !== void 0 && { targets: edit.targets }
1128
+ };
1129
+ lenses[at] = next;
1130
+ return next;
1131
+ }
1132
+ /** Every range a lens selects, with an id stable for its position. Lens
1133
+ * ranges read at the review's pins. */
1134
+ function lensSelections(lenses) {
1135
+ return lenses.flatMap((lens) => lens.targets.flatMap((target, index) => target.kind === "ranges" ? target.sources.map((source, range) => ({
1136
+ id: `${lens.id}:target:${index}:${range}`,
1137
+ source: anchorSelection(source)
1138
+ })) : []));
1139
+ }
1140
+ /** The lens id the Diff view gives changed lines no lens selects. */
1141
+ const UNCATEGORIZED_LENS_ID = "automatic-uncategorized";
1142
+ //#endregion
1143
+ //#region src/review-api/blocks/flow_diagram.ts
1144
+ /** Nodes and edges are the diagram's children: each is its own edit target,
1145
+ * so an agent can draw a diagram one unit at a time. Edges still name nodes
1146
+ * by their diagram-local key. */
1147
+ const flowNodeSchema = z.strictObject({
1148
+ ...identity,
1149
+ type: z.literal("flow_node").default("flow_node"),
1150
+ key: label$1,
1151
+ label: label$1,
1152
+ description: z.string().optional(),
1153
+ kind: z.enum([
1154
+ "process",
1155
+ "decision",
1156
+ "terminal"
1157
+ ]).optional(),
1158
+ attachments: z.array(z.strictObject({
1159
+ label: label$1,
1160
+ sources: z.array(anchorSchema).min(1),
1161
+ pins: elementPins
1162
+ })).default([]).describe("Code evidence for this node. Omit for nodes no code backs, such as start or exit states.")
1163
+ });
1164
+ const flowEdgeSchema = z.strictObject({
1165
+ ...identity,
1166
+ type: z.literal("flow_edge").default("flow_edge"),
1167
+ from: label$1,
1168
+ to: label$1,
1169
+ label: z.string().optional(),
1170
+ style: z.enum(["solid", "dashed"]).optional()
1171
+ });
1172
+ /** The edge a new node arrives with: `from` a node already drawn to this
1173
+ * one, or from this one `to` a node already drawn. Insert-time only; the
1174
+ * server stores it as an ordinary edge. */
1175
+ const flowLinkSchema = z.strictObject({
1176
+ from: label$1.optional(),
1177
+ to: label$1.optional(),
1178
+ label: z.string().optional(),
1179
+ style: z.enum(["solid", "dashed"]).optional()
1180
+ });
1181
+ const flowNodeInsertSchema = flowNodeSchema.extend({ link: flowLinkSchema.optional() });
1182
+ const flow_diagram = {
1183
+ type: "flow_diagram",
1184
+ schema: defineBlock("flow_diagram", {
1185
+ title: label$1,
1186
+ description: z.string().optional(),
1187
+ direction: z.enum(["right", "down"]).optional(),
1188
+ nodes: z.array(flowNodeSchema).min(1).max(100),
1189
+ edges: z.array(flowEdgeSchema).max(300),
1190
+ pins: elementPins
1191
+ }),
1192
+ check(block) {
1193
+ const keys = new Set(block.nodes.map((node) => node.key));
1194
+ if (keys.size !== block.nodes.length) throw new ReviewInputError("Flow node keys must be unique within the diagram.");
1195
+ for (const edge of block.edges) if (!keys.has(edge.from) || !keys.has(edge.to)) throw new ReviewInputError(`Unknown flow endpoint: ${edge.from} → ${edge.to}`);
1196
+ }
1197
+ };
1198
+ //#endregion
1199
+ //#region src/review-api/blocks/call_stack_diff.ts
1200
+ const frameSchema = z.strictObject({
1201
+ ...identity,
1202
+ key: label$1.optional(),
1203
+ parentKey: label$1.nullable().optional(),
1204
+ callSite: anchorSchema.optional(),
1205
+ source: anchorSchema,
1206
+ contextSources: z.array(anchorSchema).max(1e3).optional().describe("Supporting code owned by this frame, such as field initializers. These ranges share the frame’s diff section and coverage; they do not create call edges."),
1207
+ label: label$1.optional(),
1208
+ pins: elementPins,
1209
+ via: z.strictObject({
1210
+ kind: z.enum([
1211
+ "call",
1212
+ "queue",
1213
+ "callback",
1214
+ "rpc"
1215
+ ]),
1216
+ reason: label$1
1217
+ }).optional()
1218
+ });
1219
+ const call_stack_diff = {
1220
+ type: "call_stack_diff",
1221
+ schema: defineBlock("call_stack_diff", {
1222
+ title: label$1,
1223
+ base: z.array(frameSchema),
1224
+ head: z.array(frameSchema),
1225
+ pins: elementPins
1226
+ }),
1227
+ check(block) {
1228
+ for (const side of ["base", "head"]) {
1229
+ const keys = block[side].flatMap((frame) => frame.key ? [frame.key] : []);
1230
+ if (new Set(keys).size !== keys.length) throw new ReviewInputError(`Frame keys must be unique within ${side}.`);
1231
+ for (const [index, frame] of block[side].entries()) if (frame.parentKey && !block[side].slice(0, index).some((parent) => parent.key === frame.parentKey)) throw new ReviewInputError("A frame parentKey must name an earlier frame on the same side.");
1232
+ }
1233
+ }
1234
+ };
1235
+ //#endregion
1236
+ //#region src/review-api/blocks/callout.ts
1237
+ const calloutSchema = defineBlock("callout", {
1238
+ title: label$1.optional(),
1239
+ tone: z.enum([
1240
+ "info",
1241
+ "warning",
1242
+ "danger",
1243
+ "success"
1244
+ ]).default("info"),
1245
+ children: z.array(z.lazy(() => blockSchema))
1246
+ });
1247
+ const callout = {
1248
+ type: "callout",
1249
+ schema: calloutSchema
1250
+ };
1251
+ //#endregion
1252
+ //#region src/review-api/blocks/code.ts
1253
+ /** Shared with sequence steps. */
1254
+ const codeFields = {
1255
+ language: text$1.default("text"),
1256
+ text: text$1
1257
+ };
1258
+ const code = {
1259
+ type: "code",
1260
+ schema: defineBlock("code", {
1261
+ ...codeFields,
1262
+ caption: text$1.optional()
1263
+ })
1264
+ };
1265
+ //#endregion
1266
+ //#region src/review-api/blocks/code_peek.ts
1267
+ const code_peek = {
1268
+ type: "code_peek",
1269
+ schema: defineBlock("code_peek", {
1270
+ source: anchorSchema,
1271
+ pins: elementPins,
1272
+ caption: text$1.optional()
1273
+ })
1274
+ };
1275
+ //#endregion
1276
+ //#region src/review-api/blocks/database_lens.ts
1277
+ const fieldSchema = z.lazy(() => z.strictObject({
1278
+ label: label$1,
1279
+ dataType: label$1,
1280
+ nullable: z.boolean().optional(),
1281
+ primaryKey: z.boolean().optional(),
1282
+ references: z.strictObject({
1283
+ store: label$1,
1284
+ collection: label$1,
1285
+ field: label$1
1286
+ }).optional(),
1287
+ example: jsonValueSchema.optional(),
1288
+ fields: z.record(text$1, fieldSchema).optional()
1289
+ }));
1290
+ const databaseActorSchema = z.union([label$1, z.strictObject({
1291
+ label: label$1,
1292
+ softwareMapPath: label$1.optional()
1293
+ })]);
1294
+ const storeSchema = z.strictObject({
1295
+ label: label$1,
1296
+ storage: z.enum(["relational", "document"]),
1297
+ dataStoreKind: z.enum([
1298
+ "database",
1299
+ "objectStore",
1300
+ "bucket",
1301
+ "artifactStore",
1302
+ "fileStore"
1303
+ ]).optional(),
1304
+ softwareMapPath: label$1.optional(),
1305
+ collections: z.record(text$1, z.strictObject({
1306
+ label: label$1,
1307
+ key: label$1.optional(),
1308
+ fields: z.record(text$1, fieldSchema)
1309
+ }))
1310
+ });
1311
+ const operationSchema = z.strictObject({
1312
+ ...identity,
1313
+ kind: z.enum(["read", "write"]),
1314
+ store: label$1,
1315
+ collection: label$1,
1316
+ field: label$1.optional(),
1317
+ actor: label$1,
1318
+ label: label$1,
1319
+ detail: label$1.optional(),
1320
+ source: anchorSchema,
1321
+ pins: elementPins
1322
+ });
1323
+ const database_lens = {
1324
+ type: "database_lens",
1325
+ schema: defineBlock("database_lens", {
1326
+ title: label$1,
1327
+ actors: z.record(text$1, databaseActorSchema),
1328
+ stores: z.record(text$1, storeSchema).refine((stores) => Object.keys(stores).length > 0, "A database lens needs at least one store."),
1329
+ useCases: z.array(z.strictObject({
1330
+ ...identity,
1331
+ label: label$1,
1332
+ summary: text$1.optional(),
1333
+ operations: z.array(operationSchema).min(1, "A use case needs at least one operation.")
1334
+ })).min(1, "A database lens needs at least one use case."),
1335
+ pins: elementPins
1336
+ }),
1337
+ check(block) {
1338
+ const field = (store, collection, name) => {
1339
+ requireKey(block.stores, store);
1340
+ const collections = block.stores[store].collections;
1341
+ requireKey(collections, collection);
1342
+ if (name === void 0) return;
1343
+ let fields = collections[collection].fields;
1344
+ for (const part of name.split(".")) {
1345
+ if (!fields) throw new ReviewInputError(`Unknown component name: ${name}`);
1346
+ requireKey(fields, part);
1347
+ fields = fields[part].fields;
1348
+ }
1349
+ };
1350
+ for (const useCase of block.useCases) for (const op of useCase.operations) {
1351
+ requireKey(block.actors, op.actor);
1352
+ field(op.store, op.collection, op.field);
1353
+ }
1354
+ for (const store of Object.values(block.stores)) for (const collection of Object.values(store.collections)) for (const value of Object.values(collection.fields)) if (value.references) field(value.references.store, value.references.collection, value.references.field);
1355
+ }
1356
+ };
1357
+ //#endregion
1358
+ //#region src/review-api/blocks/divider.ts
1359
+ const divider = {
1360
+ type: "divider",
1361
+ schema: defineBlock("divider", {})
1362
+ };
1363
+ //#endregion
1364
+ //#region src/review-api/blocks/image.ts
1365
+ const image = {
1366
+ type: "image",
1367
+ schema: defineBlock("image", {
1368
+ assetId: label$1,
1369
+ alt: label$1,
1370
+ caption: text$1.optional()
1371
+ })
1372
+ };
1373
+ //#endregion
1374
+ //#region src/review-api/blocks/markdown.ts
1375
+ const markdown = {
1376
+ type: "markdown",
1377
+ schema: defineBlock("markdown", {
1378
+ markdown: text$1.describe("Safe Markdown. Repository file links must use [label](review-source:head/path#L10-L24) or review-source:base/path#L10-L24, with a repository-relative path and verified line numbers. Relative paths, absolute filesystem paths, and file/editor URLs are rejected. External links use https://, http://, or mailto:; document anchors use #heading."),
1379
+ pins: sourcePinsSchema.optional().describe("Repository and commits this block's review-source links resolve against, instead of the document's pins.")
1380
+ })
1381
+ };
1382
+ //#endregion
1383
+ //#region src/review-api/blocks/section.ts
1384
+ const sectionSchema = defineBlock("section", {
1385
+ title: label$1,
1386
+ defaultCollapsed: z.boolean().optional(),
1387
+ children: z.array(z.lazy(() => blockSchema))
1388
+ });
1389
+ const section = {
1390
+ type: "section",
1391
+ schema: sectionSchema
1392
+ };
1393
+ //#endregion
1394
+ //#region src/review-api/blocks/sequence.ts
1395
+ const stepSchema = z.strictObject({
1396
+ ...identity,
1397
+ type: z.literal("step").default("step"),
1398
+ from: label$1,
1399
+ to: label$1,
1400
+ label: label$1,
1401
+ style: z.enum([
1402
+ "call",
1403
+ "return",
1404
+ "async"
1405
+ ]).default("call"),
1406
+ source: anchorSchema.optional(),
1407
+ pins: elementPins,
1408
+ explanation: label$1.optional(),
1409
+ code: z.strictObject(codeFields).optional()
1410
+ }).refine((s) => [
1411
+ s.source,
1412
+ s.explanation,
1413
+ s.code
1414
+ ].filter((v) => v !== void 0).length === 1, "A step needs exactly one of source, explanation, or code.");
1415
+ const sequence = {
1416
+ type: "sequence",
1417
+ schema: defineBlock("sequence", {
1418
+ title: label$1,
1419
+ actors: z.record(text$1, label$1),
1420
+ steps: z.array(stepSchema),
1421
+ pins: elementPins
1422
+ }),
1423
+ check(block) {
1424
+ for (const step of block.steps) {
1425
+ requireKey(block.actors, step.from);
1426
+ requireKey(block.actors, step.to);
1427
+ }
1428
+ }
1429
+ };
1430
+ //#endregion
1431
+ //#region src/review-api/blocks/software_map.ts
1432
+ const software_map = {
1433
+ type: "software_map",
1434
+ schema: defineBlock("software_map", {
1435
+ mapVersionId: label$1,
1436
+ focusElementId: label$1.optional()
1437
+ })
1438
+ };
1439
+ //#endregion
1440
+ //#region src/review-api/blocks/trace_quote.ts
1441
+ const trace_quote = {
1442
+ type: "trace_quote",
1443
+ schema: defineBlock("trace_quote", {
1444
+ traceId: label$1,
1445
+ eventId: label$1,
1446
+ text: label$1
1447
+ })
1448
+ };
1449
+ //#endregion
1450
+ //#region src/tutorial-conversation.ts
1451
+ const tutorialAuthoringConversationSchema = z.strictObject({
1452
+ version: z.literal(1),
1453
+ title: z.string().trim().min(1),
1454
+ messages: z.array(z.strictObject({
1455
+ role: z.enum(["user", "assistant"]),
1456
+ body: z.string().trim().min(1)
1457
+ })).min(2)
1458
+ });
1459
+ //#endregion
1460
+ //#region src/review-api/blocks/tutorial.ts
1461
+ const tutorialSchema = z.discriminatedUnion("kind", [
1462
+ defineBlock("tutorial", { kind: z.literal("keymap") }),
1463
+ defineBlock("tutorial", {
1464
+ kind: z.literal("conversation"),
1465
+ conversation: tutorialAuthoringConversationSchema
1466
+ }),
1467
+ defineBlock("tutorial", {
1468
+ kind: z.literal("view"),
1469
+ view: z.enum([
1470
+ "review",
1471
+ "commits",
1472
+ "diff",
1473
+ "map"
1474
+ ]),
1475
+ label: z.string()
1476
+ }),
1477
+ defineBlock("tutorial", {
1478
+ kind: z.literal("feature"),
1479
+ feature: z.literal("softwareMap"),
1480
+ children: z.array(z.lazy(() => blockSchema))
1481
+ })
1482
+ ]);
1483
+ const tutorial = {
1484
+ type: "tutorial",
1485
+ schema: tutorialSchema
1486
+ };
1487
+ //#endregion
1488
+ //#region src/review-api/blocks/index.ts
1489
+ /** Leaf kinds share one discriminated union so unknown types read as they always have. */
1490
+ const leafSchema = z.discriminatedUnion("type", [
1491
+ markdown.schema,
1492
+ code.schema,
1493
+ divider.schema,
1494
+ code_peek.schema,
1495
+ sequence.schema,
1496
+ call_stack_diff.schema,
1497
+ database_lens.schema,
1498
+ image.schema,
1499
+ trace_quote.schema,
1500
+ software_map.schema,
1501
+ flow_diagram.schema
1502
+ ]);
1503
+ /** Every block kind, keyed by type. A kind without a definition is a compile error. */
1504
+ const blocks = {
1505
+ markdown,
1506
+ code,
1507
+ divider,
1508
+ code_peek,
1509
+ sequence,
1510
+ call_stack_diff,
1511
+ database_lens,
1512
+ image,
1513
+ trace_quote,
1514
+ software_map,
1515
+ flow_diagram,
1516
+ section,
1517
+ callout,
1518
+ tutorial
1519
+ };
1520
+ /** Discriminated by type, so a malformed block reports its own kind's
1521
+ * issues rather than every kind's. */
1522
+ const blockSchema = z.lazy(() => z.discriminatedUnion("type", blockKindSchemas()));
1523
+ /** Each block kind's schema, for unions that add kinds of their own. Read
1524
+ * lazily: the container modules import this one. */
1525
+ const blockKindSchemas = () => [
1526
+ leafSchema,
1527
+ sectionSchema,
1528
+ calloutSchema,
1529
+ tutorialSchema
1530
+ ];
1531
+ function checkBlock(type, block) {
1532
+ blocks[type].check?.(block);
1533
+ }
1534
+ /** Relationships not expressible in a field schema. Sources/resources are checked by host providers. */
1535
+ function checkReferences(document) {
1536
+ const visit = (block) => {
1537
+ checkBlock(block.type, block);
1538
+ if ("children" in block) block.children.forEach(visit);
1539
+ };
1540
+ document.forEach(visit);
1541
+ }
1542
+ //#endregion
1543
+ //#region src/review-api/document.ts
1544
+ const text = z.string();
1545
+ const label = text.trim().min(1);
1546
+ const pinsSchema = z.strictObject({
1547
+ repositoryId: label,
1548
+ base: label,
1549
+ head: label
1550
+ });
1551
+ /** The pins one source reference reads at: its own when it names them,
1552
+ * otherwise its document's. A reference with base-less pins reads base at
1553
+ * head, like a commits target without a base. */
1554
+ function anchorPins(source, documentPins) {
1555
+ if (source.pins) return {
1556
+ repositoryId: source.pins.repositoryId,
1557
+ base: source.pins.base ?? source.pins.head,
1558
+ head: source.pins.head
1559
+ };
1560
+ if (!documentPins) throw new ReviewInputError("This source names no repository or commit, and the document has no pins.");
1561
+ return documentPins;
1562
+ }
1563
+ /** Distinct explicit pins named by a document's references, for validation. */
1564
+ function explicitPins(references) {
1565
+ const seen = /* @__PURE__ */ new Map();
1566
+ for (const { source } of references) if (source.pins) {
1567
+ const pins = anchorPins(source, void 0);
1568
+ seen.set(JSON.stringify(pins), pins);
1569
+ }
1570
+ return [...seen.values()];
1571
+ }
1572
+ const targets = (repository) => z.discriminatedUnion("kind", [z.strictObject({
1573
+ kind: z.literal("worktree"),
1574
+ ...repository,
1575
+ base: label.optional()
1576
+ }), z.strictObject({
1577
+ kind: z.literal("commits"),
1578
+ ...repository,
1579
+ head: label,
1580
+ base: label.optional()
1581
+ })]);
1582
+ const reviewTargetSchema = targets({ repositoryId: label });
1583
+ /** How agents name a target: by the checkout's path, registered on acceptance. */
1584
+ const pathTargetSchema = targets({ repositoryPath: label });
1585
+ const unitTypes = /* @__PURE__ */ new Set([
1586
+ "step",
1587
+ "flow_node",
1588
+ "flow_edge"
1589
+ ]);
1590
+ const isUnit = (element) => unitTypes.has(element.type);
1591
+ /** Inline trace quotes keep paragraph/list layout while using retained resources. */
1592
+ function traceQuoteLink(href) {
1593
+ const match = /^review-trace:([^#]+)#(.+)$/.exec(href);
1594
+ return match ? {
1595
+ traceId: decodeURIComponent(match[1]),
1596
+ eventId: decodeURIComponent(match[2])
1597
+ } : void 0;
1598
+ }
1599
+ function resourceReference(block) {
1600
+ switch (block.type) {
1601
+ case "image": return {
1602
+ id: block.assetId,
1603
+ kind: "image"
1604
+ };
1605
+ case "trace_quote": return {
1606
+ id: block.traceId,
1607
+ kind: "trace"
1608
+ };
1609
+ case "software_map": return {
1610
+ id: block.mapVersionId,
1611
+ kind: "map"
1612
+ };
1613
+ default: return;
1614
+ }
1615
+ }
1616
+ function resourceReferences(document) {
1617
+ return elements(document).flatMap((block) => {
1618
+ if (block.type !== "markdown") return block.type === "image" || block.type === "trace_quote" || block.type === "software_map" ? [block] : [];
1619
+ return [...markdownNodes(parseMarkdown(block.markdown))].flatMap((node) => {
1620
+ if (node.type !== "link" || !node.url?.startsWith("review-trace:")) return [];
1621
+ const quote = traceQuoteLink(node.url);
1622
+ if (!quote) throw new ReviewInputError("Invalid trace quote link.");
1623
+ return [{
1624
+ type: "trace_quote",
1625
+ ...quote,
1626
+ text: markdownText(node)
1627
+ }];
1628
+ });
1629
+ });
1630
+ }
1631
+ /**
1632
+ * Source-bearing items share their owning element's stable identity.
1633
+ * `tolerant` skips malformed Markdown source links instead of rejecting, for
1634
+ * content that is already stored.
1635
+ */
1636
+ function documentReferences(document, { tolerant = false } = {}) {
1637
+ const reject = (message) => {
1638
+ if (tolerant) return [];
1639
+ throw new ReviewInputError(message);
1640
+ };
1641
+ const select = (anchor, pins) => {
1642
+ const parsed = parseAnchor(anchor);
1643
+ if (!parsed) return reject(`Not a source anchor: ${anchor}`);
1644
+ const source = pins ? {
1645
+ ...parsed,
1646
+ pins
1647
+ } : parsed;
1648
+ const problem = selectionProblem(source);
1649
+ return problem ? reject(problem) : [source];
1650
+ };
1651
+ return elements(document).flatMap((element) => {
1652
+ if (element.type === "markdown") return [...markdownNodes(parseMarkdown(element.markdown))].flatMap((node) => {
1653
+ if (node.type !== "link") return [];
1654
+ const href = node.url ?? "";
1655
+ if (!/^review-source:/i.test(href)) {
1656
+ if (href.startsWith("review-trace:")) return [];
1657
+ if (/^(?:https?:\/\/|mailto:|#)/i.test(href)) return [];
1658
+ return reject(`Unsupported Markdown link ${JSON.stringify(href)} in block ${element.id}. Use [label](review-source:head/path#L10-L24) or review-source:base/path#L10-L24 for repository files, or review-source:diff/path#L84-R90 across sides, with a repository-relative path and verified line numbers. External links must use https://, http://, or mailto:; document anchors use #heading.`);
1659
+ }
1660
+ let target;
1661
+ try {
1662
+ target = decodeURIComponent(href.slice(14));
1663
+ } catch {
1664
+ return reject("Invalid URL encoding in source link.");
1665
+ }
1666
+ const anchor = target.replace(/^(head|base|diff)\//i, (side) => side.toLowerCase());
1667
+ if (!parseAnchor(anchor)) return reject("Use review-source:head/path#L10-L24 (or base, or diff/path#L84-R90 across sides) for a source link.");
1668
+ return select(anchor, element.pins).map((source) => ({
1669
+ id: `${element.id}:${node.url}`,
1670
+ source
1671
+ }));
1672
+ });
1673
+ if (element.type === "code_peek") return select(element.source, element.pins).map((source) => ({
1674
+ id: element.id,
1675
+ source,
1676
+ label: element.caption,
1677
+ peek: true
1678
+ }));
1679
+ if (element.type === "sequence") return element.steps.flatMap((step) => step.source ? select(step.source, step.pins ?? element.pins).map((source) => ({
1680
+ id: step.id,
1681
+ source,
1682
+ label: step.label,
1683
+ peek: true
1684
+ })) : []);
1685
+ if (element.type === "flow_diagram") return element.nodes.flatMap((node) => node.attachments.flatMap((attachment, index) => attachment.sources.flatMap((anchor, sourceIndex) => select(anchor, attachment.pins ?? element.pins).map((source) => ({
1686
+ id: `${element.id}:${node.key}:${index}:${sourceIndex}`,
1687
+ source,
1688
+ label: attachment.label,
1689
+ peek: true
1690
+ })))));
1691
+ if (element.type === "call_stack_diff") return [...element.base, ...element.head].flatMap((frame) => {
1692
+ const pins = frame.pins ?? element.pins;
1693
+ return [
1694
+ ...select(frame.source, pins).map((source) => ({
1695
+ id: frame.id,
1696
+ source,
1697
+ label: frame.label,
1698
+ peek: true
1699
+ })),
1700
+ ...(frame.contextSources ?? []).flatMap((anchor, index) => select(anchor, pins).map((source) => ({
1701
+ id: `${frame.id}:context:${index}`,
1702
+ source
1703
+ }))),
1704
+ ...frame.callSite ? select(frame.callSite, pins).map((source) => ({
1705
+ id: `${frame.id}:call-site`,
1706
+ source,
1707
+ label: frame.label,
1708
+ peek: true
1709
+ })) : []
1710
+ ];
1711
+ });
1712
+ if (element.type === "database_lens") return element.useCases.flatMap((useCase) => useCase.operations.flatMap((operation) => select(operation.source, operation.pins ?? element.pins).map((source) => ({
1713
+ id: operation.id,
1714
+ source,
1715
+ label: operation.label,
1716
+ peek: true
1717
+ }))));
1718
+ return [];
1719
+ });
1720
+ }
1721
+ /** All authored attachments, including code peeks, select the aligned diff. */
1722
+ const selectionReferences = documentReferences;
1723
+ /** Per-side read coordinates for endpoint validation and retained source quotes. */
1724
+ function sourceReferences(document, options = {}) {
1725
+ return selectionReferences(document, options).flatMap((ref) => sourceAnchors(ref.source).map((source) => ({
1726
+ ...ref,
1727
+ source
1728
+ })));
1729
+ }
1730
+ /** Includes lenses and maps without inline ranges. */
1731
+ function hasCodeReferences(review) {
1732
+ return Boolean(sourceReferences(review.document).length || review.lenses?.length || resourceReferences(review.document).some((block) => block.type === "software_map"));
1733
+ }
1734
+ const documentSchema = z.array(blockSchema);
1735
+ /** Discriminated by type, like blockSchema, so a malformed insert reports
1736
+ * its own kind's issues. */
1737
+ const contentSchema = z.discriminatedUnion("type", [
1738
+ ...blockKindSchemas(),
1739
+ stepSchema,
1740
+ flowNodeInsertSchema,
1741
+ flowEdgeSchema
1742
+ ]);
1743
+ const placement = {
1744
+ parentId: label.optional(),
1745
+ afterId: label.optional()
1746
+ };
1747
+ function edits(content, replacement, value) {
1748
+ return z.discriminatedUnion("type", [
1749
+ z.strictObject({
1750
+ type: z.literal("insert"),
1751
+ content,
1752
+ ...placement
1753
+ }),
1754
+ z.strictObject({
1755
+ type: z.literal("replace"),
1756
+ targetId: label,
1757
+ content: replacement
1758
+ }),
1759
+ z.strictObject({
1760
+ type: z.literal("update"),
1761
+ targetId: label,
1762
+ changes: z.record(text, value)
1763
+ }),
1764
+ z.strictObject({
1765
+ type: z.literal("move"),
1766
+ targetId: label,
1767
+ ...placement
1768
+ }),
1769
+ z.strictObject({
1770
+ type: z.literal("remove"),
1771
+ targetId: label
1772
+ })
1773
+ ]);
1774
+ }
1775
+ const editSchema = edits(contentSchema, blockSchema, z.json());
1776
+ const writableBlockTypes = Object.keys(blocks).filter((type) => type !== "tutorial");
1777
+ const namedByType = (types) => z.looseObject({ type: z.enum(types) }).describe("Fields depend on type; see the tool description.");
1778
+ /** What agents are shown: content names only its type. The host parses
1779
+ * editSchema, reporting the named kind's issues. */
1780
+ const publishedEditSchema = edits(namedByType([
1781
+ ...writableBlockTypes,
1782
+ "step",
1783
+ "flow_node",
1784
+ "flow_edge"
1785
+ ]), namedByType(writableBlockTypes), z.unknown());
1786
+ /** The block an element belongs to: itself, or the diagram around a unit. */
1787
+ function enclosingBlock(document, id) {
1788
+ for (const block of elements(document)) {
1789
+ if (block.id === id) return {
1790
+ block,
1791
+ element: block
1792
+ };
1793
+ for (const list of childLists(block)) for (const element of list) if (element.id === id && isUnit(element)) return {
1794
+ block,
1795
+ element
1796
+ };
1797
+ }
1798
+ }
1799
+ /** Summarize an edit against the document it was applied to. A removed
1800
+ * element is found in the document before the edit; everything else after. */
1801
+ function summarizeEdit(edit, applied, before, after) {
1802
+ const found = enclosingBlock(edit.type === "remove" ? before : after, applied.targetId);
1803
+ if (!found?.block.id) return void 0;
1804
+ const summary = {
1805
+ type: edit.type,
1806
+ targetId: applied.targetId,
1807
+ blockId: found.block.id,
1808
+ kind: found.element.type
1809
+ };
1810
+ if (isUnit(found.element)) summary.unit = found.element.type;
1811
+ if (applied.linkId) summary.linkId = applied.linkId;
1812
+ if (edit.type === "update") summary.fields = Object.keys(edit.changes);
1813
+ if (edit.type === "insert" || edit.type === "replace") {
1814
+ const units = drawOrder(found.element);
1815
+ if (units.length) summary.units = units;
1816
+ }
1817
+ return summary;
1818
+ }
1819
+ /** The order a hand draws a diagram: a sequence step by step; a flow node by
1820
+ * node, each edge as soon as both of its ends are on the board. */
1821
+ function drawOrder(element) {
1822
+ if (element.type === "sequence") return element.steps.flatMap((step) => step.id ? [step.id] : []);
1823
+ if (element.type !== "flow_diagram") return [];
1824
+ const order = [];
1825
+ const drawn = /* @__PURE__ */ new Set();
1826
+ const waiting = [...element.edges];
1827
+ for (const node of element.nodes) {
1828
+ if (node.id) order.push(node.id);
1829
+ drawn.add(node.key);
1830
+ for (let i = 0; i < waiting.length;) {
1831
+ const edge = waiting[i];
1832
+ if (drawn.has(edge.from) && drawn.has(edge.to)) {
1833
+ if (edge.id) order.push(edge.id);
1834
+ waiting.splice(i, 1);
1835
+ } else i++;
1836
+ }
1837
+ }
1838
+ return order;
1839
+ }
1840
+ /** The arrays an element's children live in, so an edit can splice the
1841
+ * real list. A flow diagram keeps its nodes and edges apart. */
1842
+ function childLists(element) {
1843
+ if ("children" in element) return [element.children];
1844
+ if (element.type === "sequence") return [element.steps];
1845
+ if (element.type === "flow_diagram") return [element.nodes, element.edges];
1846
+ return [];
1847
+ }
1848
+ function children(element) {
1849
+ return childLists(element).flat();
1850
+ }
1851
+ function elements(document) {
1852
+ return document.flatMap((element) => [element, ...elements(children(element))]);
1853
+ }
1854
+ const unitParent = {
1855
+ step: "sequence",
1856
+ flow_node: "flow_diagram",
1857
+ flow_edge: "flow_diagram"
1858
+ };
1859
+ /** What to do instead of patching a field an update can't reach. */
1860
+ function patchRefusal(key, element) {
1861
+ if (key === "base" || key === "head") return `A call stack's frames change only by replacing it: send {type:"replace", targetId:"${element.id}", content} with the whole call_stack_diff.`;
1862
+ if (key === "nodes" || key === "edges" || key === "steps") return `Edit a diagram's ${key} one at a time by their own IDs (session_get lists them), or replace the diagram.`;
1863
+ if (key === "children") return `Edit a ${element.type}'s children by their own IDs, or insert into it with parentId "${element.id}".`;
1864
+ if (key === "id" || key === "type") return `A component's ${key} can't change; replace it instead.`;
1865
+ return `Cannot patch ${key}; replace the ${element.type} instead.`;
1866
+ }
1867
+ const structural = /* @__PURE__ */ new Set([
1868
+ "nodes",
1869
+ "edges",
1870
+ "id",
1871
+ "type",
1872
+ "children",
1873
+ "steps",
1874
+ "actors",
1875
+ "stores",
1876
+ "useCases",
1877
+ "base",
1878
+ "head"
1879
+ ]);
1880
+ const idPrefix = (element) => element.type === "sequence" || element.type === "flow_diagram" ? "diagram" : element.type === "step" ? "step" : element.type === "flow_node" ? "node" : element.type === "flow_edge" ? "edge" : "block";
1881
+ /** Flow units saved before they had ids or types get both on the next
1882
+ * write, so a stored diagram can be drawn on unit by unit. */
1883
+ function adoptFlowUnits(document, allocate) {
1884
+ for (const element of elements(document)) {
1885
+ if (element.type === "flow_diagram") {
1886
+ for (const node of element.nodes) node.type ??= "flow_node";
1887
+ for (const edge of element.edges) edge.type ??= "flow_edge";
1888
+ }
1889
+ if (isUnit(element) && element.id === void 0) element.id = allocate(idPrefix(element));
1890
+ }
1891
+ }
1892
+ /** Mutate a private candidate. Only the store owns allocation and commits. */
1893
+ /** Assign server ids to an element tree that arrives without any. */
1894
+ function assignFreshIds(element, allocate) {
1895
+ if (element.id !== void 0) throw new ReviewInputError("IDs are assigned by the server.");
1896
+ element.id = allocate(idPrefix(element));
1897
+ for (const child of children(element)) assignFreshIds(child, allocate);
1898
+ const assign = (item, prefix) => {
1899
+ if (item.id !== void 0) throw new ReviewInputError("IDs are assigned by the server.");
1900
+ item.id = allocate(prefix);
1901
+ };
1902
+ if (element.type === "call_stack_diff") for (const frame of [...element.base, ...element.head]) assign(frame, "frame");
1903
+ if (element.type === "database_lens") for (const useCase of element.useCases) {
1904
+ assign(useCase, "case");
1905
+ for (const op of useCase.operations) assign(op, "operation");
1906
+ }
1907
+ }
1908
+ function applyEdit(document, edit, allocate, options = {}) {
1909
+ adoptFlowUnits(document, allocate);
1910
+ const locate = (id) => {
1911
+ const find = (siblings) => {
1912
+ for (const element of siblings) {
1913
+ if (element.id === id) return {
1914
+ element,
1915
+ siblings
1916
+ };
1917
+ for (const list of childLists(element)) {
1918
+ const found = find(list);
1919
+ if (found) return found;
1920
+ }
1921
+ }
1922
+ };
1923
+ const found = find(document);
1924
+ if (!found) throw new ReviewInputError(`Target ${id} does not exist.`);
1925
+ return found;
1926
+ };
1927
+ const fresh = (element) => assignFreshIds(element, allocate);
1928
+ const place = (element, parentId, afterId, from) => {
1929
+ const parent = parentId ? locate(parentId).element : void 0;
1930
+ if (parent && elements([element]).includes(parent)) throw new ReviewInputError("Cannot move a block inside itself.");
1931
+ if (!isUnit(element) && parent && !("children" in parent)) throw new ReviewInputError(`${parentId} is a ${parent.type}, which holds no components: insert into a section or callout, or at the top level.`);
1932
+ if (isUnit(element) && parent?.type !== unitParent[element.type]) throw new ReviewInputError(`A ${element.type} belongs inside a ${unitParent[element.type]}.`);
1933
+ const siblings = parent ? childLists(parent)[element.type === "flow_edge" ? 1 : 0] ?? document : document;
1934
+ if (afterId === element.id) throw new ReviewInputError("An element cannot follow itself.");
1935
+ if (afterId !== void 0 && !siblings.some((s) => s.id === afterId)) throw new ReviewInputError("afterId must identify a sibling.");
1936
+ if (from) from.splice(from.indexOf(element), 1);
1937
+ const index = afterId !== void 0 ? siblings.findIndex((s) => s.id === afterId) + 1 : !parent && !from && options.placement === "first" ? 0 : siblings.length;
1938
+ siblings.splice(index, 0, element);
1939
+ };
1940
+ if (edit.type === "insert") {
1941
+ if (edit.content.type === "flow_node" && edit.content.link) {
1942
+ const { link, ...node } = edit.content;
1943
+ if (link.from === void 0 === (link.to === void 0)) throw new ReviewInputError("A link names exactly one of from or to: the node already on the board.");
1944
+ fresh(node);
1945
+ place(node, edit.parentId, edit.afterId);
1946
+ const edge = {
1947
+ type: "flow_edge",
1948
+ from: link.from ?? node.key,
1949
+ to: link.to ?? node.key
1950
+ };
1951
+ if (link.label !== void 0) edge.label = link.label;
1952
+ if (link.style !== void 0) edge.style = link.style;
1953
+ fresh(edge);
1954
+ place(edge, edit.parentId);
1955
+ return {
1956
+ targetId: node.id,
1957
+ type: node.type,
1958
+ linkId: edge.id
1959
+ };
1960
+ }
1961
+ fresh(edit.content);
1962
+ place(edit.content, edit.parentId, edit.afterId);
1963
+ return written(edit.content);
1964
+ }
1965
+ const { element, siblings } = locate(edit.targetId);
1966
+ const index = siblings.indexOf(element);
1967
+ switch (edit.type) {
1968
+ case "update": {
1969
+ if (!Object.keys(edit.changes).length) throw new ReviewInputError("Supply at least one field to update.");
1970
+ for (const key of Object.keys(edit.changes)) if (structural.has(key) || [
1971
+ "__proto__",
1972
+ "constructor",
1973
+ "prototype"
1974
+ ].includes(key)) throw new ReviewInputError(patchRefusal(key, element));
1975
+ if ("link" in edit.changes) throw new ReviewInputError("A link only comes with a new node; insert a flow_edge instead.");
1976
+ const merged = Object.fromEntries(Object.entries({
1977
+ ...element,
1978
+ ...edit.changes
1979
+ }).filter(([, value]) => value !== null));
1980
+ siblings[index] = contentSchema.parse(merged);
1981
+ break;
1982
+ }
1983
+ case "remove":
1984
+ siblings.splice(index, 1);
1985
+ if (element.type === "flow_node") {
1986
+ const diagram = elements(document).find((candidate) => candidate.type === "flow_diagram" && candidate.nodes === siblings);
1987
+ if (diagram?.type === "flow_diagram") diagram.edges = diagram.edges.filter((edge) => edge.from !== element.key && edge.to !== element.key);
1988
+ }
1989
+ break;
1990
+ case "replace":
1991
+ if (isUnit(element)) throw new ReviewInputError(`Patch the ${element.type} or replace its diagram.`);
1992
+ fresh(edit.content);
1993
+ edit.content.id = element.id;
1994
+ siblings[index] = edit.content;
1995
+ return written(edit.content);
1996
+ case "move":
1997
+ if (isUnit(element) && !childLists(locate(edit.parentId ?? "").element).includes(siblings)) throw new ReviewInputError(`Move a ${element.type} within its own diagram.`);
1998
+ place(element, edit.parentId, edit.afterId, siblings);
1999
+ }
2000
+ return {
2001
+ targetId: edit.targetId,
2002
+ type: element.type
2003
+ };
2004
+ }
2005
+ /** An inserted or replaced component and its first-level children: blocks
2006
+ * in a container, or a diagram's steps, or its nodes then its edges. */
2007
+ function written(element) {
2008
+ const applied = {
2009
+ targetId: element.id,
2010
+ type: element.type
2011
+ };
2012
+ const list = children(element).map((child) => ({
2013
+ id: child.id,
2014
+ type: child.type
2015
+ }));
2016
+ if (list.length) applied.children = list;
2017
+ return applied;
2018
+ }
2019
+ //#endregion
2020
+ //#region src/evidence.ts
2021
+ /** Quote matching shared by legacy publish and JSON accept: authors paste
2022
+ * transcript text with arbitrary line wrapping, so both sides compare on
2023
+ * whitespace-normalized text. */
2024
+ function normalizeQuoteText(text) {
2025
+ return text.trim().replace(/\s+/g, " ");
2026
+ }
2027
+ function textIncludesQuote(text, quote) {
2028
+ const normalizedQuote = normalizeQuoteText(quote);
2029
+ return normalizedQuote !== "" && normalizeQuoteText(text).includes(normalizedQuote);
2030
+ }
2031
+ //#endregion
2032
+ //#region src/review-api/trace-schema.ts
2033
+ const traceSchema = z.strictObject({
2034
+ label: z.string(),
2035
+ events: z.array(z.strictObject({
2036
+ id: z.string().min(1),
2037
+ role: z.enum([
2038
+ "user",
2039
+ "assistant",
2040
+ "tool"
2041
+ ]),
2042
+ text: z.string()
2043
+ })),
2044
+ provenance: z.enum(["client_supplied", "legacy_import"]).optional()
2045
+ });
2046
+ //#endregion
2047
+ //#region src/software-map-model.ts
2048
+ const softwareElementTypeSchema = z.enum([
2049
+ "person",
2050
+ "softwareSystem",
2051
+ "container",
2052
+ "dataStore",
2053
+ "component",
2054
+ "codeElement"
2055
+ ]);
2056
+ const softwareChangeStatusSchema = z.enum([
2057
+ "added",
2058
+ "removed",
2059
+ "modified",
2060
+ "unchanged"
2061
+ ]);
2062
+ const softwareDataStoreKindSchema = z.enum([
2063
+ "database",
2064
+ "objectStore",
2065
+ "bucket",
2066
+ "artifactStore",
2067
+ "fileStore"
2068
+ ]);
2069
+ const softwareLineRangeSchema = z.strictObject({
2070
+ fromLine: z.int().positive(),
2071
+ toLine: z.int().positive()
2072
+ });
2073
+ const softwareSourceRangeSchema = softwareLineRangeSchema.extend({ file: z.string() });
2074
+ const softwareDataStoreForeignKeyRefDataSchema = z.union([z.string(), z.strictObject({
2075
+ store: z.string().optional(),
2076
+ table: z.string(),
2077
+ field: z.string(),
2078
+ label: z.string().optional(),
2079
+ cardinality: z.enum(["one-to-one", "many-to-one"]).optional(),
2080
+ onDelete: z.string().optional(),
2081
+ onUpdate: z.string().optional()
2082
+ })]);
2083
+ const softwareDataStoreFieldDataSchema = z.lazy(() => z.record(z.string(), z.union([z.strictObject({
2084
+ type: z.string(),
2085
+ example: jsonValueSchema.optional(),
2086
+ pk: z.boolean().optional(),
2087
+ fk: softwareDataStoreForeignKeyRefDataSchema.optional(),
2088
+ schema: softwareDataStoreFieldDataSchema.optional()
2089
+ }), softwareDataStoreFieldDataSchema])));
2090
+ const normalizedSoftwareDataStoreCollectionSchema = z.strictObject({
2091
+ id: z.string(),
2092
+ label: z.string(),
2093
+ key: z.string().optional(),
2094
+ schema: softwareDataStoreFieldDataSchema
2095
+ });
2096
+ const normalizedSoftwareDataStoreSchemaSchema = z.strictObject({
2097
+ tables: z.record(z.string(), normalizedSoftwareDataStoreCollectionSchema),
2098
+ documents: z.record(z.string(), normalizedSoftwareDataStoreCollectionSchema)
2099
+ });
2100
+ const normalizedSoftwareCoverageFileSchema = z.strictObject({
2101
+ path: z.string(),
2102
+ ranges: z.array(softwareLineRangeSchema)
2103
+ });
2104
+ const normalizedSoftwareCoverageSchema = z.strictObject({
2105
+ files: z.array(normalizedSoftwareCoverageFileSchema),
2106
+ globs: z.array(z.string())
2107
+ });
2108
+ const normalizedSoftwareElementSchema = z.strictObject({
2109
+ type: softwareElementTypeSchema,
2110
+ id: z.string(),
2111
+ path: z.string(),
2112
+ parentPath: z.string().optional(),
2113
+ label: z.string(),
2114
+ description: z.string().optional(),
2115
+ changeStatus: softwareChangeStatusSchema.optional(),
2116
+ coverage: normalizedSoftwareCoverageSchema.optional(),
2117
+ external: z.boolean().optional(),
2118
+ dataStoreKind: softwareDataStoreKindSchema.optional(),
2119
+ dataStoreSchema: normalizedSoftwareDataStoreSchemaSchema.optional(),
2120
+ sourceRanges: z.array(softwareSourceRangeSchema).optional(),
2121
+ children: z.array(z.string())
2122
+ });
2123
+ const normalizedRelationshipFields = {
2124
+ id: z.string(),
2125
+ from: z.string(),
2126
+ to: z.string(),
2127
+ scopePath: z.string().optional(),
2128
+ label: z.string().optional(),
2129
+ description: z.string().optional()
2130
+ };
2131
+ const normalizedSoftwareRelationshipSchema = z.discriminatedUnion("kind", [z.strictObject({
2132
+ ...normalizedRelationshipFields,
2133
+ kind: z.literal("call"),
2134
+ nthCallSite: z.int().nonnegative()
2135
+ }), z.strictObject({
2136
+ ...normalizedRelationshipFields,
2137
+ kind: z.literal("semantic"),
2138
+ semanticKind: z.string().optional(),
2139
+ sourceRanges: z.array(softwareLineRangeSchema).optional()
2140
+ })]);
2141
+ var SoftwareModelValidationError = class extends Error {
2142
+ errors;
2143
+ constructor(errors) {
2144
+ super(`Invalid software model:\n- ${errors.join("\n- ")}`);
2145
+ this.name = "SoftwareModelValidationError";
2146
+ this.errors = errors;
2147
+ }
2148
+ };
2149
+ function defineSoftwareMap(input) {
2150
+ const errors = [];
2151
+ const elements = [];
2152
+ const elementsByPath = /* @__PURE__ */ new Map();
2153
+ const pendingRelationships = [];
2154
+ if ("views" in input) errors.push("Software model must not author views; SoftwareMap derives inline C4 projection from elements, relationships, and expansion state.");
2155
+ flattenElements({
2156
+ collection: input.people,
2157
+ type: "person",
2158
+ parentPath: void 0,
2159
+ elements,
2160
+ elementsByPath,
2161
+ pendingRelationships,
2162
+ errors
2163
+ });
2164
+ flattenElements({
2165
+ collection: input.systems,
2166
+ type: "softwareSystem",
2167
+ parentPath: void 0,
2168
+ elements,
2169
+ elementsByPath,
2170
+ pendingRelationships,
2171
+ errors
2172
+ });
2173
+ for (const [index, relationship] of (input.relationships ?? []).entries()) pendingRelationships.push({
2174
+ input: relationship,
2175
+ index
2176
+ });
2177
+ const relationships = normalizeRelationships(pendingRelationships, elementsByPath, errors);
2178
+ if (errors.length > 0) throw new SoftwareModelValidationError(errors);
2179
+ const normalizedElements = elements.map((element) => Object.freeze(element));
2180
+ return Object.freeze({
2181
+ elements: normalizedElements,
2182
+ elementsByPath: new Map(normalizedElements.map((element) => [element.path, element])),
2183
+ relationships
2184
+ });
2185
+ }
2186
+ function flattenElements({ collection, type, parentPath, elements, elementsByPath, pendingRelationships, errors }) {
2187
+ if (!collection) return;
2188
+ const siblings = entriesForCollection(collection);
2189
+ const siblingIds = /* @__PURE__ */ new Set();
2190
+ for (const [collectionKey, input] of siblings) {
2191
+ const id = input.id ?? collectionKey;
2192
+ const path = parentPath ? `${parentPath}.${id}` : id;
2193
+ if (!id) {
2194
+ errors.push(`Software ${type} under ${parentPath ?? "model"} has no id.`);
2195
+ continue;
2196
+ }
2197
+ if (siblingIds.has(id)) {
2198
+ errors.push(`Duplicate ${type} id "${id}" under ${parentPath ?? "model"}.`);
2199
+ continue;
2200
+ }
2201
+ siblingIds.add(id);
2202
+ if (elementsByPath.has(path)) {
2203
+ errors.push(`Duplicate element path "${path}".`);
2204
+ continue;
2205
+ }
2206
+ validateElementFields(type, path, input, errors);
2207
+ const draft = {
2208
+ type,
2209
+ id,
2210
+ path,
2211
+ parentPath,
2212
+ label: input.label ?? id,
2213
+ description: input.description,
2214
+ changeStatus: input.changeStatus,
2215
+ coverage: normalizeCoverage(type, path, input.coverage, errors),
2216
+ children: []
2217
+ };
2218
+ if (parentPath) elementsByPath.get(parentPath)?.children.push(path);
2219
+ if (type === "softwareSystem") draft.external = input.external;
2220
+ if (type === "dataStore") {
2221
+ const dataStore = input;
2222
+ draft.dataStoreKind = dataStore.kind ?? "database";
2223
+ draft.dataStoreSchema = normalizeDataStoreSchema(path, dataStore, errors);
2224
+ }
2225
+ if (type === "codeElement") {
2226
+ const codeElement = input;
2227
+ draft.sourceRanges = codeElement.sourceRanges;
2228
+ validateSourceRanges(codeElement.sourceRanges, `Code element "${path}" sourceRanges`, errors);
2229
+ }
2230
+ elements.push(draft);
2231
+ elementsByPath.set(path, draft);
2232
+ for (const [index, relationship] of (input.relationships ?? []).entries()) pendingRelationships.push({
2233
+ input: relationship,
2234
+ scopePath: path,
2235
+ index
2236
+ });
2237
+ if (type === "softwareSystem") {
2238
+ const system = input;
2239
+ flattenElements({
2240
+ collection: system.containers,
2241
+ type: "container",
2242
+ parentPath: path,
2243
+ elements,
2244
+ elementsByPath,
2245
+ pendingRelationships,
2246
+ errors
2247
+ });
2248
+ flattenElements({
2249
+ collection: system.dataStores,
2250
+ type: "dataStore",
2251
+ parentPath: path,
2252
+ elements,
2253
+ elementsByPath,
2254
+ pendingRelationships,
2255
+ errors
2256
+ });
2257
+ }
2258
+ if (type === "container" || type === "dataStore") flattenElements({
2259
+ collection: input.components,
2260
+ type: "component",
2261
+ parentPath: path,
2262
+ elements,
2263
+ elementsByPath,
2264
+ pendingRelationships,
2265
+ errors
2266
+ });
2267
+ if (type === "component") flattenElements({
2268
+ collection: input.codeElements,
2269
+ type: "codeElement",
2270
+ parentPath: path,
2271
+ elements,
2272
+ elementsByPath,
2273
+ pendingRelationships,
2274
+ errors
2275
+ });
2276
+ }
2277
+ }
2278
+ function entriesForCollection(collection) {
2279
+ if (Array.isArray(collection)) return collection.map((item, index) => [item.id ?? String(index), item]);
2280
+ return Object.entries(collection);
2281
+ }
2282
+ function validateElementFields(type, path, input, errors) {
2283
+ if ("additions" in input || "deletions" in input) errors.push(`Element "${path}" must not author additions or deletions; diff counts are computed automatically.`);
2284
+ if ("changeStatus" in input && input.changeStatus !== void 0 && input.changeStatus !== "added" && input.changeStatus !== "removed" && input.changeStatus !== "modified" && input.changeStatus !== "unchanged") errors.push(`Element "${path}" changeStatus must be one of "added", "removed", "modified", or "unchanged".`);
2285
+ if (type === "dataStore" && "kind" in input && input.kind !== void 0 && input.kind !== "database" && input.kind !== "objectStore" && input.kind !== "bucket" && input.kind !== "artifactStore" && input.kind !== "fileStore") errors.push(`Data store "${path}" kind must be one of "database", "objectStore", "bucket", "artifactStore", or "fileStore".`);
2286
+ if (type !== "dataStore" && "kind" in input) errors.push(`Only data stores may define kind: "${path}".`);
2287
+ if (type !== "dataStore" && ("tables" in input || "documents" in input)) errors.push(`Only data stores may define tables or documents: "${path}".`);
2288
+ if (type === "codeElement") {
2289
+ if ("codeElements" in input) errors.push(`Code element "${path}" cannot contain code elements.`);
2290
+ }
2291
+ if ("coverage" in input && input.coverage !== void 0 && type !== "softwareSystem" && type !== "container" && type !== "dataStore" && type !== "component") errors.push(`Element "${path}" coverage may only be authored on systems, containers, data stores, or components.`);
2292
+ }
2293
+ function normalizeDataStoreSchema(path, dataStore, errors) {
2294
+ validateDataStoreCollectionRecord(path, "tables", dataStore.tables, errors);
2295
+ validateDataStoreCollectionRecord(path, "documents", dataStore.documents, errors);
2296
+ const tables = normalizeDataStoreCollections(dataStore.tables);
2297
+ const documents = normalizeDataStoreCollections(dataStore.documents);
2298
+ if (Object.keys(tables).length === 0 && Object.keys(documents).length === 0) return;
2299
+ return Object.freeze({
2300
+ tables: Object.freeze(tables),
2301
+ documents: Object.freeze(documents)
2302
+ });
2303
+ }
2304
+ function validateDataStoreCollectionRecord(path, property, collections, errors) {
2305
+ if (collections === void 0) return;
2306
+ if (!isAuthoredObject(collections)) {
2307
+ errors.push(`Data store "${path}" ${property} must be an object.`);
2308
+ return;
2309
+ }
2310
+ for (const [collectionId, collection] of Object.entries(collections)) {
2311
+ if (!collectionId.trim()) {
2312
+ errors.push(`Data store "${path}" ${property} contains an empty id.`);
2313
+ continue;
2314
+ }
2315
+ if (!isAuthoredObject(collection)) {
2316
+ errors.push(`Data store "${path}" ${property}.${collectionId} must be an object.`);
2317
+ continue;
2318
+ }
2319
+ validateDataStoreFieldSchema(collection.schema, `Data store "${path}" ${property}.${collectionId}.schema`, errors);
2320
+ }
2321
+ }
2322
+ function normalizeDataStoreCollections(collections) {
2323
+ return Object.fromEntries(Object.entries(collections ?? {}).map(([id, collection]) => [id, Object.freeze({
2324
+ id,
2325
+ label: collection.label ?? id,
2326
+ key: collection.key,
2327
+ schema: collection.schema
2328
+ })]));
2329
+ }
2330
+ function validateDataStoreFieldSchema(schema, path, errors) {
2331
+ if (!isAuthoredObject(schema)) {
2332
+ errors.push(`${path} must be an object.`);
2333
+ return;
2334
+ }
2335
+ for (const [field, value] of Object.entries(schema)) {
2336
+ if (!field.trim()) {
2337
+ errors.push(`${path} contains an empty field name.`);
2338
+ continue;
2339
+ }
2340
+ const fieldPath = `${path}.${field}`;
2341
+ if (!isAuthoredObject(value)) {
2342
+ errors.push(`${fieldPath} must be a field object or nested schema.`);
2343
+ continue;
2344
+ }
2345
+ if (isDataStoreFieldLeaf(value)) {
2346
+ if (!value.type.trim()) errors.push(`${fieldPath}.type must not be empty.`);
2347
+ if (value.schema) validateDataStoreFieldSchema(value.schema, `${fieldPath}.schema`, errors);
2348
+ continue;
2349
+ }
2350
+ validateDataStoreFieldSchema(value, fieldPath, errors);
2351
+ }
2352
+ }
2353
+ const DataStoreFieldLeafSchema = z.object({ type: z.string() });
2354
+ function isDataStoreFieldLeaf(value) {
2355
+ return DataStoreFieldLeafSchema.safeParse(value).success;
2356
+ }
2357
+ function normalizeCoverage(type, path, coverage, errors) {
2358
+ if (!coverage) return void 0;
2359
+ if (type !== "softwareSystem" && type !== "container" && type !== "dataStore" && type !== "component") return;
2360
+ const files = [];
2361
+ const globs = [];
2362
+ if (coverage.files !== void 0 && !Array.isArray(coverage.files)) errors.push(`Element "${path}" coverage.files must be an array.`);
2363
+ for (const [index, file] of (coverage.files ?? []).entries()) {
2364
+ if (isAuthoredString(file)) {
2365
+ if (!file.trim()) {
2366
+ errors.push(`Element "${path}" coverage.files[${index}] is empty.`);
2367
+ continue;
2368
+ }
2369
+ files.push({
2370
+ path: file,
2371
+ ranges: []
2372
+ });
2373
+ continue;
2374
+ }
2375
+ if (!isAuthoredObject(file) || !file.path) {
2376
+ errors.push(`Element "${path}" coverage.files[${index}] must be a file path or object with path.`);
2377
+ continue;
2378
+ }
2379
+ files.push({
2380
+ path: file.path,
2381
+ ranges: file.ranges ?? []
2382
+ });
2383
+ validateLineRanges(file.ranges, `Element "${path}" coverage.files[${index}].ranges`, errors);
2384
+ }
2385
+ if (coverage.globs !== void 0 && !Array.isArray(coverage.globs)) errors.push(`Element "${path}" coverage.globs must be an array.`);
2386
+ for (const [index, glob] of (coverage.globs ?? []).entries()) {
2387
+ if (!isAuthoredString(glob) || !glob.trim()) {
2388
+ errors.push(`Element "${path}" coverage.globs[${index}] is empty.`);
2389
+ continue;
2390
+ }
2391
+ globs.push(glob);
2392
+ }
2393
+ if (files.length === 0 && globs.length === 0) return void 0;
2394
+ return Object.freeze({
2395
+ files: files.map((file) => Object.freeze(file)),
2396
+ globs
2397
+ });
2398
+ }
2399
+ function normalizeRelationships(pendingRelationships, elementsByPath, errors) {
2400
+ const relationships = [];
2401
+ for (const pending of pendingRelationships) {
2402
+ const from = resolveEndpoint(pending.input.from, pending.scopePath, elementsByPath);
2403
+ const to = resolveEndpoint(pending.input.to, pending.scopePath, elementsByPath);
2404
+ const relationshipLabel = `relationships[${pending.index}] at ${pending.scopePath ?? "model"}`;
2405
+ if (!from) errors.push(`${relationshipLabel}.from: "${pending.input.from}" does not match an element path or data store schema path. Use an existing full path or a name relative to this scope or its parent.`);
2406
+ if (!to) errors.push(`${relationshipLabel}.to: "${pending.input.to}" does not match an element path or data store schema path. Use an existing full path or a name relative to this scope or its parent.`);
2407
+ validateRelationshipFields(pending, errors);
2408
+ if (!from || !to) continue;
2409
+ const base = {
2410
+ id: relationshipIdForPending(pending),
2411
+ from,
2412
+ to,
2413
+ scopePath: pending.scopePath,
2414
+ label: pending.input.label,
2415
+ description: pending.input.description
2416
+ };
2417
+ if (pending.input.kind === "call") relationships.push(Object.freeze({
2418
+ ...base,
2419
+ kind: "call",
2420
+ nthCallSite: pending.input.nthCallSite ?? 0
2421
+ }));
2422
+ else relationships.push(Object.freeze({
2423
+ ...base,
2424
+ kind: "semantic",
2425
+ semanticKind: pending.input.semanticKind,
2426
+ sourceRanges: pending.input.sourceRanges
2427
+ }));
2428
+ }
2429
+ return relationships;
2430
+ }
2431
+ function validateRelationshipFields(pending, errors) {
2432
+ if (pending.input.kind === "call") {
2433
+ const nthCallSite = pending.input.nthCallSite ?? 0;
2434
+ if (!Number.isInteger(nthCallSite) || nthCallSite < 0) errors.push(`Call relationship "${relationshipIdForPending(pending)}" must use a non-negative integer nthCallSite.`);
2435
+ return;
2436
+ }
2437
+ validateLineRanges(pending.input.sourceRanges, `Semantic relationship "${relationshipIdForPending(pending)}" sourceRanges`, errors);
2438
+ }
2439
+ function resolveEndpoint(endpoint, scopePath, elementsByPath) {
2440
+ const candidates = [];
2441
+ if (scopePath && endpoint === ".") candidates.push(scopePath);
2442
+ if (scopePath && endpoint !== ".") {
2443
+ candidates.push(`${scopePath}.${endpoint}`);
2444
+ const parentPath = parentPathFor(scopePath);
2445
+ if (parentPath) candidates.push(`${parentPath}.${endpoint}`);
2446
+ }
2447
+ candidates.push(endpoint);
2448
+ for (const candidate of candidates) {
2449
+ if (elementsByPath.has(candidate)) return candidate;
2450
+ const schemaEndpoint = resolveDataStoreSchemaEndpoint(candidate, elementsByPath);
2451
+ if (schemaEndpoint) return schemaEndpoint;
2452
+ }
2453
+ }
2454
+ function parseDataStoreSchemaEndpoint(endpoint, elementsByPath) {
2455
+ for (const [dataStorePath, element] of elementsByPath) {
2456
+ if (element.type !== "dataStore" || !element.dataStoreSchema) continue;
2457
+ const parsed = parseDataStoreSchemaEndpointForStore(endpoint, dataStorePath, element.dataStoreSchema);
2458
+ if (parsed) return parsed;
2459
+ }
2460
+ }
2461
+ function resolveDataStoreSchemaEndpoint(endpoint, elementsByPath) {
2462
+ return parseDataStoreSchemaEndpoint(endpoint, elementsByPath) ? endpoint : void 0;
2463
+ }
2464
+ function parseDataStoreSchemaEndpointForStore(endpoint, dataStorePath, schema) {
2465
+ for (const collectionKind of ["tables", "documents"]) {
2466
+ const prefix = `${dataStorePath}.${collectionKind}.`;
2467
+ if (!endpoint.startsWith(prefix)) continue;
2468
+ const parts = endpoint.slice(prefix.length).split(".").filter(Boolean);
2469
+ if (parts.length === 0) continue;
2470
+ const [collectionId, ...fieldPath] = parts;
2471
+ const collection = schema[collectionKind][collectionId];
2472
+ if (!collection) continue;
2473
+ if (fieldPath.length > 0 && !dataStoreFieldPathExists(collection.schema, fieldPath)) continue;
2474
+ return {
2475
+ dataStorePath,
2476
+ collectionKind,
2477
+ collectionId,
2478
+ fieldPath
2479
+ };
2480
+ }
2481
+ }
2482
+ function dataStoreFieldPathExists(schema, fieldPath) {
2483
+ let current = schema;
2484
+ for (const [index, part] of fieldPath.entries()) {
2485
+ const next = current[part];
2486
+ if (!next) return false;
2487
+ if (index === fieldPath.length - 1) return true;
2488
+ if (isDataStoreFieldLeaf(next)) {
2489
+ if (!next.schema) return false;
2490
+ current = next.schema;
2491
+ } else current = next;
2492
+ }
2493
+ return fieldPath.length === 0;
2494
+ }
2495
+ function validateSourceRanges(sourceRanges, label, errors) {
2496
+ for (const [index, range] of (sourceRanges ?? []).entries()) {
2497
+ if (!isAuthoredString(range.file) || range.file.trim().length === 0) errors.push(`${label}[${index}].file must be a non-empty string.`);
2498
+ if (!Number.isInteger(range.fromLine) || !Number.isInteger(range.toLine) || range.fromLine < 1 || range.toLine < range.fromLine) errors.push(`${label}[${index}] must use positive inclusive line numbers with fromLine <= toLine.`);
2499
+ }
2500
+ }
2501
+ function validateLineRanges(ranges, label, errors) {
2502
+ for (const [index, range] of (ranges ?? []).entries()) if (!Number.isInteger(range.fromLine) || !Number.isInteger(range.toLine) || range.fromLine < 1 || range.toLine < range.fromLine) errors.push(`${label}[${index}] must use positive inclusive line numbers with fromLine <= toLine.`);
2503
+ }
2504
+ function relationshipIdForPending(pending) {
2505
+ return `${pending.scopePath ?? "model"}.relationship.${pending.index}`;
2506
+ }
2507
+ function parentPathFor(path) {
2508
+ const lastDot = path.lastIndexOf(".");
2509
+ if (lastDot === -1) return void 0;
2510
+ return path.slice(0, lastDot);
2511
+ }
2512
+ const AuthoredObjectSchema = z.object({});
2513
+ const AuthoredStringSchema = z.string();
2514
+ function isAuthoredObject(value) {
2515
+ return AuthoredObjectSchema.safeParse(value).success;
2516
+ }
2517
+ function isAuthoredString(value) {
2518
+ return AuthoredStringSchema.safeParse(value).success;
2519
+ }
2520
+ z.strictObject({
2521
+ elements: z.array(normalizedSoftwareElementSchema),
2522
+ relationships: z.array(normalizedSoftwareRelationshipSchema)
2523
+ }).extend({ format: z.literal("software-map/1") });
2524
+ //#endregion
2525
+ //#region src/diff-selection-migration.ts
2526
+ /** Upgrade retained document attachments at the read/import boundary. New edits
2527
+ * use the strict DiffSelection schema and never accept these retired forms. */
2528
+ function migrateDiffSelections(input) {
2529
+ return migrateNode(jsonValueSchema.parse(input));
2530
+ }
2531
+ function migrateNode(value, attachment = false) {
2532
+ if (Array.isArray(value)) return value.map((child) => migrateNode(child, attachment));
2533
+ if (!isJsonObject(value)) return value;
2534
+ if (attachment && isStringValue(value.file)) {
2535
+ if (isNumberValue(value.fromLine) && isNumberValue(value.toLine)) {
2536
+ const side = value.side ?? value.graph ?? "head";
2537
+ return {
2538
+ file: value.file,
2539
+ start: {
2540
+ side,
2541
+ line: value.fromLine
2542
+ },
2543
+ end: {
2544
+ side,
2545
+ line: value.toLine
2546
+ }
2547
+ };
2548
+ }
2549
+ if (isJsonObject(value.start) && isJsonObject(value.end) && "baseLine" in value.start) {
2550
+ const endpoint = (row) => isNumberValue(row.headLine) ? {
2551
+ side: "head",
2552
+ line: row.headLine
2553
+ } : {
2554
+ side: "base",
2555
+ line: row.baseLine
2556
+ };
2557
+ return {
2558
+ file: value.file,
2559
+ start: endpoint(value.start),
2560
+ end: endpoint(value.end)
2561
+ };
2562
+ }
2563
+ }
2564
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, migrateNode(child, [
2565
+ "source",
2566
+ "peek",
2567
+ "sources",
2568
+ "contextSources",
2569
+ "callSite"
2570
+ ].includes(key))]));
2571
+ }
2572
+ //#endregion
2573
+ //#region src/stored-document-migration.ts
2574
+ /** Upgrade a saved or shared Review document to the current block schema at the
2575
+ * read/import boundary. New edits use the strict schema and never accept these
2576
+ * retired forms. */
2577
+ function migrateStoredDocument(input) {
2578
+ return anchorStrings(dropSectionStatus(migrateDiffSelections(input)));
2579
+ }
2580
+ /** Lenses saved before ranges were anchor strings. */
2581
+ function migrateStoredLenses(input) {
2582
+ return anchorStrings(migrateDiffSelections(input));
2583
+ }
2584
+ const ANCHOR_KEYS = [
2585
+ "source",
2586
+ "sources",
2587
+ "callSite",
2588
+ "contextSources"
2589
+ ];
2590
+ const selectionObjectSchema = z.object({
2591
+ file: z.string(),
2592
+ start: z.object({
2593
+ side: z.enum(["base", "head"]),
2594
+ line: z.number()
2595
+ }),
2596
+ end: z.object({
2597
+ side: z.enum(["base", "head"]),
2598
+ line: z.number()
2599
+ }),
2600
+ pins: z.json().optional()
2601
+ });
2602
+ /**
2603
+ * Anchors were once selection objects, each with its own pins. They are now
2604
+ * strings, and pins sit on whatever holds them (a code peek, step, frame,
2605
+ * attachment or operation) when they differ from the enclosing block's. No
2606
+ * stored element mixed pins across its anchors. Lens ranges read at the
2607
+ * review's pins and never carried others.
2608
+ */
2609
+ function anchorStrings(value, blockPins) {
2610
+ if (Array.isArray(value)) return value.map((child) => anchorStrings(child, blockPins));
2611
+ if (!isJsonObject(value)) return value;
2612
+ const inherited = value.pins ?? blockPins;
2613
+ let pins;
2614
+ const convert = (anchor) => {
2615
+ const selection = selectionObjectSchema.safeParse(anchor);
2616
+ if (!selection.success) return anchor;
2617
+ const { pins: own, ...anchorAt } = selection.data;
2618
+ if (own) pins = own;
2619
+ return formatAnchor(anchorAt);
2620
+ };
2621
+ const migrated = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, ANCHOR_KEYS.includes(key) ? Array.isArray(child) ? child.map(convert) : convert(child) : anchorStrings(child, inherited)]));
2622
+ if (pins !== void 0 && value.kind !== "ranges" && JSON.stringify(pins) !== JSON.stringify(blockPins)) migrated.pins = pins;
2623
+ return migrated;
2624
+ }
2625
+ /** Sections once carried an optional `status` (pending, in_progress or
2626
+ * complete). The authoring lease is now the only signal of work in progress,
2627
+ * so versions saved before it was retired drop the field when read. */
2628
+ function dropSectionStatus(value) {
2629
+ if (Array.isArray(value)) return value.map(dropSectionStatus);
2630
+ if (!isJsonObject(value)) return value;
2631
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !(value.type === "section" && key === "status")).map(([key, child]) => [key, dropSectionStatus(child)]));
2632
+ }
2633
+ /** A saved `file_lens` block, from before lenses left the document. */
2634
+ const legacyFileLensSchema = z.object({
2635
+ type: z.literal("file_lens"),
2636
+ id: label$1,
2637
+ title: label$1,
2638
+ targets: lensTargetsSchema.optional(),
2639
+ patterns: z.array(label$1).min(1).optional()
2640
+ });
2641
+ /** Versions saved before lenses moved out of the document hold them as
2642
+ * `file_lens` blocks, possibly inside sections. Lift them out of a migrated
2643
+ * document, in document order, keeping each id, title and scope (legacy
2644
+ * `patterns` become a files target). Viewed state is per file, so it needs
2645
+ * no rewrite. */
2646
+ function liftFileLenses(document) {
2647
+ const lenses = [];
2648
+ const visit = (value) => {
2649
+ if (Array.isArray(value)) return value.flatMap((item) => {
2650
+ if (!isJsonObject(item) || item.type !== "file_lens") return [visit(item)];
2651
+ const legacy = legacyFileLensSchema.safeParse(item);
2652
+ if (legacy.success && (legacy.data.targets ?? legacy.data.patterns)) lenses.push({
2653
+ id: legacy.data.id,
2654
+ title: legacy.data.title,
2655
+ targets: legacy.data.targets ?? [{
2656
+ kind: "files",
2657
+ patterns: legacy.data.patterns
2658
+ }]
2659
+ });
2660
+ return [];
2661
+ });
2662
+ if (!isJsonObject(value)) return value;
2663
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, visit(child)]));
2664
+ };
2665
+ return {
2666
+ document: visit(document),
2667
+ lenses
2668
+ };
2669
+ }
2670
+ //#endregion
2671
+ export { unionIntervals as $, applyLensEdit as A, parseMarkdown as B, resourceReferences as C, summarizeEdit as D, sourceReferences as E, checkSourcePath as F, selectionKey as G, comparisonKey as H, fileLineRangeSchema as I, coverageSources as J, sourceAnchors as K, requireVisibleSource as L, lensSchema as M, lensSelections as N, checkReferences as O, SourceRangeError as P, subtractIntervals as Q, sliceSourceRange as R, resourceReference as S, selectionReferences as T, formatAnchor as U, anchorSelection as V, resolveDiffSelection as W, intersectIntervals as X, emptyCoverage as Y, scopedCoverage as Z, hasCodeReferences as _, defineSoftwareMap as a, pinsSchema as b, traceSchema as c, applyEdit as d, updateCoverage as et, assignFreshIds as f, explicitPins as g, elements as h, SoftwareModelValidationError as i, lensEditSchema as j, UNCATEGORIZED_LENS_ID as k, textIncludesQuote as l, editSchema as m, migrateStoredDocument as n, normalizedSoftwareElementSchema as o, documentSchema as p, coverageSchema as q, migrateStoredLenses as r, normalizedSoftwareRelationshipSchema as s, liftFileLenses as t, anchorPins as u, isUnit as v, reviewTargetSchema as w, publishedEditSchema as x, pathTargetSchema as y, markdownNodes as z };