knodin 0.12.2 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +16 -1
  2. package/dist/bin/cli.js +245 -18
  3. package/dist/src/agent-events.js +25 -7
  4. package/dist/src/authenticated-cursor.js +81 -0
  5. package/dist/src/backup-retention.js +345 -12
  6. package/dist/src/class-consumer-contract.js +18 -0
  7. package/dist/src/class-consumer-cursor.js +91 -0
  8. package/dist/src/class-consumer-delivery.js +22 -0
  9. package/dist/src/class-consumer-page.js +148 -0
  10. package/dist/src/cli-model.js +21 -3
  11. package/dist/src/docs-sections.js +1 -0
  12. package/dist/src/engine/apex-class-uses.js +430 -0
  13. package/dist/src/engine/apex-entry-points.js +98 -0
  14. package/dist/src/engine/apex-receiver.js +301 -0
  15. package/dist/src/engine/candidate-database.js +624 -66
  16. package/dist/src/engine/embedding-reuse.js +57 -0
  17. package/dist/src/engine/embeddings.js +22 -0
  18. package/dist/src/engine/index-coverage.js +215 -0
  19. package/dist/src/engine/index.js +3854 -448
  20. package/dist/src/engine/salesforce-components.js +460 -0
  21. package/dist/src/engine/seal.js +3 -0
  22. package/dist/src/engine/sqlite.js +44 -0
  23. package/dist/src/engine/state-paths.js +192 -31
  24. package/dist/src/evidence-bundle.js +283 -0
  25. package/dist/src/evidence-graph.js +163 -0
  26. package/dist/src/failure-diagnosis.js +80 -5
  27. package/dist/src/file-dependency.js +35 -0
  28. package/dist/src/graph-query-health.js +47 -1
  29. package/dist/src/implementation-search.js +69 -0
  30. package/dist/src/index-coverage-read.js +33 -0
  31. package/dist/src/investigation.js +195 -0
  32. package/dist/src/manager-update.js +9 -10
  33. package/dist/src/mcp-reliability.js +4 -0
  34. package/dist/src/mcp-worker-supervisor.js +122 -6
  35. package/dist/src/mirror.js +2 -0
  36. package/dist/src/progressive-evidence.js +4 -4
  37. package/dist/src/response-budget.js +129 -3
  38. package/dist/src/server.js +18 -5
  39. package/dist/src/shared-index/publisher.js +41 -1
  40. package/dist/src/shared-index/restore.js +16 -6
  41. package/dist/src/storage-budget.js +146 -0
  42. package/dist/src/storage-inventory.js +263 -0
  43. package/dist/src/storage-management.js +204 -0
  44. package/dist/src/storage-policy-contract.js +121 -0
  45. package/dist/src/tools/knodin-tools.js +224 -41
  46. package/dist/src/worktree-seed.js +7 -1
  47. package/docs/BACKUP-RETENTION.md +46 -2
  48. package/docs/CLI.md +98 -2
  49. package/docs/MCP.md +75 -0
  50. package/docs/PROGRESSIVE-EVIDENCE.md +62 -0
  51. package/docs/SALESFORCE-BINDINGS.md +121 -0
  52. package/docs/SALESFORCE-DEAD-CODE.md +45 -0
  53. package/docs/SCOPED-INDEXING.md +76 -0
  54. package/docs/apex-receiver-resolution.md +41 -0
  55. package/docs/releases/0.13.0.md +62 -0
  56. package/docs/releases/0.13.1.md +216 -0
  57. package/docs/structural-only-indexing.md +20 -0
  58. package/package.json +12 -4
