ushman-ledger 1.2.2 → 1.3.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 (61) hide show
  1. package/AGENTS.md +7 -5
  2. package/ARCHITECTURE.md +8 -2
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +37 -5
  5. package/TROUBLESHOOTING.md +17 -3
  6. package/dist/blobs.d.ts.map +1 -1
  7. package/dist/blobs.js +1 -1
  8. package/dist/builders.d.ts +33 -0
  9. package/dist/builders.d.ts.map +1 -1
  10. package/dist/builders.js +10 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +153 -43
  13. package/dist/doctor.d.ts +1 -1
  14. package/dist/doctor.d.ts.map +1 -1
  15. package/dist/doctor.js +45 -11
  16. package/dist/handle.d.ts.map +1 -1
  17. package/dist/handle.js +67 -30
  18. package/dist/helpers.d.ts.map +1 -1
  19. package/dist/helpers.js +2 -1
  20. package/dist/index.d.ts +3 -3
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +3 -3
  23. package/dist/lab-min.d.ts +1 -1
  24. package/dist/lab-min.d.ts.map +1 -1
  25. package/dist/lab-min.js +2 -1
  26. package/dist/list.d.ts +32 -0
  27. package/dist/list.d.ts.map +1 -1
  28. package/dist/list.js +1 -1
  29. package/dist/patch-resolver.d.ts.map +1 -1
  30. package/dist/patch-resolver.js +1 -1
  31. package/dist/process.d.ts +2 -0
  32. package/dist/process.d.ts.map +1 -0
  33. package/dist/process.js +16 -0
  34. package/dist/read-index.d.ts +7 -7
  35. package/dist/read-index.d.ts.map +1 -1
  36. package/dist/read-index.js +13 -9
  37. package/dist/record.d.ts.map +1 -1
  38. package/dist/record.js +1 -2
  39. package/dist/recovery.d.ts +8 -0
  40. package/dist/recovery.d.ts.map +1 -1
  41. package/dist/recovery.js +142 -30
  42. package/dist/render/retro.d.ts.map +1 -1
  43. package/dist/render/retro.js +4 -1
  44. package/dist/runtime-config.d.ts +2 -0
  45. package/dist/runtime-config.d.ts.map +1 -1
  46. package/dist/runtime-config.js +14 -0
  47. package/dist/schema/entry-core.d.ts +5 -2
  48. package/dist/schema/entry-core.d.ts.map +1 -1
  49. package/dist/schema/entry-core.js +3 -0
  50. package/dist/schema/entry-read.d.ts +57 -0
  51. package/dist/schema/entry-read.d.ts.map +1 -1
  52. package/dist/schema/entry-read.js +9 -1
  53. package/dist/schema/entry-write.d.ts +51 -0
  54. package/dist/schema/entry-write.d.ts.map +1 -1
  55. package/dist/schema/entry-write.js +9 -1
  56. package/dist/storage/filesystem.d.ts +14 -2
  57. package/dist/storage/filesystem.d.ts.map +1 -1
  58. package/dist/storage/filesystem.js +206 -39
  59. package/dist/storage/lock.d.ts.map +1 -1
  60. package/dist/storage/lock.js +38 -16
  61. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -9,13 +9,13 @@ import * as v from 'valibot';
9
9
  import { readPatchTextFromFile } from "./blobs.js";
10
10
  import { openLedger } from "./handle.js";
11
11
  import { deriveFilesChangedFromPatch } from "./patch-resolver.js";
12
- import { ChangeLogParityStatusSchema, ChangeLogSmokeResultSchema, ChangeLogSubkindSchema, LEDGER_KINDS, LEDGER_PHASES, parseLedgerRecord, WorkspaceRelativePathSchema, } from "./schema/entry.js";
12
+ import { ChangeLogParityStatusSchema, ChangeLogSmokeResultSchema, ChangeLogSubkindSchema, LEDGER_KINDS, LEDGER_PHASES, STAGE_WRITE_STAGES, parseLedgerRecord, WorkspaceRelativePathSchema, } from "./schema/entry.js";
13
13
  import { NoteSubkindSchema } from "./schema/note.js";
14
14
  import { LEDGER_LIBRARY_VERSION } from "./version.js";
15
15
  const execFileAsync = promisify(execFile);
16
16
  const DEFAULT_GIT_DIFF_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
17
17
  const DEFAULT_GIT_DIFF_TIMEOUT_MS = 30_000;
18
- const BOOLEAN_FLAG_NAMES = new Set(['from-stdin', 'help', 'json']);
18
+ const BOOLEAN_FLAG_NAMES = new Set(['from-stdin', 'help', 'json', 'progress', 'quiet']);
19
19
  const GIT_DIFF_FORMAT_ARGS = ['--no-color', '--no-ext-diff', '--src-prefix=a/', '--dst-prefix=b/'];
