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.
Files changed (63) hide show
  1. package/ARCHITECTURE.md +79 -0
  2. package/README.md +144 -5
  3. package/TROUBLESHOOTING.md +170 -0
  4. package/dist/blobs.d.ts +3 -0
  5. package/dist/blobs.d.ts.map +1 -1
  6. package/dist/blobs.js +41 -15
  7. package/dist/builders.d.ts +1 -2
  8. package/dist/builders.d.ts.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +231 -70
  11. package/dist/coverage.d.ts.map +1 -1
  12. package/dist/coverage.js +3 -2
  13. package/dist/doctor.d.ts +17 -4
  14. package/dist/doctor.d.ts.map +1 -1
  15. package/dist/doctor.js +225 -58
  16. package/dist/handle.d.ts +27 -7
  17. package/dist/handle.d.ts.map +1 -1
  18. package/dist/handle.js +96 -20
  19. package/dist/helpers.d.ts +1 -0
  20. package/dist/helpers.d.ts.map +1 -1
  21. package/dist/helpers.js +23 -0
  22. package/dist/index.d.ts +6 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -1
  25. package/dist/list.d.ts +3 -2
  26. package/dist/list.d.ts.map +1 -1
  27. package/dist/list.js +24 -12
  28. package/dist/note.d.ts +7 -0
  29. package/dist/note.d.ts.map +1 -1
  30. package/dist/note.js +6 -0
  31. package/dist/patch-resolver.d.ts +12 -0
  32. package/dist/patch-resolver.d.ts.map +1 -1
  33. package/dist/patch-resolver.js +205 -53
  34. package/dist/read-index.d.ts.map +1 -1
  35. package/dist/read-index.js +6 -5
  36. package/dist/record.d.ts.map +1 -1
  37. package/dist/record.js +3 -3
  38. package/dist/render/migration-log.d.ts +8 -1
  39. package/dist/render/migration-log.d.ts.map +1 -1
  40. package/dist/render/migration-log.js +40 -33
  41. package/dist/render/retro.d.ts.map +1 -1
  42. package/dist/render/retro.js +1 -7
  43. package/dist/render/workspace-narrative.d.ts +7 -1
  44. package/dist/render/workspace-narrative.d.ts.map +1 -1
  45. package/dist/render/workspace-narrative.js +114 -46
  46. package/dist/runtime-config.d.ts +12 -0
  47. package/dist/runtime-config.d.ts.map +1 -0
  48. package/dist/runtime-config.js +83 -0
  49. package/dist/schema/entry-read.d.ts.map +1 -1
  50. package/dist/schema/entry-read.js +1 -1
  51. package/dist/schema/entry-write.d.ts.map +1 -1
  52. package/dist/schema/entry-write.js +1 -1
  53. package/dist/schema/entry.d.ts.map +1 -1
  54. package/dist/storage/filesystem.d.ts +8 -0
  55. package/dist/storage/filesystem.d.ts.map +1 -1
  56. package/dist/storage/filesystem.js +110 -5
  57. package/dist/text-lines.d.ts +8 -0
  58. package/dist/text-lines.d.ts.map +1 -0
  59. package/dist/text-lines.js +20 -0
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.d.ts.map +1 -1
  62. package/dist/version.js +2 -1
  63. 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 GIT_DIFF_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
16
- const GIT_DIFF_TIMEOUT_MS = 30_000;
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} record [--workspace=<ws>] --kind=<kind> --phase=<phase> --summary="..." [--rationale="..."] [--action=<operator-action>] [--check-id=<check-id>] [--diff=<patch-file>] [--diff-from-git=<ref>] [--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]
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} record [--workspace=<ws>] --kind=<kind> --phase=<phase> --summary="..." [--rationale="..."] [--action=<operator-action>] [--check-id=<check-id>] [--diff=<patch-file>] [--diff-from-git=<ref>] [--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]
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 materializeGitDiff = async (workspaceRoot, gitRef) => {
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', ['diff', gitRef], {
259
+ const { stdout } = await execFileAsync('git', buildGitDiffArgs(gitOptions, gitRef), {
166
260
  cwd: workspaceRoot,
167
- maxBuffer: GIT_DIFF_MAX_BUFFER_BYTES,
168
- timeout: GIT_DIFF_TIMEOUT_MS,
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
- if (error.code === 'ENOENT') {
180
- throw new CliUsageError('git is required for --diff-from-git and was not found in PATH.');
181
- }
182
- if (error.code === 'ETIMEDOUT') {
183
- throw new CliUsageError(`git diff ${gitRef} timed out after ${GIT_DIFF_TIMEOUT_MS}ms. Narrow the diff or run git manually.`);
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 parseLimit = (flags) => {
200
- const raw = getFlag(flags, 'limit');
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
- const uniquePaths = [
292
- ...new Set(raw
293
- .split(',')
294
- .map((value) => value.trim())
295
- .filter((value) => value.length > 0)
296
- .map((filePath) => {
297
- try {
298
- return v.parse(WorkspaceRelativePathSchema, filePath);
299
- }
300
- catch {
301
- throw new CliUsageError(`--files-changed path is not a normalized workspace-relative path: ${filePath}`);
302
- }
303
- })),
304
- ];
305
- if (uniquePaths.length === 0) {
306
- throw new CliUsageError('--files-changed must include at least one workspace-relative path.');
307
- }
308
- return uniquePaths.map((filePath) => ({ path: filePath }));
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
- if (diffPath && diffFromGit) {
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 (!diffPath && !diffFromGit) {
333
- return {};
456
+ if (diffFromGitProvided && diffFromGit === '') {
457
+ throw new CliUsageError('--diff-from-git must not be empty.');
334
458
  }
335
- if (kind !== 'agent-patch' && kind !== 'operator-patch' && kind !== 'change-log') {
336
- throw new CliUsageError('--diff and --diff-from-git are only supported for patch and change-log records.');
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: kind === 'change-log' ? await readFile(resolvedDiffPath, 'utf8') : undefined,
469
+ patchText: await readPatchTextForRecordKind(kind, resolvedDiffPath),
344
470
  };
345
471
  }
346
- const materialized = await materializeGitDiff(workspaceRoot, diffFromGit ?? '');
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: kind === 'change-log' ? await readFile(materialized.patchPath, 'utf8') : undefined,
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
- : record.links,
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
- const entries = [];
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
- entries.push(entry);
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
- print(context, JSON.stringify(entries, null, 2));
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 content = await ledger.render({
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 (!getFlag(parsed.flags, 'out')) {
651
- print(context, content);
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
- for (const issue of result.issues) {
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 ? (error.stack ?? error.message) : String(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 };
@@ -1 +1 @@
1
- {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAgGA,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,CAgDnF,CAAC"}
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, FILE_STAT_CONCURRENCY, async (relativePath) => {
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
@@ -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;AA8V1E,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;;;EAyCzF,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"}