@ascenda-one/history-import 0.1.12 → 0.1.13

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/README.md CHANGED
@@ -5,11 +5,10 @@ an engineer's machine — Claude Code transcripts, Cursor's conversation store,
5
5
  VS Code local history and Copilot sessions, git — and ships it as
6
6
  provenance-classed historical events on the existing telemetry wire.
7
7
 
8
- The research behind this package (what is actually in each store, verified on
9
- a real machine 2026-08-18) lives in the Flow workspace:
10
- `asc-ascenda-app-workspace/docs/HISTORICAL_TELEMETRY_IMPORT.md`. Read it
11
- before touching an extractor — the store formats are reverse-engineered and
12
- every design rule here traces to a finding there.
8
+ The store formats these extractors read are reverse-engineered and
9
+ undocumented upstream, so they can change without notice. Each extractor's
10
+ header comment states the shape it expects and the invariants it relies on;
11
+ read that, and the fixtures under `tests/`, before changing one.
13
12
 
14
13
  ## Why this package exists here, not in the app
15
14
 
@@ -32,7 +31,7 @@ command (same pattern as hooks pairing) and this CLI does the reading.
32
31
  file contents never leave the machine. Content-level ingestion, if it ever
33
32
  ships, is a separate explicit opt-in — not this package's default path.
34
33
  5. **Aggregate before shipping.** Per-session / per-day events, not one event
35
- per bubble (one observed machine holds 17,727 bubbles).
34
+ per bubble — a single machine's stores hold tens of thousands of them.
36
35
  6. **Provenance is data.** Every event carries `historical_direct`,
37
36
  `historical_derived` or `historical_unparsed` — never the live
38
37
  `ai_work_telemetry` provenance — so no chart can pass history off as
@@ -44,51 +43,41 @@ command (same pattern as hooks pairing) and this CLI does the reading.
44
43
  |---|---|
45
44
  | `scan` (per-store inventory, content never opened) | implemented |
46
45
  | `fix-retention` (Claude `cleanupPeriodDays`, merge-not-clobber) | implemented |
47
- | Staging/snapshot (copy-then-parse, WAL-aware, APFS clone-on-write) | implemented |
46
+ | Staging/snapshot (copy-then-parse, WAL-aware, **torn down by the run that makes it**) | implemented |
47
+ | `archive` (durable content-addressed copy, dedup, verify, restore, prune) | **implemented, verified on a real 4.1 GB store** |
48
48
  | **Claude Code extractor** (human-prompt/tool-result split, session folds incl. recursive subagent transcripts, after-hours, compaction, tool failures, context-window peak, human-corrected edits, correction cadence, gap-split active minutes, epoch marker) | **implemented, verified live** |
49
49
  | **Batch shipper** (`POST /v1/tool-events/batch`, salted hashes, stable importKey) | **implemented, verified live** |
50
50
  | **Cursor extractor** (composerHeaders + bubble aggregation via SQL-side `json_extract`, prompt text never parsed into the process, subagent-composer folding, epoch marker) | **implemented, verified live** |
51
51
  | **VS Code extractor** (Timeline-history Chat-Edit day×workspace aggregation, Copilot chatSessions folding, workspace identity via `workspace.json` longest-prefix match, epoch marker) | **implemented, verified live** |
52
- | `import [--ship]` end to end | **dry run 2026-08-18: Claude Code 9,198 events (333 sessions, 8,401 human prompts, 135 after-hours sessions, 261 sessions with ≥1 tool failure) + Cursor 661 events (104 sessions, 550 human prompts, 6 after-hours sessions) + VS Code 7,338 events (548 edit-days summing to the full 13,780 verified Chat Edits — monthly rollup reproduces the documented May-2026 cliff: Apr 2,079 → May 343 → Jun 1,047 with no manual tuning; 419 Copilot sessions, 6,131 human prompts, 114 after-hours sessions, 125 sessions with ≥1 request error; 2 Timeline-history files unparsed) — 17,197 total across all three stores. Last `--ship` run: 8,720/8,720 accepted by the backend, 2026-08-18, before this extractor's friction-signal, Cursor and VS Code additions.** |
52
+ | `import [--ship]` end to end | **implemented; exercised end to end against all three stores, dry run and `--ship`, on a developer machine** |
53
53
  | git extractor | stub — throws with a pointer |
54
54
  | zsh `EXTENDED_HISTORY` apply | snippet only |
55
55
 
56
- The human-prompt classifier is the load-bearing piece: on the verified store,
57
- 108,528 user-role lines reduce to 8,272 actual typed prompts — the other 92%
58
- are tool-result round-trips that would have inflated every prompt metric ~13x.
56
+ The human-prompt classifier is the load-bearing piece: the large majority of
57
+ user-role transcript lines are tool-result round-trips, not typed prompts.
58
+ Conflating the two inflates every prompt metric by roughly an order of
59
+ magnitude, so its fixtures are the ones to keep green.
59
60
 