20
20
  const RENDER_TARGETS = [
21
21
  'retro',
@@ -25,6 +25,12 @@ const RENDER_TARGETS = [
25
25
  'migration-log-md',
26
26
  'workspace-narrative-md',
27
27
  ];
28
+ const CANONICAL_RENDER_TARGETS = new Set([
29
+ 'migration-log-md',
30
+ 'retro',
31
+ 'timeline-html',
32
+ 'workspace-narrative-md',
33
+ ]);
28
34
  const GIT_DIFF_FLAG_NAMES = ['git-diff-max-buffer-bytes', 'git-diff-timeout-ms', 'git-paths'];
29
35
  const CHANGE_LOG_RECORD_ONLY_FLAGS = [
30
36
  'commands',
@@ -38,7 +44,8 @@ const CHANGE_LOG_RECORD_ONLY_FLAGS = [
38
44
  'smoke-result',
39
45
  'subkind',
40
46
  ];
41
- 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]`;
47
+ const STAGE_WRITE_ONLY_FLAGS = ['file-path', 'stage'];
48
+ const renderRecordUsage = (commandName) => `${commandName} record [--workspace=<ws>] --kind=<kind> --phase=<phase> --summary="..." [--rationale="..."] [--file-path=<path>] [--stage=<stage>] [--action=<operator-action>] [--check-id=<check-id>] [--diff=<patch-file>] [--diff-from-git=<ref>] [--git-paths=<csv>] [--git-diff-timeout-ms=<ms>] [--git-diff-max-buffer-bytes=<bytes>] [--idempotency-key=<key>] [--subkind=<change-log-subkind>] [--files-changed=<csv>] [--hypothesis="..."] [--commands="cmd1\ncmd2" | --commands-from=<file>] [--smoke-result=<result>] [--smoke-notes="..."] [--parity-status=<status>] [--rollback-plan="..."] [--rolls-back=<entry-id>] [--from-stdin]`;
42
49
  class CliUsageError extends Error {
43
50
  }
44
51
  const DEFAULT_CONTEXT = {
@@ -54,6 +61,7 @@ const DEFAULT_CONTEXT = {
54
61
  const renderValidValues = () => `Valid values:
55
62
  kinds: ${LEDGER_KINDS.join(', ')}
56
63
  phases: ${LEDGER_PHASES.join(', ')}
64
+ stage-write stages: ${STAGE_WRITE_STAGES.join(', ')}
57
65
  note subkinds: ${NoteSubkindSchema.options.join(', ')}
58
66
  render targets: ${RENDER_TARGETS.join(', ')}
59
67
  `;
@@ -64,6 +72,8 @@ const renderRuntimeTuningHelp = () => `Runtime tuning env vars:
64
72
  USHMAN_LEDGER_READ_INDEX_REBUILD_CONCURRENCY (default: USHMAN_LEDGER_SCAN_CONCURRENCY)
65
73
  USHMAN_LEDGER_COVERAGE_FILE_STAT_CONCURRENCY (default: USHMAN_LEDGER_SCAN_CONCURRENCY)
66
74
  USHMAN_LEDGER_BLOB_HASH_CONCURRENCY (default: USHMAN_LEDGER_SCAN_CONCURRENCY)
75
+ USHMAN_LEDGER_DOCTOR_CHECKPOINT_MAX_AGE_MS (default: 86400000)
76
+ USHMAN_LEDGER_DOCTOR_OPEN_ISSUE_MAX_AGE_MS (default: 2592000000)
67
77
  USHMAN_LEDGER_MAX_PATCH_BYTES (default: 10485760)
68
78
  `;
69
79
  const renderHelp = (commandName) => `${commandName}
@@ -71,12 +81,12 @@ const renderHelp = (commandName) => `${commandName}
71
81
  Commands:
72
82
  ${renderRecordUsage(commandName)}
73
83
  ${commandName} note <subkind> [--workspace=<ws>] --phase=<phase> --summary="..." [--body=<markdown-file>] [--from-stdin]
74
- ${commandName} list [--workspace=<ws>] [--phase=<phase>] [--kind=<kind>] [--since=<iso>] [--limit=<n>] [--json]
84
+ ${commandName} list [--workspace=<ws>] [--phase=<phase>] [--kind=<kind>] [--since=<iso>] [--limit=<n>] [--json] [--quiet] [--progress]
75
85
  ${commandName} show [--workspace=<ws>] <entry-id>
76
86
  ${commandName} tail [--workspace=<ws>] [--phase=<phase>] [--limit=<n>]
77
- ${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>]
78
- ${commandName} archive [--workspace=<ws>] --out=<file.tgz>
79
- ${commandName} doctor [--workspace=<ws>] [--json]
87
+ ${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>] [--quiet] [--progress]
88
+ ${commandName} archive [--workspace=<ws>] --out=<file.tgz> [--quiet] [--progress]
89
+ ${commandName} doctor [--workspace=<ws>] [--json] [--quiet] [--progress]
80
90
  ${commandName} --version
81
91
 
82
92
  ${renderValidValues()}
@@ -94,17 +104,17 @@ ${renderRuntimeTuningHelp()}`;
94
104
  ${renderValidValues()}
95
105
  ${renderRuntimeTuningHelp()}`;
96
106
  case 'list':
97
- return `${commandName} list [--workspace=<ws>] [--phase=<phase>] [--kind=<kind>] [--since=<iso>] [--limit=<n>] [--json]
107
+ return `${commandName} list [--workspace=<ws>] [--phase=<phase>] [--kind=<kind>] [--since=<iso>] [--limit=<n>] [--json] [--quiet] [--progress]
98
108
 
99
109
  ${renderValidValues()}
100
110
  ${renderRuntimeTuningHelp()}`;
101
111
  case 'render':
102
- return `${commandName} render [--workspace=<ws>] [--to=<target>] [--phase=<phase>] [--since=<iso>] [--limit=<n>] [--out=<file>]
112
+ return `${commandName} render [--workspace=<ws>] [--to=<target>] [--phase=<phase>] [--since=<iso>] [--limit=<n>] [--out=<file>] [--quiet] [--progress]
103
113
 
104
114
  ${renderValidValues()}
105
115
  ${renderRuntimeTuningHelp()}`;
106
116
  case 'doctor':
107
- return `${commandName} doctor [--workspace=<ws>] [--json]
117
+ return `${commandName} doctor [--workspace=<ws>] [--json] [--quiet] [--progress]
108
118
 
109
119
  ${renderRuntimeTuningHelp()}`;
110
120
  default:
@@ -213,6 +223,18 @@ const parseWorkspaceRelativePathCsv = ({ flagName, raw }) => {
213
223
  }
214
224
  return uniquePaths;
215
225
  };
