ushman-ledger 1.2.0 → 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.
Files changed (63) hide show
  1. package/ARCHITECTURE.md +79 -0
  2. package/README.md +144 -5
  3. package/TROUBLESHOOTING.md +170 -0
  4. package/dist/blobs.d.ts +3 -0
  5. package/dist/blobs.d.ts.map +1 -1
  6. package/dist/blobs.js +41 -15
  7. package/dist/builders.d.ts +1 -2
  8. package/dist/builders.d.ts.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +231 -70
  11. package/dist/coverage.d.ts.map +1 -1
  12. package/dist/coverage.js +3 -2
  13. package/dist/doctor.d.ts +17 -4
  14. package/dist/doctor.d.ts.map +1 -1
  15. package/dist/doctor.js +225 -58
  16. package/dist/handle.d.ts +27 -7
  17. package/dist/handle.d.ts.map +1 -1
  18. package/dist/handle.js +96 -20
  19. package/dist/helpers.d.ts +1 -0
  20. package/dist/helpers.d.ts.map +1 -1
  21. package/dist/helpers.js +23 -0
  22. package/dist/index.d.ts +6 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -1
  25. package/dist/list.d.ts +3 -2
  26. package/dist/list.d.ts.map +1 -1
  27. package/dist/list.js +24 -12
  28. package/dist/note.d.ts +7 -0
  29. package/dist/note.d.ts.map +1 -1
  30. package/dist/note.js +6 -0
  31. package/dist/patch-resolver.d.ts +12 -0
  32. package/dist/patch-resolver.d.ts.map +1 -1
  33. package/dist/patch-resolver.js +205 -53
  34. package/dist/read-index.d.ts.map +1 -1
  35. package/dist/read-index.js +6 -5
  36. package/dist/record.d.ts.map +1 -1
  37. package/dist/record.js +3 -3
  38. package/dist/render/migration-log.d.ts +8 -1
  39. package/dist/render/migration-log.d.ts.map +1 -1
  40. package/dist/render/migration-log.js +40 -33
  41. package/dist/render/retro.d.ts.map +1 -1
  42. package/dist/render/retro.js +1 -7
  43. package/dist/render/workspace-narrative.d.ts +7 -1
  44. package/dist/render/workspace-narrative.d.ts.map +1 -1
  45. package/dist/render/workspace-narrative.js +114 -46
  46. package/dist/runtime-config.d.ts +12 -0
  47. package/dist/runtime-config.d.ts.map +1 -0
  48. package/dist/runtime-config.js +83 -0
  49. package/dist/schema/entry-read.d.ts.map +1 -1
  50. package/dist/schema/entry-read.js +1 -1
  51. package/dist/schema/entry-write.d.ts.map +1 -1
  52. package/dist/schema/entry-write.js +1 -1
  53. package/dist/schema/entry.d.ts.map +1 -1
  54. package/dist/storage/filesystem.d.ts +8 -0
  55. package/dist/storage/filesystem.d.ts.map +1 -1
  56. package/dist/storage/filesystem.js +110 -5
  57. package/dist/text-lines.d.ts +8 -0
  58. package/dist/text-lines.d.ts.map +1 -0
  59. package/dist/text-lines.js +20 -0
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.d.ts.map +1 -1
  62. package/dist/version.js +2 -1
  63. package/package.json +5 -3
