knodin 0.5.0

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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/dist/bin/cli.js +1704 -0
  4. package/dist/src/agent-integration.js +250 -0
  5. package/dist/src/artifact-refresh.js +81 -0
  6. package/dist/src/cli-args.js +267 -0
  7. package/dist/src/cli-model.js +324 -0
  8. package/dist/src/compact-structural.js +96 -0
  9. package/dist/src/competitive-constraints.js +20 -0
  10. package/dist/src/competitive-manifest.js +330 -0
  11. package/dist/src/competitive-measurement.js +183 -0
  12. package/dist/src/competitive-runner.js +453 -0
  13. package/dist/src/competitive-sandbox.js +108 -0
  14. package/dist/src/context-export.js +422 -0
  15. package/dist/src/context.js +102 -0
  16. package/dist/src/docs-sections.js +141 -0
  17. package/dist/src/doctor.js +380 -0
  18. package/dist/src/engine/ann-hnsw.js +271 -0
  19. package/dist/src/engine/embeddings.js +193 -0
  20. package/dist/src/engine/file-walker.js +43 -0
  21. package/dist/src/engine/index.js +13030 -0
  22. package/dist/src/engine/perf.js +115 -0
  23. package/dist/src/engine/prune.js +112 -0
  24. package/dist/src/engine/source-policy.js +69 -0
  25. package/dist/src/engine/sqlite.js +71 -0
  26. package/dist/src/engine/symbol-delete.js +58 -0
  27. package/dist/src/failure-diagnosis.js +590 -0
  28. package/dist/src/fleet.js +7 -0
  29. package/dist/src/git-executable.js +31 -0
  30. package/dist/src/graph-query-health.js +115 -0
  31. package/dist/src/index-activity.js +125 -0
  32. package/dist/src/init-progress-worker.js +107 -0
  33. package/dist/src/init-progress.js +155 -0
  34. package/dist/src/init.js +985 -0
  35. package/dist/src/lifecycle-health.js +213 -0
  36. package/dist/src/lsp-readonly.js +217 -0
  37. package/dist/src/output-compression.js +629 -0
  38. package/dist/src/output-telemetry.js +359 -0
  39. package/dist/src/pr-triage.js +638 -0
  40. package/dist/src/relationship-adapters.js +370 -0
  41. package/dist/src/release-attestation.js +533 -0
  42. package/dist/src/repair-progress-worker.js +121 -0
  43. package/dist/src/repair-progress.js +262 -0
  44. package/dist/src/repository-init-process.js +173 -0
  45. package/dist/src/repository-management.js +1089 -0
  46. package/dist/src/response-budget.js +184 -0
  47. package/dist/src/server.js +53 -0
  48. package/dist/src/system-config.js +615 -0
  49. package/dist/src/terminal-help.js +83 -0
  50. package/dist/src/tools/knodin-tools.js +1438 -0
  51. package/dist/src/tools/reckon-tools.js +5 -0
  52. package/dist/src/update-policy.js +944 -0
  53. package/dist/src/update-trust.js +503 -0
  54. package/dist/src/version.js +13 -0
  55. package/dist/src/visualization.js +162 -0
  56. package/dist/src/wait-for-fresh.js +98 -0
  57. package/dist/src/worktree-lifecycle.js +231 -0
  58. package/docs/CLI.md +39 -0
  59. package/docs/COMMAND-OUTPUT-COMPRESSION.md +194 -0
  60. package/docs/DEAD-CODE-AND-IMPACT.md +27 -0
  61. package/docs/DOCTOR-AND-UPDATES.md +84 -0
  62. package/docs/INDEXING-POLICY-AND-PROVENANCE.md +37 -0
  63. package/docs/INSTALLATION.md +208 -0
  64. package/docs/MCP.md +100 -0
  65. package/docs/PT-ACCESS-RECOMMENDATION.md +91 -0
  66. package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
  67. package/docs/REPOSITORIES-AND-WORKTREES.md +81 -0
  68. package/docs/SIGNED-UPDATES.md +146 -0
  69. package/docs/SYSTEMS-AND-RELATIONSHIPS.md +45 -0
  70. package/docs/TELEMETRY.md +42 -0
  71. package/docs/releases/0.3.0.md +46 -0
  72. package/docs/releases/0.4.0.md +68 -0
  73. package/docs/releases/0.4.1.md +28 -0
  74. package/docs/releases/0.4.2.md +27 -0
  75. package/docs/releases/0.4.3.md +23 -0
  76. package/docs/releases/0.5.0.md +29 -0
  77. package/package.json +110 -0
  78. package/schemas/release-attestation-v1.schema.json +210 -0
  79. package/tree-sitter-prisma.wasm +0 -0
  80. package/tree-sitter-sql.wasm +0 -0
  81. package/tree-sitter-xml.wasm +0 -0
