ushman-ledger 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +57 -5
  2. package/dist/builders.d.ts +1 -2
  3. package/dist/builders.d.ts.map +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +156 -53
  6. package/dist/doctor.d.ts.map +1 -1
  7. package/dist/doctor.js +1 -1
  8. package/dist/handle.d.ts +27 -7
  9. package/dist/handle.d.ts.map +1 -1
  10. package/dist/handle.js +96 -20
  11. package/dist/index.d.ts +4 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -1
  14. package/dist/list.d.ts +1 -1
  15. package/dist/list.d.ts.map +1 -1
  16. package/dist/list.js +7 -5
  17. package/dist/note.d.ts +7 -0
  18. package/dist/note.d.ts.map +1 -1
  19. package/dist/note.js +6 -0
  20. package/dist/patch-resolver.d.ts +12 -0
  21. package/dist/patch-resolver.d.ts.map +1 -1
  22. package/dist/patch-resolver.js +12 -0
  23. package/dist/read-index.d.ts.map +1 -1
  24. package/dist/record.d.ts.map +1 -1
  25. package/dist/record.js +1 -2
  26. package/dist/render/migration-log.d.ts +8 -1
  27. package/dist/render/migration-log.d.ts.map +1 -1
  28. package/dist/render/migration-log.js +40 -33
  29. package/dist/render/retro.d.ts.map +1 -1
  30. package/dist/render/retro.js +1 -7
  31. package/dist/render/workspace-narrative.d.ts +7 -1
  32. package/dist/render/workspace-narrative.d.ts.map +1 -1
  33. package/dist/render/workspace-narrative.js +114 -46
  34. package/dist/schema/entry-read.d.ts.map +1 -1
  35. package/dist/schema/entry-read.js +1 -1
  36. package/dist/schema/entry-write.d.ts.map +1 -1
  37. package/dist/schema/entry-write.js +1 -1
  38. package/dist/schema/entry.d.ts.map +1 -1
  39. package/dist/storage/filesystem.d.ts +7 -0
  40. package/dist/storage/filesystem.d.ts.map +1 -1
  41. package/dist/storage/filesystem.js +80 -5
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/package.json +2 -2
package/README.md CHANGED
@@ -27,9 +27,20 @@ bun link ushman-ledger
27
27
  ## Library API
28
28
 
29
29
  ```ts
30
- import { buildValidatorResultRecord, openLedger } from 'ushman-ledger';
31
-
32
- const ledger = await openLedger('/path/to/workspace');
30
+ import { readFile } from 'node:fs/promises';
31
+ import {
32
+ appendCleanupWaveNote,
33
+ appendSemanticCleanupSummaryNote,
34
+ type BuildRecordInput,
35
+ buildChangeLogRecord,
36
+ buildValidatorResultRecord,
37
+ deriveFilesChangedFromPatch,
38
+ openLedger,
39
+ } from 'ushman-ledger';
40
+
41
+ const workspaceRoot = '/path/to/workspace';
42
+ const ledger = await openLedger(workspaceRoot);
43
+ const patchText = await readFile('/tmp/change.patch', 'utf8');
33
44
 
34
45
  await ledger.record({
35
46
  emitter: { tool: 'ushman-cli', version: '1.0.0' },
@@ -48,6 +59,21 @@ await ledger.record({
48
59
  summary: 'Apply websocket shim',
49
60
  });
50
61
 
62
+ await ledger.record(
63
+ buildChangeLogRecord({
64
+ emitter: { tool: 'ushman-cli', version: '1.0.0' },
65
+ filesChanged: deriveFilesChangedFromPatch(patchText),
66
+ hypothesis: 'Smaller schema modules keep the public API stable.',
67
+ kind: 'change-log',
68
+ parityStatus: 'green',
69
+ phase: 'cleanup',
70
+ rollbackPlan: 'Revert the schema split.',
71
+ smokeResult: 'pass',
72
+ subkind: 'semantic-cleanup',
73
+ summary: 'Split schema modules',
74
+ }),
75
+ );
76
+
51
77
  await ledger.record(
52
78
  buildValidatorResultRecord({
53
79
  emitter: { tool: 'ushman-doctor', version: '1.0.0' },
@@ -58,16 +84,32 @@ await ledger.record(
58
84
  }),
59
85
  );
60
86
 
87
+ await appendCleanupWaveNote(workspaceRoot, {
88
+ body: 'Split schema definitions out of the monolithic entry module.',
89
+ phase: 'cleanup',
90
+ summary: 'Wave 1',
91
+ });
92
+
93
+ await appendSemanticCleanupSummaryNote(workspaceRoot, {
94
+ body: 'The latest semantic cleanup summary shown in workspace-narrative-md.',
95
+ phase: 'migration',
96
+ summary: 'Latest summary',
97
+ });
98
+
61
99
  await ledger.render({ to: 'retro' });
62
100
  await ledger.render({ to: 'migration-log-md' });
101
+ await ledger.renderTo({ to: 'migration-log-md', out: '/tmp/migration-log.md' });
63
102
  await ledger.archive('/tmp/ledger.tgz');
64
103
  ```
