@mainahq/core 0.2.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 (156) hide show
  1. package/README.md +31 -0
  2. package/package.json +37 -0
  3. package/src/ai/__tests__/ai.test.ts +207 -0
  4. package/src/ai/__tests__/design-approaches.test.ts +192 -0
  5. package/src/ai/__tests__/spec-questions.test.ts +191 -0
  6. package/src/ai/__tests__/tiers.test.ts +110 -0
  7. package/src/ai/commit-msg.ts +28 -0
  8. package/src/ai/design-approaches.ts +76 -0
  9. package/src/ai/index.ts +205 -0
  10. package/src/ai/pr-summary.ts +60 -0
  11. package/src/ai/spec-questions.ts +74 -0
  12. package/src/ai/tiers.ts +52 -0
  13. package/src/ai/try-generate.ts +89 -0
  14. package/src/ai/validate.ts +66 -0
  15. package/src/benchmark/__tests__/reporter.test.ts +525 -0
  16. package/src/benchmark/__tests__/runner.test.ts +113 -0
  17. package/src/benchmark/__tests__/story-loader.test.ts +152 -0
  18. package/src/benchmark/reporter.ts +332 -0
  19. package/src/benchmark/runner.ts +91 -0
  20. package/src/benchmark/story-loader.ts +88 -0
  21. package/src/benchmark/types.ts +95 -0
  22. package/src/cache/__tests__/keys.test.ts +97 -0
  23. package/src/cache/__tests__/manager.test.ts +312 -0
  24. package/src/cache/__tests__/ttl.test.ts +94 -0
  25. package/src/cache/keys.ts +44 -0
  26. package/src/cache/manager.ts +231 -0
  27. package/src/cache/ttl.ts +77 -0
  28. package/src/config/__tests__/config.test.ts +376 -0
  29. package/src/config/index.ts +198 -0
  30. package/src/context/__tests__/budget.test.ts +179 -0
  31. package/src/context/__tests__/engine.test.ts +163 -0
  32. package/src/context/__tests__/episodic.test.ts +291 -0
  33. package/src/context/__tests__/relevance.test.ts +323 -0
  34. package/src/context/__tests__/retrieval.test.ts +143 -0
  35. package/src/context/__tests__/selector.test.ts +174 -0
  36. package/src/context/__tests__/semantic.test.ts +252 -0
  37. package/src/context/__tests__/treesitter.test.ts +229 -0
  38. package/src/context/__tests__/working.test.ts +236 -0
  39. package/src/context/budget.ts +130 -0
  40. package/src/context/engine.ts +394 -0
  41. package/src/context/episodic.ts +251 -0
  42. package/src/context/relevance.ts +325 -0
  43. package/src/context/retrieval.ts +325 -0
  44. package/src/context/selector.ts +93 -0
  45. package/src/context/semantic.ts +331 -0
  46. package/src/context/treesitter.ts +216 -0
  47. package/src/context/working.ts +192 -0
  48. package/src/db/__tests__/db.test.ts +151 -0
  49. package/src/db/index.ts +211 -0
  50. package/src/db/schema.ts +84 -0
  51. package/src/design/__tests__/design.test.ts +310 -0
  52. package/src/design/__tests__/generate-hld-lld.test.ts +109 -0
  53. package/src/design/__tests__/review.test.ts +561 -0
  54. package/src/design/index.ts +297 -0
  55. package/src/design/review.ts +327 -0
  56. package/src/explain/__tests__/explain.test.ts +173 -0
  57. package/src/explain/index.ts +181 -0
  58. package/src/features/__tests__/analyzer.test.ts +358 -0
  59. package/src/features/__tests__/checklist.test.ts +454 -0
  60. package/src/features/__tests__/numbering.test.ts +319 -0
  61. package/src/features/__tests__/quality.test.ts +295 -0
  62. package/src/features/__tests__/traceability.test.ts +147 -0
  63. package/src/features/analyzer.ts +445 -0
  64. package/src/features/checklist.ts +366 -0
  65. package/src/features/index.ts +18 -0
  66. package/src/features/numbering.ts +404 -0
  67. package/src/features/quality.ts +349 -0
  68. package/src/features/test-stubs.ts +157 -0
  69. package/src/features/traceability.ts +260 -0
  70. package/src/feedback/__tests__/async-feedback.test.ts +52 -0
  71. package/src/feedback/__tests__/collector.test.ts +219 -0
  72. package/src/feedback/__tests__/compress.test.ts +150 -0
  73. package/src/feedback/__tests__/preferences.test.ts +169 -0
  74. package/src/feedback/collector.ts +135 -0
  75. package/src/feedback/compress.ts +92 -0
  76. package/src/feedback/preferences.ts +108 -0
  77. package/src/git/__tests__/git.test.ts +62 -0
  78. package/src/git/index.ts +110 -0
  79. package/src/hooks/__tests__/runner.test.ts +266 -0
  80. package/src/hooks/index.ts +8 -0
  81. package/src/hooks/runner.ts +130 -0
  82. package/src/index.ts +356 -0
  83. package/src/init/__tests__/init.test.ts +228 -0
  84. package/src/init/index.ts +364 -0
  85. package/src/language/__tests__/detect.test.ts +77 -0
  86. package/src/language/__tests__/profile.test.ts +51 -0
  87. package/src/language/detect.ts +70 -0
  88. package/src/language/profile.ts +110 -0
  89. package/src/prompts/__tests__/defaults.test.ts +52 -0
  90. package/src/prompts/__tests__/engine.test.ts +183 -0
  91. package/src/prompts/__tests__/evolution-resolve.test.ts +169 -0
  92. package/src/prompts/__tests__/evolution.test.ts +187 -0
  93. package/src/prompts/__tests__/loader.test.ts +105 -0
  94. package/src/prompts/candidates/review-v2.md +55 -0
  95. package/src/prompts/defaults/ai-review.md +49 -0
  96. package/src/prompts/defaults/commit.md +30 -0
  97. package/src/prompts/defaults/context.md +26 -0
  98. package/src/prompts/defaults/design-approaches.md +57 -0
  99. package/src/prompts/defaults/design-hld-lld.md +55 -0
  100. package/src/prompts/defaults/design.md +53 -0
  101. package/src/prompts/defaults/explain.md +31 -0
  102. package/src/prompts/defaults/fix.md +32 -0
  103. package/src/prompts/defaults/index.ts +38 -0
  104. package/src/prompts/defaults/review.md +41 -0
  105. package/src/prompts/defaults/spec-questions.md +59 -0
  106. package/src/prompts/defaults/tests.md +72 -0
  107. package/src/prompts/engine.ts +137 -0
  108. package/src/prompts/evolution.ts +409 -0
  109. package/src/prompts/loader.ts +71 -0
  110. package/src/review/__tests__/review.test.ts +288 -0
  111. package/src/review/comprehensive.ts +362 -0
  112. package/src/review/index.ts +417 -0
  113. package/src/stats/__tests__/tracker.test.ts +323 -0
  114. package/src/stats/index.ts +11 -0
  115. package/src/stats/tracker.ts +492 -0
  116. package/src/ticket/__tests__/ticket.test.ts +273 -0
  117. package/src/ticket/index.ts +185 -0
  118. package/src/utils.ts +87 -0
  119. package/src/verify/__tests__/ai-review.test.ts +242 -0
  120. package/src/verify/__tests__/coverage.test.ts +83 -0
  121. package/src/verify/__tests__/detect.test.ts +175 -0
  122. package/src/verify/__tests__/diff-filter.test.ts +338 -0
  123. package/src/verify/__tests__/fix.test.ts +478 -0
  124. package/src/verify/__tests__/linters/clippy.test.ts +45 -0
  125. package/src/verify/__tests__/linters/go-vet.test.ts +27 -0
  126. package/src/verify/__tests__/linters/ruff.test.ts +64 -0
  127. package/src/verify/__tests__/mutation.test.ts +141 -0
  128. package/src/verify/__tests__/pipeline.test.ts +553 -0
  129. package/src/verify/__tests__/proof.test.ts +97 -0
  130. package/src/verify/__tests__/secretlint.test.ts +190 -0
  131. package/src/verify/__tests__/semgrep.test.ts +217 -0
  132. package/src/verify/__tests__/slop.test.ts +366 -0
  133. package/src/verify/__tests__/sonar.test.ts +113 -0
  134. package/src/verify/__tests__/syntax-guard.test.ts +227 -0
  135. package/src/verify/__tests__/trivy.test.ts +191 -0
  136. package/src/verify/__tests__/visual.test.ts +139 -0
  137. package/src/verify/ai-review.ts +276 -0
  138. package/src/verify/coverage.ts +134 -0
  139. package/src/verify/detect.ts +171 -0
  140. package/src/verify/diff-filter.ts +183 -0
  141. package/src/verify/fix.ts +317 -0
  142. package/src/verify/linters/clippy.ts +52 -0
  143. package/src/verify/linters/go-vet.ts +32 -0
  144. package/src/verify/linters/ruff.ts +47 -0
  145. package/src/verify/mutation.ts +143 -0
  146. package/src/verify/pipeline.ts +328 -0
  147. package/src/verify/proof.ts +277 -0
  148. package/src/verify/secretlint.ts +168 -0
  149. package/src/verify/semgrep.ts +170 -0
  150. package/src/verify/slop.ts +493 -0
  151. package/src/verify/sonar.ts +146 -0
  152. package/src/verify/syntax-guard.ts +251 -0
  153. package/src/verify/trivy.ts +161 -0
  154. package/src/verify/visual.ts +460 -0
  155. package/src/workflow/__tests__/context.test.ts +110 -0
  156. package/src/workflow/context.ts +81 -0
