@mainahq/core 1.0.3 → 1.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 (71) hide show
  1. package/package.json +1 -1
  2. package/src/ai/__tests__/delegation.test.ts +55 -1
  3. package/src/ai/delegation.ts +5 -3
  4. package/src/context/__tests__/budget.test.ts +29 -6
  5. package/src/context/__tests__/engine.test.ts +1 -0
  6. package/src/context/__tests__/selector.test.ts +23 -3
  7. package/src/context/__tests__/wiki.test.ts +349 -0
  8. package/src/context/budget.ts +12 -8
  9. package/src/context/engine.ts +37 -0
  10. package/src/context/selector.ts +30 -4
  11. package/src/context/wiki.ts +296 -0
  12. package/src/db/index.ts +12 -0
  13. package/src/feedback/__tests__/capture.test.ts +166 -0
  14. package/src/feedback/__tests__/signals.test.ts +144 -0
  15. package/src/feedback/__tests__/tmp-capture-1775575256633-lah0etnzlj/feedback.db +0 -0
  16. package/src/feedback/__tests__/tmp-capture-1775575256640-2xmjme4qraa/feedback.db +0 -0
  17. package/src/feedback/capture.ts +102 -0
  18. package/src/feedback/signals.ts +68 -0
  19. package/src/index.ts +104 -0
  20. package/src/init/__tests__/init.test.ts +400 -3
  21. package/src/init/index.ts +368 -12
  22. package/src/language/__tests__/__fixtures__/detect/composer.lock +1 -0
  23. package/src/prompts/defaults/index.ts +3 -1
  24. package/src/prompts/defaults/wiki-compile.md +20 -0
  25. package/src/prompts/defaults/wiki-query.md +18 -0
  26. package/src/stats/__tests__/tool-usage.test.ts +133 -0
  27. package/src/stats/tracker.ts +92 -0
  28. package/src/verify/__tests__/pipeline.test.ts +11 -8
  29. package/src/verify/pipeline.ts +13 -1
  30. package/src/verify/tools/__tests__/wiki-lint.test.ts +784 -0
  31. package/src/verify/tools/wiki-lint-runner.ts +38 -0
  32. package/src/verify/tools/wiki-lint.ts +898 -0
  33. package/src/wiki/__tests__/compiler.test.ts +389 -0
  34. package/src/wiki/__tests__/extractors/code.test.ts +99 -0
  35. package/src/wiki/__tests__/extractors/decision.test.ts +323 -0
  36. package/src/wiki/__tests__/extractors/feature.test.ts +186 -0
  37. package/src/wiki/__tests__/extractors/workflow.test.ts +131 -0
  38. package/src/wiki/__tests__/graph.test.ts +344 -0
  39. package/src/wiki/__tests__/hooks.test.ts +119 -0
  40. package/src/wiki/__tests__/indexer.test.ts +285 -0
  41. package/src/wiki/__tests__/linker.test.ts +230 -0
  42. package/src/wiki/__tests__/louvain.test.ts +229 -0
  43. package/src/wiki/__tests__/query.test.ts +316 -0
  44. package/src/wiki/__tests__/schema.test.ts +114 -0
  45. package/src/wiki/__tests__/signals.test.ts +474 -0
  46. package/src/wiki/__tests__/state.test.ts +168 -0
  47. package/src/wiki/__tests__/tracking.test.ts +118 -0
  48. package/src/wiki/__tests__/types.test.ts +387 -0
  49. package/src/wiki/compiler.ts +1075 -0
  50. package/src/wiki/extractors/code.ts +90 -0
  51. package/src/wiki/extractors/decision.ts +217 -0
  52. package/src/wiki/extractors/feature.ts +206 -0
  53. package/src/wiki/extractors/workflow.ts +112 -0
  54. package/src/wiki/graph.ts +445 -0
  55. package/src/wiki/hooks.ts +49 -0
  56. package/src/wiki/indexer.ts +105 -0
  57. package/src/wiki/linker.ts +117 -0
  58. package/src/wiki/louvain.ts +190 -0
  59. package/src/wiki/prompts/compile-architecture.md +59 -0
  60. package/src/wiki/prompts/compile-decision.md +66 -0
  61. package/src/wiki/prompts/compile-entity.md +56 -0
  62. package/src/wiki/prompts/compile-feature.md +60 -0
  63. package/src/wiki/prompts/compile-module.md +42 -0
  64. package/src/wiki/prompts/wiki-query.md +25 -0
  65. package/src/wiki/query.ts +338 -0
  66. package/src/wiki/schema.ts +111 -0
  67. package/src/wiki/signals.ts +368 -0
  68. package/src/wiki/state.ts +89 -0
  69. package/src/wiki/tracking.ts +30 -0
  70. package/src/wiki/types.ts +169 -0
  71. package/src/workflow/context.ts +26 -0
