@kaddo/cli 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.
- package/README.md +2 -0
- package/dist/index.js +635 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -513,6 +513,8 @@ 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 |
|
|
517
|
+
| v3.25 | Guard history & drift trend: `kaddo guard --record`, `kaddo drift`; feeds impact Guard Activity + savings Drift Prevention; MCP drift/guard-history resources + tool |
|
|
516
518
|
|
|
517
519
|
**Optional modules (installed with `kaddo add`):**
|
|
518
520
|
|
package/dist/index.js
CHANGED
|
@@ -688,6 +688,14 @@ 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"
|
|
695
|
+
},
|
|
696
|
+
drift: {
|
|
697
|
+
question: "Are code and knowledge drifting apart over time?",
|
|
698
|
+
next: "Review open warnings; `kaddo guard --record` before commits"
|
|
691
699
|
}
|
|
692
700
|
};
|
|
693
701
|
function commandFooterLines(name) {
|
|
@@ -5463,6 +5471,162 @@ function collectMatchedDomains(matchedArtifactDomains) {
|
|
|
5463
5471
|
return [...seen];
|
|
5464
5472
|
}
|
|
5465
5473
|
|
|
5474
|
+
// src/core/guard-history.ts
|
|
5475
|
+
import fs2 from "fs";
|
|
5476
|
+
var HISTORY_DIR = ".kaddo/history";
|
|
5477
|
+
var RUNS_PATH = `${HISTORY_DIR}/guard-runs.jsonl`;
|
|
5478
|
+
var SUMMARY_PATH = `${HISTORY_DIR}/guard-summary.json`;
|
|
5479
|
+
function toPosix3(p2) {
|
|
5480
|
+
return p2.replace(/\\/g, "/");
|
|
5481
|
+
}
|
|
5482
|
+
function loadGuardRuns(dir) {
|
|
5483
|
+
const p2 = join(dir, RUNS_PATH);
|
|
5484
|
+
if (!exists(p2)) return [];
|
|
5485
|
+
const out = [];
|
|
5486
|
+
for (const line of readFile(p2).split(/\r?\n/)) {
|
|
5487
|
+
const t = line.trim();
|
|
5488
|
+
if (!t) continue;
|
|
5489
|
+
try {
|
|
5490
|
+
out.push(JSON.parse(t));
|
|
5491
|
+
} catch {
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
return out;
|
|
5495
|
+
}
|
|
5496
|
+
function resolveDriftThreads(runs) {
|
|
5497
|
+
const sorted = [...runs].sort((a, b) => a.generated_at.localeCompare(b.generated_at));
|
|
5498
|
+
const paths = /* @__PURE__ */ new Set();
|
|
5499
|
+
for (const r of sorted) for (const w of r.warnings) paths.add(w.code_path);
|
|
5500
|
+
const threads = [];
|
|
5501
|
+
for (const codePath of paths) {
|
|
5502
|
+
const related = /* @__PURE__ */ new Set();
|
|
5503
|
+
let firstDetected = "";
|
|
5504
|
+
let lastWarn = "";
|
|
5505
|
+
for (const r of sorted) {
|
|
5506
|
+
const w = r.warnings.find((x) => x.code_path === codePath);
|
|
5507
|
+
if (w) {
|
|
5508
|
+
if (!firstDetected) firstDetected = r.generated_at;
|
|
5509
|
+
lastWarn = r.generated_at;
|
|
5510
|
+
for (const a of w.related_artifacts) related.add(a);
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
let resolvedAt;
|
|
5514
|
+
for (const r of sorted) {
|
|
5515
|
+
if (r.generated_at <= lastWarn) continue;
|
|
5516
|
+
const touched = r.touched_files.some((f) => toPosix3(f) === codePath);
|
|
5517
|
+
const warnedHere = r.warnings.some((x) => x.code_path === codePath);
|
|
5518
|
+
const updatedRelated = r.updated_artifacts.some((a) => related.has(a));
|
|
5519
|
+
if (touched && updatedRelated && !warnedHere) {
|
|
5520
|
+
resolvedAt = r.generated_at;
|
|
5521
|
+
break;
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
threads.push({
|
|
5525
|
+
code_path: codePath,
|
|
5526
|
+
related_artifacts: [...related],
|
|
5527
|
+
first_detected: firstDetected,
|
|
5528
|
+
last_seen: lastWarn,
|
|
5529
|
+
status: resolvedAt ? "resolved" : "open",
|
|
5530
|
+
...resolvedAt ? { resolved_at: resolvedAt } : {}
|
|
5531
|
+
});
|
|
5532
|
+
}
|
|
5533
|
+
return threads;
|
|
5534
|
+
}
|
|
5535
|
+
function hotspotDir(codePath) {
|
|
5536
|
+
const i = codePath.lastIndexOf("/");
|
|
5537
|
+
return i >= 0 ? codePath.slice(0, i + 1) : codePath;
|
|
5538
|
+
}
|
|
5539
|
+
function computeTrend(runs) {
|
|
5540
|
+
if (runs.length < 2) return { direction: "unknown", reason: "Not enough recorded runs to detect a trend." };
|
|
5541
|
+
const sorted = [...runs].sort((a, b) => a.generated_at.localeCompare(b.generated_at));
|
|
5542
|
+
const mid = Math.floor(sorted.length / 2);
|
|
5543
|
+
const avg = (arr) => arr.length ? arr.reduce((s, r) => s + r.warnings.length, 0) / arr.length : 0;
|
|
5544
|
+
const earlier = avg(sorted.slice(0, mid));
|
|
5545
|
+
const recent = avg(sorted.slice(mid));
|
|
5546
|
+
if (recent < earlier) return { direction: "improving", reason: "Fewer warnings in the last recorded runs." };
|
|
5547
|
+
if (recent > earlier) return { direction: "worsening", reason: "More warnings in the last recorded runs." };
|
|
5548
|
+
return { direction: "stable", reason: "Warning volume is roughly stable across recorded runs." };
|
|
5549
|
+
}
|
|
5550
|
+
function buildGuardHistory(dir) {
|
|
5551
|
+
const runs = loadGuardRuns(dir);
|
|
5552
|
+
if (runs.length === 0) {
|
|
5553
|
+
return {
|
|
5554
|
+
available: false,
|
|
5555
|
+
total_runs: 0,
|
|
5556
|
+
first_run: null,
|
|
5557
|
+
last_run: null,
|
|
5558
|
+
runs_with_warnings: 0,
|
|
5559
|
+
runs_clean: 0,
|
|
5560
|
+
detected: 0,
|
|
5561
|
+
open: 0,
|
|
5562
|
+
resolved: 0,
|
|
5563
|
+
resolution_rate: 0,
|
|
5564
|
+
threads: [],
|
|
5565
|
+
hotspots: [],
|
|
5566
|
+
trend: { direction: "unknown", reason: "No guard history recorded yet." }
|
|
5567
|
+
};
|
|
5568
|
+
}
|
|
5569
|
+
const sorted = [...runs].sort((a, b) => a.generated_at.localeCompare(b.generated_at));
|
|
5570
|
+
const threads = resolveDriftThreads(sorted);
|
|
5571
|
+
const open = threads.filter((t) => t.status === "open").length;
|
|
5572
|
+
const resolved = threads.filter((t) => t.status === "resolved").length;
|
|
5573
|
+
const detected = threads.length;
|
|
5574
|
+
const runsWithWarnings = sorted.filter((r) => r.warnings.length > 0).length;
|
|
5575
|
+
const hotspotMap = /* @__PURE__ */ new Map();
|
|
5576
|
+
for (const t of threads) {
|
|
5577
|
+
const d = hotspotDir(t.code_path);
|
|
5578
|
+
hotspotMap.set(d, (hotspotMap.get(d) ?? 0) + 1);
|
|
5579
|
+
}
|
|
5580
|
+
const hotspots = [...hotspotMap.entries()].map(([path6, warnings]) => ({ path: path6, warnings })).sort((a, b) => b.warnings - a.warnings);
|
|
5581
|
+
return {
|
|
5582
|
+
available: true,
|
|
5583
|
+
total_runs: sorted.length,
|
|
5584
|
+
first_run: sorted[0].generated_at,
|
|
5585
|
+
last_run: sorted[sorted.length - 1].generated_at,
|
|
5586
|
+
runs_with_warnings: runsWithWarnings,
|
|
5587
|
+
runs_clean: sorted.length - runsWithWarnings,
|
|
5588
|
+
detected,
|
|
5589
|
+
open,
|
|
5590
|
+
resolved,
|
|
5591
|
+
resolution_rate: detected > 0 ? Math.round(resolved / detected * 1e3) / 10 : 0,
|
|
5592
|
+
threads,
|
|
5593
|
+
hotspots,
|
|
5594
|
+
trend: computeTrend(sorted)
|
|
5595
|
+
};
|
|
5596
|
+
}
|
|
5597
|
+
function recordGuardRun(dir, input, now = /* @__PURE__ */ new Date()) {
|
|
5598
|
+
const iso = now.toISOString();
|
|
5599
|
+
const run = {
|
|
5600
|
+
run_id: `guard-${iso.replace(/[:.]/g, "-")}`,
|
|
5601
|
+
generated_at: iso,
|
|
5602
|
+
project: input.project,
|
|
5603
|
+
scope: input.scope,
|
|
5604
|
+
touched_files: input.touched_files,
|
|
5605
|
+
matched_artifacts: input.matched_artifacts,
|
|
5606
|
+
updated_artifacts: input.updated_artifacts,
|
|
5607
|
+
warnings: input.warnings,
|
|
5608
|
+
summary: {
|
|
5609
|
+
touched_files: input.touched_files.length,
|
|
5610
|
+
matched_artifacts: input.matched_artifacts.length,
|
|
5611
|
+
warnings: input.warnings.length
|
|
5612
|
+
}
|
|
5613
|
+
};
|
|
5614
|
+
const runsAbs = join(dir, RUNS_PATH);
|
|
5615
|
+
fs2.mkdirSync(join(dir, HISTORY_DIR), { recursive: true });
|
|
5616
|
+
fs2.appendFileSync(runsAbs, JSON.stringify(run) + "\n", "utf-8");
|
|
5617
|
+
const history = buildGuardHistory(dir);
|
|
5618
|
+
const summary = {
|
|
5619
|
+
generated_at: iso,
|
|
5620
|
+
total_runs: history.total_runs,
|
|
5621
|
+
total_warnings: history.detected,
|
|
5622
|
+
open_warnings: history.open,
|
|
5623
|
+
resolved_warnings: history.resolved,
|
|
5624
|
+
last_run_id: run.run_id
|
|
5625
|
+
};
|
|
5626
|
+
writeFile(join(dir, SUMMARY_PATH), JSON.stringify(summary, null, 2) + "\n");
|
|
5627
|
+
return { run, runsPath: RUNS_PATH, summaryPath: SUMMARY_PATH };
|
|
5628
|
+
}
|
|
5629
|
+
|
|
5466
5630
|
// src/commands/guard.ts
|
|
5467
5631
|
var ARCH_DIR3 = "knowledge";
|
|
5468
5632
|
var CONFIG_PATH2 = ".kaddo/config.yml";
|
|
@@ -5665,6 +5829,47 @@ async function runGuard(opts = {}) {
|
|
|
5665
5829
|
const activeMatches = fyiMatches.filter(
|
|
5666
5830
|
(m) => !isIgnored(ignores, m.artifact.id || m.artifact.title)
|
|
5667
5831
|
);
|
|
5832
|
+
if (opts.record) {
|
|
5833
|
+
const idOf = (m) => m.artifact.id || m.artifact.title;
|
|
5834
|
+
const matchedArtifacts = [...new Set(result.matches.map(idOf))];
|
|
5835
|
+
const updatedArtifacts = [...new Set(result.matches.filter((m) => m.artifactWasModified).map(idOf))];
|
|
5836
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
5837
|
+
for (const m of result.matches) {
|
|
5838
|
+
for (const f of m.matchedFiles) {
|
|
5839
|
+
const e = byPath.get(f) ?? { related: /* @__PURE__ */ new Set(), updated: /* @__PURE__ */ new Set() };
|
|
5840
|
+
e.related.add(idOf(m));
|
|
5841
|
+
if (m.artifactWasModified) e.updated.add(idOf(m));
|
|
5842
|
+
byPath.set(f, e);
|
|
5843
|
+
}
|
|
5844
|
+
}
|
|
5845
|
+
const warnings = [];
|
|
5846
|
+
for (const [code_path, e] of byPath) {
|
|
5847
|
+
if ([...e.related].some((a) => !e.updated.has(a))) {
|
|
5848
|
+
warnings.push({
|
|
5849
|
+
type: "possible-knowledge-drift",
|
|
5850
|
+
code_path,
|
|
5851
|
+
related_artifacts: [...e.related],
|
|
5852
|
+
updated_artifacts: [...e.updated],
|
|
5853
|
+
status: "open"
|
|
5854
|
+
});
|
|
5855
|
+
}
|
|
5856
|
+
}
|
|
5857
|
+
const project = loadConfig(dir)?.project.name ?? "unknown";
|
|
5858
|
+
const rec = recordGuardRun(dir, {
|
|
5859
|
+
project,
|
|
5860
|
+
scope: includeArchived ? "active-completed-archived" : "active-and-completed",
|
|
5861
|
+
touched_files: touchedFiles,
|
|
5862
|
+
matched_artifacts: matchedArtifacts,
|
|
5863
|
+
updated_artifacts: updatedArtifacts,
|
|
5864
|
+
warnings
|
|
5865
|
+
});
|
|
5866
|
+
if (!jsonMode) {
|
|
5867
|
+
console.log("");
|
|
5868
|
+
console.log("Guard run recorded:");
|
|
5869
|
+
console.log(`- ${rec.runsPath}`);
|
|
5870
|
+
console.log(`- ${rec.summaryPath}`);
|
|
5871
|
+
}
|
|
5872
|
+
}
|
|
5668
5873
|
if (jsonMode) {
|
|
5669
5874
|
const ownerMapCI = loadOwners(dir);
|
|
5670
5875
|
const matchedDomainsCI = collectMatchedDomains(activeMatches.map((m) => m.artifact.domains));
|
|
@@ -6102,14 +6307,14 @@ var ALL_STATUSES = ["draft", "ready", "in-progress", "blocked", "completed"];
|
|
|
6102
6307
|
function scopeStatuses(scope) {
|
|
6103
6308
|
return scope === "all" ? { included: ALL_STATUSES, excluded: ["archived"] } : { included: ACTIVE_STATUSES, excluded: ["completed", "archived"] };
|
|
6104
6309
|
}
|
|
6105
|
-
function
|
|
6310
|
+
function toPosix4(p2) {
|
|
6106
6311
|
return p2.replace(/\\/g, "/");
|
|
6107
6312
|
}
|
|
6108
6313
|
function slug(s) {
|
|
6109
6314
|
return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
6110
6315
|
}
|
|
6111
6316
|
function isAdr(a) {
|
|
6112
|
-
return
|
|
6317
|
+
return toPosix4(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
|
|
6113
6318
|
}
|
|
6114
6319
|
function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
6115
6320
|
const scope = opts.scope ?? "active";
|
|
@@ -6294,14 +6499,14 @@ function loadGraphSummary(dir) {
|
|
|
6294
6499
|
|
|
6295
6500
|
// src/core/graph-hints.ts
|
|
6296
6501
|
var KNOWLEDGE3 = "knowledge";
|
|
6297
|
-
function
|
|
6502
|
+
function toPosix5(p2) {
|
|
6298
6503
|
return p2.replace(/\\/g, "/");
|
|
6299
6504
|
}
|
|
6300
6505
|
function slug2(s) {
|
|
6301
6506
|
return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
6302
6507
|
}
|
|
6303
6508
|
function isAdr2(a) {
|
|
6304
|
-
return
|
|
6509
|
+
return toPosix5(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
|
|
6305
6510
|
}
|
|
6306
6511
|
function capabilityHeadings(dir) {
|
|
6307
6512
|
const p2 = join(dir, KNOWLEDGE3, "product", "capabilities.md");
|
|
@@ -11168,10 +11373,10 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
|
11168
11373
|
ready_quality: byState.ready > 0 ? `${byState.ready} ready` : "not applicable"
|
|
11169
11374
|
},
|
|
11170
11375
|
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`." },
|
|
11171
|
-
guard_activity: {
|
|
11172
|
-
|
|
11173
|
-
note: "Guard history is not persisted. Run `kaddo guard` to
|
|
11174
|
-
},
|
|
11376
|
+
guard_activity: (() => {
|
|
11377
|
+
const gh = buildGuardHistory(dir);
|
|
11378
|
+
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." };
|
|
11379
|
+
})(),
|
|
11175
11380
|
impact_signals,
|
|
11176
11381
|
actionable_gaps: gaps,
|
|
11177
11382
|
score,
|
|
@@ -11260,8 +11465,18 @@ function renderImpactMarkdown(r) {
|
|
|
11260
11465
|
}
|
|
11261
11466
|
L.push("");
|
|
11262
11467
|
L.push("## Guard Activity", "");
|
|
11263
|
-
|
|
11264
|
-
|
|
11468
|
+
if (r.guard_activity.available) {
|
|
11469
|
+
const ga = r.guard_activity;
|
|
11470
|
+
L.push("- Guard history: available");
|
|
11471
|
+
L.push(`- Guard runs recorded: ${ga.runs_recorded}`);
|
|
11472
|
+
L.push(`- Drift warnings detected: ${ga.detected}`);
|
|
11473
|
+
L.push(`- Open warnings: ${ga.open}`);
|
|
11474
|
+
L.push(`- Resolved warnings: ${ga.resolved}`);
|
|
11475
|
+
L.push(`- Resolution rate: ${ga.resolution_rate}%`);
|
|
11476
|
+
} else {
|
|
11477
|
+
L.push("- Guard history: not available");
|
|
11478
|
+
L.push(`- Note: ${r.guard_activity.note}`);
|
|
11479
|
+
}
|
|
11265
11480
|
L.push("");
|
|
11266
11481
|
L.push("## Impact Signals", "");
|
|
11267
11482
|
const s = r.impact_signals;
|
|
@@ -11352,6 +11567,400 @@ function runReportImpact(opts = {}) {
|
|
|
11352
11567
|
console.log(content);
|
|
11353
11568
|
}
|
|
11354
11569
|
|
|
11570
|
+
// src/core/savings.ts
|
|
11571
|
+
import { parse as parseYaml15 } from "yaml";
|
|
11572
|
+
var DEFAULT_ASSUMPTIONS = {
|
|
11573
|
+
currency: "USD",
|
|
11574
|
+
hourly_cost: 40,
|
|
11575
|
+
context_preparation_minutes_saved_per_work_item: 30,
|
|
11576
|
+
rework_hours_avoided_per_resolved_drift: 2,
|
|
11577
|
+
onboarding_hours_saved_per_new_contributor: 4,
|
|
11578
|
+
review_minutes_saved_per_work_item_with_ownership: 20,
|
|
11579
|
+
clarification_minutes_saved_per_ready_work_item: 25,
|
|
11580
|
+
architecture_discovery_hours_saved_when_graph_good: 3,
|
|
11581
|
+
expected_new_contributors_per_month: 1,
|
|
11582
|
+
expected_work_items_per_month: 8
|
|
11583
|
+
};
|
|
11584
|
+
var SAVINGS_PATH = ".kaddo/savings.yml";
|
|
11585
|
+
function loadAssumptions(dir) {
|
|
11586
|
+
const p2 = join(dir, SAVINGS_PATH);
|
|
11587
|
+
if (!exists(p2)) return { assumptions: { ...DEFAULT_ASSUMPTIONS }, source: "default" };
|
|
11588
|
+
try {
|
|
11589
|
+
const raw = parseYaml15(readFile(p2));
|
|
11590
|
+
const a = raw?.assumptions ?? {};
|
|
11591
|
+
const t = raw?.team ?? {};
|
|
11592
|
+
const num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
|
|
11593
|
+
return {
|
|
11594
|
+
source: "file",
|
|
11595
|
+
assumptions: {
|
|
11596
|
+
currency: raw?.currency ?? DEFAULT_ASSUMPTIONS.currency,
|
|
11597
|
+
hourly_cost: num(raw?.hourly_cost, DEFAULT_ASSUMPTIONS.hourly_cost),
|
|
11598
|
+
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),
|
|
11599
|
+
rework_hours_avoided_per_resolved_drift: num(a.rework_hours_avoided_per_resolved_drift, DEFAULT_ASSUMPTIONS.rework_hours_avoided_per_resolved_drift),
|
|
11600
|
+
onboarding_hours_saved_per_new_contributor: num(a.onboarding_hours_saved_per_new_contributor, DEFAULT_ASSUMPTIONS.onboarding_hours_saved_per_new_contributor),
|
|
11601
|
+
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),
|
|
11602
|
+
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),
|
|
11603
|
+
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),
|
|
11604
|
+
expected_new_contributors_per_month: num(t.expected_new_contributors_per_month, DEFAULT_ASSUMPTIONS.expected_new_contributors_per_month),
|
|
11605
|
+
expected_work_items_per_month: num(t.expected_work_items_per_month, DEFAULT_ASSUMPTIONS.expected_work_items_per_month)
|
|
11606
|
+
}
|
|
11607
|
+
};
|
|
11608
|
+
} catch {
|
|
11609
|
+
return { assumptions: { ...DEFAULT_ASSUMPTIONS }, source: "default" };
|
|
11610
|
+
}
|
|
11611
|
+
}
|
|
11612
|
+
function savingsTemplate() {
|
|
11613
|
+
return [
|
|
11614
|
+
"# Kaddo savings assumptions (VS-062). Estimates are evidence-based, not exact ROI.",
|
|
11615
|
+
"# Edit these to match your team, then run `kaddo savings`.",
|
|
11616
|
+
"currency: USD",
|
|
11617
|
+
"",
|
|
11618
|
+
"hourly_cost: 40",
|
|
11619
|
+
"",
|
|
11620
|
+
"assumptions:",
|
|
11621
|
+
" context_preparation_minutes_saved_per_work_item: 30",
|
|
11622
|
+
" rework_hours_avoided_per_resolved_drift: 2",
|
|
11623
|
+
" onboarding_hours_saved_per_new_contributor: 4",
|
|
11624
|
+
" review_minutes_saved_per_work_item_with_ownership: 20",
|
|
11625
|
+
" clarification_minutes_saved_per_ready_work_item: 25",
|
|
11626
|
+
" architecture_discovery_hours_saved_when_graph_good: 3",
|
|
11627
|
+
"",
|
|
11628
|
+
"team:",
|
|
11629
|
+
" expected_new_contributors_per_month: 1",
|
|
11630
|
+
" expected_work_items_per_month: 8",
|
|
11631
|
+
""
|
|
11632
|
+
].join("\n");
|
|
11633
|
+
}
|
|
11634
|
+
var READINESS_MULT = { Low: 0.25, Medium: 0.5, High: 0.75, "Very High": 1 };
|
|
11635
|
+
var GRAPH_MULT = { empty: 0, sparse: 0.25, partial: 0.6, good: 1, unknown: 0 };
|
|
11636
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
11637
|
+
function parseRatioNumerator(s) {
|
|
11638
|
+
const m = s.match(/^(\d+)/);
|
|
11639
|
+
return m ? Number(m[1]) : 0;
|
|
11640
|
+
}
|
|
11641
|
+
var SAVINGS_DISCLAIMER = "These are evidence-based estimates, not exact ROI. Adjust assumptions with real team data.";
|
|
11642
|
+
function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
11643
|
+
const { assumptions: a, source } = loadAssumptions(dir);
|
|
11644
|
+
const impact = buildImpactReport(dir, { scope: opts.scope, scopeSource: opts.scopeSource }, now);
|
|
11645
|
+
const completed = impact.traceability.completed_work_items;
|
|
11646
|
+
const wiWithOwnership = parseRatioNumerator(impact.traceability.work_items_connected_to_code);
|
|
11647
|
+
const acceptanceCov = impact.knowledge_coverage.find((c) => c.label.toLowerCase().includes("acceptance"));
|
|
11648
|
+
const wiWithAcceptance = acceptanceCov?.have ?? 0;
|
|
11649
|
+
const graphQuality = impact.graph_quality.available ? impact.graph_quality.quality : "unknown";
|
|
11650
|
+
const readiness = impact.context_readiness.level;
|
|
11651
|
+
const ctxPrepH = round2(completed * a.context_preparation_minutes_saved_per_work_item / 60);
|
|
11652
|
+
const reviewH = round2(wiWithOwnership * a.review_minutes_saved_per_work_item_with_ownership / 60);
|
|
11653
|
+
const clarH = round2(wiWithAcceptance * a.clarification_minutes_saved_per_ready_work_item / 60);
|
|
11654
|
+
const readinessMult = READINESS_MULT[readiness] ?? 0.5;
|
|
11655
|
+
const onboardH = round2(a.expected_new_contributors_per_month * a.onboarding_hours_saved_per_new_contributor * readinessMult);
|
|
11656
|
+
const graphMult = GRAPH_MULT[graphQuality] ?? 0;
|
|
11657
|
+
const archH = round2(a.architecture_discovery_hours_saved_when_graph_good * graphMult);
|
|
11658
|
+
const guard = buildGuardHistory(dir);
|
|
11659
|
+
const driftSavings = guard.available && guard.resolved > 0 ? {
|
|
11660
|
+
available: true,
|
|
11661
|
+
hours: round2(guard.resolved * a.rework_hours_avoided_per_resolved_drift),
|
|
11662
|
+
formula: `${guard.resolved} resolved drift warnings \xD7 ${a.rework_hours_avoided_per_resolved_drift} h`
|
|
11663
|
+
} : { available: false, hours: 0, reason: "Guard history is not persisted yet." };
|
|
11664
|
+
const totalHours = round2(ctxPrepH + reviewH + clarH + onboardH + archH + driftSavings.hours);
|
|
11665
|
+
const totalValue = Math.round(totalHours * a.hourly_cost);
|
|
11666
|
+
const reasons = [];
|
|
11667
|
+
let level;
|
|
11668
|
+
if (impact.score === null || impact.score < 60 || graphQuality === "empty") {
|
|
11669
|
+
level = "Low";
|
|
11670
|
+
reasons.push("Impact score is low or the knowledge graph is empty.");
|
|
11671
|
+
} else if (impact.score >= 85 && graphQuality === "good" && source === "file" && guard.available && guard.resolved > 0) {
|
|
11672
|
+
level = "High";
|
|
11673
|
+
reasons.push("Strong evidence, custom assumptions and recorded drift resolution.");
|
|
11674
|
+
} else {
|
|
11675
|
+
level = "Medium";
|
|
11676
|
+
reasons.push("Strong knowledge and graph evidence.");
|
|
11677
|
+
}
|
|
11678
|
+
if (!guard.available) reasons.push("Guard history is not persisted yet \u2014 avoided rework is not estimated.");
|
|
11679
|
+
else reasons.push(`Guard history available (${guard.resolved} resolved drift warning(s)).`);
|
|
11680
|
+
reasons.push(source === "file" ? "Assumptions come from `.kaddo/savings.yml`." : "Savings assumptions are defaults and should be calibrated.");
|
|
11681
|
+
const actions = [];
|
|
11682
|
+
if (source === "default") actions.push("Run `kaddo savings init` to customize assumptions.");
|
|
11683
|
+
actions.push("Calibrate hourly cost and team assumptions with real team data.");
|
|
11684
|
+
actions.push("Run `kaddo impact` to verify the underlying evidence quality.");
|
|
11685
|
+
actions.push("Use this report as directional evidence, not accounting data.");
|
|
11686
|
+
actions.push("Future: persist Guard history to estimate avoided rework.");
|
|
11687
|
+
return {
|
|
11688
|
+
generated_at: now.toISOString(),
|
|
11689
|
+
project: impact.project,
|
|
11690
|
+
scope: impact.scope,
|
|
11691
|
+
currency: a.currency,
|
|
11692
|
+
disclaimer: SAVINGS_DISCLAIMER,
|
|
11693
|
+
assumptions_source: source,
|
|
11694
|
+
assumptions: {
|
|
11695
|
+
hourly_cost: a.hourly_cost,
|
|
11696
|
+
context_preparation_minutes_saved_per_work_item: a.context_preparation_minutes_saved_per_work_item,
|
|
11697
|
+
review_minutes_saved_per_work_item_with_ownership: a.review_minutes_saved_per_work_item_with_ownership,
|
|
11698
|
+
clarification_minutes_saved_per_ready_work_item: a.clarification_minutes_saved_per_ready_work_item,
|
|
11699
|
+
onboarding_hours_saved_per_new_contributor: a.onboarding_hours_saved_per_new_contributor,
|
|
11700
|
+
expected_new_contributors_per_month: a.expected_new_contributors_per_month,
|
|
11701
|
+
architecture_discovery_hours_saved_when_graph_good: a.architecture_discovery_hours_saved_when_graph_good
|
|
11702
|
+
},
|
|
11703
|
+
evidence: {
|
|
11704
|
+
knowledge_impact_score: impact.score,
|
|
11705
|
+
completed_work_items: completed,
|
|
11706
|
+
ownership_coverage_percent: impact.ownership_coverage.coverage_percent,
|
|
11707
|
+
work_items_with_acceptance_criteria: acceptanceCov ? `${acceptanceCov.have}/${acceptanceCov.total}` : "0/0",
|
|
11708
|
+
graph_quality: graphQuality,
|
|
11709
|
+
graph_nodes: impact.graph_quality.available ? impact.graph_quality.nodes : null,
|
|
11710
|
+
graph_edges: impact.graph_quality.available ? impact.graph_quality.edges : null,
|
|
11711
|
+
context_readiness: readiness,
|
|
11712
|
+
guard_history_available: guard.available
|
|
11713
|
+
},
|
|
11714
|
+
estimated_savings: {
|
|
11715
|
+
context_preparation: { hours: ctxPrepH, formula: `${completed} Work Items \xD7 ${a.context_preparation_minutes_saved_per_work_item} min` },
|
|
11716
|
+
review_effort: { hours: reviewH, formula: `${wiWithOwnership} Work Items with ownership \xD7 ${a.review_minutes_saved_per_work_item_with_ownership} min` },
|
|
11717
|
+
clarification_reduction: { hours: clarH, formula: `${wiWithAcceptance} Work Items with Acceptance Criteria \xD7 ${a.clarification_minutes_saved_per_ready_work_item} min` },
|
|
11718
|
+
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` },
|
|
11719
|
+
architecture_discovery: { hours: archH, formula: `${a.architecture_discovery_hours_saved_when_graph_good} h \xD7 ${graphMult.toFixed(2)} graph multiplier` },
|
|
11720
|
+
drift_prevention: driftSavings
|
|
11721
|
+
},
|
|
11722
|
+
total: { estimated_hours_saved: totalHours, estimated_value: totalValue, currency: a.currency },
|
|
11723
|
+
confidence: { level, reasons },
|
|
11724
|
+
suggested_actions: actions
|
|
11725
|
+
};
|
|
11726
|
+
}
|
|
11727
|
+
function renderSavingsMarkdown(r) {
|
|
11728
|
+
const L = [];
|
|
11729
|
+
L.push("# Kaddo Estimated Savings Report", "");
|
|
11730
|
+
L.push(`Generated at: ${r.generated_at}`);
|
|
11731
|
+
L.push(`Project: ${r.project}`);
|
|
11732
|
+
L.push(`Scope: ${r.scope}`);
|
|
11733
|
+
L.push(`Currency: ${r.currency}`);
|
|
11734
|
+
L.push("");
|
|
11735
|
+
L.push("## Disclaimer", "");
|
|
11736
|
+
L.push(r.disclaimer, "");
|
|
11737
|
+
L.push("## Executive Summary", "");
|
|
11738
|
+
L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} hours`);
|
|
11739
|
+
L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
|
|
11740
|
+
L.push(`- Confidence: ${r.confidence.level}`);
|
|
11741
|
+
if (r.assumptions_source === "default") L.push("- Using default assumptions. Run `kaddo savings init` to customize them.");
|
|
11742
|
+
L.push("");
|
|
11743
|
+
L.push("## Assumptions", "");
|
|
11744
|
+
L.push(`- Hourly cost: ${r.assumptions.hourly_cost} ${r.currency}`);
|
|
11745
|
+
L.push(`- Context preparation saved per Work Item: ${r.assumptions.context_preparation_minutes_saved_per_work_item} min`);
|
|
11746
|
+
L.push(`- Review saved per Work Item with ownership: ${r.assumptions.review_minutes_saved_per_work_item_with_ownership} min`);
|
|
11747
|
+
L.push(`- Clarification saved per Work Item with acceptance criteria: ${r.assumptions.clarification_minutes_saved_per_ready_work_item} min`);
|
|
11748
|
+
L.push(`- Onboarding saved per new contributor: ${r.assumptions.onboarding_hours_saved_per_new_contributor} h`);
|
|
11749
|
+
L.push(`- Expected new contributors per month: ${r.assumptions.expected_new_contributors_per_month}`);
|
|
11750
|
+
L.push(`- Architecture discovery saved when graph is good: ${r.assumptions.architecture_discovery_hours_saved_when_graph_good} h`);
|
|
11751
|
+
L.push("");
|
|
11752
|
+
L.push("## Evidence Used", "");
|
|
11753
|
+
const e = r.evidence;
|
|
11754
|
+
L.push(`- Knowledge Impact Score: ${e.knowledge_impact_score ?? "not available"}${e.knowledge_impact_score !== null ? "/100" : ""}`);
|
|
11755
|
+
L.push(`- Completed Work Items: ${e.completed_work_items}`);
|
|
11756
|
+
L.push(`- Ownership coverage: ${e.ownership_coverage_percent}%`);
|
|
11757
|
+
L.push(`- Work Items with Acceptance Criteria: ${e.work_items_with_acceptance_criteria}`);
|
|
11758
|
+
L.push(`- Graph quality: ${e.graph_quality}`);
|
|
11759
|
+
if (e.graph_nodes !== null) L.push(`- Graph nodes: ${e.graph_nodes}`);
|
|
11760
|
+
if (e.graph_edges !== null) L.push(`- Graph edges: ${e.graph_edges}`);
|
|
11761
|
+
L.push(`- Context readiness: ${e.context_readiness}`);
|
|
11762
|
+
L.push("- Guard history: not available");
|
|
11763
|
+
L.push("");
|
|
11764
|
+
L.push("## Estimated Savings", "");
|
|
11765
|
+
const s = r.estimated_savings;
|
|
11766
|
+
const line = (title, l) => {
|
|
11767
|
+
L.push(`### ${title}`, "");
|
|
11768
|
+
L.push(`- Formula: ${l.formula}`);
|
|
11769
|
+
L.push(`- Estimated: ${l.hours} h`, "");
|
|
11770
|
+
};
|
|
11771
|
+
line("Context Preparation", s.context_preparation);
|
|
11772
|
+
line("Review Effort", s.review_effort);
|
|
11773
|
+
line("Clarification Reduction", s.clarification_reduction);
|
|
11774
|
+
line("Onboarding", s.onboarding);
|
|
11775
|
+
line("Architecture Discovery", s.architecture_discovery);
|
|
11776
|
+
L.push("### Drift Prevention", "");
|
|
11777
|
+
if (s.drift_prevention.available) {
|
|
11778
|
+
L.push(`- Formula: ${s.drift_prevention.formula}`);
|
|
11779
|
+
L.push(`- Estimated: ${s.drift_prevention.hours} h`, "");
|
|
11780
|
+
} else {
|
|
11781
|
+
L.push("- Not available yet.");
|
|
11782
|
+
L.push(`- Reason: ${s.drift_prevention.reason}`, "");
|
|
11783
|
+
}
|
|
11784
|
+
L.push("## Total", "");
|
|
11785
|
+
L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} h`);
|
|
11786
|
+
L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
|
|
11787
|
+
L.push("");
|
|
11788
|
+
L.push("## Confidence", "");
|
|
11789
|
+
L.push(`Confidence: ${r.confidence.level}`, "", "Reason:");
|
|
11790
|
+
for (const reason of r.confidence.reasons) L.push(`- ${reason}`);
|
|
11791
|
+
L.push("");
|
|
11792
|
+
L.push("## Suggested Actions", "");
|
|
11793
|
+
r.suggested_actions.forEach((act, i) => L.push(`${i + 1}. ${act}`));
|
|
11794
|
+
L.push("");
|
|
11795
|
+
return L.join("\n");
|
|
11796
|
+
}
|
|
11797
|
+
function serializeSavingsJson(r) {
|
|
11798
|
+
return JSON.stringify(r, null, 2) + "\n";
|
|
11799
|
+
}
|
|
11800
|
+
|
|
11801
|
+
// src/commands/savings.ts
|
|
11802
|
+
function runSavings(opts = {}) {
|
|
11803
|
+
const dir = cwd();
|
|
11804
|
+
requireConfig(dir);
|
|
11805
|
+
const scope = opts.scope === "active" ? "active" : opts.scope === "all" ? "all" : void 0;
|
|
11806
|
+
const report = buildSavingsReport(dir, { scope });
|
|
11807
|
+
const content = opts.json ? serializeSavingsJson(report) : renderSavingsMarkdown(report);
|
|
11808
|
+
if (opts.output) {
|
|
11809
|
+
intro2("kaddo savings");
|
|
11810
|
+
writeFile(join(dir, opts.output), content);
|
|
11811
|
+
log2.success(`Wrote ${opts.output.replace(/\\/g, "/")}`);
|
|
11812
|
+
printCommandFooter("savings");
|
|
11813
|
+
outro2("Savings report written.");
|
|
11814
|
+
return;
|
|
11815
|
+
}
|
|
11816
|
+
console.log(content);
|
|
11817
|
+
}
|
|
11818
|
+
function runSavingsInit(opts = {}) {
|
|
11819
|
+
const dir = cwd();
|
|
11820
|
+
requireConfig(dir);
|
|
11821
|
+
intro2("kaddo savings init");
|
|
11822
|
+
const rel = ".kaddo/savings.yml";
|
|
11823
|
+
const full = join(dir, rel);
|
|
11824
|
+
if (exists(full) && !opts.force) {
|
|
11825
|
+
log2.warn(`${rel} already exists. Use --force to overwrite.`);
|
|
11826
|
+
outro2("Nothing changed.");
|
|
11827
|
+
return;
|
|
11828
|
+
}
|
|
11829
|
+
writeFile(full, savingsTemplate());
|
|
11830
|
+
log2.success(`Wrote ${rel}`);
|
|
11831
|
+
log2.info("Edit the assumptions, then run `kaddo savings`.");
|
|
11832
|
+
outro2("Savings assumptions ready.");
|
|
11833
|
+
}
|
|
11834
|
+
|
|
11835
|
+
// src/core/drift-report.ts
|
|
11836
|
+
function buildDriftReport(dir, now = /* @__PURE__ */ new Date()) {
|
|
11837
|
+
const h = buildGuardHistory(dir);
|
|
11838
|
+
const project = loadConfig(dir)?.project.name ?? "unknown";
|
|
11839
|
+
const actions = [];
|
|
11840
|
+
if (!h.available) {
|
|
11841
|
+
actions.push("Run `kaddo guard --record` to start recording guard history.");
|
|
11842
|
+
} else {
|
|
11843
|
+
if (h.open > 0) actions.push("Review open drift warnings and update related Work Items or ownership.");
|
|
11844
|
+
actions.push("Run `kaddo guard --record` before important commits.");
|
|
11845
|
+
actions.push("Use this report to calibrate estimated drift-prevention savings.");
|
|
11846
|
+
}
|
|
11847
|
+
return {
|
|
11848
|
+
generated_at: now.toISOString(),
|
|
11849
|
+
project,
|
|
11850
|
+
guard_history: {
|
|
11851
|
+
available: h.available,
|
|
11852
|
+
total_runs: h.total_runs,
|
|
11853
|
+
first_run: h.first_run,
|
|
11854
|
+
last_run: h.last_run,
|
|
11855
|
+
runs_with_warnings: h.runs_with_warnings,
|
|
11856
|
+
runs_clean: h.runs_clean
|
|
11857
|
+
},
|
|
11858
|
+
drift_warnings: { detected: h.detected, open: h.open, resolved: h.resolved, resolution_rate: h.resolution_rate },
|
|
11859
|
+
hotspots: h.hotspots,
|
|
11860
|
+
trend: h.trend,
|
|
11861
|
+
threads: h.threads,
|
|
11862
|
+
suggested_actions: actions
|
|
11863
|
+
};
|
|
11864
|
+
}
|
|
11865
|
+
function renderDriftMarkdown(r) {
|
|
11866
|
+
const L = [];
|
|
11867
|
+
L.push("# Kaddo Drift Trend Report", "");
|
|
11868
|
+
L.push(`Generated at: ${r.generated_at}`);
|
|
11869
|
+
L.push(`Project: ${r.project}`);
|
|
11870
|
+
L.push("");
|
|
11871
|
+
if (!r.guard_history.available) {
|
|
11872
|
+
L.push("## Guard History", "");
|
|
11873
|
+
L.push("No guard history recorded yet.");
|
|
11874
|
+
L.push("");
|
|
11875
|
+
L.push("Run `kaddo guard --record` to start recording drift evidence.");
|
|
11876
|
+
L.push("");
|
|
11877
|
+
L.push("## Suggested Actions", "");
|
|
11878
|
+
r.suggested_actions.forEach((a, i) => L.push(`${i + 1}. ${a}`));
|
|
11879
|
+
L.push("");
|
|
11880
|
+
return L.join("\n");
|
|
11881
|
+
}
|
|
11882
|
+
const w = r.drift_warnings;
|
|
11883
|
+
L.push("## Executive Summary", "");
|
|
11884
|
+
L.push(`- Guard runs recorded: ${r.guard_history.total_runs}`);
|
|
11885
|
+
L.push(`- Drift warnings detected: ${w.detected}`);
|
|
11886
|
+
L.push(`- Open warnings: ${w.open}`);
|
|
11887
|
+
L.push(`- Resolved warnings: ${w.resolved}`);
|
|
11888
|
+
L.push(`- Resolution rate: ${w.resolution_rate}%`);
|
|
11889
|
+
if (r.hotspots.length > 0) L.push(`- Most affected area: ${r.hotspots[0].path}`);
|
|
11890
|
+
L.push("");
|
|
11891
|
+
L.push("## Guard History", "");
|
|
11892
|
+
L.push(`- First run: ${r.guard_history.first_run}`);
|
|
11893
|
+
L.push(`- Last run: ${r.guard_history.last_run}`);
|
|
11894
|
+
L.push(`- Runs with warnings: ${r.guard_history.runs_with_warnings}/${r.guard_history.total_runs}`);
|
|
11895
|
+
L.push(`- Runs clean: ${r.guard_history.runs_clean}/${r.guard_history.total_runs}`);
|
|
11896
|
+
L.push("");
|
|
11897
|
+
L.push("## Drift Warnings", "");
|
|
11898
|
+
const open = r.threads.filter((t) => t.status === "open");
|
|
11899
|
+
const resolved = r.threads.filter((t) => t.status === "resolved");
|
|
11900
|
+
L.push("### Open", "");
|
|
11901
|
+
if (open.length === 0) L.push("_None._", "");
|
|
11902
|
+
for (const t of open) {
|
|
11903
|
+
L.push(`- ${t.code_path}`);
|
|
11904
|
+
L.push(` - Related artifacts: ${t.related_artifacts.join(", ") || "\u2014"}`);
|
|
11905
|
+
L.push(` - First detected: ${t.first_detected}`);
|
|
11906
|
+
L.push(` - Last seen: ${t.last_seen}`);
|
|
11907
|
+
L.push(" - Status: open");
|
|
11908
|
+
}
|
|
11909
|
+
L.push("");
|
|
11910
|
+
L.push("### Resolved", "");
|
|
11911
|
+
if (resolved.length === 0) L.push("_None._", "");
|
|
11912
|
+
for (const t of resolved) {
|
|
11913
|
+
L.push(`- ${t.code_path}`);
|
|
11914
|
+
L.push(` - Related artifacts: ${t.related_artifacts.join(", ") || "\u2014"}`);
|
|
11915
|
+
L.push(` - First detected: ${t.first_detected}`);
|
|
11916
|
+
L.push(` - Resolved at: ${t.resolved_at}`);
|
|
11917
|
+
L.push(" - Status: resolved");
|
|
11918
|
+
}
|
|
11919
|
+
L.push("");
|
|
11920
|
+
L.push("## Hotspots", "");
|
|
11921
|
+
if (r.hotspots.length === 0) L.push("_None._");
|
|
11922
|
+
for (const h of r.hotspots) L.push(`- ${h.path}: ${h.warnings} warning(s)`);
|
|
11923
|
+
L.push("");
|
|
11924
|
+
L.push("## Trend", "");
|
|
11925
|
+
L.push(`- Recent direction: ${r.trend.direction}`);
|
|
11926
|
+
L.push(`- Reason: ${r.trend.reason}`);
|
|
11927
|
+
L.push("");
|
|
11928
|
+
L.push("## Suggested Actions", "");
|
|
11929
|
+
r.suggested_actions.forEach((a, i) => L.push(`${i + 1}. ${a}`));
|
|
11930
|
+
L.push("");
|
|
11931
|
+
return L.join("\n");
|
|
11932
|
+
}
|
|
11933
|
+
function serializeDriftJson(r) {
|
|
11934
|
+
const out = {
|
|
11935
|
+
generated_at: r.generated_at,
|
|
11936
|
+
project: r.project,
|
|
11937
|
+
guard_history: r.guard_history,
|
|
11938
|
+
drift_warnings: r.drift_warnings,
|
|
11939
|
+
hotspots: r.hotspots,
|
|
11940
|
+
trend: r.trend,
|
|
11941
|
+
threads: r.threads,
|
|
11942
|
+
suggested_actions: r.suggested_actions
|
|
11943
|
+
};
|
|
11944
|
+
return JSON.stringify(out, null, 2) + "\n";
|
|
11945
|
+
}
|
|
11946
|
+
|
|
11947
|
+
// src/commands/drift.ts
|
|
11948
|
+
function runDrift(opts = {}) {
|
|
11949
|
+
const dir = cwd();
|
|
11950
|
+
requireConfig(dir);
|
|
11951
|
+
const report = buildDriftReport(dir);
|
|
11952
|
+
const content = opts.json ? serializeDriftJson(report) : renderDriftMarkdown(report);
|
|
11953
|
+
if (opts.output) {
|
|
11954
|
+
intro2("kaddo drift");
|
|
11955
|
+
writeFile(join(dir, opts.output), content);
|
|
11956
|
+
log2.success(`Wrote ${opts.output.replace(/\\/g, "/")}`);
|
|
11957
|
+
printCommandFooter("drift");
|
|
11958
|
+
outro2("Drift report written.");
|
|
11959
|
+
return;
|
|
11960
|
+
}
|
|
11961
|
+
console.log(content);
|
|
11962
|
+
}
|
|
11963
|
+
|
|
11355
11964
|
// src/index.ts
|
|
11356
11965
|
var require2 = createRequire(import.meta.url);
|
|
11357
11966
|
var { version } = require2("../package.json");
|
|
@@ -11387,7 +11996,22 @@ reportCmd.command("impact").description("Knowledge Impact Report: knowledge heal
|
|
|
11387
11996
|
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
11997
|
runReportImpact(opts);
|
|
11389
11998
|
});
|
|
11390
|
-
|
|
11999
|
+
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) => {
|
|
12000
|
+
runSavings(opts);
|
|
12001
|
+
});
|
|
12002
|
+
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) => {
|
|
12003
|
+
runSavings(opts);
|
|
12004
|
+
});
|
|
12005
|
+
savingsCmd.command("init").description("Create an editable `.kaddo/savings.yml` with savings assumptions").option("--force", "Overwrite an existing .kaddo/savings.yml").action((opts) => {
|
|
12006
|
+
runSavingsInit(opts);
|
|
12007
|
+
});
|
|
12008
|
+
reportCmd.command("drift").description("Drift Trend Report from recorded guard history (deterministic, no LLM)").option("--json", "Output JSON instead of Markdown").option("--output <path>", "Write the report to a file (e.g. .kaddo/reports/drift-report.md)").action((opts) => {
|
|
12009
|
+
runDrift(opts);
|
|
12010
|
+
});
|
|
12011
|
+
program.command("drift").description("Drift Trend Report from recorded `kaddo guard --record` history").option("--json", "Output JSON instead of Markdown").option("--output <path>", "Write the report to a file").action((opts) => {
|
|
12012
|
+
runDrift(opts);
|
|
12013
|
+
});
|
|
12014
|
+
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)").option("--record", "Record this run to .kaddo/history/ for drift trend reporting").action(async (opts) => {
|
|
11391
12015
|
await runGuard(opts);
|
|
11392
12016
|
});
|
|
11393
12017
|
var ignoreCmd = program.command("ignore").description("Manage guard ignore list");
|