@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,144 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdirSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { getFeedbackDb } from "../../db/index";
5
+ import { recordFeedback } from "../collector";
6
+ import { emitAcceptSignal, emitRejectSignal } from "../signals";
7
+
8
+ let tmpDir: string;
9
+
10
+ beforeEach(() => {
11
+ tmpDir = join(
12
+ import.meta.dir,
13
+ `tmp-signals-${Date.now()}-${Math.random().toString(36).slice(2)}`,
14
+ );
15
+ mkdirSync(tmpDir, { recursive: true });
16
+ });
17
+
18
+ afterEach(() => {
19
+ try {
20
+ rmSync(tmpDir, { recursive: true, force: true });
21
+ } catch {
22
+ /* ignore */
23
+ }
24
+ });
25
+
26
+ /** Insert a feedback row and set its workflow_id. */
27
+ function insertFeedback(
28
+ tool: string,
29
+ workflowId: string,
30
+ accepted: boolean,
31
+ ): void {
32
+ recordFeedback(tmpDir, {
33
+ promptHash: `${tool}-mcp`,
34
+ task: tool,
35
+ accepted,
36
+ timestamp: new Date().toISOString(),
37
+ });
38
+ const dbResult = getFeedbackDb(tmpDir);
39
+ if (!dbResult.ok) return;
40
+ const { db } = dbResult.value;
41
+ db.prepare(
42
+ `UPDATE feedback SET workflow_id = ?
43
+ WHERE id = (SELECT id FROM feedback ORDER BY rowid DESC LIMIT 1)`,
44
+ ).run(workflowId);
45
+ }
46
+
47
+ describe("emitAcceptSignal", () => {
48
+ test("marks recent review/verify entries as accepted", () => {
49
+ insertFeedback("reviewCode", "wf-1", false);
50
+ insertFeedback("verify", "wf-1", false);
51
+
52
+ emitAcceptSignal(tmpDir, "wf-1");
53
+
54
+ const dbResult = getFeedbackDb(tmpDir);
55
+ expect(dbResult.ok).toBe(true);
56
+ if (!dbResult.ok) return;
57
+ const { db } = dbResult.value;
58
+ const rows = db
59
+ .query("SELECT command, accepted FROM feedback WHERE workflow_id = ?")
60
+ .all("wf-1") as Array<{ command: string; accepted: number }>;
61
+ for (const row of rows) {
62
+ expect(row.accepted).toBe(1);
63
+ }
64
+ });
65
+
66
+ test("does not affect entries from a different workflow", () => {
67
+ insertFeedback("reviewCode", "wf-1", false);
68
+ insertFeedback("reviewCode", "wf-2", false);
69
+
70
+ emitAcceptSignal(tmpDir, "wf-1");
71
+
72
+ const dbResult = getFeedbackDb(tmpDir);
73
+ if (!dbResult.ok) return;
74
+ const { db } = dbResult.value;
75
+ const wf2 = db
76
+ .query("SELECT accepted FROM feedback WHERE workflow_id = ?")
77
+ .get("wf-2") as { accepted: number } | null;
78
+ expect(wf2?.accepted).toBe(0);
79
+ });
80
+
81
+ test("accepts custom tool list", () => {
82
+ insertFeedback("reviewCode", "wf-1", false);
83
+ insertFeedback("verify", "wf-1", false);
84
+
85
+ emitAcceptSignal(tmpDir, "wf-1", ["reviewCode"]);
86
+
87
+ const dbResult = getFeedbackDb(tmpDir);
88
+ if (!dbResult.ok) return;
89
+ const { db } = dbResult.value;
90
+ const review = db
91
+ .query(
92
+ "SELECT accepted FROM feedback WHERE command = ? AND workflow_id = ?",
93
+ )
94
+ .get("reviewCode", "wf-1") as { accepted: number } | null;
95
+ expect(review?.accepted).toBe(1);
96
+ const verify = db
97
+ .query(
98
+ "SELECT accepted FROM feedback WHERE command = ? AND workflow_id = ?",
99
+ )
100
+ .get("verify", "wf-1") as { accepted: number } | null;
101
+ expect(verify?.accepted).toBe(0);
102
+ });
103
+ });
104
+
105
+ describe("emitRejectSignal", () => {
106
+ test("marks the most recent entry for a tool+workflow as rejected", () => {
107
+ insertFeedback("reviewCode", "wf-1", true);
108
+ emitRejectSignal(tmpDir, "reviewCode", "wf-1");
109
+
110
+ const dbResult = getFeedbackDb(tmpDir);
111
+ if (!dbResult.ok) return;
112
+ const { db } = dbResult.value;
113
+ const row = db
114
+ .query(
115
+ "SELECT accepted FROM feedback WHERE command = ? AND workflow_id = ? ORDER BY created_at DESC LIMIT 1",
116
+ )
117
+ .get("reviewCode", "wf-1") as { accepted: number } | null;
118
+ expect(row?.accepted).toBe(0);
119
+ });
120
+
121
+ test("only rejects the most recent entry, not older ones", () => {
122
+ insertFeedback("reviewCode", "wf-1", true);
123
+ insertFeedback("reviewCode", "wf-1", true);
124
+ emitRejectSignal(tmpDir, "reviewCode", "wf-1");
125
+
126
+ const dbResult = getFeedbackDb(tmpDir);
127
+ if (!dbResult.ok) return;
128
+ const { db } = dbResult.value;
129
+ const rows = db
130
+ .query(
131
+ "SELECT accepted FROM feedback WHERE command = ? AND workflow_id = ? ORDER BY created_at ASC",
132
+ )
133
+ .all("reviewCode", "wf-1") as Array<{ accepted: number }>;
134
+ expect(rows).toHaveLength(2);
135
+ expect(rows[0]?.accepted).toBe(1);
136
+ expect(rows[1]?.accepted).toBe(0);
137
+ });
138
+
139
+ test("does not throw when no matching entries exist", () => {
140
+ expect(() => {
141
+ emitRejectSignal(tmpDir, "reviewCode", "nonexistent");
142
+ }).not.toThrow();
143
+ });
144
+ });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * MCP result capture — connects tool outputs to cache, feedback, and stats.
3
+ * This is the core of the round-trip flywheel.
4
+ */
5
+
6
+ import { hashContent } from "../cache/keys";
7
+ import { createCacheManager } from "../cache/manager";
8
+ import { trackToolUsage } from "../stats/tracker";
9
+ import { recordFeedback } from "./collector";
10
+ import { emitRejectSignal } from "./signals";
11
+
12
+ export interface CaptureInput {
13
+ tool: string;
14
+ input: Record<string, unknown>;
15
+ output: string;
16
+ promptHash?: string;
17
+ durationMs: number;
18
+ mainaDir: string;
19
+ workflowId?: string;
20
+ }
21
+
22
+ export function buildToolCacheKey(
23
+ tool: string,
24
+ input: Record<string, unknown>,
25
+ ): string {
26
+ const inputHash = hashContent(JSON.stringify(input));
27
+ return hashContent(`mcp:${tool}:${inputHash}`);
28
+ }
29
+
30
+ export function getCachedResult(
31
+ tool: string,
32
+ input: Record<string, unknown>,
33
+ mainaDir: string,
34
+ ): string | null {
35
+ try {
36
+ const cache = createCacheManager(mainaDir);
37
+ const key = buildToolCacheKey(tool, input);
38
+ const entry = cache.get(key);
39
+ if (entry !== null) {
40
+ const inputHash = hashContent(JSON.stringify(input));
41
+ trackToolUsage(mainaDir, {
42
+ tool,
43
+ inputHash,
44
+ durationMs: 0,
45
+ cacheHit: true,
46
+ });
47
+ return entry.value;
48
+ }
49
+ return null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ export function captureResult(input: CaptureInput): void {
56
+ const inputHash = hashContent(JSON.stringify(input.input));
57
+
58
+ // 1. Cache (synchronous — fast SQLite write)
59
+ try {
60
+ const cache = createCacheManager(input.mainaDir);
61
+ const key = buildToolCacheKey(input.tool, input.input);
62
+ cache.set(key, input.output, { ttl: 0 });
63
+ } catch {
64
+ // Cache failure is non-fatal
65
+ }
66
+
67
+ // 2. Reject previous result for same tool+workflow (re-run implies rejection)
68
+ if (input.workflowId) {
69
+ try {
70
+ emitRejectSignal(input.mainaDir, input.tool, input.workflowId);
71
+ } catch {
72
+ // Reject failure is non-fatal
73
+ }
74
+ }
75
+
76
+ // 3. Feedback (fire-and-forget via microtask)
77
+ queueMicrotask(() => {
78
+ try {
79
+ recordFeedback(input.mainaDir, {
80
+ promptHash: input.promptHash ?? `${input.tool}-mcp`,
81
+ task: input.tool,
82
+ accepted: true,
83
+ timestamp: new Date().toISOString(),
84
+ });
85
+ } catch {
86
+ // Never throw from background feedback
87
+ }
88
+ });
89
+
90
+ // 4. Stats (synchronous — fast SQLite write)
91
+ try {
92
+ trackToolUsage(input.mainaDir, {
93
+ tool: input.tool,
94
+ inputHash,
95
+ durationMs: input.durationMs,
96
+ cacheHit: false,
97
+ workflowId: input.workflowId,
98
+ });
99
+ } catch {
100
+ // Stats failure is non-fatal
101
+ }
102
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Implicit accept/reject signals for the RL flywheel.
3
+ * Infers outcomes from downstream user behavior instead of requiring explicit action.
4
+ */
5
+
6
+ import { join } from "node:path";
7
+ import { getFeedbackDb } from "../db/index";
8
+ import { recordWikiUsage } from "../wiki/signals";
9
+
10
+ const DEFAULT_ACCEPT_TOOLS = ["reviewCode", "verify", "checkSlop"];
11
+
12
+ export function emitAcceptSignal(
13
+ mainaDir: string,
14
+ workflowId: string,
15
+ tools?: string[],
16
+ wikiArticles?: string[],
17
+ ): void {
18
+ try {
19
+ const dbResult = getFeedbackDb(mainaDir);
20
+ if (!dbResult.ok) return;
21
+ const { db } = dbResult.value;
22
+ const targetTools = tools ?? DEFAULT_ACCEPT_TOOLS;
23
+ const placeholders = targetTools.map(() => "?").join(",");
24
+ db.prepare(
25
+ `UPDATE feedback SET accepted = 1
26
+ WHERE workflow_id = ?
27
+ AND command IN (${placeholders})`,
28
+ ).run(workflowId, ...targetTools);
29
+
30
+ // Record wiki effectiveness signal for loaded articles
31
+ if (wikiArticles && wikiArticles.length > 0) {
32
+ const wikiDir = join(mainaDir, "wiki");
33
+ recordWikiUsage(wikiDir, wikiArticles, "accept", true);
34
+ }
35
+ } catch {
36
+ // Never throw from signals
37
+ }
38
+ }
39
+
40
+ export function emitRejectSignal(
41
+ mainaDir: string,
42
+ tool: string,
43
+ workflowId: string,
44
+ wikiArticles?: string[],
45
+ ): void {
46
+ try {
47
+ const dbResult = getFeedbackDb(mainaDir);
48
+ if (!dbResult.ok) return;
49
+ const { db } = dbResult.value;
50
+ db.prepare(
51
+ `UPDATE feedback SET accepted = 0
52
+ WHERE command = ? AND workflow_id = ?
53
+ AND id = (
54
+ SELECT id FROM feedback
55
+ WHERE command = ? AND workflow_id = ?
56
+ ORDER BY created_at DESC, rowid DESC LIMIT 1
57
+ )`,
58
+ ).run(tool, workflowId, tool, workflowId);
59
+
60
+ // Record wiki effectiveness signal for loaded articles
61
+ if (wikiArticles && wikiArticles.length > 0) {
62
+ const wikiDir = join(mainaDir, "wiki");
63
+ recordWikiUsage(wikiDir, wikiArticles, tool, false);
64
+ }
65
+ } catch {
66
+ // Never throw from signals
67
+ }
68
+ }
package/src/index.ts CHANGED
@@ -154,6 +154,12 @@ export type {
154
154
  TraceDeps,
155
155
  } from "./features/traceability";
156
156
  export { traceFeature } from "./features/traceability";
157
+ export {
158
+ buildToolCacheKey,
159
+ type CaptureInput,
160
+ captureResult,
161
+ getCachedResult,
162
+ } from "./feedback/capture";
157
163
  // Feedback
158
164
  export {
159
165
  type FeedbackRecord,
@@ -176,6 +182,7 @@ export {
176
182
  type RulePreference,
177
183
  savePreferences,
178
184
  } from "./feedback/preferences";
185
+ export { emitAcceptSignal, emitRejectSignal } from "./feedback/signals";
179
186
  export {
180
187
  exportEpisodicForCloud,
181
188
  exportFeedbackForCloud,
@@ -287,12 +294,16 @@ export {
287
294
  getLatest,
288
295
  getSkipRate,
289
296
  getStats,
297
+ getToolUsageStats,
290
298
  getTrends,
291
299
  recordSnapshot,
292
300
  type SnapshotInput,
293
301
  type StatsReport,
302
+ type ToolUsageInput,
303
+ type ToolUsageStats,
294
304
  type TrendDirection,
295
305
  type TrendsReport,
306
+ trackToolUsage,
296
307
  } from "./stats/tracker";
297
308
  // Ticket
298
309
  export {
@@ -427,8 +438,101 @@ export {
427
438
  type ZapOptions,
428
439
  type ZapResult,
429
440
  } from "./verify/zap";
441
+ // Wiki — Compiler
442
+ export {
443
+ type CompilationResult as WikiCompilationResult,
444
+ type CompileOptions as WikiCompileOptions,
445
+ compile as compileWiki,
446
+ } from "./wiki/compiler";
447
+ export { type CodeEntity, extractCodeEntities } from "./wiki/extractors/code";
448
+ export {
449
+ extractDecisions,
450
+ extractSingleDecision,
451
+ } from "./wiki/extractors/decision";
452
+ export {
453
+ extractFeatures,
454
+ extractSingleFeature,
455
+ } from "./wiki/extractors/feature";
456
+ export { extractWorkflowTrace } from "./wiki/extractors/workflow";
457
+ // Wiki — Graph
458
+ export {
459
+ buildKnowledgeGraph,
460
+ computePageRank,
461
+ type GraphEdge,
462
+ type GraphNode,
463
+ type KnowledgeGraph,
464
+ mapToArticles,
465
+ } from "./wiki/graph";
466
+ // Wiki — Indexer
467
+ export { generateIndex } from "./wiki/indexer";
468
+ // Wiki — Linker
469
+ export { generateLinks, type LinkResult } from "./wiki/linker";
470
+ // Wiki — Louvain
471
+ export {
472
+ detectCommunities,
473
+ type LouvainNode,
474
+ type LouvainResult,
475
+ } from "./wiki/louvain";
476
+ // Wiki — Query
477
+ export {
478
+ queryWiki,
479
+ type WikiQueryOptions,
480
+ type WikiQueryResult,
481
+ } from "./wiki/query";
482
+ export {
483
+ DEFAULT_SCHEMA,
484
+ getArticleMaxLength,
485
+ getLinkSyntax,
486
+ validateArticleStructure,
487
+ type WikiSchema,
488
+ } from "./wiki/schema";
489
+ // Wiki — Signals
490
+ export {
491
+ type ArticleLoadSignal,
492
+ type CompilationPromptSignal,
493
+ calculateEbbinghausScore,
494
+ getPromptEffectiveness,
495
+ getWikiEffectivenessReport,
496
+ recordArticlesLoaded,
497
+ recordWikiUsage,
498
+ type WikiEffectivenessReport,
499
+ type WikiEffectivenessSignal,
500
+ } from "./wiki/signals";
501
+ export {
502
+ createEmptyState,
503
+ getChangedFiles as getWikiChangedFiles,
504
+ hashContent,
505
+ hashFile,
506
+ loadState as loadWikiState,
507
+ saveState as saveWikiState,
508
+ } from "./wiki/state";
509
+ // Wiki — Tracking
510
+ export {
511
+ trackWikiRefsRead,
512
+ trackWikiRefsWritten,
513
+ } from "./wiki/tracking";
514
+ // Wiki
515
+ export type {
516
+ ArticleType,
517
+ DecisionStatus,
518
+ EdgeType,
519
+ ExtractedDecision,
520
+ ExtractedFeature,
521
+ ExtractedWorkflowTrace,
522
+ RLSignal,
523
+ TaskItem,
524
+ WikiArticle,
525
+ WikiLink,
526
+ WikiLintCheck,
527
+ WikiLintFinding,
528
+ WikiLintResult,
529
+ WikiState,
530
+ WorkflowStep as WikiWorkflowStep,
531
+ } from "./wiki/types";
532
+ export { DECAY_HALF_LIVES } from "./wiki/types";
430
533
  // Workflow
431
534
  export {
535
+ appendWikiRefs,
432
536
  appendWorkflowStep,
433
537
  loadWorkflowContext,
434
538
  resetWorkflowContext,