@kaddo/mcp 3.23.2 → 3.25.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 +562 -6
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2086,6 +2086,129 @@ function renderExplanationAgent(exp) {
2086
2086
  return JSON.stringify({ ...rest, mapped_modules: mappedModules }, null, 2) + "\n";
2087
2087
  }
2088
2088
 
2089
+ // ../cli/src/core/guard-history.ts
2090
+ var HISTORY_DIR = ".kaddo/history";
2091
+ var RUNS_PATH = `${HISTORY_DIR}/guard-runs.jsonl`;
2092
+ var SUMMARY_PATH = `${HISTORY_DIR}/guard-summary.json`;
2093
+ function toPosix5(p) {
2094
+ return p.replace(/\\/g, "/");
2095
+ }
2096
+ function loadGuardRuns(dir) {
2097
+ const p = join(dir, RUNS_PATH);
2098
+ if (!exists(p)) return [];
2099
+ const out = [];
2100
+ for (const line of readFile(p).split(/\r?\n/)) {
2101
+ const t = line.trim();
2102
+ if (!t) continue;
2103
+ try {
2104
+ out.push(JSON.parse(t));
2105
+ } catch {
2106
+ }
2107
+ }
2108
+ return out;
2109
+ }
2110
+ function resolveDriftThreads(runs) {
2111
+ const sorted = [...runs].sort((a, b) => a.generated_at.localeCompare(b.generated_at));
2112
+ const paths = /* @__PURE__ */ new Set();
2113
+ for (const r of sorted) for (const w of r.warnings) paths.add(w.code_path);
2114
+ const threads = [];
2115
+ for (const codePath of paths) {
2116
+ const related = /* @__PURE__ */ new Set();
2117
+ let firstDetected = "";
2118
+ let lastWarn = "";
2119
+ for (const r of sorted) {
2120
+ const w = r.warnings.find((x) => x.code_path === codePath);
2121
+ if (w) {
2122
+ if (!firstDetected) firstDetected = r.generated_at;
2123
+ lastWarn = r.generated_at;
2124
+ for (const a of w.related_artifacts) related.add(a);
2125
+ }
2126
+ }
2127
+ let resolvedAt;
2128
+ for (const r of sorted) {
2129
+ if (r.generated_at <= lastWarn) continue;
2130
+ const touched = r.touched_files.some((f) => toPosix5(f) === codePath);
2131
+ const warnedHere = r.warnings.some((x) => x.code_path === codePath);
2132
+ const updatedRelated = r.updated_artifacts.some((a) => related.has(a));
2133
+ if (touched && updatedRelated && !warnedHere) {
2134
+ resolvedAt = r.generated_at;
2135
+ break;
2136
+ }
2137
+ }
2138
+ threads.push({
2139
+ code_path: codePath,
2140
+ related_artifacts: [...related],
2141
+ first_detected: firstDetected,
2142
+ last_seen: lastWarn,
2143
+ status: resolvedAt ? "resolved" : "open",
2144
+ ...resolvedAt ? { resolved_at: resolvedAt } : {}
2145
+ });
2146
+ }
2147
+ return threads;
2148
+ }
2149
+ function hotspotDir(codePath) {
2150
+ const i = codePath.lastIndexOf("/");
2151
+ return i >= 0 ? codePath.slice(0, i + 1) : codePath;
2152
+ }
2153
+ function computeTrend(runs) {
2154
+ if (runs.length < 2) return { direction: "unknown", reason: "Not enough recorded runs to detect a trend." };
2155
+ const sorted = [...runs].sort((a, b) => a.generated_at.localeCompare(b.generated_at));
2156
+ const mid = Math.floor(sorted.length / 2);
2157
+ const avg = (arr) => arr.length ? arr.reduce((s, r) => s + r.warnings.length, 0) / arr.length : 0;
2158
+ const earlier = avg(sorted.slice(0, mid));
2159
+ const recent = avg(sorted.slice(mid));
2160
+ if (recent < earlier) return { direction: "improving", reason: "Fewer warnings in the last recorded runs." };
2161
+ if (recent > earlier) return { direction: "worsening", reason: "More warnings in the last recorded runs." };
2162
+ return { direction: "stable", reason: "Warning volume is roughly stable across recorded runs." };
2163
+ }
2164
+ function buildGuardHistory(dir) {
2165
+ const runs = loadGuardRuns(dir);
2166
+ if (runs.length === 0) {
2167
+ return {
2168
+ available: false,
2169
+ total_runs: 0,
2170
+ first_run: null,
2171
+ last_run: null,
2172
+ runs_with_warnings: 0,
2173
+ runs_clean: 0,
2174
+ detected: 0,
2175
+ open: 0,
2176
+ resolved: 0,
2177
+ resolution_rate: 0,
2178
+ threads: [],
2179
+ hotspots: [],
2180
+ trend: { direction: "unknown", reason: "No guard history recorded yet." }
2181
+ };
2182
+ }
2183
+ const sorted = [...runs].sort((a, b) => a.generated_at.localeCompare(b.generated_at));
2184
+ const threads = resolveDriftThreads(sorted);
2185
+ const open = threads.filter((t) => t.status === "open").length;
2186
+ const resolved = threads.filter((t) => t.status === "resolved").length;
2187
+ const detected = threads.length;
2188
+ const runsWithWarnings = sorted.filter((r) => r.warnings.length > 0).length;
2189
+ const hotspotMap = /* @__PURE__ */ new Map();
2190
+ for (const t of threads) {
2191
+ const d = hotspotDir(t.code_path);
2192
+ hotspotMap.set(d, (hotspotMap.get(d) ?? 0) + 1);
2193
+ }
2194
+ const hotspots = [...hotspotMap.entries()].map(([path3, warnings]) => ({ path: path3, warnings })).sort((a, b) => b.warnings - a.warnings);
2195
+ return {
2196
+ available: true,
2197
+ total_runs: sorted.length,
2198
+ first_run: sorted[0].generated_at,
2199
+ last_run: sorted[sorted.length - 1].generated_at,
2200
+ runs_with_warnings: runsWithWarnings,
2201
+ runs_clean: sorted.length - runsWithWarnings,
2202
+ detected,
2203
+ open,
2204
+ resolved,
2205
+ resolution_rate: detected > 0 ? Math.round(resolved / detected * 1e3) / 10 : 0,
2206
+ threads,
2207
+ hotspots,
2208
+ trend: computeTrend(sorted)
2209
+ };
2210
+ }
2211
+
2089
2212
  // ../cli/src/core/impact-report.ts
