@kaddo/cli 3.23.2 → 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 +1 -0
  2. package/dist/index.js +263 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -513,6 +513,7 @@ create --from roadmap → owners → guard → explain`.
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
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 |
516
517
 
517
518
  **Optional modules (installed with `kaddo add`):**
518
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) {
@@ -11352,6 +11356,256 @@ function runReportImpact(opts = {}) {
11352
11356
  console.log(content);
11353
11357
  }
11354
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
+
11355
11609
  // src/index.ts
11356
11610
  var require2 = createRequire(import.meta.url);
11357
11611
  var { version } = require2("../package.json");
@@ -11387,6 +11641,15 @@ reportCmd.command("impact").description("Knowledge Impact Report: knowledge heal
11387
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) => {
11388
11642
  runReportImpact(opts);
11389
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
+ });
11390
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) => {
11391
11654
  await runGuard(opts);
11392
11655
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/cli",
3
- "version": "3.23.2",
3
+ "version": "3.24.0",
4
4
  "description": "Knowledge Driven Development toolkit",
5
5
  "license": "MIT",
6
6
  "repository": {