@kaddo/mcp 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 (2) hide show
  1. package/dist/index.js +241 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2502,6 +2502,200 @@ function serializeImpactJson(r) {
2502
2502
  return JSON.stringify(r, null, 2) + "\n";
2503
2503
  }
2504
2504
 
2505
+ // ../cli/src/core/savings.ts
2506
+ import { parse as parseYaml6 } from "yaml";
2507
+ var DEFAULT_ASSUMPTIONS = {
2508
+ currency: "USD",
2509
+ hourly_cost: 40,
2510
+ context_preparation_minutes_saved_per_work_item: 30,
2511
+ rework_hours_avoided_per_resolved_drift: 2,
2512
+ onboarding_hours_saved_per_new_contributor: 4,
2513
+ review_minutes_saved_per_work_item_with_ownership: 20,
2514
+ clarification_minutes_saved_per_ready_work_item: 25,
2515
+ architecture_discovery_hours_saved_when_graph_good: 3,
2516
+ expected_new_contributors_per_month: 1,
2517
+ expected_work_items_per_month: 8
2518
+ };
2519
+ var SAVINGS_PATH = ".kaddo/savings.yml";
2520
+ function loadAssumptions(dir) {
2521
+ const p = join(dir, SAVINGS_PATH);
2522
+ if (!exists(p)) return { assumptions: { ...DEFAULT_ASSUMPTIONS }, source: "default" };
2523
+ try {
2524
+ const raw = parseYaml6(readFile(p));
2525
+ const a = raw?.assumptions ?? {};
2526
+ const t = raw?.team ?? {};
2527
+ const num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
2528
+ return {
2529
+ source: "file",
2530
+ assumptions: {
2531
+ currency: raw?.currency ?? DEFAULT_ASSUMPTIONS.currency,
2532
+ hourly_cost: num(raw?.hourly_cost, DEFAULT_ASSUMPTIONS.hourly_cost),
2533
+ 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),
2534
+ rework_hours_avoided_per_resolved_drift: num(a.rework_hours_avoided_per_resolved_drift, DEFAULT_ASSUMPTIONS.rework_hours_avoided_per_resolved_drift),
2535
+ onboarding_hours_saved_per_new_contributor: num(a.onboarding_hours_saved_per_new_contributor, DEFAULT_ASSUMPTIONS.onboarding_hours_saved_per_new_contributor),
2536
+ 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),
2537
+ 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),
2538
+ 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),
2539
+ expected_new_contributors_per_month: num(t.expected_new_contributors_per_month, DEFAULT_ASSUMPTIONS.expected_new_contributors_per_month),
2540
+ expected_work_items_per_month: num(t.expected_work_items_per_month, DEFAULT_ASSUMPTIONS.expected_work_items_per_month)
2541
+ }
2542
+ };
2543
+ } catch {
2544
+ return { assumptions: { ...DEFAULT_ASSUMPTIONS }, source: "default" };
2545
+ }
2546
+ }
2547
+ var READINESS_MULT = { Low: 0.25, Medium: 0.5, High: 0.75, "Very High": 1 };
2548
+ var GRAPH_MULT = { empty: 0, sparse: 0.25, partial: 0.6, good: 1, unknown: 0 };
2549
+ var round2 = (n) => Math.round(n * 100) / 100;
2550
+ function parseRatioNumerator(s) {
2551
+ const m = s.match(/^(\d+)/);
2552
+ return m ? Number(m[1]) : 0;
2553
+ }
2554
+ var SAVINGS_DISCLAIMER = "These are evidence-based estimates, not exact ROI. Adjust assumptions with real team data.";
2555
+ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
2556
+ const { assumptions: a, source } = loadAssumptions(dir);
2557
+ const impact = buildImpactReport(dir, { scope: opts.scope, scopeSource: opts.scopeSource }, now);
2558
+ const completed = impact.traceability.completed_work_items;
2559
+ const wiWithOwnership = parseRatioNumerator(impact.traceability.work_items_connected_to_code);
2560
+ const acceptanceCov = impact.knowledge_coverage.find((c) => c.label.toLowerCase().includes("acceptance"));
2561
+ const wiWithAcceptance = acceptanceCov?.have ?? 0;
2562
+ const graphQuality = impact.graph_quality.available ? impact.graph_quality.quality : "unknown";
2563
+ const readiness = impact.context_readiness.level;
2564
+ const ctxPrepH = round2(completed * a.context_preparation_minutes_saved_per_work_item / 60);
2565
+ const reviewH = round2(wiWithOwnership * a.review_minutes_saved_per_work_item_with_ownership / 60);
2566
+ const clarH = round2(wiWithAcceptance * a.clarification_minutes_saved_per_ready_work_item / 60);
2567
+ const readinessMult = READINESS_MULT[readiness] ?? 0.5;
2568
+ const onboardH = round2(a.expected_new_contributors_per_month * a.onboarding_hours_saved_per_new_contributor * readinessMult);
2569
+ const graphMult = GRAPH_MULT[graphQuality] ?? 0;
2570
+ const archH = round2(a.architecture_discovery_hours_saved_when_graph_good * graphMult);
2571
+ const totalHours = round2(ctxPrepH + reviewH + clarH + onboardH + archH);
2572
+ const totalValue = Math.round(totalHours * a.hourly_cost);
2573
+ const reasons = [];
2574
+ let level;
2575
+ if (impact.score === null || impact.score < 60 || graphQuality === "empty") {
2576
+ level = "Low";
2577
+ reasons.push("Impact score is low or the knowledge graph is empty.");
2578
+ } else {
2579
+ level = "Medium";
2580
+ reasons.push("Strong knowledge and graph evidence.");
2581
+ }
2582
+ reasons.push("Guard history is not persisted yet \u2014 avoided rework is not estimated.");
2583
+ reasons.push(source === "file" ? "Assumptions come from `.kaddo/savings.yml`." : "Savings assumptions are defaults and should be calibrated.");
2584
+ const actions = [];
2585
+ if (source === "default") actions.push("Run `kaddo savings init` to customize assumptions.");
2586
+ actions.push("Calibrate hourly cost and team assumptions with real team data.");
2587
+ actions.push("Run `kaddo impact` to verify the underlying evidence quality.");
2588
+ actions.push("Use this report as directional evidence, not accounting data.");
2589
+ actions.push("Future: persist Guard history to estimate avoided rework.");
2590
+ return {
2591
+ generated_at: now.toISOString(),
2592
+ project: impact.project,
2593
+ scope: impact.scope,
2594
+ currency: a.currency,
2595
+ disclaimer: SAVINGS_DISCLAIMER,
2596
+ assumptions_source: source,
2597
+ assumptions: {
2598
+ hourly_cost: a.hourly_cost,
2599
+ context_preparation_minutes_saved_per_work_item: a.context_preparation_minutes_saved_per_work_item,
2600
+ review_minutes_saved_per_work_item_with_ownership: a.review_minutes_saved_per_work_item_with_ownership,
2601
+ clarification_minutes_saved_per_ready_work_item: a.clarification_minutes_saved_per_ready_work_item,
2602
+ onboarding_hours_saved_per_new_contributor: a.onboarding_hours_saved_per_new_contributor,
2603
+ expected_new_contributors_per_month: a.expected_new_contributors_per_month,
2604
+ architecture_discovery_hours_saved_when_graph_good: a.architecture_discovery_hours_saved_when_graph_good
2605
+ },
2606
+ evidence: {
2607
+ knowledge_impact_score: impact.score,
2608
+ completed_work_items: completed,
2609
+ ownership_coverage_percent: impact.ownership_coverage.coverage_percent,
2610
+ work_items_with_acceptance_criteria: acceptanceCov ? `${acceptanceCov.have}/${acceptanceCov.total}` : "0/0",
2611
+ graph_quality: graphQuality,
2612
+ graph_nodes: impact.graph_quality.available ? impact.graph_quality.nodes : null,
2613
+ graph_edges: impact.graph_quality.available ? impact.graph_quality.edges : null,
2614
+ context_readiness: readiness,
2615
+ guard_history_available: false
2616
+ },
2617
+ estimated_savings: {
2618
+ context_preparation: { hours: ctxPrepH, formula: `${completed} Work Items \xD7 ${a.context_preparation_minutes_saved_per_work_item} min` },
2619
+ review_effort: { hours: reviewH, formula: `${wiWithOwnership} Work Items with ownership \xD7 ${a.review_minutes_saved_per_work_item_with_ownership} min` },
2620
+ clarification_reduction: { hours: clarH, formula: `${wiWithAcceptance} Work Items with Acceptance Criteria \xD7 ${a.clarification_minutes_saved_per_ready_work_item} min` },
2621
+ 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` },
2622
+ architecture_discovery: { hours: archH, formula: `${a.architecture_discovery_hours_saved_when_graph_good} h \xD7 ${graphMult.toFixed(2)} graph multiplier` },
2623
+ drift_prevention: { available: false, hours: 0, reason: "Guard history is not persisted yet." }
2624
+ },
2625
+ total: { estimated_hours_saved: totalHours, estimated_value: totalValue, currency: a.currency },
2626
+ confidence: { level, reasons },
2627
+ suggested_actions: actions
2628
+ };
2629
+ }
2630
+ function renderSavingsMarkdown(r) {
2631
+ const L = [];
2632
+ L.push("# Kaddo Estimated Savings Report", "");
2633
+ L.push(`Generated at: ${r.generated_at}`);
2634
+ L.push(`Project: ${r.project}`);
2635
+ L.push(`Scope: ${r.scope}`);
2636
+ L.push(`Currency: ${r.currency}`);
2637
+ L.push("");
2638
+ L.push("## Disclaimer", "");
2639
+ L.push(r.disclaimer, "");
2640
+ L.push("## Executive Summary", "");
2641
+ L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} hours`);
2642
+ L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
2643
+ L.push(`- Confidence: ${r.confidence.level}`);
2644
+ if (r.assumptions_source === "default") L.push("- Using default assumptions. Run `kaddo savings init` to customize them.");
2645
+ L.push("");
2646
+ L.push("## Assumptions", "");
2647
+ L.push(`- Hourly cost: ${r.assumptions.hourly_cost} ${r.currency}`);
2648
+ L.push(`- Context preparation saved per Work Item: ${r.assumptions.context_preparation_minutes_saved_per_work_item} min`);
2649
+ L.push(`- Review saved per Work Item with ownership: ${r.assumptions.review_minutes_saved_per_work_item_with_ownership} min`);
2650
+ L.push(`- Clarification saved per Work Item with acceptance criteria: ${r.assumptions.clarification_minutes_saved_per_ready_work_item} min`);
2651
+ L.push(`- Onboarding saved per new contributor: ${r.assumptions.onboarding_hours_saved_per_new_contributor} h`);
2652
+ L.push(`- Expected new contributors per month: ${r.assumptions.expected_new_contributors_per_month}`);
2653
+ L.push(`- Architecture discovery saved when graph is good: ${r.assumptions.architecture_discovery_hours_saved_when_graph_good} h`);
2654
+ L.push("");
2655
+ L.push("## Evidence Used", "");
2656
+ const e = r.evidence;
2657
+ L.push(`- Knowledge Impact Score: ${e.knowledge_impact_score ?? "not available"}${e.knowledge_impact_score !== null ? "/100" : ""}`);
2658
+ L.push(`- Completed Work Items: ${e.completed_work_items}`);
2659
+ L.push(`- Ownership coverage: ${e.ownership_coverage_percent}%`);
2660
+ L.push(`- Work Items with Acceptance Criteria: ${e.work_items_with_acceptance_criteria}`);
2661
+ L.push(`- Graph quality: ${e.graph_quality}`);
2662
+ if (e.graph_nodes !== null) L.push(`- Graph nodes: ${e.graph_nodes}`);
2663
+ if (e.graph_edges !== null) L.push(`- Graph edges: ${e.graph_edges}`);
2664
+ L.push(`- Context readiness: ${e.context_readiness}`);
2665
+ L.push("- Guard history: not available");
2666
+ L.push("");
2667
+ L.push("## Estimated Savings", "");
2668
+ const s = r.estimated_savings;
2669
+ const line = (title, l) => {
2670
+ L.push(`### ${title}`, "");
2671
+ L.push(`- Formula: ${l.formula}`);
2672
+ L.push(`- Estimated: ${l.hours} h`, "");
2673
+ };
2674
+ line("Context Preparation", s.context_preparation);
2675
+ line("Review Effort", s.review_effort);
2676
+ line("Clarification Reduction", s.clarification_reduction);
2677
+ line("Onboarding", s.onboarding);
2678
+ line("Architecture Discovery", s.architecture_discovery);
2679
+ L.push("### Drift Prevention", "");
2680
+ L.push("- Not available yet.");
2681
+ L.push(`- Reason: ${s.drift_prevention.reason}`, "");
2682
+ L.push("## Total", "");
2683
+ L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} h`);
2684
+ L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
2685
+ L.push("");
2686
+ L.push("## Confidence", "");
2687
+ L.push(`Confidence: ${r.confidence.level}`, "", "Reason:");
2688
+ for (const reason of r.confidence.reasons) L.push(`- ${reason}`);
2689
+ L.push("");
2690
+ L.push("## Suggested Actions", "");
2691
+ r.suggested_actions.forEach((act, i) => L.push(`${i + 1}. ${act}`));
2692
+ L.push("");
2693
+ return L.join("\n");
2694
+ }
2695
+ function serializeSavingsJson(r) {
2696
+ return JSON.stringify(r, null, 2) + "\n";
2697
+ }
2698
+
2505
2699
  // src/workitems.ts
2506
2700
  import matter7 from "gray-matter";
2507
2701
  var ACTIVE_STATES2 = /* @__PURE__ */ new Set(["draft", "ready", "in-progress", "blocked"]);
@@ -2714,6 +2908,20 @@ var RESOURCES = [
2714
2908
  }
2715
2909
  return [{ uri: "kaddo://impact-report", text: renderImpactMarkdown(buildImpactReport(root, { scope: "all", scopeSource: "default" })), mimeType: "text/markdown" }];
2716
2910
  }
2911
+ },
2912
+ {
2913
+ uri: "kaddo://savings-report",
2914
+ name: "Kaddo savings report",
2915
+ description: "Estimated Savings Report \u2014 the last written report, or generated in memory (read-only).",
2916
+ mimeType: "text/markdown",
2917
+ read: (root) => {
2918
+ const saved = readText(root, ".kaddo/reports/savings-report.md");
2919
+ if (saved) return [{ uri: "kaddo://savings-report", text: saved, mimeType: "text/markdown" }];
2920
+ if (!hasKnowledge(root)) {
2921
+ return text("kaddo://savings-report", "Knowledge repository not found. Run `kaddo bootstrap` first.", "text/plain");
2922
+ }
2923
+ return [{ uri: "kaddo://savings-report", text: renderSavingsMarkdown(buildSavingsReport(root, { scope: "all", scopeSource: "default" })), mimeType: "text/markdown" }];
2924
+ }
2717
2925
  }
2718
2926
  ];
2719
2927
 
@@ -3492,6 +3700,22 @@ function generateImpactReport(root, opts = {}) {
3492
3700
  next_suggested_resources: ["kaddo://impact-report"]
3493
3701
  };
3494
3702
  }
3703
+ function generateSavingsReport(root, opts = {}) {
3704
+ requireConfig(root);
3705
+ requireKnowledge(root);
3706
+ const format = opts.format ?? "markdown";
3707
+ const report = buildSavingsReport(root, { scope: opts.scope ?? "all", scopeSource: opts.scope ? "explicit" : "default" });
3708
+ const content = format === "json" ? serializeSavingsJson(report) : renderSavingsMarkdown(report);
3709
+ const out = opts.output ?? `.kaddo/reports/savings-report.${format === "json" ? "json" : "md"}`;
3710
+ writeDerived(root, out, content);
3711
+ return {
3712
+ status: "ok",
3713
+ files_written: [out.replace(/\\/g, "/")],
3714
+ summary: `Savings report generated (${format}, ${report.scope} scope, ~${report.total.estimated_hours_saved}h).`,
3715
+ warnings: report.assumptions_source === "default" ? ["Using default assumptions \u2014 run `kaddo savings init` to calibrate."] : [],
3716
+ next_suggested_resources: ["kaddo://savings-report"]
3717
+ };
3718
+ }
3495
3719
  function generateCapsuleDraft(root) {
3496
3720
  const config = requireConfig(root);
3497
3721
  requireKnowledge(root);
@@ -3692,6 +3916,23 @@ function createServer(root) {
3692
3916
  () => generateImpactReport(root, { format: args.format, scope: args.scope, output: args.output })
3693
3917
  )
3694
3918
  );
3919
+ server.registerTool(
3920
+ "kaddo_generate_savings_report",
3921
+ {
3922
+ title: "Generate savings report",
3923
+ description: `Write the Estimated Savings Report under .kaddo/reports/. format: markdown (default) or json; scope: active or all (default all). Evidence-based estimates, not exact ROI. ${DERIVED_NOTE}`,
3924
+ inputSchema: {
3925
+ format: z2.enum(["markdown", "json"]).optional(),
3926
+ scope: z2.enum(["active", "all"]).optional(),
3927
+ output: z2.string().optional()
3928
+ }
3929
+ },
3930
+ async (args) => generated(
3931
+ root,
3932
+ "generate savings report",
3933
+ () => generateSavingsReport(root, { format: args.format, scope: args.scope, output: args.output })
3934
+ )
3935
+ );
3695
3936
  server.registerTool(
3696
3937
  "kaddo_generate_capsule_draft",
3697
3938
  { title: "Generate capsule draft", description: `Write a capsule DRAFT under .kaddo/exports/ (never registers it). ${DERIVED_NOTE}`, inputSchema: {} },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/mcp",
3
- "version": "3.23.2",
3
+ "version": "3.24.0",
4
4
  "description": "MCP server for Kaddo project knowledge: read-only resources/tools/prompts plus safe derived generation under .kaddo/.",
5
5
  "license": "MIT",
6
6
  "repository": {