@@ -0,0 +1,192 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { getCurrentBranch } from "../git/index";
4
+ import { loadWorkflowContext } from "../workflow/context";
5
+
6
+ // ── Types ────────────────────────────────────────────────────────────────────
7
+
8
+ export interface VerificationResult {
9
+ passed: boolean;
10
+ checks: { name: string; passed: boolean; output?: string }[];
11
+ timestamp: string;
12
+ }
13
+
14
+ export interface WorkingContext {
15
+ branch: string;
16
+ planContent: string | null;
17
+ workflowContext: string | null;
18
+ touchedFiles: string[];
19
+ lastVerification: VerificationResult | null;
20
+ updatedAt: string;
21
+ }
22
+
23
+ // ── Internal helpers ─────────────────────────────────────────────────────────
24
+
25
+ function contextFilePath(mainaDir: string): string {
26
+ return join(mainaDir, "context", "working.json");
27
+ }
28
+
29
+ function freshContext(branch: string): WorkingContext {
30
+ return {
31
+ branch,
32
+ planContent: null,
33
+ workflowContext: null,
34
+ touchedFiles: [],
35
+ lastVerification: null,
36
+ updatedAt: new Date().toISOString(),
37
+ };
38
+ }
39
+
40
+ async function readPlanContent(repoRoot: string): Promise<string | null> {
41
+ try {
42
+ const planPath = join(repoRoot, "PLAN.md");
43
+ const file = Bun.file(planPath);
44
+ if (await file.exists()) {
45
+ return await file.text();
46
+ }
47
+ return null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ // ── Public API ───────────────────────────────────────────────────────────────
54
+
55
+ /**
56
+ * Load the working context from disk.
57
+ *
58
+ * - If no file exists, returns a fresh context for the current branch.
59
+ * - If the saved branch differs from the current git branch, resets to a fresh
60
+ * context (branch switch detected).
61
+ * - Always refreshes planContent from PLAN.md on load.
62
+ */
63
+ export async function loadWorkingContext(
64
+ mainaDir: string,
65
+ repoRoot: string,
66
+ ): Promise<WorkingContext> {
67
+ const currentBranch = await getCurrentBranch(repoRoot);
68
+
69
+ const planContent = await readPlanContent(repoRoot);
70
+ const workflowContext = loadWorkflowContext(mainaDir);
71
+
72
+ try {
73
+ const filePath = contextFilePath(mainaDir);
74
+ const file = Bun.file(filePath);
75
+ if (!(await file.exists())) {
76
+ return { ...freshContext(currentBranch), planContent, workflowContext };
77
+ }
78
+
79
+ const raw = await file.text();
80
+ const saved: WorkingContext = JSON.parse(raw);
81
+
82
+ // Branch switch — discard stale session state
83
+ if (saved.branch !== currentBranch) {
84
+ return { ...freshContext(currentBranch), planContent, workflowContext };
85
+ }
86
+
87
+ // Refresh plan content and workflow context from disk
88
+ return { ...saved, planContent, workflowContext };
89
+ } catch {
90
+ return { ...freshContext(currentBranch), planContent, workflowContext };
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Persist the working context to `.maina/context/working.json`.
96
+ * Creates parent directories as needed. Never throws.
97
+ */
98
+ export async function saveWorkingContext(
99
+ mainaDir: string,
100
+ context: WorkingContext,
101
+ ): Promise<void> {
102
+ try {
103
+ const filePath = contextFilePath(mainaDir);
104
+ mkdirSync(dirname(filePath), { recursive: true });
105
+ // planContent is runtime-only — no need to persist it (we re-read on load)
106
+ // but we DO persist it so round-trip tests work without git side-effects
107
+ const payload = { ...context, updatedAt: new Date().toISOString() };
108
+ await Bun.write(filePath, JSON.stringify(payload, null, 2));
109
+ } catch {
110
+ // Intentionally swallowed — never throw
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Add a file path to the touchedFiles list (deduplicating) and save.
116
+ */
117
+ export async function trackFile(
118
+ mainaDir: string,
119
+ repoRoot: string,
120
+ filePath: string,
121
+ ): Promise<WorkingContext> {
122
+ const ctx = await loadWorkingContext(mainaDir, repoRoot);
123
+ if (!ctx.touchedFiles.includes(filePath)) {
124
+ ctx.touchedFiles = [...ctx.touchedFiles, filePath];
125
+ }
126
+ ctx.updatedAt = new Date().toISOString();
127
+ await saveWorkingContext(mainaDir, ctx);
128
+ return ctx;
129
+ }
130
+
131
+ /**
132
+ * Record the latest verification run result and save.
133
+ */
134
+ export async function setVerificationResult(
135
+ mainaDir: string,
136
+ repoRoot: string,
137
+ result: VerificationResult,
138
+ ): Promise<WorkingContext> {
139
+ const ctx = await loadWorkingContext(mainaDir, repoRoot);
140
+ ctx.lastVerification = result;
141
+ ctx.updatedAt = new Date().toISOString();
142
+ await saveWorkingContext(mainaDir, ctx);
143
+ return ctx;
144
+ }
145
+
146
+ /**
147
+ * Return a brand-new empty WorkingContext without touching disk.
148
+ * The branch field is set to an empty string — caller should fill it in.
149
+ */
150
+ export function resetWorkingContext(mainaDir: string): WorkingContext {
151
+ // mainaDir accepted for API symmetry / future use
152
+ void mainaDir;
153
+ return freshContext("");
154
+ }
155
+
156
+ /**
157
+ * Format the working context as a human/LLM-readable string.
158
+ */
159
+ export function assembleWorkingText(context: WorkingContext): string {
160
+ const lines: string[] = [];
161
+
162
+ lines.push(`Current branch: ${context.branch}`);
163
+ lines.push(`Touched files: ${context.touchedFiles.length}`);
164
+
165
+ if (context.touchedFiles.length > 0) {
166
+ lines.push("Files modified this session:");
167
+ for (const f of context.touchedFiles) {
168
+ lines.push(` - ${f}`);
169
+ }
170
+ }
171
+
172
+ if (context.lastVerification !== null) {
173
+ const v = context.lastVerification;
174
+ const status = v.passed ? "passed" : "failed";
175
+ lines.push(`Last verification: ${status} (${v.timestamp})`);
176
+ for (const check of v.checks) {
177
+ const checkStatus = check.passed ? "pass" : "fail";
178
+ const detail = check.output ? ` — ${check.output}` : "";
179
+ lines.push(` [${checkStatus}] ${check.name}${detail}`);
180
+ }
181
+ }
182
+
183
+ if (context.planContent !== null) {
184
+ lines.push("");
185
+ lines.push("PLAN.md:");
186
+ lines.push(context.planContent);
187
+ }
188
+
189
+ lines.push(`Updated at: ${context.updatedAt}`);
190
+
191
+ return lines.join("\n");
192
+ }
@@ -0,0 +1,151 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { mkdirSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ getCacheDb,
7
+ getContextDb,
8
+ getFeedbackDb,
9
+ initDatabase,
10
+ } from "../index.ts";
11
+
12
+ const TEST_DIR = join(tmpdir(), `maina-test-${Date.now()}`);
13
+
14
+ beforeAll(() => {
15
+ mkdirSync(TEST_DIR, { recursive: true });
16
+ });
17
+
18
+ afterAll(() => {
19
+ rmSync(TEST_DIR, { recursive: true, force: true });
20
+ });
21
+
22
+ describe("initDatabase", () => {
23
+ test("creates a database file on first access", () => {
24
+ const dbPath = join(TEST_DIR, "test.db");
25
+ const result = initDatabase(dbPath);
26
+ expect(result.ok).toBe(true);
27
+ if (!result.ok) return;
28
+ const { db } = result.value;
29
+ expect(db).toBeDefined();
30
+ db.close();
31
+ });
32
+
33
+ test("all expected tables exist after init (context db)", () => {
34
+ const mainaDir = join(TEST_DIR, "tables-test");
35
+ const result = getContextDb(mainaDir);
36
+ expect(result.ok).toBe(true);
37
+ if (!result.ok) return;
38
+ const { db } = result.value;
39
+
40
+ const rows = db
41
+ .prepare(
42
+ "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",
43
+ )
44
+ .all() as Array<{ name: string }>;
45
+ const tableNames = rows.map((r) => r.name);
46
+
47
+ expect(tableNames).toContain("episodic_entries");
48
+ expect(tableNames).toContain("semantic_entities");
49
+ expect(tableNames).toContain("dependency_edges");
50
+ db.close();
51
+ });
52
+
53
+ test("all expected tables exist after init (cache db)", () => {
54
+ const mainaDir = join(TEST_DIR, "cache-tables-test");
55
+ const result = getCacheDb(mainaDir);
56
+ expect(result.ok).toBe(true);
57
+ if (!result.ok) return;
58
+ const { db } = result.value;
59
+
60
+ const rows = db
61
+ .prepare(
62
+ "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",
63
+ )
64
+ .all() as Array<{ name: string }>;
65
+ const tableNames = rows.map((r) => r.name);
66
+
67
+ expect(tableNames).toContain("cache_entries");
68
+ db.close();
69
+ });
70
+
71
+ test("all expected tables exist after init (feedback db)", () => {
72
+ const mainaDir = join(TEST_DIR, "feedback-tables-test");
73
+ const result = getFeedbackDb(mainaDir);
74
+ expect(result.ok).toBe(true);
75
+ if (!result.ok) return;
76
+ const { db } = result.value;
77
+
78
+ const rows = db
79
+ .prepare(
80
+ "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",
81
+ )
82
+ .all() as Array<{ name: string }>;
83
+ const tableNames = rows.map((r) => r.name);
84
+
85
+ expect(tableNames).toContain("feedback");
86
+ expect(tableNames).toContain("prompt_versions");
87
+ db.close();
88
+ });
89
+ });
90
+
91
+ describe("episodic_entries CRUD", () => {
92
+ test("can insert and query episodic_entries", async () => {
93
+ const mainaDir = join(TEST_DIR, "episodic-crud");
94
+ const result = getContextDb(mainaDir);
95
+ expect(result.ok).toBe(true);
96
+ if (!result.ok) return;
97
+ const { drizzle: orm, db } = result.value;
98
+
99
+ const { episodicEntries } = await import("../schema.ts");
100
+
101
+ const id = "test-entry-1";
102
+ await orm.insert(episodicEntries).values({
103
+ id,
104
+ content: "User added auth module",
105
+ summary: "Auth",
106
+ relevance: 0.9,
107
+ accessCount: 1,
108
+ createdAt: new Date().toISOString(),
109
+ lastAccessedAt: new Date().toISOString(),
110
+ type: "observation",
111
+ });
112
+
113
+ const rows = await orm.select().from(episodicEntries);
114
+ expect(rows.length).toBe(1);
115
+ expect(rows[0]?.id).toBe(id);
116
+ expect(rows[0]?.content).toBe("User added auth module");
117
+ expect(rows[0]?.relevance).toBeCloseTo(0.9);
118
+ db.close();
119
+ });
120
+ });
121
+
122
+ describe("cache_entries CRUD", () => {
123
+ test("can insert and query cache_entries", async () => {
124
+ const mainaDir = join(TEST_DIR, "cache-crud");
125
+ const result = getCacheDb(mainaDir);
126
+ expect(result.ok).toBe(true);
127
+ if (!result.ok) return;
128
+ const { drizzle: orm, db } = result.value;
129
+
130
+ const { cacheEntries } = await import("../schema.ts");
131
+
132
+ const id = "cache-entry-1";
133
+ await orm.insert(cacheEntries).values({
134
+ id,
135
+ key: "prompt-hash-abc123",
136
+ value: '{"tokens": 500}',
137
+ promptVersion: "v1.0",
138
+ contextHash: "ctx-hash-xyz",
139
+ model: "claude-3-5-sonnet",
140
+ createdAt: new Date().toISOString(),
141
+ ttl: 3600,
142
+ });
143
+
144
+ const rows = await orm.select().from(cacheEntries);
145
+ expect(rows.length).toBe(1);
146
+ expect(rows[0]?.id).toBe(id);
147
+ expect(rows[0]?.key).toBe("prompt-hash-abc123");
148
+ expect(rows[0]?.ttl).toBe(3600);
149
+ db.close();
150
+ });
151
+ });
@@ -0,0 +1,211 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { mkdirSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+ import * as schema from "./schema.ts";
6
+
7
+ export type DbHandle = {
8
+ db: Database;
9
+ drizzle: ReturnType<typeof drizzle<typeof schema>>;
10
+ };
11
+
12
+ export type Result<T, E = string> =
13
+ | { ok: true; value: T }
14
+ | { ok: false; error: E };
15
+
16
+ function ok<T>(value: T): Result<T, never> {
17
+ return { ok: true, value };
18
+ }
19
+
20
+ function err<E>(error: E): Result<never, E> {
21
+ return { ok: false, error };
22
+ }
23
+
24
+ /**
25
+ * Create context tables: episodic_entries, semantic_entities, dependency_edges.
26
+ */
27
+ function createContextTables(db: Database): void {
28
+ db.exec(`
29
+ CREATE TABLE IF NOT EXISTS episodic_entries (
30
+ id TEXT PRIMARY KEY,
31
+ content TEXT NOT NULL,
32
+ summary TEXT,
33
+ relevance REAL,
34
+ access_count INTEGER,
35
+ created_at TEXT NOT NULL,
36
+ last_accessed_at TEXT,
37
+ type TEXT NOT NULL
38
+ );
39
+
40
+ CREATE TABLE IF NOT EXISTS semantic_entities (
41
+ id TEXT PRIMARY KEY,
42
+ file_path TEXT NOT NULL,
43
+ name TEXT NOT NULL,
44
+ kind TEXT NOT NULL,
45
+ start_line INTEGER NOT NULL,
46
+ end_line INTEGER NOT NULL,
47
+ updated_at TEXT NOT NULL
48
+ );
49
+
50
+ CREATE TABLE IF NOT EXISTS dependency_edges (
51
+ id TEXT PRIMARY KEY,
52
+ source_file TEXT NOT NULL,
53
+ target_file TEXT NOT NULL,
54
+ weight REAL,
55
+ type TEXT NOT NULL
56
+ );
57
+ `);
58
+ }
59
+
60
+ /**
61
+ * Create cache tables: cache_entries.
62
+ */
63
+ function createCacheTables(db: Database): void {
64
+ db.exec(`
65
+ CREATE TABLE IF NOT EXISTS cache_entries (
66
+ id TEXT PRIMARY KEY,
67
+ key TEXT NOT NULL UNIQUE,
68
+ value TEXT NOT NULL,
69
+ prompt_version TEXT,
70
+ context_hash TEXT,
71
+ model TEXT,
72
+ created_at TEXT NOT NULL,
73
+ ttl INTEGER
74
+ );
75
+ `);
76
+ }
77
+
78
+ /**
79
+ * Create feedback tables: feedback, prompt_versions.
80
+ */
81
+ function createFeedbackTables(db: Database): void {
82
+ db.exec(`
83
+ CREATE TABLE IF NOT EXISTS feedback (
84
+ id TEXT PRIMARY KEY,
85
+ prompt_hash TEXT NOT NULL,
86
+ command TEXT NOT NULL,
87
+ accepted INTEGER NOT NULL,
88
+ context TEXT,
89
+ created_at TEXT NOT NULL
90
+ );
91
+
92
+ CREATE TABLE IF NOT EXISTS prompt_versions (
93
+ id TEXT PRIMARY KEY,
94
+ task TEXT NOT NULL,
95
+ hash TEXT NOT NULL,
96
+ content TEXT NOT NULL,
97
+ version INTEGER NOT NULL,
98
+ accept_rate REAL,
99
+ usage_count INTEGER,
100
+ created_at TEXT NOT NULL
101
+ );
102
+ `);
103
+
104
+ // Add workflow columns (nullable — backward compatible)
105
+ try {
106
+ db.exec(`ALTER TABLE feedback ADD COLUMN workflow_step TEXT`);
107
+ } catch {
108
+ // Column already exists — safe to ignore
109
+ }
110
+ try {
111
+ db.exec(`ALTER TABLE feedback ADD COLUMN workflow_id TEXT`);
112
+ } catch {
113
+ // Column already exists — safe to ignore
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Create stats tables: commit_snapshots.
119
+ */
120
+ function createStatsTables(db: Database): void {
121
+ db.exec(`
122
+ CREATE TABLE IF NOT EXISTS commit_snapshots (
123
+ id TEXT PRIMARY KEY,
124
+ timestamp TEXT NOT NULL,
125
+ branch TEXT NOT NULL,
126
+ commit_hash TEXT NOT NULL,
127
+ verify_duration_ms INTEGER NOT NULL,
128
+ total_duration_ms INTEGER NOT NULL,
129
+ context_tokens INTEGER NOT NULL,
130
+ context_budget INTEGER NOT NULL,
131
+ context_utilization REAL NOT NULL,
132
+ cache_hits INTEGER NOT NULL,
133
+ cache_misses INTEGER NOT NULL,
134
+ findings_total INTEGER NOT NULL,
135
+ findings_errors INTEGER NOT NULL,
136
+ findings_warnings INTEGER NOT NULL,
137
+ tools_run INTEGER NOT NULL,
138
+ syntax_passed INTEGER NOT NULL,
139
+ pipeline_passed INTEGER NOT NULL,
140
+ skipped INTEGER NOT NULL DEFAULT 0
141
+ );
142
+ `);
143
+
144
+ // Migration: add skipped column to existing tables
145
+ try {
146
+ db.exec(
147
+ "ALTER TABLE commit_snapshots ADD COLUMN skipped INTEGER NOT NULL DEFAULT 0",
148
+ );
149
+ } catch {
150
+ // Column already exists — safe to ignore
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Initialise a SQLite database at the given path, creating parent directories as needed.
156
+ * Accepts an optional table creator function; if omitted no tables are created.
157
+ * Returns a Result containing the raw Database and the Drizzle ORM instance.
158
+ * Never throws — all errors are returned as Err values.
159
+ */
160
+ export function initDatabase(
161
+ dbPath: string,
162
+ tableCreator?: (db: Database) => void,
163
+ ): Result<DbHandle> {
164
+ try {
165
+ mkdirSync(dirname(dbPath), { recursive: true });
166
+ const db = new Database(dbPath, { create: true });
167
+ db.exec("PRAGMA journal_mode=WAL;");
168
+ if (tableCreator) {
169
+ tableCreator(db);
170
+ }
171
+ const orm = drizzle(db, { schema });
172
+ return ok({ db, drizzle: orm });
173
+ } catch (e) {
174
+ return err(e instanceof Error ? e.message : String(e));
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Open the context database (.maina/context/index.db).
180
+ * Contains: episodic_entries, semantic_entities, dependency_edges.
181
+ */
182
+ export function getContextDb(mainaDir: string): Result<DbHandle> {
183
+ return initDatabase(
184
+ join(mainaDir, "context", "index.db"),
185
+ createContextTables,
186
+ );
187
+ }
188
+
189
+ /**
190
+ * Open the cache database (.maina/cache/cache.db).
191
+ * Contains: cache_entries.
192
+ */
193
+ export function getCacheDb(mainaDir: string): Result<DbHandle> {
194
+ return initDatabase(join(mainaDir, "cache", "cache.db"), createCacheTables);
195
+ }
196
+
197
+ /**
198
+ * Open the feedback database (.maina/feedback.db).
199
+ * Contains: feedback, prompt_versions.
200
+ */
201
+ export function getFeedbackDb(mainaDir: string): Result<DbHandle> {
202
+ return initDatabase(join(mainaDir, "feedback.db"), createFeedbackTables);
203
+ }
204
+
205
+ /**
206
+ * Open the stats database (.maina/stats.db).
207
+ * Contains: commit_snapshots.
208
+ */
209
+ export function getStatsDb(mainaDir: string): Result<DbHandle> {
210
+ return initDatabase(join(mainaDir, "stats.db"), createStatsTables);
211
+ }
@@ -0,0 +1,84 @@
1
+ import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
2
+
3
+ export const episodicEntries = sqliteTable("episodic_entries", {
4
+ id: text("id").primaryKey(),
5
+ content: text("content").notNull(),
6
+ summary: text("summary"),
7
+ relevance: real("relevance"),
8
+ accessCount: integer("access_count"),
9
+ createdAt: text("created_at").notNull(),
10
+ lastAccessedAt: text("last_accessed_at"),
11
+ type: text("type").notNull(),
12
+ });
13
+
14
+ export const semanticEntities = sqliteTable("semantic_entities", {
15
+ id: text("id").primaryKey(),
16
+ filePath: text("file_path").notNull(),
17
+ name: text("name").notNull(),
18
+ kind: text("kind", {
19
+ enum: ["function", "class", "interface", "type", "variable"],
20
+ }).notNull(),
21
+ startLine: integer("start_line").notNull(),
22
+ endLine: integer("end_line").notNull(),
23
+ updatedAt: text("updated_at").notNull(),
24
+ });
25
+
26
+ export const dependencyEdges = sqliteTable("dependency_edges", {
27
+ id: text("id").primaryKey(),
28
+ sourceFile: text("source_file").notNull(),
29
+ targetFile: text("target_file").notNull(),
30
+ weight: real("weight"),
31
+ type: text("type", { enum: ["import", "reference"] }).notNull(),
32
+ });
33
+
34
+ export const cacheEntries = sqliteTable("cache_entries", {
35
+ id: text("id").primaryKey(),
36
+ key: text("key").notNull().unique(),
37
+ value: text("value").notNull(),
38
+ promptVersion: text("prompt_version"),
39
+ contextHash: text("context_hash"),
40
+ model: text("model"),
41
+ createdAt: text("created_at").notNull(),
42
+ ttl: integer("ttl"),
43
+ });
44
+
45
+ export const feedback = sqliteTable("feedback", {
46
+ id: text("id").primaryKey(),
47
+ promptHash: text("prompt_hash").notNull(),
48
+ command: text("command").notNull(),
49
+ accepted: integer("accepted", { mode: "boolean" }).notNull(),
50
+ context: text("context"),
51
+ createdAt: text("created_at").notNull(),
52
+ });
53
+
54
+ export const commitSnapshots = sqliteTable("commit_snapshots", {
55
+ id: text("id").primaryKey(),
56
+ timestamp: text("timestamp").notNull(),
57
+ branch: text("branch").notNull(),
58
+ commitHash: text("commit_hash").notNull(),
59
+ verifyDurationMs: integer("verify_duration_ms").notNull(),
60
+ totalDurationMs: integer("total_duration_ms").notNull(),
61
+ contextTokens: integer("context_tokens").notNull(),
62
+ contextBudget: integer("context_budget").notNull(),
63
+ contextUtilization: real("context_utilization").notNull(),
64
+ cacheHits: integer("cache_hits").notNull(),
65
+ cacheMisses: integer("cache_misses").notNull(),
66
+ findingsTotal: integer("findings_total").notNull(),
67
+ findingsErrors: integer("findings_errors").notNull(),
68
+ findingsWarnings: integer("findings_warnings").notNull(),
69
+ toolsRun: integer("tools_run").notNull(),
70
+ syntaxPassed: integer("syntax_passed", { mode: "boolean" }).notNull(),
71
+ pipelinePassed: integer("pipeline_passed", { mode: "boolean" }).notNull(),
72
+ skipped: integer("skipped", { mode: "boolean" }).notNull().default(false),
73
+ });
74
+
75
+ export const promptVersions = sqliteTable("prompt_versions", {
76
+ id: text("id").primaryKey(),
77
+ task: text("task").notNull(),
78
+ hash: text("hash").notNull(),
79
+ content: text("content").notNull(),
80
+ version: integer("version").notNull(),
81
+ acceptRate: real("accept_rate"),
82
+ usageCount: integer("usage_count"),
83
+ createdAt: text("created_at").notNull(),
84
+ });