@slowcook-ai/cli 0.19.0-alpha.8 → 0.19.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.
Files changed (194) hide show
  1. package/AGENTS.md +240 -0
  2. package/REPORTING.md +193 -0
  3. package/dist/cli.js +78 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/brand/index.d.ts +26 -0
  6. package/dist/commands/brand/index.d.ts.map +1 -0
  7. package/dist/commands/brand/index.js +257 -0
  8. package/dist/commands/brand/index.js.map +1 -0
  9. package/dist/commands/brew/agent.d.ts +7 -2
  10. package/dist/commands/brew/agent.d.ts.map +1 -1
  11. package/dist/commands/brew/agent.js +9 -0
  12. package/dist/commands/brew/agent.js.map +1 -1
  13. package/dist/commands/brew/halt.d.ts +26 -0
  14. package/dist/commands/brew/halt.d.ts.map +1 -1
  15. package/dist/commands/brew/halt.js.map +1 -1
  16. package/dist/commands/brew/index.d.ts.map +1 -1
  17. package/dist/commands/brew/index.js +142 -18
  18. package/dist/commands/brew/index.js.map +1 -1
  19. package/dist/commands/brew/pair-navigator.d.ts +119 -0
  20. package/dist/commands/brew/pair-navigator.d.ts.map +1 -0
  21. package/dist/commands/brew/pair-navigator.js +187 -0
  22. package/dist/commands/brew/pair-navigator.js.map +1 -0
  23. package/dist/commands/budget/index.d.ts +2 -0
  24. package/dist/commands/budget/index.d.ts.map +1 -0
  25. package/dist/commands/budget/index.js +252 -0
  26. package/dist/commands/budget/index.js.map +1 -0
  27. package/dist/commands/chef/drift-fix.d.ts +33 -4
  28. package/dist/commands/chef/drift-fix.d.ts.map +1 -1
  29. package/dist/commands/chef/drift-fix.js +417 -29
  30. package/dist/commands/chef/drift-fix.js.map +1 -1
  31. package/dist/commands/chef/index.js +13 -3
  32. package/dist/commands/chef/index.js.map +1 -1
  33. package/dist/commands/chef/orchestrate.d.ts +1 -1
  34. package/dist/commands/chef/orchestrate.d.ts.map +1 -1
  35. package/dist/commands/chef/orchestrate.js +32 -9
  36. package/dist/commands/chef/orchestrate.js.map +1 -1
  37. package/dist/commands/dev-env/config.d.ts +57 -0
  38. package/dist/commands/dev-env/config.d.ts.map +1 -0
  39. package/dist/commands/dev-env/config.js +96 -0
  40. package/dist/commands/dev-env/config.js.map +1 -0
  41. package/dist/commands/dev-env/index.d.ts +27 -0
  42. package/dist/commands/dev-env/index.d.ts.map +1 -0
  43. package/dist/commands/dev-env/index.js +226 -0
  44. package/dist/commands/dev-env/index.js.map +1 -0
  45. package/dist/commands/dev-env/init.d.ts +28 -0
  46. package/dist/commands/dev-env/init.d.ts.map +1 -0
  47. package/dist/commands/dev-env/init.js +135 -0
  48. package/dist/commands/dev-env/init.js.map +1 -0
  49. package/dist/commands/docs/index.d.ts +16 -0
  50. package/dist/commands/docs/index.d.ts.map +1 -0
  51. package/dist/commands/docs/index.js +127 -0
  52. package/dist/commands/docs/index.js.map +1 -0
  53. package/dist/commands/eval/index.d.ts +54 -0
  54. package/dist/commands/eval/index.d.ts.map +1 -0
  55. package/dist/commands/eval/index.js +294 -0
  56. package/dist/commands/eval/index.js.map +1 -0
  57. package/dist/commands/extract/index.d.ts.map +1 -1
  58. package/dist/commands/extract/index.js +23 -1
  59. package/dist/commands/extract/index.js.map +1 -1
  60. package/dist/commands/garnish/index.d.ts +56 -0
  61. package/dist/commands/garnish/index.d.ts.map +1 -0
  62. package/dist/commands/garnish/index.js +281 -0
  63. package/dist/commands/garnish/index.js.map +1 -0
  64. package/dist/commands/garnish/trailer.d.ts +79 -0
  65. package/dist/commands/garnish/trailer.d.ts.map +1 -0
  66. package/dist/commands/garnish/trailer.js +118 -0
  67. package/dist/commands/garnish/trailer.js.map +1 -0
  68. package/dist/commands/init/index.d.ts.map +1 -1
  69. package/dist/commands/init/index.js +33 -0
  70. package/dist/commands/init/index.js.map +1 -1
  71. package/dist/commands/init/mock-vite.d.ts +54 -0
  72. package/dist/commands/init/mock-vite.d.ts.map +1 -0
  73. package/dist/commands/init/mock-vite.js +613 -0
  74. package/dist/commands/init/mock-vite.js.map +1 -0
  75. package/dist/commands/init/mock.d.ts +6 -0
  76. package/dist/commands/init/mock.d.ts.map +1 -1
  77. package/dist/commands/init/mock.js +20 -4
  78. package/dist/commands/init/mock.js.map +1 -1
  79. package/dist/commands/init/plan.d.ts +26 -1
  80. package/dist/commands/init/plan.d.ts.map +1 -1
  81. package/dist/commands/init/plan.js +41 -3
  82. package/dist/commands/init/plan.js.map +1 -1
  83. package/dist/commands/init/templates.d.ts.map +1 -1
  84. package/dist/commands/init/templates.js +12 -4
  85. package/dist/commands/init/templates.js.map +1 -1
  86. package/dist/commands/knowledge-add.d.ts +52 -0
  87. package/dist/commands/knowledge-add.d.ts.map +1 -0
  88. package/dist/commands/knowledge-add.js +232 -0
  89. package/dist/commands/knowledge-add.js.map +1 -0
  90. package/dist/commands/map/emit-typeorm.d.ts +117 -0
  91. package/dist/commands/map/emit-typeorm.d.ts.map +1 -0
  92. package/dist/commands/map/emit-typeorm.js +341 -0
  93. package/dist/commands/map/emit-typeorm.js.map +1 -0
  94. package/dist/commands/map/index.d.ts +18 -0
  95. package/dist/commands/map/index.d.ts.map +1 -1
  96. package/dist/commands/map/index.js +28 -0
  97. package/dist/commands/map/index.js.map +1 -1
  98. package/dist/commands/plate/agent.d.ts +7 -0
  99. package/dist/commands/plate/agent.d.ts.map +1 -1
  100. package/dist/commands/plate/agent.js +1 -1
  101. package/dist/commands/plate/agent.js.map +1 -1
  102. package/dist/commands/plate/index.d.ts.map +1 -1
  103. package/dist/commands/plate/index.js +6 -0
  104. package/dist/commands/plate/index.js.map +1 -1
  105. package/dist/commands/recon/index.d.ts +16 -3
  106. package/dist/commands/recon/index.d.ts.map +1 -1
  107. package/dist/commands/recon/index.js +267 -16
  108. package/dist/commands/recon/index.js.map +1 -1
  109. package/dist/commands/recon/migration-gate.d.ts +59 -0
  110. package/dist/commands/recon/migration-gate.d.ts.map +1 -0
  111. package/dist/commands/recon/migration-gate.js +131 -0
  112. package/dist/commands/recon/migration-gate.js.map +1 -0
  113. package/dist/commands/recon/reuse.d.ts +32 -0
  114. package/dist/commands/recon/reuse.d.ts.map +1 -1
  115. package/dist/commands/recon/reuse.js +66 -0
  116. package/dist/commands/recon/reuse.js.map +1 -1
  117. package/dist/commands/recon/stale-stubs.d.ts +65 -0
  118. package/dist/commands/recon/stale-stubs.d.ts.map +1 -0
  119. package/dist/commands/recon/stale-stubs.js +84 -0
  120. package/dist/commands/recon/stale-stubs.js.map +1 -0
  121. package/dist/commands/refine/agent.d.ts +23 -0
  122. package/dist/commands/refine/agent.d.ts.map +1 -1
  123. package/dist/commands/refine/agent.js +245 -15
  124. package/dist/commands/refine/agent.js.map +1 -1
  125. package/dist/commands/refine/brownfield-answer.d.ts +81 -0
  126. package/dist/commands/refine/brownfield-answer.d.ts.map +1 -0
  127. package/dist/commands/refine/brownfield-answer.js +231 -0
  128. package/dist/commands/refine/brownfield-answer.js.map +1 -0
  129. package/dist/commands/refine/context.d.ts +27 -1
  130. package/dist/commands/refine/context.d.ts.map +1 -1
  131. package/dist/commands/refine/context.js +438 -6
  132. package/dist/commands/refine/context.js.map +1 -1
  133. package/dist/commands/refine/git-attention.d.ts +123 -0
  134. package/dist/commands/refine/git-attention.d.ts.map +1 -0
  135. package/dist/commands/refine/git-attention.js +378 -0
  136. package/dist/commands/refine/git-attention.js.map +1 -0
  137. package/dist/commands/refine/history-index.d.ts +66 -0
  138. package/dist/commands/refine/history-index.d.ts.map +1 -1
  139. package/dist/commands/refine/history-index.js +195 -8
  140. package/dist/commands/refine/history-index.js.map +1 -1
  141. package/dist/commands/refine/index.d.ts.map +1 -1
  142. package/dist/commands/refine/index.js +105 -18
  143. package/dist/commands/refine/index.js.map +1 -1
  144. package/dist/commands/refine/multifurcate.d.ts +129 -0
  145. package/dist/commands/refine/multifurcate.d.ts.map +1 -0
  146. package/dist/commands/refine/multifurcate.js +247 -0
  147. package/dist/commands/refine/multifurcate.js.map +1 -0
  148. package/dist/commands/refine/proposals-synth.d.ts +50 -1
  149. package/dist/commands/refine/proposals-synth.d.ts.map +1 -1
  150. package/dist/commands/refine/proposals-synth.js +199 -35
  151. package/dist/commands/refine/proposals-synth.js.map +1 -1
  152. package/dist/commands/refine/spec-yaml.d.ts +214 -1210
  153. package/dist/commands/refine/spec-yaml.d.ts.map +1 -1
  154. package/dist/commands/refine/spec-yaml.js +10 -0
  155. package/dist/commands/refine/spec-yaml.js.map +1 -1
  156. package/dist/commands/refresh-knowledge.d.ts +139 -0
  157. package/dist/commands/refresh-knowledge.d.ts.map +1 -0
  158. package/dist/commands/refresh-knowledge.js +1029 -0
  159. package/dist/commands/refresh-knowledge.js.map +1 -0
  160. package/dist/commands/run-mock/index.d.ts.map +1 -1
  161. package/dist/commands/run-mock/index.js +135 -22
  162. package/dist/commands/run-mock/index.js.map +1 -1
  163. package/dist/commands/testgen/agent.d.ts +13 -0
  164. package/dist/commands/testgen/agent.d.ts.map +1 -1
  165. package/dist/commands/testgen/agent.js +137 -11
  166. package/dist/commands/testgen/agent.js.map +1 -1
  167. package/dist/commands/upsert-agent-docs.d.ts +48 -0
  168. package/dist/commands/upsert-agent-docs.d.ts.map +1 -0
  169. package/dist/commands/upsert-agent-docs.js +298 -0
  170. package/dist/commands/upsert-agent-docs.js.map +1 -0
  171. package/dist/commands/vibe/agent.d.ts +7 -0
  172. package/dist/commands/vibe/agent.d.ts.map +1 -1
  173. package/dist/commands/vibe/agent.js +2 -2
  174. package/dist/commands/vibe/agent.js.map +1 -1
  175. package/dist/commands/vibe/index.d.ts.map +1 -1
  176. package/dist/commands/vibe/index.js +7 -1
  177. package/dist/commands/vibe/index.js.map +1 -1
  178. package/dist/cost-store.d.ts +52 -0
  179. package/dist/cost-store.d.ts.map +1 -0
  180. package/dist/cost-store.js +108 -0
  181. package/dist/cost-store.js.map +1 -0
  182. package/dist/lib/budget.d.ts +73 -0
  183. package/dist/lib/budget.d.ts.map +1 -0
  184. package/dist/lib/budget.js +225 -0
  185. package/dist/lib/budget.js.map +1 -0
  186. package/dist/lib/mock-shape.d.ts +29 -0
  187. package/dist/lib/mock-shape.d.ts.map +1 -0
  188. package/dist/lib/mock-shape.js +77 -0
  189. package/dist/lib/mock-shape.js.map +1 -0
  190. package/dist/lib/read-only.d.ts +22 -0
  191. package/dist/lib/read-only.d.ts.map +1 -0
  192. package/dist/lib/read-only.js +34 -0
  193. package/dist/lib/read-only.js.map +1 -0
  194. package/package.json +17 -12
