knodin 0.7.5 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +4 -1
  2. package/dist/bin/cli.js +108 -1859
  3. package/dist/bin/launcher.js +3 -10
  4. package/dist/chunks/chunk-DMQAGX77.js +654 -0
  5. package/dist/chunks/chunk-F4Z3Z766.js +4 -0
  6. package/dist/chunks/chunk-SIJAQVSX.js +3 -0
  7. package/dist/chunks/chunk-X6M4HUUE.js +2 -0
  8. package/dist/chunks/chunk-YPRMY2LP.js +8 -0
  9. package/dist/chunks/pure-compression-cli-4TA2TQD5.js +5 -0
  10. package/dist/chunks/server-7EDF4CBY.js +14 -0
  11. package/dist/chunks/structural-fast-path-KD5KQSPX.js +4 -0
  12. package/dist/src/init-progress-worker.js +2 -106
  13. package/dist/src/repair-progress-worker.js +2 -120
  14. package/docs/INSTALLATION.md +17 -1
  15. package/docs/releases/0.7.6.md +25 -0
  16. package/package.json +1 -111
  17. package/dist/src/agent-integration.js +0 -238
  18. package/dist/src/artifact-refresh.js +0 -81
  19. package/dist/src/cli-args.js +0 -274
  20. package/dist/src/cli-model.js +0 -358
  21. package/dist/src/codeflow-replay.js +0 -80
  22. package/dist/src/compact-structural.js +0 -96
  23. package/dist/src/competitive-cold-mcp.js +0 -40
  24. package/dist/src/competitive-constraints.js +0 -20
  25. package/dist/src/competitive-manifest.js +0 -411
  26. package/dist/src/competitive-measurement.js +0 -183
  27. package/dist/src/competitive-runner.js +0 -487
  28. package/dist/src/competitive-sandbox.js +0 -108
  29. package/dist/src/context-export.js +0 -422
  30. package/dist/src/context.js +0 -102
  31. package/dist/src/diagnostics.js +0 -449
  32. package/dist/src/docs-sections.js +0 -141
  33. package/dist/src/doctor.js +0 -380
  34. package/dist/src/engine/ann-hnsw.js +0 -271
  35. package/dist/src/engine/embeddings.js +0 -193
  36. package/dist/src/engine/file-walker.js +0 -43
  37. package/dist/src/engine/git-history.js +0 -289
  38. package/dist/src/engine/index.js +0 -13377
  39. package/dist/src/engine/perf.js +0 -115
  40. package/dist/src/engine/prune.js +0 -112
  41. package/dist/src/engine/scip-import.js +0 -408
  42. package/dist/src/engine/source-policy.js +0 -69
  43. package/dist/src/engine/sqlite.js +0 -71
  44. package/dist/src/engine/symbol-delete.js +0 -58
  45. package/dist/src/execution-profile.js +0 -203
  46. package/dist/src/failure-diagnosis.js +0 -649
  47. package/dist/src/fleet.js +0 -7
  48. package/dist/src/git-executable.js +0 -31
  49. package/dist/src/graph-query-health.js +0 -115
  50. package/dist/src/hook-manager-integration.js +0 -156
  51. package/dist/src/index-activity.js +0 -125
  52. package/dist/src/init-progress.js +0 -155
  53. package/dist/src/init.js +0 -1254
  54. package/dist/src/lifecycle-health.js +0 -250
  55. package/dist/src/lsp-readonly.js +0 -217
  56. package/dist/src/output-compression.js +0 -629
  57. package/dist/src/output-telemetry.js +0 -363
  58. package/dist/src/pr-triage.js +0 -638
  59. package/dist/src/progressive-evidence.js +0 -473
  60. package/dist/src/pure-compression-cli.js +0 -101
  61. package/dist/src/relationship-adapters.js +0 -370
  62. package/dist/src/release-attestation.js +0 -533
  63. package/dist/src/release-preflight.js +0 -510
  64. package/dist/src/repair-progress.js +0 -262
  65. package/dist/src/repository-init-process.js +0 -173
  66. package/dist/src/repository-management.js +0 -1231
  67. package/dist/src/response-budget.js +0 -194
  68. package/dist/src/server.js +0 -172
  69. package/dist/src/structural-fast-path.js +0 -338
  70. package/dist/src/structural-snapshot.js +0 -33
  71. package/dist/src/system-config.js +0 -615
  72. package/dist/src/terminal-help.js +0 -83
  73. package/dist/src/tools/knodin-tools.js +0 -1529
  74. package/dist/src/update-ceremony.js +0 -158
  75. package/dist/src/update-policy.js +0 -944
  76. package/dist/src/update-trust.js +0 -503
  77. package/dist/src/version.js +0 -13
  78. package/dist/src/visualization.js +0 -162
  79. package/dist/src/wait-for-fresh.js +0 -98
  80. package/dist/src/worktree-lifecycle.js +0 -231
  81. package/docs/CONTAINED-EXECUTION.md +0 -77
  82. package/docs/DIAGNOSTICS.md +0 -45
  83. package/docs/GIT-HISTORY-REVIEW.md +0 -39
  84. package/docs/PROGRESSIVE-EVIDENCE.md +0 -37
  85. package/docs/PT-ACCESS-RECOMMENDATION.md +0 -91
  86. package/docs/RELEASE-0.3-EVIDENCE.md +0 -73
  87. package/docs/SCIP-IMPORT.md +0 -57
  88. package/docs/SIGNED-UPDATES.md +0 -151
  89. package/docs/TELEMETRY.md +0 -46
  90. package/docs/assets/knodin-favicon.svg +0 -4
  91. package/docs/releases/0.3.0.md +0 -46
  92. package/docs/releases/0.4.0.md +0 -68
  93. package/docs/releases/0.4.1.md +0 -28
  94. package/docs/releases/0.4.2.md +0 -27
  95. package/docs/releases/0.4.3.md +0 -23
  96. package/docs/releases/0.5.0.md +0 -29
  97. package/docs/releases/0.5.1.md +0 -17
  98. package/docs/releases/0.6.0.md +0 -18
  99. package/docs/releases/0.7.0.md +0 -24
  100. package/docs/releases/0.7.1.md +0 -21
  101. package/docs/releases/0.7.2.md +0 -21
  102. package/docs/releases/0.7.3.md +0 -23
  103. package/docs/releases/0.7.4.md +0 -17
  104. package/docs/releases/0.7.5.md +0 -20
  105. package/schemas/release-attestation-v1.schema.json +0 -210
@@ -1,615 +0,0 @@
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 ? "knodin.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, ".knodin", "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: ".knodin/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 knodin.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 : "knodin.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, "knodin.yaml")));
318
- if (team.schemaVersion !== undefined && team.schemaVersion !== 1) {
319
- throw new Error("knodin.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
- }