ushman-ledger 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/AGENTS.md +7 -5
  2. package/ARCHITECTURE.md +85 -0
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +114 -5
  5. package/TROUBLESHOOTING.md +184 -0
  6. package/dist/blobs.d.ts +3 -0
  7. package/dist/blobs.d.ts.map +1 -1
  8. package/dist/blobs.js +41 -15
  9. package/dist/builders.d.ts +33 -0
  10. package/dist/builders.d.ts.map +1 -1
  11. package/dist/builders.js +10 -1
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +176 -59
  14. package/dist/coverage.d.ts.map +1 -1
  15. package/dist/coverage.js +3 -2
  16. package/dist/doctor.d.ts +17 -4
  17. package/dist/doctor.d.ts.map +1 -1
  18. package/dist/doctor.js +263 -62
  19. package/dist/handle.d.ts.map +1 -1
  20. package/dist/handle.js +67 -30
  21. package/dist/helpers.d.ts +1 -0
  22. package/dist/helpers.d.ts.map +1 -1
  23. package/dist/helpers.js +23 -0
  24. package/dist/index.d.ts +4 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +4 -2
  27. package/dist/list.d.ts +34 -1
  28. package/dist/list.d.ts.map +1 -1
  29. package/dist/list.js +19 -9
  30. package/dist/patch-resolver.d.ts.map +1 -1
  31. package/dist/patch-resolver.js +193 -53
  32. package/dist/process.d.ts +2 -0
  33. package/dist/process.d.ts.map +1 -0
  34. package/dist/process.js +16 -0
  35. package/dist/read-index.d.ts +7 -7
  36. package/dist/read-index.d.ts.map +1 -1
  37. package/dist/read-index.js +18 -13
  38. package/dist/record.js +2 -2
  39. package/dist/recovery.d.ts +8 -0
  40. package/dist/recovery.d.ts.map +1 -1
  41. package/dist/recovery.js +142 -30
  42. package/dist/render/retro.d.ts.map +1 -1
  43. package/dist/render/retro.js +4 -1
  44. package/dist/runtime-config.d.ts +14 -0
  45. package/dist/runtime-config.d.ts.map +1 -0
  46. package/dist/runtime-config.js +97 -0
  47. package/dist/schema/entry-core.d.ts +5 -2
  48. package/dist/schema/entry-core.d.ts.map +1 -1
  49. package/dist/schema/entry-core.js +3 -0
  50. package/dist/schema/entry-read.d.ts +57 -0
  51. package/dist/schema/entry-read.d.ts.map +1 -1
  52. package/dist/schema/entry-read.js +9 -1
  53. package/dist/schema/entry-write.d.ts +51 -0
  54. package/dist/schema/entry-write.d.ts.map +1 -1
  55. package/dist/schema/entry-write.js +9 -1
  56. package/dist/storage/filesystem.d.ts +15 -2
  57. package/dist/storage/filesystem.d.ts.map +1 -1
  58. package/dist/storage/filesystem.js +234 -37
  59. package/dist/storage/lock.d.ts.map +1 -1
  60. package/dist/storage/lock.js +38 -16
  61. package/dist/text-lines.d.ts +8 -0
  62. package/dist/text-lines.d.ts.map +1 -0
  63. package/dist/text-lines.js +20 -0
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.d.ts.map +1 -1
  66. package/dist/version.js +2 -1
  67. package/package.json +4 -2
@@ -147,6 +147,39 @@ export declare const buildChangeLogRecord: (input: BuildRecordInput<Extract<Ledg
147
147
  phase: "capture" | "intake" | "seed" | "vendor-extract" | "cleanup" | "parity" | "characterize" | "equiv" | "analyze" | "recover" | "ship" | "migration";
148
148
  summary: string;
149
149
  };
