ultra-builder-pro-cli 0.26.2 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ultra-template/contexts/TEMPLATE.md +64 -29
- package/.ultra-template/north-star.md +35 -18
- package/.ultra-template/tasks.json +1 -0
- package/.ultra-template/test-report.json +17 -1
- package/CHANGELOG.md +163 -1
- package/README.md +108 -38
- package/adapters/_shared/host-profile.cjs +68 -12
- package/adapters/_shared/plugin-core.cjs +4 -5
- package/adapters/_shared/runtime-assets.cjs +11 -2
- package/adapters/zcode.js +375 -0
- package/bin/delegate-worker.cjs +20 -0
- package/bin/delegate.cjs +94 -25
- package/bin/install.js +11 -6
- package/docs/ARCHITECTURE.md +84 -23
- package/docs/ARTIFACT-AUTHORITY.md +167 -22
- package/docs/DECISIONS.md +131 -17
- package/docs/PHILOSOPHY.md +58 -7
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +6 -0
- package/docs/RUNTIME-COMPAT-MATRIX.md +106 -13
- package/docs/SKILL-AUTHORING.md +4 -3
- package/docs/WORKFLOW-LIFECYCLE.md +126 -28
- package/docs/evals/adversarial-review-2026-08-14.md +84 -0
- package/docs/evals/zcode-automation-2026-08-14.md +103 -0
- package/hooks/README.md +27 -4
- package/hooks/_common.py +1678 -61
- package/hooks/adapters/zcode.py +227 -0
- package/hooks/compact_context.py +176 -17
- package/hooks/mid_workflow_recall.py +27 -5
- package/hooks/post_edit_guard.py +133 -23
- package/hooks/session_context.py +707 -22
- package/package.json +6 -3
- package/skills/ultra-change/SKILL.md +40 -9
- package/skills/ultra-change/references/change-contract.md +93 -27
- package/skills/ultra-change/references/execution-grant.md +109 -0
- package/skills/ultra-change/references/primary-transfer.md +273 -0
- package/skills/ultra-change/scripts/validate_primary_transfer.cjs +1928 -0
- package/skills/ultra-delegate/SKILL.md +24 -2
- package/skills/ultra-delegate/references/delegation-contract.md +21 -1
- package/skills/ultra-deliver/SKILL.md +85 -11
- package/skills/ultra-dev/SKILL.md +171 -41
- package/skills/ultra-grilling/SKILL.md +10 -7
- package/skills/ultra-grilling/references/reframing.md +7 -3
- package/skills/ultra-init/SKILL.md +44 -7
- package/skills/ultra-init/scripts/init_project.cjs +968 -21
- package/skills/ultra-plan/SKILL.md +70 -44
- package/skills/ultra-plan/references/task-evidence-v2.md +300 -0
- package/skills/ultra-plan/scripts/validate_task_evidence.cjs +859 -0
- package/skills/ultra-research/SKILL.md +38 -10
- package/skills/ultra-research/references/04-product-strategy.md +11 -2
- package/skills/ultra-research/references/21-features-scope.md +8 -0
- package/skills/ultra-research/references/22-success-metrics.md +4 -3
- package/skills/ultra-research/references/99-synthesis.md +19 -2
- package/skills/ultra-research/references/north-star-v2.md +168 -0
- package/skills/ultra-research/references/wayfinding.md +8 -0
- package/skills/ultra-research/scripts/validate_north_star.cjs +1174 -0
- package/skills/ultra-review/SKILL.md +201 -27
- package/skills/ultra-review/references/adversarial-evaluation.md +31 -0
- package/skills/ultra-review/references/spec.md +5 -0
- package/skills/ultra-review/references/unified-schema.md +146 -11
- package/skills/ultra-review/references/worker-packet.md +151 -8
- package/skills/ultra-review/scripts/review_wait.py +2653 -63
- package/skills/ultra-status/SKILL.md +95 -11
- package/skills/ultra-tdd/SKILL.md +11 -4
- package/skills/ultra-tdd/references/test-execution.md +6 -1
- package/skills/ultra-test/SKILL.md +145 -17
- package/skills/ultra-test/scripts/validate_review_transport.cjs +549 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +518 -30
|
@@ -6,13 +6,57 @@ const fs = require('node:fs');
|
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const { execFileSync } = require('node:child_process');
|
|
8
8
|
|
|
9
|
+
const MAX_SNAPSHOT_BYTES = 8 * 1024 * 1024;
|
|
10
|
+
const SNAPSHOT_CHUNK_BYTES = 64 * 1024;
|
|
11
|
+
const MAX_OBSERVATION_FILES = 256;
|
|
12
|
+
const MAX_OBSERVATION_BYTES = 16 * 1024 * 1024;
|
|
13
|
+
const GIT_TIMEOUT_MS = 5000;
|
|
14
|
+
const GIT_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
15
|
+
const PRODUCT_PATHSPEC = [
|
|
16
|
+
'--', '.',
|
|
17
|
+
':(exclude).ultra/test-report.json',
|
|
18
|
+
':(exclude).ultra/evidence/**',
|
|
19
|
+
':(exclude).ultra/reviews/**',
|
|
20
|
+
':(exclude).ultra/.runtime/**',
|
|
21
|
+
':(exclude).ultra/progress/**',
|
|
22
|
+
':(exclude).ultra/changes/active/**',
|
|
23
|
+
':(exclude).ultra/changes/archive/**',
|
|
24
|
+
':(exclude).ultra/changes/abandoned/**',
|
|
25
|
+
];
|
|
26
|
+
|
|
9
27
|
function git(cwd, args, encoding = 'utf8') {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
try {
|
|
29
|
+
return execFileSync('git', args, {
|
|
30
|
+
cwd,
|
|
31
|
+
encoding,
|
|
32
|
+
maxBuffer: GIT_MAX_OUTPUT_BYTES,
|
|
33
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
34
|
+
timeout: GIT_TIMEOUT_MS,
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const operation = args.slice(0, 2).join(' ');
|
|
38
|
+
if (error && (error.code === 'ETIMEDOUT' || (error.killed && error.signal))) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`ULTRA_SNAPSHOT_GIT_TIMEOUT: Git ${JSON.stringify(operation)} exceeded the `
|
|
41
|
+
+ `${GIT_TIMEOUT_MS}ms physical observation limit. Restore a responsive repository `
|
|
42
|
+
+ 'and Git process, then retry worktree_digest.',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (error && error.code === 'ENOBUFS') {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`ULTRA_SNAPSHOT_GIT_OUTPUT_LIMIT: Git ${JSON.stringify(operation)} exceeded the `
|
|
48
|
+
+ `${GIT_MAX_OUTPUT_BYTES}-byte output limit. Reduce or split the physical product `
|
|
49
|
+
+ 'observation, then retry worktree_digest.',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const stderr = error && error.stderr
|
|
53
|
+
? Buffer.from(error.stderr).toString('utf8').trim().split('\n')[0]
|
|
54
|
+
: 'Git returned an unknown failure';
|
|
55
|
+
throw new Error(
|
|
56
|
+
`ULTRA_SNAPSHOT_GIT_FAILED: Git ${JSON.stringify(operation)} failed (${stderr}). `
|
|
57
|
+
+ 'Repair the repository and Git operation, then retry worktree_digest.',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
16
60
|
}
|
|
17
61
|
|
|
18
62
|
function optionsFromArgs(argv) {
|
|
@@ -31,58 +75,502 @@ function optionsFromArgs(argv) {
|
|
|
31
75
|
return options;
|
|
32
76
|
}
|
|
33
77
|
|
|
78
|
+
function snapshotError(code, relative, observation) {
|
|
79
|
+
return new Error(
|
|
80
|
+
`${code}: cannot snapshot ${JSON.stringify(relative)}: ${observation}. `
|
|
81
|
+
+ 'Restore the repository path as an ordinary regular non-symlink file within the '
|
|
82
|
+
+ '8 MiB limit, then retry worktree_digest.',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function repositoryPath(root, relative) {
|
|
87
|
+
if (typeof relative !== 'string'
|
|
88
|
+
|| relative.length === 0
|
|
89
|
+
|| relative.includes('\0')
|
|
90
|
+
|| path.posix.isAbsolute(relative)
|
|
91
|
+
|| path.win32.isAbsolute(relative)) {
|
|
92
|
+
throw snapshotError('ULTRA_SNAPSHOT_PATH_ESCAPE', String(relative), 'path is not repository-relative');
|
|
93
|
+
}
|
|
94
|
+
const parts = relative.split('/');
|
|
95
|
+
if (parts.some((part) => part === '' || part === '.' || part === '..')
|
|
96
|
+
|| (path.sep === '\\' && parts.some((part) => part.includes('\\')))) {
|
|
97
|
+
throw snapshotError('ULTRA_SNAPSHOT_PATH_ESCAPE', relative, 'path escapes its repository root');
|
|
98
|
+
}
|
|
99
|
+
const absolute = path.join(root, ...parts);
|
|
100
|
+
const resolvedRelative = path.relative(root, absolute);
|
|
101
|
+
if (resolvedRelative === ''
|
|
102
|
+
|| resolvedRelative === '..'
|
|
103
|
+
|| resolvedRelative.startsWith(`..${path.sep}`)
|
|
104
|
+
|| path.isAbsolute(resolvedRelative)) {
|
|
105
|
+
throw snapshotError('ULTRA_SNAPSHOT_PATH_ESCAPE', relative, 'path escapes its repository root');
|
|
106
|
+
}
|
|
107
|
+
return { absolute, parts };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function lstatSnapshot(absolute, relative) {
|
|
111
|
+
try {
|
|
112
|
+
return fs.lstatSync(absolute, { bigint: true });
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
|
|
115
|
+
throw snapshotError('ULTRA_SNAPSHOT_MISSING', relative, 'path disappeared during observation');
|
|
116
|
+
}
|
|
117
|
+
throw snapshotError(
|
|
118
|
+
'ULTRA_SNAPSHOT_UNREADABLE',
|
|
119
|
+
relative,
|
|
120
|
+
`lstat failed with ${error && error.code ? error.code : 'an unknown error'}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function sameNode(left, right) {
|
|
126
|
+
return left.dev === right.dev
|
|
127
|
+
&& left.ino === right.ino
|
|
128
|
+
&& left.mode === right.mode;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function sameFileVersion(left, right) {
|
|
132
|
+
return sameNode(left, right)
|
|
133
|
+
&& left.size === right.size
|
|
134
|
+
&& left.mtimeNs === right.mtimeNs
|
|
135
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function walkSnapshotPath(root, relative) {
|
|
139
|
+
const resolved = repositoryPath(root, relative);
|
|
140
|
+
const parents = [];
|
|
141
|
+
let current = root;
|
|
142
|
+
const rootStat = lstatSnapshot(root, '.');
|
|
143
|
+
if (rootStat.isSymbolicLink()) {
|
|
144
|
+
throw snapshotError('ULTRA_SNAPSHOT_SYMLINK', relative, 'repository root is a symlink');
|
|
145
|
+
}
|
|
146
|
+
if (!rootStat.isDirectory()) {
|
|
147
|
+
throw snapshotError('ULTRA_SNAPSHOT_NOT_DIRECTORY', relative, 'repository root is not a directory');
|
|
148
|
+
}
|
|
149
|
+
parents.push({ absolute: root, relative: '.', stat: rootStat });
|
|
150
|
+
|
|
151
|
+
for (const part of resolved.parts.slice(0, -1)) {
|
|
152
|
+
current = path.join(current, part);
|
|
153
|
+
const observedRelative = path.relative(root, current) || '.';
|
|
154
|
+
const stat = lstatSnapshot(current, observedRelative);
|
|
155
|
+
if (stat.isSymbolicLink()) {
|
|
156
|
+
throw snapshotError(
|
|
157
|
+
'ULTRA_SNAPSHOT_SYMLINK',
|
|
158
|
+
relative,
|
|
159
|
+
`parent ${JSON.stringify(observedRelative)} is a symlink`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (!stat.isDirectory()) {
|
|
163
|
+
throw snapshotError(
|
|
164
|
+
'ULTRA_SNAPSHOT_NOT_DIRECTORY',
|
|
165
|
+
relative,
|
|
166
|
+
`parent ${JSON.stringify(observedRelative)} is not a directory`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
parents.push({ absolute: current, relative: observedRelative, stat });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const finalStat = lstatSnapshot(resolved.absolute, relative);
|
|
173
|
+
if (finalStat.isSymbolicLink()) {
|
|
174
|
+
throw snapshotError('ULTRA_SNAPSHOT_SYMLINK', relative, 'final entry is a symlink');
|
|
175
|
+
}
|
|
176
|
+
if (!finalStat.isFile()) {
|
|
177
|
+
throw snapshotError('ULTRA_SNAPSHOT_NOT_REGULAR', relative, 'final entry is not a regular file');
|
|
178
|
+
}
|
|
179
|
+
if (finalStat.size > BigInt(MAX_SNAPSHOT_BYTES)) {
|
|
180
|
+
throw snapshotError(
|
|
181
|
+
'ULTRA_SNAPSHOT_TOO_LARGE',
|
|
182
|
+
relative,
|
|
183
|
+
`regular file exceeds the ${MAX_SNAPSHOT_BYTES}-byte physical ceiling`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
absolute: resolved.absolute,
|
|
188
|
+
final: { absolute: resolved.absolute, relative, stat: finalStat },
|
|
189
|
+
parents,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function verifyFreshRewalk(root, relative, initial, phase) {
|
|
194
|
+
let current;
|
|
195
|
+
try {
|
|
196
|
+
current = walkSnapshotPath(root, relative);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
throw snapshotError(
|
|
199
|
+
'ULTRA_SNAPSHOT_REPLACED',
|
|
200
|
+
relative,
|
|
201
|
+
`${phase} rewalk no longer resolves the initial identities (${error.message})`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const parentsStable = current.parents.length === initial.parents.length
|
|
205
|
+
&& current.parents.every((entry, index) => sameNode(entry.stat, initial.parents[index].stat));
|
|
206
|
+
if (!parentsStable || !sameFileVersion(current.final.stat, initial.final.stat)) {
|
|
207
|
+
throw snapshotError(
|
|
208
|
+
'ULTRA_SNAPSHOT_REPLACED',
|
|
209
|
+
relative,
|
|
210
|
+
`${phase} rewalk observed a replaced root, parent, or final identity`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function openSnapshotFile(absolute, relative) {
|
|
216
|
+
const { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } = fs.constants;
|
|
217
|
+
if (![O_NOFOLLOW, O_NONBLOCK, O_RDONLY].every(Number.isInteger)) {
|
|
218
|
+
throw snapshotError(
|
|
219
|
+
'ULTRA_SNAPSHOT_UNSUPPORTED',
|
|
220
|
+
relative,
|
|
221
|
+
'this host cannot enforce O_NONBLOCK and O_NOFOLLOW',
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
return fs.openSync(absolute, O_RDONLY | O_NONBLOCK | O_NOFOLLOW);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
const observed = error && error.code ? error.code : 'an unknown error';
|
|
228
|
+
if (['ELOOP', 'ENOENT', 'ENOTDIR', 'ENXIO'].includes(observed)) {
|
|
229
|
+
throw snapshotError(
|
|
230
|
+
'ULTRA_SNAPSHOT_REPLACED',
|
|
231
|
+
relative,
|
|
232
|
+
`no-follow nonblocking open observed ${observed}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
throw snapshotError(
|
|
236
|
+
'ULTRA_SNAPSHOT_UNREADABLE',
|
|
237
|
+
relative,
|
|
238
|
+
`no-follow nonblocking open failed with ${observed}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function streamStableRepositoryFile(root, relative, consume, budget = null) {
|
|
244
|
+
const initial = walkSnapshotPath(root, relative);
|
|
245
|
+
if (budget) budget.reserve(relative, initial.final.stat.size);
|
|
246
|
+
let descriptor;
|
|
247
|
+
try {
|
|
248
|
+
descriptor = openSnapshotFile(initial.absolute, relative);
|
|
249
|
+
const opened = fs.fstatSync(descriptor, { bigint: true });
|
|
250
|
+
if (!opened.isFile() || !sameFileVersion(opened, initial.final.stat)) {
|
|
251
|
+
throw snapshotError(
|
|
252
|
+
'ULTRA_SNAPSHOT_REPLACED',
|
|
253
|
+
relative,
|
|
254
|
+
'opened descriptor does not match the initial regular-file identity',
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
verifyFreshRewalk(root, relative, initial, 'pre-read');
|
|
258
|
+
|
|
259
|
+
const buffer = Buffer.allocUnsafe(SNAPSHOT_CHUNK_BYTES);
|
|
260
|
+
let offset = 0;
|
|
261
|
+
for (;;) {
|
|
262
|
+
const remaining = MAX_SNAPSHOT_BYTES - offset;
|
|
263
|
+
const requested = Math.min(buffer.length, remaining + 1);
|
|
264
|
+
let bytesRead;
|
|
265
|
+
try {
|
|
266
|
+
bytesRead = fs.readSync(descriptor, buffer, 0, requested, offset);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
throw snapshotError(
|
|
269
|
+
'ULTRA_SNAPSHOT_UNREADABLE',
|
|
270
|
+
relative,
|
|
271
|
+
`stream read failed with ${error && error.code ? error.code : 'an unknown error'}`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (bytesRead === 0) break;
|
|
275
|
+
offset += bytesRead;
|
|
276
|
+
if (offset > MAX_SNAPSHOT_BYTES) {
|
|
277
|
+
throw snapshotError(
|
|
278
|
+
'ULTRA_SNAPSHOT_TOO_LARGE',
|
|
279
|
+
relative,
|
|
280
|
+
`stream exceeded the ${MAX_SNAPSHOT_BYTES}-byte physical ceiling`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
consume(buffer.subarray(0, bytesRead));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const finished = fs.fstatSync(descriptor, { bigint: true });
|
|
287
|
+
if (!finished.isFile()
|
|
288
|
+
|| !sameFileVersion(finished, initial.final.stat)
|
|
289
|
+
|| BigInt(offset) !== finished.size) {
|
|
290
|
+
throw snapshotError(
|
|
291
|
+
'ULTRA_SNAPSHOT_REPLACED',
|
|
292
|
+
relative,
|
|
293
|
+
'file identity or byte extent changed during the streamed read',
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
verifyFreshRewalk(root, relative, initial, 'post-read');
|
|
297
|
+
return initial;
|
|
298
|
+
} finally {
|
|
299
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function resourceError(relative, nextFiles, nextBytes) {
|
|
304
|
+
return new Error(
|
|
305
|
+
`ULTRA_SNAPSHOT_RESOURCE_LIMIT: cannot snapshot ${JSON.stringify(relative)}: aggregate `
|
|
306
|
+
+ `product observation would reach ${nextFiles} files and ${nextBytes} bytes, above the `
|
|
307
|
+
+ `${MAX_OBSERVATION_FILES}-file or ${MAX_OBSERVATION_BYTES}-byte physical ceiling. `
|
|
308
|
+
+ 'Reduce or split the included physical product observation, then retry worktree_digest; '
|
|
309
|
+
+ 'this ceiling is not a semantic quality verdict.',
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function observationBudget() {
|
|
314
|
+
let files = 0;
|
|
315
|
+
let bytes = 0;
|
|
316
|
+
return {
|
|
317
|
+
reserve(relative, size) {
|
|
318
|
+
const nextFiles = files + 1;
|
|
319
|
+
const nextBytes = bytes + Number(size);
|
|
320
|
+
if (nextFiles > MAX_OBSERVATION_FILES || nextBytes > MAX_OBSERVATION_BYTES) {
|
|
321
|
+
throw resourceError(relative, nextFiles, nextBytes);
|
|
322
|
+
}
|
|
323
|
+
files = nextFiles;
|
|
324
|
+
bytes = nextBytes;
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function candidateExists(file) {
|
|
330
|
+
try {
|
|
331
|
+
fs.lstatSync(file);
|
|
332
|
+
return true;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return false;
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
34
339
|
function intentSnapshot(root, changeId) {
|
|
35
340
|
if (!changeId) return { change_id: null, intent_digest: null };
|
|
36
341
|
const matches = ['active', 'archive', 'abandoned']
|
|
37
342
|
.map((state) => path.join(root, '.ultra', 'changes', state, changeId, 'intent.md'))
|
|
38
|
-
.filter(
|
|
343
|
+
.filter(candidateExists);
|
|
39
344
|
if (matches.length !== 1) {
|
|
40
345
|
throw new Error(`expected exactly one intent.md for Change ${changeId}; found ${matches.length}`);
|
|
41
346
|
}
|
|
42
|
-
const
|
|
347
|
+
const intentHash = crypto.createHash('sha256');
|
|
348
|
+
const relative = path.relative(root, matches[0]).split(path.sep).join('/');
|
|
349
|
+
const snapshot = streamStableRepositoryFile(root, relative, (chunk) => intentHash.update(chunk));
|
|
43
350
|
return {
|
|
44
351
|
change_id: changeId,
|
|
45
|
-
intent_digest:
|
|
352
|
+
intent_digest: intentHash.digest('hex'),
|
|
353
|
+
relative,
|
|
354
|
+
identity: snapshotIdentity(snapshot),
|
|
46
355
|
};
|
|
47
356
|
}
|
|
48
357
|
|
|
358
|
+
function snapshotIdentity(snapshot) {
|
|
359
|
+
const statIdentity = (stat) => [
|
|
360
|
+
stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeNs, stat.ctimeNs,
|
|
361
|
+
].map((value) => value.toString()).join(':');
|
|
362
|
+
return [
|
|
363
|
+
...snapshot.parents.map((entry) => statIdentity(entry.stat)),
|
|
364
|
+
statIdentity(snapshot.final.stat),
|
|
365
|
+
].join('|');
|
|
366
|
+
}
|
|
367
|
+
|
|
49
368
|
function excluded(file) {
|
|
50
369
|
return file === '.ultra/test-report.json'
|
|
370
|
+
|| /^\.ultra\/evidence(?:\/|$)/.test(file)
|
|
371
|
+
|| /^\.ultra\/(?:reviews|\.runtime|progress)(?:\/|$)/.test(file)
|
|
51
372
|
|| /^\.ultra\/changes\/(?:active|archive|abandoned)(?:\/|$)/.test(file);
|
|
52
373
|
}
|
|
53
374
|
|
|
375
|
+
function changedDuringObservation(observation) {
|
|
376
|
+
return new Error(
|
|
377
|
+
`ULTRA_SNAPSHOT_CHANGED_DURING_OBSERVATION: ${observation}. `
|
|
378
|
+
+ 'Finish or restore the concurrent repository change, then retry worktree_digest once; '
|
|
379
|
+
+ 'no digest was accepted.',
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function splitNul(buffer, label) {
|
|
384
|
+
if (buffer.length === 0) return [];
|
|
385
|
+
if (buffer[buffer.length - 1] !== 0) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`ULTRA_SNAPSHOT_GIT_FAILED: ${label} was not NUL terminated. `
|
|
388
|
+
+ 'Repair the Git observation, then retry worktree_digest.',
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const values = [];
|
|
392
|
+
let start = 0;
|
|
393
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
394
|
+
if (buffer[index] !== 0) continue;
|
|
395
|
+
values.push(buffer.subarray(start, index));
|
|
396
|
+
start = index + 1;
|
|
397
|
+
}
|
|
398
|
+
return values;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function decodeGitPath(bytes, label) {
|
|
402
|
+
const relative = bytes.toString('utf8');
|
|
403
|
+
if (!Buffer.from(relative, 'utf8').equals(bytes)) {
|
|
404
|
+
throw snapshotError(
|
|
405
|
+
'ULTRA_SNAPSHOT_PATH_ENCODING',
|
|
406
|
+
label,
|
|
407
|
+
'Git returned a path that is not valid UTF-8',
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return relative;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function trackedManifest(root, head) {
|
|
414
|
+
const raw = git(root, [
|
|
415
|
+
'diff', '--raw', '-z', '--no-renames', '--abbrev=64', head, ...PRODUCT_PATHSPEC,
|
|
416
|
+
], 'buffer');
|
|
417
|
+
const fields = splitNul(raw, 'tracked manifest');
|
|
418
|
+
if (fields.length % 2 !== 0) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
'ULTRA_SNAPSHOT_GIT_FAILED: tracked manifest has an incomplete raw record. '
|
|
421
|
+
+ 'Repair the Git observation, then retry worktree_digest.',
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const files = [];
|
|
425
|
+
for (let index = 0; index < fields.length; index += 2) {
|
|
426
|
+
const header = fields[index].toString('ascii');
|
|
427
|
+
const match = header.match(
|
|
428
|
+
/^:([0-7]{6}) ([0-7]{6}) ([0-9a-f]+) ([0-9a-f]+) ([A-Z])(?:[0-9]+)?$/u,
|
|
429
|
+
);
|
|
430
|
+
if (!match) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`ULTRA_SNAPSHOT_GIT_FAILED: tracked manifest record ${JSON.stringify(header)} is invalid. `
|
|
433
|
+
+ 'Repair the Git observation, then retry worktree_digest.',
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
const relative = decodeGitPath(fields[index + 1], 'tracked manifest path');
|
|
437
|
+
files.push({
|
|
438
|
+
path: relative,
|
|
439
|
+
status: match[5],
|
|
440
|
+
present: match[5] !== 'D',
|
|
441
|
+
old_mode: match[1],
|
|
442
|
+
new_mode: match[2],
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
return { raw, files };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function untrackedManifest(root) {
|
|
449
|
+
const raw = git(root, ['ls-files', '--others', '--exclude-standard', '-z'], 'buffer');
|
|
450
|
+
const entries = splitNul(raw, 'untracked manifest')
|
|
451
|
+
.map((bytes) => ({ bytes, path: decodeGitPath(bytes, 'untracked manifest path') }))
|
|
452
|
+
.filter((entry) => !excluded(entry.path))
|
|
453
|
+
.sort((left, right) => Buffer.compare(left.bytes, right.bytes));
|
|
454
|
+
return {
|
|
455
|
+
files: entries.map((entry) => entry.path),
|
|
456
|
+
raw: Buffer.concat(entries.flatMap((entry) => [entry.bytes, Buffer.from([0])])),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function exactDiff(root, head) {
|
|
461
|
+
return git(root, [
|
|
462
|
+
'diff', '--binary', '--no-ext-diff', head, ...PRODUCT_PATHSPEC,
|
|
463
|
+
], 'buffer');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function verifyObservedPaths(root, records) {
|
|
467
|
+
for (const record of records) {
|
|
468
|
+
try {
|
|
469
|
+
verifyFreshRewalk(root, record.relative, record.snapshot, 'final');
|
|
470
|
+
} catch (error) {
|
|
471
|
+
throw changedDuringObservation(
|
|
472
|
+
`included path ${JSON.stringify(record.relative)} changed after its stable read (${error.message})`,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function sameIntent(left, right) {
|
|
479
|
+
return left.change_id === right.change_id
|
|
480
|
+
&& left.intent_digest === right.intent_digest
|
|
481
|
+
&& left.relative === right.relative
|
|
482
|
+
&& left.identity === right.identity;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function verifyExpectedObservation(root, expected) {
|
|
486
|
+
const observedHead = git(root, ['rev-parse', 'HEAD']).trim();
|
|
487
|
+
const observedTracked = trackedManifest(root, expected.head);
|
|
488
|
+
const observedUntracked = untrackedManifest(root);
|
|
489
|
+
const observedDiff = exactDiff(root, expected.head);
|
|
490
|
+
if (observedHead !== expected.head
|
|
491
|
+
|| !observedTracked.raw.equals(expected.tracked.raw)
|
|
492
|
+
|| !observedUntracked.raw.equals(expected.untracked.raw)
|
|
493
|
+
|| !observedDiff.equals(expected.diff)) {
|
|
494
|
+
throw changedDuringObservation(
|
|
495
|
+
'HEAD, the exact tracked manifest or diff, or the untracked manifest changed during capture',
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const observedIntent = intentSnapshot(root, expected.intent.change_id);
|
|
500
|
+
if (!sameIntent(expected.intent, observedIntent)) {
|
|
501
|
+
throw changedDuringObservation('the Change intent identity or bytes changed during capture');
|
|
502
|
+
}
|
|
503
|
+
verifyObservedPaths(root, expected.records);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function verifyClosingObservation(root, expected) {
|
|
507
|
+
verifyExpectedObservation(root, expected);
|
|
508
|
+
verifyExpectedObservation(root, expected);
|
|
509
|
+
}
|
|
510
|
+
|
|
54
511
|
function main(options) {
|
|
55
512
|
const root = fs.realpathSync(git(options.project, ['rev-parse', '--show-toplevel']).trim());
|
|
56
513
|
const head = git(root, ['rev-parse', 'HEAD']).trim();
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
514
|
+
const tracked = trackedManifest(root, head);
|
|
515
|
+
const untracked = untrackedManifest(root);
|
|
516
|
+
const intent = intentSnapshot(root, options.changeId);
|
|
517
|
+
const budget = observationBudget();
|
|
518
|
+
const records = [];
|
|
519
|
+
|
|
520
|
+
for (const file of tracked.files) {
|
|
521
|
+
if (!file.present) {
|
|
522
|
+
budget.reserve(file.path, 0n);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const snapshot = streamStableRepositoryFile(root, file.path, () => {}, budget);
|
|
526
|
+
records.push({ relative: file.path, snapshot });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const untrackedBytes = [];
|
|
530
|
+
for (const file of untracked.files) {
|
|
531
|
+
const chunks = [];
|
|
532
|
+
const snapshot = streamStableRepositoryFile(
|
|
533
|
+
root,
|
|
534
|
+
file,
|
|
535
|
+
(chunk) => chunks.push(Buffer.from(chunk)),
|
|
536
|
+
budget,
|
|
537
|
+
);
|
|
538
|
+
records.push({ relative: file, snapshot });
|
|
539
|
+
untrackedBytes.push(chunks);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const diff = exactDiff(root, head);
|
|
543
|
+
const closingObservation = { diff, head, intent, records, tracked, untracked };
|
|
544
|
+
verifyClosingObservation(root, closingObservation);
|
|
545
|
+
|
|
68
546
|
const hash = crypto.createHash('sha256');
|
|
69
547
|
hash.update('ultra-worktree-digest-v1\0').update(head).update('\0').update(diff);
|
|
70
|
-
for (
|
|
71
|
-
hash.update(
|
|
548
|
+
for (let index = 0; index < untracked.files.length; index += 1) {
|
|
549
|
+
hash.update(untracked.files[index]).update('\0');
|
|
550
|
+
for (const chunk of untrackedBytes[index]) hash.update(chunk);
|
|
551
|
+
hash.update('\0');
|
|
72
552
|
}
|
|
73
553
|
return {
|
|
74
554
|
$schema: 'ultra-worktree-digest-v1',
|
|
75
|
-
|
|
555
|
+
change_id: intent.change_id,
|
|
556
|
+
intent_digest: intent.intent_digest,
|
|
76
557
|
head,
|
|
77
|
-
dirty: diff.length > 0 || untracked.length > 0,
|
|
558
|
+
dirty: diff.length > 0 || untracked.files.length > 0,
|
|
78
559
|
diff_digest: hash.digest('hex'),
|
|
79
|
-
untracked_files: untracked,
|
|
560
|
+
untracked_files: untracked.files,
|
|
80
561
|
};
|
|
81
562
|
}
|
|
82
563
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
564
|
+
if (require.main === module) {
|
|
565
|
+
try {
|
|
566
|
+
process.stdout.write(`${JSON.stringify(main(optionsFromArgs(process.argv.slice(2))), null, 2)}\n`);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
process.stderr.write(`${error.message}\n`);
|
|
569
|
+
process.exitCode = 1;
|
|
570
|
+
}
|
|
88
571
|
}
|
|
572
|
+
|
|
573
|
+
// The product-subject pathspec and the stable file-snapshot primitive are the
|
|
574
|
+
// single bounded mechanical definitions shared with the primary-transfer
|
|
575
|
+
// validator, so receipt reads and digest subject bytes can never drift.
|
|
576
|
+
module.exports = { PRODUCT_PATHSPEC, streamStableRepositoryFile };
|