knodin 0.7.6 → 0.8.3

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 (134) hide show
  1. package/README.md +41 -12
  2. package/benchmarks/competitors/SYNTHESIS.md +66 -0
  3. package/dist/bin/cli.js +2181 -108
  4. package/dist/bin/launcher.js +25 -3
  5. package/dist/src/agent-integration.js +304 -0
  6. package/dist/src/artifact-refresh.js +82 -0
  7. package/dist/src/cli-args.js +292 -0
  8. package/dist/src/cli-model.js +384 -0
  9. package/dist/src/codeflow-replay.js +81 -0
  10. package/dist/src/compact-structural.js +96 -0
  11. package/dist/src/compare.js +39 -0
  12. package/dist/src/competitive-cold-mcp.js +40 -0
  13. package/dist/src/competitive-constraints.js +21 -0
  14. package/dist/src/competitive-manifest.js +411 -0
  15. package/dist/src/competitive-measurement.js +183 -0
  16. package/dist/src/competitive-runner.js +487 -0
  17. package/dist/src/competitive-sandbox.js +108 -0
  18. package/dist/src/context-export.js +423 -0
  19. package/dist/src/context.js +102 -0
  20. package/dist/src/deterministic-random.js +34 -0
  21. package/dist/src/diagnostics-write-helper.js +473 -0
  22. package/dist/src/diagnostics.js +1476 -0
  23. package/dist/src/docs-sections.js +142 -0
  24. package/dist/src/doctor.js +382 -0
  25. package/dist/src/engine/ann-hnsw.js +261 -0
  26. package/dist/src/engine/embeddings.js +193 -0
  27. package/dist/src/engine/file-walker.js +49 -0
  28. package/dist/src/engine/git-history.js +289 -0
  29. package/dist/src/engine/index.js +15094 -0
  30. package/dist/src/engine/perf.js +115 -0
  31. package/dist/src/engine/prune.js +112 -0
  32. package/dist/src/engine/sarif-import.js +341 -0
  33. package/dist/src/engine/scip-import.js +423 -0
  34. package/dist/src/engine/source-policy.js +85 -0
  35. package/dist/src/engine/sqlite.js +71 -0
  36. package/dist/src/engine/state-paths.js +175 -0
  37. package/dist/src/engine/symbol-delete.js +58 -0
  38. package/dist/src/execution-profile.js +208 -0
  39. package/dist/src/failure-diagnosis.js +655 -0
  40. package/dist/src/fleet.js +7 -0
  41. package/dist/src/git-executable.js +31 -0
  42. package/dist/src/graph-layout.js +173 -0
  43. package/dist/src/graph-query-health.js +115 -0
  44. package/dist/src/hook-manager-integration.js +156 -0
  45. package/dist/src/index-activity.js +126 -0
  46. package/dist/src/init-progress-worker.js +70 -2
  47. package/dist/src/init-progress.js +155 -0
  48. package/dist/src/init.js +1295 -0
  49. package/dist/src/lifecycle-health.js +282 -0
  50. package/dist/src/lsp-readonly.js +217 -0
  51. package/dist/src/mcp-graph-worker.js +69 -0
  52. package/dist/src/mcp-reliability.js +154 -0
  53. package/dist/src/mcp-worker-supervisor.js +350 -0
  54. package/dist/src/mirror.js +290 -0
  55. package/dist/src/node-runtime.js +157 -0
  56. package/dist/src/output-compression.js +630 -0
  57. package/dist/src/output-telemetry.js +368 -0
  58. package/dist/src/pr-triage.js +638 -0
  59. package/dist/src/progress-worker-runtime.js +46 -0
  60. package/dist/src/progressive-evidence.js +477 -0
  61. package/dist/src/pure-compression-cli.js +102 -0
  62. package/dist/src/relationship-adapters.js +377 -0
  63. package/dist/src/release-attestation.js +533 -0
  64. package/dist/src/release-preflight.js +513 -0
  65. package/dist/src/repair-lease.js +85 -0
  66. package/dist/src/repair-progress-worker.js +83 -2
  67. package/dist/src/repair-progress.js +262 -0
  68. package/dist/src/repository-init-process.js +177 -0
  69. package/dist/src/repository-management.js +1261 -0
  70. package/dist/src/resource-reachability.js +456 -0
  71. package/dist/src/response-budget.js +200 -0
  72. package/dist/src/server.js +217 -0
  73. package/dist/src/structural-fast-path.js +344 -0
  74. package/dist/src/structural-snapshot.js +37 -0
  75. package/dist/src/system-config.js +638 -0
  76. package/dist/src/terminal-help.js +83 -0
  77. package/dist/src/tools/knodin-tools.js +1645 -0
  78. package/dist/src/update-ceremony.js +162 -0
  79. package/dist/src/update-policy.js +944 -0
  80. package/dist/src/update-trust.js +504 -0
  81. package/dist/src/version.js +13 -0
  82. package/dist/src/visualization.js +515 -0
  83. package/dist/src/wait-for-fresh.js +98 -0
  84. package/dist/src/worktree-lifecycle.js +234 -0
  85. package/docs/BEHAVIORAL-CONTRACT.md +114 -0
  86. package/docs/CLI.md +30 -1
  87. package/docs/COMPARISON.md +413 -0
  88. package/docs/COMPETITIVE-LANDSCAPE-2026-08.md +267 -0
  89. package/docs/CONTAINED-EXECUTION.md +77 -0
  90. package/docs/DEMO.md +49 -0
  91. package/docs/DIAGNOSTICS.md +80 -0
  92. package/docs/GIT-HISTORY-REVIEW.md +39 -0
  93. package/docs/HANDOFF.md +180 -0
  94. package/docs/INSTALLATION.md +21 -18
  95. package/docs/MCP.md +64 -8
  96. package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
  97. package/docs/PT-ACCESS-RECOMMENDATION.md +89 -0
  98. package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
  99. package/docs/REPOSITORIES-AND-WORKTREES.md +18 -6
  100. package/docs/SCIP-IMPORT.md +62 -0
  101. package/docs/SIGNED-UPDATES.md +151 -0
  102. package/docs/TELEMETRY.md +46 -0
  103. package/docs/TOKEN-OPTIMIZER-SCORECARD.md +79 -0
  104. package/docs/assets/knodin-favicon.svg +4 -0
  105. package/docs/releases/0.3.0.md +46 -0
  106. package/docs/releases/0.4.0.md +68 -0
  107. package/docs/releases/0.4.1.md +28 -0
  108. package/docs/releases/0.4.2.md +27 -0
  109. package/docs/releases/0.4.3.md +23 -0
  110. package/docs/releases/0.5.0.md +29 -0
  111. package/docs/releases/0.5.1.md +17 -0
  112. package/docs/releases/0.6.0.md +18 -0
  113. package/docs/releases/0.7.0.md +24 -0
  114. package/docs/releases/0.7.1.md +21 -0
  115. package/docs/releases/0.7.2.md +21 -0
  116. package/docs/releases/0.7.3.md +23 -0
  117. package/docs/releases/0.7.4.md +17 -0
  118. package/docs/releases/0.7.5.md +20 -0
  119. package/docs/releases/0.8.0.md +74 -0
  120. package/docs/releases/0.8.2.md +34 -0
  121. package/docs/releases/0.8.3.md +47 -0
  122. package/package.json +139 -4
  123. package/roadmap/competitive-roadmap.md +3896 -0
  124. package/schemas/release-attestation-v1.schema.json +210 -0
  125. package/schemas/support-bundle-v2.schema.json +212 -0
  126. package/dist/chunks/chunk-DMQAGX77.js +0 -654
  127. package/dist/chunks/chunk-F4Z3Z766.js +0 -4
  128. package/dist/chunks/chunk-SIJAQVSX.js +0 -3
  129. package/dist/chunks/chunk-X6M4HUUE.js +0 -2
  130. package/dist/chunks/chunk-YPRMY2LP.js +0 -8
  131. package/dist/chunks/pure-compression-cli-4TA2TQD5.js +0 -5
  132. package/dist/chunks/server-7EDF4CBY.js +0 -14
  133. package/dist/chunks/structural-fast-path-KD5KQSPX.js +0 -4
  134. package/docs/releases/0.7.6.md +0 -25
