@kaddo/mcp 3.33.0 → 3.35.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/dist/index.js +777 -466
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1683,6 +1683,572 @@ function roadmapStats(markdown, materialized) {
|
|
|
1683
1683
|
return { present: true, candidates, materialized, remaining: Math.max(0, candidates - materialized) };
|
|
1684
1684
|
}
|
|
1685
1685
|
|
|
1686
|
+
// ../cli/src/core/open-questions.ts
|
|
1687
|
+
var RESOLUTION_TOKENS = {
|
|
1688
|
+
open: "open",
|
|
1689
|
+
resolved: "resolved",
|
|
1690
|
+
assumed: "assumed",
|
|
1691
|
+
deferred: "deferred",
|
|
1692
|
+
abierta: "open",
|
|
1693
|
+
resuelta: "resolved",
|
|
1694
|
+
asumida: "assumed",
|
|
1695
|
+
diferida: "deferred"
|
|
1696
|
+
};
|
|
1697
|
+
function parseResolution(line) {
|
|
1698
|
+
const m = line.match(/^\[([a-zA-Záéí]+)\]\s*(.*)$/);
|
|
1699
|
+
if (m) {
|
|
1700
|
+
const status = RESOLUTION_TOKENS[m[1].toLowerCase()];
|
|
1701
|
+
if (status) return { text: m[2].trim(), resolution_status: status };
|
|
1702
|
+
}
|
|
1703
|
+
return { text: line.trim(), resolution_status: "open" };
|
|
1704
|
+
}
|
|
1705
|
+
var SOURCES = [
|
|
1706
|
+
"knowledge/business/business.md",
|
|
1707
|
+
"knowledge/product/product.md",
|
|
1708
|
+
"knowledge/tech/codebase.md",
|
|
1709
|
+
"knowledge/delivery/roadmap.md"
|
|
1710
|
+
];
|
|
1711
|
+
var BLOCKING = [
|
|
1712
|
+
"mvp",
|
|
1713
|
+
"stack",
|
|
1714
|
+
"arquitect",
|
|
1715
|
+
"architecture",
|
|
1716
|
+
"api",
|
|
1717
|
+
"web app",
|
|
1718
|
+
"webapp",
|
|
1719
|
+
"cli",
|
|
1720
|
+
"backend",
|
|
1721
|
+
"auth",
|
|
1722
|
+
"autenticaci",
|
|
1723
|
+
"persist",
|
|
1724
|
+
"base de datos",
|
|
1725
|
+
"database",
|
|
1726
|
+
"roadmap",
|
|
1727
|
+
"alcance",
|
|
1728
|
+
"scope",
|
|
1729
|
+
"primera versi\xF3n",
|
|
1730
|
+
"primera version",
|
|
1731
|
+
"first version",
|
|
1732
|
+
"fastify",
|
|
1733
|
+
"nestjs",
|
|
1734
|
+
"framework"
|
|
1735
|
+
];
|
|
1736
|
+
var DEFERRED = [
|
|
1737
|
+
"integraci",
|
|
1738
|
+
"integration",
|
|
1739
|
+
"analytics",
|
|
1740
|
+
"notificaci",
|
|
1741
|
+
"notification",
|
|
1742
|
+
"google calendar",
|
|
1743
|
+
"eventbrite",
|
|
1744
|
+
"meetup",
|
|
1745
|
+
"dashboard",
|
|
1746
|
+
"multi-tenant",
|
|
1747
|
+
"multitenant",
|
|
1748
|
+
"mobile",
|
|
1749
|
+
"m\xF3vil",
|
|
1750
|
+
"pago",
|
|
1751
|
+
"payment",
|
|
1752
|
+
"ticketing",
|
|
1753
|
+
"multiple tracks",
|
|
1754
|
+
"m\xFAltiples tracks",
|
|
1755
|
+
"webhook"
|
|
1756
|
+
];
|
|
1757
|
+
var IMPORTANT = [
|
|
1758
|
+
"validaci",
|
|
1759
|
+
"validation",
|
|
1760
|
+
"paginaci",
|
|
1761
|
+
"pagination",
|
|
1762
|
+
"soft delete",
|
|
1763
|
+
"sponsor",
|
|
1764
|
+
"agenda",
|
|
1765
|
+
"ubicaci",
|
|
1766
|
+
"location",
|
|
1767
|
+
"rol",
|
|
1768
|
+
"role",
|
|
1769
|
+
"permis",
|
|
1770
|
+
"permission"
|
|
1771
|
+
];
|
|
1772
|
+
function matchesAny(text2, keywords) {
|
|
1773
|
+
return keywords.some((k) => text2.includes(k));
|
|
1774
|
+
}
|
|
1775
|
+
function classifyQuestion(question) {
|
|
1776
|
+
const t = question.toLowerCase();
|
|
1777
|
+
if (matchesAny(t, BLOCKING)) {
|
|
1778
|
+
return { classification: "blocking", reason: "Affects scope, architecture or the first Work Items." };
|
|
1779
|
+
}
|
|
1780
|
+
if (matchesAny(t, DEFERRED) && !matchesAny(t, IMPORTANT)) {
|
|
1781
|
+
return { classification: "deferred", reason: "Can be moved to a later phase." };
|
|
1782
|
+
}
|
|
1783
|
+
if (matchesAny(t, IMPORTANT)) {
|
|
1784
|
+
return { classification: "important", reason: "Relevant, but a temporary assumption can unblock progress." };
|
|
1785
|
+
}
|
|
1786
|
+
return { classification: "important", reason: "Relevant, but a temporary assumption can unblock progress." };
|
|
1787
|
+
}
|
|
1788
|
+
function extractFromMarkdown(md) {
|
|
1789
|
+
const lines = md.split(/\r?\n/);
|
|
1790
|
+
const start = lines.findIndex((l) => /^#{1,6}\s+open questions\s*$/i.test(l.trim()) || /^#{1,6}\s+preguntas abiertas\s*$/i.test(l.trim()));
|
|
1791
|
+
if (start < 0) return [];
|
|
1792
|
+
const out = [];
|
|
1793
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
1794
|
+
const line = lines[i];
|
|
1795
|
+
if (/^#{1,6}\s+/.test(line)) break;
|
|
1796
|
+
const meta = line.match(/^\s+(?:[-*])\s+(\w+)\s*:\s*(.+?)\s*$/);
|
|
1797
|
+
if (meta && out.length > 0) {
|
|
1798
|
+
if (meta[1].toLowerCase() === "note") out[out.length - 1].resolution_note = meta[2].trim();
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
const m = line.match(/^\s*(?:[-*]|\d+\.)\s+(.+?)\s*$/);
|
|
1802
|
+
if (m) {
|
|
1803
|
+
const raw = m[1].trim();
|
|
1804
|
+
if (!raw || /^_.*_$/.test(raw)) continue;
|
|
1805
|
+
const { text: text2, resolution_status } = parseResolution(raw);
|
|
1806
|
+
if (text2) out.push({ text: text2, resolution_status });
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
return out;
|
|
1810
|
+
}
|
|
1811
|
+
function suggestedAssumption(question) {
|
|
1812
|
+
const t = question.toLowerCase();
|
|
1813
|
+
if (matchesAny(t, ["api", "web app", "webapp", "cli", "backend"])) {
|
|
1814
|
+
return "Start as a backend API to keep the initial scope small.";
|
|
1815
|
+
}
|
|
1816
|
+
if (matchesAny(t, ["auth", "autenticaci"])) {
|
|
1817
|
+
return "No authentication in the MVP.";
|
|
1818
|
+
}
|
|
1819
|
+
if (matchesAny(t, ["fastify", "nestjs", "framework", "stack"])) {
|
|
1820
|
+
return "Choose the lighter stack option to minimize MVP scope.";
|
|
1821
|
+
}
|
|
1822
|
+
if (matchesAny(t, ["persist", "base de datos", "database"])) {
|
|
1823
|
+
return "Use a simple local datastore (e.g. SQLite) for the MVP.";
|
|
1824
|
+
}
|
|
1825
|
+
return "Proceed with the smallest reasonable MVP interpretation and revisit later.";
|
|
1826
|
+
}
|
|
1827
|
+
function buildOpenQuestionsReport(dir, now = /* @__PURE__ */ new Date()) {
|
|
1828
|
+
const project = loadConfig(dir)?.project.name ?? "unknown";
|
|
1829
|
+
const questions = [];
|
|
1830
|
+
let seq = 0;
|
|
1831
|
+
for (const source of SOURCES) {
|
|
1832
|
+
const p = join(dir, source);
|
|
1833
|
+
if (!exists(p)) continue;
|
|
1834
|
+
let md;
|
|
1835
|
+
try {
|
|
1836
|
+
md = readFile(p);
|
|
1837
|
+
} catch {
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
for (const eq of extractFromMarkdown(md)) {
|
|
1841
|
+
seq += 1;
|
|
1842
|
+
const { classification, reason } = classifyQuestion(eq.text);
|
|
1843
|
+
questions.push({
|
|
1844
|
+
id: `OQ-${String(seq).padStart(3, "0")}`,
|
|
1845
|
+
source,
|
|
1846
|
+
section: "Open Questions",
|
|
1847
|
+
question: eq.text,
|
|
1848
|
+
classification,
|
|
1849
|
+
resolution_status: eq.resolution_status,
|
|
1850
|
+
reason,
|
|
1851
|
+
...eq.resolution_note ? { resolution_note: eq.resolution_note } : {},
|
|
1852
|
+
...classification === "blocking" && eq.resolution_status === "open" ? { suggested_assumption: suggestedAssumption(eq.text) } : {}
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
const blocking = questions.filter((q) => q.classification === "blocking");
|
|
1857
|
+
const important = questions.filter((q) => q.classification === "important");
|
|
1858
|
+
const deferred = questions.filter((q) => q.classification === "deferred");
|
|
1859
|
+
const blockingOpen = blocking.filter((q) => q.resolution_status === "open");
|
|
1860
|
+
const importantOpen = important.filter((q) => q.resolution_status === "open");
|
|
1861
|
+
const byStatus = (s) => questions.filter((q) => q.resolution_status === s);
|
|
1862
|
+
const resolved = byStatus("resolved");
|
|
1863
|
+
const assumed = byStatus("assumed");
|
|
1864
|
+
const deferredStatus = byStatus("deferred");
|
|
1865
|
+
let readiness;
|
|
1866
|
+
if (questions.length === 0) readiness = "unknown";
|
|
1867
|
+
else if (blockingOpen.length > 0) readiness = "needs_decisions";
|
|
1868
|
+
else readiness = "ready";
|
|
1869
|
+
const suggested_assumptions = blockingOpen.map((q) => q.suggested_assumption).filter(Boolean);
|
|
1870
|
+
const recommended_next_step = readiness === "needs_decisions" ? "Confirm suggested assumptions with the user (resolve, assume or defer blocking open questions) before generating the roadmap." : readiness === "ready" ? "No blocking open questions \u2014 you can proceed to the roadmap. Surfaced assumptions/deferred items are informational." : "No open questions found. Run `kaddo bootstrap` to capture early questions, or proceed.";
|
|
1871
|
+
return {
|
|
1872
|
+
generated_at: now.toISOString(),
|
|
1873
|
+
project,
|
|
1874
|
+
summary: {
|
|
1875
|
+
open_questions: questions.length,
|
|
1876
|
+
roadmap_readiness: readiness,
|
|
1877
|
+
blocking: blocking.length,
|
|
1878
|
+
important: important.length,
|
|
1879
|
+
deferred: deferred.length,
|
|
1880
|
+
blocking_open: blockingOpen.length,
|
|
1881
|
+
important_open: importantOpen.length,
|
|
1882
|
+
resolution: {
|
|
1883
|
+
open: byStatus("open").length,
|
|
1884
|
+
resolved: resolved.length,
|
|
1885
|
+
assumed: assumed.length,
|
|
1886
|
+
deferred: deferredStatus.length
|
|
1887
|
+
}
|
|
1888
|
+
},
|
|
1889
|
+
questions,
|
|
1890
|
+
blocking_questions: blocking,
|
|
1891
|
+
important_questions: important,
|
|
1892
|
+
deferred_questions: deferred,
|
|
1893
|
+
resolved_questions: resolved,
|
|
1894
|
+
assumed_questions: assumed,
|
|
1895
|
+
deferred_status_questions: deferredStatus,
|
|
1896
|
+
suggested_assumptions,
|
|
1897
|
+
recommended_next_step
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
function renderOpenQuestionsMarkdown(r) {
|
|
1901
|
+
const L = [];
|
|
1902
|
+
L.push("# Kaddo Open Questions Readiness Report", "");
|
|
1903
|
+
L.push(`Generated at: ${r.generated_at}`);
|
|
1904
|
+
L.push(`Project: ${r.project}`);
|
|
1905
|
+
L.push("");
|
|
1906
|
+
L.push("## Summary", "");
|
|
1907
|
+
L.push(`- Open questions found: ${r.summary.open_questions}`);
|
|
1908
|
+
L.push(`- Roadmap readiness: ${r.summary.roadmap_readiness}`);
|
|
1909
|
+
L.push(`- By resolution \u2014 open: ${r.summary.resolution.open}, resolved: ${r.summary.resolution.resolved}, assumed: ${r.summary.resolution.assumed}, deferred: ${r.summary.resolution.deferred}`);
|
|
1910
|
+
L.push(`- Blocking open: ${r.summary.blocking_open}, important open: ${r.summary.important_open}`);
|
|
1911
|
+
L.push("");
|
|
1912
|
+
if (r.summary.open_questions === 0) {
|
|
1913
|
+
L.push("No `## Open Questions` sections found in business / product / codebase / roadmap.", "");
|
|
1914
|
+
L.push("## Recommended Next Step", "", r.recommended_next_step, "");
|
|
1915
|
+
return L.join("\n");
|
|
1916
|
+
}
|
|
1917
|
+
const blockingOpen = r.blocking_questions.filter((q) => q.resolution_status === "open");
|
|
1918
|
+
L.push("## Blocking Open Questions", "");
|
|
1919
|
+
if (blockingOpen.length === 0) L.push("_None \u2014 nothing blocks readiness._", "");
|
|
1920
|
+
for (const q of blockingOpen) {
|
|
1921
|
+
L.push(`### ${q.id}`, "");
|
|
1922
|
+
L.push(`- Source: \`${q.source}\``);
|
|
1923
|
+
L.push(`- Question: ${q.question}`);
|
|
1924
|
+
L.push(`- Reason: ${q.reason}`);
|
|
1925
|
+
if (q.suggested_assumption) L.push(`- Suggested assumption: ${q.suggested_assumption}`);
|
|
1926
|
+
L.push("");
|
|
1927
|
+
}
|
|
1928
|
+
const renderStatusList = (title, items) => {
|
|
1929
|
+
L.push(`## ${title}`, "");
|
|
1930
|
+
if (items.length === 0) L.push("_None._");
|
|
1931
|
+
for (const q of items) L.push(`- ${q.question}${q.resolution_note ? ` \u2014 _${q.resolution_note}_` : ""}`);
|
|
1932
|
+
L.push("");
|
|
1933
|
+
};
|
|
1934
|
+
renderStatusList("Resolved", r.resolved_questions);
|
|
1935
|
+
renderStatusList("Assumed (decisions to revisit)", r.assumed_questions);
|
|
1936
|
+
renderStatusList("Deferred (out of current scope)", r.deferred_status_questions);
|
|
1937
|
+
L.push("## Important Questions", "");
|
|
1938
|
+
if (r.important_questions.length === 0) L.push("_None._");
|
|
1939
|
+
for (const q of r.important_questions) L.push(`- [${q.resolution_status}] ${q.question}`);
|
|
1940
|
+
L.push("");
|
|
1941
|
+
if (r.suggested_assumptions.length > 0) {
|
|
1942
|
+
L.push("## Suggested Assumptions", "");
|
|
1943
|
+
for (const a of r.suggested_assumptions) L.push(`- ${a}`);
|
|
1944
|
+
L.push("");
|
|
1945
|
+
}
|
|
1946
|
+
L.push("## Recommended Next Step", "", r.recommended_next_step, "");
|
|
1947
|
+
return L.join("\n");
|
|
1948
|
+
}
|
|
1949
|
+
function serializeOpenQuestionsJson(r) {
|
|
1950
|
+
return JSON.stringify(r, null, 2) + "\n";
|
|
1951
|
+
}
|
|
1952
|
+
function roadmapReadinessSummary(dir) {
|
|
1953
|
+
const r = buildOpenQuestionsReport(dir);
|
|
1954
|
+
return {
|
|
1955
|
+
roadmap_readiness: r.summary.roadmap_readiness,
|
|
1956
|
+
blocking_questions: r.summary.blocking,
|
|
1957
|
+
blocking_open: r.summary.blocking_open,
|
|
1958
|
+
important_questions: r.summary.important,
|
|
1959
|
+
deferred_questions: r.summary.deferred,
|
|
1960
|
+
resolution: r.summary.resolution,
|
|
1961
|
+
suggested_assumptions: r.suggested_assumptions,
|
|
1962
|
+
recommended_next_step: r.summary.roadmap_readiness === "needs_decisions" ? "Resolve, assume, or defer blocking open questions before generating roadmap." : r.recommended_next_step
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// ../cli/src/core/codex-adapter.ts
|
|
1967
|
+
var KNOWLEDGE_PATHS = [
|
|
1968
|
+
"knowledge/business/",
|
|
1969
|
+
"knowledge/product/",
|
|
1970
|
+
"knowledge/tech/",
|
|
1971
|
+
"knowledge/delivery/",
|
|
1972
|
+
"knowledge/agents/",
|
|
1973
|
+
"knowledge/skills/"
|
|
1974
|
+
];
|
|
1975
|
+
var GENERATED_PATHS = [
|
|
1976
|
+
".kaddo/context-pack.md",
|
|
1977
|
+
".kaddo/understand.md",
|
|
1978
|
+
".kaddo/explain.md",
|
|
1979
|
+
".kaddo/graph.json",
|
|
1980
|
+
".kaddo/reports/"
|
|
1981
|
+
];
|
|
1982
|
+
function discoverAgents(dir) {
|
|
1983
|
+
const base = join(dir, "knowledge", "agents");
|
|
1984
|
+
if (!exists(base) || !isDir(base)) return [];
|
|
1985
|
+
const out = /* @__PURE__ */ new Set();
|
|
1986
|
+
const walk = (d) => {
|
|
1987
|
+
for (const e of readDir(d)) {
|
|
1988
|
+
const full = join(d, e);
|
|
1989
|
+
if (isDir(full)) walk(full);
|
|
1990
|
+
else if (e.endsWith("-agent.md")) out.add(e.replace(/\.md$/, ""));
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1993
|
+
walk(base);
|
|
1994
|
+
return [...out].sort();
|
|
1995
|
+
}
|
|
1996
|
+
function buildCodexAdapterContext(dir) {
|
|
1997
|
+
const config = loadConfig(dir);
|
|
1998
|
+
const agents = discoverAgents(dir);
|
|
1999
|
+
const skills = discoverInstalledSkills(dir).map((s) => s.id);
|
|
2000
|
+
return {
|
|
2001
|
+
projectName: config?.project.name,
|
|
2002
|
+
projectType: config?.project.state,
|
|
2003
|
+
language: config ? config.project.language : void 0,
|
|
2004
|
+
packageManager: detectPackageManager(dir),
|
|
2005
|
+
hasAgents: agents.length > 0,
|
|
2006
|
+
agents,
|
|
2007
|
+
hasSkills: skills.length > 0,
|
|
2008
|
+
skills,
|
|
2009
|
+
hasMcpHint: detectMcpHint(dir),
|
|
2010
|
+
knowledgePaths: KNOWLEDGE_PATHS.filter((p) => exists(join(dir, p))),
|
|
2011
|
+
generatedPaths: GENERATED_PATHS
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
function detectPackageManager(dir) {
|
|
2015
|
+
if (exists(join(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
2016
|
+
if (exists(join(dir, "yarn.lock"))) return "yarn";
|
|
2017
|
+
if (exists(join(dir, "bun.lockb")) || exists(join(dir, "bun.lock"))) return "bun";
|
|
2018
|
+
if (exists(join(dir, "package-lock.json"))) return "npm";
|
|
2019
|
+
return void 0;
|
|
2020
|
+
}
|
|
2021
|
+
function detectMcpHint(dir) {
|
|
2022
|
+
for (const f of [".mcp.json", "mcp.json", ".cursor/mcp.json"]) {
|
|
2023
|
+
if (exists(join(dir, f))) return true;
|
|
2024
|
+
}
|
|
2025
|
+
return false;
|
|
2026
|
+
}
|
|
2027
|
+
var KADDO_BEGIN_MARKER = "<!-- BEGIN KADDO ADAPTER -->";
|
|
2028
|
+
var KADDO_END_MARKER = "<!-- END KADDO ADAPTER -->";
|
|
2029
|
+
var LEGACY_BEGIN_MARKER = "<!-- BEGIN KADDO CODEX ADAPTER -->";
|
|
2030
|
+
var LEGACY_END_MARKER = "<!-- END KADDO CODEX ADAPTER -->";
|
|
2031
|
+
var GENERATED_HEADER = "Generated by `kaddo adapters install ";
|
|
2032
|
+
function detectAgentsState(content) {
|
|
2033
|
+
if (content === null) return "missing";
|
|
2034
|
+
const hasBegin = content.includes(KADDO_BEGIN_MARKER) || content.includes(LEGACY_BEGIN_MARKER);
|
|
2035
|
+
const hasEnd = content.includes(KADDO_END_MARKER) || content.includes(LEGACY_END_MARKER);
|
|
2036
|
+
if (hasBegin !== hasEnd) return "invalid_kaddo_block";
|
|
2037
|
+
if (hasBegin && hasEnd) return "existing_with_kaddo_block";
|
|
2038
|
+
if (content.includes(GENERATED_HEADER)) return "generated_by_kaddo";
|
|
2039
|
+
return "existing_external";
|
|
2040
|
+
}
|
|
2041
|
+
var ADAPTERS = [
|
|
2042
|
+
{ id: "codex", label: "Codex", file: "AGENTS.md", supportsInject: true },
|
|
2043
|
+
{ id: "claude", label: "Claude Code", file: "CLAUDE.md", supportsInject: true },
|
|
2044
|
+
{ id: "opencode", label: "OpenCode", file: "AGENTS.md", supportsInject: true },
|
|
2045
|
+
{ id: "antigravity", label: "Antigravity", file: "AGENTS.md", supportsInject: true },
|
|
2046
|
+
{ id: "kiro", label: "Kiro", file: "AGENTS.md", supportsInject: true }
|
|
2047
|
+
];
|
|
2048
|
+
function findAdapter(id) {
|
|
2049
|
+
return ADAPTERS.find((a) => a.id === id);
|
|
2050
|
+
}
|
|
2051
|
+
function parseOriginAdapter(content) {
|
|
2052
|
+
const m = content.match(/Generated by `kaddo adapters install (\w+)`/);
|
|
2053
|
+
const id = m?.[1];
|
|
2054
|
+
return id && findAdapter(id) ? id : null;
|
|
2055
|
+
}
|
|
2056
|
+
function classifyAdapterState(content) {
|
|
2057
|
+
const base = detectAgentsState(content);
|
|
2058
|
+
switch (base) {
|
|
2059
|
+
case "missing":
|
|
2060
|
+
return "missing";
|
|
2061
|
+
case "invalid_kaddo_block":
|
|
2062
|
+
return "broken-markers";
|
|
2063
|
+
case "generated_by_kaddo":
|
|
2064
|
+
return "full-generated";
|
|
2065
|
+
case "existing_with_kaddo_block":
|
|
2066
|
+
return content.includes(LEGACY_BEGIN_MARKER) ? "legacy-injected" : "injected";
|
|
2067
|
+
default:
|
|
2068
|
+
return "team-owned";
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
function recommendedAction(a, state) {
|
|
2072
|
+
const cmd = `kaddo adapters install ${a.id}`;
|
|
2073
|
+
switch (state) {
|
|
2074
|
+
case "missing":
|
|
2075
|
+
return cmd;
|
|
2076
|
+
case "team-owned":
|
|
2077
|
+
return `${cmd} --inject`;
|
|
2078
|
+
case "injected":
|
|
2079
|
+
return "No action needed (run --inject to update the Kaddo block).";
|
|
2080
|
+
case "legacy-injected":
|
|
2081
|
+
return `${cmd} --inject (migrates legacy markers to neutral)`;
|
|
2082
|
+
case "full-generated":
|
|
2083
|
+
return `No action needed. Use \`${cmd} --force\` only to regenerate for ${a.label}.`;
|
|
2084
|
+
case "broken-markers":
|
|
2085
|
+
return "Fix the incomplete Kaddo adapter markers before running --inject.";
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
function buildAdapterStatuses(dir) {
|
|
2089
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2090
|
+
const read = (file) => {
|
|
2091
|
+
if (!cache.has(file)) {
|
|
2092
|
+
const full = join(dir, file);
|
|
2093
|
+
cache.set(file, exists(full) ? readFile(full) : null);
|
|
2094
|
+
}
|
|
2095
|
+
return cache.get(file);
|
|
2096
|
+
};
|
|
2097
|
+
return ADAPTERS.map((a) => {
|
|
2098
|
+
const content = read(a.file);
|
|
2099
|
+
const state = classifyAdapterState(content);
|
|
2100
|
+
const originAdapter = content ? parseOriginAdapter(content) : null;
|
|
2101
|
+
return { ...a, state, originAdapter, recommendedAction: recommendedAction(a, state) };
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// ../cli/src/core/readiness.ts
|
|
2106
|
+
var KNOWLEDGE_FILES = [
|
|
2107
|
+
{ key: "current_state", path: "knowledge/tech/current-state.md", label: "knowledge/tech/current-state.md" },
|
|
2108
|
+
{ key: "codebase", path: "knowledge/tech/codebase.md", label: "knowledge/tech/codebase.md" },
|
|
2109
|
+
{ key: "capabilities", path: "knowledge/product/capabilities.md", label: "knowledge/product/capabilities.md" },
|
|
2110
|
+
{ key: "product", path: "knowledge/product/product.md", label: "knowledge/product/product.md" },
|
|
2111
|
+
{ key: "business", path: "knowledge/business/business.md", label: "knowledge/business/business.md" }
|
|
2112
|
+
];
|
|
2113
|
+
function knowledgePresence(dir, rel) {
|
|
2114
|
+
const p = join(dir, rel);
|
|
2115
|
+
if (!exists(p)) return "missing";
|
|
2116
|
+
let md;
|
|
2117
|
+
try {
|
|
2118
|
+
md = readFile(p);
|
|
2119
|
+
} catch {
|
|
2120
|
+
return "missing";
|
|
2121
|
+
}
|
|
2122
|
+
const body = md.replace(/^---[\s\S]*?---/m, "").split(/\r?\n/).filter((l) => !/^\s*#{1,6}\s/.test(l) && !/^\s*$/.test(l) && !/^\s*<!--/.test(l)).join(" ").trim();
|
|
2123
|
+
return body.length >= 40 ? "present" : "weak";
|
|
2124
|
+
}
|
|
2125
|
+
function roadmapSignal(dir) {
|
|
2126
|
+
const p = join(dir, "knowledge/delivery/roadmap.md");
|
|
2127
|
+
if (!exists(p)) return "missing";
|
|
2128
|
+
let md;
|
|
2129
|
+
try {
|
|
2130
|
+
md = readFile(p);
|
|
2131
|
+
} catch {
|
|
2132
|
+
return "missing";
|
|
2133
|
+
}
|
|
2134
|
+
const lines = md.replace(/^---[\s\S]*?---/m, "").split(/\r?\n/);
|
|
2135
|
+
let inOpenQuestions = false;
|
|
2136
|
+
let candidates = 0;
|
|
2137
|
+
for (const l of lines) {
|
|
2138
|
+
if (/^#{1,6}\s/.test(l)) inOpenQuestions = /open questions|preguntas abiertas/i.test(l);
|
|
2139
|
+
else if (!inOpenQuestions && /^\s*(?:[-*]\s+|\|)/.test(l)) candidates += 1;
|
|
2140
|
+
}
|
|
2141
|
+
return candidates > 0 ? "has-candidates" : "empty";
|
|
2142
|
+
}
|
|
2143
|
+
function workItemsSignal(dir) {
|
|
2144
|
+
const wis = discoverWorkItems(dir);
|
|
2145
|
+
if (wis.length === 0) return "none";
|
|
2146
|
+
if (wis.some((w) => w.lifecycle === "in-progress")) return "in-progress";
|
|
2147
|
+
if (wis.some((w) => w.lifecycle === "ready")) return "ready";
|
|
2148
|
+
return "none-ready";
|
|
2149
|
+
}
|
|
2150
|
+
function installedAdapters(dir) {
|
|
2151
|
+
return buildAdapterStatuses(dir).filter(
|
|
2152
|
+
(s) => s.state === "injected" || s.state === "legacy-injected" || s.state === "full-generated" && (s.originAdapter === s.id || s.originAdapter === null)
|
|
2153
|
+
).map((s) => s.id);
|
|
2154
|
+
}
|
|
2155
|
+
function buildReadinessReport(dir, now = /* @__PURE__ */ new Date()) {
|
|
2156
|
+
const config = loadConfig(dir);
|
|
2157
|
+
const stub = (overall2, label, command) => ({
|
|
2158
|
+
project_name: config?.project.name ?? "unknown",
|
|
2159
|
+
project_type: config?.project.state ?? "unknown",
|
|
2160
|
+
overall: overall2,
|
|
2161
|
+
signals: {
|
|
2162
|
+
scan: "missing",
|
|
2163
|
+
understand: "missing",
|
|
2164
|
+
bootstrap_baseline: "incomplete",
|
|
2165
|
+
agents: "missing",
|
|
2166
|
+
skills: "missing",
|
|
2167
|
+
current_state: "missing",
|
|
2168
|
+
codebase: "missing",
|
|
2169
|
+
capabilities: "missing",
|
|
2170
|
+
product: "missing",
|
|
2171
|
+
business: "missing",
|
|
2172
|
+
roadmap: "missing",
|
|
2173
|
+
work_items: "none",
|
|
2174
|
+
adapters: [],
|
|
2175
|
+
blocking_open_questions: 0,
|
|
2176
|
+
assumed_questions: 0,
|
|
2177
|
+
resolved_questions: 0,
|
|
2178
|
+
deferred_questions: 0
|
|
2179
|
+
},
|
|
2180
|
+
recommended_next_step: { label, ...command ? { command } : {} }
|
|
2181
|
+
});
|
|
2182
|
+
if (!config) return stub("not-initialized", "Run `kaddo init` to initialize Kaddo.", "kaddo init");
|
|
2183
|
+
if (config.project.state === "new") return stub("not-applicable", "This project uses the standard new-project Kaddo flow.");
|
|
2184
|
+
if (config.project.state === "legacy") return stub("legacy-project", "Use the legacy project flow.");
|
|
2185
|
+
const scan = exists(join(dir, ".kaddo", "scan.json")) ? "available" : "missing";
|
|
2186
|
+
const understand = exists(join(dir, ".kaddo", "understand.md")) ? "available" : "missing";
|
|
2187
|
+
const presence = Object.fromEntries(KNOWLEDGE_FILES.map((f) => [f.key, knowledgePresence(dir, f.path)]));
|
|
2188
|
+
const ctx = buildCodexAdapterContext(dir);
|
|
2189
|
+
const agents = ctx.hasAgents ? "present" : "missing";
|
|
2190
|
+
const skills = ctx.hasSkills ? "present" : "missing";
|
|
2191
|
+
const bootstrapBaseline = presence.business !== "missing" && presence.product !== "missing" ? "complete" : "incomplete";
|
|
2192
|
+
const roadmap = roadmapSignal(dir);
|
|
2193
|
+
const work_items = workItemsSignal(dir);
|
|
2194
|
+
const adapters = installedAdapters(dir);
|
|
2195
|
+
const oq = buildOpenQuestionsReport(dir, now);
|
|
2196
|
+
const signals = {
|
|
2197
|
+
scan,
|
|
2198
|
+
understand,
|
|
2199
|
+
bootstrap_baseline: bootstrapBaseline,
|
|
2200
|
+
agents,
|
|
2201
|
+
skills,
|
|
2202
|
+
current_state: presence.current_state,
|
|
2203
|
+
codebase: presence.codebase,
|
|
2204
|
+
capabilities: presence.capabilities,
|
|
2205
|
+
product: presence.product,
|
|
2206
|
+
business: presence.business,
|
|
2207
|
+
roadmap,
|
|
2208
|
+
work_items,
|
|
2209
|
+
adapters,
|
|
2210
|
+
blocking_open_questions: oq.summary.blocking_open,
|
|
2211
|
+
assumed_questions: oq.summary.resolution.assumed,
|
|
2212
|
+
resolved_questions: oq.summary.resolution.resolved,
|
|
2213
|
+
deferred_questions: oq.summary.resolution.deferred
|
|
2214
|
+
};
|
|
2215
|
+
let overall;
|
|
2216
|
+
let next;
|
|
2217
|
+
const firstWeak = KNOWLEDGE_FILES.find((f) => presence[f.key] !== "present");
|
|
2218
|
+
if (scan === "missing") {
|
|
2219
|
+
overall = "initialized";
|
|
2220
|
+
next = { label: "Run `kaddo scan` to capture deterministic signals from the existing code.", command: "kaddo scan" };
|
|
2221
|
+
} else if (bootstrapBaseline === "incomplete") {
|
|
2222
|
+
overall = "bootstrap-incomplete";
|
|
2223
|
+
next = { label: "Run `kaddo bootstrap` to create the project knowledge baseline.", command: "kaddo bootstrap" };
|
|
2224
|
+
} else if (agents === "missing") {
|
|
2225
|
+
overall = "agents-missing";
|
|
2226
|
+
next = { label: "Run `kaddo add agents` to install the Kaddo agent prompt packs.", command: "kaddo add agents" };
|
|
2227
|
+
} else if (skills === "missing") {
|
|
2228
|
+
overall = "skills-missing";
|
|
2229
|
+
next = { label: "Run `kaddo add skills` to install reusable Kaddo skills.", command: "kaddo add skills" };
|
|
2230
|
+
} else if (understand === "missing") {
|
|
2231
|
+
overall = "scanned";
|
|
2232
|
+
next = { label: "Run `kaddo understand` to summarize the project context.", command: "kaddo understand" };
|
|
2233
|
+
} else if (firstWeak) {
|
|
2234
|
+
overall = "knowledge-incomplete";
|
|
2235
|
+
next = { label: `Complete \`${firstWeak.label}\` (it is ${presence[firstWeak.key]}).` };
|
|
2236
|
+
} else if (oq.summary.blocking_open > 0) {
|
|
2237
|
+
overall = "needs-decisions";
|
|
2238
|
+
next = { label: "Resolve, assume or defer the blocking open questions (`kaddo questions`).", command: "kaddo questions" };
|
|
2239
|
+
} else if (roadmap !== "has-candidates") {
|
|
2240
|
+
overall = "ready-for-roadmap";
|
|
2241
|
+
next = { label: "Run `kaddo roadmap` to draft the roadmap from the current knowledge.", command: "kaddo roadmap" };
|
|
2242
|
+
} else if (work_items === "none" || work_items === "none-ready") {
|
|
2243
|
+
overall = "ready-for-work-item";
|
|
2244
|
+
next = { label: "Run `kaddo create --from roadmap` to materialize the first Work Item.", command: "kaddo create --from roadmap" };
|
|
2245
|
+
} else {
|
|
2246
|
+
overall = "ready-for-implementation";
|
|
2247
|
+
next = adapters.length > 0 ? { label: "Implement the ready Work Item and run `kaddo guard`." } : { label: "Run `kaddo adapters list` and install the adapter for your preferred agent.", command: "kaddo adapters list" };
|
|
2248
|
+
}
|
|
2249
|
+
return { project_name: config.project.name, project_type: config.project.state, overall, signals, recommended_next_step: next };
|
|
2250
|
+
}
|
|
2251
|
+
|
|
1686
2252
|
// ../cli/src/core/project-explain.ts
|
|
1687
2253
|
var ARCH_DIR2 = "knowledge";
|
|
1688
2254
|
function normalizeTitle(t) {
|
|
@@ -1891,7 +2457,8 @@ function buildProjectExplanation(dir) {
|
|
|
1891
2457
|
roadmap,
|
|
1892
2458
|
mappedModules,
|
|
1893
2459
|
missingKnowledge,
|
|
1894
|
-
suggestedNextSteps
|
|
2460
|
+
suggestedNextSteps,
|
|
2461
|
+
readiness: buildReadinessReport(dir)
|
|
1895
2462
|
};
|
|
1896
2463
|
}
|
|
1897
2464
|
function stateLabel(state) {
|
|
@@ -1959,8 +2526,8 @@ function renderExplanationHuman(exp) {
|
|
|
1959
2526
|
lines.push("");
|
|
1960
2527
|
if (exp.workItems.total > 0) {
|
|
1961
2528
|
lines.push("## Work Items");
|
|
1962
|
-
for (const
|
|
1963
|
-
lines.push(`- ${LIFECYCLE_LABEL[
|
|
2529
|
+
for (const s2 of LIFECYCLE_STATES) {
|
|
2530
|
+
lines.push(`- ${LIFECYCLE_LABEL[s2]}: ${exp.workItems.byState[s2]}`);
|
|
1964
2531
|
}
|
|
1965
2532
|
lines.push("");
|
|
1966
2533
|
const typeEntries = Object.entries(exp.workItems.byType).sort((a, b) => b[1] - a[1]);
|
|
@@ -1970,13 +2537,13 @@ function renderExplanationHuman(exp) {
|
|
|
1970
2537
|
lines.push("");
|
|
1971
2538
|
}
|
|
1972
2539
|
const grouped = exp.workItems.initiatives.filter(
|
|
1973
|
-
(g) => LIFECYCLE_STATES.some((
|
|
2540
|
+
(g) => LIFECYCLE_STATES.some((s2) => g.states[s2] > 0)
|
|
1974
2541
|
);
|
|
1975
2542
|
if (grouped.length > 0) {
|
|
1976
2543
|
lines.push("## Work Items by Initiative");
|
|
1977
2544
|
for (const g of grouped) {
|
|
1978
|
-
const parts = LIFECYCLE_STATES.filter((
|
|
1979
|
-
(
|
|
2545
|
+
const parts = LIFECYCLE_STATES.filter((s2) => g.states[s2] > 0).map(
|
|
2546
|
+
(s2) => `${LIFECYCLE_LABEL[s2]}: ${g.states[s2]}`
|
|
1980
2547
|
);
|
|
1981
2548
|
lines.push(`- ${g.name} \u2014 ${parts.join(" \xB7 ")}`);
|
|
1982
2549
|
}
|
|
@@ -2070,15 +2637,39 @@ function renderExplanationHuman(exp) {
|
|
|
2070
2637
|
lines.push(`- Phase: ${assessment.phase}`);
|
|
2071
2638
|
if (assessment.reasons.length > 0) {
|
|
2072
2639
|
lines.push("- Reason:");
|
|
2073
|
-
for (const
|
|
2640
|
+
for (const r2 of assessment.reasons) lines.push(` - ${r2}`);
|
|
2074
2641
|
}
|
|
2075
2642
|
if (assessment.nextStep) lines.push(`- Next step: ${assessment.nextStep}`);
|
|
2076
2643
|
lines.push("");
|
|
2077
2644
|
if (exp.suggestedNextSteps.length > 0) {
|
|
2078
2645
|
lines.push("## Suggested Next Steps");
|
|
2079
|
-
exp.suggestedNextSteps.forEach((
|
|
2646
|
+
exp.suggestedNextSteps.forEach((s2, i) => lines.push(`${i + 1}. ${s2}`));
|
|
2080
2647
|
lines.push("");
|
|
2081
2648
|
}
|
|
2649
|
+
const r = exp.readiness;
|
|
2650
|
+
const s = r.signals;
|
|
2651
|
+
lines.push("## Project Readiness");
|
|
2652
|
+
lines.push(`- overall: ${r.overall}`);
|
|
2653
|
+
if (r.overall !== "not-initialized" && r.overall !== "not-applicable" && r.overall !== "legacy-project") {
|
|
2654
|
+
lines.push(`- bootstrap baseline: ${s.bootstrap_baseline}`);
|
|
2655
|
+
lines.push(`- scan: ${s.scan}`);
|
|
2656
|
+
lines.push(`- understand: ${s.understand}`);
|
|
2657
|
+
lines.push(`- agents: ${s.agents}`);
|
|
2658
|
+
lines.push(`- skills: ${s.skills}`);
|
|
2659
|
+
lines.push(`- current-state: ${s.current_state}`);
|
|
2660
|
+
lines.push(`- codebase: ${s.codebase}`);
|
|
2661
|
+
lines.push(`- capabilities: ${s.capabilities}`);
|
|
2662
|
+
lines.push(`- roadmap: ${s.roadmap}`);
|
|
2663
|
+
lines.push(`- work-items: ${s.work_items}`);
|
|
2664
|
+
lines.push(`- adapters: ${s.adapters.length > 0 ? s.adapters.join(", ") + " installed" : "none installed"}`);
|
|
2665
|
+
lines.push(`- blocking open questions: ${s.blocking_open_questions}`);
|
|
2666
|
+
lines.push(`- assumptions: ${s.assumed_questions}`);
|
|
2667
|
+
lines.push(`- deferred: ${s.deferred_questions}`);
|
|
2668
|
+
}
|
|
2669
|
+
lines.push("");
|
|
2670
|
+
lines.push("### Recommended next step");
|
|
2671
|
+
lines.push(r.recommended_next_step.label);
|
|
2672
|
+
lines.push("");
|
|
2082
2673
|
return lines.join("\n").trimEnd() + "\n";
|
|
2083
2674
|
}
|
|
2084
2675
|
function renderExplanationAgent(exp) {
|
|
@@ -2776,498 +3367,218 @@ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
|
2776
3367
|
ownership_coverage_percent: impact.ownership_coverage.coverage_percent,
|
|
2777
3368
|
work_items_with_acceptance_criteria: acceptanceCov ? `${acceptanceCov.have}/${acceptanceCov.total}` : "0/0",
|
|
2778
3369
|
graph_quality: graphQuality,
|
|
2779
|
-
graph_nodes: impact.graph_quality.available ? impact.graph_quality.nodes : null,
|
|
2780
|
-
graph_edges: impact.graph_quality.available ? impact.graph_quality.edges : null,
|
|
2781
|
-
context_readiness: readiness,
|
|
2782
|
-
guard_history_available: guard.available,
|
|
2783
|
-
guard_runs_recorded: guard.total_runs,
|
|
2784
|
-
resolved_drift_warnings: guard.resolved
|
|
2785
|
-
},
|
|
2786
|
-
estimated_savings: {
|
|
2787
|
-
context_preparation: { hours: ctxPrepH, formula: `${completed} Work Items \xD7 ${a.context_preparation_minutes_saved_per_work_item} min` },
|
|
2788
|
-
review_effort: { hours: reviewH, formula: `${wiWithOwnership} Work Items with ownership \xD7 ${a.review_minutes_saved_per_work_item_with_ownership} min` },
|
|
2789
|
-
clarification_reduction: { hours: clarH, formula: `${wiWithAcceptance} Work Items with Acceptance Criteria \xD7 ${a.clarification_minutes_saved_per_ready_work_item} min` },
|
|
2790
|
-
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` },
|
|
2791
|
-
architecture_discovery: { hours: archH, formula: `${a.architecture_discovery_hours_saved_when_graph_good} h \xD7 ${graphMult.toFixed(2)} graph multiplier` },
|
|
2792
|
-
drift_prevention: driftSavings
|
|
2793
|
-
},
|
|
2794
|
-
total: { estimated_hours_saved: totalHours, estimated_value: totalValue, currency: a.currency },
|
|
2795
|
-
confidence: { level, reasons },
|
|
2796
|
-
suggested_actions: actions
|
|
2797
|
-
};
|
|
2798
|
-
}
|
|
2799
|
-
function renderSavingsMarkdown(r) {
|
|
2800
|
-
const L = [];
|
|
2801
|
-
L.push("# Kaddo Estimated Savings Report", "");
|
|
2802
|
-
L.push(`Generated at: ${r.generated_at}`);
|
|
2803
|
-
L.push(`Project: ${r.project}`);
|
|
2804
|
-
L.push(`Scope: ${r.scope}`);
|
|
2805
|
-
L.push(`Currency: ${r.currency}`);
|
|
2806
|
-
L.push("");
|
|
2807
|
-
L.push("## Disclaimer", "");
|
|
2808
|
-
L.push(r.disclaimer, "");
|
|
2809
|
-
L.push("## Executive Summary", "");
|
|
2810
|
-
L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} hours`);
|
|
2811
|
-
L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
|
|
2812
|
-
L.push(`- Confidence: ${r.confidence.level}`);
|
|
2813
|
-
if (r.assumptions_source === "default") L.push("- Using default assumptions. Run `kaddo savings init` to customize them.");
|
|
2814
|
-
L.push("");
|
|
2815
|
-
L.push("## Assumptions", "");
|
|
2816
|
-
L.push(`- Hourly cost: ${r.assumptions.hourly_cost} ${r.currency}`);
|
|
2817
|
-
L.push(`- Context preparation saved per Work Item: ${r.assumptions.context_preparation_minutes_saved_per_work_item} min`);
|
|
2818
|
-
L.push(`- Review saved per Work Item with ownership: ${r.assumptions.review_minutes_saved_per_work_item_with_ownership} min`);
|
|
2819
|
-
L.push(`- Clarification saved per Work Item with acceptance criteria: ${r.assumptions.clarification_minutes_saved_per_ready_work_item} min`);
|
|
2820
|
-
L.push(`- Onboarding saved per new contributor: ${r.assumptions.onboarding_hours_saved_per_new_contributor} h`);
|
|
2821
|
-
L.push(`- Expected new contributors per month: ${r.assumptions.expected_new_contributors_per_month}`);
|
|
2822
|
-
L.push(`- Architecture discovery saved when graph is good: ${r.assumptions.architecture_discovery_hours_saved_when_graph_good} h`);
|
|
2823
|
-
L.push("");
|
|
2824
|
-
L.push("## Evidence Used", "");
|
|
2825
|
-
const e = r.evidence;
|
|
2826
|
-
L.push(`- Knowledge Impact Score: ${e.knowledge_impact_score ?? "not available"}${e.knowledge_impact_score !== null ? "/100" : ""}`);
|
|
2827
|
-
L.push(`- Completed Work Items: ${e.completed_work_items}`);
|
|
2828
|
-
L.push(`- Ownership coverage: ${e.ownership_coverage_percent}%`);
|
|
2829
|
-
L.push(`- Work Items with Acceptance Criteria: ${e.work_items_with_acceptance_criteria}`);
|
|
2830
|
-
L.push(`- Graph quality: ${e.graph_quality}`);
|
|
2831
|
-
if (e.graph_nodes !== null) L.push(`- Graph nodes: ${e.graph_nodes}`);
|
|
2832
|
-
if (e.graph_edges !== null) L.push(`- Graph edges: ${e.graph_edges}`);
|
|
2833
|
-
L.push(`- Context readiness: ${e.context_readiness}`);
|
|
2834
|
-
if (e.guard_history_available) {
|
|
2835
|
-
L.push("- Guard history: available");
|
|
2836
|
-
L.push(`- Guard runs recorded: ${e.guard_runs_recorded}`);
|
|
2837
|
-
L.push(`- Resolved drift warnings: ${e.resolved_drift_warnings}`);
|
|
2838
|
-
} else {
|
|
2839
|
-
L.push("- Guard history: not available");
|
|
2840
|
-
}
|
|
2841
|
-
L.push("");
|
|
2842
|
-
L.push("## Estimated Savings", "");
|
|
2843
|
-
const s = r.estimated_savings;
|
|
2844
|
-
const line = (title, l) => {
|
|
2845
|
-
L.push(`### ${title}`, "");
|
|
2846
|
-
L.push(`- Formula: ${l.formula}`);
|
|
2847
|
-
L.push(`- Estimated: ${l.hours} h`, "");
|
|
2848
|
-
};
|
|
2849
|
-
line("Context Preparation", s.context_preparation);
|
|
2850
|
-
line("Review Effort", s.review_effort);
|
|
2851
|
-
line("Clarification Reduction", s.clarification_reduction);
|
|
2852
|
-
line("Onboarding", s.onboarding);
|
|
2853
|
-
line("Architecture Discovery", s.architecture_discovery);
|
|
2854
|
-
L.push("### Drift Prevention", "");
|
|
2855
|
-
if (s.drift_prevention.available) {
|
|
2856
|
-
L.push(`- Formula: ${s.drift_prevention.formula}`);
|
|
2857
|
-
L.push(`- Estimated: ${s.drift_prevention.hours} h`);
|
|
2858
|
-
if (s.drift_prevention.reason) L.push(`- Reason: ${s.drift_prevention.reason}`);
|
|
2859
|
-
L.push("");
|
|
2860
|
-
} else {
|
|
2861
|
-
L.push("- Not available yet.");
|
|
2862
|
-
L.push(`- Reason: ${s.drift_prevention.reason}`, "");
|
|
2863
|
-
}
|
|
2864
|
-
L.push("## Total", "");
|
|
2865
|
-
L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} h`);
|
|
2866
|
-
L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
|
|
2867
|
-
L.push("");
|
|
2868
|
-
L.push("## Confidence", "");
|
|
2869
|
-
L.push(`Confidence: ${r.confidence.level}`, "", "Reason:");
|
|
2870
|
-
for (const reason of r.confidence.reasons) L.push(`- ${reason}`);
|
|
2871
|
-
L.push("");
|
|
2872
|
-
L.push("## Suggested Actions", "");
|
|
2873
|
-
r.suggested_actions.forEach((act, i) => L.push(`${i + 1}. ${act}`));
|
|
2874
|
-
L.push("");
|
|
2875
|
-
return L.join("\n");
|
|
2876
|
-
}
|
|
2877
|
-
function serializeSavingsJson(r) {
|
|
2878
|
-
return JSON.stringify(r, null, 2) + "\n";
|
|
2879
|
-
}
|
|
2880
|
-
|
|
2881
|
-
// ../cli/src/core/drift-report.ts
|
|
2882
|
-
function buildDriftReport(dir, now = /* @__PURE__ */ new Date()) {
|
|
2883
|
-
const h = buildGuardHistory(dir);
|
|
2884
|
-
const project = loadConfig(dir)?.project.name ?? "unknown";
|
|
2885
|
-
const actions = [];
|
|
2886
|
-
if (!h.available) {
|
|
2887
|
-
actions.push("Run `kaddo guard --record` to start recording guard history.");
|
|
2888
|
-
} else {
|
|
2889
|
-
if (h.open > 0) actions.push("Review open drift warnings and update related Work Items or ownership.");
|
|
2890
|
-
actions.push("Run `kaddo guard --record` before important commits.");
|
|
2891
|
-
actions.push("Use this report to calibrate estimated drift-prevention savings.");
|
|
2892
|
-
}
|
|
2893
|
-
return {
|
|
2894
|
-
generated_at: now.toISOString(),
|
|
2895
|
-
project,
|
|
2896
|
-
guard_history: {
|
|
2897
|
-
available: h.available,
|
|
2898
|
-
total_runs: h.total_runs,
|
|
2899
|
-
first_run: h.first_run,
|
|
2900
|
-
last_run: h.last_run,
|
|
2901
|
-
runs_with_warnings: h.runs_with_warnings,
|
|
2902
|
-
runs_clean: h.runs_clean
|
|
3370
|
+
graph_nodes: impact.graph_quality.available ? impact.graph_quality.nodes : null,
|
|
3371
|
+
graph_edges: impact.graph_quality.available ? impact.graph_quality.edges : null,
|
|
3372
|
+
context_readiness: readiness,
|
|
3373
|
+
guard_history_available: guard.available,
|
|
3374
|
+
guard_runs_recorded: guard.total_runs,
|
|
3375
|
+
resolved_drift_warnings: guard.resolved
|
|
2903
3376
|
},
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
3377
|
+
estimated_savings: {
|
|
3378
|
+
context_preparation: { hours: ctxPrepH, formula: `${completed} Work Items \xD7 ${a.context_preparation_minutes_saved_per_work_item} min` },
|
|
3379
|
+
review_effort: { hours: reviewH, formula: `${wiWithOwnership} Work Items with ownership \xD7 ${a.review_minutes_saved_per_work_item_with_ownership} min` },
|
|
3380
|
+
clarification_reduction: { hours: clarH, formula: `${wiWithAcceptance} Work Items with Acceptance Criteria \xD7 ${a.clarification_minutes_saved_per_ready_work_item} min` },
|
|
3381
|
+
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` },
|
|
3382
|
+
architecture_discovery: { hours: archH, formula: `${a.architecture_discovery_hours_saved_when_graph_good} h \xD7 ${graphMult.toFixed(2)} graph multiplier` },
|
|
3383
|
+
drift_prevention: driftSavings
|
|
3384
|
+
},
|
|
3385
|
+
total: { estimated_hours_saved: totalHours, estimated_value: totalValue, currency: a.currency },
|
|
3386
|
+
confidence: { level, reasons },
|
|
2908
3387
|
suggested_actions: actions
|
|
2909
3388
|
};
|
|
2910
3389
|
}
|
|
2911
|
-
function
|
|
3390
|
+
function renderSavingsMarkdown(r) {
|
|
2912
3391
|
const L = [];
|
|
2913
|
-
L.push("# Kaddo
|
|
3392
|
+
L.push("# Kaddo Estimated Savings Report", "");
|
|
2914
3393
|
L.push(`Generated at: ${r.generated_at}`);
|
|
2915
3394
|
L.push(`Project: ${r.project}`);
|
|
3395
|
+
L.push(`Scope: ${r.scope}`);
|
|
3396
|
+
L.push(`Currency: ${r.currency}`);
|
|
2916
3397
|
L.push("");
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
L.push("No guard history recorded yet.");
|
|
2920
|
-
L.push("");
|
|
2921
|
-
L.push("Run `kaddo guard --record` to start recording drift evidence.");
|
|
2922
|
-
L.push("");
|
|
2923
|
-
L.push("## Suggested Actions", "");
|
|
2924
|
-
r.suggested_actions.forEach((a, i) => L.push(`${i + 1}. ${a}`));
|
|
2925
|
-
L.push("");
|
|
2926
|
-
return L.join("\n");
|
|
2927
|
-
}
|
|
2928
|
-
const w = r.drift_warnings;
|
|
3398
|
+
L.push("## Disclaimer", "");
|
|
3399
|
+
L.push(r.disclaimer, "");
|
|
2929
3400
|
L.push("## Executive Summary", "");
|
|
2930
|
-
L.push(`-
|
|
2931
|
-
L.push(`-
|
|
2932
|
-
L.push(`-
|
|
2933
|
-
L.push(
|
|
2934
|
-
L.push(`- Resolution rate: ${w.resolution_rate}%`);
|
|
2935
|
-
if (r.hotspots.length > 0) L.push(`- Most affected area: ${r.hotspots[0].path}`);
|
|
2936
|
-
L.push("");
|
|
2937
|
-
L.push("## Guard History", "");
|
|
2938
|
-
L.push(`- First run: ${r.guard_history.first_run}`);
|
|
2939
|
-
L.push(`- Last run: ${r.guard_history.last_run}`);
|
|
2940
|
-
L.push(`- Runs with warnings: ${r.guard_history.runs_with_warnings}/${r.guard_history.total_runs}`);
|
|
2941
|
-
L.push(`- Runs clean: ${r.guard_history.runs_clean}/${r.guard_history.total_runs}`);
|
|
2942
|
-
L.push("");
|
|
2943
|
-
L.push("## Drift Warnings", "");
|
|
2944
|
-
const open = r.threads.filter((t) => t.status === "open");
|
|
2945
|
-
const resolved = r.threads.filter((t) => t.status === "resolved");
|
|
2946
|
-
L.push("### Open", "");
|
|
2947
|
-
if (open.length === 0) L.push("_None._", "");
|
|
2948
|
-
for (const t of open) {
|
|
2949
|
-
L.push(`- ${t.code_path}`);
|
|
2950
|
-
L.push(` - Related artifacts: ${t.related_artifacts.join(", ") || "\u2014"}`);
|
|
2951
|
-
L.push(` - First detected: ${t.first_detected}`);
|
|
2952
|
-
L.push(` - Last seen: ${t.last_seen}`);
|
|
2953
|
-
L.push(" - Status: open");
|
|
2954
|
-
}
|
|
2955
|
-
L.push("");
|
|
2956
|
-
L.push("### Resolved", "");
|
|
2957
|
-
if (resolved.length === 0) L.push("_None._", "");
|
|
2958
|
-
for (const t of resolved) {
|
|
2959
|
-
L.push(`- ${t.code_path}`);
|
|
2960
|
-
L.push(` - Related artifacts: ${t.related_artifacts.join(", ") || "\u2014"}`);
|
|
2961
|
-
L.push(` - First detected: ${t.first_detected}`);
|
|
2962
|
-
L.push(` - Resolved at: ${t.resolved_at}`);
|
|
2963
|
-
L.push(" - Status: resolved");
|
|
2964
|
-
}
|
|
2965
|
-
L.push("");
|
|
2966
|
-
L.push("## Hotspots", "");
|
|
2967
|
-
if (r.hotspots.length === 0) L.push("_None._");
|
|
2968
|
-
for (const h of r.hotspots) L.push(`- ${h.path}: ${h.warnings} warning(s)`);
|
|
2969
|
-
L.push("");
|
|
2970
|
-
L.push("## Trend", "");
|
|
2971
|
-
L.push(`- Recent direction: ${r.trend.direction}`);
|
|
2972
|
-
L.push(`- Reason: ${r.trend.reason}`);
|
|
3401
|
+
L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} hours`);
|
|
3402
|
+
L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
|
|
3403
|
+
L.push(`- Confidence: ${r.confidence.level}`);
|
|
3404
|
+
if (r.assumptions_source === "default") L.push("- Using default assumptions. Run `kaddo savings init` to customize them.");
|
|
2973
3405
|
L.push("");
|
|
2974
|
-
L.push("##
|
|
2975
|
-
|
|
3406
|
+
L.push("## Assumptions", "");
|
|
3407
|
+
L.push(`- Hourly cost: ${r.assumptions.hourly_cost} ${r.currency}`);
|
|
3408
|
+
L.push(`- Context preparation saved per Work Item: ${r.assumptions.context_preparation_minutes_saved_per_work_item} min`);
|
|
3409
|
+
L.push(`- Review saved per Work Item with ownership: ${r.assumptions.review_minutes_saved_per_work_item_with_ownership} min`);
|
|
3410
|
+
L.push(`- Clarification saved per Work Item with acceptance criteria: ${r.assumptions.clarification_minutes_saved_per_ready_work_item} min`);
|
|
3411
|
+
L.push(`- Onboarding saved per new contributor: ${r.assumptions.onboarding_hours_saved_per_new_contributor} h`);
|
|
3412
|
+
L.push(`- Expected new contributors per month: ${r.assumptions.expected_new_contributors_per_month}`);
|
|
3413
|
+
L.push(`- Architecture discovery saved when graph is good: ${r.assumptions.architecture_discovery_hours_saved_when_graph_good} h`);
|
|
2976
3414
|
L.push("");
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
}
|
|
2992
|
-
|
|
2993
|
-
// ../cli/src/core/open-questions.ts
|
|
2994
|
-
var RESOLUTION_TOKENS = {
|
|
2995
|
-
open: "open",
|
|
2996
|
-
resolved: "resolved",
|
|
2997
|
-
assumed: "assumed",
|
|
2998
|
-
deferred: "deferred",
|
|
2999
|
-
abierta: "open",
|
|
3000
|
-
resuelta: "resolved",
|
|
3001
|
-
asumida: "assumed",
|
|
3002
|
-
diferida: "deferred"
|
|
3003
|
-
};
|
|
3004
|
-
function parseResolution(line) {
|
|
3005
|
-
const m = line.match(/^\[([a-zA-Záéí]+)\]\s*(.*)$/);
|
|
3006
|
-
if (m) {
|
|
3007
|
-
const status = RESOLUTION_TOKENS[m[1].toLowerCase()];
|
|
3008
|
-
if (status) return { text: m[2].trim(), resolution_status: status };
|
|
3009
|
-
}
|
|
3010
|
-
return { text: line.trim(), resolution_status: "open" };
|
|
3011
|
-
}
|
|
3012
|
-
var SOURCES = [
|
|
3013
|
-
"knowledge/business/business.md",
|
|
3014
|
-
"knowledge/product/product.md",
|
|
3015
|
-
"knowledge/tech/codebase.md",
|
|
3016
|
-
"knowledge/delivery/roadmap.md"
|
|
3017
|
-
];
|
|
3018
|
-
var BLOCKING = [
|
|
3019
|
-
"mvp",
|
|
3020
|
-
"stack",
|
|
3021
|
-
"arquitect",
|
|
3022
|
-
"architecture",
|
|
3023
|
-
"api",
|
|
3024
|
-
"web app",
|
|
3025
|
-
"webapp",
|
|
3026
|
-
"cli",
|
|
3027
|
-
"backend",
|
|
3028
|
-
"auth",
|
|
3029
|
-
"autenticaci",
|
|
3030
|
-
"persist",
|
|
3031
|
-
"base de datos",
|
|
3032
|
-
"database",
|
|
3033
|
-
"roadmap",
|
|
3034
|
-
"alcance",
|
|
3035
|
-
"scope",
|
|
3036
|
-
"primera versi\xF3n",
|
|
3037
|
-
"primera version",
|
|
3038
|
-
"first version",
|
|
3039
|
-
"fastify",
|
|
3040
|
-
"nestjs",
|
|
3041
|
-
"framework"
|
|
3042
|
-
];
|
|
3043
|
-
var DEFERRED = [
|
|
3044
|
-
"integraci",
|
|
3045
|
-
"integration",
|
|
3046
|
-
"analytics",
|
|
3047
|
-
"notificaci",
|
|
3048
|
-
"notification",
|
|
3049
|
-
"google calendar",
|
|
3050
|
-
"eventbrite",
|
|
3051
|
-
"meetup",
|
|
3052
|
-
"dashboard",
|
|
3053
|
-
"multi-tenant",
|
|
3054
|
-
"multitenant",
|
|
3055
|
-
"mobile",
|
|
3056
|
-
"m\xF3vil",
|
|
3057
|
-
"pago",
|
|
3058
|
-
"payment",
|
|
3059
|
-
"ticketing",
|
|
3060
|
-
"multiple tracks",
|
|
3061
|
-
"m\xFAltiples tracks",
|
|
3062
|
-
"webhook"
|
|
3063
|
-
];
|
|
3064
|
-
var IMPORTANT = [
|
|
3065
|
-
"validaci",
|
|
3066
|
-
"validation",
|
|
3067
|
-
"paginaci",
|
|
3068
|
-
"pagination",
|
|
3069
|
-
"soft delete",
|
|
3070
|
-
"sponsor",
|
|
3071
|
-
"agenda",
|
|
3072
|
-
"ubicaci",
|
|
3073
|
-
"location",
|
|
3074
|
-
"rol",
|
|
3075
|
-
"role",
|
|
3076
|
-
"permis",
|
|
3077
|
-
"permission"
|
|
3078
|
-
];
|
|
3079
|
-
function matchesAny(text2, keywords) {
|
|
3080
|
-
return keywords.some((k) => text2.includes(k));
|
|
3081
|
-
}
|
|
3082
|
-
function classifyQuestion(question) {
|
|
3083
|
-
const t = question.toLowerCase();
|
|
3084
|
-
if (matchesAny(t, BLOCKING)) {
|
|
3085
|
-
return { classification: "blocking", reason: "Affects scope, architecture or the first Work Items." };
|
|
3086
|
-
}
|
|
3087
|
-
if (matchesAny(t, DEFERRED) && !matchesAny(t, IMPORTANT)) {
|
|
3088
|
-
return { classification: "deferred", reason: "Can be moved to a later phase." };
|
|
3089
|
-
}
|
|
3090
|
-
if (matchesAny(t, IMPORTANT)) {
|
|
3091
|
-
return { classification: "important", reason: "Relevant, but a temporary assumption can unblock progress." };
|
|
3092
|
-
}
|
|
3093
|
-
return { classification: "important", reason: "Relevant, but a temporary assumption can unblock progress." };
|
|
3094
|
-
}
|
|
3095
|
-
function extractFromMarkdown(md) {
|
|
3096
|
-
const lines = md.split(/\r?\n/);
|
|
3097
|
-
const start = lines.findIndex((l) => /^#{1,6}\s+open questions\s*$/i.test(l.trim()) || /^#{1,6}\s+preguntas abiertas\s*$/i.test(l.trim()));
|
|
3098
|
-
if (start < 0) return [];
|
|
3099
|
-
const out = [];
|
|
3100
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
3101
|
-
const line = lines[i];
|
|
3102
|
-
if (/^#{1,6}\s+/.test(line)) break;
|
|
3103
|
-
const meta = line.match(/^\s+(?:[-*])\s+(\w+)\s*:\s*(.+?)\s*$/);
|
|
3104
|
-
if (meta && out.length > 0) {
|
|
3105
|
-
if (meta[1].toLowerCase() === "note") out[out.length - 1].resolution_note = meta[2].trim();
|
|
3106
|
-
continue;
|
|
3107
|
-
}
|
|
3108
|
-
const m = line.match(/^\s*(?:[-*]|\d+\.)\s+(.+?)\s*$/);
|
|
3109
|
-
if (m) {
|
|
3110
|
-
const raw = m[1].trim();
|
|
3111
|
-
if (!raw || /^_.*_$/.test(raw)) continue;
|
|
3112
|
-
const { text: text2, resolution_status } = parseResolution(raw);
|
|
3113
|
-
if (text2) out.push({ text: text2, resolution_status });
|
|
3114
|
-
}
|
|
3115
|
-
}
|
|
3116
|
-
return out;
|
|
3117
|
-
}
|
|
3118
|
-
function suggestedAssumption(question) {
|
|
3119
|
-
const t = question.toLowerCase();
|
|
3120
|
-
if (matchesAny(t, ["api", "web app", "webapp", "cli", "backend"])) {
|
|
3121
|
-
return "Start as a backend API to keep the initial scope small.";
|
|
3122
|
-
}
|
|
3123
|
-
if (matchesAny(t, ["auth", "autenticaci"])) {
|
|
3124
|
-
return "No authentication in the MVP.";
|
|
3125
|
-
}
|
|
3126
|
-
if (matchesAny(t, ["fastify", "nestjs", "framework", "stack"])) {
|
|
3127
|
-
return "Choose the lighter stack option to minimize MVP scope.";
|
|
3415
|
+
L.push("## Evidence Used", "");
|
|
3416
|
+
const e = r.evidence;
|
|
3417
|
+
L.push(`- Knowledge Impact Score: ${e.knowledge_impact_score ?? "not available"}${e.knowledge_impact_score !== null ? "/100" : ""}`);
|
|
3418
|
+
L.push(`- Completed Work Items: ${e.completed_work_items}`);
|
|
3419
|
+
L.push(`- Ownership coverage: ${e.ownership_coverage_percent}%`);
|
|
3420
|
+
L.push(`- Work Items with Acceptance Criteria: ${e.work_items_with_acceptance_criteria}`);
|
|
3421
|
+
L.push(`- Graph quality: ${e.graph_quality}`);
|
|
3422
|
+
if (e.graph_nodes !== null) L.push(`- Graph nodes: ${e.graph_nodes}`);
|
|
3423
|
+
if (e.graph_edges !== null) L.push(`- Graph edges: ${e.graph_edges}`);
|
|
3424
|
+
L.push(`- Context readiness: ${e.context_readiness}`);
|
|
3425
|
+
if (e.guard_history_available) {
|
|
3426
|
+
L.push("- Guard history: available");
|
|
3427
|
+
L.push(`- Guard runs recorded: ${e.guard_runs_recorded}`);
|
|
3428
|
+
L.push(`- Resolved drift warnings: ${e.resolved_drift_warnings}`);
|
|
3429
|
+
} else {
|
|
3430
|
+
L.push("- Guard history: not available");
|
|
3128
3431
|
}
|
|
3129
|
-
|
|
3130
|
-
|
|
3432
|
+
L.push("");
|
|
3433
|
+
L.push("## Estimated Savings", "");
|
|
3434
|
+
const s = r.estimated_savings;
|
|
3435
|
+
const line = (title, l) => {
|
|
3436
|
+
L.push(`### ${title}`, "");
|
|
3437
|
+
L.push(`- Formula: ${l.formula}`);
|
|
3438
|
+
L.push(`- Estimated: ${l.hours} h`, "");
|
|
3439
|
+
};
|
|
3440
|
+
line("Context Preparation", s.context_preparation);
|
|
3441
|
+
line("Review Effort", s.review_effort);
|
|
3442
|
+
line("Clarification Reduction", s.clarification_reduction);
|
|
3443
|
+
line("Onboarding", s.onboarding);
|
|
3444
|
+
line("Architecture Discovery", s.architecture_discovery);
|
|
3445
|
+
L.push("### Drift Prevention", "");
|
|
3446
|
+
if (s.drift_prevention.available) {
|
|
3447
|
+
L.push(`- Formula: ${s.drift_prevention.formula}`);
|
|
3448
|
+
L.push(`- Estimated: ${s.drift_prevention.hours} h`);
|
|
3449
|
+
if (s.drift_prevention.reason) L.push(`- Reason: ${s.drift_prevention.reason}`);
|
|
3450
|
+
L.push("");
|
|
3451
|
+
} else {
|
|
3452
|
+
L.push("- Not available yet.");
|
|
3453
|
+
L.push(`- Reason: ${s.drift_prevention.reason}`, "");
|
|
3131
3454
|
}
|
|
3132
|
-
|
|
3455
|
+
L.push("## Total", "");
|
|
3456
|
+
L.push(`- Estimated time saved: ${r.total.estimated_hours_saved} h`);
|
|
3457
|
+
L.push(`- Estimated value: ${r.total.estimated_value} ${r.currency}`);
|
|
3458
|
+
L.push("");
|
|
3459
|
+
L.push("## Confidence", "");
|
|
3460
|
+
L.push(`Confidence: ${r.confidence.level}`, "", "Reason:");
|
|
3461
|
+
for (const reason of r.confidence.reasons) L.push(`- ${reason}`);
|
|
3462
|
+
L.push("");
|
|
3463
|
+
L.push("## Suggested Actions", "");
|
|
3464
|
+
r.suggested_actions.forEach((act, i) => L.push(`${i + 1}. ${act}`));
|
|
3465
|
+
L.push("");
|
|
3466
|
+
return L.join("\n");
|
|
3133
3467
|
}
|
|
3134
|
-
function
|
|
3468
|
+
function serializeSavingsJson(r) {
|
|
3469
|
+
return JSON.stringify(r, null, 2) + "\n";
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
// ../cli/src/core/drift-report.ts
|
|
3473
|
+
function buildDriftReport(dir, now = /* @__PURE__ */ new Date()) {
|
|
3474
|
+
const h = buildGuardHistory(dir);
|
|
3135
3475
|
const project = loadConfig(dir)?.project.name ?? "unknown";
|
|
3136
|
-
const
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
if (
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
md = readFile(p);
|
|
3144
|
-
} catch {
|
|
3145
|
-
continue;
|
|
3146
|
-
}
|
|
3147
|
-
for (const eq of extractFromMarkdown(md)) {
|
|
3148
|
-
seq += 1;
|
|
3149
|
-
const { classification, reason } = classifyQuestion(eq.text);
|
|
3150
|
-
questions.push({
|
|
3151
|
-
id: `OQ-${String(seq).padStart(3, "0")}`,
|
|
3152
|
-
source,
|
|
3153
|
-
section: "Open Questions",
|
|
3154
|
-
question: eq.text,
|
|
3155
|
-
classification,
|
|
3156
|
-
resolution_status: eq.resolution_status,
|
|
3157
|
-
reason,
|
|
3158
|
-
...eq.resolution_note ? { resolution_note: eq.resolution_note } : {},
|
|
3159
|
-
...classification === "blocking" && eq.resolution_status === "open" ? { suggested_assumption: suggestedAssumption(eq.text) } : {}
|
|
3160
|
-
});
|
|
3161
|
-
}
|
|
3476
|
+
const actions = [];
|
|
3477
|
+
if (!h.available) {
|
|
3478
|
+
actions.push("Run `kaddo guard --record` to start recording guard history.");
|
|
3479
|
+
} else {
|
|
3480
|
+
if (h.open > 0) actions.push("Review open drift warnings and update related Work Items or ownership.");
|
|
3481
|
+
actions.push("Run `kaddo guard --record` before important commits.");
|
|
3482
|
+
actions.push("Use this report to calibrate estimated drift-prevention savings.");
|
|
3162
3483
|
}
|
|
3163
|
-
const blocking = questions.filter((q) => q.classification === "blocking");
|
|
3164
|
-
const important = questions.filter((q) => q.classification === "important");
|
|
3165
|
-
const deferred = questions.filter((q) => q.classification === "deferred");
|
|
3166
|
-
const blockingOpen = blocking.filter((q) => q.resolution_status === "open");
|
|
3167
|
-
const importantOpen = important.filter((q) => q.resolution_status === "open");
|
|
3168
|
-
const byStatus = (s) => questions.filter((q) => q.resolution_status === s);
|
|
3169
|
-
const resolved = byStatus("resolved");
|
|
3170
|
-
const assumed = byStatus("assumed");
|
|
3171
|
-
const deferredStatus = byStatus("deferred");
|
|
3172
|
-
let readiness;
|
|
3173
|
-
if (questions.length === 0) readiness = "unknown";
|
|
3174
|
-
else if (blockingOpen.length > 0) readiness = "needs_decisions";
|
|
3175
|
-
else readiness = "ready";
|
|
3176
|
-
const suggested_assumptions = blockingOpen.map((q) => q.suggested_assumption).filter(Boolean);
|
|
3177
|
-
const recommended_next_step = readiness === "needs_decisions" ? "Confirm suggested assumptions with the user (resolve, assume or defer blocking open questions) before generating the roadmap." : readiness === "ready" ? "No blocking open questions \u2014 you can proceed to the roadmap. Surfaced assumptions/deferred items are informational." : "No open questions found. Run `kaddo bootstrap` to capture early questions, or proceed.";
|
|
3178
3484
|
return {
|
|
3179
3485
|
generated_at: now.toISOString(),
|
|
3180
3486
|
project,
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
important_open: importantOpen.length,
|
|
3189
|
-
resolution: {
|
|
3190
|
-
open: byStatus("open").length,
|
|
3191
|
-
resolved: resolved.length,
|
|
3192
|
-
assumed: assumed.length,
|
|
3193
|
-
deferred: deferredStatus.length
|
|
3194
|
-
}
|
|
3487
|
+
guard_history: {
|
|
3488
|
+
available: h.available,
|
|
3489
|
+
total_runs: h.total_runs,
|
|
3490
|
+
first_run: h.first_run,
|
|
3491
|
+
last_run: h.last_run,
|
|
3492
|
+
runs_with_warnings: h.runs_with_warnings,
|
|
3493
|
+
runs_clean: h.runs_clean
|
|
3195
3494
|
},
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
assumed_questions: assumed,
|
|
3202
|
-
deferred_status_questions: deferredStatus,
|
|
3203
|
-
suggested_assumptions,
|
|
3204
|
-
recommended_next_step
|
|
3495
|
+
drift_warnings: { detected: h.detected, open: h.open, resolved: h.resolved, resolution_rate: h.resolution_rate },
|
|
3496
|
+
hotspots: h.hotspots,
|
|
3497
|
+
trend: h.trend,
|
|
3498
|
+
threads: h.threads,
|
|
3499
|
+
suggested_actions: actions
|
|
3205
3500
|
};
|
|
3206
3501
|
}
|
|
3207
|
-
function
|
|
3502
|
+
function renderDriftMarkdown(r) {
|
|
3208
3503
|
const L = [];
|
|
3209
|
-
L.push("# Kaddo
|
|
3504
|
+
L.push("# Kaddo Drift Trend Report", "");
|
|
3210
3505
|
L.push(`Generated at: ${r.generated_at}`);
|
|
3211
3506
|
L.push(`Project: ${r.project}`);
|
|
3212
3507
|
L.push("");
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
L.push("
|
|
3508
|
+
if (!r.guard_history.available) {
|
|
3509
|
+
L.push("## Guard History", "");
|
|
3510
|
+
L.push("No guard history recorded yet.");
|
|
3511
|
+
L.push("");
|
|
3512
|
+
L.push("Run `kaddo guard --record` to start recording drift evidence.");
|
|
3513
|
+
L.push("");
|
|
3514
|
+
L.push("## Suggested Actions", "");
|
|
3515
|
+
r.suggested_actions.forEach((a, i) => L.push(`${i + 1}. ${a}`));
|
|
3516
|
+
L.push("");
|
|
3222
3517
|
return L.join("\n");
|
|
3223
3518
|
}
|
|
3224
|
-
const
|
|
3225
|
-
L.push("##
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3519
|
+
const w = r.drift_warnings;
|
|
3520
|
+
L.push("## Executive Summary", "");
|
|
3521
|
+
L.push(`- Guard runs recorded: ${r.guard_history.total_runs}`);
|
|
3522
|
+
L.push(`- Drift warnings detected: ${w.detected}`);
|
|
3523
|
+
L.push(`- Open warnings: ${w.open}`);
|
|
3524
|
+
L.push(`- Resolved warnings: ${w.resolved}`);
|
|
3525
|
+
L.push(`- Resolution rate: ${w.resolution_rate}%`);
|
|
3526
|
+
if (r.hotspots.length > 0) L.push(`- Most affected area: ${r.hotspots[0].path}`);
|
|
3527
|
+
L.push("");
|
|
3528
|
+
L.push("## Guard History", "");
|
|
3529
|
+
L.push(`- First run: ${r.guard_history.first_run}`);
|
|
3530
|
+
L.push(`- Last run: ${r.guard_history.last_run}`);
|
|
3531
|
+
L.push(`- Runs with warnings: ${r.guard_history.runs_with_warnings}/${r.guard_history.total_runs}`);
|
|
3532
|
+
L.push(`- Runs clean: ${r.guard_history.runs_clean}/${r.guard_history.total_runs}`);
|
|
3533
|
+
L.push("");
|
|
3534
|
+
L.push("## Drift Warnings", "");
|
|
3535
|
+
const open = r.threads.filter((t) => t.status === "open");
|
|
3536
|
+
const resolved = r.threads.filter((t) => t.status === "resolved");
|
|
3537
|
+
L.push("### Open", "");
|
|
3538
|
+
if (open.length === 0) L.push("_None._", "");
|
|
3539
|
+
for (const t of open) {
|
|
3540
|
+
L.push(`- ${t.code_path}`);
|
|
3541
|
+
L.push(` - Related artifacts: ${t.related_artifacts.join(", ") || "\u2014"}`);
|
|
3542
|
+
L.push(` - First detected: ${t.first_detected}`);
|
|
3543
|
+
L.push(` - Last seen: ${t.last_seen}`);
|
|
3544
|
+
L.push(" - Status: open");
|
|
3234
3545
|
}
|
|
3235
|
-
const renderStatusList = (title, items) => {
|
|
3236
|
-
L.push(`## ${title}`, "");
|
|
3237
|
-
if (items.length === 0) L.push("_None._");
|
|
3238
|
-
for (const q of items) L.push(`- ${q.question}${q.resolution_note ? ` \u2014 _${q.resolution_note}_` : ""}`);
|
|
3239
|
-
L.push("");
|
|
3240
|
-
};
|
|
3241
|
-
renderStatusList("Resolved", r.resolved_questions);
|
|
3242
|
-
renderStatusList("Assumed (decisions to revisit)", r.assumed_questions);
|
|
3243
|
-
renderStatusList("Deferred (out of current scope)", r.deferred_status_questions);
|
|
3244
|
-
L.push("## Important Questions", "");
|
|
3245
|
-
if (r.important_questions.length === 0) L.push("_None._");
|
|
3246
|
-
for (const q of r.important_questions) L.push(`- [${q.resolution_status}] ${q.question}`);
|
|
3247
3546
|
L.push("");
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
L.push(
|
|
3547
|
+
L.push("### Resolved", "");
|
|
3548
|
+
if (resolved.length === 0) L.push("_None._", "");
|
|
3549
|
+
for (const t of resolved) {
|
|
3550
|
+
L.push(`- ${t.code_path}`);
|
|
3551
|
+
L.push(` - Related artifacts: ${t.related_artifacts.join(", ") || "\u2014"}`);
|
|
3552
|
+
L.push(` - First detected: ${t.first_detected}`);
|
|
3553
|
+
L.push(` - Resolved at: ${t.resolved_at}`);
|
|
3554
|
+
L.push(" - Status: resolved");
|
|
3252
3555
|
}
|
|
3253
|
-
L.push("
|
|
3556
|
+
L.push("");
|
|
3557
|
+
L.push("## Hotspots", "");
|
|
3558
|
+
if (r.hotspots.length === 0) L.push("_None._");
|
|
3559
|
+
for (const h of r.hotspots) L.push(`- ${h.path}: ${h.warnings} warning(s)`);
|
|
3560
|
+
L.push("");
|
|
3561
|
+
L.push("## Trend", "");
|
|
3562
|
+
L.push(`- Recent direction: ${r.trend.direction}`);
|
|
3563
|
+
L.push(`- Reason: ${r.trend.reason}`);
|
|
3564
|
+
L.push("");
|
|
3565
|
+
L.push("## Suggested Actions", "");
|
|
3566
|
+
r.suggested_actions.forEach((a, i) => L.push(`${i + 1}. ${a}`));
|
|
3567
|
+
L.push("");
|
|
3254
3568
|
return L.join("\n");
|
|
3255
3569
|
}
|
|
3256
|
-
function
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
deferred_questions: r.summary.deferred,
|
|
3267
|
-
resolution: r.summary.resolution,
|
|
3268
|
-
suggested_assumptions: r.suggested_assumptions,
|
|
3269
|
-
recommended_next_step: r.summary.roadmap_readiness === "needs_decisions" ? "Resolve, assume, or defer blocking open questions before generating roadmap." : r.recommended_next_step
|
|
3570
|
+
function serializeDriftJson(r) {
|
|
3571
|
+
const out = {
|
|
3572
|
+
generated_at: r.generated_at,
|
|
3573
|
+
project: r.project,
|
|
3574
|
+
guard_history: r.guard_history,
|
|
3575
|
+
drift_warnings: r.drift_warnings,
|
|
3576
|
+
hotspots: r.hotspots,
|
|
3577
|
+
trend: r.trend,
|
|
3578
|
+
threads: r.threads,
|
|
3579
|
+
suggested_actions: r.suggested_actions
|
|
3270
3580
|
};
|
|
3581
|
+
return JSON.stringify(out, null, 2) + "\n";
|
|
3271
3582
|
}
|
|
3272
3583
|
|
|
3273
3584
|
// src/workitems.ts
|
package/package.json
CHANGED