150
+ /** Build a `stage-write` record. */
151
+ export declare const buildStageWriteRecord: (input: BuildRecordInput<Extract<LedgerRecord, {
152
+ kind: "stage-write";
153
+ }>>) => {
154
+ filePath: string;
155
+ kind: "stage-write";
156
+ rationale: string;
157
+ stage: "intake" | "seed" | "vendor-extract" | "cleanup" | "candidate-promotion";
158
+ details?: {
159
+ [x: string]: unknown;
160
+ } | undefined;
161
+ emitter: {
162
+ tool: string;
163
+ user?: string | undefined;
164
+ version: string;
165
+ };
166
+ idempotencyKey?: string | undefined;
167
+ links?: ({
168
+ affectedFiles?: string[] | undefined;
169
+ blobs?: string[] | undefined;
170
+ briefId?: string | undefined;
171
+ correctsLedgerId?: string | undefined;
172
+ gitRef?: string | undefined;
173
+ idempotencyKey?: string | undefined;
174
+ stripDecisionId?: string | undefined;
175
+ supersedesLedgerId?: string | undefined;
176
+ validatorVerdictId?: string | undefined;
177
+ } & {
178
+ [key: string]: unknown;
179
+ }) | undefined;
180
+ phase: "capture" | "intake" | "seed" | "vendor-extract" | "cleanup" | "parity" | "characterize" | "equiv" | "analyze" | "recover" | "ship" | "migration";
181
+ summary: string;
182
+ };
150
183
  /** Build a `strip-decision-reverted` record. */
151
184
  export declare const buildStripDecisionRevertedRecord: (input: BuildRecordInput<Extract<LedgerRecord, {
152
185
  kind: "strip-decision-reverted";
@@ -1 +1 @@
1
- {"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../src/builders.ts"],"names":[],"mappings":"AACA,OAAO,EAGH,KAAK,YAAY,EAIpB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,gBAAgB,CAAC,OAAO,SAAS,YAAY,IAAI,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG;IACjF,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;CACnC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,2BAA2B,GACpC,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAM3E,CAAC;AAEP,yCAAyC;AACzC,eAAO,MAAM,0BAA0B,GACnC,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAK1E,CAAC;AAEP,sEAAsE;AACtE,eAAO,MAAM,qBAAqB,GAAI,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;CAItG,CAAC;AAEP,mCAAmC;AACnC,eAAO,MAAM,oBAAoB,GAAI,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAIrG,CAAC;AAEP,gDAAgD;AAChD,eAAO,MAAM,gCAAgC,GACzC,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,yBAAyB,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAKjF,CAAC"}
1
+ {"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../src/builders.ts"],"names":[],"mappings":"AACA,OAAO,EAGH,KAAK,YAAY,EAKpB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,gBAAgB,CAAC,OAAO,SAAS,YAAY,IAAI,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG;IACjF,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;CACnC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,2BAA2B,GACpC,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAM3E,CAAC;AAEP,yCAAyC;AACzC,eAAO,MAAM,0BAA0B,GACnC,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAK1E,CAAC;AAEP,sEAAsE;AACtE,eAAO,MAAM,qBAAqB,GAAI,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;CAItG,CAAC;AAEP,mCAAmC;AACnC,eAAO,MAAM,oBAAoB,GAAI,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAIrG,CAAC;AAEP,oCAAoC;AACpC,eAAO,MAAM,qBAAqB,GAAI,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAQvG,CAAC;AAEP,gDAAgD;AAChD,eAAO,MAAM,gCAAgC,GACzC,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,yBAAyB,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAKjF,CAAC"}
package/dist/builders.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as v from 'valibot';
2
- import { ChangeLogRecordSchema, CorrectionRecordSchema, OperatorDecisionRecordSchema, StripDecisionRevertedRecordSchema, ValidatorResultRecordSchema, } from "./schema/entry.js";
2
+ import { ChangeLogRecordSchema, CorrectionRecordSchema, OperatorDecisionRecordSchema, StageWriteRecordSchema, StripDecisionRevertedRecordSchema, ValidatorResultRecordSchema, } from "./schema/entry.js";
3
3
  /** Build an `operator-decision` record with a validated structured payload. */
