ushman-ledger 1.1.0 → 1.2.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 (66) hide show
  1. package/AGENTS.md +11 -7
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +22 -3
  4. package/dist/blobs.js +3 -3
  5. package/dist/builders.d.ts +43 -0
  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 +222 -41
  10. package/dist/doctor.d.ts.map +1 -1
  11. package/dist/doctor.js +104 -4
  12. package/dist/handle.d.ts +3 -1
  13. package/dist/handle.d.ts.map +1 -1
  14. package/dist/handle.js +19 -1
  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 +3 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -2
  21. package/dist/list.d.ts +43 -1
  22. package/dist/list.d.ts.map +1 -1
  23. package/dist/note.d.ts +20 -0
  24. package/dist/note.d.ts.map +1 -1
  25. package/dist/note.js +5 -0
  26. package/dist/patch-resolver.d.ts +27 -0
  27. package/dist/patch-resolver.d.ts.map +1 -0
  28. package/dist/patch-resolver.js +184 -0
  29. package/dist/read-index.d.ts +7 -7
  30. package/dist/record.d.ts.map +1 -1
  31. package/dist/record.js +15 -39
  32. package/dist/render/migration-log.d.ts +3 -0
  33. package/dist/render/migration-log.d.ts.map +1 -0
  34. package/dist/render/migration-log.js +72 -0
  35. package/dist/render/retro.d.ts.map +1 -1
  36. package/dist/render/retro.js +40 -21
  37. package/dist/render/workspace-narrative.d.ts +6 -0
  38. package/dist/render/workspace-narrative.d.ts.map +1 -0
  39. package/dist/render/workspace-narrative.js +69 -0
  40. package/dist/schema/entry-core.d.ts +110 -0
  41. package/dist/schema/entry-core.d.ts.map +1 -0
  42. package/dist/schema/entry-core.js +143 -0
  43. package/dist/schema/entry-migrations.d.ts +3 -0
  44. package/dist/schema/entry-migrations.d.ts.map +1 -0
  45. package/dist/schema/entry-migrations.js +48 -0
  46. package/dist/schema/entry-read.d.ts +694 -0
  47. package/dist/schema/entry-read.d.ts.map +1 -0
  48. package/dist/schema/entry-read.js +92 -0
  49. package/dist/schema/entry-write.d.ts +865 -0
  50. package/dist/schema/entry-write.d.ts.map +1 -0
  51. package/dist/schema/entry-write.js +105 -0
  52. package/dist/schema/entry.d.ts +6 -1369
  53. package/dist/schema/entry.d.ts.map +1 -1
  54. package/dist/schema/entry.js +9 -286
  55. package/dist/schema/note.d.ts +1 -1
  56. package/dist/schema/note.d.ts.map +1 -1
  57. package/dist/schema/note.js +12 -1
  58. package/dist/storage/filesystem.d.ts +2 -0
  59. package/dist/storage/filesystem.d.ts.map +1 -1
  60. package/dist/storage/filesystem.js +2 -0
  61. package/dist/storage/lock-reclaimer.d.ts +2 -0
  62. package/dist/storage/lock-reclaimer.d.ts.map +1 -0
  63. package/dist/storage/lock-reclaimer.js +45 -0
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/package.json +2 -2
package/AGENTS.md CHANGED
@@ -23,13 +23,17 @@ An append-only ledger library and CLI for ushman v4 workspaces. It owns ledger s
23
23
  ## Read order
24
24
 
25
25
  1. `README.md`
26
- 2. `src/schema/entry.ts`
27
- 3. `src/storage/filesystem.ts`
28
- 4. `src/record.ts`
29
- 5. `src/handle.ts`
30
- 6. `src/coverage.ts`
31
- 7. `src/doctor.ts`
32
- 8. `src/cli.ts`
26
+ 2. `src/schema/entry-core.ts`
27
+ 3. `src/schema/entry-read.ts`
28
+ 4. `src/schema/entry-write.ts`
29
+ 5. `src/schema/entry-migrations.ts`
30
+ 6. `src/schema/entry.ts`
31
+ 7. `src/storage/filesystem.ts`
32
+ 8. `src/record.ts`
33
+ 9. `src/handle.ts`
34
+ 10. `src/coverage.ts`
35
+ 11. `src/doctor.ts`
36
+ 12. `src/cli.ts`
33
37
 