@@ -0,0 +1,79 @@
1
+ # ushman-ledger architecture
2
+
3
+ This package owns append-only ledger persistence for ushman v4 workspaces. It does not orchestrate pipelines or act as a general database layer.
4
+
5
+ ## Core flow
6
+
7
+ 1. `openLedger()` validates the workspace and reconciles any pending crash-recovery state.
8
+ 2. `record()` normalizes the input, resolves any patch/blob payload, and acquires the manifest lock.
9
+ 3. A pending commit journal is written before the entry file, manifest update, and read-index update diverge.
10
+ 4. The entry file is written atomically into the phase directory.
11
+ 5. `manifest.json` is updated atomically.
12
+ 6. `read-index.json` is updated atomically.
13
+ 7. The pending journal is removed only after the manifest and read index are durable.
14
+
15
+ The same reconciliation path is used by reads, writes, coverage, doctor, render, and archive entrypoints so callers do not need a separate recovery command.
16
+
17
+ ## Storage model
18
+
19
+ `<workspace>/.lab/ledger/`
20
+
21
+ - `manifest.json`: append-order metadata, per-phase pointers, idempotency index, and archive metadata.
22
+ - `read-index.json`: durable lightweight scan index used by list/render/coverage/doctor.
23
+ - `pending/`: append journals used to replay incomplete commits.
24
+ - `pending-archives/`: archive journals used to adopt or discard incomplete archive writes.
25
+ - `blobs/`: content-addressed patch payloads keyed by SHA-256.
26
+ - `<phase>/`: append-only entry files per ledger phase.
27
+
28
+ ## Append semantics
29
+
30
+ - Entries are immutable once written. Corrections use `correction` records linked to the original entry.
31
+ - Each phase maintains its own `prevEntryId` chain so local append history is explicit and auditable.
32
+ - Idempotency is logical-content based by default and can be overridden with an explicit `idempotencyKey`.
33
+ - Patch payloads are de-duplicated by SHA-256 and stored once under `blobs/`.
34
+
35
+ ## Read path
36
+
37
+ - `list`, render, coverage, and doctor iterate entries in manifest sequence order.
38
+ - The durable read index avoids resorting `manifest.entryLocations` on every scan.
39
+ - Limited reads filter from the read index first, then materialize only the last matching entries from disk.
40
+ - Multiple readers can run concurrently. Writers serialize only around manifest mutation and recovery work that must observe a consistent durable state.
41
+
42
+ ## Recovery model
43
+
44
+ - Atomic writes use temp files and rename.
45
+ - Pending commit journals record the target sequence before the entry file and manifest can drift.
46
+ - Startup reconciliation replays pending commits in manifest order, rebuilds missing or stale read indexes, adopts verified archives, and cleans stale temp files.
47
+ - Manifest locks are reclaimed only after stale-owner checks, so a newer owner is not deleted during handoff.
48
+ - Re-opening the ledger or rerunning any CLI command triggers the same recovery path; there is no separate manual recovery subcommand.
49
+
50
+ ## Scale and tuning
51
+
52
+ The default scan contract is intentionally conservative and can be tuned with environment variables:
53
+
54
+ - `USHMAN_LEDGER_SCAN_BATCH_SIZE`
55
+ - `USHMAN_LEDGER_SCAN_CONCURRENCY`
56
+ - `USHMAN_LEDGER_READ_INDEX_REBUILD_BATCH_SIZE`
57
+ - `USHMAN_LEDGER_READ_INDEX_REBUILD_CONCURRENCY`
58
+ - `USHMAN_LEDGER_COVERAGE_FILE_STAT_CONCURRENCY`
59
+ - `USHMAN_LEDGER_BLOB_HASH_CONCURRENCY`
60
+ - `USHMAN_LEDGER_MAX_PATCH_BYTES`
61
+
62
+ The benchmark entrypoint, `bun run bench:scale`, prints the active runtime config so measurements can be compared across different knob settings.
63
+
64
+ ## Operational ceilings
65
+
66
+ - Manifest sequence numbers stop at `Number.MAX_SAFE_INTEGER`.
67
+ - Patch/blob ingestion defaults to a 10 MiB ceiling unless `USHMAN_LEDGER_MAX_PATCH_BYTES` is raised.
68
+ - Git diff capture is the only external-binary dependency and defaults to a 30 second timeout plus a 10 MiB stdout buffer.
69
+
70
+ ## Contributor map
71
+
72
+ - `src/blobs.ts`: patch blob storage, size limits, and digest/path validation.
73
+ - `src/record.ts`: append pipeline and idempotency behavior.
74
+ - `src/recovery.ts`: pending journal replay, read-index rebuild, and temp cleanup.
75
+ - `src/read-index.ts`: durable scan index construction and maintenance.
76
+ - `src/list.ts`: manifest/read-index iteration logic.
77
+ - `src/coverage.ts`: candidate file scan plus read-index-backed coverage calculation.
78
+ - `src/doctor.ts`: integrity checks plus troubleshooting-oriented findings.
79
+ - `src/cli.ts`: CLI argument handling and automation-facing output.
package/README.md CHANGED
@@ -27,9 +27,21 @@ bun link ushman-ledger
27
27
  ## Library API
28
28
 
