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.
- package/AGENTS.md +7 -5
- package/ARCHITECTURE.md +85 -0
- package/CHANGELOG.md +11 -0
- package/README.md +114 -5
- package/TROUBLESHOOTING.md +184 -0
- package/dist/blobs.d.ts +3 -0
- package/dist/blobs.d.ts.map +1 -1
- package/dist/blobs.js +41 -15
- package/dist/builders.d.ts +33 -0
- package/dist/builders.d.ts.map +1 -1
- package/dist/builders.js +10 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +176 -59
- 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 +263 -62
- package/dist/handle.d.ts.map +1 -1
- package/dist/handle.js +67 -30
- 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 +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/list.d.ts +34 -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/process.d.ts +2 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +16 -0
- package/dist/read-index.d.ts +7 -7
- package/dist/read-index.d.ts.map +1 -1
- package/dist/read-index.js +18 -13
- package/dist/record.js +2 -2
- package/dist/recovery.d.ts +8 -0
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +142 -30
- package/dist/render/retro.d.ts.map +1 -1
- package/dist/render/retro.js +4 -1
- package/dist/runtime-config.d.ts +14 -0
- package/dist/runtime-config.d.ts.map +1 -0
- package/dist/runtime-config.js +97 -0
- package/dist/schema/entry-core.d.ts +5 -2
- package/dist/schema/entry-core.d.ts.map +1 -1
- package/dist/schema/entry-core.js +3 -0
- package/dist/schema/entry-read.d.ts +57 -0
- package/dist/schema/entry-read.d.ts.map +1 -1
- package/dist/schema/entry-read.js +9 -1
- package/dist/schema/entry-write.d.ts +51 -0
- package/dist/schema/entry-write.d.ts.map +1 -1
- package/dist/schema/entry-write.js +9 -1
- package/dist/storage/filesystem.d.ts +15 -2
- package/dist/storage/filesystem.d.ts.map +1 -1
- package/dist/storage/filesystem.js +234 -37
- package/dist/storage/lock.d.ts.map +1 -1
- package/dist/storage/lock.js +38 -16
- 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/dist/doctor.js
CHANGED
|
@@ -4,72 +4,232 @@ import { resolveBlobPath } from "./blobs.js";
|
|
|
4
4
|
import { sha256File } from "./json.js";
|
|
5
5
|
import { getOrderedEntryLocations, readManifestEntryBatch } from "./list.js";
|
|
6
6
|
import { isReadIndexCurrent, readReadIndex } from "./read-index.js";
|
|
7
|
-
import { loadLedgerState } from "./recovery.js";
|
|
7
|
+
import { listPendingCommitQuarantines, loadLedgerState } from "./recovery.js";
|
|
8
|
+
import { getLedgerRuntimeConfig } from "./runtime-config.js";
|
|
8
9
|
import { readManifest } from "./storage/filesystem.js";
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
10
|
+
const HOUR_MS = 60 * 60 * 1_000;
|
|
11
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
12
|
+
const MINUTE_MS = 60 * 1_000;
|
|
13
|
+
export const DOCTOR_FINDING_CODES = [
|
|
14
|
+
'blob-corrupt',
|
|
15
|
+
'blob-missing',
|
|
16
|
+
'blob-unreadable',
|
|
17
|
+
'change-log-rollback-missing-target',
|
|
18
|
+
'change-log-smoke-failure-missing-rollback-plan',
|
|
19
|
+
'manifest-entry-count-mismatch',
|
|
20
|
+
'manifest-entry-location-missing',
|
|
21
|
+
'manifest-entry-missing-on-disk',
|
|
22
|
+
'manifest-last-sequence-mismatch',
|
|
23
|
+
'manifest-per-phase-latest-mismatch',
|
|
24
|
+
'manifest-phase-mismatch',
|
|
25
|
+
'manifest-sequence-mismatch',
|
|
26
|
+
'open-issue-stale',
|
|
27
|
+
'pending-commit-quarantined',
|
|
28
|
+
'phase-prev-entry-mismatch',
|
|
29
|
+
'pre-change-checkpoint-stale',
|
|
30
|
+
'read-failure',
|
|
31
|
+
];
|
|
32
|
+
const createFinding = ({ code, message, metadata, remediation }) => ({
|
|
33
|
+
code,
|
|
34
|
+
message,
|
|
35
|
+
metadata,
|
|
36
|
+
remediation,
|
|
37
|
+
});
|
|
38
|
+
const buildDoctorReport = (findings) => ({
|
|
39
|
+
checkedAt: new Date().toISOString(),
|
|
40
|
+
findings: [...findings],
|
|
41
|
+
issueCount: findings.length,
|
|
42
|
+
issues: findings.map((finding) => finding.message),
|
|
43
|
+
ok: findings.length === 0,
|
|
44
|
+
});
|
|
45
|
+
const formatAgeThreshold = (ageMs) => {
|
|
46
|
+
if (ageMs >= DAY_MS && ageMs % DAY_MS === 0) {
|
|
47
|
+
const days = ageMs / DAY_MS;
|
|
48
|
+
return `${days} day${days === 1 ? '' : 's'}`;
|
|
49
|
+
}
|
|
50
|
+
if (ageMs >= HOUR_MS && ageMs % HOUR_MS === 0) {
|
|
51
|
+
return `${ageMs / HOUR_MS}h`;
|
|
52
|
+
}
|
|
53
|
+
if (ageMs >= MINUTE_MS && ageMs % MINUTE_MS === 0) {
|
|
54
|
+
return `${ageMs / MINUTE_MS}m`;
|
|
55
|
+
}
|
|
56
|
+
return `${ageMs}ms`;
|
|
57
|
+
};
|
|
58
|
+
const pushFinding = (findings, finding) => {
|
|
59
|
+
findings.push(finding);
|
|
60
|
+
};
|
|
61
|
+
const isMissingPathError = (error) => {
|
|
62
|
+
const code = error.code;
|
|
63
|
+
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
64
|
+
};
|
|
65
|
+
const buildReadFailure = (error) => buildDoctorReport([
|
|
66
|
+
createFinding({
|
|
67
|
+
code: 'read-failure',
|
|
68
|
+
message: `Failed to read ledger state: ${error instanceof Error ? (error.message ?? error.name) : String(error)}.`,
|
|
69
|
+
remediation: 'Re-open the ledger or rerun the command first so recovery can reconcile pending state. If the error persists, repair or restore the invalid manifest/read-index JSON before archiving.',
|
|
70
|
+
}),
|
|
71
|
+
]);
|
|
72
|
+
const isChangeLogEntry = (entry) => entry.kind === 'change-log';
|
|
73
|
+
const isOpenIssueNote = (entry) => entry.kind === 'note' && entry.subkind === 'open-issue';
|
|
74
|
+
const checkPrevChain = (entry, findings, previousByPhase) => {
|
|
14
75
|
const expectedPrev = previousByPhase.get(entry.phase) ?? null;
|
|
15
76
|
if (entry.prevEntryId !== expectedPrev) {
|
|
16
|
-
|
|
77
|
+
pushFinding(findings, createFinding({
|
|
78
|
+
code: 'phase-prev-entry-mismatch',
|
|
79
|
+
message: `Phase ${entry.phase} has broken prevEntryId chain at ${entry.id}: expected ${expectedPrev ?? 'null'}, found ${entry.prevEntryId ?? 'null'}.`,
|
|
80
|
+
metadata: {
|
|
81
|
+
entryId: entry.id,
|
|
82
|
+
expectedPrevEntryId: expectedPrev,
|
|
83
|
+
foundPrevEntryId: entry.prevEntryId,
|
|
84
|
+
phase: entry.phase,
|
|
85
|
+
},
|
|
86
|
+
remediation: 'Restore the edited entry or repair the phase chain so prevEntryId matches append order. Use a correction entry for content fixes instead of hand-editing ledger history.',
|
|
87
|
+
}));
|
|
17
88
|
}
|
|
18
89
|
previousByPhase.set(entry.phase, entry.id);
|
|
19
90
|
};
|
|
20
91
|
const checkBlobPresence = async (workspaceRoot, blobChecks) => {
|
|
21
|
-
const
|
|
92
|
+
const { blobHashConcurrency } = getLedgerRuntimeConfig();
|
|
93
|
+
const groupedBlobChecks = new Map();
|
|
94
|
+
for (const { blobHash, entryId } of blobChecks) {
|
|
95
|
+
const entryIds = groupedBlobChecks.get(blobHash) ?? [];
|
|
96
|
+
entryIds.push(entryId);
|
|
97
|
+
groupedBlobChecks.set(blobHash, entryIds);
|
|
98
|
+
}
|
|
99
|
+
const results = await mapWithConcurrencyLimit([...groupedBlobChecks.entries()], blobHashConcurrency, async ([blobHash, entryIds]) => {
|
|
100
|
+
const blobPath = resolveBlobPath(workspaceRoot, blobHash);
|
|
22
101
|
try {
|
|
23
|
-
const blobPath = resolveBlobPath(workspaceRoot, blobHash);
|
|
24
102
|
await stat(blobPath);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (!isMissingPathError(error)) {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
return entryIds.map((entryId) => createFinding({
|
|
109
|
+
code: 'blob-missing',
|
|
110
|
+
message: `Missing blob ${blobHash} for ${entryId}.`,
|
|
111
|
+
metadata: {
|
|
112
|
+
blobSha256: blobHash,
|
|
113
|
+
entryId,
|
|
114
|
+
},
|
|
115
|
+
remediation: 'Restore the missing blob file under .lab/ledger/blobs/ or recreate the patch entry from the original diff before archiving.',
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
25
119
|
const actualHash = await sha256File(blobPath);
|
|
26
120
|
if (actualHash !== blobHash) {
|
|
27
|
-
return
|
|
121
|
+
return entryIds.map((entryId) => createFinding({
|
|
122
|
+
code: 'blob-corrupt',
|
|
123
|
+
message: `Blob ${blobHash} for ${entryId} is corrupted (found ${actualHash}).`,
|
|
124
|
+
metadata: {
|
|
125
|
+
actualSha256: actualHash,
|
|
126
|
+
blobSha256: blobHash,
|
|
127
|
+
entryId,
|
|
128
|
+
},
|
|
129
|
+
remediation: 'Restore the original blob content under .lab/ledger/blobs/ or recreate the patch entry from the original diff before archiving.',
|
|
130
|
+
}));
|
|
28
131
|
}
|
|
29
|
-
return
|
|
132
|
+
return [];
|
|
30
133
|
}
|
|
31
|
-
catch {
|
|
32
|
-
return
|
|
134
|
+
catch (error) {
|
|
135
|
+
return entryIds.map((entryId) => createFinding({
|
|
136
|
+
code: 'blob-unreadable',
|
|
137
|
+
message: `Blob ${blobHash} for ${entryId} could not be read: ${error instanceof Error ? error.message : String(error)}.`,
|
|
138
|
+
metadata: {
|
|
139
|
+
blobSha256: blobHash,
|
|
140
|
+
entryId,
|
|
141
|
+
},
|
|
142
|
+
remediation: 'Fix the filesystem permission or read error first, then rerun doctor so blob integrity can be verified before archiving.',
|
|
143
|
+
}));
|
|
33
144
|
}
|
|
34
145
|
});
|
|
35
|
-
return results.
|
|
146
|
+
return results.flat();
|
|
147
|
+
};
|
|
148
|
+
const checkPendingCommitQuarantines = async (workspaceRoot) => {
|
|
149
|
+
const quarantines = await listPendingCommitQuarantines(workspaceRoot);
|
|
150
|
+
return quarantines.map((quarantine) => createFinding({
|
|
151
|
+
code: 'pending-commit-quarantined',
|
|
152
|
+
message: `Pending commit journal ${quarantine.originalFileName} is quarantined: ${quarantine.reason}`,
|
|
153
|
+
metadata: {
|
|
154
|
+
commitPath: quarantine.commitPath,
|
|
155
|
+
metadataPath: quarantine.metadataPath,
|
|
156
|
+
originalFileName: quarantine.originalFileName,
|
|
157
|
+
quarantinedAt: quarantine.quarantinedAt,
|
|
158
|
+
},
|
|
159
|
+
remediation: 'Inspect the quarantined journal under .lab/ledger/pending-quarantine/. Restore it to .lab/ledger/pending/ only if its entry, sequence, and manifest base are known to be safe; otherwise keep or remove it after recording an operator decision.',
|
|
160
|
+
}));
|
|
36
161
|
};
|
|
37
|
-
const checkManifestCounts = (manifest, entryCount) => {
|
|
38
|
-
const issues = [];
|
|
162
|
+
const checkManifestCounts = (findings, manifest, entryCount) => {
|
|
39
163
|
if (manifest.entryCount !== entryCount) {
|
|
40
|
-
|
|
164
|
+
pushFinding(findings, createFinding({
|
|
165
|
+
code: 'manifest-entry-count-mismatch',
|
|
166
|
+
message: `Manifest entryCount ${manifest.entryCount} does not match disk entries ${entryCount}.`,
|
|
167
|
+
metadata: {
|
|
168
|
+
diskEntryCount: entryCount,
|
|
169
|
+
manifestEntryCount: manifest.entryCount,
|
|
170
|
+
},
|
|
171
|
+
remediation: 'Rerun the command or reopen the ledger so recovery can replay pending commits. If the mismatch persists, compare manifest.json with the on-disk phase entries and repair the missing location or file.',
|
|
172
|
+
}));
|
|
41
173
|
}
|
|
42
174
|
if (manifest.lastSequence !== entryCount) {
|
|
43
|
-
|
|
175
|
+
pushFinding(findings, createFinding({
|
|
176
|
+
code: 'manifest-last-sequence-mismatch',
|
|
177
|
+
message: `Manifest lastSequence ${manifest.lastSequence} does not match disk entries ${entryCount}.`,
|
|
178
|
+
metadata: {
|
|
179
|
+
diskEntryCount: entryCount,
|
|
180
|
+
manifestLastSequence: manifest.lastSequence,
|
|
181
|
+
},
|
|
182
|
+
remediation: 'Repair manifest.json so lastSequence matches the highest durable append sequence after recovery completes.',
|
|
183
|
+
}));
|
|
44
184
|
}
|
|
45
|
-
return issues;
|
|
46
185
|
};
|
|
47
|
-
const finalizeManifestChecks = ({ entryCount,
|
|
48
|
-
|
|
186
|
+
const finalizeManifestChecks = ({ entryCount, findings, latestByPhase, manifest, unseenManifestEntryIds, }) => {
|
|
187
|
+
checkManifestCounts(findings, manifest, entryCount);
|
|
49
188
|
for (const entryId of unseenManifestEntryIds) {
|
|
50
|
-
|
|
189
|
+
pushFinding(findings, createFinding({
|
|
190
|
+
code: 'manifest-entry-missing-on-disk',
|
|
191
|
+
message: `Manifest entry location points to missing disk entry ${entryId}.`,
|
|
192
|
+
metadata: { entryId },
|
|
193
|
+
remediation: 'Restore the missing entry file or repair manifest.entryLocations so every referenced entry id exists on disk before archiving.',
|
|
194
|
+
}));
|
|
51
195
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
196
|
+
const phasesToCheck = new Set([...Object.keys(manifest.perPhaseLatest), ...latestByPhase.keys()]);
|
|
197
|
+
for (const phase of phasesToCheck) {
|
|
198
|
+
const latest = latestByPhase.get(phase);
|
|
199
|
+
const expectedEntryId = latest?.entryId ?? null;
|
|
200
|
+
const foundEntryId = manifest.perPhaseLatest[phase] ?? null;
|
|
201
|
+
if (foundEntryId !== expectedEntryId) {
|
|
202
|
+
pushFinding(findings, createFinding({
|
|
203
|
+
code: 'manifest-per-phase-latest-mismatch',
|
|
204
|
+
message: `Manifest perPhaseLatest mismatch for ${phase}: expected ${expectedEntryId ?? 'missing'}, found ${foundEntryId ?? 'missing'}.`,
|
|
205
|
+
metadata: {
|
|
206
|
+
expectedEntryId,
|
|
207
|
+
foundEntryId,
|
|
208
|
+
phase,
|
|
209
|
+
},
|
|
210
|
+
remediation: 'Repair perPhaseLatest so each phase points at the newest durable entry in that phase.',
|
|
211
|
+
}));
|
|
55
212
|
}
|
|
56
213
|
}
|
|
57
214
|
};
|
|
58
|
-
const checkManifestSequenceOrder = (entryLocations,
|
|
215
|
+
const checkManifestSequenceOrder = (entryLocations, findings) => {
|
|
59
216
|
for (let index = 0; index < entryLocations.length; index += 1) {
|
|
60
217
|
const [entryId, location] = entryLocations[index];
|
|
61
218
|
const expectedSequence = index + 1;
|
|
62
219
|
if (location.sequence !== expectedSequence) {
|
|
63
|
-
|
|
220
|
+
pushFinding(findings, createFinding({
|
|
221
|
+
code: 'manifest-sequence-mismatch',
|
|
222
|
+
message: `Manifest sequence mismatch for ${entryId}: expected ${expectedSequence}, found ${location.sequence}.`,
|
|
223
|
+
metadata: {
|
|
224
|
+
entryId,
|
|
225
|
+
expectedSequence,
|
|
226
|
+
foundSequence: location.sequence,
|
|
227
|
+
},
|
|
228
|
+
remediation: 'Repair manifest.entryLocations so sequences remain contiguous and match append order.',
|
|
229
|
+
}));
|
|
64
230
|
}
|
|
65
231
|
}
|
|
66
232
|
};
|
|
67
|
-
const buildReadFailure = (error) => ({
|
|
68
|
-
issues: [`Failed to read ledger state: ${error instanceof Error ? (error.message ?? error.name) : String(error)}.`],
|
|
69
|
-
ok: false,
|
|
70
|
-
});
|
|
71
|
-
const isChangeLogEntry = (entry) => entry.kind === 'change-log';
|
|
72
|
-
const isOpenIssueNote = (entry) => entry.kind === 'note' && entry.subkind === 'open-issue';
|
|
73
233
|
const trackResolutionLinks = (entry, resolvedLedgerIds) => {
|
|
74
234
|
if (entry.links.correctsLedgerId) {
|
|
75
235
|
resolvedLedgerIds.add(entry.links.correctsLedgerId);
|
|
@@ -87,35 +247,64 @@ const trackIdempotencyEntry = (entry, entriesByIdempotencyKey) => {
|
|
|
87
247
|
existingEntries.push({ id: entry.id, ts: entry.ts });
|
|
88
248
|
entriesByIdempotencyKey.set(idempotencyKey, existingEntries);
|
|
89
249
|
};
|
|
90
|
-
const checkChangeLogWarnings = ({ checkpointEntries, entriesByIdempotencyKey,
|
|
250
|
+
const checkChangeLogWarnings = ({ checkpointEntries, checkpointMaxAgeMs, entriesByIdempotencyKey, findings, nowMs, openIssueMaxAgeMs, openIssueEntries, resolvedLedgerIds, }) => {
|
|
251
|
+
const checkpointMaxAgeLabel = formatAgeThreshold(checkpointMaxAgeMs);
|
|
91
252
|
for (const checkpointEntry of checkpointEntries) {
|
|
92
253
|
const ageMs = nowMs - Date.parse(checkpointEntry.ts);
|
|
93
|
-
if (ageMs <=
|
|
254
|
+
if (ageMs <= checkpointMaxAgeMs) {
|
|
94
255
|
continue;
|
|
95
256
|
}
|
|
96
257
|
const idempotencyKey = checkpointEntry.links.idempotencyKey;
|
|
97
258
|
const hasFollowUp = typeof idempotencyKey === 'string' &&
|
|
98
259
|
(entriesByIdempotencyKey.get(idempotencyKey) ?? []).some((candidate) => candidate.id !== checkpointEntry.id && candidate.ts >= checkpointEntry.ts);
|
|
99
260
|
if (!hasFollowUp) {
|
|
100
|
-
|
|
261
|
+
pushFinding(findings, createFinding({
|
|
262
|
+
code: 'pre-change-checkpoint-stale',
|
|
263
|
+
message: `Pre-change checkpoint ${checkpointEntry.id} is older than ${checkpointMaxAgeLabel} and has no follow-up entry with matching idempotencyKey.`,
|
|
264
|
+
metadata: {
|
|
265
|
+
entryId: checkpointEntry.id,
|
|
266
|
+
idempotencyKey: idempotencyKey ?? null,
|
|
267
|
+
},
|
|
268
|
+
remediation: 'Append a follow-up change-log entry with the same idempotency key, or close the stale checkpoint with a correction entry explaining the abandoned work.',
|
|
269
|
+
}));
|
|
101
270
|
}
|
|
102
271
|
}
|
|
272
|
+
const openIssueMaxAgeLabel = formatAgeThreshold(openIssueMaxAgeMs);
|
|
103
273
|
for (const openIssueEntry of openIssueEntries) {
|
|
104
274
|
const ageMs = nowMs - Date.parse(openIssueEntry.ts);
|
|
105
|
-
if (ageMs <=
|
|
275
|
+
if (ageMs <= openIssueMaxAgeMs || resolvedLedgerIds.has(openIssueEntry.id)) {
|
|
106
276
|
continue;
|
|
107
277
|
}
|
|
108
|
-
|
|
278
|
+
pushFinding(findings, createFinding({
|
|
279
|
+
code: 'open-issue-stale',
|
|
280
|
+
message: `Open issue note ${openIssueEntry.id} is older than ${openIssueMaxAgeLabel} and has no resolution link.`,
|
|
281
|
+
metadata: { entryId: openIssueEntry.id },
|
|
282
|
+
remediation: 'Append a correction or superseding note that links back to the open issue once the follow-up is complete.',
|
|
283
|
+
}));
|
|
109
284
|
}
|
|
110
285
|
};
|
|
111
|
-
const inspectManifestLocation = ({ entry,
|
|
286
|
+
const inspectManifestLocation = ({ entry, findings, latestByPhase, manifest, }) => {
|
|
112
287
|
const manifestLocation = manifest.entryLocations[entry.id];
|
|
113
288
|
if (!manifestLocation) {
|
|
114
|
-
|
|
289
|
+
pushFinding(findings, createFinding({
|
|
290
|
+
code: 'manifest-entry-location-missing',
|
|
291
|
+
message: `Manifest is missing entry location for ${entry.id}.`,
|
|
292
|
+
metadata: { entryId: entry.id },
|
|
293
|
+
remediation: 'Repair manifest.entryLocations so every durable entry has a phase/sequence location before archiving.',
|
|
294
|
+
}));
|
|
115
295
|
return null;
|
|
116
296
|
}
|
|
117
297
|
if (manifestLocation.phase !== entry.phase) {
|
|
118
|
-
|
|
298
|
+
pushFinding(findings, createFinding({
|
|
299
|
+
code: 'manifest-phase-mismatch',
|
|
300
|
+
message: `Manifest phase mismatch for ${entry.id}: expected ${entry.phase}, found ${manifestLocation.phase}.`,
|
|
301
|
+
metadata: {
|
|
302
|
+
entryId: entry.id,
|
|
303
|
+
expectedPhase: entry.phase,
|
|
304
|
+
foundPhase: manifestLocation.phase,
|
|
305
|
+
},
|
|
306
|
+
remediation: 'Move the entry back to the correct phase directory or repair manifest.entryLocations so the recorded phase matches the stored entry.',
|
|
307
|
+
}));
|
|
119
308
|
}
|
|
120
309
|
const currentLatest = latestByPhase.get(entry.phase);
|
|
121
310
|
if (!currentLatest || manifestLocation.sequence > currentLatest.sequence) {
|
|
@@ -123,16 +312,26 @@ const inspectManifestLocation = ({ entry, issues, latestByPhase, manifest, }) =>
|
|
|
123
312
|
}
|
|
124
313
|
return manifestLocation;
|
|
125
314
|
};
|
|
126
|
-
const inspectNarrativeEntry = ({ checkpointEntries, entry,
|
|
315
|
+
const inspectNarrativeEntry = ({ checkpointEntries, entry, findings, openIssueEntries, }) => {
|
|
127
316
|
if (isChangeLogEntry(entry)) {
|
|
128
317
|
if (entry.subkind === 'pre-change-checkpoint') {
|
|
129
318
|
checkpointEntries.push(entry);
|
|
130
319
|
}
|
|
131
320
|
if (entry.smokeResult === 'fail' && !entry.rollbackPlan) {
|
|
132
|
-
|
|
321
|
+
pushFinding(findings, createFinding({
|
|
322
|
+
code: 'change-log-smoke-failure-missing-rollback-plan',
|
|
323
|
+
message: `Change-log entry ${entry.id} has smokeResult=fail but no rollbackPlan.`,
|
|
324
|
+
metadata: { entryId: entry.id },
|
|
325
|
+
remediation: 'Append a correction or follow-up change-log entry documenting the rollback plan before treating the failure as closed.',
|
|
326
|
+
}));
|
|
133
327
|
}
|
|
134
328
|
if (entry.subkind === 'rollback' && !entry.rollsBack) {
|
|
135
|
-
|
|
329
|
+
pushFinding(findings, createFinding({
|
|
330
|
+
code: 'change-log-rollback-missing-target',
|
|
331
|
+
message: `Change-log rollback entry ${entry.id} is missing rollsBack.`,
|
|
332
|
+
metadata: { entryId: entry.id },
|
|
333
|
+
remediation: 'Append a correction or replacement rollback entry with rollsBack pointing at the reverted ledger entry id.',
|
|
334
|
+
}));
|
|
136
335
|
}
|
|
137
336
|
}
|
|
138
337
|
if (isOpenIssueNote(entry)) {
|
|
@@ -147,14 +346,14 @@ const inspectPatchEntry = ({ blobChecks, entry, }) => {
|
|
|
147
346
|
blobChecks.push({ blobHash, entryId: entry.id });
|
|
148
347
|
}
|
|
149
348
|
};
|
|
150
|
-
const inspectDoctorEntry = ({ blobChecks, checkpointEntries,
|
|
349
|
+
const inspectDoctorEntry = ({ blobChecks, checkpointEntries, entriesByIdempotencyKey, entry, findings, latestByPhase, manifest, openIssueEntries, previousByPhase, resolvedLedgerIds, unseenManifestEntryIds, }) => {
|
|
151
350
|
unseenManifestEntryIds.delete(entry.id);
|
|
152
|
-
checkPrevChain(entry,
|
|
351
|
+
checkPrevChain(entry, findings, previousByPhase);
|
|
153
352
|
trackIdempotencyEntry(entry, entriesByIdempotencyKey);
|
|
154
353
|
trackResolutionLinks(entry, resolvedLedgerIds);
|
|
155
354
|
if (!inspectManifestLocation({
|
|
156
355
|
entry,
|
|
157
|
-
|
|
356
|
+
findings,
|
|
158
357
|
latestByPhase,
|
|
159
358
|
manifest,
|
|
160
359
|
})) {
|
|
@@ -163,7 +362,7 @@ const inspectDoctorEntry = ({ blobChecks, checkpointEntries, entry, entriesByIde
|
|
|
163
362
|
inspectNarrativeEntry({
|
|
164
363
|
checkpointEntries,
|
|
165
364
|
entry,
|
|
166
|
-
|
|
365
|
+
findings,
|
|
167
366
|
openIssueEntries,
|
|
168
367
|
});
|
|
169
368
|
inspectPatchEntry({
|
|
@@ -172,7 +371,7 @@ const inspectDoctorEntry = ({ blobChecks, checkpointEntries, entry, entriesByIde
|
|
|
172
371
|
});
|
|
173
372
|
};
|
|
174
373
|
const collectDoctorState = async (workspaceRoot, manifest, readIndex) => {
|
|
175
|
-
const
|
|
374
|
+
const findings = [];
|
|
176
375
|
const previousByPhase = new Map();
|
|
177
376
|
const latestByPhase = new Map();
|
|
178
377
|
const unseenManifestEntryIds = new Set(Object.keys(manifest.entryLocations));
|
|
@@ -183,13 +382,15 @@ const collectDoctorState = async (workspaceRoot, manifest, readIndex) => {
|
|
|
183
382
|
const resolvedLedgerIds = new Set();
|
|
184
383
|
let entryCount = 0;
|
|
185
384
|
const nowMs = Date.now();
|
|
385
|
+
const { doctorCheckpointMaxAgeMs, doctorOpenIssueMaxAgeMs, scanBatchSize, scanConcurrency } = getLedgerRuntimeConfig();
|
|
186
386
|
const orderedEntries = getOrderedEntryLocations(manifest, readIndex, {});
|
|
187
|
-
checkManifestSequenceOrder(orderedEntries,
|
|
188
|
-
for (let index = 0; index < orderedEntries.length; index +=
|
|
189
|
-
const batch = orderedEntries.slice(index, index +
|
|
387
|
+
checkManifestSequenceOrder(orderedEntries, findings);
|
|
388
|
+
for (let index = 0; index < orderedEntries.length; index += scanBatchSize) {
|
|
389
|
+
const batch = orderedEntries.slice(index, index + scanBatchSize);
|
|
190
390
|
const resolvedEntries = await readManifestEntryBatch({
|
|
191
391
|
allowMissing: true,
|
|
192
392
|
entryLocations: batch,
|
|
393
|
+
entryReadConcurrency: scanConcurrency,
|
|
193
394
|
workspaceRoot,
|
|
194
395
|
});
|
|
195
396
|
for (const resolvedEntry of resolvedEntries) {
|
|
@@ -202,7 +403,7 @@ const collectDoctorState = async (workspaceRoot, manifest, readIndex) => {
|
|
|
202
403
|
checkpointEntries,
|
|
203
404
|
entriesByIdempotencyKey,
|
|
204
405
|
entry: resolvedEntry.entry,
|
|
205
|
-
|
|
406
|
+
findings,
|
|
206
407
|
latestByPhase,
|
|
207
408
|
manifest,
|
|
208
409
|
openIssueEntries,
|
|
@@ -214,16 +415,18 @@ const collectDoctorState = async (workspaceRoot, manifest, readIndex) => {
|
|
|
214
415
|
}
|
|
215
416
|
checkChangeLogWarnings({
|
|
216
417
|
checkpointEntries,
|
|
418
|
+
checkpointMaxAgeMs: doctorCheckpointMaxAgeMs,
|
|
217
419
|
entriesByIdempotencyKey,
|
|
218
|
-
|
|
420
|
+
findings,
|
|
219
421
|
nowMs,
|
|
422
|
+
openIssueMaxAgeMs: doctorOpenIssueMaxAgeMs,
|
|
220
423
|
openIssueEntries,
|
|
221
424
|
resolvedLedgerIds,
|
|
222
425
|
});
|
|
223
426
|
return {
|
|
224
427
|
blobChecks,
|
|
225
428
|
entryCount,
|
|
226
|
-
|
|
429
|
+
findings,
|
|
227
430
|
latestByPhase,
|
|
228
431
|
unseenManifestEntryIds,
|
|
229
432
|
};
|
|
@@ -256,17 +459,15 @@ export const runLedgerDoctor = async (workspaceRoot, options = {}) => {
|
|
|
256
459
|
catch (error) {
|
|
257
460
|
return buildReadFailure(error);
|
|
258
461
|
}
|
|
259
|
-
const { blobChecks, entryCount,
|
|
260
|
-
|
|
462
|
+
const { blobChecks, entryCount, findings, latestByPhase, unseenManifestEntryIds } = doctorState;
|
|
463
|
+
findings.push(...(await checkPendingCommitQuarantines(workspaceRoot)));
|
|
464
|
+
findings.push(...(await checkBlobPresence(workspaceRoot, blobChecks)));
|
|
261
465
|
finalizeManifestChecks({
|
|
262
466
|
entryCount,
|
|
263
|
-
|
|
467
|
+
findings,
|
|
264
468
|
latestByPhase,
|
|
265
469
|
manifest: preparedState.manifest,
|
|
266
470
|
unseenManifestEntryIds,
|
|
267
471
|
});
|
|
268
|
-
return
|
|
269
|
-
issues,
|
|
270
|
-
ok: issues.length === 0,
|
|
271
|
-
};
|
|
472
|
+
return buildDoctorReport(findings);
|
|
272
473
|
};
|
package/dist/handle.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../src/handle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAA8B,KAAK,YAAY,EAAe,MAAM,WAAW,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAStD,OAAO,KAAK,EAAE,WAAW,EAAc,WAAW,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../src/handle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAA8B,KAAK,YAAY,EAAe,MAAM,WAAW,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAStD,OAAO,KAAK,EAAE,WAAW,EAAc,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAQ9E,MAAM,MAAM,YAAY,GAClB,kBAAkB,GAClB,OAAO,GACP,kBAAkB,GAClB,OAAO,GACP,eAAe,GACf,wBAAwB,CAAC;AAE/B;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEnE,iGAAiG;AACjG,MAAM,MAAM,mBAAmB,GAAG;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC;CAC7B,CAAC;AAEF,2HAA2H;AAC3H,MAAM,MAAM,qBAAqB,GAAG;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;CACjC,CAAC;AA+KF,MAAM,MAAM,YAAY,GAAG;IACvB,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,QAAQ,CAAC,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC;IACrF,QAAQ,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC;IAC5E,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,YAAY,KAAK,aAAa,CAAC,WAAW,CAAC,CAAC;IACrE,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtG,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CACnE,CAAC;AAEF,yFAAyF;AACzF,eAAO,MAAM,UAAU,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,YAAY,CA6E5E,CAAC"}
|
package/dist/handle.js
CHANGED
|
@@ -13,7 +13,7 @@ import { renderMigrationLogMarkdown } from "./render/migration-log.js";
|
|
|
13
13
|
import { renderRetroMarkdown } from "./render/retro.js";
|
|
14
14
|
import { renderTimelineHtml } from "./render/timeline-html.js";
|
|
15
15
|
import { renderWorkspaceNarrativeMarkdown } from "./render/workspace-narrative.js";
|
|
16
|
-
import { createAtomicTextFileWriter, resolveLedgerPaths, writeAtomicTextFile } from "./storage/filesystem.js";
|
|
16
|
+
import { createAtomicTextFileWriter, createExternalTempFileRegistrar, resolveLedgerPaths, writeAtomicTextFile, } from "./storage/filesystem.js";
|
|
17
17
|
const createEntryIteratorFactory = ({ filter, state, workspaceRoot, }) => {
|
|
18
18
|
return (entryOptions = {}) => iterateEntriesFromManifest(workspaceRoot, state.manifest, state.readIndex, {
|
|
19
19
|
kind: entryOptions.kind,
|
|
@@ -29,6 +29,22 @@ const collectRenderedText = async (emit) => {
|
|
|
29
29
|
});
|
|
30
30
|
return chunks.join('');
|
|
31
31
|
};
|
|
32
|
+
const attachCleanupCause = (error, cleanupError) => {
|
|
33
|
+
if (error instanceof Error) {
|
|
34
|
+
const errorWithCause = error;
|
|
35
|
+
if (errorWithCause.cause === undefined) {
|
|
36
|
+
Object.defineProperty(errorWithCause, 'cause', {
|
|
37
|
+
configurable: true,
|
|
38
|
+
enumerable: false,
|
|
39
|
+
value: cleanupError,
|
|
40
|
+
writable: true,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return error;
|
|
44
|
+
}
|
|
45
|
+
return new Error(String(error), { cause: cleanupError });
|
|
46
|
+
};
|
|
47
|
+
const getExternalTempRegistrar = (workspaceRoot, outPath) => outPath ? createExternalTempFileRegistrar(workspaceRoot) : undefined;
|
|
32
48
|
const emitRenderedTarget = async ({ createEntries, manifest, target, write, workspaceName, }) => {
|
|
33
49
|
switch (target) {
|
|
34
50
|
case 'dependency-graph':
|
|
@@ -82,6 +98,40 @@ const resolveRenderOutputPath = (workspaceRoot, target) => {
|
|
|
82
98
|
return null;
|
|
83
99
|
}
|
|
84
100
|
};
|
|
101
|
+
const runRenderToWriters = async ({ atomicWriter, createEntries, manifest, target, workspaceName, writer, }) => {
|
|
102
|
+
let hasWrittenContent = false;
|
|
103
|
+
let endsWithNewline = false;
|
|
104
|
+
try {
|
|
105
|
+
await emitRenderedTarget({
|
|
106
|
+
createEntries,
|
|
107
|
+
manifest,
|
|
108
|
+
target,
|
|
109
|
+
workspaceName,
|
|
110
|
+
write: async (chunk) => {
|
|
111
|
+
if (chunk.length > 0) {
|
|
112
|
+
hasWrittenContent = true;
|
|
113
|
+
endsWithNewline = chunk.endsWith('\n');
|
|
114
|
+
}
|
|
115
|
+
await writer?.(chunk);
|
|
116
|
+
await atomicWriter?.write(chunk);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
if (!hasWrittenContent || !endsWithNewline) {
|
|
120
|
+
await writer?.('\n');
|
|
121
|
+
await atomicWriter?.write('\n');
|
|
122
|
+
}
|
|
123
|
+
await atomicWriter?.close();
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
try {
|
|
127
|
+
await atomicWriter?.abort();
|
|
128
|
+
}
|
|
129
|
+
catch (cleanupError) {
|
|
130
|
+
throw attachCleanupCause(error, cleanupError);
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
85
135
|
/** Open a workspace ledger handle after reconciling any pending crash-recovery state. */
|
|
86
136
|
export const openLedger = async (workspaceRoot) => {
|
|
87
137
|
await readLabManifestMin(workspaceRoot);
|
|
@@ -115,7 +165,9 @@ export const openLedger = async (workspaceRoot) => {
|
|
|
115
165
|
}));
|
|
116
166
|
const outputPath = options.out ?? resolveRenderOutputPath(workspaceRoot, options.to);
|
|
117
167
|
if (outputPath) {
|
|
118
|
-
await writeAtomicTextFile(outputPath, `${content}${content.endsWith('\n') ? '' : '\n'}
|
|
168
|
+
await writeAtomicTextFile(outputPath, `${content}${content.endsWith('\n') ? '' : '\n'}`, {
|
|
169
|
+
registerTempFile: getExternalTempRegistrar(workspaceRoot, options.out),
|
|
170
|
+
});
|
|
119
171
|
}
|
|
120
172
|
return content;
|
|
121
173
|
},
|
|
@@ -127,38 +179,23 @@ export const openLedger = async (workspaceRoot) => {
|
|
|
127
179
|
workspaceRoot,
|
|
128
180
|
});
|
|
129
181
|
const outputPath = options.out ?? resolveRenderOutputPath(workspaceRoot, options.to);
|
|
130
|
-
const atomicWriter = outputPath
|
|
182
|
+
const atomicWriter = outputPath
|
|
183
|
+
? await createAtomicTextFileWriter(outputPath, {
|
|
184
|
+
registerTempFile: getExternalTempRegistrar(workspaceRoot, options.out),
|
|
185
|
+
})
|
|
186
|
+
: null;
|
|
131
187
|
const writer = options.write;
|
|
132
|
-
let hasWrittenContent = false;
|
|
133
|
-
let endsWithNewline = false;
|
|
134
188
|
if (!atomicWriter && !writer) {
|
|
135
189
|
throw new Error(`Render target ${options.to} requires either a writer callback or an output path.`);
|
|
136
190
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
hasWrittenContent = true;
|
|
146
|
-
endsWithNewline = chunk.endsWith('\n');
|
|
147
|
-
}
|
|
148
|
-
await writer?.(chunk);
|
|
149
|
-
await atomicWriter?.write(chunk);
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
if (!hasWrittenContent || !endsWithNewline) {
|
|
153
|
-
await writer?.('\n');
|
|
154
|
-
await atomicWriter?.write('\n');
|
|
155
|
-
}
|
|
156
|
-
await atomicWriter?.close();
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
await atomicWriter?.abort();
|
|
160
|
-
throw error;
|
|
161
|
-
}
|
|
191
|
+
await runRenderToWriters({
|
|
192
|
+
atomicWriter,
|
|
193
|
+
createEntries,
|
|
194
|
+
manifest: state.manifest,
|
|
195
|
+
target: options.to,
|
|
196
|
+
workspaceName: path.basename(workspaceRoot),
|
|
197
|
+
writer,
|
|
198
|
+
});
|
|
162
199
|
},
|
|
163
200
|
show: async (entryId) => {
|
|
164
201
|
const { manifest } = await loadLedgerState(workspaceRoot);
|
package/dist/helpers.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export declare const createWorkspaceFixture: () => Promise<{
|
|
|
4
4
|
}>;
|
|
5
5
|
export declare const ageFile: (filePath: string, ageMs?: number) => Promise<void>;
|
|
6
6
|
export declare const waitForFile: (filePath: string, timeoutMs?: number) => Promise<void>;
|
|
7
|
+
export declare const withEnvOverrides: <Value>(overrides: Record<string, string | undefined>, action: () => Promise<Value>) => Promise<Value>;
|
|
7
8
|
//# sourceMappingURL=helpers.d.ts.map
|
package/dist/helpers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB;;;EAuBlC,CAAC;AAEF,eAAO,MAAM,OAAO,GAAU,UAAU,MAAM,EAAE,cAAc,kBAG7D,CAAC;AAEF,eAAO,MAAM,WAAW,GAAU,UAAU,MAAM,EAAE,kBAAiB,kBAcpE,CAAC"}
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB;;;EAuBlC,CAAC;AAEF,eAAO,MAAM,OAAO,GAAU,UAAU,MAAM,EAAE,cAAc,kBAG7D,CAAC;AAEF,eAAO,MAAM,WAAW,GAAU,UAAU,MAAM,EAAE,kBAAiB,kBAcpE,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAU,KAAK,EACxC,WAAW,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EAC7C,QAAQ,MAAM,OAAO,CAAC,KAAK,CAAC,mBAuB/B,CAAC"}
|
package/dist/helpers.js
CHANGED
|
@@ -36,3 +36,26 @@ export const waitForFile = async (filePath, timeoutMs = 1_000) => {
|
|
|
36
36
|
}
|
|
37
37
|
throw new Error(`Timed out waiting for file: ${filePath}`);
|
|
38
38
|
};
|
|
39
|
+
export const withEnvOverrides = async (overrides, action) => {
|
|
40
|
+
const previousValues = new Map();
|
|
41
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
42
|
+
previousValues.set(key, process.env[key]);
|
|
43
|
+
if (value === undefined) {
|
|
44
|
+
delete process.env[key];
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
process.env[key] = value;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return await action();
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
for (const [key, value] of previousValues.entries()) {
|
|
54
|
+
if (value === undefined) {
|
|
55
|
+
delete process.env[key];
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
process.env[key] = value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|