@davmash/bygone 0.8.14 → 0.9.2
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 +40 -0
- package/README.md +2 -2
- package/cli/commandSpec.js +12 -2
- package/cli/completions.js +1 -1
- package/cli/present.js +89 -7
- package/cli/tour.js +13 -2
- package/cli/tourFile.js +9 -0
- package/cli/tourHistory.js +432 -0
- package/completions/_bygone +2 -1
- package/completions/bygone +1 -1
- package/completions/bygone.fish +1 -0
- package/docs/change-tour-format.md +199 -7
- package/docs/explore.md +21 -1
- package/docs/generating-change-tours.md +70 -17
- package/docs/present.md +134 -5
- package/docs/product-surface.md +64 -8
- package/docs/releasing.md +25 -29
- package/examples/bygone-history.bygone +52 -10
- package/examples/navigation-lab.bygone.yaml +420 -0
- package/media/diff.worker.js +3 -3
- package/media/webview.css +1 -1
- package/media/webview.js +546 -178
- package/out/changeTour.js +1391 -469
- package/out/gitComparison.js +354 -0
- package/out/standalone-main.js +13989 -4749
- package/out/tourCoverage.js +1434 -461
- package/package.json +2 -2
- package/schemas/change-tour-source.schema.json +222 -3
- package/skills/pr-tour-guide/SKILL.md +111 -11
- package/standalone/index.html +6 -1
- package/web/index.html +114 -73
- package/web/presenter.css +658 -230
- package/web/web-host.js +374 -5
package/out/changeTour.js
CHANGED
|
@@ -30,10 +30,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/changeTour.ts
|
|
31
31
|
var changeTour_exports = {};
|
|
32
32
|
__export(changeTour_exports, {
|
|
33
|
+
CHANGE_ATTENTION_VERSION: () => CHANGE_ATTENTION_VERSION,
|
|
34
|
+
buildChangeAttention: () => buildChangeAttention,
|
|
33
35
|
buildChangeInventory: () => buildChangeInventory,
|
|
34
36
|
buildChangeTourContext: () => buildChangeTourContext,
|
|
35
37
|
buildChangeTourManifest: () => buildChangeTourManifest,
|
|
36
38
|
buildDeconstructedScene: () => buildDeconstructedScene,
|
|
39
|
+
classifyChangeFileRole: () => classifyChangeFileRole,
|
|
37
40
|
compileDeconstructedScene: () => compileDeconstructedScene,
|
|
38
41
|
materializeChangeUnits: () => materializeChangeUnits,
|
|
39
42
|
parseChangeTourManifest: () => parseChangeTourManifest,
|
|
@@ -231,33 +234,544 @@ function readGitMaxBuffer() {
|
|
|
231
234
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_GIT_MAX_BUFFER_BYTES;
|
|
232
235
|
}
|
|
233
236
|
|
|
237
|
+
// src/changeTourSource.ts
|
|
238
|
+
var CHANGE_TOUR_SOURCE_VERSION = 3;
|
|
239
|
+
function parseChangeTourSource(value) {
|
|
240
|
+
if (isRecord(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_SOURCE_VERSION) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`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
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (!isRecord(value) || value.version !== 1 && value.version !== 2 && value.version !== CHANGE_TOUR_SOURCE_VERSION) {
|
|
246
|
+
throw new Error("Unsupported or missing change-tour source version.");
|
|
247
|
+
}
|
|
248
|
+
requireOnlyKeys(value, [
|
|
249
|
+
"version",
|
|
250
|
+
"title",
|
|
251
|
+
"windowTitle",
|
|
252
|
+
"sourceUrl",
|
|
253
|
+
"range",
|
|
254
|
+
"anchors",
|
|
255
|
+
"connections",
|
|
256
|
+
"chapters",
|
|
257
|
+
"tours",
|
|
258
|
+
"coverage",
|
|
259
|
+
"review"
|
|
260
|
+
], "source");
|
|
261
|
+
optionalString(value.title, "title");
|
|
262
|
+
optionalString(value.windowTitle, "windowTitle");
|
|
263
|
+
optionalString(value.sourceUrl, "sourceUrl");
|
|
264
|
+
if (value.range !== void 0) {
|
|
265
|
+
if (!isRecord(value.range)) throw new Error("range must be an object.");
|
|
266
|
+
requireOnlyKeys(value.range, ["base", "head"], "range");
|
|
267
|
+
requireString(value.range.base, "range.base");
|
|
268
|
+
requireString(value.range.head, "range.head");
|
|
269
|
+
}
|
|
270
|
+
if (value.coverage !== void 0) {
|
|
271
|
+
if (!isRecord(value.coverage) || !Array.isArray(value.coverage.exclusions)) {
|
|
272
|
+
throw new Error("coverage must contain an exclusions array.");
|
|
273
|
+
}
|
|
274
|
+
requireOnlyKeys(value.coverage, ["exclusions"], "coverage");
|
|
275
|
+
for (const [index, exclusion] of value.coverage.exclusions.entries()) {
|
|
276
|
+
const exclusionPath = `coverage.exclusions[${index}]`;
|
|
277
|
+
if (!isRecord(exclusion)) throw new Error(`${exclusionPath} must be an object.`);
|
|
278
|
+
requireOnlyKeys(exclusion, ["path", "hunks", "reason"], exclusionPath);
|
|
279
|
+
requireString(exclusion.path, `${exclusionPath}.path`);
|
|
280
|
+
requireString(exclusion.reason, `${exclusionPath}.reason`);
|
|
281
|
+
if (exclusion.hunks !== void 0) requireStringArray(exclusion.hunks, `${exclusionPath}.hunks`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!isRecord(value.anchors)) {
|
|
285
|
+
throw new Error("anchors must be an object.");
|
|
286
|
+
}
|
|
287
|
+
for (const [id, anchor] of Object.entries(value.anchors)) {
|
|
288
|
+
if (!isRecord(anchor)) {
|
|
289
|
+
throw new Error(`anchors.${id} must be an object.`);
|
|
290
|
+
}
|
|
291
|
+
requireOnlyKeys(anchor, ["file", "revision", "contains", "occurrence"], `anchors.${id}`);
|
|
292
|
+
requireString(anchor.file, `anchors.${id}.file`);
|
|
293
|
+
if (anchor.revision !== "base" && anchor.revision !== "head") {
|
|
294
|
+
throw new Error(`anchors.${id}.revision must be base or head.`);
|
|
295
|
+
}
|
|
296
|
+
requireString(anchor.contains, `anchors.${id}.contains`);
|
|
297
|
+
if (anchor.occurrence !== void 0 && (!Number.isInteger(anchor.occurrence) || Number(anchor.occurrence) < 1)) {
|
|
298
|
+
throw new Error(`anchors.${id}.occurrence must be a positive integer.`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const rawConnections = Array.isArray(value.connections) ? value.connections : isRecord(value.connections) ? Object.entries(value.connections).map(([id, connection]) => isRecord(connection) ? { id, ...connection } : connection) : null;
|
|
302
|
+
if (!rawConnections || !Array.isArray(value.chapters) || value.chapters.length === 0) {
|
|
303
|
+
throw new Error("connections must be an array or object, and chapters must be a non-empty array.");
|
|
304
|
+
}
|
|
305
|
+
const connectionIds = /* @__PURE__ */ new Set();
|
|
306
|
+
for (const [index, connection] of rawConnections.entries()) {
|
|
307
|
+
if (!isRecord(connection)) {
|
|
308
|
+
throw new Error(`connections[${index}] must be an object.`);
|
|
309
|
+
}
|
|
310
|
+
requireOnlyKeys(connection, ["id", "from", "to", "label"], `connections[${index}]`);
|
|
311
|
+
requireString(connection.id, `connections[${index}].id`);
|
|
312
|
+
requireString(connection.from, `connections[${index}].from`);
|
|
313
|
+
requireString(connection.to, `connections[${index}].to`);
|
|
314
|
+
requireString(connection.label, `connections[${index}].label`);
|
|
315
|
+
const { id, from, to } = connection;
|
|
316
|
+
if (!value.anchors[from] || !value.anchors[to]) {
|
|
317
|
+
throw new Error(`Connection ${id} references an unknown anchor.`);
|
|
318
|
+
}
|
|
319
|
+
if (connectionIds.has(id)) {
|
|
320
|
+
throw new Error(`Duplicate connection id: ${id}`);
|
|
321
|
+
}
|
|
322
|
+
connectionIds.add(id);
|
|
323
|
+
}
|
|
324
|
+
const authoredWalkthroughSteps = validateSourceChapters(
|
|
325
|
+
value.chapters,
|
|
326
|
+
"chapters",
|
|
327
|
+
value.version,
|
|
328
|
+
value.anchors,
|
|
329
|
+
connectionIds,
|
|
330
|
+
"root"
|
|
331
|
+
);
|
|
332
|
+
if (value.tours !== void 0) {
|
|
333
|
+
if (value.version !== 3) throw new Error("Independent tours require version 3.");
|
|
334
|
+
if (!isRecord(value.tours)) throw new Error("tours must be an object.");
|
|
335
|
+
requireOnlyKeys(value.tours, ["historical", "deconstructed"], "tours");
|
|
336
|
+
if (value.tours.historical === void 0 && value.tours.deconstructed === void 0) {
|
|
337
|
+
throw new Error("tours must contain a historical or deconstructed tour.");
|
|
338
|
+
}
|
|
339
|
+
for (const mode of ["historical", "deconstructed"]) {
|
|
340
|
+
const tour = value.tours[mode];
|
|
341
|
+
if (tour === void 0) continue;
|
|
342
|
+
const path = `tours.${mode}`;
|
|
343
|
+
if (!isRecord(tour)) throw new Error(`${path} must be an object.`);
|
|
344
|
+
requireOnlyKeys(tour, ["chapters"], path);
|
|
345
|
+
validateSourceChapters(tour.chapters, `${path}.chapters`, value.version, value.anchors, connectionIds, mode);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
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
|
+
return { ...value, connections: rawConnections };
|
|
353
|
+
}
|
|
354
|
+
function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, connectionIds, mode) {
|
|
355
|
+
if (!Array.isArray(chaptersValue) || chaptersValue.length === 0) {
|
|
356
|
+
throw new Error(`${chaptersPath} must be a non-empty array.`);
|
|
357
|
+
}
|
|
358
|
+
const chapterIds = /* @__PURE__ */ new Set();
|
|
359
|
+
const sceneIds = /* @__PURE__ */ new Set();
|
|
360
|
+
const authoredWalkthroughSteps = /* @__PURE__ */ new Map();
|
|
361
|
+
for (const [chapterIndex, chapter] of chaptersValue.entries()) {
|
|
362
|
+
const chapterPath = `${chaptersPath}[${chapterIndex}]`;
|
|
363
|
+
if (!isRecord(chapter) || !Array.isArray(chapter.scenes) || chapter.scenes.length === 0) {
|
|
364
|
+
throw new Error(`${chapterPath} must contain a non-empty scenes array.`);
|
|
365
|
+
}
|
|
366
|
+
requireString(chapter.id, `${chapterPath}.id`);
|
|
367
|
+
requireString(chapter.title, `${chapterPath}.title`);
|
|
368
|
+
requireOnlyKeys(chapter, ["id", "title", "scenes"], chapterPath);
|
|
369
|
+
if (chapterIds.has(chapter.id)) throw new Error(`Duplicate chapter id: ${chapter.id}`);
|
|
370
|
+
chapterIds.add(chapter.id);
|
|
371
|
+
for (const [sceneIndex, scene] of chapter.scenes.entries()) {
|
|
372
|
+
const path = `${chapterPath}.scenes[${sceneIndex}]`;
|
|
373
|
+
if (!isRecord(scene)) throw new Error(`${path} must be an object.`);
|
|
374
|
+
requireString(scene.id, `${path}.id`);
|
|
375
|
+
requireString(scene.title, `${path}.title`);
|
|
376
|
+
if (sceneIds.has(scene.id)) throw new Error(`Duplicate scene id: ${scene.id}`);
|
|
377
|
+
sceneIds.add(scene.id);
|
|
378
|
+
if (scene.overview !== void 0 && version !== 3) {
|
|
379
|
+
throw new Error(`${path}.overview requires version 3.`);
|
|
380
|
+
}
|
|
381
|
+
validateNarrative(scene, path);
|
|
382
|
+
if (scene.kind === "deconstructed-diff") {
|
|
383
|
+
if (mode === "historical") {
|
|
384
|
+
throw new Error(`${path} cannot contain a deconstructed-diff scene in the historical tour.`);
|
|
385
|
+
}
|
|
386
|
+
validateDeconstructedScene(scene, path);
|
|
387
|
+
validateSceneOverview(scene.overview, `${path}.overview`, {
|
|
388
|
+
endpointIds: deconstructedOverviewEndpointIds(scene, path)
|
|
389
|
+
});
|
|
390
|
+
const requireRealEvidence = mode === "root" && version >= 2;
|
|
391
|
+
if (requireRealEvidence || scene.stack !== void 0) {
|
|
392
|
+
validateRealStack(scene.stack, `${path}.stack`, 2);
|
|
393
|
+
}
|
|
394
|
+
if (requireRealEvidence || scene.steps !== void 0) {
|
|
395
|
+
validateWalkthroughSteps(scene, path, anchors, connectionIds);
|
|
396
|
+
if (scene.steps !== void 0) {
|
|
397
|
+
authoredWalkthroughSteps.set(scene.id, collectStepIds(scene.steps));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
|
|
403
|
+
throw new Error(`${path} must contain a non-empty steps array.`);
|
|
404
|
+
}
|
|
405
|
+
if (scene.kind === "stacked-diff") {
|
|
406
|
+
validateStackedScene(scene, path, mode === "root" ? version >= 2 ? 2 : 3 : 2);
|
|
407
|
+
validateSceneOverview(scene.overview, `${path}.overview`, {
|
|
408
|
+
endpointIds: stackedOverviewEndpointIds(scene, path)
|
|
409
|
+
});
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (scene.kind !== void 0 && scene.kind !== "walkthrough") {
|
|
413
|
+
throw new Error(`${path}.kind must be walkthrough, stacked-diff, or deconstructed-diff.`);
|
|
414
|
+
}
|
|
415
|
+
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "steps"], path);
|
|
416
|
+
validateSceneOverview(scene.overview, `${path}.overview`, { allowComparison: false });
|
|
417
|
+
validateWalkthroughSteps(scene, path, anchors, connectionIds);
|
|
418
|
+
authoredWalkthroughSteps.set(scene.id, collectStepIds(scene.steps));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (mode === "deconstructed" && !chaptersValue.some((chapter) => isRecord(chapter) && Array.isArray(chapter.scenes) && chapter.scenes.some((scene) => isRecord(scene) && scene.kind === "deconstructed-diff"))) {
|
|
422
|
+
throw new Error(`${chaptersPath} must contain at least one deconstructed-diff scene.`);
|
|
423
|
+
}
|
|
424
|
+
return authoredWalkthroughSteps;
|
|
425
|
+
}
|
|
426
|
+
function stackedOverviewEndpointIds(scene, path) {
|
|
427
|
+
if (!Array.isArray(scene.stack)) {
|
|
428
|
+
throw new Error(`${path}.stack must be an array.`);
|
|
429
|
+
}
|
|
430
|
+
return scene.stack.filter(isRecord).map((entry) => typeof entry.id === "string" ? entry.id : "").filter((id) => id.length > 0);
|
|
431
|
+
}
|
|
432
|
+
function deconstructedOverviewEndpointIds(scene, path) {
|
|
433
|
+
if (!Array.isArray(scene.stages)) {
|
|
434
|
+
throw new Error(`${path}.stages must be an array.`);
|
|
435
|
+
}
|
|
436
|
+
return [
|
|
437
|
+
"explanation-baseline",
|
|
438
|
+
...scene.stages.filter(isRecord).map((stage) => typeof stage.id === "string" ? `explanation-stage-${stage.id}` : "").filter((id) => id.length > 0)
|
|
439
|
+
];
|
|
440
|
+
}
|
|
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
|
+
function validateDeconstructedScene(scene, path) {
|
|
509
|
+
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "base", "target", "stack", "steps", "stages", "exclusions"], path);
|
|
510
|
+
optionalString(scene.base, `${path}.base`);
|
|
511
|
+
optionalString(scene.target, `${path}.target`);
|
|
512
|
+
if (!Array.isArray(scene.stages) || scene.stages.length === 0 || scene.stages.length > 12) {
|
|
513
|
+
throw new Error(`${path}.stages must contain between 1 and 12 stages.`);
|
|
514
|
+
}
|
|
515
|
+
const stageIds = /* @__PURE__ */ new Set();
|
|
516
|
+
for (const [stageIndex, stage] of scene.stages.entries()) {
|
|
517
|
+
const stagePath = `${path}.stages[${stageIndex}]`;
|
|
518
|
+
if (!isRecord(stage)) throw new Error(`${stagePath} must be an object.`);
|
|
519
|
+
requireOnlyKeys(stage, ["id", "title", "narration", "changes"], stagePath);
|
|
520
|
+
requireString(stage.id, `${stagePath}.id`);
|
|
521
|
+
requireString(stage.title, `${stagePath}.title`);
|
|
522
|
+
requireString(stage.narration, `${stagePath}.narration`);
|
|
523
|
+
if (!Array.isArray(stage.changes) || stage.changes.length === 0) {
|
|
524
|
+
throw new Error(`${stagePath}.changes must be a non-empty array.`);
|
|
525
|
+
}
|
|
526
|
+
if (stageIds.has(stage.id)) throw new Error(`Duplicate deconstructed stage id: ${stage.id}`);
|
|
527
|
+
stageIds.add(stage.id);
|
|
528
|
+
stage.changes.forEach((change, changeIndex) => validateDeconstructedSelection(
|
|
529
|
+
change,
|
|
530
|
+
`${stagePath}.changes[${changeIndex}]`,
|
|
531
|
+
false
|
|
532
|
+
));
|
|
533
|
+
}
|
|
534
|
+
if (scene.exclusions !== void 0) {
|
|
535
|
+
if (!Array.isArray(scene.exclusions)) throw new Error(`${path}.exclusions must be an array.`);
|
|
536
|
+
scene.exclusions.forEach((exclusion, exclusionIndex) => validateDeconstructedSelection(
|
|
537
|
+
exclusion,
|
|
538
|
+
`${path}.exclusions[${exclusionIndex}]`,
|
|
539
|
+
true
|
|
540
|
+
));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function validateWalkthroughSteps(scene, path, anchors, connectionIds) {
|
|
544
|
+
if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
|
|
545
|
+
throw new Error(`${path} must contain a non-empty steps array.`);
|
|
546
|
+
}
|
|
547
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
548
|
+
for (const [stepIndex, step] of scene.steps.entries()) {
|
|
549
|
+
const stepPath = `${path}.steps[${stepIndex}]`;
|
|
550
|
+
if (!isRecord(step)) throw new Error(`${stepPath} must be an object.`);
|
|
551
|
+
requireString(step.id, `${stepPath}.id`);
|
|
552
|
+
requireString(step.title, `${stepPath}.title`);
|
|
553
|
+
requireString(step.body, `${stepPath}.body`);
|
|
554
|
+
requireString(step.focus, `${stepPath}.focus`);
|
|
555
|
+
requireOnlyKeys(step, ["id", "title", "body", "focus", "connection", "depth", "requirement"], stepPath);
|
|
556
|
+
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
557
|
+
throw new Error(`${stepPath}.depth must be mentioned, explained, or contextualized.`);
|
|
558
|
+
}
|
|
559
|
+
validateStepRequirement(step.requirement, `${stepPath}.requirement`);
|
|
560
|
+
if (stepIds.has(step.id)) throw new Error(`Duplicate step id in scene ${scene.id}: ${step.id}`);
|
|
561
|
+
stepIds.add(step.id);
|
|
562
|
+
if (!anchors[step.focus]) throw new Error(`${stepPath} references unknown anchor ${step.focus}.`);
|
|
563
|
+
optionalString(step.connection, `${stepPath}.connection`);
|
|
564
|
+
if (typeof step.connection === "string" && !connectionIds.has(step.connection)) {
|
|
565
|
+
throw new Error(`${stepPath} references unknown connection ${step.connection}.`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function validateDeconstructedSelection(value, path, requiresReason) {
|
|
570
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
571
|
+
requireOnlyKeys(value, requiresReason ? ["file", "hunks", "reason"] : ["file", "hunks"], path);
|
|
572
|
+
requireString(value.file, `${path}.file`);
|
|
573
|
+
if (!requiresReason || value.hunks !== void 0) {
|
|
574
|
+
requireStringArray(value.hunks, `${path}.hunks`);
|
|
575
|
+
if (value.hunks.length === 0) throw new Error(`${path}.hunks must not be empty.`);
|
|
576
|
+
}
|
|
577
|
+
if (requiresReason) requireString(value.reason, `${path}.reason`);
|
|
578
|
+
}
|
|
579
|
+
function validateRealStack(stack, path, minimum) {
|
|
580
|
+
if (!Array.isArray(stack) || stack.length < minimum || stack.length > 6) {
|
|
581
|
+
throw new Error(`${path} must contain between ${minimum} and 6 explicit real revisions.`);
|
|
582
|
+
}
|
|
583
|
+
const ids = /* @__PURE__ */ new Set();
|
|
584
|
+
for (const [index, entry] of stack.entries()) {
|
|
585
|
+
if (!isRecord(entry)) throw new Error(`${path}[${index}] must be an object.`);
|
|
586
|
+
requireOnlyKeys(entry, ["id", "ref", "label"], `${path}[${index}]`);
|
|
587
|
+
requireString(entry.id, `${path}[${index}].id`);
|
|
588
|
+
requireString(entry.ref, `${path}[${index}].ref`);
|
|
589
|
+
optionalString(entry.label, `${path}[${index}].label`);
|
|
590
|
+
if (ids.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
|
|
591
|
+
ids.add(entry.id);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function validateStackedScene(scene, path, minimum = 3) {
|
|
595
|
+
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "stack", "files", "steps"], path);
|
|
596
|
+
if (!Array.isArray(scene.stack) || scene.stack.length < minimum || scene.stack.length > 6) {
|
|
597
|
+
throw new Error(`${path}.stack must contain between ${minimum} and 6 revisions.`);
|
|
598
|
+
}
|
|
599
|
+
const stackIds = /* @__PURE__ */ new Set();
|
|
600
|
+
for (const [index, entry] of scene.stack.entries()) {
|
|
601
|
+
const entryPath = `${path}.stack[${index}]`;
|
|
602
|
+
if (!isRecord(entry)) throw new Error(`${entryPath} must be an object.`);
|
|
603
|
+
requireOnlyKeys(entry, ["id", "ref", "label"], entryPath);
|
|
604
|
+
requireString(entry.id, `${entryPath}.id`);
|
|
605
|
+
requireString(entry.ref, `${entryPath}.ref`);
|
|
606
|
+
optionalString(entry.label, `${entryPath}.label`);
|
|
607
|
+
if (stackIds.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
|
|
608
|
+
stackIds.add(entry.id);
|
|
609
|
+
}
|
|
610
|
+
if (scene.files !== void 0) requireStringArray(scene.files, `${path}.files`);
|
|
611
|
+
if (!Array.isArray(scene.steps) || scene.steps.length === 0) throw new Error(`${path}.steps must be a non-empty array.`);
|
|
612
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
613
|
+
for (const [index, step] of scene.steps.entries()) {
|
|
614
|
+
const stepPath = `${path}.steps[${index}]`;
|
|
615
|
+
if (!isRecord(step)) throw new Error(`${stepPath} must be an object.`);
|
|
616
|
+
requireOnlyKeys(step, ["id", "title", "body", "file", "pair", "side", "lines"], stepPath);
|
|
617
|
+
requireString(step.id, `${stepPath}.id`);
|
|
618
|
+
requireString(step.title, `${stepPath}.title`);
|
|
619
|
+
requireString(step.body, `${stepPath}.body`);
|
|
620
|
+
requireString(step.file, `${stepPath}.file`);
|
|
621
|
+
const stepId = step.id;
|
|
622
|
+
if (stepIds.has(stepId)) throw new Error(`Duplicate step id in scene ${scene.id}: ${stepId}`);
|
|
623
|
+
stepIds.add(stepId);
|
|
624
|
+
if (!Array.isArray(step.pair) || step.pair.length !== 2 || step.pair.some((id) => typeof id !== "string")) {
|
|
625
|
+
throw new Error(`${stepPath}.pair must contain two stack entry ids.`);
|
|
626
|
+
}
|
|
627
|
+
const pair = step.pair;
|
|
628
|
+
const leftIndex = scene.stack.findIndex((entry) => isRecord(entry) && entry.id === pair[0]);
|
|
629
|
+
const rightIndex = scene.stack.findIndex((entry) => isRecord(entry) && entry.id === pair[1]);
|
|
630
|
+
if (leftIndex < 0 || rightIndex !== leftIndex + 1) throw new Error(`${stepPath}.pair must reference adjacent stack entries in order.`);
|
|
631
|
+
if (step.side !== void 0 && step.side !== "left" && step.side !== "right") throw new Error(`${stepPath}.side must be left or right.`);
|
|
632
|
+
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]))) {
|
|
633
|
+
throw new Error(`${stepPath}.lines must contain an ordered positive line range.`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function validateStepRequirement(value, path) {
|
|
638
|
+
if (value === void 0) return;
|
|
639
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
640
|
+
requireOnlyKeys(value, ["id", "text", "status", "source", "confidence"], path);
|
|
641
|
+
requireString(value.id, `${path}.id`);
|
|
642
|
+
requireString(value.text, `${path}.text`);
|
|
643
|
+
if (value.status !== void 0 && !["fulfilled", "gap"].includes(String(value.status))) {
|
|
644
|
+
throw new Error(`${path}.status must be fulfilled or gap.`);
|
|
645
|
+
}
|
|
646
|
+
optionalString(value.source, `${path}.source`);
|
|
647
|
+
if (value.confidence !== void 0 && !["high", "medium", "low"].includes(String(value.confidence))) {
|
|
648
|
+
throw new Error(`${path}.confidence must be high, medium, or low.`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function validateSceneOverview(value, path, options = {}) {
|
|
652
|
+
if (value === void 0) return;
|
|
653
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
654
|
+
requireOnlyKeys(value, ["kind", "path", "comparison"], path);
|
|
655
|
+
if (value.kind !== "directory-diff") {
|
|
656
|
+
throw new Error(`${path}.kind must be directory-diff.`);
|
|
657
|
+
}
|
|
658
|
+
if (value.path !== void 0) validateDirectoryOverviewPath(value.path, `${path}.path`);
|
|
659
|
+
if (value.comparison === void 0) return;
|
|
660
|
+
if (options.allowComparison === false) {
|
|
661
|
+
throw new Error(`${path}.comparison is not supported for walkthrough scenes; use the fixed base-to-head comparison.`);
|
|
662
|
+
}
|
|
663
|
+
if (!isRecord(value.comparison)) throw new Error(`${path}.comparison must be an object.`);
|
|
664
|
+
requireOnlyKeys(value.comparison, ["from", "to"], `${path}.comparison`);
|
|
665
|
+
requireString(value.comparison.from, `${path}.comparison.from`);
|
|
666
|
+
requireString(value.comparison.to, `${path}.comparison.to`);
|
|
667
|
+
if (value.comparison.from === value.comparison.to) {
|
|
668
|
+
throw new Error(`${path}.comparison.from and to must identify distinct endpoints.`);
|
|
669
|
+
}
|
|
670
|
+
if (options.endpointIds !== void 0) {
|
|
671
|
+
for (const endpoint of [value.comparison.from, value.comparison.to]) {
|
|
672
|
+
if (!options.endpointIds.includes(endpoint)) {
|
|
673
|
+
throw new Error(`${path}.comparison endpoint ${endpoint} is not defined by this scene.`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function validateDirectoryOverviewPath(value, path) {
|
|
679
|
+
requireString(value, path);
|
|
680
|
+
if (value.includes("\\")) {
|
|
681
|
+
throw new Error(`${path} must use a relative POSIX directory path without backslashes.`);
|
|
682
|
+
}
|
|
683
|
+
if (value.startsWith("/") || /^[A-Za-z]:/.test(value)) {
|
|
684
|
+
throw new Error(`${path} must be relative to the repository root.`);
|
|
685
|
+
}
|
|
686
|
+
if (value.split("/").some((segment) => segment === "..")) {
|
|
687
|
+
throw new Error(`${path} must not contain parent-directory traversal.`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
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
|
+
function validateNarrative(value, path) {
|
|
699
|
+
requireString(value.summary, `${path}.summary`);
|
|
700
|
+
requireStringArray(value.bullets, `${path}.bullets`);
|
|
701
|
+
requireStringArray(value.tags, `${path}.tags`);
|
|
702
|
+
requireString(value.takeaway, `${path}.takeaway`);
|
|
703
|
+
}
|
|
704
|
+
function isRecord(value) {
|
|
705
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
706
|
+
}
|
|
707
|
+
function requireString(value, path) {
|
|
708
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
709
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function requireFullGitOid(value, path) {
|
|
713
|
+
requireString(value, path);
|
|
714
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(value)) {
|
|
715
|
+
throw new Error(`${path} must be a full 40- or 64-character hexadecimal Git OID.`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function requireReviewString(value, path) {
|
|
719
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
720
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
function normalizeGitOid(value) {
|
|
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) {
|
|
732
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
733
|
+
throw new Error(`${path} must be an array of strings.`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function requireOnlyKeys(value, allowed, path) {
|
|
737
|
+
const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
738
|
+
if (unexpected.length > 0) {
|
|
739
|
+
throw new Error(`${path} contains unknown field${unexpected.length === 1 ? "" : "s"}: ${unexpected.join(", ")}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
234
743
|
// src/changeTourManifest.ts
|
|
235
|
-
var CHANGE_TOUR_MANIFEST_VERSION =
|
|
744
|
+
var CHANGE_TOUR_MANIFEST_VERSION = 3;
|
|
236
745
|
function parseChangeTourManifest(value) {
|
|
237
|
-
if (
|
|
746
|
+
if (isRecord2(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_MANIFEST_VERSION) {
|
|
747
|
+
throw new Error(
|
|
748
|
+
`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
|
+
);
|
|
750
|
+
}
|
|
751
|
+
if (!isRecord2(value) || value.version !== 1 && value.version !== 2 && value.version !== CHANGE_TOUR_MANIFEST_VERSION) {
|
|
238
752
|
throw new Error(`Unsupported or missing change-tour manifest version.`);
|
|
239
753
|
}
|
|
240
|
-
|
|
241
|
-
|
|
754
|
+
requireString2(value.title, "title");
|
|
755
|
+
requireString2(value.generatedAt, "generatedAt");
|
|
242
756
|
if (value.windowTitle !== void 0) {
|
|
243
|
-
|
|
757
|
+
requireString2(value.windowTitle, "windowTitle");
|
|
244
758
|
}
|
|
245
759
|
if (value.sourceUrl !== void 0) {
|
|
246
|
-
|
|
760
|
+
requireString2(value.sourceUrl, "sourceUrl");
|
|
247
761
|
}
|
|
248
|
-
if (!
|
|
762
|
+
if (!isRecord2(value.range)) {
|
|
249
763
|
throw new Error("Change-tour manifest range must be an object.");
|
|
250
764
|
}
|
|
251
765
|
for (const key of ["baseRef", "headRef", "mergeBaseOid", "headOid"]) {
|
|
252
|
-
|
|
766
|
+
requireString2(value.range[key], `range.${key}`);
|
|
253
767
|
}
|
|
254
|
-
if (!
|
|
768
|
+
if (!isRecord2(value.summary)) {
|
|
255
769
|
throw new Error("Change-tour manifest summary must be an object.");
|
|
256
770
|
}
|
|
257
771
|
for (const key of ["changedFiles", "includedScenes", "additions", "deletions", "commitCount"]) {
|
|
258
772
|
requireNonNegativeInteger(value.summary[key], `summary.${key}`);
|
|
259
773
|
}
|
|
260
|
-
|
|
774
|
+
requireStringArray2(value.summary.omittedFiles, "summary.omittedFiles");
|
|
261
775
|
if (!Array.isArray(value.commits) || !Array.isArray(value.chapters) || !Array.isArray(value.scenes)) {
|
|
262
776
|
throw new Error("Change-tour manifest commits, chapters, and scenes must be arrays.");
|
|
263
777
|
}
|
|
@@ -266,19 +780,19 @@ function parseChangeTourManifest(value) {
|
|
|
266
780
|
}
|
|
267
781
|
const sceneIds = /* @__PURE__ */ new Set();
|
|
268
782
|
for (const [index, candidate] of value.scenes.entries()) {
|
|
269
|
-
validateScene(candidate, index);
|
|
783
|
+
validateScene(candidate, index, value.version);
|
|
270
784
|
if (sceneIds.has(candidate.id)) {
|
|
271
785
|
throw new Error(`Duplicate change-tour scene id: ${candidate.id}`);
|
|
272
786
|
}
|
|
273
787
|
sceneIds.add(candidate.id);
|
|
274
788
|
}
|
|
275
789
|
for (const [index, candidate] of value.chapters.entries()) {
|
|
276
|
-
if (!
|
|
790
|
+
if (!isRecord2(candidate)) {
|
|
277
791
|
throw new Error(`chapters[${index}] must be an object.`);
|
|
278
792
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
793
|
+
requireString2(candidate.id, `chapters[${index}].id`);
|
|
794
|
+
requireString2(candidate.title, `chapters[${index}].title`);
|
|
795
|
+
requireStringArray2(candidate.sceneIds, `chapters[${index}].sceneIds`);
|
|
282
796
|
for (const sceneId of candidate.sceneIds) {
|
|
283
797
|
if (!sceneIds.has(sceneId)) {
|
|
284
798
|
throw new Error(`Chapter references unknown scene id: ${sceneId}`);
|
|
@@ -295,31 +809,135 @@ function parseChangeTourManifest(value) {
|
|
|
295
809
|
if (value.files !== void 0 && value.summary.changedFiles !== files.length) {
|
|
296
810
|
throw new Error("summary.changedFiles must match the number of files.");
|
|
297
811
|
}
|
|
812
|
+
if (value.authoringCoverage !== void 0) {
|
|
813
|
+
validateAuthoringCoverage(value.authoringCoverage, value.version);
|
|
814
|
+
}
|
|
815
|
+
if (value.tours !== void 0) {
|
|
816
|
+
if (value.version !== 3) throw new Error("Independent tours require manifest version 3.");
|
|
817
|
+
validateModeTours(value.tours, value.version);
|
|
818
|
+
}
|
|
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
|
+
}
|
|
298
831
|
return { ...value, files };
|
|
299
832
|
}
|
|
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
|
+
function validateAuthoringCoverage(value, version) {
|
|
849
|
+
if (!isRecord2(value) || !isRecord2(value.walkthrough) || !Array.isArray(value.explanationAssignments)) {
|
|
850
|
+
throw new Error("authoringCoverage must contain walkthrough and explanationAssignments.");
|
|
851
|
+
}
|
|
852
|
+
const expectedScope = version >= 2 ? "final" : "tour";
|
|
853
|
+
if (value.walkthrough.scope !== expectedScope) {
|
|
854
|
+
throw new Error(`authoringCoverage.walkthrough.scope must be ${expectedScope}.`);
|
|
855
|
+
}
|
|
856
|
+
requireNonNegativeInteger(value.walkthrough.coveredUnits, "authoringCoverage.walkthrough.coveredUnits");
|
|
857
|
+
requireNonNegativeInteger(value.walkthrough.includedUnits, "authoringCoverage.walkthrough.includedUnits");
|
|
858
|
+
requirePercentage(value.walkthrough.coveragePercent, "authoringCoverage.walkthrough.coveragePercent");
|
|
859
|
+
if (value.walkthrough.coveredUnits > value.walkthrough.includedUnits) {
|
|
860
|
+
throw new Error("authoringCoverage.walkthrough.coveredUnits must not exceed includedUnits.");
|
|
861
|
+
}
|
|
862
|
+
for (const [index, assignment] of value.explanationAssignments.entries()) {
|
|
863
|
+
const path = `authoringCoverage.explanationAssignments[${index}]`;
|
|
864
|
+
if (!isRecord2(assignment)) throw new Error(`${path} must be an object.`);
|
|
865
|
+
requireString2(assignment.sceneId, `${path}.sceneId`);
|
|
866
|
+
requireNonNegativeInteger(assignment.assignedUnits, `${path}.assignedUnits`);
|
|
867
|
+
requireNonNegativeInteger(assignment.excludedUnits, `${path}.excludedUnits`);
|
|
868
|
+
requireNonNegativeInteger(assignment.completeUnits, `${path}.completeUnits`);
|
|
869
|
+
requireNonNegativeInteger(assignment.totalUnits, `${path}.totalUnits`);
|
|
870
|
+
requirePercentage(assignment.assignmentPercent, `${path}.assignmentPercent`);
|
|
871
|
+
if (assignment.assignedUnits + assignment.excludedUnits !== assignment.completeUnits || assignment.completeUnits > assignment.totalUnits) {
|
|
872
|
+
throw new Error(`${path} contains inconsistent unit counts.`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function validateModeTours(value, version) {
|
|
877
|
+
if (!isRecord2(value)) throw new Error("Change-tour manifest tours must be an object.");
|
|
878
|
+
if (value.historical === void 0 && value.deconstructed === void 0) {
|
|
879
|
+
throw new Error("Change-tour manifest tours must contain a historical or deconstructed tour.");
|
|
880
|
+
}
|
|
881
|
+
for (const mode of ["historical", "deconstructed"]) {
|
|
882
|
+
const tour = value[mode];
|
|
883
|
+
if (tour === void 0) continue;
|
|
884
|
+
if (!isRecord2(tour) || !Array.isArray(tour.chapters) || !Array.isArray(tour.scenes) || tour.chapters.length === 0 || tour.scenes.length === 0) {
|
|
885
|
+
throw new Error(`Change-tour manifest tours.${mode} must contain non-empty chapters and scenes arrays.`);
|
|
886
|
+
}
|
|
887
|
+
const sceneIds = /* @__PURE__ */ new Set();
|
|
888
|
+
let hasDeconstructed = false;
|
|
889
|
+
for (const [index, scene] of tour.scenes.entries()) {
|
|
890
|
+
validateScene(scene, index, version);
|
|
891
|
+
if (sceneIds.has(scene.id)) {
|
|
892
|
+
throw new Error(`Duplicate ${mode} tour scene id: ${scene.id}`);
|
|
893
|
+
}
|
|
894
|
+
sceneIds.add(scene.id);
|
|
895
|
+
if (scene.kind === "deconstructed-diff") hasDeconstructed = true;
|
|
896
|
+
if (mode === "historical" && scene.kind === "deconstructed-diff") {
|
|
897
|
+
throw new Error("Historical tours cannot contain synthetic deconstructed scenes.");
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (mode === "deconstructed" && !hasDeconstructed) {
|
|
901
|
+
throw new Error("Deconstructed tours must contain a constructed deconstructed-diff scene.");
|
|
902
|
+
}
|
|
903
|
+
for (const [index, chapter] of tour.chapters.entries()) {
|
|
904
|
+
if (!isRecord2(chapter)) {
|
|
905
|
+
throw new Error(`tours.${mode}.chapters[${index}] must be an object.`);
|
|
906
|
+
}
|
|
907
|
+
requireString2(chapter.id, `tours.${mode}.chapters[${index}].id`);
|
|
908
|
+
requireString2(chapter.title, `tours.${mode}.chapters[${index}].title`);
|
|
909
|
+
requireStringArray2(chapter.sceneIds, `tours.${mode}.chapters[${index}].sceneIds`);
|
|
910
|
+
for (const sceneId of chapter.sceneIds) {
|
|
911
|
+
if (!sceneIds.has(sceneId)) {
|
|
912
|
+
throw new Error(`tours.${mode} chapter references unknown scene id: ${sceneId}`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
300
918
|
function parseChangeTourStory(value) {
|
|
301
|
-
if (!
|
|
919
|
+
if (!isRecord2(value) || !Array.isArray(value.scenes)) {
|
|
302
920
|
throw new Error("Change-tour story must contain a scenes array.");
|
|
303
921
|
}
|
|
304
922
|
if (value.title !== void 0) {
|
|
305
|
-
|
|
923
|
+
requireString2(value.title, "title");
|
|
306
924
|
}
|
|
307
925
|
if (value.sourceUrl !== void 0) {
|
|
308
|
-
|
|
926
|
+
requireString2(value.sourceUrl, "sourceUrl");
|
|
309
927
|
}
|
|
310
928
|
for (const [index, scene] of value.scenes.entries()) {
|
|
311
|
-
if (!
|
|
929
|
+
if (!isRecord2(scene) || scene.kind !== "discussion" && scene.kind !== "file") {
|
|
312
930
|
throw new Error(`story.scenes[${index}] must be a discussion or file scene.`);
|
|
313
931
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
932
|
+
requireString2(scene.chapterId, `story.scenes[${index}].chapterId`);
|
|
933
|
+
requireString2(scene.chapterTitle, `story.scenes[${index}].chapterTitle`);
|
|
934
|
+
validateNarrative2(scene, `story.scenes[${index}]`);
|
|
317
935
|
if (scene.kind === "discussion") {
|
|
318
|
-
|
|
936
|
+
requireString2(scene.title, `story.scenes[${index}].title`);
|
|
319
937
|
} else {
|
|
320
|
-
|
|
938
|
+
requireString2(scene.path, `story.scenes[${index}].path`);
|
|
321
939
|
if (scene.title !== void 0) {
|
|
322
|
-
|
|
940
|
+
requireString2(scene.title, `story.scenes[${index}].title`);
|
|
323
941
|
}
|
|
324
942
|
if (scene.focusChangeIndex !== void 0) {
|
|
325
943
|
requireNonNegativeInteger(scene.focusChangeIndex, `story.scenes[${index}].focusChangeIndex`);
|
|
@@ -328,37 +946,103 @@ function parseChangeTourStory(value) {
|
|
|
328
946
|
}
|
|
329
947
|
return value;
|
|
330
948
|
}
|
|
331
|
-
function
|
|
332
|
-
if (!
|
|
949
|
+
function validateZoom(value, version) {
|
|
950
|
+
if (!isRecord2(value.repository) || typeof value.repository.root !== "string" || !value.repository.root) {
|
|
951
|
+
throw new Error("A v2 tour requires its originating repository.root.");
|
|
952
|
+
}
|
|
953
|
+
if (!isRecord2(value.zoom) || !Array.isArray(value.zoom.revisions) || !isRecord2(value.zoom.final)) {
|
|
954
|
+
throw new Error("A v2 tour requires zoom metadata and real revision evidence.");
|
|
955
|
+
}
|
|
956
|
+
const scenes = value.scenes;
|
|
957
|
+
const revisions = value.zoom.revisions;
|
|
958
|
+
const ids = /* @__PURE__ */ new Set();
|
|
959
|
+
for (const [index, scene] of revisions.entries()) {
|
|
960
|
+
validateScene(scene, index, version);
|
|
961
|
+
if (scene.kind !== "stacked-diff") throw new Error("zoom.revisions must contain real stacked-diff scenes.");
|
|
962
|
+
if (ids.has(scene.id)) throw new Error(`Duplicate zoom revision scene: ${scene.id}`);
|
|
963
|
+
ids.add(scene.id);
|
|
964
|
+
const range = value.range;
|
|
965
|
+
if (scene.stack[0].oid !== range.mergeBaseOid || scene.stack[scene.stack.length - 1].oid !== range.headOid) {
|
|
966
|
+
throw new Error(`Revision scene ${scene.id} endpoints must match the final diff range.`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
for (const scene of scenes) {
|
|
970
|
+
if (scene.kind !== "deconstructed-diff" && scene.kind !== "stacked-diff") continue;
|
|
971
|
+
const real = revisions.find((candidate) => candidate.id === scene.id);
|
|
972
|
+
if (!real) throw new Error(`Scene ${scene.id} is missing its real revision evidence.`);
|
|
973
|
+
if (scene.kind === "deconstructed-diff" && (real.stack[0].oid !== scene.realRange.baseOid || real.stack[real.stack.length - 1].oid !== scene.realRange.targetOid)) {
|
|
974
|
+
throw new Error(`Deconstructed scene ${scene.id} real stack endpoints must match its explanation range.`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
const explanation = scenes.some((scene) => scene.kind === "deconstructed-diff");
|
|
978
|
+
const distinct = revisions.some((scene) => scene.stack.length > 2);
|
|
979
|
+
const depth = explanation ? "explanation" : distinct ? "revisions" : "final";
|
|
980
|
+
const modes = [...explanation ? ["explanation"] : [], ...distinct ? ["revisions"] : [], "final", "history"];
|
|
981
|
+
if (value.zoom.authoredDepth !== depth || JSON.stringify(value.zoom.modes) !== JSON.stringify(modes)) {
|
|
982
|
+
throw new Error("zoom depth and modes must match the maximum authored depth.");
|
|
983
|
+
}
|
|
984
|
+
if (!Array.isArray(value.files)) throw new Error("A v2 tour requires final endpoint file evidence.");
|
|
985
|
+
if (!Array.isArray(value.zoom.final.scenes) || !Array.isArray(value.zoom.final.chapters)) {
|
|
986
|
+
throw new Error("A v2 tour requires a non-stacked final tour.");
|
|
987
|
+
}
|
|
988
|
+
const finalSceneIds = /* @__PURE__ */ new Set();
|
|
989
|
+
for (const [index, scene] of value.zoom.final.scenes.entries()) {
|
|
990
|
+
validateScene(scene, index, version);
|
|
991
|
+
if (scene.kind === "stacked-diff" || scene.kind === "deconstructed-diff") {
|
|
992
|
+
throw new Error("zoom.final must contain only non-stacked scenes.");
|
|
993
|
+
}
|
|
994
|
+
if (finalSceneIds.has(scene.id)) throw new Error(`Duplicate final tour scene: ${scene.id}`);
|
|
995
|
+
finalSceneIds.add(scene.id);
|
|
996
|
+
}
|
|
997
|
+
for (const [index, chapter] of value.zoom.final.chapters.entries()) {
|
|
998
|
+
if (!isRecord2(chapter) || !Array.isArray(chapter.sceneIds)) {
|
|
999
|
+
throw new Error(`zoom.final.chapters[${index}] must contain sceneIds.`);
|
|
1000
|
+
}
|
|
1001
|
+
requireString2(chapter.id, `zoom.final.chapters[${index}].id`);
|
|
1002
|
+
requireString2(chapter.title, `zoom.final.chapters[${index}].title`);
|
|
1003
|
+
for (const sceneId of chapter.sceneIds) {
|
|
1004
|
+
requireString2(sceneId, `zoom.final.chapters[${index}].sceneIds`);
|
|
1005
|
+
if (!finalSceneIds.has(sceneId)) throw new Error(`Final tour chapter references unknown scene: ${sceneId}`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
function validateScene(value, index, version) {
|
|
1010
|
+
if (!isRecord2(value) || !["text-diff", "discussion", "walkthrough", "stacked-diff", "deconstructed-diff"].includes(String(value.kind))) {
|
|
333
1011
|
throw new Error(`scenes[${index}] must be a text-diff, discussion, walkthrough, stacked-diff, or deconstructed-diff scene.`);
|
|
334
1012
|
}
|
|
335
1013
|
for (const key of ["id", "title"]) {
|
|
336
|
-
|
|
1014
|
+
requireString2(value[key], `scenes[${index}].${key}`);
|
|
337
1015
|
}
|
|
338
|
-
|
|
1016
|
+
if (value.overview !== void 0 && version !== 3) {
|
|
1017
|
+
throw new Error(`scenes[${index}].overview requires manifest version 3.`);
|
|
1018
|
+
}
|
|
1019
|
+
validateNarrative2(value, `scenes[${index}]`);
|
|
339
1020
|
if (value.kind === "discussion") {
|
|
1021
|
+
validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
|
|
340
1022
|
return;
|
|
341
1023
|
}
|
|
342
1024
|
if (value.kind === "walkthrough") {
|
|
1025
|
+
validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
|
|
343
1026
|
if (!Array.isArray(value.steps) || value.steps.length === 0) {
|
|
344
1027
|
throw new Error(`scenes[${index}].steps must be a non-empty array.`);
|
|
345
1028
|
}
|
|
346
1029
|
for (const [stepIndex, step] of value.steps.entries()) {
|
|
347
1030
|
const path = `scenes[${index}].steps[${stepIndex}]`;
|
|
348
|
-
if (!
|
|
1031
|
+
if (!isRecord2(step) || !isRecord2(step.focus) || !isRecord2(step.diff)) {
|
|
349
1032
|
throw new Error(`${path} must contain focus and diff objects.`);
|
|
350
1033
|
}
|
|
351
|
-
for (const key of ["id", "title", "body"])
|
|
1034
|
+
for (const key of ["id", "title", "body"]) requireString2(step[key], `${path}.${key}`);
|
|
352
1035
|
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
353
1036
|
throw new Error(`${path}.depth must be mentioned, explained, or contextualized.`);
|
|
354
1037
|
}
|
|
1038
|
+
validateStepRequirement(step.requirement, `${path}.requirement`);
|
|
355
1039
|
validateResolvedAnchor(step.focus, `${path}.focus`);
|
|
356
|
-
validateScene(step.diff, index);
|
|
1040
|
+
validateScene(step.diff, index, version);
|
|
357
1041
|
if (step.diff.kind !== "text-diff") throw new Error(`${path}.diff must be a text-diff scene.`);
|
|
358
1042
|
if (step.connection !== void 0) {
|
|
359
|
-
if (!
|
|
360
|
-
|
|
361
|
-
|
|
1043
|
+
if (!isRecord2(step.connection)) throw new Error(`${path}.connection must be an object.`);
|
|
1044
|
+
requireString2(step.connection.id, `${path}.connection.id`);
|
|
1045
|
+
requireString2(step.connection.label, `${path}.connection.label`);
|
|
362
1046
|
validateResolvedAnchor(step.connection.from, `${path}.connection.from`);
|
|
363
1047
|
validateResolvedAnchor(step.connection.to, `${path}.connection.to`);
|
|
364
1048
|
}
|
|
@@ -366,68 +1050,75 @@ function validateScene(value, index) {
|
|
|
366
1050
|
return;
|
|
367
1051
|
}
|
|
368
1052
|
if (value.kind === "stacked-diff") {
|
|
369
|
-
|
|
1053
|
+
validateStackedScene2(value, index);
|
|
1054
|
+
validateSceneOverview(value.overview, `scenes[${index}].overview`, {
|
|
1055
|
+
endpointIds: value.stack.map((panel) => panel.id)
|
|
1056
|
+
});
|
|
370
1057
|
return;
|
|
371
1058
|
}
|
|
372
1059
|
if (value.kind === "deconstructed-diff") {
|
|
373
|
-
|
|
1060
|
+
validateDeconstructedScene2(value, index);
|
|
1061
|
+
validateSceneOverview(value.overview, `scenes[${index}].overview`, {
|
|
1062
|
+
endpointIds: value.panels.map((panel) => panel.id)
|
|
1063
|
+
});
|
|
374
1064
|
return;
|
|
375
1065
|
}
|
|
1066
|
+
validateSceneOverview(value.overview, `scenes[${index}].overview`, { allowComparison: false });
|
|
376
1067
|
for (const key of ["path", "leftLabel", "rightLabel", "leftContent", "rightContent"]) {
|
|
377
|
-
|
|
1068
|
+
requireString2(value[key], `scenes[${index}].${key}`);
|
|
378
1069
|
}
|
|
379
1070
|
if (value.previousPath !== void 0) {
|
|
380
|
-
|
|
1071
|
+
requireString2(value.previousPath, `scenes[${index}].previousPath`);
|
|
381
1072
|
}
|
|
382
|
-
|
|
1073
|
+
requireString2(value.changeKind, `scenes[${index}].changeKind`);
|
|
383
1074
|
requireNonNegativeInteger(value.additions, `scenes[${index}].additions`);
|
|
384
1075
|
requireNonNegativeInteger(value.deletions, `scenes[${index}].deletions`);
|
|
385
1076
|
if (value.focusChangeIndex !== void 0) {
|
|
386
1077
|
requireNonNegativeInteger(value.focusChangeIndex, `scenes[${index}].focusChangeIndex`);
|
|
387
1078
|
}
|
|
388
1079
|
}
|
|
389
|
-
function
|
|
390
|
-
if (!Array.isArray(value.stack) || value.stack.length <
|
|
391
|
-
throw new Error(`scenes[${index}].stack must contain between
|
|
1080
|
+
function validateStackedScene2(value, index) {
|
|
1081
|
+
if (!Array.isArray(value.stack) || value.stack.length < 2 || value.stack.length > 6) {
|
|
1082
|
+
throw new Error(`scenes[${index}].stack must contain between 2 and 6 revisions.`);
|
|
392
1083
|
}
|
|
393
1084
|
for (const [panelIndex, panel] of value.stack.entries()) {
|
|
394
|
-
if (!
|
|
395
|
-
for (const key of ["id", "ref", "oid", "label"])
|
|
1085
|
+
if (!isRecord2(panel)) throw new Error(`scenes[${index}].stack[${panelIndex}] must be an object.`);
|
|
1086
|
+
for (const key of ["id", "ref", "oid", "label"]) requireString2(panel[key], `scenes[${index}].stack[${panelIndex}].${key}`);
|
|
396
1087
|
}
|
|
397
1088
|
validateMultiPanelFiles(value.files, value.stack.length, index);
|
|
398
1089
|
validateMultiPanelSteps(value.steps, value.stack.length, index, false);
|
|
399
1090
|
}
|
|
400
|
-
function
|
|
1091
|
+
function validateDeconstructedScene2(value, index) {
|
|
401
1092
|
if (value.stageLabel !== "Explanation stages") {
|
|
402
1093
|
throw new Error(`scenes[${index}].stageLabel must be Explanation stages.`);
|
|
403
1094
|
}
|
|
404
|
-
if (!
|
|
1095
|
+
if (!isRecord2(value.realRange)) throw new Error(`scenes[${index}].realRange must be an object.`);
|
|
405
1096
|
for (const key of ["baseRef", "targetRef", "baseOid", "targetOid"]) {
|
|
406
|
-
|
|
1097
|
+
requireString2(value.realRange[key], `scenes[${index}].realRange.${key}`);
|
|
407
1098
|
}
|
|
408
1099
|
if (!Array.isArray(value.panels) || value.panels.length < 2 || value.panels.length > 13) {
|
|
409
1100
|
throw new Error(`scenes[${index}].panels must contain a baseline and between 1 and 12 stages.`);
|
|
410
1101
|
}
|
|
411
1102
|
const panels = value.panels;
|
|
412
1103
|
for (const [panelIndex, panel] of panels.entries()) {
|
|
413
|
-
if (!
|
|
414
|
-
for (const key of ["id", "label", "role"])
|
|
1104
|
+
if (!isRecord2(panel)) throw new Error(`scenes[${index}].panels[${panelIndex}] must be an object.`);
|
|
1105
|
+
for (const key of ["id", "label", "role"]) requireString2(panel[key], `scenes[${index}].panels[${panelIndex}].${key}`);
|
|
415
1106
|
const expectedRole = panelIndex === 0 ? "baseline" : "stage";
|
|
416
1107
|
if (panel.role !== expectedRole) throw new Error(`scenes[${index}].panels[${panelIndex}].role must be ${expectedRole}.`);
|
|
417
|
-
if (panelIndex > 0)
|
|
1108
|
+
if (panelIndex > 0) requireString2(panel.stageId, `scenes[${index}].panels[${panelIndex}].stageId`);
|
|
418
1109
|
}
|
|
419
1110
|
validateMultiPanelFiles(value.files, panels.length, index);
|
|
420
1111
|
validateMultiPanelSteps(value.steps, panels.length, index, true);
|
|
421
1112
|
if (Array.isArray(value.steps)) {
|
|
422
1113
|
let previousPairIndex = -1;
|
|
423
1114
|
value.steps.forEach((step, stepIndex) => {
|
|
424
|
-
if (!
|
|
1115
|
+
if (!isRecord2(step)) return;
|
|
425
1116
|
const pairIndex = Number(step.pairIndex);
|
|
426
1117
|
if (pairIndex < previousPairIndex) {
|
|
427
1118
|
throw new Error(`scenes[${index}].steps must remain grouped in explanation stage order.`);
|
|
428
1119
|
}
|
|
429
1120
|
previousPairIndex = pairIndex;
|
|
430
|
-
if (step.stageId !== void 0)
|
|
1121
|
+
if (step.stageId !== void 0) requireString2(step.stageId, `scenes[${index}].steps[${stepIndex}].stageId`);
|
|
431
1122
|
if (step.stageIndex !== void 0) requireNonNegativeInteger(step.stageIndex, `scenes[${index}].steps[${stepIndex}].stageIndex`);
|
|
432
1123
|
if (step.focusIndex !== void 0) requireNonNegativeInteger(step.focusIndex, `scenes[${index}].steps[${stepIndex}].focusIndex`);
|
|
433
1124
|
if (step.focusCount !== void 0) requirePositiveInteger(step.focusCount, `scenes[${index}].steps[${stepIndex}].focusCount`);
|
|
@@ -440,341 +1131,91 @@ function validateDeconstructedScene(value, index) {
|
|
|
440
1131
|
});
|
|
441
1132
|
for (let stageIndex = 0; stageIndex < panels.length - 1; stageIndex += 1) {
|
|
442
1133
|
const panel = panels[stageIndex + 1];
|
|
443
|
-
const stageSteps = value.steps.filter((step) =>
|
|
444
|
-
if (!
|
|
445
|
-
throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
|
|
446
|
-
}
|
|
447
|
-
const firstStep = stageSteps[0];
|
|
448
|
-
const fallbackStageId = isRecord(firstStep) ? firstStep.id : void 0;
|
|
449
|
-
if (stageSteps.some((step) => {
|
|
450
|
-
if (!isRecord(step)) return true;
|
|
451
|
-
return (step.stageId ?? fallbackStageId) !== panel.stageId;
|
|
452
|
-
})) {
|
|
453
|
-
throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
function validateMultiPanelFiles(value, panelCount, index) {
|
|
459
|
-
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].files must be non-empty.`);
|
|
460
|
-
for (const [fileIndex, file] of value.entries()) {
|
|
461
|
-
if (!isRecord(file)) throw new Error(`scenes[${index}].files[${fileIndex}] must be an object.`);
|
|
462
|
-
requireString(file.path, `scenes[${index}].files[${fileIndex}].path`);
|
|
463
|
-
if (!Array.isArray(file.panels) || file.panels.length !== panelCount) {
|
|
464
|
-
throw new Error(`scenes[${index}].files[${fileIndex}].panels must match panel count.`);
|
|
465
|
-
}
|
|
466
|
-
for (const [panelIndex, panel] of file.panels.entries()) {
|
|
467
|
-
if (!isRecord(panel)) throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}] must be an object.`);
|
|
468
|
-
for (const key of ["id", "label", "content"]) requireString(panel[key], `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].${key}`);
|
|
469
|
-
if (panel.path !== void 0) requireString(panel.path, `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].path`);
|
|
470
|
-
if (typeof panel.exists !== "boolean") throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}].exists must be boolean.`);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
|
|
475
|
-
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].steps must be non-empty.`);
|
|
476
|
-
for (const [stepIndex, step] of value.entries()) {
|
|
477
|
-
if (!isRecord(step)) throw new Error(`scenes[${index}].steps[${stepIndex}] must be an object.`);
|
|
478
|
-
for (const key of ["id", "title", "body", "file", "side"]) requireString(step[key], `scenes[${index}].steps[${stepIndex}].${key}`);
|
|
479
|
-
requireNonNegativeInteger(step.pairIndex, `scenes[${index}].steps[${stepIndex}].pairIndex`);
|
|
480
|
-
if (Number(step.pairIndex) >= panelCount - 1) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex is outside the panels.`);
|
|
481
|
-
if (deconstructed) {
|
|
482
|
-
requireStringArray(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
|
|
483
|
-
if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
|
|
484
|
-
}
|
|
485
|
-
if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
|
|
486
|
-
if (step.startLine !== void 0) requirePositiveInteger(step.startLine, `scenes[${index}].steps[${stepIndex}].startLine`);
|
|
487
|
-
if (step.endLine !== void 0) requirePositiveInteger(step.endLine, `scenes[${index}].steps[${stepIndex}].endLine`);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
function validateTourFile(value, index) {
|
|
491
|
-
if (!isRecord(value)) throw new Error(`files[${index}] must be an object.`);
|
|
492
|
-
if (value.kind === "text-diff") {
|
|
493
|
-
validateScene(value, index);
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
if (value.kind !== "omitted") {
|
|
497
|
-
throw new Error(`files[${index}].kind must be text-diff or omitted.`);
|
|
498
|
-
}
|
|
499
|
-
for (const key of ["id", "title", "path", "changeKind", "reason"]) {
|
|
500
|
-
requireString(value[key], `files[${index}].${key}`);
|
|
501
|
-
}
|
|
502
|
-
if (value.previousPath !== void 0) requireString(value.previousPath, `files[${index}].previousPath`);
|
|
503
|
-
requireNonNegativeInteger(value.additions, `files[${index}].additions`);
|
|
504
|
-
requireNonNegativeInteger(value.deletions, `files[${index}].deletions`);
|
|
505
|
-
}
|
|
506
|
-
function collectLegacyTourFiles(scenes) {
|
|
507
|
-
const files = /* @__PURE__ */ new Map();
|
|
508
|
-
for (const scene of scenes) {
|
|
509
|
-
if (scene.kind === "text-diff") {
|
|
510
|
-
if (!files.has(scene.path)) files.set(scene.path, scene);
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
if (scene.kind === "walkthrough") {
|
|
514
|
-
for (const step of scene.steps) {
|
|
515
|
-
if (!files.has(step.diff.path)) files.set(step.diff.path, step.diff);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
520
|
-
}
|
|
521
|
-
function validateResolvedAnchor(value, path) {
|
|
522
|
-
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
523
|
-
for (const key of ["id", "path", "revision", "excerpt"]) requireString(value[key], `${path}.${key}`);
|
|
524
|
-
if (value.revision !== "base" && value.revision !== "head") throw new Error(`${path}.revision must be base or head.`);
|
|
525
|
-
requirePositiveInteger(value.startLine, `${path}.startLine`);
|
|
526
|
-
requirePositiveInteger(value.endLine, `${path}.endLine`);
|
|
527
|
-
if (Number(value.endLine) < Number(value.startLine)) throw new Error(`${path}.endLine must not precede startLine.`);
|
|
528
|
-
}
|
|
529
|
-
function validateNarrative(value, path) {
|
|
530
|
-
requireString(value.summary, `${path}.summary`);
|
|
531
|
-
requireStringArray(value.bullets, `${path}.bullets`);
|
|
532
|
-
requireStringArray(value.tags, `${path}.tags`);
|
|
533
|
-
requireString(value.takeaway, `${path}.takeaway`);
|
|
534
|
-
}
|
|
535
|
-
function isRecord(value) {
|
|
536
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
537
|
-
}
|
|
538
|
-
function requireString(value, path) {
|
|
539
|
-
if (typeof value !== "string") {
|
|
540
|
-
throw new Error(`${path} must be a string.`);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
function requireStringArray(value, path) {
|
|
544
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
545
|
-
throw new Error(`${path} must be an array of strings.`);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
function requireNonNegativeInteger(value, path) {
|
|
549
|
-
if (!Number.isInteger(value) || Number(value) < 0) {
|
|
550
|
-
throw new Error(`${path} must be a non-negative integer.`);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
function requirePositiveInteger(value, path) {
|
|
554
|
-
if (!Number.isInteger(value) || Number(value) < 1) throw new Error(`${path} must be a positive integer.`);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// src/changeTourSource.ts
|
|
558
|
-
var CHANGE_TOUR_SOURCE_VERSION = 1;
|
|
559
|
-
function parseChangeTourSource(value) {
|
|
560
|
-
if (!isRecord2(value) || value.version !== CHANGE_TOUR_SOURCE_VERSION) {
|
|
561
|
-
throw new Error("Unsupported or missing change-tour source version.");
|
|
562
|
-
}
|
|
563
|
-
requireOnlyKeys(value, ["version", "title", "windowTitle", "sourceUrl", "range", "anchors", "connections", "chapters", "coverage"], "source");
|
|
564
|
-
optionalString(value.title, "title");
|
|
565
|
-
optionalString(value.windowTitle, "windowTitle");
|
|
566
|
-
optionalString(value.sourceUrl, "sourceUrl");
|
|
567
|
-
if (value.range !== void 0) {
|
|
568
|
-
if (!isRecord2(value.range)) throw new Error("range must be an object.");
|
|
569
|
-
requireOnlyKeys(value.range, ["base", "head"], "range");
|
|
570
|
-
requireString2(value.range.base, "range.base");
|
|
571
|
-
requireString2(value.range.head, "range.head");
|
|
572
|
-
}
|
|
573
|
-
if (value.coverage !== void 0) {
|
|
574
|
-
if (!isRecord2(value.coverage) || !Array.isArray(value.coverage.exclusions)) {
|
|
575
|
-
throw new Error("coverage must contain an exclusions array.");
|
|
576
|
-
}
|
|
577
|
-
requireOnlyKeys(value.coverage, ["exclusions"], "coverage");
|
|
578
|
-
for (const [index, exclusion] of value.coverage.exclusions.entries()) {
|
|
579
|
-
const exclusionPath = `coverage.exclusions[${index}]`;
|
|
580
|
-
if (!isRecord2(exclusion)) throw new Error(`${exclusionPath} must be an object.`);
|
|
581
|
-
requireOnlyKeys(exclusion, ["path", "hunks", "reason"], exclusionPath);
|
|
582
|
-
requireString2(exclusion.path, `${exclusionPath}.path`);
|
|
583
|
-
requireString2(exclusion.reason, `${exclusionPath}.reason`);
|
|
584
|
-
if (exclusion.hunks !== void 0) requireStringArray2(exclusion.hunks, `${exclusionPath}.hunks`);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
if (!isRecord2(value.anchors)) {
|
|
588
|
-
throw new Error("anchors must be an object.");
|
|
589
|
-
}
|
|
590
|
-
for (const [id, anchor] of Object.entries(value.anchors)) {
|
|
591
|
-
if (!isRecord2(anchor)) {
|
|
592
|
-
throw new Error(`anchors.${id} must be an object.`);
|
|
593
|
-
}
|
|
594
|
-
requireOnlyKeys(anchor, ["file", "revision", "contains", "occurrence"], `anchors.${id}`);
|
|
595
|
-
requireString2(anchor.file, `anchors.${id}.file`);
|
|
596
|
-
if (anchor.revision !== "base" && anchor.revision !== "head") {
|
|
597
|
-
throw new Error(`anchors.${id}.revision must be base or head.`);
|
|
598
|
-
}
|
|
599
|
-
requireString2(anchor.contains, `anchors.${id}.contains`);
|
|
600
|
-
if (anchor.occurrence !== void 0 && (!Number.isInteger(anchor.occurrence) || Number(anchor.occurrence) < 1)) {
|
|
601
|
-
throw new Error(`anchors.${id}.occurrence must be a positive integer.`);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
const rawConnections = Array.isArray(value.connections) ? value.connections : isRecord2(value.connections) ? Object.entries(value.connections).map(([id, connection]) => isRecord2(connection) ? { id, ...connection } : connection) : null;
|
|
605
|
-
if (!rawConnections || !Array.isArray(value.chapters) || value.chapters.length === 0) {
|
|
606
|
-
throw new Error("connections must be an array or object, and chapters must be a non-empty array.");
|
|
607
|
-
}
|
|
608
|
-
const connectionIds = /* @__PURE__ */ new Set();
|
|
609
|
-
for (const [index, connection] of rawConnections.entries()) {
|
|
610
|
-
if (!isRecord2(connection)) {
|
|
611
|
-
throw new Error(`connections[${index}] must be an object.`);
|
|
612
|
-
}
|
|
613
|
-
requireOnlyKeys(connection, ["id", "from", "to", "label"], `connections[${index}]`);
|
|
614
|
-
requireString2(connection.id, `connections[${index}].id`);
|
|
615
|
-
requireString2(connection.from, `connections[${index}].from`);
|
|
616
|
-
requireString2(connection.to, `connections[${index}].to`);
|
|
617
|
-
requireString2(connection.label, `connections[${index}].label`);
|
|
618
|
-
const { id, from, to } = connection;
|
|
619
|
-
if (!value.anchors[from] || !value.anchors[to]) {
|
|
620
|
-
throw new Error(`Connection ${id} references an unknown anchor.`);
|
|
621
|
-
}
|
|
622
|
-
if (connectionIds.has(id)) {
|
|
623
|
-
throw new Error(`Duplicate connection id: ${id}`);
|
|
624
|
-
}
|
|
625
|
-
connectionIds.add(id);
|
|
626
|
-
}
|
|
627
|
-
const chapterIds = /* @__PURE__ */ new Set();
|
|
628
|
-
const sceneIds = /* @__PURE__ */ new Set();
|
|
629
|
-
for (const [chapterIndex, chapter] of value.chapters.entries()) {
|
|
630
|
-
if (!isRecord2(chapter) || !Array.isArray(chapter.scenes) || chapter.scenes.length === 0) {
|
|
631
|
-
throw new Error(`chapters[${chapterIndex}] must contain a non-empty scenes array.`);
|
|
632
|
-
}
|
|
633
|
-
requireString2(chapter.id, `chapters[${chapterIndex}].id`);
|
|
634
|
-
requireString2(chapter.title, `chapters[${chapterIndex}].title`);
|
|
635
|
-
requireOnlyKeys(chapter, ["id", "title", "scenes"], `chapters[${chapterIndex}]`);
|
|
636
|
-
if (chapterIds.has(chapter.id)) throw new Error(`Duplicate chapter id: ${chapter.id}`);
|
|
637
|
-
chapterIds.add(chapter.id);
|
|
638
|
-
for (const [sceneIndex, scene] of chapter.scenes.entries()) {
|
|
639
|
-
const path = `chapters[${chapterIndex}].scenes[${sceneIndex}]`;
|
|
640
|
-
if (!isRecord2(scene)) throw new Error(`${path} must be an object.`);
|
|
641
|
-
requireString2(scene.id, `${path}.id`);
|
|
642
|
-
requireString2(scene.title, `${path}.title`);
|
|
643
|
-
if (sceneIds.has(scene.id)) throw new Error(`Duplicate scene id: ${scene.id}`);
|
|
644
|
-
sceneIds.add(scene.id);
|
|
645
|
-
validateNarrative2(scene, path);
|
|
646
|
-
if (scene.kind === "deconstructed-diff") {
|
|
647
|
-
validateDeconstructedScene2(scene, path);
|
|
648
|
-
continue;
|
|
649
|
-
}
|
|
650
|
-
if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
|
|
651
|
-
throw new Error(`${path} must contain a non-empty steps array.`);
|
|
652
|
-
}
|
|
653
|
-
if (scene.kind === "stacked-diff") {
|
|
654
|
-
validateStackedScene2(scene, path);
|
|
655
|
-
continue;
|
|
656
|
-
}
|
|
657
|
-
if (scene.kind !== void 0 && scene.kind !== "walkthrough") {
|
|
658
|
-
throw new Error(`${path}.kind must be walkthrough, stacked-diff, or deconstructed-diff.`);
|
|
1134
|
+
const stageSteps = value.steps.filter((step) => isRecord2(step) && Number(step.pairIndex) === stageIndex);
|
|
1135
|
+
if (!isRecord2(panel) || stageSteps.length === 0) {
|
|
1136
|
+
throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
|
|
659
1137
|
}
|
|
660
|
-
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
requireString2(step.id, `${stepPath}.id`);
|
|
668
|
-
requireString2(step.title, `${stepPath}.title`);
|
|
669
|
-
requireString2(step.body, `${stepPath}.body`);
|
|
670
|
-
requireString2(step.focus, `${stepPath}.focus`);
|
|
671
|
-
requireOnlyKeys(step, ["id", "title", "body", "focus", "connection", "depth"], stepPath);
|
|
672
|
-
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
673
|
-
throw new Error(`${stepPath}.depth must be mentioned, explained, or contextualized.`);
|
|
674
|
-
}
|
|
675
|
-
if (stepIds.has(step.id)) throw new Error(`Duplicate step id in scene ${scene.id}: ${step.id}`);
|
|
676
|
-
stepIds.add(step.id);
|
|
677
|
-
const focus = step.focus;
|
|
678
|
-
if (!value.anchors[focus]) {
|
|
679
|
-
throw new Error(`${stepPath} references unknown anchor ${focus}.`);
|
|
680
|
-
}
|
|
681
|
-
optionalString(step.connection, `${stepPath}.connection`);
|
|
682
|
-
const connection = step.connection;
|
|
683
|
-
if (typeof connection === "string" && !connectionIds.has(connection)) {
|
|
684
|
-
throw new Error(`${stepPath} references unknown connection ${connection}.`);
|
|
685
|
-
}
|
|
1138
|
+
const firstStep = stageSteps[0];
|
|
1139
|
+
const fallbackStageId = isRecord2(firstStep) ? firstStep.id : void 0;
|
|
1140
|
+
if (stageSteps.some((step) => {
|
|
1141
|
+
if (!isRecord2(step)) return true;
|
|
1142
|
+
return (step.stageId ?? fallbackStageId) !== panel.stageId;
|
|
1143
|
+
})) {
|
|
1144
|
+
throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
|
|
686
1145
|
}
|
|
687
1146
|
}
|
|
688
1147
|
}
|
|
689
|
-
return { ...value, connections: rawConnections };
|
|
690
1148
|
}
|
|
691
|
-
function
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
requireString2(stage.title, `${stagePath}.title`);
|
|
705
|
-
requireString2(stage.narration, `${stagePath}.narration`);
|
|
706
|
-
if (!Array.isArray(stage.changes) || stage.changes.length === 0) {
|
|
707
|
-
throw new Error(`${stagePath}.changes must be a non-empty array.`);
|
|
1149
|
+
function validateMultiPanelFiles(value, panelCount, index) {
|
|
1150
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].files must be non-empty.`);
|
|
1151
|
+
for (const [fileIndex, file] of value.entries()) {
|
|
1152
|
+
if (!isRecord2(file)) throw new Error(`scenes[${index}].files[${fileIndex}] must be an object.`);
|
|
1153
|
+
requireString2(file.path, `scenes[${index}].files[${fileIndex}].path`);
|
|
1154
|
+
if (!Array.isArray(file.panels) || file.panels.length !== panelCount) {
|
|
1155
|
+
throw new Error(`scenes[${index}].files[${fileIndex}].panels must match panel count.`);
|
|
1156
|
+
}
|
|
1157
|
+
for (const [panelIndex, panel] of file.panels.entries()) {
|
|
1158
|
+
if (!isRecord2(panel)) throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}] must be an object.`);
|
|
1159
|
+
for (const key of ["id", "label", "content"]) requireString2(panel[key], `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].${key}`);
|
|
1160
|
+
if (panel.path !== void 0) requireString2(panel.path, `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].path`);
|
|
1161
|
+
if (typeof panel.exists !== "boolean") throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}].exists must be boolean.`);
|
|
708
1162
|
}
|
|
709
|
-
if (stageIds.has(stage.id)) throw new Error(`Duplicate deconstructed stage id: ${stage.id}`);
|
|
710
|
-
stageIds.add(stage.id);
|
|
711
|
-
stage.changes.forEach((change, changeIndex) => validateDeconstructedSelection(
|
|
712
|
-
change,
|
|
713
|
-
`${stagePath}.changes[${changeIndex}]`,
|
|
714
|
-
false
|
|
715
|
-
));
|
|
716
|
-
}
|
|
717
|
-
if (scene.exclusions !== void 0) {
|
|
718
|
-
if (!Array.isArray(scene.exclusions)) throw new Error(`${path}.exclusions must be an array.`);
|
|
719
|
-
scene.exclusions.forEach((exclusion, exclusionIndex) => validateDeconstructedSelection(
|
|
720
|
-
exclusion,
|
|
721
|
-
`${path}.exclusions[${exclusionIndex}]`,
|
|
722
|
-
true
|
|
723
|
-
));
|
|
724
1163
|
}
|
|
725
1164
|
}
|
|
726
|
-
function
|
|
727
|
-
if (!
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
if (
|
|
1165
|
+
function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
|
|
1166
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].steps must be non-empty.`);
|
|
1167
|
+
for (const [stepIndex, step] of value.entries()) {
|
|
1168
|
+
if (!isRecord2(step)) throw new Error(`scenes[${index}].steps[${stepIndex}] must be an object.`);
|
|
1169
|
+
for (const key of ["id", "title", "body", "file", "side"]) requireString2(step[key], `scenes[${index}].steps[${stepIndex}].${key}`);
|
|
1170
|
+
requireNonNegativeInteger(step.pairIndex, `scenes[${index}].steps[${stepIndex}].pairIndex`);
|
|
1171
|
+
if (Number(step.pairIndex) >= panelCount - 1) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex is outside the panels.`);
|
|
1172
|
+
if (deconstructed) {
|
|
1173
|
+
requireStringArray2(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
|
|
1174
|
+
if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
|
|
1175
|
+
}
|
|
1176
|
+
if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
|
|
1177
|
+
if (step.startLine !== void 0) requirePositiveInteger(step.startLine, `scenes[${index}].steps[${stepIndex}].startLine`);
|
|
1178
|
+
if (step.endLine !== void 0) requirePositiveInteger(step.endLine, `scenes[${index}].steps[${stepIndex}].endLine`);
|
|
733
1179
|
}
|
|
734
|
-
if (requiresReason) requireString2(value.reason, `${path}.reason`);
|
|
735
1180
|
}
|
|
736
|
-
function
|
|
737
|
-
|
|
738
|
-
if (
|
|
739
|
-
|
|
1181
|
+
function validateTourFile(value, index) {
|
|
1182
|
+
if (!isRecord2(value)) throw new Error(`files[${index}] must be an object.`);
|
|
1183
|
+
if (value.kind === "text-diff") {
|
|
1184
|
+
validateScene(value, index, 1);
|
|
1185
|
+
return;
|
|
740
1186
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const entryPath = `${path}.stack[${index}]`;
|
|
744
|
-
if (!isRecord2(entry)) throw new Error(`${entryPath} must be an object.`);
|
|
745
|
-
requireOnlyKeys(entry, ["id", "ref", "label"], entryPath);
|
|
746
|
-
requireString2(entry.id, `${entryPath}.id`);
|
|
747
|
-
requireString2(entry.ref, `${entryPath}.ref`);
|
|
748
|
-
optionalString(entry.label, `${entryPath}.label`);
|
|
749
|
-
if (stackIds.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
|
|
750
|
-
stackIds.add(entry.id);
|
|
1187
|
+
if (value.kind !== "omitted") {
|
|
1188
|
+
throw new Error(`files[${index}].kind must be text-diff or omitted.`);
|
|
751
1189
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
stepIds.add(stepId);
|
|
766
|
-
if (!Array.isArray(step.pair) || step.pair.length !== 2 || step.pair.some((id) => typeof id !== "string")) {
|
|
767
|
-
throw new Error(`${stepPath}.pair must contain two stack entry ids.`);
|
|
1190
|
+
for (const key of ["id", "title", "path", "changeKind", "reason"]) {
|
|
1191
|
+
requireString2(value[key], `files[${index}].${key}`);
|
|
1192
|
+
}
|
|
1193
|
+
if (value.previousPath !== void 0) requireString2(value.previousPath, `files[${index}].previousPath`);
|
|
1194
|
+
requireNonNegativeInteger(value.additions, `files[${index}].additions`);
|
|
1195
|
+
requireNonNegativeInteger(value.deletions, `files[${index}].deletions`);
|
|
1196
|
+
}
|
|
1197
|
+
function collectLegacyTourFiles(scenes) {
|
|
1198
|
+
const files = /* @__PURE__ */ new Map();
|
|
1199
|
+
for (const scene of scenes) {
|
|
1200
|
+
if (scene.kind === "text-diff") {
|
|
1201
|
+
if (!files.has(scene.path)) files.set(scene.path, scene);
|
|
1202
|
+
continue;
|
|
768
1203
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
if (step.side !== void 0 && step.side !== "left" && step.side !== "right") throw new Error(`${stepPath}.side must be left or right.`);
|
|
774
|
-
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]))) {
|
|
775
|
-
throw new Error(`${stepPath}.lines must contain an ordered positive line range.`);
|
|
1204
|
+
if (scene.kind === "walkthrough") {
|
|
1205
|
+
for (const step of scene.steps) {
|
|
1206
|
+
if (!files.has(step.diff.path)) files.set(step.diff.path, step.diff);
|
|
1207
|
+
}
|
|
776
1208
|
}
|
|
777
1209
|
}
|
|
1210
|
+
return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
1211
|
+
}
|
|
1212
|
+
function validateResolvedAnchor(value, path) {
|
|
1213
|
+
if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
|
|
1214
|
+
for (const key of ["id", "path", "revision", "excerpt"]) requireString2(value[key], `${path}.${key}`);
|
|
1215
|
+
if (value.revision !== "base" && value.revision !== "head") throw new Error(`${path}.revision must be base or head.`);
|
|
1216
|
+
requirePositiveInteger(value.startLine, `${path}.startLine`);
|
|
1217
|
+
requirePositiveInteger(value.endLine, `${path}.endLine`);
|
|
1218
|
+
if (Number(value.endLine) < Number(value.startLine)) throw new Error(`${path}.endLine must not precede startLine.`);
|
|
778
1219
|
}
|
|
779
1220
|
function validateNarrative2(value, path) {
|
|
780
1221
|
requireString2(value.summary, `${path}.summary`);
|
|
@@ -786,13 +1227,8 @@ function isRecord2(value) {
|
|
|
786
1227
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
787
1228
|
}
|
|
788
1229
|
function requireString2(value, path) {
|
|
789
|
-
if (typeof value !== "string"
|
|
790
|
-
throw new Error(`${path} must be a
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
function optionalString(value, path) {
|
|
794
|
-
if (value !== void 0) {
|
|
795
|
-
requireString2(value, path);
|
|
1230
|
+
if (typeof value !== "string") {
|
|
1231
|
+
throw new Error(`${path} must be a string.`);
|
|
796
1232
|
}
|
|
797
1233
|
}
|
|
798
1234
|
function requireStringArray2(value, path) {
|
|
@@ -800,12 +1236,19 @@ function requireStringArray2(value, path) {
|
|
|
800
1236
|
throw new Error(`${path} must be an array of strings.`);
|
|
801
1237
|
}
|
|
802
1238
|
}
|
|
803
|
-
function
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1239
|
+
function requireNonNegativeInteger(value, path) {
|
|
1240
|
+
if (!Number.isInteger(value) || Number(value) < 0) {
|
|
1241
|
+
throw new Error(`${path} must be a non-negative integer.`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
function requirePercentage(value, path) {
|
|
1245
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
|
|
1246
|
+
throw new Error(`${path} must be a number from 0 to 100.`);
|
|
807
1247
|
}
|
|
808
1248
|
}
|
|
1249
|
+
function requirePositiveInteger(value, path) {
|
|
1250
|
+
if (!Number.isInteger(value) || Number(value) < 1) throw new Error(`${path} must be a positive integer.`);
|
|
1251
|
+
}
|
|
809
1252
|
|
|
810
1253
|
// src/changeInventory.ts
|
|
811
1254
|
var import_crypto = require("crypto");
|
|
@@ -1585,12 +2028,20 @@ var DIFF_TIMEOUT_MS = readPositiveIntegerEnv("BYGONE_DIFF_TIMEOUT_MS", DEFAULT_D
|
|
|
1585
2028
|
function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
1586
2029
|
const leftLines = normalizeLines(leftContent);
|
|
1587
2030
|
const rightLines = normalizeLines(rightContent);
|
|
1588
|
-
|
|
2031
|
+
if (leftContent === rightContent) {
|
|
2032
|
+
return buildIdenticalTwoWayDiffModel(leftLines);
|
|
2033
|
+
}
|
|
2034
|
+
const interned = internDiffLines(leftLines, rightLines);
|
|
2035
|
+
const internedChanges = diffArrays(interned.left, interned.right, {
|
|
1589
2036
|
timeout: options.timeoutMs ?? DIFF_TIMEOUT_MS
|
|
1590
2037
|
});
|
|
1591
|
-
if (!
|
|
2038
|
+
if (!internedChanges) {
|
|
1592
2039
|
return buildConservativeTwoWayDiffModel(leftLines, rightLines);
|
|
1593
2040
|
}
|
|
2041
|
+
const changes = internedChanges.map((change) => ({
|
|
2042
|
+
...change,
|
|
2043
|
+
value: change.value.map((lineId) => interned.lines[lineId])
|
|
2044
|
+
}));
|
|
1594
2045
|
const rows = [];
|
|
1595
2046
|
const renderedLeftLines = [];
|
|
1596
2047
|
const renderedRightLines = [];
|
|
@@ -1614,29 +2065,18 @@ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
|
1614
2065
|
}
|
|
1615
2066
|
if (change.removed && index + 1 < changes.length && changes[index + 1].added) {
|
|
1616
2067
|
const nextChange = changes[index + 1];
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
renderedRightLines.push(renderedRightLine);
|
|
1630
|
-
}
|
|
1631
|
-
if (renderedLeftLine && renderedRightLine) {
|
|
1632
|
-
applyInlineHighlightPair(renderedLeftLine, renderedRightLine);
|
|
1633
|
-
}
|
|
1634
|
-
rows.push(makeDiffRow(
|
|
1635
|
-
removedLine === void 0 ? makePlaceholder() : makeDiffCell("removed", removedLine, leftLineNumber++),
|
|
1636
|
-
addedLine === void 0 ? makePlaceholder() : makeDiffCell("added", addedLine, rightLineNumber++)
|
|
1637
|
-
));
|
|
1638
|
-
}
|
|
1639
|
-
blocks.push(makeDiffBlock("replace", leftStart, renderedLeftLines.length, rightStart, renderedRightLines.length));
|
|
2068
|
+
const nextLineNumbers = appendReplacementCandidate(
|
|
2069
|
+
removedLines,
|
|
2070
|
+
nextChange.value,
|
|
2071
|
+
leftLineNumber,
|
|
2072
|
+
rightLineNumber,
|
|
2073
|
+
rows,
|
|
2074
|
+
renderedLeftLines,
|
|
2075
|
+
renderedRightLines,
|
|
2076
|
+
blocks
|
|
2077
|
+
);
|
|
2078
|
+
leftLineNumber = nextLineNumbers.left;
|
|
2079
|
+
rightLineNumber = nextLineNumbers.right;
|
|
1640
2080
|
index++;
|
|
1641
2081
|
continue;
|
|
1642
2082
|
}
|
|
@@ -1672,7 +2112,40 @@ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
|
1672
2112
|
leftLines: renderedLeftLines,
|
|
1673
2113
|
rightLines: renderedRightLines,
|
|
1674
2114
|
blocks,
|
|
1675
|
-
hasChanges
|
|
2115
|
+
hasChanges,
|
|
2116
|
+
quality: "exact"
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
function internDiffLines(leftLines, rightLines) {
|
|
2120
|
+
const lineIds = /* @__PURE__ */ new Map();
|
|
2121
|
+
const lines = [];
|
|
2122
|
+
const intern = (line) => {
|
|
2123
|
+
const existingId = lineIds.get(line);
|
|
2124
|
+
if (existingId !== void 0) {
|
|
2125
|
+
return existingId;
|
|
2126
|
+
}
|
|
2127
|
+
const lineId = lines.length;
|
|
2128
|
+
lineIds.set(line, lineId);
|
|
2129
|
+
lines.push(line);
|
|
2130
|
+
return lineId;
|
|
2131
|
+
};
|
|
2132
|
+
return {
|
|
2133
|
+
left: leftLines.map(intern),
|
|
2134
|
+
right: rightLines.map(intern),
|
|
2135
|
+
lines
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
function buildIdenticalTwoWayDiffModel(lines) {
|
|
2139
|
+
return {
|
|
2140
|
+
rows: lines.map((line, index) => makeDiffRow(
|
|
2141
|
+
makeDiffCell("context", line, index + 1),
|
|
2142
|
+
makeDiffCell("context", line, index + 1)
|
|
2143
|
+
)),
|
|
2144
|
+
leftLines: lines.map((line, index) => makeDiffLine("context", line, index + 1)),
|
|
2145
|
+
rightLines: lines.map((line, index) => makeDiffLine("context", line, index + 1)),
|
|
2146
|
+
blocks: [],
|
|
2147
|
+
hasChanges: false,
|
|
2148
|
+
quality: "exact"
|
|
1676
2149
|
};
|
|
1677
2150
|
}
|
|
1678
2151
|
function buildConservativeTwoWayDiffModel(leftLines, rightLines) {
|
|
@@ -1699,7 +2172,8 @@ function buildConservativeTwoWayDiffModel(leftLines, rightLines) {
|
|
|
1699
2172
|
leftLines: renderedLeftLines,
|
|
1700
2173
|
rightLines: renderedRightLines,
|
|
1701
2174
|
blocks: rows.length === 0 ? [] : [makeDiffBlock(blockKind, 0, leftLines.length, 0, rightLines.length)],
|
|
1702
|
-
hasChanges: rows.length > 0
|
|
2175
|
+
hasChanges: rows.length > 0,
|
|
2176
|
+
quality: "fallback"
|
|
1703
2177
|
};
|
|
1704
2178
|
}
|
|
1705
2179
|
function normalizeLines(content) {
|
|
@@ -1712,6 +2186,100 @@ function normalizeLines(content) {
|
|
|
1712
2186
|
}
|
|
1713
2187
|
return lines;
|
|
1714
2188
|
}
|
|
2189
|
+
function appendReplacementCandidate(leftLines, rightLines, initialLeftLineNumber, initialRightLineNumber, rows, renderedLeftLines, renderedRightLines, blocks) {
|
|
2190
|
+
const aligned = alignReplacementLines(leftLines, rightLines);
|
|
2191
|
+
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 normalized = aligned.flatMap((row) => {
|
|
2194
|
+
if (row.left === void 0 || row.right === void 0 || hasCredibleLineCorrespondence(row.left, row.right)) {
|
|
2195
|
+
return [row];
|
|
2196
|
+
}
|
|
2197
|
+
return [{ left: row.left }, { right: row.right }];
|
|
2198
|
+
});
|
|
2199
|
+
let leftLineNumber = initialLeftLineNumber;
|
|
2200
|
+
let rightLineNumber = initialRightLineNumber;
|
|
2201
|
+
let blockKind;
|
|
2202
|
+
let blockLeftStart = renderedLeftLines.length;
|
|
2203
|
+
let blockRightStart = renderedRightLines.length;
|
|
2204
|
+
const flushBlock = () => {
|
|
2205
|
+
if (!blockKind) return;
|
|
2206
|
+
blocks.push(makeDiffBlock(
|
|
2207
|
+
blockKind,
|
|
2208
|
+
blockLeftStart,
|
|
2209
|
+
renderedLeftLines.length,
|
|
2210
|
+
blockRightStart,
|
|
2211
|
+
renderedRightLines.length
|
|
2212
|
+
));
|
|
2213
|
+
};
|
|
2214
|
+
for (const { left, right } of normalized) {
|
|
2215
|
+
const nextKind = coherentBlock ? "replace" : left !== void 0 && right !== void 0 ? "replace" : left !== void 0 ? "delete" : "insert";
|
|
2216
|
+
if (blockKind !== nextKind) {
|
|
2217
|
+
flushBlock();
|
|
2218
|
+
blockKind = nextKind;
|
|
2219
|
+
blockLeftStart = renderedLeftLines.length;
|
|
2220
|
+
blockRightStart = renderedRightLines.length;
|
|
2221
|
+
}
|
|
2222
|
+
let renderedLeftLine;
|
|
2223
|
+
let renderedRightLine;
|
|
2224
|
+
if (left !== void 0) {
|
|
2225
|
+
renderedLeftLine = makeDiffLine("removed", left, leftLineNumber);
|
|
2226
|
+
renderedLeftLines.push(renderedLeftLine);
|
|
2227
|
+
}
|
|
2228
|
+
if (right !== void 0) {
|
|
2229
|
+
renderedRightLine = makeDiffLine("added", right, rightLineNumber);
|
|
2230
|
+
renderedRightLines.push(renderedRightLine);
|
|
2231
|
+
}
|
|
2232
|
+
if (renderedLeftLine && renderedRightLine) {
|
|
2233
|
+
applyInlineHighlightPair(renderedLeftLine, renderedRightLine);
|
|
2234
|
+
} else if (coherentBlock) {
|
|
2235
|
+
if (renderedLeftLine) applyFullLineHighlight(renderedLeftLine);
|
|
2236
|
+
if (renderedRightLine) applyFullLineHighlight(renderedRightLine);
|
|
2237
|
+
}
|
|
2238
|
+
rows.push(makeDiffRow(
|
|
2239
|
+
left === void 0 ? makePlaceholder() : makeDiffCell("removed", left, leftLineNumber++),
|
|
2240
|
+
right === void 0 ? makePlaceholder() : makeDiffCell("added", right, rightLineNumber++)
|
|
2241
|
+
));
|
|
2242
|
+
}
|
|
2243
|
+
flushBlock();
|
|
2244
|
+
return { left: leftLineNumber, right: rightLineNumber };
|
|
2245
|
+
}
|
|
2246
|
+
function hasCredibleLineCorrespondence(left, right) {
|
|
2247
|
+
const score = scoreReplacementLinePair(left, right);
|
|
2248
|
+
if (score.eligible) return true;
|
|
2249
|
+
const leftAnchor = declarationAnchorKey(left);
|
|
2250
|
+
if (leftAnchor && leftAnchor === declarationAnchorKey(right)) return true;
|
|
2251
|
+
const leftTokens = new Set(tokenizeMatchingContent(normalizeMatchingContent(left)).filter(isSharedContentToken));
|
|
2252
|
+
return tokenizeMatchingContent(normalizeMatchingContent(right)).some((token) => leftTokens.has(token) && isSharedContentToken(token));
|
|
2253
|
+
}
|
|
2254
|
+
var NON_SUBSTANTIVE_SHARED_TOKENS = /* @__PURE__ */ new Set([
|
|
2255
|
+
"and",
|
|
2256
|
+
"are",
|
|
2257
|
+
"for",
|
|
2258
|
+
"from",
|
|
2259
|
+
"into",
|
|
2260
|
+
"that",
|
|
2261
|
+
"the",
|
|
2262
|
+
"this",
|
|
2263
|
+
"to",
|
|
2264
|
+
"with"
|
|
2265
|
+
]);
|
|
2266
|
+
function isSharedContentToken(token) {
|
|
2267
|
+
return token.length >= 3 && /^[\p{L}\p{N}_$]+$/u.test(token) && !NON_SUBSTANTIVE_SHARED_TOKENS.has(token);
|
|
2268
|
+
}
|
|
2269
|
+
function hasCoherentBlockCorrespondence(leftLines, rightLines, crediblePairs) {
|
|
2270
|
+
if (crediblePairs === 0) return false;
|
|
2271
|
+
const longestSide = Math.max(leftLines.length, rightLines.length);
|
|
2272
|
+
if (longestSide <= 2 || crediblePairs * 2 >= longestSide) return true;
|
|
2273
|
+
const roles = [...leftLines, ...rightLines].map(collectionLineRole);
|
|
2274
|
+
return roles[0] !== null && roles.every((role) => role === roles[0]);
|
|
2275
|
+
}
|
|
2276
|
+
function collectionLineRole(line) {
|
|
2277
|
+
const normalized = line.trimStart();
|
|
2278
|
+
if (/^[-*+]\s+/.test(normalized)) return "bullet";
|
|
2279
|
+
if (/^\d+[.)]\s+/.test(normalized)) return "ordered";
|
|
2280
|
+
if (/^>\s+/.test(normalized)) return "quote";
|
|
2281
|
+
return null;
|
|
2282
|
+
}
|
|
1715
2283
|
var MAX_ALIGNMENT_CELLS = 1e4;
|
|
1716
2284
|
var MAX_SCORING_LINE_LENGTH = 2e3;
|
|
1717
2285
|
var MINIMUM_MATCH_SCORE = 0.52;
|
|
@@ -1724,6 +2292,7 @@ var CONTEXTUAL_AMBIGUITY_MARGIN = 0.05;
|
|
|
1724
2292
|
var GAP_PENALTY = -0.12;
|
|
1725
2293
|
var MAX_BOUNDED_CANDIDATES = 5e3;
|
|
1726
2294
|
var MAX_LARGE_HUNK_ANCHORS = 2e3;
|
|
2295
|
+
var MAX_ALIGNMENT_CHARACTER_CELLS = 5e7;
|
|
1727
2296
|
var MAX_RARE_TOKEN_OCCURRENCES = 4;
|
|
1728
2297
|
var MAX_CONTEXTUAL_HUNK_LINES = 8;
|
|
1729
2298
|
var MAX_CONTEXTUAL_LENGTH_DELTA = 4;
|
|
@@ -1734,9 +2303,17 @@ function alignReplacementLines(leftLines, rightLines) {
|
|
|
1734
2303
|
if (rightLines.length === 0) {
|
|
1735
2304
|
return leftLines.map((left) => ({ left }));
|
|
1736
2305
|
}
|
|
2306
|
+
const leftCharacterCount = leftLines.reduce((total, line) => total + line.length, 0);
|
|
2307
|
+
const rightCharacterCount = rightLines.reduce((total, line) => total + line.length, 0);
|
|
2308
|
+
if (leftCharacterCount * rightCharacterCount > MAX_ALIGNMENT_CHARACTER_CELLS) {
|
|
2309
|
+
return [
|
|
2310
|
+
...leftLines.map((left) => ({ left })),
|
|
2311
|
+
...rightLines.map((right) => ({ right }))
|
|
2312
|
+
];
|
|
2313
|
+
}
|
|
1737
2314
|
if (leftLines.length === 1 && rightLines.length === 1) {
|
|
1738
2315
|
const singletonScore = scoreReplacementLinePair(leftLines[0], rightLines[0]);
|
|
1739
|
-
if ((singletonScore.eligible || singletonScore.score >= MINIMUM_SINGLE_PAIR_SCORE) && haveCompatibleLineRoles(leftLines[0], rightLines[0]) && isInformativeLine(normalizeMatchingContent(leftLines[0])) && isInformativeLine(normalizeMatchingContent(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]))) {
|
|
1740
2317
|
return [{ left: leftLines[0], right: rightLines[0] }];
|
|
1741
2318
|
}
|
|
1742
2319
|
return [{ left: leftLines[0] }, { right: rightLines[0] }];
|
|
@@ -2258,6 +2835,11 @@ function applyInlineHighlightPair(leftLine, rightLine) {
|
|
|
2258
2835
|
rightLine.segments = rightSegments;
|
|
2259
2836
|
}
|
|
2260
2837
|
}
|
|
2838
|
+
function applyFullLineHighlight(line) {
|
|
2839
|
+
if (/[^\s]/.test(line.content)) {
|
|
2840
|
+
line.segments = [{ kind: line.kind, text: line.content, emphasis: true }];
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2261
2843
|
function buildInlineSegments(leftContent, rightContent) {
|
|
2262
2844
|
const changes = diffWordsWithSpace(leftContent, rightContent);
|
|
2263
2845
|
const leftSegments = [];
|
|
@@ -2409,6 +2991,125 @@ function findBlankLineBoundary(lines, target, minimum, maximum) {
|
|
|
2409
2991
|
return Math.min(Math.max(target, minimum), maximum - 1);
|
|
2410
2992
|
}
|
|
2411
2993
|
|
|
2994
|
+
// src/changeAttention.ts
|
|
2995
|
+
var CHANGE_ATTENTION_VERSION = 1;
|
|
2996
|
+
var LOCKFILES = /* @__PURE__ */ new Set([
|
|
2997
|
+
"cargo.lock",
|
|
2998
|
+
"package-lock.json",
|
|
2999
|
+
"pnpm-lock.yaml",
|
|
3000
|
+
"yarn.lock",
|
|
3001
|
+
"poetry.lock",
|
|
3002
|
+
"gemfile.lock",
|
|
3003
|
+
"composer.lock"
|
|
3004
|
+
]);
|
|
3005
|
+
var MAX_FAN_IN_FILES = 250;
|
|
3006
|
+
var MAX_FAN_IN_TEXT_BYTES = 64 * 1024;
|
|
3007
|
+
function classifyChangeFileRole(filePath) {
|
|
3008
|
+
const lower = filePath.toLowerCase();
|
|
3009
|
+
const fileName = lower.split("/").pop() || lower;
|
|
3010
|
+
if (/(^|\/)(dist|build|generated|vendor)(\/|$)/.test(lower) || /(?:\.min\.js|\.map)$/.test(lower)) return "generated";
|
|
3011
|
+
if (/(^|\/)(tests?|__tests__)(\/|$)/.test(lower) || /(^|\/)test_[^/]+$/.test(lower) || /\.(test|spec)\.[^/]+$/.test(lower)) return "test";
|
|
3012
|
+
if (lower.startsWith("docs/") || fileName === "readme.md" || /\.(md|mdx|rst)$/.test(lower)) return "documentation";
|
|
3013
|
+
if (LOCKFILES.has(fileName) || /(^|\/)(pyproject\.toml|package\.json|go\.mod|go\.sum|cargo\.toml)$/.test(lower)) return "dependency";
|
|
3014
|
+
return "production";
|
|
3015
|
+
}
|
|
3016
|
+
function buildChangeAttention(inputs) {
|
|
3017
|
+
const prepared = inputs.map((input) => prepareFile(input, inputs));
|
|
3018
|
+
const production = prepared.filter((file) => file.role === "production" && !file.mechanical);
|
|
3019
|
+
const documentation = prepared.filter((file) => file.role === "documentation" && !file.mechanical);
|
|
3020
|
+
const eligible = production.length > 0 ? production : documentation;
|
|
3021
|
+
const entry = [...eligible].sort(compareRanked)[0];
|
|
3022
|
+
const fallback = [...prepared].filter((file) => !file.binary).sort((left, right) => right.changedLines - left.changedLines || left.path.localeCompare(right.path))[0];
|
|
3023
|
+
const focusCandidates = prepared.filter((file) => file.path !== (entry == null ? void 0 : entry.path) && file.role === "production" && !file.mechanical).sort(compareRanked).slice(0, Math.max(0, 7 - (entry ? 1 : 0)));
|
|
3024
|
+
const pairedTest = entry || focusCandidates.length > 0 ? prepared.filter((file) => file.role === "test" && !file.mechanical && file.changedLines >= 5 && isTestForFocus(file.path, [entry, ...focusCandidates].filter(Boolean).map((candidate) => candidate.path))).sort(compareRanked)[0] : void 0;
|
|
3025
|
+
if (pairedTest && focusCandidates.length < Math.max(0, 7 - (entry ? 1 : 0))) focusCandidates.push(pairedTest);
|
|
3026
|
+
const focusPaths = focusCandidates.map((file) => file.path);
|
|
3027
|
+
const files = prepared.map((file) => {
|
|
3028
|
+
const attention = file.path === (entry == null ? void 0 : entry.path) ? "entry" : focusPaths.includes(file.path) ? "focus" : file.mechanical ? "mechanical" : "background";
|
|
3029
|
+
return {
|
|
3030
|
+
path: file.path,
|
|
3031
|
+
previousPath: file.previousPath,
|
|
3032
|
+
changeKind: file.changeKind,
|
|
3033
|
+
role: file.role,
|
|
3034
|
+
attention,
|
|
3035
|
+
additions: file.additions,
|
|
3036
|
+
deletions: file.deletions,
|
|
3037
|
+
reason: attention === "entry" ? entryReason(file, production.length) : attentionReason(file),
|
|
3038
|
+
score: file.score
|
|
3039
|
+
};
|
|
3040
|
+
}).sort((left, right) => left.path.localeCompare(right.path));
|
|
3041
|
+
return {
|
|
3042
|
+
version: CHANGE_ATTENTION_VERSION,
|
|
3043
|
+
entryPath: (entry == null ? void 0 : entry.path) || null,
|
|
3044
|
+
entryReason: entry ? entryReason(entry, production.length) : noEntryReason(prepared),
|
|
3045
|
+
fallbackPath: (fallback == null ? void 0 : fallback.path) || null,
|
|
3046
|
+
focusPaths,
|
|
3047
|
+
backgroundPaths: files.filter((file) => file.attention === "background").map((file) => file.path),
|
|
3048
|
+
mechanicalPaths: files.filter((file) => file.attention === "mechanical").map((file) => file.path),
|
|
3049
|
+
files
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
function prepareFile(input, all) {
|
|
3053
|
+
const role = classifyChangeFileRole(input.path);
|
|
3054
|
+
const fileName = input.path.toLowerCase().split("/").pop() || input.path.toLowerCase();
|
|
3055
|
+
const mechanical = Boolean(input.binary) || role === "generated" || LOCKFILES.has(fileName);
|
|
3056
|
+
const changedLines = (input.additions ?? 0) + (input.deletions ?? 0);
|
|
3057
|
+
const contract = /(^|[\/._-])(types?|schema|models?|contracts?|interfaces?)([\/._-]|$)/i.test(input.path);
|
|
3058
|
+
const referenceHints = new Set([
|
|
3059
|
+
fileName.replace(/\.[^.]+$/, ""),
|
|
3060
|
+
...(input.symbolHints || []).map((hint) => hint.toLowerCase())
|
|
3061
|
+
].filter((hint) => hint.length >= 4));
|
|
3062
|
+
const fanIn = [...all].filter((candidate) => candidate.path !== input.path).sort((left, right) => left.path.localeCompare(right.path)).slice(0, MAX_FAN_IN_FILES).filter((candidate) => {
|
|
3063
|
+
var _a;
|
|
3064
|
+
const boundedText = (_a = candidate.headText) == null ? void 0 : _a.slice(0, MAX_FAN_IN_TEXT_BYTES).toLowerCase();
|
|
3065
|
+
return Boolean(boundedText && [...referenceHints].some((hint) => boundedText.includes(hint)));
|
|
3066
|
+
}).length;
|
|
3067
|
+
const roleScore = role === "production" ? 300 : role === "documentation" ? 150 : role === "dependency" ? 80 : role === "test" ? 50 : 0;
|
|
3068
|
+
const sizePenalty = changedLines > 2e3 ? 80 : changedLines > 500 ? 30 : 0;
|
|
3069
|
+
const readmePenalty = fileName === "readme.md" && all.filter((candidate) => !candidate.binary).length > 1 ? 100 : 0;
|
|
3070
|
+
return {
|
|
3071
|
+
...input,
|
|
3072
|
+
role,
|
|
3073
|
+
mechanical,
|
|
3074
|
+
changedLines,
|
|
3075
|
+
contract,
|
|
3076
|
+
fanIn,
|
|
3077
|
+
binary: Boolean(input.binary),
|
|
3078
|
+
score: mechanical ? -1e3 : roleScore + (contract ? 100 : 0) + fanIn * 40 + (input.commitCount || 0) * 5 - sizePenalty - readmePenalty
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
function compareRanked(left, right) {
|
|
3082
|
+
return right.score - left.score || right.fanIn - left.fanIn || left.changedLines - right.changedLines || left.path.localeCompare(right.path);
|
|
3083
|
+
}
|
|
3084
|
+
function entryReason(file, productionCount) {
|
|
3085
|
+
if (file.fanIn > 0) return `Referenced by ${file.fanIn} other changed file${file.fanIn === 1 ? "" : "s"}.`;
|
|
3086
|
+
if (file.contract) return "Contract-shaped path central to the change.";
|
|
3087
|
+
if (productionCount === 1) return "Only production file in this range.";
|
|
3088
|
+
return "Highest-ranked production file for a first pass.";
|
|
3089
|
+
}
|
|
3090
|
+
function attentionReason(file) {
|
|
3091
|
+
if (file.binary) return "Binary file.";
|
|
3092
|
+
if (file.mechanical) return file.role === "generated" ? "Generated artifact." : "Dependency lockfile.";
|
|
3093
|
+
if (file.role === "test") return "Test or proof file.";
|
|
3094
|
+
if (file.role === "documentation") return "Documentation change.";
|
|
3095
|
+
if (file.role === "dependency") return "Dependency or packaging manifest.";
|
|
3096
|
+
return "Additional production file.";
|
|
3097
|
+
}
|
|
3098
|
+
function noEntryReason(files) {
|
|
3099
|
+
if (files.every((file) => file.mechanical)) return "This change is entirely mechanical or generated.";
|
|
3100
|
+
if (files.every((file) => file.mechanical || file.role === "test")) return "This change contains only tests and mechanical files.";
|
|
3101
|
+
return "No single entry-point is reliable for this change.";
|
|
3102
|
+
}
|
|
3103
|
+
function isTestForFocus(testPath, focusPaths) {
|
|
3104
|
+
const normalizedTest = testPath.toLowerCase().replace(/\.(test|spec)(?=\.)/, "").replace(/^test_/, "");
|
|
3105
|
+
return focusPaths.some((candidate) => {
|
|
3106
|
+
var _a;
|
|
3107
|
+
const normalizedCandidate = candidate.toLowerCase();
|
|
3108
|
+
const stem = ((_a = normalizedCandidate.split("/").pop()) == null ? void 0 : _a.replace(/\.[^.]+$/, "")) || normalizedCandidate;
|
|
3109
|
+
return stem.length >= 3 && normalizedTest.includes(stem);
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
|
|
2412
3113
|
// src/changeTourContext.ts
|
|
2413
3114
|
var import_child_process3 = require("child_process");
|
|
2414
3115
|
var CHANGE_TOUR_CONTEXT_VERSION = 1;
|
|
@@ -2486,7 +3187,7 @@ function buildContextFile(repoRoot, baseOid, headOid, changedPath, maxPatchBytes
|
|
|
2486
3187
|
path: changedPath.path,
|
|
2487
3188
|
previousPath: changedPath.previousPath,
|
|
2488
3189
|
changeKind: changedPath.kind,
|
|
2489
|
-
role:
|
|
3190
|
+
role: classifyChangeFileRole(changedPath.path),
|
|
2490
3191
|
additions: stats.additions,
|
|
2491
3192
|
deletions: stats.deletions,
|
|
2492
3193
|
binary: stats.binary,
|
|
@@ -2583,15 +3284,6 @@ function readGitText(repoRoot, oid, relativePath, maxBytes) {
|
|
|
2583
3284
|
return void 0;
|
|
2584
3285
|
}
|
|
2585
3286
|
}
|
|
2586
|
-
function classifyFileRole(filePath) {
|
|
2587
|
-
const lower = filePath.toLowerCase();
|
|
2588
|
-
const fileName = lower.split("/").pop() || lower;
|
|
2589
|
-
if (/(^|\/)(tests?|__tests__)(\/|$)/.test(lower) || /(^|\/)test_[^/]+$/.test(lower) || /\.(test|spec)\.[^/]+$/.test(lower)) return "test";
|
|
2590
|
-
if (lower.startsWith("docs/") || fileName === "readme.md" || /\.(md|mdx|rst)$/.test(lower)) return "documentation";
|
|
2591
|
-
if (/(^|\/)(package-lock\.json|.*\.lock|pyproject\.toml|package\.json|go\.mod|cargo\.toml)$/.test(lower)) return "dependency";
|
|
2592
|
-
if (/(^|\/)(dist|build|generated|vendor)(\/|$)/.test(lower) || /\.(min\.js|map)$/.test(lower)) return "generated";
|
|
2593
|
-
return "production";
|
|
2594
|
-
}
|
|
2595
3287
|
function runGit2(repoRoot, args) {
|
|
2596
3288
|
return (0, import_child_process3.execFileSync)("git", args, {
|
|
2597
3289
|
cwd: repoRoot,
|
|
@@ -2616,8 +3308,21 @@ var CHAPTERS = {
|
|
|
2616
3308
|
packaging: { id: "packaging", title: "Packaging and dependencies", priority: 4 }
|
|
2617
3309
|
};
|
|
2618
3310
|
function buildChangeTourManifest(startPath, options = {}) {
|
|
2619
|
-
var _a, _b, _c, _d
|
|
2620
|
-
const
|
|
3311
|
+
var _a, _b, _c, _d;
|
|
3312
|
+
const source = options.source ? parseChangeTourSource(options.source) : void 0;
|
|
3313
|
+
const range = resolveBranchReviewRange(
|
|
3314
|
+
startPath,
|
|
3315
|
+
options.headRef ?? ((_a = source == null ? void 0 : source.range) == null ? void 0 : _a.head),
|
|
3316
|
+
options.baseRef ?? ((_b = source == null ? void 0 : source.range) == null ? void 0 : _b.base)
|
|
3317
|
+
);
|
|
3318
|
+
if (source == null ? void 0 : source.review) {
|
|
3319
|
+
validateChangeTourReview(source.review, {
|
|
3320
|
+
expectedRange: {
|
|
3321
|
+
baseOid: range.mergeBaseOid,
|
|
3322
|
+
headOid: range.headOid
|
|
3323
|
+
}
|
|
3324
|
+
});
|
|
3325
|
+
}
|
|
2621
3326
|
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_TOUR_FILE_BYTES;
|
|
2622
3327
|
const maxLineBytes = options.maxLineBytes ?? DEFAULT_MAX_TOUR_LINE_BYTES;
|
|
2623
3328
|
const omittedFiles = [];
|
|
@@ -2625,8 +3330,9 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2625
3330
|
const sceneRecords = [];
|
|
2626
3331
|
let totalAdditions = 0;
|
|
2627
3332
|
let totalDeletions = 0;
|
|
3333
|
+
let totalBinaryFiles = 0;
|
|
2628
3334
|
for (const changedPath of range.changedPaths) {
|
|
2629
|
-
const { additions, deletions } = readGitLineStats(
|
|
3335
|
+
const { additions, deletions, binary } = readGitLineStats(
|
|
2630
3336
|
range.repoRoot,
|
|
2631
3337
|
range.mergeBaseOid,
|
|
2632
3338
|
range.headOid,
|
|
@@ -2635,6 +3341,7 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2635
3341
|
);
|
|
2636
3342
|
totalAdditions += additions;
|
|
2637
3343
|
totalDeletions += deletions;
|
|
3344
|
+
if (binary) totalBinaryFiles += 1;
|
|
2638
3345
|
const pair = resolveReviewPathPair(range.changedPaths, changedPath.path);
|
|
2639
3346
|
if (!pair) {
|
|
2640
3347
|
omittedFiles.push(changedPath.path);
|
|
@@ -2675,21 +3382,50 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2675
3382
|
}
|
|
2676
3383
|
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
2677
3384
|
sceneRecords.sort((left, right) => left.chapter.priority - right.chapter.priority || left.scene.path.localeCompare(right.scene.path));
|
|
2678
|
-
|
|
2679
|
-
if (options.story &&
|
|
3385
|
+
let defaultScenes = sceneRecords.map(({ scene }, index) => ({ ...scene, id: `file-${index + 1}` }));
|
|
3386
|
+
if (options.story && source) {
|
|
2680
3387
|
throw new Error("A change tour can use either a legacy story or a source file, not both.");
|
|
2681
3388
|
}
|
|
2682
|
-
const
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
3389
|
+
const attention = buildChangeAttention(files.map((file) => ({
|
|
3390
|
+
path: file.path,
|
|
3391
|
+
previousPath: file.previousPath,
|
|
3392
|
+
changeKind: file.changeKind,
|
|
3393
|
+
additions: file.additions,
|
|
3394
|
+
deletions: file.deletions,
|
|
3395
|
+
binary: file.kind === "omitted",
|
|
3396
|
+
headText: file.kind === "text-diff" ? file.rightContent : void 0,
|
|
3397
|
+
symbolHints: file.kind === "text-diff" ? findDeclarationNames(file.rightContent) : []
|
|
3398
|
+
})));
|
|
3399
|
+
if (!options.story && !options.source && attention.entryPath) {
|
|
3400
|
+
const focusRank = (scenePath) => {
|
|
3401
|
+
const index = attention.focusPaths.indexOf(scenePath);
|
|
3402
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
3403
|
+
};
|
|
3404
|
+
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
|
+
}
|
|
3406
|
+
const authored = source ? applySource(source, defaultScenes, range.repoRoot) : options.story ? applyStory(options.story, defaultScenes) : buildGeneratedTourLanding(
|
|
3407
|
+
defaultScenes,
|
|
3408
|
+
buildChapters(sceneRecords, new Map(defaultScenes.map((scene) => [scene.path, scene.id]))),
|
|
3409
|
+
attention,
|
|
3410
|
+
{
|
|
3411
|
+
changedFiles: range.changedPaths.length,
|
|
3412
|
+
binaryFiles: totalBinaryFiles,
|
|
3413
|
+
additions: totalAdditions,
|
|
3414
|
+
deletions: totalDeletions,
|
|
3415
|
+
commitCount: range.commits.length
|
|
3416
|
+
}
|
|
3417
|
+
);
|
|
2686
3418
|
const scenes = authored.scenes;
|
|
2687
3419
|
const chapters = authored.chapters;
|
|
3420
|
+
const finalScenes = "finalScenes" in authored ? authored.finalScenes : scenes;
|
|
3421
|
+
const finalChapters = "finalChapters" in authored ? authored.finalChapters : chapters;
|
|
2688
3422
|
const manifest = {
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
3423
|
+
// Only authored v2+ files opt into repository-bound zoom modes. Generated
|
|
3424
|
+
// and legacy/story tours retain the portable v1 manifest contract.
|
|
3425
|
+
version: (source == null ? void 0 : source.version) ?? 1,
|
|
3426
|
+
title: (source == null ? void 0 : source.title) || ((_c = options.story) == null ? void 0 : _c.title) || options.title || `${range.headRef} against ${range.baseRef}`,
|
|
3427
|
+
windowTitle: source == null ? void 0 : source.windowTitle,
|
|
3428
|
+
sourceUrl: (source == null ? void 0 : source.sourceUrl) || ((_d = options.story) == null ? void 0 : _d.sourceUrl) || options.sourceUrl,
|
|
2693
3429
|
generatedAt: options.generatedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2694
3430
|
range: {
|
|
2695
3431
|
baseRef: range.baseRef,
|
|
@@ -2708,11 +3444,68 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2708
3444
|
commits: range.commits,
|
|
2709
3445
|
files,
|
|
2710
3446
|
chapters,
|
|
2711
|
-
scenes
|
|
3447
|
+
scenes,
|
|
3448
|
+
...(source == null ? void 0 : source.review) ? { review: source.review } : {}
|
|
2712
3449
|
};
|
|
3450
|
+
const authoredModeTours = (source == null ? void 0 : source.version) === 3 ? buildAuthoredModeTours(
|
|
3451
|
+
source,
|
|
3452
|
+
authored,
|
|
3453
|
+
defaultScenes,
|
|
3454
|
+
range.repoRoot,
|
|
3455
|
+
range.mergeBaseOid,
|
|
3456
|
+
range.headOid
|
|
3457
|
+
) : void 0;
|
|
3458
|
+
if (authoredModeTours) manifest.tours = authoredModeTours;
|
|
3459
|
+
if (manifest.version >= 2) {
|
|
3460
|
+
manifest.repository = { root: range.repoRoot };
|
|
3461
|
+
const revisions = [];
|
|
3462
|
+
for (const scene of scenes) {
|
|
3463
|
+
let realStack;
|
|
3464
|
+
if (scene.kind === "stacked-diff") realStack = scene;
|
|
3465
|
+
if (scene.kind === "deconstructed-diff") {
|
|
3466
|
+
const authoredScene = source == null ? void 0 : source.chapters.flatMap((chapter) => chapter.scenes).find((candidate) => candidate.id === scene.id);
|
|
3467
|
+
if ((authoredScene == null ? void 0 : authoredScene.kind) !== "deconstructed-diff" || !authoredScene.stack) {
|
|
3468
|
+
throw new Error(`Deconstructed scene ${scene.id} requires an explicit real revision stack in v2+.`);
|
|
3469
|
+
}
|
|
3470
|
+
realStack = buildStackedScene(range.repoRoot, {
|
|
3471
|
+
...authoredScene,
|
|
3472
|
+
kind: "stacked-diff",
|
|
3473
|
+
overview: compileSceneOverview(authoredScene.overview, false),
|
|
3474
|
+
stack: authoredScene.stack,
|
|
3475
|
+
files: scene.files.map((file) => file.path),
|
|
3476
|
+
steps: []
|
|
3477
|
+
}, files.map((file) => file.path));
|
|
3478
|
+
if (realStack.stack[0].oid !== scene.realRange.baseOid || realStack.stack[realStack.stack.length - 1].oid !== scene.realRange.targetOid) {
|
|
3479
|
+
throw new Error(`Deconstructed scene ${scene.id} real stack endpoints must match its explanation range.`);
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
if (realStack) {
|
|
3483
|
+
if (realStack.stack[0].oid !== range.mergeBaseOid || realStack.stack[realStack.stack.length - 1].oid !== range.headOid) {
|
|
3484
|
+
throw new Error(`Scene ${scene.id} real stack endpoints must match the tour's final diff range.`);
|
|
3485
|
+
}
|
|
3486
|
+
revisions.push(realStack);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
const explanation = scenes.some((scene) => scene.kind === "deconstructed-diff");
|
|
3490
|
+
const distinctRevisions = revisions.some((scene) => scene.stack.length > 2);
|
|
3491
|
+
manifest.zoom = {
|
|
3492
|
+
authoredDepth: explanation ? "explanation" : distinctRevisions ? "revisions" : "final",
|
|
3493
|
+
modes: [
|
|
3494
|
+
...explanation ? ["explanation"] : [],
|
|
3495
|
+
...distinctRevisions ? ["revisions"] : [],
|
|
3496
|
+
"final",
|
|
3497
|
+
"history"
|
|
3498
|
+
],
|
|
3499
|
+
revisions,
|
|
3500
|
+
final: { scenes: finalScenes, chapters: finalChapters }
|
|
3501
|
+
};
|
|
3502
|
+
}
|
|
2713
3503
|
return parseChangeTourManifest(manifest);
|
|
2714
3504
|
}
|
|
2715
3505
|
function applySource(source, defaultScenes, repoRoot) {
|
|
3506
|
+
return applySourceChapters(source, source.chapters, defaultScenes, repoRoot);
|
|
3507
|
+
}
|
|
3508
|
+
function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot) {
|
|
2716
3509
|
var _a, _b;
|
|
2717
3510
|
const available = new Map(defaultScenes.map((scene) => [scene.path, scene]));
|
|
2718
3511
|
const resolvedAnchors = /* @__PURE__ */ new Map();
|
|
@@ -2732,8 +3525,11 @@ function applySource(source, defaultScenes, repoRoot) {
|
|
|
2732
3525
|
}]));
|
|
2733
3526
|
const scenes = [];
|
|
2734
3527
|
const chapters = [];
|
|
2735
|
-
|
|
3528
|
+
const finalScenes = [];
|
|
3529
|
+
const finalChapters = [];
|
|
3530
|
+
for (const chapter of chaptersToCompile) {
|
|
2736
3531
|
const sceneIds = [];
|
|
3532
|
+
const finalSceneIds = [];
|
|
2737
3533
|
for (const authoredScene of chapter.scenes) {
|
|
2738
3534
|
if (authoredScene.kind === "deconstructed-diff") {
|
|
2739
3535
|
const deconstructedScene = buildDeconstructedManifestScene(
|
|
@@ -2745,6 +3541,16 @@ function applySource(source, defaultScenes, repoRoot) {
|
|
|
2745
3541
|
);
|
|
2746
3542
|
scenes.push(deconstructedScene);
|
|
2747
3543
|
sceneIds.push(deconstructedScene.id);
|
|
3544
|
+
if (authoredScene.steps) {
|
|
3545
|
+
const finalScene = buildWalkthroughManifestScene(
|
|
3546
|
+
{ ...authoredScene, steps: authoredScene.steps },
|
|
3547
|
+
available,
|
|
3548
|
+
resolvedAnchors,
|
|
3549
|
+
connections
|
|
3550
|
+
);
|
|
3551
|
+
finalScenes.push(finalScene);
|
|
3552
|
+
finalSceneIds.push(finalScene.id);
|
|
3553
|
+
}
|
|
2748
3554
|
continue;
|
|
2749
3555
|
}
|
|
2750
3556
|
if (authoredScene.kind === "stacked-diff") {
|
|
@@ -2755,37 +3561,105 @@ function applySource(source, defaultScenes, repoRoot) {
|
|
|
2755
3561
|
);
|
|
2756
3562
|
scenes.push(stackedScene);
|
|
2757
3563
|
sceneIds.push(stackedScene.id);
|
|
3564
|
+
for (const [index, file] of stackedScene.files.entries()) {
|
|
3565
|
+
const endpoint = available.get(file.path);
|
|
3566
|
+
if (!endpoint) continue;
|
|
3567
|
+
const finalScene = { ...endpoint, id: `${stackedScene.id}-final-${index + 1}`, title: file.path };
|
|
3568
|
+
finalScenes.push(finalScene);
|
|
3569
|
+
finalSceneIds.push(finalScene.id);
|
|
3570
|
+
}
|
|
2758
3571
|
continue;
|
|
2759
3572
|
}
|
|
2760
|
-
const scene =
|
|
2761
|
-
id: authoredScene.id,
|
|
2762
|
-
kind: "walkthrough",
|
|
2763
|
-
title: authoredScene.title,
|
|
2764
|
-
summary: authoredScene.summary,
|
|
2765
|
-
bullets: authoredScene.bullets,
|
|
2766
|
-
tags: authoredScene.tags,
|
|
2767
|
-
takeaway: authoredScene.takeaway,
|
|
2768
|
-
steps: authoredScene.steps.map((step) => {
|
|
2769
|
-
const focus = requireResolvedAnchor(resolvedAnchors, step.focus);
|
|
2770
|
-
const diff2 = available.get(focus.path);
|
|
2771
|
-
if (!diff2) throw new Error(`Step ${step.id} has no text diff for ${focus.path}.`);
|
|
2772
|
-
return {
|
|
2773
|
-
id: step.id,
|
|
2774
|
-
title: step.title,
|
|
2775
|
-
body: step.body,
|
|
2776
|
-
depth: step.depth,
|
|
2777
|
-
focus,
|
|
2778
|
-
connection: step.connection ? connections.get(step.connection) : void 0,
|
|
2779
|
-
diff: { ...diff2, id: `${authoredScene.id}-${step.id}` }
|
|
2780
|
-
};
|
|
2781
|
-
})
|
|
2782
|
-
};
|
|
3573
|
+
const scene = buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections);
|
|
2783
3574
|
scenes.push(scene);
|
|
2784
3575
|
sceneIds.push(scene.id);
|
|
3576
|
+
finalScenes.push(scene);
|
|
3577
|
+
finalSceneIds.push(scene.id);
|
|
2785
3578
|
}
|
|
2786
3579
|
chapters.push({ id: chapter.id, title: chapter.title, sceneIds });
|
|
3580
|
+
finalChapters.push({ id: chapter.id, title: chapter.title, sceneIds: finalSceneIds });
|
|
3581
|
+
}
|
|
3582
|
+
return { scenes, chapters, finalScenes, finalChapters };
|
|
3583
|
+
}
|
|
3584
|
+
function buildAuthoredModeTours(source, root, defaultScenes, repoRoot, mergeBaseOid, headOid) {
|
|
3585
|
+
var _a, _b;
|
|
3586
|
+
const tours = {};
|
|
3587
|
+
if ((_a = source.tours) == null ? void 0 : _a.historical) {
|
|
3588
|
+
const compiled = applySourceChapters(source, source.tours.historical.chapters, defaultScenes, repoRoot);
|
|
3589
|
+
validateHistoricalStackEndpoints(compiled.scenes, mergeBaseOid, headOid);
|
|
3590
|
+
tours.historical = { chapters: compiled.chapters, scenes: compiled.scenes };
|
|
3591
|
+
} else if (source.version === 3) {
|
|
3592
|
+
const fallback = buildHistoricalFallback(source.chapters, root);
|
|
3593
|
+
if (fallback) tours.historical = fallback;
|
|
3594
|
+
}
|
|
3595
|
+
if ((_b = source.tours) == null ? void 0 : _b.deconstructed) {
|
|
3596
|
+
const compiled = applySourceChapters(source, source.tours.deconstructed.chapters, defaultScenes, repoRoot);
|
|
3597
|
+
tours.deconstructed = { chapters: compiled.chapters, scenes: compiled.scenes };
|
|
3598
|
+
} else if (source.version === 3 && source.chapters.some((chapter) => chapter.scenes.some((scene) => scene.kind === "deconstructed-diff"))) {
|
|
3599
|
+
tours.deconstructed = { chapters: root.chapters, scenes: root.scenes };
|
|
3600
|
+
}
|
|
3601
|
+
return tours.historical || tours.deconstructed ? tours : void 0;
|
|
3602
|
+
}
|
|
3603
|
+
function validateHistoricalStackEndpoints(scenes, mergeBaseOid, headOid) {
|
|
3604
|
+
for (const scene of scenes) {
|
|
3605
|
+
if (scene.kind !== "stacked-diff") continue;
|
|
3606
|
+
if (scene.stack[0].oid !== mergeBaseOid || scene.stack[scene.stack.length - 1].oid !== headOid) {
|
|
3607
|
+
throw new Error(`Historical scene ${scene.id} real stack endpoints must match the tour's final diff range.`);
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
function buildHistoricalFallback(sourceChapters, root) {
|
|
3612
|
+
const regularScenes = new Map(root.scenes.map((scene) => [scene.id, scene]));
|
|
3613
|
+
const finalScenes = new Map(root.finalScenes.map((scene) => [scene.id, scene]));
|
|
3614
|
+
const scenes = [];
|
|
3615
|
+
const chapters = [];
|
|
3616
|
+
for (const chapter of sourceChapters) {
|
|
3617
|
+
const sceneIds = [];
|
|
3618
|
+
for (const authoredScene of chapter.scenes) {
|
|
3619
|
+
const scene = authoredScene.kind === "deconstructed-diff" ? authoredScene.steps ? finalScenes.get(authoredScene.id) : void 0 : regularScenes.get(authoredScene.id);
|
|
3620
|
+
if (!scene) continue;
|
|
3621
|
+
scenes.push(scene);
|
|
3622
|
+
sceneIds.push(scene.id);
|
|
3623
|
+
}
|
|
3624
|
+
if (sceneIds.length > 0) chapters.push({ id: chapter.id, title: chapter.title, sceneIds });
|
|
2787
3625
|
}
|
|
2788
|
-
return {
|
|
3626
|
+
return scenes.length > 0 ? { chapters, scenes } : void 0;
|
|
3627
|
+
}
|
|
3628
|
+
function compileSceneOverview(overview, includeComparison = true) {
|
|
3629
|
+
if (!overview) return void 0;
|
|
3630
|
+
return {
|
|
3631
|
+
kind: overview.kind,
|
|
3632
|
+
...overview.path === void 0 ? {} : { path: overview.path },
|
|
3633
|
+
...includeComparison && overview.comparison ? { comparison: { ...overview.comparison } } : {}
|
|
3634
|
+
};
|
|
3635
|
+
}
|
|
3636
|
+
function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections) {
|
|
3637
|
+
const overview = compileSceneOverview(authoredScene.overview, false);
|
|
3638
|
+
return {
|
|
3639
|
+
id: authoredScene.id,
|
|
3640
|
+
kind: "walkthrough",
|
|
3641
|
+
title: authoredScene.title,
|
|
3642
|
+
summary: authoredScene.summary,
|
|
3643
|
+
bullets: authoredScene.bullets,
|
|
3644
|
+
tags: authoredScene.tags,
|
|
3645
|
+
takeaway: authoredScene.takeaway,
|
|
3646
|
+
...overview ? { overview } : {},
|
|
3647
|
+
steps: authoredScene.steps.map((step) => {
|
|
3648
|
+
const focus = requireResolvedAnchor(resolvedAnchors, step.focus);
|
|
3649
|
+
const diff2 = available.get(focus.path);
|
|
3650
|
+
if (!diff2) throw new Error(`Step ${step.id} has no text diff for ${focus.path}.`);
|
|
3651
|
+
return {
|
|
3652
|
+
id: step.id,
|
|
3653
|
+
title: step.title,
|
|
3654
|
+
body: step.body,
|
|
3655
|
+
depth: step.depth,
|
|
3656
|
+
requirement: step.requirement,
|
|
3657
|
+
focus,
|
|
3658
|
+
connection: step.connection ? connections.get(step.connection) : void 0,
|
|
3659
|
+
diff: { ...diff2, id: `${authoredScene.id}-${step.id}` }
|
|
3660
|
+
};
|
|
3661
|
+
})
|
|
3662
|
+
};
|
|
2789
3663
|
}
|
|
2790
3664
|
function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, availableFilePaths = []) {
|
|
2791
3665
|
const compiled = buildDeconstructedScene(repoRoot, source, { baseRef, headRef: targetRef });
|
|
@@ -2844,6 +3718,7 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, a
|
|
|
2844
3718
|
});
|
|
2845
3719
|
if (files.length === 0) throw new Error(`Deconstructed scene ${source.id} has no staged files.`);
|
|
2846
3720
|
const steps = buildDeconstructedFocusSteps(source.id, compiled.stages, files);
|
|
3721
|
+
const overview = compileSceneOverview(source.overview);
|
|
2847
3722
|
return {
|
|
2848
3723
|
id: source.id,
|
|
2849
3724
|
kind: "deconstructed-diff",
|
|
@@ -2852,6 +3727,7 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, a
|
|
|
2852
3727
|
bullets: source.bullets,
|
|
2853
3728
|
tags: source.tags,
|
|
2854
3729
|
takeaway: source.takeaway,
|
|
3730
|
+
...overview ? { overview } : {},
|
|
2855
3731
|
stageLabel: "Explanation stages",
|
|
2856
3732
|
realRange: {
|
|
2857
3733
|
baseRef: baseRef || compiled.baseOid,
|
|
@@ -2951,7 +3827,7 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2951
3827
|
let totalContentBytes = 0;
|
|
2952
3828
|
const files = filePaths.flatMap((filePath) => {
|
|
2953
3829
|
const aliases = /* @__PURE__ */ new Set([filePath]);
|
|
2954
|
-
for (const changes of adjacentChanges) {
|
|
3830
|
+
for (const changes of [...adjacentChanges].reverse()) {
|
|
2955
3831
|
for (const change of changes) {
|
|
2956
3832
|
if (aliases.has(change.path) || change.previousPath && aliases.has(change.previousPath)) {
|
|
2957
3833
|
aliases.add(change.path);
|
|
@@ -2962,7 +3838,9 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2962
3838
|
let fileContentBytes = 0;
|
|
2963
3839
|
const panels = [];
|
|
2964
3840
|
for (const entry of stack) {
|
|
2965
|
-
const
|
|
3841
|
+
const candidates = [...aliases].filter((candidate) => gitBlobExists(repoRoot, entry.oid, candidate));
|
|
3842
|
+
if (candidates.length > 1) throw new Error(`Ambiguous revision paths for ${filePath} at ${entry.ref}: ${candidates.join(", ")}`);
|
|
3843
|
+
const resolvedPath = candidates[0];
|
|
2966
3844
|
if (!resolvedPath) {
|
|
2967
3845
|
panels.push({ id: entry.id, label: `${entry.label} / ${filePath} (missing)`, content: "", exists: false });
|
|
2968
3846
|
continue;
|
|
@@ -2989,7 +3867,9 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2989
3867
|
}
|
|
2990
3868
|
return [{ path: filePath, panels }];
|
|
2991
3869
|
});
|
|
3870
|
+
if (files.length === 0) throw new Error(`Stacked scene ${source.id} has no materializable text files.`);
|
|
2992
3871
|
const stackIds = stack.map((entry) => entry.id);
|
|
3872
|
+
const overview = compileSceneOverview(source.overview);
|
|
2993
3873
|
return {
|
|
2994
3874
|
id: source.id,
|
|
2995
3875
|
kind: "stacked-diff",
|
|
@@ -2998,9 +3878,17 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2998
3878
|
bullets: source.bullets,
|
|
2999
3879
|
tags: source.tags,
|
|
3000
3880
|
takeaway: source.takeaway,
|
|
3881
|
+
...overview ? { overview } : {},
|
|
3001
3882
|
stack,
|
|
3002
3883
|
files,
|
|
3003
|
-
steps: source.steps.map((
|
|
3884
|
+
steps: source.steps.length === 0 ? stack.slice(0, -1).map((_, pairIndex) => ({
|
|
3885
|
+
id: `${source.id}-revision-${pairIndex}`,
|
|
3886
|
+
title: "",
|
|
3887
|
+
body: "",
|
|
3888
|
+
file: files[0].path,
|
|
3889
|
+
pairIndex,
|
|
3890
|
+
side: "right"
|
|
3891
|
+
})) : source.steps.map((step) => {
|
|
3004
3892
|
var _a, _b;
|
|
3005
3893
|
return {
|
|
3006
3894
|
id: step.id,
|
|
@@ -3112,21 +4000,51 @@ function buildChapters(records, sceneIdByPath) {
|
|
|
3112
4000
|
}
|
|
3113
4001
|
function chapterForPath(filePath) {
|
|
3114
4002
|
const lower = filePath.toLowerCase();
|
|
3115
|
-
const
|
|
3116
|
-
if (
|
|
4003
|
+
const role = classifyChangeFileRole(filePath);
|
|
4004
|
+
if (role === "documentation" || lower.includes("architecture")) {
|
|
3117
4005
|
return CHAPTERS.context;
|
|
3118
4006
|
}
|
|
3119
|
-
if (
|
|
4007
|
+
if (role === "test") {
|
|
3120
4008
|
return CHAPTERS.proof;
|
|
3121
4009
|
}
|
|
3122
4010
|
if (/(^|\/)(models?|schema|types?|contracts?)\.[^/]+$/.test(lower)) {
|
|
3123
4011
|
return CHAPTERS.contracts;
|
|
3124
4012
|
}
|
|
3125
|
-
if (
|
|
4013
|
+
if (role === "dependency" || role === "generated") {
|
|
3126
4014
|
return CHAPTERS.packaging;
|
|
3127
4015
|
}
|
|
3128
4016
|
return CHAPTERS.behavior;
|
|
3129
4017
|
}
|
|
4018
|
+
function buildGeneratedTourLanding(scenes, chapters, attention, summary) {
|
|
4019
|
+
const entry = attention.entryPath ? `Start here: ${attention.entryPath} \u2014 ${attention.entryReason}` : `No single entry-point \u2014 ${attention.entryReason}`;
|
|
4020
|
+
const focus = attention.focusPaths.length > 0 ? `Pay attention: ${attention.focusPaths.join(", ")}` : "Pay attention: no additional production files were identified.";
|
|
4021
|
+
const skipped = attention.backgroundPaths.length + attention.mechanicalPaths.length;
|
|
4022
|
+
const overview = {
|
|
4023
|
+
id: "change-overview",
|
|
4024
|
+
kind: "discussion",
|
|
4025
|
+
title: "Change overview",
|
|
4026
|
+
summary: `${summary.changedFiles} files (${summary.changedFiles - summary.binaryFiles} text, ${summary.binaryFiles} binary) \xB7 +${summary.additions} \u2212${summary.deletions} \xB7 ${summary.commitCount} commits`,
|
|
4027
|
+
bullets: [entry, focus, `Usually skip on a first pass: ${skipped} file${skipped === 1 ? "" : "s"}. All files remain available.`],
|
|
4028
|
+
tags: ["start here", "first pass"],
|
|
4029
|
+
takeaway: attention.entryPath ? `Next: open ${attention.entryPath}.` : "Next: open the first available textual change."
|
|
4030
|
+
};
|
|
4031
|
+
return {
|
|
4032
|
+
scenes: [overview, ...scenes],
|
|
4033
|
+
chapters: [
|
|
4034
|
+
{ id: "start-here", title: "Start here", sceneIds: [overview.id] },
|
|
4035
|
+
...chapters
|
|
4036
|
+
]
|
|
4037
|
+
};
|
|
4038
|
+
}
|
|
4039
|
+
function findDeclarationNames(content) {
|
|
4040
|
+
const names = /* @__PURE__ */ new Set();
|
|
4041
|
+
for (const line of content.split("\n")) {
|
|
4042
|
+
const match = line.match(/^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|interface|type|enum|const|let|var)\s+([A-Za-z_$][\w$]*)/) || line.match(/^\s*(?:async\s+)?(?:class|def)\s+([A-Za-z_]\w*)/);
|
|
4043
|
+
if (match) names.add(match[1]);
|
|
4044
|
+
if (names.size >= 25) break;
|
|
4045
|
+
}
|
|
4046
|
+
return [...names];
|
|
4047
|
+
}
|
|
3130
4048
|
function buildSceneNote(kind, additions, deletions, previousPath) {
|
|
3131
4049
|
const label = formatChangeKind(kind);
|
|
3132
4050
|
const rename = previousPath ? ` from ${previousPath}` : "";
|
|
@@ -3189,15 +4107,19 @@ function readGitLineStats(repoRoot, baseOid, headOid, filePath, previousPath) {
|
|
|
3189
4107
|
const [additionsText = "0", deletionsText = "0"] = output.trim().split(/\s+/, 3);
|
|
3190
4108
|
return {
|
|
3191
4109
|
additions: Number.parseInt(additionsText, 10) || 0,
|
|
3192
|
-
deletions: Number.parseInt(deletionsText, 10) || 0
|
|
4110
|
+
deletions: Number.parseInt(deletionsText, 10) || 0,
|
|
4111
|
+
binary: additionsText === "-" || deletionsText === "-"
|
|
3193
4112
|
};
|
|
3194
4113
|
}
|
|
3195
4114
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3196
4115
|
0 && (module.exports = {
|
|
4116
|
+
CHANGE_ATTENTION_VERSION,
|
|
4117
|
+
buildChangeAttention,
|
|
3197
4118
|
buildChangeInventory,
|
|
3198
4119
|
buildChangeTourContext,
|
|
3199
4120
|
buildChangeTourManifest,
|
|
3200
4121
|
buildDeconstructedScene,
|
|
4122
|
+
classifyChangeFileRole,
|
|
3201
4123
|
compileDeconstructedScene,
|
|
3202
4124
|
materializeChangeUnits,
|
|
3203
4125
|
parseChangeTourManifest,
|