4
4
  export const buildOperatorDecisionRecord = (input) => v.parse(OperatorDecisionRecordSchema, {
5
5
  ...input,
@@ -21,6 +21,15 @@ export const buildChangeLogRecord = (input) => v.parse(ChangeLogRecordSchema, {
21
21
  ...input,
22
22
  kind: 'change-log',
23
23
  });
24
+ /** Build a `stage-write` record. */
25
+ export const buildStageWriteRecord = (input) => v.parse(StageWriteRecordSchema, {
26
+ ...input,
27
+ kind: 'stage-write',
28
+ links: {
29
+ ...(input.links ?? {}),
30
+ affectedFiles: [...new Set([...(input.links?.affectedFiles ?? []), input.filePath])],
31
+ },
32
+ });
24
33
  /** Build a `strip-decision-reverted` record. */
25
34
  export const buildStripDecisionRevertedRecord = (input) => v.parse(StripDecisionRevertedRecordSchema, {
26
35
  ...input,
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAuDA,KAAK,UAAU,GAAG;IACd,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE;QACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;KAC5B,CAAC;IACF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,cAAc,GAAG,aAAa,CAAC,UAAU,GAAG,MAAM,CAAC,CAAC;IAC3E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;CAC1C,CAAC;AAi5BF,eAAO,MAAM,YAAY,GAAU,MAAM,SAAS,MAAM,EAAE,EAAE,UAAS,OAAO,CAAC,UAAU,CAAM,KAAG,OAAO,CAAC,MAAM,CAqC7G,CAAC;AAEF,eAAO,MAAM,IAAI,GAAU,OAAM,SAAS,MAAM,EAA0B,KAAG,OAAO,CAAC,MAAM,CAE1F,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA6DA,KAAK,UAAU,GAAG;IACd,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE;QACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;KAC5B,CAAC;IACF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,cAAc,GAAG,aAAa,CAAC,UAAU,GAAG,MAAM,CAAC,CAAC;IAC3E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;CAC1C,CAAC;AAsjCF,eAAO,MAAM,YAAY,GAAU,MAAM,SAAS,MAAM,EAAE,EAAE,UAAS,OAAO,CAAC,UAAU,CAAM,KAAG,OAAO,CAAC,MAAM,CAqC7G,CAAC;AAEF,eAAO,MAAM,IAAI,GAAU,OAAM,SAAS,MAAM,EAA0B,KAAG,OAAO,CAAC,MAAM,CAE1F,CAAC"}
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
- import { ChangeLogParityStatusSchema, ChangeLogSmokeResultSchema, ChangeLogSubkindSchema, LEDGER_KINDS, LEDGER_PHASES, parseLedgerRecord, WorkspaceRelativePathSchema, } from "./schema/entry.js";
12
+ import { ChangeLogParityStatusSchema, ChangeLogSmokeResultSchema, ChangeLogSubkindSchema, LEDGER_KINDS, LEDGER_PHASES, STAGE_WRITE_STAGES, 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
16
  const DEFAULT_GIT_DIFF_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
16
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',
@@ -35,7 +38,8 @@ const CHANGE_LOG_RECORD_ONLY_FLAGS = [
35
38
  'smoke-result',
36
39
  'subkind',
37
40
  ];
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]`;
41
+ const STAGE_WRITE_ONLY_FLAGS = ['file-path', 'stage'];
42
+ const renderRecordUsage = (commandName) => `${commandName} record [--workspace=<ws>] --kind=<kind> --phase=<phase> --summary="..." [--rationale="..."] [--file-path=<path>] [--stage=<stage>] [--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]`;
39
43
  class CliUsageError extends Error {
40
44
  }
41
45
  const DEFAULT_CONTEXT = {
@@ -51,9 +55,21 @@ const DEFAULT_CONTEXT = {
51
55
  const renderValidValues = () => `Valid values:
52
56
  kinds: ${LEDGER_KINDS.join(', ')}
53
57
  phases: ${LEDGER_PHASES.join(', ')}
58
+ stage-write stages: ${STAGE_WRITE_STAGES.join(', ')}
54
59
  note subkinds: ${NoteSubkindSchema.options.join(', ')}
55
60
  render targets: ${RENDER_TARGETS.join(', ')}
56
61
  `;
62
+ const renderRuntimeTuningHelp = () => `Runtime tuning env vars:
63
+ USHMAN_LEDGER_SCAN_BATCH_SIZE (default: 32)
64
+ USHMAN_LEDGER_SCAN_CONCURRENCY (default: 16)
65
+ USHMAN_LEDGER_READ_INDEX_REBUILD_BATCH_SIZE (default: USHMAN_LEDGER_SCAN_BATCH_SIZE)
66
+ USHMAN_LEDGER_READ_INDEX_REBUILD_CONCURRENCY (default: USHMAN_LEDGER_SCAN_CONCURRENCY)
67
+ USHMAN_LEDGER_COVERAGE_FILE_STAT_CONCURRENCY (default: USHMAN_LEDGER_SCAN_CONCURRENCY)
68
+ USHMAN_LEDGER_BLOB_HASH_CONCURRENCY (default: USHMAN_LEDGER_SCAN_CONCURRENCY)
69
+ USHMAN_LEDGER_DOCTOR_CHECKPOINT_MAX_AGE_MS (default: 86400000)
70
+ USHMAN_LEDGER_DOCTOR_OPEN_ISSUE_MAX_AGE_MS (default: 2592000000)
71
+ USHMAN_LEDGER_MAX_PATCH_BYTES (default: 10485760)
72
+ `;
57
73
  const renderHelp = (commandName) => `${commandName}
58
74
 
59
75
  Commands:
@@ -64,28 +80,37 @@ Commands:
64
80
  ${commandName} tail [--workspace=<ws>] [--phase=<phase>] [--limit=<n>]
65
81
  ${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>]
66
82
  ${commandName} archive [--workspace=<ws>] --out=<file.tgz>
67
- ${commandName} doctor [--workspace=<ws>]
83
+ ${commandName} doctor [--workspace=<ws>] [--json]
68
84
  ${commandName} --version
69
85
 
70
- ${renderValidValues()}`;
86
+ ${renderValidValues()}
87
+ ${renderRuntimeTuningHelp()}`;
71
88
  const renderCommandHelp = (commandName, command) => {
72
89
  switch (command) {
73
90
  case 'record':
74
91
  return `${renderRecordUsage(commandName)}
75
92
 
76
- ${renderValidValues()}`;
93
+ ${renderValidValues()}
94
+ ${renderRuntimeTuningHelp()}`;
77
95
  case 'note':
78
96
  return `${commandName} note <subkind> [--workspace=<ws>] --phase=<phase> --summary="..." [--body=<markdown-file>] [--from-stdin]
79
97
 
80
- ${renderValidValues()}`;
98
+ ${renderValidValues()}
99
+ ${renderRuntimeTuningHelp()}`;
81
100
  case 'list':
82
101
  return `${commandName} list [--workspace=<ws>] [--phase=<phase>] [--kind=<kind>] [--since=<iso>] [--limit=<n>] [--json]
83
102
 
84
- ${renderValidValues()}`;
103
+ ${renderValidValues()}
104
+ ${renderRuntimeTuningHelp()}`;
85
105
  case 'render':
86
106
  return `${commandName} render [--workspace=<ws>] [--to=<target>] [--phase=<phase>] [--since=<iso>] [--limit=<n>] [--out=<file>]
87
107
 
88
- ${renderValidValues()}`;
108
+ ${renderValidValues()}
109
+ ${renderRuntimeTuningHelp()}`;
110
+ case 'doctor':
111
+ return `${commandName} doctor [--workspace=<ws>] [--json]
112
+
113
+ ${renderRuntimeTuningHelp()}`;
89
114
  default:
90
115
  return renderHelp(commandName);
91
116
  }
@@ -111,7 +136,7 @@ const parseArgv = (argv) => {
111
136
  continue;
112
137
  }
113
138
  const next = argv[index + 1];
114
- if (!next || next.startsWith('--')) {
139
+ if (BOOLEAN_FLAG_NAMES.has(name) || !next || next.startsWith('--')) {
115
140
  flags[name] = true;
116
141
  continue;
117
142
  }
@@ -192,6 +217,18 @@ const parseWorkspaceRelativePathCsv = ({ flagName, raw }) => {
192
217
  }
193
218
  return uniquePaths;
194
219
  };
220
+ const parseWorkspaceRelativePathFlag = ({ flagName, flags, required = false, }) => {
221
+ const raw = required ? getRequiredString(flags, flagName) : getFlag(flags, flagName);
222
+ if (!raw) {
223
+ return undefined;
224
+ }
225
+ try {
226
+ return v.parse(WorkspaceRelativePathSchema, raw);
227
+ }
228
+ catch {
229
+ throw new CliUsageError(`--${flagName} path is not a normalized workspace-relative path: ${raw}`);
230
+ }
231
+ };
195
232
  const isGitDiffMaxBufferError = (error) => {
196
233
  const code = getErrorCode(error);
197
234
  if (code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
@@ -211,11 +248,31 @@ const isGitDiffTimeoutError = (error) => {
211
248
  error.killed === true &&
212
249
  error.signal === 'SIGTERM');
213
250
  };
251
+ const buildGitDiffArgs = (gitOptions, gitRef) => gitOptions.scopedPaths.length === 0
252
+ ? ['diff', ...GIT_DIFF_FORMAT_ARGS, gitRef]
253
+ : ['diff', ...GIT_DIFF_FORMAT_ARGS, gitRef, '--', ...gitOptions.scopedPaths];
254
+ const throwGitDiffFailure = ({ error, gitOptions, gitRef, }) => {
255
+ if (getErrorCode(error) === 'ENOENT') {
256
+ 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.');
257
+ }
258
+ if (isGitDiffTimeoutError(error)) {
259
+ throw new CliUsageError(`git diff ${gitRef} timed out after ${gitOptions.timeoutMs}ms. Narrow the diff or increase --git-diff-timeout-ms.`);
260
+ }
261
+ if (isGitDiffMaxBufferError(error)) {
262
+ throw new CliUsageError(`git diff ${gitRef} exceeded the configured stdout buffer (${gitOptions.maxBufferBytes} bytes). Narrow the diff or increase --git-diff-max-buffer-bytes.`);
263
+ }
264
+ const stderr = typeof error === 'object' && error !== null && 'stderr' in error
265
+ ? String(error.stderr ?? '').trim()
266
+ : '';
267
+ if (stderr.length > 0) {
268
+ throw new CliUsageError(`git diff ${gitRef} failed: ${stderr}`);
269
+ }
270
+ throw new CliUsageError(`git diff ${gitRef} failed. Ensure the ref exists and the workspace is a git repository.`);
271
+ };
214
272
  const materializeGitDiff = async ({ gitOptions, gitRef, workspaceRoot, }) => {
215
273
  let tempDir;
216
274
  try {
217
- const gitArgs = gitOptions.scopedPaths.length === 0 ? ['diff', gitRef] : ['diff', gitRef, '--', ...gitOptions.scopedPaths];
218
- const { stdout } = await execFileAsync('git', gitArgs, {
275
+ const { stdout } = await execFileAsync('git', buildGitDiffArgs(gitOptions, gitRef), {
219
276
  cwd: workspaceRoot,
220
277
  maxBuffer: gitOptions.maxBufferBytes,
221
278
  timeout: gitOptions.timeoutMs,
@@ -229,16 +286,11 @@ const materializeGitDiff = async ({ gitOptions, gitRef, workspaceRoot, }) => {
229
286
  if (tempDir) {
230
287
  await rm(tempDir, { force: true, recursive: true });
231
288
  }
232
- if (getErrorCode(error) === 'ENOENT') {
233
- throw new CliUsageError('git is required for --diff-from-git and was not found in PATH.');
234
- }
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.`);
240
- }
241
- throw error;
289
+ return throwGitDiffFailure({
290
+ error,
291
+ gitOptions,
292
+ gitRef,
293
+ });
242
294
  }
243
295
  };
244
296
  const parseJsonInput = (text, sourceLabel) => {
@@ -252,6 +304,9 @@ const parseJsonInput = (text, sourceLabel) => {
252
304
  const print = (context, text) => {
253
305
  context.stdout.write(text.endsWith('\n') ? text : `${text}\n`);
254
306
  };
307
+ const printJson = (context, value) => {
308
+ print(context, JSON.stringify(value, null, 2));
309
+ };
255
310
  const parseLimit = (flags) => getFlag(flags, 'limit')
256
311
  ? parsePositiveIntegerFlag({
257
312
  defaultValue: 0,
@@ -311,6 +366,13 @@ const parseRenderTarget = (flags) => {
311
366
  }
312
367
  return target;
313
368
  };
369
+ const parseRequiredStageWriteStage = (flags) => {
370
+ const stage = getRequiredString(flags, 'stage');
371
+ if (!STAGE_WRITE_STAGES.includes(stage)) {
372
+ throw new CliUsageError(`Invalid --stage value: ${stage}. Expected one of: ${STAGE_WRITE_STAGES.join(', ')}.`);
373
+ }
374
+ return stage;
375
+ };
314
376
  const splitCommandLines = (value) => value
315
377
  .split(/\r?\n/u)
316
378
  .map((command) => command.trim())
@@ -397,7 +459,7 @@ const assertDiffRecordKindSupported = (kind) => {
397
459
  }
398
460
  throw new CliUsageError('--diff and --diff-from-git are only supported for patch and change-log records.');
399
461
  };
400
- const readPatchText = async (diffPath) => readFile(diffPath, 'utf8');
462
+ const readPatchText = async (diffPath) => readPatchTextFromFile(diffPath);
401
463
  const readPatchTextForRecordKind = async (kind, diffPath) => {
402
464
  if (kind === 'change-log' || kind === 'agent-patch' || kind === 'operator-patch') {
403
465
  return readPatchText(diffPath);
@@ -461,11 +523,13 @@ const validateRecordStdinFlags = (flags) => {
461
523
  'operator',
462
524
  'parity-status',
463
525
  'phase',
526
+ 'file-path',
464
527
  'rationale',
465
528
  'rollback-plan',
466
529
  'rolls-back',
467
530
  'smoke-notes',
468
531
  'smoke-result',
532
+ 'stage',
469
533
  'subkind',
470
534
  'summary',
471
535
  ].filter((flagName) => hasFlag(flags, flagName));
@@ -476,53 +540,87 @@ const validateRecordStdinFlags = (flags) => {
476
540
  .map((flagName) => `--${flagName}`)
477
541
  .join(', ')}. When using --from-stdin, provide all record fields in the JSON input.`);
478
542
  };
479
- const buildBaseRecordFromFlags = (parsed, context) => {
480
- const kind = getRequiredKind(parsed.flags);
481
- if (kind !== 'change-log') {
482
- rejectUnsupportedFlags('change-log', parsed.flags, CHANGE_LOG_RECORD_ONLY_FLAGS);
543
+ const applyRationaleFlag = ({ flags, kind, record, }) => {
544
+ const rationale = getFlag(flags, 'rationale');
545
+ if (!rationale) {
546
+ return rationale;
483
547
  }
484
- const record = {
485
- emitter: {
486
- tool: context.defaultEmitter.tool,
487
- version: context.defaultEmitter.version,
488
- },
489
- kind,
490
- phase: getRequiredPhase(parsed.flags),
491
- summary: getRequiredString(parsed.flags, 'summary'),
492
- };
493
- const rationale = getFlag(parsed.flags, 'rationale');
494
- if (rationale) {
495
- if (kind !== 'agent-patch' &&
496
- kind !== 'operator-patch' &&
497
- kind !== 'operator-decision' &&
498
- kind !== 'correction') {
499
- throw new CliUsageError('--rationale is only supported for patch, operator-decision, and correction records.');
500
- }
501
- if (kind !== 'operator-decision') {
502
- record.rationale = rationale;
503
- }
548
+ if (kind !== 'agent-patch' &&
549
+ kind !== 'operator-patch' &&
550
+ kind !== 'operator-decision' &&
551
+ kind !== 'correction' &&
552
+ kind !== 'stage-write') {
553
+ throw new CliUsageError('--rationale is only supported for patch, stage-write, operator-decision, and correction records.');
554
+ }
555
+ if (kind !== 'operator-decision') {
556
+ record.rationale = rationale;
504
557
  }
558
+ return rationale;
559
+ };
560
+ const applyKindSpecificRecordFields = ({ defaultTool, flags, kind, rationale, record, }) => {
505
561
  if (kind === 'agent-patch') {
506
562
  record.agent = {
507
- name: getFlag(parsed.flags, 'agent') ?? context.defaultEmitter.tool,
563
+ name: getFlag(flags, 'agent') ?? defaultTool,
508
564
  };
565
+ return;
509
566
  }
510
567
  if (kind === 'operator-patch') {
511
568
  record.operator = {
512
- name: getFlag(parsed.flags, 'operator') ?? context.defaultEmitter.tool,
569
+ name: getFlag(flags, 'operator') ?? defaultTool,
513
570
  };
571
+ return;
514
572
  }
515
573
  if (kind === 'operator-decision') {
516
- const action = getRequiredString(parsed.flags, 'action');
517
574
  record.payload = {
518
- action,
519
- checkId: getFlag(parsed.flags, 'check-id'),
575
+ action: getRequiredString(flags, 'action'),
576
+ checkId: getFlag(flags, 'check-id'),
520
577
  rationale: rationale ?? '',
521
578
  };
579
+ return;
522
580
  }
523
581
  if (kind === 'change-log') {
524
- record.subkind = parseRequiredChangeLogSubkind(parsed.flags);
582
+ record.subkind = parseRequiredChangeLogSubkind(flags);
583
+ return;
584
+ }
585
+ if (kind === 'stage-write') {
586
+ record.filePath = parseWorkspaceRelativePathFlag({
587
+ flagName: 'file-path',
588
+ flags,
589
+ required: true,
590
+ });
591
+ record.rationale = getRequiredString(flags, 'rationale');
592
+ record.stage = parseRequiredStageWriteStage(flags);
525
593
  }
594
+ };
595
+ const buildBaseRecordFromFlags = (parsed, context) => {
596
+ const kind = getRequiredKind(parsed.flags);
597
+ if (kind !== 'change-log') {
598
+ rejectUnsupportedFlags('change-log', parsed.flags, CHANGE_LOG_RECORD_ONLY_FLAGS);
599
+ }
600
+ if (kind !== 'stage-write') {
601
+ rejectUnsupportedFlags('stage-write', parsed.flags, STAGE_WRITE_ONLY_FLAGS);
602
+ }
603
+ const record = {
604
+ emitter: {
605
+ tool: context.defaultEmitter.tool,
606
+ version: context.defaultEmitter.version,
607
+ },
608
+ kind,
609
+ phase: getRequiredPhase(parsed.flags),
610
+ summary: getRequiredString(parsed.flags, 'summary'),
611
+ };
612
+ const rationale = applyRationaleFlag({
613
+ flags: parsed.flags,
614
+ kind,
615
+ record,
616
+ });
617
+ applyKindSpecificRecordFields({
618
+ defaultTool: context.defaultEmitter.tool,
619
+ flags: parsed.flags,
620
+ kind,
621
+ rationale,
622
+ record,
623
+ });
526
624
  const idempotencyKey = getFlag(parsed.flags, 'idempotency-key');
527
625
  if (idempotencyKey) {
528
626
  record.idempotencyKey = idempotencyKey;
@@ -695,16 +793,22 @@ const runNoteCli = async (parsed, context) => {
695
793
  const runListCli = async (parsed, context) => {
696
794
  const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
697
795
  if (hasFlag(parsed.flags, 'json')) {
698
- const entries = [];
796
+ let wroteEntry = false;
797
+ context.stdout.write('[\n');
699
798
  for await (const entry of ledger.list({
700
799
  kind: parseOptionalKind(parsed.flags),
701
800
  limit: parseLimit(parsed.flags),
702
801
  phase: parseOptionalPhase(parsed.flags),
703
802
  since: parseSince(parsed.flags),
704
803
  })) {
705
- entries.push(entry);
804
+ if (wroteEntry) {
805
+ context.stdout.write(',\n');
806
+ }
807
+ const renderedEntry = JSON.stringify(entry, null, 2).replaceAll('\n', '\n ');
808
+ context.stdout.write(` ${renderedEntry}`);
809
+ wroteEntry = true;
706
810
  }
707
- print(context, JSON.stringify(entries, null, 2));
811
+ context.stdout.write(wroteEntry ? '\n]\n' : ']\n');
708
812
  return 0;
709
813
  }
710
814
  for await (const entry of ledger.list({
@@ -762,16 +866,29 @@ const runArchiveCli = async (parsed, context) => {
762
866
  print(context, result.integrityHash);
763
867
  return 0;
764
868
  };
869
+ const formatDoctorFinding = (finding) => [`[${finding.code}] ${finding.message}`, `Next step: ${finding.remediation}`].join('\n');
870
+ const printDoctorFindings = (context, report) => {
871
+ context.stderr.write(`doctor found ${report.issueCount} issue${report.issueCount === 1 ? '' : 's'}.\n`);
872
+ for (const [index, finding] of report.findings.entries()) {
873
+ context.stderr.write(`\n${formatDoctorFinding(finding)}`);
874
+ if (index < report.findings.length - 1) {
875
+ context.stderr.write('\n');
876
+ }
877
+ }
878
+ context.stderr.write('\n');
879
+ };
765
880
  const runDoctorCli = async (parsed, context) => {
766
881
  const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
767
882
  const result = await ledger.doctor();
883
+ if (hasFlag(parsed.flags, 'json')) {
884
+ printJson(context, result);
885
+ return result.ok ? 0 : 1;
886
+ }
768
887
  if (result.ok) {
769
888
  print(context, 'ok');
770
889
  return 0;
771
890
  }
772
- for (const issue of result.issues) {
773
- context.stderr.write(`${issue}\n`);
774
- }
891
+ printDoctorFindings(context, result);
775
892
  return 1;
776
893
  };
777
894
  const formatCliError = (error) => {
@@ -787,7 +904,7 @@ const formatCliError = (error) => {
787
904
  })
788
905
  .join('\n');
789
906
  }
790
- return error instanceof Error ? (error.stack ?? error.message) : String(error);
907
+ return error instanceof Error ? error.message : String(error);
791
908
  };
792
909
  export const runLedgerCli = async (argv, context = {}) => {
793
910
  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", "pending-commit-quarantined", "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;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"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,EAAiD,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAUxG,eAAO,MAAM,oBAAoB,4fAkBvB,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;AAwlBF,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,CAsCtB,CAAC"}