@kaddo/cli 3.78.0 → 3.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 path3 = i.path.join(".");
120
- return path3 ? ` - ${path3}: ${i.message}` : ` - ${i.message}`;
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 path3 = join(dir, DESCRIPTOR_PATH);
356
- if (!exists(path3)) return [];
355
+ const path4 = join(dir, DESCRIPTOR_PATH);
356
+ if (!exists(path4)) return [];
357
357
  let parsed;
358
358
  try {
359
- parsed = parseYaml3(readFile(path3));
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, path3, md) {
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: path3,
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 path3 = layer.files.map((f) => `${KNOWLEDGE2}/${f}`).find((rel) => exists(join(dir, rel)));
1666
- if (path3) {
1667
- addNode({ id: layer.id, type: layer.type, label: layer.label, path: path3 });
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 path3 = agentInstallPath(file);
3064
- const installed = isFile(join(dir, path3));
3063
+ const path4 = agentInstallPath(file);
3064
+ const installed = isFile(join(dir, path4));
3065
3065
  const group = agentGroupOf(file);
3066
- return { agentPath: path3, agentInstalled: installed, installCommand: installed ? void 0 : `kaddo add agents --group ${group}` };
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);
@@ -8713,7 +8713,10 @@ function buildRefinementHandoff(dir, workItemId) {
8713
8713
  }
8714
8714
 
8715
8715
  // src/core/system-topology.ts
8716
- import { parse as parseYaml8 } from "yaml";
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";
8717
8720
  var TOPOLOGY_FILE = "knowledge/tech/system-topology.yml";
8718
8721
  var SYSTEM_ENTITY_KINDS = /* @__PURE__ */ new Set([
8719
8722
  "system",
@@ -8749,22 +8752,27 @@ function strList(v) {
8749
8752
  return Array.isArray(v) ? v.map((x) => typeof x === "string" ? x.trim() : "").filter(Boolean) : [];
8750
8753
  }
8751
8754
  function loadSystemTopology(dir) {
8752
- const path3 = join(dir, TOPOLOGY_FILE);
8753
- if (!exists(path3)) return { entities: [], relationships: [], findings: [], declared: false };
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) {
8754
8761
  let parsed;
8755
8762
  try {
8756
- parsed = parseYaml8(readFile(path3)) ?? {};
8763
+ parsed = parseYaml8(raw) ?? {};
8757
8764
  } catch {
8758
- return { entities: [], relationships: [], findings: [{ level: "blocking", message: "system-topology.yml could not be parsed." }], declared: true };
8765
+ return { entities: [], relationships: [], findings: [{ level: "blocking", message: "The topology could not be parsed." }] };
8759
8766
  }
8767
+ const sourceRevision = typeof parsed.source_revision === "string" ? parsed.source_revision : void 0;
8760
8768
  const findings = [];
8761
8769
  const validModules = /* @__PURE__ */ new Set(["core", ...loadMappedModules(dir).map((m) => m.id)]);
8762
8770
  const rawEntities = Array.isArray(parsed.entities) ? parsed.entities : [];
8763
8771
  const entities = [];
8764
8772
  const seenIds = /* @__PURE__ */ new Set();
8765
- for (const raw of rawEntities) {
8766
- if (!raw || typeof raw !== "object") continue;
8767
- const e = raw;
8773
+ for (const raw2 of rawEntities) {
8774
+ if (!raw2 || typeof raw2 !== "object") continue;
8775
+ const e = raw2;
8768
8776
  const id = typeof e.id === "string" ? e.id.trim() : "";
8769
8777
  if (!id) {
8770
8778
  findings.push({ level: "warning", message: "A topology entity is missing an id and was skipped." });
@@ -8780,12 +8788,16 @@ function loadSystemTopology(dir) {
8780
8788
  }
8781
8789
  const moduleRaw = typeof e.module === "string" ? e.module.trim() : void 0;
8782
8790
  if (moduleRaw && !validModules.has(moduleRaw)) findings.push({ level: "warning", message: `Entity "${id}" references unregistered module "${moduleRaw}".` });
8783
- const implementation = strList(e.implementation).filter((p2) => {
8791
+ const rawImpl = e.implementation ?? e.implementation_refs;
8792
+ const implementation = strList(rawImpl).filter((p2) => {
8784
8793
  if (isRelative(p2)) return true;
8785
8794
  findings.push({ level: "warning", message: `Entity "${id}" implementation ref "${p2}" is not a safe relative path and was dropped.` });
8786
8795
  return false;
8787
8796
  });
8788
- const provenance = typeof e.provenance === "string" && PROVENANCE.has(e.provenance) ? e.provenance : void 0;
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);
8789
8801
  seenIds.add(id);
8790
8802
  entities.push({
8791
8803
  id,
@@ -8794,19 +8806,20 @@ function loadSystemTopology(dir) {
8794
8806
  ...typeof e.purpose === "string" && e.purpose.trim() ? { purpose: e.purpose.trim() } : {},
8795
8807
  ...moduleRaw && validModules.has(moduleRaw) ? { moduleId: moduleRaw } : {},
8796
8808
  implementationRefs: implementation,
8797
- knowledgeRefs: strList(e.knowledge),
8809
+ knowledgeRefs: strList(e.knowledge ?? e.knowledge_refs),
8798
8810
  ...provenance ? { provenance } : {},
8799
- evidence: strList(e.evidence).filter(isRelative)
8811
+ evidence
8800
8812
  });
8801
8813
  }
8802
8814
  const entityIds = new Set(entities.map((e) => e.id));
8803
8815
  const rawRels = Array.isArray(parsed.relationships) ? parsed.relationships : [];
8804
8816
  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() : "";
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() : "";
8810
8823
  const type = typeof r.type === "string" ? r.type.trim() : "";
8811
8824
  if (!from || !to || !type) {
8812
8825
  findings.push({ level: "warning", message: "A topology relationship is missing from/to/type and was skipped." });
@@ -8820,13 +8833,101 @@ function loadSystemTopology(dir) {
8820
8833
  findings.push({ level: "blocking", message: `Relationship ${from} \u2192 ${to} references an unknown entity endpoint.` });
8821
8834
  continue;
8822
8835
  }
8823
- relationships.push({ from, to, type, evidence: strList(r.evidence).filter(isRelative) });
8836
+ const key = `${from}~${type}~${to}`;
8837
+ if (seenRels.has(key)) {
8838
+ findings.push({ level: "warning", message: `Duplicate relationship ${from} ${type} ${to} was skipped.` });
8839
+ continue;
8840
+ }
8841
+ seenRels.add(key);
8842
+ const prov = r.provenance;
8843
+ relationships.push({ from, to, type, evidence: strList(r.evidence ?? prov?.evidence_refs).filter(isRelative) });
8824
8844
  }
8825
- return { entities, relationships, findings, declared: true };
8845
+ return { entities, relationships, findings, ...sourceRevision ? { sourceRevision } : {} };
8826
8846
  }
8827
8847
  function validateSystemTopology(dir) {
8828
8848
  return loadSystemTopology(dir).findings;
8829
8849
  }
8850
+ var TopologyWriteError = class extends Error {
8851
+ code;
8852
+ constructor(code, message) {
8853
+ super(message);
8854
+ this.name = "TopologyWriteError";
8855
+ this.code = code;
8856
+ }
8857
+ };
8858
+ function topologyRevision(dir) {
8859
+ const filePath = join(dir, TOPOLOGY_FILE);
8860
+ const raw = exists(filePath) ? readFile(filePath) : "";
8861
+ return crypto2.createHash("sha256").update(raw, "utf-8").digest("hex");
8862
+ }
8863
+ function validateTopologyProposal(dir, proposalYaml) {
8864
+ const { entities, relationships, findings } = parseTopologyContent(proposalYaml, dir);
8865
+ const blocking = findings.filter((f) => f.level === "blocking").length;
8866
+ const warning = findings.filter((f) => f.level === "warning").length;
8867
+ return { findings, blocking, warning, canApply: blocking === 0, entityCount: entities.length, relationshipCount: relationships.length };
8868
+ }
8869
+ function serializeTopology(entities, relationships) {
8870
+ const doc = {
8871
+ entities: entities.map((e) => ({
8872
+ id: e.id,
8873
+ kind: e.kind,
8874
+ label: e.label,
8875
+ ...e.purpose ? { purpose: e.purpose } : {},
8876
+ ...e.moduleId ? { module: e.moduleId } : {},
8877
+ ...e.implementationRefs.length ? { implementation: e.implementationRefs } : {},
8878
+ ...e.knowledgeRefs.length ? { knowledge: e.knowledgeRefs } : {},
8879
+ ...e.provenance ? { provenance: e.provenance } : {},
8880
+ ...e.evidence.length ? { evidence: e.evidence } : {}
8881
+ })),
8882
+ relationships: relationships.map((r) => ({ from: r.from, to: r.to, type: r.type, ...r.evidence.length ? { evidence: r.evidence } : {} }))
8883
+ };
8884
+ return stringifyYaml4(doc);
8885
+ }
8886
+ function atomicWrite2(filePath, content) {
8887
+ fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
8888
+ const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
8889
+ fs3.writeFileSync(tmp, content, "utf-8");
8890
+ try {
8891
+ fs3.renameSync(tmp, filePath);
8892
+ } catch (err) {
8893
+ try {
8894
+ fs3.rmSync(tmp, { force: true });
8895
+ } catch {
8896
+ }
8897
+ throw err;
8898
+ }
8899
+ }
8900
+ function applyTopologyProposal(dir, proposalYaml, expectedRevision) {
8901
+ const validation = validateTopologyProposal(dir, proposalYaml);
8902
+ if (!validation.canApply) throw new TopologyWriteError("TOPOLOGY_INVALID", "The topology proposal has blocking findings and cannot be applied.");
8903
+ const current = topologyRevision(dir);
8904
+ if (expectedRevision != null && expectedRevision !== current) {
8905
+ throw new TopologyWriteError("TOPOLOGY_CONFLICT", "The topology changed after this proposal was created. Refresh and validate the proposal again.");
8906
+ }
8907
+ const proposal = parseTopologyContent(proposalYaml, dir);
8908
+ const existing = loadSystemTopology(dir);
8909
+ const entityMap = /* @__PURE__ */ new Map();
8910
+ for (const e of existing.entities) entityMap.set(e.id, e);
8911
+ let entitiesNew = 0, entitiesUpdated = 0;
8912
+ for (const e of proposal.entities) {
8913
+ if (entityMap.has(e.id)) entitiesUpdated++;
8914
+ else entitiesNew++;
8915
+ entityMap.set(e.id, e);
8916
+ }
8917
+ const relKey = (r) => `${r.from}~${r.type}~${r.to}`;
8918
+ const relMap = /* @__PURE__ */ new Map();
8919
+ for (const r of existing.relationships) relMap.set(relKey(r), r);
8920
+ let relationshipsNew = 0;
8921
+ for (const r of proposal.relationships) {
8922
+ if (!relMap.has(relKey(r))) relationshipsNew++;
8923
+ relMap.set(relKey(r), r);
8924
+ }
8925
+ const raw = serializeTopology([...entityMap.values()], [...relMap.values()]);
8926
+ const finalCheck = validateTopologyProposal(dir, raw);
8927
+ if (!finalCheck.canApply) throw new TopologyWriteError("TOPOLOGY_INVALID", "The merged topology is invalid; no changes were written.");
8928
+ atomicWrite2(join(dir, TOPOLOGY_FILE), raw);
8929
+ return { revision: topologyRevision(dir), entitiesNew, entitiesUpdated, relationshipsNew };
8930
+ }
8830
8931
  function buildTopologyEnrichmentHandoff(dir, projectName) {
8831
8932
  const mappedModules = loadMappedModules(dir).map((m) => m.id);
8832
8933
  const lines = [
@@ -8841,19 +8942,22 @@ function buildTopologyEnrichmentHandoff(dir, projectName) {
8841
8942
  }
8842
8943
  lines.push(
8843
8944
  "",
8844
- `Write the result to ${TOPOLOGY_FILE} as declared entities and relationships. For each entity`,
8945
+ "Build a topology PROPOSAL first \u2014 do not write canonical metadata directly. For each entity",
8845
8946
  "capture, only when supported by evidence:",
8846
8947
  "- a stable id and semantic kind (application/service/component/api/interface/datastore/queue/job/external-system);",
8847
8948
  "- a responsibility/purpose (what it does, not how);",
8848
8949
  "- the owning module/repository;",
8849
8950
  "- implementation references (relative paths);",
8850
- "- relevant Knowledge references (capability/ADR ids), not Kaddo operational assets.",
8951
+ "- relevant Knowledge references (capability/ADR ids), not Kaddo operational assets;",
8952
+ "- provenance/evidence.",
8851
8953
  "",
8852
8954
  "Capture evidence-backed technical relationships: contains, calls, depends-on, reads-from,",
8853
- "writes-to, integrates-with, runs-on. Preserve unknowns when evidence is insufficient.",
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.`,
8854
8960
  "",
8855
- "Review the proposed graph metadata (duplicate ids, dangling relationships, unknown modules,",
8856
- "invalid references) before writing.",
8857
8961
  "Do not implement application changes. Do not run mutating Git operations."
8858
8962
  );
8859
8963
  return {
@@ -9064,10 +9168,12 @@ function toNode(n, knowledgeByPath, wiModules, groupIds) {
9064
9168
  }
9065
9169
  export {
9066
9170
  TOPOLOGY_FILE,
9171
+ TopologyWriteError,
9067
9172
  WorkItemNotFoundError,
9068
9173
  WorkItemWriteError,
9069
9174
  analyzeCrossRepoEvidence,
9070
9175
  analyzeScopeCoverage,
9176
+ applyTopologyProposal,
9071
9177
  buildProjectExplanation,
9072
9178
  buildProjectRoute,
9073
9179
  buildReadinessReport,
@@ -9096,8 +9202,10 @@ export {
9096
9202
  loadMappedModules,
9097
9203
  loadSystemTopology,
9098
9204
  readFile,
9205
+ topologyRevision,
9099
9206
  transitionWorkItem,
9100
9207
  updateWorkItem,
9101
9208
  validateSystemTopology,
9209
+ validateTopologyProposal,
9102
9210
  validateWorkItem
9103
9211
  };