ushman-ledger 1.1.0 → 1.2.1

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 (68) hide show
  1. package/AGENTS.md +11 -7
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +79 -8
  4. package/dist/blobs.js +3 -3
  5. package/dist/builders.d.ts +44 -2
  6. package/dist/builders.d.ts.map +1 -1
  7. package/dist/builders.js +7 -2
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +346 -62
  10. package/dist/doctor.d.ts.map +1 -1
  11. package/dist/doctor.js +104 -4
  12. package/dist/handle.d.ts +28 -6
  13. package/dist/handle.d.ts.map +1 -1
  14. package/dist/handle.js +105 -11
  15. package/dist/helpers.d.ts +7 -0
  16. package/dist/helpers.d.ts.map +1 -0
  17. package/dist/helpers.js +38 -0
  18. package/dist/index.d.ts +5 -3
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +5 -3
  21. package/dist/list.d.ts +44 -2
  22. package/dist/list.d.ts.map +1 -1
  23. package/dist/list.js +7 -5
  24. package/dist/note.d.ts +27 -0
  25. package/dist/note.d.ts.map +1 -1
  26. package/dist/note.js +11 -0
  27. package/dist/patch-resolver.d.ts +39 -0
  28. package/dist/patch-resolver.d.ts.map +1 -0
  29. package/dist/patch-resolver.js +196 -0
  30. package/dist/read-index.d.ts +7 -7
  31. package/dist/read-index.d.ts.map +1 -1
  32. package/dist/record.d.ts.map +1 -1
  33. package/dist/record.js +15 -40
  34. package/dist/render/migration-log.d.ts +10 -0
  35. package/dist/render/migration-log.d.ts.map +1 -0
  36. package/dist/render/migration-log.js +79 -0
  37. package/dist/render/retro.d.ts.map +1 -1
  38. package/dist/render/retro.js +34 -21
  39. package/dist/render/workspace-narrative.d.ts +12 -0
  40. package/dist/render/workspace-narrative.d.ts.map +1 -0
  41. package/dist/render/workspace-narrative.js +137 -0
  42. package/dist/schema/entry-core.d.ts +110 -0
  43. package/dist/schema/entry-core.d.ts.map +1 -0
  44. package/dist/schema/entry-core.js +143 -0
  45. package/dist/schema/entry-migrations.d.ts +3 -0
  46. package/dist/schema/entry-migrations.d.ts.map +1 -0
  47. package/dist/schema/entry-migrations.js +48 -0
  48. package/dist/schema/entry-read.d.ts +694 -0
  49. package/dist/schema/entry-read.d.ts.map +1 -0
  50. package/dist/schema/entry-read.js +92 -0
  51. package/dist/schema/entry-write.d.ts +865 -0
  52. package/dist/schema/entry-write.d.ts.map +1 -0
  53. package/dist/schema/entry-write.js +105 -0
  54. package/dist/schema/entry.d.ts +6 -1369
  55. package/dist/schema/entry.d.ts.map +1 -1
  56. package/dist/schema/entry.js +9 -286
  57. package/dist/schema/note.d.ts +1 -1
  58. package/dist/schema/note.d.ts.map +1 -1
  59. package/dist/schema/note.js +12 -1
  60. package/dist/storage/filesystem.d.ts +9 -0
  61. package/dist/storage/filesystem.d.ts.map +1 -1
  62. package/dist/storage/filesystem.js +82 -5
  63. package/dist/storage/lock-reclaimer.d.ts +2 -0
  64. package/dist/storage/lock-reclaimer.d.ts.map +1 -0
  65. package/dist/storage/lock-reclaimer.js +45 -0
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.js +1 -1
  68. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -7,12 +7,35 @@ import { fileURLToPath } from 'node:url';
7
7
  import { promisify } from 'node:util';
8
8
  import * as v from 'valibot';
9
9
  import { openLedger } from "./handle.js";
10
- import { LEDGER_KINDS, LEDGER_PHASES, parseLedgerRecord } from "./schema/entry.js";
10
+ import { deriveFilesChangedFromPatch } from "./patch-resolver.js";
11
+ import { ChangeLogParityStatusSchema, ChangeLogSmokeResultSchema, ChangeLogSubkindSchema, LEDGER_KINDS, LEDGER_PHASES, parseLedgerRecord, WorkspaceRelativePathSchema, } from "./schema/entry.js";
11
12
  import { NoteSubkindSchema } from "./schema/note.js";
