@kaddo/cli 3.22.0 → 3.23.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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +393 -39
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -509,6 +509,8 @@ create --from roadmap → owners → guard → explain`.
509
509
  | v3.20 | MCP derived tools: safe regeneration of context/explain/understand/graph/capsule-draft under `.kaddo/` |
510
510
  | v3.21 | Reusable Skills layer (`kaddo add skills`); skills in context/explain/understand, agent prompts and MCP |
511
511
  | v3.22 | Guard & graph scope semantics: graph scope metadata, contextual-empty messaging, Guard ownership scope (active + completed; `--include-archived`) |
512
+ | v3.22.1 | Guard project-root path normalization: matches `code:` globs when the project is a subfolder of the Git root |
513
+ | v3.23 | Knowledge Impact Report: `kaddo report impact` (Markdown/JSON, `--output`); evidence-first health/coverage/traceability/readiness/signals; MCP resource + tool |
512
514
 
513
515
  **Optional modules (installed with `kaddo add`):**
514
516
 
package/dist/index.js CHANGED
@@ -592,8 +592,8 @@ function loadConfig(dir) {
592
592
  const parsed = configSchema.safeParse(raw ?? {});
593
593
  if (!parsed.success) {
594
594
  const issues = parsed.error.issues.map((i) => {
595
- const path5 = i.path.join(".");
596
- return path5 ? ` - ${path5}: ${i.message}` : ` - ${i.message}`;
595
+ const path6 = i.path.join(".");
596
+ return path6 ? ` - ${path6}: ${i.message}` : ` - ${i.message}`;
597
597
  }).join("\n");
598
598
  throw new ConfigError(`Invalid .kaddo/config.yml:
