@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.
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/THIRD_PARTY_NOTICES.md +93 -0
- package/dist/account-alias-DOHN1RSH.js +973 -0
- package/dist/agent-cli-ChOq0ZuO.js +117 -0
- package/dist/agent-cli-DzWfZr6G.js +2 -0
- package/dist/agent-client-CPQo7iTI.js +305 -0
- package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
- package/dist/build-info.json +1 -0
- package/dist/cli-hVgHcdsv.js +47 -0
- package/dist/cli-runner-D8y2_luU.js +5095 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -0
- package/dist/client-DFoh50W3.js +950 -0
- package/dist/desktop-discovery-DBO-PM2V.js +164 -0
- package/dist/error-message-OtiDonty.js +6 -0
- package/dist/fs-utils-BMPLt0cr.js +22 -0
- package/dist/fuzzy-match-BoAmcyak.js +30 -0
- package/dist/headless-host-BJxf4dUU.js +94 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/local-data-CxLk75rx.js +4491 -0
- package/dist/mcp-QeF8vkdf.js +140 -0
- package/dist/package-paths-B6-zxvIO.js +47 -0
- package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
- package/dist/profile-4ry2f3AM.js +76 -0
- package/dist/profile-Bh5FIrmf.js +2 -0
- package/dist/request-origin-D_4QIdoN.js +811 -0
- package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
- package/dist/review-home-paths-6zZH-1c9.js +2560 -0
- package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +2 -0
- package/dist/s3-SzLpeCG6.js +345 -0
- package/dist/s3-config-BgxkSoOy.js +2 -0
- package/dist/s3-config-CAxhO9u_.js +676 -0
- package/dist/server/desktop-host.d.ts +4 -0
- package/dist/server/desktop-host.js +1702 -0
- package/dist/server-discovery-Dbzn3w6Z.js +66 -0
- package/dist/sharing/index.d.ts +1977 -0
- package/dist/sharing/index.js +2 -0
- package/dist/src-CGP5ytbV.js +283 -0
- package/dist/src-CmdiBL20.js +3676 -0
- package/dist/src-DnwdaQ2r.js +865 -0
- package/dist/src-X9phtB2j.js +68 -0
- package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
- package/dist/tool-failure-C8zB73HV.js +98 -0
- package/dist/tutorial-trace-DrBvkaeL.js +43 -0
- package/instructions/authoring.md +36 -0
- package/instructions/file-lenses.md +13 -0
- package/instructions/scratchpad.md +23 -0
- package/instructions/trace-archaeology.md +113 -0
- package/onboarding.md +8 -0
- package/package.json +106 -3
- package/src/agent-selection.ts +100 -0
- package/src/agent-session-ref.ts +80 -0
- package/src/ask/agents.ts +351 -0
- package/src/ask/checkout-files.ts +47 -0
- package/src/ask/file-refs.ts +111 -0
- package/src/ask/pi-mcp.ts +154 -0
- package/src/ask/protocol.ts +244 -0
- package/src/ask/thread-state.ts +350 -0
- package/src/ask/thread.ts +1400 -0
- package/src/ask/threads.ts +219 -0
- package/src/ask/watch.ts +79 -0
- package/src/cli-install.ts +1041 -0
- package/src/cli-runner.ts +1461 -0
- package/src/cli-runtime-info.ts +35 -0
- package/src/cli.ts +222 -0
- package/src/connect-prompts.ts +248 -0
- package/src/cursor-deeplink.ts +11 -0
- package/src/desktop-discovery.ts +355 -0
- package/src/diff-selection-migration.ts +63 -0
- package/src/embedded-posthog-key.ts +6 -0
- package/src/error-telemetry.ts +247 -0
- package/src/evidence.ts +14 -0
- package/src/exception-telemetry.ts +126 -0
- package/src/fixtures/blocks/call_stack_diff.json +27 -0
- package/src/fixtures/blocks/callout.json +11 -0
- package/src/fixtures/blocks/code.json +9 -0
- package/src/fixtures/blocks/code_peek.json +8 -0
- package/src/fixtures/blocks/database_lens.json +51 -0
- package/src/fixtures/blocks/divider.json +1 -0
- package/src/fixtures/blocks/fixtures.ts +33 -0
- package/src/fixtures/blocks/flow_diagram.json +36 -0
- package/src/fixtures/blocks/ids.ts +8 -0
- package/src/fixtures/blocks/image.json +9 -0
- package/src/fixtures/blocks/markdown.json +7 -0
- package/src/fixtures/blocks/section.json +11 -0
- package/src/fixtures/blocks/sequence.json +31 -0
- package/src/fixtures/blocks/software_map.json +7 -0
- package/src/fixtures/blocks/trace_quote.json +9 -0
- package/src/fixtures/blocks/tutorial.json +51 -0
- package/src/fs-utils.ts +32 -0
- package/src/fuzzy-match.ts +94 -0
- package/src/install.ts +29 -0
- package/src/legacy-skills.ts +133 -0
- package/src/lens-selection.ts +230 -0
- package/src/markdown-latex-math.ts +230 -0
- package/src/markdown.ts +81 -0
- package/src/package-paths.ts +53 -0
- package/src/posthog-capture-client.ts +610 -0
- package/src/review-api/README.md +295 -0
- package/src/review-api/activity.ts +340 -0
- package/src/review-api/agent-cli.ts +219 -0
- package/src/review-api/agent-client.ts +188 -0
- package/src/review-api/anchor-quotes.ts +88 -0
- package/src/review-api/ask-history.ts +242 -0
- package/src/review-api/authoring-tools.ts +211 -0
- package/src/review-api/blocks/call_stack_diff.ts +75 -0
- package/src/review-api/blocks/callout.ts +24 -0
- package/src/review-api/blocks/code.ts +9 -0
- package/src/review-api/blocks/code_peek.ts +13 -0
- package/src/review-api/blocks/database_lens.ts +150 -0
- package/src/review-api/blocks/definition.ts +40 -0
- package/src/review-api/blocks/divider.ts +6 -0
- package/src/review-api/blocks/flow_diagram.ts +111 -0
- package/src/review-api/blocks/image.ts +10 -0
- package/src/review-api/blocks/index.ts +90 -0
- package/src/review-api/blocks/markdown.ts +17 -0
- package/src/review-api/blocks/section.ts +24 -0
- package/src/review-api/blocks/sequence.ts +56 -0
- package/src/review-api/blocks/software_map.ts +9 -0
- package/src/review-api/blocks/trace_quote.ts +10 -0
- package/src/review-api/blocks/tutorial.ts +39 -0
- package/src/review-api/checkout-fs.ts +14 -0
- package/src/review-api/client.ts +1 -0
- package/src/review-api/comparison-coverage.ts +304 -0
- package/src/review-api/component-reference.ts +30 -0
- package/src/review-api/diff-lenses.ts +175 -0
- package/src/review-api/document-headings.ts +51 -0
- package/src/review-api/document-text.ts +200 -0
- package/src/review-api/document.ts +906 -0
- package/src/review-api/file-lenses.ts +100 -0
- package/src/review-api/http.ts +1877 -0
- package/src/review-api/image-decode.ts +29 -0
- package/src/review-api/input-error.ts +9 -0
- package/src/review-api/instructions.ts +89 -0
- package/src/review-api/lens-alignment.ts +52 -0
- package/src/review-api/local-data.ts +1837 -0
- package/src/review-api/map-input.ts +154 -0
- package/src/review-api/mcp-client-agent.ts +32 -0
- package/src/review-api/mcp.ts +240 -0
- package/src/review-api/origin.ts +43 -0
- package/src/review-api/profile.ts +146 -0
- package/src/review-api/public-tools.ts +102 -0
- package/src/review-api/pull-request.ts +389 -0
- package/src/review-api/read-schemas.ts +85 -0
- package/src/review-api/recovery.ts +2 -0
- package/src/review-api/request-origin.ts +33 -0
- package/src/review-api/review-progress.ts +382 -0
- package/src/review-api/status-tool.ts +10 -0
- package/src/review-api/store-schema.ts +22 -0
- package/src/review-api/store.ts +1647 -0
- package/src/review-api/tool-failure.ts +64 -0
- package/src/review-api/trace-schema.ts +13 -0
- package/src/review-api/traces.ts +119 -0
- package/src/review-api/unsupported-files.integration.ts +91 -0
- package/src/review-api/workspaces.ts +692 -0
- package/src/review-api/worktree-source.ts +170 -0
- package/src/review-api/worktree-structural.integration.ts +320 -0
- package/src/review-app-launcher.ts +432 -0
- package/src/review-app-picker.ts +168 -0
- package/src/review-app.ts +134 -0
- package/src/review-bundled-tools.ts +249 -0
- package/src/review-checkout-paths.ts +37 -0
- package/src/review-diff-files.ts +89 -0
- package/src/review-head-checkout.ts +300 -0
- package/src/review-home-paths.ts +73 -0
- package/src/review-info.ts +59 -0
- package/src/review-instances.ts +113 -0
- package/src/review-logger.ts +183 -0
- package/src/review-preferences.ts +88 -0
- package/src/review-prepare.ts +291 -0
- package/src/review-stack.ts +112 -0
- package/src/review-telemetry.ts +1066 -0
- package/src/runtime.ts +74 -0
- package/src/server/account-alias.ts +30 -0
- package/src/server/bounded-stream.ts +34 -0
- package/src/server/bug-report.ts +301 -0
- package/src/server/client-error-budget.ts +45 -0
- package/src/server/crash-report.ts +273 -0
- package/src/server/desktop-host-shutdown.ts +64 -0
- package/src/server/desktop-host.ts +190 -0
- package/src/server/desktop-server.ts +703 -0
- package/src/server/diffr-config.ts +509 -0
- package/src/server/diffr-languages.ts +97 -0
- package/src/server/global-verb-relay.ts +190 -0
- package/src/server/headless-host.ts +149 -0
- package/src/server/hono-http.ts +162 -0
- package/src/server/http-json.ts +24 -0
- package/src/server/json-review-reporting.ts +174 -0
- package/src/server/process-error-telemetry.ts +174 -0
- package/src/server/review-api-parsers.ts +90 -0
- package/src/server/review-lifecycle-telemetry.ts +73 -0
- package/src/server/review-open-watchdog.ts +46 -0
- package/src/server/review-server-core.ts +290 -0
- package/src/server/structural-comparisons.ts +153 -0
- package/src/server/structural-diff.ts +186 -0
- package/src/server/tutorial-service.ts +241 -0
- package/src/server/ui-telemetry.ts +198 -0
- package/src/server-discovery.ts +95 -0
- package/src/session-markers.ts +132 -0
- package/src/sharing/auth.ts +41 -0
- package/src/sharing/cli.ts +83 -0
- package/src/sharing/client.ts +334 -0
- package/src/sharing/export.ts +195 -0
- package/src/sharing/host.ts +316 -0
- package/src/sharing/import.ts +787 -0
- package/src/sharing/index.ts +16 -0
- package/src/sharing/repository.ts +145 -0
- package/src/sharing/routes.ts +34 -0
- package/src/slug.ts +23 -0
- package/src/software-map-diff-counts.ts +517 -0
- package/src/software-map-model.ts +1147 -0
- package/src/software-map-topology-diff.ts +260 -0
- package/src/source.ts +92 -0
- package/src/startup-trace.ts +232 -0
- package/src/stored-document-migration.ts +151 -0
- package/src/telemetry-clean-text.ts +257 -0
- package/src/telemetry-config.ts +298 -0
- package/src/telemetry-debug-sink.ts +38 -0
- package/src/telemetry.ts +13 -0
- package/src/trace-cli.ts +156 -0
- package/src/trace-storage-cli.ts +509 -0
- package/src/tutorial-conversation.ts +18 -0
- package/src/ui-telemetry-events.ts +765 -0
- package/src/unified-diff.ts +71 -0
- package/src/viewed-coverage.ts +259 -0
- package/src/windows-cli.ts +125 -0
- package/tutorial/document.json +275 -0
- package/tutorial/runtime-manifest.json +12 -0
- package/tutorial/sample-service/package.json +9 -0
- package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
- package/tutorial/sample-service/src/app.ts +26 -0
- package/tutorial/sample-service/src/database/schema.ts +10 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
- package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
- package/tutorial/sample-service/src/orders/order-service.ts +34 -0
- package/tutorial/sample-service/src/orders/order.ts +21 -0
- package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
- package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
- package/tutorial/sample-service/tsconfig.json +12 -0
- package/tutorial/software-map.json +139 -0
- 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 };
|