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/src/server.js
CHANGED
|
@@ -12,7 +12,9 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
12
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
13
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
14
14
|
import { recordDiagnosticFailure } from "./diagnostics.js";
|
|
15
|
-
import {
|
|
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";
|
|
16
18
|
import { KNODIN_VERSION } from "./version.js";
|
|
17
19
|
const RETRY_SAFE_OPERATIONS = new Set([
|
|
18
20
|
"architecture_overview",
|
|
@@ -25,11 +27,12 @@ const RETRY_SAFE_OPERATIONS = new Set([
|
|
|
25
27
|
"search",
|
|
26
28
|
"status",
|
|
27
29
|
]);
|
|
28
|
-
|
|
30
|
+
const RECOVERY_LANE_OPERATIONS = new Set(["docs"]);
|
|
31
|
+
class SafeGatewayScheduler {
|
|
29
32
|
tail = Promise.resolve();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
async run(operation, signal, task) {
|
|
34
|
+
if (RECOVERY_LANE_OPERATIONS.has(operation))
|
|
35
|
+
return task();
|
|
33
36
|
const predecessor = this.tail;
|
|
34
37
|
let release;
|
|
35
38
|
this.tail = new Promise((resolve) => {
|
|
@@ -41,10 +44,9 @@ class GatewayRequestQueue {
|
|
|
41
44
|
throw Object.assign(new Error("MCP request cancelled before dispatch"), {
|
|
42
45
|
code: "KNODIN_REQUEST_CANCELLED",
|
|
43
46
|
});
|
|
44
|
-
return await task(
|
|
47
|
+
return await task();
|
|
45
48
|
}
|
|
46
49
|
finally {
|
|
47
|
-
this.pending--;
|
|
48
50
|
release();
|
|
49
51
|
}
|
|
50
52
|
}
|
|
@@ -65,16 +67,21 @@ export function recordMcpDiagnosticFailure(args, error, defaultRepo = process.cw
|
|
|
65
67
|
phase: "dispatch",
|
|
66
68
|
error,
|
|
67
69
|
});
|
|
70
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
68
71
|
return diagnostic.recorded
|
|
69
|
-
? new Error(`${
|
|
72
|
+
? new Error(`${message} [diagnostic ${diagnostic.correlationId}]`, { cause: error })
|
|
70
73
|
: error;
|
|
71
74
|
}
|
|
72
75
|
export function createServer(dispatch = handleKnodinTool) {
|
|
73
|
-
const
|
|
76
|
+
const scheduler = new SafeGatewayScheduler();
|
|
77
|
+
const workers = new WorkerSupervisor();
|
|
74
78
|
const server = new Server({ name: "knodin", version: KNODIN_VERSION }, {
|
|
75
79
|
capabilities: { tools: {} },
|
|
76
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.",
|
|
77
81
|
});
|
|
82
|
+
server.onclose = () => {
|
|
83
|
+
void workers.close();
|
|
84
|
+
};
|
|
78
85
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
79
86
|
tools: getKnodinTools(),
|
|
80
87
|
}));
|
|
@@ -88,53 +95,90 @@ export function createServer(dispatch = handleKnodinTool) {
|
|
|
88
95
|
const progressToken = extra._meta?.progressToken;
|
|
89
96
|
const requestId = String(extra.requestId);
|
|
90
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;
|
|
91
101
|
let heartbeat;
|
|
102
|
+
recordMcpLifecycle(context, "start");
|
|
92
103
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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" })),
|
|
105
144
|
};
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
: `Starting ${operation}`);
|
|
109
|
-
heartbeat = startHeartbeat(notify, operation, startedAt);
|
|
110
|
-
// Keep the production handler explicit: knodin's own source-evidence
|
|
111
|
-
// extractor maps this low-level MCP registration to its dispatcher.
|
|
112
|
-
return dispatch === handleKnodinTool ? await handleKnodinTool(args) : await dispatch(args);
|
|
113
|
-
});
|
|
145
|
+
const result = execution.result;
|
|
146
|
+
recordMcpLifecycle(context, "success", { elapsedMs: Date.now() - startedAt });
|
|
114
147
|
return {
|
|
148
|
+
_meta: {
|
|
149
|
+
requestId: context.requestId,
|
|
150
|
+
traceId: context.traceId,
|
|
151
|
+
...("predecessorTraceId" in execution
|
|
152
|
+
? { predecessorTraceId: execution.predecessorTraceId }
|
|
153
|
+
: {}),
|
|
154
|
+
},
|
|
115
155
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
116
156
|
};
|
|
117
157
|
}
|
|
118
158
|
catch (error) {
|
|
159
|
+
const failure = classifyMcpFailure(error);
|
|
160
|
+
recordMcpLifecycle(context, "failure", {
|
|
161
|
+
kind: failure.kind,
|
|
162
|
+
elapsedMs: Date.now() - startedAt,
|
|
163
|
+
});
|
|
119
164
|
const recorded = recordMcpDiagnosticFailure(values, error);
|
|
120
165
|
const message = recorded instanceof Error ? recorded.message : String(recorded);
|
|
121
|
-
const traceId = /\[diagnostic ([a-f0-9]{16})\]/.exec(message)?.[1] ?? `mcp-${requestId}`;
|
|
122
166
|
const retrySafe = RETRY_SAFE_OPERATIONS.has(operation);
|
|
123
167
|
return {
|
|
124
168
|
isError: true,
|
|
169
|
+
_meta: { requestId: context.requestId, traceId: context.traceId },
|
|
125
170
|
content: [
|
|
126
171
|
{
|
|
127
172
|
type: "text",
|
|
128
173
|
text: JSON.stringify({
|
|
129
|
-
code:
|
|
130
|
-
|
|
131
|
-
: "KNODIN_OPERATION_FAILED",
|
|
174
|
+
code: failure.code,
|
|
175
|
+
kind: failure.kind,
|
|
132
176
|
message,
|
|
133
|
-
traceId,
|
|
177
|
+
traceId: context.traceId,
|
|
134
178
|
requestId,
|
|
135
179
|
operation,
|
|
136
|
-
repository: typeof values?.repoPath === "string" ? values.repoPath : process.cwd(),
|
|
137
180
|
phase: "dispatch",
|
|
181
|
+
deadlineMs: context.deadlineMs,
|
|
138
182
|
retrySafe,
|
|
139
183
|
diagnosticLog: ".knodin/diagnostics/events.jsonl (only when opt-in diagnostics are enabled)",
|
|
140
184
|
recovery: retrySafe
|
|
@@ -148,6 +192,7 @@ export function createServer(dispatch = handleKnodinTool) {
|
|
|
148
192
|
finally {
|
|
149
193
|
if (heartbeat)
|
|
150
194
|
clearInterval(heartbeat);
|
|
195
|
+
recordMcpLifecycle(context, "cleanup", { elapsedMs: Date.now() - startedAt });
|
|
151
196
|
}
|
|
152
197
|
});
|
|
153
198
|
return server;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { compareBytes } from "./compare.js";
|
|
3
4
|
import { contentFingerprint, readStructuralSnapshot, } from "./structural-snapshot.js";
|
|
4
5
|
function exactFile(repo, file) {
|
|
5
6
|
try {
|
|
@@ -124,7 +125,7 @@ function directSymbols(filePath, content) {
|
|
|
124
125
|
}
|
|
125
126
|
if (found) {
|
|
126
127
|
const { kind, symbol } = found;
|
|
127
|
-
const visibility =
|
|
128
|
+
const visibility = /\b(public|private|protected|internal)\b/.exec(line)?.[1] ??
|
|
128
129
|
(python && symbol.startsWith("_") ? "private" : "default");
|
|
129
130
|
symbols.push({
|
|
130
131
|
identity: null,
|
|
@@ -212,7 +213,9 @@ export function runStructuralFastPath(repo, pattern, target, limit = 100) {
|
|
|
212
213
|
directories.set(directory, value);
|
|
213
214
|
}
|
|
214
215
|
rows = [...directories]
|
|
215
|
-
|
|
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))
|
|
216
219
|
.map(([symbol, value]) => ({ symbol, kind: "directory", ...value }));
|
|
217
220
|
fingerprint = contentFingerprint(JSON.stringify(matching.map(({ path: file, contentFingerprint: hash }) => [file, hash])));
|
|
218
221
|
}
|
|
@@ -226,6 +229,13 @@ export function runStructuralFastPath(repo, pattern, target, limit = 100) {
|
|
|
226
229
|
rows = files.flatMap((file) => file.symbols.map((symbol) => ({ ...symbol, file: file.path })));
|
|
227
230
|
}
|
|
228
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 };
|
|
229
239
|
return {
|
|
230
240
|
pattern,
|
|
231
241
|
target,
|
|
@@ -243,11 +253,7 @@ export function runStructuralFastPath(repo, pattern, target, limit = 100) {
|
|
|
243
253
|
uniqueness: pattern === "project_overview" ? "not-applicable" : "file-scoped-only",
|
|
244
254
|
evidenceGeneratedAt: snapshot?.generatedAt ?? new Date().toISOString(),
|
|
245
255
|
},
|
|
246
|
-
upgrade
|
|
247
|
-
? { operation: "explain", target }
|
|
248
|
-
: pattern === "project_overview"
|
|
249
|
-
? { operation: "query", pattern: "architecture_overview", target: "" }
|
|
250
|
-
: { operation: "query", pattern: "impact", target },
|
|
256
|
+
upgrade,
|
|
251
257
|
};
|
|
252
258
|
}
|
|
253
259
|
export function runExactSymbolFastPath(repo, symbol, filePath) {
|
|
@@ -306,7 +312,7 @@ export function runExactSymbolFastPath(repo, symbol, filePath) {
|
|
|
306
312
|
upgrade: { operation: "explain", target: symbol },
|
|
307
313
|
};
|
|
308
314
|
}
|
|
309
|
-
export function tryStructuralFastPath(argv, cwd
|
|
315
|
+
export function tryStructuralFastPath(argv, cwd) {
|
|
310
316
|
const valueAfter = (flag) => {
|
|
311
317
|
const index = argv.indexOf(flag);
|
|
312
318
|
return index >= 0
|
|
@@ -314,7 +320,7 @@ export function tryStructuralFastPath(argv, cwd = process.cwd()) {
|
|
|
314
320
|
: argv.find((value) => value.startsWith(`${flag}=`))?.slice(flag.length + 1);
|
|
315
321
|
};
|
|
316
322
|
if (argv[0] === "explain" && argv[1] && valueAfter("--file")) {
|
|
317
|
-
const repo = path.resolve(valueAfter("--repo") ?? cwd);
|
|
323
|
+
const repo = path.resolve(valueAfter("--repo") ?? cwd ?? process.cwd());
|
|
318
324
|
const result = runExactSymbolFastPath(repo, argv[1], valueAfter("--file"));
|
|
319
325
|
if (!result)
|
|
320
326
|
return false;
|
|
@@ -325,7 +331,7 @@ export function tryStructuralFastPath(argv, cwd = process.cwd()) {
|
|
|
325
331
|
!["file_summary", "batch_outline", "project_overview"].includes(argv[1] ?? ""))
|
|
326
332
|
return false;
|
|
327
333
|
const pattern = argv[1];
|
|
328
|
-
const repo = path.resolve(valueAfter("--repo") ?? cwd);
|
|
334
|
+
const repo = path.resolve(valueAfter("--repo") ?? cwd ?? process.cwd());
|
|
329
335
|
const target = pattern === "project_overview" ? "" : (argv[2] ?? "");
|
|
330
336
|
const limit = Number(valueAfter("--limit") ?? 100);
|
|
331
337
|
if (!target && pattern !== "project_overview")
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { compareBytes } from "./compare.js";
|
|
5
|
+
import { resolveStateDir } from "./engine/state-paths.js";
|
|
4
6
|
export const STRUCTURAL_SNAPSHOT_VERSION = 1;
|
|
5
7
|
export function contentFingerprint(content) {
|
|
6
8
|
return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
|
|
7
9
|
}
|
|
8
10
|
export function structuralSnapshotPath(repo) {
|
|
9
|
-
return path.join(
|
|
11
|
+
return path.join(resolveStateDir(repo), "structural-v1.json");
|
|
10
12
|
}
|
|
11
13
|
export function writeStructuralSnapshot(repo, files) {
|
|
12
14
|
const destination = structuralSnapshotPath(repo);
|
|
@@ -14,7 +16,9 @@ export function writeStructuralSnapshot(repo, files) {
|
|
|
14
16
|
const snapshot = {
|
|
15
17
|
schemaVersion: STRUCTURAL_SNAPSHOT_VERSION,
|
|
16
18
|
generatedAt: new Date().toISOString(),
|
|
17
|
-
|
|
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)),
|
|
18
22
|
};
|
|
19
23
|
const temporary = `${destination}.${process.pid}.tmp`;
|
|
20
24
|
fs.writeFileSync(temporary, `${JSON.stringify(snapshot)}\n`, { mode: 0o600 });
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { parseDocument } from "yaml";
|
|
6
|
+
import { compareBytes } from "./compare.js";
|
|
6
7
|
import { detectDuplicateCandidates, extractRepositoryRelationships, trackedRepositoryFiles, } from "./relationship-adapters.js";
|
|
7
8
|
export const RELATIONSHIP_TYPES = [
|
|
8
9
|
"depends_on",
|
|
@@ -52,6 +53,24 @@ function stringValue(value, field) {
|
|
|
52
53
|
throw new Error(`${field} must be a string`);
|
|
53
54
|
return value;
|
|
54
55
|
}
|
|
56
|
+
function repositoryInitialization(value) {
|
|
57
|
+
if (value === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
const record = asRecord(value);
|
|
60
|
+
if (record.memoryLimitMiB === undefined)
|
|
61
|
+
return {};
|
|
62
|
+
const mib = record.memoryLimitMiB;
|
|
63
|
+
if (typeof mib !== "number" ||
|
|
64
|
+
!Number.isSafeInteger(mib) ||
|
|
65
|
+
mib < 128 ||
|
|
66
|
+
!Number.isSafeInteger(mib * 1024 * 1024))
|
|
67
|
+
throw new Error("repositoryInitialization.memoryLimitMiB must be an integer of at least 128");
|
|
68
|
+
return { memoryLimitMiB: mib };
|
|
69
|
+
}
|
|
70
|
+
export function configuredRepositoryInitMemoryLimitBytes(config) {
|
|
71
|
+
const mib = config.repositoryInitialization?.memoryLimitMiB;
|
|
72
|
+
return mib === undefined ? undefined : mib * 1024 * 1024;
|
|
73
|
+
}
|
|
55
74
|
function stableLegacyIdentity(portableReference) {
|
|
56
75
|
let normalized = portableReference.replaceAll("\\", "/");
|
|
57
76
|
while (normalized.endsWith("/"))
|
|
@@ -321,6 +340,7 @@ export function loadSystemConfiguration(repoPath, options = {}) {
|
|
|
321
340
|
const repositories = (Array.isArray(team.repositories) ? team.repositories : []).map((value, index) => configuredRepository(value, index));
|
|
322
341
|
const systems = (Array.isArray(team.systems) ? team.systems : []).map((value, index) => configuredSystem(value, index));
|
|
323
342
|
const relationships = (Array.isArray(team.relationships) ? team.relationships : []).map((value, index) => declaredRelationship(value, index));
|
|
343
|
+
const initialization = repositoryInitialization(team.repositoryInitialization);
|
|
324
344
|
const xdgConfigHome = options.xdgConfigHome ?? process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
|
|
325
345
|
applyPersonalMappings(repositories, xdgConfigHome);
|
|
326
346
|
const legacyFederation = applyLegacyFederation(repo, repositories, relationships);
|
|
@@ -338,12 +358,15 @@ export function loadSystemConfiguration(repoPath, options = {}) {
|
|
|
338
358
|
relationship,
|
|
339
359
|
])).values(),
|
|
340
360
|
];
|
|
341
|
-
|
|
361
|
+
// Byte order over a NUL-joined composite key: collation may ignore the NUL
|
|
362
|
+
// separators, blurring the field boundaries the key depends on.
|
|
363
|
+
uniqueRelationships.sort((left, right) => compareBytes(`${left.from}\0${left.type}\0${left.to}`, `${right.from}\0${right.type}\0${right.to}`));
|
|
342
364
|
return {
|
|
343
365
|
schemaVersion: 1,
|
|
344
366
|
repositories,
|
|
345
367
|
systems,
|
|
346
368
|
relationships: uniqueRelationships,
|
|
369
|
+
...(initialization === undefined ? {} : { repositoryInitialization: initialization }),
|
|
347
370
|
compatibility: { legacyFederation },
|
|
348
371
|
};
|
|
349
372
|
}
|
|
@@ -525,7 +548,7 @@ export async function enrichSystemRelationships(config) {
|
|
|
525
548
|
return config;
|
|
526
549
|
return {
|
|
527
550
|
...config,
|
|
528
|
-
relationships: [...config.relationships, ...duplicates].sort((left, right) => `${left.from}\0${left.type}\0${left.to}\0${left.evidence.location}
|
|
551
|
+
relationships: [...config.relationships, ...duplicates].sort((left, right) => compareBytes(`${left.from}\0${left.type}\0${left.to}\0${left.evidence.location}`, `${right.from}\0${right.type}\0${right.to}\0${right.evidence.location}`)),
|
|
529
552
|
};
|
|
530
553
|
}
|
|
531
554
|
function repositoryForIdentity(config, identity) {
|