65
104
 
105
+ Advanced builder wrappers can reuse the exported `BuildRecordInput<T>` helper type when they want the same input contract the built-in builders accept.
106
+
66
107
  ## CLI
67
108
 
68
109
  ```bash
69
110
  ushman-ledger record --workspace=<ws> --kind=tool-invocation --phase=capture --summary="capture started"
70
111
  ushman-ledger record --workspace=<ws> --kind=agent-patch --phase=cleanup --summary="capture git diff" --rationale="track working tree change" --diff-from-git=HEAD
112
+ ushman-ledger record --workspace=<ws> --kind=change-log --subkind=smoke --phase=cleanup --summary="scope git diff" --diff-from-git=HEAD --git-paths=src/main.ts,src/cli.ts --git-diff-timeout-ms=10000 --git-diff-max-buffer-bytes=20971520
71
113
  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
114
  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"
73
115
  ushman-ledger note regression --workspace=<ws> --phase=cleanup --summary="runtime drift" --body=/tmp/note.md
@@ -99,7 +141,15 @@ Valid render targets: `retro`, `jsonl`, `timeline-html`, `dependency-graph`, `mi
99
141
  - optional narrative fields: `hypothesis`, `commandsRun`, `smokeResult`, `smokeNotes`, `parityStatus`, `rollbackPlan`
100
142
  - `rollsBack`: required when `subkind=rollback`
101
143
 
144
+ `deriveFilesChangedFromPatch()` is exported for library callers that want the same diff-to-file summary behavior as the CLI. Known limits:
145
+
146
+ - It expects `diff --git` headers to delimit files.
147
+ - It counts only textual hunk `+`/`-` lines, so binary or mode-only diffs can report zero line counts.
148
+ - It keeps the post-image path when available and rejects diff blocks that cannot be mapped to normalized workspace-relative paths.
149
+
102
150
  `migration-log-md` renders only `change-log` entries. `workspace-narrative-md` renders only the dedicated narrative note subkinds.
151
+ `render()` still returns a string for in-memory callers. `renderTo()` is the bounded-output alternative for large ledgers and can write to a callback or the canonical render file on disk.
152
+ `renderTo()` writers receive sequential chunks, can return synchronously or asynchronously, and should throw only when the render should abort.
103
153
 
104
154
  ## Workspace prerequisite
105
155
 