599
599
  ${issues}`);
@@ -684,6 +684,10 @@ var COMMAND_HELP = {
684
684
  "graph export": {
685
685
  question: "How is my project knowledge connected?",
686
686
  next: "Open .kaddo/graph.mmd, or run kaddo explain for a summary"
687
+ },
688
+ "report impact": {
689
+ question: "What impact is Kaddo having on this project?",
690
+ next: "Act on the Suggested Actions section"
687
691
  }
688
692
  };
689
693
  function commandFooterLines(name) {
@@ -4902,6 +4906,14 @@ async function isGitRepo(dir) {
4902
4906
  return false;
4903
4907
  }
4904
4908
  }
4909
+ async function getGitRoot(dir) {
4910
+ try {
4911
+ const result = await execa("git", ["rev-parse", "--show-toplevel"], { cwd: dir });
4912
+ return result.stdout.trim() || null;
4913
+ } catch {
4914
+ return null;
4915
+ }
4916
+ }
4905
4917
  async function getUntrackedFiles() {
4906
4918
  try {
4907
4919
  const result = await execa("git", ["ls-files", "--others", "--exclude-standard"]);
@@ -5048,6 +5060,20 @@ function discoverWorkItems(dir) {
5048
5060
 
5049
5061
  // src/core/diff-analysis.ts
5050
5062
  import { minimatch } from "minimatch";
5063
+ import path3 from "path";
5064
+ function toPosix2(p2) {
5065
+ return p2.replace(/\\/g, "/");
5066
+ }
5067
+ function normalizeTouchedPathForProject(touchedPath, gitRoot, projectRoot) {
5068
+ const t = toPosix2(touchedPath).replace(/^\.\//, "").replace(/^\/+/, "");
5069
+ if (!gitRoot) return t;
5070
+ const prefix = toPosix2(path3.relative(gitRoot, projectRoot)).replace(/^\.\//, "").replace(/\/+$/, "");
5071
+ if (prefix === "" || prefix === ".") return t;
5072
+ if (prefix.startsWith("..")) return t;
5073
+ if (t === prefix) return null;
5074
+ if (t.startsWith(prefix + "/")) return t.slice(prefix.length + 1);
5075
+ return null;
5076
+ }
5051
5077
  function normalizePath(p2) {
5052
5078
  return p2.replace(/\\/g, "/");
5053
5079
  }
@@ -5107,10 +5133,10 @@ function analyzeGuard(touchedFiles, artifacts, silentWithoutOwnership) {
5107
5133
  import { parse as parseYaml4, stringify as stringifyYaml2 } from "yaml";
5108
5134
  var IGNORE_FILE = ".kaddo/ignores.yml";
5109
5135
  function loadIgnores(dir) {
5110
- const path5 = join(dir, IGNORE_FILE);
5111
- if (!exists(path5)) return [];
5136
+ const path6 = join(dir, IGNORE_FILE);
5137
+ if (!exists(path6)) return [];
5112
5138
  try {
5113
- const raw = readFile(path5);
5139
+ const raw = readFile(path6);
5114
5140
  const parsed = parseYaml4(raw);
5115
5141
  return Array.isArray(parsed) ? parsed : [];
5116
5142
  } catch {
@@ -5118,7 +5144,7 @@ function loadIgnores(dir) {
5118
5144
  }
5119
5145
  }
5120
5146
  function saveIgnore(dir, entry) {
5121
- const path5 = join(dir, IGNORE_FILE);
5147
+ const path6 = join(dir, IGNORE_FILE);
5122
5148
  const existing = loadIgnores(dir);
5123
5149
  const idx = existing.findIndex((e) => e.artifact_id === entry.artifact_id);
5124
5150
  if (idx >= 0) {
@@ -5126,17 +5152,17 @@ function saveIgnore(dir, entry) {
5126
5152
  } else {
5127
5153
  existing.push(entry);
5128
5154
  }
5129
- writeFile(path5, stringifyYaml2(existing));
5155
+ writeFile(path6, stringifyYaml2(existing));
5130
5156
  }
5131
5157
  function isIgnored(ignores, artifactId) {
5132
5158
  return ignores.find((e) => e.artifact_id === artifactId);
5133
5159
  }
5134
5160
  function removeIgnore(dir, artifactId) {
5135
- const path5 = join(dir, IGNORE_FILE);
5161
+ const path6 = join(dir, IGNORE_FILE);
5136
5162
  const existing = loadIgnores(dir);
5137
5163
  const filtered = existing.filter((e) => e.artifact_id !== artifactId);
5138
5164
  if (filtered.length === existing.length) return false;
5139
- writeFile(path5, stringifyYaml2(filtered));
5165
+ writeFile(path6, stringifyYaml2(filtered));
5140
5166
  return true;
5141
5167
  }
5142
5168
 
@@ -5158,11 +5184,11 @@ function moduleArtifactCoverage(dir, id) {
5158
5184
  };
5159
5185
  }
5160
5186
  function loadMappedModules(dir) {
5161
- const path5 = join(dir, DESCRIPTOR_PATH);
5162
- if (!exists(path5)) return [];
5187
+ const path6 = join(dir, DESCRIPTOR_PATH);
5188
+ if (!exists(path6)) return [];
5163
5189
  let parsed;
5164
5190
  try {
5165
- parsed = parseYaml5(readFile(path5));
5191
+ parsed = parseYaml5(readFile(path6));
5166
5192
  } catch {
5167
5193
  return [];
5168
5194
  }
@@ -5254,7 +5280,7 @@ async function collectWorkspaceChanges(dir, mode = "head", deps = defaultDeps) {
5254
5280
  }
5255
5281
 
5256
5282
  // src/commands/guard.ts
5257
- import path3 from "path";
5283
+ import path4 from "path";
5258
5284
  import { parse as parseYaml7 } from "yaml";
5259
5285
 
5260
5286
  // src/plugins/prisma.ts
@@ -5458,7 +5484,7 @@ function printHeader(touchedFiles) {
5458
5484
  function artifactLabel(artifact, dir) {
5459
5485
  if (artifact.id) return artifact.id;
5460
5486
  if (artifact.title) return artifact.title;
5461
- const rel = path3.relative(dir, artifact.filePath).split(path3.sep).join("/");
5487
+ const rel = path4.relative(dir, artifact.filePath).split(path4.sep).join("/");
5462
5488
  return rel || artifact.filePath;
5463
5489
  }
5464
5490
  function printWorkspaceHeader(scan2) {
@@ -5524,7 +5550,7 @@ async function offerIgnore(dir, match) {
5524
5550
  log2.success(`${id} added to .kaddo/ignores.yml`);
5525
5551
  return true;
5526
5552
  }
5527
- function printCIJson(dir, touchedFiles, activeMatches, ignoredCount, pluginSignals, affectedOwners, workspace, includeArchived) {
5553
+ function printCIJson(dir, touchedFiles, activeMatches, ignoredCount, pluginSignals, affectedOwners, workspace, includeArchived, normalizedFiles, outsideProjectFiles) {
5528
5554
  const output = {
5529
5555
  kaddo_guard: true,
5530
5556
  ci: true,
@@ -5532,6 +5558,9 @@ function printCIJson(dir, touchedFiles, activeMatches, ignoredCount, pluginSigna
5532
5558
  included: includeArchived ? ["draft", "ready", "in-progress", "blocked", "completed", "archived"] : ["draft", "ready", "in-progress", "blocked", "completed"],
5533
5559
  excluded: includeArchived ? [] : ["archived"]
5534
5560
  },
5561
+ // Git-root-relative → project-root-relative mapping used for matching (VS-060.1).
5562
+ normalized_files: normalizedFiles,
5563
+ files_outside_project: outsideProjectFiles,
5535
5564
  touched_files: touchedFiles.length,
5536
5565
  fyi_count: activeMatches.length,
5537
5566
  ignored_count: ignoredCount,
@@ -5580,8 +5609,16 @@ async function runGuard(opts = {}) {
5580
5609
  const ignores = loadIgnores(dir);
5581
5610
  const plugins = resolvePlugins(config.plugins ?? []);
5582
5611
  const mode = opts.staged ? "staged" : "head";
5583
- const currentFiles = await getModifiedFiles(mode);
5584
- const untrackedFiles = opts.staged ? [] : await getUntrackedFiles();
5612
+ const currentFilesRaw = await getModifiedFiles(mode);
5613
+ const gitRoot = await getGitRoot(dir);
5614
+ const currentPairs = currentFilesRaw.map((raw) => ({
5615
+ raw,
5616
+ project: normalizeTouchedPathForProject(raw, gitRoot, dir)
5617
+ }));
5618
+ const inProjectPairs = currentPairs.filter((p2) => p2.project !== null);
5619
+ const currentFiles = inProjectPairs.map((p2) => p2.project);
5620
+ const outsideProjectFiles = currentPairs.filter((p2) => p2.project === null).map((p2) => p2.raw);
5621
+ const untrackedFiles = opts.staged ? [] : (await getUntrackedFiles()).map((f) => normalizeTouchedPathForProject(f, gitRoot, dir)).filter((f) => f !== null);
5585
5622
  const printUntracked = () => {
5586
5623
  if (untrackedFiles.length === 0 || jsonMode) return;
5587
5624
  console.log("");
@@ -5596,6 +5633,7 @@ async function runGuard(opts = {}) {
5596
5633
  }
5597
5634
  const workspaceFiles = workspaceScan ? workspaceScan.changedFiles.map((c) => c.normalizedPath) : [];
5598
5635
  const touchedFiles = [.../* @__PURE__ */ new Set([...currentFiles, ...workspaceFiles])];
5636
+ const displayTouched = [.../* @__PURE__ */ new Set([...inProjectPairs.map((p2) => p2.raw), ...workspaceFiles])];
5599
5637
  if (touchedFiles.length === 0) {
5600
5638
  if (workspaceScan) printWorkspaceHeader(workspaceScan);
5601
5639
  console.log("kaddo guard: no modified files detected.");
@@ -5631,7 +5669,7 @@ async function runGuard(opts = {}) {
5631
5669
  const ownerMapCI = loadOwners(dir);
5632
5670
  const matchedDomainsCI = collectMatchedDomains(activeMatches.map((m) => m.artifact.domains));
5633
5671
  const affectedOwnersCI = resolveAffectedOwners(matchedDomainsCI, ownerMapCI);
5634
- printCIJson(dir, touchedFiles, activeMatches, alreadyIgnoredMatches.length, pluginSignals, affectedOwnersCI, workspaceScan, includeArchived);
5672
+ printCIJson(dir, touchedFiles, activeMatches, alreadyIgnoredMatches.length, pluginSignals, affectedOwnersCI, workspaceScan, includeArchived, inProjectPairs.map((p2) => ({ raw_path: p2.raw, project_path: p2.project })), outsideProjectFiles);
5635
5673
  return;
5636
5674
  }
5637
5675
  if (result.silenced && pluginSignals.length === 0) {
@@ -5640,7 +5678,7 @@ async function runGuard(opts = {}) {
5640
5678
  }
5641
5679
  if (result.matches.length === 0 && pluginSignals.length === 0) {
5642
5680
  if (workspaceScan) printWorkspaceHeader(workspaceScan);
5643
- printHeader(touchedFiles);
5681
+ printHeader(displayTouched);
5644
5682
  printOwnershipScope(includeArchived);
5645
5683
  console.log(" No artifact ownership matches found.");
5646
5684
  console.log("");
@@ -5651,7 +5689,7 @@ async function runGuard(opts = {}) {
5651
5689
  return;
5652
5690
  }
5653
5691
  if (workspaceScan) printWorkspaceHeader(workspaceScan);
5654
- printHeader(touchedFiles);
5692
+ printHeader(displayTouched);
5655
5693
  printOwnershipScope(includeArchived);
5656
5694
  printUntracked();
5657
5695
  for (const match of activeMatches) {
@@ -5999,7 +6037,7 @@ function sectionParagraph(md, title) {
5999
6037
  }
6000
6038
  return "";
6001
6039
  }
6002
- function parseCapsule(id, path5, md) {
6040
+ function parseCapsule(id, path6, md) {
6003
6041
  const { data } = matter2(md);
6004
6042
  const updatedAt = data.updated_at ? String(data.updated_at) : void 0;
6005
6043
  let ageDays = null;
@@ -6009,7 +6047,7 @@ function parseCapsule(id, path5, md) {
6009
6047
  }
6010
6048
  return {
6011
6049
  id,
6012
- path: path5,
6050
+ path: path6,
6013
6051
  system: data.system ? String(data.system) : id,
6014
6052
  owner: data.owner ? String(data.owner) : void 0,
6015
6053
  updatedAt,
@@ -6064,14 +6102,14 @@ var ALL_STATUSES = ["draft", "ready", "in-progress", "blocked", "completed"];
6064
6102
  function scopeStatuses(scope) {
6065
6103
  return scope === "all" ? { included: ALL_STATUSES, excluded: ["archived"] } : { included: ACTIVE_STATUSES, excluded: ["completed", "archived"] };
6066
6104
  }
6067
- function toPosix2(p2) {
6105
+ function toPosix3(p2) {
6068
6106
  return p2.replace(/\\/g, "/");
6069
6107
  }
6070
6108
  function slug(s) {
6071
6109
  return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
6072
6110
  }
6073
6111
  function isAdr(a) {
6074
- return toPosix2(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
6112
+ return toPosix3(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
6075
6113
  }
6076
6114
  function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
6077
6115
  const scope = opts.scope ?? "active";
@@ -6095,9 +6133,9 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
6095
6133
  ];
6096
6134
  const presentLayers = [];
6097
6135
  for (const layer2 of layerDocs) {
6098
- const path5 = layer2.files.map((f) => `${KNOWLEDGE2}/${f}`).find((rel) => exists(join(dir, rel)));
6099
- if (path5) {
6100
- addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path5 });
6136
+ const path6 = layer2.files.map((f) => `${KNOWLEDGE2}/${f}`).find((rel) => exists(join(dir, rel)));
6137
+ if (path6) {
6138
+ addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path6 });
6101
6139
  presentLayers.push(layer2.id);
6102
6140
  }
6103
6141
  }
@@ -6256,14 +6294,14 @@ function loadGraphSummary(dir) {
6256
6294
 
6257
6295
  // src/core/graph-hints.ts
6258
6296
  var KNOWLEDGE3 = "knowledge";
6259
- function toPosix3(p2) {
6297
+ function toPosix4(p2) {
6260
6298
  return p2.replace(/\\/g, "/");
6261
6299
  }
6262
6300
  function slug2(s) {
6263
6301
  return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
6264
6302
  }
6265
6303
  function isAdr2(a) {
6266
- return toPosix3(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
6304
+ return toPosix4(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
6267
6305
  }
6268
6306
  function capabilityHeadings(dir) {
6269
6307
  const p2 = join(dir, KNOWLEDGE3, "product", "capabilities.md");
@@ -8007,7 +8045,7 @@ function runUnderstand() {
8007
8045
  }
8008
8046
 
8009
8047
  // src/core/classifier.ts
8010
- import path4 from "path";
8048
+ import path5 from "path";
8011
8049
  var MIGRATION_PATTERNS = [
8012
8050
  /supabase\/migrations\//,
8013
8051
  /prisma\/migrations\//,
@@ -8046,7 +8084,7 @@ var DEP_PATTERNS = [
8046
8084
  /^pom\.xml$/
8047
8085
  ];
8048
8086
  function normalizePath2(p2) {
8049
- return p2.split(path4.sep).join("/");
8087
+ return p2.split(path5.sep).join("/");
8050
8088
  }
8051
8089
  function matchesAny(file, patterns) {
8052
8090
  const normalized = normalizePath2(file);
@@ -8878,10 +8916,10 @@ ${globs.map((g) => ` - ${g}`).join("\n")}`);
8878
8916
  import { parse as parseYaml13, stringify as stringifyYaml5 } from "yaml";
