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.
Files changed (75) hide show
  1. package/README.md +18 -3
  2. package/benchmarks/competitors/SYNTHESIS.md +66 -0
  3. package/dist/bin/cli.js +371 -66
  4. package/dist/bin/launcher.js +16 -1
  5. package/dist/src/agent-integration.js +82 -16
  6. package/dist/src/artifact-refresh.js +2 -1
  7. package/dist/src/cli-args.js +19 -1
  8. package/dist/src/cli-model.js +28 -2
  9. package/dist/src/codeflow-replay.js +2 -1
  10. package/dist/src/compare.js +39 -0
  11. package/dist/src/competitive-constraints.js +2 -1
  12. package/dist/src/competitive-runner.js +4 -4
  13. package/dist/src/context-export.js +3 -2
  14. package/dist/src/context.js +1 -1
  15. package/dist/src/deterministic-random.js +34 -0
  16. package/dist/src/diagnostics-write-helper.js +473 -0
  17. package/dist/src/diagnostics.js +1160 -133
  18. package/dist/src/doctor.js +3 -1
  19. package/dist/src/engine/ann-hnsw.js +2 -12
  20. package/dist/src/engine/file-walker.js +8 -2
  21. package/dist/src/engine/git-history.js +12 -12
  22. package/dist/src/engine/index.js +1174 -313
  23. package/dist/src/engine/sarif-import.js +341 -0
  24. package/dist/src/engine/scip-import.js +28 -13
  25. package/dist/src/engine/source-policy.js +16 -0
  26. package/dist/src/engine/state-paths.js +175 -0
  27. package/dist/src/execution-profile.js +15 -10
  28. package/dist/src/failure-diagnosis.js +7 -1
  29. package/dist/src/graph-layout.js +173 -0
  30. package/dist/src/index-activity.js +2 -1
  31. package/dist/src/init.js +86 -45
  32. package/dist/src/lifecycle-health.js +41 -9
  33. package/dist/src/mcp-graph-worker.js +69 -0
  34. package/dist/src/mcp-reliability.js +154 -0
  35. package/dist/src/mcp-worker-supervisor.js +350 -0
  36. package/dist/src/mirror.js +290 -0
  37. package/dist/src/node-runtime.js +157 -0
  38. package/dist/src/output-compression.js +2 -1
  39. package/dist/src/output-telemetry.js +16 -11
  40. package/dist/src/progressive-evidence.js +30 -26
  41. package/dist/src/pure-compression-cli.js +4 -3
  42. package/dist/src/relationship-adapters.js +15 -8
  43. package/dist/src/release-preflight.js +13 -10
  44. package/dist/src/repair-lease.js +85 -0
  45. package/dist/src/repository-init-process.js +13 -9
  46. package/dist/src/repository-management.js +34 -4
  47. package/dist/src/response-budget.js +8 -6
  48. package/dist/src/server.js +80 -35
  49. package/dist/src/structural-fast-path.js +16 -10
  50. package/dist/src/structural-snapshot.js +6 -2
  51. package/dist/src/system-config.js +25 -2
  52. package/dist/src/tools/knodin-tools.js +142 -31
  53. package/dist/src/update-ceremony.js +9 -5
  54. package/dist/src/update-trust.js +5 -4
  55. package/dist/src/visualization.js +372 -19
  56. package/dist/src/worktree-lifecycle.js +5 -2
  57. package/docs/BEHAVIORAL-CONTRACT.md +72 -0
  58. package/docs/CLI.md +20 -1
  59. package/docs/COMPARISON.md +403 -0
  60. package/docs/COMPETITIVE-LANDSCAPE-2026-08.md +267 -0
  61. package/docs/DIAGNOSTICS.md +46 -11
  62. package/docs/HANDOFF.md +180 -0
  63. package/docs/INSTALLATION.md +21 -2
  64. package/docs/MCP.md +59 -8
  65. package/docs/PT-ACCESS-RECOMMENDATION.md +5 -7
  66. package/docs/REPOSITORIES-AND-WORKTREES.md +18 -6
  67. package/docs/SCIP-IMPORT.md +5 -0
  68. package/docs/TOKEN-OPTIMIZER-SCORECARD.md +79 -0
  69. package/docs/releases/0.5.1.md +4 -4
  70. package/docs/releases/0.8.0.md +74 -0
  71. package/docs/releases/0.8.2.md +34 -0
  72. package/package.json +17 -4
  73. package/roadmap/competitive-roadmap.md +3801 -0
  74. package/schemas/release-attestation-v1.schema.json +1 -1
  75. package/schemas/support-bundle-v2.schema.json +212 -0
@@ -1,6 +1,21 @@
1
1
  #!/usr/bin/env node
2
- import { tryStructuralFastPath } from "../src/structural-fast-path.js";
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 commandExists(command) {
20
+ function commandPath(command) {
21
21
  try {
22
- child_process.execFileSync("which", [command], { stdio: "ignore" });
23
- return true;
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 (commandExists("claude"))
55
+ if (commandPath("claude"))
33
56
  detected.push("claude");
34
- if (commandExists("codex"))
57
+ if (commandPath("codex"))
35
58
  detected.push("codex");
36
- if (commandExists("gemini"))
59
+ if (commandPath("gemini"))
37
60
  detected.push("gemini");
38
- if (commandExists("antigravity") || commandExists("agy"))
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 = path.join(path.resolve(repoPath), ".knodin");
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
  }
@@ -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("=") && GLOBAL_VALUE_FLAGS.has(argument))
147
+ if (!argument.includes("=") &&
148
+ (GLOBAL_VALUE_FLAGS.has(argument) || COMMAND_VALUE_FLAGS.has(argument)))
131
149
  index++;
132
150
  continue;
133
151
  }
@@ -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 <entry>", "write a local architecture/call-flow HTML artifact", capture)
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 = caseId.match(/^trace-(inbound|outbound|both)-/)?.[1];
140
+ const direction = /^trace-(inbound|outbound|both)-/.exec(caseId)?.[1];
141
141
  if (direction)
142
142
  dimensions.push(`direction=${direction}`);
143
- const facet = caseId.match(/^architecture-([a-z_]+)/)?.[1];
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 && contender.ok === false)
293
+ if (contender?.ok === false)
294
294
  competitorErrors++;
295
- if (knodin && knodin.ok === false)
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 = [];
@@ -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_][A-Za-z0-9_]*$/.test(task.trim())) {
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
+ }