knodin 0.8.6 → 0.8.7
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 +6 -0
- package/dist/bin/cli.js +153 -11
- package/dist/bin/launcher.js +0 -0
- package/dist/src/cli-model.js +31 -1
- package/dist/src/init.js +16 -0
- package/dist/src/repository-management.js +219 -3
- package/dist/src/skill-management.js +149 -0
- package/dist/src/system-config.js +176 -16
- package/dist/src/system-management.js +508 -0
- package/dist/src/tools/knodin-tools.js +108 -15
- package/docs/CLI.md +7 -0
- package/docs/SYSTEMS-AND-RELATIONSHIPS.md +27 -6
- package/docs/prompts/declare-multi-repository-system.md +18 -0
- package/docs/releases/0.8.7.md +27 -0
- package/package.json +5 -2
- package/skills/knodin-debug/SKILL.md +12 -0
- package/skills/knodin-debug/agents/openai.yaml +4 -0
- package/skills/knodin-explore/SKILL.md +11 -0
- package/skills/knodin-explore/agents/openai.yaml +4 -0
- package/skills/knodin-impact/SKILL.md +11 -0
- package/skills/knodin-impact/agents/openai.yaml +4 -0
- package/skills/knodin-lifecycle/SKILL.md +12 -0
- package/skills/knodin-lifecycle/agents/openai.yaml +4 -0
- package/skills/knodin-refactor/SKILL.md +12 -0
- package/skills/knodin-refactor/agents/openai.yaml +4 -0
- package/skills/knodin-review/SKILL.md +11 -0
- package/skills/knodin-review/agents/openai.yaml +4 -0
- package/skills/knodin-systems/SKILL.md +14 -0
- package/skills/knodin-systems/agents/openai.yaml +4 -0
|
@@ -0,0 +1,149 @@
|
|
|
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 { fileURLToPath } from "node:url";
|
|
6
|
+
export const KNODIN_SKILLS = [
|
|
7
|
+
"knodin-explore",
|
|
8
|
+
"knodin-debug",
|
|
9
|
+
"knodin-impact",
|
|
10
|
+
"knodin-refactor",
|
|
11
|
+
"knodin-review",
|
|
12
|
+
"knodin-systems",
|
|
13
|
+
"knodin-lifecycle",
|
|
14
|
+
];
|
|
15
|
+
const MANIFEST = ".knodin-managed.json";
|
|
16
|
+
function canonicalRoot() {
|
|
17
|
+
const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const candidates = [
|
|
19
|
+
path.resolve(moduleDirectory, "..", "skills"),
|
|
20
|
+
path.resolve(moduleDirectory, "..", "..", "skills"),
|
|
21
|
+
];
|
|
22
|
+
const root = candidates.find((candidate) => fs.existsSync(candidate));
|
|
23
|
+
if (!root)
|
|
24
|
+
throw new Error("knodin packaged skills are missing");
|
|
25
|
+
return root;
|
|
26
|
+
}
|
|
27
|
+
function destinationRoot(client, scope, repo, home) {
|
|
28
|
+
if (scope === "team") {
|
|
29
|
+
if (client === "codex")
|
|
30
|
+
return path.join(repo, ".agents", "skills");
|
|
31
|
+
if (client === "claude")
|
|
32
|
+
return path.join(repo, ".claude", "skills");
|
|
33
|
+
return path.join(repo, ".gemini", "skills");
|
|
34
|
+
}
|
|
35
|
+
if (client === "codex")
|
|
36
|
+
return path.join(home, ".agents", "skills");
|
|
37
|
+
if (client === "claude")
|
|
38
|
+
return path.join(home, ".claude", "skills");
|
|
39
|
+
return path.join(home, ".gemini", "skills");
|
|
40
|
+
}
|
|
41
|
+
function sourceDigest(source) {
|
|
42
|
+
const hash = crypto.createHash("sha256");
|
|
43
|
+
for (const relative of ["SKILL.md", path.join("agents", "openai.yaml")])
|
|
44
|
+
hash.update(fs.readFileSync(path.join(source, relative)));
|
|
45
|
+
return hash.digest("hex");
|
|
46
|
+
}
|
|
47
|
+
function managedState(target) {
|
|
48
|
+
try {
|
|
49
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(target, MANIFEST), "utf8"));
|
|
50
|
+
if (manifest.owner !== "knodin")
|
|
51
|
+
return "unmanaged";
|
|
52
|
+
return manifest.digest === sourceDigest(target) ? "managed" : "modified";
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return "unmanaged";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function installKnodinSkills(options) {
|
|
59
|
+
const result = {
|
|
60
|
+
scope: options.scope,
|
|
61
|
+
installed: [],
|
|
62
|
+
removed: [],
|
|
63
|
+
unchanged: [],
|
|
64
|
+
issues: [],
|
|
65
|
+
};
|
|
66
|
+
if (options.scope === "cli-only")
|
|
67
|
+
return result;
|
|
68
|
+
const home = options.home ?? os.homedir();
|
|
69
|
+
for (const client of options.clients) {
|
|
70
|
+
const root = destinationRoot(client, options.scope, path.resolve(options.repo), home);
|
|
71
|
+
for (const name of KNODIN_SKILLS) {
|
|
72
|
+
const source = path.join(canonicalRoot(), name);
|
|
73
|
+
const target = path.join(root, name);
|
|
74
|
+
const digest = sourceDigest(source);
|
|
75
|
+
if (fs.existsSync(target)) {
|
|
76
|
+
const state = managedState(target);
|
|
77
|
+
if (state !== "managed") {
|
|
78
|
+
result.issues.push({
|
|
79
|
+
path: target,
|
|
80
|
+
message: `${state} skill collision; left unchanged`,
|
|
81
|
+
});
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const current = JSON.parse(fs.readFileSync(path.join(target, MANIFEST), "utf8"));
|
|
87
|
+
if (current.digest === digest) {
|
|
88
|
+
result.unchanged.push(target);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Missing managed version is installed below.
|
|
94
|
+
}
|
|
95
|
+
result.installed.push(target);
|
|
96
|
+
if (options.dryRun)
|
|
97
|
+
continue;
|
|
98
|
+
fs.mkdirSync(root, { recursive: true });
|
|
99
|
+
const temporary = path.join(root, `.${name}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`);
|
|
100
|
+
fs.cpSync(source, temporary, { recursive: true, errorOnExist: true });
|
|
101
|
+
fs.writeFileSync(path.join(temporary, MANIFEST), `${JSON.stringify({ owner: "knodin", digest, client }, null, 2)}\n`);
|
|
102
|
+
if (fs.existsSync(target))
|
|
103
|
+
fs.renameSync(target, `${temporary}.old`);
|
|
104
|
+
fs.renameSync(temporary, target);
|
|
105
|
+
fs.rmSync(`${temporary}.old`, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
export function removeKnodinSkills(options) {
|
|
111
|
+
const result = {
|
|
112
|
+
scope: options.scope,
|
|
113
|
+
installed: [],
|
|
114
|
+
removed: [],
|
|
115
|
+
unchanged: [],
|
|
116
|
+
issues: [],
|
|
117
|
+
};
|
|
118
|
+
const home = options.home ?? os.homedir();
|
|
119
|
+
for (const client of options.clients) {
|
|
120
|
+
const root = destinationRoot(client, options.scope, path.resolve(options.repo), home);
|
|
121
|
+
for (const name of KNODIN_SKILLS) {
|
|
122
|
+
const target = path.join(root, name);
|
|
123
|
+
if (!fs.existsSync(target))
|
|
124
|
+
continue;
|
|
125
|
+
const state = managedState(target);
|
|
126
|
+
if (state !== "managed") {
|
|
127
|
+
result.issues.push({ path: target, message: `${state} skill collision; left unchanged` });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
result.removed.push(target);
|
|
131
|
+
if (!options.dryRun)
|
|
132
|
+
fs.rmSync(target, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
if (!options.dryRun) {
|
|
135
|
+
try {
|
|
136
|
+
fs.rmdirSync(root);
|
|
137
|
+
fs.rmdirSync(path.dirname(root));
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? ""))
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
export function inspectKnodinSkills(options) {
|
|
148
|
+
return installKnodinSkills({ ...options, dryRun: true });
|
|
149
|
+
}
|
|
@@ -190,6 +190,113 @@ function applyPersonalMappings(repositories, xdgConfigHome) {
|
|
|
190
190
|
repository.pathSource = "personal-config";
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
|
+
function personalSystemDocuments(repo, xdgConfigHome) {
|
|
194
|
+
const knodin = path.join(xdgConfigHome, "knodin");
|
|
195
|
+
const personal = asRecord(optionalYaml(path.join(knodin, "repositories.yaml")));
|
|
196
|
+
const mappings = asRecord(personal.repositories);
|
|
197
|
+
const localIds = new Set(Object.entries(mappings)
|
|
198
|
+
.filter(([, checkout]) => typeof checkout === "string" && path.resolve(checkout) === repo)
|
|
199
|
+
.map(([id]) => id));
|
|
200
|
+
if (localIds.size === 0)
|
|
201
|
+
return [];
|
|
202
|
+
let names;
|
|
203
|
+
try {
|
|
204
|
+
names = fs.readdirSync(path.join(knodin, "systems"));
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (error.code === "ENOENT")
|
|
208
|
+
return [];
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
const systemRoot = path.join(knodin, "systems");
|
|
212
|
+
const manifestPaths = names.flatMap((name) => {
|
|
213
|
+
const candidate = path.join(systemRoot, name);
|
|
214
|
+
if (name.endsWith(".yaml") || name.endsWith(".yml"))
|
|
215
|
+
return [candidate];
|
|
216
|
+
try {
|
|
217
|
+
if (!fs.statSync(candidate).isDirectory())
|
|
218
|
+
return [];
|
|
219
|
+
const nested = path.join(candidate, "knodin.yaml");
|
|
220
|
+
return fs.existsSync(nested) ? [nested] : [];
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
const sortedManifestPaths = [...manifestPaths];
|
|
227
|
+
sortedManifestPaths.sort(compareBytes);
|
|
228
|
+
return sortedManifestPaths
|
|
229
|
+
.flatMap((manifest) => {
|
|
230
|
+
try {
|
|
231
|
+
return [asRecord(readYaml(manifest))];
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
const source = fs.readFileSync(manifest, "utf8");
|
|
235
|
+
if ([...localIds].some((id) => source.includes(id)))
|
|
236
|
+
throw error;
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
.filter((document) => {
|
|
241
|
+
const repositories = Array.isArray(document.repositories) ? document.repositories : [];
|
|
242
|
+
return repositories.some((value) => localIds.has(String(asRecord(value).id ?? "")));
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function registeredSystemDocuments(repo, xdgConfigHome) {
|
|
246
|
+
const personal = asRecord(optionalYaml(path.join(xdgConfigHome, "knodin", "repositories.yaml")));
|
|
247
|
+
const mappings = asRecord(personal.repositories);
|
|
248
|
+
const localIds = new Set(Object.entries(mappings)
|
|
249
|
+
.filter(([, checkout]) => typeof checkout === "string" && path.resolve(checkout) === repo)
|
|
250
|
+
.map(([id]) => id));
|
|
251
|
+
if (localIds.size === 0)
|
|
252
|
+
return [];
|
|
253
|
+
const manifests = asRecord(personal.systemManifests);
|
|
254
|
+
const documents = [];
|
|
255
|
+
for (const registration of Object.values(manifests)) {
|
|
256
|
+
const record = asRecord(registration);
|
|
257
|
+
const paths = Array.isArray(record.manifests) ? record.manifests : [];
|
|
258
|
+
for (const manifest of paths) {
|
|
259
|
+
if (typeof manifest !== "string" || !path.isAbsolute(manifest) || !fs.existsSync(manifest))
|
|
260
|
+
continue;
|
|
261
|
+
const document = asRecord(readYaml(manifest));
|
|
262
|
+
const participates = (Array.isArray(document.repositories) ? document.repositories : []).some((value) => localIds.has(String(asRecord(value).id ?? "")));
|
|
263
|
+
if (participates)
|
|
264
|
+
documents.push(document);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return documents;
|
|
268
|
+
}
|
|
269
|
+
function availableTeamFragments(team, repo, xdgConfigHome) {
|
|
270
|
+
const systemIds = new Set((Array.isArray(team.systems) ? team.systems : []).map((value) => String(asRecord(value).id ?? "")));
|
|
271
|
+
if (systemIds.size === 0)
|
|
272
|
+
return [];
|
|
273
|
+
const personal = asRecord(optionalYaml(path.join(xdgConfigHome, "knodin", "repositories.yaml")));
|
|
274
|
+
const mappings = asRecord(personal.repositories);
|
|
275
|
+
const documents = [];
|
|
276
|
+
for (const checkout of Object.values(mappings)) {
|
|
277
|
+
if (typeof checkout !== "string" || path.resolve(checkout) === repo)
|
|
278
|
+
continue;
|
|
279
|
+
const manifest = path.join(checkout, "knodin.yaml");
|
|
280
|
+
if (!fs.existsSync(manifest))
|
|
281
|
+
continue;
|
|
282
|
+
const document = asRecord(readYaml(manifest));
|
|
283
|
+
const overlaps = (Array.isArray(document.systems) ? document.systems : []).some((value) => systemIds.has(String(asRecord(value).id ?? "")));
|
|
284
|
+
if (overlaps)
|
|
285
|
+
documents.push(document);
|
|
286
|
+
}
|
|
287
|
+
return documents;
|
|
288
|
+
}
|
|
289
|
+
function deduplicateDeclarations(values, kind) {
|
|
290
|
+
const unique = new Map();
|
|
291
|
+
for (const value of values) {
|
|
292
|
+
const existing = unique.get(value.id);
|
|
293
|
+
if (!existing)
|
|
294
|
+
unique.set(value.id, value);
|
|
295
|
+
else if (JSON.stringify(existing) !== JSON.stringify(value))
|
|
296
|
+
throw new Error(`conflicting ${kind} identity ${value.id} across available system fragments`);
|
|
297
|
+
}
|
|
298
|
+
return [...unique.values()];
|
|
299
|
+
}
|
|
193
300
|
function configuredCheckout(repository, repositories, repo) {
|
|
194
301
|
if (repository.path)
|
|
195
302
|
return repository.path;
|
|
@@ -313,6 +420,11 @@ function declaredRelationship(value, index) {
|
|
|
313
420
|
const from = stringValue(record.from, `relationships[${index}].from`);
|
|
314
421
|
const to = stringValue(record.to, `relationships[${index}].to`);
|
|
315
422
|
const source = asRecord(record.evidence);
|
|
423
|
+
const evidenceRepository = typeof source.repository === "string" && source.repository ? source.repository : undefined;
|
|
424
|
+
const evidencePath = typeof source.path === "string" && source.path ? source.path : undefined;
|
|
425
|
+
const evidenceLine = typeof source.line === "number" && Number.isSafeInteger(source.line) && source.line > 0
|
|
426
|
+
? source.line
|
|
427
|
+
: undefined;
|
|
316
428
|
const confidence = typeof source.confidence === "number" && source.confidence >= 0 && source.confidence <= 1
|
|
317
429
|
? source.confidence
|
|
318
430
|
: 1;
|
|
@@ -322,7 +434,14 @@ function declaredRelationship(value, index) {
|
|
|
322
434
|
to,
|
|
323
435
|
targetIdentity: to,
|
|
324
436
|
evidence: {
|
|
325
|
-
location: typeof source.location === "string" && source.location
|
|
437
|
+
location: typeof source.location === "string" && source.location
|
|
438
|
+
? source.location
|
|
439
|
+
: evidencePath
|
|
440
|
+
? `${evidenceRepository ? `${evidenceRepository}:` : ""}${evidencePath}${evidenceLine ? `:${evidenceLine}` : ""}`
|
|
441
|
+
: "knodin.yaml",
|
|
442
|
+
...(evidenceRepository ? { repository: evidenceRepository } : {}),
|
|
443
|
+
...(evidencePath ? { path: evidencePath } : {}),
|
|
444
|
+
...(evidenceLine ? { line: evidenceLine } : {}),
|
|
326
445
|
adapter: "knodin-yaml",
|
|
327
446
|
kind: "declared",
|
|
328
447
|
confidence,
|
|
@@ -337,11 +456,31 @@ export function loadSystemConfiguration(repoPath, options = {}) {
|
|
|
337
456
|
if (team.schemaVersion !== undefined && team.schemaVersion !== 1) {
|
|
338
457
|
throw new Error("knodin.yaml: schemaVersion must be 1");
|
|
339
458
|
}
|
|
340
|
-
const repositories = (Array.isArray(team.repositories) ? team.repositories : []).map((value, index) => configuredRepository(value, index));
|
|
341
|
-
const systems = (Array.isArray(team.systems) ? team.systems : []).map((value, index) => configuredSystem(value, index));
|
|
342
|
-
const relationships = (Array.isArray(team.relationships) ? team.relationships : []).map((value, index) => declaredRelationship(value, index));
|
|
343
|
-
const initialization = repositoryInitialization(team.repositoryInitialization);
|
|
344
459
|
const xdgConfigHome = options.xdgConfigHome ?? process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
|
|
460
|
+
const documents = [
|
|
461
|
+
team,
|
|
462
|
+
...availableTeamFragments(team, repo, xdgConfigHome),
|
|
463
|
+
...registeredSystemDocuments(repo, xdgConfigHome),
|
|
464
|
+
...personalSystemDocuments(repo, xdgConfigHome),
|
|
465
|
+
];
|
|
466
|
+
const repositories = deduplicateDeclarations(documents
|
|
467
|
+
.flatMap((document) => (Array.isArray(document.repositories) ? document.repositories : []))
|
|
468
|
+
.map((value, index) => configuredRepository(value, index)), "repository");
|
|
469
|
+
const systems = deduplicateDeclarations(documents
|
|
470
|
+
.flatMap((document) => (Array.isArray(document.systems) ? document.systems : []))
|
|
471
|
+
.map((value, index) => configuredSystem(value, index)), "system");
|
|
472
|
+
const relationships = documents
|
|
473
|
+
.flatMap((document) => (Array.isArray(document.relationships) ? document.relationships : []))
|
|
474
|
+
.map((value, index) => declaredRelationship(value, index));
|
|
475
|
+
const declaredIdentities = new Map();
|
|
476
|
+
for (const relationship of relationships) {
|
|
477
|
+
const identity = `${relationship.type}\0${relationship.from}\0${relationship.to}`;
|
|
478
|
+
const existing = declaredIdentities.get(identity);
|
|
479
|
+
if (existing && JSON.stringify(existing.evidence) !== JSON.stringify(relationship.evidence))
|
|
480
|
+
throw new Error(`conflicting relationship ${relationship.type} ${relationship.from} -> ${relationship.to} across available system fragments`);
|
|
481
|
+
declaredIdentities.set(identity, relationship);
|
|
482
|
+
}
|
|
483
|
+
const initialization = repositoryInitialization(team.repositoryInitialization);
|
|
345
484
|
applyPersonalMappings(repositories, xdgConfigHome);
|
|
346
485
|
const legacyFederation = applyLegacyFederation(repo, repositories, relationships);
|
|
347
486
|
for (const repository of repositories) {
|
|
@@ -485,34 +624,55 @@ export async function validateSystemHealth(config, systemId, status, defaultRepo
|
|
|
485
624
|
}
|
|
486
625
|
return { valid: issues.length === 0, requiresPartial: issues.length > 0, issues };
|
|
487
626
|
}
|
|
488
|
-
export function queryConfiguredSystem(config, systemId,
|
|
627
|
+
export function queryConfiguredSystem(config, systemId, requireComplete = false, healthValidation) {
|
|
489
628
|
const system = config.systems.find(({ id }) => id === systemId);
|
|
490
629
|
if (!system) {
|
|
491
630
|
return { status: "not-found", systemId, available: config.systems.map(({ id }) => id) };
|
|
492
631
|
}
|
|
493
632
|
const validation = healthValidation ?? validateSystemConfiguration(config);
|
|
494
|
-
|
|
633
|
+
const componentRepository = new Map(system.components.flatMap(({ id, repository }) => repository ? [[id, repository]] : []));
|
|
634
|
+
const unavailableRepositories = new Set(validation.issues.flatMap(({ code, identity }) => {
|
|
635
|
+
if (code === "unresolved-repository" || code === "missing-checkout")
|
|
636
|
+
return [identity];
|
|
637
|
+
if (code === "unavailable-component") {
|
|
638
|
+
const repository = componentRepository.get(identity);
|
|
639
|
+
return repository ? [repository] : [];
|
|
640
|
+
}
|
|
641
|
+
return [];
|
|
642
|
+
}));
|
|
643
|
+
if (!validation.valid && requireComplete) {
|
|
495
644
|
return {
|
|
496
645
|
status: "unavailable",
|
|
497
646
|
systemId,
|
|
498
647
|
relationships: [],
|
|
648
|
+
omittedRelationships: [],
|
|
649
|
+
unavailableRepositories: [...unavailableRepositories].sort(compareBytes),
|
|
499
650
|
omissions: validation.issues,
|
|
500
651
|
};
|
|
501
652
|
}
|
|
502
|
-
const
|
|
503
|
-
.
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
.
|
|
507
|
-
|
|
653
|
+
const omittedComponents = new Set([
|
|
654
|
+
...system.components
|
|
655
|
+
.filter(({ repository }) => repository !== undefined && unavailableRepositories.has(repository))
|
|
656
|
+
.map(({ id }) => id),
|
|
657
|
+
...validation.issues
|
|
658
|
+
.filter(({ code }) => code === "unavailable-component")
|
|
659
|
+
.map(({ identity }) => identity),
|
|
660
|
+
]);
|
|
508
661
|
const componentIds = new Set(system.components.map(({ id }) => id));
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
662
|
+
const relevantRelationships = config.relationships.filter(({ from, to }) => componentIds.has(from) || componentIds.has(to));
|
|
663
|
+
const invalidRelationships = new Set(validation.issues
|
|
664
|
+
.filter(({ code }) => ["invalid-relationship", "low-confidence-relationship", "configuration-drift"].includes(code))
|
|
665
|
+
.map(({ identity }) => identity));
|
|
666
|
+
const relationships = relevantRelationships.filter(({ from, to }) => !omittedComponents.has(from) &&
|
|
667
|
+
!omittedComponents.has(to) &&
|
|
668
|
+
!invalidRelationships.has(`${from}->${to}`));
|
|
669
|
+
const returned = new Set(relationships);
|
|
512
670
|
return {
|
|
513
671
|
status: validation.valid ? "ok" : "partial",
|
|
514
672
|
systemId,
|
|
515
673
|
relationships,
|
|
674
|
+
omittedRelationships: relevantRelationships.filter((relationship) => !returned.has(relationship)),
|
|
675
|
+
unavailableRepositories: [...unavailableRepositories].sort(compareBytes),
|
|
516
676
|
omissions: validation.issues,
|
|
517
677
|
};
|
|
518
678
|
}
|