@@ -0,0 +1,615 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { parseDocument } from "yaml";
6
+ import { detectDuplicateCandidates, extractRepositoryRelationships, trackedRepositoryFiles, } from "./relationship-adapters.js";
7
+ export const RELATIONSHIP_TYPES = [
8
+ "depends_on",
9
+ "provides_api",
10
+ "consumes_api",
11
+ "deploys",
12
+ "provisions",
13
+ "reads_output",
14
+ "references_path",
15
+ "reads_config",
16
+ "publishes_event",
17
+ "consumes_event",
18
+ "uses_image",
19
+ "generated_from",
20
+ "vendored_from",
21
+ "mirror_of",
22
+ "duplicate_candidate",
23
+ ];
24
+ function asRecord(value) {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value)
26
+ ? value
27
+ : {};
28
+ }
29
+ function readYaml(filePath) {
30
+ const document = parseDocument(fs.readFileSync(filePath, "utf-8"), {
31
+ prettyErrors: true,
32
+ strict: true,
33
+ uniqueKeys: true,
34
+ });
35
+ if (document.errors.length > 0) {
36
+ throw new Error(`${filePath}: ${document.errors.map(({ message }) => message).join("; ")}`);
37
+ }
38
+ return document.toJS({ maxAliasCount: 25 });
39
+ }
40
+ function optionalYaml(filePath) {
41
+ try {
42
+ return readYaml(filePath);
43
+ }
44
+ catch (error) {
45
+ if (error.code === "ENOENT")
46
+ return {};
47
+ throw error;
48
+ }
49
+ }
50
+ function stringValue(value, field) {
51
+ if (typeof value !== "string" || value.trim() === "")
52
+ throw new Error(`${field} must be a string`);
53
+ return value;
54
+ }
55
+ function stableLegacyIdentity(portableReference) {
56
+ let normalized = portableReference.replaceAll("\\", "/");
57
+ while (normalized.endsWith("/"))
58
+ normalized = normalized.slice(0, -1);
59
+ normalized ||= ".";
60
+ const basename = path.posix.basename(normalized).replace(/[^a-zA-Z0-9._-]+/g, "-") || "repository";
61
+ const suffix = crypto.createHash("sha256").update(normalized).digest("hex").slice(0, 10);
62
+ return `legacy-${basename}-${suffix}`;
63
+ }
64
+ function repositoryRole(value, index) {
65
+ if (value === undefined)
66
+ return "source";
67
+ if (value === "source" || value === "generated" || value === "vendored" || value === "mirrored") {
68
+ return value;
69
+ }
70
+ throw new Error(`repositories[${index}].role is unsupported`);
71
+ }
72
+ function configuredRepository(value, index) {
73
+ const record = asRecord(value);
74
+ const role = repositoryRole(record.role, index);
75
+ const explicitIndex = record.index === "full" || record.index === "provenance-only" ? record.index : undefined;
76
+ const derived = role === "generated" || role === "vendored" || role === "mirrored";
77
+ return {
78
+ id: stringValue(record.id, `repositories[${index}].id`),
79
+ role,
80
+ indexMode: explicitIndex ?? (derived ? "provenance-only" : "full"),
81
+ policySource: explicitIndex ? "reckon.yaml" : "role-default",
82
+ };
83
+ }
84
+ function configuredSystem(value, systemIndex) {
85
+ const record = asRecord(value);
86
+ const components = (Array.isArray(record.components) ? record.components : []).map((component, componentIndex) => {
87
+ const item = asRecord(component);
88
+ return {
89
+ id: stringValue(item.id, `systems[${systemIndex}].components[${componentIndex}].id`),
90
+ repository: typeof item.repository === "string" ? item.repository : undefined,
91
+ type: typeof item.type === "string" ? item.type : "component",
92
+ };
93
+ });
94
+ return {
95
+ id: stringValue(record.id, `systems[${systemIndex}].id`),
96
+ components,
97
+ };
98
+ }
99
+ function applyLegacyFederation(repo, repositories, relationships) {
100
+ const legacyPath = path.join(repo, ".reckon", "federation.json");
101
+ let legacy;
102
+ try {
103
+ legacy = asRecord(JSON.parse(fs.readFileSync(legacyPath, "utf-8")));
104
+ }
105
+ catch (error) {
106
+ if (error.code === "ENOENT")
107
+ return false;
108
+ throw error;
109
+ }
110
+ const peerPaths = Array.isArray(legacy.repos) ? legacy.repos : [];
111
+ const localRepository = repositories.find(({ path: checkoutPath }) => checkoutPath === repo);
112
+ const onlyRepository = repositories.length === 1 ? repositories[0] : undefined;
113
+ const sourceIdentity = localRepository?.id ??
114
+ (onlyRepository && onlyRepository.path === undefined ? onlyRepository.id : undefined) ??
115
+ stableLegacyIdentity(".");
116
+ if (!repositories.some(({ id }) => id === sourceIdentity)) {
117
+ repositories.push({
118
+ id: sourceIdentity,
119
+ role: "source",
120
+ indexMode: "full",
121
+ policySource: "role-default",
122
+ path: repo,
123
+ pathSource: "legacy-federation",
124
+ });
125
+ }
126
+ let migrated = false;
127
+ for (const peer of peerPaths) {
128
+ if (typeof peer !== "string")
129
+ continue;
130
+ migrated = true;
131
+ const checkoutPath = path.resolve(repo, peer);
132
+ const id = stableLegacyIdentity(peer);
133
+ if (!repositories.some((repository) => repository.path === checkoutPath)) {
134
+ repositories.push({
135
+ id,
136
+ role: "source",
137
+ indexMode: "full",
138
+ policySource: "role-default",
139
+ path: checkoutPath,
140
+ pathSource: "legacy-federation",
141
+ });
142
+ }
143
+ relationships.push({
144
+ type: "depends_on",
145
+ from: sourceIdentity,
146
+ to: id,
147
+ targetIdentity: id,
148
+ evidence: {
149
+ location: ".reckon/federation.json",
150
+ adapter: "legacy-federation",
151
+ kind: "compatibility",
152
+ confidence: 1,
153
+ freshness: "current",
154
+ indexGeneration: null,
155
+ },
156
+ });
157
+ }
158
+ return migrated;
159
+ }
160
+ function applyPersonalMappings(repositories, xdgConfigHome) {
161
+ const personal = asRecord(optionalYaml(path.join(xdgConfigHome, "knodin", "repositories.yaml")));
162
+ const personalRepositories = asRecord(personal.repositories);
163
+ for (const repository of repositories) {
164
+ const checkout = personalRepositories[repository.id];
165
+ if (typeof checkout !== "string")
166
+ continue;
167
+ if (!path.isAbsolute(checkout)) {
168
+ throw new Error(`personal repository path for ${repository.id} must be absolute; team config keeps paths out of reckon.yaml`);
169
+ }
170
+ repository.path = path.normalize(checkout);
171
+ repository.pathSource = "personal-config";
172
+ }
173
+ }
174
+ function configuredCheckout(repository, repositories, repo) {
175
+ if (repository.path)
176
+ return repository.path;
177
+ return repositories.length === 1 && repository.id === repositories[0]?.id ? repo : undefined;
178
+ }
179
+ function extractConfiguredRelationships(repositories, repo) {
180
+ const extractionPaths = new Map(repositories
181
+ .map((repository) => [configuredCheckout(repository, repositories, repo) ?? "", repository.id])
182
+ .filter(([checkout]) => checkout !== "" && fs.existsSync(checkout)));
183
+ return repositories.flatMap((repository) => {
184
+ const checkout = configuredCheckout(repository, repositories, repo);
185
+ if (!checkout || !fs.existsSync(checkout))
186
+ return [];
187
+ return extractRepositoryRelationships(repository.id, checkout, extractionPaths);
188
+ });
189
+ }
190
+ function attributeLinePolicyEvidence(line, index) {
191
+ const trimmed = line.trim();
192
+ if (!trimmed || trimmed.startsWith("#"))
193
+ return [];
194
+ const [pattern, ...attributes] = trimmed.split(/\s+/);
195
+ if (!pattern)
196
+ return [];
197
+ return attributes.flatMap((attribute) => {
198
+ if (attribute === "linguist-generated" || attribute === "linguist-generated=true")
199
+ return [
200
+ {
201
+ category: "generated",
202
+ pattern,
203
+ location: `.gitattributes:${index + 1}`,
204
+ source: "gitattributes",
205
+ },
206
+ ];
207
+ if (attribute === "linguist-vendored" || attribute === "linguist-vendored=true")
208
+ return [
209
+ {
210
+ category: "vendored",
211
+ pattern,
212
+ location: `.gitattributes:${index + 1}`,
213
+ source: "gitattributes",
214
+ },
215
+ ];
216
+ return [];
217
+ });
218
+ }
219
+ function attributesPolicyEvidence(checkout) {
220
+ const evidence = [];
221
+ const attributesPath = path.join(checkout, ".gitattributes");
222
+ try {
223
+ const lines = fs
224
+ .readFileSync(attributesPath, "utf-8")
225
+ .slice(0, 1024 * 1024)
226
+ .split(/\r?\n/);
227
+ for (const [index, line] of lines.entries())
228
+ evidence.push(...attributeLinePolicyEvidence(line, index));
229
+ }
230
+ catch (error) {
231
+ if (error.code !== "ENOENT")
232
+ throw error;
233
+ }
234
+ return evidence;
235
+ }
236
+ function metadataPolicyEvidence(file) {
237
+ const lower = file.toLowerCase();
238
+ if (!lower.endsWith(".map") &&
239
+ !/(?:^|\/)(?:bom|sbom)(?:\.[^/]*)?\.json$/.test(lower) &&
240
+ !lower.endsWith(".intoto.jsonl") &&
241
+ !lower.includes("provenance"))
242
+ return undefined;
243
+ return {
244
+ category: lower.includes("bom") || lower.endsWith(".intoto.jsonl") || lower.includes("provenance")
245
+ ? "sbom-slsa"
246
+ : "build-metadata",
247
+ pattern: file,
248
+ location: file,
249
+ source: "metadata",
250
+ };
251
+ }
252
+ function generatedHeaderPolicyEvidence(checkout, file) {
253
+ let descriptor;
254
+ try {
255
+ descriptor = fs.openSync(path.join(checkout, file), "r");
256
+ const buffer = Buffer.alloc(4096);
257
+ const bytes = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
258
+ const header = buffer.subarray(0, bytes).toString("utf-8").toLowerCase();
259
+ if (!header.includes("@generated") &&
260
+ !header.includes("code generated") &&
261
+ !header.includes("auto-generated"))
262
+ return undefined;
263
+ return {
264
+ category: "generated",
265
+ pattern: file,
266
+ location: `${file}:1`,
267
+ source: "generated-header",
268
+ };
269
+ }
270
+ catch {
271
+ // A file removed during the bounded evidence snapshot contributes no signal.
272
+ return undefined;
273
+ }
274
+ finally {
275
+ if (descriptor !== undefined)
276
+ fs.closeSync(descriptor);
277
+ }
278
+ }
279
+ function repositoryPolicyEvidence(checkout) {
280
+ const evidence = attributesPolicyEvidence(checkout);
281
+ for (const file of trackedRepositoryFiles(checkout)) {
282
+ const fileEvidence = metadataPolicyEvidence(file) ?? generatedHeaderPolicyEvidence(checkout, file);
283
+ if (fileEvidence)
284
+ evidence.push(fileEvidence);
285
+ }
286
+ return evidence;
287
+ }
288
+ function declaredRelationship(value, index) {
289
+ const record = asRecord(value);
290
+ const type = stringValue(record.type, `relationships[${index}].type`);
291
+ if (!RELATIONSHIP_TYPES.includes(type)) {
292
+ throw new Error(`relationships[${index}].type is unsupported: ${type}`);
293
+ }
294
+ const from = stringValue(record.from, `relationships[${index}].from`);
295
+ const to = stringValue(record.to, `relationships[${index}].to`);
296
+ const source = asRecord(record.evidence);
297
+ const confidence = typeof source.confidence === "number" && source.confidence >= 0 && source.confidence <= 1
298
+ ? source.confidence
299
+ : 1;
300
+ return {
301
+ type: type,
302
+ from,
303
+ to,
304
+ targetIdentity: to,
305
+ evidence: {
306
+ location: typeof source.location === "string" && source.location ? source.location : "reckon.yaml",
307
+ adapter: "knodin-yaml",
308
+ kind: "declared",
309
+ confidence,
310
+ freshness: "current",
311
+ indexGeneration: null,
312
+ },
313
+ };
314
+ }
315
+ export function loadSystemConfiguration(repoPath, options = {}) {
316
+ const repo = path.resolve(repoPath);
317
+ const team = asRecord(optionalYaml(path.join(repo, "reckon.yaml")));
318
+ if (team.schemaVersion !== undefined && team.schemaVersion !== 1) {
319
+ throw new Error("reckon.yaml: schemaVersion must be 1");
320
+ }
321
+ const repositories = (Array.isArray(team.repositories) ? team.repositories : []).map((value, index) => configuredRepository(value, index));
322
+ const systems = (Array.isArray(team.systems) ? team.systems : []).map((value, index) => configuredSystem(value, index));
323
+ const relationships = (Array.isArray(team.relationships) ? team.relationships : []).map((value, index) => declaredRelationship(value, index));
324
+ const xdgConfigHome = options.xdgConfigHome ?? process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
325
+ applyPersonalMappings(repositories, xdgConfigHome);
326
+ const legacyFederation = applyLegacyFederation(repo, repositories, relationships);
327
+ for (const repository of repositories) {
328
+ const checkout = configuredCheckout(repository, repositories, repo);
329
+ if (checkout && fs.existsSync(checkout))
330
+ repository.policyEvidence = repositoryPolicyEvidence(checkout);
331
+ }
332
+ relationships.push(...extractConfiguredRelationships(repositories, repo));
333
+ repositories.sort((left, right) => left.id.localeCompare(right.id));
334
+ systems.sort((left, right) => left.id.localeCompare(right.id));
335
+ const uniqueRelationships = [
336
+ ...new Map(relationships.map((relationship) => [
337
+ `${relationship.from}\0${relationship.type}\0${relationship.to}\0${relationship.evidence.location}`,
338
+ relationship,
339
+ ])).values(),
340
+ ];
341
+ uniqueRelationships.sort((left, right) => `${left.from}\0${left.type}\0${left.to}`.localeCompare(`${right.from}\0${right.type}\0${right.to}`));
342
+ return {
343
+ schemaVersion: 1,
344
+ repositories,
345
+ systems,
346
+ relationships: uniqueRelationships,
347
+ compatibility: { legacyFederation },
348
+ };
349
+ }
350
+ export function validateSystemConfiguration(config) {
351
+ const issues = [];
352
+ const repositories = new Set(config.repositories.map(({ id }) => id));
353
+ for (const system of config.systems)
354
+ for (const component of system.components)
355
+ if (component.repository && !repositories.has(component.repository))
356
+ issues.push({
357
+ code: "unresolved-repository",
358
+ identity: component.repository,
359
+ message: `${system.id}/${component.id} references unknown repository ${component.repository}`,
360
+ });
361
+ for (const repository of config.repositories) {
362
+ if (repository.path && !fs.existsSync(repository.path))
363
+ issues.push({
364
+ code: "missing-checkout",
365
+ identity: repository.id,
366
+ message: `${repository.id} checkout does not exist at the configured personal path`,
367
+ });
368
+ }
369
+ const componentIds = new Set(config.systems.flatMap(({ components }) => components.map(({ id }) => id)));
370
+ const knownTargets = new Set([...repositories, ...componentIds]);
371
+ for (const relationship of config.relationships)
372
+ issues.push(...validateRelationship(relationship, knownTargets));
373
+ return { valid: issues.length === 0, requiresPartial: issues.length > 0, issues };
374
+ }
375
+ function validateRelationship(relationship, knownTargets) {
376
+ const issues = [];
377
+ const externalPrefixes = [
378
+ "pkg:",
379
+ "openapi:",
380
+ "asyncapi:",
381
+ "graphql:",
382
+ "protobuf:",
383
+ "terraform:",
384
+ "terraform-state:",
385
+ "terraform-output:",
386
+ "oci:",
387
+ "config:",
388
+ ];
389
+ const extractedExternal = relationship.evidence.kind === "extracted" &&
390
+ externalPrefixes.some((prefix) => relationship.to.startsWith(prefix));
391
+ if (!knownTargets.has(relationship.from) ||
392
+ (!knownTargets.has(relationship.to) && !extractedExternal))
393
+ issues.push({
394
+ code: "invalid-relationship",
395
+ identity: `${relationship.from}->${relationship.to}`,
396
+ message: `${relationship.type} references an unknown source or target identity`,
397
+ });
398
+ if (relationship.evidence.confidence < 0.8)
399
+ issues.push({
400
+ code: "low-confidence-relationship",
401
+ identity: `${relationship.from}->${relationship.to}`,
402
+ message: `${relationship.type} confidence ${relationship.evidence.confidence} is below the system-query threshold`,
403
+ });
404
+ if (relationship.evidence.freshness === "stale")
405
+ issues.push({
406
+ code: "configuration-drift",
407
+ identity: `${relationship.from}->${relationship.to}`,
408
+ message: `${relationship.type} evidence is stale and must be refreshed`,
409
+ });
410
+ return issues;
411
+ }
412
+ async function validateComponentHealth(config, component, status, defaultRepositoryPath, existingIssues) {
413
+ if (!component.repository)
414
+ return [];
415
+ const repository = config.repositories.find(({ id }) => id === component.repository);
416
+ const checkout = repository?.path ?? (config.repositories.length === 1 ? defaultRepositoryPath : undefined);
417
+ if (!checkout) {
418
+ if (existingIssues.some(({ identity }) => identity === component.repository))
419
+ return [];
420
+ return [
421
+ {
422
+ code: "missing-checkout",
423
+ identity: component.repository,
424
+ message: `${component.id} has no personal checkout mapping`,
425
+ },
426
+ ];
427
+ }
428
+ try {
429
+ const health = await status(checkout);
430
+ const lifecycleStatus = health.lifecycle?.status;
431
+ if (health.status === "healthy" && (!lifecycleStatus || lifecycleStatus === "healthy"))
432
+ return [];
433
+ const lifecycleDetail = lifecycleStatus && lifecycleStatus !== "healthy"
434
+ ? ` and lifecycle is ${lifecycleStatus}`
435
+ : "";
436
+ return [
437
+ {
438
+ code: "unavailable-component",
439
+ identity: component.id,
440
+ message: `${component.id} graph is ${health.status}${lifecycleDetail}`,
441
+ },
442
+ ];
443
+ }
444
+ catch (error) {
445
+ return [
446
+ {
447
+ code: "unavailable-component",
448
+ identity: component.id,
449
+ message: `${component.id} health is unknown: ${error instanceof Error ? error.message : String(error)}`,
450
+ },
451
+ ];
452
+ }
453
+ }
454
+ export async function validateSystemHealth(config, systemId, status, defaultRepositoryPath) {
455
+ const validation = validateSystemConfiguration(config);
456
+ const issues = [...validation.issues];
457
+ const system = config.systems.find(({ id }) => id === systemId);
458
+ if (!system)
459
+ return validation;
460
+ for (const component of system.components) {
461
+ issues.push(...(await validateComponentHealth(config, component, status, defaultRepositoryPath, issues)));
462
+ }
463
+ return { valid: issues.length === 0, requiresPartial: issues.length > 0, issues };
464
+ }
465
+ export function queryConfiguredSystem(config, systemId, allowPartial = false, healthValidation) {
466
+ const system = config.systems.find(({ id }) => id === systemId);
467
+ if (!system) {
468
+ return { status: "not-found", systemId, available: config.systems.map(({ id }) => id) };
469
+ }
470
+ const validation = healthValidation ?? validateSystemConfiguration(config);
471
+ if (!validation.valid && !allowPartial) {
472
+ return {
473
+ status: "unavailable",
474
+ systemId,
475
+ relationships: [],
476
+ omissions: validation.issues,
477
+ };
478
+ }
479
+ const unavailableRepositories = new Set(validation.issues
480
+ .filter(({ code }) => code === "unresolved-repository" || code === "missing-checkout")
481
+ .map(({ identity }) => identity));
482
+ const omittedComponents = new Set(system.components
483
+ .filter(({ repository }) => repository !== undefined && unavailableRepositories.has(repository))
484
+ .map(({ id }) => id));
485
+ const componentIds = new Set(system.components.map(({ id }) => id));
486
+ const relationships = config.relationships.filter(({ from, to }) => (componentIds.has(from) || componentIds.has(to)) &&
487
+ !omittedComponents.has(from) &&
488
+ !omittedComponents.has(to));
489
+ return {
490
+ status: validation.valid ? "ok" : "partial",
491
+ systemId,
492
+ relationships,
493
+ omissions: validation.issues,
494
+ };
495
+ }
496
+ export function systemMembershipsForPath(config, repositoryPath) {
497
+ const resolved = path.resolve(repositoryPath);
498
+ const identities = new Set(config.repositories
499
+ .filter(({ path: checkout }) => checkout !== undefined && path.resolve(checkout) === resolved)
500
+ .map(({ id }) => id));
501
+ if (identities.size === 0 && config.repositories.length === 1 && config.repositories[0])
502
+ identities.add(config.repositories[0].id);
503
+ return config.systems
504
+ .filter(({ components }) => components.some(({ repository }) => repository !== undefined && identities.has(repository)))
505
+ .map(({ id }) => id)
506
+ .sort((left, right) => left.localeCompare(right));
507
+ }
508
+ export function indexModeForPath(config, repositoryPath) {
509
+ const resolved = path.resolve(repositoryPath);
510
+ const configured = config.repositories.find(({ path: checkout }) => checkout && path.resolve(checkout) === resolved);
511
+ if (configured)
512
+ return configured.indexMode;
513
+ if (config.repositories.length === 1 && config.repositories[0])
514
+ return config.repositories[0].indexMode;
515
+ return "full";
516
+ }
517
+ export async function enrichSystemRelationships(config) {
518
+ const checkouts = config.repositories
519
+ .filter((repository) => repository.path !== undefined && fs.existsSync(repository.path))
520
+ .map(({ id, path: checkout }) => ({ id, path: checkout }));
521
+ if (checkouts.length < 2)
522
+ return config;
523
+ const duplicates = await detectDuplicateCandidates(checkouts);
524
+ if (duplicates.length === 0)
525
+ return config;
526
+ return {
527
+ ...config,
528
+ relationships: [...config.relationships, ...duplicates].sort((left, right) => `${left.from}\0${left.type}\0${left.to}\0${left.evidence.location}`.localeCompare(`${right.from}\0${right.type}\0${right.to}\0${right.evidence.location}`)),
529
+ };
530
+ }
531
+ function repositoryForIdentity(config, identity) {
532
+ if (config.repositories.some(({ id }) => id === identity))
533
+ return identity;
534
+ for (const system of config.systems) {
535
+ const component = system.components.find(({ id }) => id === identity);
536
+ if (component?.repository)
537
+ return component.repository;
538
+ }
539
+ return undefined;
540
+ }
541
+ function localRepositoryIdentities(config, repositoryPath) {
542
+ const resolved = path.resolve(repositoryPath);
543
+ const identities = new Set(config.repositories
544
+ .filter(({ path: checkout }) => checkout && path.resolve(checkout) === resolved)
545
+ .map(({ id }) => id));
546
+ if (identities.size === 0 && config.repositories.length === 1 && config.repositories[0])
547
+ identities.add(config.repositories[0].id);
548
+ return identities;
549
+ }
550
+ function crossRepositoryIncoming(config, repositoryPath, target) {
551
+ const localRepositories = localRepositoryIdentities(config, repositoryPath);
552
+ const localComponents = new Set(config.systems.flatMap(({ components }) => components
553
+ .filter(({ repository }) => repository && localRepositories.has(repository))
554
+ .map(({ id }) => id)));
555
+ return config.relationships.filter((relationship) => {
556
+ const sourceRepository = repositoryForIdentity(config, relationship.from);
557
+ if (!sourceRepository || localRepositories.has(sourceRepository))
558
+ return false;
559
+ const targetRepository = repositoryForIdentity(config, relationship.to);
560
+ return ((targetRepository !== undefined && localRepositories.has(targetRepository)) ||
561
+ localComponents.has(relationship.to) ||
562
+ (target === "" && targetRepository === undefined) ||
563
+ (target !== undefined &&
564
+ (relationship.to === target || relationship.targetIdentity === target)));
565
+ });
566
+ }
567
+ /**
568
+ * Conservatively enrich impact/dead-code responses with declared or extracted
569
+ * cross-repository evidence. Only exact, current, high-confidence symbol
570
+ * evidence removes a dead-code candidate. Broader evidence remains a blocker,
571
+ * never proof that an arbitrary symbol is live or safe to delete.
572
+ */
573
+ export function incorporateSystemQueryEvidence(config, repositoryPath, pattern, target, value) {
574
+ if (!value || typeof value !== "object")
575
+ return value;
576
+ const root = value;
577
+ const incoming = crossRepositoryIncoming(config, repositoryPath, target);
578
+ if (incoming.length === 0)
579
+ return value;
580
+ const evidence = incoming.map((relationship) => ({
581
+ type: relationship.type,
582
+ from: relationship.from,
583
+ to: relationship.to,
584
+ targetIdentity: relationship.targetIdentity,
585
+ evidence: relationship.evidence,
586
+ }));
587
+ root.crossRepositoryEvidence = {
588
+ incoming: evidence,
589
+ interpretation: "Incoming system evidence is source-scoped; non-exact or low-confidence evidence blocks safe-deletion claims.",
590
+ };
591
+ if (pattern !== "dead_code" || !Array.isArray(root.results))
592
+ return root;
593
+ const rows = root.results;
594
+ const filtered = rows.flatMap((candidate) => {
595
+ if (!candidate || typeof candidate !== "object")
596
+ return [candidate];
597
+ const row = candidate;
598
+ const symbol = typeof row.symbol === "string" ? row.symbol : "";
599
+ const exactLive = incoming.some((relationship) => relationship.evidence.confidence >= 0.8 &&
600
+ relationship.evidence.freshness === "current" &&
601
+ (relationship.to === symbol || relationship.targetIdentity === symbol));
602
+ if (exactLive)
603
+ return [];
604
+ return [
605
+ {
606
+ ...row,
607
+ deletionSafe: false,
608
+ crossRepositoryEvidence: evidence,
609
+ },
610
+ ];
611
+ });
612
+ root.results = filtered;
613
+ root.count = filtered.length;
614
+ return root;
615
+ }