34
38
  ## Commands
35
39
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.0] - 2026-05-23
4
+
5
+ - Added `change-log` records, narrative note subkinds, and `migration-log-md` / `workspace-narrative-md` render targets.
6
+ - Split entry schema parsing into focused read/write/core modules and moved compatibility coercions behind dedicated parse migrations.
7
+ - Extracted patch-source resolution behind a dedicated resolver with focused tests and more actionable hash mismatch failures.
8
+
3
9
  ## [1.0.1] - 2026-05-16
4
10
 
5
11
  - Tightened the schema surface to the ledger primitives the v4 orchestrator actively uses.
package/README.md CHANGED
@@ -9,6 +9,7 @@ Append-only workspace ledger library and CLI for ushman v4 workspaces.
9
9
  - Content-hash idempotency
10
10
  - Patch blob storage
11
11
  - Retro / JSONL / timeline / dependency-graph rendering
12
+ - Change-log / workspace narrative markdown rendering
12
13
  - Archive integrity output
13
14
  - Doctor and coverage helpers
14
15
 
@@ -58,6 +59,7 @@ await ledger.record(
58
59
  );
59
60
 
60
61
  await ledger.render({ to: 'retro' });
62
+ await ledger.render({ to: 'migration-log-md' });
61
63
  await ledger.archive('/tmp/ledger.tgz');
62
64
  ```
63
65
 
@@ -67,22 +69,37 @@ await ledger.archive('/tmp/ledger.tgz');
67
69
  ushman-ledger record --workspace=<ws> --kind=tool-invocation --phase=capture --summary="capture started"
68
70
  ushman-ledger record --workspace=<ws> --kind=agent-patch --phase=cleanup --summary="capture git diff" --rationale="track working tree change" --diff-from-git=HEAD
69
71
  ushman-ledger record --workspace=<ws> --kind=operator-decision --phase=cleanup --summary="manual override" --action=ledger-hand-edit --check-id=manual-review --rationale="operator edited the ledger after audit"
72
+ ushman-ledger record --workspace=<ws> --kind=change-log --subkind=semantic-cleanup --phase=cleanup --summary="split schema modules" --diff=/tmp/change.patch --hypothesis="smaller schema modules keep the public API stable" --commands=$'bun test\nbun run typecheck' --smoke-result=pass --parity-status=green --rollback-plan="revert the schema split"
70
73
  ushman-ledger note regression --workspace=<ws> --phase=cleanup --summary="runtime drift" --body=/tmp/note.md
74
+ ushman-ledger note cleanup-wave --workspace=<ws> --phase=cleanup --summary="wave 1" --body=/tmp/narrative.md
71
75
  ushman-ledger list --workspace=<ws> --json
72
76
  ushman-ledger render --workspace=<ws> --to=retro
77
+ ushman-ledger render --workspace=<ws> --to=migration-log-md
78
+ ushman-ledger render --workspace=<ws> --to=workspace-narrative-md
73
79
  ushman-ledger render --workspace=<ws> --to=jsonl --out=/tmp/ledger.jsonl
74
80
  ushman-ledger render --workspace=<ws> --to=dependency-graph --out=/tmp/ledger.mmd
75
81
  ushman-ledger archive --workspace=<ws> --out=/tmp/ledger.tgz
76
82
  ushman-ledger doctor --workspace=<ws>
77
83
  ```
78
84
 
79
- Valid record kinds: `tool-invocation`, `agent-patch`, `operator-patch`, `operator-decision`, `validator-result`, `runtime-event`, `note`, `correction`, `strip-decision-reverted`
85
+ Valid record kinds: `tool-invocation`, `agent-patch`, `operator-patch`, `operator-decision`, `validator-result`, `runtime-event`, `note`, `correction`, `strip-decision-reverted`, `change-log`
80
86
 
81
87
  Valid phases: `capture`, `intake`, `seed`, `vendor-extract`, `cleanup`, `parity`, `characterize`, `equiv`, `analyze`, `recover`, `ship`, `migration`
82
88
 
83
- Valid note subkinds: `regression`, `automation`, `retro`, `operator`, `tooling-gap`
89
+ Valid note subkinds: `regression`, `automation`, `retro`, `operator`, `tooling-gap`, `cleanup-wave`, `verified-flow`, `open-issue`, `decomposition-wave`, `semantic-cleanup-summary`
84
90
 