@@ -0,0 +1,217 @@
1
+ /**
2
+ * knodin MCP server (stdio).
3
+ *
4
+ * Registration uses the low-level request-handler API — NOT the high-level
5
+ * `server.tool()` overload — so `tsc` never pays the Zod→handler-arg inference
6
+ * cost that OOMs large tool sets. Gateway `inputSchema`s are hand-written JSON
7
+ * Schema literals; handler args are narrowed manually inside each dispatcher.
8
+ */
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
14
+ import { recordDiagnosticFailure } from "./diagnostics.js";
15
+ import { assertMcpMemoryHeadroom, classifyMcpFailure, createMcpRequestContext, recordMcpLifecycle, superviseMcpRequest, } from "./mcp-reliability.js";
16
+ import { WorkerSupervisor } from "./mcp-worker-supervisor.js";
17
+ import { attachMcpRequestSignal, getKnodinTools, handleKnodinTool } from "./tools/knodin-tools.js";
18
+ import { KNODIN_VERSION } from "./version.js";
19
+ const RETRY_SAFE_OPERATIONS = new Set([
20
+ "architecture_overview",
21
+ "context",
22
+ "docs",
23
+ "explain",
24
+ "map",
25
+ "query",
26
+ "review",
27
+ "search",
28
+ "status",
29
+ ]);
30
+ const RECOVERY_LANE_OPERATIONS = new Set(["docs"]);
31
+ class SafeGatewayScheduler {
32
+ tail = Promise.resolve();
33
+ async run(operation, signal, task) {
34
+ if (RECOVERY_LANE_OPERATIONS.has(operation))
35
+ return task();
36
+ const predecessor = this.tail;
37
+ let release;
38
+ this.tail = new Promise((resolve) => {
39
+ release = resolve;
40
+ });
41
+ try {
42
+ await predecessor;
43
+ if (signal.aborted)
44
+ throw Object.assign(new Error("MCP request cancelled before dispatch"), {
45
+ code: "KNODIN_REQUEST_CANCELLED",
46
+ });
47
+ return await task();
48
+ }
49
+ finally {
50
+ release();
51
+ }
52
+ }
53
+ }
54
+ function ignoreNotificationFailure() {
55
+ // Progress is advisory; a client that stops accepting it must not fail the tool call.
56
+ }
57
+ function startHeartbeat(notify, operation, startedAt) {
58
+ return setInterval(() => {
59
+ void notify(`${operation} still running (${Math.round((Date.now() - startedAt) / 1000)}s)`).catch(ignoreNotificationFailure);
60
+ }, 10_000);
61
+ }
62
+ export function recordMcpDiagnosticFailure(args, error, defaultRepo = process.cwd()) {
63
+ const effectiveRepo = typeof args?.repoPath === "string" ? args.repoPath : defaultRepo;
64
+ const diagnostic = recordDiagnosticFailure(effectiveRepo, {
65
+ surface: "mcp",
66
+ operation: typeof args?.operation === "string" ? args.operation : "unknown",
67
+ phase: "dispatch",
68
+ error,
69
+ });
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ return diagnostic.recorded
72
+ ? new Error(`${message} [diagnostic ${diagnostic.correlationId}]`, { cause: error })
73
+ : error;
74
+ }
75
+ export function createServer(dispatch = handleKnodinTool) {
76
+ const scheduler = new SafeGatewayScheduler();
77
+ const workers = new WorkerSupervisor();
78
+ const server = new Server({ name: "knodin", version: KNODIN_VERSION }, {
79
+ capabilities: { tools: {} },
80
+ instructions: "Use knodin first for cold or unfamiliar codebase work: context to orient, explain for source-evidenced symbol context, query impact before meaningful edits, and review before handoff. Prefer direct reads for exact literals, non-code files, or files just edited this turn. If status reports repair-needed, follow its typed repair steps: repair graph damage or init displaced lifecycle routing before relying on graph evidence.",
81
+ });
82
+ server.onclose = () => {
83
+ void workers.close();
84
+ };
85
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
86
+ tools: getKnodinTools(),
87
+ }));
88
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
89
+ const { name, arguments: args } = request.params;
90
+ if (name !== "knodin") {
91
+ throw new Error(`unknown tool: ${name}`);
92
+ }
93
+ const values = args;
94
+ const operation = typeof values?.operation === "string" ? values.operation : "unknown";
95
+ const progressToken = extra._meta?.progressToken;
96
+ const requestId = String(extra.requestId);
97
+ const startedAt = Date.now();
98
+ const repoPath = typeof values?.repoPath === "string" ? values.repoPath : process.cwd();
99
+ const context = createMcpRequestContext(requestId, operation, repoPath);
100
+ let progressSequence = 0;
101
+ let heartbeat;
102
+ recordMcpLifecycle(context, "start");
103
+ try {
104
+ assertMcpMemoryHeadroom();
105
+ const supervised = dispatch === handleKnodinTool && operation !== "docs";
106
+ const execution = supervised
107
+ ? await workers.worker(repoPath).execute(context, args, extra.signal, (sequence, phase) => {
108
+ progressSequence = sequence;
109
+ recordMcpLifecycle(context, "progress", { sequence });
110
+ if (progressToken !== undefined)
111
+ void extra
112
+ .sendNotification({
113
+ method: "notifications/progress",
114
+ params: { progressToken, progress: sequence, message: phase },
115
+ })
116
+ .catch(ignoreNotificationFailure);
117
+ })
118
+ : {
119
+ result: await scheduler.run(operation, extra.signal, () => superviseMcpRequest(context, extra.signal, async (operationSignal) => {
120
+ const notify = async (message) => {
121
+ if (progressToken === undefined)
122
+ return;
123
+ progressSequence++;
124
+ recordMcpLifecycle(context, "progress", { sequence: progressSequence });
125
+ await extra.sendNotification({
126
+ method: "notifications/progress",
127
+ params: {
128
+ progressToken,
129
+ progress: progressSequence,
130
+ message,
131
+ },
132
+ });
133
+ };
134
+ await notify(`Starting ${operation} (deadline ${context.deadlineMs}ms)`);
135
+ heartbeat = startHeartbeat(notify, operation, startedAt);
136
+ // Keep the production handler explicit: knodin's own source-evidence
137
+ // extractor maps this low-level MCP registration to its dispatcher.
138
+ if (dispatch === handleKnodinTool) {
139
+ attachMcpRequestSignal(args, operationSignal);
140
+ return await handleKnodinTool(args);
141
+ }
142
+ return await dispatch(args, { signal: operationSignal });
143
+ }, { cooperativeCancellation: operation === "repair" })),
144
+ };
145
+ const result = execution.result;
146
+ recordMcpLifecycle(context, "success", { elapsedMs: Date.now() - startedAt });
147
+ return {
148
+ _meta: {
149
+ requestId: context.requestId,
150
+ traceId: context.traceId,
151
+ ...("predecessorTraceId" in execution
152
+ ? { predecessorTraceId: execution.predecessorTraceId }
153
+ : {}),
154
+ },
155
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
156
+ };
157
+ }
158
+ catch (error) {
159
+ const failure = classifyMcpFailure(error);
160
+ recordMcpLifecycle(context, "failure", {
161
+ kind: failure.kind,
162
+ elapsedMs: Date.now() - startedAt,
163
+ });
164
+ const recorded = recordMcpDiagnosticFailure(values, error);
165
+ const message = recorded instanceof Error ? recorded.message : String(recorded);
166
+ const retrySafe = RETRY_SAFE_OPERATIONS.has(operation);
167
+ return {
168
+ isError: true,
169
+ _meta: { requestId: context.requestId, traceId: context.traceId },
170
+ content: [
171
+ {
172
+ type: "text",
173
+ text: JSON.stringify({
174
+ code: failure.code,
175
+ kind: failure.kind,
176
+ message,
177
+ traceId: context.traceId,
178
+ requestId,
179
+ operation,
180
+ phase: "dispatch",
181
+ deadlineMs: context.deadlineMs,
182
+ retrySafe,
183
+ diagnosticLog: ".knodin/diagnostics/events.jsonl (only when opt-in diagnostics are enabled)",
184
+ recovery: retrySafe
185
+ ? "Retry once. If the transport closes, restart the MCP client and run `knodin doctor`; include `knodin diagnostics export` when diagnostics are enabled."
186
+ : "Do not retry automatically. Run `knodin doctor`, inspect repository state, and include `knodin diagnostics export` when diagnostics are enabled.",
187
+ }, null, 2),
188
+ },
189
+ ],
190
+ };
191
+ }
192
+ finally {
193
+ if (heartbeat)
194
+ clearInterval(heartbeat);
195
+ recordMcpLifecycle(context, "cleanup", { elapsedMs: Date.now() - startedAt });
196
+ }
197
+ });
198
+ return server;
199
+ }
200
+ async function main() {
201
+ const server = createServer();
202
+ const transport = new StdioServerTransport();
203
+ await server.connect(transport);
204
+ // stdio server: log to stderr so stdout stays a clean JSON-RPC channel.
205
+ console.error("knodin MCP server running on stdio");
206
+ }
207
+ /** Start the stdio gateway from either the direct server entry or the packaged CLI. */
208
+ export async function startServer() {
209
+ await main();
210
+ }
211
+ // Run only when invoked directly (not when imported by tests).
212
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
213
+ startServer().catch((err) => {
214
+ console.error(err);
215
+ process.exit(1);
216
+ });
217
+ }
@@ -0,0 +1,344 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { compareBytes } from "./compare.js";
4
+ import { contentFingerprint, readStructuralSnapshot, } from "./structural-snapshot.js";
5
+ function exactFile(repo, file) {
6
+ try {
7
+ return (contentFingerprint(fs.readFileSync(path.resolve(repo, file.path))) === file.contentFingerprint);
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ const DIRECT_FILE_EXTENSIONS = new Set([
14
+ ".cjs",
15
+ ".cs",
16
+ ".cts",
17
+ ".go",
18
+ ".java",
19
+ ".js",
20
+ ".jsx",
21
+ ".mjs",
22
+ ".mts",
23
+ ".py",
24
+ ".rs",
25
+ ".ts",
26
+ ".tsx",
27
+ ]);
28
+ function lexicalEndLine(lines, startIndex, python) {
29
+ if (python) {
30
+ const indent = /^\s*/.exec(lines[startIndex])?.[0].length ?? 0;
31
+ let end = startIndex + 1;
32
+ for (let index = startIndex + 1; index < lines.length; index++) {
33
+ const line = lines[index];
34
+ if (line.trim() && (/^\s*/.exec(line)?.[0].length ?? 0) <= indent)
35
+ break;
36
+ end = index + 1;
37
+ }
38
+ return end;
39
+ }
40
+ let depth = 0;
41
+ let opened = false;
42
+ for (let index = startIndex; index < lines.length; index++) {
43
+ for (const character of lines[index]) {
44
+ if (character === "{") {
45
+ opened = true;
46
+ depth++;
47
+ }
48
+ else if (character === "}")
49
+ depth--;
50
+ }
51
+ if (opened && depth <= 0)
52
+ return index + 1;
53
+ if (!opened && /[;}]\s*$/.test(lines[index]))
54
+ return index + 1;
55
+ }
56
+ return startIndex + 1;
57
+ }
58
+ function genericFunctionSymbol(line) {
59
+ const opening = line.indexOf("(");
60
+ if (opening <= 0)
61
+ return null;
62
+ let depth = 0;
63
+ let closing = -1;
64
+ for (let index = opening; index < line.length; index += 1) {
65
+ if (line[index] === "(")
66
+ depth += 1;
67
+ if (line[index] !== ")")
68
+ continue;
69
+ depth -= 1;
70
+ if (depth === 0) {
71
+ closing = index;
72
+ break;
73
+ }
74
+ }
75
+ if (closing < 0)
76
+ return null;
77
+ const suffix = line.slice(closing + 1).trimStart();
78
+ if (!suffix.startsWith("{") && !suffix.startsWith("=>"))
79
+ return null;
80
+ const prefix = line.slice(0, opening).trim();
81
+ if (/[=;{}()]/.test(prefix))
82
+ return null;
83
+ const tokens = prefix.split(/\s+/);
84
+ const symbol = tokens.at(-1) ?? "";
85
+ return tokens.length >= 2 && /^[A-Za-z_$][\w$]*$/.test(symbol) ? symbol : null;
86
+ }
87
+ function firstPatternSymbol(line, patterns) {
88
+ for (const { kind, expression } of patterns) {
89
+ const match = expression.exec(line);
90
+ if (match)
91
+ return { kind, symbol: match[1] };
92
+ }
93
+ return null;
94
+ }
95
+ function directSymbols(filePath, content) {
96
+ const extension = path.extname(filePath).toLowerCase();
97
+ const python = extension === ".py";
98
+ const lines = content.split(/\r?\n/);
99
+ const symbols = [];
100
+ const patterns = python
101
+ ? [
102
+ { kind: "class", expression: /^\s*class\s+([A-Za-z_$][\w$]*)\b/ },
103
+ { kind: "function", expression: /^\s*(?:async\s+)?def\s+([A-Za-z_$][\w$]*)\s*\(/ },
104
+ ]
105
+ : [
106
+ {
107
+ kind: "class",
108
+ expression: /^\s*(?:(?:export|public|private|protected|internal|abstract|final|sealed|static)\s+)*(?:class|interface|enum|struct|trait|record|type)\s+([A-Za-z_$][\w$]*)\b/,
109
+ },
110
+ {
111
+ kind: "function",
112
+ expression: /^\s*(?:(?:export|public|private|protected|internal|async|static|final|pub(?:\([^)]*\))?)\s+)*(?:func|fn|function)\s+([A-Za-z_$][\w$]*)\s*\(/,
113
+ },
114
+ {
115
+ kind: "function",
116
+ expression: /^\s*(?:(?:export|const|let|var|public|private|protected|static|readonly)\s+)*([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/,
117
+ },
118
+ ];
119
+ for (const [index, line] of lines.entries()) {
120
+ let found = firstPatternSymbol(line, patterns);
121
+ if (!found && !python) {
122
+ const symbol = genericFunctionSymbol(line);
123
+ if (symbol)
124
+ found = { kind: "function", symbol };
125
+ }
126
+ if (found) {
127
+ const { kind, symbol } = found;
128
+ const visibility = /\b(public|private|protected|internal)\b/.exec(line)?.[1] ??
129
+ (python && symbol.startsWith("_") ? "private" : "default");
130
+ symbols.push({
131
+ identity: null,
132
+ symbol,
133
+ kind,
134
+ line: index + 1,
135
+ endLine: lexicalEndLine(lines, index, python),
136
+ signature: line
137
+ .trim()
138
+ .split(/\s*(?:\{|=>)\s*/, 1)[0]
139
+ .slice(0, 500),
140
+ visibility,
141
+ exported: /\bexport\b/.test(line) || /\bpub\b/.test(line),
142
+ evidenceQuality: "lexical-structural-fallback",
143
+ });
144
+ }
145
+ }
146
+ return symbols;
147
+ }
148
+ function directFile(repo, filePath) {
149
+ if (!DIRECT_FILE_EXTENSIONS.has(path.extname(filePath).toLowerCase()))
150
+ return null;
151
+ const root = fs.realpathSync(repo);
152
+ const absolute = path.resolve(root, filePath);
153
+ let real;
154
+ try {
155
+ real = fs.realpathSync(absolute);
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ const relative = path.relative(root, real);
161
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
162
+ return null;
163
+ const content = fs.readFileSync(real, "utf8");
164
+ return {
165
+ path: relative.split(path.sep).join("/"),
166
+ sizeBytes: Buffer.byteLength(content),
167
+ mtimeMs: fs.statSync(real).mtimeMs,
168
+ contentFingerprint: contentFingerprint(content),
169
+ symbols: directSymbols(filePath, content),
170
+ };
171
+ }
172
+ export function runStructuralFastPath(repo, pattern, target, limit = 100) {
173
+ const snapshot = readStructuralSnapshot(repo);
174
+ if (!snapshot && pattern !== "file_summary")
175
+ return null;
176
+ let matching = (snapshot?.files ?? []).filter((file) => !target || file.path.includes(target));
177
+ if (pattern === "file_summary") {
178
+ const indexed = matching.find((file) => file.path === target);
179
+ if (!indexed || !exactFile(repo, indexed)) {
180
+ const direct = directFile(repo, target);
181
+ if (!direct)
182
+ return null;
183
+ matching = [direct];
184
+ }
185
+ }
186
+ else if (pattern === "batch_outline") {
187
+ matching = matching.flatMap((file) => {
188
+ if (exactFile(repo, file))
189
+ return [file];
190
+ const direct = directFile(repo, file.path);
191
+ return direct ? [direct] : [];
192
+ });
193
+ }
194
+ let rows = [];
195
+ let fingerprint;
196
+ let fresh = false;
197
+ if (pattern === "project_overview") {
198
+ fresh = matching.every((file) => {
199
+ try {
200
+ const stat = fs.statSync(path.resolve(repo, file.path));
201
+ return stat.size === file.sizeBytes && stat.mtimeMs === file.mtimeMs;
202
+ }
203
+ catch {
204
+ return false;
205
+ }
206
+ });
207
+ const directories = new Map();
208
+ for (const file of matching) {
209
+ const directory = file.path.includes("/") ? file.path.split("/", 1)[0] : ".";
210
+ const value = directories.get(directory) ?? { fileCount: 0, sizeBytes: 0 };
211
+ value.fileCount += 1;
212
+ value.sizeBytes += file.sizeBytes;
213
+ directories.set(directory, value);
214
+ }
215
+ rows = [...directories]
216
+ // Byte order: this sort decides which directories survive the
217
+ // `rows.slice(0, limit)` below, not merely how they read.
218
+ .sort(([a], [b]) => compareBytes(a, b))
219
+ .map(([symbol, value]) => ({ symbol, kind: "directory", ...value }));
220
+ fingerprint = contentFingerprint(JSON.stringify(matching.map(({ path: file, contentFingerprint: hash }) => [file, hash])));
221
+ }
222
+ else {
223
+ const files = pattern === "file_summary" ? matching.filter((file) => file.path === target) : matching;
224
+ fresh = files.length > 0 && files.every((file) => exactFile(repo, file));
225
+ fingerprint =
226
+ files.length === 1
227
+ ? files[0].contentFingerprint
228
+ : contentFingerprint(JSON.stringify(files.map((file) => [file.path, file.contentFingerprint])));
229
+ rows = files.flatMap((file) => file.symbols.map((symbol) => ({ ...symbol, file: file.path })));
230
+ }
231
+ const selected = rows.slice(0, limit);
232
+ let upgrade;
233
+ if (pattern === "file_summary")
234
+ upgrade = { operation: "explain", target };
235
+ else if (pattern === "project_overview")
236
+ upgrade = { operation: "query", pattern: "architecture_overview", target: "" };
237
+ else
238
+ upgrade = { operation: "query", pattern: "impact", target };
239
+ return {
240
+ pattern,
241
+ target,
242
+ count: selected.length,
243
+ results: selected,
244
+ ...(rows.length > selected.length ? { hasMore: true } : {}),
245
+ staleness: fresh ? "fresh" : "unknown",
246
+ graphEnriched: false,
247
+ evidence: {
248
+ ...(pattern === "file_summary" ? { path: target } : {}),
249
+ contentFingerprint: fingerprint,
250
+ evidenceQuality: matching.some((file) => file.symbols.some((symbol) => symbol.evidenceQuality === "lexical-structural-fallback"))
251
+ ? "direct-lexical-parser"
252
+ : "indexed-structural-snapshot",
253
+ uniqueness: pattern === "project_overview" ? "not-applicable" : "file-scoped-only",
254
+ evidenceGeneratedAt: snapshot?.generatedAt ?? new Date().toISOString(),
255
+ },
256
+ upgrade,
257
+ };
258
+ }
259
+ export function runExactSymbolFastPath(repo, symbol, filePath) {
260
+ const snapshot = readStructuralSnapshot(repo);
261
+ const indexed = snapshot?.files.find((candidate) => candidate.path === filePath);
262
+ const file = indexed && exactFile(repo, indexed) ? indexed : directFile(repo, filePath);
263
+ if (!file)
264
+ return null;
265
+ const matches = file.symbols.filter((candidate) => candidate.symbol === symbol);
266
+ if (matches.length === 0)
267
+ return null;
268
+ const fresh = exactFile(repo, file);
269
+ const evidence = {
270
+ path: filePath,
271
+ contentFingerprint: file.contentFingerprint,
272
+ evidenceQuality: file.symbols.some((candidate) => candidate.evidenceQuality === "lexical-structural-fallback")
273
+ ? "direct-lexical-parser"
274
+ : "indexed-structural-snapshot",
275
+ uniqueness: "file-scoped-only",
276
+ evidenceGeneratedAt: snapshot?.generatedAt ?? new Date().toISOString(),
277
+ };
278
+ if (matches.length > 1) {
279
+ return {
280
+ symbol,
281
+ file: filePath,
282
+ ambiguity: {
283
+ candidates: matches.map((match) => ({
284
+ identity: match.identity ?? "",
285
+ file: filePath,
286
+ kind: match.kind,
287
+ line: match.line,
288
+ })),
289
+ },
290
+ staleness: fresh ? "fresh" : "unknown",
291
+ graphEnriched: false,
292
+ evidence,
293
+ upgrade: { operation: "explain", target: symbol },
294
+ };
295
+ }
296
+ const match = matches[0];
297
+ const lines = fs.readFileSync(path.resolve(repo, filePath), "utf8").split(/\r?\n/);
298
+ const source = fresh
299
+ ? lines
300
+ .slice(match.line - 1, match.endLine)
301
+ .map((line, offset) => `${match.line + offset}: ${line}`)
302
+ .join("\n")
303
+ : undefined;
304
+ return {
305
+ symbol,
306
+ ...(match.identity ? { identity: match.identity } : {}),
307
+ file: filePath,
308
+ ...(source ? { source } : {}),
309
+ staleness: fresh ? "fresh" : "unknown",
310
+ graphEnriched: false,
311
+ evidence,
312
+ upgrade: { operation: "explain", target: symbol },
313
+ };
314
+ }
315
+ export function tryStructuralFastPath(argv, cwd) {
316
+ const valueAfter = (flag) => {
317
+ const index = argv.indexOf(flag);
318
+ return index >= 0
319
+ ? argv[index + 1]
320
+ : argv.find((value) => value.startsWith(`${flag}=`))?.slice(flag.length + 1);
321
+ };
322
+ if (argv[0] === "explain" && argv[1] && valueAfter("--file")) {
323
+ const repo = path.resolve(valueAfter("--repo") ?? cwd ?? process.cwd());
324
+ const result = runExactSymbolFastPath(repo, argv[1], valueAfter("--file"));
325
+ if (!result)
326
+ return false;
327
+ process.stdout.write(`${JSON.stringify(result)}\n`);
328
+ return true;
329
+ }
330
+ if (argv[0] !== "query" ||
331
+ !["file_summary", "batch_outline", "project_overview"].includes(argv[1] ?? ""))
332
+ return false;
333
+ const pattern = argv[1];
334
+ const repo = path.resolve(valueAfter("--repo") ?? cwd ?? process.cwd());
335
+ const target = pattern === "project_overview" ? "" : (argv[2] ?? "");
336
+ const limit = Number(valueAfter("--limit") ?? 100);
337
+ if (!target && pattern !== "project_overview")
338
+ return false;
339
+ const result = runStructuralFastPath(repo, pattern, target, Number.isInteger(limit) && limit > 0 ? limit : 100);
340
+ if (!result)
341
+ return false;
342
+ process.stdout.write(`${JSON.stringify(result)}\n`);
343
+ return true;
344
+ }
@@ -0,0 +1,37 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { compareBytes } from "./compare.js";
5
+ import { resolveStateDir } from "./engine/state-paths.js";
6
+ export const STRUCTURAL_SNAPSHOT_VERSION = 1;
7
+ export function contentFingerprint(content) {
8
+ return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
9
+ }
10
+ export function structuralSnapshotPath(repo) {
11
+ return path.join(resolveStateDir(repo), "structural-v1.json");
12
+ }
13
+ export function writeStructuralSnapshot(repo, files) {
14
+ const destination = structuralSnapshotPath(repo);
15
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
16
+ const snapshot = {
17
+ schemaVersion: STRUCTURAL_SNAPSHOT_VERSION,
18
+ generatedAt: new Date().toISOString(),
19
+ // Byte order, not collation: this array is written to disk and read back
20
+ // on any host, so the snapshot's bytes must not depend on the locale.
21
+ files: [...files].sort((left, right) => compareBytes(left.path, right.path)),
22
+ };
23
+ const temporary = `${destination}.${process.pid}.tmp`;
24
+ fs.writeFileSync(temporary, `${JSON.stringify(snapshot)}\n`, { mode: 0o600 });
25
+ fs.renameSync(temporary, destination);
26
+ }
27
+ export function readStructuralSnapshot(repo) {
28
+ try {
29
+ const parsed = JSON.parse(fs.readFileSync(structuralSnapshotPath(repo), "utf8"));
30
+ return parsed?.schemaVersion === STRUCTURAL_SNAPSHOT_VERSION && Array.isArray(parsed.files)
31
+ ? parsed
32
+ : null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }