@davmash/bygone 0.9.8 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -1
- package/README.md +74 -1
- package/cli/commandSpec.js +2 -0
- package/cli/completions.js +3 -3
- package/cli/present.js +28 -10
- package/cli/tour.js +22 -1
- package/cli/tourExport.js +147 -0
- package/cli/tourFile.js +109 -4
- package/cli/tourHistory.js +31 -1
- package/completions/_bygone +1 -1
- package/completions/bygone +1 -1
- package/completions/bygone.fish +1 -1
- package/docs/change-tour-format.md +85 -68
- package/docs/explore.md +14 -3
- package/docs/generating-change-tours.md +84 -46
- package/docs/present.md +103 -14
- package/docs/product-surface.md +19 -8
- package/docs/sharing-tours.md +95 -0
- package/docs/visual-walkthrough/README.md +89 -0
- package/docs/visual-walkthrough/build-tour.mjs +54 -0
- package/docs/visual-walkthrough/capture-desktop.cjs +42 -0
- package/docs/visual-walkthrough/capture-desktop.mjs +21 -0
- package/docs/visual-walkthrough/captures.md +49 -0
- package/docs/visual-walkthrough/create-demo.mjs +57 -0
- package/docs/visual-walkthrough/deconstructed-demo.bygone +100 -0
- package/docs/visual-walkthrough/images/01-two-files.png +0 -0
- package/docs/visual-walkthrough/images/02-many-panels.png +0 -0
- package/docs/visual-walkthrough/images/03-directories.png +0 -0
- package/docs/visual-walkthrough/images/04-search.png +0 -0
- package/docs/visual-walkthrough/images/05-repository-history.png +0 -0
- package/docs/visual-walkthrough/images/06-file-history.png +0 -0
- package/docs/visual-walkthrough/images/07-branch-review.png +0 -0
- package/docs/visual-walkthrough/images/08-tour-reader.png +0 -0
- package/docs/visual-walkthrough/images/09-focused-step.png +0 -0
- package/docs/visual-walkthrough/images/10-deconstructed.png +0 -0
- package/docs/visual-walkthrough/images/11-exploration.png +0 -0
- package/docs/visual-walkthrough/images/12-authoring.png +0 -0
- package/docs/visual-walkthrough/images/13-export.png +0 -0
- package/docs/visual-walkthrough/images/14-vscode.png +0 -0
- package/docs/visual-walkthrough/images/15-cli.png +0 -0
- package/docs/visual-walkthrough/narrative.md +134 -0
- package/docs/visual-walkthrough/outline.md +52 -0
- package/docs/visual-walkthrough/walkthrough.bygone +340 -0
- package/docs/walkthrough.md +6 -101
- package/examples/bygone-history.bygone +11 -44
- package/examples/marker-focus.bygone +116 -0
- package/examples/navigation-lab.bygone.yaml +401 -336
- package/examples/tour-orientation.bygone +349 -0
- package/media/diff.worker.js +13 -3
- package/media/webview.css +1 -1
- package/media/webview.js +394 -319
- package/out/changeTour.js +865 -397
- package/out/deepLink.js +167 -0
- package/out/historyDirectory.js +66 -0
- package/out/standalone-main.js +20514 -16821
- package/out/tourCoverage.js +850 -386
- package/out/tourDocument.js +97 -0
- package/package.json +1 -1
- package/schemas/change-tour-source.schema.json +91 -64
- package/skills/pr-tour-guide/SKILL.md +144 -128
- package/skills/pr-tour-guide/references/expectations-and-feedback.md +57 -0
- package/standalone/index.html +10 -25
- package/web/export-bootstrap.js +62 -0
- package/web/export-licenses.txt +78 -0
- package/web/export-runtime.json +12 -0
- package/web/index.html +54 -47
- package/web/presenter.css +332 -178
- package/web/web-host.js +239 -137
package/out/changeTour.js
CHANGED
|
@@ -41,9 +41,49 @@ __export(changeTour_exports, {
|
|
|
41
41
|
materializeChangeUnits: () => materializeChangeUnits,
|
|
42
42
|
parseChangeTourManifest: () => parseChangeTourManifest,
|
|
43
43
|
parseChangeTourSource: () => parseChangeTourSource,
|
|
44
|
-
parseChangeTourStory: () => parseChangeTourStory
|
|
44
|
+
parseChangeTourStory: () => parseChangeTourStory,
|
|
45
|
+
pinTourSource: () => pinTourSource
|
|
45
46
|
});
|
|
46
47
|
module.exports = __toCommonJS(changeTour_exports);
|
|
48
|
+
|
|
49
|
+
// src/tourImage.ts
|
|
50
|
+
var MAX_TOUR_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
51
|
+
function record(value) {
|
|
52
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
53
|
+
}
|
|
54
|
+
function validPath(value) {
|
|
55
|
+
return typeof value === "string" && value.length > 0 && !/[\\\x00-\x1f:]/.test(value) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
56
|
+
}
|
|
57
|
+
function validateTourImageSource(value, label) {
|
|
58
|
+
if (value === void 0) return;
|
|
59
|
+
if (!record(value) || Object.keys(value).some((key) => !["file", "revision", "alt"].includes(key)) || !validPath(value.file) || !["base", "head"].includes(String(value.revision)) || typeof value.alt !== "string" || !value.alt.trim()) {
|
|
60
|
+
throw new Error(`${label} must contain a repository-relative file, base/head revision, and non-empty alt text.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function validateTourImage(value, label) {
|
|
64
|
+
if (value === void 0) return;
|
|
65
|
+
if (!record(value) || !validPath(value.path) || typeof value.alt !== "string" || !value.alt.trim() || typeof value.revision !== "string" || !/^[a-f0-9]{40,64}$/.test(value.revision) || typeof value.dataUrl !== "string" || !value.dataUrl.startsWith("data:image/png;base64,")) {
|
|
66
|
+
throw new Error(`${label} must contain pinned, embedded PNG evidence and alt text.`);
|
|
67
|
+
}
|
|
68
|
+
const base64 = value.dataUrl.slice(22);
|
|
69
|
+
const decodedBytes = base64.length / 4 * 3 - (base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0);
|
|
70
|
+
if (decodedBytes > MAX_TOUR_IMAGE_BYTES || base64.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(base64)) throw new Error(`${label} must be a PNG of at most 8 MiB.`);
|
|
71
|
+
const header = Uint8Array.from(atob(base64.slice(0, 44)), (character) => character.charCodeAt(0));
|
|
72
|
+
validatePngHeader(header, label);
|
|
73
|
+
}
|
|
74
|
+
function validatePngHeader(bytes, label) {
|
|
75
|
+
const signature = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82];
|
|
76
|
+
if (bytes.length < 33 || signature.some((byte, index) => bytes[index] !== byte)) {
|
|
77
|
+
throw new Error(`${label} must be a PNG with an IHDR header.`);
|
|
78
|
+
}
|
|
79
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
80
|
+
const width = view.getUint32(16), height = view.getUint32(20);
|
|
81
|
+
if (!width || !height || width > 16384 || height > 16384 || width * height > 64 * 1024 * 1024) {
|
|
82
|
+
throw new Error(`${label} exceeds the PNG dimension limit.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/changeTour.ts
|
|
47
87
|
var import_child_process4 = require("child_process");
|
|
48
88
|
|
|
49
89
|
// src/gitComparison.ts
|
|
@@ -160,8 +200,8 @@ function listBranchCommits(repoRoot, mergeBaseOid, headOid, runGit3 = createGitR
|
|
|
160
200
|
"--format=%H%x00%h%x00%cI%x00%s%x00%P%x1e",
|
|
161
201
|
`${mergeBaseOid}..${headOid}`
|
|
162
202
|
], repoRoot);
|
|
163
|
-
return output.split("").map((
|
|
164
|
-
const [oid = "", shortOid = "", timestamp = "", summary = "", parents = ""] =
|
|
203
|
+
return output.split("").map((record2) => record2.replace(/^\n+|\n+$/g, "")).filter(Boolean).map((record2) => {
|
|
204
|
+
const [oid = "", shortOid = "", timestamp = "", summary = "", parents = ""] = record2.split("\0");
|
|
165
205
|
return {
|
|
166
206
|
oid,
|
|
167
207
|
shortOid,
|
|
@@ -234,18 +274,110 @@ function readGitMaxBuffer() {
|
|
|
234
274
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_GIT_MAX_BUFFER_BYTES;
|
|
235
275
|
}
|
|
236
276
|
|
|
277
|
+
// src/tourFraming.ts
|
|
278
|
+
function validateTourFraming(value, version, path) {
|
|
279
|
+
for (const key of ["opening", "conclusion"]) {
|
|
280
|
+
const passage = value[key];
|
|
281
|
+
if (passage === void 0) continue;
|
|
282
|
+
if (Number(version) < 5) throw new Error(`${path}.${key} requires version 5.`);
|
|
283
|
+
if (!passage || typeof passage !== "object" || Array.isArray(passage)) {
|
|
284
|
+
throw new Error(`${path}.${key} must be an object.`);
|
|
285
|
+
}
|
|
286
|
+
const fields = passage;
|
|
287
|
+
for (const field of Object.keys(fields)) {
|
|
288
|
+
if (!["title", "summary", "bullets"].includes(field)) throw new Error(`${path}.${key}.${field} is unsupported.`);
|
|
289
|
+
}
|
|
290
|
+
for (const field of ["title", "summary"]) requireProse(fields[field], `${path}.${key}.${field}`);
|
|
291
|
+
if (fields.bullets !== void 0) {
|
|
292
|
+
if (!Array.isArray(fields.bullets)) throw new Error(`${path}.${key}.bullets must be an array.`);
|
|
293
|
+
fields.bullets.forEach((bullet, index) => requireProse(bullet, `${path}.${key}.bullets[${index}]`));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function validateOverviewPurpose(value, version, path) {
|
|
298
|
+
if (value === void 0) return;
|
|
299
|
+
if (version < 5) {
|
|
300
|
+
if (value && typeof value === "object" && "purpose" in value) throw new Error(`${path}.purpose requires version 5.`);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
requireProse(value == null ? void 0 : value.purpose, `${path}.purpose`);
|
|
304
|
+
}
|
|
305
|
+
function requireProse(value, path) {
|
|
306
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${path} must be a non-empty string.`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/tourDetails.ts
|
|
310
|
+
function validateChangeTourDetails(value, path) {
|
|
311
|
+
if (value === void 0) return;
|
|
312
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
313
|
+
requireOnlyKeys(value, ["audience"], path);
|
|
314
|
+
validateAudience(value.audience, `${path}.audience`);
|
|
315
|
+
}
|
|
316
|
+
function validateAudience(value, path) {
|
|
317
|
+
if (value === void 0) return;
|
|
318
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
319
|
+
requireOnlyKeys(value, [
|
|
320
|
+
"codebase",
|
|
321
|
+
"language",
|
|
322
|
+
"domain",
|
|
323
|
+
"readingDemand",
|
|
324
|
+
"purpose",
|
|
325
|
+
"scope",
|
|
326
|
+
"estimatedMinutes"
|
|
327
|
+
], path);
|
|
328
|
+
if (typeof value.codebase !== "string" || !["none", "some", "maintainer"].includes(value.codebase)) {
|
|
329
|
+
throw new Error(`${path}.codebase must be none, some, or maintainer.`);
|
|
330
|
+
}
|
|
331
|
+
if (typeof value.language !== "string" || !["none", "basic", "fluent", "advanced"].includes(value.language)) {
|
|
332
|
+
throw new Error(`${path}.language must be none, basic, fluent, or advanced.`);
|
|
333
|
+
}
|
|
334
|
+
if (value.domain !== void 0) requireStringArray(value.domain, `${path}.domain`);
|
|
335
|
+
if (typeof value.readingDemand !== "string" || !["general", "technical", "specialist"].includes(value.readingDemand)) {
|
|
336
|
+
throw new Error(`${path}.readingDemand must be general, technical, or specialist.`);
|
|
337
|
+
}
|
|
338
|
+
if (typeof value.purpose !== "string" || !["overview", "explanation", "review"].includes(value.purpose)) {
|
|
339
|
+
throw new Error(`${path}.purpose must be overview, explanation, or review.`);
|
|
340
|
+
}
|
|
341
|
+
requireString(value.scope, `${path}.scope`);
|
|
342
|
+
if (value.estimatedMinutes !== void 0 && (typeof value.estimatedMinutes !== "number" || !Number.isFinite(value.estimatedMinutes) || value.estimatedMinutes <= 0)) {
|
|
343
|
+
throw new Error(`${path}.estimatedMinutes must be a finite positive number.`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function isRecord(value) {
|
|
347
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
348
|
+
}
|
|
349
|
+
function requireString(value, path) {
|
|
350
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
351
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function requireStringArray(value, path) {
|
|
355
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) {
|
|
356
|
+
throw new Error(`${path} must be an array of strings.`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function requireOnlyKeys(value, allowed, path) {
|
|
360
|
+
const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
361
|
+
if (unexpected.length > 0) {
|
|
362
|
+
throw new Error(`${path} contains unknown field${unexpected.length === 1 ? "" : "s"}: ${unexpected.join(", ")}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
237
366
|
// src/changeTourSource.ts
|
|
238
|
-
var CHANGE_TOUR_SOURCE_VERSION =
|
|
367
|
+
var CHANGE_TOUR_SOURCE_VERSION = 5;
|
|
239
368
|
function parseChangeTourSource(value) {
|
|
240
|
-
if (
|
|
369
|
+
if (isRecord2(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_SOURCE_VERSION) {
|
|
241
370
|
throw new Error(
|
|
242
371
|
`This tour uses source format version ${value.version}, but this version of Bygone supports up to version ${CHANGE_TOUR_SOURCE_VERSION}. Upgrade Bygone to open it.`
|
|
243
372
|
);
|
|
244
373
|
}
|
|
245
|
-
if (
|
|
374
|
+
if (isRecord2(value) && value.version === 3) {
|
|
375
|
+
throw new Error("Tour source format version 3 is retired. Remove the review block, set source version to 4, and recompile the tour.");
|
|
376
|
+
}
|
|
377
|
+
if (!isRecord2(value) || value.version !== 1 && value.version !== 2 && value.version !== 4 && value.version !== CHANGE_TOUR_SOURCE_VERSION) {
|
|
246
378
|
throw new Error("Unsupported or missing change-tour source version.");
|
|
247
379
|
}
|
|
248
|
-
|
|
380
|
+
requireOnlyKeys2(value, [
|
|
249
381
|
"version",
|
|
250
382
|
"title",
|
|
251
383
|
"windowTitle",
|
|
@@ -256,62 +388,65 @@ function parseChangeTourSource(value) {
|
|
|
256
388
|
"chapters",
|
|
257
389
|
"tours",
|
|
258
390
|
"coverage",
|
|
259
|
-
"
|
|
391
|
+
"opening",
|
|
392
|
+
"conclusion",
|
|
393
|
+
"details"
|
|
260
394
|
], "source");
|
|
395
|
+
validateTourFraming(value, value.version, "source");
|
|
261
396
|
optionalString(value.title, "title");
|
|
262
397
|
optionalString(value.windowTitle, "windowTitle");
|
|
263
398
|
optionalString(value.sourceUrl, "sourceUrl");
|
|
264
399
|
if (value.range !== void 0) {
|
|
265
|
-
if (!
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
400
|
+
if (!isRecord2(value.range)) throw new Error("range must be an object.");
|
|
401
|
+
requireOnlyKeys2(value.range, ["base", "head"], "range");
|
|
402
|
+
requireString2(value.range.base, "range.base");
|
|
403
|
+
requireString2(value.range.head, "range.head");
|
|
269
404
|
}
|
|
270
405
|
if (value.coverage !== void 0) {
|
|
271
|
-
if (!
|
|
406
|
+
if (!isRecord2(value.coverage) || !Array.isArray(value.coverage.exclusions)) {
|
|
272
407
|
throw new Error("coverage must contain an exclusions array.");
|
|
273
408
|
}
|
|
274
|
-
|
|
409
|
+
requireOnlyKeys2(value.coverage, ["exclusions"], "coverage");
|
|
275
410
|
for (const [index, exclusion] of value.coverage.exclusions.entries()) {
|
|
276
411
|
const exclusionPath = `coverage.exclusions[${index}]`;
|
|
277
|
-
if (!
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (exclusion.hunks !== void 0)
|
|
412
|
+
if (!isRecord2(exclusion)) throw new Error(`${exclusionPath} must be an object.`);
|
|
413
|
+
requireOnlyKeys2(exclusion, ["path", "hunks", "reason"], exclusionPath);
|
|
414
|
+
requireString2(exclusion.path, `${exclusionPath}.path`);
|
|
415
|
+
requireString2(exclusion.reason, `${exclusionPath}.reason`);
|
|
416
|
+
if (exclusion.hunks !== void 0) requireStringArray2(exclusion.hunks, `${exclusionPath}.hunks`);
|
|
282
417
|
}
|
|
283
418
|
}
|
|
284
|
-
if (!
|
|
419
|
+
if (!isRecord2(value.anchors)) {
|
|
285
420
|
throw new Error("anchors must be an object.");
|
|
286
421
|
}
|
|
287
422
|
for (const [id, anchor] of Object.entries(value.anchors)) {
|
|
288
|
-
if (!
|
|
423
|
+
if (!isRecord2(anchor)) {
|
|
289
424
|
throw new Error(`anchors.${id} must be an object.`);
|
|
290
425
|
}
|
|
291
|
-
|
|
292
|
-
|
|
426
|
+
requireOnlyKeys2(anchor, ["file", "revision", "contains", "occurrence"], `anchors.${id}`);
|
|
427
|
+
requireString2(anchor.file, `anchors.${id}.file`);
|
|
293
428
|
if (anchor.revision !== "base" && anchor.revision !== "head") {
|
|
294
429
|
throw new Error(`anchors.${id}.revision must be base or head.`);
|
|
295
430
|
}
|
|
296
|
-
|
|
431
|
+
requireString2(anchor.contains, `anchors.${id}.contains`);
|
|
297
432
|
if (anchor.occurrence !== void 0 && (!Number.isInteger(anchor.occurrence) || Number(anchor.occurrence) < 1)) {
|
|
298
433
|
throw new Error(`anchors.${id}.occurrence must be a positive integer.`);
|
|
299
434
|
}
|
|
300
435
|
}
|
|
301
|
-
const rawConnections = Array.isArray(value.connections) ? value.connections :
|
|
436
|
+
const rawConnections = Array.isArray(value.connections) ? value.connections : isRecord2(value.connections) ? Object.entries(value.connections).map(([id, connection]) => isRecord2(connection) ? { id, ...connection } : connection) : null;
|
|
302
437
|
if (!rawConnections || !Array.isArray(value.chapters) || value.chapters.length === 0) {
|
|
303
438
|
throw new Error("connections must be an array or object, and chapters must be a non-empty array.");
|
|
304
439
|
}
|
|
305
440
|
const connectionIds = /* @__PURE__ */ new Set();
|
|
306
441
|
for (const [index, connection] of rawConnections.entries()) {
|
|
307
|
-
if (!
|
|
442
|
+
if (!isRecord2(connection)) {
|
|
308
443
|
throw new Error(`connections[${index}] must be an object.`);
|
|
309
444
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
445
|
+
requireOnlyKeys2(connection, ["id", "from", "to", "label"], `connections[${index}]`);
|
|
446
|
+
requireString2(connection.id, `connections[${index}].id`);
|
|
447
|
+
requireString2(connection.from, `connections[${index}].from`);
|
|
448
|
+
requireString2(connection.to, `connections[${index}].to`);
|
|
449
|
+
requireString2(connection.label, `connections[${index}].label`);
|
|
315
450
|
const { id, from, to } = connection;
|
|
316
451
|
if (!value.anchors[from] || !value.anchors[to]) {
|
|
317
452
|
throw new Error(`Connection ${id} references an unknown anchor.`);
|
|
@@ -321,7 +456,7 @@ function parseChangeTourSource(value) {
|
|
|
321
456
|
}
|
|
322
457
|
connectionIds.add(id);
|
|
323
458
|
}
|
|
324
|
-
|
|
459
|
+
validateSourceChapters(
|
|
325
460
|
value.chapters,
|
|
326
461
|
"chapters",
|
|
327
462
|
value.version,
|
|
@@ -329,10 +464,14 @@ function parseChangeTourSource(value) {
|
|
|
329
464
|
connectionIds,
|
|
330
465
|
"root"
|
|
331
466
|
);
|
|
467
|
+
if (value.details !== void 0) {
|
|
468
|
+
if (value.version < 5) throw new Error("source.details requires version 5.");
|
|
469
|
+
validateChangeTourDetails(value.details, "details");
|
|
470
|
+
}
|
|
332
471
|
if (value.tours !== void 0) {
|
|
333
|
-
if (value.version
|
|
334
|
-
if (!
|
|
335
|
-
|
|
472
|
+
if (value.version < 4) throw new Error("Independent tours require version 4.");
|
|
473
|
+
if (!isRecord2(value.tours)) throw new Error("tours must be an object.");
|
|
474
|
+
requireOnlyKeys2(value.tours, ["historical", "deconstructed"], "tours");
|
|
336
475
|
if (value.tours.historical === void 0 && value.tours.deconstructed === void 0) {
|
|
337
476
|
throw new Error("tours must contain a historical or deconstructed tour.");
|
|
338
477
|
}
|
|
@@ -340,15 +479,16 @@ function parseChangeTourSource(value) {
|
|
|
340
479
|
const tour = value.tours[mode];
|
|
341
480
|
if (tour === void 0) continue;
|
|
342
481
|
const path = `tours.${mode}`;
|
|
343
|
-
if (!
|
|
344
|
-
|
|
482
|
+
if (!isRecord2(tour)) throw new Error(`${path} must be an object.`);
|
|
483
|
+
requireOnlyKeys2(tour, ["chapters", "opening", "conclusion", "details"], path);
|
|
484
|
+
validateTourFraming(tour, value.version, path);
|
|
345
485
|
validateSourceChapters(tour.chapters, `${path}.chapters`, value.version, value.anchors, connectionIds, mode);
|
|
486
|
+
if (tour.details !== void 0) {
|
|
487
|
+
if (value.version < 5) throw new Error(`${path}.details requires source version 5.`);
|
|
488
|
+
validateChangeTourDetails(tour.details, `${path}.details`);
|
|
489
|
+
}
|
|
346
490
|
}
|
|
347
491
|
}
|
|
348
|
-
if (value.review !== void 0) {
|
|
349
|
-
if (value.version !== 3) throw new Error("Review notes require version 3.");
|
|
350
|
-
validateChangeTourReview(value.review, { authoredWalkthroughSteps });
|
|
351
|
-
}
|
|
352
492
|
return { ...value, connections: rawConnections };
|
|
353
493
|
}
|
|
354
494
|
function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, connectionIds, mode) {
|
|
@@ -357,27 +497,31 @@ function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, c
|
|
|
357
497
|
}
|
|
358
498
|
const chapterIds = /* @__PURE__ */ new Set();
|
|
359
499
|
const sceneIds = /* @__PURE__ */ new Set();
|
|
360
|
-
const authoredWalkthroughSteps = /* @__PURE__ */ new Map();
|
|
361
500
|
for (const [chapterIndex, chapter] of chaptersValue.entries()) {
|
|
362
501
|
const chapterPath = `${chaptersPath}[${chapterIndex}]`;
|
|
363
|
-
if (!
|
|
502
|
+
if (!isRecord2(chapter) || !Array.isArray(chapter.scenes) || chapter.scenes.length === 0) {
|
|
364
503
|
throw new Error(`${chapterPath} must contain a non-empty scenes array.`);
|
|
365
504
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
505
|
+
requireString2(chapter.id, `${chapterPath}.id`);
|
|
506
|
+
requireString2(chapter.title, `${chapterPath}.title`);
|
|
507
|
+
if (chapter.summary !== void 0) {
|
|
508
|
+
if (version < 5) throw new Error(`${chapterPath}.summary requires source version 5.`);
|
|
509
|
+
requireProse2(chapter.summary, `${chapterPath}.summary`);
|
|
510
|
+
}
|
|
511
|
+
requireOnlyKeys2(chapter, ["id", "title", "summary", "scenes"], chapterPath);
|
|
369
512
|
if (chapterIds.has(chapter.id)) throw new Error(`Duplicate chapter id: ${chapter.id}`);
|
|
370
513
|
chapterIds.add(chapter.id);
|
|
371
514
|
for (const [sceneIndex, scene] of chapter.scenes.entries()) {
|
|
372
515
|
const path = `${chapterPath}.scenes[${sceneIndex}]`;
|
|
373
|
-
if (!
|
|
374
|
-
|
|
375
|
-
|
|
516
|
+
if (!isRecord2(scene)) throw new Error(`${path} must be an object.`);
|
|
517
|
+
requireString2(scene.id, `${path}.id`);
|
|
518
|
+
requireString2(scene.title, `${path}.title`);
|
|
376
519
|
if (sceneIds.has(scene.id)) throw new Error(`Duplicate scene id: ${scene.id}`);
|
|
377
520
|
sceneIds.add(scene.id);
|
|
378
|
-
if (scene.overview !== void 0 && version
|
|
379
|
-
throw new Error(`${path}.overview requires version
|
|
521
|
+
if (scene.overview !== void 0 && version < 4) {
|
|
522
|
+
throw new Error(`${path}.overview requires version 4.`);
|
|
380
523
|
}
|
|
524
|
+
validateOverviewPurpose(scene.overview, version, `${path}.overview`);
|
|
381
525
|
validateNarrative(scene, path);
|
|
382
526
|
if (scene.kind === "deconstructed-diff") {
|
|
383
527
|
if (mode === "historical") {
|
|
@@ -393,9 +537,6 @@ function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, c
|
|
|
393
537
|
}
|
|
394
538
|
if (requireRealEvidence || scene.steps !== void 0) {
|
|
395
539
|
validateWalkthroughSteps(scene, path, anchors, connectionIds);
|
|
396
|
-
if (scene.steps !== void 0) {
|
|
397
|
-
authoredWalkthroughSteps.set(scene.id, collectStepIds(scene.steps));
|
|
398
|
-
}
|
|
399
540
|
}
|
|
400
541
|
continue;
|
|
401
542
|
}
|
|
@@ -412,22 +553,20 @@ function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, c
|
|
|
412
553
|
if (scene.kind !== void 0 && scene.kind !== "walkthrough") {
|
|
413
554
|
throw new Error(`${path}.kind must be walkthrough, stacked-diff, or deconstructed-diff.`);
|
|
414
555
|
}
|
|
415
|
-
|
|
556
|
+
requireOnlyKeys2(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "steps"], path);
|
|
416
557
|
validateSceneOverview(scene.overview, `${path}.overview`, { allowComparison: false });
|
|
417
558
|
validateWalkthroughSteps(scene, path, anchors, connectionIds);
|
|
418
|
-
authoredWalkthroughSteps.set(scene.id, collectStepIds(scene.steps));
|
|
419
559
|
}
|
|
420
560
|
}
|
|
421
|
-
if (mode === "deconstructed" && !chaptersValue.some((chapter) =>
|
|
561
|
+
if (mode === "deconstructed" && !chaptersValue.some((chapter) => isRecord2(chapter) && Array.isArray(chapter.scenes) && chapter.scenes.some((scene) => isRecord2(scene) && scene.kind === "deconstructed-diff"))) {
|
|
422
562
|
throw new Error(`${chaptersPath} must contain at least one deconstructed-diff scene.`);
|
|
423
563
|
}
|
|
424
|
-
return authoredWalkthroughSteps;
|
|
425
564
|
}
|
|
426
565
|
function stackedOverviewEndpointIds(scene, path) {
|
|
427
566
|
if (!Array.isArray(scene.stack)) {
|
|
428
567
|
throw new Error(`${path}.stack must be an array.`);
|
|
429
568
|
}
|
|
430
|
-
return scene.stack.filter(
|
|
569
|
+
return scene.stack.filter(isRecord2).map((entry) => typeof entry.id === "string" ? entry.id : "").filter((id) => id.length > 0);
|
|
431
570
|
}
|
|
432
571
|
function deconstructedOverviewEndpointIds(scene, path) {
|
|
433
572
|
if (!Array.isArray(scene.stages)) {
|
|
@@ -435,78 +574,11 @@ function deconstructedOverviewEndpointIds(scene, path) {
|
|
|
435
574
|
}
|
|
436
575
|
return [
|
|
437
576
|
"explanation-baseline",
|
|
438
|
-
...scene.stages.filter(
|
|
577
|
+
...scene.stages.filter(isRecord2).map((stage) => typeof stage.id === "string" ? `explanation-stage-${stage.id}` : "").filter((id) => id.length > 0)
|
|
439
578
|
];
|
|
440
579
|
}
|
|
441
|
-
function validateChangeTourReview(value, context = {}) {
|
|
442
|
-
if (!isRecord(value)) {
|
|
443
|
-
throw new Error("review must be an object.");
|
|
444
|
-
}
|
|
445
|
-
requireOnlyKeys(value, ["baseOid", "headOid", "items"], "review");
|
|
446
|
-
requireFullGitOid(value.baseOid, "review.baseOid");
|
|
447
|
-
requireFullGitOid(value.headOid, "review.headOid");
|
|
448
|
-
if (!Array.isArray(value.items) || value.items.length === 0) {
|
|
449
|
-
throw new Error("review.items must be a non-empty array.");
|
|
450
|
-
}
|
|
451
|
-
if (context.expectedRange) {
|
|
452
|
-
if (normalizeGitOid(value.baseOid) !== normalizeGitOid(context.expectedRange.baseOid)) {
|
|
453
|
-
throw new Error(
|
|
454
|
-
`Stale review: review.baseOid ${value.baseOid} does not match the resolved range.mergeBaseOid ${context.expectedRange.baseOid}.`
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
if (normalizeGitOid(value.headOid) !== normalizeGitOid(context.expectedRange.headOid)) {
|
|
458
|
-
throw new Error(
|
|
459
|
-
`Stale review: review.headOid ${value.headOid} does not match the resolved range.headOid ${context.expectedRange.headOid}.`
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
const itemIds = /* @__PURE__ */ new Set();
|
|
464
|
-
for (const [index, item] of value.items.entries()) {
|
|
465
|
-
const itemPath = `review.items[${index}]`;
|
|
466
|
-
if (!isRecord(item)) throw new Error(`${itemPath} must be an object.`);
|
|
467
|
-
requireOnlyKeys(item, ["id", "kind", "title", "body", "evidence", "nextCheck"], itemPath);
|
|
468
|
-
requireReviewString(item.id, `${itemPath}.id`);
|
|
469
|
-
if (itemIds.has(item.id)) throw new Error(`Duplicate review item id: ${item.id}`);
|
|
470
|
-
itemIds.add(item.id);
|
|
471
|
-
if (typeof item.kind !== "string" || !["concept", "boundary", "tradeoff", "question"].includes(item.kind)) {
|
|
472
|
-
throw new Error(`${itemPath}.kind must be concept, boundary, tradeoff, or question.`);
|
|
473
|
-
}
|
|
474
|
-
requireReviewString(item.title, `${itemPath}.title`);
|
|
475
|
-
requireReviewString(item.body, `${itemPath}.body`);
|
|
476
|
-
if (!Array.isArray(item.evidence)) {
|
|
477
|
-
throw new Error(`${itemPath}.evidence must be an array.`);
|
|
478
|
-
}
|
|
479
|
-
if (item.kind !== "question" && item.evidence.length === 0) {
|
|
480
|
-
throw new Error(`${itemPath}.evidence must contain at least one reference for claims.`);
|
|
481
|
-
}
|
|
482
|
-
if (item.kind === "question" && item.nextCheck === void 0) {
|
|
483
|
-
throw new Error(`${itemPath}.nextCheck must be a non-empty string for question items.`);
|
|
484
|
-
}
|
|
485
|
-
if (item.nextCheck !== void 0) requireReviewString(item.nextCheck, `${itemPath}.nextCheck`);
|
|
486
|
-
for (const [evidenceIndex, evidence] of item.evidence.entries()) {
|
|
487
|
-
const evidencePath = `${itemPath}.evidence[${evidenceIndex}]`;
|
|
488
|
-
if (!isRecord(evidence)) throw new Error(`${evidencePath} must be an object.`);
|
|
489
|
-
requireOnlyKeys(evidence, ["sceneId", "stepId"], evidencePath);
|
|
490
|
-
requireReviewString(evidence.sceneId, `${evidencePath}.sceneId`);
|
|
491
|
-
requireReviewString(evidence.stepId, `${evidencePath}.stepId`);
|
|
492
|
-
if (context.authoredWalkthroughSteps) {
|
|
493
|
-
const stepIds = context.authoredWalkthroughSteps.get(evidence.sceneId);
|
|
494
|
-
if (!stepIds) {
|
|
495
|
-
throw new Error(
|
|
496
|
-
`${evidencePath} references unknown authored walkthrough scene ${evidence.sceneId}.`
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
if (!stepIds.has(evidence.stepId)) {
|
|
500
|
-
throw new Error(
|
|
501
|
-
`${evidencePath} references unknown authored walkthrough step ${evidence.sceneId}/${evidence.stepId}.`
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
580
|
function validateDeconstructedScene(scene, path) {
|
|
509
|
-
|
|
581
|
+
requireOnlyKeys2(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "base", "target", "stack", "steps", "stages", "exclusions"], path);
|
|
510
582
|
optionalString(scene.base, `${path}.base`);
|
|
511
583
|
optionalString(scene.target, `${path}.target`);
|
|
512
584
|
if (!Array.isArray(scene.stages) || scene.stages.length === 0 || scene.stages.length > 12) {
|
|
@@ -515,11 +587,11 @@ function validateDeconstructedScene(scene, path) {
|
|
|
515
587
|
const stageIds = /* @__PURE__ */ new Set();
|
|
516
588
|
for (const [stageIndex, stage] of scene.stages.entries()) {
|
|
517
589
|
const stagePath = `${path}.stages[${stageIndex}]`;
|
|
518
|
-
if (!
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
590
|
+
if (!isRecord2(stage)) throw new Error(`${stagePath} must be an object.`);
|
|
591
|
+
requireOnlyKeys2(stage, ["id", "title", "narration", "changes"], stagePath);
|
|
592
|
+
requireString2(stage.id, `${stagePath}.id`);
|
|
593
|
+
requireString2(stage.title, `${stagePath}.title`);
|
|
594
|
+
requireString2(stage.narration, `${stagePath}.narration`);
|
|
523
595
|
if (!Array.isArray(stage.changes) || stage.changes.length === 0) {
|
|
524
596
|
throw new Error(`${stagePath}.changes must be a non-empty array.`);
|
|
525
597
|
}
|
|
@@ -547,16 +619,17 @@ function validateWalkthroughSteps(scene, path, anchors, connectionIds) {
|
|
|
547
619
|
const stepIds = /* @__PURE__ */ new Set();
|
|
548
620
|
for (const [stepIndex, step] of scene.steps.entries()) {
|
|
549
621
|
const stepPath = `${path}.steps[${stepIndex}]`;
|
|
550
|
-
if (!
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
622
|
+
if (!isRecord2(step)) throw new Error(`${stepPath} must be an object.`);
|
|
623
|
+
requireString2(step.id, `${stepPath}.id`);
|
|
624
|
+
requireString2(step.title, `${stepPath}.title`);
|
|
625
|
+
requireString2(step.body, `${stepPath}.body`);
|
|
626
|
+
requireString2(step.focus, `${stepPath}.focus`);
|
|
627
|
+
requireOnlyKeys2(step, ["id", "title", "body", "focus", "connection", "depth", "requirement", "image"], stepPath);
|
|
556
628
|
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
557
629
|
throw new Error(`${stepPath}.depth must be mentioned, explained, or contextualized.`);
|
|
558
630
|
}
|
|
559
631
|
validateStepRequirement(step.requirement, `${stepPath}.requirement`);
|
|
632
|
+
validateTourImageSource(step.image, `${stepPath}.image`);
|
|
560
633
|
if (stepIds.has(step.id)) throw new Error(`Duplicate step id in scene ${scene.id}: ${step.id}`);
|
|
561
634
|
stepIds.add(step.id);
|
|
562
635
|
if (!anchors[step.focus]) throw new Error(`${stepPath} references unknown anchor ${step.focus}.`);
|
|
@@ -567,14 +640,14 @@ function validateWalkthroughSteps(scene, path, anchors, connectionIds) {
|
|
|
567
640
|
}
|
|
568
641
|
}
|
|
569
642
|
function validateDeconstructedSelection(value, path, requiresReason) {
|
|
570
|
-
if (!
|
|
571
|
-
|
|
572
|
-
|
|
643
|
+
if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
|
|
644
|
+
requireOnlyKeys2(value, requiresReason ? ["file", "hunks", "reason"] : ["file", "hunks"], path);
|
|
645
|
+
requireString2(value.file, `${path}.file`);
|
|
573
646
|
if (!requiresReason || value.hunks !== void 0) {
|
|
574
|
-
|
|
647
|
+
requireStringArray2(value.hunks, `${path}.hunks`);
|
|
575
648
|
if (value.hunks.length === 0) throw new Error(`${path}.hunks must not be empty.`);
|
|
576
649
|
}
|
|
577
|
-
if (requiresReason)
|
|
650
|
+
if (requiresReason) requireString2(value.reason, `${path}.reason`);
|
|
578
651
|
}
|
|
579
652
|
function validateRealStack(stack, path, minimum) {
|
|
580
653
|
if (!Array.isArray(stack) || stack.length < minimum || stack.length > 6) {
|
|
@@ -582,42 +655,42 @@ function validateRealStack(stack, path, minimum) {
|
|
|
582
655
|
}
|
|
583
656
|
const ids = /* @__PURE__ */ new Set();
|
|
584
657
|
for (const [index, entry] of stack.entries()) {
|
|
585
|
-
if (!
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
658
|
+
if (!isRecord2(entry)) throw new Error(`${path}[${index}] must be an object.`);
|
|
659
|
+
requireOnlyKeys2(entry, ["id", "ref", "label"], `${path}[${index}]`);
|
|
660
|
+
requireString2(entry.id, `${path}[${index}].id`);
|
|
661
|
+
requireString2(entry.ref, `${path}[${index}].ref`);
|
|
589
662
|
optionalString(entry.label, `${path}[${index}].label`);
|
|
590
663
|
if (ids.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
|
|
591
664
|
ids.add(entry.id);
|
|
592
665
|
}
|
|
593
666
|
}
|
|
594
667
|
function validateStackedScene(scene, path, minimum = 3) {
|
|
595
|
-
|
|
668
|
+
requireOnlyKeys2(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "stack", "files", "steps"], path);
|
|
596
669
|
if (!Array.isArray(scene.stack) || scene.stack.length < minimum || scene.stack.length > 6) {
|
|
597
670
|
throw new Error(`${path}.stack must contain between ${minimum} and 6 revisions.`);
|
|
598
671
|
}
|
|
599
672
|
const stackIds = /* @__PURE__ */ new Set();
|
|
600
673
|
for (const [index, entry] of scene.stack.entries()) {
|
|
601
674
|
const entryPath = `${path}.stack[${index}]`;
|
|
602
|
-
if (!
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
675
|
+
if (!isRecord2(entry)) throw new Error(`${entryPath} must be an object.`);
|
|
676
|
+
requireOnlyKeys2(entry, ["id", "ref", "label"], entryPath);
|
|
677
|
+
requireString2(entry.id, `${entryPath}.id`);
|
|
678
|
+
requireString2(entry.ref, `${entryPath}.ref`);
|
|
606
679
|
optionalString(entry.label, `${entryPath}.label`);
|
|
607
680
|
if (stackIds.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
|
|
608
681
|
stackIds.add(entry.id);
|
|
609
682
|
}
|
|
610
|
-
if (scene.files !== void 0)
|
|
683
|
+
if (scene.files !== void 0) requireStringArray2(scene.files, `${path}.files`);
|
|
611
684
|
if (!Array.isArray(scene.steps) || scene.steps.length === 0) throw new Error(`${path}.steps must be a non-empty array.`);
|
|
612
685
|
const stepIds = /* @__PURE__ */ new Set();
|
|
613
686
|
for (const [index, step] of scene.steps.entries()) {
|
|
614
687
|
const stepPath = `${path}.steps[${index}]`;
|
|
615
|
-
if (!
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
688
|
+
if (!isRecord2(step)) throw new Error(`${stepPath} must be an object.`);
|
|
689
|
+
requireOnlyKeys2(step, ["id", "title", "body", "file", "pair", "side", "lines"], stepPath);
|
|
690
|
+
requireString2(step.id, `${stepPath}.id`);
|
|
691
|
+
requireString2(step.title, `${stepPath}.title`);
|
|
692
|
+
requireString2(step.body, `${stepPath}.body`);
|
|
693
|
+
requireString2(step.file, `${stepPath}.file`);
|
|
621
694
|
const stepId = step.id;
|
|
622
695
|
if (stepIds.has(stepId)) throw new Error(`Duplicate step id in scene ${scene.id}: ${stepId}`);
|
|
623
696
|
stepIds.add(stepId);
|
|
@@ -625,8 +698,8 @@ function validateStackedScene(scene, path, minimum = 3) {
|
|
|
625
698
|
throw new Error(`${stepPath}.pair must contain two stack entry ids.`);
|
|
626
699
|
}
|
|
627
700
|
const pair = step.pair;
|
|
628
|
-
const leftIndex = scene.stack.findIndex((entry) =>
|
|
629
|
-
const rightIndex = scene.stack.findIndex((entry) =>
|
|
701
|
+
const leftIndex = scene.stack.findIndex((entry) => isRecord2(entry) && entry.id === pair[0]);
|
|
702
|
+
const rightIndex = scene.stack.findIndex((entry) => isRecord2(entry) && entry.id === pair[1]);
|
|
630
703
|
if (leftIndex < 0 || rightIndex !== leftIndex + 1) throw new Error(`${stepPath}.pair must reference adjacent stack entries in order.`);
|
|
631
704
|
if (step.side !== void 0 && step.side !== "left" && step.side !== "right") throw new Error(`${stepPath}.side must be left or right.`);
|
|
632
705
|
if (step.lines !== void 0 && (!Array.isArray(step.lines) || step.lines.length !== 2 || step.lines.some((line) => !Number.isInteger(line) || Number(line) < 1) || Number(step.lines[1]) < Number(step.lines[0]))) {
|
|
@@ -636,10 +709,10 @@ function validateStackedScene(scene, path, minimum = 3) {
|
|
|
636
709
|
}
|
|
637
710
|
function validateStepRequirement(value, path) {
|
|
638
711
|
if (value === void 0) return;
|
|
639
|
-
if (!
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
712
|
+
if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
|
|
713
|
+
requireOnlyKeys2(value, ["id", "text", "status", "source", "confidence"], path);
|
|
714
|
+
requireString2(value.id, `${path}.id`);
|
|
715
|
+
requireString2(value.text, `${path}.text`);
|
|
643
716
|
if (value.status !== void 0 && !["fulfilled", "gap"].includes(String(value.status))) {
|
|
644
717
|
throw new Error(`${path}.status must be fulfilled or gap.`);
|
|
645
718
|
}
|
|
@@ -650,20 +723,21 @@ function validateStepRequirement(value, path) {
|
|
|
650
723
|
}
|
|
651
724
|
function validateSceneOverview(value, path, options = {}) {
|
|
652
725
|
if (value === void 0) return;
|
|
653
|
-
if (!
|
|
654
|
-
|
|
726
|
+
if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
|
|
727
|
+
requireOnlyKeys2(value, ["kind", "path", "comparison", "purpose"], path);
|
|
655
728
|
if (value.kind !== "directory-diff") {
|
|
656
729
|
throw new Error(`${path}.kind must be directory-diff.`);
|
|
657
730
|
}
|
|
731
|
+
if (value.purpose !== void 0) requireString2(value.purpose, `${path}.purpose`);
|
|
658
732
|
if (value.path !== void 0) validateDirectoryOverviewPath(value.path, `${path}.path`);
|
|
659
733
|
if (value.comparison === void 0) return;
|
|
660
734
|
if (options.allowComparison === false) {
|
|
661
735
|
throw new Error(`${path}.comparison is not supported for walkthrough scenes; use the fixed base-to-head comparison.`);
|
|
662
736
|
}
|
|
663
|
-
if (!
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
737
|
+
if (!isRecord2(value.comparison)) throw new Error(`${path}.comparison must be an object.`);
|
|
738
|
+
requireOnlyKeys2(value.comparison, ["from", "to"], `${path}.comparison`);
|
|
739
|
+
requireString2(value.comparison.from, `${path}.comparison.from`);
|
|
740
|
+
requireString2(value.comparison.to, `${path}.comparison.to`);
|
|
667
741
|
if (value.comparison.from === value.comparison.to) {
|
|
668
742
|
throw new Error(`${path}.comparison.from and to must identify distinct endpoints.`);
|
|
669
743
|
}
|
|
@@ -676,7 +750,7 @@ function validateSceneOverview(value, path, options = {}) {
|
|
|
676
750
|
}
|
|
677
751
|
}
|
|
678
752
|
function validateDirectoryOverviewPath(value, path) {
|
|
679
|
-
|
|
753
|
+
requireString2(value, path);
|
|
680
754
|
if (value.includes("\\")) {
|
|
681
755
|
throw new Error(`${path} must use a relative POSIX directory path without backslashes.`);
|
|
682
756
|
}
|
|
@@ -687,53 +761,36 @@ function validateDirectoryOverviewPath(value, path) {
|
|
|
687
761
|
throw new Error(`${path} must not contain parent-directory traversal.`);
|
|
688
762
|
}
|
|
689
763
|
}
|
|
690
|
-
function collectStepIds(value) {
|
|
691
|
-
const ids = /* @__PURE__ */ new Set();
|
|
692
|
-
if (!Array.isArray(value)) return ids;
|
|
693
|
-
for (const step of value) {
|
|
694
|
-
if (isRecord(step) && typeof step.id === "string") ids.add(step.id);
|
|
695
|
-
}
|
|
696
|
-
return ids;
|
|
697
|
-
}
|
|
698
764
|
function validateNarrative(value, path) {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
765
|
+
requireString2(value.summary, `${path}.summary`);
|
|
766
|
+
requireStringArray2(value.bullets, `${path}.bullets`);
|
|
767
|
+
requireStringArray2(value.tags, `${path}.tags`);
|
|
768
|
+
requireString2(value.takeaway, `${path}.takeaway`);
|
|
703
769
|
}
|
|
704
|
-
function
|
|
770
|
+
function isRecord2(value) {
|
|
705
771
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
706
772
|
}
|
|
707
|
-
function
|
|
773
|
+
function requireString2(value, path) {
|
|
708
774
|
if (typeof value !== "string" || value.length === 0) {
|
|
709
775
|
throw new Error(`${path} must be a non-empty string.`);
|
|
710
776
|
}
|
|
711
777
|
}
|
|
712
|
-
function
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
throw new Error(`${path} must be a full 40- or 64-character hexadecimal Git OID.`);
|
|
778
|
+
function optionalString(value, path) {
|
|
779
|
+
if (value !== void 0) {
|
|
780
|
+
requireString2(value, path);
|
|
716
781
|
}
|
|
717
782
|
}
|
|
718
|
-
function
|
|
719
|
-
if (typeof value !== "string" || value.trim()
|
|
783
|
+
function requireProse2(value, path) {
|
|
784
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
720
785
|
throw new Error(`${path} must be a non-empty string.`);
|
|
721
786
|
}
|
|
722
787
|
}
|
|
723
|
-
function
|
|
724
|
-
return value.toLowerCase();
|
|
725
|
-
}
|
|
726
|
-
function optionalString(value, path) {
|
|
727
|
-
if (value !== void 0) {
|
|
728
|
-
requireString(value, path);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
function requireStringArray(value, path) {
|
|
788
|
+
function requireStringArray2(value, path) {
|
|
732
789
|
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
733
790
|
throw new Error(`${path} must be an array of strings.`);
|
|
734
791
|
}
|
|
735
792
|
}
|
|
736
|
-
function
|
|
793
|
+
function requireOnlyKeys2(value, allowed, path) {
|
|
737
794
|
const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
738
795
|
if (unexpected.length > 0) {
|
|
739
796
|
throw new Error(`${path} contains unknown field${unexpected.length === 1 ? "" : "s"}: ${unexpected.join(", ")}`);
|
|
@@ -741,37 +798,42 @@ function requireOnlyKeys(value, allowed, path) {
|
|
|
741
798
|
}
|
|
742
799
|
|
|
743
800
|
// src/changeTourManifest.ts
|
|
744
|
-
var CHANGE_TOUR_MANIFEST_VERSION =
|
|
745
|
-
function parseChangeTourManifest(value) {
|
|
746
|
-
if (
|
|
801
|
+
var CHANGE_TOUR_MANIFEST_VERSION = 5;
|
|
802
|
+
function parseChangeTourManifest(value, options = {}) {
|
|
803
|
+
if (isRecord3(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_MANIFEST_VERSION) {
|
|
747
804
|
throw new Error(
|
|
748
805
|
`This tour uses manifest format version ${value.version}, but this version of Bygone supports up to version ${CHANGE_TOUR_MANIFEST_VERSION}. Upgrade Bygone to open it.`
|
|
749
806
|
);
|
|
750
807
|
}
|
|
751
|
-
if (
|
|
808
|
+
if (isRecord3(value) && value.version === 3) {
|
|
809
|
+
throw new Error("Tour manifest format version 3 is retired. Remove the review block, set source version to 4, and recompile the tour.");
|
|
810
|
+
}
|
|
811
|
+
if (!isRecord3(value) || value.version !== 1 && value.version !== 2 && value.version !== 4 && value.version !== CHANGE_TOUR_MANIFEST_VERSION) {
|
|
752
812
|
throw new Error(`Unsupported or missing change-tour manifest version.`);
|
|
753
813
|
}
|
|
754
|
-
|
|
755
|
-
|
|
814
|
+
if ("review" in value) throw new Error("Change-tour manifests do not support the review field. Put observations in scene and step narrative.");
|
|
815
|
+
validateTourFraming(value, value.version, "manifest");
|
|
816
|
+
requireString3(value.title, "title");
|
|
817
|
+
requireString3(value.generatedAt, "generatedAt");
|
|
756
818
|
if (value.windowTitle !== void 0) {
|
|
757
|
-
|
|
819
|
+
requireString3(value.windowTitle, "windowTitle");
|
|
758
820
|
}
|
|
759
821
|
if (value.sourceUrl !== void 0) {
|
|
760
|
-
|
|
822
|
+
requireString3(value.sourceUrl, "sourceUrl");
|
|
761
823
|
}
|
|
762
|
-
if (!
|
|
824
|
+
if (!isRecord3(value.range)) {
|
|
763
825
|
throw new Error("Change-tour manifest range must be an object.");
|
|
764
826
|
}
|
|
765
827
|
for (const key of ["baseRef", "headRef", "mergeBaseOid", "headOid"]) {
|
|
766
|
-
|
|
828
|
+
requireString3(value.range[key], `range.${key}`);
|
|
767
829
|
}
|
|
768
|
-
if (!
|
|
830
|
+
if (!isRecord3(value.summary)) {
|
|
769
831
|
throw new Error("Change-tour manifest summary must be an object.");
|
|
770
832
|
}
|
|
771
833
|
for (const key of ["changedFiles", "includedScenes", "additions", "deletions", "commitCount"]) {
|
|
772
834
|
requireNonNegativeInteger(value.summary[key], `summary.${key}`);
|
|
773
835
|
}
|
|
774
|
-
|
|
836
|
+
requireStringArray3(value.summary.omittedFiles, "summary.omittedFiles");
|
|
775
837
|
if (!Array.isArray(value.commits) || !Array.isArray(value.chapters) || !Array.isArray(value.scenes)) {
|
|
776
838
|
throw new Error("Change-tour manifest commits, chapters, and scenes must be arrays.");
|
|
777
839
|
}
|
|
@@ -787,12 +849,13 @@ function parseChangeTourManifest(value) {
|
|
|
787
849
|
sceneIds.add(candidate.id);
|
|
788
850
|
}
|
|
789
851
|
for (const [index, candidate] of value.chapters.entries()) {
|
|
790
|
-
if (!
|
|
852
|
+
if (!isRecord3(candidate)) {
|
|
791
853
|
throw new Error(`chapters[${index}] must be an object.`);
|
|
792
854
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
855
|
+
requireString3(candidate.id, `chapters[${index}].id`);
|
|
856
|
+
requireString3(candidate.title, `chapters[${index}].title`);
|
|
857
|
+
validateChapterSummary(candidate.summary, value.version, `chapters[${index}].summary`);
|
|
858
|
+
requireStringArray3(candidate.sceneIds, `chapters[${index}].sceneIds`);
|
|
796
859
|
for (const sceneId of candidate.sceneIds) {
|
|
797
860
|
if (!sceneIds.has(sceneId)) {
|
|
798
861
|
throw new Error(`Chapter references unknown scene id: ${sceneId}`);
|
|
@@ -802,6 +865,10 @@ function parseChangeTourManifest(value) {
|
|
|
802
865
|
if (value.summary.includedScenes !== value.scenes.length) {
|
|
803
866
|
throw new Error("summary.includedScenes must match the number of scenes.");
|
|
804
867
|
}
|
|
868
|
+
if (value.details !== void 0) {
|
|
869
|
+
if (value.version !== 5) throw new Error("details requires manifest version 5.");
|
|
870
|
+
validateChangeTourDetails(value.details, "details");
|
|
871
|
+
}
|
|
805
872
|
const files = value.files === void 0 ? collectLegacyTourFiles(value.scenes) : value.files.map((candidate, index) => {
|
|
806
873
|
validateTourFile(candidate, index);
|
|
807
874
|
return candidate;
|
|
@@ -813,40 +880,14 @@ function parseChangeTourManifest(value) {
|
|
|
813
880
|
validateAuthoringCoverage(value.authoringCoverage, value.version);
|
|
814
881
|
}
|
|
815
882
|
if (value.tours !== void 0) {
|
|
816
|
-
if (value.version !==
|
|
883
|
+
if (value.version !== 4 && value.version !== 5) throw new Error("Independent tours require manifest version 4.");
|
|
817
884
|
validateModeTours(value.tours, value.version);
|
|
818
885
|
}
|
|
819
|
-
if (value.version !== 1) validateZoom(value, value.version);
|
|
820
|
-
if (value.review !== void 0) {
|
|
821
|
-
if (value.version !== 3) throw new Error("Review notes require manifest version 3.");
|
|
822
|
-
const evidenceScenes = value.version >= 2 && isRecord2(value.zoom) && isRecord2(value.zoom.final) ? value.zoom.final.scenes : value.scenes;
|
|
823
|
-
validateChangeTourReview(value.review, {
|
|
824
|
-
expectedRange: {
|
|
825
|
-
baseOid: String(value.range.mergeBaseOid),
|
|
826
|
-
headOid: String(value.range.headOid)
|
|
827
|
-
},
|
|
828
|
-
authoredWalkthroughSteps: collectAuthoredWalkthroughStepIds(evidenceScenes)
|
|
829
|
-
});
|
|
830
|
-
}
|
|
886
|
+
if (value.version !== 1) validateZoom(value, value.version, Boolean(options.exported));
|
|
831
887
|
return { ...value, files };
|
|
832
888
|
}
|
|
833
|
-
function collectAuthoredWalkthroughStepIds(value) {
|
|
834
|
-
const scenes = /* @__PURE__ */ new Map();
|
|
835
|
-
if (!Array.isArray(value)) return scenes;
|
|
836
|
-
for (const scene of value) {
|
|
837
|
-
if (!isRecord2(scene) || scene.kind !== "walkthrough" || !Array.isArray(scene.steps) || typeof scene.id !== "string") {
|
|
838
|
-
continue;
|
|
839
|
-
}
|
|
840
|
-
const stepIds = /* @__PURE__ */ new Set();
|
|
841
|
-
for (const step of scene.steps) {
|
|
842
|
-
if (isRecord2(step) && typeof step.id === "string") stepIds.add(step.id);
|
|
843
|
-
}
|
|
844
|
-
scenes.set(scene.id, stepIds);
|
|
845
|
-
}
|
|
846
|
-
return scenes;
|
|
847
|
-
}
|
|
848
889
|
function validateAuthoringCoverage(value, version) {
|
|
849
|
-
if (!
|
|
890
|
+
if (!isRecord3(value) || !isRecord3(value.walkthrough) || !Array.isArray(value.explanationAssignments)) {
|
|
850
891
|
throw new Error("authoringCoverage must contain walkthrough and explanationAssignments.");
|
|
851
892
|
}
|
|
852
893
|
const expectedScope = version >= 2 ? "final" : "tour";
|
|
@@ -861,8 +902,8 @@ function validateAuthoringCoverage(value, version) {
|
|
|
861
902
|
}
|
|
862
903
|
for (const [index, assignment] of value.explanationAssignments.entries()) {
|
|
863
904
|
const path = `authoringCoverage.explanationAssignments[${index}]`;
|
|
864
|
-
if (!
|
|
865
|
-
|
|
905
|
+
if (!isRecord3(assignment)) throw new Error(`${path} must be an object.`);
|
|
906
|
+
requireString3(assignment.sceneId, `${path}.sceneId`);
|
|
866
907
|
requireNonNegativeInteger(assignment.assignedUnits, `${path}.assignedUnits`);
|
|
867
908
|
requireNonNegativeInteger(assignment.excludedUnits, `${path}.excludedUnits`);
|
|
868
909
|
requireNonNegativeInteger(assignment.completeUnits, `${path}.completeUnits`);
|
|
@@ -874,16 +915,17 @@ function validateAuthoringCoverage(value, version) {
|
|
|
874
915
|
}
|
|
875
916
|
}
|
|
876
917
|
function validateModeTours(value, version) {
|
|
877
|
-
if (!
|
|
918
|
+
if (!isRecord3(value)) throw new Error("Change-tour manifest tours must be an object.");
|
|
878
919
|
if (value.historical === void 0 && value.deconstructed === void 0) {
|
|
879
920
|
throw new Error("Change-tour manifest tours must contain a historical or deconstructed tour.");
|
|
880
921
|
}
|
|
881
922
|
for (const mode of ["historical", "deconstructed"]) {
|
|
882
923
|
const tour = value[mode];
|
|
883
924
|
if (tour === void 0) continue;
|
|
884
|
-
if (!
|
|
925
|
+
if (!isRecord3(tour) || !Array.isArray(tour.chapters) || !Array.isArray(tour.scenes) || tour.chapters.length === 0 || tour.scenes.length === 0) {
|
|
885
926
|
throw new Error(`Change-tour manifest tours.${mode} must contain non-empty chapters and scenes arrays.`);
|
|
886
927
|
}
|
|
928
|
+
validateTourFraming(tour, version, `tours.${mode}`);
|
|
887
929
|
const sceneIds = /* @__PURE__ */ new Set();
|
|
888
930
|
let hasDeconstructed = false;
|
|
889
931
|
for (const [index, scene] of tour.scenes.entries()) {
|
|
@@ -897,16 +939,21 @@ function validateModeTours(value, version) {
|
|
|
897
939
|
throw new Error("Historical tours cannot contain synthetic deconstructed scenes.");
|
|
898
940
|
}
|
|
899
941
|
}
|
|
942
|
+
if (tour.details !== void 0) {
|
|
943
|
+
if (version !== 5) throw new Error(`tours.${mode}.details requires manifest version 5.`);
|
|
944
|
+
validateChangeTourDetails(tour.details, `tours.${mode}.details`);
|
|
945
|
+
}
|
|
900
946
|
if (mode === "deconstructed" && !hasDeconstructed) {
|
|
901
947
|
throw new Error("Deconstructed tours must contain a constructed deconstructed-diff scene.");
|
|
902
948
|
}
|
|
903
949
|
for (const [index, chapter] of tour.chapters.entries()) {
|
|
904
|
-
if (!
|
|
950
|
+
if (!isRecord3(chapter)) {
|
|
905
951
|
throw new Error(`tours.${mode}.chapters[${index}] must be an object.`);
|
|
906
952
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
953
|
+
requireString3(chapter.id, `tours.${mode}.chapters[${index}].id`);
|
|
954
|
+
requireString3(chapter.title, `tours.${mode}.chapters[${index}].title`);
|
|
955
|
+
validateChapterSummary(chapter.summary, version, `tours.${mode}.chapters[${index}].summary`);
|
|
956
|
+
requireStringArray3(chapter.sceneIds, `tours.${mode}.chapters[${index}].sceneIds`);
|
|
910
957
|
for (const sceneId of chapter.sceneIds) {
|
|
911
958
|
if (!sceneIds.has(sceneId)) {
|
|
912
959
|
throw new Error(`tours.${mode} chapter references unknown scene id: ${sceneId}`);
|
|
@@ -916,28 +963,28 @@ function validateModeTours(value, version) {
|
|
|
916
963
|
}
|
|
917
964
|
}
|
|
918
965
|
function parseChangeTourStory(value) {
|
|
919
|
-
if (!
|
|
966
|
+
if (!isRecord3(value) || !Array.isArray(value.scenes)) {
|
|
920
967
|
throw new Error("Change-tour story must contain a scenes array.");
|
|
921
968
|
}
|
|
922
969
|
if (value.title !== void 0) {
|
|
923
|
-
|
|
970
|
+
requireString3(value.title, "title");
|
|
924
971
|
}
|
|
925
972
|
if (value.sourceUrl !== void 0) {
|
|
926
|
-
|
|
973
|
+
requireString3(value.sourceUrl, "sourceUrl");
|
|
927
974
|
}
|
|
928
975
|
for (const [index, scene] of value.scenes.entries()) {
|
|
929
|
-
if (!
|
|
976
|
+
if (!isRecord3(scene) || scene.kind !== "discussion" && scene.kind !== "file") {
|
|
930
977
|
throw new Error(`story.scenes[${index}] must be a discussion or file scene.`);
|
|
931
978
|
}
|
|
932
|
-
|
|
933
|
-
|
|
979
|
+
requireString3(scene.chapterId, `story.scenes[${index}].chapterId`);
|
|
980
|
+
requireString3(scene.chapterTitle, `story.scenes[${index}].chapterTitle`);
|
|
934
981
|
validateNarrative2(scene, `story.scenes[${index}]`);
|
|
935
982
|
if (scene.kind === "discussion") {
|
|
936
|
-
|
|
983
|
+
requireString3(scene.title, `story.scenes[${index}].title`);
|
|
937
984
|
} else {
|
|
938
|
-
|
|
985
|
+
requireString3(scene.path, `story.scenes[${index}].path`);
|
|
939
986
|
if (scene.title !== void 0) {
|
|
940
|
-
|
|
987
|
+
requireString3(scene.title, `story.scenes[${index}].title`);
|
|
941
988
|
}
|
|
942
989
|
if (scene.focusChangeIndex !== void 0) {
|
|
943
990
|
requireNonNegativeInteger(scene.focusChangeIndex, `story.scenes[${index}].focusChangeIndex`);
|
|
@@ -946,11 +993,11 @@ function parseChangeTourStory(value) {
|
|
|
946
993
|
}
|
|
947
994
|
return value;
|
|
948
995
|
}
|
|
949
|
-
function validateZoom(value, version) {
|
|
950
|
-
if (!
|
|
996
|
+
function validateZoom(value, version, exported) {
|
|
997
|
+
if (!exported && (!isRecord3(value.repository) || typeof value.repository.root !== "string" || !value.repository.root)) {
|
|
951
998
|
throw new Error("A v2 tour requires its originating repository.root.");
|
|
952
999
|
}
|
|
953
|
-
if (!
|
|
1000
|
+
if (!isRecord3(value.zoom) || !Array.isArray(value.zoom.revisions) || !isRecord3(value.zoom.final)) {
|
|
954
1001
|
throw new Error("A v2 tour requires zoom metadata and real revision evidence.");
|
|
955
1002
|
}
|
|
956
1003
|
const scenes = value.scenes;
|
|
@@ -995,27 +1042,29 @@ function validateZoom(value, version) {
|
|
|
995
1042
|
finalSceneIds.add(scene.id);
|
|
996
1043
|
}
|
|
997
1044
|
for (const [index, chapter] of value.zoom.final.chapters.entries()) {
|
|
998
|
-
if (!
|
|
1045
|
+
if (!isRecord3(chapter) || !Array.isArray(chapter.sceneIds)) {
|
|
999
1046
|
throw new Error(`zoom.final.chapters[${index}] must contain sceneIds.`);
|
|
1000
1047
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1048
|
+
requireString3(chapter.id, `zoom.final.chapters[${index}].id`);
|
|
1049
|
+
requireString3(chapter.title, `zoom.final.chapters[${index}].title`);
|
|
1050
|
+
validateChapterSummary(chapter.summary, version, `zoom.final.chapters[${index}].summary`);
|
|
1003
1051
|
for (const sceneId of chapter.sceneIds) {
|
|
1004
|
-
|
|
1052
|
+
requireString3(sceneId, `zoom.final.chapters[${index}].sceneIds`);
|
|
1005
1053
|
if (!finalSceneIds.has(sceneId)) throw new Error(`Final tour chapter references unknown scene: ${sceneId}`);
|
|
1006
1054
|
}
|
|
1007
1055
|
}
|
|
1008
1056
|
}
|
|
1009
1057
|
function validateScene(value, index, version) {
|
|
1010
|
-
if (!
|
|
1058
|
+
if (!isRecord3(value) || !["text-diff", "discussion", "walkthrough", "stacked-diff", "deconstructed-diff"].includes(String(value.kind))) {
|
|
1011
1059
|
throw new Error(`scenes[${index}] must be a text-diff, discussion, walkthrough, stacked-diff, or deconstructed-diff scene.`);
|
|
1012
1060
|
}
|
|
1013
1061
|
for (const key of ["id", "title"]) {
|
|
1014
|
-
|
|
1062
|
+
requireString3(value[key], `scenes[${index}].${key}`);
|
|
1015
1063
|
}
|
|
1016
|
-
if (value.overview !== void 0 && version
|
|
1017
|
-
throw new Error(`scenes[${index}].overview requires manifest version
|
|
1064
|
+
if (value.overview !== void 0 && version < 4) {
|
|
1065
|
+
throw new Error(`scenes[${index}].overview requires manifest version 4.`);
|
|
1018
1066
|
}
|
|
1067
|
+
validateOverviewPurpose(value.overview, version, `scenes[${index}].overview`);
|
|
1019
1068
|
validateNarrative2(value, `scenes[${index}]`);
|
|
1020
1069
|
if (value.kind === "discussion") {
|
|
1021
1070
|
validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
|
|
@@ -1028,21 +1077,22 @@ function validateScene(value, index, version) {
|
|
|
1028
1077
|
}
|
|
1029
1078
|
for (const [stepIndex, step] of value.steps.entries()) {
|
|
1030
1079
|
const path = `scenes[${index}].steps[${stepIndex}]`;
|
|
1031
|
-
if (!
|
|
1080
|
+
if (!isRecord3(step) || !isRecord3(step.focus) || !isRecord3(step.diff)) {
|
|
1032
1081
|
throw new Error(`${path} must contain focus and diff objects.`);
|
|
1033
1082
|
}
|
|
1034
|
-
for (const key of ["id", "title", "body"])
|
|
1083
|
+
for (const key of ["id", "title", "body"]) requireString3(step[key], `${path}.${key}`);
|
|
1035
1084
|
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
1036
1085
|
throw new Error(`${path}.depth must be mentioned, explained, or contextualized.`);
|
|
1037
1086
|
}
|
|
1038
1087
|
validateStepRequirement(step.requirement, `${path}.requirement`);
|
|
1088
|
+
validateTourImage(step.image, `${path}.image`);
|
|
1039
1089
|
validateResolvedAnchor(step.focus, `${path}.focus`);
|
|
1040
1090
|
validateScene(step.diff, index, version);
|
|
1041
1091
|
if (step.diff.kind !== "text-diff") throw new Error(`${path}.diff must be a text-diff scene.`);
|
|
1042
1092
|
if (step.connection !== void 0) {
|
|
1043
|
-
if (!
|
|
1044
|
-
|
|
1045
|
-
|
|
1093
|
+
if (!isRecord3(step.connection)) throw new Error(`${path}.connection must be an object.`);
|
|
1094
|
+
requireString3(step.connection.id, `${path}.connection.id`);
|
|
1095
|
+
requireString3(step.connection.label, `${path}.connection.label`);
|
|
1046
1096
|
validateResolvedAnchor(step.connection.from, `${path}.connection.from`);
|
|
1047
1097
|
validateResolvedAnchor(step.connection.to, `${path}.connection.to`);
|
|
1048
1098
|
}
|
|
@@ -1065,12 +1115,12 @@ function validateScene(value, index, version) {
|
|
|
1065
1115
|
}
|
|
1066
1116
|
validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
|
|
1067
1117
|
for (const key of ["path", "leftLabel", "rightLabel", "leftContent", "rightContent"]) {
|
|
1068
|
-
|
|
1118
|
+
requireString3(value[key], `scenes[${index}].${key}`);
|
|
1069
1119
|
}
|
|
1070
1120
|
if (value.previousPath !== void 0) {
|
|
1071
|
-
|
|
1121
|
+
requireString3(value.previousPath, `scenes[${index}].previousPath`);
|
|
1072
1122
|
}
|
|
1073
|
-
|
|
1123
|
+
requireString3(value.changeKind, `scenes[${index}].changeKind`);
|
|
1074
1124
|
requireNonNegativeInteger(value.additions, `scenes[${index}].additions`);
|
|
1075
1125
|
requireNonNegativeInteger(value.deletions, `scenes[${index}].deletions`);
|
|
1076
1126
|
if (value.focusChangeIndex !== void 0) {
|
|
@@ -1082,8 +1132,8 @@ function validateStackedScene2(value, index) {
|
|
|
1082
1132
|
throw new Error(`scenes[${index}].stack must contain between 2 and 6 revisions.`);
|
|
1083
1133
|
}
|
|
1084
1134
|
for (const [panelIndex, panel] of value.stack.entries()) {
|
|
1085
|
-
if (!
|
|
1086
|
-
for (const key of ["id", "ref", "oid", "label"])
|
|
1135
|
+
if (!isRecord3(panel)) throw new Error(`scenes[${index}].stack[${panelIndex}] must be an object.`);
|
|
1136
|
+
for (const key of ["id", "ref", "oid", "label"]) requireString3(panel[key], `scenes[${index}].stack[${panelIndex}].${key}`);
|
|
1087
1137
|
}
|
|
1088
1138
|
validateMultiPanelFiles(value.files, value.stack.length, index);
|
|
1089
1139
|
validateMultiPanelSteps(value.steps, value.stack.length, index, false);
|
|
@@ -1092,33 +1142,33 @@ function validateDeconstructedScene2(value, index) {
|
|
|
1092
1142
|
if (value.stageLabel !== "Explanation stages") {
|
|
1093
1143
|
throw new Error(`scenes[${index}].stageLabel must be Explanation stages.`);
|
|
1094
1144
|
}
|
|
1095
|
-
if (!
|
|
1145
|
+
if (!isRecord3(value.realRange)) throw new Error(`scenes[${index}].realRange must be an object.`);
|
|
1096
1146
|
for (const key of ["baseRef", "targetRef", "baseOid", "targetOid"]) {
|
|
1097
|
-
|
|
1147
|
+
requireString3(value.realRange[key], `scenes[${index}].realRange.${key}`);
|
|
1098
1148
|
}
|
|
1099
1149
|
if (!Array.isArray(value.panels) || value.panels.length < 2 || value.panels.length > 13) {
|
|
1100
1150
|
throw new Error(`scenes[${index}].panels must contain a baseline and between 1 and 12 stages.`);
|
|
1101
1151
|
}
|
|
1102
1152
|
const panels = value.panels;
|
|
1103
1153
|
for (const [panelIndex, panel] of panels.entries()) {
|
|
1104
|
-
if (!
|
|
1105
|
-
for (const key of ["id", "label", "role"])
|
|
1154
|
+
if (!isRecord3(panel)) throw new Error(`scenes[${index}].panels[${panelIndex}] must be an object.`);
|
|
1155
|
+
for (const key of ["id", "label", "role"]) requireString3(panel[key], `scenes[${index}].panels[${panelIndex}].${key}`);
|
|
1106
1156
|
const expectedRole = panelIndex === 0 ? "baseline" : "stage";
|
|
1107
1157
|
if (panel.role !== expectedRole) throw new Error(`scenes[${index}].panels[${panelIndex}].role must be ${expectedRole}.`);
|
|
1108
|
-
if (panelIndex > 0)
|
|
1158
|
+
if (panelIndex > 0) requireString3(panel.stageId, `scenes[${index}].panels[${panelIndex}].stageId`);
|
|
1109
1159
|
}
|
|
1110
1160
|
validateMultiPanelFiles(value.files, panels.length, index);
|
|
1111
1161
|
validateMultiPanelSteps(value.steps, panels.length, index, true);
|
|
1112
1162
|
if (Array.isArray(value.steps)) {
|
|
1113
1163
|
let previousPairIndex = -1;
|
|
1114
1164
|
value.steps.forEach((step, stepIndex) => {
|
|
1115
|
-
if (!
|
|
1165
|
+
if (!isRecord3(step)) return;
|
|
1116
1166
|
const pairIndex = Number(step.pairIndex);
|
|
1117
1167
|
if (pairIndex < previousPairIndex) {
|
|
1118
1168
|
throw new Error(`scenes[${index}].steps must remain grouped in explanation stage order.`);
|
|
1119
1169
|
}
|
|
1120
1170
|
previousPairIndex = pairIndex;
|
|
1121
|
-
if (step.stageId !== void 0)
|
|
1171
|
+
if (step.stageId !== void 0) requireString3(step.stageId, `scenes[${index}].steps[${stepIndex}].stageId`);
|
|
1122
1172
|
if (step.stageIndex !== void 0) requireNonNegativeInteger(step.stageIndex, `scenes[${index}].steps[${stepIndex}].stageIndex`);
|
|
1123
1173
|
if (step.focusIndex !== void 0) requireNonNegativeInteger(step.focusIndex, `scenes[${index}].steps[${stepIndex}].focusIndex`);
|
|
1124
1174
|
if (step.focusCount !== void 0) requirePositiveInteger(step.focusCount, `scenes[${index}].steps[${stepIndex}].focusCount`);
|
|
@@ -1131,14 +1181,14 @@ function validateDeconstructedScene2(value, index) {
|
|
|
1131
1181
|
});
|
|
1132
1182
|
for (let stageIndex = 0; stageIndex < panels.length - 1; stageIndex += 1) {
|
|
1133
1183
|
const panel = panels[stageIndex + 1];
|
|
1134
|
-
const stageSteps = value.steps.filter((step) =>
|
|
1135
|
-
if (!
|
|
1184
|
+
const stageSteps = value.steps.filter((step) => isRecord3(step) && Number(step.pairIndex) === stageIndex);
|
|
1185
|
+
if (!isRecord3(panel) || stageSteps.length === 0) {
|
|
1136
1186
|
throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
|
|
1137
1187
|
}
|
|
1138
1188
|
const firstStep = stageSteps[0];
|
|
1139
|
-
const fallbackStageId =
|
|
1189
|
+
const fallbackStageId = isRecord3(firstStep) ? firstStep.id : void 0;
|
|
1140
1190
|
if (stageSteps.some((step) => {
|
|
1141
|
-
if (!
|
|
1191
|
+
if (!isRecord3(step)) return true;
|
|
1142
1192
|
return (step.stageId ?? fallbackStageId) !== panel.stageId;
|
|
1143
1193
|
})) {
|
|
1144
1194
|
throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
|
|
@@ -1149,15 +1199,15 @@ function validateDeconstructedScene2(value, index) {
|
|
|
1149
1199
|
function validateMultiPanelFiles(value, panelCount, index) {
|
|
1150
1200
|
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].files must be non-empty.`);
|
|
1151
1201
|
for (const [fileIndex, file] of value.entries()) {
|
|
1152
|
-
if (!
|
|
1153
|
-
|
|
1202
|
+
if (!isRecord3(file)) throw new Error(`scenes[${index}].files[${fileIndex}] must be an object.`);
|
|
1203
|
+
requireString3(file.path, `scenes[${index}].files[${fileIndex}].path`);
|
|
1154
1204
|
if (!Array.isArray(file.panels) || file.panels.length !== panelCount) {
|
|
1155
1205
|
throw new Error(`scenes[${index}].files[${fileIndex}].panels must match panel count.`);
|
|
1156
1206
|
}
|
|
1157
1207
|
for (const [panelIndex, panel] of file.panels.entries()) {
|
|
1158
|
-
if (!
|
|
1159
|
-
for (const key of ["id", "label", "content"])
|
|
1160
|
-
if (panel.path !== void 0)
|
|
1208
|
+
if (!isRecord3(panel)) throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}] must be an object.`);
|
|
1209
|
+
for (const key of ["id", "label", "content"]) requireString3(panel[key], `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].${key}`);
|
|
1210
|
+
if (panel.path !== void 0) requireString3(panel.path, `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].path`);
|
|
1161
1211
|
if (typeof panel.exists !== "boolean") throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}].exists must be boolean.`);
|
|
1162
1212
|
}
|
|
1163
1213
|
}
|
|
@@ -1165,12 +1215,12 @@ function validateMultiPanelFiles(value, panelCount, index) {
|
|
|
1165
1215
|
function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
|
|
1166
1216
|
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].steps must be non-empty.`);
|
|
1167
1217
|
for (const [stepIndex, step] of value.entries()) {
|
|
1168
|
-
if (!
|
|
1169
|
-
for (const key of ["id", "title", "body", "file", "side"])
|
|
1218
|
+
if (!isRecord3(step)) throw new Error(`scenes[${index}].steps[${stepIndex}] must be an object.`);
|
|
1219
|
+
for (const key of ["id", "title", "body", "file", "side"]) requireString3(step[key], `scenes[${index}].steps[${stepIndex}].${key}`);
|
|
1170
1220
|
requireNonNegativeInteger(step.pairIndex, `scenes[${index}].steps[${stepIndex}].pairIndex`);
|
|
1171
1221
|
if (Number(step.pairIndex) >= panelCount - 1) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex is outside the panels.`);
|
|
1172
1222
|
if (deconstructed) {
|
|
1173
|
-
|
|
1223
|
+
requireStringArray3(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
|
|
1174
1224
|
if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
|
|
1175
1225
|
}
|
|
1176
1226
|
if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
|
|
@@ -1179,7 +1229,7 @@ function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
|
|
|
1179
1229
|
}
|
|
1180
1230
|
}
|
|
1181
1231
|
function validateTourFile(value, index) {
|
|
1182
|
-
if (!
|
|
1232
|
+
if (!isRecord3(value)) throw new Error(`files[${index}] must be an object.`);
|
|
1183
1233
|
if (value.kind === "text-diff") {
|
|
1184
1234
|
validateScene(value, index, 1);
|
|
1185
1235
|
return;
|
|
@@ -1188,9 +1238,9 @@ function validateTourFile(value, index) {
|
|
|
1188
1238
|
throw new Error(`files[${index}].kind must be text-diff or omitted.`);
|
|
1189
1239
|
}
|
|
1190
1240
|
for (const key of ["id", "title", "path", "changeKind", "reason"]) {
|
|
1191
|
-
|
|
1241
|
+
requireString3(value[key], `files[${index}].${key}`);
|
|
1192
1242
|
}
|
|
1193
|
-
if (value.previousPath !== void 0)
|
|
1243
|
+
if (value.previousPath !== void 0) requireString3(value.previousPath, `files[${index}].previousPath`);
|
|
1194
1244
|
requireNonNegativeInteger(value.additions, `files[${index}].additions`);
|
|
1195
1245
|
requireNonNegativeInteger(value.deletions, `files[${index}].deletions`);
|
|
1196
1246
|
}
|
|
@@ -1210,28 +1260,35 @@ function collectLegacyTourFiles(scenes) {
|
|
|
1210
1260
|
return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
1211
1261
|
}
|
|
1212
1262
|
function validateResolvedAnchor(value, path) {
|
|
1213
|
-
if (!
|
|
1214
|
-
for (const key of ["id", "path", "revision", "excerpt"])
|
|
1263
|
+
if (!isRecord3(value)) throw new Error(`${path} must be an object.`);
|
|
1264
|
+
for (const key of ["id", "path", "revision", "excerpt"]) requireString3(value[key], `${path}.${key}`);
|
|
1215
1265
|
if (value.revision !== "base" && value.revision !== "head") throw new Error(`${path}.revision must be base or head.`);
|
|
1216
1266
|
requirePositiveInteger(value.startLine, `${path}.startLine`);
|
|
1217
1267
|
requirePositiveInteger(value.endLine, `${path}.endLine`);
|
|
1218
1268
|
if (Number(value.endLine) < Number(value.startLine)) throw new Error(`${path}.endLine must not precede startLine.`);
|
|
1219
1269
|
}
|
|
1220
1270
|
function validateNarrative2(value, path) {
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1271
|
+
requireString3(value.summary, `${path}.summary`);
|
|
1272
|
+
requireStringArray3(value.bullets, `${path}.bullets`);
|
|
1273
|
+
requireStringArray3(value.tags, `${path}.tags`);
|
|
1274
|
+
requireString3(value.takeaway, `${path}.takeaway`);
|
|
1225
1275
|
}
|
|
1226
|
-
function
|
|
1276
|
+
function validateChapterSummary(value, version, path) {
|
|
1277
|
+
if (value === void 0) return;
|
|
1278
|
+
if (version < 5) throw new Error(`${path} requires manifest version 5.`);
|
|
1279
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1280
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function isRecord3(value) {
|
|
1227
1284
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1228
1285
|
}
|
|
1229
|
-
function
|
|
1286
|
+
function requireString3(value, path) {
|
|
1230
1287
|
if (typeof value !== "string") {
|
|
1231
1288
|
throw new Error(`${path} must be a string.`);
|
|
1232
1289
|
}
|
|
1233
1290
|
}
|
|
1234
|
-
function
|
|
1291
|
+
function requireStringArray3(value, path) {
|
|
1235
1292
|
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
1236
1293
|
throw new Error(`${path} must be an array of strings.`);
|
|
1237
1294
|
}
|
|
@@ -2018,7 +2075,7 @@ function diffArrays(oldArr, newArr, callback) {
|
|
|
2018
2075
|
}
|
|
2019
2076
|
|
|
2020
2077
|
// src/diffEngine.ts
|
|
2021
|
-
var DEFAULT_MAX_INLINE_HIGHLIGHT_LINE_LENGTH =
|
|
2078
|
+
var DEFAULT_MAX_INLINE_HIGHLIGHT_LINE_LENGTH = 2e4;
|
|
2022
2079
|
var DEFAULT_DIFF_TIMEOUT_MS = 100;
|
|
2023
2080
|
var MAX_INLINE_HIGHLIGHT_LINE_LENGTH = readPositiveIntegerEnv(
|
|
2024
2081
|
"BYGONE_MAX_INLINE_HIGHLIGHT_LINE_LENGTH",
|
|
@@ -2031,6 +2088,22 @@ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
|
2031
2088
|
if (leftContent === rightContent) {
|
|
2032
2089
|
return buildIdenticalTwoWayDiffModel(leftLines);
|
|
2033
2090
|
}
|
|
2091
|
+
if (leftLines.length === rightLines.length && leftLines.every((line, index) => line === rightLines[index])) {
|
|
2092
|
+
const model = buildIdenticalTwoWayDiffModel(leftLines);
|
|
2093
|
+
const rawLeft = leftContent.match(/[^\n]*\n|[^\n]+$/g) || [];
|
|
2094
|
+
const rawRight = rightContent.match(/[^\n]*\n|[^\n]+$/g) || [];
|
|
2095
|
+
leftLines.forEach((_, index) => {
|
|
2096
|
+
if (rawLeft[index] === rawRight[index]) return;
|
|
2097
|
+
model.leftLines[index].kind = model.rows[index].left.kind = "removed";
|
|
2098
|
+
model.rightLines[index].kind = model.rows[index].right.kind = "added";
|
|
2099
|
+
model.leftLines[index].inlineDetail = model.rightLines[index].inlineDetail = "line-ending";
|
|
2100
|
+
model.blocks.push(makeDiffBlock("replace", index, index + 1, index, index + 1));
|
|
2101
|
+
});
|
|
2102
|
+
model.blocks = mergeTouchingBlocks(model.blocks);
|
|
2103
|
+
model.hasChanges = true;
|
|
2104
|
+
model.formattingOnly = true;
|
|
2105
|
+
return model;
|
|
2106
|
+
}
|
|
2034
2107
|
const interned = internDiffLines(leftLines, rightLines);
|
|
2035
2108
|
const internedChanges = diffArrays(interned.left, interned.right, {
|
|
2036
2109
|
timeout: options.timeoutMs ?? DIFF_TIMEOUT_MS
|
|
@@ -2107,14 +2180,54 @@ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
|
2107
2180
|
}
|
|
2108
2181
|
}
|
|
2109
2182
|
const hasChanges = rows.some((row) => row.left.kind !== "context" || row.right.kind !== "context");
|
|
2110
|
-
return {
|
|
2183
|
+
return markUndetailedReplacements({
|
|
2111
2184
|
rows,
|
|
2112
2185
|
leftLines: renderedLeftLines,
|
|
2113
2186
|
rightLines: renderedRightLines,
|
|
2114
|
-
blocks,
|
|
2187
|
+
blocks: mergeTouchingBlocks(blocks),
|
|
2188
|
+
reflows: blocks.filter((block) => block.reflow),
|
|
2115
2189
|
hasChanges,
|
|
2116
2190
|
quality: "exact"
|
|
2117
|
-
};
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
function markUndetailedReplacements(model) {
|
|
2194
|
+
for (const block of model.blocks) {
|
|
2195
|
+
if (block.kind !== "replace") continue;
|
|
2196
|
+
for (const side of ["left", "right"]) {
|
|
2197
|
+
for (let index = block[`${side}Start`]; index < block[`${side}End`]; index++) {
|
|
2198
|
+
const line = model[`${side}Lines`][index];
|
|
2199
|
+
if (line.kind !== "context" && !line.segments) line.inlineDetail = "omitted";
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
return model;
|
|
2204
|
+
}
|
|
2205
|
+
function mergeTouchingBlocks(blocks) {
|
|
2206
|
+
const regions = [];
|
|
2207
|
+
let oneSidedRun = false;
|
|
2208
|
+
for (const original of blocks) {
|
|
2209
|
+
const oneSided = original.kind !== "replace";
|
|
2210
|
+
const previous = regions[regions.length - 1];
|
|
2211
|
+
if (oneSided && oneSidedRun && previous.leftEnd === original.leftStart && previous.rightEnd === original.rightStart) {
|
|
2212
|
+
previous.leftEnd = original.leftEnd;
|
|
2213
|
+
previous.rightEnd = original.rightEnd;
|
|
2214
|
+
if (previous.leftEnd > previous.leftStart && previous.rightEnd > previous.rightStart) {
|
|
2215
|
+
previous.kind = "replace";
|
|
2216
|
+
}
|
|
2217
|
+
} else regions.push({ ...original });
|
|
2218
|
+
oneSidedRun = oneSided;
|
|
2219
|
+
}
|
|
2220
|
+
const merged = [];
|
|
2221
|
+
for (const original of regions) {
|
|
2222
|
+
const block = { ...original };
|
|
2223
|
+
delete block.reflow;
|
|
2224
|
+
const previous = merged[merged.length - 1];
|
|
2225
|
+
if ((previous == null ? void 0 : previous.kind) === block.kind && previous.leftEnd === block.leftStart && previous.rightEnd === block.rightStart) {
|
|
2226
|
+
previous.leftEnd = block.leftEnd;
|
|
2227
|
+
previous.rightEnd = block.rightEnd;
|
|
2228
|
+
} else merged.push(block);
|
|
2229
|
+
}
|
|
2230
|
+
return merged;
|
|
2118
2231
|
}
|
|
2119
2232
|
function internDiffLines(leftLines, rightLines) {
|
|
2120
2233
|
const lineIds = /* @__PURE__ */ new Map();
|
|
@@ -2167,14 +2280,14 @@ function buildConservativeTwoWayDiffModel(leftLines, rightLines) {
|
|
|
2167
2280
|
} else if (rightLines.length === 0) {
|
|
2168
2281
|
blockKind = "delete";
|
|
2169
2282
|
}
|
|
2170
|
-
return {
|
|
2283
|
+
return markUndetailedReplacements({
|
|
2171
2284
|
rows,
|
|
2172
2285
|
leftLines: renderedLeftLines,
|
|
2173
2286
|
rightLines: renderedRightLines,
|
|
2174
2287
|
blocks: rows.length === 0 ? [] : [makeDiffBlock(blockKind, 0, leftLines.length, 0, rightLines.length)],
|
|
2175
2288
|
hasChanges: rows.length > 0,
|
|
2176
2289
|
quality: "fallback"
|
|
2177
|
-
};
|
|
2290
|
+
});
|
|
2178
2291
|
}
|
|
2179
2292
|
function normalizeLines(content) {
|
|
2180
2293
|
if (content.length === 0) {
|
|
@@ -2189,33 +2302,64 @@ function normalizeLines(content) {
|
|
|
2189
2302
|
function appendReplacementCandidate(leftLines, rightLines, initialLeftLineNumber, initialRightLineNumber, rows, renderedLeftLines, renderedRightLines, blocks) {
|
|
2190
2303
|
const aligned = alignReplacementLines(leftLines, rightLines);
|
|
2191
2304
|
const crediblePairs = aligned.filter((row) => row.left !== void 0 && row.right !== void 0 && hasCredibleLineCorrespondence(row.left, row.right)).length;
|
|
2192
|
-
const coherentBlock = hasCoherentBlockCorrespondence(leftLines, rightLines, crediblePairs);
|
|
2193
|
-
const
|
|
2194
|
-
if (row.left === void 0 || row.right === void 0 || hasCredibleLineCorrespondence(row.left, row.right)) {
|
|
2305
|
+
const coherentBlock = !aligned.some((row) => row.reflow) && hasCoherentBlockCorrespondence(leftLines, rightLines, crediblePairs);
|
|
2306
|
+
const candidates = aligned.flatMap((row) => {
|
|
2307
|
+
if (row.reflow || row.left === void 0 || row.right === void 0 || hasCredibleLineCorrespondence(row.left, row.right)) {
|
|
2195
2308
|
return [row];
|
|
2196
2309
|
}
|
|
2197
2310
|
return [{ left: row.left }, { right: row.right }];
|
|
2198
2311
|
});
|
|
2312
|
+
const normalized = [];
|
|
2313
|
+
for (let index = 0; index < candidates.length; ) {
|
|
2314
|
+
const row = candidates[index];
|
|
2315
|
+
if (row.reflow || row.left !== void 0 && row.right !== void 0) {
|
|
2316
|
+
normalized.push(row);
|
|
2317
|
+
index++;
|
|
2318
|
+
continue;
|
|
2319
|
+
}
|
|
2320
|
+
const start = index;
|
|
2321
|
+
while (index < candidates.length && !candidates[index].reflow && (candidates[index].left === void 0 || candidates[index].right === void 0)) index++;
|
|
2322
|
+
const gap = candidates.slice(start, index);
|
|
2323
|
+
const left = gap.filter((item) => item.left !== void 0);
|
|
2324
|
+
const right = gap.filter((item) => item.right !== void 0);
|
|
2325
|
+
if (left.length === 1 && right.length === 1) {
|
|
2326
|
+
normalized.push({ left: left[0].left, right: right[0].right });
|
|
2327
|
+
} else {
|
|
2328
|
+
for (const item of gap) normalized.push(item);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2199
2331
|
let leftLineNumber = initialLeftLineNumber;
|
|
2200
2332
|
let rightLineNumber = initialRightLineNumber;
|
|
2333
|
+
let unmatchedLeft = [];
|
|
2334
|
+
let unmatchedRight = [];
|
|
2335
|
+
const flushUnmatched = () => {
|
|
2336
|
+
applyWrappedReplacementHighlights(unmatchedLeft, unmatchedRight);
|
|
2337
|
+
unmatchedLeft = [];
|
|
2338
|
+
unmatchedRight = [];
|
|
2339
|
+
};
|
|
2201
2340
|
let blockKind;
|
|
2341
|
+
let blockReflow = false;
|
|
2202
2342
|
let blockLeftStart = renderedLeftLines.length;
|
|
2203
2343
|
let blockRightStart = renderedRightLines.length;
|
|
2204
2344
|
const flushBlock = () => {
|
|
2205
2345
|
if (!blockKind) return;
|
|
2206
|
-
|
|
2346
|
+
const block = makeDiffBlock(
|
|
2207
2347
|
blockKind,
|
|
2208
2348
|
blockLeftStart,
|
|
2209
2349
|
renderedLeftLines.length,
|
|
2210
2350
|
blockRightStart,
|
|
2211
2351
|
renderedRightLines.length
|
|
2212
|
-
)
|
|
2352
|
+
);
|
|
2353
|
+
if (blockReflow) block.reflow = true;
|
|
2354
|
+
blocks.push(block);
|
|
2213
2355
|
};
|
|
2214
|
-
for (const { left, right } of normalized) {
|
|
2215
|
-
|
|
2216
|
-
|
|
2356
|
+
for (const { left, right, reflow } of normalized) {
|
|
2357
|
+
if (reflow || left !== void 0 && right !== void 0) flushUnmatched();
|
|
2358
|
+
const nextKind = coherentBlock || reflow ? "replace" : left !== void 0 && right !== void 0 ? "replace" : left !== void 0 ? "delete" : "insert";
|
|
2359
|
+
if (blockKind !== nextKind || blockReflow !== Boolean(reflow) || (reflow == null ? void 0 : reflow.start)) {
|
|
2217
2360
|
flushBlock();
|
|
2218
2361
|
blockKind = nextKind;
|
|
2362
|
+
blockReflow = Boolean(reflow);
|
|
2219
2363
|
blockLeftStart = renderedLeftLines.length;
|
|
2220
2364
|
blockRightStart = renderedRightLines.length;
|
|
2221
2365
|
}
|
|
@@ -2229,21 +2373,30 @@ function appendReplacementCandidate(leftLines, rightLines, initialLeftLineNumber
|
|
|
2229
2373
|
renderedRightLine = makeDiffLine("added", right, rightLineNumber);
|
|
2230
2374
|
renderedRightLines.push(renderedRightLine);
|
|
2231
2375
|
}
|
|
2232
|
-
if (
|
|
2376
|
+
if (reflow) {
|
|
2377
|
+
if (renderedLeftLine) renderedLeftLine.segments = reflow.leftSegments;
|
|
2378
|
+
if (renderedRightLine) renderedRightLine.segments = reflow.rightSegments;
|
|
2379
|
+
} else if (renderedLeftLine && renderedRightLine) {
|
|
2233
2380
|
applyInlineHighlightPair(renderedLeftLine, renderedRightLine);
|
|
2234
2381
|
} else if (coherentBlock) {
|
|
2235
2382
|
if (renderedLeftLine) applyFullLineHighlight(renderedLeftLine);
|
|
2236
2383
|
if (renderedRightLine) applyFullLineHighlight(renderedRightLine);
|
|
2237
2384
|
}
|
|
2385
|
+
if (!reflow && (left === void 0 || right === void 0)) {
|
|
2386
|
+
if (renderedLeftLine) unmatchedLeft.push(renderedLeftLine);
|
|
2387
|
+
if (renderedRightLine) unmatchedRight.push(renderedRightLine);
|
|
2388
|
+
}
|
|
2238
2389
|
rows.push(makeDiffRow(
|
|
2239
2390
|
left === void 0 ? makePlaceholder() : makeDiffCell("removed", left, leftLineNumber++),
|
|
2240
2391
|
right === void 0 ? makePlaceholder() : makeDiffCell("added", right, rightLineNumber++)
|
|
2241
2392
|
));
|
|
2242
2393
|
}
|
|
2243
2394
|
flushBlock();
|
|
2395
|
+
flushUnmatched();
|
|
2244
2396
|
return { left: leftLineNumber, right: rightLineNumber };
|
|
2245
2397
|
}
|
|
2246
2398
|
function hasCredibleLineCorrespondence(left, right) {
|
|
2399
|
+
if (!left.trim() && !right.trim()) return true;
|
|
2247
2400
|
const score = scoreReplacementLinePair(left, right);
|
|
2248
2401
|
if (score.eligible) return true;
|
|
2249
2402
|
const leftAnchor = declarationAnchorKey(left);
|
|
@@ -2296,24 +2449,101 @@ var MAX_ALIGNMENT_CHARACTER_CELLS = 5e7;
|
|
|
2296
2449
|
var MAX_RARE_TOKEN_OCCURRENCES = 4;
|
|
2297
2450
|
var MAX_CONTEXTUAL_HUNK_LINES = 8;
|
|
2298
2451
|
var MAX_CONTEXTUAL_LENGTH_DELTA = 4;
|
|
2452
|
+
function wrappedTextUnits(lines) {
|
|
2453
|
+
const units = [];
|
|
2454
|
+
for (let index = 0; index < lines.length; index++) {
|
|
2455
|
+
const first = lines[index];
|
|
2456
|
+
const paragraph = [first];
|
|
2457
|
+
const start = first.match(/^(\s*)(?:[\w-]+:\s+|[-*+]\s+)(\S.*)$/);
|
|
2458
|
+
if (start && !/^[|>\[{]/.test(start[2])) {
|
|
2459
|
+
while (index + 1 < lines.length) {
|
|
2460
|
+
const next = lines[index + 1];
|
|
2461
|
+
if (!next.trim() || next.length - next.trimStart().length <= start[1].length || /^\s*(?:[\w-]+:|[-*+]\s|\d+[.)]\s|#)/.test(next)) break;
|
|
2462
|
+
paragraph.push(next);
|
|
2463
|
+
index++;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
units.push(paragraph.join("\n"));
|
|
2467
|
+
}
|
|
2468
|
+
return units;
|
|
2469
|
+
}
|
|
2299
2470
|
function alignReplacementLines(leftLines, rightLines) {
|
|
2471
|
+
if ([...leftLines, ...rightLines].some((line) => line.includes("\n"))) {
|
|
2472
|
+
return alignPhysicalReplacementLines(leftLines, rightLines);
|
|
2473
|
+
}
|
|
2474
|
+
const leftUnits = wrappedTextUnits(leftLines);
|
|
2475
|
+
const rightUnits = wrappedTextUnits(rightLines);
|
|
2476
|
+
if (leftUnits.length === leftLines.length && rightUnits.length === rightLines.length) {
|
|
2477
|
+
return alignPhysicalReplacementLines(leftLines, rightLines);
|
|
2478
|
+
}
|
|
2479
|
+
return alignPhysicalReplacementLines(leftUnits, rightUnits).flatMap((row) => {
|
|
2480
|
+
var _a, _b;
|
|
2481
|
+
const left = ((_a = row.left) == null ? void 0 : _a.split("\n")) || [];
|
|
2482
|
+
const right = ((_b = row.right) == null ? void 0 : _b.split("\n")) || [];
|
|
2483
|
+
if (!left.length || !right.length) {
|
|
2484
|
+
return [...left.map((left2) => ({ left: left2 })), ...right.map((right2) => ({ right: right2 }))];
|
|
2485
|
+
}
|
|
2486
|
+
if (left.length === 1 && right.length === 1) return [row];
|
|
2487
|
+
const whitespace = buildWhitespaceOnlySegments(row.left, row.right);
|
|
2488
|
+
const segments = whitespace || buildInlineSegments(row.left, row.right);
|
|
2489
|
+
const leftSegments = segments && splitSegmentsIntoLines(segments.leftSegments);
|
|
2490
|
+
const rightSegments = segments && splitSegmentsIntoLines(segments.rightSegments);
|
|
2491
|
+
return Array.from({ length: Math.max(left.length, right.length) }, (_, index) => ({
|
|
2492
|
+
left: left[index],
|
|
2493
|
+
right: right[index],
|
|
2494
|
+
reflow: { start: index === 0, leftSegments: leftSegments == null ? void 0 : leftSegments[index], rightSegments: rightSegments == null ? void 0 : rightSegments[index] }
|
|
2495
|
+
}));
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
function alignPhysicalReplacementLines(leftLines, rightLines) {
|
|
2300
2499
|
if (leftLines.length === 0) {
|
|
2301
2500
|
return rightLines.map((right) => ({ right }));
|
|
2302
2501
|
}
|
|
2303
2502
|
if (rightLines.length === 0) {
|
|
2304
2503
|
return leftLines.map((left) => ({ left }));
|
|
2305
2504
|
}
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
if (
|
|
2505
|
+
let prefix = 0;
|
|
2506
|
+
while (prefix < Math.min(leftLines.length, rightLines.length) && leftLines[prefix].trim() === rightLines[prefix].trim()) prefix++;
|
|
2507
|
+
if (prefix) {
|
|
2309
2508
|
return [
|
|
2310
|
-
...leftLines.map((left) => ({ left })),
|
|
2311
|
-
...
|
|
2509
|
+
...leftLines.slice(0, prefix).map((left, index) => ({ left, right: rightLines[index] })),
|
|
2510
|
+
...alignReplacementLines(leftLines.slice(prefix), rightLines.slice(prefix))
|
|
2312
2511
|
];
|
|
2313
2512
|
}
|
|
2513
|
+
let leftEnd = 1;
|
|
2514
|
+
let rightEnd = 1;
|
|
2515
|
+
let leftText = leftLines[0].trim().replace(/\s+/g, " ");
|
|
2516
|
+
let rightText = rightLines[0].trim().replace(/\s+/g, " ");
|
|
2517
|
+
while (leftText && rightText && leftText !== rightText) {
|
|
2518
|
+
if (rightText.startsWith(leftText + " ") && leftEnd < leftLines.length) {
|
|
2519
|
+
leftText += " " + leftLines[leftEnd++].trim().replace(/\s+/g, " ");
|
|
2520
|
+
} else if (leftText.startsWith(rightText + " ") && rightEnd < rightLines.length) {
|
|
2521
|
+
rightText += " " + rightLines[rightEnd++].trim().replace(/\s+/g, " ");
|
|
2522
|
+
} else break;
|
|
2523
|
+
}
|
|
2524
|
+
if (leftText === rightText && (leftEnd > 1 || rightEnd > 1) && ![...leftLines, ...rightLines].some((line) => line.includes("\n"))) {
|
|
2525
|
+
const reflow = buildWhitespaceOnlySegments(leftLines.slice(0, leftEnd).join("\n"), rightLines.slice(0, rightEnd).join("\n"));
|
|
2526
|
+
if (reflow) {
|
|
2527
|
+
const leftSegments = splitSegmentsIntoLines(reflow.leftSegments);
|
|
2528
|
+
const rightSegments = splitSegmentsIntoLines(reflow.rightSegments);
|
|
2529
|
+
return [
|
|
2530
|
+
...Array.from({ length: Math.max(leftEnd, rightEnd) }, (_, index) => ({
|
|
2531
|
+
left: index < leftEnd ? leftLines[index] : void 0,
|
|
2532
|
+
right: index < rightEnd ? rightLines[index] : void 0,
|
|
2533
|
+
reflow: { start: index === 0, leftSegments: leftSegments[index], rightSegments: rightSegments[index] }
|
|
2534
|
+
})),
|
|
2535
|
+
...alignReplacementLines(leftLines.slice(leftEnd), rightLines.slice(rightEnd))
|
|
2536
|
+
];
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
const leftCharacterCount = leftLines.reduce((total, line) => total + Math.min(line.length, MAX_SCORING_LINE_LENGTH), 0);
|
|
2540
|
+
const rightCharacterCount = rightLines.reduce((total, line) => total + Math.min(line.length, MAX_SCORING_LINE_LENGTH), 0);
|
|
2541
|
+
if (leftCharacterCount * rightCharacterCount > MAX_ALIGNMENT_CHARACTER_CELLS) {
|
|
2542
|
+
return alignLargeReplacementLines(leftLines, rightLines, true);
|
|
2543
|
+
}
|
|
2314
2544
|
if (leftLines.length === 1 && rightLines.length === 1) {
|
|
2315
2545
|
const singletonScore = scoreReplacementLinePair(leftLines[0], rightLines[0]);
|
|
2316
|
-
if ((singletonScore.eligible || singletonScore.score >= MINIMUM_SINGLE_PAIR_SCORE) && hasCredibleLineCorrespondence(leftLines[0], rightLines[0]) && haveCompatibleLineRoles(leftLines[0], rightLines[0]) && isInformativeLine(normalizeMatchingContent(leftLines[0])) && isInformativeLine(normalizeMatchingContent(rightLines[0]))) {
|
|
2546
|
+
if (!leftLines[0].trim() && !rightLines[0].trim() || (singletonScore.eligible || singletonScore.score >= MINIMUM_SINGLE_PAIR_SCORE) && hasCredibleLineCorrespondence(leftLines[0], rightLines[0]) && haveCompatibleLineRoles(leftLines[0], rightLines[0]) && isInformativeLine(normalizeMatchingContent(leftLines[0])) && isInformativeLine(normalizeMatchingContent(rightLines[0]))) {
|
|
2317
2547
|
return [{ left: leftLines[0], right: rightLines[0] }];
|
|
2318
2548
|
}
|
|
2319
2549
|
return [{ left: leftLines[0] }, { right: rightLines[0] }];
|
|
@@ -2375,16 +2605,7 @@ function scoreReplacementLinePair(left, right) {
|
|
|
2375
2605
|
const normalizedRight = normalizeMatchingContent(right);
|
|
2376
2606
|
const leftInformative = isInformativeLine(normalizedLeft);
|
|
2377
2607
|
const rightInformative = isInformativeLine(normalizedRight);
|
|
2378
|
-
if (
|
|
2379
|
-
return {
|
|
2380
|
-
score: 0,
|
|
2381
|
-
eligible: false,
|
|
2382
|
-
characterSimilarity: 0,
|
|
2383
|
-
tokenSimilarity: 0,
|
|
2384
|
-
lengthSimilarity: 0
|
|
2385
|
-
};
|
|
2386
|
-
}
|
|
2387
|
-
if (normalizedLeft === normalizedRight) {
|
|
2608
|
+
if (leftInformative && rightInformative && normalizedLeft === normalizedRight) {
|
|
2388
2609
|
return {
|
|
2389
2610
|
score: 1,
|
|
2390
2611
|
eligible: true,
|
|
@@ -2393,14 +2614,30 @@ function scoreReplacementLinePair(left, right) {
|
|
|
2393
2614
|
lengthSimilarity: 1
|
|
2394
2615
|
};
|
|
2395
2616
|
}
|
|
2617
|
+
if (!leftInformative || !rightInformative) {
|
|
2618
|
+
return {
|
|
2619
|
+
score: 0,
|
|
2620
|
+
eligible: false,
|
|
2621
|
+
characterSimilarity: 0,
|
|
2622
|
+
tokenSimilarity: 0,
|
|
2623
|
+
lengthSimilarity: 0
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2396
2626
|
const leftTokens = tokenizeMatchingContent(normalizedLeft);
|
|
2397
2627
|
const rightTokens = tokenizeMatchingContent(normalizedRight);
|
|
2398
|
-
const
|
|
2628
|
+
const sample = (text) => text.length <= MAX_SCORING_LINE_LENGTH ? text : [0, Math.floor(text.length / 2) - 300, text.length - 600].map((start) => text.slice(start, start + 600)).join(" ");
|
|
2629
|
+
const characterSimilarity = lineSimilarity(sample(normalizedLeft), sample(normalizedRight));
|
|
2399
2630
|
const tokenSimilarity = multisetDiceSimilarity(leftTokens, rightTokens);
|
|
2400
2631
|
const lengthSimilarity = Math.min(normalizedLeft.length, normalizedRight.length) / Math.max(normalizedLeft.length, normalizedRight.length);
|
|
2401
2632
|
const boundarySimilarity = tokenBoundarySimilarity(leftTokens, rightTokens);
|
|
2402
|
-
const
|
|
2403
|
-
|
|
2633
|
+
const prefix = (text) => {
|
|
2634
|
+
var _a;
|
|
2635
|
+
return (_a = text.match(/^(?:(?:const|let|var)\s+)?([\p{L}_$][\p{L}\p{N}_$.-]*\s*(?::|=(?!=)))\s*/u)) == null ? void 0 : _a[1].replace(/\s+/g, "");
|
|
2636
|
+
};
|
|
2637
|
+
const leftPrefix = prefix(normalizedLeft);
|
|
2638
|
+
const prefixBonus = leftPrefix && leftPrefix === prefix(normalizedRight) ? 0.12 : 0;
|
|
2639
|
+
const score = Math.min(1, characterSimilarity * 0.52 + tokenSimilarity * 0.32 + lengthSimilarity * 0.1 + boundarySimilarity * 0.06 + prefixBonus);
|
|
2640
|
+
const hasContentEvidence = prefixBonus > 0 || characterSimilarity >= 0.52 || tokenSimilarity >= 0.45 || characterSimilarity >= 0.48 && tokenSimilarity >= 0.28;
|
|
2404
2641
|
return {
|
|
2405
2642
|
score,
|
|
2406
2643
|
eligible: hasContentEvidence && score >= MINIMUM_MATCH_SCORE,
|
|
@@ -2550,8 +2787,11 @@ function rankEligibleScores(scores) {
|
|
|
2550
2787
|
}
|
|
2551
2788
|
return { best, second };
|
|
2552
2789
|
}
|
|
2553
|
-
function alignLargeReplacementLines(leftLines, rightLines) {
|
|
2554
|
-
const anchors = collectLargeHunkAnchors(leftLines, rightLines);
|
|
2790
|
+
function alignLargeReplacementLines(leftLines, rightLines, exactOnly = false) {
|
|
2791
|
+
const anchors = collectLargeHunkAnchors(leftLines, rightLines, exactOnly);
|
|
2792
|
+
if (anchors.length === 0) {
|
|
2793
|
+
return [...leftLines.map((left) => ({ left })), ...rightLines.map((right) => ({ right }))];
|
|
2794
|
+
}
|
|
2555
2795
|
const aligned = [];
|
|
2556
2796
|
let leftStart = 0;
|
|
2557
2797
|
let rightStart = 0;
|
|
@@ -2568,8 +2808,8 @@ function alignLargeReplacementLines(leftLines, rightLines) {
|
|
|
2568
2808
|
appendBoundedSegment(aligned, leftLines.slice(leftStart), rightLines.slice(rightStart));
|
|
2569
2809
|
return aligned;
|
|
2570
2810
|
}
|
|
2571
|
-
function collectLargeHunkAnchors(leftLines, rightLines) {
|
|
2572
|
-
const candidatePairs = collectRareTokenCandidatePairs(leftLines, rightLines);
|
|
2811
|
+
function collectLargeHunkAnchors(leftLines, rightLines, exactOnly) {
|
|
2812
|
+
const candidatePairs = collectRareTokenCandidatePairs(leftLines, rightLines, exactOnly);
|
|
2573
2813
|
if (candidatePairs === null) {
|
|
2574
2814
|
return [];
|
|
2575
2815
|
}
|
|
@@ -2598,7 +2838,7 @@ function collectLargeHunkAnchors(leftLines, rightLines) {
|
|
|
2598
2838
|
const anchors = selectMaximumWeightMonotonicAnchors(confident, rightLines.length);
|
|
2599
2839
|
return anchors.length <= MAX_LARGE_HUNK_ANCHORS ? anchors : [];
|
|
2600
2840
|
}
|
|
2601
|
-
function collectRareTokenCandidatePairs(leftLines, rightLines) {
|
|
2841
|
+
function collectRareTokenCandidatePairs(leftLines, rightLines, exactOnly) {
|
|
2602
2842
|
const leftExact = buildLinePostings(leftLines, normalizeMatchingContent);
|
|
2603
2843
|
const rightExact = buildLinePostings(rightLines, normalizeMatchingContent);
|
|
2604
2844
|
const pairs = /* @__PURE__ */ new Map();
|
|
@@ -2614,6 +2854,9 @@ function collectRareTokenCandidatePairs(leftLines, rightLines) {
|
|
|
2614
2854
|
return null;
|
|
2615
2855
|
}
|
|
2616
2856
|
}
|
|
2857
|
+
if (exactOnly) {
|
|
2858
|
+
return [...pairs.values()];
|
|
2859
|
+
}
|
|
2617
2860
|
const leftTokens = buildTokenPostings(leftLines);
|
|
2618
2861
|
const rightTokens = buildTokenPostings(rightLines);
|
|
2619
2862
|
for (const [token, leftIndices] of leftTokens) {
|
|
@@ -2826,22 +3069,113 @@ function lineSimilarity(left, right) {
|
|
|
2826
3069
|
return 2 * commonLength / maxLength;
|
|
2827
3070
|
}
|
|
2828
3071
|
function applyInlineHighlightPair(leftLine, rightLine) {
|
|
2829
|
-
|
|
3072
|
+
const whitespace = buildWhitespaceOnlySegments(leftLine.content, rightLine.content);
|
|
3073
|
+
if (whitespace) {
|
|
3074
|
+
leftLine.segments = whitespace.leftSegments;
|
|
3075
|
+
rightLine.segments = whitespace.rightSegments;
|
|
2830
3076
|
return;
|
|
2831
3077
|
}
|
|
2832
|
-
const
|
|
2833
|
-
if (
|
|
2834
|
-
leftLine.segments = leftSegments;
|
|
2835
|
-
rightLine.segments = rightSegments;
|
|
2836
|
-
}
|
|
3078
|
+
const segments = buildInlineSegments(leftLine.content, rightLine.content);
|
|
3079
|
+
if (segments) {
|
|
3080
|
+
leftLine.segments = segments.leftSegments;
|
|
3081
|
+
rightLine.segments = segments.rightSegments;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
function applyWrappedReplacementHighlights(leftLines, rightLines) {
|
|
3085
|
+
if (!leftLines.length || !rightLines.length || leftLines.length === 1 && rightLines.length === 1) return;
|
|
3086
|
+
const length = (lines) => lines.reduce((total, line) => total + line.content.length + 1, -1);
|
|
3087
|
+
if (length(leftLines) > MAX_INLINE_HIGHLIGHT_LINE_LENGTH || length(rightLines) > MAX_INLINE_HIGHLIGHT_LINE_LENGTH) return;
|
|
3088
|
+
const left = leftLines.map((line) => line.content).join("\n");
|
|
3089
|
+
const right = rightLines.map((line) => line.content).join("\n");
|
|
3090
|
+
const segments = buildInlineSegments(left, right);
|
|
3091
|
+
if (!segments) return;
|
|
3092
|
+
const { leftSegments, rightSegments } = segments;
|
|
3093
|
+
const leftParts = splitSegmentsIntoLines(leftSegments);
|
|
3094
|
+
const rightParts = splitSegmentsIntoLines(rightSegments);
|
|
3095
|
+
leftLines.forEach((line, index) => {
|
|
3096
|
+
line.segments = leftParts[index];
|
|
3097
|
+
});
|
|
3098
|
+
rightLines.forEach((line, index) => {
|
|
3099
|
+
line.segments = rightParts[index];
|
|
3100
|
+
});
|
|
2837
3101
|
}
|
|
2838
3102
|
function applyFullLineHighlight(line) {
|
|
2839
3103
|
if (/[^\s]/.test(line.content)) {
|
|
2840
3104
|
line.segments = [{ kind: line.kind, text: line.content, emphasis: true }];
|
|
2841
3105
|
}
|
|
2842
3106
|
}
|
|
3107
|
+
var inlineGraphemes = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
3108
|
+
function trimReplacementEdges(left, right) {
|
|
3109
|
+
if (left === right) return left ? [{ value: left }] : [];
|
|
3110
|
+
const leftChars = Array.from(inlineGraphemes.segment(left), (part) => part.segment);
|
|
3111
|
+
const rightChars = Array.from(inlineGraphemes.segment(right), (part) => part.segment);
|
|
3112
|
+
const limit = Math.min(leftChars.length, rightChars.length);
|
|
3113
|
+
let prefix = 0;
|
|
3114
|
+
let suffix = 0;
|
|
3115
|
+
while (prefix < limit && leftChars[prefix] === rightChars[prefix]) prefix++;
|
|
3116
|
+
while (suffix < limit && leftChars[leftChars.length - suffix - 1] === rightChars[rightChars.length - suffix - 1]) suffix++;
|
|
3117
|
+
if (suffix === limit && prefix < limit) prefix = 0;
|
|
3118
|
+
suffix = Math.min(suffix, limit - prefix);
|
|
3119
|
+
return [
|
|
3120
|
+
{ value: leftChars.slice(0, prefix).join("") },
|
|
3121
|
+
{ value: leftChars.slice(prefix, leftChars.length - suffix).join(""), removed: true },
|
|
3122
|
+
{ value: rightChars.slice(prefix, rightChars.length - suffix).join(""), added: true },
|
|
3123
|
+
{ value: suffix ? leftChars.slice(-suffix).join("") : "" }
|
|
3124
|
+
].filter((change) => change.value.length > 0);
|
|
3125
|
+
}
|
|
3126
|
+
function edgeTrimmedWordChanges(left, right) {
|
|
3127
|
+
const words = diffWordsWithSpace(left, right, { timeout: DIFF_TIMEOUT_MS, maxEditLength: 2e3 });
|
|
3128
|
+
if (!words) return void 0;
|
|
3129
|
+
const boundaries = (text) => /* @__PURE__ */ new Set([
|
|
3130
|
+
...Array.from(inlineGraphemes.segment(text), (part) => part.index),
|
|
3131
|
+
text.length
|
|
3132
|
+
]);
|
|
3133
|
+
const leftBoundaries = boundaries(left);
|
|
3134
|
+
const rightBoundaries = boundaries(right);
|
|
3135
|
+
const result = [];
|
|
3136
|
+
let leftOffset = 0;
|
|
3137
|
+
let rightOffset = 0;
|
|
3138
|
+
let pendingLeft = "";
|
|
3139
|
+
let pendingRight = "";
|
|
3140
|
+
for (let index = 0; index < words.length; ) {
|
|
3141
|
+
const word = words[index];
|
|
3142
|
+
if (!word.added && !word.removed) {
|
|
3143
|
+
pendingLeft += word.value;
|
|
3144
|
+
pendingRight += word.value;
|
|
3145
|
+
leftOffset += word.value.length;
|
|
3146
|
+
rightOffset += word.value.length;
|
|
3147
|
+
index++;
|
|
3148
|
+
} else {
|
|
3149
|
+
while (index < words.length && (words[index].added || words[index].removed)) {
|
|
3150
|
+
const change = words[index++];
|
|
3151
|
+
if (change.removed) {
|
|
3152
|
+
pendingLeft += change.value;
|
|
3153
|
+
leftOffset += change.value.length;
|
|
3154
|
+
} else {
|
|
3155
|
+
pendingRight += change.value;
|
|
3156
|
+
rightOffset += change.value.length;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
if (leftBoundaries.has(leftOffset) && rightBoundaries.has(rightOffset)) {
|
|
3161
|
+
for (const change of trimReplacementEdges(pendingLeft, pendingRight)) {
|
|
3162
|
+
const previous = result[result.length - 1];
|
|
3163
|
+
if (previous && previous.added === change.added && previous.removed === change.removed) {
|
|
3164
|
+
previous.value += change.value;
|
|
3165
|
+
} else {
|
|
3166
|
+
result.push(change);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
pendingLeft = "";
|
|
3170
|
+
pendingRight = "";
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
return result;
|
|
3174
|
+
}
|
|
2843
3175
|
function buildInlineSegments(leftContent, rightContent) {
|
|
2844
|
-
|
|
3176
|
+
if (leftContent.length > MAX_INLINE_HIGHLIGHT_LINE_LENGTH || rightContent.length > MAX_INLINE_HIGHLIGHT_LINE_LENGTH) return void 0;
|
|
3177
|
+
const changes = edgeTrimmedWordChanges(leftContent, rightContent);
|
|
3178
|
+
if (!changes) return void 0;
|
|
2845
3179
|
const leftSegments = [];
|
|
2846
3180
|
const rightSegments = [];
|
|
2847
3181
|
let hasInlineChanges = false;
|
|
@@ -2857,7 +3191,7 @@ function buildInlineSegments(leftContent, rightContent) {
|
|
|
2857
3191
|
rightSegments.push(contextSegment);
|
|
2858
3192
|
continue;
|
|
2859
3193
|
}
|
|
2860
|
-
const emphasis =
|
|
3194
|
+
const emphasis = value.length > 0;
|
|
2861
3195
|
hasInlineChanges = hasInlineChanges || emphasis;
|
|
2862
3196
|
if (change.removed) {
|
|
2863
3197
|
leftSegments.push({
|
|
@@ -2880,6 +3214,48 @@ function buildInlineSegments(leftContent, rightContent) {
|
|
|
2880
3214
|
hasInlineChanges
|
|
2881
3215
|
};
|
|
2882
3216
|
}
|
|
3217
|
+
function buildWhitespaceOnlySegments(left, right) {
|
|
3218
|
+
const leftParts = left.split(/(\S+)/);
|
|
3219
|
+
const rightParts = right.split(/(\S+)/);
|
|
3220
|
+
if (leftParts.length !== rightParts.length || leftParts.some((part, index) => index % 2 === 1 && part !== rightParts[index])) return void 0;
|
|
3221
|
+
const leftSegments = [];
|
|
3222
|
+
const rightSegments = [];
|
|
3223
|
+
const append = (segments, kind, text) => {
|
|
3224
|
+
if (!text) return;
|
|
3225
|
+
const previous = segments[segments.length - 1];
|
|
3226
|
+
if ((previous == null ? void 0 : previous.kind) === kind) previous.text += text;
|
|
3227
|
+
else segments.push({ kind, text, emphasis: kind !== "context" });
|
|
3228
|
+
};
|
|
3229
|
+
leftParts.forEach((part, index) => {
|
|
3230
|
+
const other = rightParts[index];
|
|
3231
|
+
let prefix = 0;
|
|
3232
|
+
if (index > 0) {
|
|
3233
|
+
while (prefix < Math.min(part.length, other.length) && part[prefix] === other[prefix]) prefix++;
|
|
3234
|
+
}
|
|
3235
|
+
let suffix = 0;
|
|
3236
|
+
while (suffix < Math.min(part.length, other.length) - prefix && part[part.length - suffix - 1] === other[other.length - suffix - 1]) suffix++;
|
|
3237
|
+
if (index === 0) {
|
|
3238
|
+
while (prefix < Math.min(part.length, other.length) - suffix && part[prefix] === other[prefix]) prefix++;
|
|
3239
|
+
}
|
|
3240
|
+
append(leftSegments, "context", part.slice(0, prefix));
|
|
3241
|
+
append(rightSegments, "context", other.slice(0, prefix));
|
|
3242
|
+
append(leftSegments, "removed", part.slice(prefix, part.length - suffix));
|
|
3243
|
+
append(rightSegments, "added", other.slice(prefix, other.length - suffix));
|
|
3244
|
+
append(leftSegments, "context", part.slice(part.length - suffix));
|
|
3245
|
+
append(rightSegments, "context", other.slice(other.length - suffix));
|
|
3246
|
+
});
|
|
3247
|
+
return { leftSegments, rightSegments };
|
|
3248
|
+
}
|
|
3249
|
+
function splitSegmentsIntoLines(segments) {
|
|
3250
|
+
const lines = [[]];
|
|
3251
|
+
for (const segment of segments) {
|
|
3252
|
+
segment.text.split("\n").forEach((text, index) => {
|
|
3253
|
+
if (index > 0) lines.push([]);
|
|
3254
|
+
if (text) lines[lines.length - 1].push({ ...segment, text });
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
return lines;
|
|
3258
|
+
}
|
|
2883
3259
|
function makePlaceholder() {
|
|
2884
3260
|
return {
|
|
2885
3261
|
kind: "placeholder",
|
|
@@ -3309,20 +3685,16 @@ var CHAPTERS = {
|
|
|
3309
3685
|
};
|
|
3310
3686
|
function buildChangeTourManifest(startPath, options = {}) {
|
|
3311
3687
|
var _a, _b, _c, _d;
|
|
3312
|
-
|
|
3688
|
+
let source = options.source ? parseChangeTourSource(options.source) : void 0;
|
|
3313
3689
|
const range = resolveBranchReviewRange(
|
|
3314
3690
|
startPath,
|
|
3315
3691
|
options.headRef ?? ((_a = source == null ? void 0 : source.range) == null ? void 0 : _a.head),
|
|
3316
3692
|
options.baseRef ?? ((_b = source == null ? void 0 : source.range) == null ? void 0 : _b.base)
|
|
3317
3693
|
);
|
|
3318
|
-
if (source
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
headOid: range.headOid
|
|
3323
|
-
}
|
|
3324
|
-
});
|
|
3325
|
-
}
|
|
3694
|
+
if (source) source = pinTourSource(source, range.repoRoot, /* @__PURE__ */ new Map([
|
|
3695
|
+
[range.headRef, range.headOid],
|
|
3696
|
+
[range.baseRef, range.baseOid]
|
|
3697
|
+
]), range.baseOid, range.headOid);
|
|
3326
3698
|
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_TOUR_FILE_BYTES;
|
|
3327
3699
|
const maxLineBytes = options.maxLineBytes ?? DEFAULT_MAX_TOUR_LINE_BYTES;
|
|
3328
3700
|
const omittedFiles = [];
|
|
@@ -3403,7 +3775,7 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
3403
3775
|
};
|
|
3404
3776
|
defaultScenes = [...defaultScenes].sort((left, right) => Number(right.path === attention.entryPath) - Number(left.path === attention.entryPath) || focusRank(left.path) - focusRank(right.path) || left.id.localeCompare(right.id));
|
|
3405
3777
|
}
|
|
3406
|
-
const authored = source ?
|
|
3778
|
+
const authored = source ? applySourceChapters(source, source.chapters, defaultScenes, range.repoRoot, { base: range.mergeBaseOid, head: range.headOid }) : options.story ? applyStory(options.story, defaultScenes) : buildGeneratedTourLanding(
|
|
3407
3779
|
defaultScenes,
|
|
3408
3780
|
buildChapters(sceneRecords, new Map(defaultScenes.map((scene) => [scene.path, scene.id]))),
|
|
3409
3781
|
attention,
|
|
@@ -3425,6 +3797,8 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
3425
3797
|
version: (source == null ? void 0 : source.version) ?? 1,
|
|
3426
3798
|
title: (source == null ? void 0 : source.title) || ((_c = options.story) == null ? void 0 : _c.title) || options.title || `${range.headRef} against ${range.baseRef}`,
|
|
3427
3799
|
windowTitle: source == null ? void 0 : source.windowTitle,
|
|
3800
|
+
opening: source == null ? void 0 : source.opening,
|
|
3801
|
+
conclusion: source == null ? void 0 : source.conclusion,
|
|
3428
3802
|
sourceUrl: (source == null ? void 0 : source.sourceUrl) || ((_d = options.story) == null ? void 0 : _d.sourceUrl) || options.sourceUrl,
|
|
3429
3803
|
generatedAt: options.generatedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
3430
3804
|
range: {
|
|
@@ -3445,9 +3819,9 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
3445
3819
|
files,
|
|
3446
3820
|
chapters,
|
|
3447
3821
|
scenes,
|
|
3448
|
-
...(source == null ? void 0 : source.
|
|
3822
|
+
...(source == null ? void 0 : source.details) !== void 0 ? { details: source.details } : {}
|
|
3449
3823
|
};
|
|
3450
|
-
const authoredModeTours =
|
|
3824
|
+
const authoredModeTours = source && source.version >= 4 ? buildAuthoredModeTours(
|
|
3451
3825
|
source,
|
|
3452
3826
|
authored,
|
|
3453
3827
|
defaultScenes,
|
|
@@ -3502,10 +3876,7 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
3502
3876
|
}
|
|
3503
3877
|
return parseChangeTourManifest(manifest);
|
|
3504
3878
|
}
|
|
3505
|
-
function
|
|
3506
|
-
return applySourceChapters(source, source.chapters, defaultScenes, repoRoot);
|
|
3507
|
-
}
|
|
3508
|
-
function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot) {
|
|
3879
|
+
function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot, imageRevisions) {
|
|
3509
3880
|
var _a, _b;
|
|
3510
3881
|
const available = new Map(defaultScenes.map((scene) => [scene.path, scene]));
|
|
3511
3882
|
const resolvedAnchors = /* @__PURE__ */ new Map();
|
|
@@ -3546,7 +3917,8 @@ function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot)
|
|
|
3546
3917
|
{ ...authoredScene, steps: authoredScene.steps },
|
|
3547
3918
|
available,
|
|
3548
3919
|
resolvedAnchors,
|
|
3549
|
-
connections
|
|
3920
|
+
connections,
|
|
3921
|
+
(image) => compileTourImage(repoRoot, image, imageRevisions[image.revision])
|
|
3550
3922
|
);
|
|
3551
3923
|
finalScenes.push(finalScene);
|
|
3552
3924
|
finalSceneIds.push(finalScene.id);
|
|
@@ -3570,14 +3942,30 @@ function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot)
|
|
|
3570
3942
|
}
|
|
3571
3943
|
continue;
|
|
3572
3944
|
}
|
|
3573
|
-
const scene = buildWalkthroughManifestScene(
|
|
3945
|
+
const scene = buildWalkthroughManifestScene(
|
|
3946
|
+
authoredScene,
|
|
3947
|
+
available,
|
|
3948
|
+
resolvedAnchors,
|
|
3949
|
+
connections,
|
|
3950
|
+
(image) => compileTourImage(repoRoot, image, imageRevisions[image.revision])
|
|
3951
|
+
);
|
|
3574
3952
|
scenes.push(scene);
|
|
3575
3953
|
sceneIds.push(scene.id);
|
|
3576
3954
|
finalScenes.push(scene);
|
|
3577
3955
|
finalSceneIds.push(scene.id);
|
|
3578
3956
|
}
|
|
3579
|
-
chapters.push({
|
|
3580
|
-
|
|
3957
|
+
chapters.push({
|
|
3958
|
+
id: chapter.id,
|
|
3959
|
+
title: chapter.title,
|
|
3960
|
+
...chapter.summary === void 0 ? {} : { summary: chapter.summary },
|
|
3961
|
+
sceneIds
|
|
3962
|
+
});
|
|
3963
|
+
finalChapters.push({
|
|
3964
|
+
id: chapter.id,
|
|
3965
|
+
title: chapter.title,
|
|
3966
|
+
...chapter.summary === void 0 ? {} : { summary: chapter.summary },
|
|
3967
|
+
sceneIds: finalSceneIds
|
|
3968
|
+
});
|
|
3581
3969
|
}
|
|
3582
3970
|
return { scenes, chapters, finalScenes, finalChapters };
|
|
3583
3971
|
}
|
|
@@ -3585,18 +3973,41 @@ function buildAuthoredModeTours(source, root, defaultScenes, repoRoot, mergeBase
|
|
|
3585
3973
|
var _a, _b;
|
|
3586
3974
|
const tours = {};
|
|
3587
3975
|
if ((_a = source.tours) == null ? void 0 : _a.historical) {
|
|
3588
|
-
const compiled = applySourceChapters(source, source.tours.historical.chapters, defaultScenes, repoRoot);
|
|
3976
|
+
const compiled = applySourceChapters(source, source.tours.historical.chapters, defaultScenes, repoRoot, { base: mergeBaseOid, head: headOid });
|
|
3589
3977
|
validateHistoricalStackEndpoints(compiled.scenes, mergeBaseOid, headOid);
|
|
3590
|
-
tours.historical = {
|
|
3591
|
-
|
|
3978
|
+
tours.historical = {
|
|
3979
|
+
opening: source.tours.historical.opening,
|
|
3980
|
+
conclusion: source.tours.historical.conclusion,
|
|
3981
|
+
chapters: compiled.chapters,
|
|
3982
|
+
scenes: compiled.scenes,
|
|
3983
|
+
...source.tours.historical.details !== void 0 ? { details: source.tours.historical.details } : {}
|
|
3984
|
+
};
|
|
3985
|
+
} else if (source.version >= 4) {
|
|
3592
3986
|
const fallback = buildHistoricalFallback(source.chapters, root);
|
|
3593
|
-
if (fallback)
|
|
3987
|
+
if (fallback) {
|
|
3988
|
+
tours.historical = {
|
|
3989
|
+
...fallback,
|
|
3990
|
+
...root.scenes.every((scene) => scene.kind !== "deconstructed-diff") ? { opening: source.opening, conclusion: source.conclusion, details: source.details } : {}
|
|
3991
|
+
};
|
|
3992
|
+
}
|
|
3594
3993
|
}
|
|
3595
3994
|
if ((_b = source.tours) == null ? void 0 : _b.deconstructed) {
|
|
3596
|
-
const compiled = applySourceChapters(source, source.tours.deconstructed.chapters, defaultScenes, repoRoot);
|
|
3597
|
-
tours.deconstructed = {
|
|
3598
|
-
|
|
3599
|
-
|
|
3995
|
+
const compiled = applySourceChapters(source, source.tours.deconstructed.chapters, defaultScenes, repoRoot, { base: mergeBaseOid, head: headOid });
|
|
3996
|
+
tours.deconstructed = {
|
|
3997
|
+
opening: source.tours.deconstructed.opening,
|
|
3998
|
+
conclusion: source.tours.deconstructed.conclusion,
|
|
3999
|
+
chapters: compiled.chapters,
|
|
4000
|
+
scenes: compiled.scenes,
|
|
4001
|
+
...source.tours.deconstructed.details !== void 0 ? { details: source.tours.deconstructed.details } : {}
|
|
4002
|
+
};
|
|
4003
|
+
} else if (source.version >= 4 && source.chapters.some((chapter) => chapter.scenes.some((scene) => scene.kind === "deconstructed-diff"))) {
|
|
4004
|
+
tours.deconstructed = {
|
|
4005
|
+
opening: source.opening,
|
|
4006
|
+
conclusion: source.conclusion,
|
|
4007
|
+
details: source.details,
|
|
4008
|
+
chapters: root.chapters,
|
|
4009
|
+
scenes: root.scenes
|
|
4010
|
+
};
|
|
3600
4011
|
}
|
|
3601
4012
|
return tours.historical || tours.deconstructed ? tours : void 0;
|
|
3602
4013
|
}
|
|
@@ -3621,7 +4032,14 @@ function buildHistoricalFallback(sourceChapters, root) {
|
|
|
3621
4032
|
scenes.push(scene);
|
|
3622
4033
|
sceneIds.push(scene.id);
|
|
3623
4034
|
}
|
|
3624
|
-
if (sceneIds.length > 0)
|
|
4035
|
+
if (sceneIds.length > 0) {
|
|
4036
|
+
chapters.push({
|
|
4037
|
+
id: chapter.id,
|
|
4038
|
+
title: chapter.title,
|
|
4039
|
+
...chapter.summary === void 0 ? {} : { summary: chapter.summary },
|
|
4040
|
+
sceneIds
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
3625
4043
|
}
|
|
3626
4044
|
return scenes.length > 0 ? { chapters, scenes } : void 0;
|
|
3627
4045
|
}
|
|
@@ -3629,11 +4047,12 @@ function compileSceneOverview(overview, includeComparison = true) {
|
|
|
3629
4047
|
if (!overview) return void 0;
|
|
3630
4048
|
return {
|
|
3631
4049
|
kind: overview.kind,
|
|
4050
|
+
...overview.purpose === void 0 ? {} : { purpose: overview.purpose },
|
|
3632
4051
|
...overview.path === void 0 ? {} : { path: overview.path },
|
|
3633
4052
|
...includeComparison && overview.comparison ? { comparison: { ...overview.comparison } } : {}
|
|
3634
4053
|
};
|
|
3635
4054
|
}
|
|
3636
|
-
function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections) {
|
|
4055
|
+
function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections, resolveImage) {
|
|
3637
4056
|
const overview = compileSceneOverview(authoredScene.overview, false);
|
|
3638
4057
|
return {
|
|
3639
4058
|
id: authoredScene.id,
|
|
@@ -3654,6 +4073,7 @@ function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors
|
|
|
3654
4073
|
body: step.body,
|
|
3655
4074
|
depth: step.depth,
|
|
3656
4075
|
requirement: step.requirement,
|
|
4076
|
+
...step.image ? { image: resolveImage(step.image) } : {},
|
|
3657
4077
|
focus,
|
|
3658
4078
|
connection: step.connection ? connections.get(step.connection) : void 0,
|
|
3659
4079
|
diff: { ...diff2, id: `${authoredScene.id}-${step.id}` }
|
|
@@ -3766,8 +4186,10 @@ function buildDeconstructedFocusSteps(sceneId, stages, files) {
|
|
|
3766
4186
|
});
|
|
3767
4187
|
return focuses.map((focus, focusIndex) => ({
|
|
3768
4188
|
id: focusIndex === 0 ? claimDeconstructedStageId(stage.id, usedIds) : claimDeconstructedFocusId(`${stage.id}-focus-${focusIndex + 1}`, reservedIds, usedIds),
|
|
3769
|
-
title:
|
|
3770
|
-
|
|
4189
|
+
title: focusIndex === 0 ? stage.title : `${focus.file} \xB7 Evidence ${focusIndex + 1} of ${focuses.length}`,
|
|
4190
|
+
// A stage is one argument supported by multiple code focuses.
|
|
4191
|
+
// Keep its explanation once in the reading/narration sequence.
|
|
4192
|
+
body: focusIndex === 0 ? stage.narration : "",
|
|
3771
4193
|
file: focus.file,
|
|
3772
4194
|
pairIndex: stageIndex,
|
|
3773
4195
|
side: focus.side,
|
|
@@ -4070,6 +4492,8 @@ function readGitText2(repoRoot, oid, relativePath, maxBytes, maxLineBytes) {
|
|
|
4070
4492
|
if (!relativePath) {
|
|
4071
4493
|
return { kind: "text", content: "" };
|
|
4072
4494
|
}
|
|
4495
|
+
const size = Number((0, import_child_process4.execFileSync)("git", ["cat-file", "-s", `${oid}:${relativePath}`], { cwd: repoRoot, encoding: "utf8" }).trim());
|
|
4496
|
+
if (size > maxBytes) return { kind: "binary-or-large", content: "" };
|
|
4073
4497
|
const content = (0, import_child_process4.execFileSync)("git", ["show", `${oid}:${relativePath}`], {
|
|
4074
4498
|
cwd: repoRoot,
|
|
4075
4499
|
encoding: "buffer",
|
|
@@ -4111,6 +4535,49 @@ function readGitLineStats(repoRoot, baseOid, headOid, filePath, previousPath) {
|
|
|
4111
4535
|
binary: additionsText === "-" || deletionsText === "-"
|
|
4112
4536
|
};
|
|
4113
4537
|
}
|
|
4538
|
+
function pinTourSource(source, repoRoot, resolved, baseOid, headOid) {
|
|
4539
|
+
const copy = JSON.parse(JSON.stringify(source));
|
|
4540
|
+
const pin = (ref) => {
|
|
4541
|
+
let oid = resolved.get(ref);
|
|
4542
|
+
if (!oid) {
|
|
4543
|
+
oid = runGitText(repoRoot, ["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`]);
|
|
4544
|
+
resolved.set(ref, oid);
|
|
4545
|
+
}
|
|
4546
|
+
return oid;
|
|
4547
|
+
};
|
|
4548
|
+
copy.range = { base: baseOid, head: headOid };
|
|
4549
|
+
const chapters = [...copy.chapters, ...Object.values(copy.tours || {}).flatMap((tour) => (tour == null ? void 0 : tour.chapters) || [])];
|
|
4550
|
+
for (const chapter of chapters) for (const scene of chapter.scenes) {
|
|
4551
|
+
if (scene.kind === "stacked-diff") for (const entry of scene.stack) {
|
|
4552
|
+
entry.label ||= entry.ref;
|
|
4553
|
+
entry.ref = pin(entry.ref);
|
|
4554
|
+
}
|
|
4555
|
+
if (scene.kind === "deconstructed-diff") {
|
|
4556
|
+
scene.base = scene.base ? pin(scene.base) : baseOid;
|
|
4557
|
+
scene.target = scene.target ? pin(scene.target) : headOid;
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
return copy;
|
|
4561
|
+
}
|
|
4562
|
+
function compileTourImage(repoRoot, source, revision) {
|
|
4563
|
+
const object = `${revision}:${source.file}`;
|
|
4564
|
+
let size;
|
|
4565
|
+
try {
|
|
4566
|
+
size = Number((0, import_child_process4.execFileSync)("git", ["cat-file", "-s", object], {
|
|
4567
|
+
cwd: repoRoot,
|
|
4568
|
+
encoding: "utf8",
|
|
4569
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4570
|
+
}).trim());
|
|
4571
|
+
} catch {
|
|
4572
|
+
throw new Error(`Image ${source.file} is unavailable at pinned revision ${revision}.`);
|
|
4573
|
+
}
|
|
4574
|
+
if (!Number.isSafeInteger(size) || size < 33 || size > MAX_TOUR_IMAGE_BYTES) {
|
|
4575
|
+
throw new Error(`Image ${source.file} must be a PNG of at most 8 MiB.`);
|
|
4576
|
+
}
|
|
4577
|
+
const bytes = (0, import_child_process4.execFileSync)("git", ["cat-file", "blob", object], { cwd: repoRoot, maxBuffer: MAX_TOUR_IMAGE_BYTES });
|
|
4578
|
+
validatePngHeader(bytes, source.file);
|
|
4579
|
+
return { path: source.file, revision, alt: source.alt, dataUrl: `data:image/png;base64,${bytes.toString("base64")}` };
|
|
4580
|
+
}
|
|
4114
4581
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4115
4582
|
0 && (module.exports = {
|
|
4116
4583
|
CHANGE_ATTENTION_VERSION,
|
|
@@ -4124,6 +4591,7 @@ function readGitLineStats(repoRoot, baseOid, headOid, filePath, previousPath) {
|
|
|
4124
4591
|
materializeChangeUnits,
|
|
4125
4592
|
parseChangeTourManifest,
|
|
4126
4593
|
parseChangeTourSource,
|
|
4127
|
-
parseChangeTourStory
|
|
4594
|
+
parseChangeTourStory,
|
|
4595
|
+
pinTourSource
|
|
4128
4596
|
});
|
|
4129
4597
|
//# sourceMappingURL=changeTour.js.map
|