@@ -0,0 +1,1029 @@
1
+ /**
2
+ * `slowcook refresh-knowledge` — α.62
3
+ *
4
+ * Rebuilds `.brewing/repo-knowledge/auto/*.md` digests so refine (and any
5
+ * other agent) reads the consumer repo's actual shape instead of
6
+ * re-deriving it via LLM every run.
7
+ *
8
+ * Caching policy (three classes):
9
+ * 1. Cheap deterministic extractions (all of α.62: pure regex/AST,
10
+ * <2s per file). Just rebuild every time. No hash machinery.
11
+ *
12
+ * 2. Expensive deterministic extractions (e.g., git-history mining
13
+ * in α.63 — walking 500 commits to extract conventions). Stamp
14
+ * output with `<!-- last-built: ISO; input-tip-sha: <sha> -->`
15
+ * and resume from the stamped SHA on the next run (delta mining).
16
+ *
17
+ * 3. Expensive INSIGHTS (LLM-derived chef known-fixes, lesson
18
+ * extractions, PR summaries). These do NOT auto-invalidate on
19
+ * commit change — the insight is about a CLASS of problem, not
20
+ * a snapshot of code. An insight like "vitest/config not found
21
+ * means deps missing" stays true even if vitest.config.ts moves.
22
+ * Staleness is a SOFT signal:
23
+ * - Each insight carries `evidence-pr: N` + `last-verified: ISO`
24
+ * - `slowcook knowledge verify` may flag [PRECARIOUS] when the
25
+ * evidence file is substantially rewritten, but does NOT
26
+ * delete. Agents reading insights see staleness as weight,
27
+ * not as a gate.
28
+ * α.62 has no insight extractions yet; this comment locks the
29
+ * design for α.63+ to follow.
30
+ *
31
+ * Output layout (gitignored by convention):
32
+ * .brewing/repo-knowledge/auto/
33
+ * ├── backend-entities.md (TypeORM @Entity classes + columns)
34
+ * ├── backend-routes.md (HTTP controllers + handler names)
35
+ * ├── backend-enums.md (packages/enums/src/*.enum.ts values)
36
+ * ├── frontend-types.md (mock/src/types/*.ts interfaces)
37
+ * ├── frontend-components.md (mock/src/{app,components}/**.tsx)
38
+ * ├── frontend-contexts.md (mock/src/contexts/*-context.tsx hooks)
39
+ * ├── tokens.md (Tailwind brand-token vocabulary)
40
+ * ├── config.md (tsconfig paths + workspace + scripts)
41
+ * ├── migrations.md (migration file timestamps + table names)
42
+ * └── routes-inventory.md (filesystem-derived route URLs)
43
+ *
44
+ * Refine (`refine/context.ts`) reads these from disk and concatenates
45
+ * them. If the dir doesn't exist (first run), refine falls back to the
46
+ * legacy in-memory scan (α.61 readNestJsBackendDigest).
47
+ */
48
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
49
+ import { dirname, join } from "node:path";
50
+ import { execSync } from "node:child_process";
51
+ const AUTO_DIR_REL = ".brewing/repo-knowledge/auto";
52
+ const CURATED_DIR_REL = ".brewing/repo-knowledge/curated";
53
+ // --- input discovery helpers ---
54
+ function findFilesByGlob(repoRoot, pattern, opts = {}) {
55
+ const out = [];
56
+ const skipDirs = new Set(["node_modules", ".git", ".next", "dist", "build", ".turbo", "coverage", ".brewing"]);
57
+ const maxDepth = opts.maxDepth ?? 8;
58
+ const walk = (dir, depth) => {
59
+ if (depth > maxDepth)
60
+ return;
61
+ let entries = [];
62
+ try {
63
+ entries = readdirSync(dir);
64
+ }
65
+ catch {
66
+ return;
67
+ }
68
+ for (const name of entries) {
69
+ if (skipDirs.has(name))
70
+ continue;
71
+ const abs = join(dir, name);
72
+ let st;
73
+ try {
74
+ st = statSync(abs);
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ if (st.isDirectory())
80
+ walk(abs, depth + 1);
81
+ else {
82
+ const rel = abs.slice(repoRoot.length + 1);
83
+ if (pattern.test(rel))
84
+ out.push(rel);
85
+ }
86
+ }
87
+ };
88
+ walk(repoRoot, 0);
89
+ return out.sort();
90
+ }
91
+ function safeRead(repoRoot, rel) {
92
+ try {
93
+ return readFileSync(join(repoRoot, rel), "utf8");
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ // --- digest writer (cheap extractions = always rebuild) ---
100
+ function writeDigest(repoRoot, name, body) {
101
+ const abs = join(repoRoot, AUTO_DIR_REL, `${name}.md`);
102
+ mkdirSync(dirname(abs), { recursive: true });
103
+ writeFileSync(abs, `<!-- regenerated: ${new Date().toISOString()} -->\n${body}\n`, "utf8");
104
+ }
105
+ /**
106
+ * Build one digest. Cheap extractions class — always rebuilds.
107
+ */
108
+ export function buildDigest(args) {
109
+ const { repoRoot, name, inputFiles, build } = args;
110
+ if (inputFiles.length === 0)
111
+ return { body: "", built: false };
112
+ const body = build(inputFiles);
113
+ writeDigest(repoRoot, name, body);
114
+ return { body, built: true };
115
+ }
116
+ // --- extraction primitives (shared) ---
117
+ function extractTypeOrmColumns(body) {
118
+ const lines = body.split("\n");
119
+ const out = [];
120
+ const fieldRe = /^\s*(?:public|private|readonly|protected)\s+(\w+)(\?)?\s*:\s*([^;]+);/;
121
+ for (const l of lines) {
122
+ const m = l.match(fieldRe);
123
+ if (!m)
124
+ continue;
125
+ const name = m[1];
126
+ const optional = m[2] ? "?" : "";
127
+ const type = (m[3] ?? "").trim().replace(/\s+/g, " ");
128
+ out.push(`${name}${optional}: ${type}`);
129
+ }
130
+ return [...new Set(out)];
131
+ }
132
+ function extractNestRoutes(body) {
133
+ const lines = body.split("\n");
134
+ const out = [];
135
+ const httpVerbRe = /^\s*@(Get|Post|Put|Delete|Patch)\(([^)]*)\)/;
136
+ const nextHttpVerbRe = /^\s*@(Get|Post|Put|Delete|Patch)\(/;
137
+ const handlerRe = /^\s*(?:public|private|protected)?\s*(?:async\s+)?(\w+)\s*\(/;
138
+ for (let i = 0; i < lines.length; i++) {
139
+ const m = (lines[i] ?? "").match(httpVerbRe);
140
+ if (!m)
141
+ continue;
142
+ const method = m[1].toUpperCase();
143
+ const rawPath = (m[2] ?? "").trim().replace(/^['"`]|['"`]$/g, "");
144
+ let parenDepth = 0;
145
+ let handler = "?";
146
+ for (let j = i + 1; j < Math.min(i + 60, lines.length); j++) {
147
+ const ln = lines[j] ?? "";
148
+ if (parenDepth === 0 && nextHttpVerbRe.test(ln))
149
+ break;
150
+ for (const ch of ln) {
151
+ if (ch === "(")
152
+ parenDepth++;
153
+ else if (ch === ")")
154
+ parenDepth = Math.max(0, parenDepth - 1);
155
+ }
156
+ if (/^\s*@\w+/.test(ln))
157
+ continue;
158
+ if (/^\s*$/.test(ln))
159
+ continue;
160
+ const hm = ln.match(handlerRe);
161
+ if (hm) {
162
+ const word = hm[1];
163
+ if (word === "if" || word === "switch" || word === "while" || word === "for" || /^[A-Z]/.test(word))
164
+ continue;
165
+ handler = word;
166
+ break;
167
+ }
168
+ }
169
+ out.push({ method, path: rawPath, handler });
170
+ }
171
+ return out;
172
+ }
173
+ function joinPath(base, sub) {
174
+ const b = base.replace(/^\/|\/$/g, "");
175
+ const s = sub.replace(/^\/|\/$/g, "");
176
+ if (!b)
177
+ return s;
178
+ if (!s)
179
+ return b;
180
+ return `${b}/${s}`;
181
+ }
182
+ function extractTsInterfaces(body) {
183
+ const out = [];
184
+ const startRe = /export\s+interface\s+(\w+)(?:\s+extends\s+[\w\s,&<>]+?)?\s*\{/g;
185
+ let m;
186
+ while ((m = startRe.exec(body)) !== null) {
187
+ const name = m[1];
188
+ let depth = 1;
189
+ let i = m.index + m[0].length;
190
+ while (i < body.length && depth > 0) {
191
+ const ch = body[i];
192
+ if (ch === "{")
193
+ depth++;
194
+ else if (ch === "}")
195
+ depth--;
196
+ i++;
197
+ }
198
+ if (depth === 0) {
199
+ const ifaceBody = body.slice(m.index + m[0].length, i - 1).trim();
200
+ out.push({ name, body: ifaceBody });
201
+ }
202
+ }
203
+ return out;
204
+ }
205
+ function extractComponentExports(body) {
206
+ const out = [];
207
+ const compRe = /export\s+(?:default\s+)?function\s+(\w+)\s*\(/g;
208
+ const constRe = /export\s+(?:default\s+)?const\s+(\w+)\s*[:=]/g;
209
+ const names = new Set();
210
+ let m;
211
+ while ((m = compRe.exec(body)) !== null)
212
+ names.add(m[1]);
213
+ while ((m = constRe.exec(body)) !== null)
214
+ names.add(m[1]);
215
+ for (const name of names) {
216
+ if (!/^[A-Z]/.test(name))
217
+ continue;
218
+ const propsName = body.includes(`interface ${name}Props`) ? `${name}Props` : null;
219
+ out.push({ name, propsName });
220
+ }
221
+ return out;
222
+ }
223
+ function extractHookSignatures(body) {
224
+ const out = [];
225
+ const fnRe = /export\s+function\s+(use\w+)\s*\(([^)]*)\)(?::\s*([^{]+))?/g;
226
+ let m;
227
+ while ((m = fnRe.exec(body)) !== null) {
228
+ const name = m[1];
229
+ const params = (m[2] ?? "").trim();
230
+ const ret = (m[3] ?? "").trim().replace(/=>.*$/, "").trim() || "?";
231
+ out.push(`${name}(${params}): ${ret}`);
232
+ }
233
+ return out;
234
+ }
235
+ function extractTailwindTokens(body) {
236
+ const out = new Set();
237
+ const classRe = /className\s*=\s*(?:["'`]([^"'`]+)["'`]|\{["'`]([^"'`]+)["'`]\})/g;
238
+ let m;
239
+ while ((m = classRe.exec(body)) !== null) {
240
+ const classes = (m[1] ?? m[2] ?? "").split(/\s+/);
241
+ for (const c of classes) {
242
+ if (/^(bg|text|border|ring|fill|stroke)-(brand|primary|secondary|accent|surface)-/.test(c))
243
+ out.add(c);
244
+ if (/^bg-(brand|primary|secondary)-?\w*$/.test(c))
245
+ out.add(c);
246
+ }
247
+ }
248
+ return out;
249
+ }
250
+ // --- digest builders ---
251
+ export function buildBackendEntitiesDigest(repoRoot) {
252
+ const files = findFilesByGlob(repoRoot, /\/entities\/[^/]+\.entity\.ts$/);
253
+ return buildDigest({
254
+ repoRoot, name: "backend-entities", inputFiles: files,
255
+ build: (inputs) => {
256
+ const lines = [];
257
+ lines.push("# Backend entities (TypeORM)\n");
258
+ lines.push("Auto-extracted from `packages/**/entities/*.entity.ts`. Spec / code MUST reference these field names verbatim — do not invent aliases.\n");
259
+ for (const rel of inputs) {
260
+ const body = safeRead(repoRoot, rel);
261
+ if (!body)
262
+ continue;
263
+ const classMatch = body.match(/export class (\w+) extends BaseEntity/) ?? body.match(/export class (\w+)/);
264
+ if (!classMatch)
265
+ continue;
266
+ const className = classMatch[1];
267
+ const cols = extractTypeOrmColumns(body).slice(0, 30);
268
+ lines.push(`## ${className} \`${rel}\``);
269
+ for (const col of cols)
270
+ lines.push(`- ${col}`);
271
+ lines.push("");
272
+ }
273
+ return lines.join("\n");
274
+ },
275
+ });
276
+ }
277
+ export function buildBackendRoutesDigest(repoRoot) {
278
+ const files = findFilesByGlob(repoRoot, /\/(modules|controllers)\/[^/]+\/[^/]+\.controller\.ts$/);
279
+ return buildDigest({
280
+ repoRoot, name: "backend-routes", inputFiles: files,
281
+ build: (inputs) => {
282
+ const lines = [];
283
+ lines.push("# Backend HTTP routes (NestJS controllers)\n");
284
+ lines.push("Auto-extracted from `apps/**/modules/**/*.controller.ts`. Spec / code MUST reference these paths + handler names verbatim — do not invent `/api/v1/...`-style paths if they aren't here.\n");
285
+ for (const rel of inputs) {
286
+ const body = safeRead(repoRoot, rel);
287
+ if (!body)
288
+ continue;
289
+ const controllerMatch = body.match(/@Controller\(['"]([^'"]*)['"]\)/);
290
+ const base = controllerMatch ? controllerMatch[1] : "";
291
+ const routes = extractNestRoutes(body);
292
+ if (routes.length === 0)
293
+ continue;
294
+ lines.push(`## \`${rel}\` (base: \`/${base}\`)`);
295
+ for (const r of routes.slice(0, 50))
296
+ lines.push(`- \`${r.method} /${joinPath(base, r.path)}\` → \`${r.handler}\``);
297
+ if (routes.length > 50)
298
+ lines.push(`- … ${routes.length - 50} more`);
299
+ lines.push("");
300
+ }
301
+ return lines.join("\n");
302
+ },
303
+ });
304
+ }
305
+ /**
306
+ * Parse the values out of an `export enum { … }` body.
307
+ *
308
+ * Strips JSDoc block comments + line comments first, then splits on
309
+ * commas. Without the strip, enums whose every value is preceded by
310
+ * a JSDoc block (a common convention in the consumer's
311
+ * `packages/enums/src/*.enum.ts`) would yield zero parsed values —
312
+ * the JSDoc text leaks into the identifier slot, fails the
313
+ * uppercase-only filter, and the whole enum drops from the digest.
314
+ *
315
+ * Exported for testing.
316
+ */
317
+ export function parseEnumValues(enumBody) {
318
+ return enumBody
319
+ // Strip JSDoc / block comments — `/* … */` (incl. multi-line).
320
+ .replace(/\/\*[\s\S]*?\*\//g, "")
321
+ // Strip line comments — `// …` to end of line.
322
+ .replace(/\/\/.*$/gm, "")
323
+ .split(",")
324
+ .map((v) => v.trim().split("=")[0].trim().replace(/['"\s]/g, ""))
325
+ .filter((v) => v && /^[A-Z_]+$/.test(v));
326
+ }
327
+ export function buildBackendEnumsDigest(repoRoot) {
328
+ const enumsDir = join(repoRoot, "packages/enums/src");
329
+ const files = existsSync(enumsDir)
330
+ ? readdirSync(enumsDir).filter((f) => f.endsWith(".enum.ts")).map((f) => `packages/enums/src/${f}`)
331
+ : [];
332
+ return buildDigest({
333
+ repoRoot, name: "backend-enums", inputFiles: files,
334
+ build: (inputs) => {
335
+ const lines = [];
336
+ lines.push("# Backend enums (`packages/enums/src/`)\n");
337
+ lines.push("Authoritative enum values. Spec must not invent values not listed here.\n");
338
+ for (const rel of inputs) {
339
+ const body = safeRead(repoRoot, rel);
340
+ if (!body)
341
+ continue;
342
+ const enumMatch = body.match(/export enum (\w+) \{([\s\S]*?)\}/);
343
+ if (!enumMatch)
344
+ continue;
345
+ const enumName = enumMatch[1];
346
+ const values = parseEnumValues(enumMatch[2] ?? "");
347
+ if (values.length === 0)
348
+ continue;
349
+ lines.push(`## ${enumName}`);
350
+ lines.push(values.map((v) => `- ${v}`).join("\n"));
351
+ lines.push("");
352
+ }
353
+ return lines.join("\n");
354
+ },
355
+ });
356
+ }
357
+ export function buildFrontendTypesDigest(repoRoot) {
358
+ const typesDir = join(repoRoot, "mock/src/types");
359
+ const files = [];
360
+ if (existsSync(typesDir)) {
361
+ for (const f of readdirSync(typesDir)) {
362
+ if (f.endsWith(".ts"))
363
+ files.push(`mock/src/types/${f}`);
364
+ }
365
+ }
366
+ return buildDigest({
367
+ repoRoot, name: "frontend-types", inputFiles: files,
368
+ build: (inputs) => {
369
+ const lines = [];
370
+ lines.push("# Frontend mock types (`mock/src/types/`)\n");
371
+ lines.push("Canonical TypeScript interfaces for the mock UI. Specs targeting mock UI MUST use these field names; adapters between mock + backend should map field-by-field instead of inventing intermediate shapes.\n");
372
+ for (const rel of inputs) {
373
+ const body = safeRead(repoRoot, rel);
374
+ if (!body)
375
+ continue;
376
+ const ifaces = extractTsInterfaces(body);
377
+ if (ifaces.length === 0)
378
+ continue;
379
+ lines.push(`## ${rel}`);
380
+ for (const iface of ifaces) {
381
+ lines.push(`### ${iface.name}`);
382
+ lines.push("```ts");
383
+ lines.push(iface.body.slice(0, 800));
384
+ lines.push("```");
385
+ }
386
+ lines.push("");
387
+ }
388
+ return lines.join("\n");
389
+ },
390
+ });
391
+ }
392
+ export function buildFrontendComponentsDigest(repoRoot) {
393
+ const files = findFilesByGlob(repoRoot, /^mock\/src\/(app|components)\/.+\.tsx$/);
394
+ return buildDigest({
395
+ repoRoot, name: "frontend-components", inputFiles: files,
396
+ build: (inputs) => {
397
+ const lines = [];
398
+ lines.push("# Frontend mock components (`mock/src/{app,components}/`)\n");
399
+ lines.push("Auto-extracted component exports + their Props interface name (when present). Use these names + prop shapes verbatim when referencing mock components from specs.\n");
400
+ const byDir = {};
401
+ for (const rel of inputs) {
402
+ const body = safeRead(repoRoot, rel);
403
+ if (!body)
404
+ continue;
405
+ const comps = extractComponentExports(body);
406
+ for (const c of comps) {
407
+ const dir = rel.split("/").slice(0, 3).join("/");
408
+ if (!byDir[dir])
409
+ byDir[dir] = [];
410
+ byDir[dir].push({ ...c, rel });
411
+ }
412
+ }
413
+ for (const dir of Object.keys(byDir).sort()) {
414
+ lines.push(`## ${dir}/`);
415
+ for (const c of byDir[dir].slice(0, 80)) {
416
+ const props = c.propsName ? ` <${c.propsName}>` : "";
417
+ lines.push(`- \`${c.name}\`${props} — \`${c.rel}\``);
418
+ }
419
+ if (byDir[dir].length > 80)
420
+ lines.push(`- … ${byDir[dir].length - 80} more`);
421
+ lines.push("");
422
+ }
423
+ return lines.join("\n");
424
+ },
425
+ });
426
+ }
427
+ export function buildFrontendContextsDigest(repoRoot) {
428
+ const files = findFilesByGlob(repoRoot, /^mock\/src\/contexts\/[^/]+\.tsx?$/);
429
+ return buildDigest({
430
+ repoRoot, name: "frontend-contexts", inputFiles: files,
431
+ build: (inputs) => {
432
+ const lines = [];
433
+ lines.push("# Frontend mock contexts (`mock/src/contexts/`)\n");
434
+ lines.push("Hook signatures exported from mock data-context providers. Use these to consume mock data + mutators in specs.\n");
435
+ for (const rel of inputs) {
436
+ const body = safeRead(repoRoot, rel);
437
+ if (!body)
438
+ continue;
439
+ const sigs = extractHookSignatures(body);
440
+ if (sigs.length === 0)
441
+ continue;
442
+ lines.push(`## \`${rel}\``);
443
+ for (const s of sigs)
444
+ lines.push(`- \`${s}\``);
445
+ lines.push("");
446
+ }
447
+ return lines.join("\n");
448
+ },
449
+ });
450
+ }
451
+ export function buildTailwindTokensDigest(repoRoot) {
452
+ const files = findFilesByGlob(repoRoot, /^(mock|apps)\/.+\.(tsx|jsx)$/, { maxDepth: 10 });
453
+ return buildDigest({
454
+ repoRoot, name: "tokens", inputFiles: files,
455
+ build: (inputs) => {
456
+ const counts = {};
457
+ for (const rel of inputs) {
458
+ const body = safeRead(repoRoot, rel);
459
+ if (!body)
460
+ continue;
461
+ const toks = extractTailwindTokens(body);
462
+ for (const t of toks)
463
+ counts[t] = (counts[t] ?? 0) + 1;
464
+ }
465
+ const ranked = Object.entries(counts).sort((a, b) => b[1] - a[1]);
466
+ const lines = [];
467
+ lines.push("# Tailwind brand-token vocabulary\n");
468
+ lines.push("Auto-extracted from `mock/**/*.tsx` and `apps/**/*.tsx`. Specs / components SHOULD prefer these tokens over inventing new ones — the project's design system lives in these names.\n");
469
+ if (ranked.length === 0) {
470
+ lines.push("_(No brand-* / primary-* / secondary-* tokens detected.)_");
471
+ }
472
+ else {
473
+ lines.push("| Token | Usages |");
474
+ lines.push("|---|---|");
475
+ for (const [tok, n] of ranked.slice(0, 60))
476
+ lines.push(`| \`${tok}\` | ${n} |`);
477
+ if (ranked.length > 60)
478
+ lines.push(`| … ${ranked.length - 60} more | |`);
479
+ }
480
+ return lines.join("\n");
481
+ },
482
+ });
483
+ }
484
+ export function buildConfigDigest(repoRoot) {
485
+ const interesting = [
486
+ "tsconfig.json",
487
+ "tsconfig.base.json",
488
+ "tailwind.config.ts",
489
+ "tailwind.config.js",
490
+ "next.config.ts",
491
+ "next.config.js",
492
+ "next.config.mjs",
493
+ "pnpm-workspace.yaml",
494
+ "package.json",
495
+ ];
496
+ const files = [];
497
+ for (const p of interesting)
498
+ if (existsSync(join(repoRoot, p)))
499
+ files.push(p);
500
+ for (const p of ["mock/package.json", "apps/back/package.json", "apps/patient/package.json", "apps/therapist/package.json"]) {
501
+ if (existsSync(join(repoRoot, p)))
502
+ files.push(p);
503
+ }
504
+ return buildDigest({
505
+ repoRoot, name: "config", inputFiles: files,
506
+ build: (inputs) => {
507
+ const lines = [];
508
+ lines.push("# Build / config conventions\n");
509
+ lines.push("Auto-extracted from the consumer's tsconfig / tailwind / next / package.json files. Agents should respect path aliases + scripts listed here.\n");
510
+ const tscRoot = safeRead(repoRoot, "tsconfig.json");
511
+ if (tscRoot) {
512
+ try {
513
+ const parsed = JSON.parse(tscRoot.replace(/\/\*[^*]*\*\/|\/\/.*$/gm, ""));
514
+ const paths = parsed.compilerOptions?.paths;
515
+ if (paths) {
516
+ lines.push("## TypeScript path aliases (`tsconfig.json`)");
517
+ for (const [alias, targets] of Object.entries(paths)) {
518
+ lines.push(`- \`${alias}\` → \`${targets.join(" | ")}\``);
519
+ }
520
+ lines.push("");
521
+ }
522
+ }
523
+ catch { /* ignore parse errors */ }
524
+ }
525
+ const pkgPaths = inputs.filter((p) => p.endsWith("package.json"));
526
+ if (pkgPaths.length > 0) {
527
+ lines.push("## Workspace packages + run scripts");
528
+ for (const rel of pkgPaths) {
529
+ const body = safeRead(repoRoot, rel);
530
+ if (!body)
531
+ continue;
532
+ try {
533
+ const pkg = JSON.parse(body);
534
+ if (!pkg.name)
535
+ continue;
536
+ lines.push(`### \`${pkg.name}\` (\`${rel}\`)`);
537
+ const scripts = pkg.scripts ?? {};
538
+ for (const [s, cmd] of Object.entries(scripts).slice(0, 12)) {
539
+ lines.push(`- \`${s}\`: \`${cmd}\``);
540
+ }
541
+ lines.push("");
542
+ }
543
+ catch { /* ignore */ }
544
+ }
545
+ }
546
+ return lines.join("\n");
547
+ },
548
+ });
549
+ }
550
+ export function buildMigrationsDigest(repoRoot) {
551
+ const files = findFilesByGlob(repoRoot, /\/migrations\/\d+[^/]*\.ts$/);
552
+ return buildDigest({
553
+ repoRoot, name: "migrations", inputFiles: files,
554
+ build: (inputs) => {
555
+ const lines = [];
556
+ lines.push("# Database migrations index\n");
557
+ lines.push("All migrations in chronological order with extracted table names. Specs that need NEW tables MUST emit a `database_migrations:` section following the patterns established below.\n");
558
+ for (const rel of inputs) {
559
+ const body = safeRead(repoRoot, rel);
560
+ if (!body)
561
+ continue;
562
+ const fname = rel.split("/").pop();
563
+ const tables = new Set();
564
+ // Variants we've observed across consumers:
565
+ // - TypeORM raw: createTable("foo", ...)
566
+ // - SQL string: CREATE TABLE foo (
567
+ // - delgoosh custom helper: DatabaseCreateTable(qr, "foo", ...)
568
+ // - new Table({name: "foo"})
569
+ const patterns = [
570
+ /createTable\s*\(\s*['"`](\w+)['"`]/g,
571
+ /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)/gi,
572
+ /DatabaseCreateTable\s*\(\s*\w+\s*,\s*['"`](\w+)['"`]/g,
573
+ /new\s+Table\s*\(\s*\{[^}]*name\s*:\s*['"`](\w+)['"`]/g,
574
+ ];
575
+ for (const re of patterns) {
576
+ let m;
577
+ while ((m = re.exec(body)) !== null)
578
+ tables.add(m[1]);
579
+ }
580
+ lines.push(`- \`${fname}\`${tables.size ? ` — tables: ${[...tables].join(", ")}` : ""}`);
581
+ }
582
+ return lines.join("\n");
583
+ },
584
+ });
585
+ }
586
+ export function buildRoutesInventoryDigest(repoRoot) {
587
+ const files = findFilesByGlob(repoRoot, /^(mock|apps\/[^/]+)\/src\/app\/.+\/page\.tsx$/);
588
+ return buildDigest({
589
+ repoRoot, name: "routes-inventory", inputFiles: files,
590
+ build: (inputs) => {
591
+ const lines = [];
592
+ lines.push("# Filesystem route inventory (Next.js App Router)\n");
593
+ lines.push("Auto-derived URL paths from `page.tsx` file locations. Specs should reference URLs from this list rather than inventing new ones.\n");
594
+ const byApp = {};
595
+ for (const rel of inputs) {
596
+ const m = rel.match(/^((?:mock|apps\/[^/]+))\/src\/app\/(.+)\/page\.tsx$/);
597
+ if (!m)
598
+ continue;
599
+ const app = m[1];
600
+ const route = "/" + m[2]
601
+ .replace(/\([^)]+\)\//g, "")
602
+ .replace(/\[([^\]]+)\]/g, ":$1");
603
+ if (!byApp[app])
604
+ byApp[app] = [];
605
+ byApp[app].push(route);
606
+ }
607
+ for (const app of Object.keys(byApp).sort()) {
608
+ lines.push(`## \`${app}/\``);
609
+ for (const r of byApp[app].sort())
610
+ lines.push(`- \`${r}\``);
611
+ lines.push("");
612
+ }
613
+ return lines.join("\n");
614
+ },
615
+ });
616
+ }
617
+ export function refreshKnowledgeAuto(repoRoot, opts = {}) {
618
+ const builders = [
619
+ { name: "backend-entities", fn: () => buildBackendEntitiesDigest(repoRoot) },
620
+ { name: "backend-routes", fn: () => buildBackendRoutesDigest(repoRoot) },
621
+ { name: "backend-enums", fn: () => buildBackendEnumsDigest(repoRoot) },
622
+ { name: "frontend-types", fn: () => buildFrontendTypesDigest(repoRoot) },
623
+ { name: "frontend-components", fn: () => buildFrontendComponentsDigest(repoRoot) },
624
+ { name: "frontend-contexts", fn: () => buildFrontendContextsDigest(repoRoot) },
625
+ { name: "tokens", fn: () => buildTailwindTokensDigest(repoRoot) },
626
+ { name: "config", fn: () => buildConfigDigest(repoRoot) },
627
+ { name: "migrations", fn: () => buildMigrationsDigest(repoRoot) },
628
+ { name: "routes-inventory", fn: () => buildRoutesInventoryDigest(repoRoot) },
629
+ ];
630
+ const built = [];
631
+ const skippedEmpty = [];
632
+ for (const b of builders) {
633
+ if (opts.only && b.name !== opts.only)
634
+ continue;
635
+ const r = b.fn();
636
+ if (r.built)
637
+ built.push(b.name);
638
+ else
639
+ skippedEmpty.push(b.name);
640
+ }
641
+ return { built, skippedEmpty, outDir: join(repoRoot, AUTO_DIR_REL) };
642
+ }
643
+ /**
644
+ * CLI entry point. Called from cli.ts.
645
+ *
646
+ * Default behavior (no mode flag) = run both --auto and --mine-history,
647
+ * since they target different output dirs and have no overlap. Either
648
+ * mode can be requested individually with the explicit flag.
649
+ */
650
+ export async function refreshKnowledge(argv) {
651
+ const args = parseArgs(argv);
652
+ if (args.help) {
653
+ printHelp();
654
+ return;
655
+ }
656
+ const runAuto = args.mode === "auto" || args.mode === "all";
657
+ const runHistory = args.mode === "mine-history" || args.mode === "all";
658
+ console.log(`slowcook refresh-knowledge · ${args.repoRoot}`);
659
+ if (runAuto) {
660
+ const result = refreshKnowledgeAuto(args.repoRoot, { only: args.only });
661
+ console.log(` [auto] output: ${result.outDir}`);
662
+ console.log(` [auto] built: ${result.built.length > 0 ? result.built.join(", ") : "(nothing built)"}`);
663
+ if (result.skippedEmpty.length > 0) {
664
+ console.log(` [auto] skipped (no inputs): ${result.skippedEmpty.join(", ")}`);
665
+ }
666
+ }
667
+ if (runHistory) {
668
+ const result = refreshKnowledgeMineHistory(args.repoRoot, { full: args.fullHistory });
669
+ console.log(` [history] output: ${result.outDir}`);
670
+ console.log(` [history] built: ${result.built.length > 0 ? result.built.join(", ") : "(nothing built)"}`);
671
+ console.log(` [history] commits processed: ${result.commitsProcessed}${result.deltaFromSha ? ` (delta-aware: stamp had ${result.deltaFromSha.slice(0, 8)})` : ""}`);
672
+ }
673
+ }
674
+ function parseArgs(argv) {
675
+ let repoRoot = process.cwd();
676
+ let mode = "all";
677
+ let only;
678
+ let fullHistory = false;
679
+ let help = false;
680
+ let modeExplicit = false;
681
+ for (let i = 0; i < argv.length; i++) {
682
+ const a = argv[i];
683
+ const next = argv[i + 1];
684
+ if (a === "--cwd" && next) {
685
+ repoRoot = next;
686
+ i++;
687
+ }
688
+ else if (a === "--only" && next) {
689
+ only = next;
690
+ i++;
691
+ mode = "auto";
692
+ modeExplicit = true;
693
+ }
694
+ else if (a === "--auto") {
695
+ mode = modeExplicit ? mode : "auto";
696
+ modeExplicit = true;
697
+ }
698
+ else if (a === "--mine-history") {
699
+ mode = modeExplicit ? "all" : "mine-history";
700
+ modeExplicit = true;
701
+ }
702
+ else if (a === "--full") {
703
+ fullHistory = true;
704
+ }
705
+ else if (a === "--help" || a === "-h") {
706
+ help = true;
707
+ }
708
+ }
709
+ return { repoRoot, mode, only, fullHistory, help };
710
+ }
711
+ function printHelp() {
712
+ console.log(`
713
+ slowcook refresh-knowledge — rebuild repo-knowledge digests
714
+
715
+ Usage:
716
+ slowcook refresh-knowledge [--auto] [--mine-history] [--only <name>] [--cwd <path>]
717
+
718
+ Modes:
719
+ --auto rebuild auto/ digests (default if no mode given)
720
+ cheap extractions, always rebuild
721
+ --mine-history rebuild curated/ files from git history
722
+ expensive deterministic; delta-aware (re-mines new commits only)
723
+
724
+ --only <name> filters to one digest (auto mode only).
725
+
726
+ auto/ outputs are gitignored. curated/ outputs are TRACKED in git —
727
+ they're the durable organizational memory.
728
+ `);
729
+ }
730
+ function writeCurated(repoRoot, name, body) {
731
+ const abs = join(repoRoot, CURATED_DIR_REL, `${name}.md`);
732
+ mkdirSync(dirname(abs), { recursive: true });
733
+ writeFileSync(abs, body, "utf8");
734
+ }
735
+ function readStamp(repoRoot) {
736
+ const path = join(repoRoot, CURATED_DIR_REL, ".last-mined.json");
737
+ if (!existsSync(path))
738
+ return null;
739
+ try {
740
+ return JSON.parse(readFileSync(path, "utf8"));
741
+ }
742
+ catch {
743
+ return null;
744
+ }
745
+ }
746
+ function writeStamp(repoRoot, stamp) {
747
+ const abs = join(repoRoot, CURATED_DIR_REL, ".last-mined.json");
748
+ mkdirSync(dirname(abs), { recursive: true });
749
+ writeFileSync(abs, JSON.stringify(stamp, null, 2) + "\n", "utf8");
750
+ }
751
+ /**
752
+ * Walk git log, return CommitRow[] sorted oldest-first.
753
+ *
754
+ * Uses `git log --name-only --pretty=format` with a unique separator so
755
+ * we can parse robustly. The default `maxCommits` cap (1500) covers a
756
+ * typical brownfield project's history without timing out.
757
+ */
758
+ function gitLogRows(repoRoot, maxCommits = 1500) {
759
+ const sep = "<<<COMMIT>>>";
760
+ const fieldSep = "<<<F>>>";
761
+ let raw = "";
762
+ try {
763
+ raw = execSync(`git -C "${repoRoot}" log --no-merges --name-only --pretty=format:'${sep}%H${fieldSep}%P${fieldSep}%an${fieldSep}%aI${fieldSep}%s' -n ${maxCommits}`, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
764
+ }
765
+ catch {
766
+ return [];
767
+ }
768
+ const out = [];
769
+ for (const block of raw.split(sep)) {
770
+ const trimmed = block.trim();
771
+ if (!trimmed)
772
+ continue;
773
+ const lines = trimmed.split("\n");
774
+ const header = lines[0] ?? "";
775
+ const parts = header.split(fieldSep);
776
+ if (parts.length < 5)
777
+ continue;
778
+ const sha = parts[0];
779
+ const parent = parts[1].split(" ")[0] ?? "";
780
+ const author = parts[2];
781
+ const date = parts[3];
782
+ const subject = parts[4] ?? "";
783
+ const files = lines.slice(1).map((l) => l.trim()).filter((l) => l.length > 0);
784
+ out.push({ sha, parent, author, date, subject, files });
785
+ }
786
+ // Reverse so oldest is first — easier to reason about temporal accumulation.
787
+ return out.reverse();
788
+ }
789
+ /**
790
+ * commit-conventions.md — bucket by type:scope from conventional-commit
791
+ * prefixes. Tells refine which prefixes + scopes are in active use so
792
+ * spec PRs match the local style.
793
+ */
794
+ function buildCommitConventions(rows) {
795
+ const conventionRe = /^(feat|fix|chore|refactor|docs|test|perf|style|build|ci|revert)(?:\(([^)]+)\))?\s*:/;
796
+ const byType = {};
797
+ const byScope = {};
798
+ let unconventional = 0;
799
+ for (const r of rows) {
800
+ const m = r.subject.match(conventionRe);
801
+ if (!m) {
802
+ unconventional++;
803
+ continue;
804
+ }
805
+ const type = m[1];
806
+ let scope = m[2] ?? "(none)";
807
+ // Filter out `#NNN`-style "scopes" that are actually issue refs the
808
+ // author accidentally put in the parens (e.g., `fix(#618): ...`).
809
+ if (/^#\d+$/.test(scope))
810
+ scope = "(none)";
811
+ byType[type] = (byType[type] ?? 0) + 1;
812
+ byScope[scope] = (byScope[scope] ?? 0) + 1;
813
+ }
814
+ const total = rows.length;
815
+ const conventional = total - unconventional;
816
+ const lines = [];
817
+ lines.push("# Commit conventions (mined from git history)\n");
818
+ lines.push(`_Sample: ${total} non-merge commits; ${conventional} use conventional-commit prefixes (${Math.round(100 * conventional / Math.max(total, 1))}%)._`);
819
+ lines.push("");
820
+ lines.push("## Active type buckets");
821
+ const typesSorted = Object.entries(byType).sort((a, b) => b[1] - a[1]);
822
+ lines.push("| Type | Count |");
823
+ lines.push("|---|---|");
824
+ for (const [t, n] of typesSorted)
825
+ lines.push(`| \`${t}\` | ${n} |`);
826
+ lines.push("");
827
+ lines.push("## Active scopes (use these in new commit messages)");
828
+ const scopesSorted = Object.entries(byScope).sort((a, b) => b[1] - a[1]).filter(([s]) => s !== "(none)");
829
+ lines.push("| Scope | Count |");
830
+ lines.push("|---|---|");
831
+ for (const [s, n] of scopesSorted.slice(0, 30))
832
+ lines.push(`| \`${s}\` | ${n} |`);
833
+ if (scopesSorted.length > 30)
834
+ lines.push(`| … ${scopesSorted.length - 30} more | |`);
835
+ return lines.join("\n");
836
+ }
837
+ /**
838
+ * co-changes.md — file pairs that co-occur in commits >= threshold
839
+ * times. Surfaces temporal coupling that's invisible to static
840
+ * analysis (e.g., "every time `appointment.entity.ts` changes,
841
+ * `appointment.dto.ts` changes too in 12/14 cases").
842
+ *
843
+ * Quadratic in files-per-commit; bounded to commits with <=20 files
844
+ * to prevent O(n²) blow-up on huge refactors that aren't useful
845
+ * signal anyway.
846
+ */
847
+ function buildCoChanges(rows) {
848
+ const pairCounts = new Map();
849
+ const fileCounts = new Map();
850
+ for (const r of rows) {
851
+ if (r.files.length === 0 || r.files.length > 20)
852
+ continue;
853
+ const interesting = r.files.filter((f) => /\.(ts|tsx|js|jsx|sql|md)$/.test(f) && !f.startsWith(".brewing/") && !f.startsWith("node_modules/"));
854
+ for (const f of interesting)
855
+ fileCounts.set(f, (fileCounts.get(f) ?? 0) + 1);
856
+ for (let i = 0; i < interesting.length; i++) {
857
+ for (let j = i + 1; j < interesting.length; j++) {
858
+ const a = interesting[i];
859
+ const b = interesting[j];
860
+ const key = a < b ? `${a}\t${b}` : `${b}\t${a}`;
861
+ pairCounts.set(key, (pairCounts.get(key) ?? 0) + 1);
862
+ }
863
+ }
864
+ }
865
+ const lines = [];
866
+ lines.push("# File co-change map (mined from git history)\n");
867
+ lines.push("Pairs of files that historically change together. When a spec / PR touches one, also consider the other — co-change ≥3 usually signals coupling the type system can't see.\n");
868
+ const pairs = [...pairCounts.entries()]
869
+ .filter(([_, n]) => n >= 3)
870
+ .map(([key, n]) => {
871
+ const [a, b] = key.split("\t");
872
+ return { a: a, b: b, n, support: Math.min(fileCounts.get(a) ?? 1, fileCounts.get(b) ?? 1) };
873
+ })
874
+ .filter((p) => p.n >= Math.max(3, Math.floor(p.support * 0.5))) // co-occur in >=50% of either file's commits
875
+ .sort((a, b) => b.n - a.n);
876
+ lines.push("| File A | File B | Co-changes |");
877
+ lines.push("|---|---|---|");
878
+ for (const p of pairs.slice(0, 60)) {
879
+ lines.push(`| \`${p.a}\` | \`${p.b}\` | ${p.n} |`);
880
+ }
881
+ if (pairs.length > 60)
882
+ lines.push(`| … ${pairs.length - 60} more | | |`);
883
+ return lines.join("\n");
884
+ }
885
+ /**
886
+ * ownership.md — top author per top-level directory. Tells refine
887
+ * who owns what (useful for routing PM-facing questions in agent comments).
888
+ */
889
+ function buildOwnership(rows) {
890
+ // Per-directory granularity at depth 2 means we get "apps/back" or
891
+ // "packages/dtos" but also occasionally "packages/postgres" + sibling
892
+ // entries that explode the table. Special-cased: top-level dirs with
893
+ // a single file (.brewing/*.md, .githooks/*, etc.) collapse to the
894
+ // top-level dir to avoid one row per file.
895
+ const dirAuthorCount = new Map();
896
+ const TOPLEVEL_COLLAPSE = new Set([".brewing", ".github", ".cursor", ".vscode", ".husky", ".githooks"]);
897
+ for (const r of rows) {
898
+ for (const f of r.files) {
899
+ const parts = f.split("/");
900
+ const top = parts[0];
901
+ const dir = TOPLEVEL_COLLAPSE.has(top) ? top : parts.slice(0, 2).join("/") || ".";
902
+ let m = dirAuthorCount.get(dir);
903
+ if (!m) {
904
+ m = new Map();
905
+ dirAuthorCount.set(dir, m);
906
+ }
907
+ m.set(r.author, (m.get(r.author) ?? 0) + 1);
908
+ }
909
+ }
910
+ const lines = [];
911
+ lines.push("# Directory ownership (mined from git authorship)\n");
912
+ lines.push("Top contributor per directory. Use this to decide who to route a PM-facing question to when a story spans multiple areas.\n");
913
+ lines.push("| Directory | Top author | Commits |");
914
+ lines.push("|---|---|---|");
915
+ const dirsSorted = [...dirAuthorCount.keys()].sort();
916
+ for (const dir of dirsSorted) {
917
+ const authors = [...dirAuthorCount.get(dir).entries()].sort((a, b) => b[1] - a[1]);
918
+ if (authors.length === 0)
919
+ continue;
920
+ const [topAuthor, topCount] = authors[0];
921
+ lines.push(`| \`${dir}\` | ${topAuthor} | ${topCount} |`);
922
+ }
923
+ return lines.join("\n");
924
+ }
925
+ /**
926
+ * issue-traceability.md — for each `#NNN` issue/PR reference in a
927
+ * commit subject, list the commits that mention it. Cheap, useful for
928
+ * agents that want to find the PM context behind a code change.
929
+ */
930
+ function buildIssueTraceability(rows) {
931
+ const issueRe = /#(\d+)/g;
932
+ const issueToCommits = new Map();
933
+ for (const r of rows) {
934
+ let m;
935
+ issueRe.lastIndex = 0;
936
+ while ((m = issueRe.exec(r.subject)) !== null) {
937
+ const n = m[1];
938
+ if (!issueToCommits.has(n))
939
+ issueToCommits.set(n, []);
940
+ issueToCommits.get(n).push({ sha: r.sha.slice(0, 8), subject: r.subject });
941
+ }
942
+ }
943
+ const lines = [];
944
+ lines.push("# Issue / PR traceability (mined from commit subjects)\n");
945
+ lines.push("Maps `#N` references to the commits that mention them. Use to find PM intent behind a body of code changes.\n");
946
+ const issues = [...issueToCommits.entries()].sort((a, b) => parseInt(b[0], 10) - parseInt(a[0], 10));
947
+ for (const [n, commits] of issues.slice(0, 100)) {
948
+ lines.push(`### #${n}`);
949
+ for (const c of commits.slice(0, 8)) {
950
+ lines.push(`- \`${c.sha}\` ${c.subject.replace(/\|/g, "\\|")}`);
951
+ }
952
+ lines.push("");
953
+ }
954
+ if (issues.length > 100)
955
+ lines.push(`_… ${issues.length - 100} more issues with fewer commits._`);
956
+ return lines.join("\n");
957
+ }
958
+ /**
959
+ * fix-recipe-seeds.md — for each fix(*) commit, group by the files
960
+ * touched. Refine + chef use this to spot recurring failure classes
961
+ * (e.g., "vitest.config.ts has been fixed twice — known-flaky area").
962
+ * NOT a curated insight (that's the next layer); just file→fix-PRs
963
+ * map to surface the pattern.
964
+ */
965
+ function buildFixRecipeSeeds(rows) {
966
+ const fileFixCount = new Map();
967
+ for (const r of rows) {
968
+ if (!/^fix\b/.test(r.subject))
969
+ continue;
970
+ for (const f of r.files) {
971
+ if (!/\.(ts|tsx|js|jsx|json|yaml|yml)$/.test(f))
972
+ continue;
973
+ if (!fileFixCount.has(f))
974
+ fileFixCount.set(f, []);
975
+ fileFixCount.get(f).push({ sha: r.sha.slice(0, 8), subject: r.subject, date: r.date.slice(0, 10) });
976
+ }
977
+ }
978
+ const lines = [];
979
+ lines.push("# Fix-recipe seeds (mined from fix(*) commits)\n");
980
+ lines.push("Files that have been the target of fix-commits. A file with multiple fixes is a known-fragile area — check the listed commits before re-engineering. (Insights derived from these seeds live in `chef-known-fixes.md` once chef has analysed them.)\n");
981
+ const ranked = [...fileFixCount.entries()].filter(([_, fixes]) => fixes.length >= 2).sort((a, b) => b[1].length - a[1].length);
982
+ for (const [file, fixes] of ranked.slice(0, 40)) {
983
+ lines.push(`### \`${file}\` — fixed ${fixes.length}×`);
984
+ for (const f of fixes.slice(0, 6)) {
985
+ lines.push(`- \`${f.sha}\` (${f.date}) ${f.subject}`);
986
+ }
987
+ lines.push("");
988
+ }
989
+ if (ranked.length > 40)
990
+ lines.push(`_… ${ranked.length - 40} more files with 2+ fixes._`);
991
+ return lines.join("\n");
992
+ }
993
+ export function refreshKnowledgeMineHistory(repoRoot, opts = {}) {
994
+ const stamp = opts.full ? null : readStamp(repoRoot);
995
+ const maxCommits = opts.maxCommits ?? 1500;
996
+ const rows = gitLogRows(repoRoot, maxCommits);
997
+ if (rows.length === 0) {
998
+ return { outDir: join(repoRoot, CURATED_DIR_REL), built: [], commitsProcessed: 0, deltaFromSha: null };
999
+ }
1000
+ // For now: always re-mine the full window. Delta-aware merge is
1001
+ // wired up via the stamp file (consumers see `last_sha`), but the
1002
+ // actual incremental aggregation lives in a later alpha — at 1000
1003
+ // commits this still takes <2s, so the full rebuild is fine.
1004
+ const built = [];
1005
+ const write = (name, body) => {
1006
+ writeCurated(repoRoot, name, body);
1007
+ built.push(name);
1008
+ };
1009
+ write("commit-conventions", buildCommitConventions(rows));
1010
+ write("co-changes", buildCoChanges(rows));
1011
+ write("ownership", buildOwnership(rows));
1012
+ write("issue-traceability", buildIssueTraceability(rows));
1013
+ write("fix-recipe-seeds", buildFixRecipeSeeds(rows));
1014
+ // Stamp updates regardless — captures the most-recent SHA we've
1015
+ // seen so future delta-mining knows where to resume from.
1016
+ const newest = rows[rows.length - 1];
1017
+ writeStamp(repoRoot, {
1018
+ last_sha: newest.sha,
1019
+ last_mined_at: new Date().toISOString(),
1020
+ total_commits_seen: rows.length,
1021
+ });
1022
+ return {
1023
+ outDir: join(repoRoot, CURATED_DIR_REL),
1024
+ built,
1025
+ commitsProcessed: rows.length,
1026
+ deltaFromSha: stamp?.last_sha ?? null,
1027
+ };
1028
+ }
1029
+ //# sourceMappingURL=refresh-knowledge.js.map