ushman-ledger 1.2.1 → 1.2.2

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.
@@ -1,28 +1,94 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import { readFile, stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import * as v from 'valibot';
4
- import { resolveBlobPath, storePatchBlob } from "./blobs.js";
4
+ import { assertPatchTextWithinLimit, readPatchTextFromFile, resolveBlobPath, storePatchBlob, } from "./blobs.js";
5
5
  import { WorkspaceRelativePathSchema, } from "./schema/entry.js";
6
+ import { forEachLine } from "./text-lines.js";
6
7
  const readBlobText = async (workspaceRoot, blobSha256) => readFile(resolveBlobPath(workspaceRoot, blobSha256), 'utf8');
7
8
  const formatRecordContext = (record) => `${record.kind} "${record.summary}" [${record.phase}]`;
8
9
  const normalizeWorkspaceRelativePath = (value) => v.parse(WorkspaceRelativePathSchema, value.replaceAll('\\', '/'));
9
- const stripGitPathPrefix = (value) => {
10
+ const normalizeDiffPathToken = ({ headerPrefixes, side, value, }) => {
10
11
  const normalized = value.trim().replace(/^"([^"]*)"$/u, '$1');
11
12
  if (normalized === '/dev/null') {
12
13
  return null;
13
14
  }
14
- if (normalized.startsWith('a/') || normalized.startsWith('b/')) {
15
- return normalized.slice(2);
15
+ const expectedPrefix = side === 'old' ? headerPrefixes?.oldPrefix : headerPrefixes?.newPrefix;
16
+ if (expectedPrefix && normalized.startsWith(expectedPrefix)) {
17
+ return normalized.slice(expectedPrefix.length);
16
18
  }
17
19
  return normalized;
18
20
  };
19
- const inferHeaderPath = (line) => {
20
- const match = /^diff --git (?:"a\/([^"]+)"|a\/(\S+)) (?:"b\/([^"]+)"|b\/(\S+))$/u.exec(line.trim());
21
- if (!match) {
21
+ const splitPrefixCandidate = (value) => {
22
+ const slashIndex = value.indexOf('/');
23
+ if (slashIndex <= 0) {
22
24
  return null;
23
25
  }
24
- const rightPath = match[3] ?? match[4];
25
- return rightPath ? stripGitPathPrefix(`b/${rightPath}`) : null;
26
+ return {
27
+ prefix: value.slice(0, slashIndex + 1),
28
+ suffix: value.slice(slashIndex + 1),
29
+ };
30
+ };
31
+ const readDiffHeaderToken = (headerBody, startIndex) => {
32
+ let index = startIndex;
33
+ while (headerBody[index] === ' ') {
34
+ index += 1;
35
+ }
36
+ if (index >= headerBody.length) {
37
+ return null;
38
+ }
39
+ if (headerBody[index] === '"') {
40
+ const endIndex = headerBody.indexOf('"', index + 1);
41
+ if (endIndex === -1) {
42
+ return null;
43
+ }
44
+ return {
45
+ nextIndex: endIndex + 1,
46
+ token: headerBody.slice(index + 1, endIndex),
47
+ };
48
+ }
49
+ let endIndex = index;
50
+ while (endIndex < headerBody.length && headerBody[endIndex] !== ' ') {
51
+ endIndex += 1;
52
+ }
53
+ return {
54
+ nextIndex: endIndex + 1,
55
+ token: headerBody.slice(index, endIndex),
56
+ };
57
+ };
58
+ const resolveHeaderPrefixes = (oldPath, newPath) => {
59
+ if (oldPath.startsWith('a/') && newPath.startsWith('b/')) {
60
+ return {
61
+ newPrefix: 'b/',
62
+ oldPrefix: 'a/',
63
+ };
64
+ }
65
+ const oldCandidate = splitPrefixCandidate(oldPath);
66
+ const newCandidate = splitPrefixCandidate(newPath);
67
+ if (!oldCandidate ||
68
+ !newCandidate ||
69
+ oldCandidate.prefix === newCandidate.prefix ||
70
+ oldCandidate.suffix !== newCandidate.suffix) {
71
+ return null;
72
+ }
73
+ return {
74
+ newPrefix: newCandidate.prefix,
75
+ oldPrefix: oldCandidate.prefix,
76
+ };
77
+ };
78
+ const parseDiffHeaderPaths = (line) => {
79
+ const headerBody = line.trim().slice('diff --git '.length);
80
+ const firstToken = readDiffHeaderToken(headerBody, 0);
81
+ const secondToken = firstToken ? readDiffHeaderToken(headerBody, firstToken.nextIndex) : null;
82
+ if (!firstToken?.token || !secondToken?.token) {
83
+ return null;
84
+ }
85
+ const oldPath = firstToken.token;
86
+ const newPath = secondToken.token;
87
+ return {
88
+ headerPrefixes: resolveHeaderPrefixes(oldPath, newPath),
89
+ newPath,
90
+ oldPath,
91
+ };
26
92
  };
27
93
  const finalizeFileChange = (current, filesChanged) => {
28
94
  if (!current) {
@@ -38,19 +104,33 @@ const finalizeFileChange = (current, filesChanged) => {
38
104
  removed: current.removed > 0 ? current.removed : undefined,
39
105
  });
40
106
  };
41
- const createDiffFileAccumulator = (line) => ({
42
- added: 0,
43
- headerPath: inferHeaderPath(line),
44
- minusPath: null,
45
- plusPath: null,
46
- removed: 0,
47
- });
107
+ const createDiffFileAccumulator = (line) => {
108
+ const headerPaths = parseDiffHeaderPaths(line);
109
+ return {
110
+ added: 0,
111
+ headerPath: headerPaths
112
+ ? normalizeDiffPathToken({
113
+ headerPrefixes: headerPaths.headerPrefixes,
114
+ side: 'new',
115
+ value: headerPaths.newPath,
116
+ })
117
+ : null,
118
+ headerPrefixes: headerPaths?.headerPrefixes ?? null,
119
+ minusPath: null,
120
+ plusPath: null,
121
+ removed: 0,
122
+ };
123
+ };
48
124
  const applyPatchMetadataLine = (current, line) => {
49
125
  if (line.startsWith('+++ ')) {
50
126
  return {
51
127
  next: {
52
128
  ...current,
53
- plusPath: stripGitPathPrefix(line.slice(4)),
129
+ plusPath: normalizeDiffPathToken({
130
+ headerPrefixes: current.headerPrefixes,
131
+ side: 'new',
132
+ value: line.slice(4),
133
+ }),
54
134
  },
55
135
  };
56
136
  }
@@ -58,7 +138,35 @@ const applyPatchMetadataLine = (current, line) => {
58
138
  return {
59
139
  next: {
60
140
  ...current,
61
- minusPath: stripGitPathPrefix(line.slice(4)),
141
+ minusPath: normalizeDiffPathToken({
142
+ headerPrefixes: current.headerPrefixes,
143
+ side: 'old',
144
+ value: line.slice(4),
145
+ }),
146
+ },
147
+ };
148
+ }
149
+ if (line.startsWith('rename from ')) {
150
+ return {
151
+ next: {
152
+ ...current,
153
+ minusPath: normalizeDiffPathToken({
154
+ headerPrefixes: null,
155
+ side: 'old',
156
+ value: line.slice('rename from '.length),
157
+ }),
158
+ },
159
+ };
160
+ }
161
+ if (line.startsWith('rename to ')) {
162
+ return {
163
+ next: {
164
+ ...current,
165
+ plusPath: normalizeDiffPathToken({
166
+ headerPrefixes: null,
167
+ side: 'new',
168
+ value: line.slice('rename to '.length),
169
+ }),
62
170
  },
63
171
  };
64
172
  }
@@ -74,16 +182,12 @@ const applyPatchMetadataLine = (current, line) => {
74
182
  };
75
183
  const applyPatchHunkLine = (current, line) => {
76
184
  if (line.startsWith('+')) {
77
- return {
78
- ...current,
79
- added: current.added + 1,
80
- };
185
+ current.added += 1;
186
+ return current;
81
187
  }
82
188
  if (line.startsWith('-')) {
83
- return {
84
- ...current,
85
- removed: current.removed + 1,
86
- };
189
+ current.removed += 1;
190
+ return current;
87
191
  }
88
192
  return current;
89
193
  };
@@ -91,36 +195,56 @@ const mergeBlobLink = (links, blobSha256) => ({
91
195
  ...links,
92
196
  blobs: [...new Set([...(links?.blobs ?? []), blobSha256])],
93
197
  });
94
- const resolvePatchSource = async ({ record, workspaceRoot, }) => {
95
- if (record.diffPath) {
96
- const resolvedPath = path.resolve(workspaceRoot, record.diffPath);
198
+ const resolvePatchSourceFromFile = async (record, workspaceRoot) => {
199
+ const resolvedPath = path.resolve(workspaceRoot, record.diffPath);
200
+ try {
97
201
  return {
98
- patchText: await readFile(resolvedPath, 'utf8'),
202
+ patchText: await readPatchTextFromFile(resolvedPath),
99
203
  source: 'file',
100
204
  sourceLabel: resolvedPath,
101
205
  };
102
206
  }
103
- if (record.diffText) {
207
+ catch (error) {
208
+ throw new Error(`Failed to read diff file ${resolvedPath} for ${formatRecordContext(record)}: ${error instanceof Error ? error.message : String(error)}`);
209
+ }
210
+ };
211
+ const resolvePatchSourceFromInlineText = (record) => {
212
+ try {
213
+ assertPatchTextWithinLimit(record.diffText, 'inline diff text');
104
214
  return {
105
215
  patchText: record.diffText,
106
216
  source: 'inline',
107
217
  sourceLabel: 'inline diff text',
108
218
  };
109
219
  }
110
- if (record.diff) {
111
- try {
112
- return {
113
- patchText: await readBlobText(workspaceRoot, record.diff.blobSha256),
114
- source: 'blob',
115
- sourceLabel: `blob ${record.diff.blobSha256}`,
116
- };
117
- }
118
- catch (error) {
119
- if (error.code === 'ENOENT') {
120
- throw new Error(`Patch blob ${record.diff.blobSha256} was not found for ${formatRecordContext(record)}. Store it first or use diffPath.`);
121
- }
122
- throw error;
220
+ catch (error) {
221
+ throw new Error(`Failed to validate inline diff text for ${formatRecordContext(record)}: ${error instanceof Error ? error.message : String(error)}`);
222
+ }
223
+ };
224
+ const resolvePatchSourceFromStoredBlob = async (record, workspaceRoot) => {
225
+ try {
226
+ return {
227
+ patchText: await readBlobText(workspaceRoot, record.diff?.blobSha256),
228
+ source: 'blob',
229
+ sourceLabel: `blob ${record.diff?.blobSha256}`,
230
+ };
231
+ }
232
+ catch (error) {
233
+ if (error.code === 'ENOENT') {
234
+ throw new Error(`Patch blob ${record.diff?.blobSha256} was not found for ${formatRecordContext(record)}. Store it first or use diffPath.`);
123
235
  }
236
+ throw error;
237
+ }
238
+ };
239
+ const resolvePatchSource = async ({ record, workspaceRoot, }) => {
240
+ if (record.diffPath) {
241
+ return resolvePatchSourceFromFile(record, workspaceRoot);
242
+ }
243
+ if (record.diffText) {
244
+ return resolvePatchSourceFromInlineText(record);
245
+ }
246
+ if (record.diff) {
247
+ return resolvePatchSourceFromStoredBlob(record, workspaceRoot);
124
248
  }
125
249
  throw new Error(`${record.kind} records require diff, diffPath, or diffText.`);
126
250
  };
@@ -140,34 +264,34 @@ export const deriveFilesChangedFromPatch = (patchText) => {
140
264
  const filesChanged = [];
141
265
  let current;
142
266
  let insideHunk = false;
143
- for (const line of patchText.split(/\r?\n/u)) {
267
+ forEachLine(patchText, (line) => {
144
268
  if (line.startsWith('diff --git ')) {
145
269
  finalizeFileChange(current, filesChanged);
146
270
  current = createDiffFileAccumulator(line);
147
271
  insideHunk = false;
148
- continue;
272
+ return;
149
273
  }
150
274
  if (!current) {
151
- continue;
275
+ return;
152
276
  }
153
277
  if (insideHunk) {
154
278
  if (line.startsWith('@@')) {
155
- continue;
279
+ return;
156
280
  }
157
281
  current = applyPatchHunkLine(current, line);
158
- continue;
282
+ return;
159
283
  }
160
284
  const metadataUpdate = applyPatchMetadataLine(current, line);
161
285
  current = metadataUpdate.next;
162
286
  if (metadataUpdate.startHunk) {
163
287
  insideHunk = true;
164
- continue;
288
+ return;
165
289
  }
166
290
  if (!insideHunk) {
167
- continue;
291
+ return;
168
292
  }
169
293
  current = applyPatchHunkLine(current, line);
170
- }
294
+ });
171
295
  finalizeFileChange(current, filesChanged);
172
296
  return filesChanged;
173
297
  };
@@ -180,6 +304,22 @@ const buildResolvedPatchRecord = ({ links, record, storedDiff, }) => {
180
304
  };
181
305
  };
182
306
  export async function resolvePatchRecord({ record, workspaceRoot, }) {
307
+ if (record.diff && !record.diffPath && !record.diffText) {
308
+ try {
309
+ await stat(resolveBlobPath(workspaceRoot, record.diff.blobSha256));
310
+ }
311
+ catch (error) {
312
+ if (error.code === 'ENOENT') {
313
+ throw new Error(`Patch blob ${record.diff.blobSha256} was not found for ${formatRecordContext(record)}. Store it first or use diffPath.`);
314
+ }
315
+ throw new Error(`Failed to read patch blob ${record.diff.blobSha256} for ${formatRecordContext(record)}: ${error instanceof Error ? error.message : String(error)}`);
316
+ }
317
+ return buildResolvedPatchRecord({
318
+ links: mergeBlobLink(record.links, record.diff.blobSha256),
319
+ record,
320
+ storedDiff: record.diff,
321
+ });
322
+ }
183
323
  const { patchText, sourceLabel } = await resolvePatchSource({
184
324
  record,
185
325
  workspaceRoot,
@@ -1 +1 @@
1
- {"version":3,"file":"read-index.d.ts","sourceRoot":"","sources":["../src/read-index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAC;AAG7B,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;;;;aAIxB,CAAC;AAEH,QAAA,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sDAc1B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAC1E,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACxE,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"}
1
+ {"version":3,"file":"read-index.d.ts","sourceRoot":"","sources":["../src/read-index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAC;AAI7B,OAAO,EAAgB,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAoB,MAAM,mBAAmB,CAAC;AACxH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK3D,QAAA,MAAM,oBAAoB;;;;aAIxB,CAAC;AAEH,QAAA,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sDAc1B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAC1E,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACxE,MAAM,MAAM,qBAAqB,GAAG,SAAS,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AA4HhG,eAAO,MAAM,0BAA0B,GAAU,eAAe,MAAM,EAAE,UAAU,cAAc;;;;;;;;;;;EAyB/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"}
@@ -2,11 +2,10 @@ import { readFile } from 'node:fs/promises';
2
2
  import * as v from 'valibot';
3
3
  import { mapWithConcurrencyLimit } from "./async.js";
4
4
  import { stableStringify } from "./json.js";
5
+ import { getLedgerRuntimeConfig } from "./runtime-config.js";
5
6
  import { LEDGER_KINDS, parseLedgerEntry } from "./schema/entry.js";
6
7
  import { readPhaseEntryText, resolveLedgerPaths, writeAtomicTextFile } from "./storage/filesystem.js";
7
8
  const READ_INDEX_SCHEMA_VERSION = 'ushman-ledger-read-index/v1';
8
- const ENTRY_READ_BATCH_SIZE = 32;
9
- const ENTRY_READ_CONCURRENCY = 16;
10
9
  const ReadIndexEntrySchema = v.object({
11
10
  id: v.pipe(v.string(), v.minLength(1)),
12
11
  kind: v.picklist(LEDGER_KINDS),
@@ -83,7 +82,7 @@ const readIndexedEntry = async ({ entryId, phase, workspaceRoot, }) => {
83
82
  const text = await readPhaseEntryText(workspaceRoot, phase, `${entryId}.json`);
84
83
  return parseLedgerEntry(JSON.parse(text));
85
84
  };
86
- const readIndexedEntryBatch = async ({ entryLocations, workspaceRoot, }) => mapWithConcurrencyLimit(entryLocations, ENTRY_READ_CONCURRENCY, async ([entryId, location]) => readIndexedEntry({
85
+ const readIndexedEntryBatch = async ({ entryReadConcurrency, entryLocations, workspaceRoot, }) => mapWithConcurrencyLimit(entryLocations, entryReadConcurrency, async ([entryId, location]) => readIndexedEntry({
87
86
  entryId,
88
87
  phase: location.phase,
89
88
  workspaceRoot,
@@ -97,12 +96,14 @@ const collectCoveredFiles = (entry, coveredFiles) => {
97
96
  }
98
97
  };
99
98
  export const buildReadIndexFromManifest = async (workspaceRoot, manifest) => {
99
+ const { readIndexRebuildBatchSize, readIndexRebuildConcurrency } = getLedgerRuntimeConfig();
100
100
  const orderedEntryLocations = getManifestSequenceLocations(manifest);
101
101
  const entries = [];
102
102
  const coveredFiles = new Set();
103
- for (let index = 0; index < orderedEntryLocations.length; index += ENTRY_READ_BATCH_SIZE) {
104
- const batch = orderedEntryLocations.slice(index, index + ENTRY_READ_BATCH_SIZE);
103
+ for (let index = 0; index < orderedEntryLocations.length; index += readIndexRebuildBatchSize) {
104
+ const batch = orderedEntryLocations.slice(index, index + readIndexRebuildBatchSize);
105
105
  const resolvedEntries = await readIndexedEntryBatch({
106
+ entryReadConcurrency: readIndexRebuildConcurrency,
106
107
  entryLocations: batch,
107
108
  workspaceRoot,
108
109
  });
@@ -1 +1 @@
1
- {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAQA,OAAO,EACH,KAAK,WAAW,EAMnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAU3D,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;AAOF,eAAO,MAAM,wBAAwB,GAAI,eAAe,MAAM,EAAE,OAAO,qBAAqB,GAAG,IAAI,SAOlG,CAAC;AA0EF,eAAO,MAAM,YAAY,GACrB,eAAe,MAAM,EACrB,OAAO,OAAO,KACf,OAAO,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAsF5C,CAAC;AAEF,eAAO,MAAM,aAAa,GACtB,eAAe,MAAM,EACrB,UAAU,cAAc,EACxB,SAAS,MAAM,KAChB,OAAO,CAAC,WAAW,CASrB,CAAC"}
1
+ {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AASA,OAAO,EACH,KAAK,WAAW,EAMnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAU3D,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;AAOF,eAAO,MAAM,wBAAwB,GAAI,eAAe,MAAM,EAAE,OAAO,qBAAqB,GAAG,IAAI,SAOlG,CAAC;AA0EF,eAAO,MAAM,YAAY,GACrB,eAAe,MAAM,EACrB,OAAO,OAAO,KACf,OAAO,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAsF5C,CAAC;AAEF,eAAO,MAAM,aAAa,GACtB,eAAe,MAAM,EACrB,UAAU,cAAc,EACxB,SAAS,MAAM,KAChB,OAAO,CAAC,WAAW,CASrB,CAAC"}
package/dist/record.js CHANGED
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import * as v from 'valibot';
4
4
  import { sha256Hex, stableStringify } from "./json.js";
5
+ import { getNextManifestSequence } from "./manifest-update.js";
5
6
  import { updateManifestForEntry } from "./manifest-update.js";
6
7
  import { resolvePatchRecord } from "./patch-resolver.js";
7
8
  import { appendEntryToReadIndex, saveReadIndex } from "./read-index.js";
@@ -105,7 +106,7 @@ export const appendRecord = async (workspaceRoot, input) => {
105
106
  id: existingId,
106
107
  };
107
108
  }
108
- const nextSequence = manifest.lastSequence + 1;
109
+ const nextSequence = getNextManifestSequence(manifest.lastSequence);
109
110
  const { idempotencyKey: _idempotencyKey, ...recordWithoutIdempotencyKey } = normalizedRecord;
110
111
  const entryWithoutId = {
111
112
  ...recordWithoutIdempotencyKey,
@@ -0,0 +1,12 @@
1
+ export type LedgerRuntimeConfig = {
2
+ readonly blobHashConcurrency: number;
3
+ readonly coverageFileStatConcurrency: number;
4
+ readonly maxPatchBytes: number;
5
+ readonly readIndexRebuildBatchSize: number;
6
+ readonly readIndexRebuildConcurrency: number;
7
+ readonly scanBatchSize: number;
8
+ readonly scanConcurrency: number;
9
+ };
10
+ export declare const validateLedgerRuntimeConfig: (env?: NodeJS.ProcessEnv) => void;
11
+ export declare const getLedgerRuntimeConfig: (env?: NodeJS.ProcessEnv) => LedgerRuntimeConfig;
12
+ //# sourceMappingURL=runtime-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-config.d.ts","sourceRoot":"","sources":["../src/runtime-config.ts"],"names":[],"mappings":"AAgCA,MAAM,MAAM,mBAAmB,GAAG;IAC9B,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,2BAA2B,EAAE,MAAM,CAAC;IAC7C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,yBAAyB,EAAE,MAAM,CAAC;IAC3C,QAAQ,CAAC,2BAA2B,EAAE,MAAM,CAAC;IAC7C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CACpC,CAAC;AAuDF,eAAO,MAAM,2BAA2B,GAAI,MAAK,MAAM,CAAC,UAAwB,SAE/E,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,mBAgB7E,CAAC"}
@@ -0,0 +1,83 @@
1
+ const DEFAULT_SCAN_BATCH_SIZE = 32;
2
+ const DEFAULT_SCAN_CONCURRENCY = 16;
3
+ const DEFAULT_MAX_PATCH_BYTES = 10 * 1024 * 1024;
4
+ const RUNTIME_CONFIG_ENV_VARS = [
5
+ 'USHMAN_LEDGER_BLOB_HASH_CONCURRENCY',
6
+ 'USHMAN_LEDGER_COVERAGE_FILE_STAT_CONCURRENCY',
7
+ 'USHMAN_LEDGER_MAX_PATCH_BYTES',
8
+ 'USHMAN_LEDGER_READ_INDEX_REBUILD_BATCH_SIZE',
9
+ 'USHMAN_LEDGER_READ_INDEX_REBUILD_CONCURRENCY',
10
+ 'USHMAN_LEDGER_SCAN_BATCH_SIZE',
11
+ 'USHMAN_LEDGER_SCAN_CONCURRENCY',
12
+ ];
13
+ const parsePositiveIntegerEnv = ({ defaultValue, env, envVar, }) => {
14
+ const rawValue = env[envVar];
15
+ if (!rawValue) {
16
+ return defaultValue;
17
+ }
18
+ if (!/^[1-9]\d*$/u.test(rawValue)) {
19
+ throw new Error(`Invalid ${envVar} value: ${rawValue}. Expected a positive integer.`);
20
+ }
21
+ return Number.parseInt(rawValue, 10);
22
+ };
23
+ let processEnvRuntimeConfigCache;
24
+ const getRuntimeConfigSignature = (env) => RUNTIME_CONFIG_ENV_VARS.map((envVar) => `${envVar}=${env[envVar] ?? ''}`).join('\u0000');
25
+ const buildLedgerRuntimeConfig = (env) => {
26
+ const scanBatchSize = parsePositiveIntegerEnv({
27
+ defaultValue: DEFAULT_SCAN_BATCH_SIZE,
28
+ env,
29
+ envVar: 'USHMAN_LEDGER_SCAN_BATCH_SIZE',
30
+ });
31
+ const scanConcurrency = parsePositiveIntegerEnv({
32
+ defaultValue: DEFAULT_SCAN_CONCURRENCY,
33
+ env,
34
+ envVar: 'USHMAN_LEDGER_SCAN_CONCURRENCY',
35
+ });
36
+ return {
37
+ blobHashConcurrency: parsePositiveIntegerEnv({
38
+ defaultValue: scanConcurrency,
39
+ env,
40
+ envVar: 'USHMAN_LEDGER_BLOB_HASH_CONCURRENCY',
41
+ }),
42
+ coverageFileStatConcurrency: parsePositiveIntegerEnv({
43
+ defaultValue: scanConcurrency,
44
+ env,
45
+ envVar: 'USHMAN_LEDGER_COVERAGE_FILE_STAT_CONCURRENCY',
46
+ }),
47
+ maxPatchBytes: parsePositiveIntegerEnv({
48
+ defaultValue: DEFAULT_MAX_PATCH_BYTES,
49
+ env,
50
+ envVar: 'USHMAN_LEDGER_MAX_PATCH_BYTES',
51
+ }),
52
+ readIndexRebuildBatchSize: parsePositiveIntegerEnv({
53
+ defaultValue: scanBatchSize,
54
+ env,
55
+ envVar: 'USHMAN_LEDGER_READ_INDEX_REBUILD_BATCH_SIZE',
56
+ }),
57
+ readIndexRebuildConcurrency: parsePositiveIntegerEnv({
58
+ defaultValue: scanConcurrency,
59
+ env,
60
+ envVar: 'USHMAN_LEDGER_READ_INDEX_REBUILD_CONCURRENCY',
61
+ }),
62
+ scanBatchSize,
63
+ scanConcurrency,
64
+ };
65
+ };
66
+ export const validateLedgerRuntimeConfig = (env = process.env) => {
67
+ void getLedgerRuntimeConfig(env);
68
+ };
69
+ export const getLedgerRuntimeConfig = (env = process.env) => {
70
+ if (env !== process.env) {
71
+ return buildLedgerRuntimeConfig(env);
72
+ }
73
+ const signature = getRuntimeConfigSignature(env);
74
+ if (processEnvRuntimeConfigCache?.signature === signature) {
75
+ return processEnvRuntimeConfigCache.config;
76
+ }
77
+ const config = buildLedgerRuntimeConfig(env);
78
+ processEnvRuntimeConfigCache = {
79
+ config,
80
+ signature,
81
+ };
82
+ return config;
83
+ };
@@ -5,6 +5,7 @@ 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
  };
10
11
  export type LedgerPaths = {
@@ -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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"}
@@ -35,6 +35,13 @@ const runAtomicWriteTestHook = async (filePath) => {
35
35
  throw error;
36
36
  }
37
37
  };
38
+ const runAtomicWriteWriteTestHook = async (filePath) => {
39
+ const { hook } = getAtomicWriteTestHook(filePath);
40
+ if (!hook?.beforeWrite) {
41
+ return;
42
+ }
43
+ await hook.beforeWrite();
44
+ };
38
45
  export const resolveLedgerPaths = (workspaceRoot) => {
39
46
  const root = path.join(workspaceRoot, ...LEDGER_ROOT_SEGMENTS);
40
47
  return {
@@ -115,6 +122,7 @@ export const createAtomicTextFileWriter = async (filePath) => {
115
122
  const tempPath = `${filePath}.tmp.${process.pid}.${Date.now()}.${randomUUID()}`;
116
123
  const handle = await open(tempPath, 'w');
117
124
  let completed = false;
125
+ let pendingWriteError;
118
126
  let writeChain = Promise.resolve();
119
127
  const finalizeHandle = async () => {
120
128
  if (completed) {
@@ -125,21 +133,40 @@ export const createAtomicTextFileWriter = async (filePath) => {
125
133
  };
126
134
  const waitForWrites = async () => {
127
135
  await writeChain;
136
+ if (pendingWriteError) {
137
+ throw pendingWriteError;
138
+ }
128
139
  };
129
140
  return {
130
141
  abort: async () => {
131
142
  if (!(await finalizeHandle())) {
132
143
  return;
133
144
  }
134
- await waitForWrites();
145
+ let writeError;
146
+ try {
147
+ await waitForWrites();
148
+ }
149
+ catch (error) {
150
+ writeError = error;
151
+ }
135
152
  await handle.close();
136
153
  await rm(tempPath, { force: true });
154
+ if (writeError) {
155
+ throw writeError;
156
+ }
137
157
  },
138
158
  close: async () => {
139
159
  if (!(await finalizeHandle())) {
140
160
  return;
141
161
  }
142
- await waitForWrites();
162
+ try {
163
+ await waitForWrites();
164
+ }
165
+ catch (error) {
166
+ await handle.close();
167
+ await rm(tempPath, { force: true });
168
+ throw error;
169
+ }
143
170
  try {
144
171
  await handle.sync();
145
172
  }
@@ -160,9 +187,12 @@ export const createAtomicTextFileWriter = async (filePath) => {
160
187
  if (completed || chunk.length === 0) {
161
188
  return;
162
189
  }
190
+ await runAtomicWriteWriteTestHook(filePath);
163
191
  await handle.writeFile(chunk, 'utf8');
164
192
  });
165
- writeChain = nextWrite.then(() => undefined, () => undefined);
193
+ writeChain = nextWrite.catch((error) => {
194
+ pendingWriteError ??= error;
195
+ });
166
196
  await nextWrite;
167
197
  },
168
198
  };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Visit each logical line in `text`, normalizing trailing `\r` from CRLF input.
3
+ *
4
+ * The final unterminated line is still visited, which keeps diff parsing stable for
5
+ * patch content that does not end with a trailing newline.
6
+ */
7
+ export declare const forEachLine: (text: string, visit: (line: string) => void) => void;
8
+ //# sourceMappingURL=text-lines.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-lines.d.ts","sourceRoot":"","sources":["../src/text-lines.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,SAatE,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Visit each logical line in `text`, normalizing trailing `\r` from CRLF input.
3
+ *
4
+ * The final unterminated line is still visited, which keeps diff parsing stable for
5
+ * patch content that does not end with a trailing newline.
6
+ */
7
+ export const forEachLine = (text, visit) => {
8
+ let lineStart = 0;
9
+ for (let index = 0; index <= text.length; index += 1) {
10
+ if (index !== text.length && text.charCodeAt(index) !== 10) {
11
+ continue;
12
+ }
13
+ let line = text.slice(lineStart, index);
14
+ if (line.endsWith('\r')) {
15
+ line = line.slice(0, -1);
16
+ }
17
+ visit(line);
18
+ lineStart = index + 1;
19
+ }
20
+ };
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const LEDGER_LIBRARY_VERSION = "1.2.0";
1
+ export declare const LEDGER_LIBRARY_VERSION: string;
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sBAAsB,UAAU,CAAC"}
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,sBAAsB,QAAsB,CAAC"}