@kaddo/cli 3.77.0 → 3.79.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.
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>admin</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-sZtkPyjd.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-BPQdYfAp.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
getWorkItemCaptureDefinition as coreGetCaptureDefinition,
|
|
70
70
|
buildRefinementHandoff as coreBuildRefinementHandoff,
|
|
71
71
|
getSystemMapProjection as coreGetSystemMapProjection,
|
|
72
|
+
buildTopologyEnrichmentHandoff as coreBuildTopologyHandoff,
|
|
72
73
|
WorkItemWriteError,
|
|
73
74
|
exists,
|
|
74
75
|
join,
|
|
@@ -139,6 +140,11 @@ function getCaptureDefinition() {
|
|
|
139
140
|
function getSystemMap(dir) {
|
|
140
141
|
return coreGetSystemMapProjection(dir);
|
|
141
142
|
}
|
|
143
|
+
function getTopologyHandoff(dir) {
|
|
144
|
+
const config = loadConfig(dir);
|
|
145
|
+
if (!config) throw new CoreError("PROJECT_NOT_FOUND", "No Kaddo project was found.");
|
|
146
|
+
return coreBuildTopologyHandoff(dir, config.project.name ?? "this project");
|
|
147
|
+
}
|
|
142
148
|
function getRefinementHandoff(dir, workItemId) {
|
|
143
149
|
assertValidWorkItemId(workItemId);
|
|
144
150
|
try {
|
|
@@ -590,7 +596,12 @@ var SystemMapNodeSchema = z.object({
|
|
|
590
596
|
path: z.string().optional(),
|
|
591
597
|
workItemRef: z.string().optional(),
|
|
592
598
|
knowledgeRef: z.object({ id: z.string(), layer: z.string() }).optional(),
|
|
593
|
-
moduleId: z.string().optional()
|
|
599
|
+
moduleId: z.string().optional(),
|
|
600
|
+
purpose: z.string().optional(),
|
|
601
|
+
implementationRefs: z.array(z.string()).optional(),
|
|
602
|
+
knowledgeRefs: z.array(z.object({ id: z.string(), layer: z.string() })).optional(),
|
|
603
|
+
provenance: z.string().optional(),
|
|
604
|
+
evidence: z.array(z.string()).optional()
|
|
594
605
|
});
|
|
595
606
|
var SystemMapRelationshipSchema = z.object({
|
|
596
607
|
id: z.string(),
|
|
@@ -618,9 +629,20 @@ var SystemMapProjectionSchema = z.object({
|
|
|
618
629
|
coverage: z.enum(["good", "partial", "sparse", "empty"]),
|
|
619
630
|
available: z.boolean(),
|
|
620
631
|
dimensions: z.object({ system: z.number(), knowledge: z.number(), delivery: z.number(), implementation: z.number(), unknown: z.number() }),
|
|
621
|
-
topologyAvailable: z.boolean()
|
|
632
|
+
topologyAvailable: z.boolean(),
|
|
633
|
+
topologyStatus: z.enum(["unavailable", "partial", "available"]),
|
|
634
|
+
semanticEntityCount: z.number(),
|
|
635
|
+
technicalRelationshipCount: z.number(),
|
|
636
|
+
topologyFindings: z.array(z.object({ level: z.enum(["blocking", "warning"]), message: z.string() }))
|
|
622
637
|
})
|
|
623
638
|
});
|
|
639
|
+
var TopologyEnrichmentHandoffSchema = z.object({
|
|
640
|
+
projectName: z.string(),
|
|
641
|
+
recommendedAgent: z.string(),
|
|
642
|
+
recommendedSkill: z.string(),
|
|
643
|
+
targetFile: z.string(),
|
|
644
|
+
text: z.string()
|
|
645
|
+
});
|
|
624
646
|
var ErrorResponseSchema = z.object({
|
|
625
647
|
error: z.object({
|
|
626
648
|
code: z.string(),
|
|
@@ -782,6 +804,7 @@ async function createAdminServer(opts) {
|
|
|
782
804
|
app.get("/api/v1/admin/route", coreRoute(getProjectRoute));
|
|
783
805
|
app.get("/api/v1/admin/findings", coreRoute(getFindings));
|
|
784
806
|
app.get("/api/v1/admin/system", coreRoute(getSystemMap));
|
|
807
|
+
app.get("/api/v1/admin/system/topology-handoff", coreRoute(getTopologyHandoff));
|
|
785
808
|
app.get("/api/v1/admin/knowledge/inventory", coreRoute(getKnowledgeInventory));
|
|
786
809
|
app.get("/api/v1/admin/knowledge/artifact/:artifactId", async (request) => {
|
|
787
810
|
try {
|
|
@@ -940,6 +963,7 @@ export {
|
|
|
940
963
|
SystemMapNodeSchema,
|
|
941
964
|
SystemMapProjectionSchema,
|
|
942
965
|
SystemMapRelationshipSchema,
|
|
966
|
+
TopologyEnrichmentHandoffSchema,
|
|
943
967
|
ValidationResultSchema,
|
|
944
968
|
WorkItemCreateSchema,
|
|
945
969
|
WorkItemCreateWithAnswersSchema,
|
package/dist/core.js
CHANGED
|
@@ -116,8 +116,8 @@ function loadConfig(dir) {
|
|
|
116
116
|
const parsed = configSchema.safeParse(raw ?? {});
|
|
117
117
|
if (!parsed.success) {
|
|
118
118
|
const issues = parsed.error.issues.map((i) => {
|
|
119
|
-
const
|
|
120
|
-
return
|
|
119
|
+
const path4 = i.path.join(".");
|
|
120
|
+
return path4 ? ` - ${path4}: ${i.message}` : ` - ${i.message}`;
|
|
121
121
|
}).join("\n");
|
|
122
122
|
throw new ConfigError(`Invalid .kaddo/config.yml:
|
|
123
123
|
${issues}`);
|
|
@@ -352,11 +352,11 @@ function moduleArtifactCoverage(dir, id) {
|
|
|
352
352
|
};
|
|
353
353
|
}
|
|
354
354
|
function loadMappedModules(dir) {
|
|
355
|
-
const
|
|
356
|
-
if (!exists(
|
|
355
|
+
const path4 = join(dir, DESCRIPTOR_PATH);
|
|
356
|
+
if (!exists(path4)) return [];
|
|
357
357
|
let parsed;
|
|
358
358
|
try {
|
|
359
|
-
parsed = parseYaml3(readFile(
|
|
359
|
+
parsed = parseYaml3(readFile(path4));
|
|
360
360
|
} catch {
|
|
361
361
|
return [];
|
|
362
362
|
}
|
|
@@ -1590,7 +1590,7 @@ function sectionParagraph(md, title) {
|
|
|
1590
1590
|
}
|
|
1591
1591
|
return "";
|
|
1592
1592
|
}
|
|
1593
|
-
function parseCapsule(id,
|
|
1593
|
+
function parseCapsule(id, path4, md) {
|
|
1594
1594
|
const { data } = matter2(md);
|
|
1595
1595
|
const updatedAt = data.updated_at ? String(data.updated_at) : void 0;
|
|
1596
1596
|
let ageDays = null;
|
|
@@ -1600,7 +1600,7 @@ function parseCapsule(id, path3, md) {
|
|
|
1600
1600
|
}
|
|
1601
1601
|
return {
|
|
1602
1602
|
id,
|
|
1603
|
-
path:
|
|
1603
|
+
path: path4,
|
|
1604
1604
|
system: data.system ? String(data.system) : id,
|
|
1605
1605
|
owner: data.owner ? String(data.owner) : void 0,
|
|
1606
1606
|
updatedAt,
|
|
@@ -1662,9 +1662,9 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
|
1662
1662
|
];
|
|
1663
1663
|
const presentLayers = [];
|
|
1664
1664
|
for (const layer of layerDocs) {
|
|
1665
|
-
const
|
|
1666
|
-
if (
|
|
1667
|
-
addNode({ id: layer.id, type: layer.type, label: layer.label, path:
|
|
1665
|
+
const path4 = layer.files.map((f) => `${KNOWLEDGE2}/${f}`).find((rel) => exists(join(dir, rel)));
|
|
1666
|
+
if (path4) {
|
|
1667
|
+
addNode({ id: layer.id, type: layer.type, label: layer.label, path: path4 });
|
|
1668
1668
|
presentLayers.push(layer.id);
|
|
1669
1669
|
}
|
|
1670
1670
|
}
|
|
@@ -3060,10 +3060,10 @@ function resolveNextStep(dir, now = /* @__PURE__ */ new Date()) {
|
|
|
3060
3060
|
const q = (rel) => analyzeKnowledgeArtifact(dir, rel);
|
|
3061
3061
|
const resolveAgent = (agent) => {
|
|
3062
3062
|
const file = agent.endsWith(".md") ? agent : `${agent}.md`;
|
|
3063
|
-
const
|
|
3064
|
-
const installed = isFile(join(dir,
|
|
3063
|
+
const path4 = agentInstallPath(file);
|
|
3064
|
+
const installed = isFile(join(dir, path4));
|
|
3065
3065
|
const group = agentGroupOf(file);
|
|
3066
|
-
return { agentPath:
|
|
3066
|
+
return { agentPath: path4, agentInstalled: installed, installCommand: installed ? void 0 : `kaddo add agents --group ${group}` };
|
|
3067
3067
|
};
|
|
3068
3068
|
const moduleRepo = isModule(config);
|
|
3069
3069
|
const coreRepo = isCore(config);
|
|
@@ -8712,6 +8712,263 @@ function buildRefinementHandoff(dir, workItemId) {
|
|
|
8712
8712
|
};
|
|
8713
8713
|
}
|
|
8714
8714
|
|
|
8715
|
+
// src/core/system-topology.ts
|
|
8716
|
+
import { parse as parseYaml8, stringify as stringifyYaml4 } from "yaml";
|
|
8717
|
+
import fs3 from "fs";
|
|
8718
|
+
import path3 from "path";
|
|
8719
|
+
import crypto2 from "crypto";
|
|
8720
|
+
var TOPOLOGY_FILE = "knowledge/tech/system-topology.yml";
|
|
8721
|
+
var SYSTEM_ENTITY_KINDS = /* @__PURE__ */ new Set([
|
|
8722
|
+
"system",
|
|
8723
|
+
"application",
|
|
8724
|
+
"service",
|
|
8725
|
+
"component",
|
|
8726
|
+
"api",
|
|
8727
|
+
"interface",
|
|
8728
|
+
"datastore",
|
|
8729
|
+
"queue",
|
|
8730
|
+
"job",
|
|
8731
|
+
"external-system",
|
|
8732
|
+
"module",
|
|
8733
|
+
"unknown"
|
|
8734
|
+
]);
|
|
8735
|
+
var TECHNICAL_RELATIONSHIP_TYPES = /* @__PURE__ */ new Set([
|
|
8736
|
+
"contains",
|
|
8737
|
+
"depends-on",
|
|
8738
|
+
"calls",
|
|
8739
|
+
"reads-from",
|
|
8740
|
+
"writes-to",
|
|
8741
|
+
"publishes-to",
|
|
8742
|
+
"subscribes-to",
|
|
8743
|
+
"integrates-with",
|
|
8744
|
+
"runs-on",
|
|
8745
|
+
"implemented-by"
|
|
8746
|
+
]);
|
|
8747
|
+
var PROVENANCE = /* @__PURE__ */ new Set(["declared", "derived", "agent-reviewed"]);
|
|
8748
|
+
function isRelative(p2) {
|
|
8749
|
+
return typeof p2 === "string" && p2.trim() !== "" && !/^([a-zA-Z]:[\\/]|\/)/.test(p2) && !p2.includes("..");
|
|
8750
|
+
}
|
|
8751
|
+
function strList(v) {
|
|
8752
|
+
return Array.isArray(v) ? v.map((x) => typeof x === "string" ? x.trim() : "").filter(Boolean) : [];
|
|
8753
|
+
}
|
|
8754
|
+
function loadSystemTopology(dir) {
|
|
8755
|
+
const filePath = join(dir, TOPOLOGY_FILE);
|
|
8756
|
+
if (!exists(filePath)) return { entities: [], relationships: [], findings: [], declared: false };
|
|
8757
|
+
const parsed = parseTopologyContent(readFile(filePath), dir);
|
|
8758
|
+
return { ...parsed, declared: true };
|
|
8759
|
+
}
|
|
8760
|
+
function parseTopologyContent(raw, dir) {
|
|
8761
|
+
let parsed;
|
|
8762
|
+
try {
|
|
8763
|
+
parsed = parseYaml8(raw) ?? {};
|
|
8764
|
+
} catch {
|
|
8765
|
+
return { entities: [], relationships: [], findings: [{ level: "blocking", message: "The topology could not be parsed." }] };
|
|
8766
|
+
}
|
|
8767
|
+
const sourceRevision = typeof parsed.source_revision === "string" ? parsed.source_revision : void 0;
|
|
8768
|
+
const findings = [];
|
|
8769
|
+
const validModules = /* @__PURE__ */ new Set(["core", ...loadMappedModules(dir).map((m) => m.id)]);
|
|
8770
|
+
const rawEntities = Array.isArray(parsed.entities) ? parsed.entities : [];
|
|
8771
|
+
const entities = [];
|
|
8772
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
8773
|
+
for (const raw2 of rawEntities) {
|
|
8774
|
+
if (!raw2 || typeof raw2 !== "object") continue;
|
|
8775
|
+
const e = raw2;
|
|
8776
|
+
const id = typeof e.id === "string" ? e.id.trim() : "";
|
|
8777
|
+
if (!id) {
|
|
8778
|
+
findings.push({ level: "warning", message: "A topology entity is missing an id and was skipped." });
|
|
8779
|
+
continue;
|
|
8780
|
+
}
|
|
8781
|
+
if (seenIds.has(id)) {
|
|
8782
|
+
findings.push({ level: "blocking", message: `Duplicate topology entity id "${id}".` });
|
|
8783
|
+
continue;
|
|
8784
|
+
}
|
|
8785
|
+
const kind = typeof e.kind === "string" ? e.kind.trim() : "unknown";
|
|
8786
|
+
if (!SYSTEM_ENTITY_KINDS.has(kind)) {
|
|
8787
|
+
findings.push({ level: "warning", message: `Entity "${id}" has an unknown kind "${kind}"; treated as unknown.` });
|
|
8788
|
+
}
|
|
8789
|
+
const moduleRaw = typeof e.module === "string" ? e.module.trim() : void 0;
|
|
8790
|
+
if (moduleRaw && !validModules.has(moduleRaw)) findings.push({ level: "warning", message: `Entity "${id}" references unregistered module "${moduleRaw}".` });
|
|
8791
|
+
const rawImpl = e.implementation ?? e.implementation_refs;
|
|
8792
|
+
const implementation = strList(rawImpl).filter((p2) => {
|
|
8793
|
+
if (isRelative(p2)) return true;
|
|
8794
|
+
findings.push({ level: "warning", message: `Entity "${id}" implementation ref "${p2}" is not a safe relative path and was dropped.` });
|
|
8795
|
+
return false;
|
|
8796
|
+
});
|
|
8797
|
+
const prov = e.provenance;
|
|
8798
|
+
const provOrigin = typeof prov === "string" ? prov : prov && typeof prov === "object" && typeof prov.origin === "string" ? String(prov.origin) : void 0;
|
|
8799
|
+
const provenance = provOrigin && PROVENANCE.has(provOrigin) ? provOrigin : void 0;
|
|
8800
|
+
const evidence = strList(e.evidence ?? (prov && typeof prov === "object" ? prov.evidence_refs : void 0)).filter(isRelative);
|
|
8801
|
+
seenIds.add(id);
|
|
8802
|
+
entities.push({
|
|
8803
|
+
id,
|
|
8804
|
+
kind: SYSTEM_ENTITY_KINDS.has(kind) ? kind : "unknown",
|
|
8805
|
+
label: typeof e.label === "string" && e.label.trim() ? e.label.trim() : id,
|
|
8806
|
+
...typeof e.purpose === "string" && e.purpose.trim() ? { purpose: e.purpose.trim() } : {},
|
|
8807
|
+
...moduleRaw && validModules.has(moduleRaw) ? { moduleId: moduleRaw } : {},
|
|
8808
|
+
implementationRefs: implementation,
|
|
8809
|
+
knowledgeRefs: strList(e.knowledge ?? e.knowledge_refs),
|
|
8810
|
+
...provenance ? { provenance } : {},
|
|
8811
|
+
evidence
|
|
8812
|
+
});
|
|
8813
|
+
}
|
|
8814
|
+
const entityIds = new Set(entities.map((e) => e.id));
|
|
8815
|
+
const rawRels = Array.isArray(parsed.relationships) ? parsed.relationships : [];
|
|
8816
|
+
const relationships = [];
|
|
8817
|
+
const seenRels = /* @__PURE__ */ new Set();
|
|
8818
|
+
for (const raw2 of rawRels) {
|
|
8819
|
+
if (!raw2 || typeof raw2 !== "object") continue;
|
|
8820
|
+
const r = raw2;
|
|
8821
|
+
const from = typeof r.from === "string" ? r.from.trim() : typeof r.source === "string" ? r.source.trim() : "";
|
|
8822
|
+
const to = typeof r.to === "string" ? r.to.trim() : typeof r.target === "string" ? r.target.trim() : "";
|
|
8823
|
+
const type = typeof r.type === "string" ? r.type.trim() : "";
|
|
8824
|
+
if (!from || !to || !type) {
|
|
8825
|
+
findings.push({ level: "warning", message: "A topology relationship is missing from/to/type and was skipped." });
|
|
8826
|
+
continue;
|
|
8827
|
+
}
|
|
8828
|
+
if (!TECHNICAL_RELATIONSHIP_TYPES.has(type)) {
|
|
8829
|
+
findings.push({ level: "warning", message: `Relationship type "${type}" is not recognized and was skipped.` });
|
|
8830
|
+
continue;
|
|
8831
|
+
}
|
|
8832
|
+
if (!entityIds.has(from) || !entityIds.has(to)) {
|
|
8833
|
+
findings.push({ level: "blocking", message: `Relationship ${from} \u2192 ${to} references an unknown entity endpoint.` });
|
|
8834
|
+
continue;
|
|
8835
|
+
}
|
|
8836
|
+
const key = `${from}~${type}~${to}`;
|
|
8837
|
+
if (seenRels.has(key)) {
|
|
8838
|
+
findings.push({ level: "warning", message: `Duplicate relationship ${from} ${type} ${to} was skipped.` });
|
|
8839
|
+
continue;
|
|
8840
|
+
}
|
|
8841
|
+
seenRels.add(key);
|
|
8842
|
+
const prov = r.provenance;
|
|
8843
|
+
relationships.push({ from, to, type, evidence: strList(r.evidence ?? prov?.evidence_refs).filter(isRelative) });
|
|
8844
|
+
}
|
|
8845
|
+
return { entities, relationships, findings, ...sourceRevision ? { sourceRevision } : {} };
|
|
8846
|
+
}
|
|
8847
|
+
function validateSystemTopology(dir) {
|
|
8848
|
+
return loadSystemTopology(dir).findings;
|
|
8849
|
+
}
|
|
8850
|
+
var TopologyWriteError = class extends Error {
|
|
8851
|
+
code;
|
|
8852
|
+
constructor(code, message) {
|
|
8853
|
+
super(message);
|
|
8854
|
+
this.name = "TopologyWriteError";
|
|
8855
|
+
this.code = code;
|
|
8856
|
+
}
|
|
8857
|
+
};
|
|
8858
|
+
function topologyRevision(dir) {
|
|
8859
|
+
const filePath = join(dir, TOPOLOGY_FILE);
|
|
8860
|
+
const raw = exists(filePath) ? readFile(filePath) : "";
|
|
8861
|
+
return crypto2.createHash("sha256").update(raw, "utf-8").digest("hex");
|
|
8862
|
+
}
|
|
8863
|
+
function validateTopologyProposal(dir, proposalYaml) {
|
|
8864
|
+
const { entities, relationships, findings } = parseTopologyContent(proposalYaml, dir);
|
|
8865
|
+
const blocking = findings.filter((f) => f.level === "blocking").length;
|
|
8866
|
+
const warning = findings.filter((f) => f.level === "warning").length;
|
|
8867
|
+
return { findings, blocking, warning, canApply: blocking === 0, entityCount: entities.length, relationshipCount: relationships.length };
|
|
8868
|
+
}
|
|
8869
|
+
function serializeTopology(entities, relationships) {
|
|
8870
|
+
const doc = {
|
|
8871
|
+
entities: entities.map((e) => ({
|
|
8872
|
+
id: e.id,
|
|
8873
|
+
kind: e.kind,
|
|
8874
|
+
label: e.label,
|
|
8875
|
+
...e.purpose ? { purpose: e.purpose } : {},
|
|
8876
|
+
...e.moduleId ? { module: e.moduleId } : {},
|
|
8877
|
+
...e.implementationRefs.length ? { implementation: e.implementationRefs } : {},
|
|
8878
|
+
...e.knowledgeRefs.length ? { knowledge: e.knowledgeRefs } : {},
|
|
8879
|
+
...e.provenance ? { provenance: e.provenance } : {},
|
|
8880
|
+
...e.evidence.length ? { evidence: e.evidence } : {}
|
|
8881
|
+
})),
|
|
8882
|
+
relationships: relationships.map((r) => ({ from: r.from, to: r.to, type: r.type, ...r.evidence.length ? { evidence: r.evidence } : {} }))
|
|
8883
|
+
};
|
|
8884
|
+
return stringifyYaml4(doc);
|
|
8885
|
+
}
|
|
8886
|
+
function atomicWrite2(filePath, content) {
|
|
8887
|
+
fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
|
|
8888
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
8889
|
+
fs3.writeFileSync(tmp, content, "utf-8");
|
|
8890
|
+
try {
|
|
8891
|
+
fs3.renameSync(tmp, filePath);
|
|
8892
|
+
} catch (err) {
|
|
8893
|
+
try {
|
|
8894
|
+
fs3.rmSync(tmp, { force: true });
|
|
8895
|
+
} catch {
|
|
8896
|
+
}
|
|
8897
|
+
throw err;
|
|
8898
|
+
}
|
|
8899
|
+
}
|
|
8900
|
+
function applyTopologyProposal(dir, proposalYaml, expectedRevision) {
|
|
8901
|
+
const validation = validateTopologyProposal(dir, proposalYaml);
|
|
8902
|
+
if (!validation.canApply) throw new TopologyWriteError("TOPOLOGY_INVALID", "The topology proposal has blocking findings and cannot be applied.");
|
|
8903
|
+
const current = topologyRevision(dir);
|
|
8904
|
+
if (expectedRevision != null && expectedRevision !== current) {
|
|
8905
|
+
throw new TopologyWriteError("TOPOLOGY_CONFLICT", "The topology changed after this proposal was created. Refresh and validate the proposal again.");
|
|
8906
|
+
}
|
|
8907
|
+
const proposal = parseTopologyContent(proposalYaml, dir);
|
|
8908
|
+
const existing = loadSystemTopology(dir);
|
|
8909
|
+
const entityMap = /* @__PURE__ */ new Map();
|
|
8910
|
+
for (const e of existing.entities) entityMap.set(e.id, e);
|
|
8911
|
+
let entitiesNew = 0, entitiesUpdated = 0;
|
|
8912
|
+
for (const e of proposal.entities) {
|
|
8913
|
+
if (entityMap.has(e.id)) entitiesUpdated++;
|
|
8914
|
+
else entitiesNew++;
|
|
8915
|
+
entityMap.set(e.id, e);
|
|
8916
|
+
}
|
|
8917
|
+
const relKey = (r) => `${r.from}~${r.type}~${r.to}`;
|
|
8918
|
+
const relMap = /* @__PURE__ */ new Map();
|
|
8919
|
+
for (const r of existing.relationships) relMap.set(relKey(r), r);
|
|
8920
|
+
let relationshipsNew = 0;
|
|
8921
|
+
for (const r of proposal.relationships) {
|
|
8922
|
+
if (!relMap.has(relKey(r))) relationshipsNew++;
|
|
8923
|
+
relMap.set(relKey(r), r);
|
|
8924
|
+
}
|
|
8925
|
+
const raw = serializeTopology([...entityMap.values()], [...relMap.values()]);
|
|
8926
|
+
const finalCheck = validateTopologyProposal(dir, raw);
|
|
8927
|
+
if (!finalCheck.canApply) throw new TopologyWriteError("TOPOLOGY_INVALID", "The merged topology is invalid; no changes were written.");
|
|
8928
|
+
atomicWrite2(join(dir, TOPOLOGY_FILE), raw);
|
|
8929
|
+
return { revision: topologyRevision(dir), entitiesNew, entitiesUpdated, relationshipsNew };
|
|
8930
|
+
}
|
|
8931
|
+
function buildTopologyEnrichmentHandoff(dir, projectName) {
|
|
8932
|
+
const mappedModules = loadMappedModules(dir).map((m) => m.id);
|
|
8933
|
+
const lines = [
|
|
8934
|
+
`Enrich the semantic system topology for the Kaddo project "${projectName}".`,
|
|
8935
|
+
"",
|
|
8936
|
+
"Use the canonical architecture-agent and graph-metadata-review skill (via Kaddo MCP or skills).",
|
|
8937
|
+
"Inspect the actual repository and relevant mapped modules \u2014 do not infer architecture from",
|
|
8938
|
+
"filenames or directory names."
|
|
8939
|
+
];
|
|
8940
|
+
if (mappedModules.length > 0) {
|
|
8941
|
+
lines.push("", `This is a multirepo project. Inspect the relevant mapped modules (${mappedModules.join(", ")}) before finalizing the topology.`);
|
|
8942
|
+
}
|
|
8943
|
+
lines.push(
|
|
8944
|
+
"",
|
|
8945
|
+
"Build a topology PROPOSAL first \u2014 do not write canonical metadata directly. For each entity",
|
|
8946
|
+
"capture, only when supported by evidence:",
|
|
8947
|
+
"- a stable id and semantic kind (application/service/component/api/interface/datastore/queue/job/external-system);",
|
|
8948
|
+
"- a responsibility/purpose (what it does, not how);",
|
|
8949
|
+
"- the owning module/repository;",
|
|
8950
|
+
"- implementation references (relative paths);",
|
|
8951
|
+
"- relevant Knowledge references (capability/ADR ids), not Kaddo operational assets;",
|
|
8952
|
+
"- provenance/evidence.",
|
|
8953
|
+
"",
|
|
8954
|
+
"Capture evidence-backed technical relationships: contains, calls, depends-on, reads-from,",
|
|
8955
|
+
"writes-to, integrates-with, runs-on. Preserve Unknown when evidence is insufficient.",
|
|
8956
|
+
"",
|
|
8957
|
+
"Validate the proposal through Kaddo before applying it. Do not write canonical topology metadata",
|
|
8958
|
+
`unless (1) Kaddo validation succeeds and (2) the human explicitly confirms the write. The canonical`,
|
|
8959
|
+
`artifact is ${TOPOLOGY_FILE}; Core validates and applies it \u2014 do not hand-edit it.`,
|
|
8960
|
+
"",
|
|
8961
|
+
"Do not implement application changes. Do not run mutating Git operations."
|
|
8962
|
+
);
|
|
8963
|
+
return {
|
|
8964
|
+
projectName,
|
|
8965
|
+
recommendedAgent: "architecture-agent",
|
|
8966
|
+
recommendedSkill: "graph-metadata-review",
|
|
8967
|
+
targetFile: TOPOLOGY_FILE,
|
|
8968
|
+
text: lines.join("\n")
|
|
8969
|
+
};
|
|
8970
|
+
}
|
|
8971
|
+
|
|
8715
8972
|
// src/core/system-map.ts
|
|
8716
8973
|
function dimensionOf(type) {
|
|
8717
8974
|
switch (type) {
|
|
@@ -8770,7 +9027,20 @@ function emptyProjection(name, structure) {
|
|
|
8770
9027
|
nodes: [],
|
|
8771
9028
|
relationships: [],
|
|
8772
9029
|
groups: [],
|
|
8773
|
-
metadata: {
|
|
9030
|
+
metadata: {
|
|
9031
|
+
projectName: name,
|
|
9032
|
+
structure,
|
|
9033
|
+
nodeCount: 0,
|
|
9034
|
+
relationshipCount: 0,
|
|
9035
|
+
coverage: "empty",
|
|
9036
|
+
available: false,
|
|
9037
|
+
dimensions: emptyDimensions(),
|
|
9038
|
+
topologyAvailable: false,
|
|
9039
|
+
topologyStatus: "unavailable",
|
|
9040
|
+
semanticEntityCount: 0,
|
|
9041
|
+
technicalRelationshipCount: 0,
|
|
9042
|
+
topologyFindings: []
|
|
9043
|
+
}
|
|
8774
9044
|
};
|
|
8775
9045
|
}
|
|
8776
9046
|
function getSystemMapProjection(dir) {
|
|
@@ -8796,6 +9066,38 @@ function getSystemMapProjection(dir) {
|
|
|
8796
9066
|
type: e.type,
|
|
8797
9067
|
label: EDGE_LABELS[e.type] ?? e.type.replace(/_/g, " ")
|
|
8798
9068
|
}));
|
|
9069
|
+
const knowledgeById = /* @__PURE__ */ new Map();
|
|
9070
|
+
for (const ref of knowledgeByPath.values()) knowledgeById.set(ref.id, ref);
|
|
9071
|
+
const topology = loadSystemTopology(dir);
|
|
9072
|
+
const existingIds = new Set(nodes.map((n) => n.id));
|
|
9073
|
+
for (const e of topology.entities) {
|
|
9074
|
+
const nodeId = `sys:${e.id}`;
|
|
9075
|
+
const node = {
|
|
9076
|
+
id: nodeId,
|
|
9077
|
+
type: e.kind,
|
|
9078
|
+
label: e.label,
|
|
9079
|
+
dimension: "system",
|
|
9080
|
+
implementationRefs: e.implementationRefs,
|
|
9081
|
+
knowledgeRefs: e.knowledgeRefs.map((k) => knowledgeById.get(k)).filter(Boolean),
|
|
9082
|
+
evidence: e.evidence
|
|
9083
|
+
};
|
|
9084
|
+
if (e.purpose) node.purpose = e.purpose;
|
|
9085
|
+
if (e.moduleId) node.moduleId = e.moduleId;
|
|
9086
|
+
if (e.provenance) node.provenance = e.provenance;
|
|
9087
|
+
nodes.push(node);
|
|
9088
|
+
existingIds.add(nodeId);
|
|
9089
|
+
for (const ref of e.implementationRefs) {
|
|
9090
|
+
const fileId = `file:${ref}`;
|
|
9091
|
+
if (!existingIds.has(fileId)) {
|
|
9092
|
+
nodes.push({ id: fileId, type: "file", label: ref.split("/").pop() || ref, dimension: "implementation", path: ref });
|
|
9093
|
+
existingIds.add(fileId);
|
|
9094
|
+
}
|
|
9095
|
+
relationships.push({ id: `${nodeId}~implemented-by~${fileId}`, source: nodeId, target: fileId, type: "implemented-by", label: "implemented by" });
|
|
9096
|
+
}
|
|
9097
|
+
}
|
|
9098
|
+
for (const r of topology.relationships) {
|
|
9099
|
+
relationships.push({ id: `sys:${r.from}~${r.type}~sys:${r.to}`, source: `sys:${r.from}`, target: `sys:${r.to}`, type: r.type, label: r.type.replace(/-/g, " ") });
|
|
9100
|
+
}
|
|
8799
9101
|
const usedGroups = new Set(nodes.map((n) => n.moduleId).filter(Boolean));
|
|
8800
9102
|
const allGroups = [
|
|
8801
9103
|
{ id: "core", label: "core", repositoryId: "core", available: true },
|
|
@@ -8805,6 +9107,9 @@ function getSystemMapProjection(dir) {
|
|
|
8805
9107
|
const groups = allGroups.filter((g) => usedGroups.has(g.id) || g.available === false);
|
|
8806
9108
|
const dimensions = emptyDimensions();
|
|
8807
9109
|
for (const n of nodes) dimensions[n.dimension]++;
|
|
9110
|
+
const semanticEntityCount = topology.entities.length;
|
|
9111
|
+
const technicalRelationshipCount = relationships.filter((r) => TECHNICAL_RELATIONSHIP_TYPES.has(r.type)).length;
|
|
9112
|
+
const topologyStatus = semanticEntityCount === 0 ? "unavailable" : topology.relationships.length > 0 ? "available" : "partial";
|
|
8808
9113
|
return {
|
|
8809
9114
|
system: { name: config.project.name },
|
|
8810
9115
|
nodes,
|
|
@@ -8818,7 +9123,11 @@ function getSystemMapProjection(dir) {
|
|
|
8818
9123
|
coverage: hints.quality,
|
|
8819
9124
|
available: nodes.length > 0,
|
|
8820
9125
|
dimensions,
|
|
8821
|
-
topologyAvailable: dimensions.system > 0
|
|
9126
|
+
topologyAvailable: dimensions.system > 0,
|
|
9127
|
+
topologyStatus,
|
|
9128
|
+
semanticEntityCount,
|
|
9129
|
+
technicalRelationshipCount,
|
|
9130
|
+
topologyFindings: topology.findings
|
|
8822
9131
|
}
|
|
8823
9132
|
};
|
|
8824
9133
|
}
|
|
@@ -8858,14 +9167,18 @@ function toNode(n, knowledgeByPath, wiModules, groupIds) {
|
|
|
8858
9167
|
return node;
|
|
8859
9168
|
}
|
|
8860
9169
|
export {
|
|
9170
|
+
TOPOLOGY_FILE,
|
|
9171
|
+
TopologyWriteError,
|
|
8861
9172
|
WorkItemNotFoundError,
|
|
8862
9173
|
WorkItemWriteError,
|
|
8863
9174
|
analyzeCrossRepoEvidence,
|
|
8864
9175
|
analyzeScopeCoverage,
|
|
9176
|
+
applyTopologyProposal,
|
|
8865
9177
|
buildProjectExplanation,
|
|
8866
9178
|
buildProjectRoute,
|
|
8867
9179
|
buildReadinessReport,
|
|
8868
9180
|
buildRefinementHandoff,
|
|
9181
|
+
buildTopologyEnrichmentHandoff,
|
|
8869
9182
|
computeRefinementStatus,
|
|
8870
9183
|
createWorkItem,
|
|
8871
9184
|
cwd,
|
|
@@ -8887,8 +9200,12 @@ export {
|
|
|
8887
9200
|
lifecycleStateOf,
|
|
8888
9201
|
loadConfig,
|
|
8889
9202
|
loadMappedModules,
|
|
9203
|
+
loadSystemTopology,
|
|
8890
9204
|
readFile,
|
|
9205
|
+
topologyRevision,
|
|
8891
9206
|
transitionWorkItem,
|
|
8892
9207
|
updateWorkItem,
|
|
9208
|
+
validateSystemTopology,
|
|
9209
|
+
validateTopologyProposal,
|
|
8893
9210
|
validateWorkItem
|
|
8894
9211
|
};
|