@davmash/bygone 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +32 -0
- package/bin/bygone.js +8 -1
- package/cli/commandSpec.js +11 -2
- package/cli/completions.js +9 -8
- package/cli/correspondence.js +71 -0
- package/cli/present.js +61 -16
- package/cli/tour.js +8 -1
- package/cli/tourExport.js +1 -1
- package/completions/_bygone +5 -3
- package/completions/bygone +5 -5
- package/completions/bygone.fish +2 -0
- package/docs/change-tour-format.md +60 -6
- package/docs/correspondence.md +259 -0
- package/docs/generating-change-tours.md +3 -0
- package/docs/present.md +3 -0
- package/docs/releasing.md +11 -0
- package/media/diff.worker.js +10 -9
- package/media/webview.css +1 -1
- package/out/changeCorrespondence.js +739 -0
- package/out/changeTour.js +1359 -960
- package/out/gitComparison.js +4 -0
- package/out/standalone-main.js +3268 -1337
- package/out/tourCoverage.js +1852 -1448
- package/package.json +2 -2
- package/schemas/change-tour-source.schema.json +48 -3
- package/web/export-runtime.json +5 -5
- package/web/index.html +12 -0
- package/web/presenter.css +81 -0
- package/web/web-host.js +23 -20
package/out/changeTour.js
CHANGED
|
@@ -45,6 +45,642 @@ __export(changeTour_exports, {
|
|
|
45
45
|
pinTourSource: () => pinTourSource
|
|
46
46
|
});
|
|
47
47
|
module.exports = __toCommonJS(changeTour_exports);
|
|
48
|
+
var import_fs2 = require("fs");
|
|
49
|
+
var import_path = require("path");
|
|
50
|
+
|
|
51
|
+
// src/changeCorrespondence.ts
|
|
52
|
+
var import_crypto = require("crypto");
|
|
53
|
+
var import_child_process = require("child_process");
|
|
54
|
+
var import_fs = require("fs");
|
|
55
|
+
|
|
56
|
+
// node_modules/diff/lib/index.mjs
|
|
57
|
+
function Diff() {
|
|
58
|
+
}
|
|
59
|
+
Diff.prototype = {
|
|
60
|
+
diff: function diff(oldString, newString) {
|
|
61
|
+
var _options$timeout;
|
|
62
|
+
var options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
|
|
63
|
+
var callback = options.callback;
|
|
64
|
+
if (typeof options === "function") {
|
|
65
|
+
callback = options;
|
|
66
|
+
options = {};
|
|
67
|
+
}
|
|
68
|
+
this.options = options;
|
|
69
|
+
var self = this;
|
|
70
|
+
function done(value) {
|
|
71
|
+
if (callback) {
|
|
72
|
+
setTimeout(function() {
|
|
73
|
+
callback(void 0, value);
|
|
74
|
+
}, 0);
|
|
75
|
+
return true;
|
|
76
|
+
} else {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
oldString = this.castInput(oldString);
|
|
81
|
+
newString = this.castInput(newString);
|
|
82
|
+
oldString = this.removeEmpty(this.tokenize(oldString));
|
|
83
|
+
newString = this.removeEmpty(this.tokenize(newString));
|
|
84
|
+
var newLen = newString.length, oldLen = oldString.length;
|
|
85
|
+
var editLength = 1;
|
|
86
|
+
var maxEditLength = newLen + oldLen;
|
|
87
|
+
if (options.maxEditLength) {
|
|
88
|
+
maxEditLength = Math.min(maxEditLength, options.maxEditLength);
|
|
89
|
+
}
|
|
90
|
+
var maxExecutionTime = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : Infinity;
|
|
91
|
+
var abortAfterTimestamp = Date.now() + maxExecutionTime;
|
|
92
|
+
var bestPath = [{
|
|
93
|
+
oldPos: -1,
|
|
94
|
+
lastComponent: void 0
|
|
95
|
+
}];
|
|
96
|
+
var newPos = this.extractCommon(bestPath[0], newString, oldString, 0);
|
|
97
|
+
if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
98
|
+
return done([{
|
|
99
|
+
value: this.join(newString),
|
|
100
|
+
count: newString.length
|
|
101
|
+
}]);
|
|
102
|
+
}
|
|
103
|
+
var minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
|
|
104
|
+
function execEditLength() {
|
|
105
|
+
for (var diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
|
|
106
|
+
var basePath = void 0;
|
|
107
|
+
var removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
|
|
108
|
+
if (removePath) {
|
|
109
|
+
bestPath[diagonalPath - 1] = void 0;
|
|
110
|
+
}
|
|
111
|
+
var canAdd = false;
|
|
112
|
+
if (addPath) {
|
|
113
|
+
var addPathNewPos = addPath.oldPos - diagonalPath;
|
|
114
|
+
canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
|
|
115
|
+
}
|
|
116
|
+
var canRemove = removePath && removePath.oldPos + 1 < oldLen;
|
|
117
|
+
if (!canAdd && !canRemove) {
|
|
118
|
+
bestPath[diagonalPath] = void 0;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (!canRemove || canAdd && removePath.oldPos + 1 < addPath.oldPos) {
|
|
122
|
+
basePath = self.addToPath(addPath, true, void 0, 0);
|
|
123
|
+
} else {
|
|
124
|
+
basePath = self.addToPath(removePath, void 0, true, 1);
|
|
125
|
+
}
|
|
126
|
+
newPos = self.extractCommon(basePath, newString, oldString, diagonalPath);
|
|
127
|
+
if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
128
|
+
return done(buildValues(self, basePath.lastComponent, newString, oldString, self.useLongestToken));
|
|
129
|
+
} else {
|
|
130
|
+
bestPath[diagonalPath] = basePath;
|
|
131
|
+
if (basePath.oldPos + 1 >= oldLen) {
|
|
132
|
+
maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
|
|
133
|
+
}
|
|
134
|
+
if (newPos + 1 >= newLen) {
|
|
135
|
+
minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
editLength++;
|
|
140
|
+
}
|
|
141
|
+
if (callback) {
|
|
142
|
+
(function exec() {
|
|
143
|
+
setTimeout(function() {
|
|
144
|
+
if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
|
|
145
|
+
return callback();
|
|
146
|
+
}
|
|
147
|
+
if (!execEditLength()) {
|
|
148
|
+
exec();
|
|
149
|
+
}
|
|
150
|
+
}, 0);
|
|
151
|
+
})();
|
|
152
|
+
} else {
|
|
153
|
+
while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
|
|
154
|
+
var ret = execEditLength();
|
|
155
|
+
if (ret) {
|
|
156
|
+
return ret;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
addToPath: function addToPath(path, added, removed, oldPosInc) {
|
|
162
|
+
var last = path.lastComponent;
|
|
163
|
+
if (last && last.added === added && last.removed === removed) {
|
|
164
|
+
return {
|
|
165
|
+
oldPos: path.oldPos + oldPosInc,
|
|
166
|
+
lastComponent: {
|
|
167
|
+
count: last.count + 1,
|
|
168
|
+
added,
|
|
169
|
+
removed,
|
|
170
|
+
previousComponent: last.previousComponent
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
} else {
|
|
174
|
+
return {
|
|
175
|
+
oldPos: path.oldPos + oldPosInc,
|
|
176
|
+
lastComponent: {
|
|
177
|
+
count: 1,
|
|
178
|
+
added,
|
|
179
|
+
removed,
|
|
180
|
+
previousComponent: last
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
extractCommon: function extractCommon(basePath, newString, oldString, diagonalPath) {
|
|
186
|
+
var newLen = newString.length, oldLen = oldString.length, oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
|
|
187
|
+
while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newString[newPos + 1], oldString[oldPos + 1])) {
|
|
188
|
+
newPos++;
|
|
189
|
+
oldPos++;
|
|
190
|
+
commonCount++;
|
|
191
|
+
}
|
|
192
|
+
if (commonCount) {
|
|
193
|
+
basePath.lastComponent = {
|
|
194
|
+
count: commonCount,
|
|
195
|
+
previousComponent: basePath.lastComponent
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
basePath.oldPos = oldPos;
|
|
199
|
+
return newPos;
|
|
200
|
+
},
|
|
201
|
+
equals: function equals(left, right) {
|
|
202
|
+
if (this.options.comparator) {
|
|
203
|
+
return this.options.comparator(left, right);
|
|
204
|
+
} else {
|
|
205
|
+
return left === right || this.options.ignoreCase && left.toLowerCase() === right.toLowerCase();
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
removeEmpty: function removeEmpty(array) {
|
|
209
|
+
var ret = [];
|
|
210
|
+
for (var i = 0; i < array.length; i++) {
|
|
211
|
+
if (array[i]) {
|
|
212
|
+
ret.push(array[i]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return ret;
|
|
216
|
+
},
|
|
217
|
+
castInput: function castInput(value) {
|
|
218
|
+
return value;
|
|
219
|
+
},
|
|
220
|
+
tokenize: function tokenize(value) {
|
|
221
|
+
return value.split("");
|
|
222
|
+
},
|
|
223
|
+
join: function join(chars) {
|
|
224
|
+
return chars.join("");
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
function buildValues(diff2, lastComponent, newString, oldString, useLongestToken) {
|
|
228
|
+
var components = [];
|
|
229
|
+
var nextComponent;
|
|
230
|
+
while (lastComponent) {
|
|
231
|
+
components.push(lastComponent);
|
|
232
|
+
nextComponent = lastComponent.previousComponent;
|
|
233
|
+
delete lastComponent.previousComponent;
|
|
234
|
+
lastComponent = nextComponent;
|
|
235
|
+
}
|
|
236
|
+
components.reverse();
|
|
237
|
+
var componentPos = 0, componentLen = components.length, newPos = 0, oldPos = 0;
|
|
238
|
+
for (; componentPos < componentLen; componentPos++) {
|
|
239
|
+
var component = components[componentPos];
|
|
240
|
+
if (!component.removed) {
|
|
241
|
+
if (!component.added && useLongestToken) {
|
|
242
|
+
var value = newString.slice(newPos, newPos + component.count);
|
|
243
|
+
value = value.map(function(value2, i) {
|
|
244
|
+
var oldValue = oldString[oldPos + i];
|
|
245
|
+
return oldValue.length > value2.length ? oldValue : value2;
|
|
246
|
+
});
|
|
247
|
+
component.value = diff2.join(value);
|
|
248
|
+
} else {
|
|
249
|
+
component.value = diff2.join(newString.slice(newPos, newPos + component.count));
|
|
250
|
+
}
|
|
251
|
+
newPos += component.count;
|
|
252
|
+
if (!component.added) {
|
|
253
|
+
oldPos += component.count;
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
component.value = diff2.join(oldString.slice(oldPos, oldPos + component.count));
|
|
257
|
+
oldPos += component.count;
|
|
258
|
+
if (componentPos && components[componentPos - 1].added) {
|
|
259
|
+
var tmp = components[componentPos - 1];
|
|
260
|
+
components[componentPos - 1] = components[componentPos];
|
|
261
|
+
components[componentPos] = tmp;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
var finalComponent = components[componentLen - 1];
|
|
266
|
+
if (componentLen > 1 && typeof finalComponent.value === "string" && (finalComponent.added || finalComponent.removed) && diff2.equals("", finalComponent.value)) {
|
|
267
|
+
components[componentLen - 2].value += finalComponent.value;
|
|
268
|
+
components.pop();
|
|
269
|
+
}
|
|
270
|
+
return components;
|
|
271
|
+
}
|
|
272
|
+
var characterDiff = new Diff();
|
|
273
|
+
function diffChars(oldStr, newStr, options) {
|
|
274
|
+
return characterDiff.diff(oldStr, newStr, options);
|
|
275
|
+
}
|
|
276
|
+
var extendedWordChars = /^[A-Za-z\xC0-\u02C6\u02C8-\u02D7\u02DE-\u02FF\u1E00-\u1EFF]+$/;
|
|
277
|
+
var reWhitespace = /\S/;
|
|
278
|
+
var wordDiff = new Diff();
|
|
279
|
+
wordDiff.equals = function(left, right) {
|
|
280
|
+
if (this.options.ignoreCase) {
|
|
281
|
+
left = left.toLowerCase();
|
|
282
|
+
right = right.toLowerCase();
|
|
283
|
+
}
|
|
284
|
+
return left === right || this.options.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right);
|
|
285
|
+
};
|
|
286
|
+
wordDiff.tokenize = function(value) {
|
|
287
|
+
var tokens = value.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/);
|
|
288
|
+
for (var i = 0; i < tokens.length - 1; i++) {
|
|
289
|
+
if (!tokens[i + 1] && tokens[i + 2] && extendedWordChars.test(tokens[i]) && extendedWordChars.test(tokens[i + 2])) {
|
|
290
|
+
tokens[i] += tokens[i + 2];
|
|
291
|
+
tokens.splice(i + 1, 2);
|
|
292
|
+
i--;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return tokens;
|
|
296
|
+
};
|
|
297
|
+
function diffWordsWithSpace(oldStr, newStr, options) {
|
|
298
|
+
return wordDiff.diff(oldStr, newStr, options);
|
|
299
|
+
}
|
|
300
|
+
var lineDiff = new Diff();
|
|
301
|
+
lineDiff.tokenize = function(value) {
|
|
302
|
+
if (this.options.stripTrailingCr) {
|
|
303
|
+
value = value.replace(/\r\n/g, "\n");
|
|
304
|
+
}
|
|
305
|
+
var retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
|
|
306
|
+
if (!linesAndNewlines[linesAndNewlines.length - 1]) {
|
|
307
|
+
linesAndNewlines.pop();
|
|
308
|
+
}
|
|
309
|
+
for (var i = 0; i < linesAndNewlines.length; i++) {
|
|
310
|
+
var line = linesAndNewlines[i];
|
|
311
|
+
if (i % 2 && !this.options.newlineIsToken) {
|
|
312
|
+
retLines[retLines.length - 1] += line;
|
|
313
|
+
} else {
|
|
314
|
+
if (this.options.ignoreWhitespace) {
|
|
315
|
+
line = line.trim();
|
|
316
|
+
}
|
|
317
|
+
retLines.push(line);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return retLines;
|
|
321
|
+
};
|
|
322
|
+
function diffLines(oldStr, newStr, callback) {
|
|
323
|
+
return lineDiff.diff(oldStr, newStr, callback);
|
|
324
|
+
}
|
|
325
|
+
var sentenceDiff = new Diff();
|
|
326
|
+
sentenceDiff.tokenize = function(value) {
|
|
327
|
+
return value.split(/(\S.+?[.!?])(?=\s+|$)/);
|
|
328
|
+
};
|
|
329
|
+
var cssDiff = new Diff();
|
|
330
|
+
cssDiff.tokenize = function(value) {
|
|
331
|
+
return value.split(/([{}:;,]|\s+)/);
|
|
332
|
+
};
|
|
333
|
+
function _typeof(obj) {
|
|
334
|
+
"@babel/helpers - typeof";
|
|
335
|
+
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
|
|
336
|
+
_typeof = function(obj2) {
|
|
337
|
+
return typeof obj2;
|
|
338
|
+
};
|
|
339
|
+
} else {
|
|
340
|
+
_typeof = function(obj2) {
|
|
341
|
+
return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return _typeof(obj);
|
|
345
|
+
}
|
|
346
|
+
var objectPrototypeToString = Object.prototype.toString;
|
|
347
|
+
var jsonDiff = new Diff();
|
|
348
|
+
jsonDiff.useLongestToken = true;
|
|
349
|
+
jsonDiff.tokenize = lineDiff.tokenize;
|
|
350
|
+
jsonDiff.castInput = function(value) {
|
|
351
|
+
var _this$options = this.options, undefinedReplacement = _this$options.undefinedReplacement, _this$options$stringi = _this$options.stringifyReplacer, stringifyReplacer = _this$options$stringi === void 0 ? function(k, v) {
|
|
352
|
+
return typeof v === "undefined" ? undefinedReplacement : v;
|
|
353
|
+
} : _this$options$stringi;
|
|
354
|
+
return typeof value === "string" ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, " ");
|
|
355
|
+
};
|
|
356
|
+
jsonDiff.equals = function(left, right) {
|
|
357
|
+
return Diff.prototype.equals.call(jsonDiff, left.replace(/,([\r\n])/g, "$1"), right.replace(/,([\r\n])/g, "$1"));
|
|
358
|
+
};
|
|
359
|
+
function canonicalize(obj, stack, replacementStack, replacer, key) {
|
|
360
|
+
stack = stack || [];
|
|
361
|
+
replacementStack = replacementStack || [];
|
|
362
|
+
if (replacer) {
|
|
363
|
+
obj = replacer(key, obj);
|
|
364
|
+
}
|
|
365
|
+
var i;
|
|
366
|
+
for (i = 0; i < stack.length; i += 1) {
|
|
367
|
+
if (stack[i] === obj) {
|
|
368
|
+
return replacementStack[i];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
var canonicalizedObj;
|
|
372
|
+
if ("[object Array]" === objectPrototypeToString.call(obj)) {
|
|
373
|
+
stack.push(obj);
|
|
374
|
+
canonicalizedObj = new Array(obj.length);
|
|
375
|
+
replacementStack.push(canonicalizedObj);
|
|
376
|
+
for (i = 0; i < obj.length; i += 1) {
|
|
377
|
+
canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key);
|
|
378
|
+
}
|
|
379
|
+
stack.pop();
|
|
380
|
+
replacementStack.pop();
|
|
381
|
+
return canonicalizedObj;
|
|
382
|
+
}
|
|
383
|
+
if (obj && obj.toJSON) {
|
|
384
|
+
obj = obj.toJSON();
|
|
385
|
+
}
|
|
386
|
+
if (_typeof(obj) === "object" && obj !== null) {
|
|
387
|
+
stack.push(obj);
|
|
388
|
+
canonicalizedObj = {};
|
|
389
|
+
replacementStack.push(canonicalizedObj);
|
|
390
|
+
var sortedKeys = [], _key;
|
|
391
|
+
for (_key in obj) {
|
|
392
|
+
if (obj.hasOwnProperty(_key)) {
|
|
393
|
+
sortedKeys.push(_key);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
sortedKeys.sort();
|
|
397
|
+
for (i = 0; i < sortedKeys.length; i += 1) {
|
|
398
|
+
_key = sortedKeys[i];
|
|
399
|
+
canonicalizedObj[_key] = canonicalize(obj[_key], stack, replacementStack, replacer, _key);
|
|
400
|
+
}
|
|
401
|
+
stack.pop();
|
|
402
|
+
replacementStack.pop();
|
|
403
|
+
} else {
|
|
404
|
+
canonicalizedObj = obj;
|
|
405
|
+
}
|
|
406
|
+
return canonicalizedObj;
|
|
407
|
+
}
|
|
408
|
+
var arrayDiff = new Diff();
|
|
409
|
+
arrayDiff.tokenize = function(value) {
|
|
410
|
+
return value.slice();
|
|
411
|
+
};
|
|
412
|
+
arrayDiff.join = arrayDiff.removeEmpty = function(value) {
|
|
413
|
+
return value;
|
|
414
|
+
};
|
|
415
|
+
function diffArrays(oldArr, newArr, callback) {
|
|
416
|
+
return arrayDiff.diff(oldArr, newArr, callback);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/changeCorrespondence.ts
|
|
420
|
+
var MAX_BYTES = 16 * 1024 * 1024;
|
|
421
|
+
var MAX_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
422
|
+
var digest = (text) => (0, import_crypto.createHash)("sha256").update(text, "utf8").digest("hex");
|
|
423
|
+
function object(value) {
|
|
424
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid correspondence object.");
|
|
425
|
+
return value;
|
|
426
|
+
}
|
|
427
|
+
function list(value, max = 2e4) {
|
|
428
|
+
if (!Array.isArray(value) || value.length > max) throw new Error("Invalid or over-budget correspondence array.");
|
|
429
|
+
return value;
|
|
430
|
+
}
|
|
431
|
+
function string(value) {
|
|
432
|
+
if (typeof value !== "string" || !value || value.length > 4096) throw new Error("Invalid correspondence string.");
|
|
433
|
+
return value;
|
|
434
|
+
}
|
|
435
|
+
function integer(value) {
|
|
436
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error("Invalid correspondence offset.");
|
|
437
|
+
return value;
|
|
438
|
+
}
|
|
439
|
+
function filePath(value) {
|
|
440
|
+
const name = string(value);
|
|
441
|
+
if (name.startsWith("/") || name.includes("\\") || name.includes("\0") || name.split("/").some((p) => !p || p === "." || p === "..")) {
|
|
442
|
+
throw new Error("Invalid correspondence repository path.");
|
|
443
|
+
}
|
|
444
|
+
return name;
|
|
445
|
+
}
|
|
446
|
+
function hash(value) {
|
|
447
|
+
const result = string(value);
|
|
448
|
+
if (!/^[0-9a-f]{64}$/.test(result)) throw new Error("Invalid correspondence digest.");
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
451
|
+
function status(value) {
|
|
452
|
+
if (value !== "complete" && value !== "partial" && value !== "unavailable") throw new Error("Invalid correspondence analysis status.");
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
function readCorrespondenceJson(file) {
|
|
456
|
+
if ((0, import_fs.statSync)(file).size > MAX_BYTES) throw new Error("Correspondence file exceeds 16 MiB.");
|
|
457
|
+
return JSON.parse((0, import_fs.readFileSync)(file, "utf8"));
|
|
458
|
+
}
|
|
459
|
+
function validateRange(value, expected) {
|
|
460
|
+
const input = object(value);
|
|
461
|
+
for (const key of ["baseOid", "headOid"]) {
|
|
462
|
+
if (input[key] !== expected[key] || !/^[0-9a-f]{40,64}$/.test(expected[key])) {
|
|
463
|
+
throw new Error("Correspondence revisions do not match the selected merge-base/head comparison.");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return { ...expected };
|
|
467
|
+
}
|
|
468
|
+
function validateChangeCorrespondence(repoRoot, input, expected) {
|
|
469
|
+
const data = object(input);
|
|
470
|
+
if (data.version !== 1) throw new Error("Unsupported correspondence version.");
|
|
471
|
+
const range = validateRange(data.range, expected);
|
|
472
|
+
const documents = object(data.documents), symbols = object(data.symbols);
|
|
473
|
+
const result = {
|
|
474
|
+
version: 1,
|
|
475
|
+
range,
|
|
476
|
+
producer: string(data.producer),
|
|
477
|
+
status: status(data.status),
|
|
478
|
+
documents: { before: [], after: [] },
|
|
479
|
+
symbols: { before: [], after: [] },
|
|
480
|
+
relations: [],
|
|
481
|
+
unresolved: [],
|
|
482
|
+
limitations: list(data.limitations, 100).map(string)
|
|
483
|
+
};
|
|
484
|
+
const entries = /* @__PURE__ */ new Map();
|
|
485
|
+
const documentsBySide = { before: /* @__PURE__ */ new Map(), after: /* @__PURE__ */ new Map() };
|
|
486
|
+
const lineIndexes = /* @__PURE__ */ new Map();
|
|
487
|
+
const lineAt = (side, path, start) => {
|
|
488
|
+
const key = `${side}:${path}`;
|
|
489
|
+
let lines = lineIndexes.get(key);
|
|
490
|
+
if (!lines) {
|
|
491
|
+
const text = documentsBySide[side].get(path);
|
|
492
|
+
lines = [0];
|
|
493
|
+
for (let at = text.indexOf("\n"); at !== -1; at = text.indexOf("\n", at + 1)) lines.push(at + 1);
|
|
494
|
+
lineIndexes.set(key, lines);
|
|
495
|
+
}
|
|
496
|
+
let low = 0, high = lines.length;
|
|
497
|
+
while (low < high) {
|
|
498
|
+
const mid = Math.floor((low + high) / 2);
|
|
499
|
+
if (lines[mid] <= start) low = mid + 1;
|
|
500
|
+
else high = mid;
|
|
501
|
+
}
|
|
502
|
+
return low;
|
|
503
|
+
};
|
|
504
|
+
let sourceBytes = 0, symbolBytes = 0;
|
|
505
|
+
for (const side of ["before", "after"]) {
|
|
506
|
+
const texts = /* @__PURE__ */ new Map();
|
|
507
|
+
const oid = side === "before" ? range.baseOid : range.headOid;
|
|
508
|
+
for (const value of list(documents[side], 128)) {
|
|
509
|
+
const doc = object(value), name = filePath(doc.path), expectedDigest = hash(doc.digest);
|
|
510
|
+
const documentStatus = string(doc.status);
|
|
511
|
+
if (!["complete", "invalid", "unsupported"].includes(documentStatus) || texts.has(name)) throw new Error("Invalid or duplicate correspondence document.");
|
|
512
|
+
const bytes = (0, import_child_process.execFileSync)("git", ["show", `${oid}:${name}`], { cwd: repoRoot, timeout: 1e4, maxBuffer: 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
|
|
513
|
+
const text = bytes.toString("utf8");
|
|
514
|
+
sourceBytes += bytes.length;
|
|
515
|
+
if (sourceBytes > MAX_SOURCE_BYTES || !Buffer.from(text, "utf8").equals(bytes) || digest(text) !== expectedDigest) {
|
|
516
|
+
throw new Error(`Correspondence source digest mismatch or source budget exceeded: ${side}/${name}`);
|
|
517
|
+
}
|
|
518
|
+
texts.set(name, { text, status: documentStatus });
|
|
519
|
+
documentsBySide[side].set(name, text);
|
|
520
|
+
result.documents[side].push({ path: name, digest: expectedDigest, status: documentStatus, text });
|
|
521
|
+
}
|
|
522
|
+
for (const value of list(symbols[side])) {
|
|
523
|
+
const entry = object(value), name = filePath(entry.path), doc = texts.get(name);
|
|
524
|
+
const id = string(entry.id), start = integer(entry.start), end = integer(entry.end);
|
|
525
|
+
if (!doc || start >= end || end > doc.text.length || entries.has(id)) throw new Error("Invalid correspondence symbol ownership/range/ID.");
|
|
526
|
+
const text = doc.text.slice(start, end);
|
|
527
|
+
symbolBytes += Buffer.byteLength(text);
|
|
528
|
+
if (symbolBytes > MAX_SOURCE_BYTES || digest(text) !== hash(entry.digest)) throw new Error(`Correspondence symbol digest mismatch or budget exceeded: ${name}`);
|
|
529
|
+
const symbol = {
|
|
530
|
+
id,
|
|
531
|
+
path: name,
|
|
532
|
+
name: string(entry.name),
|
|
533
|
+
kind: string(entry.kind),
|
|
534
|
+
start,
|
|
535
|
+
end,
|
|
536
|
+
digest: digest(text),
|
|
537
|
+
text,
|
|
538
|
+
startLine: lineAt(side, name, start),
|
|
539
|
+
endLine: lineAt(side, name, end - 1)
|
|
540
|
+
};
|
|
541
|
+
entries.set(id, { side, symbol, documentStatus: doc.status });
|
|
542
|
+
result.symbols[side].push(symbol);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const ids = /* @__PURE__ */ new Set(), sources = /* @__PURE__ */ new Set(), destinations = /* @__PURE__ */ new Set();
|
|
546
|
+
for (const value of list(data.relations)) {
|
|
547
|
+
const relation = object(value), id = string(relation.id), sourceId = string(relation.sourceId), destinationId = string(relation.destinationId);
|
|
548
|
+
const source = entries.get(sourceId), destination = entries.get(destinationId);
|
|
549
|
+
if (ids.has(id) || sources.has(sourceId) || destinations.has(destinationId)) throw new Error("Conflicting correspondence continuations; preserve them as unresolved candidates.");
|
|
550
|
+
if ((source == null ? void 0 : source.side) !== "before" || (destination == null ? void 0 : destination.side) !== "after" || source.documentStatus !== "complete" || destination.documentStatus !== "complete") {
|
|
551
|
+
throw new Error("Correspondence endpoints must resolve to valid, parsed before/after documents.");
|
|
552
|
+
}
|
|
553
|
+
ids.add(id);
|
|
554
|
+
sources.add(sourceId);
|
|
555
|
+
destinations.add(destinationId);
|
|
556
|
+
const producers = list(relation.producers, 10).map(string);
|
|
557
|
+
if (relation.coverage !== void 0 && relation.coverage !== "complete") throw new Error("Invalid correspondence relation coverage.");
|
|
558
|
+
const covered = result.status === "partial" && relation.coverage === "complete" && producers.some((producer) => ["refactoring-miner", "bygone-strand"].includes(producer));
|
|
559
|
+
if (result.status !== "complete" && !covered && !(producers.includes("exact-declaration") && source.symbol.text === destination.symbol.text)) continue;
|
|
560
|
+
const a = source.symbol, b = destination.symbol;
|
|
561
|
+
result.relations.push({
|
|
562
|
+
id,
|
|
563
|
+
sourceId,
|
|
564
|
+
destinationId,
|
|
565
|
+
producers,
|
|
566
|
+
pathChanged: a.path !== b.path,
|
|
567
|
+
nameChanged: a.name !== b.name,
|
|
568
|
+
...relation.coverage === "complete" ? { coverage: "complete" } : {},
|
|
569
|
+
rawEqual: a.text === b.text,
|
|
570
|
+
behavior: "unassessed",
|
|
571
|
+
localDiff: (diffLines(a.text, b.text, { timeout: 100, maxEditLength: 2e3 }) || [{ value: a.text, removed: true }, { value: b.text, added: true }]).map((part) => ({
|
|
572
|
+
value: part.value,
|
|
573
|
+
...part.added ? { added: true } : {},
|
|
574
|
+
...part.removed ? { removed: true } : {}
|
|
575
|
+
}))
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
for (const value of list(data.unresolved)) {
|
|
579
|
+
const candidate = object(value);
|
|
580
|
+
const sourceIds = list(candidate.sourceIds).map(string), destinationIds = list(candidate.destinationIds).map(string);
|
|
581
|
+
if (!sourceIds.length && !destinationIds.length) throw new Error("Unresolved correspondence needs candidate endpoints.");
|
|
582
|
+
if (sourceIds.some((id) => {
|
|
583
|
+
var _a;
|
|
584
|
+
return ((_a = entries.get(id)) == null ? void 0 : _a.side) !== "before";
|
|
585
|
+
}) || destinationIds.some((id) => {
|
|
586
|
+
var _a;
|
|
587
|
+
return ((_a = entries.get(id)) == null ? void 0 : _a.side) !== "after";
|
|
588
|
+
})) {
|
|
589
|
+
throw new Error("Unresolved correspondence references unknown endpoints.");
|
|
590
|
+
}
|
|
591
|
+
if (sourceIds.some((id) => sources.has(id)) || destinationIds.some((id) => destinations.has(id))) throw new Error("Ambiguous endpoints cannot also be accepted continuations.");
|
|
592
|
+
result.unresolved.push({ reason: string(candidate.reason), sourceIds, destinationIds });
|
|
593
|
+
}
|
|
594
|
+
if (data.diagnostics !== void 0) {
|
|
595
|
+
const diagnostics = object(data.diagnostics);
|
|
596
|
+
if (diagnostics.version !== 1) throw new Error("Unsupported correspondence diagnostics version.");
|
|
597
|
+
let locationCount = 0;
|
|
598
|
+
result.diagnostics = { version: 1, scope: list(diagnostics.scope, 20).map(string), records: list(diagnostics.records).map((value) => {
|
|
599
|
+
const record2 = object(value), category = string(record2.category);
|
|
600
|
+
if (!["outside", "syntax", "rejected", "unmatched", "relationship", "problem"].includes(category)) throw new Error("Invalid correspondence diagnostic category.");
|
|
601
|
+
return {
|
|
602
|
+
category,
|
|
603
|
+
reason: string(record2.reason),
|
|
604
|
+
...record2.message !== void 0 ? { message: string(record2.message) } : {},
|
|
605
|
+
...record2.producerId !== void 0 ? { producerId: string(record2.producerId) } : {},
|
|
606
|
+
locations: list(record2.locations, 100).map((value2) => {
|
|
607
|
+
var _a, _b;
|
|
608
|
+
if (++locationCount > 4e4) throw new Error("Correspondence diagnostic location budget exceeded.");
|
|
609
|
+
const location = object(value2), path = filePath(location.path);
|
|
610
|
+
const side = location.side;
|
|
611
|
+
if (side !== void 0 && side !== "before" && side !== "after") throw new Error("Invalid diagnostic revision side.");
|
|
612
|
+
const symbolId = location.symbolId === void 0 ? void 0 : string(location.symbolId);
|
|
613
|
+
if (symbolId && (((_a = entries.get(symbolId)) == null ? void 0 : _a.side) !== side || ((_b = entries.get(symbolId)) == null ? void 0 : _b.symbol.path) !== path)) throw new Error("Invalid diagnostic symbol reference.");
|
|
614
|
+
const start = location.start === void 0 ? symbolId ? entries.get(symbolId).symbol.start : void 0 : integer(location.start);
|
|
615
|
+
const text = side ? documentsBySide[side].get(path) : void 0;
|
|
616
|
+
if (start !== void 0 && (text === void 0 || start > text.length)) throw new Error("Invalid diagnostic source location.");
|
|
617
|
+
return {
|
|
618
|
+
path,
|
|
619
|
+
...side ? { side } : {},
|
|
620
|
+
...symbolId ? { symbolId } : {},
|
|
621
|
+
...start !== void 0 ? { start, line: lineAt(side, path, start) } : {},
|
|
622
|
+
...location.reason !== void 0 ? { reason: string(location.reason) } : {}
|
|
623
|
+
};
|
|
624
|
+
})
|
|
625
|
+
};
|
|
626
|
+
}) };
|
|
627
|
+
}
|
|
628
|
+
return result;
|
|
629
|
+
}
|
|
630
|
+
function loadChangeCorrespondence(repoRoot, file, range) {
|
|
631
|
+
return validateChangeCorrespondence(repoRoot, readCorrespondenceJson(file), range);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/tourSymbol.ts
|
|
635
|
+
function compileTourSymbol(evidence, selector, focus) {
|
|
636
|
+
let before, after;
|
|
637
|
+
let status2 = "inferred";
|
|
638
|
+
if (selector.relation) {
|
|
639
|
+
const relation = evidence.relations.find((item) => item.id === selector.relation);
|
|
640
|
+
if (!relation) throw new Error(`No accepted correspondence: ${selector.relation}`);
|
|
641
|
+
before = evidence.symbols.before.find((item) => item.id === relation.sourceId);
|
|
642
|
+
after = evidence.symbols.after.find((item) => item.id === relation.destinationId);
|
|
643
|
+
} else {
|
|
644
|
+
const id = selector.symbol;
|
|
645
|
+
if (evidence.relations.some((item) => item.sourceId === id || item.destinationId === id)) throw new Error("Matched symbols must use their accepted relation ID.");
|
|
646
|
+
before = evidence.symbols.before.find((item) => item.id === id);
|
|
647
|
+
after = evidence.symbols.after.find((item) => item.id === id);
|
|
648
|
+
if (!before && !after) throw new Error(`Unknown correspondence symbol: ${id}`);
|
|
649
|
+
status2 = evidence.unresolved.some((group) => [...group.sourceIds, ...group.destinationIds].includes(id)) ? "ambiguous" : "unmatched";
|
|
650
|
+
}
|
|
651
|
+
const endpoint = focus.revision === "base" ? before : after;
|
|
652
|
+
if (!endpoint || endpoint.path !== focus.path || focus.startLine < endpoint.startLine || focus.endLine > endpoint.endLine) throw new Error("Symbol stop focus must lie within the selected declaration on its revision side.");
|
|
653
|
+
const text = (side, entry) => entry ? evidence.documents[side].find((doc) => doc.path === entry.path).text : "";
|
|
654
|
+
const result = {
|
|
655
|
+
id: selector.relation || selector.symbol,
|
|
656
|
+
range: evidence.range,
|
|
657
|
+
status: status2,
|
|
658
|
+
before,
|
|
659
|
+
after,
|
|
660
|
+
context: { leftContent: text("before", before), rightContent: text("after", after) },
|
|
661
|
+
behavior: "unassessed"
|
|
662
|
+
};
|
|
663
|
+
validateTourSymbol(result);
|
|
664
|
+
return result;
|
|
665
|
+
}
|
|
666
|
+
function validateTourSymbol(value) {
|
|
667
|
+
const fail = () => {
|
|
668
|
+
throw new Error("Invalid pinned symbol tour evidence.");
|
|
669
|
+
};
|
|
670
|
+
if (!value || typeof value !== "object") fail();
|
|
671
|
+
const data = value;
|
|
672
|
+
if (typeof data.id !== "string" || !data.id.trim() || !data.range || ![data.range.baseOid, data.range.headOid].every((oid) => typeof oid === "string" && /^[0-9a-f]{40,64}$/.test(oid)) || !["inferred", "unmatched", "ambiguous"].includes(data.status) || data.behavior !== "unassessed" || !data.context) fail();
|
|
673
|
+
const count = Number(!!data.before) + Number(!!data.after);
|
|
674
|
+
if (count !== (data.status === "inferred" ? 2 : 1)) fail();
|
|
675
|
+
for (const [entry, content] of [[data.before, data.context.leftContent], [data.after, data.context.rightContent]]) {
|
|
676
|
+
if (typeof content !== "string") fail();
|
|
677
|
+
if (!entry) {
|
|
678
|
+
if (content !== "") fail();
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (typeof entry.id !== "string" || !entry.id || typeof entry.name !== "string" || !entry.name || typeof entry.kind !== "string" || !entry.kind || typeof entry.path !== "string" || entry.path.includes("\0") || entry.path.startsWith("/") || entry.path.includes("\\") || entry.path.split("/").some((part) => !part || part === "." || part === "..") || typeof entry.digest !== "string" || !/^[0-9a-f]{64}$/.test(entry.digest) || !Number.isSafeInteger(entry.start) || !Number.isSafeInteger(entry.end) || entry.start < 0 || entry.end <= entry.start || entry.end > content.length || content.slice(entry.start, entry.end) !== entry.text || entry.startLine !== content.slice(0, entry.start).split("\n").length || entry.endLine !== content.slice(0, entry.end - 1).split("\n").length) fail();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
48
684
|
|
|
49
685
|
// src/tourImage.ts
|
|
50
686
|
var MAX_TOUR_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
@@ -84,17 +720,19 @@ function validatePngHeader(bytes, label) {
|
|
|
84
720
|
}
|
|
85
721
|
|
|
86
722
|
// src/changeTour.ts
|
|
87
|
-
var
|
|
723
|
+
var import_child_process5 = require("child_process");
|
|
88
724
|
|
|
89
725
|
// src/gitComparison.ts
|
|
90
|
-
var
|
|
726
|
+
var import_child_process2 = require("child_process");
|
|
91
727
|
var fs = __toESM(require("fs"));
|
|
92
728
|
var DEFAULT_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
729
|
+
var DEFAULT_GIT_TIMEOUT_MS = 3e4;
|
|
93
730
|
function createGitRunner(maxBuffer = readGitMaxBuffer()) {
|
|
94
|
-
return (args, cwd) => (0,
|
|
731
|
+
return (args, cwd) => (0, import_child_process2.execFileSync)("git", [...args], {
|
|
95
732
|
cwd,
|
|
96
733
|
encoding: "utf8",
|
|
97
734
|
maxBuffer,
|
|
735
|
+
timeout: DEFAULT_GIT_TIMEOUT_MS,
|
|
98
736
|
stdio: ["ignore", "pipe", "pipe"]
|
|
99
737
|
}).trimEnd();
|
|
100
738
|
}
|
|
@@ -161,12 +799,12 @@ function parseNameStatusZ(output) {
|
|
|
161
799
|
}
|
|
162
800
|
const changedPaths = [];
|
|
163
801
|
for (let index = 0; index < fields.length; ) {
|
|
164
|
-
const
|
|
165
|
-
if (!
|
|
802
|
+
const status2 = fields[index++];
|
|
803
|
+
if (!status2) {
|
|
166
804
|
continue;
|
|
167
805
|
}
|
|
168
|
-
const code =
|
|
169
|
-
const similarityText =
|
|
806
|
+
const code = status2[0];
|
|
807
|
+
const similarityText = status2.slice(1);
|
|
170
808
|
const similarity = similarityText ? Number.parseInt(similarityText, 10) : void 0;
|
|
171
809
|
if (code === "R" || code === "C") {
|
|
172
810
|
const previousPath = fields[index++];
|
|
@@ -364,7 +1002,7 @@ function requireOnlyKeys(value, allowed, path) {
|
|
|
364
1002
|
}
|
|
365
1003
|
|
|
366
1004
|
// src/changeTourSource.ts
|
|
367
|
-
var CHANGE_TOUR_SOURCE_VERSION =
|
|
1005
|
+
var CHANGE_TOUR_SOURCE_VERSION = 6;
|
|
368
1006
|
function parseChangeTourSource(value) {
|
|
369
1007
|
if (isRecord2(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_SOURCE_VERSION) {
|
|
370
1008
|
throw new Error(
|
|
@@ -374,7 +1012,7 @@ function parseChangeTourSource(value) {
|
|
|
374
1012
|
if (isRecord2(value) && value.version === 3) {
|
|
375
1013
|
throw new Error("Tour source format version 3 is retired. Remove the review block, set source version to 4, and recompile the tour.");
|
|
376
1014
|
}
|
|
377
|
-
if (!isRecord2(value) || value.version !== 1 && value.version !== 2 && value.version !== 4 && value.version !== CHANGE_TOUR_SOURCE_VERSION) {
|
|
1015
|
+
if (!isRecord2(value) || value.version !== 1 && value.version !== 2 && value.version !== 4 && value.version !== 5 && value.version !== CHANGE_TOUR_SOURCE_VERSION) {
|
|
378
1016
|
throw new Error("Unsupported or missing change-tour source version.");
|
|
379
1017
|
}
|
|
380
1018
|
requireOnlyKeys2(value, [
|
|
@@ -382,6 +1020,7 @@ function parseChangeTourSource(value) {
|
|
|
382
1020
|
"title",
|
|
383
1021
|
"windowTitle",
|
|
384
1022
|
"sourceUrl",
|
|
1023
|
+
"correspondence",
|
|
385
1024
|
"range",
|
|
386
1025
|
"anchors",
|
|
387
1026
|
"connections",
|
|
@@ -433,6 +1072,11 @@ function parseChangeTourSource(value) {
|
|
|
433
1072
|
throw new Error(`anchors.${id}.occurrence must be a positive integer.`);
|
|
434
1073
|
}
|
|
435
1074
|
}
|
|
1075
|
+
if (value.correspondence !== void 0) {
|
|
1076
|
+
if (value.version < 6) throw new Error("correspondence requires source version 6.");
|
|
1077
|
+
requireString2(value.correspondence, "correspondence");
|
|
1078
|
+
if (value.correspondence.startsWith("/") || value.correspondence.includes("\\") || value.correspondence.includes("\0") || value.correspondence.split("/").some((part) => !part || part === "." || part === "..")) throw new Error("correspondence must be a repository-relative file path.");
|
|
1079
|
+
}
|
|
436
1080
|
const rawConnections = Array.isArray(value.connections) ? value.connections : isRecord2(value.connections) ? Object.entries(value.connections).map(([id, connection]) => isRecord2(connection) ? { id, ...connection } : connection) : null;
|
|
437
1081
|
if (!rawConnections || !Array.isArray(value.chapters) || value.chapters.length === 0) {
|
|
438
1082
|
throw new Error("connections must be an array or object, and chapters must be a non-empty array.");
|
|
@@ -536,7 +1180,7 @@ function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, c
|
|
|
536
1180
|
validateRealStack(scene.stack, `${path}.stack`, 2);
|
|
537
1181
|
}
|
|
538
1182
|
if (requireRealEvidence || scene.steps !== void 0) {
|
|
539
|
-
validateWalkthroughSteps(scene, path, anchors, connectionIds);
|
|
1183
|
+
validateWalkthroughSteps(scene, path, anchors, connectionIds, version);
|
|
540
1184
|
}
|
|
541
1185
|
continue;
|
|
542
1186
|
}
|
|
@@ -555,7 +1199,7 @@ function validateSourceChapters(chaptersValue, chaptersPath, version, anchors, c
|
|
|
555
1199
|
}
|
|
556
1200
|
requireOnlyKeys2(scene, ["id", "kind", "title", "summary", "bullets", "tags", "takeaway", "overview", "steps"], path);
|
|
557
1201
|
validateSceneOverview(scene.overview, `${path}.overview`, { allowComparison: false });
|
|
558
|
-
validateWalkthroughSteps(scene, path, anchors, connectionIds);
|
|
1202
|
+
validateWalkthroughSteps(scene, path, anchors, connectionIds, version);
|
|
559
1203
|
}
|
|
560
1204
|
}
|
|
561
1205
|
if (mode === "deconstructed" && !chaptersValue.some((chapter) => isRecord2(chapter) && Array.isArray(chapter.scenes) && chapter.scenes.some((scene) => isRecord2(scene) && scene.kind === "deconstructed-diff"))) {
|
|
@@ -612,7 +1256,7 @@ function validateDeconstructedScene(scene, path) {
|
|
|
612
1256
|
));
|
|
613
1257
|
}
|
|
614
1258
|
}
|
|
615
|
-
function validateWalkthroughSteps(scene, path, anchors, connectionIds) {
|
|
1259
|
+
function validateWalkthroughSteps(scene, path, anchors, connectionIds, version) {
|
|
616
1260
|
if (!Array.isArray(scene.steps) || scene.steps.length === 0) {
|
|
617
1261
|
throw new Error(`${path} must contain a non-empty steps array.`);
|
|
618
1262
|
}
|
|
@@ -624,10 +1268,17 @@ function validateWalkthroughSteps(scene, path, anchors, connectionIds) {
|
|
|
624
1268
|
requireString2(step.title, `${stepPath}.title`);
|
|
625
1269
|
requireString2(step.body, `${stepPath}.body`);
|
|
626
1270
|
requireString2(step.focus, `${stepPath}.focus`);
|
|
627
|
-
requireOnlyKeys2(step, ["id", "title", "body", "focus", "connection", "depth", "requirement", "image"], stepPath);
|
|
1271
|
+
requireOnlyKeys2(step, ["id", "title", "body", "focus", "connection", "depth", "requirement", "image", "symbol"], stepPath);
|
|
628
1272
|
if (step.depth !== void 0 && !["mentioned", "explained", "contextualized"].includes(String(step.depth))) {
|
|
629
1273
|
throw new Error(`${stepPath}.depth must be mentioned, explained, or contextualized.`);
|
|
630
1274
|
}
|
|
1275
|
+
if (step.symbol !== void 0) {
|
|
1276
|
+
if (version < 6 || !isRecord2(step.symbol)) throw new Error(`${stepPath}.symbol requires an object and source version 6.`);
|
|
1277
|
+
requireOnlyKeys2(step.symbol, ["relation", "symbol"], `${stepPath}.symbol`);
|
|
1278
|
+
if (step.symbol.relation === void 0 === (step.symbol.symbol === void 0)) throw new Error(`${stepPath}.symbol requires exactly one relation or symbol ID.`);
|
|
1279
|
+
requireString2(step.symbol.relation ?? step.symbol.symbol, `${stepPath}.symbol ID`);
|
|
1280
|
+
if (step.image || step.connection) throw new Error(`${stepPath}.symbol cannot combine with image or connection.`);
|
|
1281
|
+
}
|
|
631
1282
|
validateStepRequirement(step.requirement, `${stepPath}.requirement`);
|
|
632
1283
|
validateTourImageSource(step.image, `${stepPath}.image`);
|
|
633
1284
|
if (stepIds.has(step.id)) throw new Error(`Duplicate step id in scene ${scene.id}: ${step.id}`);
|
|
@@ -798,7 +1449,7 @@ function requireOnlyKeys2(value, allowed, path) {
|
|
|
798
1449
|
}
|
|
799
1450
|
|
|
800
1451
|
// src/changeTourManifest.ts
|
|
801
|
-
var CHANGE_TOUR_MANIFEST_VERSION =
|
|
1452
|
+
var CHANGE_TOUR_MANIFEST_VERSION = 6;
|
|
802
1453
|
function parseChangeTourManifest(value, options = {}) {
|
|
803
1454
|
if (isRecord3(value) && Number.isInteger(value.version) && Number(value.version) > CHANGE_TOUR_MANIFEST_VERSION) {
|
|
804
1455
|
throw new Error(
|
|
@@ -808,7 +1459,7 @@ function parseChangeTourManifest(value, options = {}) {
|
|
|
808
1459
|
if (isRecord3(value) && value.version === 3) {
|
|
809
1460
|
throw new Error("Tour manifest format version 3 is retired. Remove the review block, set source version to 4, and recompile the tour.");
|
|
810
1461
|
}
|
|
811
|
-
if (!isRecord3(value) || value.version !== 1 && value.version !== 2 && value.version !== 4 && value.version !== CHANGE_TOUR_MANIFEST_VERSION) {
|
|
1462
|
+
if (!isRecord3(value) || value.version !== 1 && value.version !== 2 && value.version !== 4 && value.version !== 5 && value.version !== CHANGE_TOUR_MANIFEST_VERSION) {
|
|
812
1463
|
throw new Error(`Unsupported or missing change-tour manifest version.`);
|
|
813
1464
|
}
|
|
814
1465
|
if ("review" in value) throw new Error("Change-tour manifests do not support the review field. Put observations in scene and step narrative.");
|
|
@@ -866,7 +1517,7 @@ function parseChangeTourManifest(value, options = {}) {
|
|
|
866
1517
|
throw new Error("summary.includedScenes must match the number of scenes.");
|
|
867
1518
|
}
|
|
868
1519
|
if (value.details !== void 0) {
|
|
869
|
-
if (value.version
|
|
1520
|
+
if (value.version < 5) throw new Error("details requires manifest version 5.");
|
|
870
1521
|
validateChangeTourDetails(value.details, "details");
|
|
871
1522
|
}
|
|
872
1523
|
const files = value.files === void 0 ? collectLegacyTourFiles(value.scenes) : value.files.map((candidate, index) => {
|
|
@@ -880,10 +1531,24 @@ function parseChangeTourManifest(value, options = {}) {
|
|
|
880
1531
|
validateAuthoringCoverage(value.authoringCoverage, value.version);
|
|
881
1532
|
}
|
|
882
1533
|
if (value.tours !== void 0) {
|
|
883
|
-
if (value.version !== 4 && value.version !== 5) throw new Error("Independent tours require manifest version 4.");
|
|
1534
|
+
if (value.version !== 4 && value.version !== 5 && value.version !== 6) throw new Error("Independent tours require manifest version 4.");
|
|
884
1535
|
validateModeTours(value.tours, value.version);
|
|
885
1536
|
}
|
|
886
1537
|
if (value.version !== 1) validateZoom(value, value.version, Boolean(options.exported));
|
|
1538
|
+
const symbolScenes = [
|
|
1539
|
+
value.scenes,
|
|
1540
|
+
...isRecord3(value.zoom) && isRecord3(value.zoom.final) ? [value.zoom.final.scenes] : [],
|
|
1541
|
+
...isRecord3(value.tours) ? Object.values(value.tours).flatMap((tour) => isRecord3(tour) ? [tour.scenes] : []) : []
|
|
1542
|
+
];
|
|
1543
|
+
for (const scenes of symbolScenes) for (const scene of scenes) {
|
|
1544
|
+
if (scene.kind !== "walkthrough") continue;
|
|
1545
|
+
for (const step of scene.steps) if (step.symbol) {
|
|
1546
|
+
const symbol = step.symbol;
|
|
1547
|
+
if (symbol.range.baseOid !== value.range.mergeBaseOid || symbol.range.headOid !== value.range.headOid) throw new Error("Symbol tour evidence revisions do not match the tour.");
|
|
1548
|
+
const endpoint = step.focus.revision === "base" ? symbol.before : symbol.after;
|
|
1549
|
+
if (!endpoint || endpoint.path !== step.focus.path || step.focus.startLine < endpoint.startLine || step.focus.endLine > endpoint.endLine) throw new Error("Symbol stop focus must lie within its selected declaration.");
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
887
1552
|
return { ...value, files };
|
|
888
1553
|
}
|
|
889
1554
|
function validateAuthoringCoverage(value, version) {
|
|
@@ -940,7 +1605,7 @@ function validateModeTours(value, version) {
|
|
|
940
1605
|
}
|
|
941
1606
|
}
|
|
942
1607
|
if (tour.details !== void 0) {
|
|
943
|
-
if (version
|
|
1608
|
+
if (version < 5) throw new Error(`tours.${mode}.details requires manifest version 5.`);
|
|
944
1609
|
validateChangeTourDetails(tour.details, `tours.${mode}.details`);
|
|
945
1610
|
}
|
|
946
1611
|
if (mode === "deconstructed" && !hasDeconstructed) {
|
|
@@ -1086,6 +1751,11 @@ function validateScene(value, index, version) {
|
|
|
1086
1751
|
}
|
|
1087
1752
|
validateStepRequirement(step.requirement, `${path}.requirement`);
|
|
1088
1753
|
validateTourImage(step.image, `${path}.image`);
|
|
1754
|
+
if (step.symbol !== void 0) {
|
|
1755
|
+
if (version < 6) throw new Error(`${path}.symbol requires manifest version 6.`);
|
|
1756
|
+
if (step.image || step.connection) throw new Error(`${path}.symbol cannot combine with image or connection.`);
|
|
1757
|
+
validateTourSymbol(step.symbol);
|
|
1758
|
+
}
|
|
1089
1759
|
validateResolvedAnchor(step.focus, `${path}.focus`);
|
|
1090
1760
|
validateScene(step.diff, index, version);
|
|
1091
1761
|
if (step.diff.kind !== "text-diff") throw new Error(`${path}.diff must be a text-diff scene.`);
|
|
@@ -1120,958 +1790,598 @@ function validateScene(value, index, version) {
|
|
|
1120
1790
|
if (value.previousPath !== void 0) {
|
|
1121
1791
|
requireString3(value.previousPath, `scenes[${index}].previousPath`);
|
|
1122
1792
|
}
|
|
1123
|
-
requireString3(value.changeKind, `scenes[${index}].changeKind`);
|
|
1124
|
-
requireNonNegativeInteger(value.additions, `scenes[${index}].additions`);
|
|
1125
|
-
requireNonNegativeInteger(value.deletions, `scenes[${index}].deletions`);
|
|
1126
|
-
if (value.focusChangeIndex !== void 0) {
|
|
1127
|
-
requireNonNegativeInteger(value.focusChangeIndex, `scenes[${index}].focusChangeIndex`);
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
function validateStackedScene2(value, index) {
|
|
1131
|
-
if (!Array.isArray(value.stack) || value.stack.length < 2 || value.stack.length > 6) {
|
|
1132
|
-
throw new Error(`scenes[${index}].stack must contain between 2 and 6 revisions.`);
|
|
1133
|
-
}
|
|
1134
|
-
for (const [panelIndex, panel] of value.stack.entries()) {
|
|
1135
|
-
if (!isRecord3(panel)) throw new Error(`scenes[${index}].stack[${panelIndex}] must be an object.`);
|
|
1136
|
-
for (const key of ["id", "ref", "oid", "label"]) requireString3(panel[key], `scenes[${index}].stack[${panelIndex}].${key}`);
|
|
1137
|
-
}
|
|
1138
|
-
validateMultiPanelFiles(value.files, value.stack.length, index);
|
|
1139
|
-
validateMultiPanelSteps(value.steps, value.stack.length, index, false);
|
|
1140
|
-
}
|
|
1141
|
-
function validateDeconstructedScene2(value, index) {
|
|
1142
|
-
if (value.stageLabel !== "Explanation stages") {
|
|
1143
|
-
throw new Error(`scenes[${index}].stageLabel must be Explanation stages.`);
|
|
1144
|
-
}
|
|
1145
|
-
if (!isRecord3(value.realRange)) throw new Error(`scenes[${index}].realRange must be an object.`);
|
|
1146
|
-
for (const key of ["baseRef", "targetRef", "baseOid", "targetOid"]) {
|
|
1147
|
-
requireString3(value.realRange[key], `scenes[${index}].realRange.${key}`);
|
|
1148
|
-
}
|
|
1149
|
-
if (!Array.isArray(value.panels) || value.panels.length < 2 || value.panels.length > 13) {
|
|
1150
|
-
throw new Error(`scenes[${index}].panels must contain a baseline and between 1 and 12 stages.`);
|
|
1151
|
-
}
|
|
1152
|
-
const panels = value.panels;
|
|
1153
|
-
for (const [panelIndex, panel] of panels.entries()) {
|
|
1154
|
-
if (!isRecord3(panel)) throw new Error(`scenes[${index}].panels[${panelIndex}] must be an object.`);
|
|
1155
|
-
for (const key of ["id", "label", "role"]) requireString3(panel[key], `scenes[${index}].panels[${panelIndex}].${key}`);
|
|
1156
|
-
const expectedRole = panelIndex === 0 ? "baseline" : "stage";
|
|
1157
|
-
if (panel.role !== expectedRole) throw new Error(`scenes[${index}].panels[${panelIndex}].role must be ${expectedRole}.`);
|
|
1158
|
-
if (panelIndex > 0) requireString3(panel.stageId, `scenes[${index}].panels[${panelIndex}].stageId`);
|
|
1159
|
-
}
|
|
1160
|
-
validateMultiPanelFiles(value.files, panels.length, index);
|
|
1161
|
-
validateMultiPanelSteps(value.steps, panels.length, index, true);
|
|
1162
|
-
if (Array.isArray(value.steps)) {
|
|
1163
|
-
let previousPairIndex = -1;
|
|
1164
|
-
value.steps.forEach((step, stepIndex) => {
|
|
1165
|
-
if (!isRecord3(step)) return;
|
|
1166
|
-
const pairIndex = Number(step.pairIndex);
|
|
1167
|
-
if (pairIndex < previousPairIndex) {
|
|
1168
|
-
throw new Error(`scenes[${index}].steps must remain grouped in explanation stage order.`);
|
|
1169
|
-
}
|
|
1170
|
-
previousPairIndex = pairIndex;
|
|
1171
|
-
if (step.stageId !== void 0) requireString3(step.stageId, `scenes[${index}].steps[${stepIndex}].stageId`);
|
|
1172
|
-
if (step.stageIndex !== void 0) requireNonNegativeInteger(step.stageIndex, `scenes[${index}].steps[${stepIndex}].stageIndex`);
|
|
1173
|
-
if (step.focusIndex !== void 0) requireNonNegativeInteger(step.focusIndex, `scenes[${index}].steps[${stepIndex}].focusIndex`);
|
|
1174
|
-
if (step.focusCount !== void 0) requirePositiveInteger(step.focusCount, `scenes[${index}].steps[${stepIndex}].focusCount`);
|
|
1175
|
-
if (step.stageIndex !== void 0 && Number(step.stageIndex) !== pairIndex) {
|
|
1176
|
-
throw new Error(`scenes[${index}].steps[${stepIndex}].stageIndex must match pairIndex.`);
|
|
1177
|
-
}
|
|
1178
|
-
if (step.focusIndex !== void 0 && step.focusCount !== void 0 && Number(step.focusIndex) >= Number(step.focusCount)) {
|
|
1179
|
-
throw new Error(`scenes[${index}].steps[${stepIndex}].focusIndex must be less than focusCount.`);
|
|
1180
|
-
}
|
|
1181
|
-
});
|
|
1182
|
-
for (let stageIndex = 0; stageIndex < panels.length - 1; stageIndex += 1) {
|
|
1183
|
-
const panel = panels[stageIndex + 1];
|
|
1184
|
-
const stageSteps = value.steps.filter((step) => isRecord3(step) && Number(step.pairIndex) === stageIndex);
|
|
1185
|
-
if (!isRecord3(panel) || stageSteps.length === 0) {
|
|
1186
|
-
throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
|
|
1187
|
-
}
|
|
1188
|
-
const firstStep = stageSteps[0];
|
|
1189
|
-
const fallbackStageId = isRecord3(firstStep) ? firstStep.id : void 0;
|
|
1190
|
-
if (stageSteps.some((step) => {
|
|
1191
|
-
if (!isRecord3(step)) return true;
|
|
1192
|
-
return (step.stageId ?? fallbackStageId) !== panel.stageId;
|
|
1193
|
-
})) {
|
|
1194
|
-
throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
function validateMultiPanelFiles(value, panelCount, index) {
|
|
1200
|
-
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].files must be non-empty.`);
|
|
1201
|
-
for (const [fileIndex, file] of value.entries()) {
|
|
1202
|
-
if (!isRecord3(file)) throw new Error(`scenes[${index}].files[${fileIndex}] must be an object.`);
|
|
1203
|
-
requireString3(file.path, `scenes[${index}].files[${fileIndex}].path`);
|
|
1204
|
-
if (!Array.isArray(file.panels) || file.panels.length !== panelCount) {
|
|
1205
|
-
throw new Error(`scenes[${index}].files[${fileIndex}].panels must match panel count.`);
|
|
1206
|
-
}
|
|
1207
|
-
for (const [panelIndex, panel] of file.panels.entries()) {
|
|
1208
|
-
if (!isRecord3(panel)) throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}] must be an object.`);
|
|
1209
|
-
for (const key of ["id", "label", "content"]) requireString3(panel[key], `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].${key}`);
|
|
1210
|
-
if (panel.path !== void 0) requireString3(panel.path, `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].path`);
|
|
1211
|
-
if (typeof panel.exists !== "boolean") throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}].exists must be boolean.`);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
|
|
1216
|
-
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].steps must be non-empty.`);
|
|
1217
|
-
for (const [stepIndex, step] of value.entries()) {
|
|
1218
|
-
if (!isRecord3(step)) throw new Error(`scenes[${index}].steps[${stepIndex}] must be an object.`);
|
|
1219
|
-
for (const key of ["id", "title", "body", "file", "side"]) requireString3(step[key], `scenes[${index}].steps[${stepIndex}].${key}`);
|
|
1220
|
-
requireNonNegativeInteger(step.pairIndex, `scenes[${index}].steps[${stepIndex}].pairIndex`);
|
|
1221
|
-
if (Number(step.pairIndex) >= panelCount - 1) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex is outside the panels.`);
|
|
1222
|
-
if (deconstructed) {
|
|
1223
|
-
requireStringArray3(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
|
|
1224
|
-
if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
|
|
1225
|
-
}
|
|
1226
|
-
if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
|
|
1227
|
-
if (step.startLine !== void 0) requirePositiveInteger(step.startLine, `scenes[${index}].steps[${stepIndex}].startLine`);
|
|
1228
|
-
if (step.endLine !== void 0) requirePositiveInteger(step.endLine, `scenes[${index}].steps[${stepIndex}].endLine`);
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
function validateTourFile(value, index) {
|
|
1232
|
-
if (!isRecord3(value)) throw new Error(`files[${index}] must be an object.`);
|
|
1233
|
-
if (value.kind === "text-diff") {
|
|
1234
|
-
validateScene(value, index, 1);
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
if (value.kind !== "omitted") {
|
|
1238
|
-
throw new Error(`files[${index}].kind must be text-diff or omitted.`);
|
|
1239
|
-
}
|
|
1240
|
-
for (const key of ["id", "title", "path", "changeKind", "reason"]) {
|
|
1241
|
-
requireString3(value[key], `files[${index}].${key}`);
|
|
1242
|
-
}
|
|
1243
|
-
if (value.previousPath !== void 0) requireString3(value.previousPath, `files[${index}].previousPath`);
|
|
1244
|
-
requireNonNegativeInteger(value.additions, `files[${index}].additions`);
|
|
1245
|
-
requireNonNegativeInteger(value.deletions, `files[${index}].deletions`);
|
|
1246
|
-
}
|
|
1247
|
-
function collectLegacyTourFiles(scenes) {
|
|
1248
|
-
const files = /* @__PURE__ */ new Map();
|
|
1249
|
-
for (const scene of scenes) {
|
|
1250
|
-
if (scene.kind === "text-diff") {
|
|
1251
|
-
if (!files.has(scene.path)) files.set(scene.path, scene);
|
|
1252
|
-
continue;
|
|
1253
|
-
}
|
|
1254
|
-
if (scene.kind === "walkthrough") {
|
|
1255
|
-
for (const step of scene.steps) {
|
|
1256
|
-
if (!files.has(step.diff.path)) files.set(step.diff.path, step.diff);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
1261
|
-
}
|
|
1262
|
-
function validateResolvedAnchor(value, path) {
|
|
1263
|
-
if (!isRecord3(value)) throw new Error(`${path} must be an object.`);
|
|
1264
|
-
for (const key of ["id", "path", "revision", "excerpt"]) requireString3(value[key], `${path}.${key}`);
|
|
1265
|
-
if (value.revision !== "base" && value.revision !== "head") throw new Error(`${path}.revision must be base or head.`);
|
|
1266
|
-
requirePositiveInteger(value.startLine, `${path}.startLine`);
|
|
1267
|
-
requirePositiveInteger(value.endLine, `${path}.endLine`);
|
|
1268
|
-
if (Number(value.endLine) < Number(value.startLine)) throw new Error(`${path}.endLine must not precede startLine.`);
|
|
1269
|
-
}
|
|
1270
|
-
function validateNarrative2(value, path) {
|
|
1271
|
-
requireString3(value.summary, `${path}.summary`);
|
|
1272
|
-
requireStringArray3(value.bullets, `${path}.bullets`);
|
|
1273
|
-
requireStringArray3(value.tags, `${path}.tags`);
|
|
1274
|
-
requireString3(value.takeaway, `${path}.takeaway`);
|
|
1275
|
-
}
|
|
1276
|
-
function validateChapterSummary(value, version, path) {
|
|
1277
|
-
if (value === void 0) return;
|
|
1278
|
-
if (version < 5) throw new Error(`${path} requires manifest version 5.`);
|
|
1279
|
-
if (typeof value !== "string" || !value.trim()) {
|
|
1280
|
-
throw new Error(`${path} must be a non-empty string.`);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
function isRecord3(value) {
|
|
1284
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1285
|
-
}
|
|
1286
|
-
function requireString3(value, path) {
|
|
1287
|
-
if (typeof value !== "string") {
|
|
1288
|
-
throw new Error(`${path} must be a string.`);
|
|
1793
|
+
requireString3(value.changeKind, `scenes[${index}].changeKind`);
|
|
1794
|
+
requireNonNegativeInteger(value.additions, `scenes[${index}].additions`);
|
|
1795
|
+
requireNonNegativeInteger(value.deletions, `scenes[${index}].deletions`);
|
|
1796
|
+
if (value.focusChangeIndex !== void 0) {
|
|
1797
|
+
requireNonNegativeInteger(value.focusChangeIndex, `scenes[${index}].focusChangeIndex`);
|
|
1289
1798
|
}
|
|
1290
1799
|
}
|
|
1291
|
-
function
|
|
1292
|
-
if (!Array.isArray(value) || value.
|
|
1293
|
-
throw new Error(
|
|
1800
|
+
function validateStackedScene2(value, index) {
|
|
1801
|
+
if (!Array.isArray(value.stack) || value.stack.length < 2 || value.stack.length > 6) {
|
|
1802
|
+
throw new Error(`scenes[${index}].stack must contain between 2 and 6 revisions.`);
|
|
1294
1803
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
throw new Error(`${path} must be a non-negative integer.`);
|
|
1804
|
+
for (const [panelIndex, panel] of value.stack.entries()) {
|
|
1805
|
+
if (!isRecord3(panel)) throw new Error(`scenes[${index}].stack[${panelIndex}] must be an object.`);
|
|
1806
|
+
for (const key of ["id", "ref", "oid", "label"]) requireString3(panel[key], `scenes[${index}].stack[${panelIndex}].${key}`);
|
|
1299
1807
|
}
|
|
1808
|
+
validateMultiPanelFiles(value.files, value.stack.length, index);
|
|
1809
|
+
validateMultiPanelSteps(value.steps, value.stack.length, index, false);
|
|
1300
1810
|
}
|
|
1301
|
-
function
|
|
1302
|
-
if (
|
|
1303
|
-
throw new Error(
|
|
1811
|
+
function validateDeconstructedScene2(value, index) {
|
|
1812
|
+
if (value.stageLabel !== "Explanation stages") {
|
|
1813
|
+
throw new Error(`scenes[${index}].stageLabel must be Explanation stages.`);
|
|
1304
1814
|
}
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
// src/changeInventory.ts
|
|
1311
|
-
var import_crypto = require("crypto");
|
|
1312
|
-
var import_child_process2 = require("child_process");
|
|
1313
|
-
var CHANGE_INVENTORY_VERSION = 2;
|
|
1314
|
-
function buildChangeInventory(startPath, options = {}) {
|
|
1315
|
-
const range = resolveBranchReviewRange(startPath, options.headRef, options.baseRef);
|
|
1316
|
-
const files = range.changedPaths.map((changedPath) => buildInventoryFile(
|
|
1317
|
-
range.repoRoot,
|
|
1318
|
-
range.mergeBaseOid,
|
|
1319
|
-
range.headOid,
|
|
1320
|
-
changedPath
|
|
1321
|
-
));
|
|
1322
|
-
return {
|
|
1323
|
-
version: CHANGE_INVENTORY_VERSION,
|
|
1324
|
-
range: {
|
|
1325
|
-
repoRoot: range.repoRoot,
|
|
1326
|
-
baseRef: range.baseRef,
|
|
1327
|
-
headRef: range.headRef,
|
|
1328
|
-
baseOid: range.mergeBaseOid,
|
|
1329
|
-
headOid: range.headOid
|
|
1330
|
-
},
|
|
1331
|
-
summary: {
|
|
1332
|
-
changedFiles: files.length,
|
|
1333
|
-
textualFiles: files.filter((file) => file.material === "text").length,
|
|
1334
|
-
binaryFiles: files.filter((file) => file.material === "binary").length,
|
|
1335
|
-
changeUnits: files.reduce((total, file) => total + file.units.length, 0),
|
|
1336
|
-
additions: files.reduce((total, file) => total + (file.additions ?? 0), 0),
|
|
1337
|
-
deletions: files.reduce((total, file) => total + (file.deletions ?? 0), 0)
|
|
1338
|
-
},
|
|
1339
|
-
files
|
|
1340
|
-
};
|
|
1341
|
-
}
|
|
1342
|
-
function buildInventoryFile(repoRoot, baseOid, headOid, changedPath) {
|
|
1343
|
-
const paths = [...new Set([changedPath.previousPath, changedPath.path].filter(Boolean))];
|
|
1344
|
-
const numstat = runGit(repoRoot, ["diff", "--numstat", "--find-renames", baseOid, headOid, "--", ...paths]);
|
|
1345
|
-
const [added = "0", deleted = "0"] = numstat.trim().split(/\s+/, 3);
|
|
1346
|
-
const basePath = changedPath.kind === "added" ? void 0 : changedPath.previousPath || changedPath.path;
|
|
1347
|
-
const headPath = changedPath.kind === "deleted" ? void 0 : changedPath.path;
|
|
1348
|
-
const baseBlob = readGitBlob(repoRoot, baseOid, basePath);
|
|
1349
|
-
const headBlob = readGitBlob(repoRoot, headOid, headPath);
|
|
1350
|
-
if (!baseBlob || !headBlob) {
|
|
1351
|
-
return {
|
|
1352
|
-
path: changedPath.path,
|
|
1353
|
-
previousPath: changedPath.previousPath,
|
|
1354
|
-
changeKind: changedPath.kind,
|
|
1355
|
-
material: "unsupported",
|
|
1356
|
-
additions: Number.parseInt(added, 10) || 0,
|
|
1357
|
-
deletions: Number.parseInt(deleted, 10) || 0,
|
|
1358
|
-
units: []
|
|
1359
|
-
};
|
|
1815
|
+
if (!isRecord3(value.realRange)) throw new Error(`scenes[${index}].realRange must be an object.`);
|
|
1816
|
+
for (const key of ["baseRef", "targetRef", "baseOid", "targetOid"]) {
|
|
1817
|
+
requireString3(value.realRange[key], `scenes[${index}].realRange.${key}`);
|
|
1360
1818
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
return {
|
|
1364
|
-
path: changedPath.path,
|
|
1365
|
-
previousPath: changedPath.previousPath,
|
|
1366
|
-
changeKind: changedPath.kind,
|
|
1367
|
-
material: "binary",
|
|
1368
|
-
additions: null,
|
|
1369
|
-
deletions: null,
|
|
1370
|
-
units: []
|
|
1371
|
-
};
|
|
1819
|
+
if (!Array.isArray(value.panels) || value.panels.length < 2 || value.panels.length > 13) {
|
|
1820
|
+
throw new Error(`scenes[${index}].panels must contain a baseline and between 1 and 12 stages.`);
|
|
1372
1821
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
"--find-renames=20%",
|
|
1381
|
-
"--diff-algorithm=default",
|
|
1382
|
-
"--unified=0",
|
|
1383
|
-
"--inter-hunk-context=0",
|
|
1384
|
-
baseOid,
|
|
1385
|
-
headOid,
|
|
1386
|
-
"--",
|
|
1387
|
-
...paths
|
|
1388
|
-
]);
|
|
1389
|
-
} catch {
|
|
1390
|
-
return {
|
|
1391
|
-
path: changedPath.path,
|
|
1392
|
-
previousPath: changedPath.previousPath,
|
|
1393
|
-
changeKind: changedPath.kind,
|
|
1394
|
-
material: "unsupported",
|
|
1395
|
-
additions: Number.parseInt(added, 10) || 0,
|
|
1396
|
-
deletions: Number.parseInt(deleted, 10) || 0,
|
|
1397
|
-
units: []
|
|
1398
|
-
};
|
|
1822
|
+
const panels = value.panels;
|
|
1823
|
+
for (const [panelIndex, panel] of panels.entries()) {
|
|
1824
|
+
if (!isRecord3(panel)) throw new Error(`scenes[${index}].panels[${panelIndex}] must be an object.`);
|
|
1825
|
+
for (const key of ["id", "label", "role"]) requireString3(panel[key], `scenes[${index}].panels[${panelIndex}].${key}`);
|
|
1826
|
+
const expectedRole = panelIndex === 0 ? "baseline" : "stage";
|
|
1827
|
+
if (panel.role !== expectedRole) throw new Error(`scenes[${index}].panels[${panelIndex}].role must be ${expectedRole}.`);
|
|
1828
|
+
if (panelIndex > 0) requireString3(panel.stageId, `scenes[${index}].panels[${panelIndex}].stageId`);
|
|
1399
1829
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
for (index += 1; index < lines.length && !lines[index].startsWith("@@ ") && !lines[index].startsWith("diff --git "); index += 1) {
|
|
1422
|
-
body.push(lines[index]);
|
|
1423
|
-
}
|
|
1424
|
-
index -= 1;
|
|
1425
|
-
const { oldText, newText } = parseHunkText(body);
|
|
1426
|
-
const contextHash = (0, import_crypto.createHash)("sha256").update(JSON.stringify([oldText, newText])).digest("hex").slice(0, 16);
|
|
1427
|
-
const baseId = `hunk-${(0, import_crypto.createHash)("sha256").update(JSON.stringify([
|
|
1428
|
-
changedPath.previousPath || changedPath.path,
|
|
1429
|
-
changedPath.path,
|
|
1430
|
-
oldText,
|
|
1431
|
-
newText
|
|
1432
|
-
])).digest("hex").slice(0, 12)}`;
|
|
1433
|
-
const occurrence = (seenIds.get(baseId) || 0) + 1;
|
|
1434
|
-
seenIds.set(baseId, occurrence);
|
|
1435
|
-
units.push({
|
|
1436
|
-
id: occurrence === 1 ? baseId : `${baseId}-${occurrence}`,
|
|
1437
|
-
path: changedPath.path,
|
|
1438
|
-
previousPath: changedPath.previousPath,
|
|
1439
|
-
changeKind: changedPath.kind,
|
|
1440
|
-
baseStart: Number.parseInt(match[1], 10),
|
|
1441
|
-
baseCount: match[2] === void 0 ? 1 : Number.parseInt(match[2], 10),
|
|
1442
|
-
headStart: Number.parseInt(match[3], 10),
|
|
1443
|
-
headCount: match[4] === void 0 ? 1 : Number.parseInt(match[4], 10),
|
|
1444
|
-
additions: body.filter((line) => line.startsWith("+") && !line.startsWith("+++")).length,
|
|
1445
|
-
deletions: body.filter((line) => line.startsWith("-") && !line.startsWith("---")).length,
|
|
1446
|
-
contextHash,
|
|
1447
|
-
oldText,
|
|
1448
|
-
newText
|
|
1830
|
+
validateMultiPanelFiles(value.files, panels.length, index);
|
|
1831
|
+
validateMultiPanelSteps(value.steps, panels.length, index, true);
|
|
1832
|
+
if (Array.isArray(value.steps)) {
|
|
1833
|
+
let previousPairIndex = -1;
|
|
1834
|
+
value.steps.forEach((step, stepIndex) => {
|
|
1835
|
+
if (!isRecord3(step)) return;
|
|
1836
|
+
const pairIndex = Number(step.pairIndex);
|
|
1837
|
+
if (pairIndex < previousPairIndex) {
|
|
1838
|
+
throw new Error(`scenes[${index}].steps must remain grouped in explanation stage order.`);
|
|
1839
|
+
}
|
|
1840
|
+
previousPairIndex = pairIndex;
|
|
1841
|
+
if (step.stageId !== void 0) requireString3(step.stageId, `scenes[${index}].steps[${stepIndex}].stageId`);
|
|
1842
|
+
if (step.stageIndex !== void 0) requireNonNegativeInteger(step.stageIndex, `scenes[${index}].steps[${stepIndex}].stageIndex`);
|
|
1843
|
+
if (step.focusIndex !== void 0) requireNonNegativeInteger(step.focusIndex, `scenes[${index}].steps[${stepIndex}].focusIndex`);
|
|
1844
|
+
if (step.focusCount !== void 0) requirePositiveInteger(step.focusCount, `scenes[${index}].steps[${stepIndex}].focusCount`);
|
|
1845
|
+
if (step.stageIndex !== void 0 && Number(step.stageIndex) !== pairIndex) {
|
|
1846
|
+
throw new Error(`scenes[${index}].steps[${stepIndex}].stageIndex must match pairIndex.`);
|
|
1847
|
+
}
|
|
1848
|
+
if (step.focusIndex !== void 0 && step.focusCount !== void 0 && Number(step.focusIndex) >= Number(step.focusCount)) {
|
|
1849
|
+
throw new Error(`scenes[${index}].steps[${stepIndex}].focusIndex must be less than focusCount.`);
|
|
1850
|
+
}
|
|
1449
1851
|
});
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1852
|
+
for (let stageIndex = 0; stageIndex < panels.length - 1; stageIndex += 1) {
|
|
1853
|
+
const panel = panels[stageIndex + 1];
|
|
1854
|
+
const stageSteps = value.steps.filter((step) => isRecord3(step) && Number(step.pairIndex) === stageIndex);
|
|
1855
|
+
if (!isRecord3(panel) || stageSteps.length === 0) {
|
|
1856
|
+
throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
|
|
1857
|
+
}
|
|
1858
|
+
const firstStep = stageSteps[0];
|
|
1859
|
+
const fallbackStageId = isRecord3(firstStep) ? firstStep.id : void 0;
|
|
1860
|
+
if (stageSteps.some((step) => {
|
|
1861
|
+
if (!isRecord3(step)) return true;
|
|
1862
|
+
return (step.stageId ?? fallbackStageId) !== panel.stageId;
|
|
1863
|
+
})) {
|
|
1864
|
+
throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
|
|
1865
|
+
}
|
|
1459
1866
|
}
|
|
1460
1867
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
let previousEnd = 0;
|
|
1470
|
-
for (const unit of ordered) {
|
|
1471
|
-
const range = resolveBaseRange(unit, offsets, baseContent.length);
|
|
1472
|
-
if (range.start < previousEnd) {
|
|
1473
|
-
throw new Error(`Overlapping change units cannot be materialized independently: ${unit.id}`);
|
|
1474
|
-
}
|
|
1475
|
-
if (baseContent.slice(range.start, range.end) !== unit.oldText) {
|
|
1476
|
-
throw new Error(`Change unit ${unit.id} no longer matches its base content.`);
|
|
1868
|
+
}
|
|
1869
|
+
function validateMultiPanelFiles(value, panelCount, index) {
|
|
1870
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].files must be non-empty.`);
|
|
1871
|
+
for (const [fileIndex, file] of value.entries()) {
|
|
1872
|
+
if (!isRecord3(file)) throw new Error(`scenes[${index}].files[${fileIndex}] must be an object.`);
|
|
1873
|
+
requireString3(file.path, `scenes[${index}].files[${fileIndex}].path`);
|
|
1874
|
+
if (!Array.isArray(file.panels) || file.panels.length !== panelCount) {
|
|
1875
|
+
throw new Error(`scenes[${index}].files[${fileIndex}].panels must match panel count.`);
|
|
1477
1876
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1877
|
+
for (const [panelIndex, panel] of file.panels.entries()) {
|
|
1878
|
+
if (!isRecord3(panel)) throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}] must be an object.`);
|
|
1879
|
+
for (const key of ["id", "label", "content"]) requireString3(panel[key], `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].${key}`);
|
|
1880
|
+
if (panel.path !== void 0) requireString3(panel.path, `scenes[${index}].files[${fileIndex}].panels[${panelIndex}].path`);
|
|
1881
|
+
if (typeof panel.exists !== "boolean") throw new Error(`scenes[${index}].files[${fileIndex}].panels[${panelIndex}].exists must be boolean.`);
|
|
1481
1882
|
}
|
|
1482
|
-
previousEnd = Math.max(previousEnd, range.end);
|
|
1483
1883
|
}
|
|
1484
|
-
chunks.push(baseContent.slice(cursor));
|
|
1485
|
-
return chunks.join("");
|
|
1486
1884
|
}
|
|
1487
|
-
function
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1885
|
+
function validateMultiPanelSteps(value, panelCount, index, deconstructed) {
|
|
1886
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error(`scenes[${index}].steps must be non-empty.`);
|
|
1887
|
+
for (const [stepIndex, step] of value.entries()) {
|
|
1888
|
+
if (!isRecord3(step)) throw new Error(`scenes[${index}].steps[${stepIndex}] must be an object.`);
|
|
1889
|
+
for (const key of ["id", "title", "body", "file", "side"]) requireString3(step[key], `scenes[${index}].steps[${stepIndex}].${key}`);
|
|
1890
|
+
requireNonNegativeInteger(step.pairIndex, `scenes[${index}].steps[${stepIndex}].pairIndex`);
|
|
1891
|
+
if (Number(step.pairIndex) >= panelCount - 1) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex is outside the panels.`);
|
|
1892
|
+
if (deconstructed) {
|
|
1893
|
+
requireStringArray3(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
|
|
1894
|
+
if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
|
|
1496
1895
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
if (
|
|
1500
|
-
`;
|
|
1501
|
-
if (prefix === "+" || prefix === " ") newText += `${content}
|
|
1502
|
-
`;
|
|
1503
|
-
previousPrefix = prefix;
|
|
1896
|
+
if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
|
|
1897
|
+
if (step.startLine !== void 0) requirePositiveInteger(step.startLine, `scenes[${index}].steps[${stepIndex}].startLine`);
|
|
1898
|
+
if (step.endLine !== void 0) requirePositiveInteger(step.endLine, `scenes[${index}].steps[${stepIndex}].endLine`);
|
|
1504
1899
|
}
|
|
1505
|
-
return { oldText, newText };
|
|
1506
|
-
}
|
|
1507
|
-
function removeFinalNewline(value) {
|
|
1508
|
-
return value.endsWith("\n") ? value.slice(0, -1) : value;
|
|
1509
1900
|
}
|
|
1510
|
-
function
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1901
|
+
function validateTourFile(value, index) {
|
|
1902
|
+
if (!isRecord3(value)) throw new Error(`files[${index}] must be an object.`);
|
|
1903
|
+
if (value.kind === "text-diff") {
|
|
1904
|
+
validateScene(value, index, 1);
|
|
1905
|
+
return;
|
|
1514
1906
|
}
|
|
1515
|
-
if (
|
|
1516
|
-
|
|
1517
|
-
}
|
|
1518
|
-
function resolveBaseRange(unit, offsets, contentLength) {
|
|
1519
|
-
const startIndex = unit.baseCount === 0 ? unit.baseStart : unit.baseStart - 1;
|
|
1520
|
-
const endIndex = unit.baseCount === 0 ? startIndex : startIndex + unit.baseCount;
|
|
1521
|
-
const start = offsets[startIndex];
|
|
1522
|
-
const end = offsets[endIndex];
|
|
1523
|
-
if (start === void 0 || end === void 0 || start > contentLength || end > contentLength) {
|
|
1524
|
-
throw new Error(`Change unit ${unit.id} has an invalid base range.`);
|
|
1907
|
+
if (value.kind !== "omitted") {
|
|
1908
|
+
throw new Error(`files[${index}].kind must be text-diff or omitted.`);
|
|
1525
1909
|
}
|
|
1526
|
-
|
|
1527
|
-
}
|
|
1528
|
-
function isLosslessTextBlob(content) {
|
|
1529
|
-
if (content.includes(0)) return false;
|
|
1530
|
-
const decoded = content.toString("utf8");
|
|
1531
|
-
return Buffer.from(decoded, "utf8").equals(content);
|
|
1532
|
-
}
|
|
1533
|
-
function readGitBlob(repoRoot, oid, relativePath) {
|
|
1534
|
-
if (!relativePath) return Buffer.alloc(0);
|
|
1535
|
-
try {
|
|
1536
|
-
return (0, import_child_process2.execFileSync)("git", ["show", `${oid}:${relativePath}`], {
|
|
1537
|
-
cwd: repoRoot,
|
|
1538
|
-
encoding: "buffer",
|
|
1539
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
1540
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1541
|
-
});
|
|
1542
|
-
} catch {
|
|
1543
|
-
return null;
|
|
1910
|
+
for (const key of ["id", "title", "path", "changeKind", "reason"]) {
|
|
1911
|
+
requireString3(value[key], `files[${index}].${key}`);
|
|
1544
1912
|
}
|
|
1913
|
+
if (value.previousPath !== void 0) requireString3(value.previousPath, `files[${index}].previousPath`);
|
|
1914
|
+
requireNonNegativeInteger(value.additions, `files[${index}].additions`);
|
|
1915
|
+
requireNonNegativeInteger(value.deletions, `files[${index}].deletions`);
|
|
1545
1916
|
}
|
|
1546
|
-
function
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1552
|
-
});
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
// src/deconstructedChange.ts
|
|
1556
|
-
function buildDeconstructedScene(startPath, scene, options = {}) {
|
|
1557
|
-
return compileDeconstructedScene(buildChangeInventory(startPath, {
|
|
1558
|
-
headRef: options.headRef || scene.target,
|
|
1559
|
-
baseRef: options.baseRef || scene.base
|
|
1560
|
-
}), scene);
|
|
1561
|
-
}
|
|
1562
|
-
function compileDeconstructedScene(inventory, scene) {
|
|
1563
|
-
const ownership = /* @__PURE__ */ new Map();
|
|
1564
|
-
const excludedByFile = /* @__PURE__ */ new Map();
|
|
1565
|
-
const cumulativeByFile = /* @__PURE__ */ new Map();
|
|
1566
|
-
const wholeFileExclusions = /* @__PURE__ */ new Map();
|
|
1567
|
-
const excludedFiles = [];
|
|
1568
|
-
for (const exclusion of scene.exclusions || []) {
|
|
1569
|
-
const file = requireInventoryFile(inventory, exclusion.file, "Exclusion");
|
|
1570
|
-
if (!exclusion.hunks) {
|
|
1571
|
-
if (wholeFileExclusions.has(file)) {
|
|
1572
|
-
throw new Error(`File is excluded more than once: ${exclusion.file}`);
|
|
1573
|
-
}
|
|
1574
|
-
wholeFileExclusions.set(file, exclusion.reason);
|
|
1575
|
-
excludedFiles.push({ path: file.path, reason: exclusion.reason });
|
|
1576
|
-
}
|
|
1577
|
-
const ids = exclusion.hunks || file.units.map((unit) => unit.id);
|
|
1578
|
-
if (file.material !== "text" && exclusion.hunks) {
|
|
1579
|
-
throw new Error(`Unsupported file ${exclusion.file} must be excluded as a whole file.`);
|
|
1580
|
-
}
|
|
1581
|
-
claimHunks(file, ids, `exclusion for ${exclusion.file}`, ownership);
|
|
1582
|
-
const excluded = excludedByFile.get(file) || /* @__PURE__ */ new Set();
|
|
1583
|
-
ids.forEach((id) => excluded.add(id));
|
|
1584
|
-
excludedByFile.set(file, excluded);
|
|
1585
|
-
}
|
|
1586
|
-
const stageClaims = scene.stages.map((stage) => {
|
|
1587
|
-
const claims = [];
|
|
1588
|
-
for (const change of stage.changes) {
|
|
1589
|
-
const file = requireInventoryFile(inventory, change.file, `Stage ${stage.id}`);
|
|
1590
|
-
if (file.material !== "text") {
|
|
1591
|
-
throw new Error(`Stage ${stage.id} cannot assign unsupported file: ${change.file}`);
|
|
1592
|
-
}
|
|
1593
|
-
if (file.changeKind === "renamed") {
|
|
1594
|
-
throw new Error(`Stage ${stage.id} cannot assign renamed file ${change.file} until path-transition units are supported.`);
|
|
1595
|
-
}
|
|
1596
|
-
claimHunks(file, change.hunks, `stage ${stage.id}`, ownership);
|
|
1597
|
-
claims.push({ file, ids: [...change.hunks] });
|
|
1598
|
-
}
|
|
1599
|
-
return { stage, claims };
|
|
1600
|
-
});
|
|
1601
|
-
for (const file of inventory.files) {
|
|
1602
|
-
const wholeFileExcluded = wholeFileExclusions.has(file);
|
|
1603
|
-
if (file.material !== "text" || file.changeKind === "renamed" || file.units.length === 0) {
|
|
1604
|
-
if (!wholeFileExcluded) {
|
|
1605
|
-
const reason = file.changeKind === "renamed" ? "rename path transitions are not supported yet" : file.material !== "text" ? `${file.material} content cannot be materialized` : "the change has no textual hunks";
|
|
1606
|
-
throw new Error(`Deconstructed scene ${scene.id} must exclude ${file.path} as a whole file because ${reason}.`);
|
|
1607
|
-
}
|
|
1917
|
+
function collectLegacyTourFiles(scenes) {
|
|
1918
|
+
const files = /* @__PURE__ */ new Map();
|
|
1919
|
+
for (const scene of scenes) {
|
|
1920
|
+
if (scene.kind === "text-diff") {
|
|
1921
|
+
if (!files.has(scene.path)) files.set(scene.path, scene);
|
|
1608
1922
|
continue;
|
|
1609
1923
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1924
|
+
if (scene.kind === "walkthrough") {
|
|
1925
|
+
for (const step of scene.steps) {
|
|
1926
|
+
if (!files.has(step.diff.path)) files.set(step.diff.path, step.diff);
|
|
1613
1927
|
}
|
|
1614
1928
|
}
|
|
1615
1929
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1930
|
+
return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
1931
|
+
}
|
|
1932
|
+
function validateResolvedAnchor(value, path) {
|
|
1933
|
+
if (!isRecord3(value)) throw new Error(`${path} must be an object.`);
|
|
1934
|
+
for (const key of ["id", "path", "revision", "excerpt"]) requireString3(value[key], `${path}.${key}`);
|
|
1935
|
+
if (value.revision !== "base" && value.revision !== "head") throw new Error(`${path}.revision must be base or head.`);
|
|
1936
|
+
requirePositiveInteger(value.startLine, `${path}.startLine`);
|
|
1937
|
+
requirePositiveInteger(value.endLine, `${path}.endLine`);
|
|
1938
|
+
if (Number(value.endLine) < Number(value.startLine)) throw new Error(`${path}.endLine must not precede startLine.`);
|
|
1939
|
+
}
|
|
1940
|
+
function validateNarrative2(value, path) {
|
|
1941
|
+
requireString3(value.summary, `${path}.summary`);
|
|
1942
|
+
requireStringArray3(value.bullets, `${path}.bullets`);
|
|
1943
|
+
requireStringArray3(value.tags, `${path}.tags`);
|
|
1944
|
+
requireString3(value.takeaway, `${path}.takeaway`);
|
|
1945
|
+
}
|
|
1946
|
+
function validateChapterSummary(value, version, path) {
|
|
1947
|
+
if (value === void 0) return;
|
|
1948
|
+
if (version < 5) throw new Error(`${path} requires manifest version 5.`);
|
|
1949
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1950
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
1618
1951
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
)
|
|
1625
|
-
|
|
1626
|
-
for (const { stage, claims } of stageClaims) {
|
|
1627
|
-
const introducedHunks = [];
|
|
1628
|
-
const introducedFiles = [];
|
|
1629
|
-
const introducedChanges = [];
|
|
1630
|
-
for (const claim of claims) {
|
|
1631
|
-
const cumulative = cumulativeByFile.get(claim.file) || /* @__PURE__ */ new Set();
|
|
1632
|
-
claim.ids.forEach((id) => {
|
|
1633
|
-
cumulative.add(id);
|
|
1634
|
-
introducedHunks.push(id);
|
|
1635
|
-
});
|
|
1636
|
-
cumulativeByFile.set(claim.file, cumulative);
|
|
1637
|
-
if (!introducedFiles.includes(claim.file.path)) introducedFiles.push(claim.file.path);
|
|
1638
|
-
const introducedChange = introducedChanges.find((change) => change.file === claim.file.path);
|
|
1639
|
-
if (introducedChange) {
|
|
1640
|
-
introducedChange.hunks.push(...claim.ids);
|
|
1641
|
-
} else {
|
|
1642
|
-
introducedChanges.push({ file: claim.file.path, hunks: [...claim.ids] });
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
stages.push({
|
|
1646
|
-
id: stage.id,
|
|
1647
|
-
title: stage.title,
|
|
1648
|
-
narration: stage.narration,
|
|
1649
|
-
introducedHunks,
|
|
1650
|
-
introducedFiles,
|
|
1651
|
-
introducedChanges,
|
|
1652
|
-
cumulativeHunks: [...cumulativeByFile.values()].flatMap((ids) => [...ids]),
|
|
1653
|
-
files: materializableFiles.map((file) => buildFileState(
|
|
1654
|
-
file,
|
|
1655
|
-
cumulativeByFile.get(file) || /* @__PURE__ */ new Set(),
|
|
1656
|
-
wholeFileExclusions.has(file)
|
|
1657
|
-
))
|
|
1658
|
-
});
|
|
1952
|
+
}
|
|
1953
|
+
function isRecord3(value) {
|
|
1954
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1955
|
+
}
|
|
1956
|
+
function requireString3(value, path) {
|
|
1957
|
+
if (typeof value !== "string") {
|
|
1958
|
+
throw new Error(`${path} must be a string.`);
|
|
1659
1959
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
wholeFileExclusions.has(file)
|
|
1665
|
-
);
|
|
1666
|
-
const expectedExists = file.changeKind !== "deleted";
|
|
1667
|
-
if (finalState.exists !== expectedExists || finalState.content !== file.headContent) {
|
|
1668
|
-
throw new Error(`Final deconstructed state does not match the target for ${file.path}.`);
|
|
1669
|
-
}
|
|
1960
|
+
}
|
|
1961
|
+
function requireStringArray3(value, path) {
|
|
1962
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
1963
|
+
throw new Error(`${path} must be an array of strings.`);
|
|
1670
1964
|
}
|
|
1671
|
-
return {
|
|
1672
|
-
id: scene.id,
|
|
1673
|
-
title: scene.title,
|
|
1674
|
-
baseOid: inventory.range.baseOid,
|
|
1675
|
-
targetOid: inventory.range.headOid,
|
|
1676
|
-
baselineFiles,
|
|
1677
|
-
excludedHunks: [...excludedByFile.values()].flatMap((ids) => [...ids]),
|
|
1678
|
-
excludedFiles,
|
|
1679
|
-
stages
|
|
1680
|
-
};
|
|
1681
1965
|
}
|
|
1682
|
-
function
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
return matches[0];
|
|
1966
|
+
function requireNonNegativeInteger(value, path) {
|
|
1967
|
+
if (!Number.isInteger(value) || Number(value) < 0) {
|
|
1968
|
+
throw new Error(`${path} must be a non-negative integer.`);
|
|
1969
|
+
}
|
|
1687
1970
|
}
|
|
1688
|
-
function
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
if (!known.has(id)) throw new Error(`${owner} references an unknown hunk in ${file.path}: ${id}`);
|
|
1692
|
-
const key = unitKey(file, id);
|
|
1693
|
-
const previousOwner = ownership.get(key);
|
|
1694
|
-
if (previousOwner) throw new Error(`Change unit ${id} in ${file.path} is assigned to both ${previousOwner} and ${owner}.`);
|
|
1695
|
-
ownership.set(key, owner);
|
|
1971
|
+
function requirePercentage(value, path) {
|
|
1972
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
|
|
1973
|
+
throw new Error(`${path} must be a number from 0 to 100.`);
|
|
1696
1974
|
}
|
|
1697
1975
|
}
|
|
1698
|
-
function
|
|
1699
|
-
|
|
1976
|
+
function requirePositiveInteger(value, path) {
|
|
1977
|
+
if (!Number.isInteger(value) || Number(value) < 1) throw new Error(`${path} must be a positive integer.`);
|
|
1700
1978
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1979
|
+
|
|
1980
|
+
// src/changeInventory.ts
|
|
1981
|
+
var import_crypto2 = require("crypto");
|
|
1982
|
+
var import_child_process3 = require("child_process");
|
|
1983
|
+
var CHANGE_INVENTORY_VERSION = 2;
|
|
1984
|
+
function buildChangeInventory(startPath, options = {}) {
|
|
1985
|
+
const range = resolveBranchReviewRange(startPath, options.headRef, options.baseRef);
|
|
1986
|
+
const files = range.changedPaths.map((changedPath) => buildInventoryFile(
|
|
1987
|
+
range.repoRoot,
|
|
1988
|
+
range.mergeBaseOid,
|
|
1989
|
+
range.headOid,
|
|
1990
|
+
changedPath
|
|
1991
|
+
));
|
|
1707
1992
|
return {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1993
|
+
version: CHANGE_INVENTORY_VERSION,
|
|
1994
|
+
range: {
|
|
1995
|
+
repoRoot: range.repoRoot,
|
|
1996
|
+
baseRef: range.baseRef,
|
|
1997
|
+
headRef: range.headRef,
|
|
1998
|
+
baseOid: range.mergeBaseOid,
|
|
1999
|
+
headOid: range.headOid
|
|
2000
|
+
},
|
|
2001
|
+
summary: {
|
|
2002
|
+
changedFiles: files.length,
|
|
2003
|
+
textualFiles: files.filter((file) => file.material === "text").length,
|
|
2004
|
+
binaryFiles: files.filter((file) => file.material === "binary").length,
|
|
2005
|
+
changeUnits: files.reduce((total, file) => total + file.units.length, 0),
|
|
2006
|
+
additions: files.reduce((total, file) => total + (file.additions ?? 0), 0),
|
|
2007
|
+
deletions: files.reduce((total, file) => total + (file.deletions ?? 0), 0)
|
|
2008
|
+
},
|
|
2009
|
+
files
|
|
1714
2010
|
};
|
|
1715
2011
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
2012
|
+
function buildInventoryFile(repoRoot, baseOid, headOid, changedPath) {
|
|
2013
|
+
const paths = [...new Set([changedPath.previousPath, changedPath.path].filter(Boolean))];
|
|
2014
|
+
const numstat = runGit(repoRoot, ["diff", "--numstat", "--find-renames", baseOid, headOid, "--", ...paths]);
|
|
2015
|
+
const [added = "0", deleted = "0"] = numstat.trim().split(/\s+/, 3);
|
|
2016
|
+
const basePath = changedPath.kind === "added" ? void 0 : changedPath.previousPath || changedPath.path;
|
|
2017
|
+
const headPath = changedPath.kind === "deleted" ? void 0 : changedPath.path;
|
|
2018
|
+
const baseBlob = readGitBlob(repoRoot, baseOid, basePath);
|
|
2019
|
+
const headBlob = readGitBlob(repoRoot, headOid, headPath);
|
|
2020
|
+
if (!baseBlob || !headBlob) {
|
|
2021
|
+
return {
|
|
2022
|
+
path: changedPath.path,
|
|
2023
|
+
previousPath: changedPath.previousPath,
|
|
2024
|
+
changeKind: changedPath.kind,
|
|
2025
|
+
material: "unsupported",
|
|
2026
|
+
additions: Number.parseInt(added, 10) || 0,
|
|
2027
|
+
deletions: Number.parseInt(deleted, 10) || 0,
|
|
2028
|
+
units: []
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
const binary = added === "-" || deleted === "-" || !isLosslessTextBlob(baseBlob) || !isLosslessTextBlob(headBlob);
|
|
2032
|
+
if (binary) {
|
|
2033
|
+
return {
|
|
2034
|
+
path: changedPath.path,
|
|
2035
|
+
previousPath: changedPath.previousPath,
|
|
2036
|
+
changeKind: changedPath.kind,
|
|
2037
|
+
material: "binary",
|
|
2038
|
+
additions: null,
|
|
2039
|
+
deletions: null,
|
|
2040
|
+
units: []
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
let patch;
|
|
2044
|
+
try {
|
|
2045
|
+
patch = runGit(repoRoot, [
|
|
2046
|
+
"diff",
|
|
2047
|
+
"--no-ext-diff",
|
|
2048
|
+
"--no-textconv",
|
|
2049
|
+
"--no-color",
|
|
2050
|
+
"--find-renames=20%",
|
|
2051
|
+
"--diff-algorithm=default",
|
|
2052
|
+
"--unified=0",
|
|
2053
|
+
"--inter-hunk-context=0",
|
|
2054
|
+
baseOid,
|
|
2055
|
+
headOid,
|
|
2056
|
+
"--",
|
|
2057
|
+
...paths
|
|
2058
|
+
]);
|
|
2059
|
+
} catch {
|
|
2060
|
+
return {
|
|
2061
|
+
path: changedPath.path,
|
|
2062
|
+
previousPath: changedPath.previousPath,
|
|
2063
|
+
changeKind: changedPath.kind,
|
|
2064
|
+
material: "unsupported",
|
|
2065
|
+
additions: Number.parseInt(added, 10) || 0,
|
|
2066
|
+
deletions: Number.parseInt(deleted, 10) || 0,
|
|
2067
|
+
units: []
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
const units = parsePatchUnits(patch, changedPath);
|
|
2071
|
+
return {
|
|
2072
|
+
path: changedPath.path,
|
|
2073
|
+
previousPath: changedPath.previousPath,
|
|
2074
|
+
changeKind: changedPath.kind,
|
|
2075
|
+
material: "text",
|
|
2076
|
+
additions: Number.parseInt(added, 10) || 0,
|
|
2077
|
+
deletions: Number.parseInt(deleted, 10) || 0,
|
|
2078
|
+
units,
|
|
2079
|
+
baseContent: baseBlob.toString("utf8"),
|
|
2080
|
+
headContent: headBlob.toString("utf8")
|
|
2081
|
+
};
|
|
1719
2082
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
var self = this;
|
|
1731
|
-
function done(value) {
|
|
1732
|
-
if (callback) {
|
|
1733
|
-
setTimeout(function() {
|
|
1734
|
-
callback(void 0, value);
|
|
1735
|
-
}, 0);
|
|
1736
|
-
return true;
|
|
1737
|
-
} else {
|
|
1738
|
-
return value;
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
oldString = this.castInput(oldString);
|
|
1742
|
-
newString = this.castInput(newString);
|
|
1743
|
-
oldString = this.removeEmpty(this.tokenize(oldString));
|
|
1744
|
-
newString = this.removeEmpty(this.tokenize(newString));
|
|
1745
|
-
var newLen = newString.length, oldLen = oldString.length;
|
|
1746
|
-
var editLength = 1;
|
|
1747
|
-
var maxEditLength = newLen + oldLen;
|
|
1748
|
-
if (options.maxEditLength) {
|
|
1749
|
-
maxEditLength = Math.min(maxEditLength, options.maxEditLength);
|
|
1750
|
-
}
|
|
1751
|
-
var maxExecutionTime = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : Infinity;
|
|
1752
|
-
var abortAfterTimestamp = Date.now() + maxExecutionTime;
|
|
1753
|
-
var bestPath = [{
|
|
1754
|
-
oldPos: -1,
|
|
1755
|
-
lastComponent: void 0
|
|
1756
|
-
}];
|
|
1757
|
-
var newPos = this.extractCommon(bestPath[0], newString, oldString, 0);
|
|
1758
|
-
if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
1759
|
-
return done([{
|
|
1760
|
-
value: this.join(newString),
|
|
1761
|
-
count: newString.length
|
|
1762
|
-
}]);
|
|
1763
|
-
}
|
|
1764
|
-
var minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
|
|
1765
|
-
function execEditLength() {
|
|
1766
|
-
for (var diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
|
|
1767
|
-
var basePath = void 0;
|
|
1768
|
-
var removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
|
|
1769
|
-
if (removePath) {
|
|
1770
|
-
bestPath[diagonalPath - 1] = void 0;
|
|
1771
|
-
}
|
|
1772
|
-
var canAdd = false;
|
|
1773
|
-
if (addPath) {
|
|
1774
|
-
var addPathNewPos = addPath.oldPos - diagonalPath;
|
|
1775
|
-
canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
|
|
1776
|
-
}
|
|
1777
|
-
var canRemove = removePath && removePath.oldPos + 1 < oldLen;
|
|
1778
|
-
if (!canAdd && !canRemove) {
|
|
1779
|
-
bestPath[diagonalPath] = void 0;
|
|
1780
|
-
continue;
|
|
1781
|
-
}
|
|
1782
|
-
if (!canRemove || canAdd && removePath.oldPos + 1 < addPath.oldPos) {
|
|
1783
|
-
basePath = self.addToPath(addPath, true, void 0, 0);
|
|
1784
|
-
} else {
|
|
1785
|
-
basePath = self.addToPath(removePath, void 0, true, 1);
|
|
1786
|
-
}
|
|
1787
|
-
newPos = self.extractCommon(basePath, newString, oldString, diagonalPath);
|
|
1788
|
-
if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
1789
|
-
return done(buildValues(self, basePath.lastComponent, newString, oldString, self.useLongestToken));
|
|
1790
|
-
} else {
|
|
1791
|
-
bestPath[diagonalPath] = basePath;
|
|
1792
|
-
if (basePath.oldPos + 1 >= oldLen) {
|
|
1793
|
-
maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
|
|
1794
|
-
}
|
|
1795
|
-
if (newPos + 1 >= newLen) {
|
|
1796
|
-
minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
editLength++;
|
|
1801
|
-
}
|
|
1802
|
-
if (callback) {
|
|
1803
|
-
(function exec() {
|
|
1804
|
-
setTimeout(function() {
|
|
1805
|
-
if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
|
|
1806
|
-
return callback();
|
|
1807
|
-
}
|
|
1808
|
-
if (!execEditLength()) {
|
|
1809
|
-
exec();
|
|
1810
|
-
}
|
|
1811
|
-
}, 0);
|
|
1812
|
-
})();
|
|
1813
|
-
} else {
|
|
1814
|
-
while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
|
|
1815
|
-
var ret = execEditLength();
|
|
1816
|
-
if (ret) {
|
|
1817
|
-
return ret;
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
},
|
|
1822
|
-
addToPath: function addToPath(path, added, removed, oldPosInc) {
|
|
1823
|
-
var last = path.lastComponent;
|
|
1824
|
-
if (last && last.added === added && last.removed === removed) {
|
|
1825
|
-
return {
|
|
1826
|
-
oldPos: path.oldPos + oldPosInc,
|
|
1827
|
-
lastComponent: {
|
|
1828
|
-
count: last.count + 1,
|
|
1829
|
-
added,
|
|
1830
|
-
removed,
|
|
1831
|
-
previousComponent: last.previousComponent
|
|
1832
|
-
}
|
|
1833
|
-
};
|
|
1834
|
-
} else {
|
|
1835
|
-
return {
|
|
1836
|
-
oldPos: path.oldPos + oldPosInc,
|
|
1837
|
-
lastComponent: {
|
|
1838
|
-
count: 1,
|
|
1839
|
-
added,
|
|
1840
|
-
removed,
|
|
1841
|
-
previousComponent: last
|
|
1842
|
-
}
|
|
1843
|
-
};
|
|
2083
|
+
function parsePatchUnits(patch, changedPath) {
|
|
2084
|
+
const lines = patch.split("\n");
|
|
2085
|
+
const units = [];
|
|
2086
|
+
const seenIds = /* @__PURE__ */ new Map();
|
|
2087
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
2088
|
+
const match = lines[index].match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
2089
|
+
if (!match) continue;
|
|
2090
|
+
const body = [];
|
|
2091
|
+
for (index += 1; index < lines.length && !lines[index].startsWith("@@ ") && !lines[index].startsWith("diff --git "); index += 1) {
|
|
2092
|
+
body.push(lines[index]);
|
|
1844
2093
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
2094
|
+
index -= 1;
|
|
2095
|
+
const { oldText, newText } = parseHunkText(body);
|
|
2096
|
+
const contextHash = (0, import_crypto2.createHash)("sha256").update(JSON.stringify([oldText, newText])).digest("hex").slice(0, 16);
|
|
2097
|
+
const baseId = `hunk-${(0, import_crypto2.createHash)("sha256").update(JSON.stringify([
|
|
2098
|
+
changedPath.previousPath || changedPath.path,
|
|
2099
|
+
changedPath.path,
|
|
2100
|
+
oldText,
|
|
2101
|
+
newText
|
|
2102
|
+
])).digest("hex").slice(0, 12)}`;
|
|
2103
|
+
const occurrence = (seenIds.get(baseId) || 0) + 1;
|
|
2104
|
+
seenIds.set(baseId, occurrence);
|
|
2105
|
+
units.push({
|
|
2106
|
+
id: occurrence === 1 ? baseId : `${baseId}-${occurrence}`,
|
|
2107
|
+
path: changedPath.path,
|
|
2108
|
+
previousPath: changedPath.previousPath,
|
|
2109
|
+
changeKind: changedPath.kind,
|
|
2110
|
+
baseStart: Number.parseInt(match[1], 10),
|
|
2111
|
+
baseCount: match[2] === void 0 ? 1 : Number.parseInt(match[2], 10),
|
|
2112
|
+
headStart: Number.parseInt(match[3], 10),
|
|
2113
|
+
headCount: match[4] === void 0 ? 1 : Number.parseInt(match[4], 10),
|
|
2114
|
+
additions: body.filter((line) => line.startsWith("+") && !line.startsWith("+++")).length,
|
|
2115
|
+
deletions: body.filter((line) => line.startsWith("-") && !line.startsWith("---")).length,
|
|
2116
|
+
contextHash,
|
|
2117
|
+
oldText,
|
|
2118
|
+
newText
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
return units;
|
|
2122
|
+
}
|
|
2123
|
+
function materializeChangeUnits(baseContent, units, selectedUnitIds) {
|
|
2124
|
+
const selected = new Set(selectedUnitIds);
|
|
2125
|
+
const knownIds = new Set(units.map((unit) => unit.id));
|
|
2126
|
+
for (const id of selected) {
|
|
2127
|
+
if (!knownIds.has(id)) {
|
|
2128
|
+
throw new Error(`Unknown change unit: ${id}`);
|
|
1852
2129
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2130
|
+
}
|
|
2131
|
+
const offsets = buildLineBoundaryOffsets(baseContent);
|
|
2132
|
+
const ordered = [...units].sort((left, right) => {
|
|
2133
|
+
const leftRange = resolveBaseRange(left, offsets, baseContent.length);
|
|
2134
|
+
const rightRange = resolveBaseRange(right, offsets, baseContent.length);
|
|
2135
|
+
return leftRange.start - rightRange.start || leftRange.end - rightRange.end;
|
|
2136
|
+
});
|
|
2137
|
+
const chunks = [];
|
|
2138
|
+
let cursor = 0;
|
|
2139
|
+
let previousEnd = 0;
|
|
2140
|
+
for (const unit of ordered) {
|
|
2141
|
+
const range = resolveBaseRange(unit, offsets, baseContent.length);
|
|
2142
|
+
if (range.start < previousEnd) {
|
|
2143
|
+
throw new Error(`Overlapping change units cannot be materialized independently: ${unit.id}`);
|
|
1858
2144
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
},
|
|
1862
|
-
equals: function equals(left, right) {
|
|
1863
|
-
if (this.options.comparator) {
|
|
1864
|
-
return this.options.comparator(left, right);
|
|
1865
|
-
} else {
|
|
1866
|
-
return left === right || this.options.ignoreCase && left.toLowerCase() === right.toLowerCase();
|
|
2145
|
+
if (baseContent.slice(range.start, range.end) !== unit.oldText) {
|
|
2146
|
+
throw new Error(`Change unit ${unit.id} no longer matches its base content.`);
|
|
1867
2147
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
for (var i = 0; i < array.length; i++) {
|
|
1872
|
-
if (array[i]) {
|
|
1873
|
-
ret.push(array[i]);
|
|
1874
|
-
}
|
|
2148
|
+
if (selected.has(unit.id)) {
|
|
2149
|
+
chunks.push(baseContent.slice(cursor, range.start), unit.newText);
|
|
2150
|
+
cursor = range.end;
|
|
1875
2151
|
}
|
|
1876
|
-
|
|
1877
|
-
},
|
|
1878
|
-
castInput: function castInput(value) {
|
|
1879
|
-
return value;
|
|
1880
|
-
},
|
|
1881
|
-
tokenize: function tokenize(value) {
|
|
1882
|
-
return value.split("");
|
|
1883
|
-
},
|
|
1884
|
-
join: function join(chars) {
|
|
1885
|
-
return chars.join("");
|
|
1886
|
-
}
|
|
1887
|
-
};
|
|
1888
|
-
function buildValues(diff2, lastComponent, newString, oldString, useLongestToken) {
|
|
1889
|
-
var components = [];
|
|
1890
|
-
var nextComponent;
|
|
1891
|
-
while (lastComponent) {
|
|
1892
|
-
components.push(lastComponent);
|
|
1893
|
-
nextComponent = lastComponent.previousComponent;
|
|
1894
|
-
delete lastComponent.previousComponent;
|
|
1895
|
-
lastComponent = nextComponent;
|
|
2152
|
+
previousEnd = Math.max(previousEnd, range.end);
|
|
1896
2153
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
} else {
|
|
1910
|
-
component.value = diff2.join(newString.slice(newPos, newPos + component.count));
|
|
1911
|
-
}
|
|
1912
|
-
newPos += component.count;
|
|
1913
|
-
if (!component.added) {
|
|
1914
|
-
oldPos += component.count;
|
|
1915
|
-
}
|
|
1916
|
-
} else {
|
|
1917
|
-
component.value = diff2.join(oldString.slice(oldPos, oldPos + component.count));
|
|
1918
|
-
oldPos += component.count;
|
|
1919
|
-
if (componentPos && components[componentPos - 1].added) {
|
|
1920
|
-
var tmp = components[componentPos - 1];
|
|
1921
|
-
components[componentPos - 1] = components[componentPos];
|
|
1922
|
-
components[componentPos] = tmp;
|
|
1923
|
-
}
|
|
2154
|
+
chunks.push(baseContent.slice(cursor));
|
|
2155
|
+
return chunks.join("");
|
|
2156
|
+
}
|
|
2157
|
+
function parseHunkText(body) {
|
|
2158
|
+
let oldText = "";
|
|
2159
|
+
let newText = "";
|
|
2160
|
+
let previousPrefix = "";
|
|
2161
|
+
for (const line of body) {
|
|
2162
|
+
if (line === "\") {
|
|
2163
|
+
if (previousPrefix === "-" || previousPrefix === " ") oldText = removeFinalNewline(oldText);
|
|
2164
|
+
if (previousPrefix === "+" || previousPrefix === " ") newText = removeFinalNewline(newText);
|
|
2165
|
+
continue;
|
|
1924
2166
|
}
|
|
2167
|
+
const prefix = line[0];
|
|
2168
|
+
const content = line.slice(1);
|
|
2169
|
+
if (prefix === "-" || prefix === " ") oldText += `${content}
|
|
2170
|
+
`;
|
|
2171
|
+
if (prefix === "+" || prefix === " ") newText += `${content}
|
|
2172
|
+
`;
|
|
2173
|
+
previousPrefix = prefix;
|
|
1925
2174
|
}
|
|
1926
|
-
|
|
1927
|
-
if (componentLen > 1 && typeof finalComponent.value === "string" && (finalComponent.added || finalComponent.removed) && diff2.equals("", finalComponent.value)) {
|
|
1928
|
-
components[componentLen - 2].value += finalComponent.value;
|
|
1929
|
-
components.pop();
|
|
1930
|
-
}
|
|
1931
|
-
return components;
|
|
2175
|
+
return { oldText, newText };
|
|
1932
2176
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
return characterDiff.diff(oldStr, newStr, options);
|
|
2177
|
+
function removeFinalNewline(value) {
|
|
2178
|
+
return value.endsWith("\n") ? value.slice(0, -1) : value;
|
|
1936
2179
|
}
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
if (this.options.ignoreCase) {
|
|
1942
|
-
left = left.toLowerCase();
|
|
1943
|
-
right = right.toLowerCase();
|
|
1944
|
-
}
|
|
1945
|
-
return left === right || this.options.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right);
|
|
1946
|
-
};
|
|
1947
|
-
wordDiff.tokenize = function(value) {
|
|
1948
|
-
var tokens = value.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/);
|
|
1949
|
-
for (var i = 0; i < tokens.length - 1; i++) {
|
|
1950
|
-
if (!tokens[i + 1] && tokens[i + 2] && extendedWordChars.test(tokens[i]) && extendedWordChars.test(tokens[i + 2])) {
|
|
1951
|
-
tokens[i] += tokens[i + 2];
|
|
1952
|
-
tokens.splice(i + 1, 2);
|
|
1953
|
-
i--;
|
|
1954
|
-
}
|
|
2180
|
+
function buildLineBoundaryOffsets(content) {
|
|
2181
|
+
const offsets = [0];
|
|
2182
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
2183
|
+
if (content[index] === "\n") offsets.push(index + 1);
|
|
1955
2184
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
function diffWordsWithSpace(oldStr, newStr, options) {
|
|
1959
|
-
return wordDiff.diff(oldStr, newStr, options);
|
|
2185
|
+
if (offsets[offsets.length - 1] !== content.length) offsets.push(content.length);
|
|
2186
|
+
return offsets;
|
|
1960
2187
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
linesAndNewlines.pop();
|
|
1969
|
-
}
|
|
1970
|
-
for (var i = 0; i < linesAndNewlines.length; i++) {
|
|
1971
|
-
var line = linesAndNewlines[i];
|
|
1972
|
-
if (i % 2 && !this.options.newlineIsToken) {
|
|
1973
|
-
retLines[retLines.length - 1] += line;
|
|
1974
|
-
} else {
|
|
1975
|
-
if (this.options.ignoreWhitespace) {
|
|
1976
|
-
line = line.trim();
|
|
1977
|
-
}
|
|
1978
|
-
retLines.push(line);
|
|
1979
|
-
}
|
|
2188
|
+
function resolveBaseRange(unit, offsets, contentLength) {
|
|
2189
|
+
const startIndex = unit.baseCount === 0 ? unit.baseStart : unit.baseStart - 1;
|
|
2190
|
+
const endIndex = unit.baseCount === 0 ? startIndex : startIndex + unit.baseCount;
|
|
2191
|
+
const start = offsets[startIndex];
|
|
2192
|
+
const end = offsets[endIndex];
|
|
2193
|
+
if (start === void 0 || end === void 0 || start > contentLength || end > contentLength) {
|
|
2194
|
+
throw new Error(`Change unit ${unit.id} has an invalid base range.`);
|
|
1980
2195
|
}
|
|
1981
|
-
return
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
return
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
};
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
|
|
2000
|
-
};
|
|
2196
|
+
return { start, end };
|
|
2197
|
+
}
|
|
2198
|
+
function isLosslessTextBlob(content) {
|
|
2199
|
+
if (content.includes(0)) return false;
|
|
2200
|
+
const decoded = content.toString("utf8");
|
|
2201
|
+
return Buffer.from(decoded, "utf8").equals(content);
|
|
2202
|
+
}
|
|
2203
|
+
function readGitBlob(repoRoot, oid, relativePath) {
|
|
2204
|
+
if (!relativePath) return Buffer.alloc(0);
|
|
2205
|
+
try {
|
|
2206
|
+
return (0, import_child_process3.execFileSync)("git", ["show", `${oid}:${relativePath}`], {
|
|
2207
|
+
cwd: repoRoot,
|
|
2208
|
+
encoding: "buffer",
|
|
2209
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
2210
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2211
|
+
});
|
|
2212
|
+
} catch {
|
|
2213
|
+
return null;
|
|
2001
2214
|
}
|
|
2002
|
-
return _typeof(obj);
|
|
2003
2215
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
return
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2216
|
+
function runGit(repoRoot, args) {
|
|
2217
|
+
return (0, import_child_process3.execFileSync)("git", args, {
|
|
2218
|
+
cwd: repoRoot,
|
|
2219
|
+
encoding: "utf8",
|
|
2220
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
2221
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// src/deconstructedChange.ts
|
|
2226
|
+
function buildDeconstructedScene(startPath, scene, options = {}) {
|
|
2227
|
+
return compileDeconstructedScene(buildChangeInventory(startPath, {
|
|
2228
|
+
headRef: options.headRef || scene.target,
|
|
2229
|
+
baseRef: options.baseRef || scene.base
|
|
2230
|
+
}), scene);
|
|
2231
|
+
}
|
|
2232
|
+
function compileDeconstructedScene(inventory, scene) {
|
|
2233
|
+
const ownership = /* @__PURE__ */ new Map();
|
|
2234
|
+
const excludedByFile = /* @__PURE__ */ new Map();
|
|
2235
|
+
const cumulativeByFile = /* @__PURE__ */ new Map();
|
|
2236
|
+
const wholeFileExclusions = /* @__PURE__ */ new Map();
|
|
2237
|
+
const excludedFiles = [];
|
|
2238
|
+
for (const exclusion of scene.exclusions || []) {
|
|
2239
|
+
const file = requireInventoryFile(inventory, exclusion.file, "Exclusion");
|
|
2240
|
+
if (!exclusion.hunks) {
|
|
2241
|
+
if (wholeFileExclusions.has(file)) {
|
|
2242
|
+
throw new Error(`File is excluded more than once: ${exclusion.file}`);
|
|
2243
|
+
}
|
|
2244
|
+
wholeFileExclusions.set(file, exclusion.reason);
|
|
2245
|
+
excludedFiles.push({ path: file.path, reason: exclusion.reason });
|
|
2027
2246
|
}
|
|
2247
|
+
const ids = exclusion.hunks || file.units.map((unit) => unit.id);
|
|
2248
|
+
if (file.material !== "text" && exclusion.hunks) {
|
|
2249
|
+
throw new Error(`Unsupported file ${exclusion.file} must be excluded as a whole file.`);
|
|
2250
|
+
}
|
|
2251
|
+
claimHunks(file, ids, `exclusion for ${exclusion.file}`, ownership);
|
|
2252
|
+
const excluded = excludedByFile.get(file) || /* @__PURE__ */ new Set();
|
|
2253
|
+
ids.forEach((id) => excluded.add(id));
|
|
2254
|
+
excludedByFile.set(file, excluded);
|
|
2028
2255
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2256
|
+
const stageClaims = scene.stages.map((stage) => {
|
|
2257
|
+
const claims = [];
|
|
2258
|
+
for (const change of stage.changes) {
|
|
2259
|
+
const file = requireInventoryFile(inventory, change.file, `Stage ${stage.id}`);
|
|
2260
|
+
if (file.material !== "text") {
|
|
2261
|
+
throw new Error(`Stage ${stage.id} cannot assign unsupported file: ${change.file}`);
|
|
2262
|
+
}
|
|
2263
|
+
if (file.changeKind === "renamed") {
|
|
2264
|
+
throw new Error(`Stage ${stage.id} cannot assign renamed file ${change.file} until path-transition units are supported.`);
|
|
2265
|
+
}
|
|
2266
|
+
claimHunks(file, change.hunks, `stage ${stage.id}`, ownership);
|
|
2267
|
+
claims.push({ file, ids: [...change.hunks] });
|
|
2268
|
+
}
|
|
2269
|
+
return { stage, claims };
|
|
2270
|
+
});
|
|
2271
|
+
for (const file of inventory.files) {
|
|
2272
|
+
const wholeFileExcluded = wholeFileExclusions.has(file);
|
|
2273
|
+
if (file.material !== "text" || file.changeKind === "renamed" || file.units.length === 0) {
|
|
2274
|
+
if (!wholeFileExcluded) {
|
|
2275
|
+
const reason = file.changeKind === "renamed" ? "rename path transitions are not supported yet" : file.material !== "text" ? `${file.material} content cannot be materialized` : "the change has no textual hunks";
|
|
2276
|
+
throw new Error(`Deconstructed scene ${scene.id} must exclude ${file.path} as a whole file because ${reason}.`);
|
|
2277
|
+
}
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
for (const unit of file.units) {
|
|
2281
|
+
if (!ownership.has(unitKey(file, unit.id))) {
|
|
2282
|
+
throw new Error(`Unassigned change unit in ${file.path}: ${unit.id}`);
|
|
2283
|
+
}
|
|
2036
2284
|
}
|
|
2037
|
-
stack.pop();
|
|
2038
|
-
replacementStack.pop();
|
|
2039
|
-
return canonicalizedObj;
|
|
2040
2285
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
2286
|
+
for (const file of inventory.files) {
|
|
2287
|
+
cumulativeByFile.set(file, new Set(excludedByFile.get(file) || []));
|
|
2043
2288
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2289
|
+
const materializableFiles = inventory.files.filter((file) => file.material === "text" && file.changeKind !== "renamed" && file.baseContent !== void 0 && file.headContent !== void 0);
|
|
2290
|
+
const baselineFiles = materializableFiles.map((file) => buildFileState(
|
|
2291
|
+
file,
|
|
2292
|
+
cumulativeByFile.get(file) || /* @__PURE__ */ new Set(),
|
|
2293
|
+
wholeFileExclusions.has(file)
|
|
2294
|
+
));
|
|
2295
|
+
const stages = [];
|
|
2296
|
+
for (const { stage, claims } of stageClaims) {
|
|
2297
|
+
const introducedHunks = [];
|
|
2298
|
+
const introducedFiles = [];
|
|
2299
|
+
const introducedChanges = [];
|
|
2300
|
+
for (const claim of claims) {
|
|
2301
|
+
const cumulative = cumulativeByFile.get(claim.file) || /* @__PURE__ */ new Set();
|
|
2302
|
+
claim.ids.forEach((id) => {
|
|
2303
|
+
cumulative.add(id);
|
|
2304
|
+
introducedHunks.push(id);
|
|
2305
|
+
});
|
|
2306
|
+
cumulativeByFile.set(claim.file, cumulative);
|
|
2307
|
+
if (!introducedFiles.includes(claim.file.path)) introducedFiles.push(claim.file.path);
|
|
2308
|
+
const introducedChange = introducedChanges.find((change) => change.file === claim.file.path);
|
|
2309
|
+
if (introducedChange) {
|
|
2310
|
+
introducedChange.hunks.push(...claim.ids);
|
|
2311
|
+
} else {
|
|
2312
|
+
introducedChanges.push({ file: claim.file.path, hunks: [...claim.ids] });
|
|
2052
2313
|
}
|
|
2053
2314
|
}
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2315
|
+
stages.push({
|
|
2316
|
+
id: stage.id,
|
|
2317
|
+
title: stage.title,
|
|
2318
|
+
narration: stage.narration,
|
|
2319
|
+
introducedHunks,
|
|
2320
|
+
introducedFiles,
|
|
2321
|
+
introducedChanges,
|
|
2322
|
+
cumulativeHunks: [...cumulativeByFile.values()].flatMap((ids) => [...ids]),
|
|
2323
|
+
files: materializableFiles.map((file) => buildFileState(
|
|
2324
|
+
file,
|
|
2325
|
+
cumulativeByFile.get(file) || /* @__PURE__ */ new Set(),
|
|
2326
|
+
wholeFileExclusions.has(file)
|
|
2327
|
+
))
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
for (const file of materializableFiles) {
|
|
2331
|
+
const finalState = buildFileState(
|
|
2332
|
+
file,
|
|
2333
|
+
cumulativeByFile.get(file) || /* @__PURE__ */ new Set(),
|
|
2334
|
+
wholeFileExclusions.has(file)
|
|
2335
|
+
);
|
|
2336
|
+
const expectedExists = file.changeKind !== "deleted";
|
|
2337
|
+
if (finalState.exists !== expectedExists || finalState.content !== file.headContent) {
|
|
2338
|
+
throw new Error(`Final deconstructed state does not match the target for ${file.path}.`);
|
|
2058
2339
|
}
|
|
2059
|
-
stack.pop();
|
|
2060
|
-
replacementStack.pop();
|
|
2061
|
-
} else {
|
|
2062
|
-
canonicalizedObj = obj;
|
|
2063
2340
|
}
|
|
2064
|
-
return
|
|
2341
|
+
return {
|
|
2342
|
+
id: scene.id,
|
|
2343
|
+
title: scene.title,
|
|
2344
|
+
baseOid: inventory.range.baseOid,
|
|
2345
|
+
targetOid: inventory.range.headOid,
|
|
2346
|
+
baselineFiles,
|
|
2347
|
+
excludedHunks: [...excludedByFile.values()].flatMap((ids) => [...ids]),
|
|
2348
|
+
excludedFiles,
|
|
2349
|
+
stages
|
|
2350
|
+
};
|
|
2065
2351
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
};
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2352
|
+
function requireInventoryFile(inventory, path, owner) {
|
|
2353
|
+
const matches = inventory.files.filter((file) => file.path === path || file.previousPath === path);
|
|
2354
|
+
if (matches.length === 0) throw new Error(`${owner} references a file outside the change inventory: ${path}`);
|
|
2355
|
+
if (matches.length > 1) throw new Error(`${owner} references an ambiguous changed path: ${path}`);
|
|
2356
|
+
return matches[0];
|
|
2357
|
+
}
|
|
2358
|
+
function claimHunks(file, ids, owner, ownership) {
|
|
2359
|
+
const known = new Set(file.units.map((unit) => unit.id));
|
|
2360
|
+
for (const id of ids) {
|
|
2361
|
+
if (!known.has(id)) throw new Error(`${owner} references an unknown hunk in ${file.path}: ${id}`);
|
|
2362
|
+
const key = unitKey(file, id);
|
|
2363
|
+
const previousOwner = ownership.get(key);
|
|
2364
|
+
if (previousOwner) throw new Error(`Change unit ${id} in ${file.path} is assigned to both ${previousOwner} and ${owner}.`);
|
|
2365
|
+
ownership.set(key, owner);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
function unitKey(file, id) {
|
|
2369
|
+
return `${file.previousPath || ""}\0${file.path}\0${id}`;
|
|
2370
|
+
}
|
|
2371
|
+
function buildFileState(file, selected, wholeFileExcluded) {
|
|
2372
|
+
if (file.baseContent === void 0) throw new Error(`Missing base content for ${file.path}.`);
|
|
2373
|
+
const content = materializeChangeUnits(file.baseContent, file.units, selected);
|
|
2374
|
+
let exists = true;
|
|
2375
|
+
if (file.changeKind === "added") exists = wholeFileExcluded || selected.size > 0;
|
|
2376
|
+
if (file.changeKind === "deleted") exists = !(wholeFileExcluded || selected.size === file.units.length);
|
|
2377
|
+
return {
|
|
2378
|
+
path: file.path,
|
|
2379
|
+
previousPath: file.previousPath,
|
|
2380
|
+
changeKind: file.changeKind,
|
|
2381
|
+
exists,
|
|
2382
|
+
content: exists ? content : "",
|
|
2383
|
+
appliedHunks: [...selected]
|
|
2384
|
+
};
|
|
2075
2385
|
}
|
|
2076
2386
|
|
|
2077
2387
|
// src/diffEngine.ts
|
|
@@ -2104,17 +2414,10 @@ function buildTwoWayDiffModel(leftContent, rightContent, options = {}) {
|
|
|
2104
2414
|
model.formattingOnly = true;
|
|
2105
2415
|
return model;
|
|
2106
2416
|
}
|
|
2107
|
-
const
|
|
2108
|
-
|
|
2109
|
-
timeout: options.timeoutMs ?? DIFF_TIMEOUT_MS
|
|
2110
|
-
});
|
|
2111
|
-
if (!internedChanges) {
|
|
2417
|
+
const changes = diffAnchoredLines(leftLines, rightLines, options.timeoutMs ?? DIFF_TIMEOUT_MS);
|
|
2418
|
+
if (!changes) {
|
|
2112
2419
|
return buildConservativeTwoWayDiffModel(leftLines, rightLines);
|
|
2113
2420
|
}
|
|
2114
|
-
const changes = internedChanges.map((change) => ({
|
|
2115
|
-
...change,
|
|
2116
|
-
value: change.value.map((lineId) => interned.lines[lineId])
|
|
2117
|
-
}));
|
|
2118
2421
|
const rows = [];
|
|
2119
2422
|
const renderedLeftLines = [];
|
|
2120
2423
|
const renderedRightLines = [];
|
|
@@ -2229,6 +2532,54 @@ function mergeTouchingBlocks(blocks) {
|
|
|
2229
2532
|
}
|
|
2230
2533
|
return merged;
|
|
2231
2534
|
}
|
|
2535
|
+
function diffAnchoredLines(leftLines, rightLines, timeoutMs) {
|
|
2536
|
+
const deadline = Date.now() + timeoutMs;
|
|
2537
|
+
const namedLines = (lines) => lines.flatMap((line, index) => {
|
|
2538
|
+
const key = normalizeMatchingContent(line);
|
|
2539
|
+
return isInformativeLine(key) ? [{ key, index }] : [];
|
|
2540
|
+
});
|
|
2541
|
+
const leftNames = namedLines(leftLines);
|
|
2542
|
+
const rightNames = namedLines(rightLines);
|
|
2543
|
+
const namedChanges = diffArrays(leftNames.map((line) => line.key), rightNames.map((line) => line.key), {
|
|
2544
|
+
timeout: deadline - Date.now()
|
|
2545
|
+
});
|
|
2546
|
+
if (!namedChanges) return void 0;
|
|
2547
|
+
const changes = [];
|
|
2548
|
+
let leftStart = 0;
|
|
2549
|
+
let rightStart = 0;
|
|
2550
|
+
const appendGap = (leftEnd, rightEnd) => {
|
|
2551
|
+
if (leftStart === leftEnd && rightStart === rightEnd) return true;
|
|
2552
|
+
const interned = internDiffLines(leftLines.slice(leftStart, leftEnd), rightLines.slice(rightStart, rightEnd));
|
|
2553
|
+
const gap = diffArrays(interned.left, interned.right, { timeout: deadline - Date.now() });
|
|
2554
|
+
if (!gap) return false;
|
|
2555
|
+
changes.push(...gap.map((change) => ({ ...change, value: change.value.map((id) => interned.lines[id]) })));
|
|
2556
|
+
return true;
|
|
2557
|
+
};
|
|
2558
|
+
let leftName = 0;
|
|
2559
|
+
let rightName = 0;
|
|
2560
|
+
for (const change of namedChanges) {
|
|
2561
|
+
if (change.removed) {
|
|
2562
|
+
leftName += change.value.length;
|
|
2563
|
+
continue;
|
|
2564
|
+
}
|
|
2565
|
+
if (change.added) {
|
|
2566
|
+
rightName += change.value.length;
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
for (let index = 0; index < change.value.length; index++) {
|
|
2570
|
+
const leftIndex = leftNames[leftName++].index;
|
|
2571
|
+
const rightIndex = rightNames[rightName++].index;
|
|
2572
|
+
if (!appendGap(leftIndex, rightIndex)) return void 0;
|
|
2573
|
+
const left = leftLines[leftIndex];
|
|
2574
|
+
const right = rightLines[rightIndex];
|
|
2575
|
+
if (left === right) changes.push({ value: [left] });
|
|
2576
|
+
else changes.push({ removed: true, value: [left] }, { added: true, value: [right] });
|
|
2577
|
+
leftStart = leftIndex + 1;
|
|
2578
|
+
rightStart = rightIndex + 1;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
return appendGap(leftLines.length, rightLines.length) ? changes : void 0;
|
|
2582
|
+
}
|
|
2232
2583
|
function internDiffLines(leftLines, rightLines) {
|
|
2233
2584
|
const lineIds = /* @__PURE__ */ new Map();
|
|
2234
2585
|
const lines = [];
|
|
@@ -3209,8 +3560,8 @@ function buildInlineSegments(leftContent, rightContent) {
|
|
|
3209
3560
|
}
|
|
3210
3561
|
}
|
|
3211
3562
|
return {
|
|
3212
|
-
leftSegments,
|
|
3213
|
-
rightSegments,
|
|
3563
|
+
leftSegments: normalizeIndentationHighlights(leftSegments),
|
|
3564
|
+
rightSegments: normalizeIndentationHighlights(rightSegments),
|
|
3214
3565
|
hasInlineChanges
|
|
3215
3566
|
};
|
|
3216
3567
|
}
|
|
@@ -3244,7 +3595,40 @@ function buildWhitespaceOnlySegments(left, right) {
|
|
|
3244
3595
|
append(leftSegments, "context", part.slice(part.length - suffix));
|
|
3245
3596
|
append(rightSegments, "context", other.slice(other.length - suffix));
|
|
3246
3597
|
});
|
|
3247
|
-
return {
|
|
3598
|
+
return {
|
|
3599
|
+
leftSegments: normalizeIndentationHighlights(leftSegments),
|
|
3600
|
+
rightSegments: normalizeIndentationHighlights(rightSegments)
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
function normalizeIndentationHighlights(segments) {
|
|
3604
|
+
const result = [];
|
|
3605
|
+
splitSegmentsIntoLines(segments).forEach((line, index) => {
|
|
3606
|
+
if (index > 0) result.push({ kind: "context", text: "\n", emphasis: false });
|
|
3607
|
+
const content = line.map((segment) => segment.text).join("");
|
|
3608
|
+
const indentLength = content.match(/^[\t ]*/u)[0].length;
|
|
3609
|
+
let offset = 0;
|
|
3610
|
+
let emphasized = 0;
|
|
3611
|
+
let kind = "context";
|
|
3612
|
+
for (const segment of line) {
|
|
3613
|
+
if (offset >= indentLength) break;
|
|
3614
|
+
if (segment.emphasis) {
|
|
3615
|
+
emphasized += Math.min(segment.text.length, indentLength - offset);
|
|
3616
|
+
kind = segment.kind;
|
|
3617
|
+
}
|
|
3618
|
+
offset += segment.text.length;
|
|
3619
|
+
}
|
|
3620
|
+
if (emphasized) result.push({ kind, text: content.slice(0, emphasized), emphasis: true });
|
|
3621
|
+
if (indentLength > emphasized) {
|
|
3622
|
+
result.push({ kind: "context", text: content.slice(emphasized, indentLength), emphasis: false });
|
|
3623
|
+
}
|
|
3624
|
+
offset = 0;
|
|
3625
|
+
for (const segment of line) {
|
|
3626
|
+
const start = Math.max(0, indentLength - offset);
|
|
3627
|
+
if (start < segment.text.length) result.push({ ...segment, text: segment.text.slice(start) });
|
|
3628
|
+
offset += segment.text.length;
|
|
3629
|
+
}
|
|
3630
|
+
});
|
|
3631
|
+
return result;
|
|
3248
3632
|
}
|
|
3249
3633
|
function splitSegmentsIntoLines(segments) {
|
|
3250
3634
|
const lines = [[]];
|
|
@@ -3380,8 +3764,8 @@ var LOCKFILES = /* @__PURE__ */ new Set([
|
|
|
3380
3764
|
]);
|
|
3381
3765
|
var MAX_FAN_IN_FILES = 250;
|
|
3382
3766
|
var MAX_FAN_IN_TEXT_BYTES = 64 * 1024;
|
|
3383
|
-
function classifyChangeFileRole(
|
|
3384
|
-
const lower =
|
|
3767
|
+
function classifyChangeFileRole(filePath2) {
|
|
3768
|
+
const lower = filePath2.toLowerCase();
|
|
3385
3769
|
const fileName = lower.split("/").pop() || lower;
|
|
3386
3770
|
if (/(^|\/)(dist|build|generated|vendor)(\/|$)/.test(lower) || /(?:\.min\.js|\.map)$/.test(lower)) return "generated";
|
|
3387
3771
|
if (/(^|\/)(tests?|__tests__)(\/|$)/.test(lower) || /(^|\/)test_[^/]+$/.test(lower) || /\.(test|spec)\.[^/]+$/.test(lower)) return "test";
|
|
@@ -3487,7 +3871,7 @@ function isTestForFocus(testPath, focusPaths) {
|
|
|
3487
3871
|
}
|
|
3488
3872
|
|
|
3489
3873
|
// src/changeTourContext.ts
|
|
3490
|
-
var
|
|
3874
|
+
var import_child_process4 = require("child_process");
|
|
3491
3875
|
var CHANGE_TOUR_CONTEXT_VERSION = 1;
|
|
3492
3876
|
var DEFAULT_MAX_PATCH_BYTES = 128 * 1024;
|
|
3493
3877
|
var DEFAULT_MAX_TOTAL_PATCH_BYTES = 2 * 1024 * 1024;
|
|
@@ -3517,6 +3901,7 @@ function buildChangeTourContext(startPath, options = {}) {
|
|
|
3517
3901
|
for (const file of files) filesByRole[file.role] += 1;
|
|
3518
3902
|
return {
|
|
3519
3903
|
version: CHANGE_TOUR_CONTEXT_VERSION,
|
|
3904
|
+
...options.correspondencePath ? { correspondence: loadChangeCorrespondence(range.repoRoot, options.correspondencePath, { baseOid: range.mergeBaseOid, headOid: range.headOid }) } : {},
|
|
3520
3905
|
generatedAt: options.generatedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
3521
3906
|
range: {
|
|
3522
3907
|
baseRef: range.baseRef,
|
|
@@ -3587,7 +3972,7 @@ function readLineStats(repoRoot, baseOid, headOid, paths) {
|
|
|
3587
3972
|
function readPatch(repoRoot, baseOid, headOid, paths, binary, maxPatchBytes) {
|
|
3588
3973
|
if (binary) return { included: false, content: "", bytes: 0, reason: "binary" };
|
|
3589
3974
|
try {
|
|
3590
|
-
const buffer = (0,
|
|
3975
|
+
const buffer = (0, import_child_process4.execFileSync)("git", [
|
|
3591
3976
|
"diff",
|
|
3592
3977
|
"--no-ext-diff",
|
|
3593
3978
|
"--no-color",
|
|
@@ -3648,7 +4033,7 @@ function findSymbolHints(content, ranges) {
|
|
|
3648
4033
|
}
|
|
3649
4034
|
function readGitText(repoRoot, oid, relativePath, maxBytes) {
|
|
3650
4035
|
try {
|
|
3651
|
-
const content = (0,
|
|
4036
|
+
const content = (0, import_child_process4.execFileSync)("git", ["show", `${oid}:${relativePath}`], {
|
|
3652
4037
|
cwd: repoRoot,
|
|
3653
4038
|
encoding: "buffer",
|
|
3654
4039
|
maxBuffer: maxBytes + 1,
|
|
@@ -3661,7 +4046,7 @@ function readGitText(repoRoot, oid, relativePath, maxBytes) {
|
|
|
3661
4046
|
}
|
|
3662
4047
|
}
|
|
3663
4048
|
function runGit2(repoRoot, args) {
|
|
3664
|
-
return (0,
|
|
4049
|
+
return (0, import_child_process4.execFileSync)("git", args, {
|
|
3665
4050
|
cwd: repoRoot,
|
|
3666
4051
|
encoding: "utf8",
|
|
3667
4052
|
maxBuffer: MAX_CAPTURE_BYTES,
|
|
@@ -3878,10 +4263,21 @@ function buildChangeTourManifest(startPath, options = {}) {
|
|
|
3878
4263
|
}
|
|
3879
4264
|
function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot, imageRevisions) {
|
|
3880
4265
|
var _a, _b;
|
|
4266
|
+
const correspondence = source.correspondence ? (() => {
|
|
4267
|
+
const file = (0, import_fs2.realpathSync)((0, import_path.resolve)(repoRoot, source.correspondence));
|
|
4268
|
+
const name = (0, import_path.relative)((0, import_fs2.realpathSync)(repoRoot), file);
|
|
4269
|
+
if ((0, import_path.isAbsolute)(name) || name === ".." || name.startsWith(`..${import_path.sep}`)) throw new Error("Correspondence artifact must be inside the repository.");
|
|
4270
|
+
return loadChangeCorrespondence(repoRoot, file, { baseOid: imageRevisions.base, headOid: imageRevisions.head });
|
|
4271
|
+
})() : void 0;
|
|
4272
|
+
const compileSymbol = (step, focus) => {
|
|
4273
|
+
if (!step.symbol) return void 0;
|
|
4274
|
+
if (!correspondence) throw new Error(`Step ${step.id} requires a correspondence artifact.`);
|
|
4275
|
+
return compileTourSymbol(correspondence, step.symbol, focus);
|
|
4276
|
+
};
|
|
3881
4277
|
const available = new Map(defaultScenes.map((scene) => [scene.path, scene]));
|
|
3882
4278
|
const resolvedAnchors = /* @__PURE__ */ new Map();
|
|
3883
4279
|
for (const [id, anchor] of Object.entries(source.anchors)) {
|
|
3884
|
-
const file = available.get(anchor.file);
|
|
4280
|
+
const file = available.get(anchor.file) || (anchor.revision === "base" ? defaultScenes.find((scene) => scene.previousPath === anchor.file) : void 0);
|
|
3885
4281
|
if (!file) {
|
|
3886
4282
|
throw new Error(`Anchor ${id} references a file outside the text change range: ${anchor.file}`);
|
|
3887
4283
|
}
|
|
@@ -3918,7 +4314,8 @@ function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot,
|
|
|
3918
4314
|
available,
|
|
3919
4315
|
resolvedAnchors,
|
|
3920
4316
|
connections,
|
|
3921
|
-
(image) => compileTourImage(repoRoot, image, imageRevisions[image.revision])
|
|
4317
|
+
(image) => compileTourImage(repoRoot, image, imageRevisions[image.revision]),
|
|
4318
|
+
compileSymbol
|
|
3922
4319
|
);
|
|
3923
4320
|
finalScenes.push(finalScene);
|
|
3924
4321
|
finalSceneIds.push(finalScene.id);
|
|
@@ -3947,7 +4344,8 @@ function applySourceChapters(source, chaptersToCompile, defaultScenes, repoRoot,
|
|
|
3947
4344
|
available,
|
|
3948
4345
|
resolvedAnchors,
|
|
3949
4346
|
connections,
|
|
3950
|
-
(image) => compileTourImage(repoRoot, image, imageRevisions[image.revision])
|
|
4347
|
+
(image) => compileTourImage(repoRoot, image, imageRevisions[image.revision]),
|
|
4348
|
+
compileSymbol
|
|
3951
4349
|
);
|
|
3952
4350
|
scenes.push(scene);
|
|
3953
4351
|
sceneIds.push(scene.id);
|
|
@@ -4052,7 +4450,7 @@ function compileSceneOverview(overview, includeComparison = true) {
|
|
|
4052
4450
|
...includeComparison && overview.comparison ? { comparison: { ...overview.comparison } } : {}
|
|
4053
4451
|
};
|
|
4054
4452
|
}
|
|
4055
|
-
function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections, resolveImage) {
|
|
4453
|
+
function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors, connections, resolveImage, compileSymbol) {
|
|
4056
4454
|
const overview = compileSceneOverview(authoredScene.overview, false);
|
|
4057
4455
|
return {
|
|
4058
4456
|
id: authoredScene.id,
|
|
@@ -4065,7 +4463,7 @@ function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors
|
|
|
4065
4463
|
...overview ? { overview } : {},
|
|
4066
4464
|
steps: authoredScene.steps.map((step) => {
|
|
4067
4465
|
const focus = requireResolvedAnchor(resolvedAnchors, step.focus);
|
|
4068
|
-
const diff2 = available.get(focus.path);
|
|
4466
|
+
const diff2 = available.get(focus.path) || (focus.revision === "base" ? [...available.values()].find((scene) => scene.previousPath === focus.path) : void 0);
|
|
4069
4467
|
if (!diff2) throw new Error(`Step ${step.id} has no text diff for ${focus.path}.`);
|
|
4070
4468
|
return {
|
|
4071
4469
|
id: step.id,
|
|
@@ -4075,6 +4473,7 @@ function buildWalkthroughManifestScene(authoredScene, available, resolvedAnchors
|
|
|
4075
4473
|
requirement: step.requirement,
|
|
4076
4474
|
...step.image ? { image: resolveImage(step.image) } : {},
|
|
4077
4475
|
focus,
|
|
4476
|
+
...step.symbol ? { symbol: compileSymbol(step, focus) } : {},
|
|
4078
4477
|
connection: step.connection ? connections.get(step.connection) : void 0,
|
|
4079
4478
|
diff: { ...diff2, id: `${authoredScene.id}-${step.id}` }
|
|
4080
4479
|
};
|
|
@@ -4094,20 +4493,20 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, a
|
|
|
4094
4493
|
stageId: stage.id
|
|
4095
4494
|
}))];
|
|
4096
4495
|
const materializablePaths = new Set(compiled.baselineFiles.map((file) => file.path));
|
|
4097
|
-
const includedPaths = availableFilePaths.length > 0 ? availableFilePaths.filter((
|
|
4098
|
-
const missingIntroducedPath = compiled.stages.flatMap((stage) => stage.introducedFiles).find((
|
|
4496
|
+
const includedPaths = availableFilePaths.length > 0 ? availableFilePaths.filter((filePath2) => materializablePaths.has(filePath2)).sort() : [...materializablePaths].sort();
|
|
4497
|
+
const missingIntroducedPath = compiled.stages.flatMap((stage) => stage.introducedFiles).find((filePath2) => !includedPaths.includes(filePath2));
|
|
4099
4498
|
if (missingIntroducedPath) {
|
|
4100
4499
|
throw new Error(`Deconstructed tour file is not renderable: ${missingIntroducedPath}`);
|
|
4101
4500
|
}
|
|
4102
4501
|
const introducedPaths = new Set(compiled.stages.flatMap((stage) => stage.introducedFiles));
|
|
4103
4502
|
let totalContentBytes = 0;
|
|
4104
|
-
const files = includedPaths.flatMap((
|
|
4503
|
+
const files = includedPaths.flatMap((filePath2) => {
|
|
4105
4504
|
const states = [
|
|
4106
|
-
compiled.baselineFiles.find((file) => file.path ===
|
|
4107
|
-
...compiled.stages.map((stage) => stage.files.find((file) => file.path ===
|
|
4505
|
+
compiled.baselineFiles.find((file) => file.path === filePath2),
|
|
4506
|
+
...compiled.stages.map((stage) => stage.files.find((file) => file.path === filePath2))
|
|
4108
4507
|
];
|
|
4109
4508
|
if (states.some((state) => !state)) {
|
|
4110
|
-
throw new Error(`Deconstructed scene ${source.id} is missing virtual state for ${
|
|
4509
|
+
throw new Error(`Deconstructed scene ${source.id} is missing virtual state for ${filePath2}.`);
|
|
4111
4510
|
}
|
|
4112
4511
|
const fileContentBytes = states.reduce((total, state) => total + Buffer.byteLength((state == null ? void 0 : state.content) || "", "utf8"), 0);
|
|
4113
4512
|
const exceedsFileLimit = states.some((state) => {
|
|
@@ -4116,20 +4515,20 @@ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, a
|
|
|
4116
4515
|
});
|
|
4117
4516
|
const exceedsSceneLimit = totalContentBytes + fileContentBytes > DEFAULT_MAX_STACK_CONTENT_BYTES;
|
|
4118
4517
|
if (exceedsFileLimit || exceedsSceneLimit) {
|
|
4119
|
-
if (introducedPaths.has(
|
|
4120
|
-
throw new Error(exceedsFileLimit ? `Deconstructed tour file is too large to present: ${
|
|
4518
|
+
if (introducedPaths.has(filePath2)) {
|
|
4519
|
+
throw new Error(exceedsFileLimit ? `Deconstructed tour file is too large to present: ${filePath2}` : `Deconstructed tour content exceeds ${DEFAULT_MAX_STACK_CONTENT_BYTES} bytes.`);
|
|
4121
4520
|
}
|
|
4122
4521
|
return [];
|
|
4123
4522
|
}
|
|
4124
4523
|
totalContentBytes += fileContentBytes;
|
|
4125
4524
|
return [{
|
|
4126
|
-
path:
|
|
4525
|
+
path: filePath2,
|
|
4127
4526
|
panels: states.map((state, index) => {
|
|
4128
|
-
if (!state) throw new Error(`Missing virtual state for ${
|
|
4527
|
+
if (!state) throw new Error(`Missing virtual state for ${filePath2}.`);
|
|
4129
4528
|
return {
|
|
4130
4529
|
id: panels[index].id,
|
|
4131
|
-
label: `${panels[index].label} / ${
|
|
4132
|
-
path: state.exists ?
|
|
4530
|
+
label: `${panels[index].label} / ${filePath2}${state.exists ? "" : " (absent)"}`,
|
|
4531
|
+
path: state.exists ? filePath2 : void 0,
|
|
4133
4532
|
content: state.content,
|
|
4134
4533
|
exists: state.exists
|
|
4135
4534
|
};
|
|
@@ -4223,7 +4622,7 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
4223
4622
|
oid: runGitText(repoRoot, ["rev-parse", "--verify", `${entry.ref}^{commit}`]),
|
|
4224
4623
|
label: entry.label || entry.ref
|
|
4225
4624
|
}));
|
|
4226
|
-
const adjacentChanges = stack.slice(0, -1).map((entry, index) => parseNameStatusZ((0,
|
|
4625
|
+
const adjacentChanges = stack.slice(0, -1).map((entry, index) => parseNameStatusZ((0, import_child_process5.execFileSync)("git", [
|
|
4227
4626
|
"diff",
|
|
4228
4627
|
"--name-status",
|
|
4229
4628
|
"-z",
|
|
@@ -4247,8 +4646,8 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
4247
4646
|
...source.steps.map((step) => step.file)
|
|
4248
4647
|
]);
|
|
4249
4648
|
let totalContentBytes = 0;
|
|
4250
|
-
const files = filePaths.flatMap((
|
|
4251
|
-
const aliases = /* @__PURE__ */ new Set([
|
|
4649
|
+
const files = filePaths.flatMap((filePath2) => {
|
|
4650
|
+
const aliases = /* @__PURE__ */ new Set([filePath2]);
|
|
4252
4651
|
for (const changes of [...adjacentChanges].reverse()) {
|
|
4253
4652
|
for (const change of changes) {
|
|
4254
4653
|
if (aliases.has(change.path) || change.previousPath && aliases.has(change.previousPath)) {
|
|
@@ -4261,15 +4660,15 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
4261
4660
|
const panels = [];
|
|
4262
4661
|
for (const entry of stack) {
|
|
4263
4662
|
const candidates = [...aliases].filter((candidate) => gitBlobExists(repoRoot, entry.oid, candidate));
|
|
4264
|
-
if (candidates.length > 1) throw new Error(`Ambiguous revision paths for ${
|
|
4663
|
+
if (candidates.length > 1) throw new Error(`Ambiguous revision paths for ${filePath2} at ${entry.ref}: ${candidates.join(", ")}`);
|
|
4265
4664
|
const resolvedPath = candidates[0];
|
|
4266
4665
|
if (!resolvedPath) {
|
|
4267
|
-
panels.push({ id: entry.id, label: `${entry.label} / ${
|
|
4666
|
+
panels.push({ id: entry.id, label: `${entry.label} / ${filePath2} (missing)`, content: "", exists: false });
|
|
4268
4667
|
continue;
|
|
4269
4668
|
}
|
|
4270
4669
|
const content = readGitText2(repoRoot, entry.oid, resolvedPath, DEFAULT_MAX_TOUR_FILE_BYTES, DEFAULT_MAX_TOUR_LINE_BYTES);
|
|
4271
4670
|
if (content.kind !== "text") {
|
|
4272
|
-
if (requiredFilePaths.has(
|
|
4671
|
+
if (requiredFilePaths.has(filePath2)) {
|
|
4273
4672
|
throw new Error(`Stacked tour file is binary or too large: ${resolvedPath}`);
|
|
4274
4673
|
}
|
|
4275
4674
|
return [];
|
|
@@ -4287,7 +4686,7 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
4287
4686
|
if (totalContentBytes > DEFAULT_MAX_STACK_CONTENT_BYTES) {
|
|
4288
4687
|
throw new Error(`Stacked tour content exceeds ${DEFAULT_MAX_STACK_CONTENT_BYTES} bytes.`);
|
|
4289
4688
|
}
|
|
4290
|
-
return [{ path:
|
|
4689
|
+
return [{ path: filePath2, panels }];
|
|
4291
4690
|
});
|
|
4292
4691
|
if (files.length === 0) throw new Error(`Stacked scene ${source.id} has no materializable text files.`);
|
|
4293
4692
|
const stackIds = stack.map((entry) => entry.id);
|
|
@@ -4327,7 +4726,7 @@ function buildStackedScene(repoRoot, source, availableFilePaths) {
|
|
|
4327
4726
|
}
|
|
4328
4727
|
function gitBlobExists(repoRoot, oid, relativePath) {
|
|
4329
4728
|
try {
|
|
4330
|
-
(0,
|
|
4729
|
+
(0, import_child_process5.execFileSync)("git", ["cat-file", "-e", `${oid}:${relativePath}`], {
|
|
4331
4730
|
cwd: repoRoot,
|
|
4332
4731
|
stdio: ["ignore", "ignore", "ignore"]
|
|
4333
4732
|
});
|
|
@@ -4337,7 +4736,7 @@ function gitBlobExists(repoRoot, oid, relativePath) {
|
|
|
4337
4736
|
}
|
|
4338
4737
|
}
|
|
4339
4738
|
function runGitText(repoRoot, args) {
|
|
4340
|
-
return (0,
|
|
4739
|
+
return (0, import_child_process5.execFileSync)("git", args, {
|
|
4341
4740
|
cwd: repoRoot,
|
|
4342
4741
|
encoding: "utf8",
|
|
4343
4742
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -4420,9 +4819,9 @@ function buildChapters(records, sceneIdByPath) {
|
|
|
4420
4819
|
sceneIds: records.filter(({ chapter }) => chapter.id === definition.id).map(({ scene }) => sceneIdByPath.get(scene.path)).filter((sceneId) => Boolean(sceneId))
|
|
4421
4820
|
}));
|
|
4422
4821
|
}
|
|
4423
|
-
function chapterForPath(
|
|
4424
|
-
const lower =
|
|
4425
|
-
const role = classifyChangeFileRole(
|
|
4822
|
+
function chapterForPath(filePath2) {
|
|
4823
|
+
const lower = filePath2.toLowerCase();
|
|
4824
|
+
const role = classifyChangeFileRole(filePath2);
|
|
4426
4825
|
if (role === "documentation" || lower.includes("architecture")) {
|
|
4427
4826
|
return CHAPTERS.context;
|
|
4428
4827
|
}
|
|
@@ -4492,9 +4891,9 @@ function readGitText2(repoRoot, oid, relativePath, maxBytes, maxLineBytes) {
|
|
|
4492
4891
|
if (!relativePath) {
|
|
4493
4892
|
return { kind: "text", content: "" };
|
|
4494
4893
|
}
|
|
4495
|
-
const size = Number((0,
|
|
4894
|
+
const size = Number((0, import_child_process5.execFileSync)("git", ["cat-file", "-s", `${oid}:${relativePath}`], { cwd: repoRoot, encoding: "utf8" }).trim());
|
|
4496
4895
|
if (size > maxBytes) return { kind: "binary-or-large", content: "" };
|
|
4497
|
-
const content = (0,
|
|
4896
|
+
const content = (0, import_child_process5.execFileSync)("git", ["show", `${oid}:${relativePath}`], {
|
|
4498
4897
|
cwd: repoRoot,
|
|
4499
4898
|
encoding: "buffer",
|
|
4500
4899
|
maxBuffer: Math.max(maxBytes + 1, DEFAULT_MAX_TOUR_FILE_BYTES + 1),
|
|
@@ -4514,15 +4913,15 @@ function hasLineLongerThan(content, maxLineBytes) {
|
|
|
4514
4913
|
}
|
|
4515
4914
|
return content.length - lineStart > maxLineBytes;
|
|
4516
4915
|
}
|
|
4517
|
-
function readGitLineStats(repoRoot, baseOid, headOid,
|
|
4518
|
-
const output = (0,
|
|
4916
|
+
function readGitLineStats(repoRoot, baseOid, headOid, filePath2, previousPath) {
|
|
4917
|
+
const output = (0, import_child_process5.execFileSync)("git", [
|
|
4519
4918
|
"diff",
|
|
4520
4919
|
"--numstat",
|
|
4521
4920
|
"--find-renames",
|
|
4522
4921
|
baseOid,
|
|
4523
4922
|
headOid,
|
|
4524
4923
|
"--",
|
|
4525
|
-
...[previousPath,
|
|
4924
|
+
...[previousPath, filePath2].filter((candidate) => Boolean(candidate))
|
|
4526
4925
|
], {
|
|
4527
4926
|
cwd: repoRoot,
|
|
4528
4927
|
encoding: "utf8",
|
|
@@ -4560,10 +4959,10 @@ function pinTourSource(source, repoRoot, resolved, baseOid, headOid) {
|
|
|
4560
4959
|
return copy;
|
|
4561
4960
|
}
|
|
4562
4961
|
function compileTourImage(repoRoot, source, revision) {
|
|
4563
|
-
const
|
|
4962
|
+
const object2 = `${revision}:${source.file}`;
|
|
4564
4963
|
let size;
|
|
4565
4964
|
try {
|
|
4566
|
-
size = Number((0,
|
|
4965
|
+
size = Number((0, import_child_process5.execFileSync)("git", ["cat-file", "-s", object2], {
|
|
4567
4966
|
cwd: repoRoot,
|
|
4568
4967
|
encoding: "utf8",
|
|
4569
4968
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -4574,7 +4973,7 @@ function compileTourImage(repoRoot, source, revision) {
|
|
|
4574
4973
|
if (!Number.isSafeInteger(size) || size < 33 || size > MAX_TOUR_IMAGE_BYTES) {
|
|
4575
4974
|
throw new Error(`Image ${source.file} must be a PNG of at most 8 MiB.`);
|
|
4576
4975
|
}
|
|
4577
|
-
const bytes = (0,
|
|
4976
|
+
const bytes = (0, import_child_process5.execFileSync)("git", ["cat-file", "blob", object2], { cwd: repoRoot, maxBuffer: MAX_TOUR_IMAGE_BYTES });
|
|
4578
4977
|
validatePngHeader(bytes, source.file);
|
|
4579
4978
|
return { path: source.file, revision, alt: source.alt, dataUrl: `data:image/png;base64,${bytes.toString("base64")}` };
|
|
4580
4979
|
}
|