8879
8917
  var DESCRIPTOR_PATH2 = "knowledge/module.yml";
8880
8918
  function readDescriptor(dir) {
8881
- const path5 = join(dir, DESCRIPTOR_PATH2);
8882
- if (!exists(path5)) return null;
8919
+ const path6 = join(dir, DESCRIPTOR_PATH2);
8920
+ if (!exists(path6)) return null;
8883
8921
  try {
8884
- return parseYaml13(readFile(path5));
8922
+ return parseYaml13(readFile(path6));
8885
8923
  } catch {
8886
8924
  return null;
8887
8925
  }
@@ -10486,10 +10524,10 @@ function slugify3(name) {
10486
10524
  return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
10487
10525
  }
10488
10526
  function readModulesDescriptor(dir) {
10489
- const path5 = join(dir, DESCRIPTOR_PATH3);
10490
- if (!exists(path5)) return { version: 1, modules: [] };
10527
+ const path6 = join(dir, DESCRIPTOR_PATH3);
10528
+ if (!exists(path6)) return { version: 1, modules: [] };
10491
10529
  try {
10492
- const parsed = parseYaml14(readFile(path5));
10530
+ const parsed = parseYaml14(readFile(path6));
10493
10531
  return { version: parsed.version ?? 1, modules: parsed.modules ?? [] };
10494
10532
  } catch {
10495
10533
  return { version: 1, modules: [] };
@@ -10878,6 +10916,315 @@ function runGraphExport(opts = {}) {
10878
10916
  outro2("Knowledge graph ready.");
10879
10917
  }
10880
10918
 
10919
+ // src/core/impact-report.ts
10920
+ import matter8 from "gray-matter";
10921
+ var BROAD_GLOB = /^[^/*]+\/\*\*$/;
10922
+ function levelFromRatio(r) {
10923
+ if (r >= 0.9) return "Very High";
10924
+ if (r >= 0.7) return "High";
10925
+ if (r >= 0.4) return "Medium";
10926
+ return "Low";
10927
+ }
10928
+ function hasSection(body, re) {
10929
+ return body.split(/\r?\n/).some((l) => /^#{1,6}\s+/.test(l) && re.test(l));
10930
+ }
10931
+ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
10932
+ const exp = buildProjectExplanation(dir);
10933
+ const wis = discoverWorkItems(dir);
10934
+ const total = wis.length;
10935
+ let withOwnership = 0;
10936
+ let withSource = 0;
10937
+ let withInitiative = 0;
10938
+ let withLevel = 0;
10939
+ let withAcceptance = 0;
10940
+ let withDoD = 0;
10941
+ let connectedRoadmap = 0;
10942
+ const globCounts = /* @__PURE__ */ new Map();
10943
+ for (const wi of wis) {
10944
+ if (wi.codeGlobs.length > 0) withOwnership++;
10945
+ const hasSource = Boolean(wi.sourceId) || wi.source === "roadmap";
10946
+ if (hasSource) withSource++;
10947
+ if (hasSource || wi.initiative) connectedRoadmap++;
10948
+ if (wi.initiative) withInitiative++;
10949
+ if (wi.knowledgeLevel) withLevel++;
10950
+ for (const g2 of wi.codeGlobs) globCounts.set(g2, (globCounts.get(g2) ?? 0) + 1);
10951
+ try {
10952
+ const body = matter8(readFile(wi.filePath)).content;
10953
+ if (hasSection(body, /acceptance/i)) withAcceptance++;
10954
+ if (hasSection(body, /definition of done|^#{1,6}\s*done\b/i)) withDoD++;
10955
+ } catch {
10956
+ }
10957
+ }
10958
+ const ownedCodePaths = [...globCounts.values()].reduce((a, b) => a + b, 0);
10959
+ const broadGlobs = [...globCounts.keys()].filter((g2) => BROAD_GLOB.test(g2)).length;
10960
+ const ownershipOverlaps = [...globCounts.values()].filter((n) => n > 1).length;
10961
+ let g;
10962
+ if (opts.scope) {
10963
+ const config = loadConfig(dir);
10964
+ if (config) {
10965
+ const graph = buildGraph(dir, config, { scope: opts.scope }, now);
10966
+ const hints = buildGraphHints(dir, graph, now);
10967
+ g = { available: true, scope: graph.scope, scopeReason: graph.scope_reason, nodes: graph.nodes.length, edges: graph.edges.length, quality: hints.quality, hints: hints.summary.hints, generatedAt: graph.generated_at };
10968
+ } else {
10969
+ g = { available: false, scope: opts.scope, scopeReason: "", nodes: 0, edges: 0, quality: "unknown", hints: 0, generatedAt: "" };
10970
+ }
10971
+ } else if (exp.graph) {
10972
+ g = { available: true, scope: exp.graph.scope, scopeReason: exp.graph.scopeReason, nodes: exp.graph.nodes, edges: exp.graph.edges, quality: exp.graphHints?.quality ?? "unknown", hints: exp.graphHints?.totalHints ?? 0, generatedAt: exp.graph.generatedAt };
10973
+ } else {
10974
+ g = { available: false, scope: "n/a", scopeReason: "", nodes: 0, edges: 0, quality: "unknown", hints: 0, generatedAt: "" };
10975
+ }
10976
+ const layerStatus = (name) => exp.layers.find((l) => l.layer === name)?.status ?? "Missing";
10977
+ const byState = exp.workItems.byState;
10978
+ const skills = discoverInstalledSkills(dir);
10979
+ const knowledge_health = {
10980
+ Business: layerStatus("Business"),
10981
+ Product: layerStatus("Product"),
10982
+ Tech: layerStatus("Tech"),
10983
+ Delivery: layerStatus("Delivery"),
10984
+ Inventory: exp.knowledge.hasInventory ? "available" : "missing",
10985
+ "Context pack": exp.knowledge.hasContextPack ? "available" : "missing",
10986
+ Agents: exp.knowledge.hasAgents ? "available" : "missing",
10987
+ Skills: skills.length > 0 ? "available" : "missing"
10988
+ };
10989
+ const knowledge_coverage = [
10990
+ { label: "Work Items with ownership", have: withOwnership, total },
10991
+ { label: "Work Items with source", have: withSource, total },
10992
+ { label: "Work Items with initiative", have: withInitiative, total },
10993
+ { label: "Work Items with acceptance criteria", have: withAcceptance, total },
10994
+ { label: "Work Items with Definition of Done", have: withDoD, total },
10995
+ { label: "Work Items with knowledge level", have: withLevel, total }
10996
+ ];
10997
+ const coverageRatio = (have) => total > 0 ? have / total : 0;
10998
+ const deliveryTraceable = ["Traceable", "Structured", "Consolidated"].includes(layerStatus("Delivery"));
10999
+ const ctxReasons = [];
11000
+ let ctxScore = 0;
11001
+ if (["Consolidated", "Structured"].includes(layerStatus("Business"))) {
11002
+ ctxScore++;
11003
+ ctxReasons.push("Business knowledge is present.");
11004
+ }
11005
+ if (["Consolidated", "Structured"].includes(layerStatus("Product"))) {
11006
+ ctxScore++;
11007
+ ctxReasons.push("Product knowledge is present.");
11008
+ }
11009
+ if (layerStatus("Tech") !== "Missing") {
11010
+ ctxScore++;
11011
+ ctxReasons.push("Tech knowledge is structured.");
11012
+ }
11013
+ if (deliveryTraceable) {
11014
+ ctxScore++;
11015
+ ctxReasons.push("Delivery is traceable.");
11016
+ }
11017
+ if (exp.knowledge.hasContextPack) {
11018
+ ctxScore++;
11019
+ ctxReasons.push("Context pack is available.");
11020
+ }
11021
+ if (g.available && g.quality !== "empty") {
11022
+ ctxScore++;
11023
+ ctxReasons.push(`Graph quality is ${g.quality}.`);
11024
+ }
11025
+ const ctxLevel = ctxScore >= 6 ? "Very High" : ctxScore >= 4 ? "High" : ctxScore >= 2 ? "Medium" : "Low";
11026
+ const ambiguityRatio = total > 0 ? (withAcceptance + withDoD + withSource) / (3 * total) : 0;
11027
+ const aiReady = exp.knowledge.hasContextPack && g.available && g.quality !== "empty" && skills.length > 0 && deliveryTraceable ? "High" : exp.knowledge.hasContextPack && g.available && g.quality !== "empty" ? "Medium" : "Low";
11028
+ const onboarding = levelFromRatio(
11029
+ [layerStatus("Business"), layerStatus("Product"), layerStatus("Tech"), layerStatus("Delivery")].filter((s) => s !== "Missing").length / 4 * (exp.knowledge.hasContextPack ? 1 : 0.6)
11030
+ );
11031
+ const deliveryTrace = levelFromRatio(
11032
+ coverageRatio(withOwnership) * 0.5 + (g.available && g.edges > 0 ? 1 : 0) * 0.25 + (exp.roadmap.present ? 1 : 0) * 0.25
11033
+ );
11034
+ const impact_signals = {
11035
+ ambiguity_reduction: levelFromRatio(ambiguityRatio),
11036
+ drift_prevention: withOwnership > 0 ? "Active" : "Limited",
11037
+ onboarding_support: onboarding,
11038
+ delivery_traceability: deliveryTrace,
11039
+ ai_context_readiness: aiReady,
11040
+ maintenance_readiness: levelFromRatio((coverageRatio(withOwnership) + (deliveryTraceable ? 1 : 0)) / 2)
11041
+ };
11042
+ let score = null;
11043
+ if (total > 0) {
11044
+ const healthPts = [layerStatus("Business"), layerStatus("Product"), layerStatus("Tech"), layerStatus("Delivery")].filter((s) => s !== "Missing").length / 4 * 20;
11045
+ const covAvg = knowledge_coverage.reduce((a, c) => a + coverageRatio(c.have), 0) / knowledge_coverage.length;
11046
+ const coveragePts = covAvg * 20;
11047
+ const ownPts = coverageRatio(withOwnership) * 15;
11048
+ const traceBlend = (exp.roadmap.candidates > 0 ? exp.roadmap.materialized / exp.roadmap.candidates : 1) * 0.5 + coverageRatio(withOwnership) * 0.5;
11049
+ const tracePts = traceBlend * 20;
11050
+ const qualityMap = { good: 1, partial: 0.6, sparse: 0.3, empty: 0, unknown: 0 };
11051
+ const graphPts = (g.available ? qualityMap[g.quality] ?? 0 : 0) * 15;
11052
+ const ctxPts = { Low: 0.25, Medium: 0.5, High: 0.8, "Very High": 1 }[ctxLevel] * 10;
11053
+ score = Math.round(healthPts + coveragePts + ownPts + tracePts + graphPts + ctxPts);
11054
+ }
11055
+ const actions = [];
11056
+ if (!g.available) actions.push("Run `kaddo graph export --scope all` to inspect full traceability.");
11057
+ else if (g.scope === "active" && g.quality === "empty") actions.push("Run `kaddo graph export --scope all` to include completed Work Items.");
11058
+ if (exp.roadmap.remaining > 0) actions.push(`Materialize the ${exp.roadmap.remaining} remaining roadmap candidate(s) with \`kaddo create --from roadmap\`.`);
11059
+ if (total > 0 && withOwnership < total) actions.push("Add code ownership to Work Items without `code:` globs (`kaddo owners suggest`).");
11060
+ if (total > 0 && withInitiative < total) actions.push("Add an initiative to Work Items missing one.");
11061
+ if (g.available && g.hints > 0) actions.push("Review graph hints (`kaddo://graph-hints` or `.kaddo/graph-hints.md`).");
11062
+ if (skills.length === 0) actions.push("Install reusable skills with `kaddo add skills`.");
11063
+ actions.push("Run `kaddo guard` before committing to catch knowledge drift.");
11064
+ const summary = [];
11065
+ if (total > 0) summary.push(`${withOwnership}/${total} Work Items include code ownership.`);
11066
+ if (g.available) summary.push(`The knowledge graph is ${g.quality} with ${g.nodes} nodes and ${g.edges} edges (${g.scope} scope).`);
11067
+ else summary.push("The knowledge graph has not been exported yet.");
11068
+ if (g.available) summary.push(g.hints === 0 ? "No graph hints are currently open." : `${g.hints} graph hint(s) are open.`);
11069
+ if (exp.roadmap.present) summary.push(`Delivery: ${exp.roadmap.materialized} materialized, ${exp.roadmap.remaining} roadmap candidate(s) remaining.`);
11070
+ summary.push(`Context readiness: ${ctxLevel}.`);
11071
+ return {
11072
+ generated_at: now.toISOString(),
11073
+ project: exp.project.name,
11074
+ scope: g.scope,
11075
+ executive_summary: summary,
11076
+ knowledge_health,
11077
+ knowledge_coverage,
11078
+ ownership_coverage: {
11079
+ coverage_percent: total > 0 ? Math.round(withOwnership / total * 100) : 0,
11080
+ work_items_with_ownership: `${withOwnership}/${total}`,
11081
+ owned_code_paths: ownedCodePaths,
11082
+ broad_globs: broadGlobs,
11083
+ ownership_overlaps: ownershipOverlaps
11084
+ },
11085
+ traceability: {
11086
+ roadmap_candidates: exp.roadmap.candidates,
11087
+ materialized_work_items: exp.roadmap.materialized,
11088
+ remaining_candidates: exp.roadmap.remaining,
11089
+ completed_work_items: byState.completed,
11090
+ work_items_connected_to_roadmap: `${connectedRoadmap}/${total}`,
11091
+ work_items_connected_to_code: `${withOwnership}/${total}`,
11092
+ graph_quality: g.available ? g.quality : "not available",
11093
+ graph_nodes: g.available ? g.nodes : null,
11094
+ graph_edges: g.available ? g.edges : null,
11095
+ graph_hints: g.available ? g.hints : null
11096
+ },
11097
+ context_readiness: { level: ctxLevel, reasons: ctxReasons },
11098
+ work_item_readiness: {
11099
+ draft: byState.draft,
11100
+ ready: byState.ready,
11101
+ in_progress: byState["in-progress"],
11102
+ blocked: byState.blocked,
11103
+ completed: byState.completed,
11104
+ ready_quality: byState.ready > 0 ? `${byState.ready} ready` : "not applicable"
11105
+ },
11106
+ graph_quality: g.available ? { available: true, scope: g.scope, quality: g.quality, nodes: g.nodes, edges: g.edges, hints: g.hints, reason: g.scopeReason, last_exported: g.generatedAt } : { available: false, suggestion: "Run `kaddo graph export --scope all`." },
11107
+ guard_activity: {
11108
+ available: false,
11109
+ note: "Guard history is not persisted. Run `kaddo guard` to check drift; future versions may store runs for trend analysis."
11110
+ },
11111
+ impact_signals,
11112
+ score,
11113
+ suggested_actions: actions
11114
+ };
11115
+ }
11116
+ function ratio(have, total) {
11117
+ return `${have}/${total}`;
11118
+ }
11119
+ function renderImpactMarkdown(r) {
11120
+ const L = [];
11121
+ L.push("# Kaddo Knowledge Impact Report", "");
11122
+ L.push(`Generated at: ${r.generated_at}`);
11123
+ L.push(`Project: ${r.project}`);
11124
+ L.push(`Scope: ${r.scope}`);
11125
+ if (r.score !== null) L.push(`Knowledge Impact Score: ${r.score}/100`);
11126
+ else L.push("Knowledge Impact Score: not available");
11127
+ L.push("");
11128
+ L.push("## Executive Summary", "");
11129
+ for (const s2 of r.executive_summary) L.push(`- ${s2}`);
11130
+ L.push("");
11131
+ L.push("## Knowledge Health", "");
11132
+ for (const [k, v] of Object.entries(r.knowledge_health)) L.push(`- ${k}: ${v}`);
11133
+ L.push("");
11134
+ L.push("## Knowledge Coverage", "");
11135
+ for (const c of r.knowledge_coverage) L.push(`- ${c.label}: ${ratio(c.have, c.total)}`);
11136
+ L.push("");
11137
+ L.push("## Ownership Coverage", "");
11138
+ L.push(`- Coverage: ${r.ownership_coverage.coverage_percent}%`);
11139
+ L.push(`- Work Items with code ownership: ${r.ownership_coverage.work_items_with_ownership}`);
11140
+ L.push(`- Owned code paths: ${r.ownership_coverage.owned_code_paths}`);
11141
+ L.push(`- Broad globs detected: ${r.ownership_coverage.broad_globs}`);
11142
+ L.push(`- Ownership overlaps: ${r.ownership_coverage.ownership_overlaps}`);
11143
+ L.push("");
11144
+ L.push("## Traceability", "");
11145
+ const t = r.traceability;
11146
+ L.push(`- Roadmap candidates: ${t.roadmap_candidates}`);
11147
+ L.push(`- Materialized Work Items: ${t.materialized_work_items}`);
11148
+ L.push(`- Remaining candidates: ${t.remaining_candidates}`);
11149
+ L.push(`- Completed Work Items: ${t.completed_work_items}`);
11150
+ L.push(`- Work Items connected to roadmap: ${t.work_items_connected_to_roadmap}`);
11151
+ L.push(`- Work Items connected to code: ${t.work_items_connected_to_code}`);
11152
+ L.push(`- Graph quality: ${t.graph_quality}`);
11153
+ if (t.graph_nodes !== null) L.push(`- Graph nodes: ${t.graph_nodes}`);
11154
+ if (t.graph_edges !== null) L.push(`- Graph edges: ${t.graph_edges}`);
11155
+ if (t.graph_hints !== null) L.push(`- Graph hints: ${t.graph_hints}`);
11156
+ L.push("");
11157
+ L.push("## Context Readiness", "");
11158
+ L.push(`Context Readiness: ${r.context_readiness.level}`);
11159
+ if (r.context_readiness.reasons.length > 0) {
11160
+ L.push("", "Reason:");
11161
+ for (const reason of r.context_readiness.reasons) L.push(`- ${reason}`);
11162
+ }
11163
+ L.push("");
11164
+ L.push("## Work Item Readiness", "");
11165
+ const w = r.work_item_readiness;
11166
+ L.push(`- Draft: ${w.draft}`);
11167
+ L.push(`- Ready: ${w.ready}`);
11168
+ L.push(`- In Progress: ${w.in_progress}`);
11169
+ L.push(`- Blocked: ${w.blocked}`);
11170
+ L.push(`- Completed: ${w.completed}`);
11171
+ L.push(`- Ready quality: ${w.ready_quality}`);
11172
+ L.push("");
11173
+ L.push("## Graph Quality", "");
11174
+ if (r.graph_quality.available) {
11175
+ const gq = r.graph_quality;
11176
+ L.push(`- Scope: ${gq.scope}`);
11177
+ L.push(`- Quality: ${gq.quality}`);
11178
+ L.push(`- Nodes: ${gq.nodes}`);
11179
+ L.push(`- Edges: ${gq.edges}`);
11180
+ L.push(`- Hints: ${gq.hints}`);
11181
+ if (gq.reason) L.push(`- Reason: ${gq.reason}`);
11182
+ if (gq.last_exported) L.push(`- Last exported: ${gq.last_exported}`);
11183
+ } else {
11184
+ L.push("- Graph data not available.");
11185
+ L.push(`- Tip: ${r.graph_quality.suggestion}`);
11186
+ }
11187
+ L.push("");
11188
+ L.push("## Guard Activity", "");
11189
+ L.push("- Guard history: not available");
11190
+ L.push(`- Note: ${r.guard_activity.note}`);
11191
+ L.push("");
11192
+ L.push("## Impact Signals", "");
11193
+ const s = r.impact_signals;
11194
+ L.push(`- Ambiguity reduction: ${s.ambiguity_reduction}`);
11195
+ L.push(`- Drift prevention: ${s.drift_prevention}`);
11196
+ L.push(`- Onboarding support: ${s.onboarding_support}`);
11197
+ L.push(`- Delivery traceability: ${s.delivery_traceability}`);
11198
+ L.push(`- AI context readiness: ${s.ai_context_readiness}`);
11199
+ L.push(`- Maintenance readiness: ${s.maintenance_readiness}`);
11200
+ L.push("");
11201
+ L.push("## Suggested Actions", "");
11202
+ r.suggested_actions.forEach((a, i) => L.push(`${i + 1}. ${a}`));
11203
+ L.push("");
11204
+ return L.join("\n");
11205
+ }
11206
+ function serializeImpactJson(r) {
11207
+ return JSON.stringify(r, null, 2) + "\n";
11208
+ }
11209
+
11210
+ // src/commands/report.ts
11211
+ function runReportImpact(opts = {}) {
11212
+ const dir = cwd();
11213
+ requireConfig(dir);
11214
+ const report = buildImpactReport(dir);
11215
+ const content = opts.json ? serializeImpactJson(report) : renderImpactMarkdown(report);
11216
+ if (opts.output) {
11217
+ intro2("kaddo report impact");
11218
+ const rel = opts.output;
11219
+ writeFile(join(dir, rel), content);
11220
+ log2.success(`Wrote ${rel.replace(/\\/g, "/")}`);
11221
+ printCommandFooter("report impact");
11222
+ outro2("Impact report written.");
11223
+ return;
11224
+ }
11225
+ console.log(content);
11226
+ }
11227
+
10881
11228
  // src/index.ts
10882
11229
  var require2 = createRequire(import.meta.url);
10883
11230
  var { version } = require2("../package.json");
@@ -10899,13 +11246,20 @@ var capsuleCmd = program.command("capsule").description("Export this project as
10899
11246
  capsuleCmd.command("export").description("Write a Knowledge Capsule about this project to .kaddo/exports/").action(() => {
10900
11247
  runCapsuleExport();
10901
11248
  });
10902
- capsuleCmd.command("add <path>").description("Register an external Knowledge Capsule as project context (.kaddo/external.yml)").action((path5) => {
10903
- runCapsuleAdd(path5);
11249
+ capsuleCmd.command("add <path>").description("Register an external Knowledge Capsule as project context (.kaddo/external.yml)").action((path6) => {
11250
+ runCapsuleAdd(path6);
10904
11251
  });
10905
11252
  var graphCmd = program.command("graph").description("Export the lightweight, file-based knowledge graph of the project");
10906
11253
  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) => {
10907
11254
  runGraphExport(opts);
10908
11255
  });
11256
+ var reportCmd = program.command("report").description("Generate Kaddo reports");
11257
+ reportCmd.command("impact").description("Knowledge Impact Report: knowledge health, coverage, traceability, readiness (deterministic, no LLM)").option("--json", "Output JSON instead of Markdown").option("--output <path>", "Write the report to a file (e.g. .kaddo/reports/impact-report.md)").action((opts) => {
11258
+ runReportImpact(opts);
11259
+ });
11260
+ program.command("impact").description("Alias for `kaddo report impact`").option("--json", "Output JSON instead of Markdown").option("--output <path>", "Write the report to a file").action((opts) => {
11261
+ runReportImpact(opts);
11262
+ });
10909
11263
  program.command("guard").description("Check if modified code has related artifacts that were not updated").option("--staged", "Check only staged files").option("--no-interactive", "Disable interactive ignore prompts").option("--ci", "CI mode: output JSON, no prompts, non-blocking").option("--json", "Output JSON (alias for --ci)").option("--workspace", "Also check local mapped module repos from .kaddo/modules.yml (opt-in)").option("--include-archived", "Include archived Work Items in ownership matching (excluded by default)").action(async (opts) => {
10910
11264
  await runGuard(opts);
10911
11265
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/cli",
3
- "version": "3.22.0",
3
+ "version": "3.23.0",
4
4
  "description": "Knowledge Driven Development toolkit",
5
5
  "license": "MIT",
6
6
  "repository": {