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.
- package/AGENTS.md +7 -5
- package/ARCHITECTURE.md +8 -2
- package/CHANGELOG.md +11 -0
- package/README.md +37 -5
- package/TROUBLESHOOTING.md +17 -3
- package/dist/blobs.d.ts.map +1 -1
- package/dist/blobs.js +1 -1
- package/dist/builders.d.ts +33 -0
- package/dist/builders.d.ts.map +1 -1
- package/dist/builders.js +10 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +153 -43
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +45 -11
- package/dist/handle.d.ts.map +1 -1
- package/dist/handle.js +67 -30
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +2 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/lab-min.d.ts +1 -1
- package/dist/lab-min.d.ts.map +1 -1
- package/dist/lab-min.js +2 -1
- package/dist/list.d.ts +32 -0
- package/dist/list.d.ts.map +1 -1
- package/dist/list.js +1 -1
- package/dist/patch-resolver.d.ts.map +1 -1
- package/dist/patch-resolver.js +1 -1
- package/dist/process.d.ts +2 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +16 -0
- package/dist/read-index.d.ts +7 -7
- package/dist/read-index.d.ts.map +1 -1
- package/dist/read-index.js +13 -9
- package/dist/record.d.ts.map +1 -1
- package/dist/record.js +1 -2
- package/dist/recovery.d.ts +8 -0
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +142 -30
- package/dist/render/retro.d.ts.map +1 -1
- package/dist/render/retro.js +4 -1
- package/dist/runtime-config.d.ts +2 -0
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +14 -0
- package/dist/schema/entry-core.d.ts +5 -2
- package/dist/schema/entry-core.d.ts.map +1 -1
- package/dist/schema/entry-core.js +3 -0
- package/dist/schema/entry-read.d.ts +57 -0
- package/dist/schema/entry-read.d.ts.map +1 -1
- package/dist/schema/entry-read.js +9 -1
- package/dist/schema/entry-write.d.ts +51 -0
- package/dist/schema/entry-write.d.ts.map +1 -1
- package/dist/schema/entry-write.js +9 -1
- package/dist/storage/filesystem.d.ts +14 -2
- package/dist/storage/filesystem.d.ts.map +1 -1
- package/dist/storage/filesystem.js +206 -39
- package/dist/storage/lock.d.ts.map +1 -1
- package/dist/storage/lock.js +38 -16
- 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
|
|
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
|
|
519
|
-
const
|
|
520
|
-
if (
|
|
521
|
-
|
|
557
|
+
const applyRationaleFlag = ({ flags, kind, record, }) => {
|
|
558
|
+
const rationale = getFlag(flags, 'rationale');
|
|
559
|
+
if (!rationale) {
|
|
560
|
+
return rationale;
|
|
522
561
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 = {
|
package/dist/doctor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,
|
|
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
|
|
11
|
-
const
|
|
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 <=
|
|
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
|
|
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 <=
|
|
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
|
|
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,
|
package/dist/handle.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|