@@ -167,7 +217,9 @@ Valid render targets: `retro`, `jsonl`, `timeline-html`, `dependency-graph`, `mi
167
217
  ## Git diff capture
168
218
 
169
219
  - `--diff-from-git=<ref>` runs `git diff <ref>` against the workspace working tree.
170
- - The CLI times out `git diff` after 30 seconds and reports a usage error instead of hanging indefinitely.
220
+ - `--git-paths=<csv>` safely scopes `git diff` to literal normalized workspace-relative paths. The CLI passes them after `--`, so pathspec magic and shell token splitting are intentionally not supported here.
221
+ - `--git-diff-timeout-ms=<ms>` and `--git-diff-max-buffer-bytes=<bytes>` override the default 30 second timeout and 10 MiB stdout buffer for the git diff helper.
222
+ - The CLI reports usage errors for missing `git`, timed out git diffs, invalid scoped paths, and buffer overflows instead of surfacing raw child-process failures.
171
223
  - This is the only CLI feature that depends on an external binary. `git` must be available in `PATH`.
172
224
 
173
225
  ## Storage shape
@@ -200,5 +252,5 @@ bun run bench:scale
200
252
 
201
253
  ## Scale benchmark
202
254
 
203
- - `bun run bench:scale` creates a temporary workspace and benchmarks large-ledger paths for population, limited reads, repeated coverage, and repeated doctor runs.
255
+ - `bun run bench:scale` creates a temporary workspace and benchmarks large-ledger paths for population, limited reads, repeated coverage, repeated doctor runs, and markdown render paths.
204
256
  - Override the defaults with environment variables such as `LEDGER_BENCH_ENTRY_COUNT=100000` and `LEDGER_BENCH_CANDIDATE_FILE_COUNT=10000`.
@@ -1,5 +1,5 @@
1
1
  import { type LedgerRecord } from './schema/entry.ts';
2
- type BuildRecordInput<TRecord extends LedgerRecord> = Omit<TRecord, 'kind'> & {
2
+ export type BuildRecordInput<TRecord extends LedgerRecord> = Omit<TRecord, 'kind'> & {
3
3
  readonly kind?: TRecord['kind'];
4
4
  };
5
5
  /** Build an `operator-decision` record with a validated structured payload. */
@@ -182,5 +182,4 @@ export declare const buildStripDecisionRevertedRecord: (input: BuildRecordInput<
182
182
  phase: "capture" | "intake" | "seed" | "vendor-extract" | "cleanup" | "parity" | "characterize" | "equiv" | "analyze" | "recover" | "ship" | "migration";
183
183
  summary: string;
184
184
  };
185
- export {};
186
185
  //# sourceMappingURL=builders.d.ts.map
@@ -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,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"}
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"}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
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"}
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"}
package/dist/cli.js CHANGED
@@ -12,8 +12,8 @@ import { ChangeLogParityStatusSchema, ChangeLogSmokeResultSchema, ChangeLogSubki
12
12
  import { NoteSubkindSchema } from "./schema/note.js";
13
13
  import { LEDGER_LIBRARY_VERSION } from "./version.js";
14
14
  const execFileAsync = promisify(execFile);
15
- const GIT_DIFF_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
16
- const GIT_DIFF_TIMEOUT_MS = 30_000;
15
+ const DEFAULT_GIT_DIFF_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
16
+ const DEFAULT_GIT_DIFF_TIMEOUT_MS = 30_000;
17
17
  const RENDER_TARGETS = [
18
18
  'retro',
19
19
  'jsonl',
@@ -22,6 +22,7 @@ const RENDER_TARGETS = [
22
22
  'migration-log-md',
23
23
  'workspace-narrative-md',
24
24
  ];
25
+ const GIT_DIFF_FLAG_NAMES = ['git-diff-max-buffer-bytes', 'git-diff-timeout-ms', 'git-paths'];
25
26
  const CHANGE_LOG_RECORD_ONLY_FLAGS = [
26
27
  'commands',
27
28
  'commands-from',
@@ -34,6 +35,7 @@ const CHANGE_LOG_RECORD_ONLY_FLAGS = [
34
35
  'smoke-result',
35
36
  'subkind',
36
37
  ];
38
+ const renderRecordUsage = (commandName) => `${commandName} record [--workspace=<ws>] --kind=<kind> --phase=<phase> --summary="..." [--rationale="..."] [--action=<operator-action>] [--check-id=<check-id>] [--diff=<patch-file>] [--diff-from-git=<ref>] [--git-paths=<csv>] [--git-diff-timeout-ms=<ms>] [--git-diff-max-buffer-bytes=<bytes>] [--idempotency-key=<key>] [--subkind=<change-log-subkind>] [--files-changed=<csv>] [--hypothesis="..."] [--commands="cmd1\ncmd2" | --commands-from=<file>] [--smoke-result=<result>] [--smoke-notes="..."] [--parity-status=<status>] [--rollback-plan="..."] [--rolls-back=<entry-id>] [--from-stdin]`;
37
39
  class CliUsageError extends Error {
38
40
  }
39
41
  const DEFAULT_CONTEXT = {
@@ -55,7 +57,7 @@ const renderValidValues = () => `Valid values:
55
57
  const renderHelp = (commandName) => `${commandName}
56
58
 
57
59
  Commands:
58
- ${commandName} record [--workspace=<ws>] --kind=<kind> --phase=<phase> --summary="..." [--rationale="..."] [--action=<operator-action>] [--check-id=<check-id>] [--diff=<patch-file>] [--diff-from-git=<ref>] [--idempotency-key=<key>] [--subkind=<change-log-subkind>] [--files-changed=<csv>] [--hypothesis="..."] [--commands="cmd1\ncmd2" | --commands-from=<file>] [--smoke-result=<result>] [--smoke-notes="..."] [--parity-status=<status>] [--rollback-plan="..."] [--rolls-back=<entry-id>] [--from-stdin]
60
+ ${renderRecordUsage(commandName)}
59
61
  ${commandName} note <subkind> [--workspace=<ws>] --phase=<phase> --summary="..." [--body=<markdown-file>] [--from-stdin]
60
62
  ${commandName} list [--workspace=<ws>] [--phase=<phase>] [--kind=<kind>] [--since=<iso>] [--limit=<n>] [--json]
61
63
  ${commandName} show [--workspace=<ws>] <entry-id>
@@ -69,7 +71,7 @@ ${renderValidValues()}`;
69
71
  const renderCommandHelp = (commandName, command) => {
70
72
  switch (command) {
71
73
  case 'record':
72
- return `${commandName} record [--workspace=<ws>] --kind=<kind> --phase=<phase> --summary="..." [--rationale="..."] [--action=<operator-action>] [--check-id=<check-id>] [--diff=<patch-file>] [--diff-from-git=<ref>] [--idempotency-key=<key>] [--subkind=<change-log-subkind>] [--files-changed=<csv>] [--hypothesis="..."] [--commands="cmd1\ncmd2" | --commands-from=<file>] [--smoke-result=<result>] [--smoke-notes="..."] [--parity-status=<status>] [--rollback-plan="..."] [--rolls-back=<entry-id>] [--from-stdin]
74
+ return `${renderRecordUsage(commandName)}
73
75
 
74
76
  ${renderValidValues()}`;
75
77
  case 'note':
@@ -159,13 +161,64 @@ const ensureFileExists = async (filePath, flagName) => {
159
161
  throw error;
160
162
  }
161
163
  };
162
- const materializeGitDiff = async (workspaceRoot, gitRef) => {
164
+ const getErrorCode = (error) => typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
165
+ const parsePositiveIntegerFlag = ({ defaultValue, flagName, flags, }) => {
166
+ const raw = getFlag(flags, flagName);
167
+ if (!raw) {
168
+ return defaultValue;
169
+ }
170
+ if (!/^[1-9]\d*$/u.test(raw)) {
171
+ throw new CliUsageError(`Invalid --${flagName} value: ${raw}. Expected a positive integer.`);
172
+ }
173
+ return Number.parseInt(raw, 10);
174
+ };
175
+ const parseWorkspaceRelativePathCsv = ({ flagName, raw }) => {
176
+ const uniquePaths = [
177
+ ...new Set(raw
178
+ .split(',')
179
+ .map((value) => value.trim())
180
+ .filter((value) => value.length > 0)
181
+ .map((filePath) => {
182
+ try {
183
+ return v.parse(WorkspaceRelativePathSchema, filePath);
184
+ }
185
+ catch {
186
+ throw new CliUsageError(`--${flagName} path is not a normalized workspace-relative path: ${filePath}`);
187
+ }
188
+ })),
189
+ ];
190
+ if (uniquePaths.length === 0) {
191
+ throw new CliUsageError(`--${flagName} must include at least one workspace-relative path.`);
192
+ }
193
+ return uniquePaths;
194
+ };
195
+ const isGitDiffMaxBufferError = (error) => {
196
+ const code = getErrorCode(error);
197
+ if (code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
198
+ return true;
199
+ }
200
+ return error instanceof RangeError && error.message.includes('maxBuffer');
201
+ };
202
+ const isGitDiffTimeoutError = (error) => {
203
+ const code = getErrorCode(error);
204
+ if (code === 'ETIMEDOUT') {
205
+ return true;
206
+ }
207
+ return (typeof error === 'object' &&
208
+ error !== null &&
209
+ 'killed' in error &&
210
+ 'signal' in error &&
211
+ error.killed === true &&
212
+ error.signal === 'SIGTERM');
213
+ };
214
+ const materializeGitDiff = async ({ gitOptions, gitRef, workspaceRoot, }) => {
163
215
  let tempDir;
164
216
  try {
165
- const { stdout } = await execFileAsync('git', ['diff', gitRef], {
217
+ const gitArgs = gitOptions.scopedPaths.length === 0 ? ['diff', gitRef] : ['diff', gitRef, '--', ...gitOptions.scopedPaths];
218
+ const { stdout } = await execFileAsync('git', gitArgs, {
166
219
  cwd: workspaceRoot,
167
- maxBuffer: GIT_DIFF_MAX_BUFFER_BYTES,
168
- timeout: GIT_DIFF_TIMEOUT_MS,
220
+ maxBuffer: gitOptions.maxBufferBytes,
221
+ timeout: gitOptions.timeoutMs,
169
222
  });
170
223
  tempDir = await mkdtemp(path.join(os.tmpdir(), 'ushman-ledger-git-diff-'));
171
224
  const patchPath = path.join(tempDir, 'patch.diff');
@@ -176,11 +229,14 @@ const materializeGitDiff = async (workspaceRoot, gitRef) => {
176
229
  if (tempDir) {
177
230
  await rm(tempDir, { force: true, recursive: true });
178
231
  }
179
- if (error.code === 'ENOENT') {
232
+ if (getErrorCode(error) === 'ENOENT') {
180
233
  throw new CliUsageError('git is required for --diff-from-git and was not found in PATH.');
181
234
  }
182
- if (error.code === 'ETIMEDOUT') {
183
- throw new CliUsageError(`git diff ${gitRef} timed out after ${GIT_DIFF_TIMEOUT_MS}ms. Narrow the diff or run git manually.`);
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.`);
184
240
  }