12
13
  import { LEDGER_LIBRARY_VERSION } from "./version.js";
13
14
  const execFileAsync = promisify(execFile);
14
- const GIT_DIFF_TIMEOUT_MS = 30_000;
15
- const RENDER_TARGETS = ['retro', 'jsonl', 'timeline-html', 'dependency-graph'];
15
+ const DEFAULT_GIT_DIFF_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
16
+ const DEFAULT_GIT_DIFF_TIMEOUT_MS = 30_000;
17
+ const RENDER_TARGETS = [
18
+ 'retro',
19
+ 'jsonl',
20
+ 'timeline-html',
21
+ 'dependency-graph',
22
+ 'migration-log-md',
23
+ 'workspace-narrative-md',
24
+ ];
25
+ const GIT_DIFF_FLAG_NAMES = ['git-diff-max-buffer-bytes', 'git-diff-timeout-ms', 'git-paths'];
26
+ const CHANGE_LOG_RECORD_ONLY_FLAGS = [
27
+ 'commands',
28
+ 'commands-from',
29
+ 'files-changed',
30
+ 'hypothesis',
31
+ 'parity-status',
32
+ 'rollback-plan',
33
+ 'rolls-back',
34
+ 'smoke-notes',
35
+ 'smoke-result',
36
+ 'subkind',
37
+ ];
38
+ 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]`;
16
39
  class CliUsageError extends Error {
17
40
  }
18
41
  const DEFAULT_CONTEXT = {
@@ -34,12 +57,12 @@ const renderValidValues = () => `Valid values:
34
57
  const renderHelp = (commandName) => `${commandName}
35
58
 
36
59
  Commands:
37
- ${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>] [--from-stdin]
60
+ ${renderRecordUsage(commandName)}
38
61
  ${commandName} note <subkind> [--workspace=<ws>] --phase=<phase> --summary="..." [--body=<markdown-file>] [--from-stdin]
39
62
  ${commandName} list [--workspace=<ws>] [--phase=<phase>] [--kind=<kind>] [--since=<iso>] [--limit=<n>] [--json]
40
63
  ${commandName} show [--workspace=<ws>] <entry-id>
41
64
  ${commandName} tail [--workspace=<ws>] [--phase=<phase>] [--limit=<n>]
42
- ${commandName} render [--workspace=<ws>] [--to=retro|jsonl|timeline-html|dependency-graph] [--phase=<phase>] [--out=<file>]
65
+ ${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>]
43
66
  ${commandName} archive [--workspace=<ws>] --out=<file.tgz>
44
67
  ${commandName} doctor [--workspace=<ws>]
45
68
  ${commandName} --version
@@ -48,7 +71,7 @@ ${renderValidValues()}`;
48
71
  const renderCommandHelp = (commandName, command) => {
49
72
  switch (command) {
50
73
  case 'record':
51
- 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>] [--from-stdin]
74
+ return `${renderRecordUsage(commandName)}
52
75
 
53
76
  ${renderValidValues()}`;
54
77
  case 'note':
@@ -60,7 +83,7 @@ ${renderValidValues()}`;
60
83
 
61
84
  ${renderValidValues()}`;
62
85
  case 'render':
63
- return `${commandName} render [--workspace=<ws>] [--to=<target>] [--phase=<phase>] [--out=<file>]
86
+ return `${commandName} render [--workspace=<ws>] [--to=<target>] [--phase=<phase>] [--since=<iso>] [--limit=<n>] [--out=<file>]
64
87
 
65
88
  ${renderValidValues()}`;
66
89
  default:
@@ -99,6 +122,9 @@ const parseArgv = (argv) => {
99
122
  };
100
123
  const getFlag = (flags, name) => {
101
124
  const value = flags[name];
125
+ if (value === true) {
126
+ throw new CliUsageError(`Missing value for --${name}.`);
127
+ }
102
128
  return typeof value === 'string' ? value : undefined;
103
129
  };
104
130
  const hasFlag = (flags, name) => flags[name] === true || typeof flags[name] === 'string';
@@ -135,13 +161,64 @@ const ensureFileExists = async (filePath, flagName) => {
135
161
  throw error;
136
162
  }
137
163
  };
138
- const materializeGitDiff = async (workspaceRoot, gitRef) => {
164
+ const getErrorCode = (error) => typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
165
+ const parsePositiveIntegerFlag = ({ defaultValue, flagName, flags, }) => {
166
+ const raw = getFlag(flags, flagName);
167
+ if (!raw) {
168
+ return defaultValue;
169
+ }
170
+ if (!/^[1-9]\d*$/u.test(raw)) {
171
+ throw new CliUsageError(`Invalid --${flagName} value: ${raw}. Expected a positive integer.`);
172
+ }
173
+ return Number.parseInt(raw, 10);
174
+ };
175
+ const parseWorkspaceRelativePathCsv = ({ flagName, raw }) => {
176
+ const uniquePaths = [
177
+ ...new Set(raw
178
+ .split(',')
179
+ .map((value) => value.trim())
180
+ .filter((value) => value.length > 0)
181
+ .map((filePath) => {
182
+ try {
183
+ return v.parse(WorkspaceRelativePathSchema, filePath);
184
+ }
185
+ catch {
186
+ throw new CliUsageError(`--${flagName} path is not a normalized workspace-relative path: ${filePath}`);
187
+ }
188
+ })),
189
+ ];
190
+ if (uniquePaths.length === 0) {
191
+ throw new CliUsageError(`--${flagName} must include at least one workspace-relative path.`);
192
+ }
193
+ return uniquePaths;
194
+ };
195
+ const isGitDiffMaxBufferError = (error) => {
196
+ const code = getErrorCode(error);
197
+ if (code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
198
+ return true;
199
+ }
200
+ return error instanceof RangeError && error.message.includes('maxBuffer');
201
+ };
202
+ const isGitDiffTimeoutError = (error) => {
203
+ const code = getErrorCode(error);
204
+ if (code === 'ETIMEDOUT') {
205
+ return true;
206
+ }
207
+ return (typeof error === 'object' &&
208
+ error !== null &&
209
+ 'killed' in error &&
210
+ 'signal' in error &&
211
+ error.killed === true &&
212
+ error.signal === 'SIGTERM');
213
+ };
214
+ const materializeGitDiff = async ({ gitOptions, gitRef, workspaceRoot, }) => {
139
215
  let tempDir;
140
216
  try {
141
- const { stdout } = await execFileAsync('git', ['diff', gitRef], {
217
+ const gitArgs = gitOptions.scopedPaths.length === 0 ? ['diff', gitRef] : ['diff', gitRef, '--', ...gitOptions.scopedPaths];
218
+ const { stdout } = await execFileAsync('git', gitArgs, {
142
219
  cwd: workspaceRoot,
143
- maxBuffer: 10 * 1024 * 1024,
144
- timeout: GIT_DIFF_TIMEOUT_MS,
220
+ maxBuffer: gitOptions.maxBufferBytes,
221
+ timeout: gitOptions.timeoutMs,
145
222
  });
146
223
  tempDir = await mkdtemp(path.join(os.tmpdir(), 'ushman-ledger-git-diff-'));
147
224
  const patchPath = path.join(tempDir, 'patch.diff');
@@ -152,11 +229,14 @@ const materializeGitDiff = async (workspaceRoot, gitRef) => {
152
229
  if (tempDir) {
153
230
  await rm(tempDir, { force: true, recursive: true });
154
231
  }
155
- if (error.code === 'ENOENT') {
232
+ if (getErrorCode(error) === 'ENOENT') {
156
233
  throw new CliUsageError('git is required for --diff-from-git and was not found in PATH.');
157
234
  }
158
- if (error.code === 'ETIMEDOUT') {
159
- throw new CliUsageError(`git diff ${gitRef} timed out after ${GIT_DIFF_TIMEOUT_MS}ms. Narrow the diff or run git manually.`);
235
+ if (isGitDiffTimeoutError(error)) {
236
+ throw new CliUsageError(`git diff ${gitRef} timed out after ${gitOptions.timeoutMs}ms. Narrow the diff or increase --git-diff-timeout-ms.`);
237
+ }
238
+ if (isGitDiffMaxBufferError(error)) {
239
+ throw new CliUsageError(`git diff ${gitRef} exceeded the configured stdout buffer (${gitOptions.maxBufferBytes} bytes). Narrow the diff or increase --git-diff-max-buffer-bytes.`);
160
240
  }
161
241
  throw error;
162
242
  }
@@ -172,16 +252,13 @@ const parseJsonInput = (text, sourceLabel) => {
172
252
  const print = (context, text) => {
173
253
  context.stdout.write(text.endsWith('\n') ? text : `${text}\n`);
174
254
  };
175
- const parseLimit = (flags) => {
176
- const raw = getFlag(flags, 'limit');
177
- if (!raw) {
178
- return undefined;
179
- }
180
- if (!/^[1-9]\d*$/u.test(raw)) {
181
- throw new CliUsageError(`Invalid --limit value: ${raw}`);
182
- }
183
- return Number.parseInt(raw, 10);
184
- };
255
+ const parseLimit = (flags) => getFlag(flags, 'limit')
256
+ ? parsePositiveIntegerFlag({
257
+ defaultValue: 0,
258
+ flagName: 'limit',
259
+ flags,
260
+ })
261
+ : undefined;
185
262
  const parseOptionalKind = (flags) => {
186
263
  const kind = getFlag(flags, 'kind');
187
264
  if (!kind) {
@@ -234,18 +311,162 @@ const parseRenderTarget = (flags) => {
234
311
  }
235
312
  return target;
236
313
  };
314
+ const splitCommandLines = (value) => value
315
+ .split(/\r?\n/u)
316
+ .map((command) => command.trim())
317
+ .filter((command) => command.length > 0);
318
+ const getUsedFlags = (flags, names) => names.filter((name) => hasFlag(flags, name)).map((name) => `--${name}`);
319
+ const rejectUnsupportedFlags = (supportedKindLabel, flags, names) => {
320
+ const usedFlags = getUsedFlags(flags, names);
321
+ if (usedFlags.length === 0) {
322
+ return;
323
+ }
324
+ throw new CliUsageError(`${usedFlags.join(', ')} ${usedFlags.length === 1 ? 'is' : 'are'} only supported for ${supportedKindLabel} records.`);
325
+ };
326
+ const parseRequiredChangeLogSubkind = (flags) => {
327
+ const subkind = getRequiredString(flags, 'subkind');
328
+ if (!ChangeLogSubkindSchema.options.includes(subkind)) {
329
+ throw new CliUsageError(`Invalid --subkind value: ${subkind}. Expected one of: ${ChangeLogSubkindSchema.options.join(', ')}.`);
330
+ }
331
+ return subkind;
332
+ };
333
+ const parseOptionalChangeLogPicklist = ({ flagName, flags, options, }) => {
334
+ const value = getFlag(flags, flagName);
335
+ if (!value) {
336
+ return undefined;
337
+ }
338
+ if (!options.includes(value)) {
339
+ throw new CliUsageError(`Invalid --${flagName} value: ${value}. Expected one of: ${options.join(', ')}.`);
340
+ }
341
+ return value;
342
+ };
343
+ const parseFilesChangedCsv = (raw) => parseWorkspaceRelativePathCsv({
344
+ flagName: 'files-changed',
345
+ raw,
346
+ }).map((filePath) => ({ path: filePath }));
347
+ const parseGitDiffOptions = (flags) => {
348
+ const gitPaths = getFlag(flags, 'git-paths');
349
+ return {
350
+ maxBufferBytes: parsePositiveIntegerFlag({
351
+ defaultValue: DEFAULT_GIT_DIFF_MAX_BUFFER_BYTES,
352
+ flagName: 'git-diff-max-buffer-bytes',
353
+ flags,
354
+ }),
355
+ scopedPaths: gitPaths
356
+ ? parseWorkspaceRelativePathCsv({
357
+ flagName: 'git-paths',
358
+ raw: gitPaths,
359
+ })
360
+ : [],
361
+ timeoutMs: parsePositiveIntegerFlag({
362
+ defaultValue: DEFAULT_GIT_DIFF_TIMEOUT_MS,
363
+ flagName: 'git-diff-timeout-ms',
364
+ flags,
365
+ }),
366
+ };
367
+ };
368
+ const readCommandLines = async (flags) => {
369
+ const inlineCommands = getFlag(flags, 'commands');
370
+ const commandsFrom = getFlag(flags, 'commands-from');
371
+ if (inlineCommands && commandsFrom) {
372
+ throw new CliUsageError('Use either --commands or --commands-from, not both.');
373
+ }
374
+ if (commandsFrom) {
375
+ const commandsPath = path.resolve(commandsFrom);
376
+ await ensureFileExists(commandsPath, '--commands-from');
377
+ return splitCommandLines(await readFile(commandsPath, 'utf8'));
378
+ }
379
+ if (inlineCommands) {
380
+ return splitCommandLines(inlineCommands);
381
+ }
382
+ return undefined;
383
+ };
384
+ const validateGitDiffFlagUsage = (flags, diffFromGitProvided) => {
385
+ if (diffFromGitProvided) {
386
+ return;
387
+ }
388
+ const usedGitFlags = getUsedFlags(flags, GIT_DIFF_FLAG_NAMES);
389
+ if (usedGitFlags.length === 0) {
390
+ return;
391
+ }
392
+ throw new CliUsageError(`${usedGitFlags.join(', ')} ${usedGitFlags.length === 1 ? 'is' : 'are'} only supported with --diff-from-git.`);
393
+ };
394
+ const assertDiffRecordKindSupported = (kind) => {
395
+ if (kind === 'agent-patch' || kind === 'operator-patch' || kind === 'change-log') {
396
+ return;
397
+ }
398
+ throw new CliUsageError('--diff and --diff-from-git are only supported for patch and change-log records.');
399
+ };
400
+ const readPatchText = async (diffPath) => readFile(diffPath, 'utf8');
401
+ const readPatchTextForRecordKind = async (kind, diffPath) => {
402
+ if (kind === 'change-log' || kind === 'agent-patch' || kind === 'operator-patch') {
403
+ return readPatchText(diffPath);
404
+ }
405
+ return undefined;
406
+ };
407
+ const deriveAffectedFiles = (patchText) => [
408
+ ...new Set(deriveFilesChangedFromPatch(patchText).map((fileChange) => fileChange.path)),
409
+ ];
410
+ const resolveDiffInput = async ({ flags, kind, workspaceRoot, }) => {
411
+ const diffPath = getFlag(flags, 'diff');
412
+ const diffFromGit = getFlag(flags, 'diff-from-git');
413
+ const diffFromGitProvided = hasFlag(flags, 'diff-from-git');
414
+ if (diffPath && diffFromGitProvided) {
415
+ throw new CliUsageError('Use either --diff or --diff-from-git, not both.');
416
+ }
417
+ if (diffFromGitProvided && diffFromGit === '') {
418
+ throw new CliUsageError('--diff-from-git must not be empty.');
419
+ }
420
+ validateGitDiffFlagUsage(flags, diffFromGitProvided);
421
+ if (!diffPath && !diffFromGitProvided) {
422
+ return {};
423
+ }
424
+ assertDiffRecordKindSupported(kind);
425
+ if (diffPath) {
426
+ const resolvedDiffPath = path.resolve(diffPath);
427
+ await ensureFileExists(resolvedDiffPath, '--diff');
428
+ return {
429
+ diffPath: resolvedDiffPath,
430
+ patchText: await readPatchTextForRecordKind(kind, resolvedDiffPath),
431
+ };
432
+ }
433
+ const materialized = await materializeGitDiff({
434
+ gitOptions: parseGitDiffOptions(flags),
435
+ gitRef: diffFromGit ?? '',
436
+ workspaceRoot,
437
+ });
438
+ return {
439
+ cleanupTempDir: materialized.tempDir,
440
+ diffPath: materialized.patchPath,
441
+ gitRef: diffFromGit,
442
+ patchText: await readPatchTextForRecordKind(kind, materialized.patchPath),
443
+ };
444
+ };
237
445
  const validateRecordStdinFlags = (flags) => {
238
446
  const conflictingFlags = [
239
447
  'agent',
240
448
  'action',
241
449
  'check-id',
450
+ 'commands',
451
+ 'commands-from',
242
452
  'diff',
243
453
  'diff-from-git',
454
+ 'files-changed',
455
+ 'git-diff-max-buffer-bytes',
456
+ 'git-diff-timeout-ms',
457
+ 'git-paths',
458
+ 'hypothesis',
244
459
  'idempotency-key',
245
460
  'kind',
246
461
  'operator',
462
+ 'parity-status',
247
463
  'phase',
248
464
  'rationale',
465
+ 'rollback-plan',
466
+ 'rolls-back',
467
+ 'smoke-notes',
468
+ 'smoke-result',
469
+ 'subkind',
249
470
  'summary',
250
471
  ].filter((flagName) => hasFlag(flags, flagName));
251
472
  if (conflictingFlags.length === 0) {
@@ -257,6 +478,9 @@ const validateRecordStdinFlags = (flags) => {
257
478
  };
258
479
  const buildBaseRecordFromFlags = (parsed, context) => {
259
480
  const kind = getRequiredKind(parsed.flags);
481
+ if (kind !== 'change-log') {
482
+ rejectUnsupportedFlags('change-log', parsed.flags, CHANGE_LOG_RECORD_ONLY_FLAGS);
483
+ }
260
484
  const record = {
261
485
  emitter: {
262
486
  tool: context.defaultEmitter.tool,
@@ -296,43 +520,74 @@ const buildBaseRecordFromFlags = (parsed, context) => {
296
520
  rationale: rationale ?? '',
297
521
  };
298
522
  }
523
+ if (kind === 'change-log') {
524
+ record.subkind = parseRequiredChangeLogSubkind(parsed.flags);
525
+ }
299
526
  const idempotencyKey = getFlag(parsed.flags, 'idempotency-key');
300
527
  if (idempotencyKey) {
301
528
  record.idempotencyKey = idempotencyKey;
302
529
  }
303
530
  return { kind, record };
304
531
  };
305
- const applyPatchInputToRecord = async ({ kind, record, workspaceRoot, parsed, }) => {
306
- const diffPath = getFlag(parsed.flags, 'diff');
307
- if (diffPath) {
308
- if (kind !== 'agent-patch' && kind !== 'operator-patch') {
309
- throw new CliUsageError('--diff is only supported for patch records.');
310
- }
311
- await ensureFileExists(path.resolve(diffPath), '--diff');
312
- record.diffPath = diffPath;
313
- }
314
- const diffFromGit = getFlag(parsed.flags, 'diff-from-git');
315
- if (!diffFromGit) {
316
- return {};
317
- }
532
+ const applyPatchInputToRecord = async ({ kind, patchInput, record, }) => {
318
533
  if (kind !== 'agent-patch' && kind !== 'operator-patch') {
319
- throw new CliUsageError('--diff-from-git is only supported for patch records.');
320
- }
321
- if (diffPath) {
322
- throw new CliUsageError('Use either --diff or --diff-from-git, not both.');
534
+ return {};
323
535
  }
324
- const materialized = await materializeGitDiff(workspaceRoot, diffFromGit);
325
536
  return {
326
- cleanupTempDir: materialized.tempDir,
537
+ cleanupTempDir: patchInput.cleanupTempDir,
327
538
  record: {
328
539
  ...record,
329
- diffPath: materialized.patchPath,
330
- links: {
331
- gitRef: diffFromGit,
332
- },
540
+ diffPath: patchInput.diffPath,
541
+ links: patchInput.gitRef
542
+ ? {
543
+ affectedFiles: patchInput.patchText ? deriveAffectedFiles(patchInput.patchText) : undefined,
544
+ ...record.links,
545
+ gitRef: patchInput.gitRef,
546
+ }
547
+ : patchInput.patchText
548
+ ? {
549
+ affectedFiles: deriveAffectedFiles(patchInput.patchText),
550
+ ...record.links,
551
+ }
552
+ : record.links,
333
553
  },
334
554
  };
335
555
  };
556
+ const applyChangeLogFlagsToRecord = async ({ flags, patchInput, record, }) => {
557
+ const filesChangedFlag = getFlag(flags, 'files-changed');
558
+ if (filesChangedFlag && patchInput.patchText) {
559
+ throw new CliUsageError('Use either --files-changed or --diff/--diff-from-git to populate change-log files.');
560
+ }
561
+ return {
562
+ ...record,
563
+ commandsRun: await readCommandLines(flags),
564
+ filesChanged: filesChangedFlag
565
+ ? parseFilesChangedCsv(filesChangedFlag)
566
+ : patchInput.patchText
567
+ ? deriveFilesChangedFromPatch(patchInput.patchText)
568
+ : undefined,
569
+ hypothesis: getFlag(flags, 'hypothesis'),
570
+ links: patchInput.gitRef
571
+ ? {
572
+ ...record.links,
573
+ gitRef: patchInput.gitRef,
574
+ }
575
+ : record.links,
576
+ parityStatus: parseOptionalChangeLogPicklist({
577
+ flagName: 'parity-status',
578
+ flags,
579
+ options: ChangeLogParityStatusSchema.options,
580
+ }),
581
+ rollbackPlan: getFlag(flags, 'rollback-plan'),
582
+ rollsBack: getFlag(flags, 'rolls-back'),
583
+ smokeNotes: getFlag(flags, 'smoke-notes'),
584
+ smokeResult: parseOptionalChangeLogPicklist({
585
+ flagName: 'smoke-result',
586
+ flags,
587
+ options: ChangeLogSmokeResultSchema.options,
588
+ }),
589
+ };
590
+ };
336
591
  const buildRecordFromFlags = async (parsed, context) => {
337
592
  const workspaceRoot = getWorkspaceRoot(parsed.flags);
338
593
  if (hasFlag(parsed.flags, 'from-stdin')) {
@@ -344,17 +599,41 @@ const buildRecordFromFlags = async (parsed, context) => {
344
599
  };
345
600
  }
346
601
  const { kind, record } = buildBaseRecordFromFlags(parsed, context);
347
- const patchInput = await applyPatchInputToRecord({
348
- kind,
349
- parsed,
350
- record,
351
- workspaceRoot,
352
- });
353
- return {
354
- cleanupTempDir: patchInput.cleanupTempDir,
355
- record: patchInput.record ?? record,
356
- workspaceRoot,
357
- };
602
+ let diffInput;
603
+ try {
604
+ diffInput = await resolveDiffInput({
605
+ flags: parsed.flags,
606
+ kind,
607
+ workspaceRoot,
608
+ });
609
+ if (kind === 'change-log') {
610
+ return {
611
+ cleanupTempDir: diffInput.cleanupTempDir,
612
+ record: await applyChangeLogFlagsToRecord({
613
+ flags: parsed.flags,
614
+ patchInput: diffInput,
615
+ record,
616
+ }),
617
+ workspaceRoot,
618
+ };
619
+ }
620
+ const patchInput = await applyPatchInputToRecord({
621
+ kind,
622
+ patchInput: diffInput,
623
+ record,
624
+ });
625
+ return {
626
+ cleanupTempDir: patchInput.cleanupTempDir,
627
+ record: patchInput.record ?? record,
628
+ workspaceRoot,
629
+ };
630
+ }
631
+ catch (error) {
632
+ if (diffInput?.cleanupTempDir) {
633
+ await rm(diffInput.cleanupTempDir, { force: true, recursive: true });
634
+ }
635
+ throw error;
636
+ }
358
637
  };
359
638
  const buildNoteRecord = async (parsed, context) => {
360
639
  const [subkind] = parsed.positionals;
@@ -461,14 +740,19 @@ const runTailCli = async (parsed, context) => runListCli({
461
740
  const runRenderCli = async (parsed, context) => {
462
741
  const target = parseRenderTarget(parsed.flags);
463
742
  const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
464
- const content = await ledger.render({
743
+ const renderOptions = {
744
+ limit: parseLimit(parsed.flags),
465
745
  out: getFlag(parsed.flags, 'out'),
466
746
  phase: parseOptionalPhase(parsed.flags),
747
+ since: parseSince(parsed.flags),
467
748
  to: target,
468
- });
469
- if (!getFlag(parsed.flags, 'out')) {
470
- print(context, content);
749
+ };
750
+ if (renderOptions.out) {
751
+ await ledger.renderTo(renderOptions);
752
+ return 0;
471
753
  }
754
+ const content = await ledger.render(renderOptions);
755
+ print(context, content);
472
756
  return 0;
473
757
  };
474
758
  const runArchiveCli = async (parsed, context) => {
@@ -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;AAsL1E,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;AA+V1E,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"}