@kaddo/cli 3.24.0 → 3.25.1
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 +414 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -514,6 +514,8 @@ create --from roadmap → owners → guard → explain`.
|
|
|
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
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 |
|
|
518
|
+
| v3.25.1 | Savings guard-history messaging fix: distinguishes "no history" from "history with 0 resolved warnings" (drift prevention available at 0 h) |
|
|
517
519
|
|
|
518
520
|
**Optional modules (installed with `kaddo add`):**
|
|
519
521
|
|
package/dist/index.js
CHANGED
|
@@ -692,6 +692,10 @@ var COMMAND_HELP = {
|
|
|
692
692
|
savings: {
|
|
693
693
|
question: "How much time/value might Kaddo be saving (estimate)?",
|
|
694
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"
|
|
695
699
|
}
|
|
696
700
|
};
|
|
697
701
|
function commandFooterLines(name) {
|
|
@@ -5467,6 +5471,162 @@ function collectMatchedDomains(matchedArtifactDomains) {
|
|
|
5467
5471
|
return [...seen];
|
|
5468
5472
|
}
|
|
5469
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
|
+
|
|
5470
5630
|
// src/commands/guard.ts
|
|
5471
5631
|
var ARCH_DIR3 = "knowledge";
|
|
5472
5632
|
var CONFIG_PATH2 = ".kaddo/config.yml";
|
|
@@ -5669,6 +5829,47 @@ async function runGuard(opts = {}) {
|
|
|
5669
5829
|
const activeMatches = fyiMatches.filter(
|
|
5670
5830
|
(m) => !isIgnored(ignores, m.artifact.id || m.artifact.title)
|
|
5671
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
|
+
}
|
|
5672
5873
|
if (jsonMode) {
|
|
5673
5874
|
const ownerMapCI = loadOwners(dir);
|
|
5674
5875
|
const matchedDomainsCI = collectMatchedDomains(activeMatches.map((m) => m.artifact.domains));
|
|
@@ -6106,14 +6307,14 @@ var ALL_STATUSES = ["draft", "ready", "in-progress", "blocked", "completed"];
|
|
|
6106
6307
|
function scopeStatuses(scope) {
|
|
6107
6308
|
return scope === "all" ? { included: ALL_STATUSES, excluded: ["archived"] } : { included: ACTIVE_STATUSES, excluded: ["completed", "archived"] };
|
|
6108
6309
|
}
|
|
6109
|
-
function
|
|
6310
|
+
function toPosix4(p2) {
|
|
6110
6311
|
return p2.replace(/\\/g, "/");
|
|
6111
6312
|
}
|
|
6112
6313
|
function slug(s) {
|
|
6113
6314
|
return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
6114
6315
|
}
|
|
6115
6316
|
function isAdr(a) {
|
|
6116
|
-
return
|
|
6317
|
+
return toPosix4(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
|
|
6117
6318
|
}
|
|
6118
6319
|
function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
6119
6320
|
const scope = opts.scope ?? "active";
|
|
@@ -6298,14 +6499,14 @@ function loadGraphSummary(dir) {
|
|
|
6298
6499
|
|
|
6299
6500
|
// src/core/graph-hints.ts
|
|
6300
6501
|
var KNOWLEDGE3 = "knowledge";
|
|
6301
|
-
function
|
|
6502
|
+
function toPosix5(p2) {
|
|
6302
6503
|
return p2.replace(/\\/g, "/");
|
|
6303
6504
|
}
|
|
6304
6505
|
function slug2(s) {
|
|
6305
6506
|
return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
6306
6507
|
}
|
|
6307
6508
|
function isAdr2(a) {
|
|
6308
|
-
return
|
|
6509
|
+
return toPosix5(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
|
|
6309
6510
|
}
|
|
6310
6511
|
function capabilityHeadings(dir) {
|
|
6311
6512
|
const p2 = join(dir, KNOWLEDGE3, "product", "capabilities.md");
|
|
@@ -11172,10 +11373,10 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
|
11172
11373
|
ready_quality: byState.ready > 0 ? `${byState.ready} ready` : "not applicable"
|
|
11173
11374
|
},
|
|
11174
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`." },
|
|
11175
|
-
guard_activity: {
|
|
11176
|
-
|
|
11177
|
-
note: "Guard history is not persisted. Run `kaddo guard` to
|
|
11178
|
-
},
|
|
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
|
+
})(),
|
|
11179
11380
|
impact_signals,
|
|
11180
11381
|
actionable_gaps: gaps,
|
|
11181
11382
|
score,
|
|
@@ -11264,8 +11465,18 @@ function renderImpactMarkdown(r) {
|
|
|
11264
11465
|
}
|
|
11265
11466
|
L.push("");
|
|
11266
11467
|
L.push("## Guard Activity", "");
|
|
11267
|
-
|
|
11268
|
-
|
|
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
|
+
}
|
|
11269
11480
|
L.push("");
|
|
11270
11481
|
L.push("## Impact Signals", "");
|
|
11271
11482
|
const s = r.impact_signals;
|
|
@@ -11444,25 +11655,59 @@ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
|
11444
11655
|
const onboardH = round2(a.expected_new_contributors_per_month * a.onboarding_hours_saved_per_new_contributor * readinessMult);
|
|
11445
11656
|
const graphMult = GRAPH_MULT[graphQuality] ?? 0;
|
|
11446
11657
|
const archH = round2(a.architecture_discovery_hours_saved_when_graph_good * graphMult);
|
|
11447
|
-
const
|
|
11658
|
+
const guard = buildGuardHistory(dir);
|
|
11659
|
+
let driftSavings;
|
|
11660
|
+
if (!guard.available) {
|
|
11661
|
+
driftSavings = { available: false, hours: 0, reason: "Guard history is not persisted yet." };
|
|
11662
|
+
} else if (guard.resolved > 0) {
|
|
11663
|
+
driftSavings = {
|
|
11664
|
+
available: true,
|
|
11665
|
+
hours: round2(guard.resolved * a.rework_hours_avoided_per_resolved_drift),
|
|
11666
|
+
formula: `${guard.resolved} resolved drift warnings \xD7 ${a.rework_hours_avoided_per_resolved_drift} h`
|
|
11667
|
+
};
|
|
11668
|
+
} else {
|
|
11669
|
+
driftSavings = {
|
|
11670
|
+
available: true,
|
|
11671
|
+
hours: 0,
|
|
11672
|
+
formula: `0 resolved drift warnings \xD7 ${a.rework_hours_avoided_per_resolved_drift} h`,
|
|
11673
|
+
reason: "Guard history exists, but no resolved drift warnings have been recorded yet."
|
|
11674
|
+
};
|
|
11675
|
+
}
|
|
11676
|
+
const totalHours = round2(ctxPrepH + reviewH + clarH + onboardH + archH + driftSavings.hours);
|
|
11448
11677
|
const totalValue = Math.round(totalHours * a.hourly_cost);
|
|
11449
11678
|
const reasons = [];
|
|
11450
11679
|
let level;
|
|
11451
11680
|
if (impact.score === null || impact.score < 60 || graphQuality === "empty") {
|
|
11452
11681
|
level = "Low";
|
|
11453
11682
|
reasons.push("Impact score is low or the knowledge graph is empty.");
|
|
11683
|
+
} else if (impact.score >= 85 && graphQuality === "good" && source === "file" && guard.available && guard.resolved > 0) {
|
|
11684
|
+
level = "High";
|
|
11685
|
+
reasons.push("Strong evidence, custom assumptions and recorded drift resolution.");
|
|
11454
11686
|
} else {
|
|
11455
11687
|
level = "Medium";
|
|
11456
11688
|
reasons.push("Strong knowledge and graph evidence.");
|
|
11457
11689
|
}
|
|
11458
|
-
|
|
11690
|
+
if (!guard.available) {
|
|
11691
|
+
reasons.push("Guard history is not persisted yet \u2014 avoided rework is not estimated.");
|
|
11692
|
+
} else if (guard.resolved > 0) {
|
|
11693
|
+
reasons.push(`Guard history available (${guard.resolved} resolved drift warning(s)).`);
|
|
11694
|
+
} else {
|
|
11695
|
+
reasons.push("Guard history available, but no resolved drift warnings have been recorded yet.");
|
|
11696
|
+
}
|
|
11459
11697
|
reasons.push(source === "file" ? "Assumptions come from `.kaddo/savings.yml`." : "Savings assumptions are defaults and should be calibrated.");
|
|
11460
11698
|
const actions = [];
|
|
11461
11699
|
if (source === "default") actions.push("Run `kaddo savings init` to customize assumptions.");
|
|
11462
11700
|
actions.push("Calibrate hourly cost and team assumptions with real team data.");
|
|
11463
11701
|
actions.push("Run `kaddo impact` to verify the underlying evidence quality.");
|
|
11464
11702
|
actions.push("Use this report as directional evidence, not accounting data.");
|
|
11465
|
-
|
|
11703
|
+
if (!guard.available) {
|
|
11704
|
+
actions.push("Future: persist Guard history to estimate avoided rework.");
|
|
11705
|
+
} else if (guard.resolved === 0) {
|
|
11706
|
+
actions.push("Continue running `kaddo guard --record` before important commits to capture resolved drift warnings.");
|
|
11707
|
+
if (guard.open > 0) actions.push("Review open drift warnings with `kaddo drift`.");
|
|
11708
|
+
} else {
|
|
11709
|
+
actions.push("Use drift history to calibrate `rework_hours_avoided_per_resolved_drift`.");
|
|
11710
|
+
}
|
|
11466
11711
|
return {
|
|
11467
11712
|
generated_at: now.toISOString(),
|
|
11468
11713
|
project: impact.project,
|
|
@@ -11488,7 +11733,9 @@ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
|
11488
11733
|
graph_nodes: impact.graph_quality.available ? impact.graph_quality.nodes : null,
|
|
11489
11734
|
graph_edges: impact.graph_quality.available ? impact.graph_quality.edges : null,
|
|
11490
11735
|
context_readiness: readiness,
|
|
11491
|
-
guard_history_available:
|
|
11736
|
+
guard_history_available: guard.available,
|
|
11737
|
+
guard_runs_recorded: guard.total_runs,
|
|
11738
|
+
resolved_drift_warnings: guard.resolved
|
|
11492
11739
|
},
|
|
11493
11740
|
estimated_savings: {
|
|
11494
11741
|
context_preparation: { hours: ctxPrepH, formula: `${completed} Work Items \xD7 ${a.context_preparation_minutes_saved_per_work_item} min` },
|
|
@@ -11496,7 +11743,7 @@ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
|
11496
11743
|
clarification_reduction: { hours: clarH, formula: `${wiWithAcceptance} Work Items with Acceptance Criteria \xD7 ${a.clarification_minutes_saved_per_ready_work_item} min` },
|
|
11497
11744
|
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
11745
|
architecture_discovery: { hours: archH, formula: `${a.architecture_discovery_hours_saved_when_graph_good} h \xD7 ${graphMult.toFixed(2)} graph multiplier` },
|
|
11499
|
-
drift_prevention:
|
|
11746
|
+
drift_prevention: driftSavings
|
|
11500
11747
|
},
|
|
11501
11748
|
total: { estimated_hours_saved: totalHours, estimated_value: totalValue, currency: a.currency },
|
|
11502
11749
|
confidence: { level, reasons },
|
|
@@ -11538,7 +11785,13 @@ function renderSavingsMarkdown(r) {
|
|
|
11538
11785
|
if (e.graph_nodes !== null) L.push(`- Graph nodes: ${e.graph_nodes}`);
|
|
11539
11786
|
if (e.graph_edges !== null) L.push(`- Graph edges: ${e.graph_edges}`);
|
|
11540
11787
|
L.push(`- Context readiness: ${e.context_readiness}`);
|
|
11541
|
-
|
|
11788
|
+
if (e.guard_history_available) {
|
|
11789
|
+
L.push("- Guard history: available");
|
|
11790
|
+
L.push(`- Guard runs recorded: ${e.guard_runs_recorded}`);
|
|
11791
|
+
L.push(`- Resolved drift warnings: ${e.resolved_drift_warnings}`);
|
|
11792
|
+
} else {
|
|
11793
|
+
L.push("- Guard history: not available");
|
|
11794
|
+
}
|
|
11542
11795
|
L.push("");
|
|
11543
11796
|
L.push("## Estimated Savings", "");
|
|
11544
11797
|
const s = r.estimated_savings;
|
|
@@ -11553,8 +11806,15 @@ function renderSavingsMarkdown(r) {
|
|
|
11553
11806
|
line("Onboarding", s.onboarding);
|
|
11554
11807
|
line("Architecture Discovery", s.architecture_discovery);
|
|
11555
11808
|
L.push("### Drift Prevention", "");
|
|
11556
|
-
|
|
11557
|
-
|
|
11809
|
+
if (s.drift_prevention.available) {
|
|
11810
|
+
L.push(`- Formula: ${s.drift_prevention.formula}`);
|
|
11811
|
+
L.push(`- Estimated: ${s.drift_prevention.hours} h`);
|
|
11812
|
+
if (s.drift_prevention.reason) L.push(`- Reason: ${s.drift_prevention.reason}`);
|
|
11813
|
+
L.push("");
|
|
11814
|
+
} else {
|
|
11815
|
+
L.push("- Not available yet.");
|
|
11816
|
+
L.push(`- Reason: ${s.drift_prevention.reason}`, "");
|
|
11817
|
+
}
|
|
11558
11818
|
L.push("## Total", "");
|
|
11559
11819
|
L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} h`);
|
|
11560
11820
|
L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
|
|
@@ -11606,6 +11866,135 @@ function runSavingsInit(opts = {}) {
|
|
|
11606
11866
|
outro2("Savings assumptions ready.");
|
|
11607
11867
|
}
|
|
11608
11868
|
|
|
11869
|
+
// src/core/drift-report.ts
|
|
11870
|
+
function buildDriftReport(dir, now = /* @__PURE__ */ new Date()) {
|
|
11871
|
+
const h = buildGuardHistory(dir);
|
|
11872
|
+
const project = loadConfig(dir)?.project.name ?? "unknown";
|
|
11873
|
+
const actions = [];
|
|
11874
|
+
if (!h.available) {
|
|
11875
|
+
actions.push("Run `kaddo guard --record` to start recording guard history.");
|
|
11876
|
+
} else {
|
|
11877
|
+
if (h.open > 0) actions.push("Review open drift warnings and update related Work Items or ownership.");
|
|
11878
|
+
actions.push("Run `kaddo guard --record` before important commits.");
|
|
11879
|
+
actions.push("Use this report to calibrate estimated drift-prevention savings.");
|
|
11880
|
+
}
|
|
11881
|
+
return {
|
|
11882
|
+
generated_at: now.toISOString(),
|
|
11883
|
+
project,
|
|
11884
|
+
guard_history: {
|
|
11885
|
+
available: h.available,
|
|
11886
|
+
total_runs: h.total_runs,
|
|
11887
|
+
first_run: h.first_run,
|
|
11888
|
+
last_run: h.last_run,
|
|
11889
|
+
runs_with_warnings: h.runs_with_warnings,
|
|
11890
|
+
runs_clean: h.runs_clean
|
|
11891
|
+
},
|
|
11892
|
+
drift_warnings: { detected: h.detected, open: h.open, resolved: h.resolved, resolution_rate: h.resolution_rate },
|
|
11893
|
+
hotspots: h.hotspots,
|
|
11894
|
+
trend: h.trend,
|
|
11895
|
+
threads: h.threads,
|
|
11896
|
+
suggested_actions: actions
|
|
11897
|
+
};
|
|
11898
|
+
}
|
|
11899
|
+
function renderDriftMarkdown(r) {
|
|
11900
|
+
const L = [];
|
|
11901
|
+
L.push("# Kaddo Drift Trend Report", "");
|
|
11902
|
+
L.push(`Generated at: ${r.generated_at}`);
|
|
11903
|
+
L.push(`Project: ${r.project}`);
|
|
11904
|
+
L.push("");
|
|
11905
|
+
if (!r.guard_history.available) {
|
|
11906
|
+
L.push("## Guard History", "");
|
|
11907
|
+
L.push("No guard history recorded yet.");
|
|
11908
|
+
L.push("");
|
|
11909
|
+
L.push("Run `kaddo guard --record` to start recording drift evidence.");
|
|
11910
|
+
L.push("");
|
|
11911
|
+
L.push("## Suggested Actions", "");
|
|
11912
|
+
r.suggested_actions.forEach((a, i) => L.push(`${i + 1}. ${a}`));
|
|
11913
|
+
L.push("");
|
|
11914
|
+
return L.join("\n");
|
|
11915
|
+
}
|
|
11916
|
+
const w = r.drift_warnings;
|
|
11917
|
+
L.push("## Executive Summary", "");
|
|
11918
|
+
L.push(`- Guard runs recorded: ${r.guard_history.total_runs}`);
|
|
11919
|
+
L.push(`- Drift warnings detected: ${w.detected}`);
|
|
11920
|
+
L.push(`- Open warnings: ${w.open}`);
|
|
11921
|
+
L.push(`- Resolved warnings: ${w.resolved}`);
|
|
11922
|
+
L.push(`- Resolution rate: ${w.resolution_rate}%`);
|
|
11923
|
+
if (r.hotspots.length > 0) L.push(`- Most affected area: ${r.hotspots[0].path}`);
|
|
11924
|
+
L.push("");
|
|
11925
|
+
L.push("## Guard History", "");
|
|
11926
|
+
L.push(`- First run: ${r.guard_history.first_run}`);
|
|
11927
|
+
L.push(`- Last run: ${r.guard_history.last_run}`);
|
|
11928
|
+
L.push(`- Runs with warnings: ${r.guard_history.runs_with_warnings}/${r.guard_history.total_runs}`);
|
|
11929
|
+
L.push(`- Runs clean: ${r.guard_history.runs_clean}/${r.guard_history.total_runs}`);
|
|
11930
|
+
L.push("");
|
|
11931
|
+
L.push("## Drift Warnings", "");
|
|
11932
|
+
const open = r.threads.filter((t) => t.status === "open");
|
|
11933
|
+
const resolved = r.threads.filter((t) => t.status === "resolved");
|
|
11934
|
+
L.push("### Open", "");
|
|
11935
|
+
if (open.length === 0) L.push("_None._", "");
|
|
11936
|
+
for (const t of open) {
|
|
11937
|
+
L.push(`- ${t.code_path}`);
|
|
11938
|
+
L.push(` - Related artifacts: ${t.related_artifacts.join(", ") || "\u2014"}`);
|
|
11939
|
+
L.push(` - First detected: ${t.first_detected}`);
|
|
11940
|
+
L.push(` - Last seen: ${t.last_seen}`);
|
|
11941
|
+
L.push(" - Status: open");
|
|
11942
|
+
}
|
|
11943
|
+
L.push("");
|
|
11944
|
+
L.push("### Resolved", "");
|
|
11945
|
+
if (resolved.length === 0) L.push("_None._", "");
|
|
11946
|
+
for (const t of resolved) {
|
|
11947
|
+
L.push(`- ${t.code_path}`);
|
|
11948
|
+
L.push(` - Related artifacts: ${t.related_artifacts.join(", ") || "\u2014"}`);
|
|
11949
|
+
L.push(` - First detected: ${t.first_detected}`);
|
|
11950
|
+
L.push(` - Resolved at: ${t.resolved_at}`);
|
|
11951
|
+
L.push(" - Status: resolved");
|
|
11952
|
+
}
|
|
11953
|
+
L.push("");
|
|
11954
|
+
L.push("## Hotspots", "");
|
|
11955
|
+
if (r.hotspots.length === 0) L.push("_None._");
|
|
11956
|
+
for (const h of r.hotspots) L.push(`- ${h.path}: ${h.warnings} warning(s)`);
|
|
11957
|
+
L.push("");
|
|
11958
|
+
L.push("## Trend", "");
|
|
11959
|
+
L.push(`- Recent direction: ${r.trend.direction}`);
|
|
11960
|
+
L.push(`- Reason: ${r.trend.reason}`);
|
|
11961
|
+
L.push("");
|
|
11962
|
+
L.push("## Suggested Actions", "");
|
|
11963
|
+
r.suggested_actions.forEach((a, i) => L.push(`${i + 1}. ${a}`));
|
|
11964
|
+
L.push("");
|
|
11965
|
+
return L.join("\n");
|
|
11966
|
+
}
|
|
11967
|
+
function serializeDriftJson(r) {
|
|
11968
|
+
const out = {
|
|
11969
|
+
generated_at: r.generated_at,
|
|
11970
|
+
project: r.project,
|
|
11971
|
+
guard_history: r.guard_history,
|
|
11972
|
+
drift_warnings: r.drift_warnings,
|
|
11973
|
+
hotspots: r.hotspots,
|
|
11974
|
+
trend: r.trend,
|
|
11975
|
+
threads: r.threads,
|
|
11976
|
+
suggested_actions: r.suggested_actions
|
|
11977
|
+
};
|
|
11978
|
+
return JSON.stringify(out, null, 2) + "\n";
|
|
11979
|
+
}
|
|
11980
|
+
|
|
11981
|
+
// src/commands/drift.ts
|
|
11982
|
+
function runDrift(opts = {}) {
|
|
11983
|
+
const dir = cwd();
|
|
11984
|
+
requireConfig(dir);
|
|
11985
|
+
const report = buildDriftReport(dir);
|
|
11986
|
+
const content = opts.json ? serializeDriftJson(report) : renderDriftMarkdown(report);
|
|
11987
|
+
if (opts.output) {
|
|
11988
|
+
intro2("kaddo drift");
|
|
11989
|
+
writeFile(join(dir, opts.output), content);
|
|
11990
|
+
log2.success(`Wrote ${opts.output.replace(/\\/g, "/")}`);
|
|
11991
|
+
printCommandFooter("drift");
|
|
11992
|
+
outro2("Drift report written.");
|
|
11993
|
+
return;
|
|
11994
|
+
}
|
|
11995
|
+
console.log(content);
|
|
11996
|
+
}
|
|
11997
|
+
|
|
11609
11998
|
// src/index.ts
|
|
11610
11999
|
var require2 = createRequire(import.meta.url);
|
|
11611
12000
|
var { version } = require2("../package.json");
|
|
@@ -11650,7 +12039,13 @@ var savingsCmd = program.command("savings").description("Estimated Savings Repor
|
|
|
11650
12039
|
savingsCmd.command("init").description("Create an editable `.kaddo/savings.yml` with savings assumptions").option("--force", "Overwrite an existing .kaddo/savings.yml").action((opts) => {
|
|
11651
12040
|
runSavingsInit(opts);
|
|
11652
12041
|
});
|
|
11653
|
-
|
|
12042
|
+
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) => {
|
|
12043
|
+
runDrift(opts);
|
|
12044
|
+
});
|
|
12045
|
+
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) => {
|
|
12046
|
+
runDrift(opts);
|
|
12047
|
+
});
|
|
12048
|
+
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) => {
|
|
11654
12049
|
await runGuard(opts);
|
|
11655
12050
|
});
|
|
11656
12051
|
var ignoreCmd = program.command("ignore").description("Manage guard ignore list");
|