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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/dist/bin/cli.js +1704 -0
  4. package/dist/src/agent-integration.js +250 -0
  5. package/dist/src/artifact-refresh.js +81 -0
  6. package/dist/src/cli-args.js +267 -0
  7. package/dist/src/cli-model.js +324 -0
  8. package/dist/src/compact-structural.js +96 -0
  9. package/dist/src/competitive-constraints.js +20 -0
  10. package/dist/src/competitive-manifest.js +330 -0
  11. package/dist/src/competitive-measurement.js +183 -0
  12. package/dist/src/competitive-runner.js +453 -0
  13. package/dist/src/competitive-sandbox.js +108 -0
  14. package/dist/src/context-export.js +422 -0
  15. package/dist/src/context.js +102 -0
  16. package/dist/src/docs-sections.js +141 -0
  17. package/dist/src/doctor.js +380 -0
  18. package/dist/src/engine/ann-hnsw.js +271 -0
  19. package/dist/src/engine/embeddings.js +193 -0
  20. package/dist/src/engine/file-walker.js +43 -0
  21. package/dist/src/engine/index.js +13030 -0
  22. package/dist/src/engine/perf.js +115 -0
  23. package/dist/src/engine/prune.js +112 -0
  24. package/dist/src/engine/source-policy.js +69 -0
  25. package/dist/src/engine/sqlite.js +71 -0
  26. package/dist/src/engine/symbol-delete.js +58 -0
  27. package/dist/src/failure-diagnosis.js +590 -0
  28. package/dist/src/fleet.js +7 -0
  29. package/dist/src/git-executable.js +31 -0
  30. package/dist/src/graph-query-health.js +115 -0
  31. package/dist/src/index-activity.js +125 -0
  32. package/dist/src/init-progress-worker.js +107 -0
  33. package/dist/src/init-progress.js +155 -0
  34. package/dist/src/init.js +985 -0
  35. package/dist/src/lifecycle-health.js +213 -0
  36. package/dist/src/lsp-readonly.js +217 -0
  37. package/dist/src/output-compression.js +629 -0
  38. package/dist/src/output-telemetry.js +359 -0
  39. package/dist/src/pr-triage.js +638 -0
  40. package/dist/src/relationship-adapters.js +370 -0
  41. package/dist/src/release-attestation.js +533 -0
  42. package/dist/src/repair-progress-worker.js +121 -0
  43. package/dist/src/repair-progress.js +262 -0
  44. package/dist/src/repository-init-process.js +173 -0
  45. package/dist/src/repository-management.js +1089 -0
  46. package/dist/src/response-budget.js +184 -0
  47. package/dist/src/server.js +53 -0
  48. package/dist/src/system-config.js +615 -0
  49. package/dist/src/terminal-help.js +83 -0
  50. package/dist/src/tools/knodin-tools.js +1438 -0
  51. package/dist/src/tools/reckon-tools.js +5 -0
  52. package/dist/src/update-policy.js +944 -0
  53. package/dist/src/update-trust.js +503 -0
  54. package/dist/src/version.js +13 -0
  55. package/dist/src/visualization.js +162 -0
  56. package/dist/src/wait-for-fresh.js +98 -0
  57. package/dist/src/worktree-lifecycle.js +231 -0
  58. package/docs/CLI.md +39 -0
  59. package/docs/COMMAND-OUTPUT-COMPRESSION.md +194 -0
  60. package/docs/DEAD-CODE-AND-IMPACT.md +27 -0
  61. package/docs/DOCTOR-AND-UPDATES.md +84 -0
  62. package/docs/INDEXING-POLICY-AND-PROVENANCE.md +37 -0
  63. package/docs/INSTALLATION.md +208 -0
  64. package/docs/MCP.md +100 -0
  65. package/docs/PT-ACCESS-RECOMMENDATION.md +91 -0
  66. package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
  67. package/docs/REPOSITORIES-AND-WORKTREES.md +81 -0
  68. package/docs/SIGNED-UPDATES.md +146 -0
  69. package/docs/SYSTEMS-AND-RELATIONSHIPS.md +45 -0
  70. package/docs/TELEMETRY.md +42 -0
  71. package/docs/releases/0.3.0.md +46 -0
  72. package/docs/releases/0.4.0.md +68 -0
  73. package/docs/releases/0.4.1.md +28 -0
  74. package/docs/releases/0.4.2.md +27 -0
  75. package/docs/releases/0.4.3.md +23 -0
  76. package/docs/releases/0.5.0.md +29 -0
  77. package/package.json +110 -0
  78. package/schemas/release-attestation-v1.schema.json +210 -0
  79. package/tree-sitter-prisma.wasm +0 -0
  80. package/tree-sitter-sql.wasm +0 -0
  81. package/tree-sitter-xml.wasm +0 -0
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Local-only by design (ADR 003, "Future Considerations" section,
3
+ * docs/adr/003-local-semantic-search.md): a cloud embedding provider
4
+ * (OpenAI/Google/MiniMax, as some competitors offer) was evaluated and
5
+ * deliberately deferred, not merely unbuilt. knodin's core positioning is
6
+ * zero-auth/local-first/no-egress — sending symbol text to a cloud API for
7
+ * embedding would break that guarantee for every indexed repo, not just tune
8
+ * a knob. If ever added, it must be opt-in with `local` staying the default,
9
+ * and reads as a natural pro-tier feature precisely because enabling it is a
10
+ * deliberate trade against this file's local/no-egress guarantee.
11
+ */
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+ let embedderPromise = null;
15
+ let embedderOverride = null;
16
+ const embedderProgressListeners = new Set();
17
+ let embedderReady = false;
18
+ const EMBEDDING_DIMENSIONS = 384;
19
+ const DEFAULT_MODEL_REMOTE_HOST = "https://huggingface.co/";
20
+ /**
21
+ * One machine-level model cache shared by every repository and every installed
22
+ * knodin version. An explicit override makes managed/offline environments and
23
+ * the clean-package acceptance gate deterministic.
24
+ */
25
+ export function getModelCacheDirectory(options = {}) {
26
+ const environment = options.env ?? process.env;
27
+ const override = environment.RECKON_MODEL_CACHE?.trim();
28
+ if (override)
29
+ return path.resolve(override);
30
+ const platform = options.platform ?? process.platform;
31
+ const home = options.home ?? os.homedir();
32
+ if (platform === "win32") {
33
+ return path.join(environment.LOCALAPPDATA || path.join(home, "AppData", "Local"), "knodin", "models");
34
+ }
35
+ if (platform === "darwin")
36
+ return path.join(home, "Library", "Caches", "knodin", "models");
37
+ return path.join(environment.XDG_CACHE_HOME || path.join(home, ".cache"), "knodin", "models");
38
+ }
39
+ /**
40
+ * Remote model origin used only when the shared machine cache is cold.
41
+ * The default remains the public Hugging Face Hub; managed environments may
42
+ * point at an approved mirror without changing where model files are cached.
43
+ */
44
+ export function getModelRemoteHost(environment = process.env) {
45
+ const configured = environment.RECKON_MODEL_HOST?.trim() || DEFAULT_MODEL_REMOTE_HOST;
46
+ let parsed;
47
+ try {
48
+ parsed = new URL(configured);
49
+ }
50
+ catch {
51
+ throw new Error(`RECKON_MODEL_HOST must be an absolute HTTP(S) URL: ${configured}`);
52
+ }
53
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
54
+ throw new Error(`RECKON_MODEL_HOST must use HTTP(S): ${configured}`);
55
+ }
56
+ parsed.hash = "";
57
+ parsed.search = "";
58
+ return parsed.href.endsWith("/") ? parsed.href : `${parsed.href}/`;
59
+ }
60
+ /**
61
+ * Number of times the real ONNX model pipeline has actually been loaded in this
62
+ * process. Observability only — it never changes embedding behavior. The test
63
+ * suite uses it to guarantee that non-model suites keep the deterministic test
64
+ * embedder installed and never fall through to native model initialization
65
+ * (which, repeated across a long-lived worker, is what C50 removes).
66
+ */
67
+ let realEmbedderLoadCount = 0;
68
+ /** Returns how many times the real ONNX pipeline has loaded in this process. */
69
+ export function getRealEmbedderLoadCount() {
70
+ return realEmbedderLoadCount;
71
+ }
72
+ /**
73
+ * Overrides the embedding pipeline (e.g. with a fast mock in tests). Pass `null`
74
+ * to clear the override and fall back to the real lazily-loaded model.
75
+ */
76
+ export function setEmbedder(pipeline) {
77
+ embedderOverride = pipeline;
78
+ embedderPromise = null;
79
+ embedderReady = pipeline !== null;
80
+ }
81
+ /**
82
+ * Release the native ONNX model session if one was loaded, freeing its resident
83
+ * memory. Best-effort and test-facing: the single long-lived test worker
84
+ * (isolate:false) otherwise keeps the ~model resident for the whole run, bloating
85
+ * the process so later subprocess-spawning tests fork slowly. The dedicated
86
+ * embedding spec calls this in teardown once its real-model assertions are done.
87
+ * A no-op when only a mock embedder (or nothing) has been used.
88
+ */
89
+ export async function disposeEmbedder() {
90
+ const pending = embedderPromise;
91
+ embedderPromise = null;
92
+ embedderReady = false;
93
+ if (!pending)
94
+ return;
95
+ try {
96
+ const pipe = (await pending);
97
+ if (typeof pipe.dispose === "function")
98
+ await pipe.dispose();
99
+ }
100
+ catch {
101
+ /* best-effort native teardown; never fail a test on dispose */
102
+ }
103
+ }
104
+ /**
105
+ * Lazy-loads and caches the local embedding pipeline.
106
+ * Uses a lightweight, high-performance 384-dimension model (all-MiniLM-L6-v2)
107
+ * running in-process via ONNX Runtime Web.
108
+ * Uses dynamic imports to keep CLI and MCP server startup latency near-zero.
109
+ */
110
+ export function getEmbedder(onProgress) {
111
+ if (embedderOverride) {
112
+ onProgress?.({ status: "ready", name: "local test embedder" });
113
+ return Promise.resolve(embedderOverride);
114
+ }
115
+ if (onProgress && embedderReady)
116
+ onProgress({ status: "ready", name: "Xenova/all-MiniLM-L6-v2" });
117
+ else if (onProgress)
118
+ embedderProgressListeners.add(onProgress);
119
+ if (!embedderPromise) {
120
+ embedderPromise = (async () => {
121
+ try {
122
+ const { env, pipeline } = await import("@huggingface/transformers");
123
+ env.cacheDir = getModelCacheDirectory();
124
+ env.remoteHost = getModelRemoteHost();
125
+ const notify = (event) => {
126
+ for (const listener of embedderProgressListeners) {
127
+ try {
128
+ listener(event);
129
+ }
130
+ catch {
131
+ /* Observability must not alter model initialization. */
132
+ }
133
+ }
134
+ };
135
+ notify({ status: "loading", name: "Xenova/all-MiniLM-L6-v2" });
136
+ const pipe = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", {
137
+ progress_callback: notify,
138
+ });
139
+ realEmbedderLoadCount++;
140
+ embedderReady = true;
141
+ notify({ status: "ready", name: "Xenova/all-MiniLM-L6-v2" });
142
+ embedderProgressListeners.clear();
143
+ return pipe;
144
+ }
145
+ catch (error) {
146
+ embedderPromise = null; // Reset promise so we can retry on next call
147
+ embedderProgressListeners.clear();
148
+ console.error("Failed to initialize Hugging Face embedding pipeline:", error);
149
+ throw error;
150
+ }
151
+ })();
152
+ }
153
+ return embedderPromise;
154
+ }
155
+ /**
156
+ * Generates a normalized Float32Array embedding (384 dimensions) for a given text block.
157
+ */
158
+ export async function generateEmbeddings(texts, onModelProgress) {
159
+ if (texts.length === 0)
160
+ return [];
161
+ const extractor = await getEmbedder(onModelProgress);
162
+ const result = await extractor([...texts], { pooling: "mean", normalize: true });
163
+ const data = result.data instanceof Float32Array ? result.data : Float32Array.from(result.data);
164
+ const dims = result.dims;
165
+ if (dims?.length !== 2 || dims[0] !== texts.length || dims[1] !== EMBEDDING_DIMENSIONS) {
166
+ throw new Error(`Unexpected batched embedding shape: expected [${texts.length}, ${EMBEDDING_DIMENSIONS}], got ${JSON.stringify(dims)}`);
167
+ }
168
+ const dimension = dims[1];
169
+ if (data.length !== texts.length * dimension) {
170
+ throw new Error(`Unexpected batched embedding data length: expected ${texts.length * dimension}, got ${data.length}`);
171
+ }
172
+ return texts.map((_, index) => data.slice(index * dimension, (index + 1) * dimension));
173
+ }
174
+ /** Compatibility wrapper used by query-time search. */
175
+ export async function generateEmbedding(text) {
176
+ return (await generateEmbeddings([text]))[0];
177
+ }
178
+ /**
179
+ * Computes the cosine similarity between two normalized Float32Arrays.
180
+ * Since the vectors are pre-normalized, the cosine similarity simplifies
181
+ * to the mathematical dot product: sum(u_i * v_i).
182
+ */
183
+ export function computeSimilarity(queryVec, targetVec) {
184
+ if (queryVec.length !== targetVec.length) {
185
+ throw new Error(`Embedding dimension mismatch: query has ${queryVec.length} dims, target has ${targetVec.length}`);
186
+ }
187
+ let dotProduct = 0;
188
+ const len = queryVec.length;
189
+ for (let i = 0; i < len; i++) {
190
+ dotProduct += queryVec[i] * targetVec[i];
191
+ }
192
+ return dotProduct;
193
+ }
@@ -0,0 +1,43 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { isIndexablePath } from "./prune.js";
4
+ function comparePaths(left, right) {
5
+ return left < right ? -1 : left > right ? 1 : 0;
6
+ }
7
+ /**
8
+ * Deterministically enumerate regular files beneath a repository without
9
+ * following symlinks. Directory pruning happens before descent so dependency,
10
+ * build, VCS, worktree, and local graph state never incur a recursive scan.
11
+ */
12
+ export function walkRepoFiles(repoPath, options = {}) {
13
+ const root = path.resolve(repoPath);
14
+ const pending = [{ absolute: root, relative: "" }];
15
+ const files = [];
16
+ while (pending.length > 0) {
17
+ const directory = pending.pop();
18
+ if (!directory)
19
+ break;
20
+ let entries;
21
+ try {
22
+ entries = fs.readdirSync(directory.absolute, { withFileTypes: true });
23
+ }
24
+ catch {
25
+ continue;
26
+ }
27
+ entries.sort((left, right) => comparePaths(left.name, right.name));
28
+ for (const entry of entries) {
29
+ const relative = directory.relative ? `${directory.relative}/${entry.name}` : entry.name;
30
+ if (!isIndexablePath(relative))
31
+ continue;
32
+ const absolute = path.join(directory.absolute, entry.name);
33
+ if (entry.isDirectory()) {
34
+ pending.push({ absolute, relative });
35
+ }
36
+ else if (entry.isFile() && (options.accept?.(relative) ?? true)) {
37
+ files.push(relative);
38
+ }
39
+ }
40
+ }
41
+ files.sort(comparePaths);
42
+ return options.limit === undefined ? files : files.slice(0, Math.max(0, options.limit));
43
+ }