2090
2213
  function isBroadGlob(glob) {
2091
2214
  if (!glob.endsWith("/**")) return false;
@@ -2337,10 +2460,10 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
2337
2460
  ready_quality: byState.ready > 0 ? `${byState.ready} ready` : "not applicable"
2338
2461
  },
2339
2462
  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`." },
2340
- guard_activity: {
2341
- available: false,
2342
- note: "Guard history is not persisted. Run `kaddo guard` to check drift; future versions may store runs for trend analysis."
2343
- },
2463
+ guard_activity: (() => {
2464
+ const gh = buildGuardHistory(dir);
2465
+ return gh.available ? { available: true, runs_recorded: gh.total_runs, detected: gh.detected, open: gh.open, resolved: gh.resolved, resolution_rate: gh.resolution_rate } : { available: false, note: "Guard history is not persisted. Run `kaddo guard --record` to record runs for trend analysis." };
2466
+ })(),
2344
2467
  impact_signals,
2345
2468
  actionable_gaps: gaps,
2346
2469
  score,
@@ -2429,8 +2552,18 @@ function renderImpactMarkdown(r) {
2429
2552
  }
2430
2553
  L.push("");
2431
2554
  L.push("## Guard Activity", "");
2432
- L.push("- Guard history: not available");
2433
- L.push(`- Note: ${r.guard_activity.note}`);
2555
+ if (r.guard_activity.available) {
2556
+ const ga = r.guard_activity;
2557
+ L.push("- Guard history: available");
2558
+ L.push(`- Guard runs recorded: ${ga.runs_recorded}`);
2559
+ L.push(`- Drift warnings detected: ${ga.detected}`);
2560
+ L.push(`- Open warnings: ${ga.open}`);
2561
+ L.push(`- Resolved warnings: ${ga.resolved}`);
2562
+ L.push(`- Resolution rate: ${ga.resolution_rate}%`);
2563
+ } else {
2564
+ L.push("- Guard history: not available");
2565
+ L.push(`- Note: ${r.guard_activity.note}`);
2566
+ }
2434
2567
  L.push("");
2435
2568
  L.push("## Impact Signals", "");
2436
2569
  const s = r.impact_signals;
@@ -2502,6 +2635,327 @@ function serializeImpactJson(r) {
2502
2635
  return JSON.stringify(r, null, 2) + "\n";
2503
2636
  }
2504
2637
 
2638
+ // ../cli/src/core/savings.ts
2639
+ import { parse as parseYaml6 } from "yaml";
2640
+ var DEFAULT_ASSUMPTIONS = {
2641
+ currency: "USD",
2642
+ hourly_cost: 40,
2643
+ context_preparation_minutes_saved_per_work_item: 30,
2644
+ rework_hours_avoided_per_resolved_drift: 2,
2645
+ onboarding_hours_saved_per_new_contributor: 4,
2646
+ review_minutes_saved_per_work_item_with_ownership: 20,
2647
+ clarification_minutes_saved_per_ready_work_item: 25,
2648
+ architecture_discovery_hours_saved_when_graph_good: 3,
2649
+ expected_new_contributors_per_month: 1,
2650
+ expected_work_items_per_month: 8
2651
+ };
2652
+ var SAVINGS_PATH = ".kaddo/savings.yml";
2653
+ function loadAssumptions(dir) {
2654
+ const p = join(dir, SAVINGS_PATH);
2655
+ if (!exists(p)) return { assumptions: { ...DEFAULT_ASSUMPTIONS }, source: "default" };
2656
+ try {
2657
+ const raw = parseYaml6(readFile(p));
2658
+ const a = raw?.assumptions ?? {};
2659
+ const t = raw?.team ?? {};
2660
+ const num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
2661
+ return {
2662
+ source: "file",
2663
+ assumptions: {
2664
+ currency: raw?.currency ?? DEFAULT_ASSUMPTIONS.currency,
2665
+ hourly_cost: num(raw?.hourly_cost, DEFAULT_ASSUMPTIONS.hourly_cost),
2666
+ 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),
2667
+ rework_hours_avoided_per_resolved_drift: num(a.rework_hours_avoided_per_resolved_drift, DEFAULT_ASSUMPTIONS.rework_hours_avoided_per_resolved_drift),
2668
+ onboarding_hours_saved_per_new_contributor: num(a.onboarding_hours_saved_per_new_contributor, DEFAULT_ASSUMPTIONS.onboarding_hours_saved_per_new_contributor),
2669
+ 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),
2670
+ 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),
2671
+ 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),
2672
+ expected_new_contributors_per_month: num(t.expected_new_contributors_per_month, DEFAULT_ASSUMPTIONS.expected_new_contributors_per_month),
2673
+ expected_work_items_per_month: num(t.expected_work_items_per_month, DEFAULT_ASSUMPTIONS.expected_work_items_per_month)
2674
+ }
2675
+ };
2676
+ } catch {
2677
+ return { assumptions: { ...DEFAULT_ASSUMPTIONS }, source: "default" };
2678
+ }
2679
+ }
2680
+ var READINESS_MULT = { Low: 0.25, Medium: 0.5, High: 0.75, "Very High": 1 };
2681
+ var GRAPH_MULT = { empty: 0, sparse: 0.25, partial: 0.6, good: 1, unknown: 0 };
2682
+ var round2 = (n) => Math.round(n * 100) / 100;
2683
+ function parseRatioNumerator(s) {
2684
+ const m = s.match(/^(\d+)/);
2685
+ return m ? Number(m[1]) : 0;
2686
+ }
2687
+ var SAVINGS_DISCLAIMER = "These are evidence-based estimates, not exact ROI. Adjust assumptions with real team data.";
2688
+ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
2689
+ const { assumptions: a, source } = loadAssumptions(dir);
2690
+ const impact = buildImpactReport(dir, { scope: opts.scope, scopeSource: opts.scopeSource }, now);
2691
+ const completed = impact.traceability.completed_work_items;
2692
+ const wiWithOwnership = parseRatioNumerator(impact.traceability.work_items_connected_to_code);
2693
+ const acceptanceCov = impact.knowledge_coverage.find((c) => c.label.toLowerCase().includes("acceptance"));
2694
+ const wiWithAcceptance = acceptanceCov?.have ?? 0;
2695
+ const graphQuality = impact.graph_quality.available ? impact.graph_quality.quality : "unknown";
2696
+ const readiness = impact.context_readiness.level;
2697
+ const ctxPrepH = round2(completed * a.context_preparation_minutes_saved_per_work_item / 60);
2698
+ const reviewH = round2(wiWithOwnership * a.review_minutes_saved_per_work_item_with_ownership / 60);
2699
+ const clarH = round2(wiWithAcceptance * a.clarification_minutes_saved_per_ready_work_item / 60);
2700
+ const readinessMult = READINESS_MULT[readiness] ?? 0.5;
2701
+ const onboardH = round2(a.expected_new_contributors_per_month * a.onboarding_hours_saved_per_new_contributor * readinessMult);
2702
+ const graphMult = GRAPH_MULT[graphQuality] ?? 0;
2703
+ const archH = round2(a.architecture_discovery_hours_saved_when_graph_good * graphMult);
2704
+ const guard = buildGuardHistory(dir);
2705
+ const driftSavings = guard.available && guard.resolved > 0 ? {
2706
+ available: true,
2707
+ hours: round2(guard.resolved * a.rework_hours_avoided_per_resolved_drift),
2708
+ formula: `${guard.resolved} resolved drift warnings \xD7 ${a.rework_hours_avoided_per_resolved_drift} h`
2709
+ } : { available: false, hours: 0, reason: "Guard history is not persisted yet." };
2710
+ const totalHours = round2(ctxPrepH + reviewH + clarH + onboardH + archH + driftSavings.hours);
2711
+ const totalValue = Math.round(totalHours * a.hourly_cost);
2712
+ const reasons = [];
2713
+ let level;
2714
+ if (impact.score === null || impact.score < 60 || graphQuality === "empty") {
2715
+ level = "Low";
2716
+ reasons.push("Impact score is low or the knowledge graph is empty.");
2717
+ } else if (impact.score >= 85 && graphQuality === "good" && source === "file" && guard.available && guard.resolved > 0) {
2718
+ level = "High";
2719
+ reasons.push("Strong evidence, custom assumptions and recorded drift resolution.");
2720
+ } else {
2721
+ level = "Medium";
2722
+ reasons.push("Strong knowledge and graph evidence.");
2723
+ }
2724
+ if (!guard.available) reasons.push("Guard history is not persisted yet \u2014 avoided rework is not estimated.");
2725
+ else reasons.push(`Guard history available (${guard.resolved} resolved drift warning(s)).`);
2726
+ reasons.push(source === "file" ? "Assumptions come from `.kaddo/savings.yml`." : "Savings assumptions are defaults and should be calibrated.");
2727
+ const actions = [];
2728
+ if (source === "default") actions.push("Run `kaddo savings init` to customize assumptions.");
2729
+ actions.push("Calibrate hourly cost and team assumptions with real team data.");
2730
+ actions.push("Run `kaddo impact` to verify the underlying evidence quality.");
2731
+ actions.push("Use this report as directional evidence, not accounting data.");
2732
+ actions.push("Future: persist Guard history to estimate avoided rework.");
2733
+ return {
2734
+ generated_at: now.toISOString(),
2735
+ project: impact.project,
2736
+ scope: impact.scope,
2737
+ currency: a.currency,
2738
+ disclaimer: SAVINGS_DISCLAIMER,
2739
+ assumptions_source: source,
2740
+ assumptions: {
2741
+ hourly_cost: a.hourly_cost,
2742
+ context_preparation_minutes_saved_per_work_item: a.context_preparation_minutes_saved_per_work_item,
2743
+ review_minutes_saved_per_work_item_with_ownership: a.review_minutes_saved_per_work_item_with_ownership,
2744
+ clarification_minutes_saved_per_ready_work_item: a.clarification_minutes_saved_per_ready_work_item,
2745
+ onboarding_hours_saved_per_new_contributor: a.onboarding_hours_saved_per_new_contributor,
2746
+ expected_new_contributors_per_month: a.expected_new_contributors_per_month,
2747
+ architecture_discovery_hours_saved_when_graph_good: a.architecture_discovery_hours_saved_when_graph_good
2748
+ },
2749
+ evidence: {
2750
+ knowledge_impact_score: impact.score,
2751
+ completed_work_items: completed,
2752
+ ownership_coverage_percent: impact.ownership_coverage.coverage_percent,
2753
+ work_items_with_acceptance_criteria: acceptanceCov ? `${acceptanceCov.have}/${acceptanceCov.total}` : "0/0",
2754
+ graph_quality: graphQuality,
2755
+ graph_nodes: impact.graph_quality.available ? impact.graph_quality.nodes : null,
2756
+ graph_edges: impact.graph_quality.available ? impact.graph_quality.edges : null,
2757
+ context_readiness: readiness,
2758
+ guard_history_available: guard.available
2759
+ },
2760
+ estimated_savings: {
2761
+ context_preparation: { hours: ctxPrepH, formula: `${completed} Work Items \xD7 ${a.context_preparation_minutes_saved_per_work_item} min` },
2762
+ review_effort: { hours: reviewH, formula: `${wiWithOwnership} Work Items with ownership \xD7 ${a.review_minutes_saved_per_work_item_with_ownership} min` },
2763
+ clarification_reduction: { hours: clarH, formula: `${wiWithAcceptance} Work Items with Acceptance Criteria \xD7 ${a.clarification_minutes_saved_per_ready_work_item} min` },
2764
+ 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` },
2765
+ architecture_discovery: { hours: archH, formula: `${a.architecture_discovery_hours_saved_when_graph_good} h \xD7 ${graphMult.toFixed(2)} graph multiplier` },
2766
+ drift_prevention: driftSavings
2767
+ },
2768
+ total: { estimated_hours_saved: totalHours, estimated_value: totalValue, currency: a.currency },
2769
+ confidence: { level, reasons },
2770
+ suggested_actions: actions
2771
+ };
2772
+ }
2773
+ function renderSavingsMarkdown(r) {
2774
+ const L = [];
2775
+ L.push("# Kaddo Estimated Savings Report", "");
2776
+ L.push(`Generated at: ${r.generated_at}`);
2777
+ L.push(`Project: ${r.project}`);
2778
+ L.push(`Scope: ${r.scope}`);
2779
+ L.push(`Currency: ${r.currency}`);
2780
+ L.push("");
2781
+ L.push("## Disclaimer", "");
2782
+ L.push(r.disclaimer, "");
2783
+ L.push("## Executive Summary", "");
2784
+ L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} hours`);
2785
+ L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
2786
+ L.push(`- Confidence: ${r.confidence.level}`);
2787
+ if (r.assumptions_source === "default") L.push("- Using default assumptions. Run `kaddo savings init` to customize them.");
2788
+ L.push("");
2789
+ L.push("## Assumptions", "");
2790
+ L.push(`- Hourly cost: ${r.assumptions.hourly_cost} ${r.currency}`);
2791
+ L.push(`- Context preparation saved per Work Item: ${r.assumptions.context_preparation_minutes_saved_per_work_item} min`);
2792
+ L.push(`- Review saved per Work Item with ownership: ${r.assumptions.review_minutes_saved_per_work_item_with_ownership} min`);
2793
+ L.push(`- Clarification saved per Work Item with acceptance criteria: ${r.assumptions.clarification_minutes_saved_per_ready_work_item} min`);
2794
+ L.push(`- Onboarding saved per new contributor: ${r.assumptions.onboarding_hours_saved_per_new_contributor} h`);
2795
+ L.push(`- Expected new contributors per month: ${r.assumptions.expected_new_contributors_per_month}`);
2796
+ L.push(`- Architecture discovery saved when graph is good: ${r.assumptions.architecture_discovery_hours_saved_when_graph_good} h`);
2797
+ L.push("");
2798
+ L.push("## Evidence Used", "");
2799
+ const e = r.evidence;
2800
+ L.push(`- Knowledge Impact Score: ${e.knowledge_impact_score ?? "not available"}${e.knowledge_impact_score !== null ? "/100" : ""}`);
2801
+ L.push(`- Completed Work Items: ${e.completed_work_items}`);
2802
+ L.push(`- Ownership coverage: ${e.ownership_coverage_percent}%`);
2803
+ L.push(`- Work Items with Acceptance Criteria: ${e.work_items_with_acceptance_criteria}`);
2804
+ L.push(`- Graph quality: ${e.graph_quality}`);
2805
+ if (e.graph_nodes !== null) L.push(`- Graph nodes: ${e.graph_nodes}`);
2806
+ if (e.graph_edges !== null) L.push(`- Graph edges: ${e.graph_edges}`);
2807
+ L.push(`- Context readiness: ${e.context_readiness}`);
2808
+ L.push("- Guard history: not available");
2809
+ L.push("");
2810
+ L.push("## Estimated Savings", "");
2811
+ const s = r.estimated_savings;
2812
+ const line = (title, l) => {
2813
+ L.push(`### ${title}`, "");
2814
+ L.push(`- Formula: ${l.formula}`);
2815
+ L.push(`- Estimated: ${l.hours} h`, "");
2816
+ };
2817
+ line("Context Preparation", s.context_preparation);
2818
+ line("Review Effort", s.review_effort);
2819
+ line("Clarification Reduction", s.clarification_reduction);
2820
+ line("Onboarding", s.onboarding);
2821
+ line("Architecture Discovery", s.architecture_discovery);
2822
+ L.push("### Drift Prevention", "");
2823
+ if (s.drift_prevention.available) {
2824
+ L.push(`- Formula: ${s.drift_prevention.formula}`);
2825
+ L.push(`- Estimated: ${s.drift_prevention.hours} h`, "");
2826
+ } else {
2827
+ L.push("- Not available yet.");
2828
+ L.push(`- Reason: ${s.drift_prevention.reason}`, "");
2829
+ }
2830
+ L.push("## Total", "");
2831
+ L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} h`);
2832
+ L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
2833
+ L.push("");
2834
+ L.push("## Confidence", "");
2835
+ L.push(`Confidence: ${r.confidence.level}`, "", "Reason:");
2836
+ for (const reason of r.confidence.reasons) L.push(`- ${reason}`);
2837
+ L.push("");
2838
+ L.push("## Suggested Actions", "");
2839
+ r.suggested_actions.forEach((act, i) => L.push(`${i + 1}. ${act}`));
2840
+ L.push("");
2841
+ return L.join("\n");
2842
+ }
2843
+ function serializeSavingsJson(r) {
2844
+ return JSON.stringify(r, null, 2) + "\n";
2845
+ }
2846
+
2847
+ // ../cli/src/core/drift-report.ts
2848
+ function buildDriftReport(dir, now = /* @__PURE__ */ new Date()) {
2849
+ const h = buildGuardHistory(dir);
2850
+ const project = loadConfig(dir)?.project.name ?? "unknown";
2851
+ const actions = [];
2852
+ if (!h.available) {
2853
+ actions.push("Run `kaddo guard --record` to start recording guard history.");
2854
+ } else {
2855
+ if (h.open > 0) actions.push("Review open drift warnings and update related Work Items or ownership.");
2856
+ actions.push("Run `kaddo guard --record` before important commits.");
2857
+ actions.push("Use this report to calibrate estimated drift-prevention savings.");
2858
+ }
2859
+ return {
2860
+ generated_at: now.toISOString(),
2861
+ project,
2862
+ guard_history: {
2863
+ available: h.available,
2864
+ total_runs: h.total_runs,
2865
+ first_run: h.first_run,
2866
+ last_run: h.last_run,
2867
+ runs_with_warnings: h.runs_with_warnings,
2868
+ runs_clean: h.runs_clean
2869
+ },
2870
+ drift_warnings: { detected: h.detected, open: h.open, resolved: h.resolved, resolution_rate: h.resolution_rate },
2871
+ hotspots: h.hotspots,
2872
+ trend: h.trend,
2873
+ threads: h.threads,
2874
+ suggested_actions: actions
2875
+ };
2876
+ }
2877
+ function renderDriftMarkdown(r) {
2878
+ const L = [];
2879
+ L.push("# Kaddo Drift Trend Report", "");
2880
+ L.push(`Generated at: ${r.generated_at}`);
2881
+ L.push(`Project: ${r.project}`);
2882
+ L.push("");
2883
+ if (!r.guard_history.available) {
2884
+ L.push("## Guard History", "");
2885
+ L.push("No guard history recorded yet.");
2886
+ L.push("");
2887
+ L.push("Run `kaddo guard --record` to start recording drift evidence.");
2888
+ L.push("");
2889
+ L.push("## Suggested Actions", "");
2890
+ r.suggested_actions.forEach((a, i) => L.push(`${i + 1}. ${a}`));
2891
+ L.push("");
2892
+ return L.join("\n");
2893
+ }
2894
+ const w = r.drift_warnings;
2895
+ L.push("## Executive Summary", "");
2896
+ L.push(`- Guard runs recorded: ${r.guard_history.total_runs}`);
2897
+ L.push(`- Drift warnings detected: ${w.detected}`);
2898
+ L.push(`- Open warnings: ${w.open}`);
2899
+ L.push(`- Resolved warnings: ${w.resolved}`);
2900
+ L.push(`- Resolution rate: ${w.resolution_rate}%`);
2901
+ if (r.hotspots.length > 0) L.push(`- Most affected area: ${r.hotspots[0].path}`);
2902
+ L.push("");
2903
+ L.push("## Guard History", "");
2904
+ L.push(`- First run: ${r.guard_history.first_run}`);
2905
+ L.push(`- Last run: ${r.guard_history.last_run}`);
2906
+ L.push(`- Runs with warnings: ${r.guard_history.runs_with_warnings}/${r.guard_history.total_runs}`);
2907
+ L.push(`- Runs clean: ${r.guard_history.runs_clean}/${r.guard_history.total_runs}`);
2908
+ L.push("");
2909
+ L.push("## Drift Warnings", "");
2910
+ const open = r.threads.filter((t) => t.status === "open");
2911
+ const resolved = r.threads.filter((t) => t.status === "resolved");
2912
+ L.push("### Open", "");
2913
+ if (open.length === 0) L.push("_None._", "");
2914
+ for (const t of open) {
2915
+ L.push(`- ${t.code_path}`);
2916
+ L.push(` - Related artifacts: ${t.related_artifacts.join(", ") || "\u2014"}`);
2917
+ L.push(` - First detected: ${t.first_detected}`);
2918
+ L.push(` - Last seen: ${t.last_seen}`);
2919
+ L.push(" - Status: open");
2920
+ }
2921
+ L.push("");
2922
+ L.push("### Resolved", "");
2923
+ if (resolved.length === 0) L.push("_None._", "");
2924
+ for (const t of resolved) {
2925
+ L.push(`- ${t.code_path}`);
2926
+ L.push(` - Related artifacts: ${t.related_artifacts.join(", ") || "\u2014"}`);
2927
+ L.push(` - First detected: ${t.first_detected}`);
2928
+ L.push(` - Resolved at: ${t.resolved_at}`);
2929
+ L.push(" - Status: resolved");
2930
+ }
2931
+ L.push("");
2932
+ L.push("## Hotspots", "");
2933
+ if (r.hotspots.length === 0) L.push("_None._");
2934
+ for (const h of r.hotspots) L.push(`- ${h.path}: ${h.warnings} warning(s)`);
2935
+ L.push("");
2936
+ L.push("## Trend", "");
2937
+ L.push(`- Recent direction: ${r.trend.direction}`);
2938
+ L.push(`- Reason: ${r.trend.reason}`);
2939
+ L.push("");
2940
+ L.push("## Suggested Actions", "");
2941
+ r.suggested_actions.forEach((a, i) => L.push(`${i + 1}. ${a}`));
2942
+ L.push("");
2943
+ return L.join("\n");
2944
+ }
2945
+ function serializeDriftJson(r) {
2946
+ const out = {
2947
+ generated_at: r.generated_at,
2948
+ project: r.project,
2949
+ guard_history: r.guard_history,
2950
+ drift_warnings: r.drift_warnings,
2951
+ hotspots: r.hotspots,
2952
+ trend: r.trend,
2953
+ threads: r.threads,
2954
+ suggested_actions: r.suggested_actions
2955
+ };
2956
+ return JSON.stringify(out, null, 2) + "\n";
2957
+ }
2958
+
2505
2959
  // src/workitems.ts
2506
2960
  import matter7 from "gray-matter";
2507
2961
  var ACTIVE_STATES2 = /* @__PURE__ */ new Set(["draft", "ready", "in-progress", "blocked"]);
@@ -2714,6 +3168,44 @@ var RESOURCES = [
2714
3168
  }
2715
3169
  return [{ uri: "kaddo://impact-report", text: renderImpactMarkdown(buildImpactReport(root, { scope: "all", scopeSource: "default" })), mimeType: "text/markdown" }];
2716
3170
  }
3171
+ },
3172
+ {
3173
+ uri: "kaddo://savings-report",
3174
+ name: "Kaddo savings report",
3175
+ description: "Estimated Savings Report \u2014 the last written report, or generated in memory (read-only).",
3176
+ mimeType: "text/markdown",
3177
+ read: (root) => {
3178
+ const saved = readText(root, ".kaddo/reports/savings-report.md");
3179
+ if (saved) return [{ uri: "kaddo://savings-report", text: saved, mimeType: "text/markdown" }];
3180
+ if (!hasKnowledge(root)) {
3181
+ return text("kaddo://savings-report", "Knowledge repository not found. Run `kaddo bootstrap` first.", "text/plain");
3182
+ }
3183
+ return [{ uri: "kaddo://savings-report", text: renderSavingsMarkdown(buildSavingsReport(root, { scope: "all", scopeSource: "default" })), mimeType: "text/markdown" }];
3184
+ }
3185
+ },
3186
+ {
3187
+ uri: "kaddo://drift-report",
3188
+ name: "Kaddo drift report",
3189
+ description: "Drift Trend Report \u2014 the last written report, or generated in memory (read-only).",
3190
+ mimeType: "text/markdown",
3191
+ read: (root) => {
3192
+ const saved = readText(root, ".kaddo/reports/drift-report.md");
3193
+ if (saved) return [{ uri: "kaddo://drift-report", text: saved, mimeType: "text/markdown" }];
3194
+ return [{ uri: "kaddo://drift-report", text: renderDriftMarkdown(buildDriftReport(root)), mimeType: "text/markdown" }];
3195
+ }
3196
+ },
3197
+ {
3198
+ uri: "kaddo://guard-history",
3199
+ name: "Kaddo guard history",
3200
+ description: "Recorded guard runs (.kaddo/history/guard-runs.jsonl) \u2014 read-only.",
3201
+ mimeType: "application/jsonl",
3202
+ read: (root) => {
3203
+ const runs = readText(root, ".kaddo/history/guard-runs.jsonl");
3204
+ if (runs === null) {
3205
+ return text("kaddo://guard-history", "No guard history recorded yet. Run `kaddo guard --record`.", "text/plain");
3206
+ }
3207
+ return [{ uri: "kaddo://guard-history", text: runs, mimeType: "application/jsonl" }];
3208
+ }
2717
3209
  }
2718
3210
  ];
2719
3211
 
@@ -3492,6 +3984,37 @@ function generateImpactReport(root, opts = {}) {
3492
3984
  next_suggested_resources: ["kaddo://impact-report"]
3493
3985
  };
3494
3986
  }
3987
+ function generateSavingsReport(root, opts = {}) {
3988
+ requireConfig(root);
3989
+ requireKnowledge(root);
3990
+ const format = opts.format ?? "markdown";
3991
+ const report = buildSavingsReport(root, { scope: opts.scope ?? "all", scopeSource: opts.scope ? "explicit" : "default" });
3992
+ const content = format === "json" ? serializeSavingsJson(report) : renderSavingsMarkdown(report);
3993
+ const out = opts.output ?? `.kaddo/reports/savings-report.${format === "json" ? "json" : "md"}`;
3994
+ writeDerived(root, out, content);
3995
+ return {
3996
+ status: "ok",
3997
+ files_written: [out.replace(/\\/g, "/")],
3998
+ summary: `Savings report generated (${format}, ${report.scope} scope, ~${report.total.estimated_hours_saved}h).`,
3999
+ warnings: report.assumptions_source === "default" ? ["Using default assumptions \u2014 run `kaddo savings init` to calibrate."] : [],
4000
+ next_suggested_resources: ["kaddo://savings-report"]
4001
+ };
4002
+ }
4003
+ function generateDriftReport(root, opts = {}) {
4004
+ requireConfig(root);
4005
+ const format = opts.format ?? "markdown";
4006
+ const report = buildDriftReport(root);
4007
+ const content = format === "json" ? serializeDriftJson(report) : renderDriftMarkdown(report);
4008
+ const out = opts.output ?? `.kaddo/reports/drift-report.${format === "json" ? "json" : "md"}`;
4009
+ writeDerived(root, out, content);
4010
+ return {
4011
+ status: "ok",
4012
+ files_written: [out.replace(/\\/g, "/")],
4013
+ summary: report.guard_history.available ? `Drift report generated (${report.drift_warnings.open} open, ${report.drift_warnings.resolved} resolved).` : "Drift report generated (no guard history yet \u2014 run `kaddo guard --record`).",
4014
+ warnings: report.guard_history.available ? [] : ["No guard history recorded yet."],
4015
+ next_suggested_resources: ["kaddo://drift-report"]
4016
+ };
4017
+ }
3495
4018
  function generateCapsuleDraft(root) {
3496
4019
  const config = requireConfig(root);
3497
4020
  requireKnowledge(root);
@@ -3692,6 +4215,39 @@ function createServer(root) {
3692
4215
  () => generateImpactReport(root, { format: args.format, scope: args.scope, output: args.output })
3693
4216
  )
3694
4217
  );
4218
+ server.registerTool(
4219
+ "kaddo_generate_savings_report",
4220
+ {
4221
+ title: "Generate savings report",
4222
+ 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}`,
4223
+ inputSchema: {
4224
+ format: z2.enum(["markdown", "json"]).optional(),
4225
+ scope: z2.enum(["active", "all"]).optional(),
4226
+ output: z2.string().optional()
4227
+ }
4228
+ },
4229
+ async (args) => generated(
4230
+ root,
4231
+ "generate savings report",
4232
+ () => generateSavingsReport(root, { format: args.format, scope: args.scope, output: args.output })
4233
+ )
4234
+ );
4235
+ server.registerTool(
4236
+ "kaddo_generate_drift_report",
4237
+ {
4238
+ title: "Generate drift report",
4239
+ description: `Write the Drift Trend Report under .kaddo/reports/ from recorded guard history. format: markdown (default) or json. Does NOT record guard runs. ${DERIVED_NOTE}`,
4240
+ inputSchema: {
4241
+ format: z2.enum(["markdown", "json"]).optional(),
4242
+ output: z2.string().optional()
4243
+ }
4244
+ },
4245
+ async (args) => generated(
4246
+ root,
4247
+ "generate drift report",
4248
+ () => generateDriftReport(root, { format: args.format, output: args.output })
4249
+ )
4250
+ );
3695
4251
  server.registerTool(
3696
4252
  "kaddo_generate_capsule_draft",
3697
4253
  { 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.25.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": {