@nanonets/graft 0.7.1 → 0.8.1
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 +170 -70
- package/dist/ask/ask.d.ts +14 -0
- package/dist/ask/ask.d.ts.map +1 -1
- package/dist/ask/ask.js +24 -5
- package/dist/ask/ask.js.map +1 -1
- package/dist/claude/format.d.ts +1 -1
- package/dist/claude/format.d.ts.map +1 -1
- package/dist/claude/format.js +6 -3
- package/dist/claude/format.js.map +1 -1
- package/dist/claude/hooks.d.ts +15 -0
- package/dist/claude/hooks.d.ts.map +1 -1
- package/dist/claude/hooks.js +55 -4
- package/dist/claude/hooks.js.map +1 -1
- package/dist/claude/init.d.ts +17 -0
- package/dist/claude/init.d.ts.map +1 -1
- package/dist/claude/init.js +45 -19
- package/dist/claude/init.js.map +1 -1
- package/dist/claude/settings-merge.d.ts.map +1 -1
- package/dist/claude/settings-merge.js +9 -1
- package/dist/claude/settings-merge.js.map +1 -1
- package/dist/claude/skill-template.d.ts.map +1 -1
- package/dist/claude/skill-template.js +31 -8
- package/dist/claude/skill-template.js.map +1 -1
- package/dist/claude/state.d.ts +2 -20
- package/dist/claude/state.d.ts.map +1 -1
- package/dist/claude/state.js +9 -72
- package/dist/claude/state.js.map +1 -1
- package/dist/cli-picker.d.ts +60 -0
- package/dist/cli-picker.d.ts.map +1 -0
- package/dist/cli-picker.js +268 -0
- package/dist/cli-picker.js.map +1 -0
- package/dist/cli.js +104 -13
- package/dist/cli.js.map +1 -1
- package/dist/context/check.d.ts +21 -0
- package/dist/context/check.d.ts.map +1 -1
- package/dist/context/check.js +32 -1
- package/dist/context/check.js.map +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -0
- package/dist/engine.js.map +1 -1
- package/dist/graph/build.d.ts +13 -2
- package/dist/graph/build.d.ts.map +1 -1
- package/dist/graph/build.js +93 -19
- package/dist/graph/build.js.map +1 -1
- package/dist/graph/extract-cache.d.ts +96 -0
- package/dist/graph/extract-cache.d.ts.map +1 -0
- package/dist/graph/extract-cache.js +199 -0
- package/dist/graph/extract-cache.js.map +1 -0
- package/dist/graph/fingerprint.d.ts +70 -0
- package/dist/graph/fingerprint.d.ts.map +1 -0
- package/dist/graph/fingerprint.js +147 -0
- package/dist/graph/fingerprint.js.map +1 -0
- package/dist/graph/load.d.ts +11 -0
- package/dist/graph/load.d.ts.map +1 -1
- package/dist/graph/load.js +14 -0
- package/dist/graph/load.js.map +1 -1
- package/dist/graph/refresh.d.ts +49 -0
- package/dist/graph/refresh.d.ts.map +1 -0
- package/dist/graph/refresh.js +202 -0
- package/dist/graph/refresh.js.map +1 -0
- package/dist/graph/source-files.d.ts +18 -0
- package/dist/graph/source-files.d.ts.map +1 -0
- package/dist/graph/source-files.js +36 -0
- package/dist/graph/source-files.js.map +1 -0
- package/dist/graph/traverse-cli.d.ts.map +1 -1
- package/dist/graph/traverse-cli.js +16 -6
- package/dist/graph/traverse-cli.js.map +1 -1
- package/dist/hosts/codex-hooks.d.ts +8 -0
- package/dist/hosts/codex-hooks.d.ts.map +1 -1
- package/dist/hosts/codex-hooks.js +27 -4
- package/dist/hosts/codex-hooks.js.map +1 -1
- package/dist/hosts/init.d.ts +2 -0
- package/dist/hosts/init.d.ts.map +1 -1
- package/dist/hosts/init.js +5 -2
- package/dist/hosts/init.js.map +1 -1
- package/dist/hosts/mcp-config.d.ts +18 -0
- package/dist/hosts/mcp-config.d.ts.map +1 -1
- package/dist/hosts/mcp-config.js +31 -7
- package/dist/hosts/mcp-config.js.map +1 -1
- package/dist/hosts/plan.d.ts +32 -0
- package/dist/hosts/plan.d.ts.map +1 -0
- package/dist/hosts/plan.js +67 -0
- package/dist/hosts/plan.js.map +1 -0
- package/dist/hosts/registry.d.ts.map +1 -1
- package/dist/hosts/registry.js +9 -0
- package/dist/hosts/registry.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +5 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +2 -2
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +108 -88
- package/dist/mcp/tools.js.map +1 -1
- package/dist/search/grep-cli.js +1 -1
- package/dist/search/grep-cli.js.map +1 -1
- package/dist/util/state.d.ts +45 -0
- package/dist/util/state.d.ts.map +1 -0
- package/dist/util/state.js +121 -0
- package/dist/util/state.js.map +1 -0
- package/package.json +1 -1
package/dist/graph/build.js
CHANGED
|
@@ -6,18 +6,29 @@
|
|
|
6
6
|
* 2. Parse each with tree-sitter and emit one NodeV1 per definition.
|
|
7
7
|
* 3. Write a sorted graph.json.
|
|
8
8
|
* Edges (M2) and LLM summary/crux (M3) layer onto this without changing it.
|
|
9
|
+
*
|
|
10
|
+
* Step 2 is memoized per file (`extract-cache.ts`): a file whose bytes haven't
|
|
11
|
+
* moved replays its last parse instead of re-running tree-sitter, so a rebuild
|
|
12
|
+
* costs ~the files that changed. Everything after extraction still runs over the
|
|
13
|
+
* whole node set, so an incremental build's output is byte-identical to a cold
|
|
14
|
+
* one — the invariant `test/graph-incremental.test.ts` pins down.
|
|
9
15
|
*/
|
|
10
16
|
import { readFileSync } from "node:fs";
|
|
11
17
|
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
12
18
|
import { walkDir } from "../ingest/fs.js";
|
|
13
19
|
import { contextDirFor, ensureGitignored } from "../context/node-file.js";
|
|
14
20
|
import { extractFile, languageOf } from "./extract.js";
|
|
21
|
+
import { contentHash } from "../util/id.js";
|
|
22
|
+
import { emptyExtractCache, readExtractCache, writeExtractCache, } from "./extract-cache.js";
|
|
23
|
+
import { writeFingerprint } from "./fingerprint.js";
|
|
24
|
+
import { listSourceStats } from "./source-files.js";
|
|
15
25
|
import { resolveEdges } from "./resolve.js";
|
|
16
26
|
import { enrichGraph } from "./enrich.js";
|
|
17
27
|
import { readGraph, writeGraph, wiringPath } from "./write.js";
|
|
18
28
|
import { writeCards, writeIndex, writeCovers } from "./cards.js";
|
|
19
29
|
import { writeAskIndex } from "../ask/index-file.js";
|
|
20
30
|
import { discoverScopes, scopeOf } from "./scopes.js";
|
|
31
|
+
export { listSourceFiles } from "./source-files.js";
|
|
21
32
|
/** Minimum non-file node count for a discovered sub-scope to stand on its own
|
|
22
33
|
* (over-split guard 3). A scope with fewer nodes than this is folded into the
|
|
23
34
|
* root scope — node counts aren't known until after the graph is assembled,
|
|
@@ -45,10 +56,6 @@ function applyMinSubstanceGuard(scopes, nodes) {
|
|
|
45
56
|
}
|
|
46
57
|
return kept;
|
|
47
58
|
}
|
|
48
|
-
/** The source files a graph build parses: supported languages, minus the output dir. */
|
|
49
|
-
export function listSourceFiles(root, outDir) {
|
|
50
|
-
return walkDir(root).filter((f) => !f.startsWith(outDir) && languageOf(f) !== null);
|
|
51
|
-
}
|
|
52
59
|
/** Every Go module in the repo: each `go.mod`'s declared `module` path and the repo
|
|
53
60
|
* directory it lives in (posix, `.` for the root). Found anywhere in the tree, so a
|
|
54
61
|
* monorepo whose module is in a subdir (e.g. `backend/go.mod`) resolves too. Lets edge
|
|
@@ -74,36 +81,85 @@ function readGoModules(root) {
|
|
|
74
81
|
export async function buildGraph(dir, opts = {}) {
|
|
75
82
|
const root = resolve(dir);
|
|
76
83
|
const outDir = contextDirFor(root, opts.contextDir);
|
|
77
|
-
const files =
|
|
84
|
+
const files = listSourceStats(root, outDir);
|
|
78
85
|
const discoveredScopes = discoverScopes(root);
|
|
79
86
|
const nodes = [];
|
|
80
87
|
const rawEdges = [];
|
|
81
88
|
const sources = new Map();
|
|
82
89
|
const langs = new Set();
|
|
83
90
|
const errors = [];
|
|
84
|
-
files.
|
|
85
|
-
|
|
91
|
+
// Tier-1 memo: unchanged files replay their last parse. `entries` is rebuilt
|
|
92
|
+
// from scratch each run and keyed only by files currently on disk, so deletions
|
|
93
|
+
// fall out of both the cache and the fingerprint with no separate pruning pass.
|
|
94
|
+
const priorExtract = opts.reuse === false ? emptyExtractCache() : readExtractCache(outDir);
|
|
95
|
+
const entries = {};
|
|
96
|
+
let parsed = 0;
|
|
97
|
+
let reused = 0;
|
|
98
|
+
files.forEach((f, i) => {
|
|
99
|
+
const rel = f.rel;
|
|
86
100
|
opts.onProgress?.({ phase: "parse", index: i, total: files.length, file: rel });
|
|
87
|
-
const lang = languageOf(
|
|
101
|
+
const lang = languageOf(f.abs);
|
|
102
|
+
const cached = priorExtract.files[rel];
|
|
103
|
+
// Every file is read and hashed, every build — only the *parse* is memoized.
|
|
104
|
+
// The tempting optimization is to trust the probe's `(size, mtimeMs)` and skip
|
|
105
|
+
// the read too, but then `graft build` inherits the probe's blind spot: on a
|
|
106
|
+
// filesystem with coarse mtime granularity, a same-length edit inside the same
|
|
107
|
+
// second is invisible, so `graft check` reports drift (it always re-hashes) and
|
|
108
|
+
// the `graft build` it tells you to run refuses to repair it — forever. A stat
|
|
109
|
+
// may decide whether a *query* bothers rebuilding; it may not decide what the
|
|
110
|
+
// rebuild itself looks at. Reading is ~0.05ms/file against the ~4.6ms parse
|
|
111
|
+
// this still skips.
|
|
88
112
|
let source;
|
|
89
113
|
try {
|
|
90
|
-
source = readFileSync(
|
|
114
|
+
source = readFileSync(f.abs, "utf8");
|
|
91
115
|
}
|
|
92
116
|
catch (err) {
|
|
93
|
-
|
|
117
|
+
const message = `${rel}: ${err instanceof Error ? err.message : String(err)}`;
|
|
118
|
+
errors.push(message);
|
|
119
|
+
// Record it anyway (with the stat we do have) so the freshness probe's
|
|
120
|
+
// fast path doesn't report this file as new on every single query.
|
|
121
|
+
entries[rel] = { size: f.size, mtimeMs: f.mtimeMs, hash: "", nodes: [], rawEdges: [], error: message };
|
|
94
122
|
return;
|
|
95
123
|
}
|
|
124
|
+
const hash = contentHash(source);
|
|
125
|
+
if (cached && hash === cached.hash) {
|
|
126
|
+
entries[rel] = { ...cached, size: f.size, mtimeMs: f.mtimeMs };
|
|
127
|
+
sources.set(rel, source);
|
|
128
|
+
reused++;
|
|
129
|
+
if (cached.error) {
|
|
130
|
+
errors.push(cached.error); // this file failed to parse last time too
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
nodes.push(...cached.nodes);
|
|
134
|
+
rawEdges.push(...cached.rawEdges);
|
|
135
|
+
langs.add(lang);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
parsed++;
|
|
96
139
|
try {
|
|
97
140
|
const { nodes: fileNodes, rawEdges: fileEdges } = extractFile(rel, source, lang);
|
|
98
141
|
nodes.push(...fileNodes);
|
|
99
142
|
rawEdges.push(...fileEdges);
|
|
100
143
|
sources.set(rel, source);
|
|
101
144
|
langs.add(lang);
|
|
145
|
+
entries[rel] = { size: f.size, mtimeMs: f.mtimeMs, hash, nodes: fileNodes, rawEdges: fileEdges };
|
|
102
146
|
}
|
|
103
147
|
catch (err) {
|
|
104
|
-
|
|
148
|
+
const message = `${rel}: parse failed — ${err instanceof Error ? err.message : String(err)}`;
|
|
149
|
+
errors.push(message);
|
|
150
|
+
entries[rel] = { size: f.size, mtimeMs: f.mtimeMs, hash, nodes: [], rawEdges: [], error: message };
|
|
105
151
|
}
|
|
106
152
|
});
|
|
153
|
+
// Persist the memo BEFORE enrichment, because `enrichGraph` mutates these very
|
|
154
|
+
// node objects (summary/crux/summary_state) and the cache must only ever hold
|
|
155
|
+
// pristine Tier-1 output — otherwise a replayed node would arrive pre-enriched
|
|
156
|
+
// and a cold build and an incremental build could disagree. The meaning layer
|
|
157
|
+
// has its own cache (wiring.json itself, keyed on body_hash); this one is
|
|
158
|
+
// strictly about not re-parsing.
|
|
159
|
+
writeExtractCache(outDir, {
|
|
160
|
+
...emptyExtractCache(),
|
|
161
|
+
files: entries,
|
|
162
|
+
});
|
|
107
163
|
const edges = resolveEdges(nodes, rawEdges, { goModules: readGoModules(root) });
|
|
108
164
|
// graph.json is its own Tier-2 cache: fold in the prior meaning layer so an
|
|
109
165
|
// unchanged body is never re-summarized (and a Tier-1-only run never wipes it).
|
|
@@ -130,9 +186,6 @@ export async function buildGraph(dir, opts = {}) {
|
|
|
130
186
|
edges,
|
|
131
187
|
};
|
|
132
188
|
const graphPath = writeGraph(graph, outDir);
|
|
133
|
-
// The graph is a local, regenerable cache — make sure git ignores it. Runs on
|
|
134
|
-
// every build (cheap, idempotent), so a fresh clone's first build self-ignores.
|
|
135
|
-
ensureGitignored(root, outDir);
|
|
136
189
|
// `ask`'s token/IDF sidecar — moves per-query corpus tokenization to build
|
|
137
190
|
// time (~45% of query time on a 32k-node graph, profiled). Lives in the
|
|
138
191
|
// cache dir; `ask` falls back to live tokenization when it's absent/stale.
|
|
@@ -150,13 +203,32 @@ export async function buildGraph(dir, opts = {}) {
|
|
|
150
203
|
catch (err) {
|
|
151
204
|
errors.push(`ask-index: ${err instanceof Error ? err.message : String(err)}`);
|
|
152
205
|
}
|
|
206
|
+
// The fingerprint claims exactly one thing: "the graph on disk was built from
|
|
207
|
+
// these source bytes." Nothing about the projections below — which is why it is
|
|
208
|
+
// safe to write here, and why `graphOnly` builds (the query path, which stops
|
|
209
|
+
// right after this line) are still recorded as fresh.
|
|
210
|
+
writeFingerprint(outDir, entries);
|
|
153
211
|
// Tier-2 passive surface: project the nodes into per-file markdown cards, and
|
|
154
212
|
// refresh the INDEX roster. Pure projection — no LLM, no network.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
213
|
+
//
|
|
214
|
+
// Skipped entirely on the query path (`graphOnly`). Retrieval answers from
|
|
215
|
+
// wiring.json and the ask sidecar; the markdown surface is for humans and for
|
|
216
|
+
// the agent's own greps, and it is rebuilt by an explicit `graft build` — which
|
|
217
|
+
// is what the Claude Code `Stop` hook already runs at the end of a turn. Keeping
|
|
218
|
+
// it off the query path is what makes a refresh cheap, leaves the repo untouched
|
|
219
|
+
// (`ensureGitignored` writes `.gitignore`, which a read has no business doing),
|
|
220
|
+
// and means a failure here can never be triggered by a query.
|
|
221
|
+
let cardStats = { written: 0, pruned: 0, files: [] };
|
|
222
|
+
if (!opts.graphOnly) {
|
|
223
|
+
// The graph is a local, regenerable cache — make sure git ignores it. Cheap and
|
|
224
|
+
// idempotent, so a fresh clone's first build self-ignores.
|
|
225
|
+
ensureGitignored(root, outDir);
|
|
226
|
+
cardStats = writeCards(graph, outDir);
|
|
227
|
+
writeIndex(outDir, cardStats.files);
|
|
228
|
+
// Backfill concept nodes with their `covers:` symbol/file:line list (the
|
|
229
|
+
// OKF↔Wiring link). No-op when there are no concept nodes (a $0 build).
|
|
230
|
+
writeCovers(graph, outDir);
|
|
231
|
+
}
|
|
160
232
|
const byKind = {};
|
|
161
233
|
for (const n of nodes)
|
|
162
234
|
byKind[n.kind] = (byKind[n.kind] ?? 0) + 1;
|
|
@@ -168,6 +240,8 @@ export async function buildGraph(dir, opts = {}) {
|
|
|
168
240
|
graphPath,
|
|
169
241
|
cards: cardStats.written,
|
|
170
242
|
files: files.length,
|
|
243
|
+
parsed,
|
|
244
|
+
reused,
|
|
171
245
|
nodes: nodes.length,
|
|
172
246
|
edges: edges.length,
|
|
173
247
|
byKind,
|
package/dist/graph/build.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/graph/build.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/graph/build.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { RawEdge } from "./extract.js";
|
|
2
|
+
import type { NodeV1 } from "./types.js";
|
|
3
|
+
export interface ExtractEntry {
|
|
4
|
+
size: number;
|
|
5
|
+
mtimeMs: number;
|
|
6
|
+
/** sha256 of the file's bytes — the same value as its `kind:"file"` node's `body_hash`. */
|
|
7
|
+
hash: string;
|
|
8
|
+
nodes: NodeV1[];
|
|
9
|
+
rawEdges: RawEdge[];
|
|
10
|
+
/** Set when this file couldn't be read or parsed. The entry exists anyway so the
|
|
11
|
+
* freshness probe doesn't flag the file as new on every query; replaying it
|
|
12
|
+
* re-reports the same error and contributes no nodes, exactly as a cold build
|
|
13
|
+
* would. */
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ExtractCache {
|
|
17
|
+
version: number;
|
|
18
|
+
/** Identity of the extractor that produced these entries. */
|
|
19
|
+
extractor: string;
|
|
20
|
+
/** repo-relative source path → its last parse. */
|
|
21
|
+
files: Record<string, ExtractEntry>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Where this graft's memo lives: `<outDir>/.cache/extract.<stamp>.json`.
|
|
25
|
+
*
|
|
26
|
+
* The stamp is in the *filename*, not just inside the file, so two grafts working
|
|
27
|
+
* on one repo keep separate memos instead of evicting each other. That is the
|
|
28
|
+
* default install, not an exotic case: `graft init` wires the MCP server as
|
|
29
|
+
* `npx -y @nanonets/graft` (which resolves the latest published version) while the
|
|
30
|
+
* Claude Code hooks run the locally installed one. The moment those two versions
|
|
31
|
+
* differ, a single shared file means the prompt hook and every MCP retrieval take
|
|
32
|
+
* turns rejecting each other's entries and cold-re-parsing the whole repo — the memo
|
|
33
|
+
* would never help anyone.
|
|
34
|
+
*
|
|
35
|
+
* Null stamp → null path → no memo at all. See {@link extractorStamp}.
|
|
36
|
+
*/
|
|
37
|
+
export declare function extractCachePath(outDir: string): string | null;
|
|
38
|
+
/** Keep `.cache/` from growing a file per version forever: after writing, drop all
|
|
39
|
+
* but the newest `keep` files sharing a prefix. Best-effort and never fatal — this
|
|
40
|
+
* is a cache directory, and a failure here costs disk, not correctness. */
|
|
41
|
+
export declare function pruneSidecars(cacheDir: string, prefix: string, keep?: number): void;
|
|
42
|
+
/**
|
|
43
|
+
* Identity of the code that produces the cached parses. Both sidecars key on it,
|
|
44
|
+
* so *anything* that can change extraction output has to change this string.
|
|
45
|
+
*
|
|
46
|
+
* Content-hashed over every sibling module in this directory, plus the package
|
|
47
|
+
* version. Two earlier instincts are deliberately rejected:
|
|
48
|
+
*
|
|
49
|
+
* - **Not a timestamp.** This was `mtime:size` of `extract.js` alone, which is
|
|
50
|
+
* wrong in both directions. Too loose: the parse of `db.count()` depends on
|
|
51
|
+
* `bindings.ts` deciding that `db` is a `UserRepo` (that answer is stored *in*
|
|
52
|
+
* the cached entry, as an edge's `recvType`), and fixing a binding bug leaves
|
|
53
|
+
* `extract.js`'s emitted bytes untouched — so any build that skips rewriting
|
|
54
|
+
* unchanged output keeps the old stamp and silently replays pre-fix edges. Too
|
|
55
|
+
* tight: a rebuild that rewrites identical content moves the mtime and throws
|
|
56
|
+
* away the whole memo for nothing.
|
|
57
|
+
* - **Not a hand-kept list of modules.** A list is correct exactly until someone
|
|
58
|
+
* moves parse logic into a module nobody added to it, and the failure is silent.
|
|
59
|
+
*
|
|
60
|
+
* Hashing the directory over-invalidates a little — editing any `graph/` module
|
|
61
|
+
* costs one cold rebuild — which is no more often than a version bump already
|
|
62
|
+
* costs, and always in the safe direction. The package version is folded in so a
|
|
63
|
+
* tree-sitter grammar upgrade (which changes parse output without changing any of
|
|
64
|
+
* graft's own files) invalidates too.
|
|
65
|
+
*
|
|
66
|
+
* Measured at ~0.5ms for 21 files / 556KB, paid once per process.
|
|
67
|
+
*
|
|
68
|
+
* **Null when no identity can be established at all**, and that is deliberately not
|
|
69
|
+
* a string. It used to return `"unknown"` on failure — but `"unknown"` was then
|
|
70
|
+
* *written into the sidecars as a real identity*, and every later run compared equal
|
|
71
|
+
* to it, so any environment where stamping fails (a `pkg`/`bun-compile` single-file
|
|
72
|
+
* build, an asar-style read, a directory that can't be listed) permanently lost the
|
|
73
|
+
* ability to notice an extractor change. A sentinel that doubles as a valid value
|
|
74
|
+
* silently disables the whole mechanism. Null forces callers to decide instead, and
|
|
75
|
+
* {@link extractCachePath} decides not to have a memo.
|
|
76
|
+
*/
|
|
77
|
+
export declare function extractorStamp(): string | null;
|
|
78
|
+
/**
|
|
79
|
+
* Hash every `ext` file in `dir`, plus `version`. Null when the directory holds no
|
|
80
|
+
* such file. Separated from {@link extractorStamp} so a test can prove the property
|
|
81
|
+
* that matters: a change to *any* module in the directory moves the stamp, not just
|
|
82
|
+
* the one the old implementation happened to watch.
|
|
83
|
+
*/
|
|
84
|
+
export declare function stampDir(dir: string, ext: string, version?: string): string | null;
|
|
85
|
+
export declare function emptyExtractCache(): ExtractCache;
|
|
86
|
+
/** The cache for `outDir`, or an empty one when it's absent, unparseable, written by
|
|
87
|
+
* a different cache version, or when this graft has no identity to key on (then
|
|
88
|
+
* every build is cold, which is slow but never wrong). The stamp is in the filename,
|
|
89
|
+
* so the `extractor` field is a second check rather than the only one. */
|
|
90
|
+
export declare function readExtractCache(outDir: string): ExtractCache;
|
|
91
|
+
/** Best-effort write — a full graph is already on disk by the time this runs, so an
|
|
92
|
+
* unwritable cache dir must never fail the build (it only costs the next build its
|
|
93
|
+
* reuse). Returns false when nothing was written, including the deliberate case of
|
|
94
|
+
* having no extractor identity: a parse we can't attribute must never be replayed. */
|
|
95
|
+
export declare function writeExtractCache(outDir: string, cache: ExtractCache): boolean;
|
|
96
|
+
//# sourceMappingURL=extract-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract-cache.d.ts","sourceRoot":"","sources":["../../src/graph/extract-cache.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAMzC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB;;;gBAGY;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACrC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG9D;AAED;;2EAE2E;AAC3E,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,SAAI,GAAG,IAAI,CAa9E;AAQD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAG9C;AAoBD;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,MAAM,GAAG,IAAI,CAU9E;AAYD,wBAAgB,iBAAiB,IAAI,YAAY,CAEhD;AAED;;;0EAG0E;AAC1E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAS7D;AAED;;;sFAGsF;AACtF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAU9E"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-file memo for Tier-1 extraction — `<outDir>/.cache/extract.json`.
|
|
3
|
+
*
|
|
4
|
+
* `extractFile` is pure and file-local (`rel`, `source`, `lang` → `{nodes,
|
|
5
|
+
* rawEdges}`), which is the whole trick: an unchanged file's parse result can be
|
|
6
|
+
* replayed from disk instead of re-running tree-sitter. That turns
|
|
7
|
+
* {@link buildGraph} from "always re-parse the repo" (~4.6ms/file) into "re-parse
|
|
8
|
+
* only what moved", which is what makes a rebuild cheap enough to run *before*
|
|
9
|
+
* every query (see `graph/refresh.ts`).
|
|
10
|
+
*
|
|
11
|
+
* Everything downstream of extraction (edge resolution, enrichment, the graph
|
|
12
|
+
* write, the ask sidecar, cards) still runs over the whole merged node set, so no
|
|
13
|
+
* other invariant changes — an incremental build must produce a byte-identical
|
|
14
|
+
* `wiring.json` to a cold one.
|
|
15
|
+
*
|
|
16
|
+
* Crucially the cached nodes keep `body_text`: `writeGraph` strips it before
|
|
17
|
+
* serializing, so `wiring.json` can never be the reuse source — the ask sidecar
|
|
18
|
+
* needs a body for every node, including the ones we didn't re-parse.
|
|
19
|
+
*
|
|
20
|
+
* Lives under `.cache/` (gitignored, regenerate-anytime) next to `ask-index.json`
|
|
21
|
+
* and `summaries.json`. Two things invalidate the whole file: a bump of
|
|
22
|
+
* {@link CACHE_VERSION} for a change of on-disk shape, and a change to the
|
|
23
|
+
* extraction code itself, caught by content-hashing it — see
|
|
24
|
+
* {@link extractorStamp}. Neither asks anyone to remember to bump anything.
|
|
25
|
+
*/
|
|
26
|
+
import { readFileSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { dirname, extname, join } from "node:path";
|
|
30
|
+
import { CACHE_DIR } from "../context/node-file.js";
|
|
31
|
+
import { readJson, writeJsonAtomic } from "../util/state.js";
|
|
32
|
+
/** Bump when the on-disk shape below changes. */
|
|
33
|
+
const CACHE_VERSION = 1;
|
|
34
|
+
const EXTRACT_CACHE_PREFIX = "extract";
|
|
35
|
+
/**
|
|
36
|
+
* Where this graft's memo lives: `<outDir>/.cache/extract.<stamp>.json`.
|
|
37
|
+
*
|
|
38
|
+
* The stamp is in the *filename*, not just inside the file, so two grafts working
|
|
39
|
+
* on one repo keep separate memos instead of evicting each other. That is the
|
|
40
|
+
* default install, not an exotic case: `graft init` wires the MCP server as
|
|
41
|
+
* `npx -y @nanonets/graft` (which resolves the latest published version) while the
|
|
42
|
+
* Claude Code hooks run the locally installed one. The moment those two versions
|
|
43
|
+
* differ, a single shared file means the prompt hook and every MCP retrieval take
|
|
44
|
+
* turns rejecting each other's entries and cold-re-parsing the whole repo — the memo
|
|
45
|
+
* would never help anyone.
|
|
46
|
+
*
|
|
47
|
+
* Null stamp → null path → no memo at all. See {@link extractorStamp}.
|
|
48
|
+
*/
|
|
49
|
+
export function extractCachePath(outDir) {
|
|
50
|
+
const stamp = extractorStamp();
|
|
51
|
+
return stamp === null ? null : join(outDir, CACHE_DIR, `${EXTRACT_CACHE_PREFIX}.${stamp}.json`);
|
|
52
|
+
}
|
|
53
|
+
/** Keep `.cache/` from growing a file per version forever: after writing, drop all
|
|
54
|
+
* but the newest `keep` files sharing a prefix. Best-effort and never fatal — this
|
|
55
|
+
* is a cache directory, and a failure here costs disk, not correctness. */
|
|
56
|
+
export function pruneSidecars(cacheDir, prefix, keep = 2) {
|
|
57
|
+
try {
|
|
58
|
+
const mine = readdirSync(cacheDir)
|
|
59
|
+
.filter((f) => f.startsWith(`${prefix}.`) && f.endsWith(".json"))
|
|
60
|
+
.map((f) => {
|
|
61
|
+
const full = join(cacheDir, f);
|
|
62
|
+
return { full, mtimeMs: statSync(full).mtimeMs };
|
|
63
|
+
})
|
|
64
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
65
|
+
for (const f of mine.slice(keep))
|
|
66
|
+
rmSync(f.full, { force: true });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
/* nothing here is load-bearing */
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Computed once per process — this can't change under a running process without
|
|
73
|
+
* the module graph itself being swapped, and a long-lived MCP server holding the
|
|
74
|
+
* identity of the code it actually loaded is the correct answer anyway.
|
|
75
|
+
* `undefined` = not computed yet; `null` = computed, and there is no identity. */
|
|
76
|
+
let memoizedStamp;
|
|
77
|
+
/**
|
|
78
|
+
* Identity of the code that produces the cached parses. Both sidecars key on it,
|
|
79
|
+
* so *anything* that can change extraction output has to change this string.
|
|
80
|
+
*
|
|
81
|
+
* Content-hashed over every sibling module in this directory, plus the package
|
|
82
|
+
* version. Two earlier instincts are deliberately rejected:
|
|
83
|
+
*
|
|
84
|
+
* - **Not a timestamp.** This was `mtime:size` of `extract.js` alone, which is
|
|
85
|
+
* wrong in both directions. Too loose: the parse of `db.count()` depends on
|
|
86
|
+
* `bindings.ts` deciding that `db` is a `UserRepo` (that answer is stored *in*
|
|
87
|
+
* the cached entry, as an edge's `recvType`), and fixing a binding bug leaves
|
|
88
|
+
* `extract.js`'s emitted bytes untouched — so any build that skips rewriting
|
|
89
|
+
* unchanged output keeps the old stamp and silently replays pre-fix edges. Too
|
|
90
|
+
* tight: a rebuild that rewrites identical content moves the mtime and throws
|
|
91
|
+
* away the whole memo for nothing.
|
|
92
|
+
* - **Not a hand-kept list of modules.** A list is correct exactly until someone
|
|
93
|
+
* moves parse logic into a module nobody added to it, and the failure is silent.
|
|
94
|
+
*
|
|
95
|
+
* Hashing the directory over-invalidates a little — editing any `graph/` module
|
|
96
|
+
* costs one cold rebuild — which is no more often than a version bump already
|
|
97
|
+
* costs, and always in the safe direction. The package version is folded in so a
|
|
98
|
+
* tree-sitter grammar upgrade (which changes parse output without changing any of
|
|
99
|
+
* graft's own files) invalidates too.
|
|
100
|
+
*
|
|
101
|
+
* Measured at ~0.5ms for 21 files / 556KB, paid once per process.
|
|
102
|
+
*
|
|
103
|
+
* **Null when no identity can be established at all**, and that is deliberately not
|
|
104
|
+
* a string. It used to return `"unknown"` on failure — but `"unknown"` was then
|
|
105
|
+
* *written into the sidecars as a real identity*, and every later run compared equal
|
|
106
|
+
* to it, so any environment where stamping fails (a `pkg`/`bun-compile` single-file
|
|
107
|
+
* build, an asar-style read, a directory that can't be listed) permanently lost the
|
|
108
|
+
* ability to notice an extractor change. A sentinel that doubles as a valid value
|
|
109
|
+
* silently disables the whole mechanism. Null forces callers to decide instead, and
|
|
110
|
+
* {@link extractCachePath} decides not to have a memo.
|
|
111
|
+
*/
|
|
112
|
+
export function extractorStamp() {
|
|
113
|
+
if (memoizedStamp === undefined)
|
|
114
|
+
memoizedStamp = computeStamp();
|
|
115
|
+
return memoizedStamp;
|
|
116
|
+
}
|
|
117
|
+
function computeStamp() {
|
|
118
|
+
try {
|
|
119
|
+
const self = fileURLToPath(import.meta.url);
|
|
120
|
+
// `.js` when running from `dist/`, `.ts` under tsx — take the extension from
|
|
121
|
+
// our own filename rather than guessing which layout we're in.
|
|
122
|
+
const dir = dirname(self);
|
|
123
|
+
const hashed = stampDir(dir, extname(self), packageVersion(dir) ?? "");
|
|
124
|
+
if (hashed)
|
|
125
|
+
return hashed;
|
|
126
|
+
// Couldn't read the modules (bundled into one file, say). The version alone is
|
|
127
|
+
// a weaker identity — it can't see a local edit — but it still turns over on
|
|
128
|
+
// every upgrade, which is the case that ships broken parses to users.
|
|
129
|
+
const v = packageVersion(dir);
|
|
130
|
+
return v ? `v${v}` : null;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Hash every `ext` file in `dir`, plus `version`. Null when the directory holds no
|
|
138
|
+
* such file. Separated from {@link extractorStamp} so a test can prove the property
|
|
139
|
+
* that matters: a change to *any* module in the directory moves the stamp, not just
|
|
140
|
+
* the one the old implementation happened to watch.
|
|
141
|
+
*/
|
|
142
|
+
export function stampDir(dir, ext, version = "") {
|
|
143
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(ext)).sort();
|
|
144
|
+
if (!files.length)
|
|
145
|
+
return null;
|
|
146
|
+
const h = createHash("sha256");
|
|
147
|
+
h.update(version);
|
|
148
|
+
for (const f of files) {
|
|
149
|
+
h.update(f); // a rename is a change, even at identical content
|
|
150
|
+
h.update(readFileSync(join(dir, f)));
|
|
151
|
+
}
|
|
152
|
+
return h.digest("hex").slice(0, 16);
|
|
153
|
+
}
|
|
154
|
+
/** graft's own version, or null when it can't be read. */
|
|
155
|
+
function packageVersion(graphDir) {
|
|
156
|
+
try {
|
|
157
|
+
const pkg = JSON.parse(readFileSync(join(graphDir, "..", "..", "package.json"), "utf8"));
|
|
158
|
+
return pkg.version ?? null;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export function emptyExtractCache() {
|
|
165
|
+
return { version: CACHE_VERSION, extractor: extractorStamp() ?? "", files: {} };
|
|
166
|
+
}
|
|
167
|
+
/** The cache for `outDir`, or an empty one when it's absent, unparseable, written by
|
|
168
|
+
* a different cache version, or when this graft has no identity to key on (then
|
|
169
|
+
* every build is cold, which is slow but never wrong). The stamp is in the filename,
|
|
170
|
+
* so the `extractor` field is a second check rather than the only one. */
|
|
171
|
+
export function readExtractCache(outDir) {
|
|
172
|
+
const path = extractCachePath(outDir);
|
|
173
|
+
const stamp = extractorStamp();
|
|
174
|
+
if (path === null || stamp === null)
|
|
175
|
+
return emptyExtractCache();
|
|
176
|
+
const c = readJson(path);
|
|
177
|
+
if (!c || c.version !== CACHE_VERSION || c.extractor !== stamp || typeof c.files !== "object") {
|
|
178
|
+
return emptyExtractCache();
|
|
179
|
+
}
|
|
180
|
+
return { version: c.version, extractor: c.extractor, files: c.files ?? {} };
|
|
181
|
+
}
|
|
182
|
+
/** Best-effort write — a full graph is already on disk by the time this runs, so an
|
|
183
|
+
* unwritable cache dir must never fail the build (it only costs the next build its
|
|
184
|
+
* reuse). Returns false when nothing was written, including the deliberate case of
|
|
185
|
+
* having no extractor identity: a parse we can't attribute must never be replayed. */
|
|
186
|
+
export function writeExtractCache(outDir, cache) {
|
|
187
|
+
const path = extractCachePath(outDir);
|
|
188
|
+
if (path === null)
|
|
189
|
+
return false;
|
|
190
|
+
try {
|
|
191
|
+
writeJsonAtomic(path, cache, true);
|
|
192
|
+
pruneSidecars(join(outDir, CACHE_DIR), EXTRACT_CACHE_PREFIX);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=extract-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract-cache.js","sourceRoot":"","sources":["../../src/graph/extract-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type ExtractEntry } from "./extract-cache.js";
|
|
2
|
+
/** `[size, mtimeMs, hash]` — positional to keep the file small. */
|
|
3
|
+
type Print = [number, number, string];
|
|
4
|
+
export interface Fingerprint {
|
|
5
|
+
version: number;
|
|
6
|
+
/** The extractor that produced the graph these prints describe — the same stamp
|
|
7
|
+
* `extract.json` carries. Without it the two sidecars can disagree about whether
|
|
8
|
+
* the graph is current: an extractor change correctly drops every memo entry,
|
|
9
|
+
* yet the prints still match the tree byte-for-byte, so the probe would report
|
|
10
|
+
* clean and queries would keep answering from nodes the old extractor built. */
|
|
11
|
+
extractor: string;
|
|
12
|
+
files: Record<string, Print>;
|
|
13
|
+
}
|
|
14
|
+
/** What moved since the last build. Empty in all three arrays = nothing to do. */
|
|
15
|
+
export interface Drift {
|
|
16
|
+
/** Recorded files whose bytes differ now. */
|
|
17
|
+
changed: string[];
|
|
18
|
+
/** Source files with no record — new, or never indexed. */
|
|
19
|
+
added: string[];
|
|
20
|
+
/** Recorded files that are gone from disk. */
|
|
21
|
+
removed: string[];
|
|
22
|
+
}
|
|
23
|
+
/** `<outDir>/.cache/fingerprint.<stamp>.json` — keyed by extractor identity for the
|
|
24
|
+
* same reason the memo is (see {@link extractCachePath}): two grafts on one repo,
|
|
25
|
+
* typically an `npx` MCP server and a locally installed hook binary, must not keep
|
|
26
|
+
* invalidating each other's prints and forcing a cold rebuild on every call. */
|
|
27
|
+
export declare function fingerprintPath(outDir: string): string;
|
|
28
|
+
export declare function readFingerprint(outDir: string): Fingerprint | null;
|
|
29
|
+
/** Project the extraction cache's entries into the probe sidecar. Best-effort:
|
|
30
|
+
* the graph is already on disk when this runs, so a failed write costs the next
|
|
31
|
+
* probe its fast path and nothing more. */
|
|
32
|
+
export declare function writeFingerprint(outDir: string, entries: Record<string, ExtractEntry>): boolean;
|
|
33
|
+
/** `GRAFT_REFRESH=hash` — never trust a stat, confirm every file by its bytes. */
|
|
34
|
+
export declare function alwaysHash(): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* May a recorded `(size, mtimeMs, hash)` be trusted for the file `f` as it is on
|
|
37
|
+
* disk now, without reading it?
|
|
38
|
+
*
|
|
39
|
+
* **The probe's rule only.** `buildGraph` deliberately does not use this: it reads
|
|
40
|
+
* and hashes every file, every time. A stat may decide whether a query bothers
|
|
41
|
+
* rebuilding; it may not decide what the rebuild itself looks at — otherwise
|
|
42
|
+
* `graft check` (which always re-hashes) can report drift that the `graft build` it
|
|
43
|
+
* recommends then refuses to repair. `GRAFT_REFRESH=hash` is the escape hatch for
|
|
44
|
+
* the probe's blind spot: a same-length edit inside one mtime tick.
|
|
45
|
+
*
|
|
46
|
+
* An empty `hash` means the last build never got the bytes — always re-read.
|
|
47
|
+
* A *parse* failure keeps its real hash, so it stays on the fast path: re-reading
|
|
48
|
+
* bytes that failed to parse yesterday just fails to parse again.
|
|
49
|
+
*/
|
|
50
|
+
export declare function statUnchanged(rec: {
|
|
51
|
+
size: number;
|
|
52
|
+
mtimeMs: number;
|
|
53
|
+
hash: string;
|
|
54
|
+
}, f: {
|
|
55
|
+
size: number;
|
|
56
|
+
mtimeMs: number;
|
|
57
|
+
}): boolean;
|
|
58
|
+
export declare function isClean(d: Drift): boolean;
|
|
59
|
+
export declare function driftCount(d: Drift): number;
|
|
60
|
+
/**
|
|
61
|
+
* Diff the working tree against the last build's fingerprint. Returns null when
|
|
62
|
+
* there is no fingerprint to compare against (never built, or built by a version
|
|
63
|
+
* that didn't write one) — callers should treat that as "unknown", not "clean".
|
|
64
|
+
*
|
|
65
|
+
* `GRAFT_REFRESH=hash` skips the stat fast path and hashes every file, for the
|
|
66
|
+
* rare tooling that rewrites content while preserving size and mtime.
|
|
67
|
+
*/
|
|
68
|
+
export declare function probeDrift(root: string, outDir: string): Drift | null;
|
|
69
|
+
export {};
|
|
70
|
+
//# sourceMappingURL=fingerprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../src/graph/fingerprint.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAiC,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AActF,mEAAmE;AACnE,KAAK,KAAK,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAEtC,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB;;;;oFAIgF;IAChF,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;CAC9B;AAED,kFAAkF;AAClF,MAAM,WAAW,KAAK;IACpB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,8CAA8C;IAC9C,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;gFAGgF;AAChF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAKlE;AAED;;2CAE2C;AAC3C,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,OAAO,CAU/F;AAED,kFAAkF;AAClF,wBAAgB,UAAU,IAAI,OAAO,CAEpC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACpD,CAAC,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACnC,OAAO,CAIT;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,GAAG,OAAO,CAEzC;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAE3C;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAqCrE"}
|