226
+ const parseWorkspaceRelativePathFlag = ({ flagName, flags, required = false, }) => {
227
+ const raw = required ? getRequiredString(flags, flagName) : getFlag(flags, flagName);
228
+ if (!raw) {
229
+ return undefined;
230
+ }
231
+ try {
232
+ return v.parse(WorkspaceRelativePathSchema, raw);
233
+ }
234
+ catch {
235
+ throw new CliUsageError(`--${flagName} path is not a normalized workspace-relative path: ${raw}`);
236
+ }
237
+ };
216
238
  const isGitDiffMaxBufferError = (error) => {
217
239
  const code = getErrorCode(error);
218
240
  if (code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
@@ -291,6 +313,14 @@ const print = (context, text) => {
291
313
  const printJson = (context, value) => {
292
314
  print(context, JSON.stringify(value, null, 2));
293
315
  };
316
+ const isQuiet = (flags) => hasFlag(flags, 'quiet');
317
+ const isProgressEnabled = (flags) => hasFlag(flags, 'progress') && !isQuiet(flags);
318
+ const printProgress = (context, flags, message) => {
319
+ if (!isProgressEnabled(flags)) {
320
+ return;
321
+ }
322
+ context.stderr.write(`progress ${message}\n`);
323
+ };
294
324
  const parseLimit = (flags) => getFlag(flags, 'limit')
295
325
  ? parsePositiveIntegerFlag({
296
326
  defaultValue: 0,
@@ -350,6 +380,13 @@ const parseRenderTarget = (flags) => {
350
380
  }
351
381
  return target;
352
382
  };
383
+ const parseRequiredStageWriteStage = (flags) => {
384
+ const stage = getRequiredString(flags, 'stage');
385
+ if (!STAGE_WRITE_STAGES.includes(stage)) {
386
+ throw new CliUsageError(`Invalid --stage value: ${stage}. Expected one of: ${STAGE_WRITE_STAGES.join(', ')}.`);
387
+ }
388
+ return stage;
389
+ };
353
390
  const splitCommandLines = (value) => value
354
391
  .split(/\r?\n/u)
355
392
  .map((command) => command.trim())
@@ -500,11 +537,13 @@ const validateRecordStdinFlags = (flags) => {
500
537
  'operator',
501
538
  'parity-status',
502
539
  'phase',
540
+ 'file-path',
503
541
  'rationale',
504
542
  'rollback-plan',
505
543
  'rolls-back',
506
544
  'smoke-notes',
507
545
  'smoke-result',
546
+ 'stage',
508
547
  'subkind',
509
548
  'summary',
510
549
  ].filter((flagName) => hasFlag(flags, flagName));
@@ -515,53 +554,87 @@ const validateRecordStdinFlags = (flags) => {
515
554
  .map((flagName) => `--${flagName}`)
516
555
  .join(', ')}. When using --from-stdin, provide all record fields in the JSON input.`);
517
556
  };
518
- const buildBaseRecordFromFlags = (parsed, context) => {
519
- const kind = getRequiredKind(parsed.flags);
520
- if (kind !== 'change-log') {
521
- rejectUnsupportedFlags('change-log', parsed.flags, CHANGE_LOG_RECORD_ONLY_FLAGS);
557
+ const applyRationaleFlag = ({ flags, kind, record, }) => {
558
+ const rationale = getFlag(flags, 'rationale');
559
+ if (!rationale) {
560
+ return rationale;
522
561
  }
523
- const record = {
524
- emitter: {
525
- tool: context.defaultEmitter.tool,
526
- version: context.defaultEmitter.version,
527
- },
528
- kind,
529
- phase: getRequiredPhase(parsed.flags),
530
- summary: getRequiredString(parsed.flags, 'summary'),
531
- };
532
- const rationale = getFlag(parsed.flags, 'rationale');
533
- if (rationale) {
534
- if (kind !== 'agent-patch' &&
535
- kind !== 'operator-patch' &&
536
- kind !== 'operator-decision' &&
537
- kind !== 'correction') {
538
- throw new CliUsageError('--rationale is only supported for patch, operator-decision, and correction records.');
539
- }
540
- if (kind !== 'operator-decision') {
541
- record.rationale = rationale;
542
- }
562
+ if (kind !== 'agent-patch' &&
563
+ kind !== 'operator-patch' &&
564
+ kind !== 'operator-decision' &&
565
+ kind !== 'correction' &&
566
+ kind !== 'stage-write') {
567
+ throw new CliUsageError('--rationale is only supported for patch, stage-write, operator-decision, and correction records.');
543
568
  }
569
+ if (kind !== 'operator-decision') {
570
+ record.rationale = rationale;
571
+ }
572
+ return rationale;
573
+ };
574
+ const applyKindSpecificRecordFields = ({ defaultTool, flags, kind, rationale, record, }) => {
544
575
  if (kind === 'agent-patch') {
545
576
  record.agent = {
546
- name: getFlag(parsed.flags, 'agent') ?? context.defaultEmitter.tool,
577
+ name: getFlag(flags, 'agent') ?? defaultTool,
547
578
  };
579
+ return;
548
580
  }
549
581
  if (kind === 'operator-patch') {
550
582
  record.operator = {
551
- name: getFlag(parsed.flags, 'operator') ?? context.defaultEmitter.tool,
583
+ name: getFlag(flags, 'operator') ?? defaultTool,
552
584
  };
585
+ return;
553
586
  }
554
587
  if (kind === 'operator-decision') {
555
- const action = getRequiredString(parsed.flags, 'action');
556
588
  record.payload = {
557
- action,
558
- checkId: getFlag(parsed.flags, 'check-id'),
589
+ action: getRequiredString(flags, 'action'),
590
+ checkId: getFlag(flags, 'check-id'),
559
591
  rationale: rationale ?? '',
560
592
  };
593
+ return;
561
594
  }
562
595
  if (kind === 'change-log') {
563
- record.subkind = parseRequiredChangeLogSubkind(parsed.flags);
596
+ record.subkind = parseRequiredChangeLogSubkind(flags);
597
+ return;
598
+ }
599
+ if (kind === 'stage-write') {
600
+ record.filePath = parseWorkspaceRelativePathFlag({
601
+ flagName: 'file-path',
602
+ flags,
603
+ required: true,
604
+ });
605
+ record.rationale = getRequiredString(flags, 'rationale');
606
+ record.stage = parseRequiredStageWriteStage(flags);
564
607
  }
608
+ };
609
+ const buildBaseRecordFromFlags = (parsed, context) => {
610
+ const kind = getRequiredKind(parsed.flags);
611
+ if (kind !== 'change-log') {
612
+ rejectUnsupportedFlags('change-log', parsed.flags, CHANGE_LOG_RECORD_ONLY_FLAGS);
613
+ }
614
+ if (kind !== 'stage-write') {
615
+ rejectUnsupportedFlags('stage-write', parsed.flags, STAGE_WRITE_ONLY_FLAGS);
616
+ }
617
+ const record = {
618
+ emitter: {
619
+ tool: context.defaultEmitter.tool,
620
+ version: context.defaultEmitter.version,
621
+ },
622
+ kind,
623
+ phase: getRequiredPhase(parsed.flags),
624
+ summary: getRequiredString(parsed.flags, 'summary'),
625
+ };
626
+ const rationale = applyRationaleFlag({
627
+ flags: parsed.flags,
628
+ kind,
629
+ record,
630
+ });
631
+ applyKindSpecificRecordFields({
632
+ defaultTool: context.defaultEmitter.tool,
633
+ flags: parsed.flags,
634
+ kind,
635
+ rationale,
636
+ record,
637
+ });
565
638
  const idempotencyKey = getFlag(parsed.flags, 'idempotency-key');
566
639
  if (idempotencyKey) {
567
640
  record.idempotencyKey = idempotencyKey;
@@ -732,7 +805,9 @@ const runNoteCli = async (parsed, context) => {
732
805
  return 0;
733
806
  };
734
807
  const runListCli = async (parsed, context) => {
808
+ printProgress(context, parsed.flags, 'list start');
735
809
  const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
810
+ let entryCount = 0;
736
811
  if (hasFlag(parsed.flags, 'json')) {
737
812
  let wroteEntry = false;
738
813
  context.stdout.write('[\n');
@@ -748,8 +823,10 @@ const runListCli = async (parsed, context) => {
748
823
  const renderedEntry = JSON.stringify(entry, null, 2).replaceAll('\n', '\n ');
749
824
  context.stdout.write(` ${renderedEntry}`);
750
825
  wroteEntry = true;
826
+ entryCount += 1;
751
827
  }
752
828
  context.stdout.write(wroteEntry ? '\n]\n' : ']\n');
829
+ printProgress(context, parsed.flags, `list done entries=${entryCount}`);
753
830
  return 0;
754
831
  }
755
832
  for await (const entry of ledger.list({
@@ -758,8 +835,15 @@ const runListCli = async (parsed, context) => {
758
835
  phase: parseOptionalPhase(parsed.flags),
759
836
  since: parseSince(parsed.flags),
760
837
  })) {
761
- print(context, `${entry.id} ${entry.phase} ${entry.kind} ${entry.summary}`);
838
+ entryCount += 1;
839
+ if (isQuiet(parsed.flags)) {
840
+ print(context, entry.id);
841
+ }
842
+ else {
843
+ print(context, `${entry.id} ${entry.phase} ${entry.kind} ${entry.summary}`);
844
+ }
762
845
  }
846
+ printProgress(context, parsed.flags, `list done entries=${entryCount}`);
763
847
  return 0;
764
848
  };
765
849
  const runShowCli = async (parsed, context) => {
@@ -784,6 +868,7 @@ const runTailCli = async (parsed, context) => runListCli({
784
868
  }, context);
785
869
  const runRenderCli = async (parsed, context) => {
786
870
  const target = parseRenderTarget(parsed.flags);
871
+ printProgress(context, parsed.flags, `render start target=${target}`);
787
872
  const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
788
873
  const renderOptions = {
789
874
  limit: parseLimit(parsed.flags),
@@ -792,19 +877,29 @@ const runRenderCli = async (parsed, context) => {
792
877
  since: parseSince(parsed.flags),
793
878
  to: target,
794
879
  };
795
- if (renderOptions.out) {
880
+ if (isQuiet(parsed.flags) && !renderOptions.out && !CANONICAL_RENDER_TARGETS.has(target)) {
881
+ throw new CliUsageError(`render --quiet requires --out for ${target} output.`);
882
+ }
883
+ if (renderOptions.out || isQuiet(parsed.flags)) {
796
884
  await ledger.renderTo(renderOptions);
885
+ if (isQuiet(parsed.flags)) {
886
+ print(context, 'ok');
887
+ }
888
+ printProgress(context, parsed.flags, `render done target=${target}`);
797
889
  return 0;
798
890
  }
799
891
  const content = await ledger.render(renderOptions);
800
892
  print(context, content);
893
+ printProgress(context, parsed.flags, `render done target=${target}`);
801
894
  return 0;
802
895
  };
803
896
  const runArchiveCli = async (parsed, context) => {
804
897
  const outPath = getRequiredString(parsed.flags, 'out');
898
+ printProgress(context, parsed.flags, 'archive start');
805
899
  const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
806
900
  const result = await ledger.archive(outPath);
807
- print(context, result.integrityHash);
901
+ print(context, isQuiet(parsed.flags) ? 'ok' : result.integrityHash);
902
+ printProgress(context, parsed.flags, 'archive done');
808
903
  return 0;
809
904
  };
810
905
  const formatDoctorFinding = (finding) => [`[${finding.code}] ${finding.message}`, `Next step: ${finding.remediation}`].join('\n');
@@ -819,17 +914,32 @@ const printDoctorFindings = (context, report) => {
819
914
  context.stderr.write('\n');
820
915
  };
821
916
  const runDoctorCli = async (parsed, context) => {
917
+ printProgress(context, parsed.flags, 'doctor start');
822
918
  const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
823
919
  const result = await ledger.doctor();
824
920
  if (hasFlag(parsed.flags, 'json')) {
825
921
  printJson(context, result);
922
+ printProgress(context, parsed.flags, `doctor done issues=${result.issueCount}`);
923
+ return result.ok ? 0 : 1;
924
+ }
925
+ if (isQuiet(parsed.flags)) {
926
+ if (result.ok) {
927
+ print(context, 'ok');
928
+ }
929
+ else {
930
+ for (const finding of result.findings) {
931
+ print(context, finding.code);
932
+ }
933
+ }
826
934
  return result.ok ? 0 : 1;
827
935
  }
828
936
  if (result.ok) {
829
937
  print(context, 'ok');
938
+ printProgress(context, parsed.flags, 'doctor done issues=0');
830
939
  return 0;
831
940
  }
832
941
  printDoctorFindings(context, result);
942
+ printProgress(context, parsed.flags, `doctor done issues=${result.issueCount}`);
833
943
  return 1;
834
944
  };
835
945
  const formatCliError = (error) => {
package/dist/doctor.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type PreparedLedgerState } from './recovery.ts';
2
- export declare const DOCTOR_FINDING_CODES: readonly ["blob-corrupt", "blob-missing", "blob-unreadable", "change-log-rollback-missing-target", "change-log-smoke-failure-missing-rollback-plan", "manifest-entry-count-mismatch", "manifest-entry-location-missing", "manifest-entry-missing-on-disk", "manifest-last-sequence-mismatch", "manifest-per-phase-latest-mismatch", "manifest-phase-mismatch", "manifest-sequence-mismatch", "open-issue-stale", "phase-prev-entry-mismatch", "pre-change-checkpoint-stale", "read-failure"];
2
+ export declare const DOCTOR_FINDING_CODES: readonly ["blob-corrupt", "blob-missing", "blob-unreadable", "change-log-rollback-missing-target", "change-log-smoke-failure-missing-rollback-plan", "manifest-entry-count-mismatch", "manifest-entry-location-missing", "manifest-entry-missing-on-disk", "manifest-last-sequence-mismatch", "manifest-per-phase-latest-mismatch", "manifest-phase-mismatch", "manifest-sequence-mismatch", "open-issue-stale", "pending-commit-quarantined", "phase-prev-entry-mismatch", "pre-change-checkpoint-stale", "read-failure"];
3
3
  export type DoctorFindingCode = (typeof DOCTOR_FINDING_CODES)[number];
4
4
  export type DoctorFindingMetadataValue = boolean | null | number | string;
5
5
  export type DoctorFinding = {
@@ -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;AAS1E,eAAO,MAAM,oBAAoB,8dAiBvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AACtE,MAAM,MAAM,0BAA0B,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1E,MAAM,MAAM,aAAa,GAAG;IACxB,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,0BAA0B,CAAC,CAAC;IAC/D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,aAAa,EAAE,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACxB,CAAC;AAqjBF,eAAO,MAAM,eAAe,GACxB,eAAe,MAAM,EACrB,UAAS;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAA;CAAO,KACvF,OAAO,CAAC,YAAY,CAqCtB,CAAC"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,EAAiD,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAUxG,eAAO,MAAM,oBAAoB,4fAkBvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AACtE,MAAM,MAAM,0BAA0B,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1E,MAAM,MAAM,aAAa,GAAG;IACxB,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,0BAA0B,CAAC,CAAC;IAC/D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,aAAa,EAAE,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACxB,CAAC;AAwlBF,eAAO,MAAM,eAAe,GACxB,eAAe,MAAM,EACrB,UAAS;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAA;CAAO,KACvF,OAAO,CAAC,YAAY,CAsCtB,CAAC"}
package/dist/doctor.js CHANGED
@@ -4,11 +4,12 @@ import { resolveBlobPath } from "./blobs.js";
4
4
  import { sha256File } from "./json.js";
5
5
  import { getOrderedEntryLocations, readManifestEntryBatch } from "./list.js";
6
6
  import { isReadIndexCurrent, readReadIndex } from "./read-index.js";
7
- import { loadLedgerState } from "./recovery.js";
7
+ import { listPendingCommitQuarantines, loadLedgerState } from "./recovery.js";
8
8
  import { getLedgerRuntimeConfig } from "./runtime-config.js";
9
9
  import { readManifest } from "./storage/filesystem.js";
10
- const CHECKPOINT_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
11
- const OPEN_ISSUE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1_000;
10
+ const HOUR_MS = 60 * 60 * 1_000;
11
+ const DAY_MS = 24 * HOUR_MS;
12
+ const MINUTE_MS = 60 * 1_000;
12
13
  export const DOCTOR_FINDING_CODES = [
13
14
  'blob-corrupt',
14
15
  'blob-missing',
@@ -23,11 +24,12 @@ export const DOCTOR_FINDING_CODES = [
23
24
  'manifest-phase-mismatch',
24
25
  'manifest-sequence-mismatch',
25
26
  'open-issue-stale',
27
+ 'pending-commit-quarantined',
26
28
  'phase-prev-entry-mismatch',
27
29
  'pre-change-checkpoint-stale',
28
30
  'read-failure',
29
31
  ];
30
- const createFinding = ({ code, message, metadata, remediation, }) => ({
32
+ const createFinding = ({ code, message, metadata, remediation }) => ({
31
33
  code,
32
34
  message,
33
35
  metadata,
@@ -40,6 +42,19 @@ const buildDoctorReport = (findings) => ({
40
42
  issues: findings.map((finding) => finding.message),
41
43
  ok: findings.length === 0,
42
44
  });
45
+ const formatAgeThreshold = (ageMs) => {
46
+ if (ageMs >= DAY_MS && ageMs % DAY_MS === 0) {
47
+ const days = ageMs / DAY_MS;
48
+ return `${days} day${days === 1 ? '' : 's'}`;
49
+ }
50
+ if (ageMs >= HOUR_MS && ageMs % HOUR_MS === 0) {
51
+ return `${ageMs / HOUR_MS}h`;
52
+ }
53
+ if (ageMs >= MINUTE_MS && ageMs % MINUTE_MS === 0) {
54
+ return `${ageMs / MINUTE_MS}m`;
55
+ }
56
+ return `${ageMs}ms`;
57
+ };
43
58
  const pushFinding = (findings, finding) => {
44
59
  findings.push(finding);
45
60
  };
@@ -50,7 +65,7 @@ const isMissingPathError = (error) => {
50
65
  const buildReadFailure = (error) => buildDoctorReport([
51
66
  createFinding({
52
67
  code: 'read-failure',
53
- message: `Failed to read ledger state: ${error instanceof Error ? error.message ?? error.name : String(error)}.`,
68
+ message: `Failed to read ledger state: ${error instanceof Error ? (error.message ?? error.name) : String(error)}.`,
54
69
  remediation: 'Re-open the ledger or rerun the command first so recovery can reconcile pending state. If the error persists, repair or restore the invalid manifest/read-index JSON before archiving.',
55
70
  }),
56
71
  ]);
@@ -130,6 +145,20 @@ const checkBlobPresence = async (workspaceRoot, blobChecks) => {
130
145
  });
131
146
  return results.flat();
132
147
  };
148
+ const checkPendingCommitQuarantines = async (workspaceRoot) => {
149
+ const quarantines = await listPendingCommitQuarantines(workspaceRoot);
150
+ return quarantines.map((quarantine) => createFinding({
151
+ code: 'pending-commit-quarantined',
152
+ message: `Pending commit journal ${quarantine.originalFileName} is quarantined: ${quarantine.reason}`,
153
+ metadata: {
154
+ commitPath: quarantine.commitPath,
155
+ metadataPath: quarantine.metadataPath,
156
+ originalFileName: quarantine.originalFileName,
157
+ quarantinedAt: quarantine.quarantinedAt,
158
+ },
159
+ remediation: 'Inspect the quarantined journal under .lab/ledger/pending-quarantine/. Restore it to .lab/ledger/pending/ only if its entry, sequence, and manifest base are known to be safe; otherwise keep or remove it after recording an operator decision.',
160
+ }));
161
+ };
133
162
  const checkManifestCounts = (findings, manifest, entryCount) => {
134
163
  if (manifest.entryCount !== entryCount) {
135
164
  pushFinding(findings, createFinding({
@@ -218,10 +247,11 @@ const trackIdempotencyEntry = (entry, entriesByIdempotencyKey) => {
218
247
  existingEntries.push({ id: entry.id, ts: entry.ts });
219
248
  entriesByIdempotencyKey.set(idempotencyKey, existingEntries);
220
249
  };
221
- const checkChangeLogWarnings = ({ checkpointEntries, entriesByIdempotencyKey, findings, nowMs, openIssueEntries, resolvedLedgerIds, }) => {
250
+ const checkChangeLogWarnings = ({ checkpointEntries, checkpointMaxAgeMs, entriesByIdempotencyKey, findings, nowMs, openIssueMaxAgeMs, openIssueEntries, resolvedLedgerIds, }) => {
251
+ const checkpointMaxAgeLabel = formatAgeThreshold(checkpointMaxAgeMs);
222
252
  for (const checkpointEntry of checkpointEntries) {
223
253
  const ageMs = nowMs - Date.parse(checkpointEntry.ts);
224
- if (ageMs <= CHECKPOINT_MAX_AGE_MS) {
254
+ if (ageMs <= checkpointMaxAgeMs) {
225
255
  continue;
226
256
  }
227
257
  const idempotencyKey = checkpointEntry.links.idempotencyKey;
@@ -230,7 +260,7 @@ const checkChangeLogWarnings = ({ checkpointEntries, entriesByIdempotencyKey, fi
230
260
  if (!hasFollowUp) {
231
261
  pushFinding(findings, createFinding({
232
262
  code: 'pre-change-checkpoint-stale',
233
- message: `Pre-change checkpoint ${checkpointEntry.id} is older than 24h and has no follow-up entry with matching idempotencyKey.`,
263
+ message: `Pre-change checkpoint ${checkpointEntry.id} is older than ${checkpointMaxAgeLabel} and has no follow-up entry with matching idempotencyKey.`,
234
264
  metadata: {
235
265
  entryId: checkpointEntry.id,
236
266
  idempotencyKey: idempotencyKey ?? null,
@@ -239,14 +269,15 @@ const checkChangeLogWarnings = ({ checkpointEntries, entriesByIdempotencyKey, fi
239
269
  }));
240
270
  }
241
271
  }
272
+ const openIssueMaxAgeLabel = formatAgeThreshold(openIssueMaxAgeMs);
242
273
  for (const openIssueEntry of openIssueEntries) {
243
274
  const ageMs = nowMs - Date.parse(openIssueEntry.ts);
244
- if (ageMs <= OPEN_ISSUE_MAX_AGE_MS || resolvedLedgerIds.has(openIssueEntry.id)) {
275
+ if (ageMs <= openIssueMaxAgeMs || resolvedLedgerIds.has(openIssueEntry.id)) {
245
276
  continue;
246
277
  }
247
278
  pushFinding(findings, createFinding({
248
279
  code: 'open-issue-stale',
249
- message: `Open issue note ${openIssueEntry.id} is older than 30 days and has no resolution link.`,
280
+ message: `Open issue note ${openIssueEntry.id} is older than ${openIssueMaxAgeLabel} and has no resolution link.`,
250
281
  metadata: { entryId: openIssueEntry.id },
251
282
  remediation: 'Append a correction or superseding note that links back to the open issue once the follow-up is complete.',
252
283
  }));
@@ -351,7 +382,7 @@ const collectDoctorState = async (workspaceRoot, manifest, readIndex) => {
351
382
  const resolvedLedgerIds = new Set();
352
383
  let entryCount = 0;
353
384
  const nowMs = Date.now();
354
- const { scanBatchSize, scanConcurrency } = getLedgerRuntimeConfig();
385
+ const { doctorCheckpointMaxAgeMs, doctorOpenIssueMaxAgeMs, scanBatchSize, scanConcurrency } = getLedgerRuntimeConfig();
355
386
  const orderedEntries = getOrderedEntryLocations(manifest, readIndex, {});
356
387
  checkManifestSequenceOrder(orderedEntries, findings);
357
388
  for (let index = 0; index < orderedEntries.length; index += scanBatchSize) {
@@ -384,9 +415,11 @@ const collectDoctorState = async (workspaceRoot, manifest, readIndex) => {
384
415
  }
385
416
  checkChangeLogWarnings({
386
417
  checkpointEntries,
418
+ checkpointMaxAgeMs: doctorCheckpointMaxAgeMs,
387
419
  entriesByIdempotencyKey,
388
420
  findings,
389
421
  nowMs,
422
+ openIssueMaxAgeMs: doctorOpenIssueMaxAgeMs,
390
423
  openIssueEntries,
391
424
  resolvedLedgerIds,
392
425
  });
@@ -427,6 +460,7 @@ export const runLedgerDoctor = async (workspaceRoot, options = {}) => {
427
460
  return buildReadFailure(error);
428
461
  }
429
462
  const { blobChecks, entryCount, findings, latestByPhase, unseenManifestEntryIds } = doctorState;
463
+ findings.push(...(await checkPendingCommitQuarantines(workspaceRoot)));
430
464
  findings.push(...(await checkBlobPresence(workspaceRoot, blobChecks)));
431
465
  finalizeManifestChecks({
432
466
  entryCount,
@@ -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,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"}
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;AAQ9E,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;AA+KF,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,CA6E5E,CAAC"}