85
- Valid render targets: `retro`, `jsonl`, `timeline-html`, `dependency-graph`
91
+ Valid render targets: `retro`, `jsonl`, `timeline-html`, `dependency-graph`, `migration-log-md`, `workspace-narrative-md`
92
+
93
+ ## Change-log records
94
+
95
+ `change-log` entries are append-only structured narrative records for migration and cleanup work. They accept:
96
+
97
+ - `subkind`: `pre-change-checkpoint`, `semantic-cleanup`, `vendor-extract`, `decomposition`, `rollback`, `hotfix`, `smoke`
98
+ - `filesChanged`: explicit CSV paths via `--files-changed`, or automatic diffstat-style derivation from `--diff` / `--diff-from-git`
99
+ - optional narrative fields: `hypothesis`, `commandsRun`, `smokeResult`, `smokeNotes`, `parityStatus`, `rollbackPlan`
100
+ - `rollsBack`: required when `subkind=rollback`
101
+
102
+ `migration-log-md` renders only `change-log` entries. `workspace-narrative-md` renders only the dedicated narrative note subkinds.
86
103
 
87
104
  ## Workspace prerequisite
88
105
 
@@ -164,7 +181,9 @@ Valid render targets: `retro`, `jsonl`, `timeline-html`, `dependency-graph`
164
181
  pending-archives/
165
182
  blobs/
166
183
  render.md
184
+ render.migration-log.md
167
185
  render.timeline.html
186
+ render.workspace-narrative.md
168
187
  <phase>/
169
188
  <timestamp>-<sequence>-<hash>.json
