wicked-crew 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/audit.d.ts +13 -0
- package/dist/api/audit.d.ts.map +1 -1
- package/dist/api/audit.js +18 -2
- package/dist/api/audit.js.map +1 -1
- package/dist/api/guidance-index.d.ts +39 -0
- package/dist/api/guidance-index.d.ts.map +1 -0
- package/dist/api/guidance-index.js +67 -0
- package/dist/api/guidance-index.js.map +1 -0
- package/dist/api/open-path.d.ts +16 -0
- package/dist/api/open-path.d.ts.map +1 -1
- package/dist/api/open-path.js +22 -0
- package/dist/api/open-path.js.map +1 -1
- package/dist/api/retry-index.d.ts +30 -0
- package/dist/api/retry-index.d.ts.map +1 -0
- package/dist/api/retry-index.js +45 -0
- package/dist/api/retry-index.js.map +1 -0
- package/dist/api/routes.d.ts +53 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +352 -23
- package/dist/api/routes.js.map +1 -1
- package/dist/api/run-files.d.ts +63 -0
- package/dist/api/run-files.d.ts.map +1 -0
- package/dist/api/run-files.js +271 -0
- package/dist/api/run-files.js.map +1 -0
- package/dist/api/server.d.ts +79 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +135 -6
- package/dist/api/server.js.map +1 -1
- package/dist/api/stall-watchdog.d.ts +62 -0
- package/dist/api/stall-watchdog.d.ts.map +1 -0
- package/dist/api/stall-watchdog.js +138 -0
- package/dist/api/stall-watchdog.js.map +1 -0
- package/dist/cli/index.js +78 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/core/adapter.d.ts +24 -10
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +191 -30
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/bridge-reaper.d.ts +134 -0
- package/dist/core/bridge-reaper.d.ts.map +1 -0
- package/dist/core/bridge-reaper.js +286 -0
- package/dist/core/bridge-reaper.js.map +1 -0
- package/dist/core/deliver.d.ts +118 -0
- package/dist/core/deliver.d.ts.map +1 -0
- package/dist/core/deliver.js +241 -0
- package/dist/core/deliver.js.map +1 -0
- package/dist/core/deliverable-floor.d.ts +103 -0
- package/dist/core/deliverable-floor.d.ts.map +1 -0
- package/dist/core/deliverable-floor.js +173 -0
- package/dist/core/deliverable-floor.js.map +1 -0
- package/dist/core/exec.d.ts +2 -0
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js.map +1 -1
- package/dist/core/types.d.ts +79 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -1
- package/dist/interactive/bridge-pool.d.ts +28 -0
- package/dist/interactive/bridge-pool.d.ts.map +1 -1
- package/dist/interactive/bridge-pool.js +67 -10
- package/dist/interactive/bridge-pool.js.map +1 -1
- package/dist/interactive/chat-events.d.ts +207 -0
- package/dist/interactive/chat-events.d.ts.map +1 -0
- package/dist/interactive/chat-events.js +769 -0
- package/dist/interactive/chat-events.js.map +1 -0
- package/dist/interactive/demo-events.d.ts +283 -0
- package/dist/interactive/demo-events.d.ts.map +1 -0
- package/dist/interactive/demo-events.js +889 -0
- package/dist/interactive/demo-events.js.map +1 -0
- package/dist/interactive/draft-events.d.ts +87 -7
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +352 -49
- package/dist/interactive/draft-events.js.map +1 -1
- package/dist/interactive/edit-events.d.ts +22 -0
- package/dist/interactive/edit-events.d.ts.map +1 -1
- package/dist/interactive/edit-events.js +73 -2
- package/dist/interactive/edit-events.js.map +1 -1
- package/dist/interactive/repo-snapshot.d.ts +100 -0
- package/dist/interactive/repo-snapshot.d.ts.map +1 -0
- package/dist/interactive/repo-snapshot.js +289 -0
- package/dist/interactive/repo-snapshot.js.map +1 -0
- package/dist/projects/graph-paths.d.ts +92 -0
- package/dist/projects/graph-paths.d.ts.map +1 -0
- package/dist/projects/graph-paths.js +130 -0
- package/dist/projects/graph-paths.js.map +1 -0
- package/dist/projects/graph.d.ts +179 -0
- package/dist/projects/graph.d.ts.map +1 -0
- package/dist/projects/graph.js +775 -0
- package/dist/projects/graph.js.map +1 -0
- package/dist/projects/routes.d.ts +7 -0
- package/dist/projects/routes.d.ts.map +1 -1
- package/dist/projects/routes.js +122 -0
- package/dist/projects/routes.js.map +1 -1
- package/dist/studio/assets/index-8p8uwCxG.js +530 -0
- package/dist/studio/assets/index-D6S9zUtO.css +32 -0
- package/dist/studio/index.html +5 -3
- package/package.json +3 -3
- package/dist/studio/assets/index-CCwXa1cn.js +0 -428
- package/dist/studio/assets/index-HWxo0h41.css +0 -32
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launch-scoped REPO SNAPSHOTS for grounding governed interactive runs (CREW-UX-8 v4).
|
|
3
|
+
*
|
|
4
|
+
* WHY a snapshot instead of the live repo: the interactive seams launch their governed runs
|
|
5
|
+
* UNBOUND — on a repoRef-bound run the worker's ACP tool-permission stream closes on the first
|
|
6
|
+
* call that needs a permission prompt and the session dies (wicked-core#293) — and an unbound
|
|
7
|
+
* worker's governance boundary is {sandbox, extraWriteRoots, ~/.claude/plugins}, so even READS
|
|
8
|
+
* of the live repo root are governance-denied (wicked-core#294; the v3 "hand the worker the
|
|
9
|
+
* root path to read" design rested on read evidence gathered under a *bound* boundary, which
|
|
10
|
+
* does not transfer). The one location an unbound worker can always read is the inbox the run
|
|
11
|
+
* already writes to — write roots are readable (wicked-core#259) — so grounding means putting
|
|
12
|
+
* a copy of the repo INSIDE the inbox, crew-side, BEFORE the launch, and naming that snapshot
|
|
13
|
+
* in the task.
|
|
14
|
+
*
|
|
15
|
+
* Mechanism, in preference order:
|
|
16
|
+
* 1. `git clone --depth 1 --no-hardlinks file://<root> <dest>` — a shallow LOCAL clone:
|
|
17
|
+
* one commit of history, `--no-hardlinks` so the snapshot never aliases the live object
|
|
18
|
+
* store. Because a clone checks out EVERY tracked path, the result is then swept: tracked
|
|
19
|
+
* `node_modules` trees and every symlink are removed (see below), and the swept snapshot
|
|
20
|
+
* is re-measured against the budget before it counts as usable.
|
|
21
|
+
* 2. A plain file copy that skips `.git`/`node_modules` AND symlinks — for roots that are not
|
|
22
|
+
* git repos (or where git itself is unavailable/failing).
|
|
23
|
+
*
|
|
24
|
+
* SYMLINKS NEVER SURVIVE into a snapshot, on either path (Copilot, crew#313): the snapshot
|
|
25
|
+
* lands inside the worker-visible inbox, so a repo symlink pointing OUTSIDE the repo would
|
|
26
|
+
* hand the worker readable reach beyond the governance boundary — and a link to a huge tree
|
|
27
|
+
* would bypass the size cap the preflight walk (which skips links) enforced. The copy filter
|
|
28
|
+
* refuses them and the post-clone sweep deletes the ones a clone checked out.
|
|
29
|
+
*
|
|
30
|
+
* The snapshot is CAPPED: a working tree whose (`.git`/`node_modules`/symlink-excluded) size
|
|
31
|
+
* exceeds the budget is not snapshotted at all — the caller degrades honestly to an ungrounded
|
|
32
|
+
* launch with a visible status note, never a truncated half-repo the worker would mistake for
|
|
33
|
+
* the whole. Snapshots are launch-scoped: the seams remove them when the run reaches a
|
|
34
|
+
* terminal state (finalize/failure folds), so the inbox never accretes dead clones.
|
|
35
|
+
*
|
|
36
|
+
* OVERLAP REFUSAL: a configured draft dir could place `dest` inside the live repo (or a repo
|
|
37
|
+
* could be registered AT the inbox). Clearing a stale dest with `rm -rf` before noticing that
|
|
38
|
+
* would DELETE live repository content — so source/dest containment is checked (realpath'd,
|
|
39
|
+
* both directions) BEFORE anything is removed, and any overlap refuses the snapshot outright.
|
|
40
|
+
* For the same reason THIS helper creates `dest`'s parent directory (after the check passes) —
|
|
41
|
+
* a caller that pre-created it would already have written into the live repo (Copilot round 2).
|
|
42
|
+
*
|
|
43
|
+
* Fully ASYNC on purpose: this runs inline in a bus handler whose process also feeds ~15s UI
|
|
44
|
+
* heartbeats — a synchronous clone/copy of a big repo would starve the event loop and the
|
|
45
|
+
* canvas would read frozen (Copilot, crew#313). All I/O is fs/promises; the clone goes through
|
|
46
|
+
* the daemon's capped exec chokepoint (`execCapped`, FINDING-016).
|
|
47
|
+
*/
|
|
48
|
+
import { cp, lstat, mkdir, readdir, realpath, rm, stat } from 'node:fs/promises';
|
|
49
|
+
import path, { basename, dirname, join, resolve } from 'node:path';
|
|
50
|
+
import { pathToFileURL } from 'node:url';
|
|
51
|
+
import { execCapped } from '../core/exec.js';
|
|
52
|
+
/** Default snapshot budget (~200MB). A repo whose working tree exceeds this is not
|
|
53
|
+
* snapshotted — the launch proceeds ungrounded, honestly narrated. */
|
|
54
|
+
export const REPO_SNAPSHOT_MAX_BYTES = 200 * 1024 * 1024;
|
|
55
|
+
/** Never snapshotted: git's object store (the clone brings its own shallow one; the copy
|
|
56
|
+
* fallback wants working files only) and dependency trees (huge, reproducible, ungrounding). */
|
|
57
|
+
const SNAPSHOT_SKIP = new Set(['.git', 'node_modules']);
|
|
58
|
+
/** `true` when the tree under `root` exceeds `cap` bytes, skipping {@link SNAPSHOT_SKIP} dirs
|
|
59
|
+
* and symlinks (a link to a huge tree — or out of the repo — must not distort the estimate;
|
|
60
|
+
* the copy filter and post-clone sweep exclude exactly the same set, so the preflight counts
|
|
61
|
+
* what a snapshot would actually hold), with an early exit the moment the cap is passed so a
|
|
62
|
+
* 10GB monorepo is never fully walked just to learn it is too big. */
|
|
63
|
+
async function treeExceeds(root, cap) {
|
|
64
|
+
let total = 0;
|
|
65
|
+
const stack = [root];
|
|
66
|
+
while (stack.length > 0) {
|
|
67
|
+
const dir = stack.pop();
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
continue; // unreadable subdir — the clone/copy will surface a real error if it matters
|
|
74
|
+
}
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
if (entry.isSymbolicLink())
|
|
77
|
+
continue;
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
if (!SNAPSHOT_SKIP.has(entry.name))
|
|
80
|
+
stack.push(join(dir, entry.name));
|
|
81
|
+
}
|
|
82
|
+
else if (entry.isFile()) {
|
|
83
|
+
try {
|
|
84
|
+
total += (await stat(join(dir, entry.name))).size;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// raced deletion — ignore
|
|
88
|
+
}
|
|
89
|
+
if (total > cap)
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
/** Post-clone sweep: delete every symlink (junctions included — boundary escape, cap bypass)
|
|
97
|
+
* and every {@link SNAPSHOT_SKIP} tree a `git clone` checked out (a clone materializes ALL
|
|
98
|
+
* tracked paths — tracked `node_modules` ignores the skip list otherwise). The clone's own
|
|
99
|
+
* top-level `.git` (its shallow object store) is the one deliberate survivor. */
|
|
100
|
+
async function sweepSnapshot(root) {
|
|
101
|
+
const stack = [root];
|
|
102
|
+
while (stack.length > 0) {
|
|
103
|
+
const dir = stack.pop();
|
|
104
|
+
let entries;
|
|
105
|
+
try {
|
|
106
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
const p = join(dir, entry.name);
|
|
113
|
+
if (entry.isSymbolicLink()) {
|
|
114
|
+
await rm(p, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
else if (entry.isDirectory()) {
|
|
117
|
+
if (dir === root && entry.name === '.git')
|
|
118
|
+
continue; // the shallow clone's own store
|
|
119
|
+
if (SNAPSHOT_SKIP.has(entry.name)) {
|
|
120
|
+
await rm(p, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
stack.push(p);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Resolve `p` through its nearest EXISTING ancestor's realpath (the path itself may not exist
|
|
130
|
+
* yet — a fresh dest usually does not), so symlinked parents cannot hide an overlap. Only
|
|
131
|
+
* ENOENT falls to the ancestor walk (the `api/open-path.ts` rule, Copilot round 2): any OTHER
|
|
132
|
+
* error (EACCES, ELOOP, …) means an EXISTING component could not be resolved — a lexical
|
|
133
|
+
* reconstruction there could miss an overlap through the unresolved link, so it THROWS and
|
|
134
|
+
* the caller's overlap-check catch fails closed (no snapshot, degrade). */
|
|
135
|
+
async function realpathNearest(p) {
|
|
136
|
+
let existing = resolve(p);
|
|
137
|
+
const tail = [];
|
|
138
|
+
// Walk up until something exists; dirname() at the root returns itself, which always exists.
|
|
139
|
+
for (;;) {
|
|
140
|
+
try {
|
|
141
|
+
const real = await realpath(existing);
|
|
142
|
+
return tail.length > 0 ? join(real, ...tail) : real;
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
if (err.code !== 'ENOENT')
|
|
146
|
+
throw err;
|
|
147
|
+
const parent = dirname(existing);
|
|
148
|
+
if (parent === existing)
|
|
149
|
+
return existing; // unreachable in practice — a root always resolves
|
|
150
|
+
tail.unshift(basename(existing));
|
|
151
|
+
existing = parent;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Is `child` equal to `parent` or inside it? `relative()`-based (the `api/open-path.ts`
|
|
156
|
+
* containment pattern) — a plain `startsWith(base + sep)` never matches a ROOT base (`/`,
|
|
157
|
+
* `C:\`), whose spelling already ends in the separator (Copilot round 2). */
|
|
158
|
+
function contains(parent, child, p) {
|
|
159
|
+
const rel = p.relative(parent, child);
|
|
160
|
+
if (rel === '')
|
|
161
|
+
return true; // the same path
|
|
162
|
+
if (p.isAbsolute(rel))
|
|
163
|
+
return false; // different drive/tree entirely (win32)
|
|
164
|
+
return rel !== '..' && !rel.startsWith(`..${p.sep}`);
|
|
165
|
+
}
|
|
166
|
+
/** `true` when `a` and `b` name the same path or one contains the other — including when one
|
|
167
|
+
* of them is a filesystem root (`/`, a win32 drive root). Exported for its unit tests. */
|
|
168
|
+
export function pathsOverlap(a, b, p = path) {
|
|
169
|
+
return contains(a, b, p) || contains(b, a, p);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Materialize a snapshot of the repo at `rootPath` into `dest` (creating it), for a worker to
|
|
173
|
+
* READ as grounding context. Resolves `{ok: true}` when `dest` now holds a usable snapshot,
|
|
174
|
+
* `{ok: false, reason}` when it does not — see {@link SnapshotFailureReason}; the reason lets
|
|
175
|
+
* the caller narrate the RIGHT degrade (too large ≠ unreadable ≠ clone failed) — in which case
|
|
176
|
+
* any partial `dest` has been removed and the caller must launch UNGROUNDED (and say so on the
|
|
177
|
+
* thread). Never rejects: a grounding failure must degrade the launch, not kill it.
|
|
178
|
+
*
|
|
179
|
+
* The clone is the git-preferred path, so on a real repo the snapshot is HEAD's tracked
|
|
180
|
+
* content (uncommitted/untracked files are not included) minus swept symlinks/`node_modules`;
|
|
181
|
+
* the copy fallback takes the working tree as-is minus `.git`/`node_modules`/symlinks.
|
|
182
|
+
*/
|
|
183
|
+
export async function snapshotRepo(rootPath, dest, opts = {}) {
|
|
184
|
+
const log = opts.log ?? (() => { });
|
|
185
|
+
const maxBytes = opts.maxBytes ?? REPO_SNAPSHOT_MAX_BYTES;
|
|
186
|
+
try {
|
|
187
|
+
if (!(await stat(rootPath)).isDirectory()) {
|
|
188
|
+
log(`[repo-snapshot] repo root ${rootPath} is not a directory — no snapshot`);
|
|
189
|
+
return { ok: false, reason: 'root-unreadable' };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
log(`[repo-snapshot] repo root ${rootPath} does not exist — no snapshot`);
|
|
194
|
+
return { ok: false, reason: 'root-unreadable' };
|
|
195
|
+
}
|
|
196
|
+
// Overlap refusal comes BEFORE the stale-dest clear (Copilot, crew#313): with a configurable
|
|
197
|
+
// draft dir, `dest` can sit inside the registered repo (or vice versa) — clearing it first
|
|
198
|
+
// would `rm -rf` live repository content, and a copy could recurse into its own output.
|
|
199
|
+
try {
|
|
200
|
+
const realRoot = await realpath(rootPath);
|
|
201
|
+
const realDest = await realpathNearest(dest);
|
|
202
|
+
if (pathsOverlap(realRoot, realDest)) {
|
|
203
|
+
log(`[repo-snapshot] refusing snapshot: destination ${dest} (${realDest}) overlaps the repo root ${rootPath} (${realRoot})`);
|
|
204
|
+
return { ok: false, reason: 'dest-overlap' };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
log(`[repo-snapshot] could not resolve ${rootPath} / ${dest} for the overlap check: ${err instanceof Error ? err.message : String(err)} — no snapshot`);
|
|
209
|
+
return { ok: false, reason: 'root-unreadable' };
|
|
210
|
+
}
|
|
211
|
+
if (await treeExceeds(rootPath, maxBytes)) {
|
|
212
|
+
log(`[repo-snapshot] repo at ${rootPath} exceeds the ${maxBytes}-byte snapshot budget — no snapshot`);
|
|
213
|
+
return { ok: false, reason: 'too-large' };
|
|
214
|
+
}
|
|
215
|
+
// The snapshot's PARENT (the caller's per-run dir) is created HERE — only after containment
|
|
216
|
+
// was validated. The caller must never pre-create it: with an overlapping config that mkdir
|
|
217
|
+
// would write into the live repository before the refusal above could fire (Copilot round 2).
|
|
218
|
+
try {
|
|
219
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
log(`[repo-snapshot] could not create the snapshot parent for ${dest}: ${err instanceof Error ? err.message : String(err)} — no snapshot`);
|
|
223
|
+
return { ok: false, reason: 'copy-failed' };
|
|
224
|
+
}
|
|
225
|
+
// A stale snapshot (crashed prior run, replayed key) never survives into a new launch.
|
|
226
|
+
try {
|
|
227
|
+
await rm(dest, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
log(`[repo-snapshot] could not clear ${dest}: ${err instanceof Error ? err.message : String(err)}`);
|
|
231
|
+
return { ok: false, reason: 'dest-unclearable' };
|
|
232
|
+
}
|
|
233
|
+
// Preferred: shallow local clone. `file://` (not a plain path) is what makes --depth honored
|
|
234
|
+
// on a local source; pathToFileURL keeps the URL spelling right on Windows too.
|
|
235
|
+
try {
|
|
236
|
+
await execCapped('git', ['clone', '--depth', '1', '--no-hardlinks', pathToFileURL(rootPath).href, dest], {
|
|
237
|
+
timeout: 120_000,
|
|
238
|
+
windowsHide: true,
|
|
239
|
+
});
|
|
240
|
+
// The clone checked out EVERY tracked path — sweep what the snapshot contract excludes
|
|
241
|
+
// (tracked node_modules, all symlinks), then verify the SWEPT tree against the budget:
|
|
242
|
+
// the preflight measured the source with these exclusions, so a clean sweep normally
|
|
243
|
+
// passes, but the honest gate is what actually landed on disk.
|
|
244
|
+
await sweepSnapshot(dest);
|
|
245
|
+
if (await treeExceeds(dest, maxBytes)) {
|
|
246
|
+
log(`[repo-snapshot] clone of ${rootPath} exceeds the ${maxBytes}-byte budget after sweep — no snapshot`);
|
|
247
|
+
await rm(dest, { recursive: true, force: true }).catch(() => { });
|
|
248
|
+
return { ok: false, reason: 'too-large' };
|
|
249
|
+
}
|
|
250
|
+
return { ok: true };
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
log(`[repo-snapshot] git clone of ${rootPath} failed (${err instanceof Error ? err.message : String(err)}) — falling back to a file copy`);
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
await rm(dest, { recursive: true, force: true }); // whatever the failed clone left behind
|
|
257
|
+
await cp(rootPath, dest, {
|
|
258
|
+
recursive: true,
|
|
259
|
+
filter: async (src) => {
|
|
260
|
+
if (SNAPSHOT_SKIP.has(basename(src)))
|
|
261
|
+
return false;
|
|
262
|
+
try {
|
|
263
|
+
// Symlinks NEVER ride into the worker-visible inbox: a link out of the repo is a
|
|
264
|
+
// boundary escape, a link to a huge tree a cap bypass (Copilot, crew#313).
|
|
265
|
+
return !(await lstat(src)).isSymbolicLink();
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return false; // raced deletion / unreadable — leave it out
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
// The honest gate is what actually LANDED (same rule as the clone path's post-sweep
|
|
273
|
+
// re-measure): the preflight walked the SOURCE, and a file that grew between that walk and
|
|
274
|
+
// the copy would otherwise put an over-budget tree in the worker-visible inbox
|
|
275
|
+
// (Copilot round 2).
|
|
276
|
+
if (await treeExceeds(dest, maxBytes)) {
|
|
277
|
+
log(`[repo-snapshot] copy of ${rootPath} exceeds the ${maxBytes}-byte budget — no snapshot`);
|
|
278
|
+
await rm(dest, { recursive: true, force: true }).catch(() => { });
|
|
279
|
+
return { ok: false, reason: 'too-large' };
|
|
280
|
+
}
|
|
281
|
+
return { ok: true };
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
log(`[repo-snapshot] file copy of ${rootPath} failed too (${err instanceof Error ? err.message : String(err)}) — no snapshot`);
|
|
285
|
+
await rm(dest, { recursive: true, force: true }).catch(() => { }); // never leave a partial snapshot
|
|
286
|
+
return { ok: false, reason: 'copy-failed' };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=repo-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo-snapshot.js","sourceRoot":"","sources":["../../src/interactive/repo-snapshot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a PROJECT's co-located code graph lives, and what each member repo is called inside it.
|
|
3
|
+
*
|
|
4
|
+
* # Why a module instead of a `join()` at each call site
|
|
5
|
+
*
|
|
6
|
+
* This is `repoPaths.ts`'s posture applied one level up (see that module's header for the bill:
|
|
7
|
+
* five spellings of the per-repo graph path against a sixth inside wicked-core, and every graph
|
|
8
|
+
* query answering "nothing found" about a repo full of code). One producer, one consumer. Nothing
|
|
9
|
+
* outside this module spells a project-graph path or mints a repo label.
|
|
10
|
+
*
|
|
11
|
+
* # Why NOT inside a repo checkout
|
|
12
|
+
*
|
|
13
|
+
* The per-repo graph lives at `<repo>/.codegraph/estate.db` — inside the working tree — and we have
|
|
14
|
+
* just spent the effort removing that directory from six checkouts it had polluted. A PROJECT graph
|
|
15
|
+
* is worse on that axis, not better: it holds N repos, so there is no one checkout it belongs to,
|
|
16
|
+
* and writing it into the first member's tree would make repo A's working directory grow with repo
|
|
17
|
+
* B's symbols. It goes in the daemon's own state directory.
|
|
18
|
+
*
|
|
19
|
+
* # Why `~/.wicked-crew/project-graphs/<projectId>/`
|
|
20
|
+
*
|
|
21
|
+
* `~/.wicked-crew/` is already where this package keeps crew-side per-project state that the engine
|
|
22
|
+
* does not own — `project-settings.json` (DES-MERGE-001 §7.1) sits there today. The project graph is
|
|
23
|
+
* exactly that kind of thing: crew-owned, per-project, derived, and rebuildable. A directory per
|
|
24
|
+
* project (rather than `<projectId>.db` files in one flat folder) keeps the db and the refresh
|
|
25
|
+
* manifest that describes it together, so removing a project's graph is one `rm -rf` that cannot
|
|
26
|
+
* leave a manifest describing a database that is gone.
|
|
27
|
+
*
|
|
28
|
+
* Overridable with `WICKED_CREW_PROJECT_GRAPH_ROOT` — the same escape hatch
|
|
29
|
+
* `WICKED_CREW_PROJECT_SETTINGS` gives the settings store, and what lets the tests and the proof
|
|
30
|
+
* scripts run without touching a developer's real home.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* The estate label charset, copied from the rule that enforces it
|
|
34
|
+
* (`wicked-estate/src/repo_scope.rs::validate_label`): 1–64 chars of `[A-Za-z0-9._-]`, and never
|
|
35
|
+
* `.` or `..`.
|
|
36
|
+
*
|
|
37
|
+
* The constraint is not cosmetic and not ours to relax. estate splices the label into `files.path`
|
|
38
|
+
* AND into every SymbolId derived from it, so a `/` or a `..` in a label would forge paths inside
|
|
39
|
+
* another repo's namespace — the exact collision the whole labelling mechanism exists to prevent.
|
|
40
|
+
* A label that fails this test is refused by estate before a single row is written; we mint labels
|
|
41
|
+
* that pass it instead of discovering that at index time.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isValidRepoLabel(label: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The label one registered repo carries inside a project graph — its REGISTRY ID when that id is
|
|
46
|
+
* already a legal estate label, and a deterministic sanitized form when it is not.
|
|
47
|
+
*
|
|
48
|
+
* WHY the registry id. The label becomes a path prefix on every row the repo writes and shows up in
|
|
49
|
+
* every result this surface returns, so it has to be a name an operator can map back to a repo
|
|
50
|
+
* without a lookup table. It also has to be STABLE: estate binds a label to a repo permanently
|
|
51
|
+
* (re-indexing under a second label is refused as a duplicate; re-using a label for a different repo
|
|
52
|
+
* is refused as a collision), so a label derived from anything mutable — the repo's display name,
|
|
53
|
+
* its root path, its position in the member list — would break the next refresh after a rename or a
|
|
54
|
+
* move. The registry id is the one identifier that is both stable and already unique per repo.
|
|
55
|
+
*
|
|
56
|
+
* Every repo id in the live registry today (`wicked-ledger`, `wicked-vault`, `pageindex-proof`, …)
|
|
57
|
+
* satisfies estate's rule as-is, so the common path is the identity function and the label IS the
|
|
58
|
+
* id. That is not guaranteed for ids minted later, so the fallback is real rather than theoretical:
|
|
59
|
+
* illegal characters collapse to `-`, the stem is capped, and a 12-hex digest of the FULL id is
|
|
60
|
+
* appended — the digest is what keeps `a/b` and `a:b` (which sanitize identically) apart, and what
|
|
61
|
+
* keeps a truncated 200-character id distinct from another that shares its first 51 characters.
|
|
62
|
+
*
|
|
63
|
+
* The mapping is a pure function of the id, which is what makes it safe against estate's permanent
|
|
64
|
+
* binding: the same repo resolves to the same label on every refresh, for the life of the graph.
|
|
65
|
+
*/
|
|
66
|
+
export declare function repoLabel(repoId: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* Project ids are REJECTED, never sanitized.
|
|
69
|
+
*
|
|
70
|
+
* A sanitizing map is what would let two distinct project ids land in one directory and share one
|
|
71
|
+
* database — the same class of silent collision that made a multi-repo graph impossible in the first
|
|
72
|
+
* place, reintroduced at the project level. It is also the traversal guard: the id is a path
|
|
73
|
+
* segment, so `../../x` must never be spelled into a path. Engine-minted ids (`proj_1787…`) and the
|
|
74
|
+
* synthesized `default` both pass; anything that does not is a bug worth hearing about.
|
|
75
|
+
*/
|
|
76
|
+
export declare function assertProjectIdIsPathSafe(projectId: string): void;
|
|
77
|
+
/** The root every project graph directory hangs off. */
|
|
78
|
+
export declare function projectGraphRoot(env?: NodeJS.ProcessEnv): string;
|
|
79
|
+
/** One project's graph directory — holds the database and the manifest that describes it. */
|
|
80
|
+
export declare function projectGraphDir(projectId: string, env?: NodeJS.ProcessEnv): string;
|
|
81
|
+
/** The ONE database holding every member repo of this project, co-located under their labels. */
|
|
82
|
+
export declare function projectGraphDb(projectId: string, env?: NodeJS.ProcessEnv): string;
|
|
83
|
+
/**
|
|
84
|
+
* What the last refresh put in that database, per repo — the record incremental refresh reads.
|
|
85
|
+
*
|
|
86
|
+
* Deliberately a SIDECAR rather than a read of estate's own `repo:<label>:commit` meta: estate
|
|
87
|
+
* exposes that only through `stats`, whose output is human text with no `--json`, and deciding
|
|
88
|
+
* whether to re-index a repo by regex-matching a report is the kind of coupling that breaks on a
|
|
89
|
+
* formatting change with no test to catch it.
|
|
90
|
+
*/
|
|
91
|
+
export declare function projectGraphManifest(projectId: string, env?: NodeJS.ProcessEnv): string;
|
|
92
|
+
//# sourceMappingURL=graph-paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-paths.d.ts","sourceRoot":"","sources":["../../src/projects/graph-paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAMH;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAQvD;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAcjE;AAED,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAE7E;AAED,6FAA6F;AAC7F,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAG/F;AAED,iGAAiG;AACjG,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAE9F;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAEpG"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a PROJECT's co-located code graph lives, and what each member repo is called inside it.
|
|
3
|
+
*
|
|
4
|
+
* # Why a module instead of a `join()` at each call site
|
|
5
|
+
*
|
|
6
|
+
* This is `repoPaths.ts`'s posture applied one level up (see that module's header for the bill:
|
|
7
|
+
* five spellings of the per-repo graph path against a sixth inside wicked-core, and every graph
|
|
8
|
+
* query answering "nothing found" about a repo full of code). One producer, one consumer. Nothing
|
|
9
|
+
* outside this module spells a project-graph path or mints a repo label.
|
|
10
|
+
*
|
|
11
|
+
* # Why NOT inside a repo checkout
|
|
12
|
+
*
|
|
13
|
+
* The per-repo graph lives at `<repo>/.codegraph/estate.db` — inside the working tree — and we have
|
|
14
|
+
* just spent the effort removing that directory from six checkouts it had polluted. A PROJECT graph
|
|
15
|
+
* is worse on that axis, not better: it holds N repos, so there is no one checkout it belongs to,
|
|
16
|
+
* and writing it into the first member's tree would make repo A's working directory grow with repo
|
|
17
|
+
* B's symbols. It goes in the daemon's own state directory.
|
|
18
|
+
*
|
|
19
|
+
* # Why `~/.wicked-crew/project-graphs/<projectId>/`
|
|
20
|
+
*
|
|
21
|
+
* `~/.wicked-crew/` is already where this package keeps crew-side per-project state that the engine
|
|
22
|
+
* does not own — `project-settings.json` (DES-MERGE-001 §7.1) sits there today. The project graph is
|
|
23
|
+
* exactly that kind of thing: crew-owned, per-project, derived, and rebuildable. A directory per
|
|
24
|
+
* project (rather than `<projectId>.db` files in one flat folder) keeps the db and the refresh
|
|
25
|
+
* manifest that describes it together, so removing a project's graph is one `rm -rf` that cannot
|
|
26
|
+
* leave a manifest describing a database that is gone.
|
|
27
|
+
*
|
|
28
|
+
* Overridable with `WICKED_CREW_PROJECT_GRAPH_ROOT` — the same escape hatch
|
|
29
|
+
* `WICKED_CREW_PROJECT_SETTINGS` gives the settings store, and what lets the tests and the proof
|
|
30
|
+
* scripts run without touching a developer's real home.
|
|
31
|
+
*/
|
|
32
|
+
import { createHash } from 'node:crypto';
|
|
33
|
+
import { homedir } from 'node:os';
|
|
34
|
+
import { join } from 'node:path';
|
|
35
|
+
/**
|
|
36
|
+
* The estate label charset, copied from the rule that enforces it
|
|
37
|
+
* (`wicked-estate/src/repo_scope.rs::validate_label`): 1–64 chars of `[A-Za-z0-9._-]`, and never
|
|
38
|
+
* `.` or `..`.
|
|
39
|
+
*
|
|
40
|
+
* The constraint is not cosmetic and not ours to relax. estate splices the label into `files.path`
|
|
41
|
+
* AND into every SymbolId derived from it, so a `/` or a `..` in a label would forge paths inside
|
|
42
|
+
* another repo's namespace — the exact collision the whole labelling mechanism exists to prevent.
|
|
43
|
+
* A label that fails this test is refused by estate before a single row is written; we mint labels
|
|
44
|
+
* that pass it instead of discovering that at index time.
|
|
45
|
+
*/
|
|
46
|
+
export function isValidRepoLabel(label) {
|
|
47
|
+
return (label.length > 0 &&
|
|
48
|
+
label.length <= 64 &&
|
|
49
|
+
label !== '.' &&
|
|
50
|
+
label !== '..' &&
|
|
51
|
+
/^[A-Za-z0-9._-]+$/.test(label));
|
|
52
|
+
}
|
|
53
|
+
/** Sanitized-label budget: 51 + `-` + 12 hex = exactly estate's 64-character ceiling. */
|
|
54
|
+
const HASH_LEN = 12;
|
|
55
|
+
const STEM_LEN = 64 - 1 - HASH_LEN;
|
|
56
|
+
/**
|
|
57
|
+
* The label one registered repo carries inside a project graph — its REGISTRY ID when that id is
|
|
58
|
+
* already a legal estate label, and a deterministic sanitized form when it is not.
|
|
59
|
+
*
|
|
60
|
+
* WHY the registry id. The label becomes a path prefix on every row the repo writes and shows up in
|
|
61
|
+
* every result this surface returns, so it has to be a name an operator can map back to a repo
|
|
62
|
+
* without a lookup table. It also has to be STABLE: estate binds a label to a repo permanently
|
|
63
|
+
* (re-indexing under a second label is refused as a duplicate; re-using a label for a different repo
|
|
64
|
+
* is refused as a collision), so a label derived from anything mutable — the repo's display name,
|
|
65
|
+
* its root path, its position in the member list — would break the next refresh after a rename or a
|
|
66
|
+
* move. The registry id is the one identifier that is both stable and already unique per repo.
|
|
67
|
+
*
|
|
68
|
+
* Every repo id in the live registry today (`wicked-ledger`, `wicked-vault`, `pageindex-proof`, …)
|
|
69
|
+
* satisfies estate's rule as-is, so the common path is the identity function and the label IS the
|
|
70
|
+
* id. That is not guaranteed for ids minted later, so the fallback is real rather than theoretical:
|
|
71
|
+
* illegal characters collapse to `-`, the stem is capped, and a 12-hex digest of the FULL id is
|
|
72
|
+
* appended — the digest is what keeps `a/b` and `a:b` (which sanitize identically) apart, and what
|
|
73
|
+
* keeps a truncated 200-character id distinct from another that shares its first 51 characters.
|
|
74
|
+
*
|
|
75
|
+
* The mapping is a pure function of the id, which is what makes it safe against estate's permanent
|
|
76
|
+
* binding: the same repo resolves to the same label on every refresh, for the life of the graph.
|
|
77
|
+
*/
|
|
78
|
+
export function repoLabel(repoId) {
|
|
79
|
+
if (isValidRepoLabel(repoId))
|
|
80
|
+
return repoId;
|
|
81
|
+
const digest = createHash('sha256').update(repoId).digest('hex').slice(0, HASH_LEN);
|
|
82
|
+
const stem = repoId.replace(/[^A-Za-z0-9._-]/g, '-').slice(0, STEM_LEN);
|
|
83
|
+
return `${stem}-${digest}`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Project ids are REJECTED, never sanitized.
|
|
87
|
+
*
|
|
88
|
+
* A sanitizing map is what would let two distinct project ids land in one directory and share one
|
|
89
|
+
* database — the same class of silent collision that made a multi-repo graph impossible in the first
|
|
90
|
+
* place, reintroduced at the project level. It is also the traversal guard: the id is a path
|
|
91
|
+
* segment, so `../../x` must never be spelled into a path. Engine-minted ids (`proj_1787…`) and the
|
|
92
|
+
* synthesized `default` both pass; anything that does not is a bug worth hearing about.
|
|
93
|
+
*/
|
|
94
|
+
export function assertProjectIdIsPathSafe(projectId) {
|
|
95
|
+
if (!isValidRepoLabel(projectId)) {
|
|
96
|
+
// Name the ACTUAL rule that rejected it (Copilot on #326). `.` and `..` match the stated
|
|
97
|
+
// character set, so a message quoting only the charset tells an operator their id is legal
|
|
98
|
+
// and rejected in the same breath — and sends them looking for an illegal character that
|
|
99
|
+
// is not there.
|
|
100
|
+
const why = projectId === '.' || projectId === '..'
|
|
101
|
+
? `${JSON.stringify(projectId)} is a relative path segment, so it cannot name a directory`
|
|
102
|
+
: `expected 1-64 chars of [A-Za-z0-9._-]`;
|
|
103
|
+
throw new Error(`project id ${JSON.stringify(projectId)} cannot address a project graph directory: ${why}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** The root every project graph directory hangs off. */
|
|
107
|
+
export function projectGraphRoot(env = process.env) {
|
|
108
|
+
return env['WICKED_CREW_PROJECT_GRAPH_ROOT'] ?? join(homedir(), '.wicked-crew', 'project-graphs');
|
|
109
|
+
}
|
|
110
|
+
/** One project's graph directory — holds the database and the manifest that describes it. */
|
|
111
|
+
export function projectGraphDir(projectId, env = process.env) {
|
|
112
|
+
assertProjectIdIsPathSafe(projectId);
|
|
113
|
+
return join(projectGraphRoot(env), projectId);
|
|
114
|
+
}
|
|
115
|
+
/** The ONE database holding every member repo of this project, co-located under their labels. */
|
|
116
|
+
export function projectGraphDb(projectId, env = process.env) {
|
|
117
|
+
return join(projectGraphDir(projectId, env), 'code-graph.db');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* What the last refresh put in that database, per repo — the record incremental refresh reads.
|
|
121
|
+
*
|
|
122
|
+
* Deliberately a SIDECAR rather than a read of estate's own `repo:<label>:commit` meta: estate
|
|
123
|
+
* exposes that only through `stats`, whose output is human text with no `--json`, and deciding
|
|
124
|
+
* whether to re-index a repo by regex-matching a report is the kind of coupling that breaks on a
|
|
125
|
+
* formatting change with no test to catch it.
|
|
126
|
+
*/
|
|
127
|
+
export function projectGraphManifest(projectId, env = process.env) {
|
|
128
|
+
return join(projectGraphDir(projectId, env), 'manifest.json');
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=graph-paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-paths.js","sourceRoot":"","sources":["../../src/projects/graph-paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,OAAO,CACL,KAAK,CAAC,MAAM,GAAG,CAAC;QAChB,KAAK,CAAC,MAAM,IAAI,EAAE;QAClB,KAAK,KAAK,GAAG;QACb,KAAK,KAAK,IAAI;QACd,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,CAChC,CAAC;AACJ,CAAC;AAED,yFAAyF;AACzF,MAAM,QAAQ,GAAG,EAAE,CAAC;AACpB,MAAM,QAAQ,GAAG,EAAE,GAAG,CAAC,GAAG,QAAQ,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,SAAS,CAAC,MAAc;IACtC,IAAI,gBAAgB,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IAC5C,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACpF,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACxE,OAAO,GAAG,IAAI,IAAI,MAAM,EAAE,CAAC;AAC7B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,yBAAyB,CAAC,SAAiB;IACzD,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC,EAAE,CAAC;QACjC,yFAAyF;QACzF,2FAA2F;QAC3F,yFAAyF;QACzF,gBAAgB;QAChB,MAAM,GAAG,GACP,SAAS,KAAK,GAAG,IAAI,SAAS,KAAK,IAAI;YACrC,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,4DAA4D;YAC1F,CAAC,CAAC,uCAAuC,CAAC;QAC9C,MAAM,IAAI,KAAK,CACb,cAAc,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,8CAA8C,GAAG,EAAE,CAC3F,CAAC;IACJ,CAAC;AACH,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,gBAAgB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACnE,OAAO,GAAG,CAAC,gCAAgC,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,cAAc,EAAE,gBAAgB,CAAC,CAAC;AACpG,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,eAAe,CAAC,SAAiB,EAAE,MAAyB,OAAO,CAAC,GAAG;IACrF,yBAAyB,CAAC,SAAS,CAAC,CAAC;IACrC,OAAO,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE,SAAS,CAAC,CAAC;AAChD,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,cAAc,CAAC,SAAiB,EAAE,MAAyB,OAAO,CAAC,GAAG;IACpF,OAAO,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,eAAe,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAiB,EAAE,MAAyB,OAAO,CAAC,GAAG;IAC1F,OAAO,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,eAAe,CAAC,CAAC;AAChE,CAAC"}
|