@kaddo/cli 3.23.0 → 3.23.2

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 +151 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -511,6 +511,8 @@ create --from roadmap → owners → guard → explain`.
511
511
  | v3.22 | Guard & graph scope semantics: graph scope metadata, contextual-empty messaging, Guard ownership scope (active + completed; `--include-archived`) |
512
512
  | v3.22.1 | Guard project-root path normalization: matches `code:` globs when the project is a subfolder of the Git root |
513
513
  | v3.23 | Knowledge Impact Report: `kaddo report impact` (Markdown/JSON, `--output`); evidence-first health/coverage/traceability/readiness/signals; MCP resource + tool |
514
+ | v3.23.1 | Impact report Actionable Gaps: per-Work-Item missing source/initiative/ownership/level/acceptance/DoD/validation, broad globs, overlaps; Work-Item-specific Suggested Actions + Score Breakdown |
515
+ | v3.23.2 | Impact report defaults to `all` scope (accumulated impact), graph built in memory; `--scope active`; `scope_source`/`default_scope` in JSON |
514
516
 
515
517
  **Optional modules (installed with `kaddo add`):**
516
518
 
package/dist/index.js CHANGED
@@ -10918,7 +10918,11 @@ function runGraphExport(opts = {}) {
10918
10918
 
10919
10919
  // src/core/impact-report.ts
10920
10920
  import matter8 from "gray-matter";
10921
- var BROAD_GLOB = /^[^/*]+\/\*\*$/;
10921
+ function isBroadGlob(glob) {
10922
+ if (!glob.endsWith("/**")) return false;
10923
+ const prefix = glob.slice(0, -3);
10924
+ return prefix.length > 0 && prefix.split("/").length <= 2;
10925
+ }
10922
10926
  function levelFromRatio(r) {
10923
10927
  if (r >= 0.9) return "Very High";
10924
10928
  if (r >= 0.7) return "High";
@@ -10929,6 +10933,8 @@ function hasSection(body, re) {
10929
10933
  return body.split(/\r?\n/).some((l) => /^#{1,6}\s+/.test(l) && re.test(l));
10930
10934
  }
10931
10935
  function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
10936
+ const resolvedScope = opts.scope ?? "all";
10937
+ const scopeSource = opts.scopeSource ?? (opts.scope ? "explicit" : "default");
10932
10938
  const exp = buildProjectExplanation(dir);
10933
10939
  const wis = discoverWorkItems(dir);
10934
10940
  const total = wis.length;
@@ -10938,40 +10944,74 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
10938
10944
  let withLevel = 0;
10939
10945
  let withAcceptance = 0;
10940
10946
  let withDoD = 0;
10947
+ let withValidation = 0;
10941
10948
  let connectedRoadmap = 0;
10942
10949
  const globCounts = /* @__PURE__ */ new Map();
10950
+ const globOwners = /* @__PURE__ */ new Map();
10951
+ const gaps = {
10952
+ missing_source: [],
10953
+ missing_initiative: [],
10954
+ missing_code_ownership: [],
10955
+ missing_knowledge_level: [],
10956
+ missing_acceptance_criteria: [],
10957
+ missing_definition_of_done: [],
10958
+ missing_validation: [],
10959
+ broad_ownership_globs: [],
10960
+ ownership_overlaps: []
10961
+ };
10962
+ const gapItem = (wi, action) => ({
10963
+ id: wi.id || wi.title,
10964
+ title: wi.title,
10965
+ status: wi.lifecycle ?? wi.status,
10966
+ path: wi.relPath,
10967
+ suggested_action: action
10968
+ });
10943
10969
  for (const wi of wis) {
10970
+ const id = wi.id || wi.title;
10944
10971
  if (wi.codeGlobs.length > 0) withOwnership++;
10972
+ else gaps.missing_code_ownership.push(gapItem(wi, "Add `code:` globs to connect this Work Item to source paths (`kaddo owners suggest`)."));
10945
10973
  const hasSource = Boolean(wi.sourceId) || wi.source === "roadmap";
10946
10974
  if (hasSource) withSource++;
10975
+ else gaps.missing_source.push(gapItem(wi, "Add `source` or `source_id` if this Work Item came from the roadmap."));
10947
10976
  if (hasSource || wi.initiative) connectedRoadmap++;
10948
10977
  if (wi.initiative) withInitiative++;
10978
+ else gaps.missing_initiative.push(gapItem(wi, "Add `initiative` to connect this Work Item to a delivery initiative."));
10949
10979
  if (wi.knowledgeLevel) withLevel++;
10950
- for (const g2 of wi.codeGlobs) globCounts.set(g2, (globCounts.get(g2) ?? 0) + 1);
10980
+ else gaps.missing_knowledge_level.push(gapItem(wi, "Add `knowledge_level` (K0\u2013K4) to the front matter."));
10981
+ for (const g2 of wi.codeGlobs) {
10982
+ globCounts.set(g2, (globCounts.get(g2) ?? 0) + 1);
10983
+ globOwners.set(g2, [...globOwners.get(g2) ?? [], id]);
10984
+ if (isBroadGlob(g2)) {
10985
+ gaps.broad_ownership_globs.push({ id, title: wi.title, glob: g2, suggested_action: "Replace with specific files or narrower module paths." });
10986
+ }
10987
+ }
10951
10988
  try {
10952
10989
  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++;
10990
+ if (hasSection(body, /acceptance|criterios de aceptaci/i)) withAcceptance++;
10991
+ else gaps.missing_acceptance_criteria.push(gapItem(wi, "Add an `## Acceptance Criteria` section."));
10992
+ if (hasSection(body, /definition of done|^#{1,6}\s*dod\b|definici[oó]n de (terminado|hecho)/i)) withDoD++;
10993
+ else gaps.missing_definition_of_done.push(gapItem(wi, "Add a `## Definition of Done` section."));
10994
+ if (hasSection(body, /how to test|validation|validaci|c[oó]mo probarlo/i)) withValidation++;
10995
+ else gaps.missing_validation.push(gapItem(wi, "Add a `## How to test it` (validation) section."));
10955
10996
  } catch {
10956
10997
  }
10957
10998
  }
