fapony 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +473 -0
  3. package/fapony.ts +78 -0
  4. package/package.json +42 -0
  5. package/skill/git-commit-conventional/SKILL.md +68 -0
  6. package/skill/git-ship/SKILL.md +144 -0
  7. package/skill/move-to-done/SKILL.md +126 -0
  8. package/skill/plan-with-pony/SKILL.md +263 -0
  9. package/skill/review-pony/SKILL.md +254 -0
  10. package/src/analyze.ts +517 -0
  11. package/src/context/index.ts +11 -0
  12. package/src/context/projectHealth.ts +359 -0
  13. package/src/conventions-seed.ts +420 -0
  14. package/src/db/defaults.ts +26 -0
  15. package/src/db/getters.ts +33 -0
  16. package/src/db/index.ts +7 -0
  17. package/src/db/load.ts +57 -0
  18. package/src/db/store.ts +286 -0
  19. package/src/db/types.ts +79 -0
  20. package/src/debt.ts +667 -0
  21. package/src/digest/cli.ts +75 -0
  22. package/src/digest/collect.ts +625 -0
  23. package/src/digest/html.ts +208 -0
  24. package/src/digest/text.ts +191 -0
  25. package/src/gate.ts +153 -0
  26. package/src/gates.ts +194 -0
  27. package/src/hook.ts +436 -0
  28. package/src/init-mem.ts +71 -0
  29. package/src/init.ts +237 -0
  30. package/src/install/claude.ts +361 -0
  31. package/src/install/codex.ts +61 -0
  32. package/src/install/cursor.ts +167 -0
  33. package/src/install/detect.ts +78 -0
  34. package/src/install/opencode.ts +234 -0
  35. package/src/install/skills.ts +106 -0
  36. package/src/install/types.ts +69 -0
  37. package/src/install/utils.ts +29 -0
  38. package/src/install/zcode.ts +120 -0
  39. package/src/install.ts +176 -0
  40. package/src/lint-baseline.ts +260 -0
  41. package/src/map.ts +320 -0
  42. package/src/math.ts +13 -0
  43. package/src/mcp/evidence.ts +332 -0
  44. package/src/mcp/primitives.ts +316 -0
  45. package/src/mcp/tools/check.ts +243 -0
  46. package/src/mcp/tools/collect.ts +157 -0
  47. package/src/mcp/tools/context.ts +66 -0
  48. package/src/mcp/tools/index.ts +309 -0
  49. package/src/mcp/tools/mem.ts +95 -0
  50. package/src/mcp/tools/plans.ts +255 -0
  51. package/src/mcp/tools/report.ts +285 -0
  52. package/src/mcp/tools/stats.ts +96 -0
  53. package/src/mcp/tools/usage.ts +211 -0
  54. package/src/mcp/tools/verdict.ts +148 -0
  55. package/src/mcp/transport.ts +241 -0
  56. package/src/mcp/types.ts +54 -0
  57. package/src/mcp/worktree.ts +27 -0
  58. package/src/memory.ts +264 -0
  59. package/src/parse.ts +71 -0
  60. package/src/plan-seed.ts +599 -0
  61. package/src/price/fetch.ts +146 -0
  62. package/src/price/index.ts +8 -0
  63. package/src/price/resolve.ts +213 -0
  64. package/src/report/cli.ts +92 -0
  65. package/src/report/format.ts +37 -0
  66. package/src/report/index.ts +4 -0
  67. package/src/report/render.ts +206 -0
  68. package/src/review-seed.ts +932 -0
  69. package/src/safety.ts +18 -0
  70. package/src/session/activeSession.ts +153 -0
  71. package/src/session/claude-code.ts +412 -0
  72. package/src/session/codex.ts +347 -0
  73. package/src/session/findModel.ts +376 -0
  74. package/src/session/helpers.ts +640 -0
  75. package/src/session/index.ts +31 -0
  76. package/src/session/opencode.ts +167 -0
  77. package/src/session/registry.ts +45 -0
  78. package/src/session/types.ts +128 -0
  79. package/src/session/zcode.ts +151 -0
  80. package/src/setup.ts +242 -0
  81. package/src/stats/cli.ts +44 -0
  82. package/src/stats/data.ts +1019 -0
  83. package/src/stats/format.ts +584 -0
  84. package/src/stats/index.ts +19 -0
  85. package/src/telemetry.ts +364 -0
  86. package/src/test.ts +2 -0
  87. package/src/update.ts +212 -0
  88. package/src/usage/cache.ts +125 -0
  89. package/src/usage/cli.ts +120 -0
  90. package/src/usage/format.ts +29 -0
  91. package/src/usage/index.ts +4 -0
  92. package/src/usage/render.ts +523 -0
  93. package/src/usage/scan.ts +161 -0
  94. package/src/util.ts +32 -0
  95. package/src/web/html.ts +33 -0
  96. package/templates/PLAN.md +90 -0
  97. package/templates/SPEC.md +30 -0
  98. package/templates/mem/commands/plan.ts +360 -0
  99. package/templates/mem/commands/read.ts +194 -0
  100. package/templates/mem/commands/rotate.ts +59 -0
  101. package/templates/mem/commands/selftest.ts +450 -0
  102. package/templates/mem/commands/write.ts +214 -0
  103. package/templates/mem/mem.ts +68 -0
  104. package/templates/mem/render.ts +63 -0
  105. package/templates/mem/selectors.ts +144 -0
  106. package/templates/mem/store.ts +285 -0
