@kaddo/cli 3.23.1 → 3.24.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 +286 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -512,6 +512,8 @@ create --from roadmap → owners → guard → explain`.
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
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 |
516
+ | v3.24 | Estimated Savings Model: `kaddo savings` (+ `savings init`, `.kaddo/savings.yml`); evidence-based time/value estimates, confidence; MCP resource + tool |
515
517
 
516
518
  **Optional modules (installed with `kaddo add`):**
517
519
 
package/dist/index.js CHANGED
@@ -688,6 +688,10 @@ var COMMAND_HELP = {
688
688
  "report impact": {
689
689
  question: "What impact is Kaddo having on this project?",
690
690
  next: "Act on the Suggested Actions section"
691
+ },
692
+ savings: {
693
+ question: "How much time/value might Kaddo be saving (estimate)?",
694
+ next: "Run `kaddo savings init` to calibrate assumptions"
691
695
  }
692
696
  };
693
697
  function commandFooterLines(name) {
@@ -10933,6 +10937,8 @@ function hasSection(body, re) {
10933
10937
  return body.split(/\r?\n/).some((l) => /^#{1,6}\s+/.test(l) && re.test(l));
10934
10938
  }
10935
10939
  function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
10940
+ const resolvedScope = opts.scope ?? "all";
10941
+ const scopeSource = opts.scopeSource ?? (opts.scope ? "explicit" : "default");
10936
10942
  const exp = buildProjectExplanation(dir);
10937
10943
  const wis = discoverWorkItems(dir);
10938
10944
  const total = wis.length;
@@ -11003,19 +11009,13 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
11003
11009
  }
11004
11010
  const ownershipOverlaps = gaps.ownership_overlaps.length;
11005
11011
  let g;
11006
- if (opts.scope) {
11007
- const config = loadConfig(dir);
11008
- if (config) {
11009
- const graph = buildGraph(dir, config, { scope: opts.scope }, now);
11010
- const hints = buildGraphHints(dir, graph, now);
11011
- 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 };
11012
- } else {
11013
- g = { available: false, scope: opts.scope, scopeReason: "", nodes: 0, edges: 0, quality: "unknown", hints: 0, generatedAt: "" };
11014
- }
11015
- } else if (exp.graph) {
11016
- 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 };
11012
+ const config = loadConfig(dir);
11013
+ if (config) {
11014
+ const graph = buildGraph(dir, config, { scope: resolvedScope }, now);
11015
+ const hints = buildGraphHints(dir, graph, now);
11016
+ 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 };
11017
11017
  } else {
11018
- g = { available: false, scope: "n/a", scopeReason: "", nodes: 0, edges: 0, quality: "unknown", hints: 0, generatedAt: "" };
11018
+ g = { available: false, scope: resolvedScope, scopeReason: "", nodes: 0, edges: 0, quality: "unknown", hints: 0, generatedAt: "" };
11019
11019
  }
11020
11020
  const layerStatus = (name) => exp.layers.find((l) => l.layer === name)?.status ?? "Missing";
11021
11021
  const byState = exp.workItems.byState;
@@ -11113,7 +11113,7 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
11113
11113
  };
11114
11114
  const actions = [];
11115
11115
  if (!g.available) actions.push("Run `kaddo graph export --scope all` to inspect full traceability.");
11116
- else if (g.scope === "active" && g.quality === "empty") actions.push("Run `kaddo graph export --scope all` to include completed Work Items.");
11116
+ else if (g.scope === "active" && g.quality === "empty") actions.push("Run `kaddo impact --scope all` to measure accumulated knowledge impact.");
11117
11117
  if (exp.roadmap.remaining > 0) actions.push(`Materialize the ${exp.roadmap.remaining} remaining roadmap candidate(s) with \`kaddo create --from roadmap\`.`);