10958
10999
  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: "" };
11000
+ const broadGlobs = [...globCounts.keys()].filter((g2) => isBroadGlob(g2)).length;
11001
+ for (const [glob, owners] of globOwners) {
11002
+ if (owners.length > 1) {
11003
+ gaps.ownership_overlaps.push({ code_path: glob, work_items: [...new Set(owners)], suggested_action: "Review whether the overlap is expected or should be narrowed." });
10970
11004
  }
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 };
11005
+ }
11006
+ const ownershipOverlaps = gaps.ownership_overlaps.length;
11007
+ let g;
11008
+ const config = loadConfig(dir);
11009
+ if (config) {
11010
+ const graph = buildGraph(dir, config, { scope: resolvedScope }, now);
11011
+ const hints = buildGraphHints(dir, graph, now);
11012
+ 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 };
10973
11013
  } else {
10974
- g = { available: false, scope: "n/a", scopeReason: "", nodes: 0, edges: 0, quality: "unknown", hints: 0, generatedAt: "" };
11014
+ g = { available: false, scope: resolvedScope, scopeReason: "", nodes: 0, edges: 0, quality: "unknown", hints: 0, generatedAt: "" };
10975
11015
  }
10976
11016
  const layerStatus = (name) => exp.layers.find((l) => l.layer === name)?.status ?? "Missing";
10977
11017
  const byState = exp.workItems.byState;
@@ -11040,6 +11080,7 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
11040
11080
  maintenance_readiness: levelFromRatio((coverageRatio(withOwnership) + (deliveryTraceable ? 1 : 0)) / 2)
11041
11081
  };
11042
11082
  let score = null;
11083
+ let score_breakdown = null;
11043
11084
  if (total > 0) {
11044
11085
  const healthPts = [layerStatus("Business"), layerStatus("Product"), layerStatus("Tech"), layerStatus("Delivery")].filter((s) => s !== "Missing").length / 4 * 20;
11045
11086
  const covAvg = knowledge_coverage.reduce((a, c) => a + coverageRatio(c.have), 0) / knowledge_coverage.length;
@@ -11051,13 +11092,34 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
11051
11092
  const graphPts = (g.available ? qualityMap[g.quality] ?? 0 : 0) * 15;
11052
11093
  const ctxPts = { Low: 0.25, Medium: 0.5, High: 0.8, "Very High": 1 }[ctxLevel] * 10;
11053
11094
  score = Math.round(healthPts + coveragePts + ownPts + tracePts + graphPts + ctxPts);
11095
+ score_breakdown = {
11096
+ knowledge_health: { points: Math.round(healthPts), max: 20 },
11097
+ knowledge_coverage: { points: Math.round(coveragePts), max: 20 },
11098
+ ownership_coverage: { points: Math.round(ownPts), max: 15 },
11099
+ traceability: { points: Math.round(tracePts), max: 20 },
11100
+ graph_quality: { points: Math.round(graphPts), max: 15 },
11101
+ context_readiness: { points: Math.round(ctxPts), max: 10 }
11102
+ };
11054
11103
  }