@@ -0,0 +1,1075 @@
1
+ /**
2
+ * Wiki Compiler — full compilation orchestrator.
3
+ *
4
+ * Pipeline:
5
+ * 1. Run all extractors (code entities, features, decisions, workflow traces)
6
+ * 2. Build the unified knowledge graph
7
+ * 3. Run Louvain community detection for module boundaries
8
+ * 4. Compute PageRank
9
+ * 5. Generate articles using template-based compilation (no AI)
10
+ * 6. Generate wikilinks via linker
11
+ * 7. Generate index.md via indexer
12
+ * 8. Save state
13
+ * 9. Write all articles to disk
14
+ */
15
+
16
+ import { mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
17
+ import { dirname, join, relative } from "node:path";
18
+ import type { TryAIResult } from "../ai/try-generate";
19
+ import type { Result } from "../db/index";
20
+ import type { CodeEntity } from "./extractors/code";
21
+ import { extractCodeEntities } from "./extractors/code";
22
+ import { extractDecisions } from "./extractors/decision";
23
+ import { extractFeatures } from "./extractors/feature";
24
+ import { extractWorkflowTrace } from "./extractors/workflow";
25
+ import type { KnowledgeGraph } from "./graph";
26
+ import { buildKnowledgeGraph, computePageRank, mapToArticles } from "./graph";
27
+ import { generateIndex } from "./indexer";
28
+ import { generateLinks } from "./linker";
29
+ import { detectCommunities } from "./louvain";
30
+ import { createEmptyState, hashContent, loadState, saveState } from "./state";
31
+ import type {
32
+ ArticleType,
33
+ ExtractedDecision,
34
+ ExtractedFeature,
35
+ ExtractedWorkflowTrace,
36
+ WikiArticle,
37
+ WikiLink,
38
+ } from "./types";
39
+
40
+ // ─── Types ───────────────────────────────────────────────────────────────
41
+
42
+ export interface CompilationResult {
43
+ articles: WikiArticle[];
44
+ graph: KnowledgeGraph;
45
+ state: import("./types").WikiState;
46
+ duration: number;
47
+ stats: {
48
+ modules: number;
49
+ entities: number;
50
+ features: number;
51
+ decisions: number;
52
+ architecture: number;
53
+ };
54
+ }
55
+
56
+ export interface CompileOptions {
57
+ repoRoot: string;
58
+ mainaDir: string;
59
+ wikiDir: string;
60
+ full?: boolean;
61
+ dryRun?: boolean;
62
+ useAI?: boolean;
63
+ }
64
+
65
+ // ─── AI Enhancement ─────────────────────────────────────────────────────
66
+
67
+ /**
68
+ * Enhance a wiki article with AI-generated natural language descriptions.
69
+ * Falls back to the original content if AI is unavailable or fails.
70
+ */
71
+ async function enhanceWithAI(
72
+ article: WikiArticle,
73
+ context: string,
74
+ mainaDir: string,
75
+ ): Promise<string> {
76
+ try {
77
+ const { tryAIGenerate } = await import("../ai/try-generate");
78
+ const userPrompt = [
79
+ "## Article to Enhance",
80
+ "",
81
+ article.content,
82
+ "",
83
+ "## Surrounding Context",
84
+ "",
85
+ context,
86
+ ].join("\n");
87
+
88
+ const result: TryAIResult = await tryAIGenerate(
89
+ "wiki-compile",
90
+ mainaDir,
91
+ { task: "wiki-compile" },
92
+ userPrompt,
93
+ );
94
+
95
+ // If AI returned text, use it; otherwise fall back to original
96
+ return result.text ?? article.content;
97
+ } catch {
98
+ // AI failure — silently fall back to template-only
99
+ return article.content;
100
+ }
101
+ }
102
+
103
+ // ─── File Discovery ─────────────────────────────────────────────────────
104
+
105
+ /**
106
+ * Recursively find all TypeScript files under the repo root.
107
+ * Skips node_modules, dist, .git, and hidden directories.
108
+ */
109
+ function findSourceFiles(dir: string, rootDir: string): string[] {
110
+ const files: string[] = [];
111
+ const skipDirs = new Set([
112
+ "node_modules",
113
+ "dist",
114
+ ".git",
115
+ ".maina",
116
+ "coverage",
117
+ ]);
118
+
119
+ let entries: string[];
120
+ try {
121
+ entries = readdirSync(dir);
122
+ } catch {
123
+ return files;
124
+ }
125
+
126
+ for (const entry of entries) {
127
+ if (entry.startsWith(".") || skipDirs.has(entry)) continue;
128
+
129
+ const fullPath = join(dir, entry);
130
+ let stat: ReturnType<typeof statSync> | null = null;
131
+ try {
132
+ stat = statSync(fullPath);
133
+ } catch {
134
+ continue;
135
+ }
136
+
137
+ if (stat?.isDirectory()) {
138
+ files.push(...findSourceFiles(fullPath, rootDir));
139
+ } else if (
140
+ entry.endsWith(".ts") &&
141
+ !entry.endsWith(".test.ts") &&
142
+ !entry.endsWith(".d.ts")
143
+ ) {
144
+ files.push(relative(rootDir, fullPath));
145
+ }
146
+ }
147
+
148
+ return files;
149
+ }
150
+
151
+ // ─── Template-Based Article Generation ──────────────────────────────────
152
+
153
+ function generateModuleArticle(
154
+ moduleName: string,
155
+ memberEntities: CodeEntity[],
156
+ features: ExtractedFeature[],
157
+ decisions: ExtractedDecision[],
158
+ pageRankScores: Map<string, number>,
159
+ ): string {
160
+ const lines: string[] = [];
161
+
162
+ lines.push(`# Module: ${moduleName}`);
163
+ lines.push("");
164
+ lines.push(`> Auto-generated module article for \`${moduleName}\`.`);
165
+ lines.push("");
166
+
167
+ // Entities section
168
+ lines.push("## Entities");
169
+ lines.push("");
170
+ if (memberEntities.length === 0) {
171
+ lines.push("_No entities detected._");
172
+ } else {
173
+ // Detect duplicate entity names to enable disambiguation
174
+ const nameCounts = new Map<string, number>();
175
+ for (const entity of memberEntities) {
176
+ nameCounts.set(entity.name, (nameCounts.get(entity.name) ?? 0) + 1);
177
+ }
178
+
179
+ const sorted = [...memberEntities].sort((a, b) => {
180
+ const prA = pageRankScores.get(`entity:${a.name}`) ?? 0;
181
+ const prB = pageRankScores.get(`entity:${b.name}`) ?? 0;
182
+ return prB - prA;
183
+ });
184
+ for (const entity of sorted) {
185
+ const pr = pageRankScores.get(`entity:${entity.name}`) ?? 0;
186
+ // Disambiguate duplicate names by appending the package/top-level directory
187
+ const isDuplicate = (nameCounts.get(entity.name) ?? 0) > 1;
188
+ const displayName = isDuplicate
189
+ ? `${entity.name} (${entity.file.replace(/\\/g, "/").split("/")[0] ?? entity.file})`
190
+ : entity.name;
191
+ lines.push(
192
+ `- **${displayName}** (${entity.kind}) — \`${entity.file}:${entity.line}\` [PR: ${pr.toFixed(4)}]`,
193
+ );
194
+ }
195
+ }
196
+ lines.push("");
197
+
198
+ // Related features
199
+ const relatedFeatures = features.filter((f) =>
200
+ f.entitiesModified.some((e) => memberEntities.some((me) => me.name === e)),
201
+ );
202
+ if (relatedFeatures.length > 0) {
203
+ lines.push("## Related Features");
204
+ lines.push("");
205
+ for (const f of relatedFeatures) {
206
+ const status = f.merged ? "merged" : "in-progress";
207
+ lines.push(`- [[feature:${f.id}]] — ${f.title} (${status})`);
208
+ }
209
+ lines.push("");
210
+ }
211
+
212
+ // Related decisions — only include when the module has entities and at least one
213
+ // decision references an entity in this module
214
+ if (memberEntities.length > 0) {
215
+ const relatedDecisions = decisions.filter((d) =>
216
+ d.entityMentions.some((m) =>
217
+ memberEntities.some((e) => m.includes(e.name) || m.includes(e.file)),
218
+ ),
219
+ );
220
+ if (relatedDecisions.length > 0) {
221
+ lines.push("## Related Decisions");
222
+ lines.push("");
223
+ for (const d of relatedDecisions) {
224
+ lines.push(`- [[decision:${d.id}]] — ${d.title} [${d.status}]`);
225
+ }
226
+ lines.push("");
227
+ }
228
+ }
229
+
230
+ return lines.join("\n");
231
+ }
232
+
233
+ function generateEntityArticle(
234
+ entity: CodeEntity,
235
+ features: ExtractedFeature[],
236
+ decisions: ExtractedDecision[],
237
+ graph: KnowledgeGraph,
238
+ ): string {
239
+ const lines: string[] = [];
240
+ const entityId = `entity:${entity.name}`;
241
+
242
+ lines.push(`# Entity: ${entity.name}`);
243
+ lines.push("");
244
+ lines.push(`> ${entity.kind} in \`${entity.file}:${entity.line}\``);
245
+ lines.push("");
246
+
247
+ // Signature
248
+ lines.push("## Details");
249
+ lines.push("");
250
+ lines.push(`- **Kind:** ${entity.kind}`);
251
+ lines.push(`- **File:** \`${entity.file}\``);
252
+ lines.push(`- **Line:** ${entity.line}`);
253
+ lines.push(`- **Exported:** ${entity.exported ? "yes" : "no"}`);
254
+ const node = graph.nodes.get(entityId);
255
+ if (node) {
256
+ lines.push(`- **PageRank:** ${node.pageRank.toFixed(4)}`);
257
+ }
258
+ lines.push("");
259
+
260
+ // Callers and callees (from graph edges)
261
+ const callers: string[] = [];
262
+ const callees: string[] = [];
263
+ for (const edge of graph.edges) {
264
+ if (edge.target === entityId && edge.type === "calls") {
265
+ callers.push(edge.source);
266
+ }
267
+ if (edge.source === entityId && edge.type === "calls") {
268
+ callees.push(edge.target);
269
+ }
270
+ }
271
+
272
+ if (callers.length > 0) {
273
+ lines.push("## Callers");
274
+ lines.push("");
275
+ for (const c of callers) {
276
+ lines.push(`- [[${c}]]`);
277
+ }
278
+ lines.push("");
279
+ }
280
+
281
+ if (callees.length > 0) {
282
+ lines.push("## Callees");
283
+ lines.push("");
284
+ for (const c of callees) {
285
+ lines.push(`- [[${c}]]`);
286
+ }
287
+ lines.push("");
288
+ }
289
+
290
+ // Related features
291
+ const relatedFeatures = features.filter((f) =>
292
+ f.entitiesModified.includes(entity.name),
293
+ );
294
+ if (relatedFeatures.length > 0) {
295
+ lines.push("## Related Features");
296
+ lines.push("");
297
+ for (const f of relatedFeatures) {
298
+ lines.push(`- [[feature:${f.id}]] — ${f.title}`);
299
+ }
300
+ lines.push("");
301
+ }
302
+
303
+ // Related decisions
304
+ const relatedDecisions = decisions.filter((d) =>
305
+ d.entityMentions.some((m) => m.includes(entity.name)),
306
+ );
307
+ if (relatedDecisions.length > 0) {
308
+ lines.push("## Related Decisions");
309
+ lines.push("");
310
+ for (const d of relatedDecisions) {
311
+ lines.push(`- [[decision:${d.id}]] — ${d.title}`);
312
+ }
313
+ lines.push("");
314
+ }
315
+
316
+ return lines.join("\n");
317
+ }
318
+
319
+ function generateFeatureArticle(feature: ExtractedFeature): string {
320
+ const lines: string[] = [];
321
+
322
+ lines.push(`# Feature: ${feature.title || feature.id}`);
323
+ lines.push("");
324
+
325
+ if (feature.scope) {
326
+ // Only emit the Scope section if the content has real information
327
+ // (not just unresolved [NEEDS CLARIFICATION] placeholders)
328
+ const cleanScope = feature.scope
329
+ .replace(/\[NEEDS CLARIFICATION\][^.]*\./gi, "")
330
+ .trim();
331
+ if (cleanScope.length > 0) {
332
+ lines.push("## Scope");
333
+ lines.push("");
334
+ lines.push(cleanScope);
335
+ lines.push("");
336
+ } else {
337
+ lines.push("## Scope");
338
+ lines.push("");
339
+ lines.push("- TODO(scope): Define what this feature does.");
340
+ lines.push(
341
+ "- TODO(scope): Define what this feature explicitly does not do to prevent over-building.",
342
+ );
343
+ lines.push("");
344
+ }
345
+ }
346
+
347
+ // Spec assertions
348
+ if (feature.specAssertions.length > 0) {
349
+ lines.push("## Spec Assertions");
350
+ lines.push("");
351
+ for (const assertion of feature.specAssertions) {
352
+ lines.push(`- [ ] ${assertion}`);
353
+ }
354
+ lines.push("");
355
+ }
356
+
357
+ // Tasks
358
+ if (feature.tasks.length > 0) {
359
+ lines.push("## Tasks");
360
+ lines.push("");
361
+ const completed = feature.tasks.filter((t) => t.completed).length;
362
+ lines.push(
363
+ `Progress: ${completed}/${feature.tasks.length} (${Math.round((completed / feature.tasks.length) * 100)}%)`,
364
+ );
365
+ lines.push("");
366
+ for (const task of feature.tasks) {
367
+ const check = task.completed ? "x" : " ";
368
+ lines.push(`- [${check}] ${task.id}: ${task.description}`);
369
+ }
370
+ lines.push("");
371
+ }
372
+
373
+ // Modified entities
374
+ if (feature.entitiesModified.length > 0) {
375
+ lines.push("## Entities Modified");
376
+ lines.push("");
377
+ for (const entity of feature.entitiesModified) {
378
+ lines.push(`- [[entity:${entity}]]`);
379
+ }
380
+ lines.push("");
381
+ }
382
+
383
+ // Decisions
384
+ if (feature.decisionsCreated.length > 0) {
385
+ lines.push("## Decisions Created");
386
+ lines.push("");
387
+ for (const d of feature.decisionsCreated) {
388
+ lines.push(`- [[decision:${d}]]`);
389
+ }
390
+ lines.push("");
391
+ }
392
+
393
+ // Status
394
+ lines.push("## Status");
395
+ lines.push("");
396
+ lines.push(`- **Branch:** ${feature.branch || "_none_"}`);
397
+ lines.push(
398
+ `- **PR:** ${feature.prNumber !== null ? `#${feature.prNumber}` : "_none_"}`,
399
+ );
400
+ lines.push(`- **Merged:** ${feature.merged ? "yes" : "no"}`);
401
+ lines.push("");
402
+
403
+ return lines.join("\n");
404
+ }
405
+
406
+ function generateDecisionArticle(decision: ExtractedDecision): string {
407
+ const lines: string[] = [];
408
+
409
+ lines.push(`# Decision: ${decision.title || decision.id}`);
410
+ lines.push("");
411
+ lines.push(`> Status: **${decision.status}**`);
412
+ lines.push("");
413
+
414
+ if (decision.context) {
415
+ lines.push("## Context");
416
+ lines.push("");
417
+ lines.push(decision.context);
418
+ lines.push("");
419
+ }
420
+
421
+ if (decision.decision) {
422
+ lines.push("## Decision");
423
+ lines.push("");
424
+ lines.push(decision.decision);
425
+ lines.push("");
426
+ }
427
+
428
+ if (decision.rationale) {
429
+ lines.push("## Rationale");
430
+ lines.push("");
431
+ lines.push(decision.rationale);
432
+ lines.push("");
433
+ }
434
+
435
+ if (decision.alternativesRejected.length > 0) {
436
+ lines.push("## Alternatives Rejected");
437
+ lines.push("");
438
+ for (const alt of decision.alternativesRejected) {
439
+ lines.push(`- ${alt}`);
440
+ }
441
+ lines.push("");
442
+ }
443
+
444
+ if (decision.entityMentions.length > 0) {
445
+ lines.push("## Affected Entities");
446
+ lines.push("");
447
+ for (const entity of decision.entityMentions) {
448
+ lines.push(`- \`${entity}\``);
449
+ }
450
+ lines.push("");
451
+ }
452
+
453
+ return lines.join("\n");
454
+ }
455
+
456
+ // ─── Architecture Article Generation ───────────────────────────────────
457
+
458
+ interface ArchitectureArticle {
459
+ slug: string;
460
+ title: string;
461
+ content: string;
462
+ }
463
+
464
+ /**
465
+ * Detect the Three Engines pattern (context/, prompts/, verify/) under
466
+ * packages/core/src and generate a descriptive article.
467
+ */
468
+ function generateThreeEnginesArticle(
469
+ repoRoot: string,
470
+ ): ArchitectureArticle | null {
471
+ const coreEnginesDir = join(repoRoot, "packages", "core", "src");
472
+ const engines = ["context", "prompts", "verify"];
473
+ const detected: string[] = [];
474
+
475
+ for (const engine of engines) {
476
+ try {
477
+ const stat = statSync(join(coreEnginesDir, engine));
478
+ if (stat.isDirectory()) {
479
+ detected.push(engine);
480
+ }
481
+ } catch {
482
+ // directory doesn't exist
483
+ }
484
+ }
485
+
486
+ if (detected.length < 2) return null;
487
+
488
+ const engineFiles: Record<string, string[]> = {};
489
+ for (const engine of detected) {
490
+ try {
491
+ engineFiles[engine] = readdirSync(join(coreEnginesDir, engine))
492
+ .filter(
493
+ (f) =>
494
+ f.endsWith(".ts") &&
495
+ !f.endsWith(".test.ts") &&
496
+ !f.endsWith(".d.ts"),
497
+ )
498
+ .sort();
499
+ } catch {
500
+ engineFiles[engine] = [];
501
+ }
502
+ }
503
+
504
+ const lines: string[] = [];
505
+ lines.push("# Architecture: Three Engines");
506
+ lines.push("");
507
+ lines.push(
508
+ "> Auto-generated architecture article describing the three-engine pattern.",
509
+ );
510
+ lines.push("");
511
+ lines.push(
512
+ "Maina's core is organized around three engines that work together:",
513
+ );
514
+ lines.push("");
515
+ lines.push(
516
+ "1. **Context Engine** (`context/`) — Observes the codebase via 4-layer retrieval (Working, Episodic, Semantic, Retrieval), PageRank scoring, and dynamic token budgets.",
517
+ );
518
+ lines.push(
519
+ "2. **Prompt Engine** (`prompts/`) — Learns from project conventions via constitution loading, custom prompts, versioning, and A/B-tested evolution.",
520
+ );
521
+ lines.push(
522
+ "3. **Verify Engine** (`verify/`) — Verifies AI-generated code via a multi-stage pipeline: syntax guard, parallel tools, diff filter, AI fix, and two-stage review.",
523
+ );
524
+ lines.push("");
525
+
526
+ for (const engine of detected) {
527
+ const files = engineFiles[engine] ?? [];
528
+ lines.push(`## ${engine.charAt(0).toUpperCase() + engine.slice(1)} Engine`);
529
+ lines.push("");
530
+ if (files.length > 0) {
531
+ lines.push(`Source files (\`packages/core/src/${engine}/\`):`);
532
+ lines.push("");
533
+ for (const file of files) {
534
+ lines.push(`- \`${file}\``);
535
+ }
536
+ } else {
537
+ lines.push("_No source files detected._");
538
+ }
539
+ lines.push("");
540
+ }
541
+
542
+ return {
543
+ slug: "three-engines",
544
+ title: "Three Engines",
545
+ content: lines.join("\n"),
546
+ };
547
+ }
548
+
549
+ /**
550
+ * Describe the monorepo structure by inspecting packages/ layout.
551
+ */
552
+ function generateMonorepoArticle(repoRoot: string): ArchitectureArticle | null {
553
+ const packagesDir = join(repoRoot, "packages");
554
+ let packageNames: string[];
555
+ try {
556
+ packageNames = readdirSync(packagesDir).filter((name) => {
557
+ try {
558
+ return statSync(join(packagesDir, name)).isDirectory();
559
+ } catch {
560
+ return false;
561
+ }
562
+ });
563
+ } catch {
564
+ return null;
565
+ }
566
+
567
+ if (packageNames.length === 0) return null;
568
+
569
+ const descriptions: Record<string, string> = {
570
+ cli: "Commander entrypoint, commands (thin wrappers over engines), terminal UI",
571
+ core: "Three engines + cache + AI + git + DB + hooks",
572
+ mcp: "MCP server (delegates to engines)",
573
+ skills: "Cross-platform skills (Claude Code, Cursor, Codex, Gemini CLI)",
574
+ docs: "Documentation site",
575
+ };
576
+
577
+ const lines: string[] = [];
578
+ lines.push("# Architecture: Monorepo Structure");
579
+ lines.push("");
580
+ lines.push(
581
+ "> Auto-generated architecture article describing the monorepo layout.",
582
+ );
583
+ lines.push("");
584
+ lines.push("Maina is organized as a monorepo under `packages/`.");
585
+ lines.push("");
586
+ lines.push("## Packages");
587
+ lines.push("");
588
+
589
+ for (const name of packageNames.sort()) {
590
+ const desc = descriptions[name] ?? "_No description available._";
591
+ lines.push(`### ${name}`);
592
+ lines.push("");
593
+ lines.push(`- **Path:** \`packages/${name}/\``);
594
+ lines.push(`- **Description:** ${desc}`);
595
+
596
+ // List top-level src files if present
597
+ const srcDir = join(packagesDir, name, "src");
598
+ try {
599
+ const srcEntries = readdirSync(srcDir).filter((e) => {
600
+ try {
601
+ return statSync(join(srcDir, e)).isDirectory();
602
+ } catch {
603
+ return false;
604
+ }
605
+ });
606
+ if (srcEntries.length > 0) {
607
+ lines.push(`- **Modules:** ${srcEntries.sort().join(", ")}`);
608
+ }
609
+ } catch {
610
+ // no src directory
611
+ }
612
+ lines.push("");
613
+ }
614
+
615
+ return {
616
+ slug: "monorepo-structure",
617
+ title: "Monorepo Structure",
618
+ content: lines.join("\n"),
619
+ };
620
+ }
621
+
622
+ /**
623
+ * List all verify tools detected from the verify/ directory.
624
+ */
625
+ function generateVerifyPipelineArticle(
626
+ repoRoot: string,
627
+ ): ArchitectureArticle | null {
628
+ const verifyDir = join(repoRoot, "packages", "core", "src", "verify");
629
+ let verifyFiles: string[];
630
+ try {
631
+ verifyFiles = readdirSync(verifyDir).filter(
632
+ (f) =>
633
+ f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts"),
634
+ );
635
+ } catch {
636
+ return null;
637
+ }
638
+
639
+ if (verifyFiles.length === 0) return null;
640
+
641
+ // Also check for linters subdirectory
642
+ let linterFiles: string[] = [];
643
+ try {
644
+ linterFiles = readdirSync(join(verifyDir, "linters")).filter(
645
+ (f) =>
646
+ f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts"),
647
+ );
648
+ } catch {
649
+ // no linters directory
650
+ }
651
+
652
+ // Also check for tools subdirectory
653
+ let toolFiles: string[] = [];
654
+ try {
655
+ toolFiles = readdirSync(join(verifyDir, "tools")).filter(
656
+ (f) =>
657
+ f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts"),
658
+ );
659
+ } catch {
660
+ // no tools directory
661
+ }
662
+
663
+ const toolDescriptions: Record<string, string> = {
664
+ "syntax-guard.ts":
665
+ "Fast syntax checking (<500ms) via language-specific linters",
666
+ "slop.ts": "AI slop detection — catches lazy/generic AI output patterns",
667
+ "semgrep.ts": "Static analysis via Semgrep rules",
668
+ "trivy.ts": "Container and dependency vulnerability scanning",
669
+ "secretlint.ts": "Secret detection in code and config files",
670
+ "sonar.ts": "SonarQube code quality analysis",
671
+ "coverage.ts": "Code coverage tracking via diff-cover",
672
+ "mutation.ts": "Mutation testing via Stryker",
673
+ "ai-review.ts": "Two-stage AI review (spec compliance + code quality)",
674
+ "diff-filter.ts":
675
+ "Diff-only filter — only report findings on changed lines",
676
+ "fix.ts": "AI-powered automatic fix suggestions",
677
+ "pipeline.ts": "Verification pipeline orchestrator",
678
+ "builtin.ts": "Built-in verification checks",
679
+ "consistency.ts": "Code consistency analysis",
680
+ "typecheck.ts": "TypeScript type checking",
681
+ "detect.ts": "Language and tool detection",
682
+ "proof.ts": "Verification proof generation for PR bodies",
683
+ "visual.ts": "Visual verification with Playwright",
684
+ "lighthouse.ts": "Lighthouse performance audits",
685
+ "zap.ts": "OWASP ZAP security scanning",
686
+ };
687
+
688
+ const lines: string[] = [];
689
+ lines.push("# Architecture: Verification Pipeline");
690
+ lines.push("");
691
+ lines.push("> Auto-generated architecture article listing all verify tools.");
692
+ lines.push("");
693
+ lines.push(
694
+ "The verification pipeline runs a multi-stage process to prove AI-generated code is correct before it merges.",
695
+ );
696
+ lines.push("");
697
+ lines.push("## Pipeline Stages");
698
+ lines.push("");
699
+ lines.push("1. **Syntax Guard** — Fast linting (<500ms)");
700
+ lines.push(
701
+ "2. **Parallel Deterministic Tools** — Semgrep, Trivy, Secretlint, SonarQube, coverage, mutation",
702
+ );
703
+ lines.push("3. **Diff-Only Filter** — Only report findings on changed lines");
704
+ lines.push("4. **AI Fix** — Automatic fix suggestions");
705
+ lines.push("5. **Two-Stage AI Review** — Spec compliance, then code quality");
706
+ lines.push("");
707
+ lines.push("## Verify Tools");
708
+ lines.push("");
709
+
710
+ for (const file of verifyFiles.sort()) {
711
+ const name = file.replace(/\.ts$/, "");
712
+ const desc = toolDescriptions[file] ?? "";
713
+ if (desc) {
714
+ lines.push(`- **${name}** — ${desc}`);
715
+ } else {
716
+ lines.push(`- **${name}** — \`verify/${file}\``);
717
+ }
718
+ }
719
+ lines.push("");
720
+
721
+ if (linterFiles.length > 0) {
722
+ lines.push("## Language-Specific Linters");
723
+ lines.push("");
724
+ for (const file of linterFiles.sort()) {
725
+ const name = file.replace(/\.ts$/, "");
726
+ lines.push(`- **${name}** — \`verify/linters/${file}\``);
727
+ }
728
+ lines.push("");
729
+ }
730
+
731
+ if (toolFiles.length > 0) {
732
+ lines.push("## Additional Tools");
733
+ lines.push("");
734
+ for (const file of toolFiles.sort()) {
735
+ const name = file.replace(/\.ts$/, "");
736
+ lines.push(`- **${name}** — \`verify/tools/${file}\``);
737
+ }
738
+ lines.push("");
739
+ }
740
+
741
+ return {
742
+ slug: "verification-pipeline",
743
+ title: "Verification Pipeline",
744
+ content: lines.join("\n"),
745
+ };
746
+ }
747
+
748
+ /**
749
+ * Generate architecture articles by detecting cross-cutting patterns
750
+ * from the codebase structure.
751
+ */
752
+ function generateArchitectureArticles(repoRoot: string): WikiArticle[] {
753
+ const articles: WikiArticle[] = [];
754
+ const generators = [
755
+ generateThreeEnginesArticle,
756
+ generateMonorepoArticle,
757
+ generateVerifyPipelineArticle,
758
+ ];
759
+
760
+ for (const generator of generators) {
761
+ const result = generator(repoRoot);
762
+ if (result) {
763
+ const articlePath = `wiki/architecture/${result.slug}.md`;
764
+ articles.push(
765
+ makeArticle(
766
+ articlePath,
767
+ "architecture",
768
+ result.title,
769
+ result.content,
770
+ 0.5,
771
+ [],
772
+ [],
773
+ ),
774
+ );
775
+ }
776
+ }
777
+
778
+ return articles;
779
+ }
780
+
781
+ // ─── Article Factory ────────────────────────────────────────────────────
782
+
783
+ function makeArticle(
784
+ path: string,
785
+ type: ArticleType,
786
+ title: string,
787
+ content: string,
788
+ pageRank: number,
789
+ forwardLinks: WikiLink[],
790
+ backlinksForArticle: WikiLink[],
791
+ ): WikiArticle {
792
+ return {
793
+ path,
794
+ type,
795
+ title,
796
+ content,
797
+ contentHash: hashContent(content),
798
+ sourceHashes: [],
799
+ backlinks: backlinksForArticle,
800
+ forwardLinks,
801
+ pageRank,
802
+ lastCompiled: new Date().toISOString(),
803
+ referenceCount: forwardLinks.length + backlinksForArticle.length,
804
+ ebbinghausScore: 1.0,
805
+ };
806
+ }
807
+
808
+ // ─── Public API ──────────────────────────────────────────────────────────
809
+
810
+ /**
811
+ * Run the full wiki compilation pipeline.
812
+ * Returns a Result containing all compiled articles, the knowledge graph, and stats.
813
+ */
814
+ export async function compile(
815
+ options: CompileOptions,
816
+ ): Promise<Result<CompilationResult>> {
817
+ const start = Date.now();
818
+ const { repoRoot, mainaDir, wikiDir, dryRun } = options;
819
+
820
+ try {
821
+ // ── Step 1: Run extractors ──────────────────────────────────────
822
+ const sourceFiles = findSourceFiles(repoRoot, repoRoot);
823
+
824
+ const entityResult = extractCodeEntities(repoRoot, sourceFiles);
825
+ const codeEntities: CodeEntity[] = entityResult.ok
826
+ ? entityResult.value
827
+ : [];
828
+
829
+ const featuresDir = join(mainaDir, "features");
830
+ const featuresResult = extractFeatures(featuresDir);
831
+ const features: ExtractedFeature[] = featuresResult.ok
832
+ ? featuresResult.value
833
+ : [];
834
+
835
+ const adrDir = join(repoRoot, "adr");
836
+ const decisionsResult = extractDecisions(adrDir);
837
+ const decisions: ExtractedDecision[] = decisionsResult.ok
838
+ ? decisionsResult.value
839
+ : [];
840
+
841
+ const workflowResult = extractWorkflowTrace(mainaDir);
842
+ const traces: ExtractedWorkflowTrace[] = workflowResult.ok
843
+ ? [workflowResult.value]
844
+ : [];
845
+
846
+ // ── Step 2: Build knowledge graph ──────────────────────────────
847
+ const graph = buildKnowledgeGraph(
848
+ codeEntities,
849
+ features,
850
+ decisions,
851
+ traces,
852
+ );
853
+
854
+ // ── Step 3: Louvain community detection ────────────────────────
855
+ const louvainResult = detectCommunities(graph.adjacency);
856
+
857
+ // ── Step 4: Compute PageRank ───────────────────────────────────
858
+ const pageRankScores = computePageRank(graph);
859
+
860
+ // ── Step 5: Map nodes to article paths ─────────────────────────
861
+ const articleMap = mapToArticles(graph, louvainResult.communities);
862
+
863
+ // ── Step 6: Generate template-based articles ───────────────────
864
+ const articles: WikiArticle[] = [];
865
+
866
+ // Module articles (from Louvain communities)
867
+ for (const [commId, members] of louvainResult.communities) {
868
+ const moduleNodes = members.filter(
869
+ (m) => graph.nodes.get(m)?.type === "module",
870
+ );
871
+ const moduleName =
872
+ moduleNodes.length > 0
873
+ ? (graph.nodes.get(moduleNodes[0] ?? "")?.label ??
874
+ `cluster-${commId}`)
875
+ : `cluster-${commId}`;
876
+
877
+ const memberEntities = codeEntities.filter((e) =>
878
+ members.some((m) => m === `entity:${e.name}`),
879
+ );
880
+
881
+ const content = generateModuleArticle(
882
+ moduleName,
883
+ memberEntities,
884
+ features,
885
+ decisions,
886
+ pageRankScores,
887
+ );
888
+
889
+ const safeName = moduleName.replace(/[^a-zA-Z0-9_-]/g, "-");
890
+ const articlePath = `wiki/modules/${safeName}.md`;
891
+ const maxPR = Math.max(
892
+ 0,
893
+ ...memberEntities.map(
894
+ (e) => pageRankScores.get(`entity:${e.name}`) ?? 0,
895
+ ),
896
+ );
897
+
898
+ articles.push(
899
+ makeArticle(articlePath, "module", moduleName, content, maxPR, [], []),
900
+ );
901
+ }
902
+
903
+ // Entity articles (top 20% by PageRank)
904
+ const entityNodes = [...graph.nodes.entries()]
905
+ .filter(([, node]) => node.type === "entity")
906
+ .sort(([, a], [, b]) => b.pageRank - a.pageRank);
907
+
908
+ const top20Count = Math.max(1, Math.ceil(entityNodes.length * 0.2));
909
+ const topEntities = entityNodes.slice(0, top20Count);
910
+
911
+ for (const [, node] of topEntities) {
912
+ const entity = codeEntities.find((e) => e.name === node.label);
913
+ if (!entity) continue;
914
+
915
+ const content = generateEntityArticle(entity, features, decisions, graph);
916
+ const safeName = entity.name.replace(/[^a-zA-Z0-9_-]/g, "-");
917
+ const articlePath = `wiki/entities/${safeName}.md`;
918
+
919
+ articles.push(
920
+ makeArticle(
921
+ articlePath,
922
+ "entity",
923
+ entity.name,
924
+ content,
925
+ node.pageRank,
926
+ [],
927
+ [],
928
+ ),
929
+ );
930
+ }
931
+
932
+ // Feature articles — use feature.id as filename to match [[feature:id]] wikilinks
933
+ for (const feature of features) {
934
+ const content = generateFeatureArticle(feature);
935
+ const articlePath = `wiki/features/${feature.id}.md`;
936
+ const featureNode = graph.nodes.get(`feature:${feature.id}`);
937
+
938
+ articles.push(
939
+ makeArticle(
940
+ articlePath,
941
+ "feature",
942
+ feature.title || feature.id,
943
+ content,
944
+ featureNode?.pageRank ?? 0,
945
+ [],
946
+ [],
947
+ ),
948
+ );
949
+ }
950
+
951
+ // Decision articles — use decision.id as filename to match [[decision:id]] wikilinks
952
+ for (const decision of decisions) {
953
+ const content = generateDecisionArticle(decision);
954
+ const articlePath = `wiki/decisions/${decision.id}.md`;
955
+ const decisionNode = graph.nodes.get(`decision:${decision.id}`);
956
+
957
+ articles.push(
958
+ makeArticle(
959
+ articlePath,
960
+ "decision",
961
+ decision.title || decision.id,
962
+ content,
963
+ decisionNode?.pageRank ?? 0,
964
+ [],
965
+ [],
966
+ ),
967
+ );
968
+ }
969
+
970
+ // Architecture articles (from directory structure analysis)
971
+ const archArticles = generateArchitectureArticles(repoRoot);
972
+ articles.push(...archArticles);
973
+
974
+ // ── Step 6b: AI enhancement (optional) ────────────────────────
975
+ if (options.useAI) {
976
+ const contextSummary = [
977
+ `Repository: ${repoRoot}`,
978
+ `Entities: ${codeEntities.length}`,
979
+ `Features: ${features.length}`,
980
+ `Decisions: ${decisions.length}`,
981
+ ].join("\n");
982
+
983
+ for (const article of articles) {
984
+ // Only enhance module and entity articles — features/decisions are already rich
985
+ if (article.type === "module" || article.type === "entity") {
986
+ const enhanced = await enhanceWithAI(
987
+ article,
988
+ contextSummary,
989
+ mainaDir,
990
+ );
991
+ if (enhanced !== article.content) {
992
+ article.content = enhanced;
993
+ article.contentHash = hashContent(enhanced);
994
+ }
995
+ }
996
+ }
997
+ }
998
+
999
+ // ── Step 7: Generate wikilinks ─────────────────────────────────
1000
+ const linkResult = generateLinks(graph, articleMap);
1001
+
1002
+ // Apply links to articles
1003
+ for (const article of articles) {
1004
+ article.forwardLinks = linkResult.forwardLinks.get(article.path) ?? [];
1005
+ article.backlinks = linkResult.backlinks.get(article.path) ?? [];
1006
+ article.referenceCount =
1007
+ article.forwardLinks.length + article.backlinks.length;
1008
+ }
1009
+
1010
+ // ── Step 8: Generate index.md ──────────────────────────────────
1011
+ const indexContent = generateIndex(articles);
1012
+ articles.push(
1013
+ makeArticle(
1014
+ "wiki/index.md",
1015
+ "architecture",
1016
+ "Wiki Index",
1017
+ indexContent,
1018
+ 1.0,
1019
+ [],
1020
+ [],
1021
+ ),
1022
+ );
1023
+
1024
+ // ── Step 9: Write to disk (unless dry run) ─────────────────────
1025
+ if (!dryRun) {
1026
+ mkdirSync(wikiDir, { recursive: true });
1027
+
1028
+ for (const article of articles) {
1029
+ const fullPath = join(wikiDir, article.path.replace(/^wiki\//, ""));
1030
+ mkdirSync(dirname(fullPath), { recursive: true });
1031
+ writeFileSync(fullPath, article.content);
1032
+ }
1033
+ }
1034
+
1035
+ // ── Step 10: Save state ────────────────────────────────────────
1036
+ const state = loadState(wikiDir) ?? createEmptyState();
1037
+ state.lastFullCompile = new Date().toISOString();
1038
+ state.lastIncrementalCompile = new Date().toISOString();
1039
+
1040
+ for (const article of articles) {
1041
+ state.articleHashes[article.path] = article.contentHash;
1042
+ }
1043
+
1044
+ if (!dryRun) {
1045
+ saveState(wikiDir, state);
1046
+ }
1047
+
1048
+ // ── Build stats ────────────────────────────────────────────────
1049
+ const stats = {
1050
+ modules: articles.filter((a) => a.type === "module").length,
1051
+ entities: articles.filter((a) => a.type === "entity").length,
1052
+ features: articles.filter((a) => a.type === "feature").length,
1053
+ decisions: articles.filter((a) => a.type === "decision").length,
1054
+ architecture: articles.filter((a) => a.type === "architecture").length,
1055
+ };
1056
+
1057
+ const duration = Date.now() - start;
1058
+
1059
+ return {
1060
+ ok: true,
1061
+ value: {
1062
+ articles,
1063
+ graph,
1064
+ state,
1065
+ duration,
1066
+ stats,
1067
+ },
1068
+ };
1069
+ } catch (e) {
1070
+ return {
1071
+ ok: false,
1072
+ error: e instanceof Error ? e.message : String(e),
1073
+ };
1074
+ }
1075
+ }