@davmash/bygone 0.8.13 → 0.8.16
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 +16 -0
- package/README.md +2 -2
- package/cli/commandSpec.js +12 -2
- package/cli/completions.js +1 -1
- package/cli/present.js +42 -1
- package/cli/tour.js +13 -2
- package/cli/tourHistory.js +90 -0
- package/completions/_bygone +2 -1
- package/completions/bygone +1 -1
- package/completions/bygone.fish +1 -0
- package/docs/change-tour-format.md +46 -6
- package/docs/explore.md +36 -10
- package/docs/present.md +9 -1
- package/docs/product-surface.md +18 -14
- package/docs/releasing.md +25 -29
- package/media/diff.worker.js +3 -3
- package/media/webview.css +1 -1
- package/media/webview.js +173 -173
- package/out/changeTour.js +953 -410
- package/out/gitComparison.js +354 -0
- package/out/standalone-main.js +7678 -2124
- package/out/tourCoverage.js +992 -402
- package/package.json +1 -1
- package/schemas/change-tour-source.schema.json +58 -3
- package/skills/pr-tour-guide/SKILL.md +16 -7
- package/standalone/index.html +5 -0
- package/web/index.html +14 -0
- package/web/presenter.css +101 -0
- package/web/web-host.js +5 -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,330 @@ 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 = 2;
|
|
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 !== CHANGE_TOUR_SOURCE_VERSION) {
|
|
246
|
+
throw new Error("Unsupported or missing change-tour source version.");
|
|
247
|
+
}
|
|
248
|
+
requireOnlyKeys(value, ["version", "title", "windowTitle", "sourceUrl", "range", "anchors", "connections", "chapters", "coverage"], "source");
|
|
249
|
+
optionalString(value.title, "title");
|
|
250
|
+
optionalString(value.windowTitle, "windowTitle");
|
|
251
|
+
optionalString(value.sourceUrl, "sourceUrl");
|
|
252
|
+
if (value.range !== void 0) {
|
|
253
|
+
if (!isRecord(value.range)) throw new Error("range must be an object.");
|
|
254
|
+
requireOnlyKeys(value.range, ["base", "head"], "range");
|
|
255
|
+
requireString(value.range.base, "range.base");
|
|
256
|
+
requireString(value.range.head, "range.head");
|
|
257
|
+
}
|
|
258
|
+
if (value.coverage !== void 0) {
|
|
259
|
+
if (!isRecord(value.coverage) || !Array.isArray(value.coverage.exclusions)) {
|
|
260
|
+
throw new Error("coverage must contain an exclusions array.");
|
|
261
|
+
}
|
|
262
|
+
requireOnlyKeys(value.coverage, ["exclusions"], "coverage");
|
|
263
|
+
for (const [index, exclusion] of value.coverage.exclusions.entries()) {
|
|
264
|
+
const exclusionPath = `coverage.exclusions[${index}]`;
|
|
265
|
+
if (!isRecord(exclusion)) throw new Error(`${exclusionPath} must be an object.`);
|
|
266
|
+
requireOnlyKeys(exclusion, ["path", "hunks", "reason"], exclusionPath);
|
|
267
|
+
requireString(exclusion.path, `${exclusionPath}.path`);
|
|
268
|
+
requireString(exclusion.reason, `${exclusionPath}.reason`);
|
|
269
|
+
if (exclusion.hunks !== void 0) requireStringArray(exclusion.hunks, `${exclusionPath}.hunks`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!isRecord(value.anchors)) {
|
|
273
|
+
throw new Error("anchors must be an object.");
|
|
274
|
+
}
|
|
275
|
+
for (const [id, anchor] of Object.entries(value.anchors)) {
|
|
276
|
+
if (!isRecord(anchor)) {
|
|
277
|
+
throw new Error(`anchors.${id} must be an object.`);
|
|
278
|
+
}
|
|
279
|
+
requireOnlyKeys(anchor, ["file", "revision", "contains", "occurrence"], `anchors.${id}`);
|
|
280
|
+
requireString(anchor.file, `anchors.${id}.file`);
|
|
281
|
+
if (anchor.revision !== "base" && anchor.revision !== "head") {
|
|
282
|
+
throw new Error(`anchors.${id}.revision must be base or head.`);
|
|
283
|
+
}
|
|
284
|
+
requireString(anchor.contains, `anchors.${id}.contains`);
|
|
285
|
+
if (anchor.occurrence !== void 0 && (!Number.isInteger(anchor.occurrence) || Number(anchor.occurrence) < 1)) {
|
|
286
|
+
throw new Error(`anchors.${id}.occurrence must be a positive integer.`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const rawConnections = Array.isArray(value.connections) ? value.connections : isRecord(value.connections) ? Object.entries(value.connections).map(([id, connection]) => isRecord(connection) ? { id, ...connection } : connection) : null;
|
|
290
|
+
if (!rawConnections || !Array.isArray(value.chapters) || value.chapters.length === 0) {
|
|
291
|
+
throw new Error("connections must be an array or object, and chapters must be a non-empty array.");
|
|
292
|
+
}
|
|
293
|
+
const connectionIds = /* @__PURE__ */ new Set();
|
|
294
|
+
for (const [index, connection] of rawConnections.entries()) {
|
|
295
|
+
if (!isRecord(connection)) {
|
|
296
|
+
throw new Error(`connections[${index}] must be an object.`);
|
|
297
|
+
}
|
|
298
|
+
requireOnlyKeys(connection, ["id", "from", "to", "label"], `connections[${index}]`);
|
|
299
|
+
requireString(connection.id, `connections[${index}].id`);
|
|
300
|
+
requireString(connection.from, `connections[${index}].from`);
|
|
301
|
+
requireString(connection.to, `connections[${index}].to`);
|
|
302
|
+
requireString(connection.label, `connections[${index}].label`);
|
|
303
|
+
const { id, from, to } = connection;
|
|
304
|
+
if (!value.anchors[from] || !value.anchors[to]) {
|
|
305
|
+
throw new Error(`Connection ${id} references an unknown anchor.`);
|
|
306
|
+
}
|
|
307
|
+
if (connectionIds.has(id)) {
|
|
308
|
+
throw new Error(`Duplicate connection id: ${id}`);
|
|
309
|
+
}
|
|
310
|
+
connectionIds.add(id);
|
|
311
|
+
}
|
|
312
|
+
const chapterIds = /* @__PURE__ */ new Set();
|
|
313
|
+
const sceneIds = /* @__PURE__ */ new Set();
|
|
314
|
+
for (const [chapterIndex, chapter] of value.chapters.entries()) {
|
|
315
|
+
if (!isRecord(chapter) || !Array.isArray(chapter.scenes) || chapter.scenes.length === 0) {
|
|
316
|
+
throw new Error(`chapters[${chapterIndex}] must contain a non-empty scenes array.`);
|
|
317
|
+
}
|
|
318
|
+
requireString(chapter.id, `chapters[${chapterIndex}].id`);
|
|
319
|
+
requireString(chapter.title, `chapters[${chapterIndex}].title`);
|
|
320
|
+
requireOnlyKeys(chapter, ["id", "title", "scenes"], `chapters[${chapterIndex}]`);
|
|
321
|
+
if (chapterIds.has(chapter.id)) throw new Error(`Duplicate chapter id: ${chapter.id}`);
|
|
322
|
+
chapterIds.add(chapter.id);
|
|
323
|
+
for (const [sceneIndex, scene] of chapter.scenes.entries()) {
|
|
324
|
+
const path = `chapters[${chapterIndex}].scenes[${sceneIndex}]`;
|
|
325
|
+
if (!isRecord(scene)) throw new Error(`${path} must be an object.`);
|
|
326
|
+
requireString(scene.id, `${path}.id`);
|
|
327
|
+
requireString(scene.title, `${path}.title`);
|
|
328
|
+
if (sceneIds.has(scene.id)) throw new Error(`Duplicate scene id: ${scene.id}`);
|
|
329
|
+
sceneIds.add(scene.id);
|
|
330
|
+
validateNarrative(scene, path);
|
|
331
|
+
if (scene.kind === "deconstructed-diff") {
|
|
332
|
+
validateDeconstructedScene(scene, path);
|
|
333
|
+
if (value.version === 2 || scene.stack !== void 0) validateRealStack(scene.stack, `${path}.stack`, 2);
|
|
334
|
+
if (value.version === 2 || scene.steps !== void 0) {
|
|
335
|
+
validateWalkthroughSteps(scene, path, value.anchors, connectionIds);
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
|
|
340
|
+
throw new Error(`${path} must contain a non-empty steps array.`);
|
|
341
|
+
}
|
|
342
|
+
if (scene.kind === "stacked-diff") {
|
|
343
|
+
validateStackedScene(scene, path, value.version === 2 ? 2 : 3);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (scene.kind !== void 0 && scene.kind !== "walkthrough") {
|
|
347
|
+
throw new Error(`${path}.kind must be walkthrough, stacked-diff, or deconstructed-diff.`);
|
|
348
|
+
}
|
|
349
|
+
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "steps"], path);
|
|
350
|
+
validateWalkthroughSteps(scene, path, value.anchors, connectionIds);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return { ...value, connections: rawConnections };
|
|
354
|
+
}
|
|
355
|
+
function validateDeconstructedScene(scene, path) {
|
|
356
|
+
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "base", "target", "stack", "steps", "stages", "exclusions"], path);
|
|
357
|
+
optionalString(scene.base, `${path}.base`);
|
|
358
|
+
optionalString(scene.target, `${path}.target`);
|
|
359
|
+
if (!Array.isArray(scene.stages) || scene.stages.length === 0 || scene.stages.length > 12) {
|
|
360
|
+
throw new Error(`${path}.stages must contain between 1 and 12 stages.`);
|
|
361
|
+
}
|
|
362
|
+
const stageIds = /* @__PURE__ */ new Set();
|
|
363
|
+
for (const [stageIndex, stage] of scene.stages.entries()) {
|
|
364
|
+
const stagePath = `${path}.stages[${stageIndex}]`;
|
|
365
|
+
if (!isRecord(stage)) throw new Error(`${stagePath} must be an object.`);
|
|
366
|
+
requireOnlyKeys(stage, ["id", "title", "narration", "changes"], stagePath);
|
|
367
|
+
requireString(stage.id, `${stagePath}.id`);
|
|
368
|
+
requireString(stage.title, `${stagePath}.title`);
|
|
369
|
+
requireString(stage.narration, `${stagePath}.narration`);
|
|
370
|
+
if (!Array.isArray(stage.changes) || stage.changes.length === 0) {
|
|
371
|
+
throw new Error(`${stagePath}.changes must be a non-empty array.`);
|
|
372
|
+
}
|
|
373
|
+
if (stageIds.has(stage.id)) throw new Error(`Duplicate deconstructed stage id: ${stage.id}`);
|
|
374
|
+
stageIds.add(stage.id);
|
|
375
|
+
stage.changes.forEach((change, changeIndex) => validateDeconstructedSelection(
|
|
376
|
+
change,
|
|
377
|
+
`${stagePath}.changes[${changeIndex}]`,
|
|
378
|
+
false
|
|
379
|
+
));
|
|
380
|
+
}
|
|
381
|
+
if (scene.exclusions !== void 0) {
|
|
382
|
+
if (!Array.isArray(scene.exclusions)) throw new Error(`${path}.exclusions must be an array.`);
|
|
383
|
+
scene.exclusions.forEach((exclusion, exclusionIndex) => validateDeconstructedSelection(
|
|
384
|
+
exclusion,
|
|
385
|
+
`${path}.exclusions[${exclusionIndex}]`,
|
|
386
|
+
true
|
|
387
|
+
));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function validateWalkthroughSteps(scene, path, anchors, connectionIds) {
|
|
391
|
+
if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
|
|
392
|
+
throw new Error(`${path} must contain a non-empty steps array.`);
|
|
393
|
+
}
|
|
394
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
395
|
+
for (const [stepIndex, step] of scene.steps.entries()) {
|
|
396
|
+
const stepPath = `${path}.steps[${stepIndex}]`;
|
|
397
|
+
if (!isRecord(step)) throw new Error(`${stepPath} must be an object.`);
|
|
398
|
+
requireString(step.id, `${stepPath}.id`);
|
|
399
|
+
requireString(step.title, `${stepPath}.title`);
|
|
400
|
+
requireString(step.body, `${stepPath}.body`);
|
|
401
|
+
requireString(step.focus, `${stepPath}.focus`);
|
|
402
|
+
requireOnlyKeys(step, ["id", "title", "body", "focus", "connection", "depth", "requirement"], stepPath);
|
|
403
|
+
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
404
|
+
throw new Error(`${stepPath}.depth must be mentioned, explained, or contextualized.`);
|
|
405
|
+
}
|
|
406
|
+
validateStepRequirement(step.requirement, `${stepPath}.requirement`);
|
|
407
|
+
if (stepIds.has(step.id)) throw new Error(`Duplicate step id in scene ${scene.id}: ${step.id}`);
|
|
408
|
+
stepIds.add(step.id);
|
|
409
|
+
if (!anchors[step.focus]) throw new Error(`${stepPath} references unknown anchor ${step.focus}.`);
|
|
410
|
+
optionalString(step.connection, `${stepPath}.connection`);
|
|
411
|
+
if (typeof step.connection === "string" && !connectionIds.has(step.connection)) {
|
|
412
|
+
throw new Error(`${stepPath} references unknown connection ${step.connection}.`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function validateDeconstructedSelection(value, path, requiresReason) {
|
|
417
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
418
|
+
requireOnlyKeys(value, requiresReason ? ["file", "hunks", "reason"] : ["file", "hunks"], path);
|
|
419
|
+
requireString(value.file, `${path}.file`);
|
|
420
|
+
if (!requiresReason || value.hunks !== void 0) {
|
|
421
|
+
requireStringArray(value.hunks, `${path}.hunks`);
|
|
422
|
+
if (value.hunks.length === 0) throw new Error(`${path}.hunks must not be empty.`);
|
|
423
|
+
}
|
|
424
|
+
if (requiresReason) requireString(value.reason, `${path}.reason`);
|
|
425
|
+
}
|
|
426
|
+
function validateRealStack(stack, path, minimum) {
|
|
427
|
+
if (!Array.isArray(stack) || stack.length < minimum || stack.length > 6) {
|
|
428
|
+
throw new Error(`${path} must contain between ${minimum} and 6 explicit real revisions.`);
|
|
429
|
+
}
|
|
430
|
+
const ids = /* @__PURE__ */ new Set();
|
|
431
|
+
for (const [index, entry] of stack.entries()) {
|
|
432
|
+
if (!isRecord(entry)) throw new Error(`${path}[${index}] must be an object.`);
|
|
433
|
+
requireOnlyKeys(entry, ["id", "ref", "label"], `${path}[${index}]`);
|
|
434
|
+
requireString(entry.id, `${path}[${index}].id`);
|
|
435
|
+
requireString(entry.ref, `${path}[${index}].ref`);
|
|
436
|
+
optionalString(entry.label, `${path}[${index}].label`);
|
|
437
|
+
if (ids.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
|
|
438
|
+
ids.add(entry.id);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function validateStackedScene(scene, path, minimum = 3) {
|
|
442
|
+
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "stack", "files", "steps"], path);
|
|
443
|
+
if (!Array.isArray(scene.stack) || scene.stack.length < minimum || scene.stack.length > 6) {
|
|
444
|
+
throw new Error(`${path}.stack must contain between ${minimum} and 6 revisions.`);
|
|
445
|
+
}
|
|
446
|
+
const stackIds = /* @__PURE__ */ new Set();
|
|
447
|
+
for (const [index, entry] of scene.stack.entries()) {
|
|
448
|
+
const entryPath = `${path}.stack[${index}]`;
|
|
449
|
+
if (!isRecord(entry)) throw new Error(`${entryPath} must be an object.`);
|
|
450
|
+
requireOnlyKeys(entry, ["id", "ref", "label"], entryPath);
|
|
451
|
+
requireString(entry.id, `${entryPath}.id`);
|
|
452
|
+
requireString(entry.ref, `${entryPath}.ref`);
|
|
453
|
+
optionalString(entry.label, `${entryPath}.label`);
|
|
454
|
+
if (stackIds.has(entry.id)) throw new Error(`Duplicate stack entry id: ${entry.id}`);
|
|
455
|
+
stackIds.add(entry.id);
|
|
456
|
+
}
|
|
457
|
+
if (scene.files !== void 0) requireStringArray(scene.files, `${path}.files`);
|
|
458
|
+
if (!Array.isArray(scene.steps) || scene.steps.length === 0) throw new Error(`${path}.steps must be a non-empty array.`);
|
|
459
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
460
|
+
for (const [index, step] of scene.steps.entries()) {
|
|
461
|
+
const stepPath = `${path}.steps[${index}]`;
|
|
462
|
+
if (!isRecord(step)) throw new Error(`${stepPath} must be an object.`);
|
|
463
|
+
requireOnlyKeys(step, ["id", "title", "body", "file", "pair", "side", "lines"], stepPath);
|
|
464
|
+
requireString(step.id, `${stepPath}.id`);
|
|
465
|
+
requireString(step.title, `${stepPath}.title`);
|
|
466
|
+
requireString(step.body, `${stepPath}.body`);
|
|
467
|
+
requireString(step.file, `${stepPath}.file`);
|
|
468
|
+
const stepId = step.id;
|
|
469
|
+
if (stepIds.has(stepId)) throw new Error(`Duplicate step id in scene ${scene.id}: ${stepId}`);
|
|
470
|
+
stepIds.add(stepId);
|
|
471
|
+
if (!Array.isArray(step.pair) || step.pair.length !== 2 || step.pair.some((id) => typeof id !== "string")) {
|
|
472
|
+
throw new Error(`${stepPath}.pair must contain two stack entry ids.`);
|
|
473
|
+
}
|
|
474
|
+
const pair = step.pair;
|
|
475
|
+
const leftIndex = scene.stack.findIndex((entry) => isRecord(entry) && entry.id === pair[0]);
|
|
476
|
+
const rightIndex = scene.stack.findIndex((entry) => isRecord(entry) && entry.id === pair[1]);
|
|
477
|
+
if (leftIndex < 0 || rightIndex !== leftIndex + 1) throw new Error(`${stepPath}.pair must reference adjacent stack entries in order.`);
|
|
478
|
+
if (step.side !== void 0 && step.side !== "left" && step.side !== "right") throw new Error(`${stepPath}.side must be left or right.`);
|
|
479
|
+
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]))) {
|
|
480
|
+
throw new Error(`${stepPath}.lines must contain an ordered positive line range.`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function validateStepRequirement(value, path) {
|
|
485
|
+
if (value === void 0) return;
|
|
486
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
487
|
+
requireOnlyKeys(value, ["id", "text", "status", "source", "confidence"], path);
|
|
488
|
+
requireString(value.id, `${path}.id`);
|
|
489
|
+
requireString(value.text, `${path}.text`);
|
|
490
|
+
if (value.status !== void 0 && !["fulfilled", "gap"].includes(String(value.status))) {
|
|
491
|
+
throw new Error(`${path}.status must be fulfilled or gap.`);
|
|
492
|
+
}
|
|
493
|
+
optionalString(value.source, `${path}.source`);
|
|
494
|
+
if (value.confidence !== void 0 && !["high", "medium", "low"].includes(String(value.confidence))) {
|
|
495
|
+
throw new Error(`${path}.confidence must be high, medium, or low.`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function validateNarrative(value, path) {
|
|
499
|
+
requireString(value.summary, `${path}.summary`);
|
|
500
|
+
requireStringArray(value.bullets, `${path}.bullets`);
|
|
501
|
+
requireStringArray(value.tags, `${path}.tags`);
|
|
502
|
+
requireString(value.takeaway, `${path}.takeaway`);
|
|
503
|
+
}
|
|
504
|
+
function isRecord(value) {
|
|
505
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
506
|
+
}
|
|
507
|
+
function requireString(value, path) {
|
|
508
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
509
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function optionalString(value, path) {
|
|
513
|
+
if (value !== void 0) {
|
|
514
|
+
requireString(value, path);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function requireStringArray(value, path) {
|
|
518
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
519
|
+
throw new Error(`${path} must be an array of strings.`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function requireOnlyKeys(value, allowed, path) {
|
|
523
|
+
const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
524
|
+
if (unexpected.length > 0) {
|
|
525
|
+
throw new Error(`${path} contains unknown field${unexpected.length === 1 ? "" : "s"}: ${unexpected.join(", ")}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
234
529
|
// src/changeTourManifest.ts
|
|
235
|
-
var CHANGE_TOUR_MANIFEST_VERSION =
|
|
530
|
+
var CHANGE_TOUR_MANIFEST_VERSION = 2;
|
|
236
531
|
function parseChangeTourManifest(value) {
|
|
237
|
-
if (
|
|
532
|
+
if (isRecord2(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_MANIFEST_VERSION) {
|
|
533
|
+
throw new Error(
|
|
534
|
+
`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.`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
if (!isRecord2(value) || value.version !== 1 && value.version !== CHANGE_TOUR_MANIFEST_VERSION) {
|
|
238
538
|
throw new Error(`Unsupported or missing change-tour manifest version.`);
|
|
239
539
|
}
|
|
240
|
-
|
|
241
|
-
|
|
540
|
+
requireString2(value.title, "title");
|
|
541
|
+
requireString2(value.generatedAt, "generatedAt");
|
|
242
542
|
if (value.windowTitle !== void 0) {
|
|
243
|
-
|
|
543
|
+
requireString2(value.windowTitle, "windowTitle");
|
|
244
544
|
}
|
|
245
545
|
if (value.sourceUrl !== void 0) {
|
|
246
|
-
|
|
546
|
+
requireString2(value.sourceUrl, "sourceUrl");
|
|
247
547
|
}
|
|
248
|
-
if (!
|
|
548
|
+
if (!isRecord2(value.range)) {
|
|
249
549
|
throw new Error("Change-tour manifest range must be an object.");
|
|
250
550
|
}
|
|
251
551
|
for (const key of ["baseRef", "headRef", "mergeBaseOid", "headOid"]) {
|
|
252
|
-
|
|
552
|
+
requireString2(value.range[key], `range.${key}`);
|
|
253
553
|
}
|
|
254
|
-
if (!
|
|
554
|
+
if (!isRecord2(value.summary)) {
|
|
255
555
|
throw new Error("Change-tour manifest summary must be an object.");
|
|
256
556
|
}
|
|
257
557
|
for (const key of ["changedFiles", "includedScenes", "additions", "deletions", "commitCount"]) {
|
|
258
558
|
requireNonNegativeInteger(value.summary[key], `summary.${key}`);
|
|
259
559
|
}
|
|
260
|
-
|
|
560
|
+
requireStringArray2(value.summary.omittedFiles, "summary.omittedFiles");
|
|
261
561
|
if (!Array.isArray(value.commits) || !Array.isArray(value.chapters) || !Array.isArray(value.scenes)) {
|
|
262
562
|
throw new Error("Change-tour manifest commits, chapters, and scenes must be arrays.");
|
|
263
563
|
}
|
|
@@ -273,12 +573,12 @@ function parseChangeTourManifest(value) {
|
|
|
273
573
|
sceneIds.add(candidate.id);
|
|
274
574
|
}
|
|
275
575
|
for (const [index, candidate] of value.chapters.entries()) {
|
|
276
|
-
if (!
|
|
576
|
+
if (!isRecord2(candidate)) {
|
|
277
577
|
throw new Error(`chapters[${index}] must be an object.`);
|
|
278
578
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
579
|
+
requireString2(candidate.id, `chapters[${index}].id`);
|
|
580
|
+
requireString2(candidate.title, `chapters[${index}].title`);
|
|
581
|
+
requireStringArray2(candidate.sceneIds, `chapters[${index}].sceneIds`);
|
|
282
582
|
for (const sceneId of candidate.sceneIds) {
|
|
283
583
|
if (!sceneIds.has(sceneId)) {
|
|
284
584
|
throw new Error(`Chapter references unknown scene id: ${sceneId}`);
|
|
@@ -295,31 +595,63 @@ function parseChangeTourManifest(value) {
|
|
|
295
595
|
if (value.files !== void 0 && value.summary.changedFiles !== files.length) {
|
|
296
596
|
throw new Error("summary.changedFiles must match the number of files.");
|
|
297
597
|
}
|
|
598
|
+
if (value.authoringCoverage !== void 0) {
|
|
599
|
+
validateAuthoringCoverage(value.authoringCoverage, value.version);
|
|
600
|
+
}
|
|
601
|
+
if (value.version === 2) validateZoom(value);
|
|
298
602
|
return { ...value, files };
|
|
299
603
|
}
|
|
604
|
+
function validateAuthoringCoverage(value, version) {
|
|
605
|
+
if (!isRecord2(value) || !isRecord2(value.walkthrough) || !Array.isArray(value.explanationAssignments)) {
|
|
606
|
+
throw new Error("authoringCoverage must contain walkthrough and explanationAssignments.");
|
|
607
|
+
}
|
|
608
|
+
const expectedScope = version === 2 ? "final" : "tour";
|
|
609
|
+
if (value.walkthrough.scope !== expectedScope) {
|
|
610
|
+
throw new Error(`authoringCoverage.walkthrough.scope must be ${expectedScope}.`);
|
|
611
|
+
}
|
|
612
|
+
requireNonNegativeInteger(value.walkthrough.coveredUnits, "authoringCoverage.walkthrough.coveredUnits");
|
|
613
|
+
requireNonNegativeInteger(value.walkthrough.includedUnits, "authoringCoverage.walkthrough.includedUnits");
|
|
614
|
+
requirePercentage(value.walkthrough.coveragePercent, "authoringCoverage.walkthrough.coveragePercent");
|
|
615
|
+
if (value.walkthrough.coveredUnits > value.walkthrough.includedUnits) {
|
|
616
|
+
throw new Error("authoringCoverage.walkthrough.coveredUnits must not exceed includedUnits.");
|
|
617
|
+
}
|
|
618
|
+
for (const [index, assignment] of value.explanationAssignments.entries()) {
|
|
619
|
+
const path = `authoringCoverage.explanationAssignments[${index}]`;
|
|
620
|
+
if (!isRecord2(assignment)) throw new Error(`${path} must be an object.`);
|
|
621
|
+
requireString2(assignment.sceneId, `${path}.sceneId`);
|
|
622
|
+
requireNonNegativeInteger(assignment.assignedUnits, `${path}.assignedUnits`);
|
|
623
|
+
requireNonNegativeInteger(assignment.excludedUnits, `${path}.excludedUnits`);
|
|
624
|
+
requireNonNegativeInteger(assignment.completeUnits, `${path}.completeUnits`);
|
|
625
|
+
requireNonNegativeInteger(assignment.totalUnits, `${path}.totalUnits`);
|
|
626
|
+
requirePercentage(assignment.assignmentPercent, `${path}.assignmentPercent`);
|
|
627
|
+
if (assignment.assignedUnits + assignment.excludedUnits !== assignment.completeUnits || assignment.completeUnits > assignment.totalUnits) {
|
|
628
|
+
throw new Error(`${path} contains inconsistent unit counts.`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
300
632
|
function parseChangeTourStory(value) {
|
|
301
|
-
if (!
|
|
633
|
+
if (!isRecord2(value) || !Array.isArray(value.scenes)) {
|
|
302
634
|
throw new Error("Change-tour story must contain a scenes array.");
|
|
303
635
|
}
|
|
304
636
|
if (value.title !== void 0) {
|
|
305
|
-
|
|
637
|
+
requireString2(value.title, "title");
|
|
306
638
|
}
|
|
307
639
|
if (value.sourceUrl !== void 0) {
|
|
308
|
-
|
|
640
|
+
requireString2(value.sourceUrl, "sourceUrl");
|
|
309
641
|
}
|
|
310
642
|
for (const [index, scene] of value.scenes.entries()) {
|
|
311
|
-
if (!
|
|
643
|
+
if (!isRecord2(scene) || scene.kind !== "discussion" && scene.kind !== "file") {
|
|
312
644
|
throw new Error(`story.scenes[${index}] must be a discussion or file scene.`);
|
|
313
645
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
646
|
+
requireString2(scene.chapterId, `story.scenes[${index}].chapterId`);
|
|
647
|
+
requireString2(scene.chapterTitle, `story.scenes[${index}].chapterTitle`);
|
|
648
|
+
validateNarrative2(scene, `story.scenes[${index}]`);
|
|
317
649
|
if (scene.kind === "discussion") {
|
|
318
|
-
|
|
650
|
+
requireString2(scene.title, `story.scenes[${index}].title`);
|
|
319
651
|
} else {
|
|
320
|
-
|
|
652
|
+
requireString2(scene.path, `story.scenes[${index}].path`);
|
|
321
653
|
if (scene.title !== void 0) {
|
|
322
|
-
|
|
654
|
+
requireString2(scene.title, `story.scenes[${index}].title`);
|
|
323
655
|
}
|
|
324
656
|
if (scene.focusChangeIndex !== void 0) {
|
|
325
657
|
requireNonNegativeInteger(scene.focusChangeIndex, `story.scenes[${index}].focusChangeIndex`);
|
|
@@ -328,14 +660,74 @@ function parseChangeTourStory(value) {
|
|
|
328
660
|
}
|
|
329
661
|
return value;
|
|
330
662
|
}
|
|
663
|
+
function validateZoom(value) {
|
|
664
|
+
if (!isRecord2(value.repository) || typeof value.repository.root !== "string" || !value.repository.root) {
|
|
665
|
+
throw new Error("A v2 tour requires its originating repository.root.");
|
|
666
|
+
}
|
|
667
|
+
if (!isRecord2(value.zoom) || !Array.isArray(value.zoom.revisions) || !isRecord2(value.zoom.final)) {
|
|
668
|
+
throw new Error("A v2 tour requires zoom metadata and real revision evidence.");
|
|
669
|
+
}
|
|
670
|
+
const scenes = value.scenes;
|
|
671
|
+
const revisions = value.zoom.revisions;
|
|
672
|
+
const ids = /* @__PURE__ */ new Set();
|
|
673
|
+
for (const [index, scene] of revisions.entries()) {
|
|
674
|
+
validateScene(scene, index);
|
|
675
|
+
if (scene.kind !== "stacked-diff") throw new Error("zoom.revisions must contain real stacked-diff scenes.");
|
|
676
|
+
if (ids.has(scene.id)) throw new Error(`Duplicate zoom revision scene: ${scene.id}`);
|
|
677
|
+
ids.add(scene.id);
|
|
678
|
+
const range = value.range;
|
|
679
|
+
if (scene.stack[0].oid !== range.mergeBaseOid || scene.stack[scene.stack.length - 1].oid !== range.headOid) {
|
|
680
|
+
throw new Error(`Revision scene ${scene.id} endpoints must match the final diff range.`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
for (const scene of scenes) {
|
|
684
|
+
if (scene.kind !== "deconstructed-diff" && scene.kind !== "stacked-diff") continue;
|
|
685
|
+
const real = revisions.find((candidate) => candidate.id === scene.id);
|
|
686
|
+
if (!real) throw new Error(`Scene ${scene.id} is missing its real revision evidence.`);
|
|
687
|
+
if (scene.kind === "deconstructed-diff" && (real.stack[0].oid !== scene.realRange.baseOid || real.stack[real.stack.length - 1].oid !== scene.realRange.targetOid)) {
|
|
688
|
+
throw new Error(`Deconstructed scene ${scene.id} real stack endpoints must match its explanation range.`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const explanation = scenes.some((scene) => scene.kind === "deconstructed-diff");
|
|
692
|
+
const distinct = revisions.some((scene) => scene.stack.length > 2);
|
|
693
|
+
const depth = explanation ? "explanation" : distinct ? "revisions" : "final";
|
|
694
|
+
const modes = [...explanation ? ["explanation"] : [], ...distinct ? ["revisions"] : [], "final", "history"];
|
|
695
|
+
if (value.zoom.authoredDepth !== depth || JSON.stringify(value.zoom.modes) !== JSON.stringify(modes)) {
|
|
696
|
+
throw new Error("zoom depth and modes must match the maximum authored depth.");
|
|
697
|
+
}
|
|
698
|
+
if (!Array.isArray(value.files)) throw new Error("A v2 tour requires final endpoint file evidence.");
|
|
699
|
+
if (!Array.isArray(value.zoom.final.scenes) || !Array.isArray(value.zoom.final.chapters)) {
|
|
700
|
+
throw new Error("A v2 tour requires a non-stacked final tour.");
|
|
701
|
+
}
|
|
702
|
+
const finalSceneIds = /* @__PURE__ */ new Set();
|
|
703
|
+
for (const [index, scene] of value.zoom.final.scenes.entries()) {
|
|
704
|
+
validateScene(scene, index);
|
|
705
|
+
if (scene.kind === "stacked-diff" || scene.kind === "deconstructed-diff") {
|
|
706
|
+
throw new Error("zoom.final must contain only non-stacked scenes.");
|
|
707
|
+
}
|
|
708
|
+
if (finalSceneIds.has(scene.id)) throw new Error(`Duplicate final tour scene: ${scene.id}`);
|
|
709
|
+
finalSceneIds.add(scene.id);
|
|
710
|
+
}
|
|
711
|
+
for (const [index, chapter] of value.zoom.final.chapters.entries()) {
|
|
712
|
+
if (!isRecord2(chapter) || !Array.isArray(chapter.sceneIds)) {
|
|
713
|
+
throw new Error(`zoom.final.chapters[${index}] must contain sceneIds.`);
|
|
714
|
+
}
|
|
715
|
+
requireString2(chapter.id, `zoom.final.chapters[${index}].id`);
|
|
716
|
+
requireString2(chapter.title, `zoom.final.chapters[${index}].title`);
|
|
717
|
+
for (const sceneId of chapter.sceneIds) {
|
|
718
|
+
requireString2(sceneId, `zoom.final.chapters[${index}].sceneIds`);
|
|
719
|
+
if (!finalSceneIds.has(sceneId)) throw new Error(`Final tour chapter references unknown scene: ${sceneId}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
331
723
|
function validateScene(value, index) {
|
|
332
|
-
if (!
|
|
724
|
+
if (!isRecord2(value) || !["text-diff", "discussion", "walkthrough", "stacked-diff", "deconstructed-diff"].includes(String(value.kind))) {
|
|
333
725
|
throw new Error(`scenes[${index}] must be a text-diff, discussion, walkthrough, stacked-diff, or deconstructed-diff scene.`);
|
|
334
726
|
}
|
|
335
727
|
for (const key of ["id", "title"]) {
|
|
336
|
-
|
|
728
|
+
requireString2(value[key], `scenes[${index}].${key}`);
|
|
337
729
|
}
|
|
338
|
-
|
|
730
|
+
validateNarrative2(value, `scenes[${index}]`);
|
|
339
731
|
if (value.kind === "discussion") {
|
|
340
732
|
return;
|
|
341
733
|
}
|
|
@@ -345,20 +737,21 @@ function validateScene(value, index) {
|
|
|
345
737
|
}
|
|
346
738
|
for (const [stepIndex, step] of value.steps.entries()) {
|
|
347
739
|
const path = `scenes[${index}].steps[${stepIndex}]`;
|
|
348
|
-
if (!
|
|
740
|
+
if (!isRecord2(step) || !isRecord2(step.focus) || !isRecord2(step.diff)) {
|
|
349
741
|
throw new Error(`${path} must contain focus and diff objects.`);
|
|
350
742
|
}
|
|
351
|
-
for (const key of ["id", "title", "body"])
|
|
743
|
+
for (const key of ["id", "title", "body"]) requireString2(step[key], `${path}.${key}`);
|
|
352
744
|
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
353
745
|
throw new Error(`${path}.depth must be mentioned, explained, or contextualized.`);
|
|
354
746
|
}
|
|
747
|
+
validateStepRequirement(step.requirement, `${path}.requirement`);
|
|
355
748
|
validateResolvedAnchor(step.focus, `${path}.focus`);
|
|
356
749
|
validateScene(step.diff, index);
|
|
357
750
|
if (step.diff.kind !== "text-diff") throw new Error(`${path}.diff must be a text-diff scene.`);
|
|
358
751
|
if (step.connection !== void 0) {
|
|
359
|
-
if (!
|
|
360
|
-
|
|
361
|
-
|
|
752
|
+
if (!isRecord2(step.connection)) throw new Error(`${path}.connection must be an object.`);
|
|
753
|
+
requireString2(step.connection.id, `${path}.connection.id`);
|
|
754
|
+
requireString2(step.connection.label, `${path}.connection.label`);
|
|
362
755
|
validateResolvedAnchor(step.connection.from, `${path}.connection.from`);
|
|
363
756
|
validateResolvedAnchor(step.connection.to, `${path}.connection.to`);
|
|
364
757
|
}
|
|
@@ -366,68 +759,68 @@ function validateScene(value, index) {
|
|
|
366
759
|
return;
|
|
367
760
|
}
|
|
368
761
|
if (value.kind === "stacked-diff") {
|
|
369
|
-
|
|
762
|
+
validateStackedScene2(value, index);
|
|
370
763
|
return;
|
|
371
764
|
}
|
|
372
765
|
if (value.kind === "deconstructed-diff") {
|
|
373
|
-
|
|
766
|
+
validateDeconstructedScene2(value, index);
|
|
374
767
|
return;
|
|
375
768
|
}
|
|
376
769
|
for (const key of ["path", "leftLabel", "rightLabel", "leftContent", "rightContent"]) {
|
|
377
|
-
|
|
770
|
+
requireString2(value[key], `scenes[${index}].${key}`);
|
|
378
771
|
}
|
|
379
772
|
if (value.previousPath !== void 0) {
|
|
380
|
-
|
|
773
|
+
requireString2(value.previousPath, `scenes[${index}].previousPath`);
|
|
381
774
|
}
|
|
382
|
-
|
|
775
|
+
requireString2(value.changeKind, `scenes[${index}].changeKind`);
|
|
383
776
|
requireNonNegativeInteger(value.additions, `scenes[${index}].additions`);
|
|
384
777
|
requireNonNegativeInteger(value.deletions, `scenes[${index}].deletions`);
|
|
385
778
|
if (value.focusChangeIndex !== void 0) {
|
|
386
779
|
requireNonNegativeInteger(value.focusChangeIndex, `scenes[${index}].focusChangeIndex`);
|
|
387
780
|
}
|
|
388
781
|
}
|
|
389
|
-
function
|
|
390
|
-
if (!Array.isArray(value.stack) || value.stack.length <
|
|
391
|
-
throw new Error(`scenes[${index}].stack must contain between
|
|
782
|
+
function validateStackedScene2(value, index) {
|
|
783
|
+
if (!Array.isArray(value.stack) || value.stack.length < 2 || value.stack.length > 6) {
|
|
784
|
+
throw new Error(`scenes[${index}].stack must contain between 2 and 6 revisions.`);
|
|
392
785
|
}
|
|
393
786
|
for (const [panelIndex, panel] of value.stack.entries()) {
|
|
394
|
-
if (!
|
|
395
|
-
for (const key of ["id", "ref", "oid", "label"])
|
|
787
|
+
if (!isRecord2(panel)) throw new Error(`scenes[${index}].stack[${panelIndex}] must be an object.`);
|
|
788
|
+
for (const key of ["id", "ref", "oid", "label"]) requireString2(panel[key], `scenes[${index}].stack[${panelIndex}].${key}`);
|
|
396
789
|
}
|
|
397
790
|
validateMultiPanelFiles(value.files, value.stack.length, index);
|
|
398
791
|
validateMultiPanelSteps(value.steps, value.stack.length, index, false);
|
|
399
792
|
}
|
|
400
|
-
function
|
|
793
|
+
function validateDeconstructedScene2(value, index) {
|
|
401
794
|
if (value.stageLabel !== "Explanation stages") {
|
|
402
795
|
throw new Error(`scenes[${index}].stageLabel must be Explanation stages.`);
|
|
403
796
|
}
|
|
404
|
-
if (!
|
|
797
|
+
if (!isRecord2(value.realRange)) throw new Error(`scenes[${index}].realRange must be an object.`);
|
|
405
798
|
for (const key of ["baseRef", "targetRef", "baseOid", "targetOid"]) {
|
|
406
|
-
|
|
799
|
+
requireString2(value.realRange[key], `scenes[${index}].realRange.${key}`);
|
|
407
800
|
}
|
|
408
801
|
if (!Array.isArray(value.panels) || value.panels.length < 2 || value.panels.length > 13) {
|
|
409
802
|
throw new Error(`scenes[${index}].panels must contain a baseline and between 1 and 12 stages.`);
|
|
410
803
|
}
|
|
411
804
|
const panels = value.panels;
|
|
412
805
|
for (const [panelIndex, panel] of panels.entries()) {
|
|
413
|
-
if (!
|
|
414
|
-
for (const key of ["id", "label", "role"])
|
|
806
|
+
if (!isRecord2(panel)) throw new Error(`scenes[${index}].panels[${panelIndex}] must be an object.`);
|
|
807
|
+
for (const key of ["id", "label", "role"]) requireString2(panel[key], `scenes[${index}].panels[${panelIndex}].${key}`);
|
|
415
808
|
const expectedRole = panelIndex === 0 ? "baseline" : "stage";
|
|
416
809
|
if (panel.role !== expectedRole) throw new Error(`scenes[${index}].panels[${panelIndex}].role must be ${expectedRole}.`);
|
|
417
|
-
if (panelIndex > 0)
|
|
810
|
+
if (panelIndex > 0) requireString2(panel.stageId, `scenes[${index}].panels[${panelIndex}].stageId`);
|
|
418
811
|
}
|
|
419
812
|
validateMultiPanelFiles(value.files, panels.length, index);
|
|
420
813
|
validateMultiPanelSteps(value.steps, panels.length, index, true);
|
|
421
814
|
if (Array.isArray(value.steps)) {
|
|
422
815
|
let previousPairIndex = -1;
|
|
423
816
|
value.steps.forEach((step, stepIndex) => {
|
|
424
|
-
if (!
|
|
817
|
+
if (!isRecord2(step)) return;
|
|
425
818
|
const pairIndex = Number(step.pairIndex);
|
|
426
819
|
if (pairIndex < previousPairIndex) {
|
|
427
820
|
throw new Error(`scenes[${index}].steps must remain grouped in explanation stage order.`);
|
|
428
821
|
}
|
|
429
822
|
previousPairIndex = pairIndex;
|
|
430
|
-
if (step.stageId !== void 0)
|
|
823
|
+
if (step.stageId !== void 0) requireString2(step.stageId, `scenes[${index}].steps[${stepIndex}].stageId`);
|
|
431
824
|
if (step.stageIndex !== void 0) requireNonNegativeInteger(step.stageIndex, `scenes[${index}].steps[${stepIndex}].stageIndex`);
|
|
432
825
|
if (step.focusIndex !== void 0) requireNonNegativeInteger(step.focusIndex, `scenes[${index}].steps[${stepIndex}].focusIndex`);
|
|
433
826
|
if (step.focusCount !== void 0) requirePositiveInteger(step.focusCount, `scenes[${index}].steps[${stepIndex}].focusCount`);
|
|
@@ -440,14 +833,14 @@ function validateDeconstructedScene(value, index) {
|
|
|
440
833
|
});
|
|
441
834
|
for (let stageIndex = 0; stageIndex < panels.length - 1; stageIndex += 1) {
|
|
442
835
|
const panel = panels[stageIndex + 1];
|
|
443
|
-
const stageSteps = value.steps.filter((step) =>
|
|
444
|
-
if (!
|
|
836
|
+
const stageSteps = value.steps.filter((step) => isRecord2(step) && Number(step.pairIndex) === stageIndex);
|
|
837
|
+
if (!isRecord2(panel) || stageSteps.length === 0) {
|
|
445
838
|
throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
|
|
446
839
|
}
|
|
447
840
|
const firstStep = stageSteps[0];
|
|
448
|
-
const fallbackStageId =
|
|
841
|
+
const fallbackStageId = isRecord2(firstStep) ? firstStep.id : void 0;
|
|
449
842
|
if (stageSteps.some((step) => {
|
|
450
|
-
if (!
|
|
843
|
+
if (!isRecord2(step)) return true;
|
|
451
844
|
return (step.stageId ?? fallbackStageId) !== panel.stageId;
|
|
452
845
|
})) {
|
|
453
846
|
throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
|
|
@@ -458,15 +851,15 @@ function validateDeconstructedScene(value, index) {
|
|
|
458
851
|
function validateMultiPanelFiles(value, panelCount, index) {
|
|
459
852
|
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].files must be non-empty.`);
|
|
460
853
|
for (const [fileIndex, file] of value.entries()) {
|
|
461
|
-
if (!
|
|
462
|
-
|
|
854
|
+
if (!isRecord2(file)) throw new Error(`scenes[${index}].files[${fileIndex}] must be an object.`);
|
|
855
|
+
requireString2(file.path, `scenes[${index}].files[${fileIndex}].path`);
|
|
463
856
|
if (!Array.isArray(file.panels) || file.panels.length !== panelCount) {
|
|
464
857
|
throw new Error(`scenes[${index}].files[${fileIndex}].panels must match panel count.`);
|
|
465
858
|
}
|
|
466
859
|
for (const [panelIndex, panel] of file.panels.entries()) {
|
|
467
|
-
if (!
|
|
468
|
-
for (const key of ["id", "label", "content"])
|
|
469
|
-
if (panel.path !== void 0)
|
|
860
|
+
if (!isRecord2(panel)) throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}] must be an object.`);
|
|
861
|
+
for (const key of ["id", "label", "content"]) requireString2(panel[key], `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].${key}`);
|
|
862
|
+
if (panel.path !== void 0) requireString2(panel.path, `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].path`);
|
|
470
863
|
if (typeof panel.exists !== "boolean") throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}].exists must be boolean.`);
|
|
471
864
|
}
|
|
472
865
|
}
|
|
@@ -474,12 +867,12 @@ function validateMultiPanelFiles(value, panelCount, index) {
|
|
|
474
867
|
function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
|
|
475
868
|
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].steps must be non-empty.`);
|
|
476
869
|
for (const [stepIndex, step] of value.entries()) {
|
|
477
|
-
if (!
|
|
478
|
-
for (const key of ["id", "title", "body", "file", "side"])
|
|
870
|
+
if (!isRecord2(step)) throw new Error(`scenes[${index}].steps[${stepIndex}] must be an object.`);
|
|
871
|
+
for (const key of ["id", "title", "body", "file", "side"]) requireString2(step[key], `scenes[${index}].steps[${stepIndex}].${key}`);
|
|
479
872
|
requireNonNegativeInteger(step.pairIndex, `scenes[${index}].steps[${stepIndex}].pairIndex`);
|
|
480
873
|
if (Number(step.pairIndex) >= panelCount - 1) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex is outside the panels.`);
|
|
481
874
|
if (deconstructed) {
|
|
482
|
-
|
|
875
|
+
requireStringArray2(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
|
|
483
876
|
if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
|
|
484
877
|
}
|
|
485
878
|
if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
|
|
@@ -488,7 +881,7 @@ function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
|
|
|
488
881
|
}
|
|
489
882
|
}
|
|
490
883
|
function validateTourFile(value, index) {
|
|
491
|
-
if (!
|
|
884
|
+
if (!isRecord2(value)) throw new Error(`files[${index}] must be an object.`);
|
|
492
885
|
if (value.kind === "text-diff") {
|
|
493
886
|
validateScene(value, index);
|
|
494
887
|
return;
|
|
@@ -497,9 +890,9 @@ function validateTourFile(value, index) {
|
|
|
497
890
|
throw new Error(`files[${index}].kind must be text-diff or omitted.`);
|
|
498
891
|
}
|
|
499
892
|
for (const key of ["id", "title", "path", "changeKind", "reason"]) {
|
|
500
|
-
|
|
893
|
+
requireString2(value[key], `files[${index}].${key}`);
|
|
501
894
|
}
|
|
502
|
-
if (value.previousPath !== void 0)
|
|
895
|
+
if (value.previousPath !== void 0) requireString2(value.previousPath, `files[${index}].previousPath`);
|
|
503
896
|
requireNonNegativeInteger(value.additions, `files[${index}].additions`);
|
|
504
897
|
requireNonNegativeInteger(value.deletions, `files[${index}].deletions`);
|
|
505
898
|
}
|
|
@@ -519,263 +912,13 @@ function collectLegacyTourFiles(scenes) {
|
|
|
519
912
|
return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
520
913
|
}
|
|
521
914
|
function validateResolvedAnchor(value, path) {
|
|
522
|
-
if (!
|
|
523
|
-
for (const key of ["id", "path", "revision", "excerpt"])
|
|
915
|
+
if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
|
|
916
|
+
for (const key of ["id", "path", "revision", "excerpt"]) requireString2(value[key], `${path}.${key}`);
|
|
524
917
|
if (value.revision !== "base" && value.revision !== "head") throw new Error(`${path}.revision must be base or head.`);
|
|
525
918
|
requirePositiveInteger(value.startLine, `${path}.startLine`);
|
|
526
919
|
requirePositiveInteger(value.endLine, `${path}.endLine`);
|
|
527
920
|
if (Number(value.endLine) < Number(value.startLine)) throw new Error(`${path}.endLine must not precede startLine.`);
|
|
528
921
|
}
|
|
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.`);
|
|
659
|
-
}
|
|
660
|
-
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "steps"], path);
|
|
661
|
-
const stepIds = /* @__PURE__ */ new Set();
|
|
662
|
-
for (const [stepIndex, step] of scene.steps.entries()) {
|
|
663
|
-
const stepPath = `${path}.steps[${stepIndex}]`;
|
|
664
|
-
if (!isRecord2(step)) {
|
|
665
|
-
throw new Error(`${stepPath} must be an object.`);
|
|
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
|
-
}
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
return { ...value, connections: rawConnections };
|
|
690
|
-
}
|
|
691
|
-
function validateDeconstructedScene2(scene, path) {
|
|
692
|
-
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "base", "target", "stages", "exclusions"], path);
|
|
693
|
-
optionalString(scene.base, `${path}.base`);
|
|
694
|
-
optionalString(scene.target, `${path}.target`);
|
|
695
|
-
if (!Array.isArray(scene.stages) || scene.stages.length === 0 || scene.stages.length > 12) {
|
|
696
|
-
throw new Error(`${path}.stages must contain between 1 and 12 stages.`);
|
|
697
|
-
}
|
|
698
|
-
const stageIds = /* @__PURE__ */ new Set();
|
|
699
|
-
for (const [stageIndex, stage] of scene.stages.entries()) {
|
|
700
|
-
const stagePath = `${path}.stages[${stageIndex}]`;
|
|
701
|
-
if (!isRecord2(stage)) throw new Error(`${stagePath} must be an object.`);
|
|
702
|
-
requireOnlyKeys(stage, ["id", "title", "narration", "changes"], stagePath);
|
|
703
|
-
requireString2(stage.id, `${stagePath}.id`);
|
|
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.`);
|
|
708
|
-
}
|
|
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
|
-
}
|
|
725
|
-
}
|
|
726
|
-
function validateDeconstructedSelection(value, path, requiresReason) {
|
|
727
|
-
if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
|
|
728
|
-
requireOnlyKeys(value, requiresReason ? ["file", "hunks", "reason"] : ["file", "hunks"], path);
|
|
729
|
-
requireString2(value.file, `${path}.file`);
|
|
730
|
-
if (!requiresReason || value.hunks !== void 0) {
|
|
731
|
-
requireStringArray2(value.hunks, `${path}.hunks`);
|
|
732
|
-
if (value.hunks.length === 0) throw new Error(`${path}.hunks must not be empty.`);
|
|
733
|
-
}
|
|
734
|
-
if (requiresReason) requireString2(value.reason, `${path}.reason`);
|
|
735
|
-
}
|
|
736
|
-
function validateStackedScene2(scene, path) {
|
|
737
|
-
requireOnlyKeys(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "stack", "files", "steps"], path);
|
|
738
|
-
if (!Array.isArray(scene.stack) || scene.stack.length < 3 || scene.stack.length > 6) {
|
|
739
|
-
throw new Error(`${path}.stack must contain between 3 and 6 revisions.`);
|
|
740
|
-
}
|
|
741
|
-
const stackIds = /* @__PURE__ */ new Set();
|
|
742
|
-
for (const [index, entry] of scene.stack.entries()) {
|
|
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);
|
|
751
|
-
}
|
|
752
|
-
if (scene.files !== void 0) requireStringArray2(scene.files, `${path}.files`);
|
|
753
|
-
if (!Array.isArray(scene.steps) || scene.steps.length === 0) throw new Error(`${path}.steps must be a non-empty array.`);
|
|
754
|
-
const stepIds = /* @__PURE__ */ new Set();
|
|
755
|
-
for (const [index, step] of scene.steps.entries()) {
|
|
756
|
-
const stepPath = `${path}.steps[${index}]`;
|
|
757
|
-
if (!isRecord2(step)) throw new Error(`${stepPath} must be an object.`);
|
|
758
|
-
requireOnlyKeys(step, ["id", "title", "body", "file", "pair", "side", "lines"], stepPath);
|
|
759
|
-
requireString2(step.id, `${stepPath}.id`);
|
|
760
|
-
requireString2(step.title, `${stepPath}.title`);
|
|
761
|
-
requireString2(step.body, `${stepPath}.body`);
|
|
762
|
-
requireString2(step.file, `${stepPath}.file`);
|
|
763
|
-
const stepId = step.id;
|
|
764
|
-
if (stepIds.has(stepId)) throw new Error(`Duplicate step id in scene ${scene.id}: ${stepId}`);
|
|
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.`);
|
|
768
|
-
}
|
|
769
|
-
const pair = step.pair;
|
|
770
|
-
const leftIndex = scene.stack.findIndex((entry) => isRecord2(entry) && entry.id === pair[0]);
|
|
771
|
-
const rightIndex = scene.stack.findIndex((entry) => isRecord2(entry) && entry.id === pair[1]);
|
|
772
|
-
if (leftIndex < 0 || rightIndex !== leftIndex + 1) throw new Error(`${stepPath}.pair must reference adjacent stack entries in order.`);
|
|
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.`);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
922
|
function validateNarrative2(value, path) {
|
|
780
923
|
requireString2(value.summary, `${path}.summary`);
|
|
781
924
|
requireStringArray2(value.bullets, `${path}.bullets`);
|
|
@@ -786,13 +929,8 @@ function isRecord2(value) {
|
|
|
786
929
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
787
930
|
}
|
|
788
931
|
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);
|
|
932
|
+
if (typeof value !== "string") {
|
|
933
|
+
throw new Error(`${path} must be a string.`);
|
|
796
934
|
}
|
|
797
935
|
}
|
|
798
936
|
function requireStringArray2(value, path) {
|
|
@@ -800,12 +938,19 @@ function requireStringArray2(value, path) {
|
|
|
800
938
|
throw new Error(`${path} must be an array of strings.`);
|
|
801
939
|
}
|
|
802
940
|
}
|
|
803
|
-
function
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
941
|
+
function requireNonNegativeInteger(value, path) {
|
|
942
|
+
if (!Number.isInteger(value) || Number(value) < 0) {
|
|
943
|
+
throw new Error(`${path} must be a non-negative integer.`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function requirePercentage(value, path) {
|
|
947
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
|
|
948
|
+
throw new Error(`${path} must be a number from 0 to 100.`);
|
|
807
949
|
}
|
|
808
950
|
}
|
|
951
|
+
function requirePositiveInteger(value, path) {
|
|
952
|
+
if (!Number.isInteger(value) || Number(value) < 1) throw new Error(`${path} must be a positive integer.`);
|
|
953
|
+
}
|
|
809
954
|
|
|
810
955
|
// src/changeInventory.ts
|
|
811
956
|
var import_crypto = require("crypto");
|
|
@@ -1585,12 +1730,20 @@ var DIFF_TIMEOUT_MS = readPositiveIntegerEnv("BYGONE_DIFF_TIMEOUT_MS", DEFAULT_D
|
|
|
1585
1730
|
function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
1586
1731
|
const leftLines = normalizeLines(leftContent);
|
|
1587
1732
|
const rightLines = normalizeLines(rightContent);
|
|
1588
|
-
|
|
1733
|
+
if (leftContent === rightContent) {
|
|
1734
|
+
return buildIdenticalTwoWayDiffModel(leftLines);
|
|
1735
|
+
}
|
|
1736
|
+
const interned = internDiffLines(leftLines, rightLines);
|
|
1737
|
+
const internedChanges = diffArrays(interned.left, interned.right, {
|
|
1589
1738
|
timeout: options.timeoutMs ?? DIFF_TIMEOUT_MS
|
|
1590
1739
|
});
|
|
1591
|
-
if (!
|
|
1740
|
+
if (!internedChanges) {
|
|
1592
1741
|
return buildConservativeTwoWayDiffModel(leftLines, rightLines);
|
|
1593
1742
|
}
|
|
1743
|
+
const changes = internedChanges.map((change) => ({
|
|
1744
|
+
...change,
|
|
1745
|
+
value: change.value.map((lineId) => interned.lines[lineId])
|
|
1746
|
+
}));
|
|
1594
1747
|
const rows = [];
|
|
1595
1748
|
const renderedLeftLines = [];
|
|
1596
1749
|
const renderedRightLines = [];
|
|
@@ -1614,29 +1767,18 @@ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
|
1614
1767
|
}
|
|
1615
1768
|
if (change.removed && index + 1 < changes.length && changes[index + 1].added) {
|
|
1616
1769
|
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));
|
|
1770
|
+
const nextLineNumbers = appendReplacementCandidate(
|
|
1771
|
+
removedLines,
|
|
1772
|
+
nextChange.value,
|
|
1773
|
+
leftLineNumber,
|
|
1774
|
+
rightLineNumber,
|
|
1775
|
+
rows,
|
|
1776
|
+
renderedLeftLines,
|
|
1777
|
+
renderedRightLines,
|
|
1778
|
+
blocks
|
|
1779
|
+
);
|
|
1780
|
+
leftLineNumber = nextLineNumbers.left;
|
|
1781
|
+
rightLineNumber = nextLineNumbers.right;
|
|
1640
1782
|
index++;
|
|
1641
1783
|
continue;
|
|
1642
1784
|
}
|
|
@@ -1672,7 +1814,40 @@ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
|
1672
1814
|
leftLines: renderedLeftLines,
|
|
1673
1815
|
rightLines: renderedRightLines,
|
|
1674
1816
|
blocks,
|
|
1675
|
-
hasChanges
|
|
1817
|
+
hasChanges,
|
|
1818
|
+
quality: "exact"
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
function internDiffLines(leftLines, rightLines) {
|
|
1822
|
+
const lineIds = /* @__PURE__ */ new Map();
|
|
1823
|
+
const lines = [];
|
|
1824
|
+
const intern = (line) => {
|
|
1825
|
+
const existingId = lineIds.get(line);
|
|
1826
|
+
if (existingId !== void 0) {
|
|
1827
|
+
return existingId;
|
|
1828
|
+
}
|
|
1829
|
+
const lineId = lines.length;
|
|
1830
|
+
lineIds.set(line, lineId);
|
|
1831
|
+
lines.push(line);
|
|
1832
|
+
return lineId;
|
|
1833
|
+
};
|
|
1834
|
+
return {
|
|
1835
|
+
left: leftLines.map(intern),
|
|
1836
|
+
right: rightLines.map(intern),
|
|
1837
|
+
lines
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
function buildIdenticalTwoWayDiffModel(lines) {
|
|
1841
|
+
return {
|
|
1842
|
+
rows: lines.map((line, index) => makeDiffRow(
|
|
1843
|
+
makeDiffCell("context", line, index + 1),
|
|
1844
|
+
makeDiffCell("context", line, index + 1)
|
|
1845
|
+
)),
|
|
1846
|
+
leftLines: lines.map((line, index) => makeDiffLine("context", line, index + 1)),
|
|
1847
|
+
rightLines: lines.map((line, index) => makeDiffLine("context", line, index + 1)),
|
|
1848
|
+
blocks: [],
|
|
1849
|
+
hasChanges: false,
|
|
1850
|
+
quality: "exact"
|
|
1676
1851
|
};
|
|
1677
1852
|
}
|
|
1678
1853
|
function buildConservativeTwoWayDiffModel(leftLines, rightLines) {
|
|
@@ -1699,7 +1874,8 @@ function buildConservativeTwoWayDiffModel(leftLines, rightLines) {
|
|
|
1699
1874
|
leftLines: renderedLeftLines,
|
|
1700
1875
|
rightLines: renderedRightLines,
|
|
1701
1876
|
blocks: rows.length === 0 ? [] : [makeDiffBlock(blockKind, 0, leftLines.length, 0, rightLines.length)],
|
|
1702
|
-
hasChanges: rows.length > 0
|
|
1877
|
+
hasChanges: rows.length > 0,
|
|
1878
|
+
quality: "fallback"
|
|
1703
1879
|
};
|
|
1704
1880
|
}
|
|
1705
1881
|
function normalizeLines(content) {
|
|
@@ -1712,6 +1888,100 @@ function normalizeLines(content) {
|
|
|
1712
1888
|
}
|
|
1713
1889
|
return lines;
|
|
1714
1890
|
}
|
|
1891
|
+
function appendReplacementCandidate(leftLines, rightLines, initialLeftLineNumber, initialRightLineNumber, rows, renderedLeftLines, renderedRightLines, blocks) {
|
|
1892
|
+
const aligned = alignReplacementLines(leftLines, rightLines);
|
|
1893
|
+
const crediblePairs = aligned.filter((row) => row.left !== void 0 && row.right !== void 0 && hasCredibleLineCorrespondence(row.left, row.right)).length;
|
|
1894
|
+
const coherentBlock = hasCoherentBlockCorrespondence(leftLines, rightLines, crediblePairs);
|
|
1895
|
+
const normalized = aligned.flatMap((row) => {
|
|
1896
|
+
if (row.left === void 0 || row.right === void 0 || hasCredibleLineCorrespondence(row.left, row.right)) {
|
|
1897
|
+
return [row];
|
|
1898
|
+
}
|
|
1899
|
+
return [{ left: row.left }, { right: row.right }];
|
|
1900
|
+
});
|
|
1901
|
+
let leftLineNumber = initialLeftLineNumber;
|
|
1902
|
+
let rightLineNumber = initialRightLineNumber;
|
|
1903
|
+
let blockKind;
|
|
1904
|
+
let blockLeftStart = renderedLeftLines.length;
|
|
1905
|
+
let blockRightStart = renderedRightLines.length;
|
|
1906
|
+
const flushBlock = () => {
|
|
1907
|
+
if (!blockKind) return;
|
|
1908
|
+
blocks.push(makeDiffBlock(
|
|
1909
|
+
blockKind,
|
|
1910
|
+
blockLeftStart,
|
|
1911
|
+
renderedLeftLines.length,
|
|
1912
|
+
blockRightStart,
|
|
1913
|
+
renderedRightLines.length
|
|
1914
|
+
));
|
|
1915
|
+
};
|
|
1916
|
+
for (const { left, right } of normalized) {
|
|
1917
|
+
const nextKind = coherentBlock ? "replace" : left !== void 0 && right !== void 0 ? "replace" : left !== void 0 ? "delete" : "insert";
|
|
1918
|
+
if (blockKind !== nextKind) {
|
|
1919
|
+
flushBlock();
|
|
1920
|
+
blockKind = nextKind;
|
|
1921
|
+
blockLeftStart = renderedLeftLines.length;
|
|
1922
|
+
blockRightStart = renderedRightLines.length;
|
|
1923
|
+
}
|
|
1924
|
+
let renderedLeftLine;
|
|
1925
|
+
let renderedRightLine;
|
|
1926
|
+
if (left !== void 0) {
|
|
1927
|
+
renderedLeftLine = makeDiffLine("removed", left, leftLineNumber);
|
|
1928
|
+
renderedLeftLines.push(renderedLeftLine);
|
|
1929
|
+
}
|
|
1930
|
+
if (right !== void 0) {
|
|
1931
|
+
renderedRightLine = makeDiffLine("added", right, rightLineNumber);
|
|
1932
|
+
renderedRightLines.push(renderedRightLine);
|
|
1933
|
+
}
|
|
1934
|
+
if (renderedLeftLine && renderedRightLine) {
|
|
1935
|
+
applyInlineHighlightPair(renderedLeftLine, renderedRightLine);
|
|
1936
|
+
} else if (coherentBlock) {
|
|
1937
|
+
if (renderedLeftLine) applyFullLineHighlight(renderedLeftLine);
|
|
1938
|
+
if (renderedRightLine) applyFullLineHighlight(renderedRightLine);
|
|
1939
|
+
}
|
|
1940
|
+
rows.push(makeDiffRow(
|
|
1941
|
+
left === void 0 ? makePlaceholder() : makeDiffCell("removed", left, leftLineNumber++),
|
|
1942
|
+
right === void 0 ? makePlaceholder() : makeDiffCell("added", right, rightLineNumber++)
|
|
1943
|
+
));
|
|
1944
|
+
}
|
|
1945
|
+
flushBlock();
|
|
1946
|
+
return { left: leftLineNumber, right: rightLineNumber };
|
|
1947
|
+
}
|
|
1948
|
+
function hasCredibleLineCorrespondence(left, right) {
|
|
1949
|
+
const score = scoreReplacementLinePair(left, right);
|
|
1950
|
+
if (score.eligible) return true;
|
|
1951
|
+
const leftAnchor = declarationAnchorKey(left);
|
|
1952
|
+
if (leftAnchor && leftAnchor === declarationAnchorKey(right)) return true;
|
|
1953
|
+
const leftTokens = new Set(tokenizeMatchingContent(normalizeMatchingContent(left)).filter(isSharedContentToken));
|
|
1954
|
+
return tokenizeMatchingContent(normalizeMatchingContent(right)).some((token) => leftTokens.has(token) && isSharedContentToken(token));
|
|
1955
|
+
}
|
|
1956
|
+
var NON_SUBSTANTIVE_SHARED_TOKENS = /* @__PURE__ */ new Set([
|
|
1957
|
+
"and",
|
|
1958
|
+
"are",
|
|
1959
|
+
"for",
|
|
1960
|
+
"from",
|
|
1961
|
+
"into",
|
|
1962
|
+
"that",
|
|
1963
|
+
"the",
|
|
1964
|
+
"this",
|
|
1965
|
+
"to",
|
|
1966
|
+
"with"
|
|
1967
|
+
]);
|
|
1968
|
+
function isSharedContentToken(token) {
|
|
1969
|
+
return token.length >= 3 && /^[\p{L}\p{N}_$]+$/u.test(token) && !NON_SUBSTANTIVE_SHARED_TOKENS.has(token);
|
|
1970
|
+
}
|
|
1971
|
+
function hasCoherentBlockCorrespondence(leftLines, rightLines, crediblePairs) {
|
|
1972
|
+
if (crediblePairs === 0) return false;
|
|
1973
|
+
const longestSide = Math.max(leftLines.length, rightLines.length);
|
|
1974
|
+
if (longestSide <= 2 || crediblePairs * 2 >= longestSide) return true;
|
|
1975
|
+
const roles = [...leftLines, ...rightLines].map(collectionLineRole);
|
|
1976
|
+
return roles[0] !== null && roles.every((role) => role === roles[0]);
|
|
1977
|
+
}
|
|
1978
|
+
function collectionLineRole(line) {
|
|
1979
|
+
const normalized = line.trimStart();
|
|
1980
|
+
if (/^[-*+]\s+/.test(normalized)) return "bullet";
|
|
1981
|
+
if (/^\d+[.)]\s+/.test(normalized)) return "ordered";
|
|
1982
|
+
if (/^>\s+/.test(normalized)) return "quote";
|
|
1983
|
+
return null;
|
|
1984
|
+
}
|
|
1715
1985
|
var MAX_ALIGNMENT_CELLS = 1e4;
|
|
1716
1986
|
var MAX_SCORING_LINE_LENGTH = 2e3;
|
|
1717
1987
|
var MINIMUM_MATCH_SCORE = 0.52;
|
|
@@ -1724,6 +1994,7 @@ var CONTEXTUAL_AMBIGUITY_MARGIN = 0.05;
|
|
|
1724
1994
|
var GAP_PENALTY = -0.12;
|
|
1725
1995
|
var MAX_BOUNDED_CANDIDATES = 5e3;
|
|
1726
1996
|
var MAX_LARGE_HUNK_ANCHORS = 2e3;
|
|
1997
|
+
var MAX_ALIGNMENT_CHARACTER_CELLS = 5e7;
|
|
1727
1998
|
var MAX_RARE_TOKEN_OCCURRENCES = 4;
|
|
1728
1999
|
var MAX_CONTEXTUAL_HUNK_LINES = 8;
|
|
1729
2000
|
var MAX_CONTEXTUAL_LENGTH_DELTA = 4;
|
|
@@ -1734,9 +2005,17 @@ function alignReplacementLines(leftLines, rightLines) {
|
|
|
1734
2005
|
if (rightLines.length === 0) {
|
|
1735
2006
|
return leftLines.map((left) => ({ left }));
|
|
1736
2007
|
}
|
|
2008
|
+
const leftCharacterCount = leftLines.reduce((total, line) => total + line.length, 0);
|
|
2009
|
+
const rightCharacterCount = rightLines.reduce((total, line) => total + line.length, 0);
|
|
2010
|
+
if (leftCharacterCount * rightCharacterCount > MAX_ALIGNMENT_CHARACTER_CELLS) {
|
|
2011
|
+
return [
|
|
2012
|
+
...leftLines.map((left) => ({ left })),
|
|
2013
|
+
...rightLines.map((right) => ({ right }))
|
|
2014
|
+
];
|
|
2015
|
+
}
|
|
1737
2016
|
if (leftLines.length === 1 && rightLines.length === 1) {
|
|
1738
2017
|
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]))) {
|
|
2018
|
+
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
2019
|
return [{ left: leftLines[0], right: rightLines[0] }];
|
|
1741
2020
|
}
|
|
1742
2021
|
return [{ left: leftLines[0] }, { right: rightLines[0] }];
|
|
@@ -2258,6 +2537,11 @@ function applyInlineHighlightPair(leftLine, rightLine) {
|
|
|
2258
2537
|
rightLine.segments = rightSegments;
|
|
2259
2538
|
}
|
|
2260
2539
|
}
|
|
2540
|
+
function applyFullLineHighlight(line) {
|
|
2541
|
+
if (/[^\s]/.test(line.content)) {
|
|
2542
|
+
line.segments = [{ kind: line.kind, text: line.content, emphasis: true }];
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2261
2545
|
function buildInlineSegments(leftContent, rightContent) {
|
|
2262
2546
|
const changes = diffWordsWithSpace(leftContent, rightContent);
|
|
2263
2547
|
const leftSegments = [];
|
|
@@ -2409,6 +2693,125 @@ function findBlankLineBoundary(lines, target, minimum, maximum) {
|
|
|
2409
2693
|
return Math.min(Math.max(target, minimum), maximum - 1);
|
|
2410
2694
|
}
|
|
2411
2695
|
|
|
2696
|
+
// src/changeAttention.ts
|
|
2697
|
+
var CHANGE_ATTENTION_VERSION = 1;
|
|
2698
|
+
var LOCKFILES = /* @__PURE__ */ new Set([
|
|
2699
|
+
"cargo.lock",
|
|
2700
|
+
"package-lock.json",
|
|
2701
|
+
"pnpm-lock.yaml",
|
|
2702
|
+
"yarn.lock",
|
|
2703
|
+
"poetry.lock",
|
|
2704
|
+
"gemfile.lock",
|
|
2705
|
+
"composer.lock"
|
|
2706
|
+
]);
|
|
2707
|
+
var MAX_FAN_IN_FILES = 250;
|
|
2708
|
+
var MAX_FAN_IN_TEXT_BYTES = 64 * 1024;
|
|
2709
|
+
function classifyChangeFileRole(filePath) {
|
|
2710
|
+
const lower = filePath.toLowerCase();
|
|
2711
|
+
const fileName = lower.split("/").pop() || lower;
|
|
2712
|
+
if (/(^|\/)(dist|build|generated|vendor)(\/|$)/.test(lower) || /(?:\.min\.js|\.map)$/.test(lower)) return "generated";
|
|
2713
|
+
if (/(^|\/)(tests?|__tests__)(\/|$)/.test(lower) || /(^|\/)test_[^/]+$/.test(lower) || /\.(test|spec)\.[^/]+$/.test(lower)) return "test";
|
|
2714
|
+
if (lower.startsWith("docs/") || fileName === "readme.md" || /\.(md|mdx|rst)$/.test(lower)) return "documentation";
|
|
2715
|
+
if (LOCKFILES.has(fileName) || /(^|\/)(pyproject\.toml|package\.json|go\.mod|go\.sum|cargo\.toml)$/.test(lower)) return "dependency";
|
|
2716
|
+
return "production";
|
|
2717
|
+
}
|
|
2718
|
+
function buildChangeAttention(inputs) {
|
|
2719
|
+
const prepared = inputs.map((input) => prepareFile(input, inputs));
|
|
2720
|
+
const production = prepared.filter((file) => file.role === "production" && !file.mechanical);
|
|
2721
|
+
const documentation = prepared.filter((file) => file.role === "documentation" && !file.mechanical);
|
|
2722
|
+
const eligible = production.length > 0 ? production : documentation;
|
|
2723
|
+
const entry = [...eligible].sort(compareRanked)[0];
|
|
2724
|
+
const fallback = [...prepared].filter((file) => !file.binary).sort((left, right) => right.changedLines - left.changedLines || left.path.localeCompare(right.path))[0];
|
|
2725
|
+
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)));
|
|
2726
|
+
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;
|
|
2727
|
+
if (pairedTest && focusCandidates.length < Math.max(0, 7 - (entry ? 1 : 0))) focusCandidates.push(pairedTest);
|
|
2728
|
+
const focusPaths = focusCandidates.map((file) => file.path);
|
|
2729
|
+
const files = prepared.map((file) => {
|
|
2730
|
+
const attention = file.path === (entry == null ? void 0 : entry.path) ? "entry" : focusPaths.includes(file.path) ? "focus" : file.mechanical ? "mechanical" : "background";
|
|
2731
|
+
return {
|
|
2732
|
+
path: file.path,
|
|
2733
|
+
previousPath: file.previousPath,
|
|
2734
|
+
changeKind: file.changeKind,
|
|
2735
|
+
role: file.role,
|
|
2736
|
+
attention,
|
|
2737
|
+
additions: file.additions,
|
|
2738
|
+
deletions: file.deletions,
|
|
2739
|
+
reason: attention === "entry" ? entryReason(file, production.length) : attentionReason(file),
|
|
2740
|
+
score: file.score
|
|
2741
|
+
};
|
|
2742
|
+
}).sort((left, right) => left.path.localeCompare(right.path));
|
|
2743
|
+
return {
|
|
2744
|
+
version: CHANGE_ATTENTION_VERSION,
|
|
2745
|
+
entryPath: (entry == null ? void 0 : entry.path) || null,
|
|
2746
|
+
entryReason: entry ? entryReason(entry, production.length) : noEntryReason(prepared),
|
|
2747
|
+
fallbackPath: (fallback == null ? void 0 : fallback.path) || null,
|
|
2748
|
+
focusPaths,
|
|
2749
|
+
backgroundPaths: files.filter((file) => file.attention === "background").map((file) => file.path),
|
|
2750
|
+
mechanicalPaths: files.filter((file) => file.attention === "mechanical").map((file) => file.path),
|
|
2751
|
+
files
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
function prepareFile(input, all) {
|
|
2755
|
+
const role = classifyChangeFileRole(input.path);
|
|
2756
|
+
const fileName = input.path.toLowerCase().split("/").pop() || input.path.toLowerCase();
|
|
2757
|
+
const mechanical = Boolean(input.binary) || role === "generated" || LOCKFILES.has(fileName);
|
|
2758
|
+
const changedLines = (input.additions ?? 0) + (input.deletions ?? 0);
|
|
2759
|
+
const contract = /(^|[\/._-])(types?|schema|models?|contracts?|interfaces?)([\/._-]|$)/i.test(input.path);
|
|
2760
|
+
const referenceHints = new Set([
|
|
2761
|
+
fileName.replace(/\.[^.]+$/, ""),
|
|
2762
|
+
...(input.symbolHints || []).map((hint) => hint.toLowerCase())
|
|
2763
|
+
].filter((hint) => hint.length >= 4));
|
|
2764
|
+
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) => {
|
|
2765
|
+
var _a;
|
|
2766
|
+
const boundedText = (_a = candidate.headText) == null ? void 0 : _a.slice(0, MAX_FAN_IN_TEXT_BYTES).toLowerCase();
|
|
2767
|
+
return Boolean(boundedText && [...referenceHints].some((hint) => boundedText.includes(hint)));
|
|
2768
|
+
}).length;
|
|
2769
|
+
const roleScore = role === "production" ? 300 : role === "documentation" ? 150 : role === "dependency" ? 80 : role === "test" ? 50 : 0;
|
|
2770
|
+
const sizePenalty = changedLines > 2e3 ? 80 : changedLines > 500 ? 30 : 0;
|
|
2771
|
+
const readmePenalty = fileName === "readme.md" && all.filter((candidate) => !candidate.binary).length > 1 ? 100 : 0;
|
|
2772
|
+
return {
|
|
2773
|
+
...input,
|
|
2774
|
+
role,
|
|
2775
|
+
mechanical,
|
|
2776
|
+
changedLines,
|
|
2777
|
+
contract,
|
|
2778
|
+
fanIn,
|
|
2779
|
+
binary: Boolean(input.binary),
|
|
2780
|
+
score: mechanical ? -1e3 : roleScore + (contract ? 100 : 0) + fanIn * 40 + (input.commitCount || 0) * 5 - sizePenalty - readmePenalty
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
function compareRanked(left, right) {
|
|
2784
|
+
return right.score - left.score || right.fanIn - left.fanIn || left.changedLines - right.changedLines || left.path.localeCompare(right.path);
|
|
2785
|
+
}
|
|
2786
|
+
function entryReason(file, productionCount) {
|
|
2787
|
+
if (file.fanIn > 0) return `Referenced by ${file.fanIn} other changed file${file.fanIn === 1 ? "" : "s"}.`;
|
|
2788
|
+
if (file.contract) return "Contract-shaped path central to the change.";
|
|
2789
|
+
if (productionCount === 1) return "Only production file in this range.";
|
|
2790
|
+
return "Highest-ranked production file for a first pass.";
|
|
2791
|
+
}
|
|
2792
|
+
function attentionReason(file) {
|
|
2793
|
+
if (file.binary) return "Binary file.";
|
|
2794
|
+
if (file.mechanical) return file.role === "generated" ? "Generated artifact." : "Dependency lockfile.";
|
|
2795
|
+
if (file.role === "test") return "Test or proof file.";
|
|
2796
|
+
if (file.role === "documentation") return "Documentation change.";
|
|
2797
|
+
if (file.role === "dependency") return "Dependency or packaging manifest.";
|
|
2798
|
+
return "Additional production file.";
|
|
2799
|
+
}
|
|
2800
|
+
function noEntryReason(files) {
|
|
2801
|
+
if (files.every((file) => file.mechanical)) return "This change is entirely mechanical or generated.";
|
|
2802
|
+
if (files.every((file) => file.mechanical || file.role === "test")) return "This change contains only tests and mechanical files.";
|
|
2803
|
+
return "No single entry-point is reliable for this change.";
|
|
2804
|
+
}
|
|
2805
|
+
function isTestForFocus(testPath, focusPaths) {
|
|
2806
|
+
const normalizedTest = testPath.toLowerCase().replace(/\.(test|spec)(?=\.)/, "").replace(/^test_/, "");
|
|
2807
|
+
return focusPaths.some((candidate) => {
|
|
2808
|
+
var _a;
|
|
2809
|
+
const normalizedCandidate = candidate.toLowerCase();
|
|
2810
|
+
const stem = ((_a = normalizedCandidate.split("/").pop()) == null ? void 0 : _a.replace(/\.[^.]+$/, "")) || normalizedCandidate;
|
|
2811
|
+
return stem.length >= 3 && normalizedTest.includes(stem);
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2412
2815
|
// src/changeTourContext.ts
|
|
2413
2816
|
var import_child_process3 = require("child_process");
|
|
2414
2817
|
var CHANGE_TOUR_CONTEXT_VERSION = 1;
|
|
@@ -2486,7 +2889,7 @@ function buildContextFile(repoRoot, baseOid, headOid, changedPath, maxPatchBytes
|
|
|
2486
2889
|
path: changedPath.path,
|
|
2487
2890
|
previousPath: changedPath.previousPath,
|
|
2488
2891
|
changeKind: changedPath.kind,
|
|
2489
|
-
role:
|
|
2892
|
+
role: classifyChangeFileRole(changedPath.path),
|
|
2490
2893
|
additions: stats.additions,
|
|
2491
2894
|
deletions: stats.deletions,
|
|
2492
2895
|
binary: stats.binary,
|
|
@@ -2583,15 +2986,6 @@ function readGitText(repoRoot, oid, relativePath, maxBytes) {
|
|
|
2583
2986
|
return void 0;
|
|
2584
2987
|
}
|
|
2585
2988
|
}
|
|
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
2989
|
function runGit2(repoRoot, args) {
|
|
2596
2990
|
return (0, import_child_process3.execFileSync)("git", args, {
|
|
2597
2991
|
cwd: repoRoot,
|
|
@@ -2616,8 +3010,12 @@ var CHAPTERS = {
|
|
|
2616
3010
|
packaging: { id: "packaging", title: "Packaging and dependencies", priority: 4 }
|
|
2617
3011
|
};
|
|
2618
3012
|
function buildChangeTourManifest(startPath, options = {}) {
|
|
2619
|
-
var _a, _b, _c, _d, _e;
|
|
2620
|
-
const range = resolveBranchReviewRange(
|
|
3013
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
3014
|
+
const range = resolveBranchReviewRange(
|
|
3015
|
+
startPath,
|
|
3016
|
+
options.headRef ?? ((_b = (_a = options.source) == null ? void 0 : _a.range) == null ? void 0 : _b.head),
|
|
3017
|
+
options.baseRef ?? ((_d = (_c = options.source) == null ? void 0 : _c.range) == null ? void 0 : _d.base)
|
|
3018
|
+
);
|
|
2621
3019
|
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_TOUR_FILE_BYTES;
|
|
2622
3020
|
const maxLineBytes = options.maxLineBytes ?? DEFAULT_MAX_TOUR_LINE_BYTES;
|
|
2623
3021
|
const omittedFiles = [];
|
|
@@ -2625,8 +3023,9 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2625
3023
|
const sceneRecords = [];
|
|
2626
3024
|
let totalAdditions = 0;
|
|
2627
3025
|
let totalDeletions = 0;
|
|
3026
|
+
let totalBinaryFiles = 0;
|
|
2628
3027
|
for (const changedPath of range.changedPaths) {
|
|
2629
|
-
const { additions, deletions } = readGitLineStats(
|
|
3028
|
+
const { additions, deletions, binary } = readGitLineStats(
|
|
2630
3029
|
range.repoRoot,
|
|
2631
3030
|
range.mergeBaseOid,
|
|
2632
3031
|
range.headOid,
|
|
@@ -2635,6 +3034,7 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2635
3034
|
);
|
|
2636
3035
|
totalAdditions += additions;
|
|
2637
3036
|
totalDeletions += deletions;
|
|
3037
|
+
if (binary) totalBinaryFiles += 1;
|
|
2638
3038
|
const pair = resolveReviewPathPair(range.changedPaths, changedPath.path);
|
|
2639
3039
|
if (!pair) {
|
|
2640
3040
|
omittedFiles.push(changedPath.path);
|
|
@@ -2675,21 +3075,50 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2675
3075
|
}
|
|
2676
3076
|
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
2677
3077
|
sceneRecords.sort((left, right) => left.chapter.priority - right.chapter.priority || left.scene.path.localeCompare(right.scene.path));
|
|
2678
|
-
|
|
3078
|
+
let defaultScenes = sceneRecords.map(({ scene }, index) => ({ ...scene, id: `file-${index + 1}` }));
|
|
2679
3079
|
if (options.story && options.source) {
|
|
2680
3080
|
throw new Error("A change tour can use either a legacy story or a source file, not both.");
|
|
2681
3081
|
}
|
|
2682
|
-
const
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
3082
|
+
const attention = buildChangeAttention(files.map((file) => ({
|
|
3083
|
+
path: file.path,
|
|
3084
|
+
previousPath: file.previousPath,
|
|
3085
|
+
changeKind: file.changeKind,
|
|
3086
|
+
additions: file.additions,
|
|
3087
|
+
deletions: file.deletions,
|
|
3088
|
+
binary: file.kind === "omitted",
|
|
3089
|
+
headText: file.kind === "text-diff" ? file.rightContent : void 0,
|
|
3090
|
+
symbolHints: file.kind === "text-diff" ? findDeclarationNames(file.rightContent) : []
|
|
3091
|
+
})));
|
|
3092
|
+
if (!options.story && !options.source && attention.entryPath) {
|
|
3093
|
+
const focusRank = (scenePath) => {
|
|
3094
|
+
const index = attention.focusPaths.indexOf(scenePath);
|
|
3095
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
3096
|
+
};
|
|
3097
|
+
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));
|
|
3098
|
+
}
|
|
3099
|
+
const authored = options.source ? applySource(parseChangeTourSource(options.source), defaultScenes, range.repoRoot) : options.story ? applyStory(options.story, defaultScenes) : buildGeneratedTourLanding(
|
|
3100
|
+
defaultScenes,
|
|
3101
|
+
buildChapters(sceneRecords, new Map(defaultScenes.map((scene) => [scene.path, scene.id]))),
|
|
3102
|
+
attention,
|
|
3103
|
+
{
|
|
3104
|
+
changedFiles: range.changedPaths.length,
|
|
3105
|
+
binaryFiles: totalBinaryFiles,
|
|
3106
|
+
additions: totalAdditions,
|
|
3107
|
+
deletions: totalDeletions,
|
|
3108
|
+
commitCount: range.commits.length
|
|
3109
|
+
}
|
|
3110
|
+
);
|
|
2686
3111
|
const scenes = authored.scenes;
|
|
2687
3112
|
const chapters = authored.chapters;
|
|
3113
|
+
const finalScenes = "finalScenes" in authored ? authored.finalScenes : scenes;
|
|
3114
|
+
const finalChapters = "finalChapters" in authored ? authored.finalChapters : chapters;
|
|
2688
3115
|
const manifest = {
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
3116
|
+
// Only authored v2 files opt into repository-bound zoom modes. Generated
|
|
3117
|
+
// and legacy/story tours retain the portable v1 manifest contract.
|
|
3118
|
+
version: ((_e = options.source) == null ? void 0 : _e.version) ?? 1,
|
|
3119
|
+
title: ((_f = options.source) == null ? void 0 : _f.title) || ((_g = options.story) == null ? void 0 : _g.title) || options.title || `${range.headRef} against ${range.baseRef}`,
|
|
3120
|
+
windowTitle: (_h = options.source) == null ? void 0 : _h.windowTitle,
|
|
3121
|
+
sourceUrl: ((_i = options.source) == null ? void 0 : _i.sourceUrl) || ((_j = options.story) == null ? void 0 : _j.sourceUrl) || options.sourceUrl,
|
|
2693
3122
|
generatedAt: options.generatedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2694
3123
|
range: {
|
|
2695
3124
|
baseRef: range.baseRef,
|
|
@@ -2710,6 +3139,49 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
2710
3139
|
chapters,
|
|
2711
3140
|
scenes
|
|
2712
3141
|
};
|
|
3142
|
+
if (manifest.version === 2) {
|
|
3143
|
+
manifest.repository = { root: range.repoRoot };
|
|
3144
|
+
const revisions = [];
|
|
3145
|
+
for (const scene of scenes) {
|
|
3146
|
+
let realStack;
|
|
3147
|
+
if (scene.kind === "stacked-diff") realStack = scene;
|
|
3148
|
+
if (scene.kind === "deconstructed-diff") {
|
|
3149
|
+
const authoredScene = (_k = options.source) == null ? void 0 : _k.chapters.flatMap((chapter) => chapter.scenes).find((candidate) => candidate.id === scene.id);
|
|
3150
|
+
if ((authoredScene == null ? void 0 : authoredScene.kind) !== "deconstructed-diff" || !authoredScene.stack) {
|
|
3151
|
+
throw new Error(`Deconstructed scene ${scene.id} requires an explicit real revision stack in v2.`);
|
|
3152
|
+
}
|
|
3153
|
+
realStack = buildStackedScene(range.repoRoot, {
|
|
3154
|
+
...authoredScene,
|
|
3155
|
+
kind: "stacked-diff",
|
|
3156
|
+
stack: authoredScene.stack,
|
|
3157
|
+
files: scene.files.map((file) => file.path),
|
|
3158
|
+
steps: []
|
|
3159
|
+
}, files.map((file) => file.path));
|
|
3160
|
+
if (realStack.stack[0].oid !== scene.realRange.baseOid || realStack.stack[realStack.stack.length - 1].oid !== scene.realRange.targetOid) {
|
|
3161
|
+
throw new Error(`Deconstructed scene ${scene.id} real stack endpoints must match its explanation range.`);
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
if (realStack) {
|
|
3165
|
+
if (realStack.stack[0].oid !== range.mergeBaseOid || realStack.stack[realStack.stack.length - 1].oid !== range.headOid) {
|
|
3166
|
+
throw new Error(`Scene ${scene.id} real stack endpoints must match the tour's final diff range.`);
|
|
3167
|
+
}
|
|
3168
|
+
revisions.push(realStack);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
const explanation = scenes.some((scene) => scene.kind === "deconstructed-diff");
|
|
3172
|
+
const distinctRevisions = revisions.some((scene) => scene.stack.length > 2);
|
|
3173
|
+
manifest.zoom = {
|
|
3174
|
+
authoredDepth: explanation ? "explanation" : distinctRevisions ? "revisions" : "final",
|
|
3175
|
+
modes: [
|
|
3176
|
+
...explanation ? ["explanation"] : [],
|
|
3177
|
+
...distinctRevisions ? ["revisions"] : [],
|
|
3178
|
+
"final",
|
|
3179
|
+
"history"
|
|
3180
|
+
],
|
|
3181
|
+
revisions,
|
|
3182
|
+
final: { scenes: finalScenes, chapters: finalChapters }
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
2713
3185
|
return parseChangeTourManifest(manifest);
|
|
2714
3186
|
}
|
|
2715
3187
|
function applySource(source, defaultScenes, repoRoot) {
|
|
@@ -2732,8 +3204,11 @@ function applySource(source, defaultScenes, repoRoot) {
|
|
|
2732
3204
|
}]));
|
|
2733
3205
|
const scenes = [];
|
|
2734
3206
|
const chapters = [];
|
|
3207
|
+
const finalScenes = [];
|
|
3208
|
+
const finalChapters = [];
|
|
2735
3209
|
for (const chapter of source.chapters) {
|
|
2736
3210
|
const sceneIds = [];
|
|
3211
|
+
const finalSceneIds = [];
|
|
2737
3212
|
for (const authoredScene of chapter.scenes) {
|
|
2738
3213
|
if (authoredScene.kind === "deconstructed-diff") {
|
|
2739
3214
|
const deconstructedScene = buildDeconstructedManifestScene(
|
|
@@ -2745,6 +3220,16 @@ function applySource(source, defaultScenes, repoRoot) {
|
|
|
2745
3220
|
);
|
|
2746
3221
|
scenes.push(deconstructedScene);
|
|
2747
3222
|
sceneIds.push(deconstructedScene.id);
|
|
3223
|
+
if (authoredScene.steps) {
|
|
3224
|
+
const finalScene = buildWalkthroughManifestScene(
|
|
3225
|
+
{ ...authoredScene, steps: authoredScene.steps },
|
|
3226
|
+
available,
|
|
3227
|
+
resolvedAnchors,
|
|
3228
|
+
connections
|
|
3229
|
+
);
|
|
3230
|
+
finalScenes.push(finalScene);
|
|
3231
|
+
finalSceneIds.push(finalScene.id);
|
|
3232
|
+
}
|
|
2748
3233
|
continue;
|
|
2749
3234
|
}
|
|
2750
3235
|
if (authoredScene.kind === "stacked-diff") {
|
|
@@ -2755,37 +3240,51 @@ function applySource(source, defaultScenes, repoRoot) {
|
|
|
2755
3240
|
);
|
|
2756
3241
|
scenes.push(stackedScene);
|
|
2757
3242
|
sceneIds.push(stackedScene.id);
|
|
3243
|
+
for (const [index, file] of stackedScene.files.entries()) {
|
|
3244
|
+
const endpoint = available.get(file.path);
|
|
3245
|
+
if (!endpoint) continue;
|
|
3246
|
+
const finalScene = { ...endpoint, id: `${stackedScene.id}-final-${index + 1}`, title: file.path };
|
|
3247
|
+
finalScenes.push(finalScene);
|
|
3248
|
+
finalSceneIds.push(finalScene.id);
|
|
3249
|
+
}
|
|
2758
3250
|
continue;
|
|
2759
3251
|
}
|
|
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
|
-
};
|
|
3252
|
+
const scene = buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections);
|
|
2783
3253
|
scenes.push(scene);
|
|
2784
3254
|
sceneIds.push(scene.id);
|
|
3255
|
+
finalScenes.push(scene);
|
|
3256
|
+
finalSceneIds.push(scene.id);
|
|
2785
3257
|
}
|
|
2786
3258
|
chapters.push({ id: chapter.id, title: chapter.title, sceneIds });
|
|
3259
|
+
finalChapters.push({ id: chapter.id, title: chapter.title, sceneIds: finalSceneIds });
|
|
2787
3260
|
}
|
|
2788
|
-
return { scenes, chapters };
|
|
3261
|
+
return { scenes, chapters, finalScenes, finalChapters };
|
|
3262
|
+
}
|
|
3263
|
+
function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections) {
|
|
3264
|
+
return {
|
|
3265
|
+
id: authoredScene.id,
|
|
3266
|
+
kind: "walkthrough",
|
|
3267
|
+
title: authoredScene.title,
|
|
3268
|
+
summary: authoredScene.summary,
|
|
3269
|
+
bullets: authoredScene.bullets,
|
|
3270
|
+
tags: authoredScene.tags,
|
|
3271
|
+
takeaway: authoredScene.takeaway,
|
|
3272
|
+
steps: authoredScene.steps.map((step) => {
|
|
3273
|
+
const focus = requireResolvedAnchor(resolvedAnchors, step.focus);
|
|
3274
|
+
const diff2 = available.get(focus.path);
|
|
3275
|
+
if (!diff2) throw new Error(`Step ${step.id} has no text diff for ${focus.path}.`);
|
|
3276
|
+
return {
|
|
3277
|
+
id: step.id,
|
|
3278
|
+
title: step.title,
|
|
3279
|
+
body: step.body,
|
|
3280
|
+
depth: step.depth,
|
|
3281
|
+
requirement: step.requirement,
|
|
3282
|
+
focus,
|
|
3283
|
+
connection: step.connection ? connections.get(step.connection) : void 0,
|
|
3284
|
+
diff: { ...diff2, id: `${authoredScene.id}-${step.id}` }
|
|
3285
|
+
};
|
|
3286
|
+
})
|
|
3287
|
+
};
|
|
2789
3288
|
}
|
|
2790
3289
|
function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, availableFilePaths = []) {
|
|
2791
3290
|
const compiled = buildDeconstructedScene(repoRoot, source, { baseRef, headRef: targetRef });
|
|
@@ -2951,7 +3450,7 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2951
3450
|
let totalContentBytes = 0;
|
|
2952
3451
|
const files = filePaths.flatMap((filePath) => {
|
|
2953
3452
|
const aliases = /* @__PURE__ */ new Set([filePath]);
|
|
2954
|
-
for (const changes of adjacentChanges) {
|
|
3453
|
+
for (const changes of [...adjacentChanges].reverse()) {
|
|
2955
3454
|
for (const change of changes) {
|
|
2956
3455
|
if (aliases.has(change.path) || change.previousPath && aliases.has(change.previousPath)) {
|
|
2957
3456
|
aliases.add(change.path);
|
|
@@ -2962,7 +3461,9 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2962
3461
|
let fileContentBytes = 0;
|
|
2963
3462
|
const panels = [];
|
|
2964
3463
|
for (const entry of stack) {
|
|
2965
|
-
const
|
|
3464
|
+
const candidates = [...aliases].filter((candidate) => gitBlobExists(repoRoot, entry.oid, candidate));
|
|
3465
|
+
if (candidates.length > 1) throw new Error(`Ambiguous revision paths for ${filePath} at ${entry.ref}: ${candidates.join(", ")}`);
|
|
3466
|
+
const resolvedPath = candidates[0];
|
|
2966
3467
|
if (!resolvedPath) {
|
|
2967
3468
|
panels.push({ id: entry.id, label: `${entry.label} / ${filePath} (missing)`, content: "", exists: false });
|
|
2968
3469
|
continue;
|
|
@@ -2989,6 +3490,7 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
2989
3490
|
}
|
|
2990
3491
|
return [{ path: filePath, panels }];
|
|
2991
3492
|
});
|
|
3493
|
+
if (files.length === 0) throw new Error(`Stacked scene ${source.id} has no materializable text files.`);
|
|
2992
3494
|
const stackIds = stack.map((entry) => entry.id);
|
|
2993
3495
|
return {
|
|
2994
3496
|
id: source.id,
|
|
@@ -3000,7 +3502,14 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
3000
3502
|
takeaway: source.takeaway,
|
|
3001
3503
|
stack,
|
|
3002
3504
|
files,
|
|
3003
|
-
steps: source.steps.map((
|
|
3505
|
+
steps: source.steps.length === 0 ? stack.slice(0, -1).map((_, pairIndex) => ({
|
|
3506
|
+
id: `${source.id}-revision-${pairIndex}`,
|
|
3507
|
+
title: "",
|
|
3508
|
+
body: "",
|
|
3509
|
+
file: files[0].path,
|
|
3510
|
+
pairIndex,
|
|
3511
|
+
side: "right"
|
|
3512
|
+
})) : source.steps.map((step) => {
|
|
3004
3513
|
var _a, _b;
|
|
3005
3514
|
return {
|
|
3006
3515
|
id: step.id,
|
|
@@ -3112,21 +3621,51 @@ function buildChapters(records, sceneIdByPath) {
|
|
|
3112
3621
|
}
|
|
3113
3622
|
function chapterForPath(filePath) {
|
|
3114
3623
|
const lower = filePath.toLowerCase();
|
|
3115
|
-
const
|
|
3116
|
-
if (
|
|
3624
|
+
const role = classifyChangeFileRole(filePath);
|
|
3625
|
+
if (role === "documentation" || lower.includes("architecture")) {
|
|
3117
3626
|
return CHAPTERS.context;
|
|
3118
3627
|
}
|
|
3119
|
-
if (
|
|
3628
|
+
if (role === "test") {
|
|
3120
3629
|
return CHAPTERS.proof;
|
|
3121
3630
|
}
|
|
3122
3631
|
if (/(^|\/)(models?|schema|types?|contracts?)\.[^/]+$/.test(lower)) {
|
|
3123
3632
|
return CHAPTERS.contracts;
|
|
3124
3633
|
}
|
|
3125
|
-
if (
|
|
3634
|
+
if (role === "dependency" || role === "generated") {
|
|
3126
3635
|
return CHAPTERS.packaging;
|
|
3127
3636
|
}
|
|
3128
3637
|
return CHAPTERS.behavior;
|
|
3129
3638
|
}
|
|
3639
|
+
function buildGeneratedTourLanding(scenes, chapters, attention, summary) {
|
|
3640
|
+
const entry = attention.entryPath ? `Start here: ${attention.entryPath} \u2014 ${attention.entryReason}` : `No single entry-point \u2014 ${attention.entryReason}`;
|
|
3641
|
+
const focus = attention.focusPaths.length > 0 ? `Pay attention: ${attention.focusPaths.join(", ")}` : "Pay attention: no additional production files were identified.";
|
|
3642
|
+
const skipped = attention.backgroundPaths.length + attention.mechanicalPaths.length;
|
|
3643
|
+
const overview = {
|
|
3644
|
+
id: "change-overview",
|
|
3645
|
+
kind: "discussion",
|
|
3646
|
+
title: "Change overview",
|
|
3647
|
+
summary: `${summary.changedFiles} files (${summary.changedFiles - summary.binaryFiles} text, ${summary.binaryFiles} binary) \xB7 +${summary.additions} \u2212${summary.deletions} \xB7 ${summary.commitCount} commits`,
|
|
3648
|
+
bullets: [entry, focus, `Usually skip on a first pass: ${skipped} file${skipped === 1 ? "" : "s"}. All files remain available.`],
|
|
3649
|
+
tags: ["start here", "first pass"],
|
|
3650
|
+
takeaway: attention.entryPath ? `Next: open ${attention.entryPath}.` : "Next: open the first available textual change."
|
|
3651
|
+
};
|
|
3652
|
+
return {
|
|
3653
|
+
scenes: [overview, ...scenes],
|
|
3654
|
+
chapters: [
|
|
3655
|
+
{ id: "start-here", title: "Start here", sceneIds: [overview.id] },
|
|
3656
|
+
...chapters
|
|
3657
|
+
]
|
|
3658
|
+
};
|
|
3659
|
+
}
|
|
3660
|
+
function findDeclarationNames(content) {
|
|
3661
|
+
const names = /* @__PURE__ */ new Set();
|
|
3662
|
+
for (const line of content.split("\n")) {
|
|
3663
|
+
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*)/);
|
|
3664
|
+
if (match) names.add(match[1]);
|
|
3665
|
+
if (names.size >= 25) break;
|
|
3666
|
+
}
|
|
3667
|
+
return [...names];
|
|
3668
|
+
}
|
|
3130
3669
|
function buildSceneNote(kind, additions, deletions, previousPath) {
|
|
3131
3670
|
const label = formatChangeKind(kind);
|
|
3132
3671
|
const rename = previousPath ? ` from ${previousPath}` : "";
|
|
@@ -3189,15 +3728,19 @@ function readGitLineStats(repoRoot, baseOid, headOid, filePath, previousPath) {
|
|
|
3189
3728
|
const [additionsText = "0", deletionsText = "0"] = output.trim().split(/\s+/, 3);
|
|
3190
3729
|
return {
|
|
3191
3730
|
additions: Number.parseInt(additionsText, 10) || 0,
|
|
3192
|
-
deletions: Number.parseInt(deletionsText, 10) || 0
|
|
3731
|
+
deletions: Number.parseInt(deletionsText, 10) || 0,
|
|
3732
|
+
binary: additionsText === "-" || deletionsText === "-"
|
|
3193
3733
|
};
|
|
3194
3734
|
}
|
|
3195
3735
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3196
3736
|
0 && (module.exports = {
|
|
3737
|
+
CHANGE_ATTENTION_VERSION,
|
|
3738
|
+
buildChangeAttention,
|
|
3197
3739
|
buildChangeInventory,
|
|
3198
3740
|
buildChangeTourContext,
|
|
3199
3741
|
buildChangeTourManifest,
|
|
3200
3742
|
buildDeconstructedScene,
|
|
3743
|
+
classifyChangeFileRole,
|
|
3201
3744
|
compileDeconstructedScene,
|
|
3202
3745
|
materializeChangeUnits,
|
|
3203
3746
|
parseChangeTourManifest,
|