11118
11118
  for (const a of [
11119
11119
  groupedAction(gaps.missing_code_ownership, "Add `code:` ownership to"),
@@ -11138,6 +11138,8 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
11138
11138
  generated_at: now.toISOString(),
11139
11139
  project: exp.project.name,
11140
11140
  scope: g.scope,
11141
+ default_scope: "all",
11142
+ scope_source: scopeSource,
11141
11143
  executive_summary: summary,
11142
11144
  knowledge_health,
11143
11145
  knowledge_coverage,
@@ -11192,6 +11194,11 @@ function renderImpactMarkdown(r) {
11192
11194
  L.push(`Scope: ${r.scope}`);
11193
11195
  if (r.score !== null) L.push(`Knowledge Impact Score: ${r.score}/100`);
11194
11196
  else L.push("Knowledge Impact Score: not available");
11197
+ if (r.scope_source === "default") {
11198
+ L.push("Scope note: Impact reports use `all` by default to measure accumulated knowledge impact.");
11199
+ } else if (r.scope === "active" && r.graph_quality.available === false) {
11200
+ L.push("Tip: Run `kaddo impact --scope all` to inspect accumulated knowledge impact.");
11201
+ }
11195
11202
  L.push("");
11196
11203
  L.push("## Executive Summary", "");
11197
11204
  for (const s2 of r.executive_summary) L.push(`- ${s2}`);
@@ -11248,6 +11255,9 @@ function renderImpactMarkdown(r) {
11248
11255
  L.push(`- Hints: ${gq.hints}`);
11249
11256
  if (gq.reason) L.push(`- Reason: ${gq.reason}`);
11250
11257
  if (gq.last_exported) L.push(`- Last exported: ${gq.last_exported}`);
11258
+ if (gq.scope === "active" && gq.quality === "empty") {
11259
+ L.push("- Tip: Run `kaddo impact --scope all` to inspect accumulated knowledge impact.");
11260
+ }
11251
11261
  } else {
11252
11262
  L.push("- Graph data not available.");
11253
11263
  L.push(`- Tip: ${r.graph_quality.suggestion}`);
@@ -11331,7 +11341,8 @@ function serializeImpactJson(r) {
11331
11341
  function runReportImpact(opts = {}) {
11332
11342
  const dir = cwd();
11333
11343
  requireConfig(dir);
11334
- const report = buildImpactReport(dir);
11344
+ const scope = opts.scope === "active" ? "active" : opts.scope === "all" ? "all" : void 0;
11345
+ const report = buildImpactReport(dir, { scope });
11335
11346
  const content = opts.json ? serializeImpactJson(report) : renderImpactMarkdown(report);
11336
11347
  if (opts.output) {
11337
11348
  intro2("kaddo report impact");
@@ -11345,6 +11356,256 @@ function runReportImpact(opts = {}) {
11345
11356
  console.log(content);
11346
11357
  }
11347
11358
 
11359
+ // src/core/savings.ts
11360
+ import { parse as parseYaml15 } from "yaml";
11361
+ var DEFAULT_ASSUMPTIONS = {
11362
+ currency: "USD",
11363
+ hourly_cost: 40,
11364
+ context_preparation_minutes_saved_per_work_item: 30,
11365
+ rework_hours_avoided_per_resolved_drift: 2,
11366
+ onboarding_hours_saved_per_new_contributor: 4,
11367
+ review_minutes_saved_per_work_item_with_ownership: 20,
11368
+ clarification_minutes_saved_per_ready_work_item: 25,
11369
+ architecture_discovery_hours_saved_when_graph_good: 3,
11370
+ expected_new_contributors_per_month: 1,
11371
+ expected_work_items_per_month: 8
11372
+ };
11373
+ var SAVINGS_PATH = ".kaddo/savings.yml";
11374
+ function loadAssumptions(dir) {
11375
+ const p2 = join(dir, SAVINGS_PATH);
11376
+ if (!exists(p2)) return { assumptions: { ...DEFAULT_ASSUMPTIONS }, source: "default" };
11377
+ try {
11378
+ const raw = parseYaml15(readFile(p2));
11379
+ const a = raw?.assumptions ?? {};
11380
+ const t = raw?.team ?? {};
11381
+ const num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
11382
+ return {
11383
+ source: "file",
11384
+ assumptions: {
11385
+ currency: raw?.currency ?? DEFAULT_ASSUMPTIONS.currency,
11386
+ hourly_cost: num(raw?.hourly_cost, DEFAULT_ASSUMPTIONS.hourly_cost),
11387
+ context_preparation_minutes_saved_per_work_item: num(a.context_preparation_minutes_saved_per_work_item, DEFAULT_ASSUMPTIONS.context_preparation_minutes_saved_per_work_item),
11388
+ rework_hours_avoided_per_resolved_drift: num(a.rework_hours_avoided_per_resolved_drift, DEFAULT_ASSUMPTIONS.rework_hours_avoided_per_resolved_drift),
11389
+ onboarding_hours_saved_per_new_contributor: num(a.onboarding_hours_saved_per_new_contributor, DEFAULT_ASSUMPTIONS.onboarding_hours_saved_per_new_contributor),
11390
+ review_minutes_saved_per_work_item_with_ownership: num(a.review_minutes_saved_per_work_item_with_ownership, DEFAULT_ASSUMPTIONS.review_minutes_saved_per_work_item_with_ownership),
11391
+ clarification_minutes_saved_per_ready_work_item: num(a.clarification_minutes_saved_per_ready_work_item, DEFAULT_ASSUMPTIONS.clarification_minutes_saved_per_ready_work_item),
11392
+ architecture_discovery_hours_saved_when_graph_good: num(a.architecture_discovery_hours_saved_when_graph_good, DEFAULT_ASSUMPTIONS.architecture_discovery_hours_saved_when_graph_good),
11393
+ expected_new_contributors_per_month: num(t.expected_new_contributors_per_month, DEFAULT_ASSUMPTIONS.expected_new_contributors_per_month),
11394
+ expected_work_items_per_month: num(t.expected_work_items_per_month, DEFAULT_ASSUMPTIONS.expected_work_items_per_month)
11395
+ }
11396
+ };
11397
+ } catch {
11398
+ return { assumptions: { ...DEFAULT_ASSUMPTIONS }, source: "default" };
11399
+ }
11400
+ }
11401
+ function savingsTemplate() {
11402
+ return [
11403
+ "# Kaddo savings assumptions (VS-062). Estimates are evidence-based, not exact ROI.",
11404
+ "# Edit these to match your team, then run `kaddo savings`.",
11405
+ "currency: USD",
11406
+ "",
11407
+ "hourly_cost: 40",
11408
+ "",
11409
+ "assumptions:",
11410
+ " context_preparation_minutes_saved_per_work_item: 30",
11411
+ " rework_hours_avoided_per_resolved_drift: 2",
11412
+ " onboarding_hours_saved_per_new_contributor: 4",
11413
+ " review_minutes_saved_per_work_item_with_ownership: 20",
11414
+ " clarification_minutes_saved_per_ready_work_item: 25",
11415
+ " architecture_discovery_hours_saved_when_graph_good: 3",
11416
+ "",
11417
+ "team:",
11418
+ " expected_new_contributors_per_month: 1",
11419
+ " expected_work_items_per_month: 8",
11420
+ ""
11421
+ ].join("\n");
11422
+ }
11423
+ var READINESS_MULT = { Low: 0.25, Medium: 0.5, High: 0.75, "Very High": 1 };
11424
+ var GRAPH_MULT = { empty: 0, sparse: 0.25, partial: 0.6, good: 1, unknown: 0 };
11425
+ var round2 = (n) => Math.round(n * 100) / 100;
11426
+ function parseRatioNumerator(s) {
11427
+ const m = s.match(/^(\d+)/);
11428
+ return m ? Number(m[1]) : 0;
11429
+ }
11430
+ var SAVINGS_DISCLAIMER = "These are evidence-based estimates, not exact ROI. Adjust assumptions with real team data.";
11431
+ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
11432
+ const { assumptions: a, source } = loadAssumptions(dir);
11433
+ const impact = buildImpactReport(dir, { scope: opts.scope, scopeSource: opts.scopeSource }, now);
11434
+ const completed = impact.traceability.completed_work_items;
11435
+ const wiWithOwnership = parseRatioNumerator(impact.traceability.work_items_connected_to_code);
11436
+ const acceptanceCov = impact.knowledge_coverage.find((c) => c.label.toLowerCase().includes("acceptance"));
11437
+ const wiWithAcceptance = acceptanceCov?.have ?? 0;
11438
+ const graphQuality = impact.graph_quality.available ? impact.graph_quality.quality : "unknown";
11439
+ const readiness = impact.context_readiness.level;
11440
+ const ctxPrepH = round2(completed * a.context_preparation_minutes_saved_per_work_item / 60);
11441
+ const reviewH = round2(wiWithOwnership * a.review_minutes_saved_per_work_item_with_ownership / 60);
11442
+ const clarH = round2(wiWithAcceptance * a.clarification_minutes_saved_per_ready_work_item / 60);
11443
+ const readinessMult = READINESS_MULT[readiness] ?? 0.5;
11444
+ const onboardH = round2(a.expected_new_contributors_per_month * a.onboarding_hours_saved_per_new_contributor * readinessMult);
11445
+ const graphMult = GRAPH_MULT[graphQuality] ?? 0;
11446
+ const archH = round2(a.architecture_discovery_hours_saved_when_graph_good * graphMult);
11447
+ const totalHours = round2(ctxPrepH + reviewH + clarH + onboardH + archH);
11448
+ const totalValue = Math.round(totalHours * a.hourly_cost);
11449
+ const reasons = [];
11450
+ let level;
11451
+ if (impact.score === null || impact.score < 60 || graphQuality === "empty") {
11452
+ level = "Low";
11453
+ reasons.push("Impact score is low or the knowledge graph is empty.");
11454
+ } else {
11455
+ level = "Medium";
11456
+ reasons.push("Strong knowledge and graph evidence.");
11457
+ }
11458
+ reasons.push("Guard history is not persisted yet \u2014 avoided rework is not estimated.");
11459
+ reasons.push(source === "file" ? "Assumptions come from `.kaddo/savings.yml`." : "Savings assumptions are defaults and should be calibrated.");
11460
+ const actions = [];
11461
+ if (source === "default") actions.push("Run `kaddo savings init` to customize assumptions.");
11462
+ actions.push("Calibrate hourly cost and team assumptions with real team data.");
11463
+ actions.push("Run `kaddo impact` to verify the underlying evidence quality.");
11464
+ actions.push("Use this report as directional evidence, not accounting data.");
11465
+ actions.push("Future: persist Guard history to estimate avoided rework.");
11466
+ return {
11467
+ generated_at: now.toISOString(),
11468
+ project: impact.project,
11469
+ scope: impact.scope,
11470
+ currency: a.currency,
11471
+ disclaimer: SAVINGS_DISCLAIMER,
11472
+ assumptions_source: source,
11473
+ assumptions: {
11474
+ hourly_cost: a.hourly_cost,
11475
+ context_preparation_minutes_saved_per_work_item: a.context_preparation_minutes_saved_per_work_item,
11476
+ review_minutes_saved_per_work_item_with_ownership: a.review_minutes_saved_per_work_item_with_ownership,
11477
+ clarification_minutes_saved_per_ready_work_item: a.clarification_minutes_saved_per_ready_work_item,
11478
+ onboarding_hours_saved_per_new_contributor: a.onboarding_hours_saved_per_new_contributor,
11479
+ expected_new_contributors_per_month: a.expected_new_contributors_per_month,
11480
+ architecture_discovery_hours_saved_when_graph_good: a.architecture_discovery_hours_saved_when_graph_good
11481
+ },
11482
+ evidence: {
11483
+ knowledge_impact_score: impact.score,
11484
+ completed_work_items: completed,
11485
+ ownership_coverage_percent: impact.ownership_coverage.coverage_percent,
11486
+ work_items_with_acceptance_criteria: acceptanceCov ? `${acceptanceCov.have}/${acceptanceCov.total}` : "0/0",
11487
+ graph_quality: graphQuality,
11488
+ graph_nodes: impact.graph_quality.available ? impact.graph_quality.nodes : null,
11489
+ graph_edges: impact.graph_quality.available ? impact.graph_quality.edges : null,
11490
+ context_readiness: readiness,
11491
+ guard_history_available: false
11492
+ },
11493
+ estimated_savings: {
11494
+ context_preparation: { hours: ctxPrepH, formula: `${completed} Work Items \xD7 ${a.context_preparation_minutes_saved_per_work_item} min` },
11495
+ review_effort: { hours: reviewH, formula: `${wiWithOwnership} Work Items with ownership \xD7 ${a.review_minutes_saved_per_work_item_with_ownership} min` },
11496
+ clarification_reduction: { hours: clarH, formula: `${wiWithAcceptance} Work Items with Acceptance Criteria \xD7 ${a.clarification_minutes_saved_per_ready_work_item} min` },
11497
+ onboarding: { hours: onboardH, formula: `${a.expected_new_contributors_per_month} contributor \xD7 ${a.onboarding_hours_saved_per_new_contributor} h \xD7 ${readinessMult.toFixed(2)} readiness multiplier` },
11498
+ architecture_discovery: { hours: archH, formula: `${a.architecture_discovery_hours_saved_when_graph_good} h \xD7 ${graphMult.toFixed(2)} graph multiplier` },
11499
+ drift_prevention: { available: false, hours: 0, reason: "Guard history is not persisted yet." }
11500
+ },
11501
+ total: { estimated_hours_saved: totalHours, estimated_value: totalValue, currency: a.currency },
11502
+ confidence: { level, reasons },
11503
+ suggested_actions: actions
11504
+ };
11505
+ }
11506
+ function renderSavingsMarkdown(r) {
11507
+ const L = [];
11508
+ L.push("# Kaddo Estimated Savings Report", "");
11509
+ L.push(`Generated at: ${r.generated_at}`);
11510
+ L.push(`Project: ${r.project}`);
11511
+ L.push(`Scope: ${r.scope}`);
11512
+ L.push(`Currency: ${r.currency}`);
11513
+ L.push("");
11514
+ L.push("## Disclaimer", "");
11515
+ L.push(r.disclaimer, "");
11516
+ L.push("## Executive Summary", "");
11517
+ L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} hours`);
11518
+ L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
11519
+ L.push(`- Confidence: ${r.confidence.level}`);
11520
+ if (r.assumptions_source === "default") L.push("- Using default assumptions. Run `kaddo savings init` to customize them.");
11521
+ L.push("");
11522
+ L.push("## Assumptions", "");
11523
+ L.push(`- Hourly cost: ${r.assumptions.hourly_cost} ${r.currency}`);
11524
+ L.push(`- Context preparation saved per Work Item: ${r.assumptions.context_preparation_minutes_saved_per_work_item} min`);
11525
+ L.push(`- Review saved per Work Item with ownership: ${r.assumptions.review_minutes_saved_per_work_item_with_ownership} min`);
11526
+ L.push(`- Clarification saved per Work Item with acceptance criteria: ${r.assumptions.clarification_minutes_saved_per_ready_work_item} min`);
11527
+ L.push(`- Onboarding saved per new contributor: ${r.assumptions.onboarding_hours_saved_per_new_contributor} h`);
11528
+ L.push(`- Expected new contributors per month: ${r.assumptions.expected_new_contributors_per_month}`);
11529
+ L.push(`- Architecture discovery saved when graph is good: ${r.assumptions.architecture_discovery_hours_saved_when_graph_good} h`);
11530
+ L.push("");
11531
+ L.push("## Evidence Used", "");
11532
+ const e = r.evidence;
11533
+ L.push(`- Knowledge Impact Score: ${e.knowledge_impact_score ?? "not available"}${e.knowledge_impact_score !== null ? "/100" : ""}`);
11534
+ L.push(`- Completed Work Items: ${e.completed_work_items}`);
11535
+ L.push(`- Ownership coverage: ${e.ownership_coverage_percent}%`);
11536
+ L.push(`- Work Items with Acceptance Criteria: ${e.work_items_with_acceptance_criteria}`);
11537
+ L.push(`- Graph quality: ${e.graph_quality}`);
11538
+ if (e.graph_nodes !== null) L.push(`- Graph nodes: ${e.graph_nodes}`);
11539
+ if (e.graph_edges !== null) L.push(`- Graph edges: ${e.graph_edges}`);
11540
+ L.push(`- Context readiness: ${e.context_readiness}`);
11541
+ L.push("- Guard history: not available");
11542
+ L.push("");
11543
+ L.push("## Estimated Savings", "");
11544
+ const s = r.estimated_savings;
11545
+ const line = (title, l) => {
11546
+ L.push(`### ${title}`, "");
11547
+ L.push(`- Formula: ${l.formula}`);
11548
+ L.push(`- Estimated: ${l.hours} h`, "");
11549
+ };
11550
+ line("Context Preparation", s.context_preparation);
11551
+ line("Review Effort", s.review_effort);
11552
+ line("Clarification Reduction", s.clarification_reduction);
11553
+ line("Onboarding", s.onboarding);
11554
+ line("Architecture Discovery", s.architecture_discovery);
11555
+ L.push("### Drift Prevention", "");
11556
+ L.push("- Not available yet.");
11557
+ L.push(`- Reason: ${s.drift_prevention.reason}`, "");
11558
+ L.push("## Total", "");
11559
+ L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} h`);
11560
+ L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
11561
+ L.push("");
11562
+ L.push("## Confidence", "");
11563
+ L.push(`Confidence: ${r.confidence.level}`, "", "Reason:");
11564
+ for (const reason of r.confidence.reasons) L.push(`- ${reason}`);
11565
+ L.push("");
11566
+ L.push("## Suggested Actions", "");
11567
+ r.suggested_actions.forEach((act, i) => L.push(`${i + 1}. ${act}`));
11568
+ L.push("");
11569
+ return L.join("\n");
11570
+ }
11571
+ function serializeSavingsJson(r) {
11572
+ return JSON.stringify(r, null, 2) + "\n";
11573
+ }
11574
+
11575
+ // src/commands/savings.ts
11576
+ function runSavings(opts = {}) {
11577
+ const dir = cwd();
11578
+ requireConfig(dir);
11579
+ const scope = opts.scope === "active" ? "active" : opts.scope === "all" ? "all" : void 0;
11580
+ const report = buildSavingsReport(dir, { scope });
11581
+ const content = opts.json ? serializeSavingsJson(report) : renderSavingsMarkdown(report);
11582
+ if (opts.output) {
11583
+ intro2("kaddo savings");
11584
+ writeFile(join(dir, opts.output), content);
11585
+ log2.success(`Wrote ${opts.output.replace(/\\/g, "/")}`);
11586
+ printCommandFooter("savings");
11587
+ outro2("Savings report written.");
11588
+ return;
11589
+ }
11590
+ console.log(content);
11591
+ }
11592
+ function runSavingsInit(opts = {}) {
11593
+ const dir = cwd();
11594
+ requireConfig(dir);
11595
+ intro2("kaddo savings init");
11596
+ const rel = ".kaddo/savings.yml";
11597
+ const full = join(dir, rel);
11598
+ if (exists(full) && !opts.force) {
11599
+ log2.warn(`${rel} already exists. Use --force to overwrite.`);
11600
+ outro2("Nothing changed.");
11601
+ return;
11602
+ }
11603
+ writeFile(full, savingsTemplate());
11604
+ log2.success(`Wrote ${rel}`);
11605
+ log2.info("Edit the assumptions, then run `kaddo savings`.");
11606
+ outro2("Savings assumptions ready.");
11607
+ }
11608
+
11348
11609
  // src/index.ts
11349
11610
  var require2 = createRequire(import.meta.url);
11350
11611
  var { version } = require2("../package.json");
@@ -11374,12 +11635,21 @@ graphCmd.command("export").description("Write the knowledge graph to .kaddo/grap
11374
11635
  runGraphExport(opts);
11375
11636
  });
11376
11637
  var reportCmd = program.command("report").description("Generate Kaddo reports");
11377
- 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) => {
11638
+ 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) => {
11378
11639
  runReportImpact(opts);
11379
11640
  });
11380
- 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) => {
11641
+ 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) => {
11381
11642
  runReportImpact(opts);
11382
11643
  });
11644
+ reportCmd.command("savings").description("Estimated Savings Report: evidence-based time/effort/value estimates (deterministic, no LLM)").option("--json", "Output JSON instead of Markdown").option("--scope <scope>", "Scope: all (default) or active").option("--output <path>", "Write the report to a file (e.g. .kaddo/reports/savings-report.md)").action((opts) => {
11645
+ runSavings(opts);
11646
+ });
11647
+ var savingsCmd = program.command("savings").description("Estimated Savings Report from impact evidence + configurable assumptions").option("--json", "Output JSON instead of Markdown").option("--scope <scope>", "Scope: all (default) or active").option("--output <path>", "Write the report to a file").action((opts) => {
11648
+ runSavings(opts);
11649
+ });
11650
+ savingsCmd.command("init").description("Create an editable `.kaddo/savings.yml` with savings assumptions").option("--force", "Overwrite an existing .kaddo/savings.yml").action((opts) => {
11651
+ runSavingsInit(opts);
11652
+ });
11383
11653
  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) => {
11384
11654
  await runGuard(opts);
11385
11655
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/cli",
3
- "version": "3.23.1",
3
+ "version": "3.24.0",
4
4
  "description": "Knowledge Driven Development toolkit",
5
5
  "license": "MIT",
6
6
  "repository": {