@@ -0,0 +1,206 @@
1
+ // src/report/render.ts — HTML report rendering
2
+
3
+ import type { StatsData } from "../stats/data.js";
4
+ import { esc } from "../web/html.js";
5
+ import {
6
+ fmtMinutes,
7
+ fmtRate,
8
+ insufficientData,
9
+ latestRunFreshness,
10
+ MIN_SAMPLE_SIZE,
11
+ } from "./format.js";
12
+
13
+ /** Render StatsData as HTML. Exported for tests. */
14
+ export function renderReportHtml(
15
+ stats: StatsData,
16
+ generated_at: string,
17
+ ownerName?: string,
18
+ ): string {
19
+ const { runs, gates } = statsToRender(stats);
20
+
21
+ const models = [...new Set(stats.byModel.map((m) => m.model))];
22
+ const grades = [...new Set(stats.byGrade.map((g) => g.grade))];
23
+ const worktrees = [...new Set(stats.byWorktree.map((w) => w.worktree))];
24
+
25
+ const owner = ownerName?.trim() ? esc(ownerName.trim()) : "";
26
+ const optionAll = `<option value="">all</option>`;
27
+ const options = (xs: string[]) =>
28
+ optionAll +
29
+ xs.map((x) => `<option value="${esc(x)}">${esc(x)}</option>`).join("");
30
+
31
+ return `<!DOCTYPE html>
32
+ <html lang="en">
33
+ <head>
34
+ <meta charset="utf-8">
35
+ <meta name="viewport" content="width=device-width, initial-scale=1">
36
+ <title>fapony report — ${generated_at.split("T")[0]}</title>
37
+ <style>
38
+ :root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; --green: #3fb950; --red: #f85149; --yellow: #d29922; --muted: #8b949e; }
39
+ * { box-sizing: border-box; margin: 0; padding: 0; }
40
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg); color: var(--fg); line-height: 1.6; padding: 2rem; max-width: 960px; margin: 0 auto; }
41
+ .header { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; }
42
+ .owner { color: var(--muted); font-size: 0.9rem; font-weight: 400; white-space: nowrap; }
43
+ h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
44
+ h2 { font-size: 1.1rem; color: var(--accent); margin: 1.5rem 0 0.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; }
45
+ .meta { color: var(--muted); font-size: 0.85rem; margin-bottom: 1.5rem; }
46
+ .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.5rem; margin-bottom: 1.5rem; }
47
+ .stat { background: #161b22; border: 1px solid var(--border); border-radius: 6px; padding: 1rem; text-align: center; }
48
+ .stat .value { font-size: 1.8rem; font-weight: 700; color: var(--accent); }
49
+ .stat .label { font-size: 0.8rem; color: var(--muted); margin-top: 0.3rem; }
50
+ table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; font-size: 0.9rem; }
51
+ th, td { padding: 0.5rem 0.8rem; text-align: left; border-bottom: 1px solid var(--border); }
52
+ th { color: var(--muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; }
53
+ tr:hover { background: #161b22; }
54
+ .pass { color: var(--green); }
55
+ .fail { color: var(--red); }
56
+ .warn { color: var(--yellow); }
57
+ .insufficient { background: #1c1917; border: 1px solid var(--yellow); border-radius: 6px; padding: 0.8rem 1rem; margin: 0.5rem 0; font-size: 0.9rem; color: var(--yellow); }
58
+ .methodology { background: #161b22; border: 1px solid var(--border); border-radius: 6px; padding: 1.2rem; margin-top: 1.5rem; font-size: 0.85rem; color: var(--muted); }
59
+ .methodology h3 { color: var(--fg); font-size: 0.95rem; margin-bottom: 0.5rem; }
60
+ .methodology ul { padding-left: 1.5rem; }
61
+ .methodology li { margin-bottom: 0.3rem; }
62
+ .sample { font-size: 0.8rem; color: var(--muted); }
63
+ .filters { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-bottom: 1rem; align-items: end; }
64
+ .filters label { font-size: 0.85rem; color: var(--muted); display: flex; flex-direction: column; gap: 0.2rem; }
65
+ .filters select { background: #161b22; color: var(--fg); border: 1px solid var(--border); border-radius: 4px; padding: 0.3rem 0.5rem; font-size: 0.85rem; }
66
+ </style>
67
+ </head>
68
+ <body>
69
+
70
+ <div class="header">
71
+ <h1>fapony verification report</h1>
72
+ ${owner ? `<span class="owner">${owner}</span>` : ""}
73
+ </div>
74
+ <div class="meta">
75
+ Generated: ${generated_at} · Latest data: ${latestRunFreshness(stats.latestRunAt)} · Schema v2 · Scope: ${stats.scope ? esc(stats.scope) : `all projects (${stats.byWorktree.length})`}
76
+ </div>
77
+
78
+ <h2>Summary</h2>
79
+ ${insufficientData(stats.runs.total, "runs")}
80
+ <div class="summary">
81
+ <div class="stat"><div class="value">${stats.runs.total}</div><div class="label">total runs</div></div>
82
+ <div class="stat"><div class="value ${stats.runs.passRate >= 0.8 ? "pass" : stats.runs.passRate >= 0.5 ? "warn" : "fail"}">${fmtRate(stats.runs.passRate)}</div><div class="label">pass rate</div></div>
83
+ <div class="stat"><div class="value ${stats.runs.stallRate <= 0.1 ? "pass" : "fail"}">${fmtRate(stats.runs.stallRate)}</div><div class="label">stall rate</div></div>
84
+ <div class="stat"><div class="value">${stats.runs.avgRounds.toFixed(1)}</div><div class="label">avg rounds</div></div>
85
+ <div class="stat"><div class="value">${fmtMinutes(stats.runs.avgMinutes)}</div><div class="label">avg time</div></div>
86
+ </div>
87
+
88
+ <div class="filters">
89
+ <label>model <select id="f-model">${options(models)}</select></label>
90
+ <label>grade <select id="f-grade">${options(grades)}</select></label>
91
+ <label>worktree <select id="f-worktree">${options(worktrees)}</select></label>
92
+ </div>
93
+
94
+ <h2>By Model <span class="sample">(n=${gates})</span></h2>
95
+ ${insufficientData(gates, "gates")}
96
+ <table id="t-model">
97
+ <thead><tr>${stats.scope ? "" : "<th>Project</th>"}<th>Client</th><th>Provider</th><th>Model</th><th>Agent</th><th>Gates</th><th>Avg Quality</th></tr></thead>
98
+ <tbody>
99
+ ${stats.byModel
100
+ .map(
101
+ (m) => ` <tr data-model="${esc(m.model)}">
102
+ ${stats.scope ? "" : `<td>${esc(m.worktree)}</td>`}
103
+ <td>${esc(m.client)}</td>
104
+ <td>${esc(m.provider)}</td>
105
+ <td>${esc(m.model)}</td>
106
+ <td>${esc(m.agent)}</td>
107
+ <td>${m.gateCount}</td>
108
+ <td>${m.avgQuality.toFixed(1)} <span class="sample">/ 5</span></td>
109
+ </tr>`,
110
+ )
111
+ .join("\n")}
112
+ </tbody>
113
+ </table>
114
+
115
+ <h2>By Grade <span class="sample">(n=${gates})</span></h2>
116
+ <table id="t-grade">
117
+ <thead><tr><th>Grade</th><th>Count</th><th>%</th></tr></thead>
118
+ <tbody>
119
+ ${stats.byGrade
120
+ .map((g) => {
121
+ const pct = gates ? ((g.count / gates) * 100).toFixed(0) : "0";
122
+ const cls = g.grade.startsWith("pass")
123
+ ? "pass"
124
+ : g.grade === "fail"
125
+ ? "fail"
126
+ : "warn";
127
+ return ` <tr data-grade="${esc(g.grade)}">
128
+ <td class="${cls}">${esc(g.grade)}</td>
129
+ <td>${g.count}</td>
130
+ <td>${pct}%</td>
131
+ </tr>`;
132
+ })
133
+ .join("\n")}
134
+ </tbody>
135
+ </table>
136
+
137
+ <h2>By Worktree <span class="sample">(n=${runs})</span></h2>
138
+ <table id="t-worktree">
139
+ <thead><tr><th>Worktree</th><th>Runs</th><th>Passed</th><th>Stalled</th><th>Pass Rate</th><th>Pending plans</th></tr></thead>
140
+ <tbody>
141
+ ${stats.byWorktree
142
+ .map((w) => {
143
+ const rate = w.runs ? w.passed / w.runs : 0;
144
+ return ` <tr data-worktree="${esc(w.worktree)}">
145
+ <td>${esc(w.worktree)}</td>
146
+ <td>${w.runs}</td>
147
+ <td class="pass">${w.passed}</td>
148
+ <td class="fail">${w.stalled}</td>
149
+ <td class="${rate >= 0.8 ? "pass" : rate >= 0.5 ? "warn" : "fail"}">${fmtRate(rate)}</td>
150
+ <td>${w.pending === null ? "—" : w.pending}</td>
151
+ </tr>`;
152
+ })
153
+ .join("\n")}
154
+ </tbody>
155
+ </table>
156
+
157
+ <div class="methodology">
158
+ <h3>Methodology</h3>
159
+ <ul>
160
+ <li><strong>Pass rate:</strong> passed / (passed + stopped + stalled) — terminal runs only, running/awaiting_review excluded.</li>
161
+ <li><strong>Quality score:</strong> pass-excellent=5, pass-good=4, pass-adequate=3, pass=3 (legacy), uncertain=1, fail=0 — the same canonical mapping <code>fapony stats</code> uses.</li>
162
+ <li><strong>Sample size:</strong> data with fewer than ${MIN_SAMPLE_SIZE} samples is flagged as insufficient for comparison.</li>
163
+ <li><strong>Freshness:</strong> based on the most recent run creation timestamp.</li>
164
+ <li><strong>Worktree names:</strong> paths are redacted to basenames; two different paths sharing a basename merge into one row.</li>
165
+ <li><strong>Selection bias:</strong> this data represents your local workflow only — not a representative sample of all agent usage.</li>
166
+ <li><strong>No content:</strong> no source code, diffs, plans, commit messages, or gate notes are included in this report.</li>
167
+ </ul>
168
+ </div>
169
+
170
+ <div class="meta" style="margin-top: 1rem; text-align: center;">
171
+ fapony verification report · generated from local SQLite · opt-in only
172
+ </div>
173
+
174
+ <script>
175
+ (function () {
176
+ function bind(selectId, tableId, attr) {
177
+ var sel = document.getElementById(selectId);
178
+ var rows = document.querySelectorAll("#" + tableId + " tbody tr");
179
+ sel.addEventListener("change", function () {
180
+ var v = sel.value;
181
+ rows.forEach(function (r) {
182
+ r.style.display = !v || r.getAttribute(attr) === v ? "" : "none";
183
+ });
184
+ });
185
+ }
186
+ bind("f-model", "t-model", "data-model");
187
+ bind("f-grade", "t-grade", "data-grade");
188
+ bind("f-worktree", "t-worktree", "data-worktree");
189
+ })();
190
+ </script>
191
+
192
+ </body>
193
+ </html>`;
194
+ }
195
+
196
+ // --- Helpers: extract only what renderReportHtml needs from StatsData ---
197
+
198
+ function statsToRender(stats: StatsData): {
199
+ runs: number;
200
+ gates: number;
201
+ } {
202
+ return {
203
+ runs: stats.runs.total,
204
+ gates: stats.byModel.reduce((s, m) => s + m.gateCount, 0),
205
+ };
206
+ }