knodin 0.7.4 → 0.7.6
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.
- package/README.md +4 -1
- package/dist/bin/cli.js +108 -1859
- package/dist/bin/launcher.js +3 -10
- package/dist/chunks/chunk-DMQAGX77.js +654 -0
- package/dist/chunks/chunk-F4Z3Z766.js +4 -0
- package/dist/chunks/chunk-SIJAQVSX.js +3 -0
- package/dist/chunks/chunk-X6M4HUUE.js +2 -0
- package/dist/chunks/chunk-YPRMY2LP.js +8 -0
- package/dist/chunks/pure-compression-cli-4TA2TQD5.js +5 -0
- package/dist/chunks/server-7EDF4CBY.js +14 -0
- package/dist/chunks/structural-fast-path-KD5KQSPX.js +4 -0
- package/dist/src/init-progress-worker.js +2 -106
- package/dist/src/repair-progress-worker.js +2 -120
- package/docs/INSTALLATION.md +17 -1
- package/docs/releases/0.7.6.md +25 -0
- package/package.json +1 -110
- package/dist/src/agent-integration.js +0 -238
- package/dist/src/artifact-refresh.js +0 -81
- package/dist/src/cli-args.js +0 -274
- package/dist/src/cli-model.js +0 -358
- package/dist/src/codeflow-replay.js +0 -80
- package/dist/src/compact-structural.js +0 -96
- package/dist/src/competitive-cold-mcp.js +0 -40
- package/dist/src/competitive-constraints.js +0 -20
- package/dist/src/competitive-manifest.js +0 -411
- package/dist/src/competitive-measurement.js +0 -183
- package/dist/src/competitive-runner.js +0 -487
- package/dist/src/competitive-sandbox.js +0 -108
- package/dist/src/context-export.js +0 -422
- package/dist/src/context.js +0 -102
- package/dist/src/diagnostics.js +0 -449
- package/dist/src/docs-sections.js +0 -141
- package/dist/src/doctor.js +0 -380
- package/dist/src/engine/ann-hnsw.js +0 -271
- package/dist/src/engine/embeddings.js +0 -193
- package/dist/src/engine/file-walker.js +0 -43
- package/dist/src/engine/git-history.js +0 -289
- package/dist/src/engine/index.js +0 -13377
- package/dist/src/engine/perf.js +0 -115
- package/dist/src/engine/prune.js +0 -112
- package/dist/src/engine/scip-import.js +0 -408
- package/dist/src/engine/source-policy.js +0 -69
- package/dist/src/engine/sqlite.js +0 -71
- package/dist/src/engine/symbol-delete.js +0 -58
- package/dist/src/execution-profile.js +0 -203
- package/dist/src/failure-diagnosis.js +0 -649
- package/dist/src/fleet.js +0 -7
- package/dist/src/git-executable.js +0 -31
- package/dist/src/graph-query-health.js +0 -115
- package/dist/src/hook-manager-integration.js +0 -156
- package/dist/src/index-activity.js +0 -125
- package/dist/src/init-progress.js +0 -155
- package/dist/src/init.js +0 -1254
- package/dist/src/lifecycle-health.js +0 -250
- package/dist/src/lsp-readonly.js +0 -217
- package/dist/src/output-compression.js +0 -629
- package/dist/src/output-telemetry.js +0 -363
- package/dist/src/pr-triage.js +0 -638
- package/dist/src/progressive-evidence.js +0 -473
- package/dist/src/pure-compression-cli.js +0 -101
- package/dist/src/relationship-adapters.js +0 -370
- package/dist/src/release-attestation.js +0 -533
- package/dist/src/release-preflight.js +0 -510
- package/dist/src/repair-progress.js +0 -262
- package/dist/src/repository-init-process.js +0 -173
- package/dist/src/repository-management.js +0 -1231
- package/dist/src/response-budget.js +0 -194
- package/dist/src/server.js +0 -73
- package/dist/src/structural-fast-path.js +0 -338
- package/dist/src/structural-snapshot.js +0 -33
- package/dist/src/system-config.js +0 -615
- package/dist/src/terminal-help.js +0 -83
- package/dist/src/tools/knodin-tools.js +0 -1529
- package/dist/src/update-ceremony.js +0 -158
- package/dist/src/update-policy.js +0 -944
- package/dist/src/update-trust.js +0 -503
- package/dist/src/version.js +0 -13
- package/dist/src/visualization.js +0 -162
- package/dist/src/wait-for-fresh.js +0 -98
- package/dist/src/worktree-lifecycle.js +0 -231
- package/docs/CONTAINED-EXECUTION.md +0 -77
- package/docs/DIAGNOSTICS.md +0 -45
- package/docs/GIT-HISTORY-REVIEW.md +0 -39
- package/docs/PROGRESSIVE-EVIDENCE.md +0 -37
- package/docs/PT-ACCESS-RECOMMENDATION.md +0 -91
- package/docs/RELEASE-0.3-EVIDENCE.md +0 -73
- package/docs/SCIP-IMPORT.md +0 -57
- package/docs/SIGNED-UPDATES.md +0 -151
- package/docs/TELEMETRY.md +0 -46
- package/docs/assets/knodin-favicon.svg +0 -4
- package/docs/releases/0.3.0.md +0 -46
- package/docs/releases/0.4.0.md +0 -68
- package/docs/releases/0.4.1.md +0 -28
- package/docs/releases/0.4.2.md +0 -27
- package/docs/releases/0.4.3.md +0 -23
- package/docs/releases/0.5.0.md +0 -29
- package/docs/releases/0.5.1.md +0 -17
- package/docs/releases/0.6.0.md +0 -18
- package/docs/releases/0.7.0.md +0 -24
- package/docs/releases/0.7.1.md +0 -21
- package/docs/releases/0.7.2.md +0 -21
- package/docs/releases/0.7.3.md +0 -23
- package/docs/releases/0.7.4.md +0 -17
- package/schemas/release-attestation-v1.schema.json +0 -210
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
const encoder = new TextEncoder();
|
|
2
|
-
export const MIN_RESPONSE_BUDGET_BYTES = 256;
|
|
3
|
-
const MAX_REPORTED_TOTAL_PATHS = 128;
|
|
4
|
-
function bytes(value) {
|
|
5
|
-
return encoder.encode(JSON.stringify(value)).byteLength;
|
|
6
|
-
}
|
|
7
|
-
function positiveInt(value, fallback) {
|
|
8
|
-
return value == null || !Number.isFinite(value) ? fallback : Math.max(1, Math.trunc(value));
|
|
9
|
-
}
|
|
10
|
-
function collectArrays(value, path = "$") {
|
|
11
|
-
if (Array.isArray(value)) {
|
|
12
|
-
return [{ path, value }, ...value.flatMap((item, i) => collectArrays(item, `${path}/${i}`))];
|
|
13
|
-
}
|
|
14
|
-
if (!value || typeof value !== "object")
|
|
15
|
-
return [];
|
|
16
|
-
return Object.entries(value).flatMap(([key, child]) => collectArrays(child, `${path}/${key}`));
|
|
17
|
-
}
|
|
18
|
-
function collectStrings(value, path = "$") {
|
|
19
|
-
if (!value || typeof value !== "object")
|
|
20
|
-
return [];
|
|
21
|
-
const entries = Array.isArray(value) ? value.entries() : Object.entries(value);
|
|
22
|
-
const found = [];
|
|
23
|
-
for (const [key, child] of entries) {
|
|
24
|
-
const childPath = `${path}/${String(key)}`;
|
|
25
|
-
if (typeof child === "string") {
|
|
26
|
-
found.push({ parent: value, key, path: childPath, value: child });
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
found.push(...collectStrings(child, childPath));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return found;
|
|
33
|
-
}
|
|
34
|
-
const PROTECTED_CONTRACT_KEYS = new Set([
|
|
35
|
-
"identity",
|
|
36
|
-
"symbol",
|
|
37
|
-
"file",
|
|
38
|
-
"filePath",
|
|
39
|
-
"error",
|
|
40
|
-
"status",
|
|
41
|
-
"operation",
|
|
42
|
-
"kind",
|
|
43
|
-
"name",
|
|
44
|
-
"target",
|
|
45
|
-
"to",
|
|
46
|
-
]);
|
|
47
|
-
function isTruncatableDetail(entry) {
|
|
48
|
-
return typeof entry.key !== "string" || !PROTECTED_CONTRACT_KEYS.has(entry.key);
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Applies limits to the value that is actually JSON-serialized. Token accounting
|
|
52
|
-
* intentionally uses the deterministic local estimate of four UTF-8 bytes/token;
|
|
53
|
-
* callers can therefore reproduce the ceiling without installing a tokenizer.
|
|
54
|
-
*/
|
|
55
|
-
export function applyResponseBudget(value, operation, request, defaults) {
|
|
56
|
-
const byteRequest = positiveInt(request.bytes, defaults.bytes);
|
|
57
|
-
const tokenLimit = positiveInt(request.tokens, defaults.tokens);
|
|
58
|
-
const byteLimit = Math.min(byteRequest, tokenLimit * 4);
|
|
59
|
-
if (byteLimit < MIN_RESPONSE_BUDGET_BYTES) {
|
|
60
|
-
throw new RangeError(`response budget must allow at least ${MIN_RESPONSE_BUDGET_BYTES} serialized bytes (64 estimated tokens)`);
|
|
61
|
-
}
|
|
62
|
-
const itemLimit = positiveInt(request.items, defaults.items);
|
|
63
|
-
const payload = structuredClone(value);
|
|
64
|
-
const root = Array.isArray(payload)
|
|
65
|
-
? { results: payload }
|
|
66
|
-
: payload && typeof payload === "object"
|
|
67
|
-
? payload
|
|
68
|
-
: { result: payload };
|
|
69
|
-
const totals = {};
|
|
70
|
-
let truncated = false;
|
|
71
|
-
const initialArrays = collectArrays(root);
|
|
72
|
-
const reportedTotals = [...initialArrays]
|
|
73
|
-
.sort((left, right) => left.path.split("/").length - right.path.split("/").length ||
|
|
74
|
-
left.path.localeCompare(right.path))
|
|
75
|
-
.slice(0, MAX_REPORTED_TOTAL_PATHS);
|
|
76
|
-
for (const entry of reportedTotals)
|
|
77
|
-
totals[entry.path] = entry.value.length;
|
|
78
|
-
for (const entry of initialArrays) {
|
|
79
|
-
if (entry.value.length > itemLimit) {
|
|
80
|
-
entry.value.splice(itemLimit);
|
|
81
|
-
truncated = true;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
const metadata = {
|
|
85
|
-
byteLimit,
|
|
86
|
-
tokenLimit,
|
|
87
|
-
itemLimit,
|
|
88
|
-
serializedBytes: 0,
|
|
89
|
-
estimatedTokens: 0,
|
|
90
|
-
truncated: false,
|
|
91
|
-
totals,
|
|
92
|
-
...(initialArrays.length > reportedTotals.length
|
|
93
|
-
? { omittedTotalPaths: initialArrays.length - reportedTotals.length }
|
|
94
|
-
: {}),
|
|
95
|
-
};
|
|
96
|
-
root.responseBudget = metadata;
|
|
97
|
-
// Preserve collection contracts and identifiers where possible: large source,
|
|
98
|
-
// artifact and diagnostic strings are the first expendable detail.
|
|
99
|
-
while (bytes(root) > byteLimit) {
|
|
100
|
-
const candidate = collectStrings(root)
|
|
101
|
-
.filter((entry) => !entry.path.startsWith("$/responseBudget") &&
|
|
102
|
-
entry.value.length > 0 &&
|
|
103
|
-
isTruncatableDetail(entry))
|
|
104
|
-
.sort((a, b) => b.value.length - a.value.length)[0];
|
|
105
|
-
if (!candidate)
|
|
106
|
-
break;
|
|
107
|
-
const marker = "\n… [truncated]";
|
|
108
|
-
if (candidate.value.length <= marker.length + 1) {
|
|
109
|
-
if (Array.isArray(candidate.parent))
|
|
110
|
-
candidate.parent.splice(candidate.key, 1);
|
|
111
|
-
else
|
|
112
|
-
delete candidate.parent[candidate.key];
|
|
113
|
-
truncated = true;
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
const keep = Math.max(0, Math.floor((candidate.value.length - marker.length) / 2));
|
|
117
|
-
candidate.parent[candidate.key] =
|
|
118
|
-
`${candidate.value.slice(0, keep)}${marker}`;
|
|
119
|
-
truncated = true;
|
|
120
|
-
}
|
|
121
|
-
// Only drop tail items after scalar detail has been exhausted.
|
|
122
|
-
while (bytes(root) > byteLimit) {
|
|
123
|
-
const candidate = collectArrays(root)
|
|
124
|
-
.filter((entry) => !entry.path.startsWith("$/responseBudget") && entry.value.length > 0)
|
|
125
|
-
.sort((a, b) => bytes(b.value) - bytes(a.value))[0];
|
|
126
|
-
if (!candidate)
|
|
127
|
-
break;
|
|
128
|
-
candidate.value.pop();
|
|
129
|
-
truncated = true;
|
|
130
|
-
}
|
|
131
|
-
metadata.truncated = truncated;
|
|
132
|
-
if (truncated) {
|
|
133
|
-
metadata.continuation = {
|
|
134
|
-
operation,
|
|
135
|
-
after: "returned-items",
|
|
136
|
-
instruction: `Repeat ${operation} with a narrower selector, larger budget, or query drill-down.`,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
// Metadata changes the byte count, so converge after setting it. The reserve
|
|
140
|
-
// above is normally enough; this final pass handles unusually long paths.
|
|
141
|
-
metadata.serializedBytes = bytes(root);
|
|
142
|
-
metadata.estimatedTokens = Math.ceil(metadata.serializedBytes / 4);
|
|
143
|
-
if (bytes(root) > byteLimit && metadata.continuation) {
|
|
144
|
-
metadata.continuation.instruction = "Narrow or increase budget.";
|
|
145
|
-
metadata.serializedBytes = bytes(root);
|
|
146
|
-
metadata.estimatedTokens = Math.ceil(metadata.serializedBytes / 4);
|
|
147
|
-
}
|
|
148
|
-
while (bytes(root) > byteLimit) {
|
|
149
|
-
const candidate = collectStrings(root)
|
|
150
|
-
.filter((entry) => !entry.path.startsWith("$/responseBudget") &&
|
|
151
|
-
entry.value.length > 0 &&
|
|
152
|
-
isTruncatableDetail(entry))
|
|
153
|
-
.sort((a, b) => b.value.length - a.value.length)[0];
|
|
154
|
-
if (!candidate)
|
|
155
|
-
break;
|
|
156
|
-
const marker = "\n… [truncated]";
|
|
157
|
-
if (candidate.value.length <= marker.length + 1) {
|
|
158
|
-
if (Array.isArray(candidate.parent))
|
|
159
|
-
candidate.parent.splice(candidate.key, 1);
|
|
160
|
-
else
|
|
161
|
-
delete candidate.parent[candidate.key];
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
const keep = Math.max(0, Math.floor((candidate.value.length - marker.length) / 2));
|
|
165
|
-
candidate.parent[candidate.key] =
|
|
166
|
-
`${candidate.value.slice(0, keep)}${marker}`;
|
|
167
|
-
}
|
|
168
|
-
metadata.truncated = true;
|
|
169
|
-
}
|
|
170
|
-
metadata.serializedBytes = bytes(root);
|
|
171
|
-
metadata.estimatedTokens = Math.ceil(metadata.serializedBytes / 4);
|
|
172
|
-
while (bytes(root) > byteLimit) {
|
|
173
|
-
const candidate = collectArrays(root)
|
|
174
|
-
.filter((entry) => !entry.path.startsWith("$/responseBudget") && entry.value.length > 0)
|
|
175
|
-
.sort((a, b) => bytes(b.value) - bytes(a.value))[0];
|
|
176
|
-
if (!candidate)
|
|
177
|
-
break;
|
|
178
|
-
candidate.value.pop();
|
|
179
|
-
metadata.truncated = true;
|
|
180
|
-
metadata.serializedBytes = bytes(root);
|
|
181
|
-
metadata.estimatedTokens = Math.ceil(metadata.serializedBytes / 4);
|
|
182
|
-
}
|
|
183
|
-
for (let iteration = 0; iteration < 8; iteration++) {
|
|
184
|
-
const actual = bytes(root);
|
|
185
|
-
const tokens = Math.ceil(actual / 4);
|
|
186
|
-
if (metadata.serializedBytes === actual && metadata.estimatedTokens === tokens)
|
|
187
|
-
break;
|
|
188
|
-
metadata.serializedBytes = actual;
|
|
189
|
-
metadata.estimatedTokens = tokens;
|
|
190
|
-
}
|
|
191
|
-
if (bytes(root) > byteLimit)
|
|
192
|
-
throw new RangeError(`response budget ${byteLimit} cannot preserve the ${operation} response contract; narrow the request or increase the budget`);
|
|
193
|
-
return root;
|
|
194
|
-
}
|
package/dist/src/server.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
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 { getKnodinTools, handleKnodinTool } from "./tools/knodin-tools.js";
|
|
16
|
-
import { KNODIN_VERSION } from "./version.js";
|
|
17
|
-
export function recordMcpDiagnosticFailure(args, error, defaultRepo = process.cwd()) {
|
|
18
|
-
const effectiveRepo = typeof args?.repoPath === "string" ? args.repoPath : defaultRepo;
|
|
19
|
-
const diagnostic = recordDiagnosticFailure(effectiveRepo, {
|
|
20
|
-
surface: "mcp",
|
|
21
|
-
operation: typeof args?.operation === "string" ? args.operation : "unknown",
|
|
22
|
-
phase: "dispatch",
|
|
23
|
-
error,
|
|
24
|
-
});
|
|
25
|
-
return diagnostic.recorded
|
|
26
|
-
? new Error(`${error instanceof Error ? error.message : String(error)} [diagnostic ${diagnostic.correlationId}]`, { cause: error })
|
|
27
|
-
: error;
|
|
28
|
-
}
|
|
29
|
-
export function createServer() {
|
|
30
|
-
const server = new Server({ name: "knodin", version: KNODIN_VERSION }, {
|
|
31
|
-
capabilities: { tools: {} },
|
|
32
|
-
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.",
|
|
33
|
-
});
|
|
34
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
35
|
-
tools: getKnodinTools(),
|
|
36
|
-
}));
|
|
37
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
38
|
-
const { name, arguments: args } = request.params;
|
|
39
|
-
if (name !== "knodin") {
|
|
40
|
-
throw new Error(`unknown tool: ${name}`);
|
|
41
|
-
}
|
|
42
|
-
let result;
|
|
43
|
-
try {
|
|
44
|
-
result = await handleKnodinTool(args);
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
const values = args;
|
|
48
|
-
throw recordMcpDiagnosticFailure(values, error);
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
return server;
|
|
55
|
-
}
|
|
56
|
-
async function main() {
|
|
57
|
-
const server = createServer();
|
|
58
|
-
const transport = new StdioServerTransport();
|
|
59
|
-
await server.connect(transport);
|
|
60
|
-
// stdio server: log to stderr so stdout stays a clean JSON-RPC channel.
|
|
61
|
-
console.error("knodin MCP server running on stdio");
|
|
62
|
-
}
|
|
63
|
-
/** Start the stdio gateway from either the direct server entry or the packaged CLI. */
|
|
64
|
-
export async function startServer() {
|
|
65
|
-
await main();
|
|
66
|
-
}
|
|
67
|
-
// Run only when invoked directly (not when imported by tests).
|
|
68
|
-
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
69
|
-
startServer().catch((err) => {
|
|
70
|
-
console.error(err);
|
|
71
|
-
process.exit(1);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { contentFingerprint, readStructuralSnapshot, } from "./structural-snapshot.js";
|
|
4
|
-
function exactFile(repo, file) {
|
|
5
|
-
try {
|
|
6
|
-
return (contentFingerprint(fs.readFileSync(path.resolve(repo, file.path))) === file.contentFingerprint);
|
|
7
|
-
}
|
|
8
|
-
catch {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
const DIRECT_FILE_EXTENSIONS = new Set([
|
|
13
|
-
".cjs",
|
|
14
|
-
".cs",
|
|
15
|
-
".cts",
|
|
16
|
-
".go",
|
|
17
|
-
".java",
|
|
18
|
-
".js",
|
|
19
|
-
".jsx",
|
|
20
|
-
".mjs",
|
|
21
|
-
".mts",
|
|
22
|
-
".py",
|
|
23
|
-
".rs",
|
|
24
|
-
".ts",
|
|
25
|
-
".tsx",
|
|
26
|
-
]);
|
|
27
|
-
function lexicalEndLine(lines, startIndex, python) {
|
|
28
|
-
if (python) {
|
|
29
|
-
const indent = /^\s*/.exec(lines[startIndex])?.[0].length ?? 0;
|
|
30
|
-
let end = startIndex + 1;
|
|
31
|
-
for (let index = startIndex + 1; index < lines.length; index++) {
|
|
32
|
-
const line = lines[index];
|
|
33
|
-
if (line.trim() && (/^\s*/.exec(line)?.[0].length ?? 0) <= indent)
|
|
34
|
-
break;
|
|
35
|
-
end = index + 1;
|
|
36
|
-
}
|
|
37
|
-
return end;
|
|
38
|
-
}
|
|
39
|
-
let depth = 0;
|
|
40
|
-
let opened = false;
|
|
41
|
-
for (let index = startIndex; index < lines.length; index++) {
|
|
42
|
-
for (const character of lines[index]) {
|
|
43
|
-
if (character === "{") {
|
|
44
|
-
opened = true;
|
|
45
|
-
depth++;
|
|
46
|
-
}
|
|
47
|
-
else if (character === "}")
|
|
48
|
-
depth--;
|
|
49
|
-
}
|
|
50
|
-
if (opened && depth <= 0)
|
|
51
|
-
return index + 1;
|
|
52
|
-
if (!opened && /[;}]\s*$/.test(lines[index]))
|
|
53
|
-
return index + 1;
|
|
54
|
-
}
|
|
55
|
-
return startIndex + 1;
|
|
56
|
-
}
|
|
57
|
-
function genericFunctionSymbol(line) {
|
|
58
|
-
const opening = line.indexOf("(");
|
|
59
|
-
if (opening <= 0)
|
|
60
|
-
return null;
|
|
61
|
-
let depth = 0;
|
|
62
|
-
let closing = -1;
|
|
63
|
-
for (let index = opening; index < line.length; index += 1) {
|
|
64
|
-
if (line[index] === "(")
|
|
65
|
-
depth += 1;
|
|
66
|
-
if (line[index] !== ")")
|
|
67
|
-
continue;
|
|
68
|
-
depth -= 1;
|
|
69
|
-
if (depth === 0) {
|
|
70
|
-
closing = index;
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (closing < 0)
|
|
75
|
-
return null;
|
|
76
|
-
const suffix = line.slice(closing + 1).trimStart();
|
|
77
|
-
if (!suffix.startsWith("{") && !suffix.startsWith("=>"))
|
|
78
|
-
return null;
|
|
79
|
-
const prefix = line.slice(0, opening).trim();
|
|
80
|
-
if (/[=;{}()]/.test(prefix))
|
|
81
|
-
return null;
|
|
82
|
-
const tokens = prefix.split(/\s+/);
|
|
83
|
-
const symbol = tokens.at(-1) ?? "";
|
|
84
|
-
return tokens.length >= 2 && /^[A-Za-z_$][\w$]*$/.test(symbol) ? symbol : null;
|
|
85
|
-
}
|
|
86
|
-
function firstPatternSymbol(line, patterns) {
|
|
87
|
-
for (const { kind, expression } of patterns) {
|
|
88
|
-
const match = expression.exec(line);
|
|
89
|
-
if (match)
|
|
90
|
-
return { kind, symbol: match[1] };
|
|
91
|
-
}
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
function directSymbols(filePath, content) {
|
|
95
|
-
const extension = path.extname(filePath).toLowerCase();
|
|
96
|
-
const python = extension === ".py";
|
|
97
|
-
const lines = content.split(/\r?\n/);
|
|
98
|
-
const symbols = [];
|
|
99
|
-
const patterns = python
|
|
100
|
-
? [
|
|
101
|
-
{ kind: "class", expression: /^\s*class\s+([A-Za-z_$][\w$]*)\b/ },
|
|
102
|
-
{ kind: "function", expression: /^\s*(?:async\s+)?def\s+([A-Za-z_$][\w$]*)\s*\(/ },
|
|
103
|
-
]
|
|
104
|
-
: [
|
|
105
|
-
{
|
|
106
|
-
kind: "class",
|
|
107
|
-
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/,
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
kind: "function",
|
|
111
|
-
expression: /^\s*(?:(?:export|public|private|protected|internal|async|static|final|pub(?:\([^)]*\))?)\s+)*(?:func|fn|function)\s+([A-Za-z_$][\w$]*)\s*\(/,
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
kind: "function",
|
|
115
|
-
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*=>/,
|
|
116
|
-
},
|
|
117
|
-
];
|
|
118
|
-
for (const [index, line] of lines.entries()) {
|
|
119
|
-
let found = firstPatternSymbol(line, patterns);
|
|
120
|
-
if (!found && !python) {
|
|
121
|
-
const symbol = genericFunctionSymbol(line);
|
|
122
|
-
if (symbol)
|
|
123
|
-
found = { kind: "function", symbol };
|
|
124
|
-
}
|
|
125
|
-
if (found) {
|
|
126
|
-
const { kind, symbol } = found;
|
|
127
|
-
const visibility = line.match(/\b(public|private|protected|internal)\b/)?.[1] ??
|
|
128
|
-
(python && symbol.startsWith("_") ? "private" : "default");
|
|
129
|
-
symbols.push({
|
|
130
|
-
identity: null,
|
|
131
|
-
symbol,
|
|
132
|
-
kind,
|
|
133
|
-
line: index + 1,
|
|
134
|
-
endLine: lexicalEndLine(lines, index, python),
|
|
135
|
-
signature: line
|
|
136
|
-
.trim()
|
|
137
|
-
.split(/\s*(?:\{|=>)\s*/, 1)[0]
|
|
138
|
-
.slice(0, 500),
|
|
139
|
-
visibility,
|
|
140
|
-
exported: /\bexport\b/.test(line) || /\bpub\b/.test(line),
|
|
141
|
-
evidenceQuality: "lexical-structural-fallback",
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return symbols;
|
|
146
|
-
}
|
|
147
|
-
function directFile(repo, filePath) {
|
|
148
|
-
if (!DIRECT_FILE_EXTENSIONS.has(path.extname(filePath).toLowerCase()))
|
|
149
|
-
return null;
|
|
150
|
-
const root = fs.realpathSync(repo);
|
|
151
|
-
const absolute = path.resolve(root, filePath);
|
|
152
|
-
let real;
|
|
153
|
-
try {
|
|
154
|
-
real = fs.realpathSync(absolute);
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
const relative = path.relative(root, real);
|
|
160
|
-
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
161
|
-
return null;
|
|
162
|
-
const content = fs.readFileSync(real, "utf8");
|
|
163
|
-
return {
|
|
164
|
-
path: relative.split(path.sep).join("/"),
|
|
165
|
-
sizeBytes: Buffer.byteLength(content),
|
|
166
|
-
mtimeMs: fs.statSync(real).mtimeMs,
|
|
167
|
-
contentFingerprint: contentFingerprint(content),
|
|
168
|
-
symbols: directSymbols(filePath, content),
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
export function runStructuralFastPath(repo, pattern, target, limit = 100) {
|
|
172
|
-
const snapshot = readStructuralSnapshot(repo);
|
|
173
|
-
if (!snapshot && pattern !== "file_summary")
|
|
174
|
-
return null;
|
|
175
|
-
let matching = (snapshot?.files ?? []).filter((file) => !target || file.path.includes(target));
|
|
176
|
-
if (pattern === "file_summary") {
|
|
177
|
-
const indexed = matching.find((file) => file.path === target);
|
|
178
|
-
if (!indexed || !exactFile(repo, indexed)) {
|
|
179
|
-
const direct = directFile(repo, target);
|
|
180
|
-
if (!direct)
|
|
181
|
-
return null;
|
|
182
|
-
matching = [direct];
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
else if (pattern === "batch_outline") {
|
|
186
|
-
matching = matching.flatMap((file) => {
|
|
187
|
-
if (exactFile(repo, file))
|
|
188
|
-
return [file];
|
|
189
|
-
const direct = directFile(repo, file.path);
|
|
190
|
-
return direct ? [direct] : [];
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
let rows = [];
|
|
194
|
-
let fingerprint;
|
|
195
|
-
let fresh = false;
|
|
196
|
-
if (pattern === "project_overview") {
|
|
197
|
-
fresh = matching.every((file) => {
|
|
198
|
-
try {
|
|
199
|
-
const stat = fs.statSync(path.resolve(repo, file.path));
|
|
200
|
-
return stat.size === file.sizeBytes && stat.mtimeMs === file.mtimeMs;
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
const directories = new Map();
|
|
207
|
-
for (const file of matching) {
|
|
208
|
-
const directory = file.path.includes("/") ? file.path.split("/", 1)[0] : ".";
|
|
209
|
-
const value = directories.get(directory) ?? { fileCount: 0, sizeBytes: 0 };
|
|
210
|
-
value.fileCount += 1;
|
|
211
|
-
value.sizeBytes += file.sizeBytes;
|
|
212
|
-
directories.set(directory, value);
|
|
213
|
-
}
|
|
214
|
-
rows = [...directories]
|
|
215
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
216
|
-
.map(([symbol, value]) => ({ symbol, kind: "directory", ...value }));
|
|
217
|
-
fingerprint = contentFingerprint(JSON.stringify(matching.map(({ path: file, contentFingerprint: hash }) => [file, hash])));
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
const files = pattern === "file_summary" ? matching.filter((file) => file.path === target) : matching;
|
|
221
|
-
fresh = files.length > 0 && files.every((file) => exactFile(repo, file));
|
|
222
|
-
fingerprint =
|
|
223
|
-
files.length === 1
|
|
224
|
-
? files[0].contentFingerprint
|
|
225
|
-
: contentFingerprint(JSON.stringify(files.map((file) => [file.path, file.contentFingerprint])));
|
|
226
|
-
rows = files.flatMap((file) => file.symbols.map((symbol) => ({ ...symbol, file: file.path })));
|
|
227
|
-
}
|
|
228
|
-
const selected = rows.slice(0, limit);
|
|
229
|
-
return {
|
|
230
|
-
pattern,
|
|
231
|
-
target,
|
|
232
|
-
count: selected.length,
|
|
233
|
-
results: selected,
|
|
234
|
-
...(rows.length > selected.length ? { hasMore: true } : {}),
|
|
235
|
-
staleness: fresh ? "fresh" : "unknown",
|
|
236
|
-
graphEnriched: false,
|
|
237
|
-
evidence: {
|
|
238
|
-
...(pattern === "file_summary" ? { path: target } : {}),
|
|
239
|
-
contentFingerprint: fingerprint,
|
|
240
|
-
evidenceQuality: matching.some((file) => file.symbols.some((symbol) => symbol.evidenceQuality === "lexical-structural-fallback"))
|
|
241
|
-
? "direct-lexical-parser"
|
|
242
|
-
: "indexed-structural-snapshot",
|
|
243
|
-
uniqueness: pattern === "project_overview" ? "not-applicable" : "file-scoped-only",
|
|
244
|
-
evidenceGeneratedAt: snapshot?.generatedAt ?? new Date().toISOString(),
|
|
245
|
-
},
|
|
246
|
-
upgrade: pattern === "file_summary"
|
|
247
|
-
? { operation: "explain", target }
|
|
248
|
-
: pattern === "project_overview"
|
|
249
|
-
? { operation: "query", pattern: "architecture_overview", target: "" }
|
|
250
|
-
: { operation: "query", pattern: "impact", target },
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
export function runExactSymbolFastPath(repo, symbol, filePath) {
|
|
254
|
-
const snapshot = readStructuralSnapshot(repo);
|
|
255
|
-
const indexed = snapshot?.files.find((candidate) => candidate.path === filePath);
|
|
256
|
-
const file = indexed && exactFile(repo, indexed) ? indexed : directFile(repo, filePath);
|
|
257
|
-
if (!file)
|
|
258
|
-
return null;
|
|
259
|
-
const matches = file.symbols.filter((candidate) => candidate.symbol === symbol);
|
|
260
|
-
if (matches.length === 0)
|
|
261
|
-
return null;
|
|
262
|
-
const fresh = exactFile(repo, file);
|
|
263
|
-
const evidence = {
|
|
264
|
-
path: filePath,
|
|
265
|
-
contentFingerprint: file.contentFingerprint,
|
|
266
|
-
evidenceQuality: file.symbols.some((candidate) => candidate.evidenceQuality === "lexical-structural-fallback")
|
|
267
|
-
? "direct-lexical-parser"
|
|
268
|
-
: "indexed-structural-snapshot",
|
|
269
|
-
uniqueness: "file-scoped-only",
|
|
270
|
-
evidenceGeneratedAt: snapshot?.generatedAt ?? new Date().toISOString(),
|
|
271
|
-
};
|
|
272
|
-
if (matches.length > 1) {
|
|
273
|
-
return {
|
|
274
|
-
symbol,
|
|
275
|
-
file: filePath,
|
|
276
|
-
ambiguity: {
|
|
277
|
-
candidates: matches.map((match) => ({
|
|
278
|
-
identity: match.identity ?? "",
|
|
279
|
-
file: filePath,
|
|
280
|
-
kind: match.kind,
|
|
281
|
-
line: match.line,
|
|
282
|
-
})),
|
|
283
|
-
},
|
|
284
|
-
staleness: fresh ? "fresh" : "unknown",
|
|
285
|
-
graphEnriched: false,
|
|
286
|
-
evidence,
|
|
287
|
-
upgrade: { operation: "explain", target: symbol },
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
const match = matches[0];
|
|
291
|
-
const lines = fs.readFileSync(path.resolve(repo, filePath), "utf8").split(/\r?\n/);
|
|
292
|
-
const source = fresh
|
|
293
|
-
? lines
|
|
294
|
-
.slice(match.line - 1, match.endLine)
|
|
295
|
-
.map((line, offset) => `${match.line + offset}: ${line}`)
|
|
296
|
-
.join("\n")
|
|
297
|
-
: undefined;
|
|
298
|
-
return {
|
|
299
|
-
symbol,
|
|
300
|
-
...(match.identity ? { identity: match.identity } : {}),
|
|
301
|
-
file: filePath,
|
|
302
|
-
...(source ? { source } : {}),
|
|
303
|
-
staleness: fresh ? "fresh" : "unknown",
|
|
304
|
-
graphEnriched: false,
|
|
305
|
-
evidence,
|
|
306
|
-
upgrade: { operation: "explain", target: symbol },
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
export function tryStructuralFastPath(argv, cwd = process.cwd()) {
|
|
310
|
-
const valueAfter = (flag) => {
|
|
311
|
-
const index = argv.indexOf(flag);
|
|
312
|
-
return index >= 0
|
|
313
|
-
? argv[index + 1]
|
|
314
|
-
: argv.find((value) => value.startsWith(`${flag}=`))?.slice(flag.length + 1);
|
|
315
|
-
};
|
|
316
|
-
if (argv[0] === "explain" && argv[1] && valueAfter("--file")) {
|
|
317
|
-
const repo = path.resolve(valueAfter("--repo") ?? cwd);
|
|
318
|
-
const result = runExactSymbolFastPath(repo, argv[1], valueAfter("--file"));
|
|
319
|
-
if (!result)
|
|
320
|
-
return false;
|
|
321
|
-
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
322
|
-
return true;
|
|
323
|
-
}
|
|
324
|
-
if (argv[0] !== "query" ||
|
|
325
|
-
!["file_summary", "batch_outline", "project_overview"].includes(argv[1] ?? ""))
|
|
326
|
-
return false;
|
|
327
|
-
const pattern = argv[1];
|
|
328
|
-
const repo = path.resolve(valueAfter("--repo") ?? cwd);
|
|
329
|
-
const target = pattern === "project_overview" ? "" : (argv[2] ?? "");
|
|
330
|
-
const limit = Number(valueAfter("--limit") ?? 100);
|
|
331
|
-
if (!target && pattern !== "project_overview")
|
|
332
|
-
return false;
|
|
333
|
-
const result = runStructuralFastPath(repo, pattern, target, Number.isInteger(limit) && limit > 0 ? limit : 100);
|
|
334
|
-
if (!result)
|
|
335
|
-
return false;
|
|
336
|
-
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
337
|
-
return true;
|
|
338
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
export const STRUCTURAL_SNAPSHOT_VERSION = 1;
|
|
5
|
-
export function contentFingerprint(content) {
|
|
6
|
-
return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
|
|
7
|
-
}
|
|
8
|
-
export function structuralSnapshotPath(repo) {
|
|
9
|
-
return path.join(path.resolve(repo), ".knodin", "structural-v1.json");
|
|
10
|
-
}
|
|
11
|
-
export function writeStructuralSnapshot(repo, files) {
|
|
12
|
-
const destination = structuralSnapshotPath(repo);
|
|
13
|
-
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
14
|
-
const snapshot = {
|
|
15
|
-
schemaVersion: STRUCTURAL_SNAPSHOT_VERSION,
|
|
16
|
-
generatedAt: new Date().toISOString(),
|
|
17
|
-
files: [...files].sort((left, right) => left.path.localeCompare(right.path)),
|
|
18
|
-
};
|
|
19
|
-
const temporary = `${destination}.${process.pid}.tmp`;
|
|
20
|
-
fs.writeFileSync(temporary, `${JSON.stringify(snapshot)}\n`, { mode: 0o600 });
|
|
21
|
-
fs.renameSync(temporary, destination);
|
|
22
|
-
}
|
|
23
|
-
export function readStructuralSnapshot(repo) {
|
|
24
|
-
try {
|
|
25
|
-
const parsed = JSON.parse(fs.readFileSync(structuralSnapshotPath(repo), "utf8"));
|
|
26
|
-
return parsed?.schemaVersion === STRUCTURAL_SNAPSHOT_VERSION && Array.isArray(parsed.files)
|
|
27
|
-
? parsed
|
|
28
|
-
: null;
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
}
|