185
241
  throw error;
186
242
  }
@@ -196,16 +252,13 @@ const parseJsonInput = (text, sourceLabel) => {
196
252
  const print = (context, text) => {
197
253
  context.stdout.write(text.endsWith('\n') ? text : `${text}\n`);
198
254
  };
199
- const parseLimit = (flags) => {
200
- const raw = getFlag(flags, 'limit');
201
- if (!raw) {
202
- return undefined;
203
- }
204
- if (!/^[1-9]\d*$/u.test(raw)) {
205
- throw new CliUsageError(`Invalid --limit value: ${raw}`);
206
- }
207
- return Number.parseInt(raw, 10);
208
- };
255
+ const parseLimit = (flags) => getFlag(flags, 'limit')
256
+ ? parsePositiveIntegerFlag({
257
+ defaultValue: 0,
258
+ flagName: 'limit',
259
+ flags,
260
+ })
261
+ : undefined;
209
262
  const parseOptionalKind = (flags) => {
210
263
  const kind = getFlag(flags, 'kind');
211
264
  if (!kind) {
@@ -287,25 +340,30 @@ const parseOptionalChangeLogPicklist = ({ flagName, flags, options, }) => {
287
340
  }
288
341
  return value;
289
342
  };
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 }));
343
+ const parseFilesChangedCsv = (raw) => parseWorkspaceRelativePathCsv({
344
+ flagName: 'files-changed',
345
+ raw,
346
+ }).map((filePath) => ({ path: filePath }));
347
+ const parseGitDiffOptions = (flags) => {
348
+ const gitPaths = getFlag(flags, 'git-paths');
349
+ return {
350
+ maxBufferBytes: parsePositiveIntegerFlag({
351
+ defaultValue: DEFAULT_GIT_DIFF_MAX_BUFFER_BYTES,
352
+ flagName: 'git-diff-max-buffer-bytes',
353
+ flags,
354
+ }),
355
+ scopedPaths: gitPaths
356
+ ? parseWorkspaceRelativePathCsv({
357
+ flagName: 'git-paths',
358
+ raw: gitPaths,
359
+ })
360
+ : [],
361
+ timeoutMs: parsePositiveIntegerFlag({
362
+ defaultValue: DEFAULT_GIT_DIFF_TIMEOUT_MS,
363
+ flagName: 'git-diff-timeout-ms',
364
+ flags,
365
+ }),
366
+ };
309
367
  };
310
368
  const readCommandLines = async (flags) => {
311
369
  const inlineCommands = getFlag(flags, 'commands');
@@ -323,32 +381,65 @@ const readCommandLines = async (flags) => {
323
381
  }
324
382
  return undefined;
325
383
  };
384
+ const validateGitDiffFlagUsage = (flags, diffFromGitProvided) => {
385
+ if (diffFromGitProvided) {
386
+ return;
387
+ }
388
+ const usedGitFlags = getUsedFlags(flags, GIT_DIFF_FLAG_NAMES);
389
+ if (usedGitFlags.length === 0) {
390
+ return;
391
+ }
392
+ throw new CliUsageError(`${usedGitFlags.join(', ')} ${usedGitFlags.length === 1 ? 'is' : 'are'} only supported with --diff-from-git.`);
393
+ };
394
+ const assertDiffRecordKindSupported = (kind) => {
395
+ if (kind === 'agent-patch' || kind === 'operator-patch' || kind === 'change-log') {
396
+ return;
397
+ }
398
+ throw new CliUsageError('--diff and --diff-from-git are only supported for patch and change-log records.');
399
+ };
400
+ const readPatchText = async (diffPath) => readFile(diffPath, 'utf8');
401
+ const readPatchTextForRecordKind = async (kind, diffPath) => {
402
+ if (kind === 'change-log' || kind === 'agent-patch' || kind === 'operator-patch') {
403
+ return readPatchText(diffPath);
404
+ }
405
+ return undefined;
406
+ };
407
+ const deriveAffectedFiles = (patchText) => [
408
+ ...new Set(deriveFilesChangedFromPatch(patchText).map((fileChange) => fileChange.path)),
409
+ ];
326
410
  const resolveDiffInput = async ({ flags, kind, workspaceRoot, }) => {
327
411
  const diffPath = getFlag(flags, 'diff');
328
412
  const diffFromGit = getFlag(flags, 'diff-from-git');
329
- if (diffPath && diffFromGit) {
413
+ const diffFromGitProvided = hasFlag(flags, 'diff-from-git');
414
+ if (diffPath && diffFromGitProvided) {
330
415
  throw new CliUsageError('Use either --diff or --diff-from-git, not both.');
331
416
  }
332
- if (!diffPath && !diffFromGit) {
333
- return {};
417
+ if (diffFromGitProvided && diffFromGit === '') {
418
+ throw new CliUsageError('--diff-from-git must not be empty.');
334
419
  }
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.');
420
+ validateGitDiffFlagUsage(flags, diffFromGitProvided);
421
+ if (!diffPath && !diffFromGitProvided) {
422
+ return {};
337
423
  }
424
+ assertDiffRecordKindSupported(kind);
338
425
  if (diffPath) {
339
426
  const resolvedDiffPath = path.resolve(diffPath);
340
427
  await ensureFileExists(resolvedDiffPath, '--diff');
341
428
  return {
342
429
  diffPath: resolvedDiffPath,
343
- patchText: kind === 'change-log' ? await readFile(resolvedDiffPath, 'utf8') : undefined,
430
+ patchText: await readPatchTextForRecordKind(kind, resolvedDiffPath),
344
431
  };
345
432
  }
346
- const materialized = await materializeGitDiff(workspaceRoot, diffFromGit ?? '');
433
+ const materialized = await materializeGitDiff({
434
+ gitOptions: parseGitDiffOptions(flags),
435
+ gitRef: diffFromGit ?? '',
436
+ workspaceRoot,
437
+ });
347
438
  return {
348
439
  cleanupTempDir: materialized.tempDir,
349
440
  diffPath: materialized.patchPath,
350
441
  gitRef: diffFromGit,
351
- patchText: kind === 'change-log' ? await readFile(materialized.patchPath, 'utf8') : undefined,
442
+ patchText: await readPatchTextForRecordKind(kind, materialized.patchPath),
352
443
  };
353
444
  };
354
445
  const validateRecordStdinFlags = (flags) => {
@@ -361,6 +452,9 @@ const validateRecordStdinFlags = (flags) => {
361
452
  'diff',
362
453
  'diff-from-git',
363
454
  'files-changed',
455
+ 'git-diff-max-buffer-bytes',
456
+ 'git-diff-timeout-ms',
457
+ 'git-paths',
364
458
  'hypothesis',
365
459
  'idempotency-key',
366
460
  'kind',
@@ -446,10 +540,16 @@ const applyPatchInputToRecord = async ({ kind, patchInput, record, }) => {
446
540
  diffPath: patchInput.diffPath,
447
541
  links: patchInput.gitRef
448
542
  ? {
543
+ affectedFiles: patchInput.patchText ? deriveAffectedFiles(patchInput.patchText) : undefined,
449
544
  ...record.links,
450
545
  gitRef: patchInput.gitRef,
451
546
  }
452
- : record.links,
547
+ : patchInput.patchText
548
+ ? {
549
+ affectedFiles: deriveAffectedFiles(patchInput.patchText),
550
+ ...record.links,
551
+ }
552
+ : record.links,
453
553
  },
454
554
  };
455
555
  };
@@ -640,16 +740,19 @@ const runTailCli = async (parsed, context) => runListCli({
640
740
  const runRenderCli = async (parsed, context) => {
641
741
  const target = parseRenderTarget(parsed.flags);
642
742
  const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
643
- const content = await ledger.render({
743
+ const renderOptions = {
644
744
  limit: parseLimit(parsed.flags),
645
745
  out: getFlag(parsed.flags, 'out'),
646
746
  phase: parseOptionalPhase(parsed.flags),
647
747
  since: parseSince(parsed.flags),
648
748
  to: target,
649
- });
650
- if (!getFlag(parsed.flags, 'out')) {
651
- print(context, content);
749
+ };
750
+ if (renderOptions.out) {
751
+ await ledger.renderTo(renderOptions);
752
+ return 0;
652
753
  }
754
+ const content = await ledger.render(renderOptions);
755
+ print(context, content);
653
756
  return 0;
654
757
  };
655
758
  const runArchiveCli = async (parsed, context) => {
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,EAAmB,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AA8V1E,eAAO,MAAM,eAAe,GACxB,eAAe,MAAM,EACrB,UAAS;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAA;CAAO;;;EAyCzF,CAAC"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,EAAmB,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;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"}
package/dist/doctor.js CHANGED
@@ -200,8 +200,8 @@ const collectDoctorState = async (workspaceRoot, manifest, readIndex) => {
200
200
  inspectDoctorEntry({
201
201
  blobChecks,
202
202
  checkpointEntries,
203
- entry: resolvedEntry.entry,
204
203
  entriesByIdempotencyKey,
204
+ entry: resolvedEntry.entry,
205
205
  issues,
206
206
  latestByPhase,
207
207
  manifest,
package/dist/handle.d.ts CHANGED
@@ -4,6 +4,30 @@ import { type LedgerFilter } from './list.ts';
4
4
  import { appendNote, type NoteBody } from './note.ts';
5
5
  import type { LedgerEntry, LedgerPhase } from './schema/entry.ts';
6
6
  export type RenderTarget = 'dependency-graph' | 'jsonl' | 'migration-log-md' | 'retro' | 'timeline-html' | 'workspace-narrative-md';
7
+ /**
8
+ * Chunk writer used by `renderTo()` for bounded render emission.
9
+ *
10
+ * Writers may complete synchronously or asynchronously. Throwing rejects the render and causes any in-progress
11
+ * atomic output file to be aborted.
12
+ */
13
+ export type RenderWriter = (chunk: string) => Promise<void> | void;
14
+ /** Options for `render()`, which returns the rendered text and optionally mirrors it to disk. */
15
+ export type LedgerRenderOptions = {
16
+ readonly limit?: number;
17
+ readonly out?: string;
18
+ readonly phase?: LedgerPhase;
19
+ readonly since?: string;
20
+ readonly to: RenderTarget;
21
+ };
22
+ /** Options for `renderTo()`, which writes to a callback and/or canonical render file without buffering the full output. */
23
+ export type LedgerRenderToOptions = {
24
+ readonly limit?: number;
25
+ readonly out?: string;
26
+ readonly phase?: LedgerPhase;
27
+ readonly since?: string;
28
+ readonly to: RenderTarget;
29
+ readonly write?: RenderWriter;
30
+ };
7
31
  export type LedgerHandle = {
8
32
  readonly archive: (outPath: string) => Promise<{
9
33
  integrityHash: string;
@@ -17,14 +41,10 @@ export type LedgerHandle = {
17
41
  readonly record: (entry: unknown) => Promise<{
18
42
  id: string;
19
43
  }>;
20
- readonly render: (options: {
21
- out?: string;
22
- limit?: number;
23
- phase?: LedgerPhase;
24
- since?: string;
25
- to: RenderTarget;
26
- }) => Promise<string>;
44
+ readonly render: (options: LedgerRenderOptions) => Promise<string>;
45
+ readonly renderTo: (options: LedgerRenderToOptions) => Promise<void>;
27
46
  readonly show: (entryId: string) => Promise<LedgerEntry | null>;
28
47
  };
48
+ /** Open a workspace ledger handle after reconciling any pending crash-recovery state. */
29
49
  export declare const openLedger: (workspaceRoot: string) => Promise<LedgerHandle>;
30
50
  //# sourceMappingURL=handle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../src/handle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAA8B,KAAK,YAAY,EAAe,MAAM,WAAW,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAStD,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGlE,MAAM,MAAM,YAAY,GAClB,kBAAkB,GAClB,OAAO,GACP,kBAAkB,GAClB,OAAO,GACP,eAAe,GACf,wBAAwB,CAAC;AAkD/B,MAAM,MAAM,YAAY,GAAG;IACvB,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,QAAQ,CAAC,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC;IACrF,QAAQ,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC;IAC5E,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,YAAY,KAAK,aAAa,CAAC,WAAW,CAAC,CAAC;IACrE,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtG,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE;QACvB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,WAAW,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,EAAE,EAAE,YAAY,CAAC;KACpB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CACnE,CAAC;AAEF,eAAO,MAAM,UAAU,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,YAAY,CAyC5E,CAAC"}
1
+ {"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../src/handle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAA8B,KAAK,YAAY,EAAe,MAAM,WAAW,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAStD,OAAO,KAAK,EAAE,WAAW,EAAc,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG9E,MAAM,MAAM,YAAY,GAClB,kBAAkB,GAClB,OAAO,GACP,kBAAkB,GAClB,OAAO,GACP,eAAe,GACf,wBAAwB,CAAC;AAE/B;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEnE,iGAAiG;AACjG,MAAM,MAAM,mBAAmB,GAAG;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC;CAC7B,CAAC;AAEF,2HAA2H;AAC3H,MAAM,MAAM,qBAAqB,GAAG;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;CACjC,CAAC;AA4GF,MAAM,MAAM,YAAY,GAAG;IACvB,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,QAAQ,CAAC,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC;IACrF,QAAQ,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC;IAC5E,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,YAAY,KAAK,aAAa,CAAC,WAAW,CAAC,CAAC;IACrE,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtG,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CACnE,CAAC;AAEF,yFAAyF;AACzF,eAAO,MAAM,UAAU,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,YAAY,CAyF5E,CAAC"}