29
29
  ```ts
30
- import { buildValidatorResultRecord, openLedger } from 'ushman-ledger';
31
-
32
- const ledger = await openLedger('/path/to/workspace');
30
+ import { readFile } from 'node:fs/promises';
31
+ import {
32
+ appendCleanupWaveNote,
33
+ appendSemanticCleanupSummaryNote,
34
+ type BuildRecordInput,
35
+ buildChangeLogRecord,
36
+ buildValidatorResultRecord,
37
+ deriveFilesChangedFromPatch,
38
+ getLedgerRuntimeConfig,
39
+ openLedger,
40
+ } from 'ushman-ledger';
41
+
42
+ const workspaceRoot = '/path/to/workspace';
43
+ const ledger = await openLedger(workspaceRoot);
44
+ const patchText = await readFile('/tmp/change.patch', 'utf8');
33
45
 
34
46
  await ledger.record({
35
47
  emitter: { tool: 'ushman-cli', version: '1.0.0' },
@@ -48,6 +60,21 @@ await ledger.record({
48
60
  summary: 'Apply websocket shim',
49
61
  });
50
62
 
63
+ await ledger.record(
64
+ buildChangeLogRecord({
65
+ emitter: { tool: 'ushman-cli', version: '1.0.0' },
66
+ filesChanged: deriveFilesChangedFromPatch(patchText),
67
+ hypothesis: 'Smaller schema modules keep the public API stable.',
68
+ kind: 'change-log',
69
+ parityStatus: 'green',
70
+ phase: 'cleanup',
71
+ rollbackPlan: 'Revert the schema split.',
72
+ smokeResult: 'pass',
73
+ subkind: 'semantic-cleanup',
74
+ summary: 'Split schema modules',
75
+ }),
76
+ );
77
+
51
78
  await ledger.record(
52
79
  buildValidatorResultRecord({
53
80
  emitter: { tool: 'ushman-doctor', version: '1.0.0' },
@@ -58,9 +85,40 @@ await ledger.record(
58
85
  }),
59
86
  );
60
87
 
88
+ await appendCleanupWaveNote(workspaceRoot, {
89
+ body: 'Split schema definitions out of the monolithic entry module.',
90
+ phase: 'cleanup',
91
+ summary: 'Wave 1',
92
+ });
93
+
94
+ await appendSemanticCleanupSummaryNote(workspaceRoot, {
95
+ body: 'The latest semantic cleanup summary shown in workspace-narrative-md.',
96
+ phase: 'migration',
97
+ summary: 'Latest summary',
98
+ });
99
+
61
100
  await ledger.render({ to: 'retro' });
62
101
  await ledger.render({ to: 'migration-log-md' });
102
+ await ledger.renderTo({ to: 'migration-log-md', out: '/tmp/migration-log.md' });
63
103
  await ledger.archive('/tmp/ledger.tgz');
104
+
105
+ const config = getLedgerRuntimeConfig();
106
+ console.log(`Using scan concurrency ${config.scanConcurrency}`);
107
+ ```
108
+
109
+ Advanced builder wrappers can reuse the exported `BuildRecordInput<T>` helper type when they want the same input contract the built-in builders accept.
110
+
111
+ Example:
112
+
113
+ ```ts
114
+ import type { BuildRecordInput, LedgerRecord } from 'ushman-ledger';
115
+
116
+ type RuntimeEventInput = BuildRecordInput<Extract<LedgerRecord, { kind: 'runtime-event' }>>;
117
+
118
+ const buildRuntimeEventRecord = (input: RuntimeEventInput) => ({
119
+ ...input,
120
+ kind: 'runtime-event' as const,
121
+ });
64
122
  ```
65
123
 
66
124
  ## CLI
@@ -68,6 +126,7 @@ await ledger.archive('/tmp/ledger.tgz');
68
126
  ```bash
69
127
  ushman-ledger record --workspace=<ws> --kind=tool-invocation --phase=capture --summary="capture started"
70
128
  ushman-ledger record --workspace=<ws> --kind=agent-patch --phase=cleanup --summary="capture git diff" --rationale="track working tree change" --diff-from-git=HEAD
129
+ ushman-ledger record --workspace=<ws> --kind=change-log --subkind=smoke --phase=cleanup --summary="scope git diff" --diff-from-git=HEAD --git-paths=src/main.ts,src/cli.ts --git-diff-timeout-ms=10000 --git-diff-max-buffer-bytes=20971520
71
130
  ushman-ledger record --workspace=<ws> --kind=operator-decision --phase=cleanup --summary="manual override" --action=ledger-hand-edit --check-id=manual-review --rationale="operator edited the ledger after audit"
72
131
  ushman-ledger record --workspace=<ws> --kind=change-log --subkind=semantic-cleanup --phase=cleanup --summary="split schema modules" --diff=/tmp/change.patch --hypothesis="smaller schema modules keep the public API stable" --commands=$'bun test\nbun run typecheck' --smoke-result=pass --parity-status=green --rollback-plan="revert the schema split"
73
132
  ushman-ledger note regression --workspace=<ws> --phase=cleanup --summary="runtime drift" --body=/tmp/note.md
@@ -80,6 +139,7 @@ ushman-ledger render --workspace=<ws> --to=jsonl --out=/tmp/ledger.jsonl
80
139
  ushman-ledger render --workspace=<ws> --to=dependency-graph --out=/tmp/ledger.mmd
81
140
  ushman-ledger archive --workspace=<ws> --out=/tmp/ledger.tgz
82
141
  ushman-ledger doctor --workspace=<ws>
142
+ ushman-ledger doctor --workspace=<ws> --json
83
143
  ```
84
144
 
85
145
  Valid record kinds: `tool-invocation`, `agent-patch`, `operator-patch`, `operator-decision`, `validator-result`, `runtime-event`, `note`, `correction`, `strip-decision-reverted`, `change-log`
@@ -99,7 +159,15 @@ Valid render targets: `retro`, `jsonl`, `timeline-html`, `dependency-graph`, `mi
99
159
  - optional narrative fields: `hypothesis`, `commandsRun`, `smokeResult`, `smokeNotes`, `parityStatus`, `rollbackPlan`
100
160
  - `rollsBack`: required when `subkind=rollback`
101
161
 
162
+ `deriveFilesChangedFromPatch()` is exported for library callers that want the same diff-to-file summary behavior as the CLI. Known limits:
163
+
164
+ - It expects `diff --git` headers to delimit files.
165
+ - It counts only textual hunk `+`/`-` lines, so binary or mode-only diffs can report zero line counts.
166
+ - It keeps the post-image path when available and rejects diff blocks that cannot be mapped to normalized workspace-relative paths.
167
+
102
168
  `migration-log-md` renders only `change-log` entries. `workspace-narrative-md` renders only the dedicated narrative note subkinds.
169
+ `render()` still returns a string for in-memory callers. `renderTo()` is the bounded-output alternative for large ledgers and can write to a callback or the canonical render file on disk.
170
+ `renderTo()` writers receive sequential chunks, can return synchronously or asynchronously, and should throw only when the render should abort.
103
171
 
104
172
  ## Workspace prerequisite
105
173
 
@@ -167,8 +235,12 @@ Valid render targets: `retro`, `jsonl`, `timeline-html`, `dependency-graph`, `mi
167
235
  ## Git diff capture
