@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/index.js CHANGED
@@ -1361,8 +1361,8 @@ function loadConfig(dir) {
1361
1361
  const parsed = configSchema.safeParse(raw ?? {});
1362
1362
  if (!parsed.success) {
1363
1363
  const issues = parsed.error.issues.map((i) => {
1364
- const path9 = i.path.join(".");
1365
- return path9 ? ` - ${path9}: ${i.message}` : ` - ${i.message}`;
1364
+ const path10 = i.path.join(".");
1365
+ return path10 ? ` - ${path10}: ${i.message}` : ` - ${i.message}`;
1366
1366
  }).join("\n");
1367
1367
  throw new ConfigError(`Invalid .kaddo/config.yml:
1368
1368
  ${issues}`);
@@ -6732,10 +6732,10 @@ function analyzeGuard(touchedFiles, artifacts, silentWithoutOwnership) {
6732
6732
  import { parse as parseYaml4, stringify as stringifyYaml2 } from "yaml";
6733
6733
  var IGNORE_FILE = ".kaddo/ignores.yml";
6734
6734
  function loadIgnores(dir) {
6735
- const path9 = join(dir, IGNORE_FILE);
6736
- if (!exists(path9)) return [];
6735
+ const path10 = join(dir, IGNORE_FILE);
6736
+ if (!exists(path10)) return [];
6737
6737
  try {
6738
- const raw = readFile(path9);
6738
+ const raw = readFile(path10);
6739
6739
  const parsed = parseYaml4(raw);
6740
6740
  return Array.isArray(parsed) ? parsed : [];
6741
6741
  } catch {
@@ -6743,7 +6743,7 @@ function loadIgnores(dir) {
6743
6743
  }
6744
6744
  }
6745
6745
  function saveIgnore(dir, entry) {
6746
- const path9 = join(dir, IGNORE_FILE);
6746
+ const path10 = join(dir, IGNORE_FILE);
6747
6747
  const existing = loadIgnores(dir);
6748
6748
  const idx = existing.findIndex((e) => e.artifact_id === entry.artifact_id);
6749
6749
  if (idx >= 0) {
@@ -6751,17 +6751,17 @@ function saveIgnore(dir, entry) {
6751
6751
  } else {
6752
6752
  existing.push(entry);
6753
6753
  }
6754
- writeFile(path9, stringifyYaml2(existing));
6754
+ writeFile(path10, stringifyYaml2(existing));
6755
6755
  }
6756
6756
  function isIgnored(ignores, artifactId) {
6757
6757
  return ignores.find((e) => e.artifact_id === artifactId);
6758
6758
  }
6759
6759
  function removeIgnore(dir, artifactId) {
6760
- const path9 = join(dir, IGNORE_FILE);
6760
+ const path10 = join(dir, IGNORE_FILE);
6761
6761
  const existing = loadIgnores(dir);
6762
6762
  const filtered = existing.filter((e) => e.artifact_id !== artifactId);
6763
6763
  if (filtered.length === existing.length) return false;
6764
- writeFile(path9, stringifyYaml2(filtered));
6764
+ writeFile(path10, stringifyYaml2(filtered));
6765
6765
  return true;
6766
6766
  }
6767
6767
 
@@ -6783,11 +6783,11 @@ function moduleArtifactCoverage(dir, id) {
6783
6783
  };
6784
6784
  }
6785
6785
  function loadMappedModules(dir) {
6786
- const path9 = join(dir, DESCRIPTOR_PATH);
6787
- if (!exists(path9)) return [];
6786
+ const path10 = join(dir, DESCRIPTOR_PATH);
6787
+ if (!exists(path10)) return [];
6788
6788
  let parsed;
6789
6789
  try {
6790
- parsed = parseYaml5(readFile(path9));
6790
+ parsed = parseYaml5(readFile(path10));
6791
6791
  } catch {
6792
6792
  return [];
6793
6793
  }
@@ -7531,7 +7531,7 @@ function buildGuardHistory(dir) {
7531
7531
  const d = hotspotDir(t.code_path);
7532
7532
  hotspotMap.set(d, (hotspotMap.get(d) ?? 0) + 1);
7533
7533
  }
7534
- const hotspots = [...hotspotMap.entries()].map(([path9, warnings]) => ({ path: path9, warnings })).sort((a, b) => b.warnings - a.warnings);
7534
+ const hotspots = [...hotspotMap.entries()].map(([path10, warnings]) => ({ path: path10, warnings })).sort((a, b) => b.warnings - a.warnings);
7535
7535
  return {
7536
7536
  available: true,
7537
7537
  total_runs: sorted.length,
@@ -10102,10 +10102,10 @@ function slugify2(name) {
10102
10102
  return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
10103
10103
  }
10104
10104
  function readModulesDescriptor(dir) {
10105
- const path9 = join(dir, DESCRIPTOR_PATH2);
10106
- if (!exists(path9)) return { version: 1, modules: [] };
10105
+ const path10 = join(dir, DESCRIPTOR_PATH2);
10106
+ if (!exists(path10)) return { version: 1, modules: [] };
10107
10107
  try {
10108
- const parsed = parseYaml9(readFile(path9));
10108
+ const parsed = parseYaml9(readFile(path10));
10109
10109
  return { version: parsed.version ?? 1, modules: parsed.modules ?? [] };
10110
10110
  } catch {
10111
10111
  return { version: 1, modules: [] };
@@ -10485,7 +10485,7 @@ function sectionParagraph(md, title) {
10485
10485
  }
10486
10486
  return "";
10487
10487
  }
10488
- function parseCapsule(id, path9, md) {
10488
+ function parseCapsule(id, path10, md) {
10489
10489
  const { data } = matter3(md);
10490
10490
  const updatedAt = data.updated_at ? String(data.updated_at) : void 0;
10491
10491
  let ageDays = null;
@@ -10495,7 +10495,7 @@ function parseCapsule(id, path9, md) {
10495
10495
  }
10496
10496
  return {
10497
10497
  id,
10498
- path: path9,
10498
+ path: path10,
10499
10499
  system: data.system ? String(data.system) : id,
10500
10500
  owner: data.owner ? String(data.owner) : void 0,
10501
10501
  updatedAt,
@@ -10581,9 +10581,9 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
10581
10581
  ];
10582
10582
  const presentLayers = [];
10583
10583
  for (const layer2 of layerDocs) {
10584
- const path9 = layer2.files.map((f) => `${KNOWLEDGE3}/${f}`).find((rel) => exists(join(dir, rel)));
10585
- if (path9) {
10586
- addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path9 });
10584
+ const path10 = layer2.files.map((f) => `${KNOWLEDGE3}/${f}`).find((rel) => exists(join(dir, rel)));
10585
+ if (path10) {
10586
+ addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path10 });
10587
10587
  presentLayers.push(layer2.id);
10588
10588
  }
10589
10589
  }
@@ -12119,10 +12119,10 @@ function resolveNextStep(dir, now = /* @__PURE__ */ new Date()) {
12119
12119
  const q = (rel) => analyzeKnowledgeArtifact(dir, rel);
12120
12120
  const resolveAgent = (agent) => {
12121
12121
  const file = agent.endsWith(".md") ? agent : `${agent}.md`;
12122
- const path9 = agentInstallPath(file);
12123
- const installed = isFile(join(dir, path9));
12122
+ const path10 = agentInstallPath(file);
12123
+ const installed = isFile(join(dir, path10));
12124
12124
  const group = agentGroupOf(file);
12125
- return { agentPath: path9, agentInstalled: installed, installCommand: installed ? void 0 : `kaddo add agents --group ${group}` };
12125
+ return { agentPath: path10, agentInstalled: installed, installCommand: installed ? void 0 : `kaddo add agents --group ${group}` };
12126
12126
  };
12127
12127
  const moduleRepo = isModule(config);
12128
12128
  const coreRepo = isCore(config);
@@ -17097,6 +17097,287 @@ function runGraphExport(opts = {}) {
17097
17097
  outro2("Knowledge graph ready.");
17098
17098
  }
17099
17099
 
17100
+ // src/core/system-topology.ts
17101
+ import { parse as parseYaml16, stringify as stringifyYaml7 } from "yaml";
17102
+ import fs3 from "fs";
17103
+ import path7 from "path";
17104
+ import crypto from "crypto";
17105
+ var TOPOLOGY_FILE = "knowledge/tech/system-topology.yml";
17106
+ var SYSTEM_ENTITY_KINDS = /* @__PURE__ */ new Set([
17107
+ "system",
17108
+ "application",
17109
+ "service",
17110
+ "component",
17111
+ "api",
17112
+ "interface",
17113
+ "datastore",
17114
+ "queue",
17115
+ "job",
17116
+ "external-system",
17117
+ "module",
17118
+ "unknown"
17119
+ ]);
17120
+ var TECHNICAL_RELATIONSHIP_TYPES = /* @__PURE__ */ new Set([
17121
+ "contains",
17122
+ "depends-on",
17123
+ "calls",
17124
+ "reads-from",
17125
+ "writes-to",
17126
+ "publishes-to",
17127
+ "subscribes-to",
17128
+ "integrates-with",
17129
+ "runs-on",
17130
+ "implemented-by"
17131
+ ]);
17132
+ var PROVENANCE = /* @__PURE__ */ new Set(["declared", "derived", "agent-reviewed"]);
17133
+ function isRelative(p2) {
17134
+ return typeof p2 === "string" && p2.trim() !== "" && !/^([a-zA-Z]:[\\/]|\/)/.test(p2) && !p2.includes("..");
17135
+ }
17136
+ function strList(v) {
17137
+ return Array.isArray(v) ? v.map((x) => typeof x === "string" ? x.trim() : "").filter(Boolean) : [];
17138
+ }
17139
+ function loadSystemTopology(dir) {
17140
+ const filePath = join(dir, TOPOLOGY_FILE);
17141
+ if (!exists(filePath)) return { entities: [], relationships: [], findings: [], declared: false };
17142
+ const parsed = parseTopologyContent(readFile(filePath), dir);
17143
+ return { ...parsed, declared: true };
17144
+ }
17145
+ function parseTopologyContent(raw, dir) {
17146
+ let parsed;
17147
+ try {
17148
+ parsed = parseYaml16(raw) ?? {};
17149
+ } catch {
17150
+ return { entities: [], relationships: [], findings: [{ level: "blocking", message: "The topology could not be parsed." }] };
17151
+ }
17152
+ const sourceRevision = typeof parsed.source_revision === "string" ? parsed.source_revision : void 0;
17153
+ const findings = [];
17154
+ const validModules = /* @__PURE__ */ new Set(["core", ...loadMappedModules(dir).map((m) => m.id)]);
17155
+ const rawEntities = Array.isArray(parsed.entities) ? parsed.entities : [];
17156
+ const entities = [];
17157
+ const seenIds = /* @__PURE__ */ new Set();
17158
+ for (const raw2 of rawEntities) {
17159
+ if (!raw2 || typeof raw2 !== "object") continue;
17160
+ const e = raw2;
17161
+ const id = typeof e.id === "string" ? e.id.trim() : "";
17162
+ if (!id) {
17163
+ findings.push({ level: "warning", message: "A topology entity is missing an id and was skipped." });
17164
+ continue;
17165
+ }
17166
+ if (seenIds.has(id)) {
17167
+ findings.push({ level: "blocking", message: `Duplicate topology entity id "${id}".` });
17168
+ continue;
17169
+ }
17170
+ const kind = typeof e.kind === "string" ? e.kind.trim() : "unknown";
17171
+ if (!SYSTEM_ENTITY_KINDS.has(kind)) {
17172
+ findings.push({ level: "warning", message: `Entity "${id}" has an unknown kind "${kind}"; treated as unknown.` });
17173
+ }
17174
+ const moduleRaw = typeof e.module === "string" ? e.module.trim() : void 0;
17175
+ if (moduleRaw && !validModules.has(moduleRaw)) findings.push({ level: "warning", message: `Entity "${id}" references unregistered module "${moduleRaw}".` });
17176
+ const rawImpl = e.implementation ?? e.implementation_refs;
17177
+ const implementation = strList(rawImpl).filter((p2) => {
17178
+ if (isRelative(p2)) return true;
17179
+ findings.push({ level: "warning", message: `Entity "${id}" implementation ref "${p2}" is not a safe relative path and was dropped.` });
17180
+ return false;
17181
+ });
17182
+ const prov = e.provenance;
17183
+ const provOrigin = typeof prov === "string" ? prov : prov && typeof prov === "object" && typeof prov.origin === "string" ? String(prov.origin) : void 0;
17184
+ const provenance = provOrigin && PROVENANCE.has(provOrigin) ? provOrigin : void 0;
17185
+ const evidence = strList(e.evidence ?? (prov && typeof prov === "object" ? prov.evidence_refs : void 0)).filter(isRelative);
17186
+ seenIds.add(id);
17187
+ entities.push({
17188
+ id,
17189
+ kind: SYSTEM_ENTITY_KINDS.has(kind) ? kind : "unknown",
17190
+ label: typeof e.label === "string" && e.label.trim() ? e.label.trim() : id,
17191
+ ...typeof e.purpose === "string" && e.purpose.trim() ? { purpose: e.purpose.trim() } : {},
17192
+ ...moduleRaw && validModules.has(moduleRaw) ? { moduleId: moduleRaw } : {},
17193
+ implementationRefs: implementation,
17194
+ knowledgeRefs: strList(e.knowledge ?? e.knowledge_refs),
17195
+ ...provenance ? { provenance } : {},
17196
+ evidence
17197
+ });
17198
+ }
17199
+ const entityIds = new Set(entities.map((e) => e.id));
17200
+ const rawRels = Array.isArray(parsed.relationships) ? parsed.relationships : [];
17201
+ const relationships = [];
17202
+ const seenRels = /* @__PURE__ */ new Set();
17203
+ for (const raw2 of rawRels) {
17204
+ if (!raw2 || typeof raw2 !== "object") continue;
17205
+ const r = raw2;
17206
+ const from = typeof r.from === "string" ? r.from.trim() : typeof r.source === "string" ? r.source.trim() : "";
17207
+ const to = typeof r.to === "string" ? r.to.trim() : typeof r.target === "string" ? r.target.trim() : "";
17208
+ const type = typeof r.type === "string" ? r.type.trim() : "";
17209
+ if (!from || !to || !type) {
17210
+ findings.push({ level: "warning", message: "A topology relationship is missing from/to/type and was skipped." });
17211
+ continue;
17212
+ }
17213
+ if (!TECHNICAL_RELATIONSHIP_TYPES.has(type)) {
17214
+ findings.push({ level: "warning", message: `Relationship type "${type}" is not recognized and was skipped.` });
17215
+ continue;
17216
+ }
17217
+ if (!entityIds.has(from) || !entityIds.has(to)) {
17218
+ findings.push({ level: "blocking", message: `Relationship ${from} \u2192 ${to} references an unknown entity endpoint.` });
17219
+ continue;
17220
+ }
17221
+ const key = `${from}~${type}~${to}`;
17222
+ if (seenRels.has(key)) {
17223
+ findings.push({ level: "warning", message: `Duplicate relationship ${from} ${type} ${to} was skipped.` });
17224
+ continue;
17225
+ }
17226
+ seenRels.add(key);
17227
+ const prov = r.provenance;
17228
+ relationships.push({ from, to, type, evidence: strList(r.evidence ?? prov?.evidence_refs).filter(isRelative) });
17229
+ }
17230
+ return { entities, relationships, findings, ...sourceRevision ? { sourceRevision } : {} };
17231
+ }
17232
+ var TopologyWriteError = class extends Error {
17233
+ code;
17234
+ constructor(code, message) {
17235
+ super(message);
17236
+ this.name = "TopologyWriteError";
17237
+ this.code = code;
17238
+ }
17239
+ };
17240
+ function topologyRevision(dir) {
17241
+ const filePath = join(dir, TOPOLOGY_FILE);
17242
+ const raw = exists(filePath) ? readFile(filePath) : "";
17243
+ return crypto.createHash("sha256").update(raw, "utf-8").digest("hex");
17244
+ }
17245
+ function validateTopologyProposal(dir, proposalYaml) {
17246
+ const { entities, relationships, findings } = parseTopologyContent(proposalYaml, dir);
17247
+ const blocking = findings.filter((f) => f.level === "blocking").length;
17248
+ const warning = findings.filter((f) => f.level === "warning").length;
17249
+ return { findings, blocking, warning, canApply: blocking === 0, entityCount: entities.length, relationshipCount: relationships.length };
17250
+ }
17251
+ function serializeTopology(entities, relationships) {
17252
+ const doc = {
17253
+ entities: entities.map((e) => ({
17254
+ id: e.id,
17255
+ kind: e.kind,
17256
+ label: e.label,
17257
+ ...e.purpose ? { purpose: e.purpose } : {},
17258
+ ...e.moduleId ? { module: e.moduleId } : {},
17259
+ ...e.implementationRefs.length ? { implementation: e.implementationRefs } : {},
17260
+ ...e.knowledgeRefs.length ? { knowledge: e.knowledgeRefs } : {},
17261
+ ...e.provenance ? { provenance: e.provenance } : {},
17262
+ ...e.evidence.length ? { evidence: e.evidence } : {}
17263
+ })),
17264
+ relationships: relationships.map((r) => ({ from: r.from, to: r.to, type: r.type, ...r.evidence.length ? { evidence: r.evidence } : {} }))
17265
+ };
17266
+ return stringifyYaml7(doc);
17267
+ }
17268
+ function atomicWrite(filePath, content) {
17269
+ fs3.mkdirSync(path7.dirname(filePath), { recursive: true });
17270
+ const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
17271
+ fs3.writeFileSync(tmp, content, "utf-8");
17272
+ try {
17273
+ fs3.renameSync(tmp, filePath);
17274
+ } catch (err) {
17275
+ try {
17276
+ fs3.rmSync(tmp, { force: true });
17277
+ } catch {
17278
+ }
17279
+ throw err;
17280
+ }
17281
+ }
17282
+ function applyTopologyProposal(dir, proposalYaml, expectedRevision) {
17283
+ const validation = validateTopologyProposal(dir, proposalYaml);
17284
+ if (!validation.canApply) throw new TopologyWriteError("TOPOLOGY_INVALID", "The topology proposal has blocking findings and cannot be applied.");
17285
+ const current = topologyRevision(dir);
17286
+ if (expectedRevision != null && expectedRevision !== current) {
17287
+ throw new TopologyWriteError("TOPOLOGY_CONFLICT", "The topology changed after this proposal was created. Refresh and validate the proposal again.");
17288
+ }
17289
+ const proposal = parseTopologyContent(proposalYaml, dir);
17290
+ const existing = loadSystemTopology(dir);
17291
+ const entityMap = /* @__PURE__ */ new Map();
17292
+ for (const e of existing.entities) entityMap.set(e.id, e);
17293
+ let entitiesNew = 0, entitiesUpdated = 0;
17294
+ for (const e of proposal.entities) {
17295
+ if (entityMap.has(e.id)) entitiesUpdated++;
17296
+ else entitiesNew++;
17297
+ entityMap.set(e.id, e);
17298
+ }
17299
+ const relKey = (r) => `${r.from}~${r.type}~${r.to}`;
17300
+ const relMap = /* @__PURE__ */ new Map();
17301
+ for (const r of existing.relationships) relMap.set(relKey(r), r);
17302
+ let relationshipsNew = 0;
17303
+ for (const r of proposal.relationships) {
17304
+ if (!relMap.has(relKey(r))) relationshipsNew++;
17305
+ relMap.set(relKey(r), r);
17306
+ }
17307
+ const raw = serializeTopology([...entityMap.values()], [...relMap.values()]);
17308
+ const finalCheck = validateTopologyProposal(dir, raw);
17309
+ if (!finalCheck.canApply) throw new TopologyWriteError("TOPOLOGY_INVALID", "The merged topology is invalid; no changes were written.");
17310
+ atomicWrite(join(dir, TOPOLOGY_FILE), raw);
17311
+ return { revision: topologyRevision(dir), entitiesNew, entitiesUpdated, relationshipsNew };
17312
+ }
17313
+
17314
+ // src/commands/topology.ts
17315
+ function requireProject(dir) {
17316
+ if (!loadConfig(dir)) {
17317
+ log2.error("No Kaddo project was found in the current directory.");
17318
+ process.exit(1);
17319
+ }
17320
+ }
17321
+ function readProposal(dir, file) {
17322
+ const abs = join(dir, file);
17323
+ if (!exists(abs) && !exists(file)) {
17324
+ log2.error(`Proposal file not found: ${file}`);
17325
+ process.exit(1);
17326
+ }
17327
+ return readFile(exists(abs) ? abs : file);
17328
+ }
17329
+ function printFindings(v) {
17330
+ log2.info(`${v.entityCount} entities \xB7 ${v.relationshipCount} relationships`);
17331
+ log2.info(`${v.blocking} blocking \xB7 ${v.warning} warning`);
17332
+ for (const f of v.findings) {
17333
+ const line = `[${f.level}] ${f.message}`;
17334
+ if (f.level === "blocking") log2.error(line);
17335
+ else log2.warn(line);
17336
+ }
17337
+ }
17338
+ async function runTopologyValidate(file) {
17339
+ const dir = cwd();
17340
+ requireProject(dir);
17341
+ intro2("kaddo topology validate");
17342
+ const proposal = readProposal(dir, file);
17343
+ const v = validateTopologyProposal(dir, proposal);
17344
+ printFindings(v);
17345
+ if (v.canApply) outro2("Proposal is valid. Apply it with `kaddo topology apply`.");
17346
+ else {
17347
+ outro2("Proposal has blocking findings and cannot be applied.");
17348
+ process.exit(1);
17349
+ }
17350
+ }
17351
+ async function runTopologyApply(file, opts = {}) {
17352
+ const dir = cwd();
17353
+ requireProject(dir);
17354
+ intro2("kaddo topology apply");
17355
+ const proposal = readProposal(dir, file);
17356
+ const v = validateTopologyProposal(dir, proposal);
17357
+ printFindings(v);
17358
+ if (!v.canApply) {
17359
+ cancel2("Cannot apply: the proposal has blocking findings.");
17360
+ process.exit(1);
17361
+ }
17362
+ if (!opts.yes) {
17363
+ const ok = await confirm2({ message: `Apply this topology enrichment to ${join("", "knowledge/tech/system-topology.yml")}? (writes the project artifact; no Git commit is created)` });
17364
+ if (!ok) {
17365
+ cancel2("Apply cancelled.");
17366
+ process.exit(0);
17367
+ }
17368
+ }
17369
+ try {
17370
+ const result = applyTopologyProposal(dir, proposal, topologyRevision(dir));
17371
+ outro2(`Applied. ${result.entitiesNew} new / ${result.entitiesUpdated} updated entities, ${result.relationshipsNew} new relationships.`);
17372
+ } catch (err) {
17373
+ if (err instanceof TopologyWriteError) {
17374
+ cancel2(err.message);
17375
+ process.exit(1);
17376
+ }
17377
+ throw err;
17378
+ }
17379
+ }
17380
+
17100
17381
  // src/core/impact-report.ts
17101
17382
  import matter10 from "gray-matter";
17102
17383
  function isBroadGlob(glob) {
@@ -17544,7 +17825,7 @@ function runReportImpact(opts = {}) {
17544
17825
  }
17545
17826
 
17546
17827
  // src/core/savings.ts
17547
- import { parse as parseYaml16 } from "yaml";
17828
+ import { parse as parseYaml17 } from "yaml";
17548
17829
  var DEFAULT_ASSUMPTIONS = {
17549
17830
  currency: "USD",
17550
17831
  hourly_cost: 40,
@@ -17562,7 +17843,7 @@ function loadAssumptions(dir) {
17562
17843
  const p2 = join(dir, SAVINGS_PATH);
17563
17844
  if (!exists(p2)) return { assumptions: { ...DEFAULT_ASSUMPTIONS }, source: "default" };
17564
17845
  try {
17565
- const raw = parseYaml16(readFile(p2));
17846
+ const raw = parseYaml17(readFile(p2));
17566
17847
  const a = raw?.assumptions ?? {};
17567
17848
  const t = raw?.team ?? {};
17568
17849
  const num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
@@ -18244,7 +18525,7 @@ function runAdr(opts = {}) {
18244
18525
  }
18245
18526
 
18246
18527
  // src/commands/tech.ts
18247
- import fs3 from "fs";
18528
+ import fs4 from "fs";
18248
18529
  var DISCOVERY_FILES = ["architecture-notes.md", "decision-candidates.md"];
18249
18530
  var DISCOVERY_DIR = "knowledge/tech/discovery";
18250
18531
  function runTechOrganize(dir = cwd()) {
@@ -18261,7 +18542,7 @@ function runTechOrganize(dir = cwd()) {
18261
18542
  continue;
18262
18543
  }
18263
18544
  ensureDir(join(dir, DISCOVERY_DIR));
18264
- fs3.renameSync(from, to);
18545
+ fs4.renameSync(from, to);
18265
18546
  moved.push(`knowledge/tech/${file} \u2192 ${DISCOVERY_DIR}/${file}`);
18266
18547
  }
18267
18548
  if (moved.length > 0) {
@@ -18343,8 +18624,8 @@ function runAssetsUpdate(kind, opts = {}) {
18343
18624
  }
18344
18625
 
18345
18626
  // src/commands/ready.ts
18346
- import fs4 from "fs";
18347
- import path7 from "path";
18627
+ import fs5 from "fs";
18628
+ import path8 from "path";
18348
18629
  import matter11 from "gray-matter";
18349
18630
  function validateReadiness(content, frontmatter) {
18350
18631
  const warnings = [];
@@ -18407,10 +18688,10 @@ function transitionToReady(filePath) {
18407
18688
  const posix = filePath.replace(/\\/g, "/");
18408
18689
  let newPath = filePath;
18409
18690
  if (posix.includes("/work-items/draft/")) {
18410
- const readyDir = path7.dirname(filePath).replace(/[/\\]draft$/, path7.sep + "ready");
18691
+ const readyDir = path8.dirname(filePath).replace(/[/\\]draft$/, path8.sep + "ready");
18411
18692
  ensureDir(readyDir);
18412
- const fileName = path7.basename(filePath);
18413
- newPath = path7.join(readyDir, fileName);
18693
+ const fileName = path8.basename(filePath);
18694
+ newPath = path8.join(readyDir, fileName);
18414
18695
  }
18415
18696
  return { newPath, newContent };
18416
18697
  }
@@ -18478,11 +18759,11 @@ async function runReady(id, opts = {}) {
18478
18759
  const { newPath, newContent } = transitionToReady(wi.filePath);
18479
18760
  if (newPath !== wi.filePath) {
18480
18761
  writeFile(newPath, newContent);
18481
- fs4.unlinkSync(wi.filePath);
18762
+ fs5.unlinkSync(wi.filePath);
18482
18763
  log2.success(`Updated status: ready`);
18483
18764
  log2.success(`Moved file:`);
18484
- log2.info(` ${path7.relative(dir, wi.filePath)}`);
18485
- log2.info(` \u2192 ${path7.relative(dir, newPath)}`);
18765
+ log2.info(` ${path8.relative(dir, wi.filePath)}`);
18766
+ log2.info(` \u2192 ${path8.relative(dir, newPath)}`);
18486
18767
  } else {
18487
18768
  writeFile(wi.filePath, newContent);
18488
18769
  log2.success(`Updated status: ready`);
@@ -18493,7 +18774,7 @@ async function runReady(id, opts = {}) {
18493
18774
  }
18494
18775
 
18495
18776
  // src/commands/admin.ts
18496
- import path8 from "path";
18777
+ import path9 from "path";
18497
18778
  import { fileURLToPath as fileURLToPath2 } from "url";
18498
18779
  import net from "net";
18499
18780
  function isPortAvailable(port, host) {
@@ -18508,13 +18789,13 @@ function isPortAvailable(port, host) {
18508
18789
  });
18509
18790
  }
18510
18791
  function resolveStaticDir() {
18511
- const __dirname = path8.dirname(fileURLToPath2(import.meta.url));
18792
+ const __dirname = path9.dirname(fileURLToPath2(import.meta.url));
18512
18793
  const candidates = [
18513
18794
  // Bundled inside CLI dist (npm install)
18514
- path8.resolve(__dirname, "admin-dist"),
18795
+ path9.resolve(__dirname, "admin-dist"),
18515
18796
  // Monorepo development
18516
- path8.resolve(__dirname, "..", "..", "admin", "dist"),
18517
- path8.resolve(__dirname, "..", "node_modules", "@kaddo", "admin", "dist")
18797
+ path9.resolve(__dirname, "..", "..", "admin", "dist"),
18798
+ path9.resolve(__dirname, "..", "node_modules", "@kaddo", "admin", "dist")
18518
18799
  ];
18519
18800
  for (const c of candidates) {
18520
18801
  if (exists(join(c, "index.html"))) return c;
@@ -18556,8 +18837,8 @@ async function runAdmin(opts = {}) {
18556
18837
  }
18557
18838
  let adminServer;
18558
18839
  try {
18559
- const __dirname = path8.dirname(fileURLToPath2(import.meta.url));
18560
- const bundled = path8.resolve(__dirname, "admin-server", "index.js");
18840
+ const __dirname = path9.dirname(fileURLToPath2(import.meta.url));
18841
+ const bundled = path9.resolve(__dirname, "admin-server", "index.js");
18561
18842
  if (exists(bundled)) {
18562
18843
  const { pathToFileURL } = await import("url");
18563
18844
  adminServer = await import(pathToFileURL(bundled).href);
@@ -18636,13 +18917,20 @@ var capsuleCmd = program.command("capsule").description("Export this project as
18636
18917
  capsuleCmd.command("export").description("Write a Knowledge Capsule about this project to .kaddo/exports/").option("--scope <scope>", 'Export scope: "system" includes all mapped modules').option("--module <id>", "Export a capsule for a specific mapped module").action((opts) => {
18637
18918
  runCapsuleExport(opts);
18638
18919
  });
18639
- capsuleCmd.command("add <path>").description("Register an external Knowledge Capsule as project context (.kaddo/external.yml)").action((path9) => {
18640
- runCapsuleAdd(path9);
18920
+ capsuleCmd.command("add <path>").description("Register an external Knowledge Capsule as project context (.kaddo/external.yml)").action((path10) => {
18921
+ runCapsuleAdd(path10);
18641
18922
  });
18642
18923
  var graphCmd = program.command("graph").description("Export the lightweight, file-based knowledge graph of the project");
18643
18924
  graphCmd.command("export").description("Write the knowledge graph to .kaddo/graph.json and .kaddo/graph.mmd").option("--scope <scope>", "Graph scope: active (default) or all").option("--format <format>", "Output format: json, mermaid (default: both)").action((opts) => {
18644
18925
  runGraphExport(opts);
18645
18926
  });
18927
+ var topologyCmd = program.command("topology").description("Validate and apply a semantic system topology proposal (agent proposes, human confirms, Core applies)");
18928
+ topologyCmd.command("validate <file>").description("Validate a topology proposal file deterministically (no write)").action((file) => {
18929
+ runTopologyValidate(file);
18930
+ });
18931
+ topologyCmd.command("apply <file>").description("Apply a validated topology proposal to the canonical artifact after human confirmation").option("-y, --yes", "Skip the confirmation prompt (for already-approved automation)").action((file, opts) => {
18932
+ runTopologyApply(file, opts);
18933
+ });
18646
18934
  var reportCmd = program.command("report").description("Generate Kaddo reports");
18647
18935
  reportCmd.command("impact").description("Knowledge Impact Report: knowledge health, coverage, traceability, readiness (deterministic, no LLM)").option("--json", "Output JSON instead of Markdown").option("--scope <scope>", "Scope: all (default \u2014 accumulated impact) or active").option("--output <path>", "Write the report to a file (e.g. .kaddo/reports/impact-report.md)").action((opts) => {
18648
18936
  runReportImpact(opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/cli",
3
- "version": "3.78.0",
3
+ "version": "3.79.0",
4
4
  "description": "Knowledge Driven Development toolkit",
5
5
  "license": "MIT",
6
6
  "repository": {