@@ -0,0 +1,57 @@
1
+ import { createHash } from "node:crypto";
2
+ import { EMBEDDING_DIMENSIONS } from "./embeddings.js";
3
+ // ~12 MiB of vector payload, plus fixed-size keys/digests and SQLite overhead.
4
+ // A disposable per-graph acceleration table, not another source of graph truth.
5
+ export const EMBEDDING_REUSE_MAX_ENTRIES = 8192;
6
+ // Bump whenever embeddingInputFor/source preparation changes, even if model stays pinned.
7
+ export const EMBEDDING_INPUT_REPRESENTATION = "name-kind-summary-source:1000:v1";
8
+ const VECTOR_BYTES = EMBEDDING_DIMENSIONS * Float32Array.BYTES_PER_ELEMENT;
9
+ function digest(bytes) {
10
+ return createHash("sha256").update(bytes).digest("hex");
11
+ }
12
+ export function embeddingReuseKey(modelIdentity, input, representation = EMBEDDING_INPUT_REPRESENTATION) {
13
+ return digest(JSON.stringify([modelIdentity, representation, input]));
14
+ }
15
+ /** Exact-input cache independent of ephemeral symbol IDs. Stores no source text. */
16
+ export class EmbeddingReuseCache {
17
+ db;
18
+ maxEntries;
19
+ constructor(db, maxEntries = EMBEDDING_REUSE_MAX_ENTRIES) {
20
+ this.db = db;
21
+ this.maxEntries = maxEntries;
22
+ if (!Number.isSafeInteger(maxEntries) ||
23
+ maxEntries < 1 ||
24
+ maxEntries > EMBEDDING_REUSE_MAX_ENTRIES)
25
+ throw new Error("Invalid embedding reuse cache bound");
26
+ db.run(`CREATE TABLE IF NOT EXISTS embedding_reuse_v1 (
27
+ cacheKey TEXT PRIMARY KEY CHECK(length(cacheKey) = 64),
28
+ vector BLOB NOT NULL CHECK(length(vector) = ${VECTOR_BYTES}),
29
+ digest TEXT NOT NULL CHECK(length(digest) = 64)
30
+ )`);
31
+ this.prune();
32
+ }
33
+ get(key) {
34
+ const row = this.db
35
+ .query("SELECT vector, digest FROM embedding_reuse_v1 WHERE cacheKey = ?")
36
+ .get(key);
37
+ if (!row)
38
+ return;
39
+ if (row.vector.byteLength === VECTOR_BYTES && digest(row.vector) === row.digest) {
40
+ const vector = new Float32Array(Uint8Array.from(row.vector).buffer);
41
+ if (vector.every(Number.isFinite))
42
+ return vector;
43
+ }
44
+ this.db.run("DELETE FROM embedding_reuse_v1 WHERE cacheKey = ?", [key]);
45
+ }
46
+ /** Call within the embedding batch transaction; reinsertion refreshes LRU order. */
47
+ put(key, vector) {
48
+ if (vector.length !== EMBEDDING_DIMENSIONS || !vector.every(Number.isFinite))
49
+ return;
50
+ const bytes = new Uint8Array(vector.buffer, vector.byteOffset, vector.byteLength);
51
+ this.db.run("INSERT OR REPLACE INTO embedding_reuse_v1(cacheKey, vector, digest) VALUES (?, ?, ?)", [key, bytes, digest(bytes)]);
52
+ }
53
+ /** Bound all generations together; stale model/input keys compete in the same LRU. */
54
+ prune() {
55
+ this.db.run("DELETE FROM embedding_reuse_v1 WHERE rowid IN (SELECT rowid FROM embedding_reuse_v1 ORDER BY rowid DESC LIMIT -1 OFFSET ?)", [this.maxEntries]);
56
+ }
57
+ }
@@ -9,6 +9,7 @@
9
9
  * and reads as a natural pro-tier feature precisely because enabling it is a
10
10
  * deliberate trade against this file's local/no-egress guarantee.
11
11
  */
12
+ import { randomUUID } from "node:crypto";
12
13
  import os from "node:os";
13
14
  import path from "node:path";
14
15
  let embedderPromise = null;