170
189
  ```
package/dist/blobs.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, stat } from 'node:fs/promises';
1
+ import { mkdir, readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { sha256File, sha256Hex } from "./json.js";
4
4
  import { resolveLedgerPaths, writeAtomicTextFile } from "./storage/filesystem.js";
@@ -34,11 +34,11 @@ export const storePatchBlob = async (workspaceRoot, patchText) => {
34
34
  const blobPath = buildBlobPath(workspaceRoot, blobSha256);
35
35
  let shouldWrite = true;
36
36
  try {
37
- await stat(blobPath);
38
37
  shouldWrite = (await sha256File(blobPath)) !== blobSha256;
39
38
  }
40
39
  catch (error) {
41
- if (error.code !== 'ENOENT') {
40
+ const code = error.code;
41
+ if (code !== 'ENOENT') {
42
42
  throw error;
43
43
  }
44
44
  }
@@ -104,6 +104,49 @@ export declare const buildCorrectionRecord: (input: BuildRecordInput<Extract<Led
104
104
  phase: "capture" | "intake" | "seed" | "vendor-extract" | "cleanup" | "parity" | "characterize" | "equiv" | "analyze" | "recover" | "ship" | "migration";
105
105
  summary: string;
106
106
  };
107
+ /** Build a `change-log` record. */
108
+ export declare const buildChangeLogRecord: (input: BuildRecordInput<Extract<LedgerRecord, {
109
+ kind: "change-log";
110
+ }>>) => {
111
+ commandsRun?: string[] | undefined;
112
+ filesChanged: {
113
+ added?: number | undefined;
114
+ path: string;
115
+ removed?: number | undefined;
116
+ }[];
117
+ hypothesis?: string | undefined;
118
+ kind: "change-log";
119
+ parityStatus?: "not-run" | "green" | "yellow" | "red" | undefined;
120
+ rollbackPlan?: string | undefined;
121
+ rollsBack?: string | undefined;
122
+ smokeNotes?: string | undefined;
123
+ smokeResult?: "pass" | "fail" | "partial" | "not-run" | undefined;
124
+ subkind: "vendor-extract" | "pre-change-checkpoint" | "semantic-cleanup" | "decomposition" | "rollback" | "hotfix" | "smoke";
125
+ details?: {
126
+ [x: string]: unknown;
127
+ } | undefined;
128
+ emitter: {
129
+ tool: string;
130
+ user?: string | undefined;
131
+ version: string;
132
+ };
133
+ idempotencyKey?: string | undefined;
134
+ links?: ({
135
+ affectedFiles?: string[] | undefined;
136
+ blobs?: string[] | undefined;
137
+ briefId?: string | undefined;
138
+ correctsLedgerId?: string | undefined;
139
+ gitRef?: string | undefined;
140
+ idempotencyKey?: string | undefined;
141
+ stripDecisionId?: string | undefined;
142
+ supersedesLedgerId?: string | undefined;
143
+ validatorVerdictId?: string | undefined;
144
+ } & {
145
+ [key: string]: unknown;
146
+ }) | undefined;
147
+ phase: "capture" | "intake" | "seed" | "vendor-extract" | "cleanup" | "parity" | "characterize" | "equiv" | "analyze" | "recover" | "ship" | "migration";
148
+ summary: string;
149
+ };
107
150
  /** Build a `strip-decision-reverted` record. */
108
151
  export declare const buildStripDecisionRevertedRecord: (input: BuildRecordInput<Extract<LedgerRecord, {
109
152
  kind: "strip-decision-reverted";
@@ -1 +1 @@
1
- {"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../src/builders.ts"],"names":[],"mappings":"AACA,OAAO,EAEH,KAAK,YAAY,EAKpB,MAAM,mBAAmB,CAAC;AAE3B,KAAK,gBAAgB,CAAC,OAAO,SAAS,YAAY,IAAI,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG;IAC1E,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,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,EAIpB,MAAM,mBAAmB,CAAC;AAE3B,KAAK,gBAAgB,CAAC,OAAO,SAAS,YAAY,IAAI,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG;IAC1E,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"}
package/dist/builders.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import * as v from 'valibot';
2
- import { CorrectionRecordSchema, OperatorDecisionPayloadSchema, OperatorDecisionRecordSchema, StripDecisionRevertedRecordSchema, ValidatorResultRecordSchema, } from "./schema/entry.js";
2
+ import { ChangeLogRecordSchema, CorrectionRecordSchema, OperatorDecisionRecordSchema, 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,
6
6
  kind: 'operator-decision',
7
- payload: v.parse(OperatorDecisionPayloadSchema, input.payload),
7
+ payload: input.payload,
8
8
  });
9
9
  /** Build a `validator-result` record. */
10
10
  export const buildValidatorResultRecord = (input) => v.parse(ValidatorResultRecordSchema, {
@@ -16,6 +16,11 @@ export const buildCorrectionRecord = (input) => v.parse(CorrectionRecordSchema,
16
16
  ...input,
17
17
  kind: 'correction',
18
18
  });
19
+ /** Build a `change-log` record. */
20
+ export const buildChangeLogRecord = (input) => v.parse(ChangeLogRecordSchema, {
21
+ ...input,
22
+ kind: 'change-log',
23
+ });
19
24
  /** Build a `strip-decision-reverted` record. */
20
25
  export const buildStripDecisionRevertedRecord = (input) => v.parse(StripDecisionRevertedRecordSchema, {
21
26
  ...input,
package/dist/cli.d.ts.map CHANGED
@@ -1 +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;AAqjBF,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":";AAmDA,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;AAiwBF,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
@@ -7,12 +7,33 @@ 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);
15
+ const GIT_DIFF_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
14
16
  const GIT_DIFF_TIMEOUT_MS = 30_000;
15
- const RENDER_TARGETS = ['retro', 'jsonl', 'timeline-html', 'dependency-graph'];
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 CHANGE_LOG_RECORD_ONLY_FLAGS = [
26
+ 'commands',
27
+ 'commands-from',
28
+ 'files-changed',
29
+ 'hypothesis',
30
+ 'parity-status',
31
+ 'rollback-plan',
32
+ 'rolls-back',
33
+ 'smoke-notes',
34
+ 'smoke-result',
35
+ 'subkind',
36
+ ];
16
37
  class CliUsageError extends Error {
17
38
  }
18
39
  const DEFAULT_CONTEXT = {
@@ -34,12 +55,12 @@ const renderValidValues = () => `Valid values:
34
55
  const renderHelp = (commandName) => `${commandName}
35
56
 
36
57
  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]
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]
38
59
  ${commandName} note <subkind> [--workspace=<ws>] --phase=<phase> --summary="..." [--body=<markdown-file>] [--from-stdin]
39
60
  ${commandName} list [--workspace=<ws>] [--phase=<phase>] [--kind=<kind>] [--since=<iso>] [--limit=<n>] [--json]
40
61
  ${commandName} show [--workspace=<ws>] <entry-id>
41
62
  ${commandName} tail [--workspace=<ws>] [--phase=<phase>] [--limit=<n>]
42
- ${commandName} render [--workspace=<ws>] [--to=retro|jsonl|timeline-html|dependency-graph] [--phase=<phase>] [--out=<file>]
63
+ ${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
64
  ${commandName} archive [--workspace=<ws>] --out=<file.tgz>
44
65
  ${commandName} doctor [--workspace=<ws>]
45
66
  ${commandName} --version
@@ -48,7 +69,7 @@ ${renderValidValues()}`;
48
69
  const renderCommandHelp = (commandName, command) => {
49
70
  switch (command) {
50
71
  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]
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]
52
73
 
