ushman-ledger 1.2.0 → 1.2.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/ARCHITECTURE.md +79 -0
- package/README.md +144 -5
- package/TROUBLESHOOTING.md +170 -0
- package/dist/blobs.d.ts +3 -0
- package/dist/blobs.d.ts.map +1 -1
- package/dist/blobs.js +41 -15
- package/dist/builders.d.ts +1 -2
- package/dist/builders.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +231 -70
- package/dist/coverage.d.ts.map +1 -1
- package/dist/coverage.js +3 -2
- package/dist/doctor.d.ts +17 -4
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +225 -58
- package/dist/handle.d.ts +27 -7
- package/dist/handle.d.ts.map +1 -1
- package/dist/handle.js +96 -20
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +23 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/list.d.ts +3 -2
- package/dist/list.d.ts.map +1 -1
- package/dist/list.js +24 -12
- package/dist/note.d.ts +7 -0
- package/dist/note.d.ts.map +1 -1
- package/dist/note.js +6 -0
- package/dist/patch-resolver.d.ts +12 -0
- package/dist/patch-resolver.d.ts.map +1 -1
- package/dist/patch-resolver.js +205 -53
- package/dist/read-index.d.ts.map +1 -1
- package/dist/read-index.js +6 -5
- package/dist/record.d.ts.map +1 -1
- package/dist/record.js +3 -3
- package/dist/render/migration-log.d.ts +8 -1
- package/dist/render/migration-log.d.ts.map +1 -1
- package/dist/render/migration-log.js +40 -33
- package/dist/render/retro.d.ts.map +1 -1
- package/dist/render/retro.js +1 -7
- package/dist/render/workspace-narrative.d.ts +7 -1
- package/dist/render/workspace-narrative.d.ts.map +1 -1
- package/dist/render/workspace-narrative.js +114 -46
- package/dist/runtime-config.d.ts +12 -0
- package/dist/runtime-config.d.ts.map +1 -0
- package/dist/runtime-config.js +83 -0
- package/dist/schema/entry-read.d.ts.map +1 -1
- package/dist/schema/entry-read.js +1 -1
- package/dist/schema/entry-write.d.ts.map +1 -1
- package/dist/schema/entry-write.js +1 -1
- package/dist/schema/entry.d.ts.map +1 -1
- package/dist/storage/filesystem.d.ts +8 -0
- package/dist/storage/filesystem.d.ts.map +1 -1
- package/dist/storage/filesystem.js +110 -5
- package/dist/text-lines.d.ts +8 -0
- package/dist/text-lines.d.ts.map +1 -0
- package/dist/text-lines.js +20 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -1
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -6,14 +6,17 @@ import path from 'node:path';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
import * as v from 'valibot';
|
|
9
|
+
import { readPatchTextFromFile } from "./blobs.js";
|
|
9
10
|
import { openLedger } from "./handle.js";
|
|
10
11
|
import { deriveFilesChangedFromPatch } from "./patch-resolver.js";
|
|
11
12
|
import { ChangeLogParityStatusSchema, ChangeLogSmokeResultSchema, ChangeLogSubkindSchema, LEDGER_KINDS, LEDGER_PHASES, parseLedgerRecord, WorkspaceRelativePathSchema, } from "./schema/entry.js";
|
|
12
13
|
import { NoteSubkindSchema } from "./schema/note.js";
|
|
13
14
|
import { LEDGER_LIBRARY_VERSION } from "./version.js";
|
|
14
15
|
const execFileAsync = promisify(execFile);
|
|
15
|
-
const
|
|
16
|
-
const
|
|
16
|
+
const DEFAULT_GIT_DIFF_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
17
|
+
const DEFAULT_GIT_DIFF_TIMEOUT_MS = 30_000;
|
|
18
|
+
const BOOLEAN_FLAG_NAMES = new Set(['from-stdin', 'help', 'json']);
|
|
19
|
+
const GIT_DIFF_FORMAT_ARGS = ['--no-color', '--no-ext-diff', '--src-prefix=a/', '--dst-prefix=b/'];
|
|
17
20
|
const RENDER_TARGETS = [
|
|
18
21
|
'retro',
|
|
19
22
|
'jsonl',
|
|
@@ -22,6 +25,7 @@ const RENDER_TARGETS = [
|
|
|
22
25
|
'migration-log-md',
|
|
23
26
|
'workspace-narrative-md',
|
|
24
27
|
];
|
|
28
|
+
const GIT_DIFF_FLAG_NAMES = ['git-diff-max-buffer-bytes', 'git-diff-timeout-ms', 'git-paths'];
|
|
25
29
|
const CHANGE_LOG_RECORD_ONLY_FLAGS = [
|
|
26
30
|
'commands',
|
|
27
31
|
'commands-from',
|
|
@@ -34,6 +38,7 @@ const CHANGE_LOG_RECORD_ONLY_FLAGS = [
|
|
|
34
38
|
'smoke-result',
|
|
35
39
|
'subkind',
|
|
36
40
|
];
|
|
41
|
+
const renderRecordUsage = (commandName) => `${commandName} record [--workspace=<ws>] --kind=<kind> --phase=<phase> --summary="..." [--rationale="..."] [--action=<operator-action>] [--check-id=<check-id>] [--diff=<patch-file>] [--diff-from-git=<ref>] [--git-paths=<csv>] [--git-diff-timeout-ms=<ms>] [--git-diff-max-buffer-bytes=<bytes>] [--idempotency-key=<key>] [--subkind=<change-log-subkind>] [--files-changed=<csv>] [--hypothesis="..."] [--commands="cmd1\ncmd2" | --commands-from=<file>] [--smoke-result=<result>] [--smoke-notes="..."] [--parity-status=<status>] [--rollback-plan="..."] [--rolls-back=<entry-id>] [--from-stdin]`;
|
|
37
42
|
class CliUsageError extends Error {
|
|
38
43
|
}
|
|
39
44
|
const DEFAULT_CONTEXT = {
|
|
@@ -52,38 +57,56 @@ const renderValidValues = () => `Valid values:
|
|
|
52
57
|
note subkinds: ${NoteSubkindSchema.options.join(', ')}
|
|
53
58
|
render targets: ${RENDER_TARGETS.join(', ')}
|
|
54
59
|
`;
|
|
60
|
+
const renderRuntimeTuningHelp = () => `Runtime tuning env vars:
|
|
61
|
+
USHMAN_LEDGER_SCAN_BATCH_SIZE (default: 32)
|
|
62
|
+
USHMAN_LEDGER_SCAN_CONCURRENCY (default: 16)
|
|
63
|
+
USHMAN_LEDGER_READ_INDEX_REBUILD_BATCH_SIZE (default: USHMAN_LEDGER_SCAN_BATCH_SIZE)
|
|
64
|
+
USHMAN_LEDGER_READ_INDEX_REBUILD_CONCURRENCY (default: USHMAN_LEDGER_SCAN_CONCURRENCY)
|
|
65
|
+
USHMAN_LEDGER_COVERAGE_FILE_STAT_CONCURRENCY (default: USHMAN_LEDGER_SCAN_CONCURRENCY)
|
|
66
|
+
USHMAN_LEDGER_BLOB_HASH_CONCURRENCY (default: USHMAN_LEDGER_SCAN_CONCURRENCY)
|
|
67
|
+
USHMAN_LEDGER_MAX_PATCH_BYTES (default: 10485760)
|
|
68
|
+
`;
|
|
55
69
|
const renderHelp = (commandName) => `${commandName}
|
|
56
70
|
|
|
57
71
|
Commands:
|
|
58
|
-
${commandName}
|
|
72
|
+
${renderRecordUsage(commandName)}
|
|
59
73
|
${commandName} note <subkind> [--workspace=<ws>] --phase=<phase> --summary="..." [--body=<markdown-file>] [--from-stdin]
|
|
60
74
|
${commandName} list [--workspace=<ws>] [--phase=<phase>] [--kind=<kind>] [--since=<iso>] [--limit=<n>] [--json]
|
|
61
75
|
${commandName} show [--workspace=<ws>] <entry-id>
|
|
62
76
|
${commandName} tail [--workspace=<ws>] [--phase=<phase>] [--limit=<n>]
|
|
63
77
|
${commandName} render [--workspace=<ws>] [--to=retro|jsonl|timeline-html|dependency-graph|migration-log-md|workspace-narrative-md] [--phase=<phase>] [--since=<iso>] [--limit=<n>] [--out=<file>]
|
|
64
78
|
${commandName} archive [--workspace=<ws>] --out=<file.tgz>
|
|
65
|
-
${commandName} doctor [--workspace=<ws>]
|
|
79
|
+
${commandName} doctor [--workspace=<ws>] [--json]
|
|
66
80
|
${commandName} --version
|
|
67
81
|
|
|
68
|
-
${renderValidValues()}
|
|
82
|
+
${renderValidValues()}
|
|
83
|
+
${renderRuntimeTuningHelp()}`;
|
|
69
84
|
const renderCommandHelp = (commandName, command) => {
|
|
70
85
|
switch (command) {
|
|
71
86
|
case 'record':
|
|
72
|
-
return `${commandName}
|
|
87
|
+
return `${renderRecordUsage(commandName)}
|
|
73
88
|
|
|
74
|
-
${renderValidValues()}
|
|
89
|
+
${renderValidValues()}
|
|
90
|
+
${renderRuntimeTuningHelp()}`;
|
|
75
91
|
case 'note':
|
|
76
92
|
return `${commandName} note <subkind> [--workspace=<ws>] --phase=<phase> --summary="..." [--body=<markdown-file>] [--from-stdin]
|
|
77
93
|
|
|
78
|
-
${renderValidValues()}
|
|
94
|
+
${renderValidValues()}
|
|
95
|
+
${renderRuntimeTuningHelp()}`;
|
|
79
96
|
case 'list':
|
|
80
97
|
return `${commandName} list [--workspace=<ws>] [--phase=<phase>] [--kind=<kind>] [--since=<iso>] [--limit=<n>] [--json]
|
|
81
98
|
|
|
82
|
-
${renderValidValues()}
|
|
99
|
+
${renderValidValues()}
|
|
100
|
+
${renderRuntimeTuningHelp()}`;
|
|
83
101
|
case 'render':
|
|
84
102
|
return `${commandName} render [--workspace=<ws>] [--to=<target>] [--phase=<phase>] [--since=<iso>] [--limit=<n>] [--out=<file>]
|
|
85
103
|
|
|
86
|
-
${renderValidValues()}
|
|
104
|
+
${renderValidValues()}
|
|
105
|
+
${renderRuntimeTuningHelp()}`;
|
|
106
|
+
case 'doctor':
|
|
107
|
+
return `${commandName} doctor [--workspace=<ws>] [--json]
|
|
108
|
+
|
|
109
|
+
${renderRuntimeTuningHelp()}`;
|
|
87
110
|
default:
|
|
88
111
|
return renderHelp(commandName);
|
|
89
112
|
}
|
|
@@ -109,7 +132,7 @@ const parseArgv = (argv) => {
|
|
|
109
132
|
continue;
|
|
110
133
|
}
|
|
111
134
|
const next = argv[index + 1];
|
|
112
|
-
if (!next || next.startsWith('--')) {
|
|
135
|
+
if (BOOLEAN_FLAG_NAMES.has(name) || !next || next.startsWith('--')) {
|
|
113
136
|
flags[name] = true;
|
|
114
137
|
continue;
|
|
115
138
|
}
|
|
@@ -159,13 +182,84 @@ const ensureFileExists = async (filePath, flagName) => {
|
|
|
159
182
|
throw error;
|
|
160
183
|
}
|
|
161
184
|
};
|
|
162
|
-
const
|
|
185
|
+
const getErrorCode = (error) => typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
|
|
186
|
+
const parsePositiveIntegerFlag = ({ defaultValue, flagName, flags, }) => {
|
|
187
|
+
const raw = getFlag(flags, flagName);
|
|
188
|
+
if (!raw) {
|
|
189
|
+
return defaultValue;
|
|
190
|
+
}
|
|
191
|
+
if (!/^[1-9]\d*$/u.test(raw)) {
|
|
192
|
+
throw new CliUsageError(`Invalid --${flagName} value: ${raw}. Expected a positive integer.`);
|
|
193
|
+
}
|
|
194
|
+
return Number.parseInt(raw, 10);
|
|
195
|
+
};
|
|
196
|
+
const parseWorkspaceRelativePathCsv = ({ flagName, raw }) => {
|
|
197
|
+
const uniquePaths = [
|
|
198
|
+
...new Set(raw
|
|
199
|
+
.split(',')
|
|
200
|
+
.map((value) => value.trim())
|
|
201
|
+
.filter((value) => value.length > 0)
|
|
202
|
+
.map((filePath) => {
|
|
203
|
+
try {
|
|
204
|
+
return v.parse(WorkspaceRelativePathSchema, filePath);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
throw new CliUsageError(`--${flagName} path is not a normalized workspace-relative path: ${filePath}`);
|
|
208
|
+
}
|
|
209
|
+
})),
|
|
210
|
+
];
|
|
211
|
+
if (uniquePaths.length === 0) {
|
|
212
|
+
throw new CliUsageError(`--${flagName} must include at least one workspace-relative path.`);
|
|
213
|
+
}
|
|
214
|
+
return uniquePaths;
|
|
215
|
+
};
|
|
216
|
+
const isGitDiffMaxBufferError = (error) => {
|
|
217
|
+
const code = getErrorCode(error);
|
|
218
|
+
if (code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
return error instanceof RangeError && error.message.includes('maxBuffer');
|
|
222
|
+
};
|
|
223
|
+
const isGitDiffTimeoutError = (error) => {
|
|
224
|
+
const code = getErrorCode(error);
|
|
225
|
+
if (code === 'ETIMEDOUT') {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return (typeof error === 'object' &&
|
|
229
|
+
error !== null &&
|
|
230
|
+
'killed' in error &&
|
|
231
|
+
'signal' in error &&
|
|
232
|
+
error.killed === true &&
|
|
233
|
+
error.signal === 'SIGTERM');
|
|
234
|
+
};
|
|
235
|
+
const buildGitDiffArgs = (gitOptions, gitRef) => gitOptions.scopedPaths.length === 0
|
|
236
|
+
? ['diff', ...GIT_DIFF_FORMAT_ARGS, gitRef]
|
|
237
|
+
: ['diff', ...GIT_DIFF_FORMAT_ARGS, gitRef, '--', ...gitOptions.scopedPaths];
|
|
238
|
+
const throwGitDiffFailure = ({ error, gitOptions, gitRef, }) => {
|
|
239
|
+
if (getErrorCode(error) === 'ENOENT') {
|
|
240
|
+
throw new CliUsageError('git is required for --diff-from-git and was not found in PATH. Install git or use --diff with a patch file.');
|
|
241
|
+
}
|
|
242
|
+
if (isGitDiffTimeoutError(error)) {
|
|
243
|
+
throw new CliUsageError(`git diff ${gitRef} timed out after ${gitOptions.timeoutMs}ms. Narrow the diff or increase --git-diff-timeout-ms.`);
|
|
244
|
+
}
|
|
245
|
+
if (isGitDiffMaxBufferError(error)) {
|
|
246
|
+
throw new CliUsageError(`git diff ${gitRef} exceeded the configured stdout buffer (${gitOptions.maxBufferBytes} bytes). Narrow the diff or increase --git-diff-max-buffer-bytes.`);
|
|
247
|
+
}
|
|
248
|
+
const stderr = typeof error === 'object' && error !== null && 'stderr' in error
|
|
249
|
+
? String(error.stderr ?? '').trim()
|
|
250
|
+
: '';
|
|
251
|
+
if (stderr.length > 0) {
|
|
252
|
+
throw new CliUsageError(`git diff ${gitRef} failed: ${stderr}`);
|
|
253
|
+
}
|
|
254
|
+
throw new CliUsageError(`git diff ${gitRef} failed. Ensure the ref exists and the workspace is a git repository.`);
|
|
255
|
+
};
|
|
256
|
+
const materializeGitDiff = async ({ gitOptions, gitRef, workspaceRoot, }) => {
|
|
163
257
|
let tempDir;
|
|
164
258
|
try {
|
|
165
|
-
const { stdout } = await execFileAsync('git',
|
|
259
|
+
const { stdout } = await execFileAsync('git', buildGitDiffArgs(gitOptions, gitRef), {
|
|
166
260
|
cwd: workspaceRoot,
|
|
167
|
-
maxBuffer:
|
|
168
|
-
timeout:
|
|
261
|
+
maxBuffer: gitOptions.maxBufferBytes,
|
|
262
|
+
timeout: gitOptions.timeoutMs,
|
|
169
263
|
});
|
|
170
264
|
tempDir = await mkdtemp(path.join(os.tmpdir(), 'ushman-ledger-git-diff-'));
|
|
171
265
|
const patchPath = path.join(tempDir, 'patch.diff');
|
|
@@ -176,13 +270,11 @@ const materializeGitDiff = async (workspaceRoot, gitRef) => {
|
|
|
176
270
|
if (tempDir) {
|
|
177
271
|
await rm(tempDir, { force: true, recursive: true });
|
|
178
272
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
throw error;
|
|
273
|
+
return throwGitDiffFailure({
|
|
274
|
+
error,
|
|
275
|
+
gitOptions,
|
|
276
|
+
gitRef,
|
|
277
|
+
});
|
|
186
278
|
}
|
|
187
279
|
};
|
|
188
280
|
const parseJsonInput = (text, sourceLabel) => {
|
|
@@ -196,16 +288,16 @@ const parseJsonInput = (text, sourceLabel) => {
|
|
|
196
288
|
const print = (context, text) => {
|
|
197
289
|
context.stdout.write(text.endsWith('\n') ? text : `${text}\n`);
|
|
198
290
|
};
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
if (!raw) {
|
|
202
|
-
return undefined;
|
|
203
|
-
}
|
|
204
|
-
if (!/^[1-9]\d*$/u.test(raw)) {
|
|
205
|
-
throw new CliUsageError(`Invalid --limit value: ${raw}`);
|
|
206
|
-
}
|
|
207
|
-
return Number.parseInt(raw, 10);
|
|
291
|
+
const printJson = (context, value) => {
|
|
292
|
+
print(context, JSON.stringify(value, null, 2));
|
|
208
293
|
};
|
|
294
|
+
const parseLimit = (flags) => getFlag(flags, 'limit')
|
|
295
|
+
? parsePositiveIntegerFlag({
|
|
296
|
+
defaultValue: 0,
|
|
297
|
+
flagName: 'limit',
|
|
298
|
+
flags,
|
|
299
|
+
})
|
|
300
|
+
: undefined;
|
|
209
301
|
const parseOptionalKind = (flags) => {
|
|
210
302
|
const kind = getFlag(flags, 'kind');
|
|
211
303
|
if (!kind) {
|
|
@@ -287,25 +379,30 @@ const parseOptionalChangeLogPicklist = ({ flagName, flags, options, }) => {
|
|
|
287
379
|
}
|
|
288
380
|
return value;
|
|
289
381
|
};
|
|
290
|
-
const parseFilesChangedCsv = (raw) => {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
382
|
+
const parseFilesChangedCsv = (raw) => parseWorkspaceRelativePathCsv({
|
|
383
|
+
flagName: 'files-changed',
|
|
384
|
+
raw,
|
|
385
|
+
}).map((filePath) => ({ path: filePath }));
|
|
386
|
+
const parseGitDiffOptions = (flags) => {
|
|
387
|
+
const gitPaths = getFlag(flags, 'git-paths');
|
|
388
|
+
return {
|
|
389
|
+
maxBufferBytes: parsePositiveIntegerFlag({
|
|
390
|
+
defaultValue: DEFAULT_GIT_DIFF_MAX_BUFFER_BYTES,
|
|
391
|
+
flagName: 'git-diff-max-buffer-bytes',
|
|
392
|
+
flags,
|
|
393
|
+
}),
|
|
394
|
+
scopedPaths: gitPaths
|
|
395
|
+
? parseWorkspaceRelativePathCsv({
|
|
396
|
+
flagName: 'git-paths',
|
|
397
|
+
raw: gitPaths,
|
|
398
|
+
})
|
|
399
|
+
: [],
|
|
400
|
+
timeoutMs: parsePositiveIntegerFlag({
|
|
401
|
+
defaultValue: DEFAULT_GIT_DIFF_TIMEOUT_MS,
|
|
402
|
+
flagName: 'git-diff-timeout-ms',
|
|
403
|
+
flags,
|
|
404
|
+
}),
|
|
405
|
+
};
|
|
309
406
|
};
|
|
310
407
|
const readCommandLines = async (flags) => {
|
|
311
408
|
const inlineCommands = getFlag(flags, 'commands');
|
|
@@ -323,32 +420,65 @@ const readCommandLines = async (flags) => {
|
|
|
323
420
|
}
|
|
324
421
|
return undefined;
|
|
325
422
|
};
|
|
423
|
+
const validateGitDiffFlagUsage = (flags, diffFromGitProvided) => {
|
|
424
|
+
if (diffFromGitProvided) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const usedGitFlags = getUsedFlags(flags, GIT_DIFF_FLAG_NAMES);
|
|
428
|
+
if (usedGitFlags.length === 0) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
throw new CliUsageError(`${usedGitFlags.join(', ')} ${usedGitFlags.length === 1 ? 'is' : 'are'} only supported with --diff-from-git.`);
|
|
432
|
+
};
|
|
433
|
+
const assertDiffRecordKindSupported = (kind) => {
|
|
434
|
+
if (kind === 'agent-patch' || kind === 'operator-patch' || kind === 'change-log') {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
throw new CliUsageError('--diff and --diff-from-git are only supported for patch and change-log records.');
|
|
438
|
+
};
|
|
439
|
+
const readPatchText = async (diffPath) => readPatchTextFromFile(diffPath);
|
|
440
|
+
const readPatchTextForRecordKind = async (kind, diffPath) => {
|
|
441
|
+
if (kind === 'change-log' || kind === 'agent-patch' || kind === 'operator-patch') {
|
|
442
|
+
return readPatchText(diffPath);
|
|
443
|
+
}
|
|
444
|
+
return undefined;
|
|
445
|
+
};
|
|
446
|
+
const deriveAffectedFiles = (patchText) => [
|
|
447
|
+
...new Set(deriveFilesChangedFromPatch(patchText).map((fileChange) => fileChange.path)),
|
|
448
|
+
];
|
|
326
449
|
const resolveDiffInput = async ({ flags, kind, workspaceRoot, }) => {
|
|
327
450
|
const diffPath = getFlag(flags, 'diff');
|
|
328
451
|
const diffFromGit = getFlag(flags, 'diff-from-git');
|
|
329
|
-
|
|
452
|
+
const diffFromGitProvided = hasFlag(flags, 'diff-from-git');
|
|
453
|
+
if (diffPath && diffFromGitProvided) {
|
|
330
454
|
throw new CliUsageError('Use either --diff or --diff-from-git, not both.');
|
|
331
455
|
}
|
|
332
|
-
if (
|
|
333
|
-
|
|
456
|
+
if (diffFromGitProvided && diffFromGit === '') {
|
|
457
|
+
throw new CliUsageError('--diff-from-git must not be empty.');
|
|
334
458
|
}
|
|
335
|
-
|
|
336
|
-
|
|
459
|
+
validateGitDiffFlagUsage(flags, diffFromGitProvided);
|
|
460
|
+
if (!diffPath && !diffFromGitProvided) {
|
|
461
|
+
return {};
|
|
337
462
|
}
|
|
463
|
+
assertDiffRecordKindSupported(kind);
|
|
338
464
|
if (diffPath) {
|
|
339
465
|
const resolvedDiffPath = path.resolve(diffPath);
|
|
340
466
|
await ensureFileExists(resolvedDiffPath, '--diff');
|
|
341
467
|
return {
|
|
342
468
|
diffPath: resolvedDiffPath,
|
|
343
|
-
patchText:
|
|
469
|
+
patchText: await readPatchTextForRecordKind(kind, resolvedDiffPath),
|
|
344
470
|
};
|
|
345
471
|
}
|
|
346
|
-
const materialized = await materializeGitDiff(
|
|
472
|
+
const materialized = await materializeGitDiff({
|
|
473
|
+
gitOptions: parseGitDiffOptions(flags),
|
|
474
|
+
gitRef: diffFromGit ?? '',
|
|
475
|
+
workspaceRoot,
|
|
476
|
+
});
|
|
347
477
|
return {
|
|
348
478
|
cleanupTempDir: materialized.tempDir,
|
|
349
479
|
diffPath: materialized.patchPath,
|
|
350
480
|
gitRef: diffFromGit,
|
|
351
|
-
patchText:
|
|
481
|
+
patchText: await readPatchTextForRecordKind(kind, materialized.patchPath),
|
|
352
482
|
};
|
|
353
483
|
};
|
|
354
484
|
const validateRecordStdinFlags = (flags) => {
|
|
@@ -361,6 +491,9 @@ const validateRecordStdinFlags = (flags) => {
|
|
|
361
491
|
'diff',
|
|
362
492
|
'diff-from-git',
|
|
363
493
|
'files-changed',
|
|
494
|
+
'git-diff-max-buffer-bytes',
|
|
495
|
+
'git-diff-timeout-ms',
|
|
496
|
+
'git-paths',
|
|
364
497
|
'hypothesis',
|
|
365
498
|
'idempotency-key',
|
|
366
499
|
'kind',
|
|
@@ -446,10 +579,16 @@ const applyPatchInputToRecord = async ({ kind, patchInput, record, }) => {
|
|
|
446
579
|
diffPath: patchInput.diffPath,
|
|
447
580
|
links: patchInput.gitRef
|
|
448
581
|
? {
|
|
582
|
+
affectedFiles: patchInput.patchText ? deriveAffectedFiles(patchInput.patchText) : undefined,
|
|
449
583
|
...record.links,
|
|
450
584
|
gitRef: patchInput.gitRef,
|
|
451
585
|
}
|
|
452
|
-
:
|
|
586
|
+
: patchInput.patchText
|
|
587
|
+
? {
|
|
588
|
+
affectedFiles: deriveAffectedFiles(patchInput.patchText),
|
|
589
|
+
...record.links,
|
|
590
|
+
}
|
|
591
|
+
: record.links,
|
|
453
592
|
},
|
|
454
593
|
};
|
|
455
594
|
};
|
|
@@ -595,16 +734,22 @@ const runNoteCli = async (parsed, context) => {
|
|
|
595
734
|
const runListCli = async (parsed, context) => {
|
|
596
735
|
const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
|
|
597
736
|
if (hasFlag(parsed.flags, 'json')) {
|
|
598
|
-
|
|
737
|
+
let wroteEntry = false;
|
|
738
|
+
context.stdout.write('[\n');
|
|
599
739
|
for await (const entry of ledger.list({
|
|
600
740
|
kind: parseOptionalKind(parsed.flags),
|
|
601
741
|
limit: parseLimit(parsed.flags),
|
|
602
742
|
phase: parseOptionalPhase(parsed.flags),
|
|
603
743
|
since: parseSince(parsed.flags),
|
|
604
744
|
})) {
|
|
605
|
-
|
|
745
|
+
if (wroteEntry) {
|
|
746
|
+
context.stdout.write(',\n');
|
|
747
|
+
}
|
|
748
|
+
const renderedEntry = JSON.stringify(entry, null, 2).replaceAll('\n', '\n ');
|
|
749
|
+
context.stdout.write(` ${renderedEntry}`);
|
|
750
|
+
wroteEntry = true;
|
|
606
751
|
}
|
|
607
|
-
|
|
752
|
+
context.stdout.write(wroteEntry ? '\n]\n' : ']\n');
|
|
608
753
|
return 0;
|
|
609
754
|
}
|
|
610
755
|
for await (const entry of ledger.list({
|
|
@@ -640,16 +785,19 @@ const runTailCli = async (parsed, context) => runListCli({
|
|
|
640
785
|
const runRenderCli = async (parsed, context) => {
|
|
641
786
|
const target = parseRenderTarget(parsed.flags);
|
|
642
787
|
const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
|
|
643
|
-
const
|
|
788
|
+
const renderOptions = {
|
|
644
789
|
limit: parseLimit(parsed.flags),
|
|
645
790
|
out: getFlag(parsed.flags, 'out'),
|
|
646
791
|
phase: parseOptionalPhase(parsed.flags),
|
|
647
792
|
since: parseSince(parsed.flags),
|
|
648
793
|
to: target,
|
|
649
|
-
}
|
|
650
|
-
if (
|
|
651
|
-
|
|
794
|
+
};
|
|
795
|
+
if (renderOptions.out) {
|
|
796
|
+
await ledger.renderTo(renderOptions);
|
|
797
|
+
return 0;
|
|
652
798
|
}
|
|
799
|
+
const content = await ledger.render(renderOptions);
|
|
800
|
+
print(context, content);
|
|
653
801
|
return 0;
|
|
654
802
|
};
|
|
655
803
|
const runArchiveCli = async (parsed, context) => {
|
|
@@ -659,16 +807,29 @@ const runArchiveCli = async (parsed, context) => {
|
|
|
659
807
|
print(context, result.integrityHash);
|
|
660
808
|
return 0;
|
|
661
809
|
};
|
|
810
|
+
const formatDoctorFinding = (finding) => [`[${finding.code}] ${finding.message}`, `Next step: ${finding.remediation}`].join('\n');
|
|
811
|
+
const printDoctorFindings = (context, report) => {
|
|
812
|
+
context.stderr.write(`doctor found ${report.issueCount} issue${report.issueCount === 1 ? '' : 's'}.\n`);
|
|
813
|
+
for (const [index, finding] of report.findings.entries()) {
|
|
814
|
+
context.stderr.write(`\n${formatDoctorFinding(finding)}`);
|
|
815
|
+
if (index < report.findings.length - 1) {
|
|
816
|
+
context.stderr.write('\n');
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
context.stderr.write('\n');
|
|
820
|
+
};
|
|
662
821
|
const runDoctorCli = async (parsed, context) => {
|
|
663
822
|
const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
|
|
664
823
|
const result = await ledger.doctor();
|
|
824
|
+
if (hasFlag(parsed.flags, 'json')) {
|
|
825
|
+
printJson(context, result);
|
|
826
|
+
return result.ok ? 0 : 1;
|
|
827
|
+
}
|
|
665
828
|
if (result.ok) {
|
|
666
829
|
print(context, 'ok');
|
|
667
830
|
return 0;
|
|
668
831
|
}
|
|
669
|
-
|
|
670
|
-
context.stderr.write(`${issue}\n`);
|
|
671
|
-
}
|
|
832
|
+
printDoctorFindings(context, result);
|
|
672
833
|
return 1;
|
|
673
834
|
};
|
|
674
835
|
const formatCliError = (error) => {
|
|
@@ -684,7 +845,7 @@ const formatCliError = (error) => {
|
|
|
684
845
|
})
|
|
685
846
|
.join('\n');
|
|
686
847
|
}
|
|
687
|
-
return error instanceof Error ?
|
|
848
|
+
return error instanceof Error ? error.message : String(error);
|
|
688
849
|
};
|
|
689
850
|
export const runLedgerCli = async (argv, context = {}) => {
|
|
690
851
|
const mergedContext = { ...DEFAULT_CONTEXT, ...context };
|
package/dist/coverage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AA+FA,MAAM,MAAM,cAAc,GAAG;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,CAAC;CACrC,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,cAAc,CAiDnF,CAAC"}
|
package/dist/coverage.js
CHANGED
|
@@ -4,10 +4,10 @@ import { mapWithConcurrencyLimit } from "./async.js";
|
|
|
4
4
|
import { CANDIDATE_EXCLUDE_GLOBS, CANDIDATE_FILE_GLOBS } from "./candidate-paths.js";
|
|
5
5
|
import { readLabManifestMin } from "./lab-min.js";
|
|
6
6
|
import { loadLedgerState } from "./recovery.js";
|
|
7
|
+
import { getLedgerRuntimeConfig } from "./runtime-config.js";
|
|
7
8
|
const EXCLUDED_ROOTS = new Set(CANDIDATE_EXCLUDE_GLOBS.map((glob) => glob.replace(/\/\*\*$/u, '')));
|
|
8
9
|
const CANDIDATE_DIRECTORIES = CANDIDATE_FILE_GLOBS.filter((glob) => glob.endsWith('/**/*')).map((glob) => glob.slice(0, -5));
|
|
9
10
|
const CANDIDATE_FILES = CANDIDATE_FILE_GLOBS.filter((glob) => !glob.endsWith('/**/*'));
|
|
10
|
-
const FILE_STAT_CONCURRENCY = 16;
|
|
11
11
|
const toPosix = (value) => value.replaceAll(path.sep, '/');
|
|
12
12
|
const isMissingPathError = (error) => {
|
|
13
13
|
const code = error.code;
|
|
@@ -85,13 +85,14 @@ const getWorkspaceInitMs = async (workspaceRoot) => {
|
|
|
85
85
|
return labManifestStat.birthtimeMs || labManifestStat.mtimeMs;
|
|
86
86
|
};
|
|
87
87
|
export const computeCoverage = async (workspaceRoot) => {
|
|
88
|
+
const { coverageFileStatConcurrency } = getLedgerRuntimeConfig();
|
|
88
89
|
const [{ readIndex }, candidateFiles, workspaceInitMs] = await Promise.all([
|
|
89
90
|
loadLedgerState(workspaceRoot),
|
|
90
91
|
collectCandidateFiles(workspaceRoot),
|
|
91
92
|
getWorkspaceInitMs(workspaceRoot),
|
|
92
93
|
]);
|
|
93
94
|
const coverageIndex = new Set(readIndex.coveredFiles.map((filePath) => toPosix(filePath)));
|
|
94
|
-
const candidateStats = await mapWithConcurrencyLimit(candidateFiles,
|
|
95
|
+
const candidateStats = await mapWithConcurrencyLimit(candidateFiles, coverageFileStatConcurrency, async (relativePath) => {
|
|
95
96
|
try {
|
|
96
97
|
return {
|
|
97
98
|
mtimeMs: (await stat(path.join(workspaceRoot, relativePath))).mtimeMs,
|
package/dist/doctor.d.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { type PreparedLedgerState } from './recovery.ts';
|
|
2
|
+
export declare const DOCTOR_FINDING_CODES: readonly ["blob-corrupt", "blob-missing", "blob-unreadable", "change-log-rollback-missing-target", "change-log-smoke-failure-missing-rollback-plan", "manifest-entry-count-mismatch", "manifest-entry-location-missing", "manifest-entry-missing-on-disk", "manifest-last-sequence-mismatch", "manifest-per-phase-latest-mismatch", "manifest-phase-mismatch", "manifest-sequence-mismatch", "open-issue-stale", "phase-prev-entry-mismatch", "pre-change-checkpoint-stale", "read-failure"];
|
|
3
|
+
export type DoctorFindingCode = (typeof DOCTOR_FINDING_CODES)[number];
|
|
4
|
+
export type DoctorFindingMetadataValue = boolean | null | number | string;
|
|
5
|
+
export type DoctorFinding = {
|
|
6
|
+
readonly code: DoctorFindingCode;
|
|
7
|
+
readonly message: string;
|
|
8
|
+
readonly metadata?: Record<string, DoctorFindingMetadataValue>;
|
|
9
|
+
readonly remediation: string;
|
|
10
|
+
};
|
|
11
|
+
export type DoctorReport = {
|
|
12
|
+
readonly checkedAt: string;
|
|
13
|
+
readonly findings: DoctorFinding[];
|
|
14
|
+
readonly issueCount: number;
|
|
15
|
+
readonly issues: string[];
|
|
16
|
+
readonly ok: boolean;
|
|
17
|
+
};
|
|
2
18
|
export declare const runLedgerDoctor: (workspaceRoot: string, options?: {
|
|
3
19
|
readonly skipPrepare?: boolean;
|
|
4
20
|
readonly state?: PreparedLedgerState;
|
|
5
|
-
}) => Promise<
|
|
6
|
-
issues: string[];
|
|
7
|
-
ok: boolean;
|
|
8
|
-
}>;
|
|
21
|
+
}) => Promise<DoctorReport>;
|
|
9
22
|
//# sourceMappingURL=doctor.d.ts.map
|
package/dist/doctor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,EAAmB,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,EAAmB,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAS1E,eAAO,MAAM,oBAAoB,8dAiBvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AACtE,MAAM,MAAM,0BAA0B,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1E,MAAM,MAAM,aAAa,GAAG;IACxB,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,0BAA0B,CAAC,CAAC;IAC/D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,aAAa,EAAE,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACxB,CAAC;AAqjBF,eAAO,MAAM,eAAe,GACxB,eAAe,MAAM,EACrB,UAAS;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAA;CAAO,KACvF,OAAO,CAAC,YAAY,CAqCtB,CAAC"}
|