@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 +63 -41
- package/dist/archive.js +446 -0
- package/dist/archive.js.map +1 -0
- package/dist/cli.js +1398 -309
- package/dist/cli.js.map +1 -1
- package/dist/extractors/claudeCode.js +51 -49
- package/dist/extractors/claudeCode.js.map +1 -1
- package/dist/extractors/cursor.js +18 -21
- package/dist/extractors/cursor.js.map +1 -1
- package/dist/extractors/git.js +1 -1
- package/dist/extractors/git.js.map +1 -1
- package/dist/extractors/vscode.js +54 -35
- package/dist/extractors/vscode.js.map +1 -1
- package/dist/localHandoff.js +13 -3
- package/dist/localHandoff.js.map +1 -1
- package/dist/ship.js +102 -9
- package/dist/ship.js.map +1 -1
- package/dist/staging.js +175 -4
- package/dist/staging.js.map +1 -1
- package/dist/types.js +13 -17
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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,
|
|
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 | **
|
|
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:
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
- **Consent scope
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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.
|
package/dist/archive.js
ADDED
|
@@ -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"}
|