anvil-dev-framework 0.1.6

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 (190) hide show
  1. package/README.md +719 -0
  2. package/VERSION +1 -0
  3. package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
  4. package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
  5. package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
  6. package/docs/INSTALLATION.md +984 -0
  7. package/docs/anvil-hud.md +469 -0
  8. package/docs/anvil-init.md +255 -0
  9. package/docs/anvil-state.md +210 -0
  10. package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
  11. package/docs/command-reference.md +2022 -0
  12. package/docs/hooks-tts.md +368 -0
  13. package/docs/implementation-guide.md +810 -0
  14. package/docs/linear-github-integration.md +247 -0
  15. package/docs/local-issues.md +677 -0
  16. package/docs/patterns/README.md +419 -0
  17. package/docs/planning-responsibilities.md +139 -0
  18. package/docs/session-workflow.md +573 -0
  19. package/docs/simplification-plan-template.md +297 -0
  20. package/docs/simplification-principles.md +129 -0
  21. package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
  22. package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
  23. package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
  24. package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
  25. package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
  26. package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
  27. package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
  28. package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
  29. package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
  30. package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
  31. package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
  32. package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
  33. package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
  34. package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
  35. package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
  36. package/docs/sync.md +122 -0
  37. package/global/CLAUDE.md +140 -0
  38. package/global/agents/verify-app.md +164 -0
  39. package/global/commands/anvil-settings.md +527 -0
  40. package/global/commands/anvil-sync.md +121 -0
  41. package/global/commands/change.md +197 -0
  42. package/global/commands/clarify.md +252 -0
  43. package/global/commands/cleanup.md +292 -0
  44. package/global/commands/commit-push-pr.md +207 -0
  45. package/global/commands/decay-review.md +127 -0
  46. package/global/commands/discover.md +158 -0
  47. package/global/commands/doc-coverage.md +122 -0
  48. package/global/commands/evidence.md +307 -0
  49. package/global/commands/explore.md +121 -0
  50. package/global/commands/force-exit.md +135 -0
  51. package/global/commands/handoff.md +191 -0
  52. package/global/commands/healthcheck.md +302 -0
  53. package/global/commands/hud.md +84 -0
  54. package/global/commands/insights.md +319 -0
  55. package/global/commands/linear-setup.md +184 -0
  56. package/global/commands/lint-fix.md +198 -0
  57. package/global/commands/orient.md +510 -0
  58. package/global/commands/plan.md +228 -0
  59. package/global/commands/ralph.md +346 -0
  60. package/global/commands/ready.md +182 -0
  61. package/global/commands/release.md +305 -0
  62. package/global/commands/retro.md +96 -0
  63. package/global/commands/shard.md +166 -0
  64. package/global/commands/spec.md +227 -0
  65. package/global/commands/sprint.md +184 -0
  66. package/global/commands/tasks.md +228 -0
  67. package/global/commands/test-and-commit.md +151 -0
  68. package/global/commands/validate.md +132 -0
  69. package/global/commands/verify.md +251 -0
  70. package/global/commands/weekly-review.md +156 -0
  71. package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
  72. package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
  73. package/global/hooks/anvil_memory_observe.ts +322 -0
  74. package/global/hooks/anvil_memory_session.ts +166 -0
  75. package/global/hooks/anvil_memory_stop.ts +187 -0
  76. package/global/hooks/parse_transcript.py +116 -0
  77. package/global/hooks/post_merge_cleanup.sh +132 -0
  78. package/global/hooks/post_tool_format.sh +215 -0
  79. package/global/hooks/ralph_context_monitor.py +240 -0
  80. package/global/hooks/ralph_stop.sh +502 -0
  81. package/global/hooks/statusline.sh +1110 -0
  82. package/global/hooks/statusline_agent_sync.py +224 -0
  83. package/global/hooks/stop_gate.sh +250 -0
  84. package/global/lib/.claude/anvil-state.json +21 -0
  85. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  86. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  87. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  88. package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
  89. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  90. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  91. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  92. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  93. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  94. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  95. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  96. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  97. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  98. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  99. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  100. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  101. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  102. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  103. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  104. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  105. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  106. package/global/lib/agent_registry.py +995 -0
  107. package/global/lib/anvil-state.sh +435 -0
  108. package/global/lib/claim_service.py +515 -0
  109. package/global/lib/coderabbit_service.py +314 -0
  110. package/global/lib/config_service.py +423 -0
  111. package/global/lib/coordination_service.py +331 -0
  112. package/global/lib/doc_coverage_service.py +1305 -0
  113. package/global/lib/gate_logger.py +316 -0
  114. package/global/lib/github_service.py +310 -0
  115. package/global/lib/handoff_generator.py +775 -0
  116. package/global/lib/hygiene_service.py +712 -0
  117. package/global/lib/issue_models.py +257 -0
  118. package/global/lib/issue_provider.py +339 -0
  119. package/global/lib/linear_data_service.py +210 -0
  120. package/global/lib/linear_provider.py +987 -0
  121. package/global/lib/linear_provider.py.backup +671 -0
  122. package/global/lib/local_provider.py +486 -0
  123. package/global/lib/orient_fast.py +457 -0
  124. package/global/lib/quality_service.py +470 -0
  125. package/global/lib/ralph_prompt_generator.py +563 -0
  126. package/global/lib/ralph_state.py +1202 -0
  127. package/global/lib/state_manager.py +417 -0
  128. package/global/lib/transcript_parser.py +597 -0
  129. package/global/lib/verification_runner.py +557 -0
  130. package/global/lib/verify_iteration.py +490 -0
  131. package/global/lib/verify_subagent.py +250 -0
  132. package/global/skills/README.md +155 -0
  133. package/global/skills/quality-gates/SKILL.md +252 -0
  134. package/global/skills/skill-template/SKILL.md +109 -0
  135. package/global/skills/testing-strategies/SKILL.md +337 -0
  136. package/global/templates/CHANGE-template.md +105 -0
  137. package/global/templates/HANDOFF-template.md +63 -0
  138. package/global/templates/PLAN-template.md +111 -0
  139. package/global/templates/SPEC-template.md +93 -0
  140. package/global/templates/ralph/PROMPT.md.template +89 -0
  141. package/global/templates/ralph/fix_plan.md.template +31 -0
  142. package/global/templates/ralph/progress.txt.template +23 -0
  143. package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
  144. package/global/tests/test_doc_coverage.py +520 -0
  145. package/global/tests/test_issue_models.py +299 -0
  146. package/global/tests/test_local_provider.py +323 -0
  147. package/global/tools/README.md +178 -0
  148. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  149. package/global/tools/anvil-hud.py +3622 -0
  150. package/global/tools/anvil-hud.py.bak +3318 -0
  151. package/global/tools/anvil-issue.py +432 -0
  152. package/global/tools/anvil-memory/CLAUDE.md +49 -0
  153. package/global/tools/anvil-memory/README.md +42 -0
  154. package/global/tools/anvil-memory/bun.lock +25 -0
  155. package/global/tools/anvil-memory/bunfig.toml +9 -0
  156. package/global/tools/anvil-memory/package.json +23 -0
  157. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
  158. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
  159. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
  160. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
  161. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
  162. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
  163. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
  164. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
  165. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
  166. package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
  167. package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
  168. package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
  169. package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
  170. package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
  171. package/global/tools/anvil-memory/src/commands/get.ts +115 -0
  172. package/global/tools/anvil-memory/src/commands/init.ts +94 -0
  173. package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
  174. package/global/tools/anvil-memory/src/commands/search.ts +112 -0
  175. package/global/tools/anvil-memory/src/db.ts +638 -0
  176. package/global/tools/anvil-memory/src/index.ts +205 -0
  177. package/global/tools/anvil-memory/src/types.ts +122 -0
  178. package/global/tools/anvil-memory/tsconfig.json +29 -0
  179. package/global/tools/ralph-loop.sh +359 -0
  180. package/package.json +45 -0
  181. package/scripts/anvil +822 -0
  182. package/scripts/extract_patterns.py +222 -0
  183. package/scripts/init-project.sh +541 -0
  184. package/scripts/install.sh +229 -0
  185. package/scripts/postinstall.js +41 -0
  186. package/scripts/rollback.sh +188 -0
  187. package/scripts/sync.sh +623 -0
  188. package/scripts/test-statusline.sh +248 -0
  189. package/scripts/update_claude_md.py +224 -0
  190. package/scripts/verify.sh +255 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Integration tests for Anvil Memory hooks
