@kaddo/cli 3.78.0 → 3.80.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.
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);
|
|
@@ -7752,6 +7752,265 @@ function analyzeCrossRepoEvidence(input) {
|
|
|
7752
7752
|
|
|
7753
7753
|
// src/core/work-items.ts
|
|
7754
7754
|
import matter6 from "gray-matter";
|
|
7755
|
+
|
|
7756
|
+
// src/core/system-topology.ts
|
|
7757
|
+
import { parse as parseYaml8, stringify as stringifyYaml4 } from "yaml";
|
|
7758
|
+
import fs2 from "fs";
|
|
7759
|
+
import path2 from "path";
|
|
7760
|
+
import crypto from "crypto";
|
|
7761
|
+
var TOPOLOGY_FILE = "knowledge/tech/system-topology.yml";
|
|
7762
|
+
var SYSTEM_ENTITY_KINDS = /* @__PURE__ */ new Set([
|
|
7763
|
+
"system",
|
|
7764
|
+
"application",
|
|
7765
|
+
"service",
|
|
7766
|
+
"component",
|
|
7767
|
+
"api",
|
|
7768
|
+
"interface",
|
|
7769
|
+
"datastore",
|
|
7770
|
+
"queue",
|
|
7771
|
+
"job",
|
|
7772
|
+
"external-system",
|
|
7773
|
+
"module",
|
|
7774
|
+
"unknown"
|
|
7775
|
+
]);
|
|
7776
|
+
var TECHNICAL_RELATIONSHIP_TYPES = /* @__PURE__ */ new Set([
|
|
7777
|
+
"contains",
|
|
7778
|
+
"depends-on",
|
|
7779
|
+
"calls",
|
|
7780
|
+
"reads-from",
|
|
7781
|
+
"writes-to",
|
|
7782
|
+
"publishes-to",
|
|
7783
|
+
"subscribes-to",
|
|
7784
|
+
"integrates-with",
|
|
7785
|
+
"runs-on",
|
|
7786
|
+
"implemented-by"
|
|
7787
|
+
]);
|
|
7788
|
+
var PROVENANCE = /* @__PURE__ */ new Set(["declared", "derived", "agent-reviewed"]);
|
|
7789
|
+
function isRelative(p2) {
|
|
7790
|
+
return typeof p2 === "string" && p2.trim() !== "" && !/^([a-zA-Z]:[\\/]|\/)/.test(p2) && !p2.includes("..");
|
|
7791
|
+
}
|
|
7792
|
+
function strList(v) {
|
|
7793
|
+
return Array.isArray(v) ? v.map((x) => typeof x === "string" ? x.trim() : "").filter(Boolean) : [];
|
|
7794
|
+
}
|
|
7795
|
+
function loadSystemTopology(dir) {
|
|
7796
|
+
const filePath = join(dir, TOPOLOGY_FILE);
|
|
7797
|
+
if (!exists(filePath)) return { entities: [], relationships: [], findings: [], declared: false };
|
|
7798
|
+
const parsed = parseTopologyContent(readFile(filePath), dir);
|
|
7799
|
+
return { ...parsed, declared: true };
|
|
7800
|
+
}
|
|
7801
|
+
function parseTopologyContent(raw, dir) {
|
|
7802
|
+
let parsed;
|
|
7803
|
+
try {
|
|
7804
|
+
parsed = parseYaml8(raw) ?? {};
|
|
7805
|
+
} catch {
|
|
7806
|
+
return { entities: [], relationships: [], findings: [{ level: "blocking", message: "The topology could not be parsed." }] };
|
|
7807
|
+
}
|
|
7808
|
+
const sourceRevision = typeof parsed.source_revision === "string" ? parsed.source_revision : void 0;
|
|
7809
|
+
const findings = [];
|
|
7810
|
+
const validModules = /* @__PURE__ */ new Set(["core", ...loadMappedModules(dir).map((m) => m.id)]);
|
|
7811
|
+
const rawEntities = Array.isArray(parsed.entities) ? parsed.entities : [];
|
|
7812
|
+
const entities = [];
|
|
7813
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
7814
|
+
for (const raw2 of rawEntities) {
|
|
7815
|
+
if (!raw2 || typeof raw2 !== "object") continue;
|
|
7816
|
+
const e = raw2;
|
|
7817
|
+
const id = typeof e.id === "string" ? e.id.trim() : "";
|
|
7818
|
+
if (!id) {
|
|
7819
|
+
findings.push({ level: "warning", message: "A topology entity is missing an id and was skipped." });
|
|
7820
|
+
continue;
|
|
7821
|
+
}
|
|
7822
|
+
if (seenIds.has(id)) {
|
|
7823
|
+
findings.push({ level: "blocking", message: `Duplicate topology entity id "${id}".` });
|
|
7824
|
+
continue;
|
|
7825
|
+
}
|
|
7826
|
+
const kind = typeof e.kind === "string" ? e.kind.trim() : "unknown";
|
|
7827
|
+
if (!SYSTEM_ENTITY_KINDS.has(kind)) {
|
|
7828
|
+
findings.push({ level: "warning", message: `Entity "${id}" has an unknown kind "${kind}"; treated as unknown.` });
|
|
7829
|
+
}
|
|
7830
|
+
const moduleRaw = typeof e.module === "string" ? e.module.trim() : void 0;
|
|
7831
|
+
if (moduleRaw && !validModules.has(moduleRaw)) findings.push({ level: "warning", message: `Entity "${id}" references unregistered module "${moduleRaw}".` });
|
|
7832
|
+
const rawImpl = e.implementation ?? e.implementation_refs;
|
|
7833
|
+
const implementation = strList(rawImpl).filter((p2) => {
|
|
7834
|
+
if (isRelative(p2)) return true;
|
|
7835
|
+
findings.push({ level: "warning", message: `Entity "${id}" implementation ref "${p2}" is not a safe relative path and was dropped.` });
|
|
7836
|
+
return false;
|
|
7837
|
+
});
|
|
7838
|
+
const prov = e.provenance;
|
|
7839
|
+
const provOrigin = typeof prov === "string" ? prov : prov && typeof prov === "object" && typeof prov.origin === "string" ? String(prov.origin) : void 0;
|
|
7840
|
+
const provenance = provOrigin && PROVENANCE.has(provOrigin) ? provOrigin : void 0;
|
|
7841
|
+
const evidence = strList(e.evidence ?? (prov && typeof prov === "object" ? prov.evidence_refs : void 0)).filter(isRelative);
|
|
7842
|
+
seenIds.add(id);
|
|
7843
|
+
entities.push({
|
|
7844
|
+
id,
|
|
7845
|
+
kind: SYSTEM_ENTITY_KINDS.has(kind) ? kind : "unknown",
|
|
7846
|
+
label: typeof e.label === "string" && e.label.trim() ? e.label.trim() : id,
|
|
7847
|
+
...typeof e.purpose === "string" && e.purpose.trim() ? { purpose: e.purpose.trim() } : {},
|
|
7848
|
+
...moduleRaw && validModules.has(moduleRaw) ? { moduleId: moduleRaw } : {},
|
|
7849
|
+
implementationRefs: implementation,
|
|
7850
|
+
knowledgeRefs: strList(e.knowledge ?? e.knowledge_refs),
|
|
7851
|
+
...provenance ? { provenance } : {},
|
|
7852
|
+
evidence
|
|
7853
|
+
});
|
|
7854
|
+
}
|
|
7855
|
+
const entityIds = new Set(entities.map((e) => e.id));
|
|
7856
|
+
const rawRels = Array.isArray(parsed.relationships) ? parsed.relationships : [];
|
|
7857
|
+
const relationships = [];
|
|
7858
|
+
const seenRels = /* @__PURE__ */ new Set();
|
|
7859
|
+
for (const raw2 of rawRels) {
|
|
7860
|
+
if (!raw2 || typeof raw2 !== "object") continue;
|
|
7861
|
+
const r = raw2;
|
|
7862
|
+
const from = typeof r.from === "string" ? r.from.trim() : typeof r.source === "string" ? r.source.trim() : "";
|
|
7863
|
+
const to = typeof r.to === "string" ? r.to.trim() : typeof r.target === "string" ? r.target.trim() : "";
|
|
7864
|
+
const type = typeof r.type === "string" ? r.type.trim() : "";
|
|
7865
|
+
if (!from || !to || !type) {
|
|
7866
|
+
findings.push({ level: "warning", message: "A topology relationship is missing from/to/type and was skipped." });
|
|
7867
|
+
continue;
|
|
7868
|
+
}
|
|
7869
|
+
if (!TECHNICAL_RELATIONSHIP_TYPES.has(type)) {
|
|
7870
|
+
findings.push({ level: "warning", message: `Relationship type "${type}" is not recognized and was skipped.` });
|
|
7871
|
+
continue;
|
|
7872
|
+
}
|
|
7873
|
+
if (!entityIds.has(from) || !entityIds.has(to)) {
|
|
7874
|
+
findings.push({ level: "blocking", message: `Relationship ${from} \u2192 ${to} references an unknown entity endpoint.` });
|
|
7875
|
+
continue;
|
|
7876
|
+
}
|
|
7877
|
+
const key = `${from}~${type}~${to}`;
|
|
7878
|
+
if (seenRels.has(key)) {
|
|
7879
|
+
findings.push({ level: "warning", message: `Duplicate relationship ${from} ${type} ${to} was skipped.` });
|
|
7880
|
+
continue;
|
|
7881
|
+
}
|
|
7882
|
+
seenRels.add(key);
|
|
7883
|
+
const prov = r.provenance;
|
|
7884
|
+
relationships.push({ from, to, type, evidence: strList(r.evidence ?? prov?.evidence_refs).filter(isRelative) });
|
|
7885
|
+
}
|
|
7886
|
+
return { entities, relationships, findings, ...sourceRevision ? { sourceRevision } : {} };
|
|
7887
|
+
}
|
|
7888
|
+
function validateSystemTopology(dir) {
|
|
7889
|
+
return loadSystemTopology(dir).findings;
|
|
7890
|
+
}
|
|
7891
|
+
var TopologyWriteError = class extends Error {
|
|
7892
|
+
code;
|
|
7893
|
+
constructor(code, message) {
|
|
7894
|
+
super(message);
|
|
7895
|
+
this.name = "TopologyWriteError";
|
|
7896
|
+
this.code = code;
|
|
7897
|
+
}
|
|
7898
|
+
};
|
|
7899
|
+
function topologyRevision(dir) {
|
|
7900
|
+
const filePath = join(dir, TOPOLOGY_FILE);
|
|
7901
|
+
const raw = exists(filePath) ? readFile(filePath) : "";
|
|
7902
|
+
return crypto.createHash("sha256").update(raw, "utf-8").digest("hex");
|
|
7903
|
+
}
|
|
7904
|
+
function validateTopologyProposal(dir, proposalYaml) {
|
|
7905
|
+
const { entities, relationships, findings } = parseTopologyContent(proposalYaml, dir);
|
|
7906
|
+
const blocking = findings.filter((f) => f.level === "blocking").length;
|
|
7907
|
+
const warning = findings.filter((f) => f.level === "warning").length;
|
|
7908
|
+
return { findings, blocking, warning, canApply: blocking === 0, entityCount: entities.length, relationshipCount: relationships.length };
|
|
7909
|
+
}
|
|
7910
|
+
function serializeTopology(entities, relationships) {
|
|
7911
|
+
const doc = {
|
|
7912
|
+
entities: entities.map((e) => ({
|
|
7913
|
+
id: e.id,
|
|
7914
|
+
kind: e.kind,
|
|
7915
|
+
label: e.label,
|
|
7916
|
+
...e.purpose ? { purpose: e.purpose } : {},
|
|
7917
|
+
...e.moduleId ? { module: e.moduleId } : {},
|
|
7918
|
+
...e.implementationRefs.length ? { implementation: e.implementationRefs } : {},
|
|
7919
|
+
...e.knowledgeRefs.length ? { knowledge: e.knowledgeRefs } : {},
|
|
7920
|
+
...e.provenance ? { provenance: e.provenance } : {},
|
|
7921
|
+
...e.evidence.length ? { evidence: e.evidence } : {}
|
|
7922
|
+
})),
|
|
7923
|
+
relationships: relationships.map((r) => ({ from: r.from, to: r.to, type: r.type, ...r.evidence.length ? { evidence: r.evidence } : {} }))
|
|
7924
|
+
};
|
|
7925
|
+
return stringifyYaml4(doc);
|
|
7926
|
+
}
|
|
7927
|
+
function atomicWrite(filePath, content) {
|
|
7928
|
+
fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
|
|
7929
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
7930
|
+
fs2.writeFileSync(tmp, content, "utf-8");
|
|
7931
|
+
try {
|
|
7932
|
+
fs2.renameSync(tmp, filePath);
|
|
7933
|
+
} catch (err) {
|
|
7934
|
+
try {
|
|
7935
|
+
fs2.rmSync(tmp, { force: true });
|
|
7936
|
+
} catch {
|
|
7937
|
+
}
|
|
7938
|
+
throw err;
|
|
7939
|
+
}
|
|
7940
|
+
}
|
|
7941
|
+
function applyTopologyProposal(dir, proposalYaml, expectedRevision) {
|
|
7942
|
+
const validation = validateTopologyProposal(dir, proposalYaml);
|
|
7943
|
+
if (!validation.canApply) throw new TopologyWriteError("TOPOLOGY_INVALID", "The topology proposal has blocking findings and cannot be applied.");
|
|
7944
|
+
const current = topologyRevision(dir);
|
|
7945
|
+
if (expectedRevision != null && expectedRevision !== current) {
|
|
7946
|
+
throw new TopologyWriteError("TOPOLOGY_CONFLICT", "The topology changed after this proposal was created. Refresh and validate the proposal again.");
|
|
7947
|
+
}
|
|
7948
|
+
const proposal = parseTopologyContent(proposalYaml, dir);
|
|
7949
|
+
const existing = loadSystemTopology(dir);
|
|
7950
|
+
const entityMap = /* @__PURE__ */ new Map();
|
|
7951
|
+
for (const e of existing.entities) entityMap.set(e.id, e);
|
|
7952
|
+
let entitiesNew = 0, entitiesUpdated = 0;
|
|
7953
|
+
for (const e of proposal.entities) {
|
|
7954
|
+
if (entityMap.has(e.id)) entitiesUpdated++;
|
|
7955
|
+
else entitiesNew++;
|
|
7956
|
+
entityMap.set(e.id, e);
|
|
7957
|
+
}
|
|
7958
|
+
const relKey = (r) => `${r.from}~${r.type}~${r.to}`;
|
|
7959
|
+
const relMap = /* @__PURE__ */ new Map();
|
|
7960
|
+
for (const r of existing.relationships) relMap.set(relKey(r), r);
|
|
7961
|
+
let relationshipsNew = 0;
|
|
7962
|
+
for (const r of proposal.relationships) {
|
|
7963
|
+
if (!relMap.has(relKey(r))) relationshipsNew++;
|
|
7964
|
+
relMap.set(relKey(r), r);
|
|
7965
|
+
}
|
|
7966
|
+
const raw = serializeTopology([...entityMap.values()], [...relMap.values()]);
|
|
7967
|
+
const finalCheck = validateTopologyProposal(dir, raw);
|
|
7968
|
+
if (!finalCheck.canApply) throw new TopologyWriteError("TOPOLOGY_INVALID", "The merged topology is invalid; no changes were written.");
|
|
7969
|
+
atomicWrite(join(dir, TOPOLOGY_FILE), raw);
|
|
7970
|
+
return { revision: topologyRevision(dir), entitiesNew, entitiesUpdated, relationshipsNew };
|
|
7971
|
+
}
|
|
7972
|
+
function buildTopologyEnrichmentHandoff(dir, projectName) {
|
|
7973
|
+
const mappedModules = loadMappedModules(dir).map((m) => m.id);
|
|
7974
|
+
const lines = [
|
|
7975
|
+
`Enrich the semantic system topology for the Kaddo project "${projectName}".`,
|
|
7976
|
+
"",
|
|
7977
|
+
"Use the canonical architecture-agent and graph-metadata-review skill (via Kaddo MCP or skills).",
|
|
7978
|
+
"Inspect the actual repository and relevant mapped modules \u2014 do not infer architecture from",
|
|
7979
|
+
"filenames or directory names."
|
|
7980
|
+
];
|
|
7981
|
+
if (mappedModules.length > 0) {
|
|
7982
|
+
lines.push("", `This is a multirepo project. Inspect the relevant mapped modules (${mappedModules.join(", ")}) before finalizing the topology.`);
|
|
7983
|
+
}
|
|
7984
|
+
lines.push(
|
|
7985
|
+
"",
|
|
7986
|
+
"Build a topology PROPOSAL first \u2014 do not write canonical metadata directly. For each entity",
|
|
7987
|
+
"capture, only when supported by evidence:",
|
|
7988
|
+
"- a stable id and semantic kind (application/service/component/api/interface/datastore/queue/job/external-system);",
|
|
7989
|
+
"- a responsibility/purpose (what it does, not how);",
|
|
7990
|
+
"- the owning module/repository;",
|
|
7991
|
+
"- implementation references (relative paths);",
|
|
7992
|
+
"- relevant Knowledge references (capability/ADR ids), not Kaddo operational assets;",
|
|
7993
|
+
"- provenance/evidence.",
|
|
7994
|
+
"",
|
|
7995
|
+
"Capture evidence-backed technical relationships: contains, calls, depends-on, reads-from,",
|
|
7996
|
+
"writes-to, integrates-with, runs-on. Preserve Unknown when evidence is insufficient.",
|
|
7997
|
+
"",
|
|
7998
|
+
"Validate the proposal through Kaddo before applying it. Do not write canonical topology metadata",
|
|
7999
|
+
`unless (1) Kaddo validation succeeds and (2) the human explicitly confirms the write. The canonical`,
|
|
8000
|
+
`artifact is ${TOPOLOGY_FILE}; Core validates and applies it \u2014 do not hand-edit it.`,
|
|
8001
|
+
"",
|
|
8002
|
+
"Do not implement application changes. Do not run mutating Git operations."
|
|
8003
|
+
);
|
|
8004
|
+
return {
|
|
8005
|
+
projectName,
|
|
8006
|
+
recommendedAgent: "architecture-agent",
|
|
8007
|
+
recommendedSkill: "graph-metadata-review",
|
|
8008
|
+
targetFile: TOPOLOGY_FILE,
|
|
8009
|
+
text: lines.join("\n")
|
|
8010
|
+
};
|
|
8011
|
+
}
|
|
8012
|
+
|
|
8013
|
+
// src/core/work-items.ts
|
|
7755
8014
|
function computeRefinementStatus(wi) {
|
|
7756
8015
|
const aspects = {
|
|
7757
8016
|
outcome: Boolean(wi.currentBehavior?.trim() || wi.targetBehavior?.trim()),
|
|
@@ -7858,10 +8117,23 @@ function getWorkItem(dir, workItemId) {
|
|
|
7858
8117
|
decisions: parseDecisions(match.decisions, knowledgeById),
|
|
7859
8118
|
relatedKnowledge: parseRelatedKnowledge(fm, knowledgeById),
|
|
7860
8119
|
source: parseWorkItemSource(fm),
|
|
7861
|
-
path: match.relPath
|
|
8120
|
+
path: match.relPath,
|
|
8121
|
+
...parseSystemImpact(dir, fm)
|
|
7862
8122
|
};
|
|
7863
8123
|
return { ...detail, refinement: computeRefinementStatus(detail) };
|
|
7864
8124
|
}
|
|
8125
|
+
function parseSystemImpact(dir, fm) {
|
|
8126
|
+
const topology = loadSystemTopology(dir);
|
|
8127
|
+
const byId = new Map(topology.entities.map((e) => [e.id, e]));
|
|
8128
|
+
const resolve = (id) => {
|
|
8129
|
+
const e = byId.get(id);
|
|
8130
|
+
return { id, nodeId: `sys:${id}`, label: e?.label ?? id, kind: e?.kind ?? "unknown", moduleId: e?.moduleId ?? null };
|
|
8131
|
+
};
|
|
8132
|
+
const affected = Array.isArray(fm.affected_system_entities) ? fm.affected_system_entities.map(String).filter(Boolean).map(resolve) : [];
|
|
8133
|
+
const reviewed = Array.isArray(fm.reviewed_system_entities) ? fm.reviewed_system_entities.filter((r) => Boolean(r) && typeof r === "object").map((r) => ({ ...resolve(String(r.id ?? "")), status: String(r.status ?? "unknown"), reason: r.reason ? String(r.reason) : null })).filter((r) => r.id) : [];
|
|
8134
|
+
const graphRevision = typeof fm.graph_revision === "string" && fm.graph_revision.trim() ? fm.graph_revision.trim() : null;
|
|
8135
|
+
return { affectedSystemEntities: affected, reviewedSystemEntities: reviewed, graphRevision };
|
|
8136
|
+
}
|
|
7865
8137
|
function readBody(filePath) {
|
|
7866
8138
|
try {
|
|
7867
8139
|
const raw = readFile(filePath);
|
|
@@ -8029,9 +8301,9 @@ function parseRelatedKnowledge(fm, knowledgeById) {
|
|
|
8029
8301
|
}
|
|
8030
8302
|
|
|
8031
8303
|
// src/core/work-item-write.ts
|
|
8032
|
-
import
|
|
8033
|
-
import
|
|
8034
|
-
import
|
|
8304
|
+
import fs3 from "fs";
|
|
8305
|
+
import path3 from "path";
|
|
8306
|
+
import crypto2 from "crypto";
|
|
8035
8307
|
import matter7 from "gray-matter";
|
|
8036
8308
|
|
|
8037
8309
|
// src/core/knowledge-levels.ts
|
|
@@ -8234,7 +8506,7 @@ var VALID_COVERAGE = /* @__PURE__ */ new Set(["affected", "reviewed-not-affected
|
|
|
8234
8506
|
var VALID_CONFIDENCE = /* @__PURE__ */ new Set(["high", "medium", "low"]);
|
|
8235
8507
|
var EDITABLE_STATES = ["draft"];
|
|
8236
8508
|
function revisionOf(raw) {
|
|
8237
|
-
return
|
|
8509
|
+
return crypto2.createHash("sha256").update(raw, "utf-8").digest("hex");
|
|
8238
8510
|
}
|
|
8239
8511
|
function slugify2(s) {
|
|
8240
8512
|
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
|
|
@@ -8244,7 +8516,7 @@ function nextWorkItemId(dir) {
|
|
|
8244
8516
|
let max = 0;
|
|
8245
8517
|
const walk = (d) => {
|
|
8246
8518
|
if (!exists(d)) return;
|
|
8247
|
-
for (const entry of
|
|
8519
|
+
for (const entry of fs3.readdirSync(d)) {
|
|
8248
8520
|
const full = join(d, entry);
|
|
8249
8521
|
if (isFile(full)) {
|
|
8250
8522
|
const m = entry.match(/WI-(\d+)/);
|
|
@@ -8257,15 +8529,15 @@ function nextWorkItemId(dir) {
|
|
|
8257
8529
|
walk(wiDir);
|
|
8258
8530
|
return `WI-${String(max + 1).padStart(3, "0")}`;
|
|
8259
8531
|
}
|
|
8260
|
-
function
|
|
8261
|
-
|
|
8532
|
+
function atomicWrite2(filePath, content) {
|
|
8533
|
+
fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
|
|
8262
8534
|
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
8263
|
-
|
|
8535
|
+
fs3.writeFileSync(tmp, content, "utf-8");
|
|
8264
8536
|
try {
|
|
8265
|
-
|
|
8537
|
+
fs3.renameSync(tmp, filePath);
|
|
8266
8538
|
} catch (err) {
|
|
8267
8539
|
try {
|
|
8268
|
-
|
|
8540
|
+
fs3.rmSync(tmp, { force: true });
|
|
8269
8541
|
} catch {
|
|
8270
8542
|
}
|
|
8271
8543
|
throw err;
|
|
@@ -8565,7 +8837,7 @@ function createWorkItem(dir, opts) {
|
|
|
8565
8837
|
const filePath = join(dir, relPath);
|
|
8566
8838
|
if (exists(filePath)) throw new WorkItemWriteError("INVALID_INPUT", `Work Item file already exists: ${relPath}`);
|
|
8567
8839
|
const raw = serialize(data, body);
|
|
8568
|
-
|
|
8840
|
+
atomicWrite2(filePath, raw);
|
|
8569
8841
|
return { id, path: relPath, revision: revisionOf(raw) };
|
|
8570
8842
|
}
|
|
8571
8843
|
function validateInput(input) {
|
|
@@ -8587,7 +8859,7 @@ function updateWorkItem(dir, id, input, expectedRevision) {
|
|
|
8587
8859
|
const nextData = applyFrontmatter(data, input);
|
|
8588
8860
|
const nextBody = mergeBody(content, input);
|
|
8589
8861
|
const nextRaw = serialize(nextData, nextBody);
|
|
8590
|
-
|
|
8862
|
+
atomicWrite2(filePath, nextRaw);
|
|
8591
8863
|
return { revision: revisionOf(nextRaw), path: relPath };
|
|
8592
8864
|
}
|
|
8593
8865
|
function validateWorkItem(dir, id) {
|
|
@@ -8615,6 +8887,26 @@ function validateWorkItem(dir, id) {
|
|
|
8615
8887
|
for (const s of input.impactAnalysis) {
|
|
8616
8888
|
if (s.status === "unknown") findings.push({ level: "fyi", message: `Impact on ${s.surface} is unknown.` });
|
|
8617
8889
|
}
|
|
8890
|
+
const topology = loadSystemTopology(dir);
|
|
8891
|
+
const entityById = new Map(topology.entities.map((e) => [e.id, e]));
|
|
8892
|
+
const fm = data;
|
|
8893
|
+
const affectedEntities = Array.isArray(fm.affected_system_entities) ? fm.affected_system_entities.map(String) : [];
|
|
8894
|
+
for (const eid of affectedEntities) {
|
|
8895
|
+
const e = entityById.get(eid);
|
|
8896
|
+
if (!e) {
|
|
8897
|
+
findings.push({ level: "blocking", message: `Affected system entity "${eid}" does not exist in the semantic topology.` });
|
|
8898
|
+
continue;
|
|
8899
|
+
}
|
|
8900
|
+
if (e.moduleId && !input.affectedModules.includes(e.moduleId)) {
|
|
8901
|
+
findings.push({ level: "warning", message: `System entity "${e.label}" belongs to module "${e.moduleId}", which is not in affected_modules.` });
|
|
8902
|
+
}
|
|
8903
|
+
}
|
|
8904
|
+
if (Array.isArray(fm.reviewed_system_entities)) {
|
|
8905
|
+
for (const r of fm.reviewed_system_entities) {
|
|
8906
|
+
const rid = r && typeof r === "object" ? String(r.id ?? "") : "";
|
|
8907
|
+
if (rid && !entityById.has(rid)) findings.push({ level: "warning", message: `Reviewed system entity "${rid}" does not exist in the semantic topology.` });
|
|
8908
|
+
}
|
|
8909
|
+
}
|
|
8618
8910
|
const canMarkReady = !findings.some((f) => f.level === "blocking");
|
|
8619
8911
|
return { findings, canMarkReady };
|
|
8620
8912
|
}
|
|
@@ -8636,235 +8928,19 @@ function transitionWorkItem(dir, id, to, expectedRevision) {
|
|
|
8636
8928
|
}
|
|
8637
8929
|
const nextData = { ...data, status: to };
|
|
8638
8930
|
const nextRaw = serialize(nextData, content);
|
|
8639
|
-
const filename =
|
|
8931
|
+
const filename = path3.basename(filePath);
|
|
8640
8932
|
const targetRel = `${WORK_ITEMS_DIR}/${to}/${filename}`;
|
|
8641
8933
|
const targetPath = join(dir, targetRel);
|
|
8642
|
-
|
|
8643
|
-
if (
|
|
8934
|
+
atomicWrite2(targetPath, nextRaw);
|
|
8935
|
+
if (path3.resolve(targetPath) !== path3.resolve(filePath)) {
|
|
8644
8936
|
try {
|
|
8645
|
-
|
|
8937
|
+
fs3.rmSync(filePath, { force: true });
|
|
8646
8938
|
} catch {
|
|
8647
8939
|
}
|
|
8648
8940
|
}
|
|
8649
8941
|
return { revision: revisionOf(nextRaw), status: to, path: targetRel };
|
|
8650
8942
|
}
|
|
8651
8943
|
|
|
8652
|
-
// src/core/work-item-refinement.ts
|
|
8653
|
-
function toCapture(q) {
|
|
8654
|
-
return { id: q.id, prompt: q.prompt, placeholder: q.placeholder, field: q.frontMatterField, required: q.required };
|
|
8655
|
-
}
|
|
8656
|
-
function getWorkItemCaptureDefinition() {
|
|
8657
|
-
const questions = {};
|
|
8658
|
-
for (const type of WORK_ITEM_TYPES2) {
|
|
8659
|
-
questions[type] = getLevel(getLevelForType(type)).questions.map(toCapture);
|
|
8660
|
-
}
|
|
8661
|
-
return {
|
|
8662
|
-
types: WORK_ITEM_TYPES2.map((t) => ({ value: t, label: t.charAt(0).toUpperCase() + t.slice(1) })),
|
|
8663
|
-
questions
|
|
8664
|
-
};
|
|
8665
|
-
}
|
|
8666
|
-
var RECOMMENDED_AGENT = "work-item-agent";
|
|
8667
|
-
var RECOMMENDED_SKILL = "work-item-refinement";
|
|
8668
|
-
function buildRefinementHandoff(dir, workItemId) {
|
|
8669
|
-
const wi = getWorkItem(dir, workItemId);
|
|
8670
|
-
const config = loadConfig(dir);
|
|
8671
|
-
const projectName = config?.project.name ?? "this project";
|
|
8672
|
-
const mappedModules = loadMappedModules(dir).map((m) => m.id);
|
|
8673
|
-
const multirepo = mappedModules.length > 0;
|
|
8674
|
-
const lines = [
|
|
8675
|
-
`Refine Work Item ${wi.id} \u2014 "${wi.title}" \u2014 in project "${projectName}" using Kaddo.`,
|
|
8676
|
-
"",
|
|
8677
|
-
"Use a Kaddo-enabled agent with access to this repository. Drive the refinement with the",
|
|
8678
|
-
`canonical ${RECOMMENDED_AGENT} and the ${RECOMMENDED_SKILL} skill (via Kaddo MCP or skills).`,
|
|
8679
|
-
"",
|
|
8680
|
-
"Inspect the actual implementation before defining scope \u2014 do not guess affected modules from",
|
|
8681
|
-
"the Work Item title. Read the current behavior in the code first, then classify."
|
|
8682
|
-
];
|
|
8683
|
-
if (multirepo) {
|
|
8684
|
-
lines.push(
|
|
8685
|
-
"",
|
|
8686
|
-
`This is a multirepo project. Evaluate the scope across all relevant mapped modules (${mappedModules.join(", ")})`,
|
|
8687
|
-
"before finalizing affected_modules and module_coverage."
|
|
8688
|
-
);
|
|
8689
|
-
}
|
|
8690
|
-
lines.push(
|
|
8691
|
-
"",
|
|
8692
|
-
`Update the canonical Work Item ${wi.id} with:`,
|
|
8693
|
-
"- current and target behavior;",
|
|
8694
|
-
"- the end-to-end flow (journey);",
|
|
8695
|
-
"- affected modules;",
|
|
8696
|
-
"- module coverage;",
|
|
8697
|
-
"- impact analysis across the relevant surfaces;",
|
|
8698
|
-
"- scope confidence and open unknowns;",
|
|
8699
|
-
"- acceptance criteria;",
|
|
8700
|
-
"- relevant Knowledge / ADR relationships.",
|
|
8701
|
-
"",
|
|
8702
|
-
"Do not implement the Work Item. Do not run mutating Git operations."
|
|
8703
|
-
);
|
|
8704
|
-
return {
|
|
8705
|
-
workItemId: wi.id,
|
|
8706
|
-
title: wi.title,
|
|
8707
|
-
projectName,
|
|
8708
|
-
refinement: wi.refinement,
|
|
8709
|
-
recommendedAgent: RECOMMENDED_AGENT,
|
|
8710
|
-
recommendedSkill: RECOMMENDED_SKILL,
|
|
8711
|
-
text: lines.join("\n")
|
|
8712
|
-
};
|
|
8713
|
-
}
|
|
8714
|
-
|
|
8715
|
-
// src/core/system-topology.ts
|
|
8716
|
-
import { parse as parseYaml8 } from "yaml";
|
|
8717
|
-
var TOPOLOGY_FILE = "knowledge/tech/system-topology.yml";
|
|
8718
|
-
var SYSTEM_ENTITY_KINDS = /* @__PURE__ */ new Set([
|
|
8719
|
-
"system",
|
|
8720
|
-
"application",
|
|
8721
|
-
"service",
|
|
8722
|
-
"component",
|
|
8723
|
-
"api",
|
|
8724
|
-
"interface",
|
|
8725
|
-
"datastore",
|
|
8726
|
-
"queue",
|
|
8727
|
-
"job",
|
|
8728
|
-
"external-system",
|
|
8729
|
-
"module",
|
|
8730
|
-
"unknown"
|
|
8731
|
-
]);
|
|
8732
|
-
var TECHNICAL_RELATIONSHIP_TYPES = /* @__PURE__ */ new Set([
|
|
8733
|
-
"contains",
|
|
8734
|
-
"depends-on",
|
|
8735
|
-
"calls",
|
|
8736
|
-
"reads-from",
|
|
8737
|
-
"writes-to",
|
|
8738
|
-
"publishes-to",
|
|
8739
|
-
"subscribes-to",
|
|
8740
|
-
"integrates-with",
|
|
8741
|
-
"runs-on",
|
|
8742
|
-
"implemented-by"
|
|
8743
|
-
]);
|
|
8744
|
-
var PROVENANCE = /* @__PURE__ */ new Set(["declared", "derived", "agent-reviewed"]);
|
|
8745
|
-
function isRelative(p2) {
|
|
8746
|
-
return typeof p2 === "string" && p2.trim() !== "" && !/^([a-zA-Z]:[\\/]|\/)/.test(p2) && !p2.includes("..");
|
|
8747
|
-
}
|
|
8748
|
-
function strList(v) {
|
|
8749
|
-
return Array.isArray(v) ? v.map((x) => typeof x === "string" ? x.trim() : "").filter(Boolean) : [];
|
|
8750
|
-
}
|
|
8751
|
-
function loadSystemTopology(dir) {
|
|
8752
|
-
const path3 = join(dir, TOPOLOGY_FILE);
|
|
8753
|
-
if (!exists(path3)) return { entities: [], relationships: [], findings: [], declared: false };
|
|
8754
|
-
let parsed;
|
|
8755
|
-
try {
|
|
8756
|
-
parsed = parseYaml8(readFile(path3)) ?? {};
|
|
8757
|
-
} catch {
|
|
8758
|
-
return { entities: [], relationships: [], findings: [{ level: "blocking", message: "system-topology.yml could not be parsed." }], declared: true };
|
|
8759
|
-
}
|
|
8760
|
-
const findings = [];
|
|
8761
|
-
const validModules = /* @__PURE__ */ new Set(["core", ...loadMappedModules(dir).map((m) => m.id)]);
|
|
8762
|
-
const rawEntities = Array.isArray(parsed.entities) ? parsed.entities : [];
|
|
8763
|
-
const entities = [];
|
|
8764
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
8765
|
-
for (const raw of rawEntities) {
|
|
8766
|
-
if (!raw || typeof raw !== "object") continue;
|
|
8767
|
-
const e = raw;
|
|
8768
|
-
const id = typeof e.id === "string" ? e.id.trim() : "";
|
|
8769
|
-
if (!id) {
|
|
8770
|
-
findings.push({ level: "warning", message: "A topology entity is missing an id and was skipped." });
|
|
8771
|
-
continue;
|
|
8772
|
-
}
|
|
8773
|
-
if (seenIds.has(id)) {
|
|
8774
|
-
findings.push({ level: "blocking", message: `Duplicate topology entity id "${id}".` });
|
|
8775
|
-
continue;
|
|
8776
|
-
}
|
|
8777
|
-
const kind = typeof e.kind === "string" ? e.kind.trim() : "unknown";
|
|
8778
|
-
if (!SYSTEM_ENTITY_KINDS.has(kind)) {
|
|
8779
|
-
findings.push({ level: "warning", message: `Entity "${id}" has an unknown kind "${kind}"; treated as unknown.` });
|
|
8780
|
-
}
|
|
8781
|
-
const moduleRaw = typeof e.module === "string" ? e.module.trim() : void 0;
|
|
8782
|
-
if (moduleRaw && !validModules.has(moduleRaw)) findings.push({ level: "warning", message: `Entity "${id}" references unregistered module "${moduleRaw}".` });
|
|
8783
|
-
const implementation = strList(e.implementation).filter((p2) => {
|
|
8784
|
-
if (isRelative(p2)) return true;
|
|
8785
|
-
findings.push({ level: "warning", message: `Entity "${id}" implementation ref "${p2}" is not a safe relative path and was dropped.` });
|
|
8786
|
-
return false;
|
|
8787
|
-
});
|
|
8788
|
-
const provenance = typeof e.provenance === "string" && PROVENANCE.has(e.provenance) ? e.provenance : void 0;
|
|
8789
|
-
seenIds.add(id);
|
|
8790
|
-
entities.push({
|
|
8791
|
-
id,
|
|
8792
|
-
kind: SYSTEM_ENTITY_KINDS.has(kind) ? kind : "unknown",
|
|
8793
|
-
label: typeof e.label === "string" && e.label.trim() ? e.label.trim() : id,
|
|
8794
|
-
...typeof e.purpose === "string" && e.purpose.trim() ? { purpose: e.purpose.trim() } : {},
|
|
8795
|
-
...moduleRaw && validModules.has(moduleRaw) ? { moduleId: moduleRaw } : {},
|
|
8796
|
-
implementationRefs: implementation,
|
|
8797
|
-
knowledgeRefs: strList(e.knowledge),
|
|
8798
|
-
...provenance ? { provenance } : {},
|
|
8799
|
-
evidence: strList(e.evidence).filter(isRelative)
|
|
8800
|
-
});
|
|
8801
|
-
}
|
|
8802
|
-
const entityIds = new Set(entities.map((e) => e.id));
|
|
8803
|
-
const rawRels = Array.isArray(parsed.relationships) ? parsed.relationships : [];
|
|
8804
|
-
const relationships = [];
|
|
8805
|
-
for (const raw of rawRels) {
|
|
8806
|
-
if (!raw || typeof raw !== "object") continue;
|
|
8807
|
-
const r = raw;
|
|
8808
|
-
const from = typeof r.from === "string" ? r.from.trim() : "";
|
|
8809
|
-
const to = typeof r.to === "string" ? r.to.trim() : "";
|
|
8810
|
-
const type = typeof r.type === "string" ? r.type.trim() : "";
|
|
8811
|
-
if (!from || !to || !type) {
|
|
8812
|
-
findings.push({ level: "warning", message: "A topology relationship is missing from/to/type and was skipped." });
|
|
8813
|
-
continue;
|
|
8814
|
-
}
|
|
8815
|
-
if (!TECHNICAL_RELATIONSHIP_TYPES.has(type)) {
|
|
8816
|
-
findings.push({ level: "warning", message: `Relationship type "${type}" is not recognized and was skipped.` });
|
|
8817
|
-
continue;
|
|
8818
|
-
}
|
|
8819
|
-
if (!entityIds.has(from) || !entityIds.has(to)) {
|
|
8820
|
-
findings.push({ level: "blocking", message: `Relationship ${from} \u2192 ${to} references an unknown entity endpoint.` });
|
|
8821
|
-
continue;
|
|
8822
|
-
}
|
|
8823
|
-
relationships.push({ from, to, type, evidence: strList(r.evidence).filter(isRelative) });
|
|
8824
|
-
}
|
|
8825
|
-
return { entities, relationships, findings, declared: true };
|
|
8826
|
-
}
|
|
8827
|
-
function validateSystemTopology(dir) {
|
|
8828
|
-
return loadSystemTopology(dir).findings;
|
|
8829
|
-
}
|
|
8830
|
-
function buildTopologyEnrichmentHandoff(dir, projectName) {
|
|
8831
|
-
const mappedModules = loadMappedModules(dir).map((m) => m.id);
|
|
8832
|
-
const lines = [
|
|
8833
|
-
`Enrich the semantic system topology for the Kaddo project "${projectName}".`,
|
|
8834
|
-
"",
|
|
8835
|
-
"Use the canonical architecture-agent and graph-metadata-review skill (via Kaddo MCP or skills).",
|
|
8836
|
-
"Inspect the actual repository and relevant mapped modules \u2014 do not infer architecture from",
|
|
8837
|
-
"filenames or directory names."
|
|
8838
|
-
];
|
|
8839
|
-
if (mappedModules.length > 0) {
|
|
8840
|
-
lines.push("", `This is a multirepo project. Inspect the relevant mapped modules (${mappedModules.join(", ")}) before finalizing the topology.`);
|
|
8841
|
-
}
|
|
8842
|
-
lines.push(
|
|
8843
|
-
"",
|
|
8844
|
-
`Write the result to ${TOPOLOGY_FILE} as declared entities and relationships. For each entity`,
|
|
8845
|
-
"capture, only when supported by evidence:",
|
|
8846
|
-
"- a stable id and semantic kind (application/service/component/api/interface/datastore/queue/job/external-system);",
|
|
8847
|
-
"- a responsibility/purpose (what it does, not how);",
|
|
8848
|
-
"- the owning module/repository;",
|
|
8849
|
-
"- implementation references (relative paths);",
|
|
8850
|
-
"- relevant Knowledge references (capability/ADR ids), not Kaddo operational assets.",
|
|
8851
|
-
"",
|
|
8852
|
-
"Capture evidence-backed technical relationships: contains, calls, depends-on, reads-from,",
|
|
8853
|
-
"writes-to, integrates-with, runs-on. Preserve unknowns when evidence is insufficient.",
|
|
8854
|
-
"",
|
|
8855
|
-
"Review the proposed graph metadata (duplicate ids, dangling relationships, unknown modules,",
|
|
8856
|
-
"invalid references) before writing.",
|
|
8857
|
-
"Do not implement application changes. Do not run mutating Git operations."
|
|
8858
|
-
);
|
|
8859
|
-
return {
|
|
8860
|
-
projectName,
|
|
8861
|
-
recommendedAgent: "architecture-agent",
|
|
8862
|
-
recommendedSkill: "graph-metadata-review",
|
|
8863
|
-
targetFile: TOPOLOGY_FILE,
|
|
8864
|
-
text: lines.join("\n")
|
|
8865
|
-
};
|
|
8866
|
-
}
|
|
8867
|
-
|
|
8868
8944
|
// src/core/system-map.ts
|
|
8869
8945
|
function dimensionOf(type) {
|
|
8870
8946
|
switch (type) {
|
|
@@ -9046,6 +9122,118 @@ function getSystemNodeContext(dir, nodeId) {
|
|
|
9046
9122
|
}
|
|
9047
9123
|
return { node, incoming, outgoing };
|
|
9048
9124
|
}
|
|
9125
|
+
var DEFAULT_MAX_DEPTH = 2;
|
|
9126
|
+
var DEFAULT_MAX_NODES = 50;
|
|
9127
|
+
function searchSystemNodes(dir, query) {
|
|
9128
|
+
const q = query.trim().toLowerCase();
|
|
9129
|
+
if (!q) return [];
|
|
9130
|
+
const rank = (n) => n.dimension === "system" ? 0 : n.dimension === "knowledge" ? 1 : n.dimension === "delivery" ? 2 : n.dimension === "unknown" ? 3 : 4;
|
|
9131
|
+
return getSystemMapProjection(dir).nodes.filter(
|
|
9132
|
+
(n) => n.label.toLowerCase().includes(q) || n.type.toLowerCase().includes(q) || (n.moduleId ?? "").toLowerCase().includes(q) || (n.purpose ?? "").toLowerCase().includes(q)
|
|
9133
|
+
).sort((a, b) => rank(a) - rank(b));
|
|
9134
|
+
}
|
|
9135
|
+
function getSystemNeighbors(dir, nodeId, opts = {}) {
|
|
9136
|
+
const projection = getSystemMapProjection(dir);
|
|
9137
|
+
const byId = new Map(projection.nodes.map((n) => [n.id, n]));
|
|
9138
|
+
if (!byId.has(nodeId)) return null;
|
|
9139
|
+
const maxDepth = Math.max(1, opts.maxDepth ?? DEFAULT_MAX_DEPTH);
|
|
9140
|
+
const maxNodes = Math.max(1, opts.maxNodes ?? DEFAULT_MAX_NODES);
|
|
9141
|
+
const relTypes = opts.relationshipTypes && opts.relationshipTypes.length ? new Set(opts.relationshipTypes) : null;
|
|
9142
|
+
const modules = opts.moduleFilter && opts.moduleFilter.length ? new Set(opts.moduleFilter) : null;
|
|
9143
|
+
const includeNode = (n) => !modules || n.moduleId != null && modules.has(n.moduleId) || n.id === nodeId;
|
|
9144
|
+
const visited = /* @__PURE__ */ new Set([nodeId]);
|
|
9145
|
+
const nodes = [];
|
|
9146
|
+
const rels = [];
|
|
9147
|
+
let frontier = [nodeId];
|
|
9148
|
+
let truncated = false;
|
|
9149
|
+
for (let depth = 0; depth < maxDepth && frontier.length > 0 && !truncated; depth++) {
|
|
9150
|
+
const next = [];
|
|
9151
|
+
for (const current of frontier) {
|
|
9152
|
+
for (const r of projection.relationships) {
|
|
9153
|
+
if (relTypes && !relTypes.has(r.type)) continue;
|
|
9154
|
+
const other = r.source === current ? r.target : r.target === current ? r.source : null;
|
|
9155
|
+
if (!other) continue;
|
|
9156
|
+
const otherNode = byId.get(other);
|
|
9157
|
+
if (!otherNode || !includeNode(otherNode)) continue;
|
|
9158
|
+
if (!rels.some((x) => x.id === r.id)) rels.push(r);
|
|
9159
|
+
if (!visited.has(other)) {
|
|
9160
|
+
if (nodes.length >= maxNodes) {
|
|
9161
|
+
truncated = true;
|
|
9162
|
+
break;
|
|
9163
|
+
}
|
|
9164
|
+
visited.add(other);
|
|
9165
|
+
nodes.push(otherNode);
|
|
9166
|
+
next.push(other);
|
|
9167
|
+
}
|
|
9168
|
+
}
|
|
9169
|
+
if (truncated) break;
|
|
9170
|
+
}
|
|
9171
|
+
frontier = next;
|
|
9172
|
+
}
|
|
9173
|
+
return { seed: nodeId, nodes, relationships: rels, truncated };
|
|
9174
|
+
}
|
|
9175
|
+
function findSystemPaths(dir, source, target, opts = {}) {
|
|
9176
|
+
const projection = getSystemMapProjection(dir);
|
|
9177
|
+
const byId = new Map(projection.nodes.map((n) => [n.id, n]));
|
|
9178
|
+
if (!byId.has(source) || !byId.has(target)) return [];
|
|
9179
|
+
const maxDepth = Math.max(1, opts.maxDepth ?? 4);
|
|
9180
|
+
const out = /* @__PURE__ */ new Map();
|
|
9181
|
+
for (const r of projection.relationships) {
|
|
9182
|
+
if (!out.has(r.source)) out.set(r.source, []);
|
|
9183
|
+
out.get(r.source).push(r.target);
|
|
9184
|
+
}
|
|
9185
|
+
const paths = [];
|
|
9186
|
+
const walk = (node, path4) => {
|
|
9187
|
+
if (paths.length >= 20) return;
|
|
9188
|
+
if (node === target && path4.length > 1) {
|
|
9189
|
+
paths.push([...path4]);
|
|
9190
|
+
return;
|
|
9191
|
+
}
|
|
9192
|
+
if (path4.length > maxDepth) return;
|
|
9193
|
+
for (const nxt of out.get(node) ?? []) {
|
|
9194
|
+
if (path4.includes(nxt)) continue;
|
|
9195
|
+
walk(nxt, [...path4, nxt]);
|
|
9196
|
+
}
|
|
9197
|
+
};
|
|
9198
|
+
walk(source, [source]);
|
|
9199
|
+
return paths;
|
|
9200
|
+
}
|
|
9201
|
+
function getImpactCandidates(dir, seedIds, opts = {}) {
|
|
9202
|
+
const projection = getSystemMapProjection(dir);
|
|
9203
|
+
const byId = new Map(projection.nodes.map((n) => [n.id, n]));
|
|
9204
|
+
const seeds = seedIds.filter((s) => byId.has(s));
|
|
9205
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
9206
|
+
let truncated = false;
|
|
9207
|
+
for (const seed of seeds) {
|
|
9208
|
+
const neighborhood = getSystemNeighbors(dir, seed, opts);
|
|
9209
|
+
if (!neighborhood) continue;
|
|
9210
|
+
if (neighborhood.truncated) truncated = true;
|
|
9211
|
+
for (const n of neighborhood.nodes) {
|
|
9212
|
+
if (seeds.includes(n.id)) continue;
|
|
9213
|
+
const rel = neighborhood.relationships.find((r) => r.source === seed && r.target === n.id || r.target === seed && r.source === n.id);
|
|
9214
|
+
const reason = rel ? { relationship: rel.label, from: byId.get(rel.source)?.label ?? rel.source, to: byId.get(rel.target)?.label ?? rel.target } : { relationship: "related to", from: byId.get(seed)?.label ?? seed, to: n.label };
|
|
9215
|
+
const existing = candidates.get(n.id);
|
|
9216
|
+
if (existing) {
|
|
9217
|
+
existing.reason.push(reason);
|
|
9218
|
+
continue;
|
|
9219
|
+
}
|
|
9220
|
+
const path4 = findSystemPaths(dir, seed, n.id, { maxDepth: opts.maxDepth ?? DEFAULT_MAX_DEPTH })[0];
|
|
9221
|
+
candidates.set(n.id, {
|
|
9222
|
+
nodeId: n.id,
|
|
9223
|
+
label: n.label,
|
|
9224
|
+
kind: n.type,
|
|
9225
|
+
...n.moduleId ? { moduleId: n.moduleId } : {},
|
|
9226
|
+
reason: [reason],
|
|
9227
|
+
...path4 ? { graphPath: path4.map((id) => byId.get(id)?.label ?? id) } : {},
|
|
9228
|
+
...n.implementationRefs?.length ? { implementationRefs: n.implementationRefs } : {},
|
|
9229
|
+
...n.knowledgeRefs?.length ? { knowledgeRefs: n.knowledgeRefs } : {},
|
|
9230
|
+
status: "candidate",
|
|
9231
|
+
provenance: { source: "graph-assisted" }
|
|
9232
|
+
});
|
|
9233
|
+
}
|
|
9234
|
+
}
|
|
9235
|
+
return { seeds, candidates: [...candidates.values()], truncated, topologyStatus: projection.metadata.topologyStatus };
|
|
9236
|
+
}
|
|
9049
9237
|
function toNode(n, knowledgeByPath, wiModules, groupIds) {
|
|
9050
9238
|
const node = { id: n.id, type: n.type, label: n.label, dimension: dimensionOf(n.type) };
|
|
9051
9239
|
if (n.status) node.status = n.status;
|
|
@@ -9062,12 +9250,92 @@ function toNode(n, knowledgeByPath, wiModules, groupIds) {
|
|
|
9062
9250
|
}
|
|
9063
9251
|
return node;
|
|
9064
9252
|
}
|
|
9253
|
+
|
|
9254
|
+
// src/core/work-item-refinement.ts
|
|
9255
|
+
function toCapture(q) {
|
|
9256
|
+
return { id: q.id, prompt: q.prompt, placeholder: q.placeholder, field: q.frontMatterField, required: q.required };
|
|
9257
|
+
}
|
|
9258
|
+
function getWorkItemCaptureDefinition() {
|
|
9259
|
+
const questions = {};
|
|
9260
|
+
for (const type of WORK_ITEM_TYPES2) {
|
|
9261
|
+
questions[type] = getLevel(getLevelForType(type)).questions.map(toCapture);
|
|
9262
|
+
}
|
|
9263
|
+
return {
|
|
9264
|
+
types: WORK_ITEM_TYPES2.map((t) => ({ value: t, label: t.charAt(0).toUpperCase() + t.slice(1) })),
|
|
9265
|
+
questions
|
|
9266
|
+
};
|
|
9267
|
+
}
|
|
9268
|
+
var RECOMMENDED_AGENT = "work-item-agent";
|
|
9269
|
+
var RECOMMENDED_SKILL = "work-item-refinement";
|
|
9270
|
+
function buildRefinementHandoff(dir, workItemId) {
|
|
9271
|
+
const wi = getWorkItem(dir, workItemId);
|
|
9272
|
+
const config = loadConfig(dir);
|
|
9273
|
+
const projectName = config?.project.name ?? "this project";
|
|
9274
|
+
const mappedModules = loadMappedModules(dir).map((m) => m.id);
|
|
9275
|
+
const multirepo = mappedModules.length > 0;
|
|
9276
|
+
const lines = [
|
|
9277
|
+
`Refine Work Item ${wi.id} \u2014 "${wi.title}" \u2014 in project "${projectName}" using Kaddo.`,
|
|
9278
|
+
"",
|
|
9279
|
+
"Use a Kaddo-enabled agent with access to this repository. Drive the refinement with the",
|
|
9280
|
+
`canonical ${RECOMMENDED_AGENT} and the ${RECOMMENDED_SKILL} skill (via Kaddo MCP or skills).`,
|
|
9281
|
+
"",
|
|
9282
|
+
"Inspect the actual implementation before defining scope \u2014 do not guess affected modules from",
|
|
9283
|
+
"the Work Item title. Read the current behavior in the code first, then classify."
|
|
9284
|
+
];
|
|
9285
|
+
if (multirepo) {
|
|
9286
|
+
lines.push(
|
|
9287
|
+
"",
|
|
9288
|
+
`This is a multirepo project. Evaluate the scope across all relevant mapped modules (${mappedModules.join(", ")})`,
|
|
9289
|
+
"before finalizing affected_modules and module_coverage."
|
|
9290
|
+
);
|
|
9291
|
+
}
|
|
9292
|
+
const topology = getSystemMapProjection(dir).metadata.topologyStatus;
|
|
9293
|
+
if (topology !== "unavailable") {
|
|
9294
|
+
lines.push(
|
|
9295
|
+
"",
|
|
9296
|
+
`The semantic system Graph is ${topology}. Identify the relevant system entry points, then use`,
|
|
9297
|
+
"the Kaddo Graph (search / neighbors / paths) to find connected components, dependencies, APIs,",
|
|
9298
|
+
"datastores and external systems. Treat Graph-derived entities as IMPACT CANDIDATES, not",
|
|
9299
|
+
"confirmed scope: inspect each candidate in the repository and classify it as affected,",
|
|
9300
|
+
"reviewed-not-affected or unknown, preserving the reason/evidence. A missing Graph edge does not",
|
|
9301
|
+
"mean no impact \u2014 especially when coverage is partial."
|
|
9302
|
+
);
|
|
9303
|
+
} else {
|
|
9304
|
+
lines.push("", "The semantic system Graph is not available yet; refine using the repository and Knowledge.");
|
|
9305
|
+
}
|
|
9306
|
+
lines.push(
|
|
9307
|
+
"",
|
|
9308
|
+
`Update the canonical Work Item ${wi.id} with:`,
|
|
9309
|
+
"- current and target behavior;",
|
|
9310
|
+
"- the end-to-end flow (journey);",
|
|
9311
|
+
"- affected modules;",
|
|
9312
|
+
"- affected system entities (affected_system_entities) and reviewed_system_entities;",
|
|
9313
|
+
"- module coverage;",
|
|
9314
|
+
"- impact analysis across the relevant surfaces;",
|
|
9315
|
+
"- scope confidence and open unknowns;",
|
|
9316
|
+
"- acceptance criteria;",
|
|
9317
|
+
"- relevant Knowledge / ADR relationships.",
|
|
9318
|
+
"",
|
|
9319
|
+
"Do not implement the Work Item. Do not run mutating Git operations."
|
|
9320
|
+
);
|
|
9321
|
+
return {
|
|
9322
|
+
workItemId: wi.id,
|
|
9323
|
+
title: wi.title,
|
|
9324
|
+
projectName,
|
|
9325
|
+
refinement: wi.refinement,
|
|
9326
|
+
recommendedAgent: RECOMMENDED_AGENT,
|
|
9327
|
+
recommendedSkill: RECOMMENDED_SKILL,
|
|
9328
|
+
text: lines.join("\n")
|
|
9329
|
+
};
|
|
9330
|
+
}
|
|
9065
9331
|
export {
|
|
9066
9332
|
TOPOLOGY_FILE,
|
|
9333
|
+
TopologyWriteError,
|
|
9067
9334
|
WorkItemNotFoundError,
|
|
9068
9335
|
WorkItemWriteError,
|
|
9069
9336
|
analyzeCrossRepoEvidence,
|
|
9070
9337
|
analyzeScopeCoverage,
|
|
9338
|
+
applyTopologyProposal,
|
|
9071
9339
|
buildProjectExplanation,
|
|
9072
9340
|
buildProjectRoute,
|
|
9073
9341
|
buildReadinessReport,
|
|
@@ -9079,7 +9347,10 @@ export {
|
|
|
9079
9347
|
discoverKnowledge,
|
|
9080
9348
|
discoverWorkItems,
|
|
9081
9349
|
exists,
|
|
9350
|
+
findSystemPaths,
|
|
9351
|
+
getImpactCandidates,
|
|
9082
9352
|
getSystemMapProjection,
|
|
9353
|
+
getSystemNeighbors,
|
|
9083
9354
|
getSystemNodeContext,
|
|
9084
9355
|
getWorkItem,
|
|
9085
9356
|
getWorkItemCaptureDefinition,
|
|
@@ -9096,8 +9367,11 @@ export {
|
|
|
9096
9367
|
loadMappedModules,
|
|
9097
9368
|
loadSystemTopology,
|
|
9098
9369
|
readFile,
|
|
9370
|
+
searchSystemNodes,
|
|
9371
|
+
topologyRevision,
|
|
9099
9372
|
transitionWorkItem,
|
|
9100
9373
|
updateWorkItem,
|
|
9101
9374
|
validateSystemTopology,
|
|
9375
|
+
validateTopologyProposal,
|
|
9102
9376
|
validateWorkItem
|
|
9103
9377
|
};
|