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,213 @@
1
+ import childProcess from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ const MANAGED_MARKERS = ["KNODIN MANAGED HOOK", "RECKON-GRAPH MANAGED HOOK"];
5
+ const HOOK_NAMES = ["post-commit", "post-checkout", "post-merge", "post-rewrite"];
6
+ const HOOK_FAILURE_FILE = "last-refresh-failure";
7
+ function readEvent(target) {
8
+ try {
9
+ const [kind = "", legacyBefore = "", legacyAfter = "", ...rest] = fs
10
+ .readFileSync(target, "utf-8")
11
+ .split(/\r?\n/);
12
+ if (!kind)
13
+ return null;
14
+ const metadata = new Map([legacyBefore, legacyAfter, ...rest]
15
+ .map((line) => {
16
+ const separator = line.indexOf("=");
17
+ return separator < 0 ? null : [line.slice(0, separator), line.slice(separator + 1)];
18
+ })
19
+ .filter((entry) => entry !== null));
20
+ return {
21
+ sequence: metadata.get("sequence") || null,
22
+ kind,
23
+ before: metadata.get("before") ||
24
+ (kind === "commit" || kind === "checkout" || kind === "merge" ? legacyBefore : "") ||
25
+ null,
26
+ after: metadata.get("after") ||
27
+ (kind === "commit" || kind === "checkout" || kind === "merge" ? legacyAfter : "") ||
28
+ null,
29
+ worktree: metadata.get("worktree") || null,
30
+ at: metadata.get("at") || fs.statSync(target).mtime.toISOString(),
31
+ };
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ function readLastEvent(repo) {
38
+ return readEvent(path.join(repo, ".reckon", "hooks", "last-refresh-event"));
39
+ }
40
+ function readPendingEvent(repo) {
41
+ try {
42
+ const directory = path.join(repo, ".reckon", "hooks", "events");
43
+ const next = fs
44
+ .readdirSync(directory)
45
+ .filter((entry) => entry.endsWith(".event"))
46
+ .sort((left, right) => left.localeCompare(right))[0];
47
+ return next ? readEvent(path.join(directory, next)) : null;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ function processorState(repo) {
54
+ const pidPath = path.join(repo, ".reckon", "hooks", "refresh.lock", "pid");
55
+ if (!fs.existsSync(path.dirname(pidPath)))
56
+ return { state: "idle", pid: null };
57
+ let pid = null;
58
+ try {
59
+ const candidate = Number(fs.readFileSync(pidPath, "utf-8"));
60
+ if (Number.isInteger(candidate) && candidate > 0)
61
+ pid = candidate;
62
+ }
63
+ catch {
64
+ return { state: "stale-lock", pid: null };
65
+ }
66
+ if (pid === null)
67
+ return { state: "stale-lock", pid: null };
68
+ try {
69
+ process.kill(pid, 0);
70
+ return { state: "running", pid };
71
+ }
72
+ catch (error) {
73
+ if (error.code === "EPERM")
74
+ return { state: "running", pid };
75
+ return { state: "stale-lock", pid };
76
+ }
77
+ }
78
+ function queuedEventCount(repo) {
79
+ try {
80
+ return fs
81
+ .readdirSync(path.join(repo, ".reckon", "hooks", "events"))
82
+ .filter((entry) => entry.endsWith(".event")).length;
83
+ }
84
+ catch {
85
+ return 0;
86
+ }
87
+ }
88
+ function activeHooksDirectory(repo) {
89
+ const output = childProcess.execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-path", "hooks"], { cwd: repo, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
90
+ return output.trim();
91
+ }
92
+ function inspectManagedHooks(hooksDirectory) {
93
+ const activeHooks = [];
94
+ const missingHooks = [];
95
+ for (const hookName of HOOK_NAMES) {
96
+ try {
97
+ const hookPath = path.join(hooksDirectory, hookName);
98
+ const content = fs.readFileSync(hookPath, "utf-8");
99
+ const mode = fs.statSync(hookPath).mode;
100
+ const active = (mode & 0o111) !== 0 &&
101
+ MANAGED_MARKERS.some((marker) => content.startsWith(`#!/bin/sh\n# ${marker}: ${hookName}\n`));
102
+ (active ? activeHooks : missingHooks).push(hookName);
103
+ }
104
+ catch {
105
+ missingHooks.push(hookName);
106
+ }
107
+ }
108
+ return { activeHooks, missingHooks };
109
+ }
110
+ function readRefreshFailure(repo) {
111
+ const failurePath = path.join(repo, ".reckon", "hooks", HOOK_FAILURE_FILE);
112
+ if (!fs.existsSync(failurePath))
113
+ return null;
114
+ try {
115
+ return (fs.readFileSync(failurePath, "utf-8").split(/\r?\n/, 1)[0]?.trim() ||
116
+ "last background refresh failed");
117
+ }
118
+ catch {
119
+ return "last background refresh failed";
120
+ }
121
+ }
122
+ /** Read-only proof that committed Git activity can still reach knodin's indexer. */
123
+ export function inspectLifecycleHealth(repoPath) {
124
+ const repo = path.resolve(repoPath);
125
+ if (!fs.existsSync(path.join(repo, ".reckon", "db.sqlite"))) {
126
+ return {
127
+ status: "unconfigured",
128
+ refreshCapable: false,
129
+ hooksDirectory: null,
130
+ activeHooks: [],
131
+ missingHooks: [...HOOK_NAMES],
132
+ issues: ["local graph database is not initialized"],
133
+ queuedEvents: 0,
134
+ processor: { state: "idle", pid: null },
135
+ pendingEvent: null,
136
+ lastEvent: null,
137
+ lastError: null,
138
+ };
139
+ }
140
+ let hooksDirectory;
141
+ try {
142
+ hooksDirectory = activeHooksDirectory(repo);
143
+ }
144
+ catch {
145
+ return {
146
+ status: "degraded",
147
+ refreshCapable: false,
148
+ hooksDirectory: null,
149
+ activeHooks: [],
150
+ missingHooks: [...HOOK_NAMES],
151
+ issues: ["active Git hooks directory could not be resolved"],
152
+ queuedEvents: queuedEventCount(repo),
153
+ processor: processorState(repo),
154
+ pendingEvent: readPendingEvent(repo),
155
+ lastEvent: readLastEvent(repo),
156
+ lastError: null,
157
+ };
158
+ }
159
+ const { activeHooks, missingHooks } = inspectManagedHooks(hooksDirectory);
160
+ const background = path.join(repo, ".reckon", "hooks", "background-index.sh");
161
+ const backgroundReady = fs.existsSync(background) && (fs.statSync(background).mode & 0o111) !== 0;
162
+ const issues = missingHooks.map((hook) => `${hook} no longer routes through knodin's active Git hook path`);
163
+ if (!backgroundReady)
164
+ issues.push("background indexer is missing or not executable");
165
+ const lastError = readRefreshFailure(repo);
166
+ if (lastError)
167
+ issues.push(`${lastError} See .reckon/indexer.log for details`);
168
+ const refreshCapable = missingHooks.length === 0 && backgroundReady && issues.length === 0;
169
+ return {
170
+ status: refreshCapable ? "healthy" : "degraded",
171
+ refreshCapable,
172
+ hooksDirectory,
173
+ activeHooks,
174
+ missingHooks,
175
+ issues,
176
+ queuedEvents: queuedEventCount(repo),
177
+ processor: processorState(repo),
178
+ pendingEvent: readPendingEvent(repo),
179
+ lastEvent: readLastEvent(repo),
180
+ lastError,
181
+ };
182
+ }
183
+ /** Merge graph and refresh-path health without reporting absent evidence as healthy. */
184
+ export function attachLifecycleHealth(repo, graph) {
185
+ const lifecycle = inspectLifecycleHealth(repo);
186
+ if (lifecycle.queuedEvents > 0 &&
187
+ graph.status !== "repair-needed" &&
188
+ graph.status !== "indexing" &&
189
+ graph.freshness) {
190
+ return {
191
+ ...graph,
192
+ status: "stale",
193
+ freshness: { ...graph.freshness, state: "queued" },
194
+ lifecycle,
195
+ };
196
+ }
197
+ if (graph.status !== "healthy" || lifecycle.status !== "degraded")
198
+ return { ...graph, lifecycle };
199
+ return {
200
+ ...graph,
201
+ status: "repair-needed",
202
+ missing: {
203
+ files: [...graph.missing.files],
204
+ records: [...graph.missing.records, ...lifecycle.issues],
205
+ },
206
+ repairSteps: [
207
+ ...graph.repairSteps,
208
+ "Run `knodin init` to restore managed lifecycle hooks.",
209
+ "Run `knodin status` again to verify graph and refresh-path health.",
210
+ ],
211
+ lifecycle,
212
+ };
213
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Read-only local language-service adapters.
3
+ *
4
+ * This deliberately embeds no daemon and never invokes workspace commands. The
5
+ * first adapter is TypeScript's compiler language service, which implements the
6
+ * same source-position operations exposed by an LSP server. Other languages are
7
+ * explicitly unavailable until a similarly local, read-only adapter is added.
8
+ */
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ const TYPESCRIPT_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"]);
12
+ function unavailable(pattern, target, reason) {
13
+ return {
14
+ pattern,
15
+ target,
16
+ count: 0,
17
+ results: [],
18
+ adapter: "none",
19
+ available: false,
20
+ readOnly: true,
21
+ reason,
22
+ };
23
+ }
24
+ function parseTarget(repoPath, target) {
25
+ const match = target.match(/^(.*?)(?::(\d+):(\d+))?$/);
26
+ if (!match?.[1])
27
+ return undefined;
28
+ const file = match[1].replaceAll("\\", "/");
29
+ const absoluteFile = path.resolve(repoPath, file);
30
+ const relative = path.relative(repoPath, absoluteFile);
31
+ if (relative.startsWith("..") || path.isAbsolute(relative))
32
+ return undefined;
33
+ if ((match[2] === undefined) !== (match[3] === undefined))
34
+ return undefined;
35
+ return {
36
+ file: relative.replaceAll(path.sep, "/"),
37
+ absoluteFile,
38
+ ...(match[2] ? { line: Number(match[2]), column: Number(match[3]) } : {}),
39
+ };
40
+ }
41
+ function filesForService(repoPath, ts) {
42
+ const isInsideRepo = (file) => {
43
+ try {
44
+ const relativePath = path.relative(repoPath, fs.realpathSync(file));
45
+ return !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ };
51
+ const config = ts.findConfigFile(repoPath, ts.sys.fileExists, "tsconfig.json");
52
+ if (config) {
53
+ const loaded = ts.readConfigFile(config, ts.sys.readFile);
54
+ if (!loaded.error) {
55
+ const parsed = ts.parseJsonConfigFileContent(loaded.config, ts.sys, path.dirname(config));
56
+ return parsed.fileNames.filter((file) => TYPESCRIPT_EXTENSIONS.has(path.extname(file)) && isInsideRepo(file));
57
+ }
58
+ }
59
+ const files = [];
60
+ const visit = (directory) => {
61
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
62
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".reckon")
63
+ continue;
64
+ const candidate = path.join(directory, entry.name);
65
+ if (entry.isDirectory())
66
+ visit(candidate);
67
+ else if (entry.isFile() && TYPESCRIPT_EXTENSIONS.has(path.extname(entry.name)))
68
+ files.push(candidate);
69
+ }
70
+ };
71
+ visit(repoPath);
72
+ return files;
73
+ }
74
+ function createService(repoPath, files, ts) {
75
+ const versions = new Map(files.map((file) => [file, "0"]));
76
+ const options = {
77
+ target: ts.ScriptTarget.ES2022,
78
+ module: ts.ModuleKind.NodeNext,
79
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
80
+ strict: true,
81
+ skipLibCheck: true,
82
+ };
83
+ return ts.createLanguageService({
84
+ getCompilationSettings: () => options,
85
+ getScriptFileNames: () => files,
86
+ getScriptVersion: (file) => versions.get(file) ?? "0",
87
+ getScriptSnapshot: (file) => {
88
+ try {
89
+ return ts.ScriptSnapshot.fromString(fs.readFileSync(file, "utf-8"));
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ },
95
+ getCurrentDirectory: () => repoPath,
96
+ getDefaultLibFileName: (settings) => ts.getDefaultLibFilePath(settings),
97
+ fileExists: ts.sys.fileExists,
98
+ readFile: ts.sys.readFile,
99
+ readDirectory: ts.sys.readDirectory,
100
+ directoryExists: ts.sys.directoryExists,
101
+ getDirectories: ts.sys.getDirectories,
102
+ });
103
+ }
104
+ function relative(repoPath, file) {
105
+ return path.relative(repoPath, file).replaceAll(path.sep, "/");
106
+ }
107
+ function positionFor(source, line, column) {
108
+ if (!Number.isInteger(line) || !Number.isInteger(column) || line < 1 || column < 1)
109
+ return undefined;
110
+ const lines = source.split("\n");
111
+ if (line > lines.length || column > lines[line - 1].length + 1)
112
+ return undefined;
113
+ return (lines.slice(0, line - 1).reduce((offset, current) => offset + current.length + 1, 0) +
114
+ column -
115
+ 1);
116
+ }
117
+ function diagnosticRow(repoPath, diagnostic, ts) {
118
+ const file = diagnostic.file;
119
+ const location = file && diagnostic.start !== undefined
120
+ ? file.getLineAndCharacterOfPosition(diagnostic.start)
121
+ : undefined;
122
+ return {
123
+ file: file ? relative(repoPath, file.fileName) : undefined,
124
+ line: location ? location.line + 1 : undefined,
125
+ column: location ? location.character + 1 : undefined,
126
+ kind: ts.DiagnosticCategory[diagnostic.category].toLowerCase(),
127
+ message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
128
+ code: diagnostic.code,
129
+ };
130
+ }
131
+ function navigationRows(repoPath, entries, ts) {
132
+ return (entries ?? []).map((entry) => {
133
+ const source = fs.readFileSync(entry.fileName, "utf-8");
134
+ const location = ts
135
+ .createSourceFile(entry.fileName, source, ts.ScriptTarget.Latest)
136
+ .getLineAndCharacterOfPosition(entry.textSpan.start);
137
+ return {
138
+ file: relative(repoPath, entry.fileName),
139
+ line: location.line + 1,
140
+ column: location.character + 1,
141
+ kind: entry.kind,
142
+ symbol: "name" in entry ? entry.name : undefined,
143
+ };
144
+ });
145
+ }
146
+ /** Executes a bounded, local compiler-language-service query without writes. */
147
+ export async function runReadOnlyLspQuery(pattern, target, repoPath) {
148
+ let ts;
149
+ try {
150
+ ts = await import("typescript");
151
+ }
152
+ catch {
153
+ return unavailable(pattern, target, "No local TypeScript language-service adapter is installed; install TypeScript to enable this read-only query.");
154
+ }
155
+ let canonicalRepoPath;
156
+ try {
157
+ canonicalRepoPath = fs.realpathSync(repoPath);
158
+ }
159
+ catch {
160
+ return unavailable(pattern, target, "Repository path must be an accessible directory.");
161
+ }
162
+ const parsed = parseTarget(repoPath, target);
163
+ if (!parsed || !fs.existsSync(parsed.absoluteFile))
164
+ return unavailable(pattern, target, "Target must be an existing repo-relative file, optionally with :line:column.");
165
+ if (!TYPESCRIPT_EXTENSIONS.has(path.extname(parsed.absoluteFile)))
166
+ return unavailable(pattern, target, `No local LSP adapter is installed for ${path.extname(parsed.absoluteFile) || "this file type"}.`);
167
+ let realTarget;
168
+ try {
169
+ realTarget = fs.realpathSync(parsed.absoluteFile);
170
+ const relativeTarget = path.relative(canonicalRepoPath, realTarget);
171
+ if (relativeTarget.startsWith("..") || path.isAbsolute(relativeTarget))
172
+ return unavailable(pattern, target, "Target must resolve inside the repository.");
173
+ }
174
+ catch {
175
+ return unavailable(pattern, target, "Target must be a readable regular file.");
176
+ }
177
+ if (pattern !== "lsp_diagnostics" && (parsed.line === undefined || parsed.column === undefined))
178
+ return unavailable(pattern, target, "Navigation queries require target syntax file:line:column.");
179
+ const files = filesForService(canonicalRepoPath, ts);
180
+ if (!files.includes(realTarget))
181
+ files.push(realTarget);
182
+ const service = createService(canonicalRepoPath, files, ts);
183
+ try {
184
+ let rows;
185
+ if (pattern === "lsp_diagnostics") {
186
+ rows = [
187
+ ...service.getSyntacticDiagnostics(realTarget),
188
+ ...service.getSemanticDiagnostics(realTarget),
189
+ ].map((diagnostic) => diagnosticRow(canonicalRepoPath, diagnostic, ts));
190
+ }
191
+ else {
192
+ const line = parsed.line;
193
+ const column = parsed.column;
194
+ if (line === undefined || column === undefined)
195
+ return unavailable(pattern, target, "Navigation queries require target syntax file:line:column.");
196
+ const position = positionFor(fs.readFileSync(realTarget, "utf-8"), line, column);
197
+ if (position === undefined)
198
+ return unavailable(pattern, target, "Target line and column must identify a position inside the source file.");
199
+ rows = navigationRows(canonicalRepoPath, pattern === "lsp_implementations"
200
+ ? service.getImplementationAtPosition(realTarget, position)
201
+ : service.getDefinitionAtPosition(realTarget, position), ts);
202
+ }
203
+ return {
204
+ pattern,
205
+ target,
206
+ count: rows.length,
207
+ results: rows.slice(0, 100),
208
+ hasMore: rows.length > 100 || undefined,
209
+ adapter: "typescript-language-service",
210
+ available: true,
211
+ readOnly: true,
212
+ };
213
+ }
214
+ finally {
215
+ service.dispose();
216
+ }
217
+ }