knodin 0.5.0 → 0.6.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/README.md +166 -534
- package/dist/bin/cli.js +5 -5
- package/dist/src/agent-integration.js +1 -15
- package/dist/src/artifact-refresh.js +1 -1
- package/dist/src/competitive-sandbox.js +4 -4
- package/dist/src/context-export.js +1 -1
- package/dist/src/docs-sections.js +5 -5
- package/dist/src/engine/ann-hnsw.js +5 -5
- package/dist/src/engine/embeddings.js +4 -4
- package/dist/src/engine/index.js +33 -33
- package/dist/src/engine/prune.js +2 -2
- package/dist/src/engine/symbol-delete.js +1 -1
- package/dist/src/index-activity.js +1 -1
- package/dist/src/init-progress-worker.js +1 -1
- package/dist/src/init.js +42 -59
- package/dist/src/lifecycle-health.js +10 -11
- package/dist/src/lsp-readonly.js +1 -1
- package/dist/src/output-compression.js +1 -1
- package/dist/src/output-telemetry.js +4 -4
- package/dist/src/pr-triage.js +2 -2
- package/dist/src/repair-progress-worker.js +1 -1
- package/dist/src/repository-init-process.js +1 -1
- package/dist/src/repository-management.js +3 -3
- package/dist/src/server.js +1 -1
- package/dist/src/system-config.js +7 -7
- package/dist/src/tools/knodin-tools.js +2 -2
- package/dist/src/update-policy.js +4 -4
- package/dist/src/wait-for-fresh.js +2 -2
- package/dist/src/worktree-lifecycle.js +2 -2
- package/docs/COMMAND-OUTPUT-COMPRESSION.md +1 -1
- package/docs/DOCTOR-AND-UPDATES.md +2 -2
- package/docs/INSTALLATION.md +5 -5
- package/docs/REPOSITORIES-AND-WORKTREES.md +1 -1
- package/docs/SYSTEMS-AND-RELATIONSHIPS.md +2 -2
- package/docs/TELEMETRY.md +2 -2
- package/docs/assets/knodin-favicon.svg +4 -0
- package/docs/releases/0.5.1.md +17 -0
- package/docs/releases/0.6.0.md +18 -0
- package/package.json +8 -6
- package/schemas/release-attestation-v1.schema.json +1 -1
- package/dist/src/tools/reckon-tools.js +0 -5
package/dist/bin/cli.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* knodin explain <symbol> [minimal] edit-ready source + direct call paths + blast radius
|
|
6
6
|
* knodin review [base] risk-scored context with explicit git diff scopes
|
|
7
7
|
* knodin map subsystems + confidence-tagged edges
|
|
8
|
-
* knodin wiki [--force] write .
|
|
8
|
+
* knodin wiki [--force] write .knodin/wiki/ (index.md + per-community pages)
|
|
9
9
|
* knodin serve run the MCP server on stdio
|
|
10
10
|
*
|
|
11
11
|
* The same engine backs both the CLI and the MCP server, so any assistant and
|
|
@@ -313,7 +313,7 @@ function createProgressWorkerRenderer(workerName, startMessage) {
|
|
|
313
313
|
: [workerPath];
|
|
314
314
|
const worker = spawn(process.execPath, workerArgs, {
|
|
315
315
|
stdio: ["pipe", "ignore", "inherit"],
|
|
316
|
-
env: { ...process.env,
|
|
316
|
+
env: { ...process.env, KNODIN_FORCE_PROGRESS_TTY: "1" },
|
|
317
317
|
});
|
|
318
318
|
let failed = false;
|
|
319
319
|
const closed = new Promise((resolve) => {
|
|
@@ -788,7 +788,7 @@ async function main() {
|
|
|
788
788
|
output = await checkTrustedUpdate(options);
|
|
789
789
|
}
|
|
790
790
|
finally {
|
|
791
|
-
if (process.env.
|
|
791
|
+
if (process.env.KNODIN_UPDATE_BACKGROUND === "1")
|
|
792
792
|
releaseScheduledUpdateCheck();
|
|
793
793
|
}
|
|
794
794
|
}
|
|
@@ -812,7 +812,7 @@ async function main() {
|
|
|
812
812
|
const background = spawn(executable, [...arguments_, "update", "check", "--json"], {
|
|
813
813
|
detached: true,
|
|
814
814
|
stdio: "ignore",
|
|
815
|
-
env: { ...process.env,
|
|
815
|
+
env: { ...process.env, KNODIN_UPDATE_BACKGROUND: "1" },
|
|
816
816
|
});
|
|
817
817
|
background.unref();
|
|
818
818
|
}
|
|
@@ -1130,7 +1130,7 @@ async function main() {
|
|
|
1130
1130
|
const scope = explicitScope(rawRest);
|
|
1131
1131
|
if (!scope)
|
|
1132
1132
|
throw new Error("knodin configure: missing --scope");
|
|
1133
|
-
if (!fs.existsSync(path.join(repo, ".
|
|
1133
|
+
if (!fs.existsSync(path.join(repo, ".knodin", "db.sqlite"))) {
|
|
1134
1134
|
throw new Error("knodin configure changes agent integration only; this repository is not initialized. Run `knodin init` first.");
|
|
1135
1135
|
}
|
|
1136
1136
|
const agents = scope === "team" ? [] : integrationAgents(repo);
|
|
@@ -7,11 +7,8 @@ export function parseInitScope(value) {
|
|
|
7
7
|
throw new Error("knodin: --scope must be personal, team, or cli-only");
|
|
8
8
|
}
|
|
9
9
|
const MCP_NAME = "knodin";
|
|
10
|
-
const LEGACY_MCP_NAME = "reckon-graph";
|
|
11
10
|
const TOML_START = "# knodin:start";
|
|
12
11
|
const TOML_END = "# knodin:end";
|
|
13
|
-
const LEGACY_TOML_START = "# reckon-graph:start";
|
|
14
|
-
const LEGACY_TOML_END = "# reckon-graph:end";
|
|
15
12
|
const TOML_BLOCK = `${TOML_START}
|
|
16
13
|
[mcp_servers."${MCP_NAME}"]
|
|
17
14
|
command = "knodin"
|
|
@@ -70,16 +67,6 @@ export function configurePersonalAgents(options) {
|
|
|
70
67
|
for (const agent of options.detected) {
|
|
71
68
|
if (agent !== "claude")
|
|
72
69
|
continue;
|
|
73
|
-
const removeLegacy = {
|
|
74
|
-
executable: "claude",
|
|
75
|
-
args: ["mcp", "remove", LEGACY_MCP_NAME, "--scope", "local"],
|
|
76
|
-
};
|
|
77
|
-
try {
|
|
78
|
-
run(removeLegacy);
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
// A missing legacy registration is already the desired state.
|
|
82
|
-
}
|
|
83
70
|
const command = options.remove
|
|
84
71
|
? {
|
|
85
72
|
executable: "claude",
|
|
@@ -143,7 +130,6 @@ function updateJsonMcp(filePath, remove) {
|
|
|
143
130
|
const servers = typeof existing === "object" && existing !== null && !Array.isArray(existing)
|
|
144
131
|
? { ...existing }
|
|
145
132
|
: {};
|
|
146
|
-
delete servers[LEGACY_MCP_NAME];
|
|
147
133
|
if (remove)
|
|
148
134
|
delete servers[MCP_NAME];
|
|
149
135
|
else
|
|
@@ -176,7 +162,7 @@ function updateTomlMcp(filePath, remove) {
|
|
|
176
162
|
? `${content.slice(0, start)}${content.slice(end + endMarker.length)}`.trim()
|
|
177
163
|
: content.trim();
|
|
178
164
|
}
|
|
179
|
-
const without = removeManagedBlock(
|
|
165
|
+
const without = removeManagedBlock(existing, TOML_START, TOML_END);
|
|
180
166
|
const next = remove ? without : [without, TOML_BLOCK].filter(Boolean).join("\n\n");
|
|
181
167
|
if (!next) {
|
|
182
168
|
fs.rmSync(filePath, { force: true });
|
|
@@ -75,7 +75,7 @@ export function refreshExternalGraphArtifacts(repoPath, event, runtime = default
|
|
|
75
75
|
}
|
|
76
76
|
/** Persist an audit record locally; no source content or tool output is retained. */
|
|
77
77
|
export function writeArtifactRefreshRecord(repoPath, result) {
|
|
78
|
-
const knodinDir = path.join(path.resolve(repoPath), ".
|
|
78
|
+
const knodinDir = path.join(path.resolve(repoPath), ".knodin");
|
|
79
79
|
fs.mkdirSync(knodinDir, { recursive: true });
|
|
80
80
|
fs.appendFileSync(path.join(knodinDir, "artifact-refresh.jsonl"), `${JSON.stringify({ at: new Date().toISOString(), ...result })}\n`, "utf8");
|
|
81
81
|
}
|
|
@@ -65,8 +65,8 @@ export function createCompetitiveSandbox(sourceRepository, sourceFixture, compet
|
|
|
65
65
|
GIT_CONFIG_GLOBAL: path.join(config, "gitconfig"),
|
|
66
66
|
GIT_CONFIG_NOSYSTEM: "1",
|
|
67
67
|
PATH: `${binaries}${path.delimiter}${process.env.PATH ?? ""}`,
|
|
68
|
-
|
|
69
|
-
...(realBun ? {
|
|
68
|
+
KNODIN_BENCHMARK_SOURCE_REPOSITORY: sourceRepository,
|
|
69
|
+
...(realBun ? { KNODIN_BENCHMARK_REAL_BUN: realBun } : {}),
|
|
70
70
|
};
|
|
71
71
|
try {
|
|
72
72
|
if (realBun) {
|
|
@@ -74,9 +74,9 @@ export function createCompetitiveSandbox(sourceRepository, sourceFixture, compet
|
|
|
74
74
|
fs.writeFileSync(bunShim, `#!/bin/sh
|
|
75
75
|
if [ "$1" = "src/server.ts" ]; then
|
|
76
76
|
shift
|
|
77
|
-
exec node --import tsx "$
|
|
77
|
+
exec node --import tsx "$KNODIN_BENCHMARK_SOURCE_REPOSITORY/src/server.ts" "$@"
|
|
78
78
|
fi
|
|
79
|
-
exec "$
|
|
79
|
+
exec "$KNODIN_BENCHMARK_REAL_BUN" "$@"
|
|
80
80
|
`, { mode: 0o755 });
|
|
81
81
|
}
|
|
82
82
|
runGit(["clone", "--quiet", "--shared", "--no-checkout", sourceRepository, repository], root, environment);
|
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { RE2 } from "re2-wasm";
|
|
5
5
|
import { measurePerfPhaseSync } from "./engine/perf.js";
|
|
6
|
-
const DEFAULT_EXCLUDES = [".git/**", ".
|
|
6
|
+
const DEFAULT_EXCLUDES = [".git/**", ".knodin/**", "node_modules/**", "dist/**", "build/**"];
|
|
7
7
|
const GLOB_CACHE_LIMIT = 256;
|
|
8
8
|
const globCache = new Map();
|
|
9
9
|
function normalizeRelative(value) {
|
|
@@ -39,12 +39,12 @@ The \`query\` operation lets you ask precise, structured questions about your co
|
|
|
39
39
|
|
|
40
40
|
Federation is knodin's internal query-composition mechanism, not the primary
|
|
41
41
|
user-facing model. Use \`knodin repos\` for operational checkout management and
|
|
42
|
-
\`knodin system\` with commit-ready \`
|
|
42
|
+
\`knodin system\` with commit-ready \`knodin.yaml\` stable identities for
|
|
43
43
|
cooperating components. Sibling directory placement and shared search matches
|
|
44
44
|
never establish system membership.
|
|
45
45
|
|
|
46
|
-
Legacy \`.
|
|
47
|
-
silently discarded. Migrate it to \`
|
|
46
|
+
Legacy \`.knodin/federation.json\` remains a compatibility input and is not
|
|
47
|
+
silently discarded. Migrate it to \`knodin.yaml\` plus personal XDG checkout
|
|
48
48
|
paths. The \`federated_repos\` query remains available for inspecting the
|
|
49
49
|
engine's currently composed repository paths; it does not prove those
|
|
50
50
|
repositories form one system.`,
|
|
@@ -82,7 +82,7 @@ Go, Rust, PHP, Ruby, Kotlin, Swift, Perl, PowerShell, Bash, and MuleSoft/RAML ar
|
|
|
82
82
|
|
|
83
83
|
Run \`knodin status\` (or MCP \`operation: "status"\`) to inspect the local schema/model/version, file and symbol coverage, orphaned or missing records, and the last successful reconciliation. Its repair steps are actionable. Run \`knodin repair\` (or MCP \`operation: "repair"\`) to rebuild only missing/damaged rows and verify health; healthy indexed files are not deleted or rebuilt.
|
|
84
84
|
|
|
85
|
-
The MCP \`telemetry\` operation reads process-local metadata-only measurements: actual response bytes and \`gpt-tokenizer@3.4.0:o200k_base\` tokens, executed baselines where available, negative or positive savings, latency, RSS, schema cost, truncation, and detail mode. It never contains source or file paths and does not change existing operation response shapes. Telemetry is not persisted by default. Set MCP \`persistTelemetry: true\` to append metadata-only JSONL to the repo-root \`.
|
|
85
|
+
The MCP \`telemetry\` operation reads process-local metadata-only measurements: actual response bytes and \`gpt-tokenizer@3.4.0:o200k_base\` tokens, executed baselines where available, negative or positive savings, latency, RSS, schema cost, truncation, and detail mode. It never contains source or file paths and does not change existing operation response shapes. Telemetry is not persisted by default. Set MCP \`persistTelemetry: true\` to append metadata-only JSONL to the repo-root \`.knodin-telemetry.jsonl\`; knodin never sends it over the network. Use \`telemetryAction: "report"\` or \`knodin telemetry report\` for a static repository-local HTML report.
|
|
86
86
|
|
|
87
87
|
Common issues and how to resolve them when using knodin:
|
|
88
88
|
|
|
@@ -100,7 +100,7 @@ The \`prs\` triage operation requires the official GitHub CLI (\`gh\`) to be ins
|
|
|
100
100
|
|
|
101
101
|
### Legacy federation configuration
|
|
102
102
|
If legacy federation configuration fails, validate
|
|
103
|
-
\`.
|
|
103
|
+
\`.knodin/federation.json\`, then migrate stable identities to \`knodin.yaml\`
|
|
104
104
|
and local paths to XDG configuration. Do not infer membership from siblings.`,
|
|
105
105
|
};
|
|
106
106
|
const DOC_TOPIC_FILES = {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* The durable lesson, now a hard gate: no approximate search path may engage BY
|
|
12
12
|
* DEFAULT without a measured >=95% top-10 recall vs the exact scan on a real
|
|
13
13
|
* corpus, with the method stated. This module is therefore OPT-IN ONLY, behind
|
|
14
|
-
* `
|
|
14
|
+
* `KNODIN_ANN=1`; `search()`'s default remains the exact scan at every corpus
|
|
15
15
|
* size. See scripts/ann-bench.ts for the recall/latency harness.
|
|
16
16
|
*
|
|
17
17
|
* MEASURED, GATE NOT CLEARED (so it stays opt-in). scripts/ann-bench.ts on
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* 88.0% top-10 is far better than R11(a)'s LSH (43.3% top-10, AND slower) — the
|
|
26
26
|
* three queries LSH scored 0% on ("session token", "webhook handler", "metrics
|
|
27
27
|
* collection") here score 80/80/90% top-10 — but it misses the hard 95% floor,
|
|
28
|
-
* so the exact scan REMAINS the default. Recall is tunable via
|
|
28
|
+
* so the exact scan REMAINS the default. Recall is tunable via KNODIN_ANN_EF (a
|
|
29
29
|
* larger ef trades latency for recall); default-engagement would require a fresh
|
|
30
30
|
* measurement clearing 95% at the corpus size where the path engages. Given the
|
|
31
31
|
* exact scan is 38ms at 18.6k and extrapolates sub-second to ~250k symbols,
|
|
@@ -257,11 +257,11 @@ export function buildHnswIndex(rows, params = {}) {
|
|
|
257
257
|
* at a corpus size where the path would actually engage (see module header).
|
|
258
258
|
*/
|
|
259
259
|
export function annEnabled() {
|
|
260
|
-
return process.env.
|
|
260
|
+
return process.env.KNODIN_ANN === "1";
|
|
261
261
|
}
|
|
262
|
-
/** efSearch beam width, overridable via
|
|
262
|
+
/** efSearch beam width, overridable via KNODIN_ANN_EF (a benchmark knob). */
|
|
263
263
|
export function annEfSearch() {
|
|
264
|
-
const raw = process.env.
|
|
264
|
+
const raw = process.env.KNODIN_ANN_EF;
|
|
265
265
|
if (raw) {
|
|
266
266
|
const n = Number.parseInt(raw, 10);
|
|
267
267
|
if (Number.isFinite(n) && n > 0)
|
|
@@ -24,7 +24,7 @@ const DEFAULT_MODEL_REMOTE_HOST = "https://huggingface.co/";
|
|
|
24
24
|
*/
|
|
25
25
|
export function getModelCacheDirectory(options = {}) {
|
|
26
26
|
const environment = options.env ?? process.env;
|
|
27
|
-
const override = environment.
|
|
27
|
+
const override = environment.KNODIN_MODEL_CACHE?.trim();
|
|
28
28
|
if (override)
|
|
29
29
|
return path.resolve(override);
|
|
30
30
|
const platform = options.platform ?? process.platform;
|
|
@@ -42,16 +42,16 @@ export function getModelCacheDirectory(options = {}) {
|
|
|
42
42
|
* point at an approved mirror without changing where model files are cached.
|
|
43
43
|
*/
|
|
44
44
|
export function getModelRemoteHost(environment = process.env) {
|
|
45
|
-
const configured = environment.
|
|
45
|
+
const configured = environment.KNODIN_MODEL_HOST?.trim() || DEFAULT_MODEL_REMOTE_HOST;
|
|
46
46
|
let parsed;
|
|
47
47
|
try {
|
|
48
48
|
parsed = new URL(configured);
|
|
49
49
|
}
|
|
50
50
|
catch {
|
|
51
|
-
throw new Error(`
|
|
51
|
+
throw new Error(`KNODIN_MODEL_HOST must be an absolute HTTP(S) URL: ${configured}`);
|
|
52
52
|
}
|
|
53
53
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
54
|
-
throw new Error(`
|
|
54
|
+
throw new Error(`KNODIN_MODEL_HOST must use HTTP(S): ${configured}`);
|
|
55
55
|
}
|
|
56
56
|
parsed.hash = "";
|
|
57
57
|
parsed.search = "";
|
package/dist/src/engine/index.js
CHANGED
|
@@ -796,7 +796,7 @@ const FLOW_MAX_ENTRIES = 200;
|
|
|
796
796
|
const flowsCache = new Map();
|
|
797
797
|
const fileToFlowLookupCache = new Map();
|
|
798
798
|
const FILE_TO_FLOW_LOOKUP_CACHE_LIMIT = 8;
|
|
799
|
-
// R18: opt-in ANN (
|
|
799
|
+
// R18: opt-in ANN (KNODIN_ANN=1). An in-memory HNSW index over a repo's
|
|
800
800
|
// embeddings, built lazily on first ANN query and cached per repo + invalidated
|
|
801
801
|
// by indexGeneration exactly like mapCache/flowsCache — so a reindex rebuilds it
|
|
802
802
|
// and close() drops it. Holds NO stored state (no schema bump, nothing to
|
|
@@ -4814,7 +4814,7 @@ export function parseSimpleNameFromUniqueId(uniqueId) {
|
|
|
4814
4814
|
export async function indexDbtManifestFile(content, _absolutePath, relativePath, repoPath, db) {
|
|
4815
4815
|
try {
|
|
4816
4816
|
const manifest = JSON.parse(content);
|
|
4817
|
-
const dbtDir = path.join(repoPath, ".
|
|
4817
|
+
const dbtDir = path.join(repoPath, ".knodin/dbt");
|
|
4818
4818
|
await fs.promises.mkdir(dbtDir, { recursive: true });
|
|
4819
4819
|
const assets = [];
|
|
4820
4820
|
// 1. Process nodes (models, seeds, tests)
|
|
@@ -4951,9 +4951,9 @@ export async function indexDbtManifestFile(content, _absolutePath, relativePath,
|
|
|
4951
4951
|
db.run("BEGIN TRANSACTION;");
|
|
4952
4952
|
try {
|
|
4953
4953
|
// Clear out any previous records associated with this dbt manifest or virtual paths
|
|
4954
|
-
deleteSymbolsMatchingPath(db, ".
|
|
4955
|
-
db.run("DELETE FROM \"references\" WHERE callerFile LIKE '.
|
|
4956
|
-
db.run("DELETE FROM dependencies WHERE fromFile LIKE '.
|
|
4954
|
+
deleteSymbolsMatchingPath(db, ".knodin/dbt/%");
|
|
4955
|
+
db.run("DELETE FROM \"references\" WHERE callerFile LIKE '.knodin/dbt/%'");
|
|
4956
|
+
db.run("DELETE FROM dependencies WHERE fromFile LIKE '.knodin/dbt/%'");
|
|
4957
4957
|
deleteSymbolsForFile(db, relativePath);
|
|
4958
4958
|
db.run('DELETE FROM "references" WHERE callerFile = ?', [relativePath]);
|
|
4959
4959
|
db.run("DELETE FROM dependencies WHERE fromFile = ?", [relativePath]);
|
|
@@ -4971,7 +4971,7 @@ export async function indexDbtManifestFile(content, _absolutePath, relativePath,
|
|
|
4971
4971
|
`);
|
|
4972
4972
|
for (const asset of assets) {
|
|
4973
4973
|
const safeId = asset.unique_id.replace(/[/\\]/g, "_");
|
|
4974
|
-
const virtualFilePath = `.
|
|
4974
|
+
const virtualFilePath = `.knodin/dbt/${safeId}.sql`;
|
|
4975
4975
|
const absVirtualPath = path.join(repoPath, virtualFilePath);
|
|
4976
4976
|
let code = asset.sqlCode;
|
|
4977
4977
|
if (!code) {
|
|
@@ -4990,7 +4990,7 @@ export async function indexDbtManifestFile(content, _absolutePath, relativePath,
|
|
|
4990
4990
|
for (const depId of asset.depends_on_nodes) {
|
|
4991
4991
|
const depAsset = assetMap.get(depId);
|
|
4992
4992
|
const safeDepId = depId.replace(/[/\\]/g, "_");
|
|
4993
|
-
const depVirtualFilePath = `.
|
|
4993
|
+
const depVirtualFilePath = `.knodin/dbt/${safeDepId}.sql`;
|
|
4994
4994
|
insertDep.run(virtualFilePath, depVirtualFilePath, "dbt-lineage");
|
|
4995
4995
|
const depSimpleName = depAsset ? depAsset.name : parseSimpleNameFromUniqueId(depId);
|
|
4996
4996
|
insertRef.run(asset.name, virtualFilePath, depSimpleName, depVirtualFilePath, 1, 1);
|
|
@@ -5247,7 +5247,7 @@ const MAX_EMBEDDING_BATCH_SIZE = 32;
|
|
|
5247
5247
|
// every symbol without turning source preparation into an unbounded memory sink.
|
|
5248
5248
|
const EMBEDDING_SOURCE_CACHE_MAX_BYTES = 64 * 1024 * 1024;
|
|
5249
5249
|
function embeddingBatchSize() {
|
|
5250
|
-
const raw = process.env.
|
|
5250
|
+
const raw = process.env.KNODIN_EMBEDDING_BATCH_SIZE;
|
|
5251
5251
|
if (raw === undefined)
|
|
5252
5252
|
return DEFAULT_EMBEDDING_BATCH_SIZE;
|
|
5253
5253
|
const parsed = Number(raw);
|
|
@@ -5829,11 +5829,11 @@ const WATCHED_FRESHNESS_LEASE_MS = 60_000;
|
|
|
5829
5829
|
const FRESHNESS_FALLBACK_FILE_BOUND = 50000;
|
|
5830
5830
|
/**
|
|
5831
5831
|
* The non-git fallback's file bound, overridable with
|
|
5832
|
-
* `
|
|
5832
|
+
* `KNODIN_FRESHNESS_FILE_BOUND` for repos where even a stat sweep of the
|
|
5833
5833
|
* indexed set is too much to pay per query.
|
|
5834
5834
|
*/
|
|
5835
5835
|
function freshnessFallbackBound() {
|
|
5836
|
-
const raw = process.env.
|
|
5836
|
+
const raw = process.env.KNODIN_FRESHNESS_FILE_BOUND;
|
|
5837
5837
|
if (raw !== undefined) {
|
|
5838
5838
|
const parsed = Number.parseInt(raw, 10);
|
|
5839
5839
|
if (Number.isFinite(parsed) && parsed >= 0)
|
|
@@ -6336,9 +6336,9 @@ async function getOrInitDb(repoPath, options = {}) {
|
|
|
6336
6336
|
dbPath = ":memory:";
|
|
6337
6337
|
}
|
|
6338
6338
|
else {
|
|
6339
|
-
const knodinDir = path.join(normalizedPath, ".
|
|
6339
|
+
const knodinDir = path.join(normalizedPath, ".knodin");
|
|
6340
6340
|
await fs.promises.mkdir(knodinDir, { recursive: true });
|
|
6341
|
-
// Ensure .
|
|
6341
|
+
// Ensure .knodin is in .gitignore
|
|
6342
6342
|
try {
|
|
6343
6343
|
const gitignorePath = path.join(normalizedPath, ".gitignore");
|
|
6344
6344
|
let gitignoreContent = "";
|
|
@@ -6346,11 +6346,11 @@ async function getOrInitDb(repoPath, options = {}) {
|
|
|
6346
6346
|
gitignoreContent = await fs.promises.readFile(gitignorePath, "utf-8");
|
|
6347
6347
|
}
|
|
6348
6348
|
const lines = gitignoreContent.split("\n").map((l) => l.trim());
|
|
6349
|
-
if (!lines.includes(".
|
|
6350
|
-
!lines.includes(".
|
|
6351
|
-
!lines.includes("/.
|
|
6349
|
+
if (!lines.includes(".knodin") &&
|
|
6350
|
+
!lines.includes(".knodin/") &&
|
|
6351
|
+
!lines.includes("/.knodin")) {
|
|
6352
6352
|
const prefix = gitignoreContent.length > 0 && !gitignoreContent.endsWith("\n") ? "\n" : "";
|
|
6353
|
-
await fs.promises.appendFile(gitignorePath, `${prefix}\n# knodin\n.
|
|
6353
|
+
await fs.promises.appendFile(gitignorePath, `${prefix}\n# knodin\n.knodin\n`);
|
|
6354
6354
|
}
|
|
6355
6355
|
}
|
|
6356
6356
|
catch (e) {
|
|
@@ -6401,7 +6401,7 @@ async function getOrInitDb(repoPath, options = {}) {
|
|
|
6401
6401
|
// an unknown response shape, preserving C9's precision-first rule. v21
|
|
6402
6402
|
// adds source-only LWR/Experience Bundle topology and must revisit its
|
|
6403
6403
|
// newly indexable JSON files.
|
|
6404
|
-
const
|
|
6404
|
+
const KNODIN_SCHEMA_VERSION = 21;
|
|
6405
6405
|
// Highest version whose upgrade needs the stored data REBUILT. Versions
|
|
6406
6406
|
// above it migrate in place, so an upgrade costs a DELETE rather than a
|
|
6407
6407
|
// full re-index + re-embed (~40 min of CPU on an 18k-symbol corpus).
|
|
@@ -6411,7 +6411,7 @@ async function getOrInitDb(repoPath, options = {}) {
|
|
|
6411
6411
|
const needsMcpBackfill = storedVersion < 17;
|
|
6412
6412
|
// v13 -> v14 purges embeddings orphaned while the FK cascade was inert.
|
|
6413
6413
|
// Runs after the CREATE TABLEs below, since the table must exist.
|
|
6414
|
-
const needsOrphanPurge = storedVersion <
|
|
6414
|
+
const needsOrphanPurge = storedVersion < KNODIN_SCHEMA_VERSION;
|
|
6415
6415
|
if (storedVersion < LAST_REBUILD_SCHEMA_VERSION) {
|
|
6416
6416
|
db.run("DROP TRIGGER IF EXISTS after_symbol_insert;");
|
|
6417
6417
|
db.run("DROP TRIGGER IF EXISTS after_symbol_delete;");
|
|
@@ -6426,8 +6426,8 @@ async function getOrInitDb(repoPath, options = {}) {
|
|
|
6426
6426
|
db.run("DROP TABLE IF EXISTS mcp_tools;");
|
|
6427
6427
|
db.run("DROP TABLE IF EXISTS api_contracts;");
|
|
6428
6428
|
}
|
|
6429
|
-
if (storedVersion <
|
|
6430
|
-
db.run(`PRAGMA user_version = ${
|
|
6429
|
+
if (storedVersion < KNODIN_SCHEMA_VERSION) {
|
|
6430
|
+
db.run(`PRAGMA user_version = ${KNODIN_SCHEMA_VERSION};`);
|
|
6431
6431
|
}
|
|
6432
6432
|
db.run(`
|
|
6433
6433
|
CREATE TABLE IF NOT EXISTS symbols (
|
|
@@ -6712,15 +6712,15 @@ async function getFederatedRepos(repoPath) {
|
|
|
6712
6712
|
const repos = [resolvedRepoPath];
|
|
6713
6713
|
// Federation is EXPLICIT by default: a repo is treated as standalone unless it
|
|
6714
6714
|
// is told otherwise, so results never silently depend on what else happens to
|
|
6715
|
-
// live next to it. Two opt-in signals, read from `.
|
|
6715
|
+
// live next to it. Two opt-in signals, read from `.knodin/federation.json`
|
|
6716
6716
|
// ({ "repos"?: string[], "autoDiscover"?: boolean }):
|
|
6717
6717
|
// • repos — sibling paths (relative to this repo) to federate to.
|
|
6718
6718
|
// • autoDiscover — scan the parent directory for already-indexed siblings.
|
|
6719
|
-
// autoDiscover can also be forced with
|
|
6719
|
+
// autoDiscover can also be forced with KNODIN_AUTO_FEDERATE=1. Without either
|
|
6720
6720
|
// signal, knodin federates to exactly the repos listed in `repos` (possibly
|
|
6721
6721
|
// none) and nothing else.
|
|
6722
|
-
let autoDiscover = process.env.
|
|
6723
|
-
const configPath = path.join(resolvedRepoPath, ".
|
|
6722
|
+
let autoDiscover = process.env.KNODIN_AUTO_FEDERATE === "1" || process.env.KNODIN_AUTO_FEDERATE === "true";
|
|
6723
|
+
const configPath = path.join(resolvedRepoPath, ".knodin/federation.json");
|
|
6724
6724
|
if (fs.existsSync(configPath)) {
|
|
6725
6725
|
try {
|
|
6726
6726
|
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
@@ -6744,7 +6744,7 @@ async function getFederatedRepos(repoPath) {
|
|
|
6744
6744
|
return repos;
|
|
6745
6745
|
}
|
|
6746
6746
|
// Auto-discovery (opt-in): siblings in the parent directory that knodin has
|
|
6747
|
-
// already indexed (i.e. have a .
|
|
6747
|
+
// already indexed (i.e. have a .knodin/db.sqlite). Under test the DB is
|
|
6748
6748
|
// in-memory, so no repo has that on-disk marker; it would only ever match
|
|
6749
6749
|
// repos a *production* run indexed elsewhere on the machine, so test
|
|
6750
6750
|
// federation is driven purely by in-process `dbInstances` — skip the disk
|
|
@@ -6761,7 +6761,7 @@ async function getFederatedRepos(repoPath) {
|
|
|
6761
6761
|
if (siblingPath === resolvedRepoPath)
|
|
6762
6762
|
continue;
|
|
6763
6763
|
const isAlreadyIndexed = dbInstances.has(path.resolve(siblingPath)) ||
|
|
6764
|
-
(!isTest && fs.existsSync(path.join(siblingPath, ".
|
|
6764
|
+
(!isTest && fs.existsSync(path.join(siblingPath, ".knodin", "db.sqlite")));
|
|
6765
6765
|
if (isAlreadyIndexed) {
|
|
6766
6766
|
repos.push(siblingPath);
|
|
6767
6767
|
}
|
|
@@ -8643,7 +8643,7 @@ function renderCommunityPage(community, map) {
|
|
|
8643
8643
|
lines.push("");
|
|
8644
8644
|
return lines.join("\n");
|
|
8645
8645
|
}
|
|
8646
|
-
/** Renders `.
|
|
8646
|
+
/** Renders `.knodin/wiki/index.md`: repo path, community table, one link per page. */
|
|
8647
8647
|
function renderWikiIndex(resolvedRepoPath, map, slugs) {
|
|
8648
8648
|
const lines = [];
|
|
8649
8649
|
lines.push("# knodin Wiki", "");
|
|
@@ -8681,14 +8681,14 @@ function writeWikiPageIfChanged(dirPath, relFile, content, force, written, skipp
|
|
|
8681
8681
|
written.push(relFile);
|
|
8682
8682
|
}
|
|
8683
8683
|
/**
|
|
8684
|
-
* Generates the graph wiki (R5): `.
|
|
8684
|
+
* Generates the graph wiki (R5): `.knodin/wiki/index.md` + one page per
|
|
8685
8685
|
* `map()` community. Reuses `engine.map(, "standard")` for community/hub/bridge/edge
|
|
8686
8686
|
* data — no community-detection logic is duplicated here.
|
|
8687
8687
|
*/
|
|
8688
8688
|
async function writeWiki(engine, repoPath, force) {
|
|
8689
8689
|
const resolvedRepoPath = path.resolve(repoPath);
|
|
8690
8690
|
const map = await engine.map(repoPath, "standard");
|
|
8691
|
-
const wikiDir = path.join(resolvedRepoPath, ".
|
|
8691
|
+
const wikiDir = path.join(resolvedRepoPath, ".knodin", "wiki");
|
|
8692
8692
|
fs.mkdirSync(wikiDir, { recursive: true });
|
|
8693
8693
|
// Slugify community names into unique page filenames (lowercase, hyphenated).
|
|
8694
8694
|
const slugs = new Map();
|
|
@@ -9651,7 +9651,7 @@ export function createEngine() {
|
|
|
9651
9651
|
// repair and timestamp drift before reporting it. Reuse an active DB in
|
|
9652
9652
|
// long-lived processes, or open the existing local DB read-only.
|
|
9653
9653
|
const activeDb = dbInstances.get(resolved);
|
|
9654
|
-
const diskDbPath = path.join(resolved, ".
|
|
9654
|
+
const diskDbPath = path.join(resolved, ".knodin", "db.sqlite");
|
|
9655
9655
|
const db = activeDb ??
|
|
9656
9656
|
(fs.existsSync(diskDbPath) ? new Database(diskDbPath, { readonly: true }) : null);
|
|
9657
9657
|
const verifiedAt = new Date().toISOString();
|
|
@@ -10057,7 +10057,7 @@ export function createEngine() {
|
|
|
10057
10057
|
// Repair partial current-schema databases before normal initialization,
|
|
10058
10058
|
// whose reconciliation queries require these columns. ALTER preserves all
|
|
10059
10059
|
// healthy index_state rows; the reconciler then refreshes their snapshots.
|
|
10060
|
-
const diskDbPath = path.join(resolved, ".
|
|
10060
|
+
const diskDbPath = path.join(resolved, ".knodin", "db.sqlite");
|
|
10061
10061
|
if (fs.existsSync(diskDbPath)) {
|
|
10062
10062
|
const activeDb = dbInstances.get(resolved);
|
|
10063
10063
|
const schemaDb = activeDb ?? new Database(diskDbPath);
|
|
@@ -10551,7 +10551,7 @@ export function createEngine() {
|
|
|
10551
10551
|
// full-corpus graph. Keep the opt-in ANN path for unfiltered searches;
|
|
10552
10552
|
// filtered requests use the exact default over only eligible rows.
|
|
10553
10553
|
if (ann.annEnabled() && !hasFilters) {
|
|
10554
|
-
// R18 OPT-IN ANN PATH (
|
|
10554
|
+
// R18 OPT-IN ANN PATH (KNODIN_ANN=1). Build/reuse an in-memory HNSW for
|
|
10555
10555
|
// this repo and query it instead of computing cosine over every row. The
|
|
10556
10556
|
// exact scan below stays the DEFAULT at every corpus size; this path only
|
|
10557
10557
|
// engages behind the env flag, until a measured >=95% top-10 recall gate
|
|
@@ -12933,7 +12933,7 @@ export function createEngine() {
|
|
|
12933
12933
|
for (const rel of wouldChange) {
|
|
12934
12934
|
const abs = path.resolve(resolvedRepoPath, rel);
|
|
12935
12935
|
const content = newContents.get(rel);
|
|
12936
|
-
const tmp = path.join(path.dirname(abs), `.${path.basename(abs)}.
|
|
12936
|
+
const tmp = path.join(path.dirname(abs), `.${path.basename(abs)}.knodin-rename-${process.pid}-${Date.now()}.tmp`);
|
|
12937
12937
|
fs.writeFileSync(tmp, content, "utf-8");
|
|
12938
12938
|
fs.renameSync(tmp, abs);
|
|
12939
12939
|
}
|
package/dist/src/engine/prune.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
/**
|
|
3
3
|
* Single source of truth for "which directories does knodin ignore" (R14).
|
|
4
4
|
*
|
|
5
|
-
* Before this module, the same intent — skip node_modules, .git, .
|
|
5
|
+
* Before this module, the same intent — skip node_modules, .git, .knodin,
|
|
6
6
|
* build output, caches, VCS internals — was encoded THREE separate times:
|
|
7
7
|
* 1. src/__tests__/setup.ts's `PRUNE_DIRS` regex (glob-shim exclude filter)
|
|
8
8
|
* 2. the indexer's own directory filter (`EXCLUDED_DIR_SEGMENTS` / `isIndexablePath`)
|
|
@@ -25,7 +25,7 @@ import path from "node:path";
|
|
|
25
25
|
export const PRUNE_DIR_NAMES = new Set([
|
|
26
26
|
"node_modules",
|
|
27
27
|
".git",
|
|
28
|
-
".
|
|
28
|
+
".knodin",
|
|
29
29
|
// Local state maintained by other code-intelligence tools. It is neither
|
|
30
30
|
// repository source nor stable project configuration, and indexing it makes
|
|
31
31
|
// a healthy checkout look perpetually stale as those tools update databases
|
|
@@ -45,7 +45,7 @@ export function deleteSymbolsForFile(db, filePath) {
|
|
|
45
45
|
/**
|
|
46
46
|
* Delete every symbol whose file path matches a LIKE pattern, plus its
|
|
47
47
|
* embeddings. Used by the dbt path, whose symbols live under synthetic
|
|
48
|
-
* `.
|
|
48
|
+
* `.knodin/dbt/%` paths rather than one real file.
|
|
49
49
|
*/
|
|
50
50
|
export function deleteSymbolsMatchingPath(db, likePattern) {
|
|
51
51
|
db.run("DELETE FROM symbol_embeddings WHERE symbolId IN (SELECT id FROM symbols WHERE filePath LIKE ?)", [likePattern]);
|
|
@@ -2,7 +2,7 @@ import child_process from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
function activityPath(repo) {
|
|
5
|
-
return path.join(path.resolve(repo), ".
|
|
5
|
+
return path.join(path.resolve(repo), ".knodin", "index-activity.json");
|
|
6
6
|
}
|
|
7
7
|
const ACTIVITY_CLOCK_SKEW_MS = 2_000;
|
|
8
8
|
const MAX_ACTIVITY_AGE_MS = 7 * 24 * 60 * 60 * 1_000;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { clearLine as clearTerminalLine, moveCursor as moveTerminalCursor, cursorTo as moveTerminalCursorTo, } from "node:readline";
|
|
3
3
|
import ora from "ora";
|
|
4
4
|
import { formatProgress, normalizeTerminalDimension, } from "./init-progress.js";
|
|
5
|
-
const terminalEnabled = process.stderr.isTTY === true || process.env.
|
|
5
|
+
const terminalEnabled = process.stderr.isTTY === true || process.env.KNODIN_FORCE_PROGRESS_TTY === "1";
|
|
6
6
|
// Ora divides rendered text width by `stream.columns`. Some pseudo-terminals
|
|
7
7
|
// report zero columns, which turns its clear loop into an infinite loop. Keep
|
|
8
8
|
// the real terminal methods while guaranteeing finite viewport dimensions.
|