ushman-ledger 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -5
- package/ARCHITECTURE.md +85 -0
- package/CHANGELOG.md +11 -0
- package/README.md +114 -5
- package/TROUBLESHOOTING.md +184 -0
- package/dist/blobs.d.ts +3 -0
- package/dist/blobs.d.ts.map +1 -1
- package/dist/blobs.js +41 -15
- 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 +176 -59
- package/dist/coverage.d.ts.map +1 -1
- package/dist/coverage.js +3 -2
- package/dist/doctor.d.ts +17 -4
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +263 -62
- package/dist/handle.d.ts.map +1 -1
- package/dist/handle.js +67 -30
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +23 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/list.d.ts +34 -1
- package/dist/list.d.ts.map +1 -1
- package/dist/list.js +19 -9
- package/dist/patch-resolver.d.ts.map +1 -1
- package/dist/patch-resolver.js +193 -53
- 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 +18 -13
- package/dist/record.js +2 -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 +14 -0
- package/dist/runtime-config.d.ts.map +1 -0
- package/dist/runtime-config.js +97 -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 +15 -2
- package/dist/storage/filesystem.d.ts.map +1 -1
- package/dist/storage/filesystem.js +234 -37
- package/dist/storage/lock.d.ts.map +1 -1
- package/dist/storage/lock.js +38 -16
- package/dist/text-lines.d.ts +8 -0
- package/dist/text-lines.d.ts.map +1 -0
- package/dist/text-lines.js +20 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -1
- package/package.json +4 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as v from 'valibot';
|
|
2
|
-
import { AgentPatchDiffSchema, ChangeLogFileChangeSchema, ChangeLogParityStatusSchema, ChangeLogSmokeResultSchema, ChangeLogSubkindSchema, ledgerEntryBaseEntries, NonEmptyTrimmedStringSchema, OperatorDecisionPayloadSchema, StripDecisionRevertedPayloadSchema, } from "./entry-core.js";
|
|
2
|
+
import { AgentPatchDiffSchema, ChangeLogFileChangeSchema, ChangeLogParityStatusSchema, ChangeLogSmokeResultSchema, ChangeLogSubkindSchema, ledgerEntryBaseEntries, NonEmptyTrimmedStringSchema, OperatorDecisionPayloadSchema, StageWriteStageSchema, StripDecisionRevertedPayloadSchema, WorkspaceRelativePathSchema, } from "./entry-core.js";
|
|
3
3
|
import { NoteSubkindSchema } from "./note.js";
|
|
4
4
|
export const ToolInvocationEntrySchema = v.object({
|
|
5
5
|
...ledgerEntryBaseEntries,
|
|
@@ -8,6 +8,13 @@ export const ToolInvocationEntrySchema = v.object({
|
|
|
8
8
|
exitCode: v.optional(v.pipe(v.number(), v.integer())),
|
|
9
9
|
kind: v.literal('tool-invocation'),
|
|
10
10
|
});
|
|
11
|
+
export const StageWriteEntrySchema = v.object({
|
|
12
|
+
...ledgerEntryBaseEntries,
|
|
13
|
+
filePath: WorkspaceRelativePathSchema,
|
|
14
|
+
kind: v.literal('stage-write'),
|
|
15
|
+
rationale: NonEmptyTrimmedStringSchema,
|
|
16
|
+
stage: StageWriteStageSchema,
|
|
17
|
+
});
|
|
11
18
|
const patchEntryEntries = {
|
|
12
19
|
...ledgerEntryBaseEntries,
|
|
13
20
|
diff: AgentPatchDiffSchema,
|
|
@@ -80,6 +87,7 @@ export const ChangeLogEntrySchema = v.object({
|
|
|
80
87
|
});
|
|
81
88
|
export const LedgerEntrySchema = v.variant('kind', [
|
|
82
89
|
ToolInvocationEntrySchema,
|
|
90
|
+
StageWriteEntrySchema,
|
|
83
91
|
AgentPatchEntrySchema,
|
|
84
92
|
OperatorPatchEntrySchema,
|
|
85
93
|
OperatorDecisionEntrySchema,
|
|
@@ -25,6 +25,32 @@ export declare const ToolInvocationRecordSchema: v.ObjectSchema<{
|
|
|
25
25
|
readonly phase: v.PicklistSchema<readonly ["capture", "intake", "seed", "vendor-extract", "cleanup", "parity", "characterize", "equiv", "analyze", "recover", "ship", "migration"], undefined>;
|
|
26
26
|
readonly summary: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>;
|
|
27
27
|
}, undefined>;
|
|
28
|
+
export declare const StageWriteRecordSchema: v.ObjectSchema<{
|
|
29
|
+
readonly filePath: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.CheckAction<string, "Expected a normalized workspace-relative path.">]>;
|
|
30
|
+
readonly kind: v.LiteralSchema<"stage-write", undefined>;
|
|
31
|
+
readonly rationale: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>;
|
|
32
|
+
readonly stage: v.PicklistSchema<readonly ["intake", "seed", "vendor-extract", "cleanup", "candidate-promotion"], undefined>;
|
|
33
|
+
readonly details: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
|
|
34
|
+
readonly emitter: v.ObjectSchema<{
|
|
35
|
+
readonly tool: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>;
|
|
36
|
+
readonly user: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>, undefined>;
|
|
37
|
+
readonly version: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>;
|
|
38
|
+
}, undefined>;
|
|
39
|
+
readonly idempotencyKey: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>, undefined>;
|
|
40
|
+
readonly links: v.OptionalSchema<v.ObjectWithRestSchema<{
|
|
41
|
+
readonly affectedFiles: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.CheckAction<string, "Expected a normalized workspace-relative path.">]>, undefined>, undefined>;
|
|
42
|
+
readonly blobs: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
43
|
+
readonly briefId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
44
|
+
readonly correctsLedgerId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
45
|
+
readonly gitRef: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
46
|
+
readonly idempotencyKey: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
47
|
+
readonly stripDecisionId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
48
|
+
readonly supersedesLedgerId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
49
|
+
readonly validatorVerdictId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
50
|
+
}, v.UnknownSchema, undefined>, undefined>;
|
|
51
|
+
readonly phase: v.PicklistSchema<readonly ["capture", "intake", "seed", "vendor-extract", "cleanup", "parity", "characterize", "equiv", "analyze", "recover", "ship", "migration"], undefined>;
|
|
52
|
+
readonly summary: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>;
|
|
53
|
+
}, undefined>;
|
|
28
54
|
export declare const AgentPatchRecordSchema: v.SchemaWithPipe<readonly [v.ObjectSchema<{
|
|
29
55
|
readonly agent: v.ObjectSchema<{
|
|
30
56
|
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>;
|
|
@@ -459,6 +485,31 @@ export declare const LedgerRecordSchema: v.VariantSchema<"kind", [v.ObjectSchema
|
|
|
459
485
|
}, v.UnknownSchema, undefined>, undefined>;
|
|
460
486
|
readonly phase: v.PicklistSchema<readonly ["capture", "intake", "seed", "vendor-extract", "cleanup", "parity", "characterize", "equiv", "analyze", "recover", "ship", "migration"], undefined>;
|
|
461
487
|
readonly summary: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>;
|
|
488
|
+
}, undefined>, v.ObjectSchema<{
|
|
489
|
+
readonly filePath: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.CheckAction<string, "Expected a normalized workspace-relative path.">]>;
|
|
490
|
+
readonly kind: v.LiteralSchema<"stage-write", undefined>;
|
|
491
|
+
readonly rationale: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>;
|
|
492
|
+
readonly stage: v.PicklistSchema<readonly ["intake", "seed", "vendor-extract", "cleanup", "candidate-promotion"], undefined>;
|
|
493
|
+
readonly details: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
|
|
494
|
+
readonly emitter: v.ObjectSchema<{
|
|
495
|
+
readonly tool: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>;
|
|
496
|
+
readonly user: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>, undefined>;
|
|
497
|
+
readonly version: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>;
|
|
498
|
+
}, undefined>;
|
|
499
|
+
readonly idempotencyKey: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>, undefined>;
|
|
500
|
+
readonly links: v.OptionalSchema<v.ObjectWithRestSchema<{
|
|
501
|
+
readonly affectedFiles: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.CheckAction<string, "Expected a normalized workspace-relative path.">]>, undefined>, undefined>;
|
|
502
|
+
readonly blobs: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
503
|
+
readonly briefId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
504
|
+
readonly correctsLedgerId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
505
|
+
readonly gitRef: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
506
|
+
readonly idempotencyKey: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
507
|
+
readonly stripDecisionId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
508
|
+
readonly supersedesLedgerId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
509
|
+
readonly validatorVerdictId: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
510
|
+
}, v.UnknownSchema, undefined>, undefined>;
|
|
511
|
+
readonly phase: v.PicklistSchema<readonly ["capture", "intake", "seed", "vendor-extract", "cleanup", "parity", "characterize", "equiv", "analyze", "recover", "ship", "migration"], undefined>;
|
|
512
|
+
readonly summary: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>;
|
|
462
513
|
}, undefined>, v.SchemaWithPipe<readonly [v.ObjectSchema<{
|
|
463
514
|
readonly agent: v.ObjectSchema<{
|
|
464
515
|
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-write.d.ts","sourceRoot":"","sources":["../../src/schema/entry-write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"entry-write.d.ts","sourceRoot":"","sources":["../../src/schema/entry-write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAC;AAsC7B,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;aAMrC,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;aAMjC,CAAC;AAUH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8HAUlC,CAAC;AAEF,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8HASrC,CAAC;AAEF,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;aAIvC,CAAC;AAEH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;aAOtC,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;aAOnC,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;aAK3B,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yDAOlC,CAAC;AAEF,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;aAI5C,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qDAkBjC,CAAC;AAEF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kEAY7B,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAC1E,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as v from 'valibot';
|
|
2
|
-
import { AgentPatchDiffSchema, ChangeLogFileChangeSchema, ChangeLogParityStatusSchema, ChangeLogSmokeResultSchema, ChangeLogSubkindSchema, NonEmptyTrimmedStringSchema, OperatorDecisionPayloadSchema, recordEntryBaseEntries, StripDecisionRevertedPayloadSchema, } from "./entry-core.js";
|
|
2
|
+
import { AgentPatchDiffSchema, ChangeLogFileChangeSchema, ChangeLogParityStatusSchema, ChangeLogSmokeResultSchema, ChangeLogSubkindSchema, NonEmptyTrimmedStringSchema, OperatorDecisionPayloadSchema, recordEntryBaseEntries, StageWriteStageSchema, StripDecisionRevertedPayloadSchema, WorkspaceRelativePathSchema, } from "./entry-core.js";
|
|
3
3
|
import { NoteSubkindSchema } from "./note.js";
|
|
4
4
|
const validatePatchRecord = (value) => {
|
|
5
5
|
const provided = [value.diff, value.diffPath, value.diffText].filter((entry) => Boolean(entry));
|
|
@@ -19,6 +19,13 @@ export const ToolInvocationRecordSchema = v.object({
|
|
|
19
19
|
exitCode: v.optional(v.pipe(v.number(), v.integer())),
|
|
20
20
|
kind: v.literal('tool-invocation'),
|
|
21
21
|
});
|
|
22
|
+
export const StageWriteRecordSchema = v.object({
|
|
23
|
+
...recordEntryBaseEntries,
|
|
24
|
+
filePath: WorkspaceRelativePathSchema,
|
|
25
|
+
kind: v.literal('stage-write'),
|
|
26
|
+
rationale: NonEmptyTrimmedStringSchema,
|
|
27
|
+
stage: StageWriteStageSchema,
|
|
28
|
+
});
|
|
22
29
|
const patchRecordEntries = {
|
|
23
30
|
...recordEntryBaseEntries,
|
|
24
31
|
diff: v.optional(AgentPatchDiffSchema),
|
|
@@ -93,6 +100,7 @@ export const ChangeLogRecordSchema = v.pipe(v.object({
|
|
|
93
100
|
}), v.check((value) => value.subkind !== 'rollback' || Boolean(value.rollsBack), 'change-log rollback records require rollsBack'));
|
|
94
101
|
export const LedgerRecordSchema = v.variant('kind', [
|
|
95
102
|
ToolInvocationRecordSchema,
|
|
103
|
+
StageWriteRecordSchema,
|
|
96
104
|
AgentPatchRecordSchema,
|
|
97
105
|
OperatorPatchRecordSchema,
|
|
98
106
|
OperatorDecisionRecordSchema,
|
|
@@ -5,13 +5,25 @@ export declare const LEDGER_ROOT_SEGMENTS: readonly [".lab", "ledger"];
|
|
|
5
5
|
export declare const LEDGER_STALE_TEMP_MS = 60000;
|
|
6
6
|
type AtomicWriteTestHook = {
|
|
7
7
|
readonly beforeRename?: () => Promise<void>;
|
|
8
|
+
readonly beforeWrite?: () => Promise<void>;
|
|
8
9
|
readonly retainOnThrow?: boolean;
|
|
9
10
|
};
|
|
11
|
+
type AtomicTempFileRegistration = {
|
|
12
|
+
readonly remove: () => Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
type AtomicWriteOptions = {
|
|
15
|
+
readonly registerTempFile?: (context: {
|
|
16
|
+
readonly finalPath: string;
|
|
17
|
+
readonly tempPath: string;
|
|
18
|
+
}) => Promise<AtomicTempFileRegistration>;
|
|
19
|
+
};
|
|
10
20
|
export type LedgerPaths = {
|
|
11
21
|
readonly blobsDir: string;
|
|
22
|
+
readonly externalTempFilesDir: string;
|
|
12
23
|
readonly lockFile: (phase: LedgerPhase) => string;
|
|
13
24
|
readonly manifestLockFile: string;
|
|
14
25
|
readonly manifestFile: string;
|
|
26
|
+
readonly pendingCommitQuarantineDir: string;
|
|
15
27
|
readonly pendingCommitsDir: string;
|
|
16
28
|
readonly pendingArchivesDir: string;
|
|
17
29
|
readonly phaseDir: (phase: LedgerPhase) => string;
|
|
@@ -31,9 +43,10 @@ export type AtomicTextFileWriter = {
|
|
|
31
43
|
export declare const resolveLedgerPaths: (workspaceRoot: string) => LedgerPaths;
|
|
32
44
|
export declare const ensureLedgerDirectories: (workspaceRoot: string) => Promise<LedgerPaths>;
|
|
33
45
|
export declare const setAtomicWriteTestHook: (filePath: string, hook: AtomicWriteTestHook | null) => void;
|
|
34
|
-
export declare const writeAtomicTextFile: (filePath: string, text: string) => Promise<void>;
|
|
35
|
-
export declare const createAtomicTextFileWriter: (filePath: string) => Promise<AtomicTextFileWriter>;
|
|
46
|
+
export declare const writeAtomicTextFile: (filePath: string, text: string, options?: AtomicWriteOptions) => Promise<void>;
|
|
47
|
+
export declare const createAtomicTextFileWriter: (filePath: string, options?: AtomicWriteOptions) => Promise<AtomicTextFileWriter>;
|
|
36
48
|
export declare const writeAtomicJsonFile: (filePath: string, value: unknown) => Promise<void>;
|
|
49
|
+
export declare const createExternalTempFileRegistrar: (workspaceRoot: string) => NonNullable<AtomicWriteOptions["registerTempFile"]>;
|
|
37
50
|
export declare const readManifest: (workspaceRoot: string) => Promise<LedgerManifest>;
|
|
38
51
|
export declare const saveManifest: (workspaceRoot: string, manifest: LedgerManifest) => Promise<void>;
|
|
39
52
|
export declare const cleanupStaleTempFiles: (workspaceRoot: string) => Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filesystem.d.ts","sourceRoot":"","sources":["../../src/storage/filesystem.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"filesystem.d.ts","sourceRoot":"","sources":["../../src/storage/filesystem.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import { mkdir, open, readdir, readFile, rename, rm, stat } from 'node:fs/promis
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import * as v from 'valibot';
|
|
5
5
|
import { stableStringify } from "../json.js";
|
|
6
|
+
import { isProcessAlive } from "../process.js";
|
|
6
7
|
import { readLabManifestMin } from "../lab-min.js";
|
|
7
8
|
import { LEDGER_PHASES } from "../schema/entry.js";
|
|
8
9
|
import { LedgerManifestSchema } from "../schema/manifest.js";
|
|
@@ -35,13 +36,22 @@ const runAtomicWriteTestHook = async (filePath) => {
|
|
|
35
36
|
throw error;
|
|
36
37
|
}
|
|
37
38
|
};
|
|
39
|
+
const runAtomicWriteWriteTestHook = async (filePath) => {
|
|
40
|
+
const { hook } = getAtomicWriteTestHook(filePath);
|
|
41
|
+
if (!hook?.beforeWrite) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
await hook.beforeWrite();
|
|
45
|
+
};
|
|
38
46
|
export const resolveLedgerPaths = (workspaceRoot) => {
|
|
39
47
|
const root = path.join(workspaceRoot, ...LEDGER_ROOT_SEGMENTS);
|
|
40
48
|
return {
|
|
41
49
|
blobsDir: path.join(root, 'blobs'),
|
|
50
|
+
externalTempFilesDir: path.join(root, 'external-temp-files'),
|
|
42
51
|
lockFile: (phase) => path.join(root, phase, '.lock'),
|
|
43
52
|
manifestFile: path.join(root, 'manifest.json'),
|
|
44
53
|
manifestLockFile: path.join(root, '.manifest.lock'),
|
|
54
|
+
pendingCommitQuarantineDir: path.join(root, 'pending-quarantine'),
|
|
45
55
|
pendingArchivesDir: path.join(root, 'pending-archives'),
|
|
46
56
|
pendingCommitsDir: path.join(root, 'pending'),
|
|
47
57
|
phaseDir: (phase) => path.join(root, phase),
|
|
@@ -58,7 +68,9 @@ export const ensureLedgerDirectories = async (workspaceRoot) => {
|
|
|
58
68
|
const paths = resolveLedgerPaths(workspaceRoot);
|
|
59
69
|
await mkdir(paths.root, { recursive: true });
|
|
60
70
|
await mkdir(paths.blobsDir, { recursive: true });
|
|
71
|
+
await mkdir(paths.externalTempFilesDir, { recursive: true });
|
|
61
72
|
await mkdir(paths.pendingCommitsDir, { recursive: true });
|
|
73
|
+
await mkdir(paths.pendingCommitQuarantineDir, { recursive: true });
|
|
62
74
|
await mkdir(paths.pendingArchivesDir, { recursive: true });
|
|
63
75
|
await Promise.all(LEDGER_PHASES.map(async (phase) => {
|
|
64
76
|
await mkdir(paths.phaseDir(phase), { recursive: true });
|
|
@@ -73,6 +85,36 @@ export const setAtomicWriteTestHook = (filePath, hook) => {
|
|
|
73
85
|
}
|
|
74
86
|
atomicWriteTestHooks.set(key, hook);
|
|
75
87
|
};
|
|
88
|
+
const withPrimaryErrorPreserved = (error, cleanupError) => {
|
|
89
|
+
if (error instanceof Error) {
|
|
90
|
+
const errorWithCause = error;
|
|
91
|
+
if (errorWithCause.cause === undefined) {
|
|
92
|
+
Object.defineProperty(errorWithCause, 'cause', {
|
|
93
|
+
configurable: true,
|
|
94
|
+
enumerable: false,
|
|
95
|
+
value: cleanupError,
|
|
96
|
+
writable: true,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return error;
|
|
100
|
+
}
|
|
101
|
+
return new Error(String(error), { cause: cleanupError });
|
|
102
|
+
};
|
|
103
|
+
const removeTempRegistration = async (tempRegistration) => {
|
|
104
|
+
if (!tempRegistration) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await tempRegistration.remove();
|
|
108
|
+
};
|
|
109
|
+
const throwWithTempRegistrationCleanup = async ({ error, tempRegistration, }) => {
|
|
110
|
+
try {
|
|
111
|
+
await removeTempRegistration(tempRegistration);
|
|
112
|
+
}
|
|
113
|
+
catch (cleanupError) {
|
|
114
|
+
throw withPrimaryErrorPreserved(error, cleanupError);
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
};
|
|
76
118
|
const createManifest = async (workspaceRoot) => {
|
|
77
119
|
const labManifest = await readLabManifestMin(workspaceRoot);
|
|
78
120
|
const now = new Date().toISOString();
|
|
@@ -90,31 +132,75 @@ const createManifest = async (workspaceRoot) => {
|
|
|
90
132
|
workspaceId: labManifest.workspaceId,
|
|
91
133
|
});
|
|
92
134
|
};
|
|
93
|
-
export const writeAtomicTextFile = async (filePath, text) => {
|
|
135
|
+
export const writeAtomicTextFile = async (filePath, text, options = {}) => {
|
|
94
136
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
95
137
|
const tempPath = `${filePath}.tmp.${process.pid}.${Date.now()}.${randomUUID()}`;
|
|
96
|
-
const
|
|
138
|
+
const tempRegistration = await options.registerTempFile?.({ finalPath: filePath, tempPath });
|
|
139
|
+
let handle;
|
|
97
140
|
try {
|
|
98
|
-
await
|
|
99
|
-
|
|
141
|
+
handle = await open(tempPath, 'w');
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
await throwWithTempRegistrationCleanup({
|
|
145
|
+
error,
|
|
146
|
+
tempRegistration,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const fileHandle = handle;
|
|
150
|
+
if (!fileHandle) {
|
|
151
|
+
throw new Error(`Failed to open atomic temp file: ${tempPath}`);
|
|
152
|
+
}
|
|
153
|
+
let writeError;
|
|
154
|
+
try {
|
|
155
|
+
await fileHandle.writeFile(text, 'utf8');
|
|
156
|
+
await fileHandle.sync();
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
writeError = error;
|
|
100
160
|
}
|
|
101
161
|
finally {
|
|
102
|
-
await
|
|
162
|
+
await fileHandle.close();
|
|
163
|
+
}
|
|
164
|
+
if (writeError) {
|
|
165
|
+
await rm(tempPath, { force: true });
|
|
166
|
+
await throwWithTempRegistrationCleanup({
|
|
167
|
+
error: writeError,
|
|
168
|
+
tempRegistration,
|
|
169
|
+
});
|
|
103
170
|
}
|
|
104
171
|
await runAtomicWriteTestHook(filePath);
|
|
105
172
|
try {
|
|
106
173
|
await rename(tempPath, filePath);
|
|
174
|
+
await removeTempRegistration(tempRegistration);
|
|
107
175
|
}
|
|
108
176
|
catch (error) {
|
|
109
177
|
await rm(tempPath, { force: true });
|
|
110
|
-
|
|
178
|
+
await throwWithTempRegistrationCleanup({
|
|
179
|
+
error,
|
|
180
|
+
tempRegistration,
|
|
181
|
+
});
|
|
111
182
|
}
|
|
112
183
|
};
|
|
113
|
-
export const createAtomicTextFileWriter = async (filePath) => {
|
|
184
|
+
export const createAtomicTextFileWriter = async (filePath, options = {}) => {
|
|
114
185
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
115
186
|
const tempPath = `${filePath}.tmp.${process.pid}.${Date.now()}.${randomUUID()}`;
|
|
116
|
-
const
|
|
187
|
+
const tempRegistration = await options.registerTempFile?.({ finalPath: filePath, tempPath });
|
|
188
|
+
let handle;
|
|
189
|
+
try {
|
|
190
|
+
handle = await open(tempPath, 'w');
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
await throwWithTempRegistrationCleanup({
|
|
194
|
+
error,
|
|
195
|
+
tempRegistration,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const fileHandle = handle;
|
|
199
|
+
if (!fileHandle) {
|
|
200
|
+
throw new Error(`Failed to open atomic temp file: ${tempPath}`);
|
|
201
|
+
}
|
|
117
202
|
let completed = false;
|
|
203
|
+
let pendingWriteError;
|
|
118
204
|
let writeChain = Promise.resolve();
|
|
119
205
|
const finalizeHandle = async () => {
|
|
120
206
|
if (completed) {
|
|
@@ -125,34 +211,61 @@ export const createAtomicTextFileWriter = async (filePath) => {
|
|
|
125
211
|
};
|
|
126
212
|
const waitForWrites = async () => {
|
|
127
213
|
await writeChain;
|
|
214
|
+
if (pendingWriteError) {
|
|
215
|
+
throw pendingWriteError;
|
|
216
|
+
}
|
|
128
217
|
};
|
|
129
218
|
return {
|
|
130
219
|
abort: async () => {
|
|
131
220
|
if (!(await finalizeHandle())) {
|
|
132
221
|
return;
|
|
133
222
|
}
|
|
134
|
-
|
|
135
|
-
|
|
223
|
+
let writeError;
|
|
224
|
+
try {
|
|
225
|
+
await waitForWrites();
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
writeError = error;
|
|
229
|
+
}
|
|
230
|
+
await fileHandle.close();
|
|
136
231
|
await rm(tempPath, { force: true });
|
|
232
|
+
await throwWithTempRegistrationCleanup({
|
|
233
|
+
error: writeError,
|
|
234
|
+
tempRegistration,
|
|
235
|
+
});
|
|
137
236
|
},
|
|
138
237
|
close: async () => {
|
|
139
238
|
if (!(await finalizeHandle())) {
|
|
140
239
|
return;
|
|
141
240
|
}
|
|
142
|
-
await waitForWrites();
|
|
143
241
|
try {
|
|
144
|
-
await
|
|
242
|
+
await waitForWrites();
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
await fileHandle.close();
|
|
246
|
+
await rm(tempPath, { force: true });
|
|
247
|
+
await throwWithTempRegistrationCleanup({
|
|
248
|
+
error,
|
|
249
|
+
tempRegistration,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
await fileHandle.sync();
|
|
145
254
|
}
|
|
146
255
|
finally {
|
|
147
|
-
await
|
|
256
|
+
await fileHandle.close();
|
|
148
257
|
}
|
|
149
258
|
await runAtomicWriteTestHook(filePath);
|
|
150
259
|
try {
|
|
151
260
|
await rename(tempPath, filePath);
|
|
261
|
+
await removeTempRegistration(tempRegistration);
|
|
152
262
|
}
|
|
153
263
|
catch (error) {
|
|
154
264
|
await rm(tempPath, { force: true });
|
|
155
|
-
|
|
265
|
+
await throwWithTempRegistrationCleanup({
|
|
266
|
+
error,
|
|
267
|
+
tempRegistration,
|
|
268
|
+
});
|
|
156
269
|
}
|
|
157
270
|
},
|
|
158
271
|
write: async (chunk) => {
|
|
@@ -160,9 +273,12 @@ export const createAtomicTextFileWriter = async (filePath) => {
|
|
|
160
273
|
if (completed || chunk.length === 0) {
|
|
161
274
|
return;
|
|
162
275
|
}
|
|
163
|
-
await
|
|
276
|
+
await runAtomicWriteWriteTestHook(filePath);
|
|
277
|
+
await fileHandle.write(chunk, undefined, 'utf8');
|
|
278
|
+
});
|
|
279
|
+
writeChain = nextWrite.catch((error) => {
|
|
280
|
+
pendingWriteError ??= error;
|
|
164
281
|
});
|
|
165
|
-
writeChain = nextWrite.then(() => undefined, () => undefined);
|
|
166
282
|
await nextWrite;
|
|
167
283
|
},
|
|
168
284
|
};
|
|
@@ -170,6 +286,26 @@ export const createAtomicTextFileWriter = async (filePath) => {
|
|
|
170
286
|
export const writeAtomicJsonFile = async (filePath, value) => {
|
|
171
287
|
await writeAtomicTextFile(filePath, `${stableStringify(value, true)}\n`);
|
|
172
288
|
};
|
|
289
|
+
const EXTERNAL_TEMP_FILE_SCHEMA_VERSION = 'ushman-ledger-external-temp-file/v1';
|
|
290
|
+
const ExternalTempFileRecordSchema = v.object({
|
|
291
|
+
createdAt: v.pipe(v.string(), v.isoTimestamp()),
|
|
292
|
+
finalPath: v.pipe(v.string(), v.minLength(1)),
|
|
293
|
+
schemaVersion: v.literal(EXTERNAL_TEMP_FILE_SCHEMA_VERSION),
|
|
294
|
+
tempPath: v.pipe(v.string(), v.minLength(1)),
|
|
295
|
+
});
|
|
296
|
+
export const createExternalTempFileRegistrar = (workspaceRoot) => async ({ finalPath, tempPath }) => {
|
|
297
|
+
const paths = await ensureLedgerDirectories(workspaceRoot);
|
|
298
|
+
const recordPath = path.join(paths.externalTempFilesDir, `${process.pid}.${Date.now()}.${randomUUID()}.json`);
|
|
299
|
+
await writeAtomicJsonFile(recordPath, v.parse(ExternalTempFileRecordSchema, {
|
|
300
|
+
createdAt: new Date().toISOString(),
|
|
301
|
+
finalPath: path.resolve(finalPath),
|
|
302
|
+
schemaVersion: EXTERNAL_TEMP_FILE_SCHEMA_VERSION,
|
|
303
|
+
tempPath: path.resolve(tempPath),
|
|
304
|
+
}));
|
|
305
|
+
return {
|
|
306
|
+
remove: async () => rm(recordPath, { force: true }),
|
|
307
|
+
};
|
|
308
|
+
};
|
|
173
309
|
const parseManifestText = (filePath, text) => {
|
|
174
310
|
try {
|
|
175
311
|
return v.parse(LedgerManifestSchema, JSON.parse(text));
|
|
@@ -238,22 +374,6 @@ const parseTempOwnerPid = (fileName) => {
|
|
|
238
374
|
}
|
|
239
375
|
return Number.parseInt(match[1], 10);
|
|
240
376
|
};
|
|
241
|
-
const isProcessAlive = (pid) => {
|
|
242
|
-
try {
|
|
243
|
-
process.kill(pid, 0);
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
const code = error.code;
|
|
248
|
-
if (code === 'EPERM') {
|
|
249
|
-
return true;
|
|
250
|
-
}
|
|
251
|
-
if (code === 'ESRCH') {
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
254
|
-
throw error;
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
377
|
const resolveFinalPathFromTemp = (filePath) => {
|
|
258
378
|
const fileName = path.basename(filePath);
|
|
259
379
|
const tempMarkerIndex = fileName.lastIndexOf('.tmp.');
|
|
@@ -274,10 +394,12 @@ const cleanupStaleTempFile = async (fullPath) => {
|
|
|
274
394
|
throw error;
|
|
275
395
|
}
|
|
276
396
|
const ownerPid = parseTempOwnerPid(path.basename(fullPath));
|
|
277
|
-
if (
|
|
278
|
-
|
|
397
|
+
if (ownerPid !== null) {
|
|
398
|
+
if (isProcessAlive(ownerPid)) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
279
401
|
}
|
|
280
|
-
if (
|
|
402
|
+
else if (Date.now() - fileStat.mtimeMs < LEDGER_STALE_TEMP_MS) {
|
|
281
403
|
return;
|
|
282
404
|
}
|
|
283
405
|
const finalPath = resolveFinalPathFromTemp(fullPath);
|
|
@@ -295,6 +417,73 @@ const cleanupStaleTempFile = async (fullPath) => {
|
|
|
295
417
|
}
|
|
296
418
|
await rm(fullPath, { force: true });
|
|
297
419
|
};
|
|
420
|
+
const readExternalTempFileRecord = async (recordPath) => {
|
|
421
|
+
try {
|
|
422
|
+
const text = await readFile(recordPath, 'utf8');
|
|
423
|
+
return {
|
|
424
|
+
kind: 'valid',
|
|
425
|
+
record: v.parse(ExternalTempFileRecordSchema, JSON.parse(text)),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
if (error.code === 'ENOENT') {
|
|
430
|
+
return {
|
|
431
|
+
kind: 'missing',
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
kind: 'invalid',
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
const pathExists = async (filePath) => {
|
|
440
|
+
try {
|
|
441
|
+
await stat(filePath);
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
if (error.code === 'ENOENT') {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const isTrackedAtomicTempRecord = (record) => path.dirname(record.tempPath) === path.dirname(record.finalPath) &&
|
|
452
|
+
path.basename(record.tempPath).startsWith(`${path.basename(record.finalPath)}.tmp.`);
|
|
453
|
+
const quarantineInvalidExternalTempRecord = async (recordPath) => {
|
|
454
|
+
const quarantinedPath = `${recordPath}.invalid.${Date.now()}.${randomUUID()}`;
|
|
455
|
+
try {
|
|
456
|
+
await rename(recordPath, quarantinedPath);
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
if (error.code === 'ENOENT') {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
const cleanupTrackedExternalTempFile = async (recordPath) => {
|
|
466
|
+
const recordResult = await readExternalTempFileRecord(recordPath);
|
|
467
|
+
if (recordResult.kind === 'missing') {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (recordResult.kind === 'invalid') {
|
|
471
|
+
await quarantineInvalidExternalTempRecord(recordPath);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (!isTrackedAtomicTempRecord(recordResult.record)) {
|
|
475
|
+
await quarantineInvalidExternalTempRecord(recordPath);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (!(await pathExists(recordResult.record.tempPath))) {
|
|
479
|
+
await rm(recordPath, { force: true });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
await cleanupStaleTempFile(recordResult.record.tempPath);
|
|
483
|
+
if (!(await pathExists(recordResult.record.tempPath))) {
|
|
484
|
+
await rm(recordPath, { force: true });
|
|
485
|
+
}
|
|
486
|
+
};
|
|
298
487
|
const listLedgerTempFiles = async (root) => {
|
|
299
488
|
const entries = await readdir(root, { withFileTypes: true });
|
|
300
489
|
const tempFiles = [];
|
|
@@ -312,8 +501,16 @@ const listLedgerTempFiles = async (root) => {
|
|
|
312
501
|
};
|
|
313
502
|
export const cleanupStaleTempFiles = async (workspaceRoot) => {
|
|
314
503
|
const paths = await ensureLedgerDirectories(workspaceRoot);
|
|
315
|
-
const tempFiles = await
|
|
316
|
-
|
|
504
|
+
const [tempFiles, externalTempRecords] = await Promise.all([
|
|
505
|
+
listLedgerTempFiles(paths.root),
|
|
506
|
+
readdir(paths.externalTempFilesDir, { withFileTypes: true }),
|
|
507
|
+
]);
|
|
508
|
+
await Promise.all([
|
|
509
|
+
...tempFiles.map(async (filePath) => cleanupStaleTempFile(filePath)),
|
|
510
|
+
...externalTempRecords
|
|
511
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
512
|
+
.map(async (entry) => cleanupTrackedExternalTempFile(path.join(paths.externalTempFilesDir, entry.name))),
|
|
513
|
+
]);
|
|
317
514
|
};
|
|
318
515
|
export const readPhaseEntryFileNames = async (workspaceRoot, phase) => {
|
|
319
516
|
const paths = await ensureLedgerDirectories(workspaceRoot);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../src/storage/lock.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../src/storage/lock.ts"],"names":[],"mappings":"AAIA,KAAK,WAAW,GAAG;IACf,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE;QACjB,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC,OAAO,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACtG,CAAC;IACF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC9B,CAAC;AAyMF,MAAM,MAAM,UAAU,GAAG;IACrB,QAAQ,CAAC,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC,CAAC;AAkCF,eAAO,MAAM,WAAW,GAAU,UAAU,MAAM,EAAE,UAAS,OAAO,CAAC,WAAW,CAAM,KAAG,OAAO,CAAC,UAAU,CAgD1G,CAAC"}
|