11104
+ const idsOf = (items) => items.map((i) => i.id);
11105
+ const groupedAction = (items, verb) => {
11106
+ if (items.length === 0) return null;
11107
+ const ids = idsOf(items);
11108
+ return ids.length <= 3 ? `${verb} ${ids.join(", ")}.` : `${verb} ${ids.length} Work Items: ${ids.slice(0, 3).join(", ")}, \u2026`;
11109
+ };
11055
11110
  const actions = [];
11056
11111
  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.");
11112
+ else if (g.scope === "active" && g.quality === "empty") actions.push("Run `kaddo impact --scope all` to measure accumulated knowledge impact.");
11058
11113
  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.");
11114
+ for (const a of [
11115
+ groupedAction(gaps.missing_code_ownership, "Add `code:` ownership to"),
11116
+ groupedAction(gaps.missing_source, "Add `source`/`source_id` to"),
11117
+ groupedAction(gaps.missing_initiative, "Add an `initiative` to"),
11118
+ groupedAction(gaps.missing_acceptance_criteria, "Add Acceptance Criteria to"),
11119
+ groupedAction(gaps.missing_definition_of_done, "Add a Definition of Done to"),
11120
+ groupedAction(gaps.missing_validation, "Add a validation (How to test it) section to")
11121
+ ]) if (a) actions.push(a);
11122
+ if (gaps.broad_ownership_globs.length > 0) actions.push(`Narrow ${gaps.broad_ownership_globs.length} broad ownership glob(s) into specific paths.`);
11061
11123
  if (g.available && g.hints > 0) actions.push("Review graph hints (`kaddo://graph-hints` or `.kaddo/graph-hints.md`).");
11062
11124
  if (skills.length === 0) actions.push("Install reusable skills with `kaddo add skills`.");
11063
11125
  actions.push("Run `kaddo guard` before committing to catch knowledge drift.");
@@ -11072,6 +11134,8 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
11072
11134
  generated_at: now.toISOString(),
11073
11135
  project: exp.project.name,
11074
11136
  scope: g.scope,
11137
+ default_scope: "all",
11138
+ scope_source: scopeSource,
11075
11139
  executive_summary: summary,
11076
11140
  knowledge_health,
11077
11141
  knowledge_coverage,
@@ -11109,7 +11173,9 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
11109
11173
  note: "Guard history is not persisted. Run `kaddo guard` to check drift; future versions may store runs for trend analysis."
11110
11174
  },
11111
11175
  impact_signals,
11176
+ actionable_gaps: gaps,
11112
11177
  score,
11178
+ score_breakdown,
11113
11179
  suggested_actions: actions
11114
11180
  };
11115
11181
  }
