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,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { open, readFile, rename, rm, stat } from 'node:fs/promises';
2
+ import { open, readFile, rename, rm } from 'node:fs/promises';
3
+ import { isProcessAlive } from "../process.js";
3
4
  const DEFAULT_LOCK_OPTIONS = {
4
5
  // Give writers enough time to detect and reclaim a stale lock before timing out.
5
6
  retryDelayMs: 10,
@@ -7,6 +8,12 @@ const DEFAULT_LOCK_OPTIONS = {
7
8
  timeoutMs: 35_000,
8
9
  };
9
10
  const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
11
+ const parseOwnerPid = (value) => {
12
+ if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
13
+ return null;
14
+ }
15
+ return value;
16
+ };
10
17
  const readLockState = async (lockPath) => {
11
18
  try {
12
19
  const rawText = await readFile(lockPath, 'utf8');
@@ -14,6 +21,7 @@ const readLockState = async (lockPath) => {
14
21
  const parsed = JSON.parse(rawText);
15
22
  const startedAt = typeof parsed.startedAt === 'string' ? Date.parse(parsed.startedAt) : Number.NaN;
16
23
  return {
24
+ ownerPid: parseOwnerPid(parsed.pid),
17
25
  rawText,
18
26
  startedAtMs: Number.isNaN(startedAt) ? null : startedAt,
19
27
  token: typeof parsed.token === 'string' && parsed.token.length > 0 ? parsed.token : null,
@@ -21,6 +29,7 @@ const readLockState = async (lockPath) => {
21
29
  }
22
30
  catch {
23
31
  return {
32
+ ownerPid: null,
24
33
  rawText,
25
34
  startedAtMs: null,
26
35
  token: null,
@@ -35,6 +44,9 @@ const readLockState = async (lockPath) => {
35
44
  }
36
45
  };
37
46
  const isFreshLockState = (lockState, staleMs) => {
47
+ if (lockState.ownerPid !== null && !isProcessAlive(lockState.ownerPid)) {
48
+ return false;
49
+ }
38
50
  if (lockState.startedAtMs === null) {
39
51
  return false;
40
52
  }
@@ -62,17 +74,17 @@ const writeExclusiveTextFile = async (filePath, text) => {
62
74
  await handle.close();
63
75
  }
64
76
  };
65
- const reclaimMarkerExists = async (lockPath) => {
66
- try {
67
- await stat(buildReclaimMarkerPath(lockPath));
68
- return true;
77
+ const hasActiveReclaimMarker = async (lockPath, staleMs) => {
78
+ const reclaimMarkerPath = buildReclaimMarkerPath(lockPath);
79
+ const reclaimMarkerState = await readLockState(reclaimMarkerPath);
80
+ if (!reclaimMarkerState) {
81
+ return false;
69
82
  }
70
- catch (error) {
71
- if (error.code === 'ENOENT') {
72
- return false;
73
- }
74
- throw error;
83
+ if (isFreshLockState(reclaimMarkerState, staleMs)) {
84
+ return true;
75
85
  }
86
+ await rm(reclaimMarkerPath, { force: true });
87
+ return false;
76
88
  };
77
89
  const acquireReclaimMarker = async (lockPath) => {
78
90
  const reclaimMarkerPath = buildReclaimMarkerPath(lockPath);
@@ -126,17 +138,24 @@ const reclaimIfObservedStateMatches = async ({ lockPath, observedState, testHook
126
138
  }
127
139
  throw error;
128
140
  }
141
+ let shouldRemoveQuarantine = false;
129
142
  try {
130
143
  const quarantinedState = await readLockState(quarantinePath);
131
144
  if (!quarantinedState || quarantinedState.rawText !== observedState.rawText) {
132
145
  await restoreQuarantinedLock(lockPath, quarantinePath);
133
146
  return false;
134
147
  }
135
- await rm(quarantinePath, { force: true });
148
+ shouldRemoveQuarantine = true;
136
149
  return true;
137
150
  }
151
+ catch (error) {
152
+ await restoreQuarantinedLock(lockPath, quarantinePath);
153
+ throw error;
154
+ }
138
155
  finally {
139
- await rm(quarantinePath, { force: true });
156
+ if (shouldRemoveQuarantine) {
157
+ await rm(quarantinePath, { force: true });
158
+ }
140
159
  }
141
160
  }
142
161
  finally {
@@ -155,7 +174,10 @@ const reclaimIfStale = async (lockPath, options) => {
155
174
  });
156
175
  };
157
176
  const describeCurrentLockState = async (lockPath) => {
158
- const [currentState, reclaiming] = await Promise.all([readLockState(lockPath), reclaimMarkerExists(lockPath)]);
177
+ const [currentState, reclaiming] = await Promise.all([
178
+ readLockState(lockPath),
179
+ hasActiveReclaimMarker(lockPath, DEFAULT_LOCK_OPTIONS.staleMs),
180
+ ]);
159
181
  if (!currentState) {
160
182
  return reclaiming ? 'lock missing while stale reclamation marker is present' : 'lock file is missing';
161
183
  }
@@ -163,7 +185,7 @@ const describeCurrentLockState = async (lockPath) => {
163
185
  return `${ageMs}, token=${currentState.token ?? 'missing'}, reclaiming=${reclaiming ? 'yes' : 'no'}`;
164
186
  };
165
187
  const assertOwnership = async (lockPath, token) => {
166
- if (await reclaimMarkerExists(lockPath)) {
188
+ if (await hasActiveReclaimMarker(lockPath, DEFAULT_LOCK_OPTIONS.staleMs)) {
167
189
  throw new Error(`Lost ledger lock ownership for ${lockPath}: stale reclamation is in progress.`);
168
190
  }
169
191
  const currentState = await readLockState(lockPath);
@@ -182,7 +204,7 @@ export const acquireLock = async (lockPath, options = {}) => {
182
204
  const mergedOptions = { ...DEFAULT_LOCK_OPTIONS, ...options };
183
205
  const deadline = Date.now() + mergedOptions.timeoutMs;
184
206
  while (Date.now() <= deadline) {
185
- if (await reclaimMarkerExists(lockPath)) {
207
+ if (await hasActiveReclaimMarker(lockPath, mergedOptions.staleMs)) {
186
208
  await sleep(mergedOptions.retryDelayMs);
187
209
  continue;
188
210
  }
@@ -201,7 +223,7 @@ export const acquireLock = async (lockPath, options = {}) => {
201
223
  finally {
202
224
  await handle.close();
203
225
  }
204
- if (await reclaimMarkerExists(lockPath)) {
226
+ if (await hasActiveReclaimMarker(lockPath, mergedOptions.staleMs)) {
205
227
  await releaseIfOwned(lockPath, token);
206
228
  await sleep(mergedOptions.retryDelayMs);
207
229
  continue;
@@ -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"}
package/dist/version.js CHANGED
@@ -1 +1,2 @@
1
- export const LEDGER_LIBRARY_VERSION = '1.2.0';
1
+ import packageJson from '../package.json' with { type: 'json' };
2
+ export const LEDGER_LIBRARY_VERSION = packageJson.version;
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "description": "Append-only workspace ledger library and CLI for Ushman v4.",
18
18
  "devDependencies": {
19
- "@biomejs/biome": "^2.4.15",
19
+ "@biomejs/biome": "^2.4.16",
20
20
  "@types/bun": "^1.3.14",
21
21
  "@types/node": "^25.9.1",
22
22
  "typescript": "^6.0.3"
@@ -37,7 +37,9 @@
37
37
  },
38
38
  "files": [
39
39
  "dist",
40
+ "ARCHITECTURE.md",
40
41
  "CHANGELOG.md",
42
+ "TROUBLESHOOTING.md",
41
43
  "README.md",
42
44
  "AGENTS.md",
43
45
  "LICENSE.md"
@@ -68,5 +70,5 @@
68
70
  },
69
71
  "type": "module",
70
72
  "types": "dist/index.d.ts",
71
- "version": "1.2.1"
73
+ "version": "1.3.0"
72
74
  }