168
236
 
169
237
  - `--diff-from-git=<ref>` runs `git diff <ref>` against the workspace working tree.
170
- - The CLI times out `git diff` after 30 seconds and reports a usage error instead of hanging indefinitely.
238
+ - `--git-paths=<csv>` safely scopes `git diff` to literal normalized workspace-relative paths. The CLI passes them after `--`, so pathspec magic and shell token splitting are intentionally not supported here.
239
+ - `--git-paths=<csv>` and `--files-changed=<csv>` do not support literal commas inside individual file names.
240
+ - `--git-diff-timeout-ms=<ms>` and `--git-diff-max-buffer-bytes=<bytes>` override the default 30 second timeout and 10 MiB stdout buffer for the git diff helper.
241
+ - The CLI reports usage errors for missing `git`, timed out git diffs, invalid scoped paths, and buffer overflows instead of surfacing raw child-process failures.
171
242
  - This is the only CLI feature that depends on an external binary. `git` must be available in `PATH`.
243
+ - `--from-stdin` accepts structured JSON records only. Raw patch text still needs `--diff=<file>` or `--diff-from-git=<ref>`.
172
244
 
173
245
  ## Storage shape
174
246
 
@@ -200,5 +272,72 @@ bun run bench:scale
200
272
 
201
273
  ## Scale benchmark
202
274
 
203
- - `bun run bench:scale` creates a temporary workspace and benchmarks large-ledger paths for population, limited reads, repeated coverage, and repeated doctor runs.
275
+ - `bun run bench:scale` creates a temporary workspace and benchmarks large-ledger paths for population, limited reads, repeated coverage, repeated doctor runs, and markdown render paths.
204
276
  - Override the defaults with environment variables such as `LEDGER_BENCH_ENTRY_COUNT=100000` and `LEDGER_BENCH_CANDIDATE_FILE_COUNT=10000`.