@@ -20,6 +21,27 @@ export const EMBEDDING_MODEL_ID = "Xenova/all-MiniLM-L6-v2";
20
21
  export const EMBEDDING_MODEL_REVISION = "751bff37182d3f1213fa05d7196b954e230abad9";
21
22
  export const EMBEDDING_MODEL_DTYPE = "q8";
22
23
  const DEFAULT_MODEL_REMOTE_HOST = "https://huggingface.co/";
24
+ const overrideCacheIdentities = new WeakMap();
25
+ /** Pin reuse to the pipeline contract; test overrides never alias the real model. */
26
+ export function getEmbeddingCacheIdentity() {
27
+ if (embedderOverride) {
28
+ let identity = overrideCacheIdentities.get(embedderOverride);
29
+ if (!identity) {
30
+ identity = `override:${randomUUID()}`;
31
+ overrideCacheIdentities.set(embedderOverride, identity);
32
+ }
33
+ return identity;
34
+ }
35
+ return JSON.stringify([
36
+ EMBEDDING_MODEL_ID,
37
+ EMBEDDING_MODEL_REVISION,
38
+ EMBEDDING_MODEL_DTYPE,
39
+ EMBEDDING_DIMENSIONS,
40
+ "mean",
41
+ "normalize:true",
42
+ getModelRemoteHost(),
43
+ ]);
44
+ }
23
45
  /**
24
46
  * One machine-level model cache shared by every repository and every installed
25
47
  * knodin version. An explicit override makes managed/offline environments and
@@ -0,0 +1,215 @@
1
+ import crypto from "node:crypto";
2
+ import path from "node:path";
3
+ import { compareBytes } from "../compare.js";
4
+ export const INDEX_COVERAGE_KEY = "indexCoverageIntent";
5
+ export const INDEX_COVERAGE_PENDING_KEY = "indexCoveragePending";
6
+ export const INDEX_COVERAGE_COMPLETE_KEY = "indexCoverageComplete";
7
+ export function canonicalCoveragePath(value) {
8
+ return (value.length > 0 &&
9
+ value.length <= 4096 &&
10
+ !/[\\\0]/.test(value) &&
11
+ !path.posix.isAbsolute(value) &&
12
+ !/^[A-Za-z]:/.test(value) &&
13
+ path.posix.normalize(value) === value &&
14
+ value !== "." &&
15
+ !value.split("/").includes(".."));
16
+ }
17
+ // Preserve Array.sort's UTF-16 code-unit ordering; locale ordering changes digests.
18
+ const sort = (values) => [...new Set(values)].sort(compareBytes);
19
+ function normalize(intent) {
20
+ if (intent.mode === "full")
21
+ return { ...intent, prefixes: [], files: [] };
22
+ const prefixes = [];
23
+ const accepted = new Set();
24
+ for (const value of sort(intent.prefixes)) {
25
+ let parent = value;
26
+ let covered = false;
27
+ while (parent.includes("/")) {
28
+ parent = parent.slice(0, parent.lastIndexOf("/"));
29
+ if (accepted.has(parent)) {
30
+ covered = true;
31
+ break;
32
+ }
33
+ }
34
+ if (!covered) {
35
+ prefixes.push(value);
36
+ accepted.add(value);
37
+ }
38
+ }
39
+ const prefixIntent = { ...intent, prefixes, files: [] };
40
+ return {
41
+ ...intent,
42
+ prefixes,
43
+ files: sort(intent.files).filter((file) => !coverageContains(prefixIntent, file)),
44
+ };
45
+ }
46
+ export function readIndexCoverage(raw, pending, complete) {
47
+ if (raw === null && pending === null && complete === null)
48
+ return {
49
+ intent: { version: 1, mode: "full", revision: 0, prefixes: [], files: [] },
50
+ origin: "legacy",
51
+ intentState: "unknown",
52
+ };
53
+ try {
54
+ if (raw === null || Buffer.byteLength(raw) > 16_000_000)
55
+ throw new Error("missing or oversized coverage intent");
56
+ const value = JSON.parse(raw);
57
+ if (!value ||
58
+ Object.keys(value).some((key) => !["version", "mode", "revision", "prefixes", "files"].includes(key)) ||
59
+ value.version !== 1 ||
60
+ !["full", "scoped"].includes(value.mode) ||
61
+ !Number.isSafeInteger(value.revision) ||
62
+ value.revision < 1 ||
63
+ !Array.isArray(value.prefixes) ||
64
+ !Array.isArray(value.files) ||
65
+ value.prefixes.length + value.files.length > 100_000 ||
66
+ ![...value.prefixes, ...value.files].every((item) => typeof item === "string" && canonicalCoveragePath(item)) ||
67
+ (value.mode === "full" && (value.prefixes.length > 0 || value.files.length > 0)) ||
68
+ (value.mode === "scoped" && value.prefixes.length + value.files.length === 0))
69
+ throw new Error("invalid coverage intent");
70
+ const normalized = normalize(value);
71
+ if (JSON.stringify(normalized.prefixes) !== JSON.stringify(value.prefixes) ||
72
+ JSON.stringify(normalized.files) !== JSON.stringify(value.files) ||
73
+ (pending !== null && pending !== String(value.revision)) ||
74
+ (complete !== null &&
75
+ (!/^[1-9]\d*$/.test(complete) ||
76
+ !Number.isSafeInteger(Number(complete)) ||
77
+ Number(complete) > value.revision)))
78
+ throw new Error("noncanonical coverage intent");
79
+ return {
80
+ intent: normalized,
81
+ origin: "explicit",
82
+ intentState: pending !== null ? "pending" : complete === String(value.revision) ? "complete" : "unknown",
83
+ };
84
+ }
85
+ catch {
86
+ return { intent: null, origin: "invalid", intentState: "unknown" };
87
+ }
88
+ }
89
+ export function requireIndexCoverage(state) {
90
+ if (!state.intent)
91
+ throw new Error("knodin: malformed index coverage intent; refusing automatic indexing or scope expansion. Restore valid coverage metadata or explicitly rebuild a full candidate.");
92
+ return state.intent;
93
+ }
94
+ const matchers = new WeakMap();
95
+ function matcher(intent) {
96
+ let known = matchers.get(intent);
97
+ if (!known) {
98
+ known = {
99
+ files: new Set(intent.files),
100
+ prefixes: new Set(intent.prefixes),
101
+ parents: new Set(),
102
+ };
103
+ for (const selector of [...intent.files, ...intent.prefixes]) {
104
+ let parent = selector;
105
+ while (parent.includes("/")) {
106
+ parent = parent.slice(0, parent.lastIndexOf("/"));
107
+ known.parents.add(parent);
108
+ }
109
+ }
110
+ matchers.set(intent, known);
111
+ }
112
+ return known;
113
+ }
114
+ export function coverageContains(intent, file) {
115
+ if (!canonicalCoveragePath(file))
116
+ return false;
117
+ if (intent.mode === "full")
118
+ return true;
119
+ const known = matcher(intent);
120
+ if (known.files.has(file))
121
+ return true;
122
+ let prefix = file;
123
+ for (;;) {
124
+ if (known.prefixes.has(prefix))
125
+ return true;
126
+ const end = prefix.lastIndexOf("/");
127
+ if (end < 0)
128
+ return false;
129
+ prefix = prefix.slice(0, end);
130
+ }
131
+ }
132
+ export function coverageMayContain(intent, directory) {
133
+ return (directory === "" ||
134
+ intent.mode === "full" ||
135
+ coverageContains(intent, directory) ||
136
+ matcher(intent).parents.has(directory));
137
+ }
138
+ export function mergeIndexCoverage(previous, requested, fresh) {
139
+ const old = requireIndexCoverage(previous);
140
+ if (requested &&
141
+ (requested.prefixes.length + requested.files.length > 100_000 ||
142
+ ![...requested.prefixes, ...requested.files].every(canonicalCoveragePath)))
143
+ throw new Error("knodin: invalid or oversized requested index coverage selectors");
144
+ const full = requested === null || (!fresh && old.mode === "full");
145
+ const next = normalize({
146
+ version: 1,
147
+ mode: full ? "full" : "scoped",
148
+ revision: Math.max(1, old.revision + 1),
149
+ prefixes: full ? [] : [...(fresh ? [] : old.prefixes), ...(requested?.prefixes ?? [])],
150
+ files: full ? [] : [...(fresh ? [] : old.files), ...(requested?.files ?? [])],
151
+ });
152
+ if (!readIndexCoverage(JSON.stringify(next), String(next.revision), null).intent)
153
+ throw new Error("knodin: merged index coverage exceeds the supported intent bounds");
154
+ if (previous.origin === "explicit" &&
155
+ JSON.stringify({ ...next, revision: old.revision }) === JSON.stringify(old))
156
+ return old;
157
+ return next;
158
+ }
159
+ /** Bound argv size independently of the OS ARG_MAX; every pathspec remains literal. */
160
+ export function coverageGitPathspecBatches(intent) {
161
+ if (intent?.mode !== "scoped")
162
+ return [[]];
163
+ const batches = [];
164
+ let batch = ["--"];
165
+ let bytes = 3;
166
+ for (const selector of [...intent.prefixes, ...intent.files]) {
167
+ const value = `:(literal)${selector}`;
168
+ const size = Buffer.byteLength(value) + 1;
169
+ if (batch.length > 1 && (bytes + size > 32_768 || batch.length > 256)) {
170
+ batches.push(batch);
171
+ batch = ["--"];
172
+ bytes = 3;
173
+ }
174
+ batch.push(value);
175
+ bytes += size;
176
+ }
177
+ if (batch.length > 1)
178
+ batches.push(batch);
179
+ return batches;
180
+ }
181
+ const digests = new WeakMap();
182
+ export function indexCoverageSummary(state) {
183
+ const intent = state.intent;
184
+ const selected = [
185
+ ...(intent?.prefixes ?? []).slice(0, 8).map((value) => ({ kind: "prefix", value })),
186
+ ...(intent?.files ?? [])
187
+ .slice(0, Math.max(0, 8 - (intent?.prefixes.length ?? 0)))
188
+ .map((value) => ({ kind: "file", value })),
189
+ ];
190
+ if (intent && !digests.has(intent))
191
+ digests.set(intent, crypto.createHash("sha256").update(JSON.stringify(intent)).digest("hex"));
192
+ return {
193
+ version: 1,
194
+ mode: intent?.mode ?? "unknown",
195
+ revision: intent?.revision ?? null,
196
+ digest: intent ? (digests.get(intent) ?? null) : null,
197
+ origin: state.origin,
198
+ prefixCount: intent?.prefixes.length ?? 0,
199
+ fileCount: intent?.files.length ?? 0,
200
+ selectorPreview: {
201
+ prefixes: selected.filter((x) => x.kind === "prefix").map((x) => x.value.slice(0, 160)),
202
+ files: selected.filter((x) => x.kind === "file").map((x) => x.value.slice(0, 160)),
203
+ truncated: !!intent &&
204
+ (intent.prefixes.length + intent.files.length > 8 ||
205
+ selected.some((x) => x.value.length > 160)),
206
+ },
207
+ intentState: state.intentState,
208
+ repositoryComplete: intent?.mode === "full" && state.intentState === "complete",
209
+ negativeScope: !intent || state.intentState !== "complete"
210
+ ? "unknown"
211
+ : intent.mode === "full"
212
+ ? "repository"
213
+ : "declared-scope",
214
+ };
215
+ }