@kaddo/cli 3.79.0 → 3.81.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/admin-dist/assets/index-DNkI-1xG.css +2 -0
- package/dist/admin-dist/assets/{index-sZtkPyjd.js → index-xgxqXGxn.js} +3 -3
- package/dist/admin-dist/index.html +2 -2
- package/dist/admin-server/index.js +22 -0
- package/dist/core.js +573 -369
- package/package.json +1 -1
- package/dist/admin-dist/assets/index-BPQdYfAp.css +0 -2
package/dist/core.js
CHANGED
|
@@ -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,51 @@ 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 parseGraphReason(v) {
|
|
8126
|
+
if (!v || typeof v !== "object") return null;
|
|
8127
|
+
const o = v;
|
|
8128
|
+
const relationship = typeof o.relationship === "string" && o.relationship.trim() ? o.relationship.trim() : null;
|
|
8129
|
+
const path4 = Array.isArray(o.path) ? o.path.map(String).filter(Boolean) : [];
|
|
8130
|
+
return relationship || path4.length ? { relationship, path: path4 } : null;
|
|
8131
|
+
}
|
|
8132
|
+
function parseExplain(o) {
|
|
8133
|
+
const refs = Array.isArray(o.evidence) ? o.evidence : Array.isArray(o.evidence_refs) ? o.evidence_refs : [];
|
|
8134
|
+
return {
|
|
8135
|
+
reason: typeof o.reason === "string" && o.reason.trim() ? o.reason.trim() : null,
|
|
8136
|
+
graphReason: parseGraphReason(o.graph_reason),
|
|
8137
|
+
evidenceRefs: refs.map(String).filter(Boolean),
|
|
8138
|
+
evidenceSummary: typeof o.evidence_summary === "string" && o.evidence_summary.trim() ? o.evidence_summary.trim() : null
|
|
8139
|
+
};
|
|
8140
|
+
}
|
|
8141
|
+
function emptyExplain() {
|
|
8142
|
+
return { reason: null, graphReason: null, evidenceRefs: [], evidenceSummary: null };
|
|
8143
|
+
}
|
|
8144
|
+
function parseSystemImpact(dir, fm) {
|
|
8145
|
+
const topology = loadSystemTopology(dir);
|
|
8146
|
+
const byId = new Map(topology.entities.map((e) => [e.id, e]));
|
|
8147
|
+
const resolve = (id) => {
|
|
8148
|
+
const e = byId.get(id);
|
|
8149
|
+
return { id, nodeId: `sys:${id}`, label: e?.label ?? id, kind: e?.kind ?? "unknown", moduleId: e?.moduleId ?? null };
|
|
8150
|
+
};
|
|
8151
|
+
const affected = Array.isArray(fm.affected_system_entities) ? fm.affected_system_entities.map((raw) => {
|
|
8152
|
+
if (typeof raw === "string") return raw ? { ...resolve(raw), ...emptyExplain() } : null;
|
|
8153
|
+
if (raw && typeof raw === "object") {
|
|
8154
|
+
const o = raw;
|
|
8155
|
+
const id = String(o.id ?? "");
|
|
8156
|
+
return id ? { ...resolve(id), ...parseExplain(o) } : null;
|
|
8157
|
+
}
|
|
8158
|
+
return null;
|
|
8159
|
+
}).filter((e) => e != null) : [];
|
|
8160
|
+
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 ?? "")), ...parseExplain(r), status: String(r.status ?? "unknown") })).filter((r) => r.id) : [];
|
|
8161
|
+
const graphRevision = typeof fm.graph_revision === "string" && fm.graph_revision.trim() ? fm.graph_revision.trim() : null;
|
|
8162
|
+
const graphCoverage = topology.entities.length === 0 ? "unavailable" : topology.relationships.length > 0 ? "available" : "partial";
|
|
8163
|
+
return { affectedSystemEntities: affected, reviewedSystemEntities: reviewed, graphRevision, graphCoverage };
|
|
8164
|
+
}
|
|
7865
8165
|
function readBody(filePath) {
|
|
7866
8166
|
try {
|
|
7867
8167
|
const raw = readFile(filePath);
|
|
@@ -8029,9 +8329,9 @@ function parseRelatedKnowledge(fm, knowledgeById) {
|
|
|
8029
8329
|
}
|
|
8030
8330
|
|
|
8031
8331
|
// src/core/work-item-write.ts
|
|
8032
|
-
import
|
|
8033
|
-
import
|
|
8034
|
-
import
|
|
8332
|
+
import fs3 from "fs";
|
|
8333
|
+
import path3 from "path";
|
|
8334
|
+
import crypto2 from "crypto";
|
|
8035
8335
|
import matter7 from "gray-matter";
|
|
8036
8336
|
|
|
8037
8337
|
// src/core/knowledge-levels.ts
|
|
@@ -8234,7 +8534,7 @@ var VALID_COVERAGE = /* @__PURE__ */ new Set(["affected", "reviewed-not-affected
|
|
|
8234
8534
|
var VALID_CONFIDENCE = /* @__PURE__ */ new Set(["high", "medium", "low"]);
|
|
8235
8535
|
var EDITABLE_STATES = ["draft"];
|
|
8236
8536
|
function revisionOf(raw) {
|
|
8237
|
-
return
|
|
8537
|
+
return crypto2.createHash("sha256").update(raw, "utf-8").digest("hex");
|
|
8238
8538
|
}
|
|
8239
8539
|
function slugify2(s) {
|
|
8240
8540
|
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
|
|
@@ -8244,7 +8544,7 @@ function nextWorkItemId(dir) {
|
|
|
8244
8544
|
let max = 0;
|
|
8245
8545
|
const walk = (d) => {
|
|
8246
8546
|
if (!exists(d)) return;
|
|
8247
|
-
for (const entry of
|
|
8547
|
+
for (const entry of fs3.readdirSync(d)) {
|
|
8248
8548
|
const full = join(d, entry);
|
|
8249
8549
|
if (isFile(full)) {
|
|
8250
8550
|
const m = entry.match(/WI-(\d+)/);
|
|
@@ -8257,15 +8557,15 @@ function nextWorkItemId(dir) {
|
|
|
8257
8557
|
walk(wiDir);
|
|
8258
8558
|
return `WI-${String(max + 1).padStart(3, "0")}`;
|
|
8259
8559
|
}
|
|
8260
|
-
function
|
|
8261
|
-
|
|
8560
|
+
function atomicWrite2(filePath, content) {
|
|
8561
|
+
fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
|
|
8262
8562
|
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
8263
|
-
|
|
8563
|
+
fs3.writeFileSync(tmp, content, "utf-8");
|
|
8264
8564
|
try {
|
|
8265
|
-
|
|
8565
|
+
fs3.renameSync(tmp, filePath);
|
|
8266
8566
|
} catch (err) {
|
|
8267
8567
|
try {
|
|
8268
|
-
|
|
8568
|
+
fs3.rmSync(tmp, { force: true });
|
|
8269
8569
|
} catch {
|
|
8270
8570
|
}
|
|
8271
8571
|
throw err;
|
|
@@ -8565,7 +8865,7 @@ function createWorkItem(dir, opts) {
|
|
|
8565
8865
|
const filePath = join(dir, relPath);
|
|
8566
8866
|
if (exists(filePath)) throw new WorkItemWriteError("INVALID_INPUT", `Work Item file already exists: ${relPath}`);
|
|
8567
8867
|
const raw = serialize(data, body);
|
|
8568
|
-
|
|
8868
|
+
atomicWrite2(filePath, raw);
|
|
8569
8869
|
return { id, path: relPath, revision: revisionOf(raw) };
|
|
8570
8870
|
}
|
|
8571
8871
|
function validateInput(input) {
|
|
@@ -8587,7 +8887,7 @@ function updateWorkItem(dir, id, input, expectedRevision) {
|
|
|
8587
8887
|
const nextData = applyFrontmatter(data, input);
|
|
8588
8888
|
const nextBody = mergeBody(content, input);
|
|
8589
8889
|
const nextRaw = serialize(nextData, nextBody);
|
|
8590
|
-
|
|
8890
|
+
atomicWrite2(filePath, nextRaw);
|
|
8591
8891
|
return { revision: revisionOf(nextRaw), path: relPath };
|
|
8592
8892
|
}
|
|
8593
8893
|
function validateWorkItem(dir, id) {
|
|
@@ -8599,374 +8899,75 @@ function validateWorkItem(dir, id) {
|
|
|
8599
8899
|
const modules = validModuleIds(dir);
|
|
8600
8900
|
for (const c of input.moduleCoverage) {
|
|
8601
8901
|
if (c.status === "affected" && !input.affectedModules.includes(c.id)) {
|
|
8602
|
-
findings.push({ level: "blocking", message: `${c.id} is marked affected in module coverage but is missing from affected_modules.` });
|
|
8603
|
-
}
|
|
8604
|
-
}
|
|
8605
|
-
for (const m of input.affectedModules) {
|
|
8606
|
-
if (!modules.has(m)) findings.push({ level: "blocking", message: `Module "${m}" is not registered in this project.` });
|
|
8607
|
-
}
|
|
8608
|
-
if (!input.targetBehavior?.trim()) findings.push({ level: "warning", message: "Target behavior is not defined." });
|
|
8609
|
-
if (input.acceptanceCriteria.length === 0) findings.push({ level: "warning", message: "No acceptance criteria have been defined." });
|
|
8610
|
-
if (input.scopeConfidence?.level === "low") findings.push({ level: "warning", message: "Scope confidence is Low." });
|
|
8611
|
-
if (!input.scopeConfidence) findings.push({ level: "warning", message: "Scope confidence has not been assessed." });
|
|
8612
|
-
for (const c of input.moduleCoverage) {
|
|
8613
|
-
if (c.status === "reviewed-not-affected") findings.push({ level: "fyi", message: `${c.id} was reviewed and is not affected.` });
|
|
8614
|
-
}
|
|
8615
|
-
for (const s of input.impactAnalysis) {
|
|
8616
|
-
if (s.status === "unknown") findings.push({ level: "fyi", message: `Impact on ${s.surface} is unknown.` });
|
|
8617
|
-
}
|
|
8618
|
-
const canMarkReady = !findings.some((f) => f.level === "blocking");
|
|
8619
|
-
return { findings, canMarkReady };
|
|
8620
|
-
}
|
|
8621
|
-
var ALLOWED = { draft: ["ready"], ready: ["draft"] };
|
|
8622
|
-
function transitionWorkItem(dir, id, to, expectedRevision) {
|
|
8623
|
-
const { filePath } = findArtifact(dir, id);
|
|
8624
|
-
const raw = readFile(filePath);
|
|
8625
|
-
if (revisionOf(raw) !== expectedRevision) {
|
|
8626
|
-
throw new WorkItemWriteError("WORK_ITEM_CONFLICT", "This Work Item changed outside Kaddo Admin. Reload the latest version before continuing.");
|
|
8627
|
-
}
|
|
8628
|
-
const { data, content } = matter7(raw);
|
|
8629
|
-
const from = lifecycleStateOf({ status: String(data.status ?? ""), filePath });
|
|
8630
|
-
if (!(ALLOWED[from] ?? []).includes(to)) {
|
|
8631
|
-
throw new WorkItemWriteError("INVALID_TRANSITION", `Cannot transition a ${from} Work Item to ${to}.`);
|
|
8632
|
-
}
|
|
8633
|
-
if (to === "ready") {
|
|
8634
|
-
const { canMarkReady } = validateWorkItem(dir, id);
|
|
8635
|
-
if (!canMarkReady) throw new WorkItemWriteError("INVALID_TRANSITION", "This Work Item has blocking issues and cannot be marked Ready.");
|
|
8636
|
-
}
|
|
8637
|
-
const nextData = { ...data, status: to };
|
|
8638
|
-
const nextRaw = serialize(nextData, content);
|
|
8639
|
-
const filename = path2.basename(filePath);
|
|
8640
|
-
const targetRel = `${WORK_ITEMS_DIR}/${to}/${filename}`;
|
|
8641
|
-
const targetPath = join(dir, targetRel);
|
|
8642
|
-
atomicWrite(targetPath, nextRaw);
|
|
8643
|
-
if (path2.resolve(targetPath) !== path2.resolve(filePath)) {
|
|
8644
|
-
try {
|
|
8645
|
-
fs2.rmSync(filePath, { force: true });
|
|
8646
|
-
} catch {
|
|
8647
|
-
}
|
|
8648
|
-
}
|
|
8649
|
-
return { revision: revisionOf(nextRaw), status: to, path: targetRel };
|
|
8650
|
-
}
|
|
8651
|
-
|
|
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, 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;
|
|
8902
|
+
findings.push({ level: "blocking", message: `${c.id} is marked affected in module coverage but is missing from affected_modules.` });
|
|
8831
8903
|
}
|
|
8832
|
-
|
|
8833
|
-
|
|
8904
|
+
}
|
|
8905
|
+
for (const m of input.affectedModules) {
|
|
8906
|
+
if (!modules.has(m)) findings.push({ level: "blocking", message: `Module "${m}" is not registered in this project.` });
|
|
8907
|
+
}
|
|
8908
|
+
if (!input.targetBehavior?.trim()) findings.push({ level: "warning", message: "Target behavior is not defined." });
|
|
8909
|
+
if (input.acceptanceCriteria.length === 0) findings.push({ level: "warning", message: "No acceptance criteria have been defined." });
|
|
8910
|
+
if (input.scopeConfidence?.level === "low") findings.push({ level: "warning", message: "Scope confidence is Low." });
|
|
8911
|
+
if (!input.scopeConfidence) findings.push({ level: "warning", message: "Scope confidence has not been assessed." });
|
|
8912
|
+
for (const c of input.moduleCoverage) {
|
|
8913
|
+
if (c.status === "reviewed-not-affected") findings.push({ level: "fyi", message: `${c.id} was reviewed and is not affected.` });
|
|
8914
|
+
}
|
|
8915
|
+
for (const s of input.impactAnalysis) {
|
|
8916
|
+
if (s.status === "unknown") findings.push({ level: "fyi", message: `Impact on ${s.surface} is unknown.` });
|
|
8917
|
+
}
|
|
8918
|
+
const topology = loadSystemTopology(dir);
|
|
8919
|
+
const entityById = new Map(topology.entities.map((e) => [e.id, e]));
|
|
8920
|
+
const fm = data;
|
|
8921
|
+
const idOf = (raw2) => typeof raw2 === "string" ? raw2 : raw2 && typeof raw2 === "object" ? String(raw2.id ?? "") : "";
|
|
8922
|
+
const affectedEntities = Array.isArray(fm.affected_system_entities) ? fm.affected_system_entities.map(idOf).filter(Boolean) : [];
|
|
8923
|
+
for (const eid of affectedEntities) {
|
|
8924
|
+
const e = entityById.get(eid);
|
|
8925
|
+
if (!e) {
|
|
8926
|
+
findings.push({ level: "blocking", message: `Affected system entity "${eid}" does not exist in the semantic topology.` });
|
|
8834
8927
|
continue;
|
|
8835
8928
|
}
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
findings.push({ level: "warning", message: `Duplicate relationship ${from} ${type} ${to} was skipped.` });
|
|
8839
|
-
continue;
|
|
8929
|
+
if (e.moduleId && !input.affectedModules.includes(e.moduleId)) {
|
|
8930
|
+
findings.push({ level: "warning", message: `System entity "${e.label}" belongs to module "${e.moduleId}", which is not in affected_modules.` });
|
|
8840
8931
|
}
|
|
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
8932
|
}
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
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 {
|
|
8933
|
+
if (Array.isArray(fm.reviewed_system_entities)) {
|
|
8934
|
+
for (const r of fm.reviewed_system_entities) {
|
|
8935
|
+
const rid = r && typeof r === "object" ? String(r.id ?? "") : "";
|
|
8936
|
+
if (rid && !entityById.has(rid)) findings.push({ level: "warning", message: `Reviewed system entity "${rid}" does not exist in the semantic topology.` });
|
|
8896
8937
|
}
|
|
8897
|
-
throw err;
|
|
8898
8938
|
}
|
|
8939
|
+
const canMarkReady = !findings.some((f) => f.level === "blocking");
|
|
8940
|
+
return { findings, canMarkReady };
|
|
8899
8941
|
}
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
const
|
|
8904
|
-
if (
|
|
8905
|
-
throw new
|
|
8942
|
+
var ALLOWED = { draft: ["ready"], ready: ["draft"] };
|
|
8943
|
+
function transitionWorkItem(dir, id, to, expectedRevision) {
|
|
8944
|
+
const { filePath } = findArtifact(dir, id);
|
|
8945
|
+
const raw = readFile(filePath);
|
|
8946
|
+
if (revisionOf(raw) !== expectedRevision) {
|
|
8947
|
+
throw new WorkItemWriteError("WORK_ITEM_CONFLICT", "This Work Item changed outside Kaddo Admin. Reload the latest version before continuing.");
|
|
8906
8948
|
}
|
|
8907
|
-
const
|
|
8908
|
-
const
|
|
8909
|
-
|
|
8910
|
-
|
|
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);
|
|
8949
|
+
const { data, content } = matter7(raw);
|
|
8950
|
+
const from = lifecycleStateOf({ status: String(data.status ?? ""), filePath });
|
|
8951
|
+
if (!(ALLOWED[from] ?? []).includes(to)) {
|
|
8952
|
+
throw new WorkItemWriteError("INVALID_TRANSITION", `Cannot transition a ${from} Work Item to ${to}.`);
|
|
8916
8953
|
}
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
let relationshipsNew = 0;
|
|
8921
|
-
for (const r of proposal.relationships) {
|
|
8922
|
-
if (!relMap.has(relKey(r))) relationshipsNew++;
|
|
8923
|
-
relMap.set(relKey(r), r);
|
|
8954
|
+
if (to === "ready") {
|
|
8955
|
+
const { canMarkReady } = validateWorkItem(dir, id);
|
|
8956
|
+
if (!canMarkReady) throw new WorkItemWriteError("INVALID_TRANSITION", "This Work Item has blocking issues and cannot be marked Ready.");
|
|
8924
8957
|
}
|
|
8925
|
-
const
|
|
8926
|
-
const
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
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.`);
|
|
8958
|
+
const nextData = { ...data, status: to };
|
|
8959
|
+
const nextRaw = serialize(nextData, content);
|
|
8960
|
+
const filename = path3.basename(filePath);
|
|
8961
|
+
const targetRel = `${WORK_ITEMS_DIR}/${to}/${filename}`;
|
|
8962
|
+
const targetPath = join(dir, targetRel);
|
|
8963
|
+
atomicWrite2(targetPath, nextRaw);
|
|
8964
|
+
if (path3.resolve(targetPath) !== path3.resolve(filePath)) {
|
|
8965
|
+
try {
|
|
8966
|
+
fs3.rmSync(filePath, { force: true });
|
|
8967
|
+
} catch {
|
|
8968
|
+
}
|
|
8942
8969
|
}
|
|
8943
|
-
|
|
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
|
+
return { revision: revisionOf(nextRaw), status: to, path: targetRel };
|
|
8970
8971
|
}
|
|
8971
8972
|
|
|
8972
8973
|
// src/core/system-map.ts
|
|
@@ -9150,6 +9151,118 @@ function getSystemNodeContext(dir, nodeId) {
|
|
|
9150
9151
|
}
|
|
9151
9152
|
return { node, incoming, outgoing };
|
|
9152
9153
|
}
|
|
9154
|
+
var DEFAULT_MAX_DEPTH = 2;
|
|
9155
|
+
var DEFAULT_MAX_NODES = 50;
|
|
9156
|
+
function searchSystemNodes(dir, query) {
|
|
9157
|
+
const q = query.trim().toLowerCase();
|
|
9158
|
+
if (!q) return [];
|
|
9159
|
+
const rank = (n) => n.dimension === "system" ? 0 : n.dimension === "knowledge" ? 1 : n.dimension === "delivery" ? 2 : n.dimension === "unknown" ? 3 : 4;
|
|
9160
|
+
return getSystemMapProjection(dir).nodes.filter(
|
|
9161
|
+
(n) => n.label.toLowerCase().includes(q) || n.type.toLowerCase().includes(q) || (n.moduleId ?? "").toLowerCase().includes(q) || (n.purpose ?? "").toLowerCase().includes(q)
|
|
9162
|
+
).sort((a, b) => rank(a) - rank(b));
|
|
9163
|
+
}
|
|
9164
|
+
function getSystemNeighbors(dir, nodeId, opts = {}) {
|
|
9165
|
+
const projection = getSystemMapProjection(dir);
|
|
9166
|
+
const byId = new Map(projection.nodes.map((n) => [n.id, n]));
|
|
9167
|
+
if (!byId.has(nodeId)) return null;
|
|
9168
|
+
const maxDepth = Math.max(1, opts.maxDepth ?? DEFAULT_MAX_DEPTH);
|
|
9169
|
+
const maxNodes = Math.max(1, opts.maxNodes ?? DEFAULT_MAX_NODES);
|
|
9170
|
+
const relTypes = opts.relationshipTypes && opts.relationshipTypes.length ? new Set(opts.relationshipTypes) : null;
|
|
9171
|
+
const modules = opts.moduleFilter && opts.moduleFilter.length ? new Set(opts.moduleFilter) : null;
|
|
9172
|
+
const includeNode = (n) => !modules || n.moduleId != null && modules.has(n.moduleId) || n.id === nodeId;
|
|
9173
|
+
const visited = /* @__PURE__ */ new Set([nodeId]);
|
|
9174
|
+
const nodes = [];
|
|
9175
|
+
const rels = [];
|
|
9176
|
+
let frontier = [nodeId];
|
|
9177
|
+
let truncated = false;
|
|
9178
|
+
for (let depth = 0; depth < maxDepth && frontier.length > 0 && !truncated; depth++) {
|
|
9179
|
+
const next = [];
|
|
9180
|
+
for (const current of frontier) {
|
|
9181
|
+
for (const r of projection.relationships) {
|
|
9182
|
+
if (relTypes && !relTypes.has(r.type)) continue;
|
|
9183
|
+
const other = r.source === current ? r.target : r.target === current ? r.source : null;
|
|
9184
|
+
if (!other) continue;
|
|
9185
|
+
const otherNode = byId.get(other);
|
|
9186
|
+
if (!otherNode || !includeNode(otherNode)) continue;
|
|
9187
|
+
if (!rels.some((x) => x.id === r.id)) rels.push(r);
|
|
9188
|
+
if (!visited.has(other)) {
|
|
9189
|
+
if (nodes.length >= maxNodes) {
|
|
9190
|
+
truncated = true;
|
|
9191
|
+
break;
|
|
9192
|
+
}
|
|
9193
|
+
visited.add(other);
|
|
9194
|
+
nodes.push(otherNode);
|
|
9195
|
+
next.push(other);
|
|
9196
|
+
}
|
|
9197
|
+
}
|
|
9198
|
+
if (truncated) break;
|
|
9199
|
+
}
|
|
9200
|
+
frontier = next;
|
|
9201
|
+
}
|
|
9202
|
+
return { seed: nodeId, nodes, relationships: rels, truncated };
|
|
9203
|
+
}
|
|
9204
|
+
function findSystemPaths(dir, source, target, opts = {}) {
|
|
9205
|
+
const projection = getSystemMapProjection(dir);
|
|
9206
|
+
const byId = new Map(projection.nodes.map((n) => [n.id, n]));
|
|
9207
|
+
if (!byId.has(source) || !byId.has(target)) return [];
|
|
9208
|
+
const maxDepth = Math.max(1, opts.maxDepth ?? 4);
|
|
9209
|
+
const out = /* @__PURE__ */ new Map();
|
|
9210
|
+
for (const r of projection.relationships) {
|
|
9211
|
+
if (!out.has(r.source)) out.set(r.source, []);
|
|
9212
|
+
out.get(r.source).push(r.target);
|
|
9213
|
+
}
|
|
9214
|
+
const paths = [];
|
|
9215
|
+
const walk = (node, path4) => {
|
|
9216
|
+
if (paths.length >= 20) return;
|
|
9217
|
+
if (node === target && path4.length > 1) {
|
|
9218
|
+
paths.push([...path4]);
|
|
9219
|
+
return;
|
|
9220
|
+
}
|
|
9221
|
+
if (path4.length > maxDepth) return;
|
|
9222
|
+
for (const nxt of out.get(node) ?? []) {
|
|
9223
|
+
if (path4.includes(nxt)) continue;
|
|
9224
|
+
walk(nxt, [...path4, nxt]);
|
|
9225
|
+
}
|
|
9226
|
+
};
|
|
9227
|
+
walk(source, [source]);
|
|
9228
|
+
return paths;
|
|
9229
|
+
}
|
|
9230
|
+
function getImpactCandidates(dir, seedIds, opts = {}) {
|
|
9231
|
+
const projection = getSystemMapProjection(dir);
|
|
9232
|
+
const byId = new Map(projection.nodes.map((n) => [n.id, n]));
|
|
9233
|
+
const seeds = seedIds.filter((s) => byId.has(s));
|
|
9234
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
9235
|
+
let truncated = false;
|
|
9236
|
+
for (const seed of seeds) {
|
|
9237
|
+
const neighborhood = getSystemNeighbors(dir, seed, opts);
|
|
9238
|
+
if (!neighborhood) continue;
|
|
9239
|
+
if (neighborhood.truncated) truncated = true;
|
|
9240
|
+
for (const n of neighborhood.nodes) {
|
|
9241
|
+
if (seeds.includes(n.id)) continue;
|
|
9242
|
+
const rel = neighborhood.relationships.find((r) => r.source === seed && r.target === n.id || r.target === seed && r.source === n.id);
|
|
9243
|
+
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 };
|
|
9244
|
+
const existing = candidates.get(n.id);
|
|
9245
|
+
if (existing) {
|
|
9246
|
+
existing.reason.push(reason);
|
|
9247
|
+
continue;
|
|
9248
|
+
}
|
|
9249
|
+
const path4 = findSystemPaths(dir, seed, n.id, { maxDepth: opts.maxDepth ?? DEFAULT_MAX_DEPTH })[0];
|
|
9250
|
+
candidates.set(n.id, {
|
|
9251
|
+
nodeId: n.id,
|
|
9252
|
+
label: n.label,
|
|
9253
|
+
kind: n.type,
|
|
9254
|
+
...n.moduleId ? { moduleId: n.moduleId } : {},
|
|
9255
|
+
reason: [reason],
|
|
9256
|
+
...path4 ? { graphPath: path4.map((id) => byId.get(id)?.label ?? id) } : {},
|
|
9257
|
+
...n.implementationRefs?.length ? { implementationRefs: n.implementationRefs } : {},
|
|
9258
|
+
...n.knowledgeRefs?.length ? { knowledgeRefs: n.knowledgeRefs } : {},
|
|
9259
|
+
status: "candidate",
|
|
9260
|
+
provenance: { source: "graph-assisted" }
|
|
9261
|
+
});
|
|
9262
|
+
}
|
|
9263
|
+
}
|
|
9264
|
+
return { seeds, candidates: [...candidates.values()], truncated, topologyStatus: projection.metadata.topologyStatus };
|
|
9265
|
+
}
|
|
9153
9266
|
function toNode(n, knowledgeByPath, wiModules, groupIds) {
|
|
9154
9267
|
const node = { id: n.id, type: n.type, label: n.label, dimension: dimensionOf(n.type) };
|
|
9155
9268
|
if (n.status) node.status = n.status;
|
|
@@ -9166,6 +9279,93 @@ function toNode(n, knowledgeByPath, wiModules, groupIds) {
|
|
|
9166
9279
|
}
|
|
9167
9280
|
return node;
|
|
9168
9281
|
}
|
|
9282
|
+
|
|
9283
|
+
// src/core/work-item-refinement.ts
|
|
9284
|
+
function toCapture(q) {
|
|
9285
|
+
return { id: q.id, prompt: q.prompt, placeholder: q.placeholder, field: q.frontMatterField, required: q.required };
|
|
9286
|
+
}
|
|
9287
|
+
function getWorkItemCaptureDefinition() {
|
|
9288
|
+
const questions = {};
|
|
9289
|
+
for (const type of WORK_ITEM_TYPES2) {
|
|
9290
|
+
questions[type] = getLevel(getLevelForType(type)).questions.map(toCapture);
|
|
9291
|
+
}
|
|
9292
|
+
return {
|
|
9293
|
+
types: WORK_ITEM_TYPES2.map((t) => ({ value: t, label: t.charAt(0).toUpperCase() + t.slice(1) })),
|
|
9294
|
+
questions
|
|
9295
|
+
};
|
|
9296
|
+
}
|
|
9297
|
+
var RECOMMENDED_AGENT = "work-item-agent";
|
|
9298
|
+
var RECOMMENDED_SKILL = "work-item-refinement";
|
|
9299
|
+
function buildRefinementHandoff(dir, workItemId) {
|
|
9300
|
+
const wi = getWorkItem(dir, workItemId);
|
|
9301
|
+
const config = loadConfig(dir);
|
|
9302
|
+
const projectName = config?.project.name ?? "this project";
|
|
9303
|
+
const mappedModules = loadMappedModules(dir).map((m) => m.id);
|
|
9304
|
+
const multirepo = mappedModules.length > 0;
|
|
9305
|
+
const lines = [
|
|
9306
|
+
`Refine Work Item ${wi.id} \u2014 "${wi.title}" \u2014 in project "${projectName}" using Kaddo.`,
|
|
9307
|
+
"",
|
|
9308
|
+
`Use the canonical ${RECOMMENDED_AGENT} and the ${RECOMMENDED_SKILL} skill (via Kaddo MCP or skills),`,
|
|
9309
|
+
"with access to this repository.",
|
|
9310
|
+
"",
|
|
9311
|
+
"Inspect the actual implementation and the relevant mapped modules before defining the final scope \u2014",
|
|
9312
|
+
"do not guess affected modules from the Work Item title. Read the current behavior in the code first,",
|
|
9313
|
+
"then classify."
|
|
9314
|
+
];
|
|
9315
|
+
if (multirepo) {
|
|
9316
|
+
lines.push(
|
|
9317
|
+
"",
|
|
9318
|
+
`This is a multirepo project. Evaluate the scope across all relevant mapped modules (${mappedModules.join(", ")})`,
|
|
9319
|
+
"before finalizing affected_modules and module_coverage."
|
|
9320
|
+
);
|
|
9321
|
+
}
|
|
9322
|
+
const topology = getSystemMapProjection(dir).metadata.topologyStatus;
|
|
9323
|
+
if (topology !== "unavailable") {
|
|
9324
|
+
lines.push(
|
|
9325
|
+
"",
|
|
9326
|
+
`The semantic system Graph is ${topology}. When semantic system topology is available:`,
|
|
9327
|
+
"1. identify the relevant system entry points;",
|
|
9328
|
+
"2. query the Kaddo Graph (search / neighbors / paths) for connected entities;",
|
|
9329
|
+
"3. treat Graph results as IMPACT CANDIDATES, not confirmed scope;",
|
|
9330
|
+
"4. inspect the actual implementation in the repository for each relevant candidate;",
|
|
9331
|
+
"5. classify each candidate as affected, reviewed-not-affected or unknown;",
|
|
9332
|
+
"6. preserve the evidence and reasons behind each classification.",
|
|
9333
|
+
"",
|
|
9334
|
+
"A missing Graph relationship does not mean no impact, especially when Graph coverage is partial \u2014",
|
|
9335
|
+
"keep inspecting the repository beyond the Graph candidates when the task requires it."
|
|
9336
|
+
);
|
|
9337
|
+
} else {
|
|
9338
|
+
lines.push(
|
|
9339
|
+
"",
|
|
9340
|
+
"The semantic system Graph is unavailable. Continue repository-driven refinement normally, using the",
|
|
9341
|
+
"repository, Knowledge and mapped modules \u2014 the Graph is enrichment, not a prerequisite."
|
|
9342
|
+
);
|
|
9343
|
+
}
|
|
9344
|
+
lines.push(
|
|
9345
|
+
"",
|
|
9346
|
+
`Update the canonical Work Item ${wi.id} with:`,
|
|
9347
|
+
"- current and target behavior;",
|
|
9348
|
+
"- the end-to-end flow (journey);",
|
|
9349
|
+
"- affected modules;",
|
|
9350
|
+
"- affected system entities (affected_system_entities) and reviewed_system_entities;",
|
|
9351
|
+
"- module coverage;",
|
|
9352
|
+
"- impact analysis across the relevant surfaces;",
|
|
9353
|
+
"- scope confidence and open unknowns;",
|
|
9354
|
+
"- acceptance criteria;",
|
|
9355
|
+
"- relevant Knowledge / ADR relationships.",
|
|
9356
|
+
"",
|
|
9357
|
+
"Do not implement the Work Item. Do not run mutating Git operations."
|
|
9358
|
+
);
|
|
9359
|
+
return {
|
|
9360
|
+
workItemId: wi.id,
|
|
9361
|
+
title: wi.title,
|
|
9362
|
+
projectName,
|
|
9363
|
+
refinement: wi.refinement,
|
|
9364
|
+
recommendedAgent: RECOMMENDED_AGENT,
|
|
9365
|
+
recommendedSkill: RECOMMENDED_SKILL,
|
|
9366
|
+
text: lines.join("\n")
|
|
9367
|
+
};
|
|
9368
|
+
}
|
|
9169
9369
|
export {
|
|
9170
9370
|
TOPOLOGY_FILE,
|
|
9171
9371
|
TopologyWriteError,
|
|
@@ -9185,7 +9385,10 @@ export {
|
|
|
9185
9385
|
discoverKnowledge,
|
|
9186
9386
|
discoverWorkItems,
|
|
9187
9387
|
exists,
|
|
9388
|
+
findSystemPaths,
|
|
9389
|
+
getImpactCandidates,
|
|
9188
9390
|
getSystemMapProjection,
|
|
9391
|
+
getSystemNeighbors,
|
|
9189
9392
|
getSystemNodeContext,
|
|
9190
9393
|
getWorkItem,
|
|
9191
9394
|
getWorkItemCaptureDefinition,
|
|
@@ -9202,6 +9405,7 @@ export {
|
|
|
9202
9405
|
loadMappedModules,
|
|
9203
9406
|
loadSystemTopology,
|
|
9204
9407
|
readFile,
|
|
9408
|
+
searchSystemNodes,
|
|
9205
9409
|
topologyRevision,
|
|
9206
9410
|
transitionWorkItem,
|
|
9207
9411
|
updateWorkItem,
|