ushman-ledger 0.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 (101) hide show
  1. package/AGENTS.md +41 -0
  2. package/CHANGELOG.md +23 -0
  3. package/LICENSE.md +21 -0
  4. package/README.md +233 -0
  5. package/dist/archive-journal.d.ts +63 -0
  6. package/dist/archive-journal.d.ts.map +1 -0
  7. package/dist/archive-journal.js +220 -0
  8. package/dist/archive.d.ts +30 -0
  9. package/dist/archive.d.ts.map +1 -0
  10. package/dist/archive.js +117 -0
  11. package/dist/async.d.ts +2 -0
  12. package/dist/async.d.ts.map +1 -0
  13. package/dist/async.js +20 -0
  14. package/dist/blobs.d.ts +10 -0
  15. package/dist/blobs.d.ts.map +1 -0
  16. package/dist/blobs.js +58 -0
  17. package/dist/builders.d.ts +465 -0
  18. package/dist/builders.d.ts.map +1 -0
  19. package/dist/builders.js +73 -0
  20. package/dist/candidate-paths.d.ts +3 -0
  21. package/dist/candidate-paths.d.ts.map +1 -0
  22. package/dist/candidate-paths.js +11 -0
  23. package/dist/cli.d.ts +15 -0
  24. package/dist/cli.d.ts.map +1 -0
  25. package/dist/cli.js +562 -0
  26. package/dist/coverage.d.ts +8 -0
  27. package/dist/coverage.d.ts.map +1 -0
  28. package/dist/coverage.js +128 -0
  29. package/dist/doctor.d.ts +9 -0
  30. package/dist/doctor.d.ts.map +1 -0
  31. package/dist/doctor.js +172 -0
  32. package/dist/handle.d.ts +28 -0
  33. package/dist/handle.d.ts.map +1 -0
  34. package/dist/handle.js +90 -0
  35. package/dist/index.d.ts +11 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +9 -0
  38. package/dist/json.d.ts +4 -0
  39. package/dist/json.d.ts.map +1 -0
  40. package/dist/json.js +25 -0
  41. package/dist/lab-min.d.ts +9 -0
  42. package/dist/lab-min.d.ts.map +1 -0
  43. package/dist/lab-min.js +23 -0
  44. package/dist/list.d.ts +582 -0
  45. package/dist/list.d.ts.map +1 -0
  46. package/dist/list.js +139 -0
  47. package/dist/manifest-update.d.ts +13 -0
  48. package/dist/manifest-update.d.ts.map +1 -0
  49. package/dist/manifest-update.js +43 -0
  50. package/dist/note.d.ts +13 -0
  51. package/dist/note.d.ts.map +1 -0
  52. package/dist/note.js +15 -0
  53. package/dist/patch-metadata.d.ts +37 -0
  54. package/dist/patch-metadata.d.ts.map +1 -0
  55. package/dist/patch-metadata.js +300 -0
  56. package/dist/read-index.d.ts +114 -0
  57. package/dist/read-index.d.ts.map +1 -0
  58. package/dist/read-index.js +210 -0
  59. package/dist/record.d.ts +25 -0
  60. package/dist/record.d.ts.map +1 -0
  61. package/dist/record.js +268 -0
  62. package/dist/recovery.d.ts +39 -0
  63. package/dist/recovery.d.ts.map +1 -0
  64. package/dist/recovery.js +189 -0
  65. package/dist/render/analytics-summary.d.ts +58 -0
  66. package/dist/render/analytics-summary.d.ts.map +1 -0
  67. package/dist/render/analytics-summary.js +151 -0
  68. package/dist/render/dependency-graph.d.ts +3 -0
  69. package/dist/render/dependency-graph.d.ts.map +1 -0
  70. package/dist/render/dependency-graph.js +18 -0
  71. package/dist/render/jsonl.d.ts +3 -0
  72. package/dist/render/jsonl.d.ts.map +1 -0
  73. package/dist/render/jsonl.js +8 -0
  74. package/dist/render/retro.d.ts +6 -0
  75. package/dist/render/retro.d.ts.map +1 -0
  76. package/dist/render/retro.js +124 -0
  77. package/dist/render/timeline-html.d.ts +3 -0
  78. package/dist/render/timeline-html.d.ts.map +1 -0
  79. package/dist/render/timeline-html.js +37 -0
  80. package/dist/schema/entry.d.ts +3298 -0
  81. package/dist/schema/entry.d.ts.map +1 -0
  82. package/dist/schema/entry.js +619 -0
  83. package/dist/schema/manifest.d.ts +42 -0
  84. package/dist/schema/manifest.d.ts.map +1 -0
  85. package/dist/schema/manifest.js +27 -0
  86. package/dist/schema/note.d.ts +10 -0
  87. package/dist/schema/note.d.ts.map +1 -0
  88. package/dist/schema/note.js +2 -0
  89. package/dist/storage/filesystem.d.ts +35 -0
  90. package/dist/storage/filesystem.d.ts.map +1 -0
  91. package/dist/storage/filesystem.js +258 -0
  92. package/dist/storage/lock.d.ts +18 -0
  93. package/dist/storage/lock.d.ts.map +1 -0
  94. package/dist/storage/lock.js +224 -0
  95. package/dist/uuid.d.ts +7 -0
  96. package/dist/uuid.d.ts.map +1 -0
  97. package/dist/uuid.js +25 -0
  98. package/dist/version.d.ts +2 -0
  99. package/dist/version.d.ts.map +1 -0
  100. package/dist/version.js +1 -0
  101. package/package.json +73 -0
