knodin 0.5.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/LICENSE +21 -0
- package/README.md +590 -0
- package/dist/bin/cli.js +1704 -0
- package/dist/src/agent-integration.js +250 -0
- package/dist/src/artifact-refresh.js +81 -0
- package/dist/src/cli-args.js +267 -0
- package/dist/src/cli-model.js +324 -0
- package/dist/src/compact-structural.js +96 -0
- package/dist/src/competitive-constraints.js +20 -0
- package/dist/src/competitive-manifest.js +330 -0
- package/dist/src/competitive-measurement.js +183 -0
- package/dist/src/competitive-runner.js +453 -0
- package/dist/src/competitive-sandbox.js +108 -0
- package/dist/src/context-export.js +422 -0
- package/dist/src/context.js +102 -0
- package/dist/src/docs-sections.js +141 -0
- package/dist/src/doctor.js +380 -0
- package/dist/src/engine/ann-hnsw.js +271 -0
- package/dist/src/engine/embeddings.js +193 -0
- package/dist/src/engine/file-walker.js +43 -0
- package/dist/src/engine/index.js +13030 -0
- package/dist/src/engine/perf.js +115 -0
- package/dist/src/engine/prune.js +112 -0
- package/dist/src/engine/source-policy.js +69 -0
- package/dist/src/engine/sqlite.js +71 -0
- package/dist/src/engine/symbol-delete.js +58 -0
- package/dist/src/failure-diagnosis.js +590 -0
- package/dist/src/fleet.js +7 -0
- package/dist/src/git-executable.js +31 -0
- package/dist/src/graph-query-health.js +115 -0
- package/dist/src/index-activity.js +125 -0
- package/dist/src/init-progress-worker.js +107 -0
- package/dist/src/init-progress.js +155 -0
- package/dist/src/init.js +985 -0
- package/dist/src/lifecycle-health.js +213 -0
- package/dist/src/lsp-readonly.js +217 -0
- package/dist/src/output-compression.js +629 -0
- package/dist/src/output-telemetry.js +359 -0
- package/dist/src/pr-triage.js +638 -0
- package/dist/src/relationship-adapters.js +370 -0
- package/dist/src/release-attestation.js +533 -0
- package/dist/src/repair-progress-worker.js +121 -0
- package/dist/src/repair-progress.js +262 -0
- package/dist/src/repository-init-process.js +173 -0
- package/dist/src/repository-management.js +1089 -0
- package/dist/src/response-budget.js +184 -0
- package/dist/src/server.js +53 -0
- package/dist/src/system-config.js +615 -0
- package/dist/src/terminal-help.js +83 -0
- package/dist/src/tools/knodin-tools.js +1438 -0
- package/dist/src/tools/reckon-tools.js +5 -0
- package/dist/src/update-policy.js +944 -0
- package/dist/src/update-trust.js +503 -0
- package/dist/src/version.js +13 -0
- package/dist/src/visualization.js +162 -0
- package/dist/src/wait-for-fresh.js +98 -0
- package/dist/src/worktree-lifecycle.js +231 -0
- package/docs/CLI.md +39 -0
- package/docs/COMMAND-OUTPUT-COMPRESSION.md +194 -0
- package/docs/DEAD-CODE-AND-IMPACT.md +27 -0
- package/docs/DOCTOR-AND-UPDATES.md +84 -0
- package/docs/INDEXING-POLICY-AND-PROVENANCE.md +37 -0
- package/docs/INSTALLATION.md +208 -0
- package/docs/MCP.md +100 -0
- package/docs/PT-ACCESS-RECOMMENDATION.md +91 -0
- package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
- package/docs/REPOSITORIES-AND-WORKTREES.md +81 -0
- package/docs/SIGNED-UPDATES.md +146 -0
- package/docs/SYSTEMS-AND-RELATIONSHIPS.md +45 -0
- package/docs/TELEMETRY.md +42 -0
- package/docs/releases/0.3.0.md +46 -0
- package/docs/releases/0.4.0.md +68 -0
- package/docs/releases/0.4.1.md +28 -0
- package/docs/releases/0.4.2.md +27 -0
- package/docs/releases/0.4.3.md +23 -0
- package/docs/releases/0.5.0.md +29 -0
- package/package.json +110 -0
- package/schemas/release-attestation-v1.schema.json +210 -0
- package/tree-sitter-prisma.wasm +0 -0
- package/tree-sitter-sql.wasm +0 -0
- package/tree-sitter-xml.wasm +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** Opt-in phase timing used by the R43 benchmark harness. */
|
|
2
|
+
export const PERF_PHASES = [
|
|
3
|
+
"db_open_migration",
|
|
4
|
+
"file_collection",
|
|
5
|
+
"parser_wasm_init",
|
|
6
|
+
"read",
|
|
7
|
+
"parse_extract",
|
|
8
|
+
"sqlite_write",
|
|
9
|
+
"model_load",
|
|
10
|
+
"embedding_inference_write",
|
|
11
|
+
"search_row_fetch_decode",
|
|
12
|
+
"similarity",
|
|
13
|
+
"fts",
|
|
14
|
+
"map_community",
|
|
15
|
+
"fusion",
|
|
16
|
+
"freshness_probe",
|
|
17
|
+
"graph_analytics",
|
|
18
|
+
"traversal_snapshot",
|
|
19
|
+
"architecture_facets",
|
|
20
|
+
"architecture_edge_materialization",
|
|
21
|
+
"context_composition",
|
|
22
|
+
"status_audit",
|
|
23
|
+
"pack_walk",
|
|
24
|
+
"pack_serialize",
|
|
25
|
+
"artifact_read",
|
|
26
|
+
"artifact_grep",
|
|
27
|
+
"review_diff_discovery",
|
|
28
|
+
"review_churn",
|
|
29
|
+
"review_parsing",
|
|
30
|
+
"review_flow_lookup",
|
|
31
|
+
"review_serialization",
|
|
32
|
+
];
|
|
33
|
+
let active = null;
|
|
34
|
+
function emptyPhases() {
|
|
35
|
+
return Object.fromEntries(PERF_PHASES.map((phase) => [phase, 0]));
|
|
36
|
+
}
|
|
37
|
+
function peakRss() {
|
|
38
|
+
try {
|
|
39
|
+
const kilobytes = process.resourceUsage().maxRSS;
|
|
40
|
+
return Number.isFinite(kilobytes) && kilobytes >= 0 ? kilobytes * 1024 : undefined;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Start a measurement explicitly. Starting while active resets that measurement. */
|
|
47
|
+
export function beginPerfSession() {
|
|
48
|
+
active = { startedAt: performance.now(), phases: emptyPhases(), peakRssBytes: peakRss() };
|
|
49
|
+
}
|
|
50
|
+
/** Finish the active measurement, or return null when instrumentation is disabled. */
|
|
51
|
+
export function endPerfSession() {
|
|
52
|
+
const session = active;
|
|
53
|
+
if (!session)
|
|
54
|
+
return null;
|
|
55
|
+
active = null;
|
|
56
|
+
const currentRss = peakRss();
|
|
57
|
+
if (currentRss !== undefined) {
|
|
58
|
+
session.peakRssBytes = Math.max(session.peakRssBytes ?? 0, currentRss);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
phases: { ...session.phases },
|
|
62
|
+
totalMs: Math.max(0, performance.now() - session.startedAt),
|
|
63
|
+
...(session.peakRssBytes === undefined ? {} : { peakRssBytes: session.peakRssBytes }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function record(phase, startedAt) {
|
|
67
|
+
const session = active;
|
|
68
|
+
if (!session)
|
|
69
|
+
return;
|
|
70
|
+
session.phases[phase] += Math.max(0, performance.now() - startedAt);
|
|
71
|
+
const currentRss = peakRss();
|
|
72
|
+
if (currentRss !== undefined)
|
|
73
|
+
session.peakRssBytes = Math.max(session.peakRssBytes ?? 0, currentRss);
|
|
74
|
+
}
|
|
75
|
+
/** Start a phase that spans an existing try/finally block. The returned closer is idempotent. */
|
|
76
|
+
export function beginPerfPhase(phase) {
|
|
77
|
+
if (!active)
|
|
78
|
+
return () => undefined;
|
|
79
|
+
const startedAt = performance.now();
|
|
80
|
+
let closed = false;
|
|
81
|
+
return () => {
|
|
82
|
+
if (closed)
|
|
83
|
+
return;
|
|
84
|
+
closed = true;
|
|
85
|
+
record(phase, startedAt);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Time an async phase. Nested phases are retained independently and errors still close timers. */
|
|
89
|
+
export async function measurePerfPhase(phase, work) {
|
|
90
|
+
if (!active)
|
|
91
|
+
return work();
|
|
92
|
+
const startedAt = performance.now();
|
|
93
|
+
try {
|
|
94
|
+
return await work();
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
record(phase, startedAt);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Synchronous counterpart for filesystem, parsing, and SQLite calls. */
|
|
101
|
+
export function measurePerfPhaseSync(phase, work) {
|
|
102
|
+
if (!active)
|
|
103
|
+
return work();
|
|
104
|
+
const startedAt = performance.now();
|
|
105
|
+
try {
|
|
106
|
+
return work();
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
record(phase, startedAt);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Test-only state reset; deliberately does not enable timing. */
|
|
113
|
+
export function resetPerfForTests() {
|
|
114
|
+
active = null;
|
|
115
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Single source of truth for "which directories does knodin ignore" (R14).
|
|
4
|
+
*
|
|
5
|
+
* Before this module, the same intent — skip node_modules, .git, .reckon,
|
|
6
|
+
* build output, caches, VCS internals — was encoded THREE separate times:
|
|
7
|
+
* 1. src/__tests__/setup.ts's `PRUNE_DIRS` regex (glob-shim exclude filter)
|
|
8
|
+
* 2. the indexer's own directory filter (`EXCLUDED_DIR_SEGMENTS` / `isIndexablePath`)
|
|
9
|
+
* 3. the chokidar watcher's `makeWatchIgnorePredicate` (added by R13)
|
|
10
|
+
*
|
|
11
|
+
* They drifted. R13's bug was exactly this shape: the watcher's copy silently
|
|
12
|
+
* stopped matching anything for an entire chokidar major version while the
|
|
13
|
+
* indexer's copy kept working, so indexing looked correct and only the
|
|
14
|
+
* watcher melted (it registered ~12.6k files and starved the event loop under
|
|
15
|
+
* Bun). This module is the ONE list; every consumer below is a thin adapter
|
|
16
|
+
* over it, so a future drift is structurally impossible rather than merely
|
|
17
|
+
* discouraged.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Directory segments never worth indexing or watching: build output, deps,
|
|
21
|
+
* VCS/tool state, caches. Matched by whole path SEGMENT, never substring, so
|
|
22
|
+
* a real file/dir like `distributor.ts` or `build-graph.ts` is never wrongly
|
|
23
|
+
* pruned.
|
|
24
|
+
*/
|
|
25
|
+
export const PRUNE_DIR_NAMES = new Set([
|
|
26
|
+
"node_modules",
|
|
27
|
+
".git",
|
|
28
|
+
".reckon",
|
|
29
|
+
// Local state maintained by other code-intelligence tools. It is neither
|
|
30
|
+
// repository source nor stable project configuration, and indexing it makes
|
|
31
|
+
// a healthy checkout look perpetually stale as those tools update databases
|
|
32
|
+
// and caches in the background.
|
|
33
|
+
".gitnexus",
|
|
34
|
+
".codegraph",
|
|
35
|
+
".code-review-graph",
|
|
36
|
+
".aider.tags.cache.v4",
|
|
37
|
+
"dist",
|
|
38
|
+
"build",
|
|
39
|
+
"coverage",
|
|
40
|
+
".next",
|
|
41
|
+
".turbo",
|
|
42
|
+
".svelte-kit",
|
|
43
|
+
"vendor",
|
|
44
|
+
"__pycache__",
|
|
45
|
+
]);
|
|
46
|
+
/** True when a single path segment (one directory name) is a pruned directory. */
|
|
47
|
+
export function isPrunedSegment(segment) {
|
|
48
|
+
return PRUNE_DIR_NAMES.has(segment);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Agent worktrees are checkouts of this same repo living under
|
|
52
|
+
* `.claude/worktrees/*`; indexing/watching them would double-count every
|
|
53
|
+
* symbol. `segments` is the full split relative path; `i` is the segment
|
|
54
|
+
* index currently being tested against the two-segment `.claude/worktrees`
|
|
55
|
+
* pattern.
|
|
56
|
+
*/
|
|
57
|
+
function isClaudeWorktreeSegment(segments, i) {
|
|
58
|
+
return segments[i] === ".claude" && segments[i + 1] === "worktrees";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* True when ANY segment of an already-split repo-relative path trips the
|
|
62
|
+
* ignore policy (a pruned directory name, or the `.claude/worktrees` root).
|
|
63
|
+
* This is the one walk both `isIndexablePath` (indexer) and
|
|
64
|
+
* `makeWatchIgnorePredicate` (watcher) run — the code that used to be
|
|
65
|
+
* duplicated between them.
|
|
66
|
+
*/
|
|
67
|
+
export function pathSegmentsAreIgnored(segments) {
|
|
68
|
+
for (let i = 0; i < segments.length; i++) {
|
|
69
|
+
if (isPrunedSegment(segments[i]))
|
|
70
|
+
return true;
|
|
71
|
+
if (isClaudeWorktreeSegment(segments, i))
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Indexer adapter: true when a repo-relative path (either OS separator)
|
|
78
|
+
* should be indexed.
|
|
79
|
+
*/
|
|
80
|
+
export function isIndexablePath(relPath) {
|
|
81
|
+
return !pathSegmentsAreIgnored(relPath.split(/[/\\]/));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Watcher adapter: builds chokidar's `ignored` predicate for a given repo
|
|
85
|
+
* root.
|
|
86
|
+
*
|
|
87
|
+
* chokidar v4 removed glob support; v5 accepts only a function, a regex, or a
|
|
88
|
+
* literal path. Matching here is done on the path RELATIVE to the repo, so a
|
|
89
|
+
* repo that happens to live under a directory named e.g. `build/` does not
|
|
90
|
+
* prune its own root (the R13 fix this preserves).
|
|
91
|
+
*/
|
|
92
|
+
export function makeWatchIgnorePredicate(repoPath) {
|
|
93
|
+
return (targetPath) => {
|
|
94
|
+
const relative = path.relative(repoPath, targetPath);
|
|
95
|
+
// The watch root itself ("") and anything outside it are never pruned.
|
|
96
|
+
if (relative === "" || relative.startsWith(".."))
|
|
97
|
+
return false;
|
|
98
|
+
return pathSegmentsAreIgnored(relative.split(path.sep));
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Policy-test adapter: a regex equivalent to the segment-based policy above.
|
|
103
|
+
* Anchored on `/` boundaries so it only matches whole segments — same guarantee
|
|
104
|
+
* as `isPrunedSegment` — never a substring inside an unrelated name.
|
|
105
|
+
*/
|
|
106
|
+
export function makePruneDirsRegex() {
|
|
107
|
+
const escaped = [...PRUNE_DIR_NAMES].map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
108
|
+
// `.claude/worktrees` is a two-segment pattern, expressed directly rather
|
|
109
|
+
// than through the single-segment name list.
|
|
110
|
+
escaped.push("\\.claude/worktrees");
|
|
111
|
+
return new RegExp(`(?:^|/)(?:${escaped.join("|")})(?:/|$)`);
|
|
112
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { isIndexablePath } from "./prune.js";
|
|
3
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
4
|
+
".ts",
|
|
5
|
+
".tsx",
|
|
6
|
+
".mts",
|
|
7
|
+
".cts",
|
|
8
|
+
".js",
|
|
9
|
+
".jsx",
|
|
10
|
+
".mjs",
|
|
11
|
+
".cjs",
|
|
12
|
+
".py",
|
|
13
|
+
".cls",
|
|
14
|
+
".trigger",
|
|
15
|
+
".page",
|
|
16
|
+
".component",
|
|
17
|
+
".app",
|
|
18
|
+
".cmp",
|
|
19
|
+
".design",
|
|
20
|
+
".docs",
|
|
21
|
+
".evt",
|
|
22
|
+
".intf",
|
|
23
|
+
".svg",
|
|
24
|
+
".sql",
|
|
25
|
+
".pkb",
|
|
26
|
+
".pks",
|
|
27
|
+
".prisma",
|
|
28
|
+
".java",
|
|
29
|
+
".cs",
|
|
30
|
+
".go",
|
|
31
|
+
".rs",
|
|
32
|
+
".clp",
|
|
33
|
+
".ws",
|
|
34
|
+
".xml",
|
|
35
|
+
".tf",
|
|
36
|
+
".tfvars",
|
|
37
|
+
".hcl",
|
|
38
|
+
".dockerfile",
|
|
39
|
+
".lsif",
|
|
40
|
+
]);
|
|
41
|
+
function isSalesforceBundleMarkup(normalized) {
|
|
42
|
+
return (/^force-app\/main\/default\/lwc\/([A-Za-z][A-Za-z0-9_]*)\/\1\.(?:html|css)$/.test(normalized) ||
|
|
43
|
+
/^force-app\/main\/default\/aura\/([A-Za-z][A-Za-z0-9_]*)\/\1\.(?:css)$/.test(normalized));
|
|
44
|
+
}
|
|
45
|
+
function isSalesforceExperienceJson(normalized) {
|
|
46
|
+
return (normalized === "lwr.config.json" ||
|
|
47
|
+
/^force-app\/main\/default\/experiences\/[A-Za-z][A-Za-z0-9_]*\/(?:routes|views|themes)\/[^/]+\.json$/.test(normalized));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Canonical, path-only candidate policy shared by full-repository collection,
|
|
51
|
+
* file watching, repair selection, and Git-hook refresh. Content-sensitive
|
|
52
|
+
* formats (currently dbt manifest.json) are accepted as candidates here and
|
|
53
|
+
* validated by the parser before symbols are persisted.
|
|
54
|
+
*/
|
|
55
|
+
export function isIndexableSourcePath(relativePath) {
|
|
56
|
+
if (!isIndexablePath(relativePath))
|
|
57
|
+
return false;
|
|
58
|
+
const normalized = relativePath.replaceAll("\\", "/");
|
|
59
|
+
const base = path.posix.basename(normalized);
|
|
60
|
+
if (base === "Dockerfile")
|
|
61
|
+
return true;
|
|
62
|
+
if (normalized.endsWith(".json")) {
|
|
63
|
+
return base === "manifest.json" || isSalesforceExperienceJson(normalized);
|
|
64
|
+
}
|
|
65
|
+
if (normalized.endsWith(".html") || normalized.endsWith(".css")) {
|
|
66
|
+
return isSalesforceBundleMarkup(normalized);
|
|
67
|
+
}
|
|
68
|
+
return SOURCE_EXTENSIONS.has(path.posix.extname(normalized));
|
|
69
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production SQLite adapter for the Node runtime.
|
|
3
|
+
*
|
|
4
|
+
* knodin's engine was originally written against the compact `bun:sqlite`
|
|
5
|
+
* surface. Node 24 ships `node:sqlite` without a runtime flag, so this adapter
|
|
6
|
+
* preserves the engine's existing typed `query<Row, Params>()` contract while
|
|
7
|
+
* using Node's native `DatabaseSync` implementation underneath.
|
|
8
|
+
*/
|
|
9
|
+
import { DatabaseSync } from "node:sqlite";
|
|
10
|
+
/**
|
|
11
|
+
* knodin's call sites pass bind params either spread (`stmt.all(a, b)`) or as a
|
|
12
|
+
* single array (`db.run(sql, [a, b])`). node:sqlite wants them spread — normalize a
|
|
13
|
+
* lone array argument back into a spread list. SQLite has no array column type,
|
|
14
|
+
* so a single array can only ever mean "the parameter list", never one value.
|
|
15
|
+
*/
|
|
16
|
+
function normalize(params) {
|
|
17
|
+
if (params.length === 1 && Array.isArray(params[0]))
|
|
18
|
+
return params[0];
|
|
19
|
+
return params;
|
|
20
|
+
}
|
|
21
|
+
class Statement {
|
|
22
|
+
// node:sqlite's parameter union is intentionally narrower than the engine's
|
|
23
|
+
// generic call-site contract. Runtime validation still belongs to SQLite.
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: adapter boundary around native bindings.
|
|
25
|
+
stmt;
|
|
26
|
+
// biome-ignore lint/suspicious/noExplicitAny: adapter boundary around native bindings.
|
|
27
|
+
constructor(stmt) {
|
|
28
|
+
this.stmt = stmt;
|
|
29
|
+
}
|
|
30
|
+
all(...params) {
|
|
31
|
+
return this.stmt.all(...normalize(params));
|
|
32
|
+
}
|
|
33
|
+
get(...params) {
|
|
34
|
+
return (this.stmt.get(...normalize(params)) ?? null);
|
|
35
|
+
}
|
|
36
|
+
run(...params) {
|
|
37
|
+
return this.stmt.run(...normalize(params));
|
|
38
|
+
}
|
|
39
|
+
finalize() {
|
|
40
|
+
// node:sqlite statements are released on GC; there is nothing to finalize.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export class Database {
|
|
44
|
+
db;
|
|
45
|
+
constructor(databasePath, options = {}) {
|
|
46
|
+
this.db = new DatabaseSync(databasePath, { readOnly: options.readonly === true });
|
|
47
|
+
}
|
|
48
|
+
query(sql) {
|
|
49
|
+
return new Statement(this.db.prepare(sql));
|
|
50
|
+
}
|
|
51
|
+
prepare(sql) {
|
|
52
|
+
return new Statement(this.db.prepare(sql));
|
|
53
|
+
}
|
|
54
|
+
run(sql, ...params) {
|
|
55
|
+
if (params.length === 0) {
|
|
56
|
+
// DDL / PRAGMA / BEGIN / COMMIT / multi-statement schema — `exec` avoids
|
|
57
|
+
// prepare() edge cases and handles more-than-one statement in one string.
|
|
58
|
+
this.db.exec(sql);
|
|
59
|
+
return { changes: 0, lastInsertRowid: 0 };
|
|
60
|
+
}
|
|
61
|
+
// biome-ignore lint/suspicious/noExplicitAny: adapter boundary around native bindings.
|
|
62
|
+
const statement = this.db.prepare(sql);
|
|
63
|
+
return statement.run(...normalize(params));
|
|
64
|
+
}
|
|
65
|
+
exec(sql) {
|
|
66
|
+
this.db.exec(sql);
|
|
67
|
+
}
|
|
68
|
+
close() {
|
|
69
|
+
this.db.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for "delete symbols, and everything keyed off them" (R17).
|
|
3
|
+
*
|
|
4
|
+
* `symbol_embeddings.symbolId` declares
|
|
5
|
+
* `REFERENCES symbols(id) ON DELETE CASCADE`, but SQLite disables foreign-key
|
|
6
|
+
* enforcement per connection by default, so for the life of the project that
|
|
7
|
+
* cascade never fired: `symbols.id` is AUTOINCREMENT, every re-index of a file
|
|
8
|
+
* deletes its symbols and reinserts them under NEW ids, and the embeddings
|
|
9
|
+
* pointing at the old ids were stranded forever. On `/path/to/large-repository` that had
|
|
10
|
+
* reached 37,258 orphans against 18,629 live symbols — 67% of the table, all
|
|
11
|
+
* of it scanned by semantic search's exact cosine pass.
|
|
12
|
+
*
|
|
13
|
+
* `getOrInitDb` now sets `PRAGMA foreign_keys = ON`, which makes the declared
|
|
14
|
+
* cascade real. These helpers are the second layer: they delete the embeddings
|
|
15
|
+
* EXPLICITLY, so correctness does not depend on a per-connection pragma that
|
|
16
|
+
* some future connection path forgets to set. Every `DELETE FROM symbols` site
|
|
17
|
+
* in the engine goes through here — the R14 lesson (one policy, many thin
|
|
18
|
+
* adapters) applied to deletion instead of ignore rules.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Shared predicate for "this embedding's symbol no longer exists" — the
|
|
22
|
+
* single source of truth for both the DELETE below and the `stats` orphan
|
|
23
|
+
* COUNT in `src/engine/index.ts`, which previously hardcoded the identical
|
|
24
|
+
* SQL as an independent string literal.
|
|
25
|
+
*/
|
|
26
|
+
export const ORPHANED_EMBEDDING_PREDICATE = "symbolId NOT IN (SELECT id FROM symbols)";
|
|
27
|
+
/**
|
|
28
|
+
* Purge embeddings whose symbol no longer exists.
|
|
29
|
+
*
|
|
30
|
+
* Idempotent and cheap — a single correlated DELETE. Used by the 13 → 14
|
|
31
|
+
* migration to clean indexes polluted before the cascade worked, and safe to
|
|
32
|
+
* call on an empty or freshly-created table.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately does NOT touch live embeddings: re-embedding a real corpus is
|
|
35
|
+
* ~40 minutes of CPU, so this must stay a purge and never become a wipe.
|
|
36
|
+
*/
|
|
37
|
+
export function purgeOrphanEmbeddings(db) {
|
|
38
|
+
db.run(`DELETE FROM symbol_embeddings WHERE ${ORPHANED_EMBEDDING_PREDICATE}`);
|
|
39
|
+
}
|
|
40
|
+
/** Delete every symbol declared in one repo-relative file, plus its embeddings. */
|
|
41
|
+
export function deleteSymbolsForFile(db, filePath) {
|
|
42
|
+
db.run("DELETE FROM symbol_embeddings WHERE symbolId IN (SELECT id FROM symbols WHERE filePath = ?)", [filePath]);
|
|
43
|
+
db.run("DELETE FROM symbols WHERE filePath = ?", [filePath]);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Delete every symbol whose file path matches a LIKE pattern, plus its
|
|
47
|
+
* embeddings. Used by the dbt path, whose symbols live under synthetic
|
|
48
|
+
* `.reckon/dbt/%` paths rather than one real file.
|
|
49
|
+
*/
|
|
50
|
+
export function deleteSymbolsMatchingPath(db, likePattern) {
|
|
51
|
+
db.run("DELETE FROM symbol_embeddings WHERE symbolId IN (SELECT id FROM symbols WHERE filePath LIKE ?)", [likePattern]);
|
|
52
|
+
db.run("DELETE FROM symbols WHERE filePath LIKE ?", [likePattern]);
|
|
53
|
+
}
|
|
54
|
+
/** Delete every symbol in the index, plus every embedding. Full re-index only. */
|
|
55
|
+
export function deleteAllSymbols(db) {
|
|
56
|
+
db.run("DELETE FROM symbol_embeddings;");
|
|
57
|
+
db.run("DELETE FROM symbols;");
|
|
58
|
+
}
|