ushman-ledger 0.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 (101) hide show
  1. package/AGENTS.md +41 -0
  2. package/CHANGELOG.md +23 -0
  3. package/LICENSE.md +21 -0
  4. package/README.md +233 -0
  5. package/dist/archive-journal.d.ts +63 -0
  6. package/dist/archive-journal.d.ts.map +1 -0
  7. package/dist/archive-journal.js +220 -0
  8. package/dist/archive.d.ts +30 -0
  9. package/dist/archive.d.ts.map +1 -0
  10. package/dist/archive.js +117 -0
  11. package/dist/async.d.ts +2 -0
  12. package/dist/async.d.ts.map +1 -0
  13. package/dist/async.js +20 -0
  14. package/dist/blobs.d.ts +10 -0
  15. package/dist/blobs.d.ts.map +1 -0
  16. package/dist/blobs.js +58 -0
  17. package/dist/builders.d.ts +465 -0
  18. package/dist/builders.d.ts.map +1 -0
  19. package/dist/builders.js +73 -0
  20. package/dist/candidate-paths.d.ts +3 -0
  21. package/dist/candidate-paths.d.ts.map +1 -0
  22. package/dist/candidate-paths.js +11 -0
  23. package/dist/cli.d.ts +15 -0
  24. package/dist/cli.d.ts.map +1 -0
  25. package/dist/cli.js +562 -0
  26. package/dist/coverage.d.ts +8 -0
  27. package/dist/coverage.d.ts.map +1 -0
  28. package/dist/coverage.js +128 -0
  29. package/dist/doctor.d.ts +9 -0
  30. package/dist/doctor.d.ts.map +1 -0
  31. package/dist/doctor.js +172 -0
  32. package/dist/handle.d.ts +28 -0
  33. package/dist/handle.d.ts.map +1 -0
  34. package/dist/handle.js +90 -0
  35. package/dist/index.d.ts +11 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +9 -0
  38. package/dist/json.d.ts +4 -0
  39. package/dist/json.d.ts.map +1 -0
  40. package/dist/json.js +25 -0
  41. package/dist/lab-min.d.ts +9 -0
  42. package/dist/lab-min.d.ts.map +1 -0
  43. package/dist/lab-min.js +23 -0
  44. package/dist/list.d.ts +582 -0
  45. package/dist/list.d.ts.map +1 -0
  46. package/dist/list.js +139 -0
  47. package/dist/manifest-update.d.ts +13 -0
  48. package/dist/manifest-update.d.ts.map +1 -0
  49. package/dist/manifest-update.js +43 -0
  50. package/dist/note.d.ts +13 -0
  51. package/dist/note.d.ts.map +1 -0
  52. package/dist/note.js +15 -0
  53. package/dist/patch-metadata.d.ts +37 -0
  54. package/dist/patch-metadata.d.ts.map +1 -0
  55. package/dist/patch-metadata.js +300 -0
  56. package/dist/read-index.d.ts +114 -0
  57. package/dist/read-index.d.ts.map +1 -0
  58. package/dist/read-index.js +210 -0
  59. package/dist/record.d.ts +25 -0
  60. package/dist/record.d.ts.map +1 -0
  61. package/dist/record.js +268 -0
  62. package/dist/recovery.d.ts +39 -0
  63. package/dist/recovery.d.ts.map +1 -0
  64. package/dist/recovery.js +189 -0
  65. package/dist/render/analytics-summary.d.ts +58 -0
  66. package/dist/render/analytics-summary.d.ts.map +1 -0
  67. package/dist/render/analytics-summary.js +151 -0
  68. package/dist/render/dependency-graph.d.ts +3 -0
  69. package/dist/render/dependency-graph.d.ts.map +1 -0
  70. package/dist/render/dependency-graph.js +18 -0
  71. package/dist/render/jsonl.d.ts +3 -0
  72. package/dist/render/jsonl.d.ts.map +1 -0
  73. package/dist/render/jsonl.js +8 -0
  74. package/dist/render/retro.d.ts +6 -0
  75. package/dist/render/retro.d.ts.map +1 -0
  76. package/dist/render/retro.js +124 -0
  77. package/dist/render/timeline-html.d.ts +3 -0
  78. package/dist/render/timeline-html.d.ts.map +1 -0
  79. package/dist/render/timeline-html.js +37 -0
  80. package/dist/schema/entry.d.ts +3298 -0
  81. package/dist/schema/entry.d.ts.map +1 -0
  82. package/dist/schema/entry.js +619 -0
  83. package/dist/schema/manifest.d.ts +42 -0
  84. package/dist/schema/manifest.d.ts.map +1 -0
  85. package/dist/schema/manifest.js +27 -0
  86. package/dist/schema/note.d.ts +10 -0
  87. package/dist/schema/note.d.ts.map +1 -0
  88. package/dist/schema/note.js +2 -0
  89. package/dist/storage/filesystem.d.ts +35 -0
  90. package/dist/storage/filesystem.d.ts.map +1 -0
  91. package/dist/storage/filesystem.js +258 -0
  92. package/dist/storage/lock.d.ts +18 -0
  93. package/dist/storage/lock.d.ts.map +1 -0
  94. package/dist/storage/lock.js +224 -0
  95. package/dist/uuid.d.ts +7 -0
  96. package/dist/uuid.d.ts.map +1 -0
  97. package/dist/uuid.js +25 -0
  98. package/dist/version.d.ts +2 -0
  99. package/dist/version.d.ts.map +1 -0
  100. package/dist/version.js +1 -0
  101. package/package.json +73 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../src/builders.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import { AgentPatchRecordSchema, CorrectionRecordSchema, DescopeBriefRecordSchema, MergeReturnRecordSchema, MergeReturnRejectedRecordSchema, OperatorDecisionPayloadSchema, OperatorDecisionRecordSchema, OperatorPatchRecordSchema, PatchPayloadWriteSchema, RevertRecordSchema, ReworkTestRetiredRecordSchema, RollbackRecordSchema, StageTransitionRecordSchema, StripDecisionRevertedRecordSchema, ValidatorResultRecordSchema, } from "./schema/entry.js";
