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,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
+ }