@davmash/bygone 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +64 -0
  3. package/bin/bygone.js +18 -1
  4. package/cli/commandSpec.js +22 -2
  5. package/cli/completions.js +30 -8
  6. package/cli/correspondence.js +71 -0
  7. package/cli/dependencies.js +62 -0
  8. package/cli/evidence.js +67 -0
  9. package/cli/present.js +61 -16
  10. package/cli/tour.js +18 -1
  11. package/cli/tourExport.js +1 -1
  12. package/completions/_bygone +15 -3
  13. package/completions/bygone +13 -5
  14. package/completions/bygone.fish +9 -0
  15. package/docs/change-tour-format.md +60 -6
  16. package/docs/correspondence.md +259 -0
  17. package/docs/dependencies.md +94 -0
  18. package/docs/evidence-review.md +163 -0
  19. package/docs/generating-change-tours.md +3 -0
  20. package/docs/present.md +3 -0
  21. package/docs/product-surface.md +2 -1
  22. package/docs/releasing.md +11 -0
  23. package/docs/vscode-integration.md +183 -0
  24. package/media/diff.worker.js +10 -9
  25. package/media/webview.css +1 -1
  26. package/media/webview.js +167 -167
  27. package/out/changeCorrespondence.js +739 -0
  28. package/out/changeTour.js +1366 -967
  29. package/out/dependencyGraph.js +213939 -0
  30. package/out/gitComparison.js +4 -0
  31. package/out/requirements.js +427 -0
  32. package/out/reviewDependencies.js +669 -0
  33. package/out/reviewWorkspace.js +1129 -0
  34. package/out/standalone-main.js +3382 -1341
  35. package/out/tourCoverage.js +1852 -1448
  36. package/package.json +2 -2
  37. package/schemas/change-tour-source.schema.json +48 -3
  38. package/web/export-runtime.json +6 -6
  39. package/web/index.html +13 -0
  40. package/web/presenter.css +81 -0
  41. package/web/web-host.js +23 -20
