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.
Files changed (67) hide show
  1. package/AGENTS.md +7 -5
  2. package/ARCHITECTURE.md +85 -0
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +114 -5
  5. package/TROUBLESHOOTING.md +184 -0
  6. package/dist/blobs.d.ts +3 -0
  7. package/dist/blobs.d.ts.map +1 -1
  8. package/dist/blobs.js +41 -15
  9. package/dist/builders.d.ts +33 -0
  10. package/dist/builders.d.ts.map +1 -1
  11. package/dist/builders.js +10 -1
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +176 -59
  14. package/dist/coverage.d.ts.map +1 -1
  15. package/dist/coverage.js +3 -2
  16. package/dist/doctor.d.ts +17 -4
  17. package/dist/doctor.d.ts.map +1 -1
  18. package/dist/doctor.js +263 -62
  19. package/dist/handle.d.ts.map +1 -1
  20. package/dist/handle.js +67 -30
  21. package/dist/helpers.d.ts +1 -0
  22. package/dist/helpers.d.ts.map +1 -1
  23. package/dist/helpers.js +23 -0
  24. package/dist/index.d.ts +4 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +4 -2
  27. package/dist/list.d.ts +34 -1
  28. package/dist/list.d.ts.map +1 -1
  29. package/dist/list.js +19 -9
  30. package/dist/patch-resolver.d.ts.map +1 -1
  31. package/dist/patch-resolver.js +193 -53
  32. package/dist/process.d.ts +2 -0
  33. package/dist/process.d.ts.map +1 -0
  34. package/dist/process.js +16 -0
  35. package/dist/read-index.d.ts +7 -7
  36. package/dist/read-index.d.ts.map +1 -1
  37. package/dist/read-index.js +18 -13
  38. package/dist/record.js +2 -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 +14 -0
  45. package/dist/runtime-config.d.ts.map +1 -0
  46. package/dist/runtime-config.js +97 -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 +15 -2
  57. package/dist/storage/filesystem.d.ts.map +1 -1
  58. package/dist/storage/filesystem.js +234 -37
  59. package/dist/storage/lock.d.ts.map +1 -1
  60. package/dist/storage/lock.js +38 -16
  61. package/dist/text-lines.d.ts +8 -0
  62. package/dist/text-lines.d.ts.map +1 -0
  63. package/dist/text-lines.js +20 -0
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.d.ts.map +1 -1
  66. package/dist/version.js +2 -1
  67. 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;AAoC7B,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;aAMrC,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kEAW7B,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
+ {"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":"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"}
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 handle = await open(tempPath, 'w');
138
+ const tempRegistration = await options.registerTempFile?.({ finalPath: filePath, tempPath });
139
+ let handle;
97
140
  try {
98
- await handle.writeFile(text, 'utf8');
99
- await handle.sync();
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 handle.close();
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
- throw error;
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 handle = await open(tempPath, 'w');
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
- await waitForWrites();
135
- await handle.close();
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 handle.sync();
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 handle.close();
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
- throw error;
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 handle.writeFile(chunk, 'utf8');
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 (Date.now() - fileStat.mtimeMs < LEDGER_STALE_TEMP_MS) {
278
- return;
397
+ if (ownerPid !== null) {
398
+ if (isProcessAlive(ownerPid)) {
399
+ return;
400
+ }
279
401
  }
280
- if (ownerPid !== null && isProcessAlive(ownerPid)) {
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 listLedgerTempFiles(paths.root);
316
- await Promise.all(tempFiles.map(async (filePath) => cleanupStaleTempFile(filePath)));
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":"AAGA,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;AAqLF,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;AA+BF,eAAO,MAAM,WAAW,GAAU,UAAU,MAAM,EAAE,UAAS,OAAO,CAAC,WAAW,CAAM,KAAG,OAAO,CAAC,UAAU,CAgD1G,CAAC"}
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"}