knodin 0.7.5 → 0.8.2
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 +18 -3
- package/benchmarks/competitors/SYNTHESIS.md +66 -0
- package/dist/bin/cli.js +371 -66
- package/dist/bin/launcher.js +16 -1
- package/dist/src/agent-integration.js +82 -16
- package/dist/src/artifact-refresh.js +2 -1
- package/dist/src/cli-args.js +19 -1
- package/dist/src/cli-model.js +28 -2
- package/dist/src/codeflow-replay.js +2 -1
- package/dist/src/compare.js +39 -0
- package/dist/src/competitive-constraints.js +2 -1
- package/dist/src/competitive-runner.js +4 -4
- package/dist/src/context-export.js +3 -2
- package/dist/src/context.js +1 -1
- package/dist/src/deterministic-random.js +34 -0
- package/dist/src/diagnostics-write-helper.js +473 -0
- package/dist/src/diagnostics.js +1160 -133
- package/dist/src/doctor.js +3 -1
- package/dist/src/engine/ann-hnsw.js +2 -12
- package/dist/src/engine/file-walker.js +8 -2
- package/dist/src/engine/git-history.js +12 -12
- package/dist/src/engine/index.js +1174 -313
- package/dist/src/engine/sarif-import.js +341 -0
- package/dist/src/engine/scip-import.js +28 -13
- package/dist/src/engine/source-policy.js +16 -0
- package/dist/src/engine/state-paths.js +175 -0
- package/dist/src/execution-profile.js +15 -10
- package/dist/src/failure-diagnosis.js +7 -1
- package/dist/src/graph-layout.js +173 -0
- package/dist/src/index-activity.js +2 -1
- package/dist/src/init.js +86 -45
- package/dist/src/lifecycle-health.js +41 -9
- package/dist/src/mcp-graph-worker.js +69 -0
- package/dist/src/mcp-reliability.js +154 -0
- package/dist/src/mcp-worker-supervisor.js +350 -0
- package/dist/src/mirror.js +290 -0
- package/dist/src/node-runtime.js +157 -0
- package/dist/src/output-compression.js +2 -1
- package/dist/src/output-telemetry.js +16 -11
- package/dist/src/progressive-evidence.js +30 -26
- package/dist/src/pure-compression-cli.js +4 -3
- package/dist/src/relationship-adapters.js +15 -8
- package/dist/src/release-preflight.js +13 -10
- package/dist/src/repair-lease.js +85 -0
- package/dist/src/repository-init-process.js +13 -9
- package/dist/src/repository-management.js +34 -4
- package/dist/src/response-budget.js +8 -6
- package/dist/src/server.js +80 -35
- package/dist/src/structural-fast-path.js +16 -10
- package/dist/src/structural-snapshot.js +6 -2
- package/dist/src/system-config.js +25 -2
- package/dist/src/tools/knodin-tools.js +142 -31
- package/dist/src/update-ceremony.js +9 -5
- package/dist/src/update-trust.js +5 -4
- package/dist/src/visualization.js +372 -19
- package/dist/src/worktree-lifecycle.js +5 -2
- package/docs/BEHAVIORAL-CONTRACT.md +72 -0
- package/docs/CLI.md +20 -1
- package/docs/COMPARISON.md +403 -0
- package/docs/COMPETITIVE-LANDSCAPE-2026-08.md +267 -0
- package/docs/DIAGNOSTICS.md +46 -11
- package/docs/HANDOFF.md +180 -0
- package/docs/INSTALLATION.md +21 -2
- package/docs/MCP.md +59 -8
- package/docs/PT-ACCESS-RECOMMENDATION.md +5 -7
- package/docs/REPOSITORIES-AND-WORKTREES.md +18 -6
- package/docs/SCIP-IMPORT.md +5 -0
- package/docs/TOKEN-OPTIMIZER-SCORECARD.md +79 -0
- package/docs/releases/0.5.1.md +4 -4
- package/docs/releases/0.8.0.md +74 -0
- package/docs/releases/0.8.2.md +34 -0
- package/package.json +17 -4
- package/roadmap/competitive-roadmap.md +3801 -0
- package/schemas/release-attestation-v1.schema.json +1 -1
- package/schemas/support-bundle-v2.schema.json +212 -0
package/dist/bin/launcher.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { handoffCurrentProcessToSupportedNodeRuntime } from "../src/node-runtime.js";
|
|
3
|
+
let runtime;
|
|
4
|
+
try {
|
|
5
|
+
runtime = handoffCurrentProcessToSupportedNodeRuntime();
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9
|
+
process.stderr.write(`knodin: ${message}\n`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
if (runtime.kind === "relaunched") {
|
|
13
|
+
if (runtime.signal)
|
|
14
|
+
process.kill(process.pid, runtime.signal);
|
|
15
|
+
process.exit(runtime.status ?? 1);
|
|
16
|
+
}
|
|
3
17
|
const argv = process.argv.slice(2);
|
|
18
|
+
const { tryStructuralFastPath } = await import("../src/structural-fast-path.js");
|
|
4
19
|
const fast = tryStructuralFastPath(argv);
|
|
5
20
|
if (!fast) {
|
|
6
21
|
const { tryPureCompressionFastPath } = await import("../src/pure-compression-cli.js");
|
|
@@ -17,10 +17,33 @@ ${TOML_END}`;
|
|
|
17
17
|
function errorMessage(error) {
|
|
18
18
|
return error instanceof Error ? error.message : String(error);
|
|
19
19
|
}
|
|
20
|
-
function
|
|
20
|
+
function commandPath(command) {
|
|
21
21
|
try {
|
|
22
|
-
|
|
23
|
-
return
|
|
22
|
+
const resolver = process.platform === "win32" ? "where.exe" : "which";
|
|
23
|
+
return (child_process
|
|
24
|
+
.execFileSync(resolver, [command], {
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
27
|
+
})
|
|
28
|
+
.split(/\r?\n/, 1)[0]
|
|
29
|
+
?.trim() || null);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function hasGitHubCopilotExtension() {
|
|
36
|
+
const code = commandPath("code");
|
|
37
|
+
if (!code)
|
|
38
|
+
return false;
|
|
39
|
+
try {
|
|
40
|
+
const extensions = child_process.execFileSync(code, ["--list-extensions"], {
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
43
|
+
});
|
|
44
|
+
return extensions
|
|
45
|
+
.split(/\r?\n/)
|
|
46
|
+
.some((extension) => ["github.copilot", "github.copilot-chat"].includes(extension.toLowerCase()));
|
|
24
47
|
}
|
|
25
48
|
catch {
|
|
26
49
|
return false;
|
|
@@ -29,13 +52,15 @@ function commandExists(command) {
|
|
|
29
52
|
/** Detect primary MCP-capable coding agents without launching them. */
|
|
30
53
|
export function detectSupportedAgents() {
|
|
31
54
|
const detected = [];
|
|
32
|
-
if (
|
|
55
|
+
if (commandPath("claude"))
|
|
33
56
|
detected.push("claude");
|
|
34
|
-
if (
|
|
57
|
+
if (commandPath("codex"))
|
|
35
58
|
detected.push("codex");
|
|
36
|
-
if (
|
|
59
|
+
if (commandPath("gemini"))
|
|
37
60
|
detected.push("gemini");
|
|
38
|
-
if (
|
|
61
|
+
if (hasGitHubCopilotExtension())
|
|
62
|
+
detected.push("copilot");
|
|
63
|
+
if (commandPath("antigravity") || commandPath("agy"))
|
|
39
64
|
detected.push("antigravity");
|
|
40
65
|
return detected;
|
|
41
66
|
}
|
|
@@ -126,6 +151,15 @@ function assertSafeProjectPath(repo, filePath) {
|
|
|
126
151
|
function formatProjectJson(document) {
|
|
127
152
|
return `${JSON.stringify(document, null, "\t").replace(/"args": \[\n(\t+)"serve"\n\t+\]/g, '"args": ["serve"]')}\n`;
|
|
128
153
|
}
|
|
154
|
+
function removeEmptyParent(filePath) {
|
|
155
|
+
try {
|
|
156
|
+
fs.rmdirSync(path.dirname(filePath));
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? ""))
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
129
163
|
function updateJsonMcp(filePath, remove) {
|
|
130
164
|
const document = readJsonObject(filePath);
|
|
131
165
|
const existing = document.mcpServers;
|
|
@@ -148,6 +182,40 @@ function updateJsonMcp(filePath, remove) {
|
|
|
148
182
|
fs.writeFileSync(filePath, formatProjectJson(document), "utf-8");
|
|
149
183
|
return true;
|
|
150
184
|
}
|
|
185
|
+
function updateVsCodeMcp(filePath, remove) {
|
|
186
|
+
const document = readJsonObject(filePath);
|
|
187
|
+
const existing = document.servers;
|
|
188
|
+
if (existing !== undefined &&
|
|
189
|
+
(typeof existing !== "object" || existing === null || Array.isArray(existing)))
|
|
190
|
+
throw new Error(`${filePath}: expected "servers" to be an object`);
|
|
191
|
+
const servers = typeof existing === "object" && existing !== null && !Array.isArray(existing)
|
|
192
|
+
? { ...existing }
|
|
193
|
+
: {};
|
|
194
|
+
if (remove)
|
|
195
|
+
delete servers[MCP_NAME];
|
|
196
|
+
else
|
|
197
|
+
servers[MCP_NAME] = { type: "stdio", command: "knodin", args: ["serve"] };
|
|
198
|
+
if (Object.keys(servers).length > 0)
|
|
199
|
+
document.servers = servers;
|
|
200
|
+
else
|
|
201
|
+
delete document.servers;
|
|
202
|
+
if (remove && Object.keys(document).length === 0) {
|
|
203
|
+
fs.rmSync(filePath, { force: true });
|
|
204
|
+
removeEmptyParent(filePath);
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
208
|
+
fs.writeFileSync(filePath, formatProjectJson(document), "utf-8");
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
/** Strips a marker-delimited managed block, leaving surrounding user config intact. */
|
|
212
|
+
function removeManagedBlock(content, startMarker, endMarker) {
|
|
213
|
+
const start = content.indexOf(startMarker);
|
|
214
|
+
const end = content.indexOf(endMarker);
|
|
215
|
+
return start >= 0 && end >= start
|
|
216
|
+
? `${content.slice(0, start)}${content.slice(end + endMarker.length)}`.trim()
|
|
217
|
+
: content.trim();
|
|
218
|
+
}
|
|
151
219
|
function updateTomlMcp(filePath, remove) {
|
|
152
220
|
let existing = "";
|
|
153
221
|
try {
|
|
@@ -157,13 +225,6 @@ function updateTomlMcp(filePath, remove) {
|
|
|
157
225
|
if (error.code !== "ENOENT")
|
|
158
226
|
throw error;
|
|
159
227
|
}
|
|
160
|
-
function removeManagedBlock(content, startMarker, endMarker) {
|
|
161
|
-
const start = content.indexOf(startMarker);
|
|
162
|
-
const end = content.indexOf(endMarker);
|
|
163
|
-
return start >= 0 && end >= start
|
|
164
|
-
? `${content.slice(0, start)}${content.slice(end + endMarker.length)}`.trim()
|
|
165
|
-
: content.trim();
|
|
166
|
-
}
|
|
167
228
|
const without = removeManagedBlock(existing, TOML_START, TOML_END);
|
|
168
229
|
const next = remove ? without : [without, TOML_BLOCK].filter(Boolean).join("\n\n");
|
|
169
230
|
if (!next) {
|
|
@@ -178,8 +239,8 @@ function updateTomlMcp(filePath, remove) {
|
|
|
178
239
|
export function configureProjectAgents(options) {
|
|
179
240
|
const repo = path.resolve(options.repo);
|
|
180
241
|
const remove = options.scope === "cli-only";
|
|
181
|
-
const agents = options.scope === "team"
|
|
182
|
-
? ["claude", "codex", "gemini", "antigravity"]
|
|
242
|
+
const agents = options.scope === "team" || options.scope === "cli-only"
|
|
243
|
+
? ["claude", "codex", "gemini", "copilot", "antigravity"]
|
|
183
244
|
: options.agents;
|
|
184
245
|
const commandResult = options.scope === "team"
|
|
185
246
|
? { configured: [], removed: [], failed: [], projectFiles: [] }
|
|
@@ -216,6 +277,11 @@ export function configureProjectAgents(options) {
|
|
|
216
277
|
assertSafeProjectPath(repo, path.join(repo, relative));
|
|
217
278
|
remains = updateJsonMcp(path.join(repo, relative), remove);
|
|
218
279
|
}
|
|
280
|
+
else if (agent === "copilot") {
|
|
281
|
+
relative = ".vscode/mcp.json";
|
|
282
|
+
assertSafeProjectPath(repo, path.join(repo, relative));
|
|
283
|
+
remains = updateVsCodeMcp(path.join(repo, relative), remove);
|
|
284
|
+
}
|
|
219
285
|
else if (agent === "antigravity") {
|
|
220
286
|
relative = ".agents/mcp_config.json";
|
|
221
287
|
assertSafeProjectPath(repo, path.join(repo, relative));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { resolveStateDir } from "./engine/state-paths.js";
|
|
4
5
|
const REFRESH_TIMEOUT_MS = 30_000;
|
|
5
6
|
function defaultRuntime() {
|
|
6
7
|
return {
|
|
@@ -75,7 +76,7 @@ export function refreshExternalGraphArtifacts(repoPath, event, runtime = default
|
|
|
75
76
|
}
|
|
76
77
|
/** Persist an audit record locally; no source content or tool output is retained. */
|
|
77
78
|
export function writeArtifactRefreshRecord(repoPath, result) {
|
|
78
|
-
const knodinDir =
|
|
79
|
+
const knodinDir = resolveStateDir(repoPath);
|
|
79
80
|
fs.mkdirSync(knodinDir, { recursive: true });
|
|
80
81
|
fs.appendFileSync(path.join(knodinDir, "artifact-refresh.jsonl"), `${JSON.stringify({ at: new Date().toISOString(), ...result })}\n`, "utf8");
|
|
81
82
|
}
|
package/dist/src/cli-args.js
CHANGED
|
@@ -29,6 +29,23 @@ const GLOBAL_VALUE_FLAGS = new Set([
|
|
|
29
29
|
"--limit",
|
|
30
30
|
]);
|
|
31
31
|
const GLOBAL_BOOLEAN_FLAGS = new Set(["--exclude-tests", "--data-flow", "--json"]);
|
|
32
|
+
/**
|
|
33
|
+
* Command-scoped flags that take a value. These are NOT global — they are not
|
|
34
|
+
* hoisted or reordered — but their value must never be mistaken for a
|
|
35
|
+
* positional path. `knodin index --sarif results.sarif` asks to import that log,
|
|
36
|
+
* not to index the log as a source file.
|
|
37
|
+
*/
|
|
38
|
+
const COMMAND_VALUE_FLAGS = new Set([
|
|
39
|
+
"--scip",
|
|
40
|
+
"--scip-max-bytes",
|
|
41
|
+
"--scip-max-files",
|
|
42
|
+
"--scip-max-facts",
|
|
43
|
+
"--scip-timeout-ms",
|
|
44
|
+
"--sarif",
|
|
45
|
+
"--sarif-max-bytes",
|
|
46
|
+
"--sarif-max-findings",
|
|
47
|
+
"--sarif-timeout-ms",
|
|
48
|
+
]);
|
|
32
49
|
const isDir = (p) => {
|
|
33
50
|
try {
|
|
34
51
|
return fs.statSync(p).isDirectory();
|
|
@@ -127,7 +144,8 @@ export function extractPositionals(args) {
|
|
|
127
144
|
for (let index = 0; index < args.length; index++) {
|
|
128
145
|
const argument = args[index];
|
|
129
146
|
if (argument.startsWith("--")) {
|
|
130
|
-
if (!argument.includes("=") &&
|
|
147
|
+
if (!argument.includes("=") &&
|
|
148
|
+
(GLOBAL_VALUE_FLAGS.has(argument) || COMMAND_VALUE_FLAGS.has(argument)))
|
|
131
149
|
index++;
|
|
132
150
|
continue;
|
|
133
151
|
}
|
package/dist/src/cli-model.js
CHANGED
|
@@ -53,6 +53,17 @@ function addGlobalOptions(program) {
|
|
|
53
53
|
.option("--exclude-tests", "exclude tests from impact")
|
|
54
54
|
.option("--data-flow", "include bounded data-flow evidence");
|
|
55
55
|
}
|
|
56
|
+
function addRemoteCommands(program, capture) {
|
|
57
|
+
const remote = program
|
|
58
|
+
.command("remote")
|
|
59
|
+
.description("mirror repositories that are reachable but not checked out locally");
|
|
60
|
+
leaf(remote, "add <url>", "acquire a read-only mirror and index it", capture)
|
|
61
|
+
.option("--no-index", "acquire the clone without building a graph")
|
|
62
|
+
.option("--defer-semantic", "build structure only; leave semantic search coverage for a later `knodin index`");
|
|
63
|
+
leaf(remote, "list", "list acquired mirrors with their snapshot and size", capture);
|
|
64
|
+
leaf(remote, "remove <identity>", "delete a mirror's clone, graph, and registry entry", capture);
|
|
65
|
+
leaf(remote, "refresh <identity>", "fetch the mirror again and re-index it", capture);
|
|
66
|
+
}
|
|
56
67
|
function addRepositoryCommands(program, capture) {
|
|
57
68
|
const repos = program.command("repos").description("manage a portfolio of repositories");
|
|
58
69
|
for (const action of ["discover", "init", "status", "doctor"]) {
|
|
@@ -63,6 +74,7 @@ function addRepositoryCommands(program, capture) {
|
|
|
63
74
|
command
|
|
64
75
|
.addOption(option("--include <selector>", "include a repository id or path", "collect"))
|
|
65
76
|
.addOption(option("--exclude <selector>", "exclude a repository id or path", "collect"))
|
|
77
|
+
.addOption(option("--memory-limit-mib <mib>", "optional per-repository worker RSS ceiling (default: uncapped)", "integer"))
|
|
66
78
|
.option("--manifest <path>", "write or resume a portfolio manifest")
|
|
67
79
|
.option("--dry-run", "report actions without changing repositories");
|
|
68
80
|
}
|
|
@@ -101,8 +113,10 @@ function addGraphCommands(program, capture) {
|
|
|
101
113
|
.option("--sort <mode>", "relevance, name, size, degree, or complexity")
|
|
102
114
|
.option("--relations <kinds>", "comma-separated relationship kinds");
|
|
103
115
|
leaf(program, "wiki", "write local architecture wiki pages", capture).option("--force", "rewrite unchanged pages");
|
|
104
|
-
leaf(program, "visualize
|
|
116
|
+
leaf(program, "visualize [entry]", "write a local architecture/call-flow HTML artifact", capture)
|
|
105
117
|
.requiredOption("--output <path>", "repo-relative HTML output path")
|
|
118
|
+
.option("--scope <scope>", "call-flow (default) or repo")
|
|
119
|
+
.option("--granularity <granularity>", "repo scope: file (default) or symbol")
|
|
106
120
|
.addOption(option("--depth <count>", "call-flow depth", "integer"))
|
|
107
121
|
.addOption(option("--max-bytes <count>", "hard artifact budget", "integer"));
|
|
108
122
|
leaf(program, "search <query> [limit]", "hybrid symbol search", capture)
|
|
@@ -212,7 +226,15 @@ function createCliProgram(capture = () => { }) {
|
|
|
212
226
|
leaf(program, "index [files...]", "index a repository or selected files", capture)
|
|
213
227
|
.option("--clean", "rebuild selected index state")
|
|
214
228
|
.option("--force", "force clean indexing")
|
|
215
|
-
.option("--scip <file>", "opt in to a bounded local SCIP protobuf import")
|
|
229
|
+
.option("--scip <file>", "opt in to a bounded local SCIP protobuf import")
|
|
230
|
+
.addOption(option("--scip-max-bytes <count>", "raise the SCIP input size ceiling", "integer"))
|
|
231
|
+
.addOption(option("--scip-max-files <count>", "raise the SCIP document ceiling", "integer"))
|
|
232
|
+
.addOption(option("--scip-max-facts <count>", "raise the SCIP fact ceiling", "integer"))
|
|
233
|
+
.addOption(option("--scip-timeout-ms <count>", "raise the SCIP read budget", "integer"))
|
|
234
|
+
.option("--sarif <file>", "import bounded local analyzer findings from a SARIF log")
|
|
235
|
+
.addOption(option("--sarif-max-bytes <count>", "raise the SARIF log size ceiling", "integer"))
|
|
236
|
+
.addOption(option("--sarif-max-findings <count>", "raise the imported findings ceiling", "integer"))
|
|
237
|
+
.addOption(option("--sarif-timeout-ms <count>", "raise the SARIF read budget", "integer"));
|
|
216
238
|
leaf(program, "doctor", "diagnose installation, clients, hooks, graph, and updates", capture).option("--client <client>", "claude, codex, gemini, or antigravity");
|
|
217
239
|
leaf(program, "status", "report graph, lifecycle, integration, and update state", capture)
|
|
218
240
|
.option("--deep", "run a full graph audit")
|
|
@@ -231,6 +253,7 @@ function createCliProgram(capture = () => { }) {
|
|
|
231
253
|
leaf(program, "hook-refresh <kind> [values...]", "internal Git lifecycle refresh", capture);
|
|
232
254
|
leaf(program, "refresh-artifacts [event]", "refresh external graph artifacts", capture);
|
|
233
255
|
addRepositoryCommands(program, capture);
|
|
256
|
+
addRemoteCommands(program, capture);
|
|
234
257
|
addGraphCommands(program, capture);
|
|
235
258
|
addArtifactCommands(program, capture);
|
|
236
259
|
const system = program.command("system").description("inspect declared multi-repository systems");
|
|
@@ -273,6 +296,8 @@ function createCliProgram(capture = () => { }) {
|
|
|
273
296
|
.addArgument(new Argument("<action>").choices([
|
|
274
297
|
"enable",
|
|
275
298
|
"status",
|
|
299
|
+
"preview",
|
|
300
|
+
"archive",
|
|
276
301
|
"collect",
|
|
277
302
|
"inspect",
|
|
278
303
|
"clear",
|
|
@@ -283,6 +308,7 @@ function createCliProgram(capture = () => { }) {
|
|
|
283
308
|
.addOption(option("--retention-days <count>", "local event retention in days", "integer"))
|
|
284
309
|
.option("--since <duration>", "collection window, such as 24h or 7d")
|
|
285
310
|
.option("--output <path>", "repository-contained .json.gz bundle path")
|
|
311
|
+
.option("--preview-id <id>", "exact prior preview to archive")
|
|
286
312
|
.action((...values) => capture(values.at(-1)));
|
|
287
313
|
return program;
|
|
288
314
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compareBytes } from "./compare.js";
|
|
1
2
|
export const EXPECTED_RELATIONSHIPS = [
|
|
2
3
|
"packages/api/route.ts->packages/auth/service.ts",
|
|
3
4
|
"packages/api/route.ts->packages/audit/store.ts",
|
|
@@ -60,7 +61,7 @@ export function blastRadiusCompleteness(relationships) {
|
|
|
60
61
|
return {
|
|
61
62
|
origin,
|
|
62
63
|
expected: [...expected],
|
|
63
|
-
found: [...found].sort(),
|
|
64
|
+
found: [...found].sort(compareBytes),
|
|
64
65
|
missing: [...expected].filter((file) => !found.has(file)),
|
|
65
66
|
completeness: [...expected].filter((file) => found.has(file)).length / expected.size,
|
|
66
67
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordering primitives for values that get persisted or compared across machines.
|
|
3
|
+
*
|
|
4
|
+
* `Array.prototype.sort()` with no comparator coerces elements to strings and
|
|
5
|
+
* compares UTF-16 code units. For an array of strings that is already the order
|
|
6
|
+
* these helpers produce — so adopting them is behaviour-preserving. For anything
|
|
7
|
+
* else it is not: `[10, 9].sort()` yields `[10, 9]`, which is the bug the rule
|
|
8
|
+
* exists to catch.
|
|
9
|
+
*
|
|
10
|
+
* The reason these are hand-written rather than `String.localeCompare`:
|
|
11
|
+
* collation is machine-dependent. It varies with the host locale and with the
|
|
12
|
+
* ICU data the runtime was built against, so two machines can order identical
|
|
13
|
+
* input differently. knodin persists sort order into the graph and compares it
|
|
14
|
+
* across checkouts, and a SARIF import shipped with exactly that bug earlier
|
|
15
|
+
* today. Byte order is a function of the bytes alone.
|
|
16
|
+
*/
|
|
17
|
+
/** Lexicographic by UTF-16 code unit — identical to the default sort order. */
|
|
18
|
+
export function compareBytes(left, right) {
|
|
19
|
+
if (left < right)
|
|
20
|
+
return -1;
|
|
21
|
+
return left > right ? 1 : 0;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Byte order, descending.
|
|
25
|
+
*
|
|
26
|
+
* Written out rather than expressed as `compareBytes(right, left)` so call
|
|
27
|
+
* sites do not have to swap their arguments. A swap reads as a bug — and is
|
|
28
|
+
* reported as one, since a reversed call whose parameters share the callee's
|
|
29
|
+
* names is indistinguishable from a genuine mistake.
|
|
30
|
+
*/
|
|
31
|
+
export function compareBytesDescending(left, right) {
|
|
32
|
+
if (left > right)
|
|
33
|
+
return -1;
|
|
34
|
+
return left < right ? 1 : 0;
|
|
35
|
+
}
|
|
36
|
+
/** Numeric ascending. The default sort would order these lexicographically. */
|
|
37
|
+
export function compareNumbers(left, right) {
|
|
38
|
+
return left - right;
|
|
39
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { compareBytes } from "./compare.js";
|
|
1
2
|
/** Verifies the local-only, single-gateway contract that qualifies a competitive replay. */
|
|
2
3
|
export function assessCompetitiveConstraints(tools) {
|
|
3
|
-
const mcpToolNames = tools.map((tool) => tool.name).sort();
|
|
4
|
+
const mcpToolNames = tools.map((tool) => tool.name).sort(compareBytes);
|
|
4
5
|
const schemaBytes = Buffer.byteLength(JSON.stringify(tools));
|
|
5
6
|
const violations = [];
|
|
6
7
|
if (tools.length !== 1)
|
|
@@ -137,10 +137,10 @@ export function applyCompetitiveCaseContracts(competitor, raw, fixtureRevision)
|
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
139
|
const dimensions = [...mapping.scope.dimensions];
|
|
140
|
-
const direction =
|
|
140
|
+
const direction = /^trace-(inbound|outbound|both)-/.exec(caseId)?.[1];
|
|
141
141
|
if (direction)
|
|
142
142
|
dimensions.push(`direction=${direction}`);
|
|
143
|
-
const facet =
|
|
143
|
+
const facet = /^architecture-([a-z_]+)/.exec(caseId)?.[1];
|
|
144
144
|
if (facet)
|
|
145
145
|
dimensions.push(`facet=${facet}`);
|
|
146
146
|
return {
|
|
@@ -290,9 +290,9 @@ export function normalizeRawResult(competitor, raw) {
|
|
|
290
290
|
blocked++;
|
|
291
291
|
const contender = findCompetitorSide(test, competitor);
|
|
292
292
|
const knodin = test.knodin;
|
|
293
|
-
if (contender
|
|
293
|
+
if (contender?.ok === false)
|
|
294
294
|
competitorErrors++;
|
|
295
|
-
if (knodin
|
|
295
|
+
if (knodin?.ok === false)
|
|
296
296
|
knodinErrors++;
|
|
297
297
|
const oracle = test.oracle;
|
|
298
298
|
const contract = test.caseContract;
|
|
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { RE2 } from "re2-wasm";
|
|
5
|
+
import { compareBytes } from "./compare.js";
|
|
5
6
|
import { measurePerfPhaseSync } from "./engine/perf.js";
|
|
6
7
|
const DEFAULT_EXCLUDES = [".git/**", ".knodin/**", "node_modules/**", "dist/**", "build/**"];
|
|
7
8
|
const GLOB_CACHE_LIMIT = 256;
|
|
@@ -221,7 +222,7 @@ export function exportContext(repoPath, request = {}) {
|
|
|
221
222
|
const excludePatterns = compilePatterns(exclude);
|
|
222
223
|
const policyRules = request.policies ?? {};
|
|
223
224
|
const policies = Object.keys(policyRules)
|
|
224
|
-
.sort()
|
|
225
|
+
.sort(compareBytes)
|
|
225
226
|
.map((pattern) => ({
|
|
226
227
|
pattern: globRegex(normalizeRelative(pattern)),
|
|
227
228
|
policy: policyRules[pattern],
|
|
@@ -233,7 +234,7 @@ export function exportContext(repoPath, request = {}) {
|
|
|
233
234
|
.filter((file) => matchesCompiled(file, includePatterns) &&
|
|
234
235
|
!matchesCompiled(file, excludePatterns) &&
|
|
235
236
|
!omitted.has(file))
|
|
236
|
-
.sort();
|
|
237
|
+
.sort(compareBytes);
|
|
237
238
|
let tree = request.includeTree ? [...candidates] : undefined;
|
|
238
239
|
let git = gitSections(repo, request.git);
|
|
239
240
|
const files = [];
|
package/dist/src/context.js
CHANGED
|
@@ -24,7 +24,7 @@ export function suggestNextOperation(task) {
|
|
|
24
24
|
return "map";
|
|
25
25
|
}
|
|
26
26
|
// A bare identifier-shaped token (a single symbol name) → explain it directly.
|
|
27
|
-
if (/^[A-Za-z_]
|
|
27
|
+
if (/^[A-Za-z_]\w*$/.test(task.trim())) {
|
|
28
28
|
return "explain";
|
|
29
29
|
}
|
|
30
30
|
// Otherwise no exact symbol is known → fuzzy search to find a starting point.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic pseudo-random number generation.
|
|
3
|
+
*
|
|
4
|
+
* knodin never calls `Math.random` in production code: index topology, graph
|
|
5
|
+
* layout, and every artifact they feed must be reproducible so that a rerun
|
|
6
|
+
* over unchanged evidence produces a byte-identical result. Callers that need
|
|
7
|
+
* randomness seed one of these generators instead.
|
|
8
|
+
*/
|
|
9
|
+
/** Default seed (golden-ratio constant) shared by callers without their own. */
|
|
10
|
+
export const DEFAULT_PRNG_SEED = 0x9e3779b9;
|
|
11
|
+
/** Deterministic PRNG so derived structures are reproducible across runs. */
|
|
12
|
+
export function mulberry32(seed) {
|
|
13
|
+
let a = seed >>> 0;
|
|
14
|
+
return () => {
|
|
15
|
+
a |= 0;
|
|
16
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
17
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
18
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
19
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Folds a string into a 32-bit seed (FNV-1a). Lets a caller derive a stable
|
|
24
|
+
* seed from repository-scoped evidence such as a commit id, so the same
|
|
25
|
+
* evidence always produces the same output.
|
|
26
|
+
*/
|
|
27
|
+
export function seedFromString(value) {
|
|
28
|
+
let hash = 0x811c9dc5;
|
|
29
|
+
for (let index = 0; index < value.length; index++) {
|
|
30
|
+
hash ^= value.charCodeAt(index);
|
|
31
|
+
hash = Math.imul(hash, 0x01000193);
|
|
32
|
+
}
|
|
33
|
+
return hash >>> 0;
|
|
34
|
+
}
|