@@ -0,0 +1,114 @@
1
+ import { z } from 'zod';
2
+ import { type LedgerEntry, type LedgerKind, type LedgerPhase } from './schema/entry.ts';
3
+ import type { LedgerManifest } from './schema/manifest.ts';
4
+ declare const ReadIndexEntrySchema: z.ZodObject<{
5
+ id: z.ZodString;
6
+ kind: z.ZodEnum<{
7
+ "tool-invocation": "tool-invocation";
8
+ "agent-patch": "agent-patch";
9
+ "operator-patch": "operator-patch";
10
+ "stage-transition": "stage-transition";
11
+ "operator-decision": "operator-decision";
12
+ "validator-result": "validator-result";
13
+ "runtime-event": "runtime-event";
14
+ note: "note";
15
+ correction: "correction";
16
+ "strip-decision-reverted": "strip-decision-reverted";
17
+ "descope-brief": "descope-brief";
18
+ "merge-return": "merge-return";
19
+ "merge-return-rejected": "merge-return-rejected";
20
+ revert: "revert";
21
+ rollback: "rollback";
22
+ "rework.test_retired": "rework.test_retired";
23
+ }>;
24
+ ts: z.ZodString;
25
+ }, z.core.$strip>;
26
+ declare const LedgerReadIndexSchema: z.ZodObject<{
27
+ coveredFiles: z.ZodDefault<z.ZodArray<z.ZodString>>;
28
+ entries: z.ZodDefault<z.ZodArray<z.ZodObject<{
29
+ id: z.ZodString;
30
+ kind: z.ZodEnum<{
31
+ "tool-invocation": "tool-invocation";
32
+ "agent-patch": "agent-patch";
33
+ "operator-patch": "operator-patch";
34
+ "stage-transition": "stage-transition";
35
+ "operator-decision": "operator-decision";
36
+ "validator-result": "validator-result";
37
+ "runtime-event": "runtime-event";
38
+ note: "note";
39
+ correction: "correction";
40
+ "strip-decision-reverted": "strip-decision-reverted";
41
+ "descope-brief": "descope-brief";
42
+ "merge-return": "merge-return";
43
+ "merge-return-rejected": "merge-return-rejected";
44
+ revert: "revert";
45
+ rollback: "rollback";
46
+ "rework.test_retired": "rework.test_retired";
47
+ }>;
48
+ ts: z.ZodString;
49
+ }, z.core.$strip>>>;
50
+ entryCount: z.ZodNumber;
51
+ lastEntryId: z.ZodNullable<z.ZodString>;
52
+ lastSequence: z.ZodNumber;
53
+ schemaVersion: z.ZodLiteral<"ushman-ledger-read-index/v1">;
54
+ }, z.core.$strip>;
55
+ export type LedgerReadIndex = z.infer<typeof LedgerReadIndexSchema>;
56
+ export type ReadIndexEntry = z.infer<typeof ReadIndexEntrySchema>;
57
+ export type ManifestEntryLocation = readonly [string, {
58
+ phase: LedgerPhase;
59
+ sequence: number;
60
+ }];
61
+ export declare const buildReadIndexFromManifest: (workspaceRoot: string, manifest: LedgerManifest) => Promise<{
62
+ coveredFiles: string[];
63
+ entries: {
64
+ id: string;
65
+ kind: "tool-invocation" | "agent-patch" | "operator-patch" | "stage-transition" | "operator-decision" | "validator-result" | "runtime-event" | "note" | "correction" | "strip-decision-reverted" | "descope-brief" | "merge-return" | "merge-return-rejected" | "revert" | "rollback" | "rework.test_retired";
66
+ ts: string;
67
+ }[];
68
+ entryCount: number;
69
+ lastEntryId: string | null;
70
+ lastSequence: number;
71
+ schemaVersion: "ushman-ledger-read-index/v1";
72
+ }>;
73
+ export declare const isReadIndexCurrent: (index: LedgerReadIndex, manifest: LedgerManifest) => boolean;
74
+ export declare const readReadIndex: (workspaceRoot: string) => Promise<LedgerReadIndex | null>;
75
+ export declare const saveReadIndex: (workspaceRoot: string, readIndex: LedgerReadIndex) => Promise<void>;
76
+ export declare const ensureReadIndexUnderLock: (workspaceRoot: string, manifest: LedgerManifest) => Promise<{
77
+ coveredFiles: string[];
78
+ entries: {
79
+ id: string;
80
+ kind: "tool-invocation" | "agent-patch" | "operator-patch" | "stage-transition" | "operator-decision" | "validator-result" | "runtime-event" | "note" | "correction" | "strip-decision-reverted" | "descope-brief" | "merge-return" | "merge-return-rejected" | "revert" | "rollback" | "rework.test_retired";
81
+ ts: string;
82
+ }[];
83
+ entryCount: number;
84
+ lastEntryId: string | null;
85
+ lastSequence: number;
86
+ schemaVersion: "ushman-ledger-read-index/v1";
87
+ }>;
88
+ export declare const appendEntryToReadIndex: ({ entry, readIndex, sequence, }: {
89
+ readonly entry: LedgerEntry;
90
+ readonly readIndex: LedgerReadIndex;
91
+ readonly sequence: number;
92
+ }) => {
93
+ coveredFiles: string[];
94
+ entries: {
95
+ id: string;
96
+ kind: "tool-invocation" | "agent-patch" | "operator-patch" | "stage-transition" | "operator-decision" | "validator-result" | "runtime-event" | "note" | "correction" | "strip-decision-reverted" | "descope-brief" | "merge-return" | "merge-return-rejected" | "revert" | "rollback" | "rework.test_retired";
97
+ ts: string;
98
+ }[];
99
+ entryCount: number;
100
+ lastEntryId: string | null;
101
+ lastSequence: number;
102
+ schemaVersion: "ushman-ledger-read-index/v1";
103
+ };
104
+ export declare const matchesReadIndexFilter: ({ filter, manifest, readIndexEntry, }: {
105
+ readonly filter: {
106
+ readonly kind?: LedgerKind;
107
+ readonly phase?: LedgerPhase;
108
+ readonly since?: string;
109
+ };
110
+ readonly manifest: LedgerManifest;
111
+ readonly readIndexEntry: ReadIndexEntry;
112
+ }) => boolean;
113
+ export {};
114
+ //# sourceMappingURL=read-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-index.d.ts","sourceRoot":"","sources":["../src/read-index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAgB,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAoB,MAAM,mBAAmB,CAAC;AACxH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAO3D,QAAA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;iBAIxB,CAAC;AAEH,QAAA,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyBrB,CAAC;AAEP,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACpE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,SAAS,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AA0HhG,eAAO,MAAM,0BAA0B,GAAU,eAAe,MAAM,EAAE,UAAU,cAAc;;;;;;;;;;;EAuB/F,CAAC;AAYF,eAAO,MAAM,kBAAkB,GAAI,OAAO,eAAe,EAAE,UAAU,cAAc,YAG1C,CAAC;AAE1C,eAAO,MAAM,aAAa,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAWzF,CAAC;AAEF,eAAO,MAAM,aAAa,GAAU,eAAe,MAAM,EAAE,WAAW,eAAe,kBAGpF,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAU,eAAe,MAAM,EAAE,UAAU,cAAc;;;;;;;;;;;EAiB7F,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAI,iCAIpC;IACC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC7B;;;;;;;;;;;CAYA,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAI,uCAIpC;IACC,QAAQ,CAAC,MAAM,EAAE;QACb,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;QAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;QAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC;IACF,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;CAC3C,YAeA,CAAC"}
@@ -0,0 +1,210 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { z } from 'zod';
3
+ import { mapWithConcurrencyLimit } from "./async.js";
4
+ import { stableStringify } from "./json.js";
5
+ import { LEDGER_KINDS, parseLedgerEntry } from "./schema/entry.js";
6
+ import { readPhaseEntryText, resolveLedgerPaths, writeAtomicTextFile } from "./storage/filesystem.js";
7
+ const READ_INDEX_SCHEMA_VERSION = 'ushman-ledger-read-index/v1';
8
+ const ENTRY_READ_BATCH_SIZE = 32;
9
+ const ENTRY_READ_CONCURRENCY = 16;
10
+ const ReadIndexEntrySchema = z.object({
11
+ id: z.string().min(1),
12
+ kind: z.enum(LEDGER_KINDS),
13
+ ts: z.string().datetime({ offset: true }),
14
+ });
15
+ const LedgerReadIndexSchema = z
16
+ .object({
17
+ coveredFiles: z.array(z.string()).default([]),
18
+ entries: z.array(ReadIndexEntrySchema).default([]),
19
+ entryCount: z.number().int().nonnegative(),
20
+ lastEntryId: z.string().min(1).nullable(),
21
+ lastSequence: z.number().int().nonnegative(),
22
+ schemaVersion: z.literal(READ_INDEX_SCHEMA_VERSION),
23
+ })
24
+ .superRefine((value, ctx) => {
25
+ if (value.entryCount !== value.entries.length) {
26
+ ctx.addIssue({
27
+ code: 'custom',
28
+ message: 'entryCount must match entries.length',
29
+ path: ['entryCount'],
30
+ });
31
+ }
32
+ const expectedLastEntryId = value.entries.at(-1)?.id ?? null;
33
+ if (value.lastEntryId !== expectedLastEntryId) {
34
+ ctx.addIssue({
35
+ code: 'custom',
36
+ message: 'lastEntryId must match the final indexed entry',
37
+ path: ['lastEntryId'],
38
+ });
39
+ }
40
+ });
41
+ const parseReadIndexText = (filePath, text) => {
42
+ try {
43
+ return LedgerReadIndexSchema.parse(JSON.parse(text));
44
+ }
45
+ catch (error) {
46
+ throw new Error(`Invalid ledger read index at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
47
+ }
48
+ };
49
+ const buildReadIndexEntry = (entry) => ({
50
+ id: entry.id,
51
+ kind: entry.kind,
52
+ ts: entry.ts,
53
+ });
54
+ const sortBySequence = (left, right) => left[1].sequence - right[1].sequence;
55
+ const getManifestSequenceLocations = (manifest) => Object.entries(manifest.entryLocations).sort(sortBySequence);
56
+ const mergeSortedUniquePaths = (existingPaths, additionalPaths) => {
57
+ if (additionalPaths.length === 0) {
58
+ return [...existingPaths];
59
+ }
60
+ const normalizedAdditionalPaths = [...new Set(additionalPaths)].sort((left, right) => left.localeCompare(right));
61
+ const mergedPaths = [];
62
+ let existingIndex = 0;
63
+ let additionalIndex = 0;
64
+ while (existingIndex < existingPaths.length || additionalIndex < normalizedAdditionalPaths.length) {
65
+ const existingPath = existingPaths[existingIndex];
66
+ const additionalPath = normalizedAdditionalPaths[additionalIndex];
67
+ if (existingPath === undefined) {
68
+ mergedPaths.push(...normalizedAdditionalPaths.slice(additionalIndex));
69
+ break;
70
+ }
71
+ if (additionalPath === undefined) {
72
+ mergedPaths.push(...existingPaths.slice(existingIndex));
73
+ break;
74
+ }
75
+ const comparison = existingPath.localeCompare(additionalPath);
76
+ if (comparison < 0) {
77
+ mergedPaths.push(existingPath);
78
+ existingIndex += 1;
79
+ continue;
80
+ }
81
+ if (comparison > 0) {
82
+ mergedPaths.push(additionalPath);
83
+ additionalIndex += 1;
84
+ continue;
85
+ }
86
+ mergedPaths.push(existingPath);
87
+ existingIndex += 1;
88
+ additionalIndex += 1;
89
+ }
90
+ return mergedPaths;
91
+ };
92
+ const buildReadIndex = ({ coveredFiles, entries, lastSequence, }) => LedgerReadIndexSchema.parse({
93
+ coveredFiles,
94
+ entries,
95
+ entryCount: entries.length,
96
+ lastEntryId: entries.at(-1)?.id ?? null,
97
+ lastSequence,
98
+ schemaVersion: READ_INDEX_SCHEMA_VERSION,
99
+ });
100
+ const readIndexedEntry = async ({ entryId, phase, workspaceRoot, }) => {
101
+ const text = await readPhaseEntryText(workspaceRoot, phase, `${entryId}.json`);
102
+ return parseLedgerEntry(JSON.parse(text));
103
+ };
104
+ const readIndexedEntryBatch = async ({ entryLocations, workspaceRoot, }) => mapWithConcurrencyLimit(entryLocations, ENTRY_READ_CONCURRENCY, async ([entryId, location]) => readIndexedEntry({
105
+ entryId,
106
+ phase: location.phase,
107
+ workspaceRoot,
108
+ }));
109
+ const collectCoveredFiles = (entry, coveredFiles) => {
110
+ if (entry.kind !== 'agent-patch' && entry.kind !== 'operator-patch') {
111
+ return;
112
+ }
113
+ for (const affectedFile of entry.links.affectedFiles ?? []) {
114
+ coveredFiles.add(affectedFile);
115
+ }
116
+ };
117
+ export const buildReadIndexFromManifest = async (workspaceRoot, manifest) => {
118
+ const orderedEntryLocations = getManifestSequenceLocations(manifest);
119
+ const entries = [];
120
+ const coveredFiles = new Set();
121
+ for (let index = 0; index < orderedEntryLocations.length; index += ENTRY_READ_BATCH_SIZE) {
122
+ const batch = orderedEntryLocations.slice(index, index + ENTRY_READ_BATCH_SIZE);
123
+ const resolvedEntries = await readIndexedEntryBatch({
124
+ entryLocations: batch,
125
+ workspaceRoot,
126
+ });
127
+ for (const entry of resolvedEntries) {
128
+ entries.push(buildReadIndexEntry(entry));
129
+ collectCoveredFiles(entry, coveredFiles);
130
+ }
131
+ }
132
+ return buildReadIndex({
133
+ coveredFiles: [...coveredFiles].sort((left, right) => left.localeCompare(right)),
134
+ entries,
135
+ lastSequence: manifest.lastSequence,
136
+ });
137
+ };
138
+ const hasMatchingTailEntry = (index, manifest) => {
139
+ if (manifest.lastSequence === 0) {
140
+ return index.lastEntryId === null;
141
+ }
142
+ if (!index.lastEntryId) {
143
+ return false;
144
+ }
145
+ return manifest.entryLocations[index.lastEntryId]?.sequence === manifest.lastSequence;
146
+ };
147
+ export const isReadIndexCurrent = (index, manifest) => index.entryCount === manifest.entryCount &&
148
+ index.lastSequence === manifest.lastSequence &&
149
+ hasMatchingTailEntry(index, manifest);
150
+ export const readReadIndex = async (workspaceRoot) => {
151
+ const filePath = resolveLedgerPaths(workspaceRoot).readIndexFile;
152
+ try {
153
+ const text = await readFile(filePath, 'utf8');
154
+ return parseReadIndexText(filePath, text);
155
+ }
156
+ catch (error) {
157
+ if (error.code === 'ENOENT') {
158
+ return null;
159
+ }
160
+ throw error;
161
+ }
162
+ };
163
+ export const saveReadIndex = async (workspaceRoot, readIndex) => {
164
+ const filePath = resolveLedgerPaths(workspaceRoot).readIndexFile;
165
+ await writeAtomicTextFile(filePath, `${stableStringify(LedgerReadIndexSchema.parse(readIndex), true)}\n`);
166
+ };
167
+ export const ensureReadIndexUnderLock = async (workspaceRoot, manifest) => {
168
+ let currentReadIndex = null;
169
+ try {
170
+ currentReadIndex = await readReadIndex(workspaceRoot);
171
+ }
172
+ catch (error) {
173
+ if (!(error instanceof Error) || !error.message.startsWith('Invalid ledger read index')) {
174
+ throw error;
175
+ }
176
+ }
177
+ if (currentReadIndex && isReadIndexCurrent(currentReadIndex, manifest)) {
178
+ return currentReadIndex;
179
+ }
180
+ const rebuiltReadIndex = await buildReadIndexFromManifest(workspaceRoot, manifest);
181
+ await saveReadIndex(workspaceRoot, rebuiltReadIndex);
182
+ return rebuiltReadIndex;
183
+ };
184
+ export const appendEntryToReadIndex = ({ entry, readIndex, sequence, }) => {
185
+ const nextEntries = [...readIndex.entries, buildReadIndexEntry(entry)];
186
+ const nextCoveredFiles = entry.kind === 'agent-patch' || entry.kind === 'operator-patch'
187
+ ? mergeSortedUniquePaths(readIndex.coveredFiles, entry.links.affectedFiles ?? [])
188
+ : [...readIndex.coveredFiles];
189
+ return buildReadIndex({
190
+ coveredFiles: nextCoveredFiles,
191
+ entries: nextEntries,
192
+ lastSequence: sequence,
193
+ });
194
+ };
195
+ export const matchesReadIndexFilter = ({ filter, manifest, readIndexEntry, }) => {
196
+ const manifestLocation = manifest.entryLocations[readIndexEntry.id];
197
+ if (!manifestLocation) {
198
+ return false;
199
+ }
200
+ if (filter.phase && manifestLocation.phase !== filter.phase) {
201
+ return false;
202
+ }
203
+ if (filter.kind && readIndexEntry.kind !== filter.kind) {
204
+ return false;
205
+ }
206
+ if (filter.since && readIndexEntry.ts < filter.since) {
207
+ return false;
208
+ }
209
+ return true;
210
+ };
@@ -0,0 +1,25 @@
1
+ import { type LedgerEntry } from './schema/entry.ts';
2
+ import type { LedgerManifest } from './schema/manifest.ts';
3
+ type AppendRecordTestHooks = {
4
+ readonly afterEntryWrite?: (context: {
5
+ entry: LedgerEntry;
6
+ }) => Promise<void>;
7
+ readonly afterManifestSave?: (context: {
8
+ entry: LedgerEntry;
9
+ }) => Promise<void>;
10
+ readonly afterPendingCommitWrite?: (context: {
11
+ entry: LedgerEntry;
12
+ pendingCommitPath: string;
13
+ }) => Promise<void>;
14
+ readonly afterReadIndexSave?: (context: {
15
+ entry: LedgerEntry;
16
+ }) => Promise<void>;
17
+ };
18
+ export declare const setAppendRecordTestHooks: (workspaceRoot: string, hooks: AppendRecordTestHooks | null) => void;
19
+ export declare const appendRecord: (workspaceRoot: string, input: unknown) => Promise<{
20
+ entry: LedgerEntry;
21
+ id: string;
22
+ }>;
23
+ export declare const readEntryById: (workspaceRoot: string, manifest: LedgerManifest, entryId: string) => Promise<LedgerEntry>;
24
+ export {};
25
+ //# sourceMappingURL=record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAQA,OAAO,EAEH,KAAK,WAAW,EAQnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAW3D,KAAK,qBAAqB,GAAG;IACzB,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChF,QAAQ,CAAC,uBAAuB,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpF,CAAC;AAuBF,eAAO,MAAM,wBAAwB,GAAI,eAAe,MAAM,EAAE,OAAO,qBAAqB,GAAG,IAAI,SAOlG,CAAC;AAgNF,eAAO,MAAM,YAAY,GACrB,eAAe,MAAM,EACrB,OAAO,OAAO,KACf,OAAO,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAqF5C,CAAC;AAEF,eAAO,MAAM,aAAa,GACtB,eAAe,MAAM,EACrB,UAAU,cAAc,EACxB,SAAS,MAAM,KAChB,OAAO,CAAC,WAAW,CAUrB,CAAC"}
package/dist/record.js ADDED
@@ -0,0 +1,268 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { resolveBlobPath, storePatchBlob } from "./blobs.js";
4
+ import { sha256Hex, stableStringify } from "./json.js";
5
+ import { updateManifestForEntry } from "./manifest-update.js";
6
+ import { derivePatchPayloadFromDiffText } from "./patch-metadata.js";
7
+ import { appendEntryToReadIndex, saveReadIndex } from "./read-index.js";
8
+ import { reconcileLedgerStateUnderLock, removePendingCommit, writePendingCommit } from "./recovery.js";
9
+ import { LedgerEntrySchema, OperatorDecisionPayloadSchema, PatchPayloadWriteSchema, parseLedgerEntry, parseLedgerRecord, ValidatorResultPayloadSchema, } from "./schema/entry.js";
10
+ import { ensureLedgerDirectories, readManifest, resolveLedgerPaths, saveManifest, writeEntryFile, } from "./storage/filesystem.js";
11
+ import { acquireLock } from "./storage/lock.js";
12
+ import { createDeterministicUuidV7 } from "./uuid.js";
13
+ const appendRecordTestHooks = new Map();
14
+ const resolveWorkspaceKey = (workspaceRoot) => path.resolve(workspaceRoot);
15
+ const getAppendRecordTestHooks = (workspaceRoot) => appendRecordTestHooks.get(resolveWorkspaceKey(workspaceRoot));
16
+ const warnedDeprecations = new Set();
17
+ const warnOnce = (key, message) => {
18
+ if (warnedDeprecations.has(key)) {
19
+ return;
20
+ }
21
+ warnedDeprecations.add(key);
22
+ process.emitWarning(message, {
23
+ code: key,
24
+ type: 'DeprecationWarning',
25
+ });
26
+ };
27
+ export const setAppendRecordTestHooks = (workspaceRoot, hooks) => {
28
+ const key = resolveWorkspaceKey(workspaceRoot);
29
+ if (!hooks) {
30
+ appendRecordTestHooks.delete(key);
31
+ return;
32
+ }
33
+ appendRecordTestHooks.set(key, hooks);
34
+ };
35
+ const readBlobText = async (workspaceRoot, diff) => readFile(resolveBlobPath(workspaceRoot, diff.blobSha256), 'utf8');
36
+ const addIdempotencyMetadata = (record) => {
37
+ if (!record.idempotencyKey) {
38
+ return record;
39
+ }
40
+ return {
41
+ ...record,
42
+ links: {
43
+ ...record.links,
44
+ idempotencyKey: record.idempotencyKey,
45
+ },
46
+ };
47
+ };
48
+ const buildLogicalHash = (record) => {
49
+ if (record.idempotencyKey) {
50
+ return `key:${record.idempotencyKey}`;
51
+ }
52
+ return sha256Hex(stableStringify(record));
53
+ };
54
+ const fingerprintManifest = (manifest) => sha256Hex(stableStringify(manifest));
55
+ const assertManifestFingerprint = async ({ expectedFingerprint, manifestPath, workspaceRoot, }) => {
56
+ const currentManifest = await readManifest(workspaceRoot);
57
+ const currentFingerprint = fingerprintManifest(currentManifest);
58
+ if (currentFingerprint !== expectedFingerprint) {
59
+ throw new Error(`Ledger manifest changed while holding ${manifestPath}; aborting append so recovery can replay safely.`);
60
+ }
61
+ };
62
+ const toEntryIdTimestamp = (timestamp) => timestamp.replaceAll(':', '-').replaceAll('.', '-');
63
+ const buildEntryId = (entryWithoutId, sequence) => {
64
+ const bodyHash = sha256Hex(stableStringify(entryWithoutId));
65
+ return `${toEntryIdTimestamp(entryWithoutId.ts)}-${sequence.toString().padStart(8, '0')}-${bodyHash.slice(0, 12)}`;
66
+ };
67
+ const normalizeValidatorResultRecord = (record) => {
68
+ if (record.kind !== 'validator-result') {
69
+ return record;
70
+ }
71
+ const payloadId = record.payload?.id;
72
+ if (payloadId) {
73
+ return {
74
+ ...record,
75
+ payload: ValidatorResultPayloadSchema.parse(record.payload),
76
+ };
77
+ }
78
+ warnOnce('USHMAN_LEDGER_VALIDATOR_ID_COMPAT', 'validator-result records should provide payload.id. A compatibility UUIDv7 was synthesized for this append.');
79
+ return {
80
+ ...record,
81
+ payload: {
82
+ id: createDeterministicUuidV7({
83
+ seed: stableStringify(record),
84
+ timestamp: 0,
85
+ }),
86
+ },
87
+ };
88
+ };
89
+ const resolvePatchTextAndBlob = async ({ record, workspaceRoot, }) => {
90
+ if (record.diffPath) {
91
+ const patchText = await readFile(path.resolve(record.diffPath), 'utf8');
92
+ return {
93
+ patchText,
94
+ storedDiff: await storePatchBlob(workspaceRoot, patchText),
95
+ };
96
+ }
97
+ if (record.payload?.diff) {
98
+ const storedDiff = await storePatchBlob(workspaceRoot, record.payload.diff);
99
+ if (record.diff && record.diff.blobSha256 !== storedDiff.blobSha256) {
100
+ throw new Error(`Provided diff blob ${record.diff.blobSha256} does not match payload.diff hash ${storedDiff.blobSha256}.`);
101
+ }
102
+ return {
103
+ patchText: record.payload.diff,
104
+ storedDiff,
105
+ };
106
+ }
107
+ if (!record.diff) {
108
+ throw new Error(`${record.kind} records require payload.diff, diffPath, or diff metadata.`);
109
+ }
110
+ try {
111
+ const patchText = await readBlobText(workspaceRoot, record.diff);
112
+ return {
113
+ patchText,
114
+ storedDiff: record.diff,
115
+ };
116
+ }
117
+ catch (error) {
118
+ if (error.code === 'ENOENT') {
119
+ throw new Error(`Patch blob ${record.diff.blobSha256} was not found. Store it first or use diffPath.`);
120
+ }
121
+ throw error;
122
+ }
123
+ };
124
+ const resolvePatchPayload = async ({ patchText, record, workspaceRoot, }) => {
125
+ const derivedPayload = await derivePatchPayloadFromDiffText({
126
+ patchText,
127
+ workspaceRoot,
128
+ });
129
+ const mergedPayload = PatchPayloadWriteSchema.parse({
130
+ ...derivedPayload,
131
+ ...record.payload,
132
+ diff: record.payload?.diff ?? patchText,
133
+ diffSha256: record.payload?.diffSha256 ?? derivedPayload.diffSha256,
134
+ fileSha256After: record.payload?.fileSha256After ?? derivedPayload.fileSha256After,
135
+ fileSha256Before: record.payload?.fileSha256Before ?? derivedPayload.fileSha256Before,
136
+ hunks: record.payload?.hunks ?? derivedPayload.hunks,
137
+ touchedPaths: record.payload?.touchedPaths ?? derivedPayload.touchedPaths,
138
+ });
139
+ return mergedPayload;
140
+ };
141
+ const resolvePatchRecord = async ({ record, workspaceRoot, }) => {
142
+ const { patchText, storedDiff } = await resolvePatchTextAndBlob({
143
+ record,
144
+ workspaceRoot,
145
+ });
146
+ const payload = await resolvePatchPayload({
147
+ patchText,
148
+ record,
149
+ workspaceRoot,
150
+ });
151
+ if (payload.diffSha256 !== storedDiff.blobSha256) {
152
+ throw new Error(`Patch payload hash ${payload.diffSha256} does not match stored blob hash ${storedDiff.blobSha256}.`);
153
+ }
154
+ return {
155
+ ...record,
156
+ diff: storedDiff,
157
+ links: {
158
+ ...record.links,
159
+ affectedFiles: [...new Set([...(record.links?.affectedFiles ?? []), ...payload.touchedPaths])],
160
+ blobs: [...new Set([...(record.links?.blobs ?? []), storedDiff.blobSha256])],
161
+ },
162
+ payload,
163
+ };
164
+ };
165
+ const normalizeRecord = async ({ record, workspaceRoot, }) => {
166
+ const normalizedRecord = normalizeValidatorResultRecord(record);
167
+ if (normalizedRecord.kind === 'agent-patch' || normalizedRecord.kind === 'operator-patch') {
168
+ return resolvePatchRecord({
169
+ record: normalizedRecord,
170
+ workspaceRoot,
171
+ });
172
+ }
173
+ return normalizedRecord.kind === 'operator-decision'
174
+ ? {
175
+ ...normalizedRecord,
176
+ payload: OperatorDecisionPayloadSchema.parse(normalizedRecord.payload),
177
+ }
178
+ : normalizedRecord;
179
+ };
180
+ export const appendRecord = async (workspaceRoot, input) => {
181
+ const parsed = parseLedgerRecord(input);
182
+ const recordWithMetadata = addIdempotencyMetadata(parsed);
183
+ const normalizedRecord = await normalizeRecord({
184
+ record: recordWithMetadata,
185
+ workspaceRoot,
186
+ });
187
+ const paths = await ensureLedgerDirectories(workspaceRoot);
188
+ const lock = await acquireLock(paths.manifestLockFile);
189
+ try {
190
+ const { manifest, readIndex } = await reconcileLedgerStateUnderLock(workspaceRoot);
191
+ const manifestFingerprint = fingerprintManifest(manifest);
192
+ const keyedLogicalHash = parsed.idempotencyKey ? buildLogicalHash(recordWithMetadata) : null;
193
+ if (keyedLogicalHash) {
194
+ const existingId = manifest.idempotencyIndex[parsed.phase]?.[keyedLogicalHash];
195
+ if (existingId) {
196
+ return {
197
+ entry: await readEntryById(workspaceRoot, manifest, existingId),
198
+ id: existingId,
199
+ };
200
+ }
201
+ }
202
+ await lock.assertOwnership();
203
+ await assertManifestFingerprint({
204
+ expectedFingerprint: manifestFingerprint,
205
+ manifestPath: paths.manifestLockFile,
206
+ workspaceRoot,
207
+ });
208
+ const logicalHash = keyedLogicalHash ?? buildLogicalHash(normalizedRecord);
209
+ const existingId = manifest.idempotencyIndex[normalizedRecord.phase]?.[logicalHash];
210
+ if (existingId) {
211
+ return {
212
+ entry: await readEntryById(workspaceRoot, manifest, existingId),
213
+ id: existingId,
214
+ };
215
+ }
216
+ const nextSequence = manifest.lastSequence + 1;
217
+ const { idempotencyKey: _idempotencyKey, ...recordWithoutIdempotencyKey } = normalizedRecord;
218
+ const entryWithoutId = {
219
+ ...recordWithoutIdempotencyKey,
220
+ links: recordWithoutIdempotencyKey.links ?? {},
221
+ prevEntryId: manifest.perPhaseLatest[normalizedRecord.phase] ?? null,
222
+ schemaVersion: 'ushman-ledger-entry/v1',
223
+ ts: new Date().toISOString(),
224
+ };
225
+ const entry = LedgerEntrySchema.parse({
226
+ ...entryWithoutId,
227
+ id: buildEntryId(entryWithoutId, nextSequence),
228
+ });
229
+ const testHooks = getAppendRecordTestHooks(workspaceRoot);
230
+ const pendingCommitPath = await writePendingCommit({
231
+ entry,
232
+ logicalHash,
233
+ manifest,
234
+ workspaceRoot,
235
+ });
236
+ await testHooks?.afterPendingCommitWrite?.({ entry, pendingCommitPath });
237
+ await writeEntryFile(workspaceRoot, entry.phase, `${entry.id}.json`, entry);
238
+ await testHooks?.afterEntryWrite?.({ entry });
239
+ await lock.assertOwnership();
240
+ await assertManifestFingerprint({
241
+ expectedFingerprint: manifestFingerprint,
242
+ manifestPath: paths.manifestLockFile,
243
+ workspaceRoot,
244
+ });
245
+ await saveManifest(workspaceRoot, updateManifestForEntry({ entry, logicalHash, manifest }));
246
+ await testHooks?.afterManifestSave?.({ entry });
247
+ await lock.assertOwnership();
248
+ await saveReadIndex(workspaceRoot, appendEntryToReadIndex({
249
+ entry,
250
+ readIndex,
251
+ sequence: nextSequence,
252
+ }));
253
+ await testHooks?.afterReadIndexSave?.({ entry });
254
+ await removePendingCommit(pendingCommitPath);
255
+ return { entry, id: entry.id };
256
+ }
257
+ finally {
258
+ await lock.release();
259
+ }
260
+ };
261
+ export const readEntryById = async (workspaceRoot, manifest, entryId) => {
262
+ const location = manifest.entryLocations[entryId];
263
+ if (!location) {
264
+ throw new Error(`Ledger entry not found: ${entryId}`);
265
+ }
266
+ const text = await readFile(path.join(resolveLedgerPaths(workspaceRoot).phaseDir(location.phase), `${entryId}.json`), 'utf8');
267
+ return parseLedgerEntry(JSON.parse(text));
268
+ };
@@ -0,0 +1,39 @@
1
+ import { type LedgerReadIndex } from './read-index.ts';
2
+ import { type LedgerEntry } from './schema/entry.ts';
3
+ import type { LedgerManifest } from './schema/manifest.ts';
4
+ export type PreparedLedgerState = {
5
+ readonly manifest: LedgerManifest;
6
+ readonly readIndex: LedgerReadIndex;
7
+ };
8
+ export declare const writePendingCommit: ({ entry, logicalHash, manifest, workspaceRoot, }: {
9
+ readonly entry: LedgerEntry;
10
+ readonly logicalHash: string;
11
+ readonly manifest: LedgerManifest;
12
+ readonly workspaceRoot: string;
13
+ }) => Promise<string>;
14
+ export declare const removePendingCommit: (filePath: string) => Promise<void>;
15
+ export declare const reconcilePendingCommitsUnderLock: (workspaceRoot: string) => Promise<{
16
+ [x: string]: unknown;
17
+ archives: {
18
+ createdAt: string;
19
+ integrityHash: string;
20
+ outPath: string;
21
+ }[];
22
+ createdAt: string;
23
+ entryCount: number;
24
+ entryLocations: Record<string, {
25
+ phase: "seed" | "capture" | "intake" | "vendor-extract" | "cleanup" | "parity" | "characterize" | "equiv" | "analyze" | "recover" | "ship" | "migration";
26
+ sequence: number;
27
+ }>;
28
+ idempotencyIndex: Record<string, Record<string, string>>;
29
+ lastSequence: number;
30
+ perPhaseCounts: Record<string, number>;
31
+ perPhaseLatest: Record<string, string>;
32
+ schemaVersion: "ushman-ledger-manifest/v1";
33
+ updatedAt: string;
34
+ workspaceId: string;
35
+ }>;
36
+ export declare const reconcileLedgerStateUnderLock: (workspaceRoot: string) => Promise<PreparedLedgerState>;
37
+ export declare const loadLedgerState: (workspaceRoot: string) => Promise<PreparedLedgerState>;
38
+ export declare const prepareLedgerState: (workspaceRoot: string) => Promise<void>;
39
+ //# sourceMappingURL=recovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../src/recovery.ts"],"names":[],"mappings":"AAKA,OAAO,EAA4B,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACjF,OAAO,EAAE,KAAK,WAAW,EAAqB,MAAM,mBAAmB,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AA0B3D,MAAM,MAAM,mBAAmB,GAAG;IAC9B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;CACvC,CAAC;AA0JF,eAAO,MAAM,kBAAkB,GAAU,kDAKtC;IACC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC,oBAeA,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAU,UAAU,MAAM,kBAEzD,CAAC;AAEF,eAAO,MAAM,gCAAgC,GAAU,eAAe,MAAM;;;;;;;;;;;;;;;;;;;;EAyB3E,CAAC;AAEF,eAAO,MAAM,6BAA6B,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,mBAAmB,CAQtG,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,mBAAmB,CAQxF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAU,eAAe,MAAM,kBAE7D,CAAC"}