53
74
  ${renderValidValues()}`;
54
75
  case 'note':
@@ -60,7 +81,7 @@ ${renderValidValues()}`;
60
81
 
61
82
  ${renderValidValues()}`;
62
83
  case 'render':
63
- return `${commandName} render [--workspace=<ws>] [--to=<target>] [--phase=<phase>] [--out=<file>]
84
+ return `${commandName} render [--workspace=<ws>] [--to=<target>] [--phase=<phase>] [--since=<iso>] [--limit=<n>] [--out=<file>]
64
85
 
65
86
  ${renderValidValues()}`;
66
87
  default:
@@ -99,6 +120,9 @@ const parseArgv = (argv) => {
99
120
  };
100
121
  const getFlag = (flags, name) => {
101
122
  const value = flags[name];
123
+ if (value === true) {
124
+ throw new CliUsageError(`Missing value for --${name}.`);
125
+ }
102
126
  return typeof value === 'string' ? value : undefined;
103
127
  };
104
128
  const hasFlag = (flags, name) => flags[name] === true || typeof flags[name] === 'string';
@@ -140,7 +164,7 @@ const materializeGitDiff = async (workspaceRoot, gitRef) => {
140
164
  try {
141
165
  const { stdout } = await execFileAsync('git', ['diff', gitRef], {
142
166
  cwd: workspaceRoot,
143
- maxBuffer: 10 * 1024 * 1024,
167
+ maxBuffer: GIT_DIFF_MAX_BUFFER_BYTES,
144
168
  timeout: GIT_DIFF_TIMEOUT_MS,
145
169
  });
146
170
  tempDir = await mkdtemp(path.join(os.tmpdir(), 'ushman-ledger-git-diff-'));
@@ -234,18 +258,121 @@ const parseRenderTarget = (flags) => {
234
258
  }
235
259
  return target;
236
260
  };
261
+ const splitCommandLines = (value) => value
262
+ .split(/\r?\n/u)
263
+ .map((command) => command.trim())
264
+ .filter((command) => command.length > 0);
265
+ const getUsedFlags = (flags, names) => names.filter((name) => hasFlag(flags, name)).map((name) => `--${name}`);
266
+ const rejectUnsupportedFlags = (supportedKindLabel, flags, names) => {
267
+ const usedFlags = getUsedFlags(flags, names);
268
+ if (usedFlags.length === 0) {
269
+ return;
270
+ }
271
+ throw new CliUsageError(`${usedFlags.join(', ')} ${usedFlags.length === 1 ? 'is' : 'are'} only supported for ${supportedKindLabel} records.`);
272
+ };
273
+ const parseRequiredChangeLogSubkind = (flags) => {
274
+ const subkind = getRequiredString(flags, 'subkind');
275
+ if (!ChangeLogSubkindSchema.options.includes(subkind)) {
276
+ throw new CliUsageError(`Invalid --subkind value: ${subkind}. Expected one of: ${ChangeLogSubkindSchema.options.join(', ')}.`);
277
+ }
278
+ return subkind;
279
+ };
280
+ const parseOptionalChangeLogPicklist = ({ flagName, flags, options, }) => {
281
+ const value = getFlag(flags, flagName);
282
+ if (!value) {
283
+ return undefined;
284
+ }
285
+ if (!options.includes(value)) {
286
+ throw new CliUsageError(`Invalid --${flagName} value: ${value}. Expected one of: ${options.join(', ')}.`);
287
+ }
288
+ return value;
289
+ };
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 }));
309
+ };
310
+ const readCommandLines = async (flags) => {
311
+ const inlineCommands = getFlag(flags, 'commands');
312
+ const commandsFrom = getFlag(flags, 'commands-from');
313
+ if (inlineCommands && commandsFrom) {
314
+ throw new CliUsageError('Use either --commands or --commands-from, not both.');
315
+ }
316
+ if (commandsFrom) {
317
+ const commandsPath = path.resolve(commandsFrom);
318
+ await ensureFileExists(commandsPath, '--commands-from');
319
+ return splitCommandLines(await readFile(commandsPath, 'utf8'));
320
+ }
321
+ if (inlineCommands) {
322
+ return splitCommandLines(inlineCommands);
323
+ }
324
+ return undefined;
325
+ };
326
+ const resolveDiffInput = async ({ flags, kind, workspaceRoot, }) => {
327
+ const diffPath = getFlag(flags, 'diff');
328
+ const diffFromGit = getFlag(flags, 'diff-from-git');
329
+ if (diffPath && diffFromGit) {
330
+ throw new CliUsageError('Use either --diff or --diff-from-git, not both.');
331
+ }
332
+ if (!diffPath && !diffFromGit) {
333
+ return {};
334
+ }
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.');
337
+ }
338
+ if (diffPath) {
339
+ const resolvedDiffPath = path.resolve(diffPath);
340
+ await ensureFileExists(resolvedDiffPath, '--diff');
341
+ return {
342
+ diffPath: resolvedDiffPath,
343
+ patchText: kind === 'change-log' ? await readFile(resolvedDiffPath, 'utf8') : undefined,
344
+ };
345
+ }
346
+ const materialized = await materializeGitDiff(workspaceRoot, diffFromGit ?? '');
347
+ return {
348
+ cleanupTempDir: materialized.tempDir,
349
+ diffPath: materialized.patchPath,
350
+ gitRef: diffFromGit,
351
+ patchText: kind === 'change-log' ? await readFile(materialized.patchPath, 'utf8') : undefined,
352
+ };
353
+ };
237
354
  const validateRecordStdinFlags = (flags) => {
238
355
  const conflictingFlags = [
239
356
  'agent',
240
357
  'action',
241
358
  'check-id',
359
+ 'commands',
360
+ 'commands-from',
242
361
  'diff',
243
362
  'diff-from-git',
363
+ 'files-changed',
364
+ 'hypothesis',
244
365
  'idempotency-key',
245
366
  'kind',
246
367
  'operator',
368
+ 'parity-status',
247
369
  'phase',
248
370
  'rationale',
371
+ 'rollback-plan',
372
+ 'rolls-back',
373
+ 'smoke-notes',
374
+ 'smoke-result',
375
+ 'subkind',
249
376
  'summary',
250
377
  ].filter((flagName) => hasFlag(flags, flagName));
251
378
  if (conflictingFlags.length === 0) {
@@ -257,6 +384,9 @@ const validateRecordStdinFlags = (flags) => {
257
384
  };
258
385
  const buildBaseRecordFromFlags = (parsed, context) => {
259
386
  const kind = getRequiredKind(parsed.flags);
387
+ if (kind !== 'change-log') {
388
+ rejectUnsupportedFlags('change-log', parsed.flags, CHANGE_LOG_RECORD_ONLY_FLAGS);
389
+ }
260
390
  const record = {
261
391
  emitter: {
262
392
  tool: context.defaultEmitter.tool,
@@ -296,43 +426,68 @@ const buildBaseRecordFromFlags = (parsed, context) => {
296
426
  rationale: rationale ?? '',
297
427
  };
298
428
  }
429
+ if (kind === 'change-log') {
430
+ record.subkind = parseRequiredChangeLogSubkind(parsed.flags);
431
+ }
299
432
  const idempotencyKey = getFlag(parsed.flags, 'idempotency-key');
300
433
  if (idempotencyKey) {
301
434
  record.idempotencyKey = idempotencyKey;
302
435
  }
303
436
  return { kind, record };
304
437
  };
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
- }
438
+ const applyPatchInputToRecord = async ({ kind, patchInput, record, }) => {
318
439
  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.');
440
+ return {};
323
441
  }
324
- const materialized = await materializeGitDiff(workspaceRoot, diffFromGit);
325
442
  return {
326
- cleanupTempDir: materialized.tempDir,
443
+ cleanupTempDir: patchInput.cleanupTempDir,
327
444
  record: {
328
445
  ...record,
329
- diffPath: materialized.patchPath,
330
- links: {
331
- gitRef: diffFromGit,
332
- },
446
+ diffPath: patchInput.diffPath,
447
+ links: patchInput.gitRef
448
+ ? {
449
+ ...record.links,
450
+ gitRef: patchInput.gitRef,
451
+ }
452
+ : record.links,
333
453
  },
334
454
  };
335
455
  };
456
+ const applyChangeLogFlagsToRecord = async ({ flags, patchInput, record, }) => {
457
+ const filesChangedFlag = getFlag(flags, 'files-changed');
458
+ if (filesChangedFlag && patchInput.patchText) {
459
+ throw new CliUsageError('Use either --files-changed or --diff/--diff-from-git to populate change-log files.');
460
+ }
461
+ return {
462
+ ...record,
463
+ commandsRun: await readCommandLines(flags),
464
+ filesChanged: filesChangedFlag
465
+ ? parseFilesChangedCsv(filesChangedFlag)
466
+ : patchInput.patchText
467
+ ? deriveFilesChangedFromPatch(patchInput.patchText)
468
+ : undefined,
469
+ hypothesis: getFlag(flags, 'hypothesis'),
470
+ links: patchInput.gitRef
471
+ ? {
472
+ ...record.links,
473
+ gitRef: patchInput.gitRef,
474
+ }
475
+ : record.links,
476
+ parityStatus: parseOptionalChangeLogPicklist({
477
+ flagName: 'parity-status',
478
+ flags,
479
+ options: ChangeLogParityStatusSchema.options,
480
+ }),
481
+ rollbackPlan: getFlag(flags, 'rollback-plan'),
482
+ rollsBack: getFlag(flags, 'rolls-back'),
483
+ smokeNotes: getFlag(flags, 'smoke-notes'),
484
+ smokeResult: parseOptionalChangeLogPicklist({
485
+ flagName: 'smoke-result',
486
+ flags,
487
+ options: ChangeLogSmokeResultSchema.options,
488
+ }),
489
+ };
490
+ };
336
491
  const buildRecordFromFlags = async (parsed, context) => {
337
492
  const workspaceRoot = getWorkspaceRoot(parsed.flags);
338
493
  if (hasFlag(parsed.flags, 'from-stdin')) {
@@ -344,17 +499,41 @@ const buildRecordFromFlags = async (parsed, context) => {
344
499
  };
345
500
  }
346
501
  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
- };
502
+ let diffInput;
503
+ try {
504
+ diffInput = await resolveDiffInput({
505
+ flags: parsed.flags,
506
+ kind,
507
+ workspaceRoot,
508
+ });
509
+ if (kind === 'change-log') {
510
+ return {
511
+ cleanupTempDir: diffInput.cleanupTempDir,
512
+ record: await applyChangeLogFlagsToRecord({
513
+ flags: parsed.flags,
514
+ patchInput: diffInput,
515
+ record,
516
+ }),
517
+ workspaceRoot,
518
+ };
519
+ }
520
+ const patchInput = await applyPatchInputToRecord({
521
+ kind,
522
+ patchInput: diffInput,
523
+ record,
524
+ });
525
+ return {
526
+ cleanupTempDir: patchInput.cleanupTempDir,
527
+ record: patchInput.record ?? record,
528
+ workspaceRoot,
529
+ };
530
+ }
531
+ catch (error) {
532
+ if (diffInput?.cleanupTempDir) {
533
+ await rm(diffInput.cleanupTempDir, { force: true, recursive: true });
534
+ }
535
+ throw error;
536
+ }
358
537
  };
359
538
  const buildNoteRecord = async (parsed, context) => {
360
539
  const [subkind] = parsed.positionals;
@@ -462,8 +641,10 @@ const runRenderCli = async (parsed, context) => {
462
641
  const target = parseRenderTarget(parsed.flags);
463
642
  const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
464
643
  const content = await ledger.render({
644
+ limit: parseLimit(parsed.flags),
465
645
  out: getFlag(parsed.flags, 'out'),
466
646
  phase: parseOptionalPhase(parsed.flags),
647
+ since: parseSince(parsed.flags),
467
648
  to: target,
468
649
  });
469
650
  if (!getFlag(parsed.flags, 'out')) {
@@ -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;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"}