@@ -0,0 +1,1129 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ try {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ } catch (e) {
12
+ throw mod = 0, e;
13
+ }
14
+ };
15
+ var __export = (target, all) => {
16
+ for (var name in all)
17
+ __defProp(target, name, { get: all[name], enumerable: true });
18
+ };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") {
21
+ for (let key of __getOwnPropNames(from))
22
+ if (!__hasOwnProp.call(to, key) && key !== except)
23
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
24
+ }
25
+ return to;
26
+ };
27
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
28
+ // If the importer is in node compatibility mode or this is not an ESM
29
+ // file that has been converted to a CommonJS file using a Babel-
30
+ // compatible transform (i.e. "__esModule" has not been set), then set
31
+ // "default" to the CommonJS "module.exports" for node compatibility.
32
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
33
+ mod
34
+ ));
35
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
36
+
37
+ // packages/strand/src/input.js
38
+ var require_input = __commonJS({
39
+ "packages/strand/src/input.js"(exports2, module2) {
40
+ "use strict";
41
+ var LIMITS = Object.freeze({ maxFiles: 50, maxFileBytes: 1048576, maxBytes: 4194304, maxRecords: 2e4, timeoutMs: 3e4, maxOutputBytes: 16777216 });
42
+ function inputProblem(before, after, limits) {
43
+ if (!before || !after || typeof before !== "object" || typeof after !== "object" || Array.isArray(before) || Array.isArray(after)) return "invalid-snapshot-map";
44
+ if ((/* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])).size > Math.min(limits.maxFiles, LIMITS.maxFiles)) return "file-budget";
45
+ let bytes = 0;
46
+ for (const files of [before, after]) for (const value of Object.values(files)) {
47
+ if (typeof value !== "string") return "invalid-snapshot-text";
48
+ const size = Buffer.byteLength(value);
49
+ bytes += size;
50
+ if (size > Math.min(limits.maxFileBytes, LIMITS.maxFileBytes)) return "file-byte-budget";
51
+ }
52
+ return bytes > Math.min(limits.maxBytes, LIMITS.maxBytes) ? "comparison-byte-budget" : null;
53
+ }
54
+ function boundedProcessOptions(options, timeoutKey, outputKey, defaultTimeout) {
55
+ const timeout = options[timeoutKey] === void 0 ? defaultTimeout : options[timeoutKey];
56
+ const maxBuffer = options[outputKey] === void 0 ? LIMITS.maxOutputBytes : options[outputKey];
57
+ if (![timeout, maxBuffer].every((value) => Number.isSafeInteger(value) && value > 0)) return null;
58
+ return { timeout: Math.min(timeout, defaultTimeout), maxBuffer: Math.min(maxBuffer, LIMITS.maxOutputBytes) };
59
+ }
60
+ module2.exports = { LIMITS, inputProblem, boundedProcessOptions };
61
+ }
62
+ });
63
+
64
+ // packages/strand/src/source-references.js
65
+ var require_source_references = __commonJS({
66
+ "packages/strand/src/source-references.js"(exports2, module2) {
67
+ "use strict";
68
+ var { createHash: createHash3 } = require("node:crypto");
69
+ var { inputProblem, LIMITS } = require_input();
70
+ var digest3 = (text) => createHash3("sha256").update(text, "utf8").digest("hex");
71
+ var key = (ref) => JSON.stringify([ref.revision, ref.path, ref.start, ref.end, ref.digest]);
72
+ function identifier(value) {
73
+ if (typeof value !== "string" || !value.trim() || value.length > 1e5) {
74
+ throw new TypeError("Expected bounded nonempty identifier");
75
+ }
76
+ return value;
77
+ }
78
+ function parseSourceRef3(value) {
79
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Expected source reference");
80
+ const revision2 = identifier(value.revision), path = identifier(value.path);
81
+ if (path.startsWith("/") || /^[A-Za-z]:/.test(path) || path.includes("\\") || path.includes("\0") || path.split("/").some((part) => !part || part === "." || part === "..")) {
82
+ throw new TypeError("Invalid relative source path");
83
+ }
84
+ if (!Number.isSafeInteger(value.start) || !Number.isSafeInteger(value.end) || value.start < 0 || value.end <= value.start) {
85
+ throw new TypeError("Invalid UTF-16 range");
86
+ }
87
+ if (typeof value.digest !== "string" || !/^[a-f0-9]{64}$/.test(value.digest)) throw new TypeError("Expected SHA-256");
88
+ return { revision: revision2, path, start: value.start, end: value.end, digest: value.digest };
89
+ }
90
+ function checkSnapshot(snapshot) {
91
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) throw new TypeError("Expected snapshot");
92
+ identifier(snapshot.revision);
93
+ const problem = inputProblem(snapshot.files, {}, LIMITS);
94
+ if (problem) throw new TypeError(problem);
95
+ }
96
+ function sourceAt(snapshot, path) {
97
+ return Object.prototype.hasOwnProperty.call(snapshot.files, path) ? snapshot.files[path] : void 0;
98
+ }
99
+ function pinSourceRef2(snapshot, path, start, end) {
100
+ checkSnapshot(snapshot);
101
+ const ref = parseSourceRef3({ revision: snapshot.revision, path, start, end, digest: "0".repeat(64) });
102
+ const text = sourceAt(snapshot, ref.path);
103
+ if (typeof text !== "string" || end > text.length) throw new TypeError("Source is unavailable or range exceeds file");
104
+ return { ...ref, digest: digest3(text.slice(start, end)) };
105
+ }
106
+ function resolveChecked(ref, snapshot) {
107
+ if (snapshot.revision !== ref.revision) return { status: "broken", reason: "Revision unavailable in this comparison" };
108
+ const text = sourceAt(snapshot, ref.path);
109
+ if (typeof text !== "string" || ref.end > text.length) return { status: "broken", reason: "Source path or range unavailable" };
110
+ const slice = text.slice(ref.start, ref.end);
111
+ if (digest3(slice) !== ref.digest) return { status: "broken", reason: "Source digest mismatch" };
112
+ return { status: "valid", text: slice };
113
+ }
114
+ function resolveSourceRef3(value, snapshot) {
115
+ const ref = parseSourceRef3(value);
116
+ checkSnapshot(snapshot);
117
+ return resolveChecked(ref, snapshot);
118
+ }
119
+ function boundedList(value) {
120
+ if (!Array.isArray(value) || value.length > LIMITS.maxRecords) throw new TypeError("Expected bounded reference array");
121
+ return value;
122
+ }
123
+ function relocateSourceRefs2(values, before, after, pairs) {
124
+ checkSnapshot(before);
125
+ checkSnapshot(after);
126
+ const problem = inputProblem(before.files, after.files, LIMITS);
127
+ if (problem) throw new TypeError(problem);
128
+ if (before.revision === after.revision) throw new TypeError("Relocation requires distinct snapshot revisions");
129
+ const refs = boundedList(values).map(parseSourceRef3);
130
+ const candidates = boundedList(pairs).map((pair) => {
131
+ if (!pair || typeof pair !== "object") throw new TypeError("Expected correspondence pair");
132
+ return { before: parseSourceRef3(pair.before), after: parseSourceRef3(pair.after) };
133
+ });
134
+ const destinations = /* @__PURE__ */ new Map(), usedAfter = /* @__PURE__ */ new Set();
135
+ for (const pair of candidates) {
136
+ const from = key(pair.before), to = key(pair.after);
137
+ if (destinations.has(from) || usedAfter.has(to)) throw new TypeError("Duplicate correspondence endpoint");
138
+ destinations.set(from, pair.after);
139
+ usedAfter.add(to);
140
+ }
141
+ for (const pair of candidates) {
142
+ if (resolveChecked(pair.before, before).status !== "valid" || resolveChecked(pair.after, after).status !== "valid") {
143
+ throw new TypeError("Invalid correspondence endpoint");
144
+ }
145
+ }
146
+ return refs.map((ref) => {
147
+ let destination;
148
+ if (resolveChecked(ref, before).status === "valid") {
149
+ destination = sourceAt(before, ref.path) === sourceAt(after, ref.path) ? { ...ref, revision: after.revision } : destinations.get(key(ref));
150
+ }
151
+ if (!destination) return { status: "unresolved", before: ref };
152
+ const status2 = ref.digest !== destination.digest ? "changed" : ref.path !== destination.path || ref.start !== destination.start ? "relocated" : "unchanged";
153
+ return { status: status2, before: ref, after: { ...destination } };
154
+ });
155
+ }
156
+ module2.exports = { parseSourceRef: parseSourceRef3, pinSourceRef: pinSourceRef2, resolveSourceRef: resolveSourceRef3, relocateSourceRefs: relocateSourceRefs2 };
157
+ }
158
+ });
159
+
160
+ // packages/strand/source-references.js
161
+ var require_source_references2 = __commonJS({
162
+ "packages/strand/source-references.js"(exports2, module2) {
163
+ "use strict";
164
+ module2.exports = require_source_references();
165
+ }
166
+ });
167
+
168
+ // src/reviewWorkspace.ts
169
+ var reviewWorkspace_exports = {};
170
+ __export(reviewWorkspace_exports, {
171
+ commitOid: () => commitOid,
172
+ comparisonPaths: () => comparisonPaths,
173
+ git: () => git,
174
+ inspectWorkspace: () => inspectWorkspace,
175
+ lineReference: () => lineReference,
176
+ loadReview: () => loadReview,
177
+ readEvidenceJson: () => readEvidenceJson,
178
+ readPinnedFile: () => readPinnedFile,
179
+ readSnapshot: () => readSnapshot,
180
+ refreshWorkspace: () => refreshWorkspace,
181
+ repositoryRoot: () => repositoryRoot,
182
+ sourceText: () => sourceText,
183
+ writeReview: () => writeReview
184
+ });
185
+ module.exports = __toCommonJS(reviewWorkspace_exports);
186
+ var import_child_process2 = require("child_process");
187
+ var import_fs2 = require("fs");
188
+ var import_path = require("path");
189
+ var import_util = require("util");
190
+ var import_source_references2 = __toESM(require_source_references2());
191
+
192
+ // src/requirements.ts
193
+ var import_crypto = require("crypto");
194
+ var import_source_references = __toESM(require_source_references2());
195
+ var digest = (text) => (0, import_crypto.createHash)("sha256").update(text, "utf8").digest("hex");
196
+ function record(value) {
197
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected object");
198
+ return value;
199
+ }
200
+ function string(value) {
201
+ if (typeof value !== "string" || !value.trim() || value.length > 1e5) throw new Error("Expected bounded nonempty string");
202
+ return value;
203
+ }
204
+ function list(value) {
205
+ if (!Array.isArray(value) || value.length > 1e3) throw new Error("Expected bounded array");
206
+ return value;
207
+ }
208
+ function choice(value, choices) {
209
+ if (!choices.includes(value)) throw new Error(`Expected ${choices.join("/")}`);
210
+ return value;
211
+ }
212
+ function hash(value) {
213
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) throw new Error("Expected SHA-256");
214
+ return value;
215
+ }
216
+ function revision(value) {
217
+ if (typeof value !== "string" || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value)) throw new Error("Expected immutable Git revision");
218
+ return value;
219
+ }
220
+ function sourceRef(value) {
221
+ const ref = (0, import_source_references.parseSourceRef)(value);
222
+ revision(ref.revision);
223
+ return ref;
224
+ }
225
+ function unique(values) {
226
+ if (new Set(values).size !== values.length) throw new Error("Duplicate ID");
227
+ }
228
+ function sources(value) {
229
+ const result = list(value).map((value2) => {
230
+ const s = record(value2);
231
+ const source = { title: string(s.title), text: string(s.text), quote: string(s.quote) };
232
+ if (!source.text.includes(source.quote)) throw new Error("Source quotation does not exist");
233
+ return source;
234
+ });
235
+ if (!result.length) throw new Error("Expected at least one source");
236
+ return result;
237
+ }
238
+ function parseAnalysis(value) {
239
+ const a = record(value), scope = list(a.scope).map(string);
240
+ if (!scope.length) throw new Error("Expected analysis scope");
241
+ return {
242
+ base: revision(a.base),
243
+ head: revision(a.head),
244
+ producer: string(a.producer),
245
+ status: choice(a.status, ["complete", "partial", "unavailable"]),
246
+ scope,
247
+ limitations: list(a.limitations).map(string),
248
+ artifactDigest: hash(a.artifactDigest)
249
+ };
250
+ }
251
+ function parseRequirement(value) {
252
+ const r = record(value);
253
+ return { id: string(r.id), text: string(r.text), sources: sources(r.sources) };
254
+ }
255
+ function parseQuestion(value, requirement = string) {
256
+ const q = record(value);
257
+ const alternatives = list(q.alternatives).map((value2) => {
258
+ const a = record(value2);
259
+ return { text: string(a.text), sources: sources(a.sources) };
260
+ });
261
+ const common = { id: string(q.id), requirement: requirement(q.requirement), text: string(q.text), alternatives };
262
+ const status2 = choice(q.status, ["open", "resolved"]);
263
+ if (status2 === "open" && q.resolution !== void 0) throw new Error("Open question cannot have a resolution");
264
+ return status2 === "open" ? { ...common, status: status2 } : { ...common, status: status2, resolution: string(q.resolution) };
265
+ }
266
+ function parseLink(value, requirement = string) {
267
+ const x = record(value);
268
+ return { id: string(x.id), requirement: requirement(x.requirement), role: choice(x.role, ["implementation", "test", "context", "counterexample"]), rationale: string(x.rationale), ref: sourceRef(x.ref) };
269
+ }
270
+ function parseRun(value, requirement = string) {
271
+ const x = record(value), associations = list(x.requirements).map((value2) => {
272
+ const a = record(value2);
273
+ return { requirement: requirement(a.requirement), fingerprint: hash(a.fingerprint), scope: string(a.scope) };
274
+ });
275
+ unique(associations.map((a) => a.requirement));
276
+ return { revision: revision(x.revision), requirements: associations, command: string(x.command), outcome: choice(x.outcome, ["passed", "failed"]), outputDigest: hash(x.outputDigest) };
277
+ }
278
+ function correspondenceEvidence(artifact, scope) {
279
+ const ref = (side, id) => {
280
+ const symbol = artifact.symbols[side].find((s) => s.id === id);
281
+ if (!symbol) throw new Error("Unknown correspondence symbol");
282
+ return sourceRef({
283
+ revision: side === "before" ? artifact.range.baseOid : artifact.range.headOid,
284
+ path: symbol.path,
285
+ start: symbol.start,
286
+ end: symbol.end,
287
+ digest: symbol.digest
288
+ });
289
+ };
290
+ return {
291
+ pairs: artifact.relations.map((r) => ({ before: ref("before", r.sourceId), after: ref("after", r.destinationId) })),
292
+ analysis: parseAnalysis({
293
+ base: artifact.range.baseOid,
294
+ head: artifact.range.headOid,
295
+ producer: artifact.producer,
296
+ status: artifact.status,
297
+ scope: [...scope, ...artifact.diagnostics?.scope || []],
298
+ limitations: artifact.limitations,
299
+ artifactDigest: digest(JSON.stringify(artifact))
300
+ })
301
+ };
302
+ }
303
+ function parseReview(value) {
304
+ const r = record(value);
305
+ if (r.version !== 2) throw new Error("Unsupported review version");
306
+ const requirements = list(r.requirements).map(parseRequirement);
307
+ unique(requirements.map((r2) => r2.id));
308
+ const requirement = (value2) => {
309
+ const id = string(value2);
310
+ if (!requirements.some((r2) => r2.id === id)) throw new Error(`Unknown requirement: ${id}`);
311
+ return id;
312
+ };
313
+ const questions = list(r.questions).map((value2) => parseQuestion(value2, requirement));
314
+ unique(questions.map((q) => q.id));
315
+ const links = list(r.links).map((value2) => parseLink(value2, requirement));
316
+ unique(links.map((l) => l.id));
317
+ const runs = list(r.runs).map((value2) => parseRun(value2, requirement));
318
+ const decisions = list(r.decisions).map((value2) => {
319
+ const x = record(value2);
320
+ return { requirement: requirement(x.requirement), fingerprint: hash(x.fingerprint), note: string(x.note) };
321
+ });
322
+ unique(decisions.map((d) => d.requirement));
323
+ return { version: 2, repository: string(r.repository), base: revision(r.base), head: revision(r.head), requirements, questions, links, runs, analyses: list(r.analyses).map(parseAnalysis), decisions };
324
+ }
325
+ function snapshotsMatch(review, base, head) {
326
+ if (review.repository !== base.repository || review.repository !== head.repository || review.base !== base.revision || review.head !== head.revision) throw new Error("Repository or comparison mismatch");
327
+ }
328
+ function sourceSubset(snapshot, paths) {
329
+ const present = [...new Set(paths)].filter((path) => Object.prototype.hasOwnProperty.call(snapshot.files, path));
330
+ return { revision: snapshot.revision, files: Object.fromEntries(present.map((path) => [path, snapshot.files[path]])) };
331
+ }
332
+ function resolveReference(ref, snapshot) {
333
+ return (0, import_source_references.resolveSourceRef)(ref, sourceSubset(snapshot, [ref.path]));
334
+ }
335
+ function requirementFingerprint(review, id) {
336
+ const requirement = review.requirements.find((r) => r.id === id);
337
+ if (!requirement) throw new Error(`Unknown requirement: ${id}`);
338
+ return digest(JSON.stringify({
339
+ repository: review.repository,
340
+ requirement: parseRequirement(requirement),
341
+ questions: review.questions.filter((q) => q.requirement === id).map((q) => parseQuestion(q))
342
+ }));
343
+ }
344
+ function fingerprint(review, id) {
345
+ return digest(JSON.stringify({
346
+ repository: review.repository,
347
+ base: review.base,
348
+ head: review.head,
349
+ requirement: requirementFingerprint(review, id),
350
+ links: review.links.filter((l) => l.requirement === id).map((l) => parseLink(l)),
351
+ runs: review.runs.filter((r) => r.requirements.some((a) => a.requirement === id)).map((r) => parseRun(r)),
352
+ analyses: review.analyses.map(parseAnalysis)
353
+ }));
354
+ }
355
+ function inspect(review, base, head) {
356
+ snapshotsMatch(review, base, head);
357
+ const links = review.links.map((link) => ({ ...link, resolution: resolveReference(link.ref, link.ref.revision === base.revision ? base : head) }));
358
+ const requirements = review.requirements.map((requirement) => {
359
+ const attached = links.filter((l) => l.requirement === requirement.id);
360
+ const valid = attached.filter((l) => l.resolution.status === "valid");
361
+ const key = requirementFingerprint(review, requirement.id);
362
+ const runs = review.runs.flatMap((run) => run.requirements.filter((a) => a.requirement === requirement.id).map((association) => ({
363
+ ...run,
364
+ association,
365
+ applicability: run.revision !== head.revision ? "historical-revision" : association.fingerprint === key ? "current" : "stale-requirement"
366
+ })));
367
+ const currentRuns = runs.filter((r) => r.applicability === "current");
368
+ const decision = review.decisions.find((d) => d.requirement === requirement.id);
369
+ return {
370
+ ...requirement,
371
+ questions: review.questions.filter((q) => q.requirement === requirement.id),
372
+ runs,
373
+ links: attached,
374
+ implementationLinked: valid.some((l) => l.role === "implementation"),
375
+ testLinked: valid.some((l) => l.role === "test"),
376
+ counterexampleLinked: valid.some((l) => l.role === "counterexample"),
377
+ verification: currentRuns.length ? currentRuns.some((r) => r.outcome === "failed") ? "recorded-failure" : "recorded-pass" : runs.some((r) => r.applicability === "stale-requirement") ? "needs-reassessment" : "not-recorded",
378
+ reviewer: !decision ? "unreviewed" : decision.fingerprint === fingerprint(review, requirement.id) && attached.every((l) => l.resolution.status === "valid") ? "inspected" : "needs-review",
379
+ note: decision?.note
380
+ };
381
+ });
382
+ const changedPaths = [.../* @__PURE__ */ new Set([...Object.keys(base.files), ...Object.keys(head.files)])].filter((p) => base.files[p] !== head.files[p]).sort();
383
+ return {
384
+ requirements,
385
+ analyses: review.analyses.map((analysis) => ({
386
+ ...analysis,
387
+ relevance: analysis.head !== review.head ? "historical" : analysis.base === review.base ? "current-comparison" : "current-head-delta"
388
+ })),
389
+ changedPathsWithoutLinks: changedPaths.filter((p) => !links.some((l) => l.ref.path === p && l.resolution.status === "valid"))
390
+ };
391
+ }
392
+ function refresh(review, oldHead, newHead, evidence, requirements = review.requirements) {
393
+ if (review.repository !== oldHead.repository || review.repository !== newHead.repository || review.head !== oldHead.revision) throw new Error("Repository or old head mismatch");
394
+ revision(oldHead.revision);
395
+ revision(newHead.revision);
396
+ if (evidence !== void 0) {
397
+ record(evidence);
398
+ if (!Array.isArray(evidence.pairs)) throw new Error("Expected correspondence pairs");
399
+ }
400
+ const analysis = evidence === void 0 ? void 0 : parseAnalysis(evidence.analysis);
401
+ const pairs = evidence === void 0 ? [] : evidence.pairs;
402
+ if (analysis && (analysis.base !== oldHead.revision || analysis.head !== newHead.revision)) throw new Error("Analysis comparison mismatch");
403
+ if (analysis?.status === "unavailable" && pairs.length) throw new Error("Unavailable analysis cannot supply pairs");
404
+ const current = review.links.filter((link) => link.ref.revision !== review.base);
405
+ let relocated;
406
+ if (oldHead.revision === newHead.revision) {
407
+ if (pairs.length) throw new Error("Same-revision updates cannot supply correspondence pairs");
408
+ relocated = current.map(({ ref }) => resolveReference(ref, oldHead).status === "valid" && resolveReference(ref, newHead).status === "valid" ? { status: "unchanged", before: ref, after: ref } : { status: "unresolved", before: ref });
409
+ } else {
410
+ const paths = current.map((link) => link.ref.path);
411
+ const before = sourceSubset(oldHead, [...paths, ...pairs.map((pair) => pair.before.path)]);
412
+ const after = sourceSubset(newHead, [...paths, ...pairs.map((pair) => pair.after.path)]);
413
+ relocated = (0, import_source_references.relocateSourceRefs)(current.map((link) => link.ref), before, after, pairs);
414
+ }
415
+ const transitions = [];
416
+ let index = 0;
417
+ const links = review.links.map((link) => {
418
+ const before = link.ref;
419
+ if (before.revision === review.base) {
420
+ transitions.push({ link: link.id, status: "base-evidence", before });
421
+ return link;
422
+ }
423
+ const transition = relocated[index++];
424
+ transitions.push({ ...transition, link: link.id });
425
+ return transition.status === "unresolved" ? link : { ...link, ref: transition.after };
426
+ });
427
+ const next = parseReview({ ...review, head: newHead.revision, requirements, links, analyses: analysis ? [...review.analyses, analysis] : review.analyses });
428
+ return { review: next, transitions };
429
+ }
430
+
431
+ // src/changeCorrespondence.ts
432
+ var import_crypto2 = require("crypto");
433
+ var import_child_process = require("child_process");
434
+ var import_fs = require("fs");
435
+
436
+ // node_modules/diff/lib/index.mjs
437
+ function Diff() {
438
+ }
439
+ Diff.prototype = {
440
+ diff: function diff(oldString, newString) {
441
+ var _options$timeout;
442
+ var options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
443
+ var callback = options.callback;
444
+ if (typeof options === "function") {
445
+ callback = options;
446
+ options = {};
447
+ }
448
+ this.options = options;
449
+ var self = this;
450
+ function done(value) {
451
+ if (callback) {
452
+ setTimeout(function() {
453
+ callback(void 0, value);
454
+ }, 0);
455
+ return true;
456
+ } else {
457
+ return value;
458
+ }
459
+ }
460
+ oldString = this.castInput(oldString);
461
+ newString = this.castInput(newString);
462
+ oldString = this.removeEmpty(this.tokenize(oldString));
463
+ newString = this.removeEmpty(this.tokenize(newString));
464
+ var newLen = newString.length, oldLen = oldString.length;
465
+ var editLength = 1;
466
+ var maxEditLength = newLen + oldLen;
467
+ if (options.maxEditLength) {
468
+ maxEditLength = Math.min(maxEditLength, options.maxEditLength);
469
+ }
470
+ var maxExecutionTime = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : Infinity;
471
+ var abortAfterTimestamp = Date.now() + maxExecutionTime;
472
+ var bestPath = [{
473
+ oldPos: -1,
474
+ lastComponent: void 0
475
+ }];
476
+ var newPos = this.extractCommon(bestPath[0], newString, oldString, 0);
477
+ if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
478
+ return done([{
479
+ value: this.join(newString),
480
+ count: newString.length
481
+ }]);
482
+ }
483
+ var minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
484
+ function execEditLength() {
485
+ for (var diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
486
+ var basePath = void 0;
487
+ var removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
488
+ if (removePath) {
489
+ bestPath[diagonalPath - 1] = void 0;
490
+ }
491
+ var canAdd = false;
492
+ if (addPath) {
493
+ var addPathNewPos = addPath.oldPos - diagonalPath;
494
+ canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
495
+ }
496
+ var canRemove = removePath && removePath.oldPos + 1 < oldLen;
497
+ if (!canAdd && !canRemove) {
498
+ bestPath[diagonalPath] = void 0;
499
+ continue;
500
+ }
501
+ if (!canRemove || canAdd && removePath.oldPos + 1 < addPath.oldPos) {
502
+ basePath = self.addToPath(addPath, true, void 0, 0);
503
+ } else {
504
+ basePath = self.addToPath(removePath, void 0, true, 1);
505
+ }
506
+ newPos = self.extractCommon(basePath, newString, oldString, diagonalPath);
507
+ if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
508
+ return done(buildValues(self, basePath.lastComponent, newString, oldString, self.useLongestToken));
509
+ } else {
510
+ bestPath[diagonalPath] = basePath;
511
+ if (basePath.oldPos + 1 >= oldLen) {
512
+ maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
513
+ }
514
+ if (newPos + 1 >= newLen) {
515
+ minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
516
+ }
517
+ }
518
+ }
519
+ editLength++;
520
+ }
521
+ if (callback) {
522
+ (function exec() {
523
+ setTimeout(function() {
524
+ if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
525
+ return callback();
526
+ }
527
+ if (!execEditLength()) {
528
+ exec();
529
+ }
530
+ }, 0);
531
+ })();
532
+ } else {
533
+ while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
534
+ var ret = execEditLength();
535
+ if (ret) {
536
+ return ret;
537
+ }
538
+ }
539
+ }
540
+ },
541
+ addToPath: function addToPath(path, added, removed, oldPosInc) {
542
+ var last = path.lastComponent;
543
+ if (last && last.added === added && last.removed === removed) {
544
+ return {
545
+ oldPos: path.oldPos + oldPosInc,
546
+ lastComponent: {
547
+ count: last.count + 1,
548
+ added,
549
+ removed,
550
+ previousComponent: last.previousComponent
551
+ }
552
+ };
553
+ } else {
554
+ return {
555
+ oldPos: path.oldPos + oldPosInc,
556
+ lastComponent: {
557
+ count: 1,
558
+ added,
559
+ removed,
560
+ previousComponent: last
561
+ }
562
+ };
563
+ }
564
+ },
565
+ extractCommon: function extractCommon(basePath, newString, oldString, diagonalPath) {
566
+ var newLen = newString.length, oldLen = oldString.length, oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
567
+ while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newString[newPos + 1], oldString[oldPos + 1])) {
568
+ newPos++;
569
+ oldPos++;
570
+ commonCount++;
571
+ }
572
+ if (commonCount) {
573
+ basePath.lastComponent = {
574
+ count: commonCount,
575
+ previousComponent: basePath.lastComponent
576
+ };
577
+ }
578
+ basePath.oldPos = oldPos;
579
+ return newPos;
580
+ },
581
+ equals: function equals(left, right) {
582
+ if (this.options.comparator) {
583
+ return this.options.comparator(left, right);
584
+ } else {
585
+ return left === right || this.options.ignoreCase && left.toLowerCase() === right.toLowerCase();
586
+ }
587
+ },
588
+ removeEmpty: function removeEmpty(array) {
589
+ var ret = [];
590
+ for (var i = 0; i < array.length; i++) {
591
+ if (array[i]) {
592
+ ret.push(array[i]);
593
+ }
594
+ }
595
+ return ret;
596
+ },
597
+ castInput: function castInput(value) {
598
+ return value;
599
+ },
600
+ tokenize: function tokenize(value) {
601
+ return value.split("");
602
+ },
603
+ join: function join(chars) {
604
+ return chars.join("");
605
+ }
606
+ };
607
+ function buildValues(diff2, lastComponent, newString, oldString, useLongestToken) {
608
+ var components = [];
609
+ var nextComponent;
610
+ while (lastComponent) {
611
+ components.push(lastComponent);
612
+ nextComponent = lastComponent.previousComponent;
613
+ delete lastComponent.previousComponent;
614
+ lastComponent = nextComponent;
615
+ }
616
+ components.reverse();
617
+ var componentPos = 0, componentLen = components.length, newPos = 0, oldPos = 0;
618
+ for (; componentPos < componentLen; componentPos++) {
619
+ var component = components[componentPos];
620
+ if (!component.removed) {
621
+ if (!component.added && useLongestToken) {
622
+ var value = newString.slice(newPos, newPos + component.count);
623
+ value = value.map(function(value2, i) {
624
+ var oldValue = oldString[oldPos + i];
625
+ return oldValue.length > value2.length ? oldValue : value2;
626
+ });
627
+ component.value = diff2.join(value);
628
+ } else {
629
+ component.value = diff2.join(newString.slice(newPos, newPos + component.count));
630
+ }
631
+ newPos += component.count;
632
+ if (!component.added) {
633
+ oldPos += component.count;
634
+ }
635
+ } else {
636
+ component.value = diff2.join(oldString.slice(oldPos, oldPos + component.count));
637
+ oldPos += component.count;
638
+ if (componentPos && components[componentPos - 1].added) {
639
+ var tmp = components[componentPos - 1];
640
+ components[componentPos - 1] = components[componentPos];
641
+ components[componentPos] = tmp;
642
+ }
643
+ }
644
+ }
645
+ var finalComponent = components[componentLen - 1];
646
+ if (componentLen > 1 && typeof finalComponent.value === "string" && (finalComponent.added || finalComponent.removed) && diff2.equals("", finalComponent.value)) {
647
+ components[componentLen - 2].value += finalComponent.value;
648
+ components.pop();
649
+ }
650
+ return components;
651
+ }
652
+ var characterDiff = new Diff();
653
+ var extendedWordChars = /^[A-Za-z\xC0-\u02C6\u02C8-\u02D7\u02DE-\u02FF\u1E00-\u1EFF]+$/;
654
+ var reWhitespace = /\S/;
655
+ var wordDiff = new Diff();
656
+ wordDiff.equals = function(left, right) {
657
+ if (this.options.ignoreCase) {
658
+ left = left.toLowerCase();
659
+ right = right.toLowerCase();
660
+ }
661
+ return left === right || this.options.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right);
662
+ };
663
+ wordDiff.tokenize = function(value) {
664
+ var tokens = value.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/);
665
+ for (var i = 0; i < tokens.length - 1; i++) {
666
+ if (!tokens[i + 1] && tokens[i + 2] && extendedWordChars.test(tokens[i]) && extendedWordChars.test(tokens[i + 2])) {
667
+ tokens[i] += tokens[i + 2];
668
+ tokens.splice(i + 1, 2);
669
+ i--;
670
+ }
671
+ }
672
+ return tokens;
673
+ };
674
+ var lineDiff = new Diff();
675
+ lineDiff.tokenize = function(value) {
676
+ if (this.options.stripTrailingCr) {
677
+ value = value.replace(/\r\n/g, "\n");
678
+ }
679
+ var retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
680
+ if (!linesAndNewlines[linesAndNewlines.length - 1]) {
681
+ linesAndNewlines.pop();
682
+ }
683
+ for (var i = 0; i < linesAndNewlines.length; i++) {
684
+ var line = linesAndNewlines[i];
685
+ if (i % 2 && !this.options.newlineIsToken) {
686
+ retLines[retLines.length - 1] += line;
687
+ } else {
688
+ if (this.options.ignoreWhitespace) {
689
+ line = line.trim();
690
+ }
691
+ retLines.push(line);
692
+ }
693
+ }
694
+ return retLines;
695
+ };
696
+ function diffLines(oldStr, newStr, callback) {
697
+ return lineDiff.diff(oldStr, newStr, callback);
698
+ }
699
+ var sentenceDiff = new Diff();
700
+ sentenceDiff.tokenize = function(value) {
701
+ return value.split(/(\S.+?[.!?])(?=\s+|$)/);
702
+ };
703
+ var cssDiff = new Diff();
704
+ cssDiff.tokenize = function(value) {
705
+ return value.split(/([{}:;,]|\s+)/);
706
+ };
707
+ function _typeof(obj) {
708
+ "@babel/helpers - typeof";
709
+ if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
710
+ _typeof = function(obj2) {
711
+ return typeof obj2;
712
+ };
713
+ } else {
714
+ _typeof = function(obj2) {
715
+ return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
716
+ };
717
+ }
718
+ return _typeof(obj);
719
+ }
720
+ var objectPrototypeToString = Object.prototype.toString;
721
+ var jsonDiff = new Diff();
722
+ jsonDiff.useLongestToken = true;
723
+ jsonDiff.tokenize = lineDiff.tokenize;
724
+ jsonDiff.castInput = function(value) {
725
+ var _this$options = this.options, undefinedReplacement = _this$options.undefinedReplacement, _this$options$stringi = _this$options.stringifyReplacer, stringifyReplacer = _this$options$stringi === void 0 ? function(k, v) {
726
+ return typeof v === "undefined" ? undefinedReplacement : v;
727
+ } : _this$options$stringi;
728
+ return typeof value === "string" ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, " ");
729
+ };
730
+ jsonDiff.equals = function(left, right) {
731
+ return Diff.prototype.equals.call(jsonDiff, left.replace(/,([\r\n])/g, "$1"), right.replace(/,([\r\n])/g, "$1"));
732
+ };
733
+ function canonicalize(obj, stack, replacementStack, replacer, key) {
734
+ stack = stack || [];
735
+ replacementStack = replacementStack || [];
736
+ if (replacer) {
737
+ obj = replacer(key, obj);
738
+ }
739
+ var i;
740
+ for (i = 0; i < stack.length; i += 1) {
741
+ if (stack[i] === obj) {
742
+ return replacementStack[i];
743
+ }
744
+ }
745
+ var canonicalizedObj;
746
+ if ("[object Array]" === objectPrototypeToString.call(obj)) {
747
+ stack.push(obj);
748
+ canonicalizedObj = new Array(obj.length);
749
+ replacementStack.push(canonicalizedObj);
750
+ for (i = 0; i < obj.length; i += 1) {
751
+ canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key);
752
+ }
753
+ stack.pop();
754
+ replacementStack.pop();
755
+ return canonicalizedObj;
756
+ }
757
+ if (obj && obj.toJSON) {
758
+ obj = obj.toJSON();
759
+ }
760
+ if (_typeof(obj) === "object" && obj !== null) {
761
+ stack.push(obj);
762
+ canonicalizedObj = {};
763
+ replacementStack.push(canonicalizedObj);
764
+ var sortedKeys = [], _key;
765
+ for (_key in obj) {
766
+ if (obj.hasOwnProperty(_key)) {
767
+ sortedKeys.push(_key);
768
+ }
769
+ }
770
+ sortedKeys.sort();
771
+ for (i = 0; i < sortedKeys.length; i += 1) {
772
+ _key = sortedKeys[i];
773
+ canonicalizedObj[_key] = canonicalize(obj[_key], stack, replacementStack, replacer, _key);
774
+ }
775
+ stack.pop();
776
+ replacementStack.pop();
777
+ } else {
778
+ canonicalizedObj = obj;
779
+ }
780
+ return canonicalizedObj;
781
+ }
782
+ var arrayDiff = new Diff();
783
+ arrayDiff.tokenize = function(value) {
784
+ return value.slice();
785
+ };
786
+ arrayDiff.join = arrayDiff.removeEmpty = function(value) {
787
+ return value;
788
+ };
789
+
790
+ // src/changeCorrespondence.ts
791
+ var MAX_BYTES = 16 * 1024 * 1024;
792
+ var MAX_SOURCE_BYTES = 8 * 1024 * 1024;
793
+ var digest2 = (text) => (0, import_crypto2.createHash)("sha256").update(text, "utf8").digest("hex");
794
+ function object(value) {
795
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid correspondence object.");
796
+ return value;
797
+ }
798
+ function list2(value, max = 2e4) {
799
+ if (!Array.isArray(value) || value.length > max) throw new Error("Invalid or over-budget correspondence array.");
800
+ return value;
801
+ }
802
+ function string2(value) {
803
+ if (typeof value !== "string" || !value || value.length > 4096) throw new Error("Invalid correspondence string.");
804
+ return value;
805
+ }
806
+ function integer(value) {
807
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error("Invalid correspondence offset.");
808
+ return value;
809
+ }
810
+ function filePath(value) {
811
+ const name = string2(value);
812
+ if (name.startsWith("/") || name.includes("\\") || name.includes("\0") || name.split("/").some((p) => !p || p === "." || p === "..")) {
813
+ throw new Error("Invalid correspondence repository path.");
814
+ }
815
+ return name;
816
+ }
817
+ function hash2(value) {
818
+ const result = string2(value);
819
+ if (!/^[0-9a-f]{64}$/.test(result)) throw new Error("Invalid correspondence digest.");
820
+ return result;
821
+ }
822
+ function status(value) {
823
+ if (value !== "complete" && value !== "partial" && value !== "unavailable") throw new Error("Invalid correspondence analysis status.");
824
+ return value;
825
+ }
826
+ function readCorrespondenceJson(file) {
827
+ if ((0, import_fs.statSync)(file).size > MAX_BYTES) throw new Error("Correspondence file exceeds 16 MiB.");
828
+ return JSON.parse((0, import_fs.readFileSync)(file, "utf8"));
829
+ }
830
+ function validateRange(value, expected) {
831
+ const input = object(value);
832
+ for (const key of ["baseOid", "headOid"]) {
833
+ if (input[key] !== expected[key] || !/^[0-9a-f]{40,64}$/.test(expected[key])) {
834
+ throw new Error("Correspondence revisions do not match the selected merge-base/head comparison.");
835
+ }
836
+ }
837
+ return { ...expected };
838
+ }
839
+ function validateChangeCorrespondence(repoRoot, input, expected) {
840
+ const data = object(input);
841
+ if (data.version !== 1) throw new Error("Unsupported correspondence version.");
842
+ const range = validateRange(data.range, expected);
843
+ const documents = object(data.documents), symbols = object(data.symbols);
844
+ const result = {
845
+ version: 1,
846
+ range,
847
+ producer: string2(data.producer),
848
+ status: status(data.status),
849
+ documents: { before: [], after: [] },
850
+ symbols: { before: [], after: [] },
851
+ relations: [],
852
+ unresolved: [],
853
+ limitations: list2(data.limitations, 100).map(string2)
854
+ };
855
+ const entries = /* @__PURE__ */ new Map();
856
+ const documentsBySide = { before: /* @__PURE__ */ new Map(), after: /* @__PURE__ */ new Map() };
857
+ const lineIndexes = /* @__PURE__ */ new Map();
858
+ const lineAt = (side, path, start) => {
859
+ const key = `${side}:${path}`;
860
+ let lines = lineIndexes.get(key);
861
+ if (!lines) {
862
+ const text = documentsBySide[side].get(path);
863
+ lines = [0];
864
+ for (let at = text.indexOf("\n"); at !== -1; at = text.indexOf("\n", at + 1)) lines.push(at + 1);
865
+ lineIndexes.set(key, lines);
866
+ }
867
+ let low = 0, high = lines.length;
868
+ while (low < high) {
869
+ const mid = Math.floor((low + high) / 2);
870
+ if (lines[mid] <= start) low = mid + 1;
871
+ else high = mid;
872
+ }
873
+ return low;
874
+ };
875
+ let sourceBytes = 0, symbolBytes = 0;
876
+ for (const side of ["before", "after"]) {
877
+ const texts = /* @__PURE__ */ new Map();
878
+ const oid = side === "before" ? range.baseOid : range.headOid;
879
+ for (const value of list2(documents[side], 128)) {
880
+ const doc = object(value), name = filePath(doc.path), expectedDigest = hash2(doc.digest);
881
+ const documentStatus = string2(doc.status);
882
+ if (!["complete", "invalid", "unsupported"].includes(documentStatus) || texts.has(name)) throw new Error("Invalid or duplicate correspondence document.");
883
+ const bytes = (0, import_child_process.execFileSync)("git", ["show", `${oid}:${name}`], { cwd: repoRoot, timeout: 1e4, maxBuffer: 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
884
+ const text = bytes.toString("utf8");
885
+ sourceBytes += bytes.length;
886
+ if (sourceBytes > MAX_SOURCE_BYTES || !Buffer.from(text, "utf8").equals(bytes) || digest2(text) !== expectedDigest) {
887
+ throw new Error(`Correspondence source digest mismatch or source budget exceeded: ${side}/${name}`);
888
+ }
889
+ texts.set(name, { text, status: documentStatus });
890
+ documentsBySide[side].set(name, text);
891
+ result.documents[side].push({ path: name, digest: expectedDigest, status: documentStatus, text });
892
+ }
893
+ for (const value of list2(symbols[side])) {
894
+ const entry = object(value), name = filePath(entry.path), doc = texts.get(name);
895
+ const id = string2(entry.id), start = integer(entry.start), end = integer(entry.end);
896
+ if (!doc || start >= end || end > doc.text.length || entries.has(id)) throw new Error("Invalid correspondence symbol ownership/range/ID.");
897
+ const text = doc.text.slice(start, end);
898
+ symbolBytes += Buffer.byteLength(text);
899
+ if (symbolBytes > MAX_SOURCE_BYTES || digest2(text) !== hash2(entry.digest)) throw new Error(`Correspondence symbol digest mismatch or budget exceeded: ${name}`);
900
+ const symbol = {
901
+ id,
902
+ path: name,
903
+ name: string2(entry.name),
904
+ kind: string2(entry.kind),
905
+ start,
906
+ end,
907
+ digest: digest2(text),
908
+ text,
909
+ startLine: lineAt(side, name, start),
910
+ endLine: lineAt(side, name, end - 1)
911
+ };
912
+ entries.set(id, { side, symbol, documentStatus: doc.status });
913
+ result.symbols[side].push(symbol);
914
+ }
915
+ }
916
+ const ids = /* @__PURE__ */ new Set(), sources2 = /* @__PURE__ */ new Set(), destinations = /* @__PURE__ */ new Set();
917
+ for (const value of list2(data.relations)) {
918
+ const relation = object(value), id = string2(relation.id), sourceId = string2(relation.sourceId), destinationId = string2(relation.destinationId);
919
+ const source = entries.get(sourceId), destination = entries.get(destinationId);
920
+ if (ids.has(id) || sources2.has(sourceId) || destinations.has(destinationId)) throw new Error("Conflicting correspondence continuations; preserve them as unresolved candidates.");
921
+ if (source?.side !== "before" || destination?.side !== "after" || source.documentStatus !== "complete" || destination.documentStatus !== "complete") {
922
+ throw new Error("Correspondence endpoints must resolve to valid, parsed before/after documents.");
923
+ }
924
+ ids.add(id);
925
+ sources2.add(sourceId);
926
+ destinations.add(destinationId);
927
+ const producers = list2(relation.producers, 10).map(string2);
928
+ if (relation.coverage !== void 0 && relation.coverage !== "complete") throw new Error("Invalid correspondence relation coverage.");
929
+ const covered = result.status === "partial" && relation.coverage === "complete" && producers.some((producer) => ["refactoring-miner", "bygone-strand"].includes(producer));
930
+ if (result.status !== "complete" && !covered && !(producers.includes("exact-declaration") && source.symbol.text === destination.symbol.text)) continue;
931
+ const a = source.symbol, b = destination.symbol;
932
+ result.relations.push({
933
+ id,
934
+ sourceId,
935
+ destinationId,
936
+ producers,
937
+ pathChanged: a.path !== b.path,
938
+ nameChanged: a.name !== b.name,
939
+ ...relation.coverage === "complete" ? { coverage: "complete" } : {},
940
+ rawEqual: a.text === b.text,
941
+ behavior: "unassessed",
942
+ localDiff: (diffLines(a.text, b.text, { timeout: 100, maxEditLength: 2e3 }) || [{ value: a.text, removed: true }, { value: b.text, added: true }]).map((part) => ({
943
+ value: part.value,
944
+ ...part.added ? { added: true } : {},
945
+ ...part.removed ? { removed: true } : {}
946
+ }))
947
+ });
948
+ }
949
+ for (const value of list2(data.unresolved)) {
950
+ const candidate = object(value);
951
+ const sourceIds = list2(candidate.sourceIds).map(string2), destinationIds = list2(candidate.destinationIds).map(string2);
952
+ if (!sourceIds.length && !destinationIds.length) throw new Error("Unresolved correspondence needs candidate endpoints.");
953
+ if (sourceIds.some((id) => entries.get(id)?.side !== "before") || destinationIds.some((id) => entries.get(id)?.side !== "after")) {
954
+ throw new Error("Unresolved correspondence references unknown endpoints.");
955
+ }
956
+ if (sourceIds.some((id) => sources2.has(id)) || destinationIds.some((id) => destinations.has(id))) throw new Error("Ambiguous endpoints cannot also be accepted continuations.");
957
+ result.unresolved.push({ reason: string2(candidate.reason), sourceIds, destinationIds });
958
+ }
959
+ if (data.diagnostics !== void 0) {
960
+ const diagnostics = object(data.diagnostics);
961
+ if (diagnostics.version !== 1) throw new Error("Unsupported correspondence diagnostics version.");
962
+ let locationCount = 0;
963
+ result.diagnostics = { version: 1, scope: list2(diagnostics.scope, 20).map(string2), records: list2(diagnostics.records).map((value) => {
964
+ const record2 = object(value), category = string2(record2.category);
965
+ if (!["outside", "syntax", "rejected", "unmatched", "relationship", "problem"].includes(category)) throw new Error("Invalid correspondence diagnostic category.");
966
+ return {
967
+ category,
968
+ reason: string2(record2.reason),
969
+ ...record2.message !== void 0 ? { message: string2(record2.message) } : {},
970
+ ...record2.producerId !== void 0 ? { producerId: string2(record2.producerId) } : {},
971
+ locations: list2(record2.locations, 100).map((value2) => {
972
+ if (++locationCount > 4e4) throw new Error("Correspondence diagnostic location budget exceeded.");
973
+ const location = object(value2), path = filePath(location.path);
974
+ const side = location.side;
975
+ if (side !== void 0 && side !== "before" && side !== "after") throw new Error("Invalid diagnostic revision side.");
976
+ const symbolId = location.symbolId === void 0 ? void 0 : string2(location.symbolId);
977
+ if (symbolId && (entries.get(symbolId)?.side !== side || entries.get(symbolId)?.symbol.path !== path)) throw new Error("Invalid diagnostic symbol reference.");
978
+ const start = location.start === void 0 ? symbolId ? entries.get(symbolId).symbol.start : void 0 : integer(location.start);
979
+ const text = side ? documentsBySide[side].get(path) : void 0;
980
+ if (start !== void 0 && (text === void 0 || start > text.length)) throw new Error("Invalid diagnostic source location.");
981
+ return {
982
+ path,
983
+ ...side ? { side } : {},
984
+ ...symbolId ? { symbolId } : {},
985
+ ...start !== void 0 ? { start, line: lineAt(side, path, start) } : {},
986
+ ...location.reason !== void 0 ? { reason: string2(location.reason) } : {}
987
+ };
988
+ })
989
+ };
990
+ }) };
991
+ }
992
+ return result;
993
+ }
994
+ function loadChangeCorrespondence(repoRoot, file, range) {
995
+ return validateChangeCorrespondence(repoRoot, readCorrespondenceJson(file), range);
996
+ }
997
+
998
+ // src/reviewWorkspace.ts
999
+ var MAX_BYTES2 = 16 * 1024 * 1024;
1000
+ function readEvidenceJson(file) {
1001
+ if ((0, import_fs2.statSync)(file).size > MAX_BYTES2) throw new Error("Evidence file exceeds 16 MiB");
1002
+ return JSON.parse((0, import_fs2.readFileSync)(file, "utf8"));
1003
+ }
1004
+ function git(root, args, deadline) {
1005
+ const remaining = deadline === void 0 ? 1e4 : deadline - Date.now();
1006
+ if (remaining <= 0) throw new Error("Dependency source validation time budget exhausted");
1007
+ return (0, import_child_process2.execFileSync)("git", ["--no-replace-objects", "-C", root, ...args], { timeout: Math.min(1e4, remaining), maxBuffer: MAX_BYTES2, stdio: ["ignore", "pipe", "pipe"] });
1008
+ }
1009
+ function commitOid(root, revision2, deadline) {
1010
+ const oid = git(root, ["rev-parse", "--verify", "--end-of-options", `${revision2}^{commit}`], deadline).toString().trim();
1011
+ if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(oid)) throw new Error("Expected a Git commit");
1012
+ return oid;
1013
+ }
1014
+ function repositoryRoot(directory) {
1015
+ return git(directory, ["rev-parse", "--show-toplevel"]).toString().trim();
1016
+ }
1017
+ function readPinnedFile(root, revision2, file, deadline) {
1018
+ (0, import_source_references2.parseSourceRef)({ revision: revision2, path: file, start: 0, end: 1, digest: "0".repeat(64) });
1019
+ if (commitOid(root, revision2, deadline) !== revision2) throw new Error("Evidence requires a full immutable commit ID");
1020
+ const entry = git(root, ["ls-tree", "-z", revision2, "--", `:(literal)${file}`], deadline).toString();
1021
+ if (!entry) return void 0;
1022
+ const match = /^(100644|100755) blob ([a-f0-9]+)\t([^\0]+)\0$/.exec(entry);
1023
+ if (!match || match[3] !== file) throw new Error(`Evidence is not a regular file: ${file}`);
1024
+ const bytes = git(root, ["cat-file", "blob", match[2]], deadline);
1025
+ if (bytes.length > 1024 * 1024 || bytes.includes(0)) throw new Error(`Evidence requires text up to 1 MiB: ${file}`);
1026
+ return { text: new import_util.TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes), blob: match[2] };
1027
+ }
1028
+ function readSnapshot(root, repository, revision2, paths) {
1029
+ const files = /* @__PURE__ */ Object.create(null);
1030
+ let bytes = 0;
1031
+ for (const file of new Set(paths)) {
1032
+ const source = readPinnedFile(root, revision2, file);
1033
+ if (source) {
1034
+ bytes += Buffer.byteLength(source.text);
1035
+ if (bytes > MAX_BYTES2) throw new Error("Review source budget exceeds 16 MiB");
1036
+ files[file] = source.text;
1037
+ }
1038
+ }
1039
+ return { repository, revision: revision2, files };
1040
+ }
1041
+ function changedPathGroups(root, base, head) {
1042
+ const records = git(root, ["diff", "--name-status", "--find-renames", "-z", base, head, "--"]).toString().split("\0");
1043
+ const groups = [];
1044
+ for (let i = 0; i < records.length && records[i]; ) {
1045
+ const status2 = records[i++], before = records[i++];
1046
+ groups.push(status2.startsWith("R") ? [before, records[i++]] : [before]);
1047
+ }
1048
+ if (groups.reduce((count, group) => count + group.length, 0) > 2e3) throw new Error("Review exceeds 2000 changed paths");
1049
+ return groups;
1050
+ }
1051
+ function comparisonPaths(root, base, head, file) {
1052
+ const records = git(root, ["diff", "--name-status", "--find-renames", "-z", base, ...head ? [head] : [], "--"]).toString().split("\0");
1053
+ for (let i = 0; i < records.length && records[i]; ) {
1054
+ const status2 = records[i++], before = records[i++];
1055
+ const after = status2.startsWith("R") || status2.startsWith("C") ? records[i++] : before;
1056
+ if (status2.startsWith("R") && (file === before || file === after)) return { before, after };
1057
+ }
1058
+ return { before: file, after: file };
1059
+ }
1060
+ function inspectWorkspace(root, review) {
1061
+ const paths = review.links.map((link) => link.ref.path);
1062
+ const base = readSnapshot(root, review.repository, review.base, paths);
1063
+ const head = readSnapshot(root, review.repository, review.head, paths);
1064
+ const result = inspect(review, base, head);
1065
+ const covered = new Set(result.requirements.flatMap((r) => r.links.filter((l) => l.resolution.status === "valid").map((l) => l.ref.path)));
1066
+ return {
1067
+ ...result,
1068
+ changedPathsWithoutLinks: changedPathGroups(root, review.base, review.head).filter((group) => !group.some((file) => covered.has(file))).flat(),
1069
+ checkoutHead: commitOid(root, "HEAD"),
1070
+ dirtyWorkingTreeExcluded: true
1071
+ };
1072
+ }
1073
+ function loadReview(root, file) {
1074
+ const review = parseReview(readEvidenceJson(file));
1075
+ inspectWorkspace(root, review);
1076
+ return review;
1077
+ }
1078
+ function refreshWorkspace(root, review, head, correspondence, requirements = review.requirements) {
1079
+ const revision2 = commitOid(root, head);
1080
+ try {
1081
+ git(root, ["merge-base", "--is-ancestor", review.head, revision2]);
1082
+ } catch {
1083
+ throw new Error("Refresh requires a descendant of the captured head; import a new review for a rewritten or unrelated comparison.");
1084
+ }
1085
+ const artifact = correspondence ? loadChangeCorrespondence(root, correspondence, { baseOid: review.head, headOid: revision2 }) : void 0;
1086
+ const evidence = artifact ? correspondenceEvidence(artifact, ["Explicitly supplied correspondence artifact"]) : void 0;
1087
+ const beforePaths = [...review.links.map((l) => l.ref.path), ...evidence?.pairs.map((p) => p.before.path) ?? []];
1088
+ const afterPaths = [...review.links.map((l) => l.ref.path), ...evidence?.pairs.map((p) => p.after.path) ?? []];
1089
+ return refresh(
1090
+ review,
1091
+ readSnapshot(root, review.repository, review.head, beforePaths),
1092
+ readSnapshot(root, review.repository, revision2, afterPaths),
1093
+ evidence,
1094
+ requirements
1095
+ );
1096
+ }
1097
+ function writeReview(file, review) {
1098
+ (0, import_fs2.writeFileSync)((0, import_path.resolve)(file), JSON.stringify(parseReview(review), null, 2) + "\n", { flag: "wx" });
1099
+ }
1100
+ function sourceText(root, ref) {
1101
+ const file = readPinnedFile(root, ref.revision, ref.path);
1102
+ if (!file || (0, import_source_references2.resolveSourceRef)(ref, { revision: ref.revision, files: { [ref.path]: file.text } }).status !== "valid") throw new Error("Pinned evidence no longer validates");
1103
+ return file.text;
1104
+ }
1105
+ function lineReference(root, revision2, file, start, end) {
1106
+ const source = readPinnedFile(root, revision2, file);
1107
+ if (!source || !Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < start) throw new Error("Invalid line anchor");
1108
+ const offsets = [0];
1109
+ for (let i = 0; i < source.text.length; i++) if (source.text[i] === "\n" && i + 1 < source.text.length) offsets.push(i + 1);
1110
+ if (end > offsets.length) throw new Error("Line anchor exceeds pinned file");
1111
+ return (0, import_source_references2.pinSourceRef)({ revision: revision2, files: { [file]: source.text } }, file, offsets[start - 1], offsets[end] ?? source.text.length);
1112
+ }
1113
+ // Annotate the CommonJS export names for ESM import in node:
1114
+ 0 && (module.exports = {
1115
+ commitOid,
1116
+ comparisonPaths,
1117
+ git,
1118
+ inspectWorkspace,
1119
+ lineReference,
1120
+ loadReview,
1121
+ readEvidenceJson,
1122
+ readPinnedFile,
1123
+ readSnapshot,
1124
+ refreshWorkspace,
1125
+ repositoryRoot,
1126
+ sourceText,
1127
+ writeReview
1128
+ });
1129
+ //# sourceMappingURL=reviewWorkspace.js.map