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.
- package/ARCHITECTURE.md +79 -0
- package/README.md +144 -5
- 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/builders.d.ts +1 -2
- package/dist/builders.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +231 -70
- 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 +225 -58
- package/dist/handle.d.ts +27 -7
- package/dist/handle.d.ts.map +1 -1
- package/dist/handle.js +96 -20
- 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 +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/list.d.ts +3 -2
- package/dist/list.d.ts.map +1 -1
- package/dist/list.js +24 -12
- package/dist/note.d.ts +7 -0
- package/dist/note.d.ts.map +1 -1
- package/dist/note.js +6 -0
- package/dist/patch-resolver.d.ts +12 -0
- package/dist/patch-resolver.d.ts.map +1 -1
- package/dist/patch-resolver.js +205 -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 +3 -3
- package/dist/render/migration-log.d.ts +8 -1
- package/dist/render/migration-log.d.ts.map +1 -1
- package/dist/render/migration-log.js +40 -33
- package/dist/render/retro.d.ts.map +1 -1
- package/dist/render/retro.js +1 -7
- package/dist/render/workspace-narrative.d.ts +7 -1
- package/dist/render/workspace-narrative.d.ts.map +1 -1
- package/dist/render/workspace-narrative.js +114 -46
- 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/schema/entry-read.d.ts.map +1 -1
- package/dist/schema/entry-read.js +1 -1
- package/dist/schema/entry-write.d.ts.map +1 -1
- package/dist/schema/entry-write.js +1 -1
- package/dist/schema/entry.d.ts.map +1 -1
- package/dist/storage/filesystem.d.ts +8 -0
- package/dist/storage/filesystem.d.ts.map +1 -1
- package/dist/storage/filesystem.js +110 -5
- 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 +5 -3
package/dist/doctor.js
CHANGED
|
@@ -5,71 +5,202 @@ import { sha256File } from "./json.js";
|
|
|
5
5
|
import { getOrderedEntryLocations, readManifestEntryBatch } from "./list.js";
|
|
6
6
|
import { isReadIndexCurrent, readReadIndex } from "./read-index.js";
|
|
7
7
|
import { loadLedgerState } from "./recovery.js";
|
|
8
|
+
import { getLedgerRuntimeConfig } from "./runtime-config.js";
|
|
8
9
|
import { readManifest } from "./storage/filesystem.js";
|
|
9
|
-
const BLOB_HASH_CONCURRENCY = 16;
|
|
10
10
|
const CHECKPOINT_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
|
|
11
|
-
const ENTRY_READ_BATCH_SIZE = 32;
|
|
12
11
|
const OPEN_ISSUE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
13
|
-
const
|
|
12
|
+
export const DOCTOR_FINDING_CODES = [
|
|
13
|
+
'blob-corrupt',
|
|
14
|
+
'blob-missing',
|
|
15
|
+
'blob-unreadable',
|
|
16
|
+
'change-log-rollback-missing-target',
|
|
17
|
+
'change-log-smoke-failure-missing-rollback-plan',
|
|
18
|
+
'manifest-entry-count-mismatch',
|
|
19
|
+
'manifest-entry-location-missing',
|
|
20
|
+
'manifest-entry-missing-on-disk',
|
|
21
|
+
'manifest-last-sequence-mismatch',
|
|
22
|
+
'manifest-per-phase-latest-mismatch',
|
|
23
|
+
'manifest-phase-mismatch',
|
|
24
|
+
'manifest-sequence-mismatch',
|
|
25
|
+
'open-issue-stale',
|
|
26
|
+
'phase-prev-entry-mismatch',
|
|
27
|
+
'pre-change-checkpoint-stale',
|
|
28
|
+
'read-failure',
|
|
29
|
+
];
|
|
30
|
+
const createFinding = ({ code, message, metadata, remediation, }) => ({
|
|
31
|
+
code,
|
|
32
|
+
message,
|
|
33
|
+
metadata,
|
|
34
|
+
remediation,
|
|
35
|
+
});
|
|
36
|
+
const buildDoctorReport = (findings) => ({
|
|
37
|
+
checkedAt: new Date().toISOString(),
|
|
38
|
+
findings: [...findings],
|
|
39
|
+
issueCount: findings.length,
|
|
40
|
+
issues: findings.map((finding) => finding.message),
|
|
41
|
+
ok: findings.length === 0,
|
|
42
|
+
});
|
|
43
|
+
const pushFinding = (findings, finding) => {
|
|
44
|
+
findings.push(finding);
|
|
45
|
+
};
|
|
46
|
+
const isMissingPathError = (error) => {
|
|
47
|
+
const code = error.code;
|
|
48
|
+
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
49
|
+
};
|
|
50
|
+
const buildReadFailure = (error) => buildDoctorReport([
|
|
51
|
+
createFinding({
|
|
52
|
+
code: 'read-failure',
|
|
53
|
+
message: `Failed to read ledger state: ${error instanceof Error ? error.message ?? error.name : String(error)}.`,
|
|
54
|
+
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.',
|
|
55
|
+
}),
|
|
56
|
+
]);
|
|
57
|
+
const isChangeLogEntry = (entry) => entry.kind === 'change-log';
|
|
58
|
+
const isOpenIssueNote = (entry) => entry.kind === 'note' && entry.subkind === 'open-issue';
|
|
59
|
+
const checkPrevChain = (entry, findings, previousByPhase) => {
|
|
14
60
|
const expectedPrev = previousByPhase.get(entry.phase) ?? null;
|
|
15
61
|
if (entry.prevEntryId !== expectedPrev) {
|
|
16
|
-
|
|
62
|
+
pushFinding(findings, createFinding({
|
|
63
|
+
code: 'phase-prev-entry-mismatch',
|
|
64
|
+
message: `Phase ${entry.phase} has broken prevEntryId chain at ${entry.id}: expected ${expectedPrev ?? 'null'}, found ${entry.prevEntryId ?? 'null'}.`,
|
|
65
|
+
metadata: {
|
|
66
|
+
entryId: entry.id,
|
|
67
|
+
expectedPrevEntryId: expectedPrev,
|
|
68
|
+
foundPrevEntryId: entry.prevEntryId,
|
|
69
|
+
phase: entry.phase,
|
|
70
|
+
},
|
|
71
|
+
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.',
|
|
72
|
+
}));
|
|
17
73
|
}
|
|
18
74
|
previousByPhase.set(entry.phase, entry.id);
|
|
19
75
|
};
|
|
20
76
|
const checkBlobPresence = async (workspaceRoot, blobChecks) => {
|
|
21
|
-
const
|
|
77
|
+
const { blobHashConcurrency } = getLedgerRuntimeConfig();
|
|
78
|
+
const groupedBlobChecks = new Map();
|
|
79
|
+
for (const { blobHash, entryId } of blobChecks) {
|
|
80
|
+
const entryIds = groupedBlobChecks.get(blobHash) ?? [];
|
|
81
|
+
entryIds.push(entryId);
|
|
82
|
+
groupedBlobChecks.set(blobHash, entryIds);
|
|
83
|
+
}
|
|
84
|
+
const results = await mapWithConcurrencyLimit([...groupedBlobChecks.entries()], blobHashConcurrency, async ([blobHash, entryIds]) => {
|
|
85
|
+
const blobPath = resolveBlobPath(workspaceRoot, blobHash);
|
|
22
86
|
try {
|
|
23
|
-
const blobPath = resolveBlobPath(workspaceRoot, blobHash);
|
|
24
87
|
await stat(blobPath);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (!isMissingPathError(error)) {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
return entryIds.map((entryId) => createFinding({
|
|
94
|
+
code: 'blob-missing',
|
|
95
|
+
message: `Missing blob ${blobHash} for ${entryId}.`,
|
|
96
|
+
metadata: {
|
|
97
|
+
blobSha256: blobHash,
|
|
98
|
+
entryId,
|
|
99
|
+
},
|
|
100
|
+
remediation: 'Restore the missing blob file under .lab/ledger/blobs/ or recreate the patch entry from the original diff before archiving.',
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
25
104
|
const actualHash = await sha256File(blobPath);
|
|
26
105
|
if (actualHash !== blobHash) {
|
|
27
|
-
return
|
|
106
|
+
return entryIds.map((entryId) => createFinding({
|
|
107
|
+
code: 'blob-corrupt',
|
|
108
|
+
message: `Blob ${blobHash} for ${entryId} is corrupted (found ${actualHash}).`,
|
|
109
|
+
metadata: {
|
|
110
|
+
actualSha256: actualHash,
|
|
111
|
+
blobSha256: blobHash,
|
|
112
|
+
entryId,
|
|
113
|
+
},
|
|
114
|
+
remediation: 'Restore the original blob content under .lab/ledger/blobs/ or recreate the patch entry from the original diff before archiving.',
|
|
115
|
+
}));
|
|
28
116
|
}
|
|
29
|
-
return
|
|
117
|
+
return [];
|
|
30
118
|
}
|
|
31
|
-
catch {
|
|
32
|
-
return
|
|
119
|
+
catch (error) {
|
|
120
|
+
return entryIds.map((entryId) => createFinding({
|
|
121
|
+
code: 'blob-unreadable',
|
|
122
|
+
message: `Blob ${blobHash} for ${entryId} could not be read: ${error instanceof Error ? error.message : String(error)}.`,
|
|
123
|
+
metadata: {
|
|
124
|
+
blobSha256: blobHash,
|
|
125
|
+
entryId,
|
|
126
|
+
},
|
|
127
|
+
remediation: 'Fix the filesystem permission or read error first, then rerun doctor so blob integrity can be verified before archiving.',
|
|
128
|
+
}));
|
|
33
129
|
}
|
|
34
130
|
});
|
|
35
|
-
return results.
|
|
131
|
+
return results.flat();
|
|
36
132
|
};
|
|
37
|
-
const checkManifestCounts = (manifest, entryCount) => {
|
|
38
|
-
const issues = [];
|
|
133
|
+
const checkManifestCounts = (findings, manifest, entryCount) => {
|
|
39
134
|
if (manifest.entryCount !== entryCount) {
|
|
40
|
-
|
|
135
|
+
pushFinding(findings, createFinding({
|
|
136
|
+
code: 'manifest-entry-count-mismatch',
|
|
137
|
+
message: `Manifest entryCount ${manifest.entryCount} does not match disk entries ${entryCount}.`,
|
|
138
|
+
metadata: {
|
|
139
|
+
diskEntryCount: entryCount,
|
|
140
|
+
manifestEntryCount: manifest.entryCount,
|
|
141
|
+
},
|
|
142
|
+
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.',
|
|
143
|
+
}));
|
|
41
144
|
}
|
|
42
145
|
if (manifest.lastSequence !== entryCount) {
|
|
43
|
-
|
|
146
|
+
pushFinding(findings, createFinding({
|
|
147
|
+
code: 'manifest-last-sequence-mismatch',
|
|
148
|
+
message: `Manifest lastSequence ${manifest.lastSequence} does not match disk entries ${entryCount}.`,
|
|
149
|
+
metadata: {
|
|
150
|
+
diskEntryCount: entryCount,
|
|
151
|
+
manifestLastSequence: manifest.lastSequence,
|
|
152
|
+
},
|
|
153
|
+
remediation: 'Repair manifest.json so lastSequence matches the highest durable append sequence after recovery completes.',
|
|
154
|
+
}));
|
|
44
155
|
}
|
|
45
|
-
return issues;
|
|
46
156
|
};
|
|
47
|
-
const finalizeManifestChecks = ({ entryCount,
|
|
48
|
-
|
|
157
|
+
const finalizeManifestChecks = ({ entryCount, findings, latestByPhase, manifest, unseenManifestEntryIds, }) => {
|
|
158
|
+
checkManifestCounts(findings, manifest, entryCount);
|
|
49
159
|
for (const entryId of unseenManifestEntryIds) {
|
|
50
|
-
|
|
160
|
+
pushFinding(findings, createFinding({
|
|
161
|
+
code: 'manifest-entry-missing-on-disk',
|
|
162
|
+
message: `Manifest entry location points to missing disk entry ${entryId}.`,
|
|
163
|
+
metadata: { entryId },
|
|
164
|
+
remediation: 'Restore the missing entry file or repair manifest.entryLocations so every referenced entry id exists on disk before archiving.',
|
|
165
|
+
}));
|
|
51
166
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
167
|
+
const phasesToCheck = new Set([...Object.keys(manifest.perPhaseLatest), ...latestByPhase.keys()]);
|
|
168
|
+
for (const phase of phasesToCheck) {
|
|
169
|
+
const latest = latestByPhase.get(phase);
|
|
170
|
+
const expectedEntryId = latest?.entryId ?? null;
|
|
171
|
+
const foundEntryId = manifest.perPhaseLatest[phase] ?? null;
|
|
172
|
+
if (foundEntryId !== expectedEntryId) {
|
|
173
|
+
pushFinding(findings, createFinding({
|
|
174
|
+
code: 'manifest-per-phase-latest-mismatch',
|
|
175
|
+
message: `Manifest perPhaseLatest mismatch for ${phase}: expected ${expectedEntryId ?? 'missing'}, found ${foundEntryId ?? 'missing'}.`,
|
|
176
|
+
metadata: {
|
|
177
|
+
expectedEntryId,
|
|
178
|
+
foundEntryId,
|
|
179
|
+
phase,
|
|
180
|
+
},
|
|
181
|
+
remediation: 'Repair perPhaseLatest so each phase points at the newest durable entry in that phase.',
|
|
182
|
+
}));
|
|
55
183
|
}
|
|
56
184
|
}
|
|
57
185
|
};
|
|
58
|
-
const checkManifestSequenceOrder = (entryLocations,
|
|
186
|
+
const checkManifestSequenceOrder = (entryLocations, findings) => {
|
|
59
187
|
for (let index = 0; index < entryLocations.length; index += 1) {
|
|
60
188
|
const [entryId, location] = entryLocations[index];
|
|
61
189
|
const expectedSequence = index + 1;
|
|
62
190
|
if (location.sequence !== expectedSequence) {
|
|
63
|
-
|
|
191
|
+
pushFinding(findings, createFinding({
|
|
192
|
+
code: 'manifest-sequence-mismatch',
|
|
193
|
+
message: `Manifest sequence mismatch for ${entryId}: expected ${expectedSequence}, found ${location.sequence}.`,
|
|
194
|
+
metadata: {
|
|
195
|
+
entryId,
|
|
196
|
+
expectedSequence,
|
|
197
|
+
foundSequence: location.sequence,
|
|
198
|
+
},
|
|
199
|
+
remediation: 'Repair manifest.entryLocations so sequences remain contiguous and match append order.',
|
|
200
|
+
}));
|
|
64
201
|
}
|
|
65
202
|
}
|
|
66
203
|
};
|
|
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
204
|
const trackResolutionLinks = (entry, resolvedLedgerIds) => {
|
|
74
205
|
if (entry.links.correctsLedgerId) {
|
|
75
206
|
resolvedLedgerIds.add(entry.links.correctsLedgerId);
|
|
@@ -87,7 +218,7 @@ const trackIdempotencyEntry = (entry, entriesByIdempotencyKey) => {
|
|
|
87
218
|
existingEntries.push({ id: entry.id, ts: entry.ts });
|
|
88
219
|
entriesByIdempotencyKey.set(idempotencyKey, existingEntries);
|
|
89
220
|
};
|
|
90
|
-
const checkChangeLogWarnings = ({ checkpointEntries, entriesByIdempotencyKey,
|
|
221
|
+
const checkChangeLogWarnings = ({ checkpointEntries, entriesByIdempotencyKey, findings, nowMs, openIssueEntries, resolvedLedgerIds, }) => {
|
|
91
222
|
for (const checkpointEntry of checkpointEntries) {
|
|
92
223
|
const ageMs = nowMs - Date.parse(checkpointEntry.ts);
|
|
93
224
|
if (ageMs <= CHECKPOINT_MAX_AGE_MS) {
|
|
@@ -97,7 +228,15 @@ const checkChangeLogWarnings = ({ checkpointEntries, entriesByIdempotencyKey, is
|
|
|
97
228
|
const hasFollowUp = typeof idempotencyKey === 'string' &&
|
|
98
229
|
(entriesByIdempotencyKey.get(idempotencyKey) ?? []).some((candidate) => candidate.id !== checkpointEntry.id && candidate.ts >= checkpointEntry.ts);
|
|
99
230
|
if (!hasFollowUp) {
|
|
100
|
-
|
|
231
|
+
pushFinding(findings, createFinding({
|
|
232
|
+
code: 'pre-change-checkpoint-stale',
|
|
233
|
+
message: `Pre-change checkpoint ${checkpointEntry.id} is older than 24h and has no follow-up entry with matching idempotencyKey.`,
|
|
234
|
+
metadata: {
|
|
235
|
+
entryId: checkpointEntry.id,
|
|
236
|
+
idempotencyKey: idempotencyKey ?? null,
|
|
237
|
+
},
|
|
238
|
+
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.',
|
|
239
|
+
}));
|
|
101
240
|
}
|
|
102
241
|
}
|
|
103
242
|
for (const openIssueEntry of openIssueEntries) {
|
|
@@ -105,17 +244,36 @@ const checkChangeLogWarnings = ({ checkpointEntries, entriesByIdempotencyKey, is
|
|
|
105
244
|
if (ageMs <= OPEN_ISSUE_MAX_AGE_MS || resolvedLedgerIds.has(openIssueEntry.id)) {
|
|
106
245
|
continue;
|
|
107
246
|
}
|
|
108
|
-
|
|
247
|
+
pushFinding(findings, createFinding({
|
|
248
|
+
code: 'open-issue-stale',
|
|
249
|
+
message: `Open issue note ${openIssueEntry.id} is older than 30 days and has no resolution link.`,
|
|
250
|
+
metadata: { entryId: openIssueEntry.id },
|
|
251
|
+
remediation: 'Append a correction or superseding note that links back to the open issue once the follow-up is complete.',
|
|
252
|
+
}));
|
|
109
253
|
}
|
|
110
254
|
};
|
|
111
|
-
const inspectManifestLocation = ({ entry,
|
|
255
|
+
const inspectManifestLocation = ({ entry, findings, latestByPhase, manifest, }) => {
|
|
112
256
|
const manifestLocation = manifest.entryLocations[entry.id];
|
|
113
257
|
if (!manifestLocation) {
|
|
114
|
-
|
|
258
|
+
pushFinding(findings, createFinding({
|
|
259
|
+
code: 'manifest-entry-location-missing',
|
|
260
|
+
message: `Manifest is missing entry location for ${entry.id}.`,
|
|
261
|
+
metadata: { entryId: entry.id },
|
|
262
|
+
remediation: 'Repair manifest.entryLocations so every durable entry has a phase/sequence location before archiving.',
|
|
263
|
+
}));
|
|
115
264
|
return null;
|
|
116
265
|
}
|
|
117
266
|
if (manifestLocation.phase !== entry.phase) {
|
|
118
|
-
|
|
267
|
+
pushFinding(findings, createFinding({
|
|
268
|
+
code: 'manifest-phase-mismatch',
|
|
269
|
+
message: `Manifest phase mismatch for ${entry.id}: expected ${entry.phase}, found ${manifestLocation.phase}.`,
|
|
270
|
+
metadata: {
|
|
271
|
+
entryId: entry.id,
|
|
272
|
+
expectedPhase: entry.phase,
|
|
273
|
+
foundPhase: manifestLocation.phase,
|
|
274
|
+
},
|
|
275
|
+
remediation: 'Move the entry back to the correct phase directory or repair manifest.entryLocations so the recorded phase matches the stored entry.',
|
|
276
|
+
}));
|
|
119
277
|
}
|
|
120
278
|
const currentLatest = latestByPhase.get(entry.phase);
|
|
121
279
|
if (!currentLatest || manifestLocation.sequence > currentLatest.sequence) {
|
|
@@ -123,16 +281,26 @@ const inspectManifestLocation = ({ entry, issues, latestByPhase, manifest, }) =>
|
|
|
123
281
|
}
|
|
124
282
|
return manifestLocation;
|
|
125
283
|
};
|
|
126
|
-
const inspectNarrativeEntry = ({ checkpointEntries, entry,
|
|
284
|
+
const inspectNarrativeEntry = ({ checkpointEntries, entry, findings, openIssueEntries, }) => {
|
|
127
285
|
if (isChangeLogEntry(entry)) {
|
|
128
286
|
if (entry.subkind === 'pre-change-checkpoint') {
|
|
129
287
|
checkpointEntries.push(entry);
|
|
130
288
|
}
|
|
131
289
|
if (entry.smokeResult === 'fail' && !entry.rollbackPlan) {
|
|
132
|
-
|
|
290
|
+
pushFinding(findings, createFinding({
|
|
291
|
+
code: 'change-log-smoke-failure-missing-rollback-plan',
|
|
292
|
+
message: `Change-log entry ${entry.id} has smokeResult=fail but no rollbackPlan.`,
|
|
293
|
+
metadata: { entryId: entry.id },
|
|
294
|
+
remediation: 'Append a correction or follow-up change-log entry documenting the rollback plan before treating the failure as closed.',
|
|
295
|
+
}));
|
|
133
296
|
}
|
|
134
297
|
if (entry.subkind === 'rollback' && !entry.rollsBack) {
|
|
135
|
-
|
|
298
|
+
pushFinding(findings, createFinding({
|
|
299
|
+
code: 'change-log-rollback-missing-target',
|
|
300
|
+
message: `Change-log rollback entry ${entry.id} is missing rollsBack.`,
|
|
301
|
+
metadata: { entryId: entry.id },
|
|
302
|
+
remediation: 'Append a correction or replacement rollback entry with rollsBack pointing at the reverted ledger entry id.',
|
|
303
|
+
}));
|
|
136
304
|
}
|
|
137
305
|
}
|
|
138
306
|
if (isOpenIssueNote(entry)) {
|
|
@@ -147,14 +315,14 @@ const inspectPatchEntry = ({ blobChecks, entry, }) => {
|
|
|
147
315
|
blobChecks.push({ blobHash, entryId: entry.id });
|
|
148
316
|
}
|
|
149
317
|
};
|
|
150
|
-
const inspectDoctorEntry = ({ blobChecks, checkpointEntries,
|
|
318
|
+
const inspectDoctorEntry = ({ blobChecks, checkpointEntries, entriesByIdempotencyKey, entry, findings, latestByPhase, manifest, openIssueEntries, previousByPhase, resolvedLedgerIds, unseenManifestEntryIds, }) => {
|
|
151
319
|
unseenManifestEntryIds.delete(entry.id);
|
|
152
|
-
checkPrevChain(entry,
|
|
320
|
+
checkPrevChain(entry, findings, previousByPhase);
|
|
153
321
|
trackIdempotencyEntry(entry, entriesByIdempotencyKey);
|
|
154
322
|
trackResolutionLinks(entry, resolvedLedgerIds);
|
|
155
323
|
if (!inspectManifestLocation({
|
|
156
324
|
entry,
|
|
157
|
-
|
|
325
|
+
findings,
|
|
158
326
|
latestByPhase,
|
|
159
327
|
manifest,
|
|
160
328
|
})) {
|
|
@@ -163,7 +331,7 @@ const inspectDoctorEntry = ({ blobChecks, checkpointEntries, entry, entriesByIde
|
|
|
163
331
|
inspectNarrativeEntry({
|
|
164
332
|
checkpointEntries,
|
|
165
333
|
entry,
|
|
166
|
-
|
|
334
|
+
findings,
|
|
167
335
|
openIssueEntries,
|
|
168
336
|
});
|
|
169
337
|
inspectPatchEntry({
|
|
@@ -172,7 +340,7 @@ const inspectDoctorEntry = ({ blobChecks, checkpointEntries, entry, entriesByIde
|
|
|
172
340
|
});
|
|
173
341
|
};
|
|
174
342
|
const collectDoctorState = async (workspaceRoot, manifest, readIndex) => {
|
|
175
|
-
const
|
|
343
|
+
const findings = [];
|
|
176
344
|
const previousByPhase = new Map();
|
|
177
345
|
const latestByPhase = new Map();
|
|
178
346
|
const unseenManifestEntryIds = new Set(Object.keys(manifest.entryLocations));
|
|
@@ -183,13 +351,15 @@ const collectDoctorState = async (workspaceRoot, manifest, readIndex) => {
|
|
|
183
351
|
const resolvedLedgerIds = new Set();
|
|
184
352
|
let entryCount = 0;
|
|
185
353
|
const nowMs = Date.now();
|
|
354
|
+
const { scanBatchSize, scanConcurrency } = getLedgerRuntimeConfig();
|
|
186
355
|
const orderedEntries = getOrderedEntryLocations(manifest, readIndex, {});
|
|
187
|
-
checkManifestSequenceOrder(orderedEntries,
|
|
188
|
-
for (let index = 0; index < orderedEntries.length; index +=
|
|
189
|
-
const batch = orderedEntries.slice(index, index +
|
|
356
|
+
checkManifestSequenceOrder(orderedEntries, findings);
|
|
357
|
+
for (let index = 0; index < orderedEntries.length; index += scanBatchSize) {
|
|
358
|
+
const batch = orderedEntries.slice(index, index + scanBatchSize);
|
|
190
359
|
const resolvedEntries = await readManifestEntryBatch({
|
|
191
360
|
allowMissing: true,
|
|
192
361
|
entryLocations: batch,
|
|
362
|
+
entryReadConcurrency: scanConcurrency,
|
|
193
363
|
workspaceRoot,
|
|
194
364
|
});
|
|
195
365
|
for (const resolvedEntry of resolvedEntries) {
|
|
@@ -200,9 +370,9 @@ const collectDoctorState = async (workspaceRoot, manifest, readIndex) => {
|
|
|
200
370
|
inspectDoctorEntry({
|
|
201
371
|
blobChecks,
|
|
202
372
|
checkpointEntries,
|
|
203
|
-
entry: resolvedEntry.entry,
|
|
204
373
|
entriesByIdempotencyKey,
|
|
205
|
-
|
|
374
|
+
entry: resolvedEntry.entry,
|
|
375
|
+
findings,
|
|
206
376
|
latestByPhase,
|
|
207
377
|
manifest,
|
|
208
378
|
openIssueEntries,
|
|
@@ -215,7 +385,7 @@ const collectDoctorState = async (workspaceRoot, manifest, readIndex) => {
|
|
|
215
385
|
checkChangeLogWarnings({
|
|
216
386
|
checkpointEntries,
|
|
217
387
|
entriesByIdempotencyKey,
|
|
218
|
-
|
|
388
|
+
findings,
|
|
219
389
|
nowMs,
|
|
220
390
|
openIssueEntries,
|
|
221
391
|
resolvedLedgerIds,
|
|
@@ -223,7 +393,7 @@ const collectDoctorState = async (workspaceRoot, manifest, readIndex) => {
|
|
|
223
393
|
return {
|
|
224
394
|
blobChecks,
|
|
225
395
|
entryCount,
|
|
226
|
-
|
|
396
|
+
findings,
|
|
227
397
|
latestByPhase,
|
|
228
398
|
unseenManifestEntryIds,
|
|
229
399
|
};
|
|
@@ -256,17 +426,14 @@ export const runLedgerDoctor = async (workspaceRoot, options = {}) => {
|
|
|
256
426
|
catch (error) {
|
|
257
427
|
return buildReadFailure(error);
|
|
258
428
|
}
|
|
259
|
-
const { blobChecks, entryCount,
|
|
260
|
-
|
|
429
|
+
const { blobChecks, entryCount, findings, latestByPhase, unseenManifestEntryIds } = doctorState;
|
|
430
|
+
findings.push(...(await checkBlobPresence(workspaceRoot, blobChecks)));
|
|
261
431
|
finalizeManifestChecks({
|
|
262
432
|
entryCount,
|
|
263
|
-
|
|
433
|
+
findings,
|
|
264
434
|
latestByPhase,
|
|
265
435
|
manifest: preparedState.manifest,
|
|
266
436
|
unseenManifestEntryIds,
|
|
267
437
|
});
|
|
268
|
-
return
|
|
269
|
-
issues,
|
|
270
|
-
ok: issues.length === 0,
|
|
271
|
-
};
|
|
438
|
+
return buildDoctorReport(findings);
|
|
272
439
|
};
|
package/dist/handle.d.ts
CHANGED
|
@@ -4,6 +4,30 @@ import { type LedgerFilter } from './list.ts';
|
|
|
4
4
|
import { appendNote, type NoteBody } from './note.ts';
|
|
5
5
|
import type { LedgerEntry, LedgerPhase } from './schema/entry.ts';
|
|
6
6
|
export type RenderTarget = 'dependency-graph' | 'jsonl' | 'migration-log-md' | 'retro' | 'timeline-html' | 'workspace-narrative-md';
|
|
7
|
+
/**
|
|
8
|
+
* Chunk writer used by `renderTo()` for bounded render emission.
|
|
9
|
+
*
|
|
10
|
+
* Writers may complete synchronously or asynchronously. Throwing rejects the render and causes any in-progress
|
|
11
|
+
* atomic output file to be aborted.
|
|
12
|
+
*/
|
|
13
|
+
export type RenderWriter = (chunk: string) => Promise<void> | void;
|
|
14
|
+
/** Options for `render()`, which returns the rendered text and optionally mirrors it to disk. */
|
|
15
|
+
export type LedgerRenderOptions = {
|
|
16
|
+
readonly limit?: number;
|
|
17
|
+
readonly out?: string;
|
|
18
|
+
readonly phase?: LedgerPhase;
|
|
19
|
+
readonly since?: string;
|
|
20
|
+
readonly to: RenderTarget;
|
|
21
|
+
};
|
|
22
|
+
/** Options for `renderTo()`, which writes to a callback and/or canonical render file without buffering the full output. */
|
|
23
|
+
export type LedgerRenderToOptions = {
|
|
24
|
+
readonly limit?: number;
|
|
25
|
+
readonly out?: string;
|
|
26
|
+
readonly phase?: LedgerPhase;
|
|
27
|
+
readonly since?: string;
|
|
28
|
+
readonly to: RenderTarget;
|
|
29
|
+
readonly write?: RenderWriter;
|
|
30
|
+
};
|
|
7
31
|
export type LedgerHandle = {
|
|
8
32
|
readonly archive: (outPath: string) => Promise<{
|
|
9
33
|
integrityHash: string;
|
|
@@ -17,14 +41,10 @@ export type LedgerHandle = {
|
|
|
17
41
|
readonly record: (entry: unknown) => Promise<{
|
|
18
42
|
id: string;
|
|
19
43
|
}>;
|
|
20
|
-
readonly render: (options:
|
|
21
|
-
|
|
22
|
-
limit?: number;
|
|
23
|
-
phase?: LedgerPhase;
|
|
24
|
-
since?: string;
|
|
25
|
-
to: RenderTarget;
|
|
26
|
-
}) => Promise<string>;
|
|
44
|
+
readonly render: (options: LedgerRenderOptions) => Promise<string>;
|
|
45
|
+
readonly renderTo: (options: LedgerRenderToOptions) => Promise<void>;
|
|
27
46
|
readonly show: (entryId: string) => Promise<LedgerEntry | null>;
|
|
28
47
|
};
|
|
48
|
+
/** Open a workspace ledger handle after reconciling any pending crash-recovery state. */
|
|
29
49
|
export declare const openLedger: (workspaceRoot: string) => Promise<LedgerHandle>;
|
|
30
50
|
//# sourceMappingURL=handle.d.ts.map
|
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,
|
|
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;AAG9E,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;AA4GF,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,CAyF5E,CAAC"}
|
package/dist/handle.js
CHANGED
|
@@ -13,28 +13,58 @@ 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 { resolveLedgerPaths, writeAtomicTextFile } from "./storage/filesystem.js";
|
|
17
|
-
const
|
|
18
|
-
|
|
16
|
+
import { createAtomicTextFileWriter, resolveLedgerPaths, writeAtomicTextFile } from "./storage/filesystem.js";
|
|
17
|
+
const createEntryIteratorFactory = ({ filter, state, workspaceRoot, }) => {
|
|
18
|
+
return (entryOptions = {}) => iterateEntriesFromManifest(workspaceRoot, state.manifest, state.readIndex, {
|
|
19
|
+
kind: entryOptions.kind,
|
|
20
|
+
limit: filter.limit,
|
|
21
|
+
phase: filter.phase,
|
|
22
|
+
since: filter.since,
|
|
23
|
+
}, entryOptions.direction);
|
|
24
|
+
};
|
|
25
|
+
const collectRenderedText = async (emit) => {
|
|
26
|
+
const chunks = [];
|
|
27
|
+
await emit((chunk) => {
|
|
28
|
+
chunks.push(chunk);
|
|
29
|
+
});
|
|
30
|
+
return chunks.join('');
|
|
31
|
+
};
|
|
32
|
+
const emitRenderedTarget = async ({ createEntries, manifest, target, write, workspaceName, }) => {
|
|
19
33
|
switch (target) {
|
|
20
34
|
case 'dependency-graph':
|
|
21
|
-
|
|
35
|
+
await write(await renderDependencyGraph(createEntries()));
|
|
36
|
+
return;
|
|
22
37
|
case 'jsonl':
|
|
23
|
-
|
|
38
|
+
await write(await renderJsonl(createEntries()));
|
|
39
|
+
return;
|
|
24
40
|
case 'migration-log-md':
|
|
25
|
-
|
|
41
|
+
await renderMigrationLogMarkdown({
|
|
42
|
+
entries: createEntries({
|
|
43
|
+
direction: 'desc',
|
|
44
|
+
kind: 'change-log',
|
|
45
|
+
}),
|
|
46
|
+
write,
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
26
49
|
case 'timeline-html':
|
|
27
|
-
|
|
50
|
+
await write(await renderTimelineHtml(createEntries()));
|
|
51
|
+
return;
|
|
28
52
|
case 'retro':
|
|
29
|
-
|
|
30
|
-
entries:
|
|
53
|
+
await write(await renderRetroMarkdown({
|
|
54
|
+
entries: createEntries(),
|
|
31
55
|
manifest,
|
|
32
|
-
});
|
|
56
|
+
}));
|
|
57
|
+
return;
|
|
33
58
|
case 'workspace-narrative-md':
|
|
34
|
-
|
|
35
|
-
entries:
|
|
36
|
-
|
|
59
|
+
await renderWorkspaceNarrativeMarkdown({
|
|
60
|
+
entries: createEntries({
|
|
61
|
+
direction: 'desc',
|
|
62
|
+
kind: 'note',
|
|
63
|
+
}),
|
|
64
|
+
workspaceName,
|
|
65
|
+
write,
|
|
37
66
|
});
|
|
67
|
+
return;
|
|
38
68
|
}
|
|
39
69
|
};
|
|
40
70
|
const resolveRenderOutputPath = (workspaceRoot, target) => {
|
|
@@ -52,6 +82,7 @@ const resolveRenderOutputPath = (workspaceRoot, target) => {
|
|
|
52
82
|
return null;
|
|
53
83
|
}
|
|
54
84
|
};
|
|
85
|
+
/** Open a workspace ledger handle after reconciling any pending crash-recovery state. */
|
|
55
86
|
export const openLedger = async (workspaceRoot) => {
|
|
56
87
|
await readLabManifestMin(workspaceRoot);
|
|
57
88
|
await prepareLedgerState(workspaceRoot);
|
|
@@ -69,21 +100,66 @@ export const openLedger = async (workspaceRoot) => {
|
|
|
69
100
|
return { id: result.id };
|
|
70
101
|
},
|
|
71
102
|
render: async (options) => {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
since: options.since,
|
|
77
|
-
},
|
|
78
|
-
target: options.to,
|
|
103
|
+
const state = await loadLedgerState(workspaceRoot);
|
|
104
|
+
const createEntries = createEntryIteratorFactory({
|
|
105
|
+
filter: options,
|
|
106
|
+
state,
|
|
79
107
|
workspaceRoot,
|
|
80
108
|
});
|
|
109
|
+
const content = await collectRenderedText((write) => emitRenderedTarget({
|
|
110
|
+
createEntries,
|
|
111
|
+
manifest: state.manifest,
|
|
112
|
+
target: options.to,
|
|
113
|
+
workspaceName: path.basename(workspaceRoot),
|
|
114
|
+
write,
|
|
115
|
+
}));
|
|
81
116
|
const outputPath = options.out ?? resolveRenderOutputPath(workspaceRoot, options.to);
|
|
82
117
|
if (outputPath) {
|
|
83
118
|
await writeAtomicTextFile(outputPath, `${content}${content.endsWith('\n') ? '' : '\n'}`);
|
|
84
119
|
}
|
|
85
120
|
return content;
|
|
86
121
|
},
|
|
122
|
+
renderTo: async (options) => {
|
|
123
|
+
const state = await loadLedgerState(workspaceRoot);
|
|
124
|
+
const createEntries = createEntryIteratorFactory({
|
|
125
|
+
filter: options,
|
|
126
|
+
state,
|
|
127
|
+
workspaceRoot,
|
|
128
|
+
});
|
|
129
|
+
const outputPath = options.out ?? resolveRenderOutputPath(workspaceRoot, options.to);
|
|
130
|
+
const atomicWriter = outputPath ? await createAtomicTextFileWriter(outputPath) : null;
|
|
131
|
+
const writer = options.write;
|
|
132
|
+
let hasWrittenContent = false;
|
|
133
|
+
let endsWithNewline = false;
|
|
134
|
+
if (!atomicWriter && !writer) {
|
|
135
|
+
throw new Error(`Render target ${options.to} requires either a writer callback or an output path.`);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await emitRenderedTarget({
|
|
139
|
+
createEntries,
|
|
140
|
+
manifest: state.manifest,
|
|
141
|
+
target: options.to,
|
|
142
|
+
workspaceName: path.basename(workspaceRoot),
|
|
143
|
+
write: async (chunk) => {
|
|
144
|
+
if (chunk.length > 0) {
|
|
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
|
+
}
|
|
162
|
+
},
|
|
87
163
|
show: async (entryId) => {
|
|
88
164
|
const { manifest } = await loadLedgerState(workspaceRoot);
|
|
89
165
|
if (!manifest.entryLocations[entryId]) {
|
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"}
|