@nanonets/graft 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -15
- package/dist/ask/ask.js +1 -1
- package/dist/ask/ask.js.map +1 -1
- package/dist/claude/format.d.ts +40 -13
- package/dist/claude/format.d.ts.map +1 -1
- package/dist/claude/format.js +54 -15
- 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 +52 -3
- 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 +47 -21
- package/dist/claude/init.js.map +1 -1
- package/dist/claude/settings-merge.d.ts.map +1 -1
- package/dist/claude/settings-merge.js +19 -2
- 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 +13 -5
- package/dist/claude/skill-template.js.map +1 -1
- package/dist/claude/state.d.ts +5 -20
- package/dist/claude/state.d.ts.map +1 -1
- package/dist/claude/state.js +10 -73
- 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 +101 -12
- package/dist/cli.js.map +1 -1
- package/dist/context/node-file.d.ts +21 -0
- package/dist/context/node-file.d.ts.map +1 -1
- package/dist/context/node-file.js +63 -1
- package/dist/context/node-file.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 +98 -20
- package/dist/graph/build.js.map +1 -1
- package/dist/graph/cards.d.ts.map +1 -1
- package/dist/graph/cards.js +8 -0
- package/dist/graph/cards.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 +1 -1
- package/dist/graph/traverse-cli.js +2 -2
- package/dist/graph/traverse.d.ts +2 -2
- package/dist/graph/traverse.js +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 +29 -1
- package/dist/hosts/mcp-config.d.ts.map +1 -1
- package/dist/hosts/mcp-config.js +77 -10
- 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/instructions.d.ts +29 -0
- package/dist/mcp/instructions.d.ts.map +1 -0
- package/dist/mcp/instructions.js +55 -0
- package/dist/mcp/instructions.js.map +1 -0
- package/dist/mcp/server.d.ts +6 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +15 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +4 -2
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +144 -104
- package/dist/mcp/tools.js.map +1 -1
- package/dist/search/grep-cli.js +1 -1
- package/dist/search/grep.js +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
|
@@ -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"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cheap "has the working tree moved?" probe that gates the pre-query rebuild.
|
|
3
|
+
*
|
|
4
|
+
* Every graft retrieval call runs this, so it has to be ~free on the common
|
|
5
|
+
* unchanged path: a walk + one `stat` per source file, no reads, no parsing.
|
|
6
|
+
* Measured at ~3ms for 280 files. Only files whose `(size, mtimeMs)` disagree
|
|
7
|
+
* with the last build's record get read and hashed, which is what keeps a `touch`
|
|
8
|
+
* or a `git checkout` of identical bytes from triggering a pointless rebuild.
|
|
9
|
+
*
|
|
10
|
+
* Note graft has no notion of git here (it never shells out to git, never reads
|
|
11
|
+
* `.git`): drift is measured against the bytes in the working tree, so an
|
|
12
|
+
* uncommitted, staged, or committed edit all look the same — which is the point.
|
|
13
|
+
*
|
|
14
|
+
* `<outDir>/.cache/fingerprint.json` is a projection of the extraction cache
|
|
15
|
+
* (`extract-cache.ts`) minus the parse results, written by the same build. Keeping
|
|
16
|
+
* it separate means the probe reads ~10KB instead of the multi-MB parse cache. The
|
|
17
|
+
* two can only ever disagree by one sidecar going missing, and both directions
|
|
18
|
+
* degrade safely: no fingerprint → "unknown, rebuild"; no parse cache → the
|
|
19
|
+
* rebuild is just cold.
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { CACHE_DIR } from "../context/node-file.js";
|
|
24
|
+
import { contentHash } from "../util/id.js";
|
|
25
|
+
import { readJson, writeJsonAtomic } from "../util/state.js";
|
|
26
|
+
import { extractorStamp, pruneSidecars } from "./extract-cache.js";
|
|
27
|
+
import { listSourceStats } from "./source-files.js";
|
|
28
|
+
const FINGERPRINT_PREFIX = "fingerprint";
|
|
29
|
+
const FINGERPRINT_VERSION = 1;
|
|
30
|
+
/** The identity this graft's prints are filed under. Unlike the extract memo, a
|
|
31
|
+
* missing extractor identity is *not* disqualifying here: freshness is a claim about
|
|
32
|
+
* source bytes, and a build with no memo is merely cold, never wrong. So stamp what
|
|
33
|
+
* we can and fall back to a shared bucket. */
|
|
34
|
+
function stamp() {
|
|
35
|
+
return extractorStamp() ?? "nostamp";
|
|
36
|
+
}
|
|
37
|
+
/** `<outDir>/.cache/fingerprint.<stamp>.json` — keyed by extractor identity for the
|
|
38
|
+
* same reason the memo is (see {@link extractCachePath}): two grafts on one repo,
|
|
39
|
+
* typically an `npx` MCP server and a locally installed hook binary, must not keep
|
|
40
|
+
* invalidating each other's prints and forcing a cold rebuild on every call. */
|
|
41
|
+
export function fingerprintPath(outDir) {
|
|
42
|
+
return join(outDir, CACHE_DIR, `${FINGERPRINT_PREFIX}.${stamp()}.json`);
|
|
43
|
+
}
|
|
44
|
+
export function readFingerprint(outDir) {
|
|
45
|
+
const f = readJson(fingerprintPath(outDir));
|
|
46
|
+
if (!f || f.version !== FINGERPRINT_VERSION || typeof f.files !== "object" || !f.files)
|
|
47
|
+
return null;
|
|
48
|
+
if (f.extractor !== stamp())
|
|
49
|
+
return null; // different extractor — re-extract, don't trust these prints
|
|
50
|
+
return f;
|
|
51
|
+
}
|
|
52
|
+
/** Project the extraction cache's entries into the probe sidecar. Best-effort:
|
|
53
|
+
* the graph is already on disk when this runs, so a failed write costs the next
|
|
54
|
+
* probe its fast path and nothing more. */
|
|
55
|
+
export function writeFingerprint(outDir, entries) {
|
|
56
|
+
const files = {};
|
|
57
|
+
for (const [rel, e] of Object.entries(entries))
|
|
58
|
+
files[rel] = [e.size, e.mtimeMs, e.hash];
|
|
59
|
+
try {
|
|
60
|
+
writeJsonAtomic(fingerprintPath(outDir), { version: FINGERPRINT_VERSION, extractor: stamp(), files }, true);
|
|
61
|
+
pruneSidecars(join(outDir, CACHE_DIR), FINGERPRINT_PREFIX);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** `GRAFT_REFRESH=hash` — never trust a stat, confirm every file by its bytes. */
|
|
69
|
+
export function alwaysHash() {
|
|
70
|
+
return process.env.GRAFT_REFRESH === "hash";
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* May a recorded `(size, mtimeMs, hash)` be trusted for the file `f` as it is on
|
|
74
|
+
* disk now, without reading it?
|
|
75
|
+
*
|
|
76
|
+
* **The probe's rule only.** `buildGraph` deliberately does not use this: it reads
|
|
77
|
+
* and hashes every file, every time. A stat may decide whether a query bothers
|
|
78
|
+
* rebuilding; it may not decide what the rebuild itself looks at — otherwise
|
|
79
|
+
* `graft check` (which always re-hashes) can report drift that the `graft build` it
|
|
80
|
+
* recommends then refuses to repair. `GRAFT_REFRESH=hash` is the escape hatch for
|
|
81
|
+
* the probe's blind spot: a same-length edit inside one mtime tick.
|
|
82
|
+
*
|
|
83
|
+
* An empty `hash` means the last build never got the bytes — always re-read.
|
|
84
|
+
* A *parse* failure keeps its real hash, so it stays on the fast path: re-reading
|
|
85
|
+
* bytes that failed to parse yesterday just fails to parse again.
|
|
86
|
+
*/
|
|
87
|
+
export function statUnchanged(rec, f) {
|
|
88
|
+
if (alwaysHash())
|
|
89
|
+
return false;
|
|
90
|
+
if (!rec.hash)
|
|
91
|
+
return false;
|
|
92
|
+
return rec.size === f.size && rec.mtimeMs === f.mtimeMs;
|
|
93
|
+
}
|
|
94
|
+
export function isClean(d) {
|
|
95
|
+
return d.changed.length === 0 && d.added.length === 0 && d.removed.length === 0;
|
|
96
|
+
}
|
|
97
|
+
export function driftCount(d) {
|
|
98
|
+
return d.changed.length + d.added.length + d.removed.length;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Diff the working tree against the last build's fingerprint. Returns null when
|
|
102
|
+
* there is no fingerprint to compare against (never built, or built by a version
|
|
103
|
+
* that didn't write one) — callers should treat that as "unknown", not "clean".
|
|
104
|
+
*
|
|
105
|
+
* `GRAFT_REFRESH=hash` skips the stat fast path and hashes every file, for the
|
|
106
|
+
* rare tooling that rewrites content while preserving size and mtime.
|
|
107
|
+
*/
|
|
108
|
+
export function probeDrift(root, outDir) {
|
|
109
|
+
const fp = readFingerprint(outDir);
|
|
110
|
+
if (!fp)
|
|
111
|
+
return null;
|
|
112
|
+
const drift = { changed: [], added: [], removed: [] };
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
for (const f of listSourceStats(root, outDir)) {
|
|
115
|
+
seen.add(f.rel);
|
|
116
|
+
const print = fp.files[f.rel];
|
|
117
|
+
if (!print) {
|
|
118
|
+
drift.added.push(f.rel);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const [size, mtimeMs, hash] = print;
|
|
122
|
+
if (statUnchanged({ size, mtimeMs, hash }, f))
|
|
123
|
+
continue;
|
|
124
|
+
// Suspect: confirm by bytes, so a touch (or a checkout that restores the
|
|
125
|
+
// same content) doesn't cost a rebuild. An entry with an empty hash lands
|
|
126
|
+
// here every time by design — that's a file the last build couldn't read, and
|
|
127
|
+
// the only way to learn it's readable again is to try.
|
|
128
|
+
let now;
|
|
129
|
+
try {
|
|
130
|
+
now = contentHash(readFileSync(f.abs, "utf8"));
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
continue; // unreadable right now — leave it to the next probe
|
|
134
|
+
}
|
|
135
|
+
if (now !== hash)
|
|
136
|
+
drift.changed.push(f.rel);
|
|
137
|
+
}
|
|
138
|
+
for (const rel of Object.keys(fp.files)) {
|
|
139
|
+
if (!seen.has(rel))
|
|
140
|
+
drift.removed.push(rel);
|
|
141
|
+
}
|
|
142
|
+
drift.changed.sort();
|
|
143
|
+
drift.added.sort();
|
|
144
|
+
drift.removed.sort();
|
|
145
|
+
return drift;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=fingerprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../../src/graph/fingerprint.ts"],"names":[],"mappings":"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"}
|
package/dist/graph/load.d.ts
CHANGED
|
@@ -15,4 +15,15 @@ export declare function loadGraphCached(outDir: string): GraphV1 | null;
|
|
|
15
15
|
/** Cached `readAskIndex(outDir)` — same semantics, keyed on the sidecar file.
|
|
16
16
|
* Returns a shared cached reference; callers must not mutate the returned index. */
|
|
17
17
|
export declare function loadAskIndexCached(outDir: string): AskIndex | null;
|
|
18
|
+
/**
|
|
19
|
+
* Drop the cached graph + sidecar for `outDir`, forcing the next load to re-read.
|
|
20
|
+
*
|
|
21
|
+
* The `(mtimeMs, size)` key above is a good invalidator for a build that happened
|
|
22
|
+
* in *another* process, but not for one this process just performed: the
|
|
23
|
+
* pre-query auto-refresh (`refresh.ts`) rewrites the graph and then immediately
|
|
24
|
+
* queries it, and on a filesystem with coarse mtime resolution a same-tick rewrite
|
|
25
|
+
* of the same size would be served from the stale entry. Callers that rebuild must
|
|
26
|
+
* therefore invalidate explicitly rather than trust the clock.
|
|
27
|
+
*/
|
|
28
|
+
export declare function invalidateGraphCaches(outDir: string): void;
|
|
18
29
|
//# sourceMappingURL=load.d.ts.map
|
package/dist/graph/load.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/graph/load.ts"],"names":[],"mappings":"AA2BA,OAAO,EAA8B,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACjF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAW1C;6EAC6E;AAC7E,eAAO,MAAM,YAAY;;;CAA4B,CAAC;AAEtD,+EAA+E;AAC/E,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC;AAmCD;;oFAEoF;AACpF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAK9D;AAED;oFACoF;AACpF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAKlE"}
|
|
1
|
+
{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/graph/load.ts"],"names":[],"mappings":"AA2BA,OAAO,EAA8B,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACjF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAW1C;6EAC6E;AAC7E,eAAO,MAAM,YAAY;;;CAA4B,CAAC;AAEtD,+EAA+E;AAC/E,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC;AAmCD;;oFAEoF;AACpF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAK9D;AAED;oFACoF;AACpF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAKlE;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAG1D"}
|
package/dist/graph/load.js
CHANGED
|
@@ -80,4 +80,18 @@ export function loadAskIndexCached(outDir) {
|
|
|
80
80
|
__parseCount.askIndex++;
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Drop the cached graph + sidecar for `outDir`, forcing the next load to re-read.
|
|
85
|
+
*
|
|
86
|
+
* The `(mtimeMs, size)` key above is a good invalidator for a build that happened
|
|
87
|
+
* in *another* process, but not for one this process just performed: the
|
|
88
|
+
* pre-query auto-refresh (`refresh.ts`) rewrites the graph and then immediately
|
|
89
|
+
* queries it, and on a filesystem with coarse mtime resolution a same-tick rewrite
|
|
90
|
+
* of the same size would be served from the stale entry. Callers that rebuild must
|
|
91
|
+
* therefore invalidate explicitly rather than trust the clock.
|
|
92
|
+
*/
|
|
93
|
+
export function invalidateGraphCaches(outDir) {
|
|
94
|
+
graphCache.delete(wiringPath(outDir));
|
|
95
|
+
askIndexCache.delete(askIndexPath(outDir));
|
|
96
|
+
}
|
|
83
97
|
//# sourceMappingURL=load.js.map
|
package/dist/graph/load.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/graph/load.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAiB,MAAM,sBAAsB,CAAC;AASjF,MAAM,UAAU,GAAG,IAAI,GAAG,EAA+B,CAAC;AAC1D,MAAM,aAAa,GAAG,IAAI,GAAG,EAAgC,CAAC;AAE9D;6EAC6E;AAC7E,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;AAEtD,+EAA+E;AAC/E,MAAM,UAAU,kBAAkB;IAChC,YAAY,CAAC,KAAK,GAAG,CAAC,CAAC;IACvB,YAAY,CAAC,QAAQ,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED,SAAS,MAAM,CAAC,IAAY;IAC1B,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QACzB,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CACjB,KAAiC,EACjC,IAAY,EACZ,KAAqB,EACrB,OAAmB;IAEnB,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IACxB,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,sEAAsE;QACtE,sEAAsE;QACtE,+CAA+C;QAC/C,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,MAAM,IAAI,MAAM,CAAC,OAAO,KAAK,EAAE,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC;QACvE,OAAO,MAAM,CAAC,KAAK,CAAC;IACtB,CAAC;IACD,OAAO,EAAE,CAAC;IACV,MAAM,KAAK,GAAG,KAAK,EAAE,CAAC;IACtB,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/D,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;oFAEoF;AACpF,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IAChC,OAAO,UAAU,CAAC,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE;QAC9D,YAAY,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;AACL,CAAC;AAED;oFACoF;AACpF,MAAM,UAAU,kBAAkB,CAAC,MAAc;IAC/C,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAClC,OAAO,UAAU,CAAC,aAAa,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE;QACtE,YAAY,CAAC,QAAQ,EAAE,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
1
|
+
{"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/graph/load.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Drift } from "./fingerprint.js";
|
|
2
|
+
export interface RefreshResult {
|
|
3
|
+
/** True when the graph on disk was rebuilt by this call. */
|
|
4
|
+
refreshed: boolean;
|
|
5
|
+
/** What the probe found, when it ran and found something. */
|
|
6
|
+
drift?: Drift;
|
|
7
|
+
/** One-line explanation for the agent/user, when there's something worth saying. */
|
|
8
|
+
note?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface RefreshOptions {
|
|
11
|
+
contextDir?: string;
|
|
12
|
+
/** Skip everything (the `--no-refresh` flag). */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Release the lock if this process is asked to die while holding it. Returns the
|
|
17
|
+
* un-hook.
|
|
18
|
+
*
|
|
19
|
+
* Not hypothetical: the Claude Code prompt hook runs `graft ask` with a timeout, and
|
|
20
|
+
* `execFileSync` enforces it with SIGTERM. Node's default disposition for SIGTERM is
|
|
21
|
+
* to exit without unwinding, so the `finally` below never runs and the lock outlives
|
|
22
|
+
* the process — after which the background sync is blocked and every query waits and
|
|
23
|
+
* then answers stale until the lock ages out. Ctrl-C on a CLI query is the same story
|
|
24
|
+
* with SIGINT.
|
|
25
|
+
*
|
|
26
|
+
* Adding a listener replaces that default disposition, so re-raise it explicitly
|
|
27
|
+
* afterwards: remove ourselves, then `process.kill(process.pid, sig)`, so the exit
|
|
28
|
+
* status still says "terminated by signal" for whoever is waiting on us.
|
|
29
|
+
*/
|
|
30
|
+
export declare function releaseOnSignal(cache: string): () => void;
|
|
31
|
+
/**
|
|
32
|
+
* Rebuild `root`'s structural graph if the working tree has moved since the last
|
|
33
|
+
* build. Cheap and side-effect-free when nothing changed, which is the usual case.
|
|
34
|
+
*
|
|
35
|
+
* `root` is the repository root (the same value the query itself is given), not
|
|
36
|
+
* the context dir.
|
|
37
|
+
*/
|
|
38
|
+
export declare function ensureFreshGraph(root: string, opts?: RefreshOptions): Promise<RefreshResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Workspace variant: refresh each child repo's own graph (a workspace parent holds
|
|
41
|
+
* an index, never nodes). Children are independent, but they're refreshed in
|
|
42
|
+
* sequence — a fan-out of concurrent tree-sitter builds would spike CPU on the
|
|
43
|
+
* very path that's supposed to feel free.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ensureFreshChildren(root: string, children: string[], opts?: RefreshOptions): Promise<RefreshResult>;
|
|
46
|
+
/** The one-line note a CLI/MCP surface prints when a refresh actually happened.
|
|
47
|
+
* Null when there's nothing to say (the overwhelmingly common case). */
|
|
48
|
+
export declare function refreshNote(r: RefreshResult): string | null;
|
|
49
|
+
//# sourceMappingURL=refresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../../src/graph/refresh.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAmC,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAU/E,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,SAAS,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAcD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,IAAI,CAWzD;AAYD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,aAAa,CAAC,CAwDtG;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAAE,EAClB,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,aAAa,CAAC,CAsBxB;AAED;wEACwE;AACxE,wBAAgB,WAAW,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAK3D"}
|