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.
- package/ARCHITECTURE.md +79 -0
- package/README.md +87 -0
- package/TROUBLESHOOTING.md +170 -0
- package/dist/blobs.d.ts +3 -0
- package/dist/blobs.d.ts.map +1 -1
- package/dist/blobs.js +41 -15
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +85 -27
- package/dist/coverage.d.ts.map +1 -1
- package/dist/coverage.js +3 -2
- package/dist/doctor.d.ts +17 -4
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +224 -57
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +23 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/list.d.ts +2 -1
- package/dist/list.d.ts.map +1 -1
- package/dist/list.js +19 -9
- package/dist/patch-resolver.d.ts.map +1 -1
- package/dist/patch-resolver.js +193 -53
- package/dist/read-index.d.ts.map +1 -1
- package/dist/read-index.js +6 -5
- package/dist/record.d.ts.map +1 -1
- package/dist/record.js +2 -1
- package/dist/runtime-config.d.ts +12 -0
- package/dist/runtime-config.d.ts.map +1 -0
- package/dist/runtime-config.js +83 -0
- package/dist/storage/filesystem.d.ts +1 -0
- package/dist/storage/filesystem.d.ts.map +1 -1
- package/dist/storage/filesystem.js +33 -3
- package/dist/text-lines.d.ts +8 -0
- package/dist/text-lines.d.ts.map +1 -0
- package/dist/text-lines.js +20 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -1
- package/package.json +4 -2
package/ARCHITECTURE.md
ADDED
|
@@ -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
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
buildChangeLogRecord,
|
|
36
36
|
buildValidatorResultRecord,
|
|
37
37
|
deriveFilesChangedFromPatch,
|
|
38
|
+
getLedgerRuntimeConfig,
|
|
38
39
|
openLedger,
|
|
39
40
|
} from 'ushman-ledger';
|
|
40
41
|
|
|
@@ -100,10 +101,26 @@ await ledger.render({ to: 'retro' });
|
|
|
100
101
|
await ledger.render({ to: 'migration-log-md' });
|
|
101
102
|
await ledger.renderTo({ to: 'migration-log-md', out: '/tmp/migration-log.md' });
|
|
102
103
|
await ledger.archive('/tmp/ledger.tgz');
|
|
104
|
+
|
|
105
|
+
const config = getLedgerRuntimeConfig();
|
|
106
|
+
console.log(`Using scan concurrency ${config.scanConcurrency}`);
|
|
103
107
|
```
|
|
104
108
|
|
|
105
109
|
Advanced builder wrappers can reuse the exported `BuildRecordInput<T>` helper type when they want the same input contract the built-in builders accept.
|
|
106
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
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
107
124
|
## CLI
|
|
108
125
|
|
|
109
126
|
```bash
|
|
@@ -122,6 +139,7 @@ ushman-ledger render --workspace=<ws> --to=jsonl --out=/tmp/ledger.jsonl
|
|
|
122
139
|
ushman-ledger render --workspace=<ws> --to=dependency-graph --out=/tmp/ledger.mmd
|
|
123
140
|
ushman-ledger archive --workspace=<ws> --out=/tmp/ledger.tgz
|
|
124
141
|
ushman-ledger doctor --workspace=<ws>
|
|
142
|
+
ushman-ledger doctor --workspace=<ws> --json
|
|
125
143
|
```
|
|
126
144
|
|
|
127
145
|
Valid record kinds: `tool-invocation`, `agent-patch`, `operator-patch`, `operator-decision`, `validator-result`, `runtime-event`, `note`, `correction`, `strip-decision-reverted`, `change-log`
|
|
@@ -218,9 +236,11 @@ Valid render targets: `retro`, `jsonl`, `timeline-html`, `dependency-graph`, `mi
|
|
|
218
236
|
|
|
219
237
|
- `--diff-from-git=<ref>` runs `git diff <ref>` against the workspace working tree.
|
|
220
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.
|
|
221
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.
|
|
222
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.
|
|
223
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>`.
|
|
224
244
|
|
|
225
245
|
## Storage shape
|
|
226
246
|
|
|
@@ -254,3 +274,70 @@ bun run bench:scale
|
|
|
254
274
|
|
|
255
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.
|
|
256
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;
|
package/dist/blobs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blobs.d.ts","sourceRoot":"","sources":["../src/blobs.ts"],"names":[],"mappings":"
|
|
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 {
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
19
|
+
return;
|
|
13
20
|
}
|
|
14
21
|
if (!insideHunk || line.startsWith('+++') || line.startsWith('---')) {
|
|
15
|
-
|
|
22
|
+
return;
|
|
16
23
|
}
|
|
17
24
|
if (line.startsWith('+')) {
|
|
18
25
|
addedLines += 1;
|
|
19
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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));
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
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"}
|