277
+ - The benchmark prints the active ledger runtime tuning so large-ledger measurements can be tied back to the scan/rebuild settings that produced them.
278
+
279
+ ## Operational limits
280
+
281
+ - The manifest sequence counter is capped at `Number.MAX_SAFE_INTEGER`. Appends fail before overflow so sequence ordering stays exact.
282
+ - Patch/blob ingestion is capped at 10 MiB by default across `diffText`, `diffPath`, `storePatchBlob()`, and CLI git-diff capture. Raise `USHMAN_LEDGER_MAX_PATCH_BYTES` only when the larger diff size is intentional and operationally acceptable.
283
+ - CLI git diff capture also defaults to a 30 second timeout and a 10 MiB stdout buffer. Use `--git-diff-timeout-ms` and `--git-diff-max-buffer-bytes` when a specific capture needs more headroom.
284
+
285
+ ## Runtime tuning
286
+
287
+ - `USHMAN_LEDGER_SCAN_BATCH_SIZE`: batch size for list/render/doctor entry reads. Default `32`.
288
+ - `USHMAN_LEDGER_SCAN_CONCURRENCY`: concurrent entry reads for list/render/doctor. Default `16`.
289
+ - `USHMAN_LEDGER_READ_INDEX_REBUILD_BATCH_SIZE`: batch size used when rebuilding `read-index.json`. Defaults to `USHMAN_LEDGER_SCAN_BATCH_SIZE`.
290
+ - `USHMAN_LEDGER_READ_INDEX_REBUILD_CONCURRENCY`: concurrent entry reads used while rebuilding `read-index.json`. Defaults to `USHMAN_LEDGER_SCAN_CONCURRENCY`.
291
+ - `USHMAN_LEDGER_COVERAGE_FILE_STAT_CONCURRENCY`: concurrent `stat()` calls during coverage scans. Defaults to `USHMAN_LEDGER_SCAN_CONCURRENCY`.
292
+ - `USHMAN_LEDGER_BLOB_HASH_CONCURRENCY`: concurrent blob integrity hashes during `doctor`. Defaults to `USHMAN_LEDGER_SCAN_CONCURRENCY`.
293
+ - `USHMAN_LEDGER_MAX_PATCH_BYTES`: maximum accepted patch/blob input size in bytes. Default `10485760`.
294
+
295
+ All runtime tuning values must be positive integers. Invalid values fail fast with an explicit error so automation does not silently run with an unexpected fallback.
296
+
297
+ ## Doctor output
298
+
299
+ - `ushman-ledger doctor` prints human-oriented findings with a stable finding code and a remediation step for each issue.
300
+ - `ushman-ledger doctor --json` returns a machine-readable envelope with `checkedAt`, `ok`, `issueCount`, `issues`, and structured `findings`.
301
+ - Each JSON finding includes a stable `code`, a human `message`, and a `remediation` string intended for operators and CI surfaces.
302
+
303
+ Example:
304
+
305
+ ```bash
306
+ ushman-ledger doctor --workspace="$WS" --json | jq '.findings[] | { code, message, remediation }'
307
+ ```
308
+
309
+ See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for the current finding codes and the expected recovery flow.
310
+
311
+ ## Advanced usage
312
+
313
+ Use a stable `idempotencyKey` to bracket a multi-step cleanup wave, then stream the migration log without buffering the whole render in memory:
314
+
315
+ ```ts
316
+ import {
317
+ buildChangeLogRecord,
318
+ openLedger,
319
+ } from 'ushman-ledger';
320
+
321
+ const ledger = await openLedger(workspaceRoot);
322
+
323
+ await ledger.record(
324
+ buildChangeLogRecord({
325
+ emitter: { tool: 'ushman-cli', version: '1.0.0' },
326
+ idempotencyKey: 'cleanup-wave-7',
327
+ kind: 'change-log',
328
+ phase: 'cleanup',
329
+ subkind: 'pre-change-checkpoint',
330
+ summary: 'Wave 7 checkpoint',
331
+ }),
332
+ );
333
+
334
+ await ledger.renderTo({
335
+ to: 'migration-log-md',
336
+ write: (chunk) => process.stdout.write(chunk),
337
+ });
338
+ ```
339
+
340
+ ## Further reading
341
+
342
+ - [ARCHITECTURE.md](ARCHITECTURE.md)
343
+ - [TROUBLESHOOTING.md](TROUBLESHOOTING.md)
@@ -0,0 +1,170 @@
1
+ # ushman-ledger troubleshooting
2
+
3
+ Start with:
4
+
5
+ ```bash
6
+ ushman-ledger doctor --workspace="$WS"
7
+ ```
8
+
9
+ For automation or CI:
10
+
11
+ ```bash
12
+ ushman-ledger doctor --workspace="$WS" --json
13
+ ```
14
+
15
+ The JSON report contains:
16
+
17
+ - `ok`: overall health boolean
18
+ - `issueCount`: number of findings
19
+ - `issues`: legacy message-only list
20
+ - `findings`: structured findings with `code`, `message`, `remediation`, and optional metadata
21
+
22
+ The default human-readable output prints one finding per block:
23
+
24
+ ```text
25
+ [finding-code] human-readable message
26
+ Next step: remediation guidance
27
+ ```
28
+
29
+ ## Common findings
30
+
31
+ ### `manifest-entry-count-mismatch`
32
+
33
+ Meaning: `manifest.json` disagrees with the number of durable entry files on disk.
34
+
35
+ Action:
36
+ - Re-open the ledger or rerun the failed command first so recovery can replay pending commits.
37
+ - If the mismatch persists, compare `manifest.entryLocations` against the phase directories and repair the missing file or location.
38
+
39
+ ### `manifest-last-sequence-mismatch`
40
+
41
+ Meaning: the manifest sequence counter no longer matches the durable append count.
42
+
43
+ Action:
44
+ - Finish recovery first.
45
+ - If recovery is already clean, repair `manifest.lastSequence` so it matches the highest durable append sequence.
46
+
47
+ ### `manifest-entry-missing-on-disk`
48
+
49
+ Meaning: the manifest references an entry id whose file is missing.
50
+
51
+ Action:
52
+ - Restore the missing entry file from a known-good copy, or repair the manifest location if it was edited incorrectly.
53
+ - Do not archive until the manifest and disk agree.
54
+
55
+ ### `manifest-entry-location-missing`
56
+
57
+ Meaning: an entry file exists on disk, but `manifest.entryLocations` has no location for that entry id.
58
+
59
+ Action:
60
+ - Repair `manifest.entryLocations` so the durable entry is reachable by phase and sequence.
61
+ - Rerun `doctor` afterward to catch any follow-on sequence or latest-pointer drift.
62
+
63
+ ### `manifest-phase-mismatch`
64
+
65
+ Meaning: an entry file exists, but the manifest points at the wrong phase.
66
+
67
+ Action:
68
+ - Restore the entry to the correct phase directory or repair `manifest.entryLocations`.
69
+
70
+ ### `manifest-per-phase-latest-mismatch`
71
+
72
+ Meaning: `perPhaseLatest` does not point at the newest entry in that phase.
73
+
74
+ Action:
75
+ - Repair the manifest so the latest pointer matches the highest sequence entry in the phase.
76
+
77
+ ### `manifest-sequence-mismatch`
78
+
79
+ Meaning: manifest sequence numbers are no longer contiguous or ordered.
80
+
81
+ Action:
82
+ - Repair `manifest.entryLocations` so sequence numbers are gap-free and monotonic.
83
+
84
+ ### `phase-prev-entry-mismatch`
85
+
86
+ Meaning: an entry’s `prevEntryId` no longer matches the prior append in its phase chain.
87
+
88
+ Action:
89
+ - Restore the edited entry or repair the phase chain.
90
+ - For content fixes, append a `correction` entry instead of rewriting history in place.
91
+
92
+ ### `blob-missing`
93
+
94
+ Meaning: a patch/blob referenced by an entry is missing under `.lab/ledger/blobs/`.
95
+
96
+ Action:
97
+ - Restore the blob file or recreate the patch entry from the original diff.
98
+
99
+ ### `blob-corrupt`
100
+
101
+ Meaning: the blob exists but its SHA-256 digest no longer matches the entry metadata.
102
+
103
+ Action:
104
+ - Restore the original blob content or recreate the patch entry from the original diff.
105
+
106
+ ### `blob-unreadable`
107
+
108
+ Meaning: the blob exists but doctor could not read it to verify the digest.
109
+
110
+ Action:
111
+ - Fix the filesystem permission or transient I/O problem first.
112
+ - Rerun `doctor` before attempting `archive`.
113
+
114
+ ### `change-log-smoke-failure-missing-rollback-plan`
115
+
116
+ Meaning: a `change-log` entry recorded `smokeResult=fail` without documenting a rollback plan.
117
+
118
+ Action:
119
+ - Append a correction or follow-up `change-log` entry that documents the rollback path.
120
+
121
+ ### `change-log-rollback-missing-target`
122
+
123
+ Meaning: a rollback `change-log` entry did not declare which ledger entry it reverts.
124
+
125
+ Action:
126
+ - Append a correction or replacement rollback entry with `rollsBack` populated.
127
+
128
+ ### `pre-change-checkpoint-stale`
129
+
130
+ Meaning: a pre-change checkpoint aged past 24 hours without a follow-up entry using the same `idempotencyKey`.
131
+
132
+ Action:
133
+ - Append the follow-up change-log entry, or close the stale checkpoint with a correction entry explaining the abandoned work.
134
+
135
+ ### `open-issue-stale`
136
+
137
+ Meaning: an `open-issue` note is older than 30 days and has no correction/supersession link.
138
+
139
+ Action:
140
+ - Append a correction or superseding note linking back to the old issue once the follow-up is complete.
141
+
142
+ ### `read-failure`
143
+
144
+ Meaning: the ledger could not be parsed or reconciled cleanly before checks started.
145
+
146
+ Action:
147
+ - Fix invalid JSON in `manifest.json` or `read-index.json`.
148
+ - Re-open the ledger afterward so reconciliation can rebuild any missing derived state.
149
+
150
+ ## Recovery workflow
151
+
152
+ 1. Re-open the ledger or rerun the failing ledger command once. Many issues resolve after reconciliation replays pending commits or rebuilds the read index automatically.
153
+ 2. Run `doctor --json` and capture the structured findings if the problem persists.
154
+ 3. Repair the lowest-level corruption first: missing entry file, missing blob, invalid manifest JSON, or stale manifest pointer.
155
+ 4. Rerun `doctor`.
156
+ 5. Only run `archive` after `doctor` returns `ok: true`.
157
+
158
+ ## Common operator scenarios
159
+
160
+ ### Ledger lock held by a dead process
161
+
162
+ Rerun the original ledger command first. Lock reclamation happens during normal startup and append flows, so you usually do not need to delete lock files by hand.
163
+
164
+ ### Manifest and disk disagree after a crash
165
+
166
+ Re-open the ledger first so pending commits can be replayed. If `doctor` still reports `manifest-entry-missing-on-disk` or `manifest-entry-location-missing`, repair the missing entry file or manifest location before archiving.
167
+
168
+ ### Manual ledger edits
169
+
170
+ Prefer appending `correction` or `operator-decision` records instead of editing historical entry files directly. If historical files were edited already, use `doctor` to identify the damaged chain or manifest pointers before appending new records.
package/dist/blobs.d.ts CHANGED
@@ -4,6 +4,9 @@ export type StoredPatchBlob = {
4
4
  readonly bytes: number;
5
5
  readonly removedLines: number;
6
6
  };