2
+ import { generateUuidV7 } from "./uuid.js";
3
+ /** Build an `agent-patch` record, deriving structured payload fields later if only `diffPath` is supplied. */
4
+ export const buildAgentPatchRecord = (input) => AgentPatchRecordSchema.parse({
5
+ ...input,
6
+ kind: 'agent-patch',
7
+ payload: input.payload ? PatchPayloadWriteSchema.parse(input.payload) : undefined,
8
+ });
9
+ /** Build an `operator-patch` record, deriving structured payload fields later if only `diffPath` is supplied. */
10
+ export const buildOperatorPatchRecord = (input) => OperatorPatchRecordSchema.parse({
11
+ ...input,
12
+ kind: 'operator-patch',
13
+ payload: input.payload ? PatchPayloadWriteSchema.parse(input.payload) : undefined,
14
+ });
15
+ /** Build a `stage-transition` record with a validated structured payload. */
16
+ export const buildStageTransitionRecord = (input) => StageTransitionRecordSchema.parse({
17
+ ...input,
18
+ kind: 'stage-transition',
19
+ });
20
+ /** Build an `operator-decision` record using the required structured payload shape. */
21
+ export const buildOperatorDecisionRecord = (input) => OperatorDecisionRecordSchema.parse({
22
+ ...input,
23
+ kind: 'operator-decision',
24
+ payload: OperatorDecisionPayloadSchema.parse(input.payload),
25
+ });
26
+ /** Build a `validator-result` record and synthesize `payload.id` only when the caller omits it. */
27
+ export const buildValidatorResultRecord = (input) => ValidatorResultRecordSchema.parse({
28
+ ...input,
29
+ kind: 'validator-result',
30
+ payload: {
31
+ id: input.payload?.id ?? generateUuidV7(),
32
+ },
33
+ });
34
+ /** Build a `correction` record with validated linkage fields. */
35
+ export const buildCorrectionRecord = (input) => CorrectionRecordSchema.parse({
36
+ ...input,
37
+ kind: 'correction',
38
+ });
39
+ /** Build a `strip-decision-reverted` record. */
40
+ export const buildStripDecisionRevertedRecord = (input) => StripDecisionRevertedRecordSchema.parse({
41
+ ...input,
42
+ kind: 'strip-decision-reverted',
43
+ });
44
+ /** Build a `descope-brief` record. */
45
+ export const buildDescopeBriefRecord = (input) => DescopeBriefRecordSchema.parse({
46
+ ...input,
47
+ kind: 'descope-brief',
48
+ });
49
+ /** Build a `merge-return` record. */
50
+ export const buildMergeReturnRecord = (input) => MergeReturnRecordSchema.parse({
51
+ ...input,
52
+ kind: 'merge-return',
53
+ });
54
+ /** Build a `merge-return-rejected` record. */
55
+ export const buildMergeReturnRejectedRecord = (input) => MergeReturnRejectedRecordSchema.parse({
56
+ ...input,
57
+ kind: 'merge-return-rejected',
58
+ });
59
+ /** Build a `revert` record. */
60
+ export const buildRevertRecord = (input) => RevertRecordSchema.parse({
61
+ ...input,
62
+ kind: 'revert',
63
+ });
64
+ /** Build a `rollback` record. */
65
+ export const buildRollbackRecord = (input) => RollbackRecordSchema.parse({
66
+ ...input,
67
+ kind: 'rollback',
68
+ });
69
+ /** Build a `rework.test_retired` record. */
70
+ export const buildReworkTestRetiredRecord = (input) => ReworkTestRetiredRecordSchema.parse({
71
+ ...input,
72
+ kind: 'rework.test_retired',
73
+ });
@@ -0,0 +1,3 @@
1
+ export declare const CANDIDATE_FILE_GLOBS: readonly ["src/**/*", "public/**/*", "index.html", "vite.config.ts", "vite.config.js", "package.json", "tsconfig.json"];
2
+ export declare const CANDIDATE_EXCLUDE_GLOBS: readonly ["node_modules/**", "dist/**", ".vite/**", ".lab/**", "asl/**"];
3
+ //# sourceMappingURL=candidate-paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"candidate-paths.d.ts","sourceRoot":"","sources":["../src/candidate-paths.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,oBAAoB,yHAQvB,CAAC;AAEX,eAAO,MAAM,uBAAuB,0EAA2E,CAAC"}
@@ -0,0 +1,11 @@
1
+ // PLACEHOLDER — replace with import from @ushman/lab-types after ushman M1 lands.
2
+ export const CANDIDATE_FILE_GLOBS = [
3
+ 'src/**/*',
4
+ 'public/**/*',
5
+ 'index.html',
6
+ 'vite.config.ts',
7
+ 'vite.config.js',
8
+ 'package.json',
9
+ 'tsconfig.json',
10
+ ];
11
+ export const CANDIDATE_EXCLUDE_GLOBS = ['node_modules/**', 'dist/**', '.vite/**', '.lab/**', 'asl/**'];
package/dist/cli.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ type CliContext = {
3
+ readonly commandName: string;
4
+ readonly defaultEmitter: {
5
+ readonly tool: string;
6
+ readonly version: string;
7
+ };
8
+ readonly stdin: NodeJS.ReadableStream & AsyncIterable<Uint8Array | string>;
9
+ readonly stderr: NodeJS.WritableStream;
10
+ readonly stdout: NodeJS.WritableStream;
11
+ };
12
+ export declare const runLedgerCli: (argv: readonly string[], context?: Partial<CliContext>) => Promise<number>;
13
+ export declare const main: (argv?: readonly string[]) => Promise<number>;
14
+ export {};
15
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAoBA,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;AA2jBF,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 ADDED
@@ -0,0 +1,562 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from 'node:child_process';
3
+ import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { promisify } from 'node:util';
8
+ import { ZodError } from 'zod';
9
+ import { openLedger } from "./handle.js";
10
+ import { LEDGER_KINDS, LEDGER_PHASES, parseLedgerRecord } from "./schema/entry.js";
11
+ import { NoteSubkindSchema } from "./schema/note.js";
12
+ import { LEDGER_LIBRARY_VERSION } from "./version.js";
13
+ const execFileAsync = promisify(execFile);
14
+ const GIT_DIFF_TIMEOUT_MS = 30_000;
15
+ const RENDER_TARGETS = ['retro', 'jsonl', 'timeline-html', 'dependency-graph', 'analytics-summary'];
16
+ class CliUsageError extends Error {
17
+ }
18
+ const DEFAULT_CONTEXT = {
19
+ commandName: 'ushman-ledger',
20
+ defaultEmitter: {
21
+ tool: 'ushman-ledger',
22
+ version: LEDGER_LIBRARY_VERSION,
23
+ },
24
+ stderr: process.stderr,
25
+ stdin: process.stdin,
26
+ stdout: process.stdout,
27
+ };
28
+ const renderValidValues = () => `Valid values:
29
+ kinds: ${LEDGER_KINDS.join(', ')}
30
+ phases: ${LEDGER_PHASES.join(', ')}
31
+ note subkinds: ${NoteSubkindSchema.options.join(', ')}
32
+ render targets: ${RENDER_TARGETS.join(', ')}
33
+ `;
34
+ const renderHelp = (commandName) => `${commandName}
35
+
36
+ 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]
38
+ ${commandName} note <subkind> [--workspace=<ws>] --phase=<phase> --summary="..." [--body=<markdown-file>] [--from-stdin]
39
+ ${commandName} list [--workspace=<ws>] [--phase=<phase>] [--kind=<kind>] [--since=<iso>] [--limit=<n>] [--json]
40
+ ${commandName} show [--workspace=<ws>] <entry-id>
41
+ ${commandName} tail [--workspace=<ws>] [--phase=<phase>] [--limit=<n>]
42
+ ${commandName} render [--workspace=<ws>] [--to=retro|jsonl|timeline-html|dependency-graph|analytics-summary] [--phase=<phase>] [--out=<file>] [--fresh] [--json]
43
+ ${commandName} archive [--workspace=<ws>] --out=<file.tgz>
44
+ ${commandName} doctor [--workspace=<ws>]
45
+ ${commandName} --version
46
+
47
+ ${renderValidValues()}`;
48
+ const renderCommandHelp = (commandName, command) => {
49
+ switch (command) {
50
+ 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]
52
+
53
+ ${renderValidValues()}`;
54
+ case 'note':
55
+ return `${commandName} note <subkind> [--workspace=<ws>] --phase=<phase> --summary="..." [--body=<markdown-file>] [--from-stdin]
56
+
57
+ ${renderValidValues()}`;
58
+ case 'list':
59
+ return `${commandName} list [--workspace=<ws>] [--phase=<phase>] [--kind=<kind>] [--since=<iso>] [--limit=<n>] [--json]
60
+
61
+ ${renderValidValues()}`;
62
+ case 'render':
63
+ return `${commandName} render [--workspace=<ws>] [--to=<target>] [--phase=<phase>] [--out=<file>] [--fresh] [--json]
64
+
65
+ Use \`--fresh\` to bypass the cached analytics summary. \`--json\` remains a compatibility alias for the same behavior.
66
+
67
+ ${renderValidValues()}`;
68
+ default:
69
+ return renderHelp(commandName);
70
+ }
71
+ };
72
+ const parseArgv = (argv) => {
73
+ const flags = {};
74
+ const positionals = [];
75
+ for (let index = 0; index < argv.length; index += 1) {
76
+ const token = argv[index];
77
+ if (!token) {
78
+ continue;
79
+ }
80
+ if (!token.startsWith('--')) {
81
+ positionals.push(token);
82
+ continue;
83
+ }
84
+ const body = token.slice(2);
85
+ const equalsIndex = body.indexOf('=');
86
+ const name = equalsIndex !== -1 ? body.slice(0, equalsIndex) : body;
87
+ if (equalsIndex !== -1) {
88
+ const inlineValue = body.slice(equalsIndex + 1);
89
+ flags[name] = inlineValue;
90
+ continue;
91
+ }
92
+ const next = argv[index + 1];
93
+ if (!next || next.startsWith('--')) {
94
+ flags[name] = true;
95
+ continue;
96
+ }
97
+ flags[name] = next;
98
+ index += 1;
99
+ }
100
+ return { flags, positionals };
101
+ };
102
+ const getFlag = (flags, name) => {
103
+ const value = flags[name];
104
+ return typeof value === 'string' ? value : undefined;
105
+ };
106
+ const hasFlag = (flags, name) => flags[name] === true || typeof flags[name] === 'string';
107
+ const readStdinText = async (stdin) => {
108
+ const readableStdin = stdin;
109
+ if (readableStdin.isTTY) {
110
+ throw new CliUsageError('--from-stdin requires piped input.');
111
+ }
112
+ const chunks = [];
113
+ for await (const chunk of stdin) {
114
+ chunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'));
115
+ }
116
+ return chunks.join('');
117
+ };
118
+ const getWorkspaceRoot = (flags) => path.resolve(getFlag(flags, 'workspace') ?? '.');
119
+ const getRequiredString = (flags, name) => {
120
+ const value = getFlag(flags, name);
121
+ if (!value) {
122
+ throw new CliUsageError(`Missing required --${name}.`);
123
+ }
124
+ return value;
125
+ };
126
+ const ensureFileExists = async (filePath, flagName) => {
127
+ try {
128
+ const fileStat = await stat(filePath);
129
+ if (!fileStat.isFile()) {
130
+ throw new CliUsageError(`${flagName} must point to a file: ${filePath}`);
131
+ }
132
+ }
133
+ catch (error) {
134
+ if (error.code === 'ENOENT') {
135
+ throw new CliUsageError(`${flagName} file not found: ${filePath}`);
136
+ }
137
+ throw error;
138
+ }
139
+ };
140
+ const materializeGitDiff = async (workspaceRoot, gitRef) => {
141
+ let tempDir;
142
+ try {
143
+ const { stdout } = await execFileAsync('git', ['diff', gitRef], {
144
+ cwd: workspaceRoot,
145
+ maxBuffer: 10 * 1024 * 1024,
146
+ timeout: GIT_DIFF_TIMEOUT_MS,
147
+ });
148
+ tempDir = await mkdtemp(path.join(os.tmpdir(), 'ushman-ledger-git-diff-'));
149
+ const patchPath = path.join(tempDir, 'patch.diff');
150
+ await writeFile(patchPath, stdout, 'utf8');
151
+ return { patchPath, tempDir };
152
+ }
153
+ catch (error) {
154
+ if (tempDir) {
155
+ await rm(tempDir, { force: true, recursive: true });
156
+ }
157
+ if (error.code === 'ENOENT') {
158
+ throw new CliUsageError('git is required for --diff-from-git and was not found in PATH.');
159
+ }
160
+ if (error.code === 'ETIMEDOUT') {
161
+ throw new CliUsageError(`git diff ${gitRef} timed out after ${GIT_DIFF_TIMEOUT_MS}ms. Narrow the diff or run git manually.`);
162
+ }
163
+ throw error;
164
+ }
165
+ };
166
+ const parseJsonInput = (text, sourceLabel) => {
167
+ try {
168
+ return JSON.parse(text);
169
+ }
170
+ catch (error) {
171
+ throw new CliUsageError(`Invalid JSON from ${sourceLabel}: ${error instanceof Error ? error.message : String(error)}`);
172
+ }
173
+ };
174
+ const print = (context, text) => {
175
+ context.stdout.write(text.endsWith('\n') ? text : `${text}\n`);
176
+ };
177
+ const parseLimit = (flags) => {
178
+ const raw = getFlag(flags, 'limit');
179
+ if (!raw) {
180
+ return undefined;
181
+ }
182
+ if (!/^[1-9]\d*$/u.test(raw)) {
183
+ throw new CliUsageError(`Invalid --limit value: ${raw}`);
184
+ }
185
+ return Number.parseInt(raw, 10);
186
+ };
187
+ const parseOptionalKind = (flags) => {
188
+ const kind = getFlag(flags, 'kind');
189
+ if (!kind) {
190
+ return undefined;
191
+ }
192
+ if (!LEDGER_KINDS.includes(kind)) {
193
+ throw new CliUsageError(`Invalid --kind value: ${kind}. Expected one of: ${LEDGER_KINDS.join(', ')}.`);
194
+ }
195
+ return kind;
196
+ };
197
+ const getRequiredKind = (flags) => {
198
+ const kind = parseOptionalKind(flags);
199
+ if (!kind) {
200
+ throw new CliUsageError('Missing required --kind.');
201
+ }
202
+ return kind;
203
+ };
204
+ const parseOptionalPhase = (flags) => {
205
+ const phase = getFlag(flags, 'phase');
206
+ if (!phase) {
207
+ return undefined;
208
+ }
209
+ if (!LEDGER_PHASES.includes(phase)) {
210
+ throw new CliUsageError(`Invalid --phase value: ${phase}. Expected one of: ${LEDGER_PHASES.join(', ')}.`);
211
+ }
212
+ return phase;
213
+ };
214
+ const getRequiredPhase = (flags) => {
215
+ const phase = parseOptionalPhase(flags);
216
+ if (!phase) {
217
+ throw new CliUsageError('Missing required --phase.');
218
+ }
219
+ return phase;
220
+ };
221
+ const parseSince = (flags) => {
222
+ const since = getFlag(flags, 'since');
223
+ if (!since) {
224
+ return undefined;
225
+ }
226
+ const parsed = Date.parse(since);
227
+ if (Number.isNaN(parsed)) {
228
+ throw new CliUsageError(`Invalid --since value: ${since}. Expected an ISO-8601 timestamp.`);
229
+ }
230
+ return new Date(parsed).toISOString();
231
+ };
232
+ const parseRenderTarget = (flags) => {
233
+ const target = getFlag(flags, 'to') ?? 'retro';
234
+ if (!RENDER_TARGETS.includes(target)) {
235
+ throw new CliUsageError(`Invalid --to value: ${target}. Expected one of: ${RENDER_TARGETS.join(', ')}.`);
236
+ }
237
+ return target;
238
+ };
239
+ const validateRecordStdinFlags = (flags) => {
240
+ const conflictingFlags = [
241
+ 'agent',
242
+ 'action',
243
+ 'check-id',
244
+ 'diff',
245
+ 'diff-from-git',
246
+ 'idempotency-key',
247
+ 'kind',
248
+ 'operator',
249
+ 'phase',
250
+ 'rationale',
251
+ 'summary',
252
+ ].filter((flagName) => hasFlag(flags, flagName));
253
+ if (conflictingFlags.length === 0) {
254
+ return;
255
+ }
256
+ throw new CliUsageError(`--from-stdin cannot be combined with record field flags: ${conflictingFlags
257
+ .map((flagName) => `--${flagName}`)
258
+ .join(', ')}. When using --from-stdin, provide all record fields in the JSON input.`);
259
+ };
260
+ const buildBaseRecordFromFlags = (parsed, context) => {
261
+ const kind = getRequiredKind(parsed.flags);
262
+ const record = {
263
+ emitter: {
264
+ tool: context.defaultEmitter.tool,
265
+ version: context.defaultEmitter.version,
266
+ },
267
+ kind,
268
+ phase: getRequiredPhase(parsed.flags),
269
+ summary: getRequiredString(parsed.flags, 'summary'),
270
+ };
271
+ const rationale = getFlag(parsed.flags, 'rationale');
272
+ if (rationale) {
273
+ if (kind !== 'agent-patch' &&
274
+ kind !== 'operator-patch' &&
275
+ kind !== 'operator-decision' &&
276
+ kind !== 'correction') {
277
+ throw new CliUsageError('--rationale is only supported for patch, operator-decision, and correction records.');
278
+ }
279
+ if (kind !== 'operator-decision') {
280
+ record.rationale = rationale;
281
+ }
282
+ }
283
+ if (kind === 'agent-patch') {
284
+ record.agent = {
285
+ name: getFlag(parsed.flags, 'agent') ?? context.defaultEmitter.tool,
286
+ };
287
+ }
288
+ if (kind === 'operator-patch') {
289
+ record.operator = {
290
+ name: getFlag(parsed.flags, 'operator') ?? context.defaultEmitter.tool,
291
+ };
292
+ }
293
+ if (kind === 'operator-decision') {
294
+ const action = getRequiredString(parsed.flags, 'action');
295
+ record.payload = {
296
+ action,
297
+ checkId: getFlag(parsed.flags, 'check-id'),
298
+ rationale: rationale ?? '',
299
+ };
300
+ }
301
+ const idempotencyKey = getFlag(parsed.flags, 'idempotency-key');
302
+ if (idempotencyKey) {
303
+ record.idempotencyKey = idempotencyKey;
304
+ }
305
+ return { kind, record };
306
+ };
307
+ const applyPatchInputToRecord = async ({ kind, record, workspaceRoot, parsed, }) => {
308
+ const diffPath = getFlag(parsed.flags, 'diff');
309
+ if (diffPath) {
310
+ if (kind !== 'agent-patch' && kind !== 'operator-patch') {
311
+ throw new CliUsageError('--diff is only supported for patch records.');
312
+ }
313
+ await ensureFileExists(path.resolve(diffPath), '--diff');
314
+ record.diffPath = diffPath;
315
+ }
316
+ const diffFromGit = getFlag(parsed.flags, 'diff-from-git');
317
+ if (!diffFromGit) {
318
+ return {};
319
+ }
320
+ if (kind !== 'agent-patch' && kind !== 'operator-patch') {
321
+ throw new CliUsageError('--diff-from-git is only supported for patch records.');
322
+ }
323
+ if (diffPath) {
324
+ throw new CliUsageError('Use either --diff or --diff-from-git, not both.');
325
+ }
326
+ const materialized = await materializeGitDiff(workspaceRoot, diffFromGit);
327
+ return {
328
+ cleanupTempDir: materialized.tempDir,
329
+ record: {
330
+ ...record,
331
+ diffPath: materialized.patchPath,
332
+ links: {
333
+ gitRef: diffFromGit,
334
+ },
335
+ },
336
+ };
337
+ };
338
+ const buildRecordFromFlags = async (parsed, context) => {
339
+ const workspaceRoot = getWorkspaceRoot(parsed.flags);
340
+ if (hasFlag(parsed.flags, 'from-stdin')) {
341
+ validateRecordStdinFlags(parsed.flags);
342
+ const stdinRecord = parseLedgerRecord(parseJsonInput(await readStdinText(context.stdin), 'stdin'));
343
+ return {
344
+ record: stdinRecord,
345
+ workspaceRoot,
346
+ };
347
+ }
348
+ const { kind, record } = buildBaseRecordFromFlags(parsed, context);
349
+ const patchInput = await applyPatchInputToRecord({
350
+ kind,
351
+ parsed,
352
+ record,
353
+ workspaceRoot,
354
+ });
355
+ return {
356
+ cleanupTempDir: patchInput.cleanupTempDir,
357
+ record: patchInput.record ?? record,
358
+ workspaceRoot,
359
+ };
360
+ };
361
+ const buildNoteRecord = async (parsed, context) => {
362
+ const [subkind] = parsed.positionals;
363
+ if (!subkind) {
364
+ throw new CliUsageError('Missing note subkind.');
365
+ }
366
+ if (!NoteSubkindSchema.options.includes(subkind)) {
367
+ throw new CliUsageError(`Invalid note subkind: ${subkind}. Expected one of: ${NoteSubkindSchema.options.join(', ')}.`);
368
+ }
369
+ const bodyFromFile = getFlag(parsed.flags, 'body');
370
+ if (bodyFromFile) {
371
+ await ensureFileExists(path.resolve(bodyFromFile), '--body');
372
+ }
373
+ const body = hasFlag(parsed.flags, 'from-stdin')
374
+ ? await readStdinText(context.stdin)
375
+ : bodyFromFile
376
+ ? await readFile(path.resolve(bodyFromFile), 'utf8')
377
+ : '';
378
+ const phase = getRequiredString(parsed.flags, 'phase');
379
+ if (!LEDGER_PHASES.includes(phase)) {
380
+ throw new CliUsageError(`Invalid --phase value: ${phase}. Expected one of: ${LEDGER_PHASES.join(', ')}.`);
381
+ }
382
+ return {
383
+ record: {
384
+ body,
385
+ emitter: {
386
+ tool: context.defaultEmitter.tool,
387
+ version: context.defaultEmitter.version,
388
+ },
389
+ kind: 'note',
390
+ phase: phase,
391
+ subkind: subkind,
392
+ summary: getRequiredString(parsed.flags, 'summary'),
393
+ },
394
+ workspaceRoot: getWorkspaceRoot(parsed.flags),
395
+ };
396
+ };
397
+ const runRecordCli = async (parsed, context) => {
398
+ const { cleanupTempDir, record, workspaceRoot } = await buildRecordFromFlags(parsed, context);
399
+ try {
400
+ const ledger = await openLedger(workspaceRoot);
401
+ const result = await ledger.record(record);
402
+ print(context, result.id);
403
+ return 0;
404
+ }
405
+ finally {
406
+ if (cleanupTempDir) {
407
+ await rm(cleanupTempDir, { force: true, recursive: true });
408
+ }
409
+ }
410
+ };
411
+ const runNoteCli = async (parsed, context) => {
412
+ const { record, workspaceRoot } = await buildNoteRecord(parsed, context);
413
+ const ledger = await openLedger(workspaceRoot);
414
+ const result = await ledger.record(record);
415
+ print(context, result.id);
416
+ return 0;
417
+ };
418
+ const runListCli = async (parsed, context) => {
419
+ const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
420
+ if (hasFlag(parsed.flags, 'json')) {
421
+ const entries = [];
422
+ for await (const entry of ledger.list({
423
+ kind: parseOptionalKind(parsed.flags),
424
+ limit: parseLimit(parsed.flags),
425
+ phase: parseOptionalPhase(parsed.flags),
426
+ since: parseSince(parsed.flags),
427
+ })) {
428
+ entries.push(entry);
429
+ }
430
+ print(context, JSON.stringify(entries, null, 2));
431
+ return 0;
432
+ }
433
+ for await (const entry of ledger.list({
434
+ kind: parseOptionalKind(parsed.flags),
435
+ limit: parseLimit(parsed.flags),
436
+ phase: parseOptionalPhase(parsed.flags),
437
+ since: parseSince(parsed.flags),
438
+ })) {
439
+ print(context, `${entry.id} ${entry.phase} ${entry.kind} ${entry.summary}`);
440
+ }
441
+ return 0;
442
+ };
443
+ const runShowCli = async (parsed, context) => {
444
+ const entryId = parsed.positionals[0];
445
+ if (!entryId) {
446
+ throw new CliUsageError('Missing entry id.');
447
+ }
448
+ const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
449
+ const entry = await ledger.show(entryId);
450
+ if (!entry) {
451
+ throw new Error(`Ledger entry not found: ${entryId}`);
452
+ }
453
+ print(context, JSON.stringify(entry, null, 2));
454
+ return 0;
455
+ };
456
+ const runTailCli = async (parsed, context) => runListCli({
457
+ ...parsed,
458
+ flags: {
459
+ ...parsed.flags,
460
+ limit: getFlag(parsed.flags, 'limit') ?? '10',
461
+ },
462
+ }, context);
463
+ const runRenderCli = async (parsed, context) => {
464
+ const target = parseRenderTarget(parsed.flags);
465
+ const wantsFresh = hasFlag(parsed.flags, 'fresh') || hasFlag(parsed.flags, 'json');
466
+ if (wantsFresh && target !== 'analytics-summary') {
467
+ throw new CliUsageError('--fresh and --json are only supported for render --to=analytics-summary.');
468
+ }
469
+ const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
470
+ const content = await ledger.render({
471
+ fresh: wantsFresh,
472
+ out: getFlag(parsed.flags, 'out'),
473
+ phase: parseOptionalPhase(parsed.flags),
474
+ to: target,
475
+ });
476
+ if (!getFlag(parsed.flags, 'out')) {
477
+ print(context, content);
478
+ }
479
+ return 0;
480
+ };
481
+ const runArchiveCli = async (parsed, context) => {
482
+ const outPath = getRequiredString(parsed.flags, 'out');
483
+ const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
484
+ const result = await ledger.archive(outPath);
485
+ print(context, result.integrityHash);
486
+ return 0;
487
+ };
488
+ const runDoctorCli = async (parsed, context) => {
489
+ const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
490
+ const result = await ledger.doctor();
491
+ if (result.ok) {
492
+ print(context, 'ok');
493
+ return 0;
494
+ }
495
+ for (const issue of result.issues) {
496
+ context.stderr.write(`${issue}\n`);
497
+ }
498
+ return 1;
499
+ };
500
+ const formatCliError = (error) => {
501
+ if (error instanceof CliUsageError) {
502
+ return error.message;
503
+ }
504
+ if (error instanceof ZodError) {
505
+ return error.issues
506
+ .map((issue) => {
507
+ const pathLabel = issue.path.length > 0 ? issue.path.join('.') : 'input';
508
+ return `${pathLabel}: ${issue.message}`;
509
+ })
510
+ .join('\n');
511
+ }
512
+ return error instanceof Error ? (error.stack ?? error.message) : String(error);
513
+ };
514
+ export const runLedgerCli = async (argv, context = {}) => {
515
+ const mergedContext = { ...DEFAULT_CONTEXT, ...context };
516
+ const [command, ...rest] = argv;
517
+ if (!command || command === '--help' || command === '-h' || command === 'help') {
518
+ print(mergedContext, renderHelp(mergedContext.commandName));
519
+ return 0;
520
+ }
521
+ if (command === '--version' || command === '-v' || command === 'version') {
522
+ print(mergedContext, LEDGER_LIBRARY_VERSION);
523
+ return 0;
524
+ }
525
+ const parsed = parseArgv(rest);
526
+ if (hasFlag(parsed.flags, 'help') || rest.includes('-h')) {
527
+ print(mergedContext, renderCommandHelp(mergedContext.commandName, command));
528
+ return 0;
529
+ }
530
+ switch (command) {
531
+ case 'record':
532
+ return runRecordCli(parsed, mergedContext);
533
+ case 'note':
534
+ return runNoteCli(parsed, mergedContext);
535
+ case 'list':
536
+ return runListCli(parsed, mergedContext);
537
+ case 'show':
538
+ return runShowCli(parsed, mergedContext);
539
+ case 'tail':
540
+ return runTailCli(parsed, mergedContext);
541
+ case 'render':
542
+ return runRenderCli(parsed, mergedContext);
543
+ case 'archive':
544
+ return runArchiveCli(parsed, mergedContext);
545
+ case 'doctor':
546
+ return runDoctorCli(parsed, mergedContext);
547
+ default:
548
+ throw new Error(`Unknown command: ${command}`);
549
+ }
550
+ };
551
+ export const main = async (argv = process.argv.slice(2)) => {
552
+ return runLedgerCli(argv);
553
+ };
554
+ const isDirectExecution = () => Boolean(process.argv[1]) && path.resolve(fileURLToPath(import.meta.url)) === path.resolve(process.argv[1] ?? '');
555
+ if (isDirectExecution()) {
556
+ main().then((code) => {
557
+ process.exit(code);
558
+ }, (error) => {
559
+ process.stderr.write(`${formatCliError(error)}\n`);
560
+ process.exit(error instanceof CliUsageError || error instanceof ZodError ? 1 : 2);
561
+ });
562
+ }