@@ -11124,6 +11190,11 @@ function renderImpactMarkdown(r) {
11124
11190
  L.push(`Scope: ${r.scope}`);
11125
11191
  if (r.score !== null) L.push(`Knowledge Impact Score: ${r.score}/100`);
11126
11192
  else L.push("Knowledge Impact Score: not available");
11193
+ if (r.scope_source === "default") {
11194
+ L.push("Scope note: Impact reports use `all` by default to measure accumulated knowledge impact.");
11195
+ } else if (r.scope === "active" && r.graph_quality.available === false) {
11196
+ L.push("Tip: Run `kaddo impact --scope all` to inspect accumulated knowledge impact.");
11197
+ }
11127
11198
  L.push("");
11128
11199
  L.push("## Executive Summary", "");
11129
11200
  for (const s2 of r.executive_summary) L.push(`- ${s2}`);
@@ -11180,6 +11251,9 @@ function renderImpactMarkdown(r) {
11180
11251
  L.push(`- Hints: ${gq.hints}`);
11181
11252
  if (gq.reason) L.push(`- Reason: ${gq.reason}`);
11182
11253
  if (gq.last_exported) L.push(`- Last exported: ${gq.last_exported}`);
11254
+ if (gq.scope === "active" && gq.quality === "empty") {
11255
+ L.push("- Tip: Run `kaddo impact --scope all` to inspect accumulated knowledge impact.");
11256
+ }
11183
11257
  } else {
11184
11258
  L.push("- Graph data not available.");
11185
11259
  L.push(`- Tip: ${r.graph_quality.suggestion}`);
@@ -11198,6 +11272,58 @@ function renderImpactMarkdown(r) {
11198
11272
  L.push(`- AI context readiness: ${s.ai_context_readiness}`);
11199
11273
  L.push(`- Maintenance readiness: ${s.maintenance_readiness}`);
11200
11274
  L.push("");
11275
+ L.push("## Actionable Gaps", "");
11276
+ const gp = r.actionable_gaps;
11277
+ const gapSection = (title, items) => {
11278
+ if (items.length === 0) return;
11279
+ L.push(`### ${title}`, "");
11280
+ for (const it of items) {
11281
+ L.push(`- ${it.id} \u2014 ${it.title}`);
11282
+ L.push(` - Path: ${it.path}`);
11283
+ L.push(` - Suggested action: ${it.suggested_action}`);
11284
+ }
11285
+ L.push("");
11286
+ };
11287
+ gapSection("Work Items missing source", gp.missing_source);
11288
+ gapSection("Work Items missing initiative", gp.missing_initiative);
11289
+ gapSection("Work Items missing code ownership", gp.missing_code_ownership);
11290
+ gapSection("Work Items missing knowledge level", gp.missing_knowledge_level);
11291
+ gapSection("Work Items missing acceptance criteria", gp.missing_acceptance_criteria);
11292
+ gapSection("Work Items missing Definition of Done", gp.missing_definition_of_done);
11293
+ gapSection("Work Items missing validation", gp.missing_validation);
11294
+ if (gp.broad_ownership_globs.length > 0) {
11295
+ L.push("### Broad ownership globs", "");
11296
+ for (const b of gp.broad_ownership_globs) {
11297
+ L.push(`- ${b.id} \u2014 ${b.title}`);
11298
+ L.push(` - Glob: \`${b.glob}\``);
11299
+ L.push(` - Suggested action: ${b.suggested_action}`);
11300
+ }
11301
+ L.push("");
11302
+ }
11303
+ if (gp.ownership_overlaps.length > 0) {
11304
+ L.push("### Ownership overlaps", "");
11305
+ for (const o of gp.ownership_overlaps) {
11306
+ L.push(`- \`${o.code_path}\``);
11307
+ L.push(` - Owned by: ${o.work_items.join(", ")}`);
11308
+ L.push(` - Suggested action: ${o.suggested_action}`);
11309
+ }
11310
+ L.push("");
11311
+ }
11312
+ const anyGaps = gp.missing_source.length + gp.missing_initiative.length + gp.missing_code_ownership.length + gp.missing_knowledge_level.length + gp.missing_acceptance_criteria.length + gp.missing_definition_of_done.length + gp.missing_validation.length + gp.broad_ownership_globs.length + gp.ownership_overlaps.length;
11313
+ if (anyGaps === 0) {
11314
+ L.push("No actionable knowledge gaps detected. \u{1F389}", "");
11315
+ }
11316
+ if (r.score_breakdown) {
11317
+ L.push("## Score Breakdown", "");
11318
+ const b = r.score_breakdown;
11319
+ L.push(`- Knowledge Health: ${b.knowledge_health.points}/${b.knowledge_health.max}`);
11320
+ L.push(`- Knowledge Coverage: ${b.knowledge_coverage.points}/${b.knowledge_coverage.max}`);
11321
+ L.push(`- Ownership Coverage: ${b.ownership_coverage.points}/${b.ownership_coverage.max}`);
11322
+ L.push(`- Traceability: ${b.traceability.points}/${b.traceability.max}`);
11323
+ L.push(`- Graph Quality: ${b.graph_quality.points}/${b.graph_quality.max}`);
11324
+ L.push(`- Context Readiness: ${b.context_readiness.points}/${b.context_readiness.max}`);
11325
+ L.push("");
11326
+ }
11201
11327
  L.push("## Suggested Actions", "");
11202
11328
  r.suggested_actions.forEach((a, i) => L.push(`${i + 1}. ${a}`));
11203
11329
  L.push("");
@@ -11211,7 +11337,8 @@ function serializeImpactJson(r) {
11211
11337
  function runReportImpact(opts = {}) {
11212
11338
  const dir = cwd();
11213
11339
  requireConfig(dir);
11214
- const report = buildImpactReport(dir);
11340
+ const scope = opts.scope === "active" ? "active" : opts.scope === "all" ? "all" : void 0;
11341
+ const report = buildImpactReport(dir, { scope });
11215
11342
  const content = opts.json ? serializeImpactJson(report) : renderImpactMarkdown(report);
11216
11343
  if (opts.output) {
11217
11344
  intro2("kaddo report impact");
@@ -11254,10 +11381,10 @@ graphCmd.command("export").description("Write the knowledge graph to .kaddo/grap
11254
11381
  runGraphExport(opts);
11255
11382
  });
11256
11383
  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) => {
11384
+ 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) => {
11258
11385
  runReportImpact(opts);
11259
11386
  });
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) => {
11387
+ program.command("impact").description("Alias for `kaddo report impact`").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").action((opts) => {
11261
11388
  runReportImpact(opts);
11262
11389
  });
11263
11390
  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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/cli",
3
- "version": "3.23.0",
3
+ "version": "3.23.2",
4
4
  "description": "Knowledge Driven Development toolkit",
5
5
  "license": "MIT",
6
6
  "repository": {