3
+ *
4
+ * Tests hook execution as subprocesses with stdin/stdout.
5
+ * Validates JSON output structure, error handling, and graceful degradation.
6
+ */
7
+
8
+ import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test';
9
+ import { existsSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { AnvilMemoryDb } from '../db';
12
+ import { cleanupDb, getTestDbPath, ensureTestDir } from './test-utils';
13
+
14
+ // Project paths
15
+ const PROJECT_ROOT = join(__dirname, '../../../../..');
16
+ const HOOKS_DIR = join(PROJECT_ROOT, 'global/hooks');
17
+
18
+ /**
19
+ * Runs a hook as a subprocess with given stdin input
20
+ */
21
+ async function runHook(
22
+ hookName: string,
23
+ input: Record<string, unknown>,
24
+ dbPath?: string
25
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
26
+ const hookPath = join(HOOKS_DIR, hookName);
27
+ const inputJson = JSON.stringify(input);
28
+
29
+ // Set DB_PATH environment variable if provided
30
+ const env = { ...process.env };
31
+ if (dbPath) {
32
+ env.ANVIL_MEMORY_DB_PATH = dbPath;
33
+ }
34
+
35
+ const proc = Bun.spawn(['bun', 'run', hookPath], {
36
+ stdin: new Blob([inputJson]),
37
+ stdout: 'pipe',
38
+ stderr: 'pipe',
39
+ cwd: PROJECT_ROOT,
40
+ env,
41
+ });
42
+
43
+ const stdout = await new Response(proc.stdout).text();
44
+ const stderr = await new Response(proc.stderr).text();
45
+ await proc.exited;
46
+
47
+ return {
48
+ stdout: stdout.trim(),
49
+ stderr: stderr.trim(),
50
+ exitCode: proc.exitCode ?? 0,
51
+ };
52
+ }
53
+
54
+ describe('anvil_memory_observe hook', () => {
55
+ let testDbPath: string;
56
+
57
+ beforeAll(() => {
58
+ ensureTestDir();
59
+ });
60
+
61
+ beforeEach(() => {
62
+ testDbPath = getTestDbPath('hook-observe');
63
+ const db = new AnvilMemoryDb(testDbPath);
64
+ db.init();
65
+ db.close();
66
+ });
67
+
68
+ afterEach(() => {
69
+ cleanupDb(testDbPath);
70
+ });
71
+
72
+ test('outputs empty JSON for skipped tools', async () => {
73
+ const result = await runHook('anvil_memory_observe.ts', {
74
+ tool_name: 'Glob',
75
+ tool_result: 'Some file results',
76
+ });
77
+
78
+ expect(result.exitCode).toBe(0);
79
+ expect(result.stdout).toBe('{}');
80
+ });
81
+
82
+ test('outputs empty JSON for empty results', async () => {
83
+ const result = await runHook('anvil_memory_observe.ts', {
84
+ tool_name: 'Write',
85
+ tool_result: '',
86
+ });
87
+
88
+ expect(result.exitCode).toBe(0);
89
+ expect(result.stdout).toBe('{}');
90
+ });
91
+
92
+ test('outputs empty JSON for short results', async () => {
93
+ const result = await runHook('anvil_memory_observe.ts', {
94
+ tool_name: 'Write',
95
+ tool_result: 'too short',
96
+ });
97
+
98
+ expect(result.exitCode).toBe(0);
99
+ expect(result.stdout).toBe('{}');
100
+ });
101
+
102
+ test('outputs empty JSON without database', async () => {
103
+ // Use non-existent db path
104
+ const result = await runHook('anvil_memory_observe.ts', {
105
+ tool_name: 'Write',
106
+ tool_input: { file_path: '/test/file.ts' },
107
+ tool_result: 'File written successfully to /test/file.ts. This is a significant result.',
108
+ }, '/tmp/nonexistent-db-abc123.db');
109
+
110
+ expect(result.exitCode).toBe(0);
111
+ expect(result.stdout).toBe('{}');
112
+ });
113
+
114
+ test('handles malformed JSON input gracefully', async () => {
115
+ const hookPath = join(HOOKS_DIR, 'anvil_memory_observe.ts');
116
+
117
+ const proc = Bun.spawn(['bun', 'run', hookPath], {
118
+ stdin: new Blob(['not valid json']),
119
+ stdout: 'pipe',
120
+ stderr: 'pipe',
121
+ cwd: PROJECT_ROOT,
122
+ });
123
+
124
+ const stdout = await new Response(proc.stdout).text();
125
+ await proc.exited;
126
+
127
+ expect(proc.exitCode).toBe(0);
128
+ expect(stdout.trim()).toBe('{}');
129
+ });
130
+ });
131
+
132
+ describe('anvil_memory_session hook', () => {
133
+ let testDbPath: string;
134
+
135
+ beforeAll(() => {
136
+ ensureTestDir();
137
+ });
138
+
139
+ beforeEach(() => {
140
+ // Create isolated test database for session hook tests
141
+ testDbPath = getTestDbPath('hook-session');
142
+ const db = new AnvilMemoryDb(testDbPath);
143
+ db.init();
144
+ db.close();
145
+ });
146
+
147
+ afterEach(() => {
148
+ cleanupDb(testDbPath);
149
+ });
150
+
151
+ test('outputs valid JSON response', async () => {
152
+ // Use isolated test database
153
+ const result = await runHook('anvil_memory_session.ts', {}, testDbPath);
154
+
155
+ expect(result.exitCode).toBe(0);
156
+ // Should output valid JSON (either {} or {additionalContext: ...})
157
+ expect(() => JSON.parse(result.stdout)).not.toThrow();
158
+ });
159
+
160
+ test('response has correct structure', async () => {
161
+ const result = await runHook('anvil_memory_session.ts', {}, testDbPath);
162
+
163
+ expect(result.exitCode).toBe(0);
164
+ const output = JSON.parse(result.stdout);
165
+ // Should be either empty or have additionalContext
166
+ if (Object.keys(output).length > 0) {
167
+ expect(output.additionalContext).toBeDefined();
168
+ expect(typeof output.additionalContext).toBe('string');
169
+ }
170
+ });
171
+
172
+ test('handles malformed JSON input gracefully', async () => {
173
+ const hookPath = join(HOOKS_DIR, 'anvil_memory_session.ts');
174
+
175
+ const env = { ...process.env, ANVIL_MEMORY_DB_PATH: testDbPath };
176
+ const proc = Bun.spawn(['bun', 'run', hookPath], {
177
+ stdin: new Blob(['{ invalid json']),
178
+ stdout: 'pipe',
179
+ stderr: 'pipe',
180
+ cwd: PROJECT_ROOT,
181
+ env,
182
+ });
183
+
184
+ const stdout = await new Response(proc.stdout).text();
185
+ await proc.exited;
186
+
187
+ expect(proc.exitCode).toBe(0);
188
+ // Should still output valid JSON (empty or with context)
189
+ expect(() => JSON.parse(stdout)).not.toThrow();
190
+ });
191
+ });
192
+
193
+ describe('anvil_memory_stop hook', () => {
194
+ let testDbPath: string;
195
+
196
+ beforeAll(() => {
197
+ ensureTestDir();
198
+ });
199
+
200
+ beforeEach(() => {
201
+ testDbPath = getTestDbPath('hook-stop');
202
+ const db = new AnvilMemoryDb(testDbPath);
203
+ db.init();
204
+ db.close();
205
+ });
206
+
207
+ afterEach(() => {
208
+ cleanupDb(testDbPath);
209
+ });
210
+
211
+ test('outputs empty JSON on success', async () => {
212
+ const result = await runHook('anvil_memory_stop.ts', {
213
+ duration_seconds: 300,
214
+ context_used_percent: 50,
215
+ });
216
+
217
+ expect(result.exitCode).toBe(0);
218
+ expect(result.stdout).toBe('{}');
219
+ });
220
+
221
+ test('outputs empty JSON without database', async () => {
222
+ const result = await runHook('anvil_memory_stop.ts', {}, '/tmp/nonexistent-db-stop123.db');
223
+
224
+ expect(result.exitCode).toBe(0);
225
+ expect(result.stdout).toBe('{}');
226
+ });
227
+
228
+ test('handles empty input', async () => {
229
+ const result = await runHook('anvil_memory_stop.ts', {});
230
+
231
+ expect(result.exitCode).toBe(0);
232
+ expect(result.stdout).toBe('{}');
233
+ });
234
+
235
+ test('handles malformed JSON input gracefully', async () => {
236
+ const hookPath = join(HOOKS_DIR, 'anvil_memory_stop.ts');
237
+
238
+ const proc = Bun.spawn(['bun', 'run', hookPath], {
239
+ stdin: new Blob(['not json at all']),
240
+ stdout: 'pipe',
241
+ stderr: 'pipe',
242
+ cwd: PROJECT_ROOT,
243
+ });
244
+
245
+ const stdout = await new Response(proc.stdout).text();
246
+ await proc.exited;
247
+
248
+ expect(proc.exitCode).toBe(0);
249
+ expect(stdout.trim()).toBe('{}');
250
+ });
251
+ });
252
+
253
+ describe('Hook helper functions', () => {
254
+ // These tests verify the internal logic by importing the modules directly
255
+ // We test the main execution flow in the subprocess tests above
256
+
257
+ test('hooks exist at expected paths', () => {
258
+ expect(existsSync(join(HOOKS_DIR, 'anvil_memory_observe.ts'))).toBe(true);
259
+ expect(existsSync(join(HOOKS_DIR, 'anvil_memory_session.ts'))).toBe(true);
260
+ expect(existsSync(join(HOOKS_DIR, 'anvil_memory_stop.ts'))).toBe(true);
261
+ });
262
+
263
+ test('hooks are executable', async () => {
264
+ // Each hook should run and exit cleanly with empty input
265
+ const hooks = ['anvil_memory_observe.ts', 'anvil_memory_session.ts', 'anvil_memory_stop.ts'];
266
+
267
+ for (const hook of hooks) {
268
+ const result = await runHook(hook, {});
269
+ expect(result.exitCode).toBe(0);
270
+ }
271
+ });
272
+ });
@@ -0,0 +1,427 @@
1
+ /**
2
+ * Performance tests for Anvil Memory Database
3
+ *
4
+ * Verifies:
5
+ * - Query plans use indexes appropriately
6
+ * - Operations complete in <100ms
7
+ * - FTS5 queries are optimized
8
+ * - System scales to 10k+ observations
9
+ */
10
+
11
+ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
12
+ import { AnvilMemoryDb } from '../db';
13
+ import {
14
+ cleanupDb,
15
+ getTestDbPath,
16
+ ensureTestDir,
17
+ measureTime,
18
+ randomType,
19
+ randomWords,
20
+ } from './test-utils';
21
+
22
+ describe('Query Plan Analysis', () => {
23
+ let db: AnvilMemoryDb;
24
+ let testDbPath: string;
25
+
26
+ beforeAll(() => {
27
+ ensureTestDir();
28
+ testDbPath = getTestDbPath('perf-query');
29
+ db = new AnvilMemoryDb(testDbPath);
30
+ db.init();
31
+
32
+ // Add sample data for query plan analysis
33
+ for (let i = 0; i < 100; i++) {
34
+ db.createObservation({
35
+ timestamp: new Date(Date.now() - i * 60000).toISOString(),
36
+ type: randomType(),
37
+ title: `Observation ${i}: ${randomWords(3)}`,
38
+ content: randomWords(20),
39
+ project: i % 3 === 0 ? 'project-a' : 'project-b',
40
+ files: [`file${i}.ts`],
41
+ concepts: [randomWords(1), randomWords(1)],
42
+ });
43
+ }
44
+ });
45
+
46
+ afterAll(() => {
47
+ db.close();
48
+ cleanupDb(testDbPath);
49
+ });
50
+
51
+ test('getObservation uses PRIMARY KEY index', () => {
52
+ const rawDb = db.getDb();
53
+ const plan = rawDb.query('EXPLAIN QUERY PLAN SELECT * FROM observations WHERE id = ?').all(1);
54
+ const planStr = JSON.stringify(plan);
55
+
56
+ // Should use primary key (SEARCH using INTEGER PRIMARY KEY)
57
+ expect(planStr).toMatch(/SEARCH|PRIMARY/i);
58
+ expect(planStr).not.toMatch(/SCAN/i);
59
+ });
60
+
61
+ test('getRecentObservations uses timestamp index', () => {
62
+ const rawDb = db.getDb();
63
+ const plan = rawDb.query('EXPLAIN QUERY PLAN SELECT * FROM observations ORDER BY timestamp DESC LIMIT ?').all(20);
64
+
65
+ // Should use idx_observations_timestamp or be efficient
66
+ // Note: SQLite may choose SCAN if table is small, which is actually faster
67
+ expect(plan.length).toBeGreaterThan(0);
68
+ });
69
+
70
+ test('getRecentObservations with type filter uses index', () => {
71
+ const rawDb = db.getDb();
72
+ const plan = rawDb.query("EXPLAIN QUERY PLAN SELECT * FROM observations WHERE type IN (?, ?) ORDER BY timestamp DESC LIMIT ?").all('bugfix', 'feature', 20);
73
+
74
+ // Query plan should exist; actual index usage depends on table size
75
+ expect(plan.length).toBeGreaterThan(0);
76
+ });
77
+
78
+ test('getRecentObservations with project filter uses index', () => {
79
+ const rawDb = db.getDb();
80
+ const plan = rawDb.query('EXPLAIN QUERY PLAN SELECT * FROM observations WHERE project = ? ORDER BY timestamp DESC LIMIT ?').all('project-a', 20);
81
+
82
+ // Query plan should exist; actual index usage depends on table size
83
+ expect(plan.length).toBeGreaterThan(0);
84
+ });
85
+
86
+ test('searchObservations FTS5 query is optimized', () => {
87
+ const rawDb = db.getDb();
88
+ const plan = rawDb.query(`
89
+ EXPLAIN QUERY PLAN
90
+ SELECT o.* FROM observations o
91
+ JOIN observations_fts fts ON o.id = fts.rowid
92
+ WHERE observations_fts MATCH ?
93
+ ORDER BY rank
94
+ LIMIT ?
95
+ `).all('database', 20);
96
+
97
+ // FTS5 queries should use virtual table efficiently
98
+ expect(plan.length).toBeGreaterThan(0);
99
+ });
100
+
101
+ test('getSession uses PRIMARY KEY index', () => {
102
+ const rawDb = db.getDb();
103
+ const plan = rawDb.query('EXPLAIN QUERY PLAN SELECT * FROM sessions WHERE id = ?').all(1);
104
+ const planStr = JSON.stringify(plan);
105
+
106
+ expect(planStr).toMatch(/SEARCH|PRIMARY/i);
107
+ });
108
+
109
+ test('getCheckpoint uses PRIMARY KEY index', () => {
110
+ const rawDb = db.getDb();
111
+ const plan = rawDb.query('EXPLAIN QUERY PLAN SELECT * FROM checkpoints WHERE id = ?').all(1);
112
+ const planStr = JSON.stringify(plan);
113
+
114
+ expect(planStr).toMatch(/SEARCH|PRIMARY/i);
115
+ });
116
+ });
117
+
118
+ describe('Operation Timing (<100ms target)', () => {
119
+ let db: AnvilMemoryDb;
120
+ let testDbPath: string;
121
+
122
+ beforeAll(() => {
123
+ ensureTestDir();
124
+ testDbPath = getTestDbPath('perf-timing');
125
+ db = new AnvilMemoryDb(testDbPath);
126
+ db.init();
127
+
128
+ // Add 1000 observations for timing tests
129
+ for (let i = 0; i < 1000; i++) {
130
+ db.createObservation({
131
+ timestamp: new Date(Date.now() - i * 60000).toISOString(),
132
+ type: randomType(),
133
+ title: `Observation ${i}: ${randomWords(3)}`,
134
+ content: randomWords(50),
135
+ project: `project-${i % 10}`,
136
+ files: [`src/file${i}.ts`, `test/file${i}.test.ts`],
137
+ concepts: [randomWords(1), randomWords(1), randomWords(1)],
138
+ });
139
+ }
140
+ });
141
+
142
+ afterAll(() => {
143
+ db.close();
144
+ cleanupDb(testDbPath);
145
+ });
146
+
147
+ test('createObservation completes in <100ms', () => {
148
+ const { timeMs } = measureTime(() => {
149
+ db.createObservation({
150
+ timestamp: new Date().toISOString(),
151
+ type: 'discovery',
152
+ title: 'Test observation',
153
+ content: 'Test content for timing',
154
+ project: 'test-project',
155
+ });
156
+ });
157
+
158
+ expect(timeMs).toBeLessThan(100);
159
+ console.log(`createObservation: ${timeMs.toFixed(2)}ms`);
160
+ });
161
+
162
+ test('getObservation completes in <100ms', () => {
163
+ const { timeMs } = measureTime(() => {
164
+ db.getObservation(500);
165
+ });
166
+
167
+ expect(timeMs).toBeLessThan(100);
168
+ console.log(`getObservation: ${timeMs.toFixed(2)}ms`);
169
+ });
170
+
171
+ test('searchObservations completes in <100ms', () => {
172
+ const { timeMs } = measureTime(() => {
173
+ db.searchObservations('database optimization', 20);
174
+ });
175
+
176
+ expect(timeMs).toBeLessThan(100);
177
+ console.log(`searchObservations: ${timeMs.toFixed(2)}ms`);
178
+ });
179
+
180
+ test('getRecentObservations completes in <100ms', () => {
181
+ const { timeMs } = measureTime(() => {
182
+ db.getRecentObservations(50);
183
+ });
184
+
185
+ expect(timeMs).toBeLessThan(100);
186
+ console.log(`getRecentObservations: ${timeMs.toFixed(2)}ms`);
187
+ });
188
+
189
+ test('getRecentObservations with filters completes in <100ms', () => {
190
+ const { timeMs } = measureTime(() => {
191
+ db.getRecentObservations(50, ['bugfix', 'feature'], 'project-1');
192
+ });
193
+
194
+ expect(timeMs).toBeLessThan(100);
195
+ console.log(`getRecentObservations (filtered): ${timeMs.toFixed(2)}ms`);
196
+ });
197
+
198
+ test('createSession completes in <100ms', () => {
199
+ const { timeMs } = measureTime(() => {
200
+ db.createSession({
201
+ started_at: new Date().toISOString(),
202
+ project: 'test-project',
203
+ branch: 'feature/test',
204
+ });
205
+ });
206
+
207
+ expect(timeMs).toBeLessThan(100);
208
+ console.log(`createSession: ${timeMs.toFixed(2)}ms`);
209
+ });
210
+
211
+ test('createCheckpoint completes in <100ms', () => {
212
+ const { timeMs } = measureTime(() => {
213
+ db.createCheckpoint({
214
+ timestamp: new Date().toISOString(),
215
+ level: 'L1',
216
+ context_percent: 50,
217
+ });
218
+ });
219
+
220
+ expect(timeMs).toBeLessThan(100);
221
+ console.log(`createCheckpoint: ${timeMs.toFixed(2)}ms`);
222
+ });
223
+
224
+ test('createPrompt completes in <100ms', () => {
225
+ const { timeMs } = measureTime(() => {
226
+ db.createPrompt({
227
+ timestamp: new Date().toISOString(),
228
+ content: 'Help me implement a new feature',
229
+ project: 'test-project',
230
+ });
231
+ });
232
+
233
+ expect(timeMs).toBeLessThan(100);
234
+ console.log(`createPrompt: ${timeMs.toFixed(2)}ms`);
235
+ });
236
+ });
237
+
238
+ describe('Scale Testing (10k+ observations)', () => {
239
+ let db: AnvilMemoryDb;
240
+ let testDbPath: string;
241
+
242
+ beforeAll(() => {
243
+ ensureTestDir();
244
+ testDbPath = getTestDbPath('perf-scale');
245
+ db = new AnvilMemoryDb(testDbPath);
246
+ db.init();
247
+
248
+ console.log('Creating 10,000 observations for scale test...');
249
+ const startTime = performance.now();
250
+
251
+ // Batch insert for faster setup
252
+ const rawDb = db.getDb();
253
+ rawDb.exec('BEGIN TRANSACTION');
254
+
255
+ for (let i = 0; i < 10000; i++) {
256
+ db.createObservation({
257
+ timestamp: new Date(Date.now() - i * 60000).toISOString(),
258
+ type: randomType(),
259
+ title: `Scale test observation ${i}: ${randomWords(3)}`,
260
+ content: randomWords(100),
261
+ project: `project-${i % 50}`,
262
+ files: [`src/module${i % 100}/file${i}.ts`],
263
+ concepts: [randomWords(1), randomWords(1)],
264
+ work_tokens: Math.floor(Math.random() * 1000),
265
+ });
266
+
267
+ // Log progress every 1000
268
+ if ((i + 1) % 1000 === 0) {
269
+ console.log(` Created ${i + 1} observations...`);
270
+ }
271
+ }
272
+
273
+ rawDb.exec('COMMIT');
274
+
275
+ const setupTime = performance.now() - startTime;
276
+ console.log(`Setup complete in ${(setupTime / 1000).toFixed(2)}s`);
277
+ });
278
+
279
+ afterAll(() => {
280
+ db.close();
281
+ cleanupDb(testDbPath);
282
+ });
283
+
284
+ test('searchObservations at 10k scale completes in <100ms', () => {
285
+ const { result, timeMs } = measureTime(() => {
286
+ return db.searchObservations('database performance', 20);
287
+ });
288
+
289
+ expect(timeMs).toBeLessThan(100);
290
+ console.log(`searchObservations (10k): ${timeMs.toFixed(2)}ms, found ${result.length} results`);
291
+ });
292
+
293
+ test('getRecentObservations at 10k scale completes in <100ms', () => {
294
+ const { result, timeMs } = measureTime(() => {
295
+ return db.getRecentObservations(50);
296
+ });
297
+
298
+ expect(timeMs).toBeLessThan(100);
299
+ expect(result.length).toBe(50);
300
+ console.log(`getRecentObservations (10k): ${timeMs.toFixed(2)}ms`);
301
+ });
302
+
303
+ test('getRecentObservations with type filter at 10k scale completes in <100ms', () => {
304
+ const { result, timeMs } = measureTime(() => {
305
+ return db.getRecentObservations(50, ['bugfix', 'feature']);
306
+ });
307
+
308
+ expect(timeMs).toBeLessThan(100);
309
+ console.log(`getRecentObservations filtered by type (10k): ${timeMs.toFixed(2)}ms, found ${result.length} results`);
310
+ });
311
+
312
+ test('getRecentObservations with project filter at 10k scale completes in <100ms', () => {
313
+ const { result, timeMs } = measureTime(() => {
314
+ return db.getRecentObservations(50, undefined, 'project-1');
315
+ });
316
+
317
+ expect(timeMs).toBeLessThan(100);
318
+ console.log(`getRecentObservations filtered by project (10k): ${timeMs.toFixed(2)}ms, found ${result.length} results`);
319
+ });
320
+
321
+ test('getObservation at 10k scale completes in <100ms', () => {
322
+ const { result, timeMs } = measureTime(() => {
323
+ return db.getObservation(5000);
324
+ });
325
+
326
+ expect(timeMs).toBeLessThan(100);
327
+ expect(result).not.toBeNull();
328
+ console.log(`getObservation (10k): ${timeMs.toFixed(2)}ms`);
329
+ });
330
+
331
+ test('createObservation at 10k scale completes in <100ms', () => {
332
+ const { timeMs } = measureTime(() => {
333
+ db.createObservation({
334
+ timestamp: new Date().toISOString(),
335
+ type: 'discovery',
336
+ title: 'New observation at scale',
337
+ content: 'Content after 10k observations exist',
338
+ project: 'test-project',
339
+ });
340
+ });
341
+
342
+ expect(timeMs).toBeLessThan(100);
343
+ console.log(`createObservation (10k): ${timeMs.toFixed(2)}ms`);
344
+ });
345
+
346
+ test('integrityCheck at 10k scale completes in reasonable time', () => {
347
+ const { result, timeMs } = measureTime(() => {
348
+ return db.integrityCheck();
349
+ });
350
+
351
+ expect(result.ok).toBe(true);
352
+ // Integrity check can take longer, allow up to 1 second
353
+ expect(timeMs).toBeLessThan(1000);
354
+ console.log(`integrityCheck (10k): ${timeMs.toFixed(2)}ms`);
355
+ });
356
+ });
357
+
358
+ describe('FTS5 Search Quality', () => {
359
+ let db: AnvilMemoryDb;
360
+ let testDbPath: string;
361
+
362
+ beforeAll(() => {
363
+ ensureTestDir();
364
+ testDbPath = getTestDbPath('perf-fts');
365
+ db = new AnvilMemoryDb(testDbPath);
366
+ db.init();
367
+
368
+ // Add specific observations for search quality testing
369
+ db.createObservation({
370
+ timestamp: new Date().toISOString(),
371
+ type: 'discovery',
372
+ title: 'SQLite FTS5 full-text search implementation',
373
+ content: 'Implemented full-text search using FTS5 with Porter stemmer',
374
+ concepts: ['database', 'search', 'fts5'],
375
+ });
376
+
377
+ db.createObservation({
378
+ timestamp: new Date().toISOString(),
379
+ type: 'bugfix',
380
+ title: 'Fixed database connection leak',
381
+ content: 'Resolved issue where database connections were not properly closed',
382
+ concepts: ['database', 'memory', 'leak'],
383
+ });
384
+
385
+ db.createObservation({
386
+ timestamp: new Date().toISOString(),
387
+ type: 'feature',
388
+ title: 'Added user authentication',
389
+ content: 'Implemented JWT-based authentication for API endpoints',
390
+ concepts: ['auth', 'jwt', 'security'],
391
+ });
392
+ });
393
+
394
+ afterAll(() => {
395
+ db.close();
396
+ cleanupDb(testDbPath);
397
+ });
398
+
399
+ test('FTS5 finds exact matches', () => {
400
+ const results = db.searchObservations('FTS5');
401
+ expect(results.length).toBeGreaterThan(0);
402
+ expect(results[0]!.title).toContain('FTS5');
403
+ });
404
+
405
+ test('FTS5 finds stemmed matches', () => {
406
+ // "searching" should match "search" due to Porter stemmer
407
+ const results = db.searchObservations('searching');
408
+ expect(results.length).toBeGreaterThan(0);
409
+ });
410
+
411
+ test('FTS5 finds matches in concepts', () => {
412
+ const results = db.searchObservations('jwt');
413
+ expect(results.length).toBeGreaterThan(0);
414
+ expect(results[0]!.concepts).toContain('jwt');
415
+ });
416
+
417
+ test('FTS5 handles multi-word queries', () => {
418
+ const results = db.searchObservations('database connection');
419
+ expect(results.length).toBeGreaterThan(0);
420
+ });
421
+
422
+ test('FTS5 returns results in relevance order', () => {
423
+ const results = db.searchObservations('database');
424
+ // Should find multiple results with "database"
425
+ expect(results.length).toBeGreaterThanOrEqual(2);
426
+ });
427
+ });