60
61
  ## Gaps that block a real user running this twice
61
62
 
62
- Both blockers are **implemented but unmerged** — asc-core-be branch
63
- `claude/historical-import-dedup-and-consent`, not `origin/main` (verified
64
- 19 Aug 2026). Neither is enforced by the backend a published client would
65
- actually reach. What they become **once that branch merges**:
66
-
67
- - **Consent scope — specified, not yet enforced.** `historical_import` is a
68
- real `ToolConsentScope` here, and on that branch it is backed by
69
- `ConsentType.HistoricalImport` (507) with ingestion enforcing it.
70
-
71
- On `origin/main` today none of that exists: `ResolveConsentType` does not
72
- recognise the scope and falls to its default arm, `AiDataProcessing` — the
73
- lease already granted for live IDE telemetry. A backfill would ride in on
74
- live-telemetry consent, which is exactly what the separate scope exists to
75
- stop. **This gates publishing the package**, independently of any tier or
76
- entitlement decision: paying for a capability is not the same act as
77
- agreeing to a specific read of nine months of local history.
78
-
79
- How it behaves once merged: Enforcement keys on the event's **provenance**, not
80
- on the scope string it sends: any of the three `historical_*` classes
81
- requires an active historical-import lease, so a client that keeps claiming
82
- `ide_telemetry` over backdated events is rejected rather than waved through
83
- to the default consent type. Nothing grants the lease at pairing — a paired
84
- tool has consent to watch you from now on, never to read backwards.
85
- - **Idempotency — closed.** `(pairedUser, toolInstallation, importKey)` is
86
- unique in the database, checked before insert and enforced by a partial
87
- unique index for the concurrent case. A replayed event answers `duplicate`,
88
- writes nothing at all (not the event, not `lastSeenAt`, not an audit row),
89
- and the batch response counts duplicates apart from both accepted and
90
- rejected. `importKey` is **required** on any historical event. (Same caveat:
91
- this is the branch's behaviour, not main's.)
63
+ Both blockers are enforced server-side, and both are **still landing**. Until
64
+ they are live in the deployed backend, this package stays unpublished — a
65
+ client anyone could install must not be able to backfill history against a
66
+ backend that has not yet gated it.
67
+
68
+ - **Consent scope.** `historical_import` is a distinct `ToolConsentScope`,
69
+ separate from the lease granted for live IDE telemetry, and a backfill
70
+ requires it. Paying for a capability is not the same act as agreeing to a
71
+ specific read of months of local history, and pairing grants neither: a
72
+ paired tool has consent to watch you from now on, never to read backwards.
73
+ Enforcement keys on the event's **provenance** rather than the scope string
74
+ a client sends, so backdated events cannot ride in under live-telemetry
75
+ consent.
76
+ - **Idempotency.** `(pairedUser, toolInstallation, importKey)` is unique, and
77
+ `importKey` is required on any historical event. A replayed event answers
78
+ `duplicate` and writes nothing at all (not the event, not `lastSeenAt`, not
79
+ an audit row); the batch response counts duplicates apart from both accepted
80
+ and rejected.
92
81
 
93
82
  **The dedup key is the source record ref alone — not `extractionId` + ref,
94
83
  as this list originally said.** An extraction id is minted per run, so
@@ -124,4 +113,37 @@ ascenda-history-import scan # human-readable inventory
124
113
  ascenda-history-import scan --json # what the app's consent surface renders
125
114
  ascenda-history-import fix-retention # dry-run; --apply to write
126
115
  ascenda-history-import import # Claude Code + Cursor + VS Code, dry run; --ship to send