7
+ export declare const assertValidBlobSha256: (sha256: string) => string;
8
+ export declare const assertPatchTextWithinLimit: (patchText: string, sourceLabel: string) => number;
9
+ export declare const readPatchTextFromFile: (patchPath: string) => Promise<string>;
7
10
  export declare const storePatchBlob: (workspaceRoot: string, patchText: string) => Promise<StoredPatchBlob>;
8
11
  export declare const storePatchBlobFromFile: (workspaceRoot: string, patchPath: string) => Promise<StoredPatchBlob>;
9
12
  export declare const resolveBlobPath: (workspaceRoot: string, sha256: string) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"blobs.d.ts","sourceRoot":"","sources":["../src/blobs.ts"],"names":[],"mappings":"AAiCA,MAAM,MAAM,eAAe,GAAG;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,cAAc,GAAU,eAAe,MAAM,EAAE,WAAW,MAAM,KAAG,OAAO,CAAC,eAAe,CAsBtG,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAU,eAAe,MAAM,EAAE,WAAW,MAAM,KAAG,OAAO,CAAC,eAAe,CAE9G,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,eAAe,MAAM,EAAE,QAAQ,MAAM,WAAyC,CAAC"}
1
+ {"version":3,"file":"blobs.d.ts","sourceRoot":"","sources":["../src/blobs.ts"],"names":[],"mappings":"AA8CA,MAAM,MAAM,eAAe,GAAG;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,QAAQ,MAAM,WAKnD,CAAC;AAEF,eAAO,MAAM,0BAA0B,GAAI,WAAW,MAAM,EAAE,aAAa,MAAM,WAOhF,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAU,WAAW,MAAM,oBAO5D,CAAC;AAEF,eAAO,MAAM,cAAc,GAAU,eAAe,MAAM,EAAE,WAAW,MAAM,KAAG,OAAO,CAAC,eAAe,CAmBtG,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAU,eAAe,MAAM,EAAE,WAAW,MAAM,KAAG,OAAO,CAAC,eAAe,CAE9G,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,eAAe,MAAM,EAAE,QAAQ,MAAM,WACN,CAAC"}
package/dist/blobs.js CHANGED
@@ -1,49 +1,75 @@
1
- import { mkdir, readFile } from 'node:fs/promises';
1
+ import { readFile, stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { sha256File, sha256Hex } from "./json.js";
3
+ import { sha256Hex } from "./json.js";
4
+ import { getLedgerRuntimeConfig } from "./runtime-config.js";
4
5
  import { resolveLedgerPaths, writeAtomicTextFile } from "./storage/filesystem.js";
6
+ import { forEachLine } from "./text-lines.js";
7
+ const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/u;
5
8
  const countPatchLines = (patchText) => {
6
9
  let addedLines = 0;
7
10
  let removedLines = 0;
8
11
  let insideHunk = false;
9
- for (const line of patchText.split(/\r?\n/u)) {
12
+ forEachLine(patchText, (line) => {
13
+ if (line.startsWith('diff --git ')) {
14
+ insideHunk = false;
15
+ return;
16
+ }
10
17
  if (line.startsWith('@@')) {
11
18
  insideHunk = true;
12
- continue;
19
+ return;
13
20
  }
14
21
  if (!insideHunk || line.startsWith('+++') || line.startsWith('---')) {
15
- continue;
22
+ return;
16
23
  }
17
24
  if (line.startsWith('+')) {
18
25
  addedLines += 1;
19
- continue;
26
+ return;
20
27
  }
21
28
  if (line.startsWith('-')) {
22
29
  removedLines += 1;
23
30
  }
24
- }
31
+ });
25
32
  return { addedLines, removedLines };
26
33
  };
34
+ const formatPatchLimitError = ({ bytes, limitBytes, sourceLabel }) => `Patch input from ${sourceLabel} is ${bytes} bytes, exceeding the configured limit of ${limitBytes} bytes. Reduce the diff size or increase USHMAN_LEDGER_MAX_PATCH_BYTES.`;
27
35
  const buildBlobPath = (workspaceRoot, sha256) => {
28
36
  const paths = resolveLedgerPaths(workspaceRoot);
29
37
  return path.join(paths.blobsDir, sha256.slice(0, 2), `${sha256}.patch`);
30
38
  };
31
- export const storePatchBlob = async (workspaceRoot, patchText) => {
39
+ export const assertValidBlobSha256 = (sha256) => {
40
+ if (!SHA256_HEX_PATTERN.test(sha256)) {
41
+ throw new Error(`Invalid patch blob digest: ${sha256}. Expected a lowercase SHA-256 hex digest.`);
42
+ }
43
+ return sha256;
44
+ };
45
+ export const assertPatchTextWithinLimit = (patchText, sourceLabel) => {
46
+ const limitBytes = getLedgerRuntimeConfig().maxPatchBytes;
32
47
  const bytes = Buffer.byteLength(patchText, 'utf8');
48
+ if (bytes > limitBytes) {
49
+ throw new Error(formatPatchLimitError({ bytes, limitBytes, sourceLabel }));
50
+ }
51
+ return bytes;
52
+ };
53
+ export const readPatchTextFromFile = async (patchPath) => {
54
+ const fileStat = await stat(patchPath);
55
+ const limitBytes = getLedgerRuntimeConfig().maxPatchBytes;
56
+ if (fileStat.size > limitBytes) {
57
+ throw new Error(formatPatchLimitError({ bytes: fileStat.size, limitBytes, sourceLabel: patchPath }));
58
+ }
59
+ return readFile(patchPath, 'utf8');
60
+ };
61
+ export const storePatchBlob = async (workspaceRoot, patchText) => {
62
+ const bytes = assertPatchTextWithinLimit(patchText, 'inline diff text');
33
63
  const blobSha256 = sha256Hex(patchText);
34
64
  const blobPath = buildBlobPath(workspaceRoot, blobSha256);
35
- let shouldWrite = true;
36
65
  try {
37
- shouldWrite = (await sha256File(blobPath)) !== blobSha256;
66
+ await stat(blobPath);
38
67
  }
39
68
  catch (error) {
40
69
  const code = error.code;
41
70
  if (code !== 'ENOENT') {
42
71
  throw error;
43
72
  }
44
- }
45
- if (shouldWrite) {
46
- await mkdir(path.dirname(blobPath), { recursive: true });
47
73
  await writeAtomicTextFile(blobPath, patchText);
48
74
  }
49
75
  return {
@@ -53,6 +79,6 @@ export const storePatchBlob = async (workspaceRoot, patchText) => {
53
79
  };
54
80
  };
55
81
  export const storePatchBlobFromFile = async (workspaceRoot, patchPath) => {
56
- return storePatchBlob(workspaceRoot, await readFile(patchPath, 'utf8'));
82
+ return storePatchBlob(workspaceRoot, await readPatchTextFromFile(patchPath));
57
83
  };
58
- export const resolveBlobPath = (workspaceRoot, sha256) => buildBlobPath(workspaceRoot, sha256);
84
+ export const resolveBlobPath = (workspaceRoot, sha256) => buildBlobPath(workspaceRoot, assertValidBlobSha256(sha256));
@@ -1,5 +1,5 @@
1
1
  import { type LedgerRecord } from './schema/entry.ts';
2
- type BuildRecordInput<TRecord extends LedgerRecord> = Omit<TRecord, 'kind'> & {
2
+ export type BuildRecordInput<TRecord extends LedgerRecord> = Omit<TRecord, 'kind'> & {
3
3
  readonly kind?: TRecord['kind'];
4
4
  };
5
5
  /** Build an `operator-decision` record with a validated structured payload. */
@@ -182,5 +182,4 @@ export declare const buildStripDecisionRevertedRecord: (input: BuildRecordInput<
182
182
  phase: "capture" | "intake" | "seed" | "vendor-extract" | "cleanup" | "parity" | "characterize" | "equiv" | "analyze" | "recover" | "ship" | "migration";
183
183
  summary: string;
184
184
  };
185
- export {};
186
185
  //# sourceMappingURL=builders.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../src/builders.ts"],"names":[],"mappings":"AACA,OAAO,EAGH,KAAK,YAAY,EAIpB,MAAM,mBAAmB,CAAC;AAE3B,KAAK,gBAAgB,CAAC,OAAO,SAAS,YAAY,IAAI,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG;IAC1E,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;CACnC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,2BAA2B,GACpC,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAM3E,CAAC;AAEP,yCAAyC;AACzC,eAAO,MAAM,0BAA0B,GACnC,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAK1E,CAAC;AAEP,sEAAsE;AACtE,eAAO,MAAM,qBAAqB,GAAI,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;CAItG,CAAC;AAEP,mCAAmC;AACnC,eAAO,MAAM,oBAAoB,GAAI,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAIrG,CAAC;AAEP,gDAAgD;AAChD,eAAO,MAAM,gCAAgC,GACzC,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,yBAAyB,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAKjF,CAAC"}
1
+ {"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../src/builders.ts"],"names":[],"mappings":"AACA,OAAO,EAGH,KAAK,YAAY,EAIpB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,gBAAgB,CAAC,OAAO,SAAS,YAAY,IAAI,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG;IACjF,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;CACnC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,2BAA2B,GACpC,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAM3E,CAAC;AAEP,yCAAyC;AACzC,eAAO,MAAM,0BAA0B,GACnC,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAK1E,CAAC;AAEP,sEAAsE;AACtE,eAAO,MAAM,qBAAqB,GAAI,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;CAItG,CAAC;AAEP,mCAAmC;AACnC,eAAO,MAAM,oBAAoB,GAAI,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAIrG,CAAC;AAEP,gDAAgD;AAChD,eAAO,MAAM,gCAAgC,GACzC,OAAO,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,yBAAyB,CAAA;CAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAKjF,CAAC"}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAmDA,KAAK,UAAU,GAAG;IACd,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE;QACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;KAC5B,CAAC;IACF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,cAAc,GAAG,aAAa,CAAC,UAAU,GAAG,MAAM,CAAC,CAAC;IAC3E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;CAC1C,CAAC;AAiwBF,eAAO,MAAM,YAAY,GAAU,MAAM,SAAS,MAAM,EAAE,EAAE,UAAS,OAAO,CAAC,UAAU,CAAM,KAAG,OAAO,CAAC,MAAM,CAqC7G,CAAC;AAEF,eAAO,MAAM,IAAI,GAAU,OAAM,SAAS,MAAM,EAA0B,KAAG,OAAO,CAAC,MAAM,CAE1F,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA2DA,KAAK,UAAU,GAAG;IACd,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE;QACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;KAC5B,CAAC;IACF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,cAAc,GAAG,aAAa,CAAC,UAAU,GAAG,MAAM,CAAC,CAAC;IAC3E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;CAC1C,CAAC;AA29BF,eAAO,MAAM,YAAY,GAAU,MAAM,SAAS,MAAM,EAAE,EAAE,UAAS,OAAO,CAAC,UAAU,CAAM,KAAG,OAAO,CAAC,MAAM,CAqC7G,CAAC;AAEF,eAAO,MAAM,IAAI,GAAU,OAAM,SAAS,MAAM,EAA0B,KAAG,OAAO,CAAC,MAAM,CAE1F,CAAC"}