116
+ ascenda-history-import archive # the durable copy; --verify / --list / --restore <dir> / --prune
127
117
  ```
118
+
119
+ ### Staging is scaffolding; `archive` is the copy
120
+
121
+ `import` snapshots each store, extracts, and **deletes the snapshot in a
122
+ `finally`** — success or failure — keeping only the ~10 MB `events.jsonl`.
123
+ It also sweeps snapshots left by earlier runs. This is not tidiness: nineteen
124
+ runs once left 254 GB on a 926 GB disk and took free space to 279 MB, and the
125
+ first thing to notice was unrelated tooling failing with `ENOSPC`.
126
+
127
+ The comment that made it possible claimed APFS clone-on-write made a snapshot
128
+ free. Measured on macOS 15 / APFS / Node 24, it is not — `fs.copyFile` with
129
+ `COPYFILE_FICLONE` costs exactly as much as a plain copy (450 MB source,
130
+ 464 MB consumed), while `/bin/cp -c` on the same file costs zero. Node's copy
131
+ path does not use reflinks here. Never assume a Node copy is cheap.
132
+
133
+ `fix-retention` is the other half, and is also **not a backup**: it stops
134
+ Claude Code trimming itself *in place* and does nothing if that store is lost.
135
+ `archive` is the durable copy:
136
+
137
+ - lives outside `staging/`, so no sweep can reach it;
138
+ - content-addressed, so re-archiving an unchanged 4 GB store costs ~0
139
+ (measured: first generation 4.1 GB in 20 s, second 48 MB in 2.8 s);
140
+ - keeps every generation, so a transcript that grew is recoverable at both
141
+ states;
142
+ - `--verify` re-hashes every blob, and **exits non-zero** if any is missing or
143
+ corrupted;
144
+ - `--restore <dir>` never writes to the live store;
145
+ - `--prune --keep N` bounds it, because storage that only grows is the same
146
+ defect wearing a different hat.
147
+
148
+ VS Code chat sessions (15 GB, which VS Code is not deleting) are skipped by
149
+ default; `--include-vscode-sessions` opts in.
@@ -0,0 +1,446 @@
1
+ /**
2
+ * The durable copy — the one thing in this package allowed to outlive the run
3
+ * that made it.
4
+ *
5
+ * Everything else here is scaffolding: `staging/` is torn down by the run that
6
+ * creates it, and `fix-retention` stops Claude Code's rolling purge *in
7
+ * place*. Retention-in-place is not a backup. It keeps the live store from
8
+ * trimming itself; it does nothing if that store is deleted, corrupted, or
9
+ * lost with the machine. Until now the only thing filling that role was an
10
+ * accident — abandoned staging snapshots nobody was maintaining, which is
11
+ * exactly how 254 GB accumulated unnoticed.
12
+ *
13
+ * So this is deliberate, and built to three rules the accident broke:
14
+ *
15
+ * 1. **It lives outside `staging/`.** `sweepStagingRoot` walks the staging
16
+ * root and only the staging root; nothing here is reachable from it. An
17
+ * archive a cleanup can delete is not an archive.
18
+ * 2. **It deduplicates.** Content-addressed blobs: an unchanged transcript is
19
+ * stored once no matter how many times it is archived. Re-archiving a 3 GB
20
+ * store that has not changed costs kilobytes.
21
+ * 3. **It reports its own size, and can be pruned.** An archive that only
22
+ * grows is the same defect wearing a different hat. `pruneArchive` exists
23
+ * and the CLI prints the total every run, because the failure this package
24
+ * has already demonstrated is disk consumed silently.
25
+ *
26
+ * Layout:
27
+ *
28
+ * archive/
29
+ * objects/<aa>/<sha256> content-addressed, written once
30
+ * manifests/<generation>.json one per archive run
31
+ *
32
+ * A manifest IS the generation: it names every file in the store at that
33
+ * moment and the blob holding its contents. Because blobs are shared, a
34
+ * transcript that grew between runs keeps BOTH versions — which is what an
35
+ * archive is for — while one that did not costs nothing to keep again.
36
+ *
37
+ * The manifest is written LAST. A run killed halfway leaves unreferenced blobs
38
+ * (collected by `pruneArchive`), never a manifest promising data that is not
39
+ * there.
40
+ */
41
+ import { createHash } from "node:crypto";
42
+ import { createReadStream } from "node:fs";
43
+ import * as fs from "node:fs/promises";
44
+ import * as path from "node:path";
45
+ import { pipeline } from "node:stream/promises";
46
+ export const ARCHIVE_SCHEMA = 1;
47
+ export function defaultArchiveRoot(home) {
48
+ return path.join(home, ".ascenda", "history-import", "archive");
49
+ }
50
+ /**
51
+ * What gets archived by default, and why.
52
+ *
53
+ * Claude Code and Cursor are here because they are the stores that actually
54
+ * evaporate: Claude Code purges on a rolling window (disarmed only by
55
+ * `fix-retention`, and only on this machine), and Cursor's db is a single file
56
+ * whose loss takes everything with it.
57
+ *
58
+ * VS Code chat sessions are NOT here by default. They are 15 GB, VS Code is
59
+ * not deleting them, and copying them every run is the precise mistake that
60
+ * filled a disk. `--include-vscode-sessions` opts in.
61
+ */
62
+ export function defaultArchiveSources(paths, includeVsCodeSessions = false) {
63
+ const sources = [
64
+ { store: "claude_code", root: paths.claudeProjects, label: "projects" },
65
+ { store: "cursor", root: paths.cursorStateDb, siblings: ["-wal", "-shm"], label: "state" },
66
+ { store: "vscode", root: paths.vscodeHistory, label: "history" }
67
+ ];
68
+ if (includeVsCodeSessions) {
69
+ sources.push({ store: "vscode", root: paths.vscodeWorkspaceStorage, label: "workspaceStorage" });
70
+ }
71
+ return sources;
72
+ }
73
+ function blobPath(archiveRoot, sha256) {
74
+ return path.join(archiveRoot, "objects", sha256.slice(0, 2), sha256);
75
+ }
76
+ function manifestDir(archiveRoot) {
77
+ return path.join(archiveRoot, "manifests");
78
+ }
79
+ export async function hashFile(file) {
80
+ const hash = createHash("sha256");
81
+ // Streamed rather than read whole: Cursor's store is ~940 MB and several VS
82
+ // Code sessions on this machine are close enough to V8's maximum string
83
+ // length that reading one into memory is a live hazard.
84
+ await pipeline(createReadStream(file), hash);
85
+ return hash.digest("hex");
86
+ }
87
+ /** Every regular file under `root`, or `root` itself when it is a file. */
88
+ async function walkFiles(root) {
89
+ let st;
90
+ try {
91
+ st = await fs.stat(root);
92
+ }
93
+ catch {
94
+ return [];
95
+ }
96
+ if (!st.isDirectory())
97
+ return [{ absolute: root, relative: path.basename(root) }];
98
+ const found = [];
99
+ async function walk(dir, prefix) {
100
+ let entries;
101
+ try {
102
+ entries = await fs.readdir(dir, { withFileTypes: true });
103
+ }
104
+ catch {
105
+ return; // Unreadable directory — surfaced through the caller's counter.
106
+ }
107
+ for (const entry of entries) {
108
+ const absolute = path.join(dir, entry.name);
109
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
110
+ if (entry.isDirectory())
111
+ await walk(absolute, relative);
112
+ else if (entry.isFile())
113
+ found.push({ absolute, relative });
114
+ }
115
+ }
116
+ await walk(root, "");
117
+ return found;
118
+ }
119
+ export async function listManifests(archiveRoot) {
120
+ try {
121
+ return (await fs.readdir(manifestDir(archiveRoot)))
122
+ .filter((f) => f.endsWith(".json"))
123
+ .map((f) => f.slice(0, -".json".length))
124
+ .sort();
125
+ }
126
+ catch {
127
+ return [];
128
+ }
129
+ }
130
+ export async function readManifest(archiveRoot, generation) {
131
+ try {
132
+ const raw = await fs.readFile(path.join(manifestDir(archiveRoot), `${generation}.json`), "utf8");
133
+ const parsed = JSON.parse(raw);
134
+ if (parsed.schema !== ARCHIVE_SCHEMA)
135
+ return null;
136
+ return parsed;
137
+ }
138
+ catch {
139
+ return null;
140
+ }
141
+ }
142
+ export async function readLatestManifest(archiveRoot) {
143
+ const generations = await listManifests(archiveRoot);
144
+ const latest = generations[generations.length - 1];
145
+ return latest ? readManifest(archiveRoot, latest) : null;
146
+ }
147
+ /**
148
+ * Archive every source, storing only contents the archive does not hold.
149
+ *
150
+ * The fast path matters: re-hashing 4 GB every run to discover nothing changed
151
+ * would make this expensive enough to skip, and a backup people skip is not a
152
+ * backup. A file whose (path, size, mtime) matches the previous generation
153
+ * reuses that generation's hash — but only after confirming the blob is still
154
+ * on disk, so a pruned or damaged archive re-materialises the file rather than
155
+ * silently recording a reference to nothing.
156
+ */
157
+ export async function archiveStores(options) {
158
+ const { archiveRoot, sources, generation, now } = options;
159
+ const result = {
160
+ generation,
161
+ manifestPath: path.join(manifestDir(archiveRoot), `${generation}.json`),
162
+ filesArchived: 0,
163
+ filesDeduplicated: 0,
164
+ newBytes: 0,
165
+ totalBytes: 0,
166
+ perStore: {},
167
+ unreadable: 0
168
+ };
169
+ await fs.mkdir(path.join(archiveRoot, "objects"), { recursive: true });
170
+ await fs.mkdir(manifestDir(archiveRoot), { recursive: true });
171
+ const previous = await readLatestManifest(archiveRoot);
172
+ const previousByKey = new Map();
173
+ for (const file of previous?.files ?? []) {
174
+ previousByKey.set(`${file.store}/${file.relativePath}`, file);
175
+ }
176
+ const files = [];
177
+ const roots = {};
178
+ for (const source of sources) {
179
+ roots[`${source.store}/${source.label}`] = source.root;
180
+ const targets = await walkFiles(source.root);
181
+ for (const suffix of source.siblings ?? []) {
182
+ const absolute = source.root + suffix;
183
+ try {
184
+ await fs.stat(absolute);
185
+ targets.push({ absolute, relative: path.basename(absolute) });
186
+ }
187
+ catch {
188
+ // No journal sibling — a checkpointed or non-WAL db. Fine.
189
+ }
190
+ }
191
+ const storeStats = (result.perStore[source.store] ??= {
192
+ files: 0,
193
+ newBytes: 0
194
+ });
195
+ for (const target of targets) {
196
+ // Namespaced by label so two sources of the same store cannot collide.
197
+ const relativePath = `${source.label}/${target.relative}`;
198
+ let st;
199
+ try {
200
+ st = await fs.stat(target.absolute);
201
+ }
202
+ catch {
203
+ result.unreadable += 1;
204
+ continue;
205
+ }
206
+ const prior = previousByKey.get(`${source.store}/${relativePath}`);
207
+ let sha256 = null;
208
+ if (prior && prior.size === st.size && prior.mtimeMs === st.mtimeMs) {
209
+ // Unchanged since the last generation — but only trust that if the
210
+ // blob it points at is genuinely still there.
211
+ try {
212
+ await fs.stat(blobPath(archiveRoot, prior.sha256));
213
+ sha256 = prior.sha256;
214
+ }
215
+ catch {
216
+ sha256 = null; // Blob gone: re-archive rather than record a lie.
217
+ }
218
+ }
219
+ if (sha256 === null) {
220
+ try {
221
+ sha256 = await hashFile(target.absolute);
222
+ }
223
+ catch {
224
+ result.unreadable += 1;
225
+ continue;
226
+ }
227
+ }
228
+ const destination = blobPath(archiveRoot, sha256);
229
+ let alreadyHeld = true;
230
+ try {
231
+ await fs.stat(destination);
232
+ }
233
+ catch {
234
+ alreadyHeld = false;
235
+ }
236
+ if (alreadyHeld) {
237
+ result.filesDeduplicated += 1;
238
+ }
239
+ else {
240
+ await fs.mkdir(path.dirname(destination), { recursive: true });
241
+ // Write under a temporary name and rename. A blob is addressed by its
242
+ // contents, so a half-written one sitting under its final name would
243
+ // be indistinguishable from a good one forever after.
244
+ const temporary = `${destination}.tmp-${generation}`;
245
+ try {
246
+ await fs.copyFile(target.absolute, temporary);
247
+ await fs.rename(temporary, destination);
248
+ result.newBytes += st.size;
249
+ storeStats.newBytes += st.size;
250
+ }
251
+ catch {
252
+ await fs.rm(temporary, { force: true });
253
+ result.unreadable += 1;
254
+ continue;
255
+ }
256
+ }
257
+ files.push({
258
+ store: source.store,
259
+ relativePath,
260
+ sha256,
261
+ size: st.size,
262
+ mtimeMs: st.mtimeMs
263
+ });
264
+ result.filesArchived += 1;
265
+ result.totalBytes += st.size;
266
+ storeStats.files += 1;
267
+ }
268
+ }
269
+ const manifest = {
270
+ schema: ARCHIVE_SCHEMA,
271
+ generation,
272
+ createdAt: now,
273
+ roots,
274
+ files
275
+ };
276
+ // Last, and atomically: a manifest is a promise that every blob it names is
277
+ // on disk, and a run killed mid-copy must not be able to make that promise.
278
+ const temporary = `${result.manifestPath}.tmp`;
279
+ await fs.writeFile(temporary, JSON.stringify(manifest, null, 2) + "\n", "utf8");
280
+ await fs.rename(temporary, result.manifestPath);
281
+ return result;
282
+ }
283
+ /**
284
+ * Re-hash every blob a generation names.
285
+ *
286
+ * An unverified backup is a belief, not a fact — and this package's recurring
287
+ * defect has been surfaces reporting something adjacent to what they claim.
288
+ * `--verify` turns "it archived" into something checkable.
289
+ */
290
+ export async function verifyArchive(archiveRoot, manifest, deep = true) {
291
+ const result = { generation: manifest.generation, checked: 0, missing: [], corrupted: [] };
292
+ const hashed = new Set();
293
+ for (const file of manifest.files) {
294
+ const blob = blobPath(archiveRoot, file.sha256);
295
+ result.checked += 1;
296
+ try {
297
+ await fs.stat(blob);
298
+ }
299
+ catch {
300
+ result.missing.push(file.relativePath);
301
+ continue;
302
+ }
303
+ if (!deep || hashed.has(file.sha256))
304
+ continue;
305
+ hashed.add(file.sha256);
306
+ let actual;
307
+ try {
308
+ actual = await hashFile(blob);
309
+ }
310
+ catch {
311
+ result.corrupted.push(file.relativePath);
312
+ continue;
313
+ }
314
+ if (actual !== file.sha256)
315
+ result.corrupted.push(file.relativePath);
316
+ }
317
+ return result;
318
+ }
319
+ /**
320
+ * Materialise a generation into `destination`.
321
+ *
322
+ * NEVER into the live store. Restoring over `~/.claude/projects` in place
323
+ * would make this an archive command that can destroy the thing it exists to
324
+ * protect, and a mistyped `--generation` would be unrecoverable. The caller
325
+ * copies back by hand, having looked at what came out.
326
+ */
327
+ export async function restoreArchive(archiveRoot, manifest, destination) {
328
+ const result = { restored: 0, skipped: 0, destination };
329
+ for (const file of manifest.files) {
330
+ const blob = blobPath(archiveRoot, file.sha256);
331
+ const target = path.join(destination, file.store, file.relativePath);
332
+ try {
333
+ await fs.mkdir(path.dirname(target), { recursive: true });
334
+ await fs.copyFile(blob, target);
335
+ result.restored += 1;
336
+ }
337
+ catch {
338
+ result.skipped += 1;
339
+ }
340
+ }
341
+ return result;
342
+ }
343
+ /**
344
+ * Drop all but the newest `keep` generations, then delete every blob no
345
+ * surviving manifest references.
346
+ *
347
+ * This exists because the alternative is the defect this package just spent a
348
+ * commit fixing: storage that only ever grows, silently, until something
349
+ * unrelated fails with ENOSPC. Unreferenced blobs left behind by a killed run
350
+ * are collected here too.
351
+ */
352
+ export async function pruneArchive(archiveRoot, keep) {
353
+ const result = { generationsRemoved: [], blobsRemoved: 0, freedBytes: 0 };
354
+ const generations = await listManifests(archiveRoot);
355
+ const doomed = keep >= generations.length ? [] : generations.slice(0, generations.length - keep);
356
+ for (const generation of doomed) {
357
+ try {
358
+ await fs.rm(path.join(manifestDir(archiveRoot), `${generation}.json`), { force: true });
359
+ result.generationsRemoved.push(generation);
360
+ }
361
+ catch {
362
+ // Left in place, so still counted as live below — the safe direction. A
363
+ // blob is deleted only when NO manifest wants it.
364
+ }
365
+ }
366
+ const live = new Set();
367
+ for (const generation of await listManifests(archiveRoot)) {
368
+ const manifest = await readManifest(archiveRoot, generation);
369
+ for (const file of manifest?.files ?? [])
370
+ live.add(file.sha256);
371
+ }
372
+ const objectsRoot = path.join(archiveRoot, "objects");
373
+ let shards = [];
374
+ try {
375
+ shards = (await fs.readdir(objectsRoot, { withFileTypes: true }))
376
+ .filter((e) => e.isDirectory())
377
+ .map((e) => e.name);
378
+ }
379
+ catch {
380
+ return result;
381
+ }
382
+ for (const shard of shards) {
383
+ let blobs = [];
384
+ try {
385
+ blobs = await fs.readdir(path.join(objectsRoot, shard));
386
+ }
387
+ catch {
388
+ continue;
389
+ }
390
+ for (const blob of blobs) {
391
+ // A `.tmp-*` leftover from a killed run is unreferenced by definition.
392
+ if (live.has(blob))
393
+ continue;
394
+ const target = path.join(objectsRoot, shard, blob);
395
+ let size = 0;
396
+ try {
397
+ size = (await fs.stat(target)).size;
398
+ }
399
+ catch {
400
+ continue;
401
+ }
402
+ try {
403
+ await fs.rm(target, { force: true });
404
+ result.blobsRemoved += 1;
405
+ result.freedBytes += size;
406
+ }
407
+ catch {
408
+ // Referenced by nothing and still on disk — reported again next run.
409
+ }
410
+ }
411
+ }
412
+ return result;
413
+ }
414
+ /** Bytes the archive occupies, blobs deduplicated as they are stored. */
415
+ export async function archiveSizeBytes(archiveRoot) {
416
+ const objectsRoot = path.join(archiveRoot, "objects");
417
+ let total = 0;
418
+ let shards = [];
419
+ try {
420
+ shards = (await fs.readdir(objectsRoot, { withFileTypes: true }))
421
+ .filter((e) => e.isDirectory())
422
+ .map((e) => e.name);
423
+ }
424
+ catch {
425
+ return 0;
426
+ }
427
+ for (const shard of shards) {
428
+ let blobs = [];
429
+ try {
430
+ blobs = await fs.readdir(path.join(objectsRoot, shard));
431
+ }
432
+ catch {
433
+ continue;
434
+ }
435
+ for (const blob of blobs) {
436
+ try {
437
+ total += (await fs.stat(path.join(objectsRoot, shard, blob))).size;
438
+ }
439
+ catch {
440
+ // Vanished between listing and stat — not worth failing a size report.
441
+ }
442
+ }
443
+ }
444
+ return total;
445
+ }
446
+ //# sourceMappingURL=archive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive.js","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAIhD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEhC,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,gBAAgB,EAAE,SAAS,CAAC,CAAC;AAClE,CAAC;AA+BD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAiB,EACjB,qBAAqB,GAAG,KAAK;IAE7B,MAAM,OAAO,GAAoB;QAC/B,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,KAAK,CAAC,cAAc,EAAE,KAAK,EAAE,UAAU,EAAE;QACvE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,aAAa,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE;QAC1F,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,aAAa,EAAE,KAAK,EAAE,SAAS,EAAE;KACjE,CAAC;IACF,IAAI,qBAAqB,EAAE,CAAC;QAC1B,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,sBAAsB,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC,CAAC;IACnG,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,QAAQ,CAAC,WAAmB,EAAE,MAAc;IACnD,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,WAAW,CAAC,WAAmB;IACtC,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY;IACzC,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClC,4EAA4E;IAC5E,wEAAwE;IACxE,wDAAwD;IACxD,MAAM,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;IAC7C,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED,2EAA2E;AAC3E,KAAK,UAAU,SAAS,CAAC,IAAY;IACnC,IAAI,EAAE,CAAC;IACP,IAAI,CAAC;QACH,EAAE,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE;QAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAElF,MAAM,KAAK,GAA6C,EAAE,CAAC;IAC3D,KAAK,UAAU,IAAI,CAAC,GAAW,EAAE,MAAc;QAC7C,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3D,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,gEAAgE;QAC1E,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAC5C,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;YACjE,IAAI,KAAK,CAAC,WAAW,EAAE;gBAAE,MAAM,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;iBACnD,IAAI,KAAK,CAAC,MAAM,EAAE;gBAAE,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;IACD,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACrB,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,WAAmB;IACrD,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC;aAChD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;aAClC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;aACvC,IAAI,EAAE,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,WAAmB,EACnB,UAAkB;IAElB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,GAAG,UAAU,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;QACjG,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAoB,CAAC;QAClD,IAAI,MAAM,CAAC,MAAM,KAAK,cAAc;YAAE,OAAO,IAAI,CAAC;QAClD,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,WAAmB;IAC1D,MAAM,WAAW,GAAG,MAAM,aAAa,CAAC,WAAW,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,WAAW,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACnD,OAAO,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC3D,CAAC;AA2BD;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAuB;IACzD,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IAC1D,MAAM,MAAM,GAAkB;QAC5B,UAAU;QACV,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,GAAG,UAAU,OAAO,CAAC;QACvE,aAAa,EAAE,CAAC;QAChB,iBAAiB,EAAE,CAAC;QACpB,QAAQ,EAAE,CAAC;QACX,UAAU,EAAE,CAAC;QACb,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,CAAC;KACd,CAAC;IAEF,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvE,MAAM,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE9D,MAAM,QAAQ,GAAG,MAAM,kBAAkB,CAAC,WAAW,CAAC,CAAC;IACvD,MAAM,aAAa,GAAG,IAAI,GAAG,EAAwB,CAAC;IACtD,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,KAAK,IAAI,EAAE,EAAE,CAAC;QACzC,aAAa,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,YAAY,EAAE,EAAE,IAAI,CAAC,CAAC;IAChE,CAAC;IAED,MAAM,KAAK,GAAmB,EAAE,CAAC;IACjC,MAAM,KAAK,GAA2B,EAAE,CAAC;IAEzC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,KAAK,CAAC,GAAG,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;QACvD,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC7C,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;YAC3C,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC;YACtC,IAAI,CAAC;gBACH,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACxB,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;YAChE,CAAC;YAAC,MAAM,CAAC;gBACP,2DAA2D;YAC7D,CAAC;QACH,CAAC;QAED,MAAM,UAAU,GAAuB,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK;YACxE,KAAK,EAAE,CAAC;YACR,QAAQ,EAAE,CAAC;SACZ,CAAC,CAAC;QAEH,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,uEAAuE;YACvE,MAAM,YAAY,GAAG,GAAG,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YAC1D,IAAI,EAAE,CAAC;YACP,IAAI,CAAC;gBACH,EAAE,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YACtC,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC;gBACvB,SAAS;YACX,CAAC;YAED,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,IAAI,YAAY,EAAE,CAAC,CAAC;YACnE,IAAI,MAAM,GAAkB,IAAI,CAAC;YAEjC,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO,KAAK,EAAE,CAAC,OAAO,EAAE,CAAC;gBACpE,mEAAmE;gBACnE,8CAA8C;gBAC9C,IAAI,CAAC;oBACH,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;oBACnD,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;gBACxB,CAAC;gBAAC,MAAM,CAAC;oBACP,MAAM,GAAG,IAAI,CAAC,CAAC,kDAAkD;gBACnE,CAAC;YACH,CAAC;YAED,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBACpB,IAAI,CAAC;oBACH,MAAM,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBAC3C,CAAC;gBAAC,MAAM,CAAC;oBACP,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC;oBACvB,SAAS;gBACX,CAAC;YACH,CAAC;YAED,MAAM,WAAW,GAAG,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;YAClD,IAAI,WAAW,GAAG,IAAI,CAAC;YACvB,IAAI,CAAC;gBACH,MAAM,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAC7B,CAAC;YAAC,MAAM,CAAC;gBACP,WAAW,GAAG,KAAK,CAAC;YACtB,CAAC;YAED,IAAI,WAAW,EAAE,CAAC;gBAChB,MAAM,CAAC,iBAAiB,IAAI,CAAC,CAAC;YAChC,CAAC;iBAAM,CAAC;gBACN,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC/D,sEAAsE;gBACtE,qEAAqE;gBACrE,sDAAsD;gBACtD,MAAM,SAAS,GAAG,GAAG,WAAW,QAAQ,UAAU,EAAE,CAAC;gBACrD,IAAI,CAAC;oBACH,MAAM,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;oBAC9C,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;oBACxC,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,IAAI,CAAC;oBAC3B,UAAU,CAAC,QAAQ,IAAI,EAAE,CAAC,IAAI,CAAC;gBACjC,CAAC;gBAAC,MAAM,CAAC;oBACP,MAAM,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;oBACxC,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC;oBACvB,SAAS;gBACX,CAAC;YACH,CAAC;YAED,KAAK,CAAC,IAAI,CAAC;gBACT,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,YAAY;gBACZ,MAAM;gBACN,IAAI,EAAE,EAAE,CAAC,IAAI;gBACb,OAAO,EAAE,EAAE,CAAC,OAAO;aACpB,CAAC,CAAC;YACH,MAAM,CAAC,aAAa,IAAI,CAAC,CAAC;YAC1B,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,IAAI,CAAC;YAC7B,UAAU,CAAC,KAAK,IAAI,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAoB;QAChC,MAAM,EAAE,cAAc;QACtB,UAAU;QACV,SAAS,EAAE,GAAG;QACd,KAAK;QACL,KAAK;KACN,CAAC;IACF,4EAA4E;IAC5E,4EAA4E;IAC5E,MAAM,SAAS,GAAG,GAAG,MAAM,CAAC,YAAY,MAAM,CAAC;IAC/C,MAAM,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IAChF,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IAEhD,OAAO,MAAM,CAAC;AAChB,CAAC;AAaD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,WAAmB,EACnB,QAAyB,EACzB,IAAI,GAAG,IAAI;IAEX,MAAM,MAAM,GAAiB,EAAE,UAAU,EAAE,QAAQ,CAAC,UAAU,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;IACzG,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;QAClC,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAChD,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACvC,SAAS;QACX,CAAC;QACD,IAAI,CAAC,IAAI,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC;YAAE,SAAS;QAC/C,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACxB,IAAI,MAAc,CAAC;QACnB,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACzC,SAAS;QACX,CAAC;QACD,IAAI,MAAM,KAAK,IAAI,CAAC,MAAM;YAAE,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAYD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,WAAmB,EACnB,QAAyB,EACzB,WAAmB;IAEnB,MAAM,MAAM,GAAkB,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC;IACvE,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;QAClC,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAChD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QACrE,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC1D,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAChC,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAYD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,WAAmB,EAAE,IAAY;IAClE,MAAM,MAAM,GAAgB,EAAE,kBAAkB,EAAE,EAAE,EAAE,YAAY,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;IACvF,MAAM,WAAW,GAAG,MAAM,aAAa,CAAC,WAAW,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,IAAI,IAAI,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAEjG,KAAK,MAAM,UAAU,IAAI,MAAM,EAAE,CAAC;QAChC,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,GAAG,UAAU,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACxF,MAAM,CAAC,kBAAkB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,wEAAwE;YACxE,kDAAkD;QACpD,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,UAAU,IAAI,MAAM,aAAa,CAAC,WAAW,CAAC,EAAE,CAAC;QAC1D,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QAC7D,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,KAAK,IAAI,EAAE;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAClE,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACtD,IAAI,MAAM,GAAa,EAAE,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;aAC9D,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;aAC9B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,GAAa,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;QAC1D,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,uEAAuE;YACvE,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,SAAS;YAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;YACnD,IAAI,IAAI,GAAG,CAAC,CAAC;YACb,IAAI,CAAC;gBACH,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;YACtC,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;gBACrC,MAAM,CAAC,YAAY,IAAI,CAAC,CAAC;gBACzB,MAAM,CAAC,UAAU,IAAI,IAAI,CAAC;YAC5B,CAAC;YAAC,MAAM,CAAC;gBACP,qEAAqE;YACvE,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,WAAmB;IACxD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACtD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,MAAM,GAAa,EAAE,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;aAC9D,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;aAC9B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAC;IACX,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,GAAa,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;QAC1D,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC;gBACH,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YACrE,CAAC;YAAC,MAAM,CAAC;gBACP,uEAAuE;YACzE,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}