@popoverai/dotrequirements 0.11.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 (127) hide show
  1. package/README.md +478 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.js +82 -0
  4. package/dist/commands/init.d.ts +6 -0
  5. package/dist/commands/init.js +355 -0
  6. package/dist/commands/link.d.ts +15 -0
  7. package/dist/commands/link.js +156 -0
  8. package/dist/commands/login.d.ts +12 -0
  9. package/dist/commands/login.js +117 -0
  10. package/dist/commands/logout.d.ts +5 -0
  11. package/dist/commands/logout.js +17 -0
  12. package/dist/commands/mcp-setup.d.ts +5 -0
  13. package/dist/commands/mcp-setup.js +367 -0
  14. package/dist/commands/mcp.d.ts +6 -0
  15. package/dist/commands/mcp.js +10 -0
  16. package/dist/commands/pull.d.ts +7 -0
  17. package/dist/commands/pull.js +171 -0
  18. package/dist/commands/push.d.ts +11 -0
  19. package/dist/commands/push.js +310 -0
  20. package/dist/commands/test.d.ts +6 -0
  21. package/dist/commands/test.js +78 -0
  22. package/dist/config.d.ts +5 -0
  23. package/dist/config.js +16 -0
  24. package/dist/convex.d.ts +56 -0
  25. package/dist/convex.js +58 -0
  26. package/dist/harness/cache.d.ts +135 -0
  27. package/dist/harness/cache.js +342 -0
  28. package/dist/harness/convexReporting.d.ts +15 -0
  29. package/dist/harness/convexReporting.js +136 -0
  30. package/dist/harness/coverageCache.d.ts +30 -0
  31. package/dist/harness/coverageCache.js +70 -0
  32. package/dist/harness/finalize.d.ts +48 -0
  33. package/dist/harness/finalize.js +299 -0
  34. package/dist/harness/index.d.ts +70 -0
  35. package/dist/harness/index.js +103 -0
  36. package/dist/harness/localReporting.d.ts +6 -0
  37. package/dist/harness/localReporting.js +49 -0
  38. package/dist/harness/prepare.d.ts +41 -0
  39. package/dist/harness/prepare.js +83 -0
  40. package/dist/harness/requirementsLoader.d.ts +45 -0
  41. package/dist/harness/requirementsLoader.js +201 -0
  42. package/dist/harness/tracking.d.ts +49 -0
  43. package/dist/harness/tracking.js +179 -0
  44. package/dist/harness/types.d.ts +12 -0
  45. package/dist/harness/types.js +6 -0
  46. package/dist/mcp/convexClient.d.ts +43 -0
  47. package/dist/mcp/convexClient.js +101 -0
  48. package/dist/mcp/grep.d.ts +24 -0
  49. package/dist/mcp/grep.js +261 -0
  50. package/dist/mcp/index.d.ts +3 -0
  51. package/dist/mcp/index.js +1758 -0
  52. package/dist/mcp/requirements.d.ts +47 -0
  53. package/dist/mcp/requirements.js +141 -0
  54. package/dist/mcp/testCodeExtractor.d.ts +22 -0
  55. package/dist/mcp/testCodeExtractor.js +152 -0
  56. package/dist/mcp/types.d.ts +27 -0
  57. package/dist/mcp/types.js +2 -0
  58. package/dist/schema/browser.d.ts +12 -0
  59. package/dist/schema/browser.js +24 -0
  60. package/dist/schema/builder.d.ts +25 -0
  61. package/dist/schema/builder.js +125 -0
  62. package/dist/schema/conversions.d.ts +69 -0
  63. package/dist/schema/conversions.js +201 -0
  64. package/dist/schema/index.d.ts +14 -0
  65. package/dist/schema/index.js +24 -0
  66. package/dist/schema/parser-core.d.ts +61 -0
  67. package/dist/schema/parser-core.js +247 -0
  68. package/dist/schema/parser.d.ts +44 -0
  69. package/dist/schema/parser.js +295 -0
  70. package/dist/schema/resolver.d.ts +66 -0
  71. package/dist/schema/resolver.js +185 -0
  72. package/dist/schema/schemas.d.ts +312 -0
  73. package/dist/schema/schemas.js +258 -0
  74. package/dist/schema/test-schema.d.ts +5 -0
  75. package/dist/schema/test-schema.js +81 -0
  76. package/dist/templates/antigravity-gemini.md +3 -0
  77. package/dist/templates/antigravity-overview-rule.md +3 -0
  78. package/dist/templates/antigravity-test-rule.md +3 -0
  79. package/dist/templates/behavioral-core.md +25 -0
  80. package/dist/templates/claude-code-overview-skill.md +6 -0
  81. package/dist/templates/claude-code-skill.md +6 -0
  82. package/dist/templates/claude-code-test-skill.md +6 -0
  83. package/dist/templates/codex-agents.md +3 -0
  84. package/dist/templates/codex-overview-agents.md +3 -0
  85. package/dist/templates/codex-test-agents.md +3 -0
  86. package/dist/templates/cursor-overview-rule.mdc +5 -0
  87. package/dist/templates/cursor-rule.mdc +5 -0
  88. package/dist/templates/cursor-test-rule.mdc +5 -0
  89. package/dist/templates/example-requirements.d.ts +8 -0
  90. package/dist/templates/example-requirements.js +88 -0
  91. package/dist/templates/example-requirements.ts +88 -0
  92. package/dist/templates/overview-core.md +27 -0
  93. package/dist/templates/requirements-readme.d.ts +5 -0
  94. package/dist/templates/requirements-readme.js +31 -0
  95. package/dist/templates/requirements-readme.ts +30 -0
  96. package/dist/templates/test-writing-core.md +72 -0
  97. package/dist/utils/brand.d.ts +5 -0
  98. package/dist/utils/brand.js +8 -0
  99. package/dist/utils/browser-launch.d.ts +19 -0
  100. package/dist/utils/browser-launch.js +36 -0
  101. package/dist/utils/detect-existing-project.d.ts +5 -0
  102. package/dist/utils/detect-existing-project.js +34 -0
  103. package/dist/utils/env.d.ts +19 -0
  104. package/dist/utils/env.js +56 -0
  105. package/dist/utils/gitignore.d.ts +7 -0
  106. package/dist/utils/gitignore.js +29 -0
  107. package/dist/utils/local-project.d.ts +31 -0
  108. package/dist/utils/local-project.js +33 -0
  109. package/dist/utils/oauth-callback-server.d.ts +28 -0
  110. package/dist/utils/oauth-callback-server.js +156 -0
  111. package/dist/utils/oauth-flow.d.ts +22 -0
  112. package/dist/utils/oauth-flow.js +120 -0
  113. package/dist/utils/project-discovery.d.ts +57 -0
  114. package/dist/utils/project-discovery.js +146 -0
  115. package/dist/utils/project-name.d.ts +8 -0
  116. package/dist/utils/project-name.js +48 -0
  117. package/dist/utils/project-selector.d.ts +25 -0
  118. package/dist/utils/project-selector.js +69 -0
  119. package/dist/utils/prompts.d.ts +33 -0
  120. package/dist/utils/prompts.js +60 -0
  121. package/dist/utils/templates.d.ts +29 -0
  122. package/dist/utils/templates.js +67 -0
  123. package/dist/utils/token-refresh.d.ts +24 -0
  124. package/dist/utils/token-refresh.js +69 -0
  125. package/dist/utils/token-storage.d.ts +31 -0
  126. package/dist/utils/token-storage.js +57 -0
  127. package/package.json +82 -0
@@ -0,0 +1,1758 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
+ import { loadAllRequirements, searchRequirements, getRequirementById, getRequirementTree, formatRequirementTree, } from './requirements.js';
6
+ import { findAllTestReferences, getReferencedRequirementIds, } from './grep.js';
7
+ import { findTestCodeForRequirement, findFilesWithRequirement, } from './testCodeExtractor.js';
8
+ import { glob } from 'glob';
9
+ import { loadConvexConfig, getRequirementCoverage as queryRequirementCoverage, getProjectCoverage as queryProjectCoverage, } from './convexClient.js';
10
+ import { discoverProjects, resolveProject, } from '../utils/project-discovery.js';
11
+ import { isLocalOnlyProject, CLOUD_FEATURES_REQUIRE_AUTH_MESSAGE } from '../utils/local-project.js';
12
+ // Get project paths from environment variables
13
+ // Supports both PROJ_* pattern (Antigravity) and REQUIREMENTS_DIR fallback
14
+ function getProjectPathsFromEnv() {
15
+ const projects = new Map();
16
+ // Check for PROJ_* env vars (project ID -> path mapping)
17
+ for (const [key, value] of Object.entries(process.env)) {
18
+ if (key.startsWith('PROJ_') && value) {
19
+ const projectId = key.substring(5); // Remove 'PROJ_' prefix
20
+ projects.set(projectId, value);
21
+ }
22
+ }
23
+ return projects;
24
+ }
25
+ // Get workspace root from environment or default to cwd
26
+ const WORKSPACE_ROOT = process.env.REQUIREMENTS_DIR || process.cwd();
27
+ const PROJECT_PATHS = getProjectPathsFromEnv();
28
+ // Cache for loaded requirements (refreshed on each tool call for now)
29
+ let cachedRequirements = new Map();
30
+ let cachedDiscoveryResult = null;
31
+ async function getRequirements(projectId) {
32
+ const project = await getProjectFromDiscovery(projectId);
33
+ const cacheKey = project.projectId;
34
+ if (!cachedRequirements.has(cacheKey)) {
35
+ const { flattened } = await loadAllRequirements(project.path);
36
+ cachedRequirements.set(cacheKey, flattened);
37
+ }
38
+ return cachedRequirements.get(cacheKey);
39
+ }
40
+ async function getProjectFromDiscovery(projectId) {
41
+ const { isConfiguredProject } = await import('../utils/project-discovery.js');
42
+ // If we have PROJ_* env vars, use those instead of filesystem discovery
43
+ if (PROJECT_PATHS.size > 0) {
44
+ const projects = [];
45
+ // Validate all env var projects
46
+ for (const [id, path] of PROJECT_PATHS.entries()) {
47
+ const project = isConfiguredProject(path);
48
+ if (project) {
49
+ projects.push(project);
50
+ }
51
+ }
52
+ // Build discovery result from env var projects
53
+ if (projects.length === 0) {
54
+ cachedDiscoveryResult = {
55
+ type: 'none',
56
+ message: 'No valid dotrequirements projects found in PROJ_* environment variables',
57
+ };
58
+ }
59
+ else if (projects.length === 1) {
60
+ cachedDiscoveryResult = { type: 'single', project: projects[0] };
61
+ }
62
+ else {
63
+ cachedDiscoveryResult = { type: 'multiple', projects };
64
+ }
65
+ return resolveProject(cachedDiscoveryResult, projectId);
66
+ }
67
+ // Fall back to filesystem discovery if no PROJ_* env vars
68
+ if (!cachedDiscoveryResult) {
69
+ cachedDiscoveryResult = await discoverProjects(WORKSPACE_ROOT);
70
+ }
71
+ return resolveProject(cachedDiscoveryResult, projectId);
72
+ }
73
+ // Invalidate cache (call before operations that should see fresh data)
74
+ function invalidateCache() {
75
+ cachedRequirements.clear();
76
+ cachedDiscoveryResult = null;
77
+ }
78
+ // Tool definitions
79
+ const tools = [
80
+ {
81
+ name: 'debug_mcp_environment',
82
+ description: 'DEBUG TOOL: Returns diagnostic information about the MCP server environment (working directory, env vars, etc.)',
83
+ inputSchema: {
84
+ type: 'object',
85
+ properties: {},
86
+ required: [],
87
+ },
88
+ },
89
+ {
90
+ name: 'search_requirements',
91
+ description: 'Search requirements by text or regex query. Searches requirement IDs, content, and labels. Returns matching requirements with their full context.',
92
+ inputSchema: {
93
+ type: 'object',
94
+ properties: {
95
+ query: {
96
+ type: 'string',
97
+ description: 'Text or regex pattern to search for in requirement IDs, content, or labels',
98
+ },
99
+ useRegex: {
100
+ type: 'boolean',
101
+ description: 'If true, treat query as a regular expression (default: false)',
102
+ },
103
+ projectId: {
104
+ type: 'string',
105
+ description: 'Optional: Specify which project to search (required when multiple projects exist)',
106
+ },
107
+ },
108
+ required: ['query'],
109
+ },
110
+ },
111
+ {
112
+ name: 'get_requirement',
113
+ description: 'Get a requirement and all its children, including test coverage information. Accepts any requirement path (e.g., "REQ-123" returns the root and all children, "REQ-123.0" returns that node and its children). Shows which tests reference this requirement and includes the test code.',
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: {
117
+ id: {
118
+ type: 'string',
119
+ description: 'The requirement ID or path (e.g., "REQ-123" for root, "REQ-123.0" for child)',
120
+ },
121
+ projectId: {
122
+ type: 'string',
123
+ description: 'Optional: Specify which project to search (required when multiple projects exist)',
124
+ },
125
+ },
126
+ required: ['id'],
127
+ },
128
+ },
129
+ {
130
+ name: 'list_untested_requirements',
131
+ description: 'List all requirements that have no test references. Compares requirements in .requirements/ files against requirement() calls in the codebase.',
132
+ inputSchema: {
133
+ type: 'object',
134
+ properties: {
135
+ projectId: {
136
+ type: 'string',
137
+ description: 'Optional: Specify which project to search (required when multiple projects exist)',
138
+ },
139
+ },
140
+ required: [],
141
+ },
142
+ },
143
+ {
144
+ name: 'list_all_requirements',
145
+ description: 'List all requirements in the workspace. Returns a summary of all requirements with their IDs, labels, and content.',
146
+ inputSchema: {
147
+ type: 'object',
148
+ properties: {
149
+ projectId: {
150
+ type: 'string',
151
+ description: 'Optional: Specify which project to search (required when multiple projects exist)',
152
+ },
153
+ },
154
+ required: [],
155
+ },
156
+ },
157
+ {
158
+ name: 'get_requirements_by_test',
159
+ description: 'Get the semantic meaning of requirement() references in a test file. Returns requirement content with line numbers where they are referenced.',
160
+ inputSchema: {
161
+ type: 'object',
162
+ properties: {
163
+ testFile: {
164
+ type: 'string',
165
+ description: 'Path to the test file to analyze',
166
+ },
167
+ projectId: {
168
+ type: 'string',
169
+ description: 'Optional: Specify which project to search (required when multiple projects exist)',
170
+ },
171
+ },
172
+ required: ['testFile'],
173
+ },
174
+ },
175
+ {
176
+ name: 'get_tests_by_requirement',
177
+ description: 'Show which requirements from a requirements file have test coverage. Returns a coverage report showing which requirements are tested and where the tests are located. This is the inverse of get_requirements_by_test.',
178
+ inputSchema: {
179
+ type: 'object',
180
+ properties: {
181
+ requirementsFile: {
182
+ type: 'string',
183
+ description: 'Path to the requirements file (e.g., ".requirements/auth.requirements.md")',
184
+ },
185
+ projectId: {
186
+ type: 'string',
187
+ description: 'Optional: Specify which project to search (required when multiple projects exist)',
188
+ },
189
+ },
190
+ required: ['requirementsFile'],
191
+ },
192
+ },
193
+ {
194
+ name: 'get_requirement_coverage',
195
+ description: 'Get test coverage information for a specific requirement from dot•requirements cloud. Shows when the requirement was last tested, on which branch, and in which test file. Requires DOTREQUIREMENTS_PROJECT_SECRET to be configured.',
196
+ inputSchema: {
197
+ type: 'object',
198
+ properties: {
199
+ requirementKey: {
200
+ type: 'string',
201
+ description: 'The requirement key (e.g., "REQ-123", "AUTH-LOGIN")',
202
+ },
203
+ projectId: {
204
+ type: 'string',
205
+ description: 'Optional: Specify which project to use (required when multiple projects exist)',
206
+ },
207
+ },
208
+ required: ['requirementKey'],
209
+ },
210
+ },
211
+ {
212
+ name: 'get_project_coverage_summary',
213
+ description: 'Get a summary of test coverage for all requirements in the project from dot•requirements cloud. Shows which requirements have been tested and which haven\'t. Optionally filter by branch or time range. Requires DOTREQUIREMENTS_PROJECT_SECRET to be configured.',
214
+ inputSchema: {
215
+ type: 'object',
216
+ properties: {
217
+ branch: {
218
+ type: 'string',
219
+ description: 'Optional: Filter coverage to a specific git branch (e.g., "main")',
220
+ },
221
+ sinceTimestamp: {
222
+ type: 'number',
223
+ description: 'Optional: Only show coverage since this timestamp (milliseconds since epoch)',
224
+ },
225
+ projectId: {
226
+ type: 'string',
227
+ description: 'Optional: Specify which project to use (required when multiple projects exist)',
228
+ },
229
+ },
230
+ required: [],
231
+ },
232
+ },
233
+ {
234
+ name: 'create_requirement_document',
235
+ description: 'Returns a well-formatted Markdown template that demonstrates the requirements file format. Use this to understand the format before creating requirements files. AI should use this template as a reference, then use filesystem tools (Read/Write) to create actual files.',
236
+ inputSchema: {
237
+ type: 'object',
238
+ properties: {
239
+ filePath: {
240
+ type: 'string',
241
+ description: 'Optional: Suggested file path for documentation purposes (e.g., ".requirements/auth.requirements.md")',
242
+ },
243
+ },
244
+ required: [],
245
+ },
246
+ },
247
+ {
248
+ name: 'validate_requirements',
249
+ description: 'Validate a requirements Markdown file without pushing to cloud. Checks schema validity and returns detailed error messages. Works offline (no network/auth required). Use this to verify files before pushing.',
250
+ inputSchema: {
251
+ type: 'object',
252
+ properties: {
253
+ filePath: {
254
+ type: 'string',
255
+ description: 'Path to the Markdown file to validate (e.g., ".requirements/auth.requirements.md")',
256
+ },
257
+ },
258
+ required: ['filePath'],
259
+ },
260
+ },
261
+ {
262
+ name: 'push_requirements',
263
+ description: 'Push local requirements from .requirements/ directory to dot•requirements cloud. First call returns diff summary for user review. Second call with confirmed=true executes the push. Requires DOTREQUIREMENTS_PROJECT_SECRET environment variable.',
264
+ inputSchema: {
265
+ type: 'object',
266
+ properties: {
267
+ filePath: {
268
+ type: 'string',
269
+ description: 'Optional: Push specific file only (e.g., ".requirements/auth.requirements.md"). If omitted, pushes all files.',
270
+ },
271
+ confirmed: {
272
+ type: 'boolean',
273
+ description: 'Set to true to execute the push after reviewing the diff. First call should omit this.',
274
+ },
275
+ projectId: {
276
+ type: 'string',
277
+ description: 'Optional: Specify which project to use (required when multiple projects exist)',
278
+ },
279
+ },
280
+ required: [],
281
+ },
282
+ },
283
+ {
284
+ name: 'style_check',
285
+ description: 'Check requirements files or test files for style issues and best practices. Uses AI to provide actionable feedback on writing style, clarity, and conventions. Supports requirements files (*.requirements.md) and test files (*.test.*, *.spec.*). Requires DOTREQUIREMENTS_PROJECT_ID and DOTREQUIREMENTS_PROJECT_SECRET to be configured.',
286
+ inputSchema: {
287
+ type: 'object',
288
+ properties: {
289
+ filePath: {
290
+ type: 'string',
291
+ description: 'Path to the file to check (e.g., ".requirements/auth.requirements.md" or "src/auth.test.ts")',
292
+ },
293
+ model: {
294
+ type: 'string',
295
+ description: 'Optional: AI model to use for style checking (default: "anthropic/claude-haiku-4.5"). Supported models: "anthropic/claude-haiku-4.5", "google/gemini-3-flash"',
296
+ },
297
+ },
298
+ required: ['filePath'],
299
+ },
300
+ },
301
+ {
302
+ name: 'review_test',
303
+ description: 'Comprehensively review a test file for both style and semantic correctness. Checks if tests actually validate what the requirements specify (not just style). Loads referenced requirements and validates that test setup, actions, and assertions match requirement preconditions, triggers, and outcomes. Requires DOTREQUIREMENTS_PROJECT_ID and DOTREQUIREMENTS_PROJECT_SECRET to be configured.',
304
+ inputSchema: {
305
+ type: 'object',
306
+ properties: {
307
+ testFilePath: {
308
+ type: 'string',
309
+ description: 'Path to the test file to review (e.g., "src/auth.test.ts")',
310
+ },
311
+ projectId: {
312
+ type: 'string',
313
+ description: 'Optional: Specify which project to use (required when multiple projects exist)',
314
+ },
315
+ },
316
+ required: ['testFilePath'],
317
+ },
318
+ },
319
+ ];
320
+ // Prompt definitions
321
+ const prompts = [
322
+ {
323
+ name: 'capture-requirements',
324
+ description: 'Guide the user through capturing requirements for a new feature before implementation',
325
+ },
326
+ {
327
+ name: 'write-tests',
328
+ description: 'Guide writing tests that reference and validate requirements',
329
+ },
330
+ ];
331
+ // Create server
332
+ const server = new Server({
333
+ name: 'dotrequirements',
334
+ version: '0.1.0',
335
+ }, {
336
+ capabilities: {
337
+ tools: {},
338
+ prompts: {},
339
+ },
340
+ });
341
+ // Handle list tools
342
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
343
+ return { tools };
344
+ });
345
+ // Handle list prompts
346
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
347
+ return { prompts };
348
+ });
349
+ // Handle get prompt
350
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
351
+ const { name } = request.params;
352
+ if (name === 'capture-requirements') {
353
+ return {
354
+ messages: [
355
+ {
356
+ role: 'user',
357
+ content: {
358
+ type: 'text',
359
+ text: `# Capture Requirements for New Feature
360
+
361
+ Let's document what this feature should do before building it.
362
+
363
+ ## Questions to Ask
364
+
365
+ 1. What problem does this solve for users?
366
+ 2. What are the key behaviors we need to support?
367
+ 3. Are there edge cases or error conditions to handle?
368
+ 4. How will we know it works correctly?
369
+
370
+ ## Workflow
371
+
372
+ ### Step 1: Create Requirements Document
373
+
374
+ Use \`create_requirement_document\` to get a template with:
375
+ - Format guidance (discovers existing patterns in your codebase)
376
+ - Style principles for writing clear, testable requirements
377
+ - Examples of well-written requirements
378
+
379
+ Fill in:
380
+ - Project ID (from .env.local)
381
+ - Document title
382
+ - Requirement IDs, descriptions, and expected behaviors
383
+
384
+ ### Step 2: Validate & Refine (Optional)
385
+
386
+ - \`validate_requirements\` - Check syntax is correct (works offline)
387
+ - \`style_check\` - Get AI-powered feedback on writing quality and clarity
388
+
389
+ ### Step 3: Push to Cloud
390
+
391
+ - \`push_requirements\` - Sync to dot•requirements cloud
392
+ - First call shows a diff preview
393
+ - Second call with \`confirmed: true\` executes the push
394
+
395
+ ### Step 4: Implement & Test
396
+
397
+ After capturing requirements:
398
+ 1. Implement the feature
399
+ 2. Write tests that reference requirements using \`requirement('REQ-ID')\`
400
+ 3. Use the test writing skill/prompt for guidance on test structure`,
401
+ },
402
+ },
403
+ ],
404
+ };
405
+ }
406
+ if (name === 'write-tests') {
407
+ return {
408
+ messages: [
409
+ {
410
+ role: 'user',
411
+ content: {
412
+ type: 'text',
413
+ text: `# Write Tests for Requirements
414
+
415
+ Let's write tests that validate the requirements.
416
+
417
+ ## Core Principles
418
+
419
+ ### 1. Use requirement() AS the test description
420
+
421
+ \`\`\`typescript
422
+ // ✅ Good - requirement() is the description
423
+ test(requirement('AUTH-1'), () => { /* test code */ });
424
+ describe(requirement('LOGIN-1.given'), () => { /* setup */ });
425
+ it(requirement('LOGIN-1.then'), () => { /* assert */ });
426
+
427
+ // ❌ Bad - requirement() in body or comments
428
+ test("user can log in", () => { requirement('AUTH-1'); /* test code */ });
429
+ // LOGIN-1: User can log in
430
+ \`\`\`
431
+
432
+ ### 2. Structure tests to match requirement hierarchy
433
+
434
+ For structured requirements, nest describe/it blocks:
435
+
436
+ \`\`\`typescript
437
+ describe(requirement('LOGIN-1'), () => {
438
+ describe(requirement('LOGIN-1.given'), () => {
439
+ // Arrange: Set up preconditions
440
+ });
441
+
442
+ describe(requirement('LOGIN-1.when'), () => {
443
+ // Act: Trigger the behavior
444
+
445
+ it(requirement('LOGIN-1.then'), () => {
446
+ // Assert: Verify outcome
447
+ });
448
+ });
449
+ });
450
+ \`\`\`
451
+
452
+ For simple requirements, one test block is fine:
453
+
454
+ \`\`\`typescript
455
+ test(requirement('AUTH-1'), () => {
456
+ // arrange, act, assert all in one
457
+ });
458
+ \`\`\`
459
+
460
+ ### 3. Comments describe the TEST, not the requirement
461
+
462
+ \`\`\`typescript
463
+ // ✅ Good - comment explains test implementation
464
+ describe(requirement('REQ-1.0'), () => {
465
+ // Mock user with valid credentials
466
+ });
467
+
468
+ // ❌ Bad - verbatim copy of requirement text
469
+ describe(requirement('REQ-1.0'), () => {
470
+ // 0. When a registered user provides valid credentials, they are authenticated
471
+ });
472
+ \`\`\`
473
+
474
+ ### 4. NEVER document coverage in comments
475
+
476
+ Our MCP tools track coverage automatically. Don't create a second source of truth.
477
+
478
+ \`\`\`typescript
479
+ // ❌ Bad - creates second source of truth
480
+ /**
481
+ * Requirements coverage:
482
+ * - AUTH-9.0: Backend creates project ✓
483
+ * - AUTH-9.1: CLI writes to .env.local ✓
484
+ */
485
+
486
+ // ✅ Good - use MCP tools to check coverage
487
+ // Use get_requirement, get_requirements_by_test
488
+ \`\`\`
489
+
490
+ ## Requirement Path Formats
491
+
492
+ - \`requirement('AUTH-1')\` - root requirement
493
+ - \`requirement('AUTH-1.0')\` - by numeric position
494
+ - \`requirement('AUTH-1.given')\` - by label (case-insensitive)
495
+ - \`requirement('AUTH-1.given#1')\` - disambiguate duplicate labels
496
+ - \`requirement('AUTH-1.then.and')\` - nested label path
497
+
498
+ ## Workflow
499
+
500
+ After writing tests:
501
+
502
+ 1. Run tests to ensure they don't throw errors
503
+ 2. **ALWAYS run \`review_test\`** on the test file to validate tests actually check what requirements specify
504
+ 3. Fix any issues identified by the review
505
+
506
+ ## MCP Tools for Test Writing
507
+
508
+ - \`get_requirement\` - Get requirement tree with existing coverage
509
+ - \`get_requirements_by_test\` - See requirements referenced in a test file
510
+ - \`list_untested_requirements\` - Find requirements without tests
511
+ - \`review_test\` - Validate test actually checks what requirement specifies (run after writing tests)`,
512
+ },
513
+ },
514
+ ],
515
+ };
516
+ }
517
+ throw new Error(`Unknown prompt: ${name}`);
518
+ });
519
+ // Handle tool calls
520
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
521
+ const { name, arguments: args } = request.params;
522
+ // Refresh cache for each request to pick up changes
523
+ invalidateCache();
524
+ try {
525
+ switch (name) {
526
+ case 'debug_mcp_environment': {
527
+ // Check for Antigravity-specific env vars
528
+ const antigravityVars = Object.entries(process.env)
529
+ .filter(([key]) => key.startsWith('GEMINI_') || key.startsWith('ANTIGRAVITY_'))
530
+ .map(([key, value]) => ` ${key}: ${value}`)
531
+ .join('\n');
532
+ // Show discovered projects from env vars
533
+ const projectList = Array.from(PROJECT_PATHS.entries())
534
+ .map(([id, path]) => ` ${id}: ${path}`)
535
+ .join('\n');
536
+ return {
537
+ content: [
538
+ {
539
+ type: 'text',
540
+ text: `# MCP Server Environment Debug Info
541
+
542
+ **process.cwd():** ${process.cwd()}
543
+ **WORKSPACE_ROOT:** ${WORKSPACE_ROOT}
544
+ **REQUIREMENTS_DIR env:** ${process.env.REQUIREMENTS_DIR || '(not set)'}
545
+ **HOME:** ${process.env.HOME || '(not set)'}
546
+ **PWD:** ${process.env.PWD || '(not set)'}
547
+ **OLDPWD:** ${process.env.OLDPWD || '(not set)'}
548
+
549
+ **Antigravity/Gemini env vars:**
550
+ ${antigravityVars || '(none found)'}
551
+
552
+ **Projects from PROJ_* env vars:**
553
+ ${projectList || '(none found)'}`,
554
+ },
555
+ ],
556
+ };
557
+ }
558
+ case 'search_requirements': {
559
+ const { query, useRegex = false, projectId } = args;
560
+ const requirements = await getRequirements(projectId);
561
+ let results;
562
+ if (useRegex) {
563
+ try {
564
+ const regex = new RegExp(query, 'i');
565
+ results = requirements.filter((r) => regex.test(r.id) ||
566
+ regex.test(r.content) ||
567
+ regex.test(r.label));
568
+ }
569
+ catch (error) {
570
+ return {
571
+ content: [
572
+ {
573
+ type: 'text',
574
+ text: `Invalid regular expression: ${error instanceof Error ? error.message : String(error)}`,
575
+ },
576
+ ],
577
+ isError: true,
578
+ };
579
+ }
580
+ }
581
+ else {
582
+ results = searchRequirements(requirements, query);
583
+ }
584
+ // Filter to root requirements only (path.length === 0)
585
+ const rootResults = results.filter((r) => r.path.length === 0);
586
+ if (rootResults.length === 0) {
587
+ return {
588
+ content: [
589
+ {
590
+ type: 'text',
591
+ text: `No requirements found matching "${query}"${useRegex ? ' (regex)' : ''}`,
592
+ },
593
+ ],
594
+ };
595
+ }
596
+ // For each root result, get the full tree
597
+ const formatted = rootResults
598
+ .map((r) => {
599
+ const tree = getRequirementTree(requirements, r.id);
600
+ const treeFormatted = formatRequirementTree(tree);
601
+ return `**${r.id}** (${r.label})\n${r.content}\n_Source: ${r.documentTitle}_\n\n\`\`\`\n${treeFormatted}\n\`\`\``;
602
+ })
603
+ .join('\n\n---\n\n');
604
+ return {
605
+ content: [
606
+ {
607
+ type: 'text',
608
+ text: `Found ${rootResults.length} requirement(s) matching "${query}"${useRegex ? ' (regex)' : ''}:\n\n${formatted}`,
609
+ },
610
+ ],
611
+ };
612
+ }
613
+ case 'get_requirement': {
614
+ const { id, projectId } = args;
615
+ const project = await getProjectFromDiscovery(projectId);
616
+ const requirements = await getRequirements(projectId);
617
+ // Get the tree starting from this ID
618
+ const tree = getRequirementTree(requirements, id);
619
+ if (tree.length === 0) {
620
+ return {
621
+ content: [
622
+ {
623
+ type: 'text',
624
+ text: `Requirement "${id}" not found`,
625
+ },
626
+ ],
627
+ };
628
+ }
629
+ const root = tree[0];
630
+ const formatted = formatRequirementTree(tree);
631
+ // Find test files that reference this requirement
632
+ const testFiles = await glob('**/*.{test,spec}.{js,jsx,ts,tsx}', {
633
+ cwd: project.path,
634
+ absolute: true,
635
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
636
+ });
637
+ const matchingFiles = findFilesWithRequirement(testFiles, id);
638
+ let testCodeSections = [];
639
+ for (const file of matchingFiles) {
640
+ const testRefs = findTestCodeForRequirement(file, id);
641
+ // Deduplicate by removing nested blocks - keep only outermost blocks
642
+ const deduplicated = testRefs.filter((ref, i) => {
643
+ // Check if this ref is contained within any other ref
644
+ const isNested = testRefs.some((other, j) => {
645
+ if (i === j)
646
+ return false;
647
+ // other contains ref if it starts before or at the same line and ends after or at the same line
648
+ return other.startLine <= ref.startLine && other.endLine >= ref.endLine &&
649
+ (other.startLine < ref.startLine || other.endLine > ref.endLine);
650
+ });
651
+ return !isNested;
652
+ });
653
+ for (const ref of deduplicated) {
654
+ testCodeSections.push(`**${ref.requirementId}** tested in \`${ref.file}:${ref.startLine}-${ref.endLine}\`:\n\n\`\`\`typescript\n${ref.code}\n\`\`\``);
655
+ }
656
+ }
657
+ const testSection = testCodeSections.length > 0
658
+ ? `\n\n## Test Coverage (${testCodeSections.length} reference(s)):\n\n${testCodeSections.join('\n\n---\n\n')}`
659
+ : '\n\n## Test Coverage\n\nNo tests found referencing this requirement.';
660
+ return {
661
+ content: [
662
+ {
663
+ type: 'text',
664
+ text: `# ${id}\n\n**Document:** ${root.documentTitle}\n**Source:** ${root.sourceFile}\n\n## Requirement Tree (${tree.length} node(s)):\n\n\`\`\`\n${formatted}\n\`\`\`${testSection}`,
665
+ },
666
+ ],
667
+ };
668
+ }
669
+ case 'list_untested_requirements': {
670
+ const { projectId } = args;
671
+ const project = await getProjectFromDiscovery(projectId);
672
+ const requirements = await getRequirements(projectId);
673
+ const referencedIds = await getReferencedRequirementIds(project.path);
674
+ // Get root requirements only for this comparison
675
+ const rootRequirements = requirements.filter((r) => r.path.length === 0);
676
+ const untested = rootRequirements.filter((r) => !referencedIds.has(r.id));
677
+ if (untested.length === 0) {
678
+ return {
679
+ content: [
680
+ {
681
+ type: 'text',
682
+ text: `All ${rootRequirements.length} requirements have test references!`,
683
+ },
684
+ ],
685
+ };
686
+ }
687
+ const formatted = untested
688
+ .map((r) => `- **${r.id}**: ${r.content}`)
689
+ .join('\n');
690
+ return {
691
+ content: [
692
+ {
693
+ type: 'text',
694
+ text: `Found ${untested.length} untested requirement(s) out of ${rootRequirements.length} total:\n\n${formatted}`,
695
+ },
696
+ ],
697
+ };
698
+ }
699
+ case 'list_all_requirements': {
700
+ const { projectId } = args;
701
+ const project = await getProjectFromDiscovery(projectId);
702
+ const requirements = await getRequirements(projectId);
703
+ if (requirements.length === 0) {
704
+ return {
705
+ content: [
706
+ {
707
+ type: 'text',
708
+ text: `No requirements found in project: ${project.path}\n\nMake sure .requirements/ directory exists with Markdown files (*.requirements.md).`,
709
+ },
710
+ ],
711
+ };
712
+ }
713
+ // Group by root ID
714
+ const grouped = new Map();
715
+ for (const req of requirements) {
716
+ const existing = grouped.get(req.rootId) || [];
717
+ existing.push(req);
718
+ grouped.set(req.rootId, existing);
719
+ }
720
+ const formatted = Array.from(grouped.entries())
721
+ .map(([rootId, reqs]) => {
722
+ const root = reqs.find((r) => r.path.length === 0);
723
+ const childCount = reqs.length - 1;
724
+ return `- **${rootId}**: ${root?.content || '(no content)'} (${childCount} children)`;
725
+ })
726
+ .join('\n');
727
+ return {
728
+ content: [
729
+ {
730
+ type: 'text',
731
+ text: `Found ${grouped.size} requirement(s) with ${requirements.length} total nodes:\n\n${formatted}`,
732
+ },
733
+ ],
734
+ };
735
+ }
736
+ case 'get_requirements_by_test': {
737
+ const { testFile, projectId } = args;
738
+ const requirements = await getRequirements(projectId);
739
+ // Find all requirement references in the test file
740
+ const { findRequirementsInFile } = await import('./grep.js');
741
+ const refs = await findRequirementsInFile(testFile);
742
+ if (refs.length === 0) {
743
+ return {
744
+ content: [
745
+ {
746
+ type: 'text',
747
+ text: `No requirement references found in "${testFile}"`,
748
+ },
749
+ ],
750
+ };
751
+ }
752
+ // For each reference, get the requirement content
753
+ const entries = refs.map((ref) => {
754
+ const req = getRequirementById(requirements, ref.requirementId);
755
+ const reqContent = req
756
+ ? `**${req.label}:** ${req.content}`
757
+ : `(requirement not found)`;
758
+ return `**Line ${ref.line}:** \`${ref.requirementId}\` - ${reqContent}`;
759
+ });
760
+ return {
761
+ content: [
762
+ {
763
+ type: 'text',
764
+ text: `Requirements referenced in "${testFile}":\n\n${entries.join('\n\n')}`,
765
+ },
766
+ ],
767
+ };
768
+ }
769
+ case 'get_tests_by_requirement': {
770
+ const { requirementsFile, projectId } = args;
771
+ const fs = await import('fs');
772
+ const path = await import('path');
773
+ const fullPath = path.resolve(WORKSPACE_ROOT, requirementsFile);
774
+ // Verify file exists
775
+ if (!fs.existsSync(fullPath)) {
776
+ return {
777
+ content: [
778
+ {
779
+ type: 'text',
780
+ text: `Requirements file not found: ${requirementsFile}`,
781
+ },
782
+ ],
783
+ isError: true,
784
+ };
785
+ }
786
+ // Verify it's a requirements file
787
+ if (!requirementsFile.endsWith('.requirements.md')) {
788
+ return {
789
+ content: [
790
+ {
791
+ type: 'text',
792
+ text: `File must be a requirements file: *.requirements.md`,
793
+ },
794
+ ],
795
+ isError: true,
796
+ };
797
+ }
798
+ // Load all requirements from this file
799
+ const requirements = await getRequirements(projectId);
800
+ const fileRequirements = requirements.filter(req => path.resolve(WORKSPACE_ROOT, req.sourceFile) === fullPath);
801
+ if (fileRequirements.length === 0) {
802
+ return {
803
+ content: [
804
+ {
805
+ type: 'text',
806
+ text: `No requirements found in "${requirementsFile}"`,
807
+ },
808
+ ],
809
+ };
810
+ }
811
+ // Get unique root requirement IDs from this file
812
+ const rootIds = new Set(fileRequirements.map(req => req.rootId));
813
+ // Find all test references in the workspace
814
+ const allTestRefs = await findAllTestReferences(WORKSPACE_ROOT);
815
+ // Build coverage map: requirement ID -> list of test references
816
+ const coverageMap = new Map();
817
+ for (const ref of allTestRefs) {
818
+ if (!coverageMap.has(ref.requirementId)) {
819
+ coverageMap.set(ref.requirementId, []);
820
+ }
821
+ coverageMap.get(ref.requirementId).push(ref);
822
+ }
823
+ // Categorize requirements as covered or not covered
824
+ const covered = [];
825
+ const notCovered = [];
826
+ for (const rootId of rootIds) {
827
+ // Check if this requirement ID or any of its children are referenced
828
+ const reqAndChildren = fileRequirements.filter(r => r.rootId === rootId);
829
+ const allIds = reqAndChildren.map(r => r.id);
830
+ const testsForThisReq = [];
831
+ for (const id of allIds) {
832
+ const refs = coverageMap.get(id);
833
+ if (refs) {
834
+ testsForThisReq.push(...refs);
835
+ }
836
+ }
837
+ if (testsForThisReq.length > 0) {
838
+ covered.push({ id: rootId, tests: testsForThisReq });
839
+ }
840
+ else {
841
+ notCovered.push(rootId);
842
+ }
843
+ }
844
+ // Format output
845
+ let output = `# Test Coverage for \`${requirementsFile}\`\n\n`;
846
+ if (covered.length > 0) {
847
+ output += `## Covered (${covered.length})\n\n`;
848
+ for (const { id, tests } of covered) {
849
+ // Group tests by file
850
+ const testsByFile = new Map();
851
+ for (const test of tests) {
852
+ if (!testsByFile.has(test.file)) {
853
+ testsByFile.set(test.file, []);
854
+ }
855
+ testsByFile.get(test.file).push(test.line);
856
+ }
857
+ output += `- **${id}**: ${tests.length} test${tests.length === 1 ? '' : 's'}\n`;
858
+ for (const [file, lines] of testsByFile.entries()) {
859
+ const relPath = path.relative(WORKSPACE_ROOT, file);
860
+ const lineList = lines.sort((a, b) => a - b).join(', ');
861
+ output += ` - \`${relPath}\` (lines ${lineList})\n`;
862
+ }
863
+ }
864
+ output += '\n';
865
+ }
866
+ if (notCovered.length > 0) {
867
+ output += `## Not Covered (${notCovered.length})\n\n`;
868
+ for (const id of notCovered) {
869
+ output += `- **${id}**: No tests found\n`;
870
+ }
871
+ }
872
+ return {
873
+ content: [
874
+ {
875
+ type: 'text',
876
+ text: output,
877
+ },
878
+ ],
879
+ };
880
+ }
881
+ case 'get_requirement_coverage': {
882
+ const { requirementKey, projectId } = args;
883
+ const project = await getProjectFromDiscovery(projectId);
884
+ const config = loadConvexConfig(project.path);
885
+ if (!config) {
886
+ return {
887
+ content: [
888
+ {
889
+ type: 'text',
890
+ text: 'Coverage queries require DOTREQUIREMENTS_PROJECT_ID and DOTREQUIREMENTS_PROJECT_SECRET to be configured in your .env file.',
891
+ },
892
+ ],
893
+ isError: true,
894
+ };
895
+ }
896
+ // AUTHZ-2.1: Check if project is local-only
897
+ if (isLocalOnlyProject(config.projectId)) {
898
+ return {
899
+ content: [
900
+ {
901
+ type: 'text',
902
+ text: CLOUD_FEATURES_REQUIRE_AUTH_MESSAGE,
903
+ },
904
+ ],
905
+ isError: true,
906
+ };
907
+ }
908
+ const coverage = await queryRequirementCoverage(requirementKey, config.projectId, config.projectSecret, config.convexUrl);
909
+ if (!coverage.lastTestedAt) {
910
+ return {
911
+ content: [
912
+ {
913
+ type: 'text',
914
+ text: `**${requirementKey}** has not been tested yet.`,
915
+ },
916
+ ],
917
+ };
918
+ }
919
+ const lastTested = new Date(coverage.lastTestedAt);
920
+ const testLocation = coverage.testFile
921
+ ? `\n**Test Location:** \`${coverage.testFile}${coverage.testLine ? `:${coverage.testLine}` : ''}\``
922
+ : '';
923
+ let branchInfo = `\n**Branch:** ${coverage.branch}`;
924
+ if (coverage.allBranches.length > 1) {
925
+ const otherBranches = coverage.allBranches
926
+ .filter((b) => b.branch !== coverage.branch)
927
+ .map((b) => `- ${b.branch}: ${new Date(b.lastTestedAt).toISOString()}`)
928
+ .join('\n');
929
+ branchInfo += `\n\n**Also tested on:**\n${otherBranches}`;
930
+ }
931
+ return {
932
+ content: [
933
+ {
934
+ type: 'text',
935
+ text: `# Coverage for ${requirementKey}\n\n**Last Tested:** ${lastTested.toISOString()}${branchInfo}${testLocation}`,
936
+ },
937
+ ],
938
+ };
939
+ }
940
+ case 'get_project_coverage_summary': {
941
+ const { branch, sinceTimestamp, projectId } = args;
942
+ const project = await getProjectFromDiscovery(projectId);
943
+ const config = loadConvexConfig(project.path);
944
+ if (!config) {
945
+ return {
946
+ content: [
947
+ {
948
+ type: 'text',
949
+ text: 'Coverage queries require DOTREQUIREMENTS_PROJECT_ID and DOTREQUIREMENTS_PROJECT_SECRET to be configured in your .env file.',
950
+ },
951
+ ],
952
+ isError: true,
953
+ };
954
+ }
955
+ // AUTHZ-2.1: Check if project is local-only
956
+ if (isLocalOnlyProject(config.projectId)) {
957
+ return {
958
+ content: [
959
+ {
960
+ type: 'text',
961
+ text: CLOUD_FEATURES_REQUIRE_AUTH_MESSAGE,
962
+ },
963
+ ],
964
+ isError: true,
965
+ };
966
+ }
967
+ const coverage = await queryProjectCoverage(config.projectId, config.projectSecret, config.convexUrl, { branch, sinceTimestamp });
968
+ const total = coverage.tested.length + coverage.untested.length;
969
+ const percentage = total > 0 ? ((coverage.tested.length / total) * 100).toFixed(1) : '0.0';
970
+ let filterInfo = '';
971
+ if (branch) {
972
+ filterInfo += `\n**Branch:** ${branch}`;
973
+ }
974
+ if (sinceTimestamp) {
975
+ filterInfo += `\n**Since:** ${new Date(sinceTimestamp).toISOString()}`;
976
+ }
977
+ let testedSection = '';
978
+ if (coverage.tested.length > 0) {
979
+ const testedList = coverage.tested
980
+ .map((t) => {
981
+ const date = new Date(t.lastTestedAt).toISOString();
982
+ const location = t.testFile
983
+ ? ` (\`${t.testFile}${t.testLine ? `:${t.testLine}` : ''}\`)`
984
+ : '';
985
+ return `- **${t.requirementKey}** [${t.branch}]: ${date}${location}`;
986
+ })
987
+ .join('\n');
988
+ testedSection = `\n\n## Tested Requirements (${coverage.tested.length})\n\n${testedList}`;
989
+ }
990
+ let untestedSection = '';
991
+ if (coverage.untested.length > 0) {
992
+ const untestedList = coverage.untested
993
+ .map((key) => `- ${key}`)
994
+ .join('\n');
995
+ untestedSection = `\n\n## Untested Requirements (${coverage.untested.length})\n\n${untestedList}`;
996
+ }
997
+ return {
998
+ content: [
999
+ {
1000
+ type: 'text',
1001
+ text: `# Project Coverage Summary\n\n**Total Requirements:** ${total}\n**Coverage:** ${percentage}%${filterInfo}${testedSection}${untestedSection}`,
1002
+ },
1003
+ ],
1004
+ };
1005
+ }
1006
+ case 'create_requirement_document': {
1007
+ const { filePath = '.requirements/example.requirements.md' } = args;
1008
+ // Discover existing label and key patterns in the codebase
1009
+ // Try to get requirements, but don't fail if no project is configured
1010
+ let requirements = [];
1011
+ try {
1012
+ requirements = await getRequirements();
1013
+ }
1014
+ catch {
1015
+ // No project configured - that's okay, we'll just not have discovered patterns
1016
+ }
1017
+ const labels = new Set();
1018
+ const keyPrefixes = new Set();
1019
+ for (const req of requirements) {
1020
+ // findRequirementsFiles now excludes example/fixture files automatically
1021
+ // Collect labels (empty string means unlabeled, so skip those)
1022
+ if (req.label && req.label.trim()) {
1023
+ labels.add(req.label);
1024
+ }
1025
+ // Collect requirement key prefixes (root IDs only)
1026
+ // Extract prefix from keys like "AUTH-1" -> "AUTH", "LOGIN-FLOW-2" -> "LOGIN-FLOW"
1027
+ if (req.path.length === 0 && req.id) {
1028
+ const lastDashIndex = req.id.lastIndexOf('-');
1029
+ if (lastDashIndex > 0) {
1030
+ const prefix = req.id.substring(0, lastDashIndex);
1031
+ keyPrefixes.add(prefix);
1032
+ }
1033
+ }
1034
+ }
1035
+ const discoveredLabels = Array.from(labels).sort();
1036
+ const discoveredPrefixes = Array.from(keyPrefixes).sort();
1037
+ let labelGuidance = '';
1038
+ let keyGuidance = '';
1039
+ // Generate label guidance
1040
+ if (discoveredLabels.length > 0) {
1041
+ const labelList = discoveredLabels.slice(0, 10).map(l => `"${l}"`).join(', ');
1042
+ const more = discoveredLabels.length > 10 ? ` (and ${discoveredLabels.length - 10} more)` : '';
1043
+ labelGuidance = `**Existing labels in this codebase:** ${labelList}${more}
1044
+
1045
+ **Use these existing labels** to maintain consistency. If you're unsure which labels to use for a new requirement, ask the user.`;
1046
+ }
1047
+ else {
1048
+ labelGuidance = `**No existing requirements found in this codebase.**
1049
+
1050
+ **Default to unlabeled requirements** (\`0. → content\`). If the user wants labels, ask them which format they prefer. Do not choose an opinionated framework like Given/When/Then without explicit user consent.`;
1051
+ }
1052
+ // Generate key guidance
1053
+ if (discoveredPrefixes.length > 0) {
1054
+ const prefixList = discoveredPrefixes.map(p => `"${p}"`).join(', ');
1055
+ keyGuidance = `**Existing requirement key prefixes in this codebase:** ${prefixList}
1056
+
1057
+ **Match the existing pattern** when creating new requirement keys. Use the same domain prefixes and sequential numbering style.`;
1058
+ }
1059
+ else {
1060
+ keyGuidance = `**No existing requirements found in this codebase.**
1061
+
1062
+ **Use concise domain prefixes** like \`AUTH-1\`, \`LOGIN-1\`, etc. Start numbering at 1 and increment sequentially.`;
1063
+ }
1064
+ const template = `---
1065
+ projectId: "your-project-id"
1066
+ pulledAt: "${new Date().toISOString()}"
1067
+ version: 1
1068
+ document:
1069
+ title: "Example Requirements"
1070
+ ---
1071
+
1072
+ # Example Requirements
1073
+
1074
+ This template demonstrates the dotrequirements Markdown format and style guidelines.
1075
+
1076
+ ## Syntax Overview
1077
+
1078
+ **File naming:** Use \`*.requirements.md\` pattern (colocated: \`auth.requirements.md\` or centralized: \`.requirements/auth.requirements.md\`)
1079
+
1080
+ **Block format:**
1081
+ \`\`\`dotrequirements
1082
+ KEY: Root requirement content
1083
+ 0. → First criterion (unlabeled)
1084
+ 1. Label → Second criterion (with label)
1085
+ 1.0. → Nested criterion (unlabeled)
1086
+ \`\`\`
1087
+
1088
+ - First line: \`KEY: content\` (requirement key and description)
1089
+ - Criteria: \`position. Label → content\` or \`position. → content\` (unlabeled)
1090
+ - Position: \`0\`, \`1\`, \`2\` (top-level) or \`0.0\`, \`1.0\` (nested) - defines hierarchy
1091
+ - Delimiter: \`→\` or \`->\` separates optional label from content
1092
+
1093
+ ## Requirement Keys: Concise and Sequential
1094
+
1095
+ ${keyGuidance}
1096
+
1097
+ **Key format:** \`DOMAIN-FEATURE-N\` where N is sequential (1, 2, 3...)
1098
+
1099
+ **Best practices:**
1100
+ 1. **Concise domains** - Use short, clear prefixes
1101
+ - ✅ \`AUTHZ-1\` (authorization)
1102
+ - ✅ \`AUTH-1\` (authentication)
1103
+ - ❌ \`AUTHORIZATION-1\` (too verbose)
1104
+ - ❌ \`REQ-IDENTITY-ACCESS-AUTHZ-1\` (too nested)
1105
+
1106
+ 2. **Sequential, 1-indexed numbering** - Start at 1, no padding
1107
+ - ✅ \`LOGIN-1\`, \`LOGIN-2\`, \`LOGIN-3\`
1108
+ - ❌ \`LOGIN-0\` (don't use 0-indexing for requirement IDs)
1109
+ - ❌ \`LOGIN-001\` (no zero-padding)
1110
+
1111
+ 3. **Unique across project** - Each key must be unique in the entire project
1112
+
1113
+ 4. **Match existing patterns** - Check existing requirements first and follow their convention
1114
+
1115
+ ## Labels: Match Your Codebase or Ask the User
1116
+
1117
+ ${labelGuidance}
1118
+
1119
+ ## Style Principles for Requirements
1120
+
1121
+ **1. Use Concrete Examples**: Replace vague language with specific, testable conditions.
1122
+ - ❌ "users can log in" or "works properly"
1123
+ - ✅ "When a registered user provides valid credentials, they are authenticated"
1124
+
1125
+ **2. Write Natural, Concise Prose**: Avoid terseness and verbosity. Use declarative style (not "should").
1126
+ - ❌ "registered user with valid credentials is authenticated" (too terse)
1127
+ - ❌ "A registered user, whose account was created on Tuesday and whose life story is as follows..." (too verbose)
1128
+ - ✅ "When a registered user provides valid credentials, they are authenticated"
1129
+
1130
+ **3. Keep Arrange/Act/Assert in Mind**: Well-written requirements describe preconditions, trigger, and result.
1131
+ - ✅ "When [preconditions:] a registered user [trigger:] provides valid credentials, [result:] they are authenticated"
1132
+
1133
+ **4. Be Framework Neutral**: Don't prescribe Given/When/Then vs AC vs other formats - focus on content quality.
1134
+
1135
+ **5. Use Named Personas**: Establish personas in parent requirements, reuse in children.
1136
+ - ✅ Parent: "A registered user, Jamie, can log in normally" → Child: "When Jamie provides valid credentials, they are authenticated"
1137
+
1138
+ **6. Use User-Centric Language**: Describe user experience, not technical internals.
1139
+ - ❌ "they are redirected to app.dotrequirements.io/redirect/dashboard"
1140
+ - ✅ "they are automatically brought to the dashboard"
1141
+
1142
+ **7. Single Action Per Requirement**: Don't chain multiple actions with "and then".
1143
+ - ❌ "When Robin provides credentials, requests an OTP, then provides the OTP..."
1144
+ - ✅ Break into separate requirements for each action
1145
+
1146
+ **8. Each Requirement Should Be Independent**: Requirements within a block should be independently testable. If they share preconditions or form a sequence, either nest them or restate context.
1147
+ - ❌ "0. → When Jordan submits signup, an account is created" / "1. → Welcome email is sent" / "2. → Dashboard appears"
1148
+ - ✅ Option A: Restate context - "1. → When Jordan submits signup, a welcome email is sent"
1149
+ - ✅ Option B: Use nesting - "0. When → Jordan submits signup" / " 0.0. Then → an account is created"
1150
+ - Test: Can you understand what's being tested by reading just one requirement, or do you need to read its siblings?
1151
+
1152
+ **9. Focus on Behavior, Not Design**: Describe what happens, not UI specifics.
1153
+ - ❌ "enters valid credentials into two single-line input fields and presses a green button"
1154
+ - ✅ "provides valid credentials"
1155
+
1156
+ **10. Focus on Outcomes, Not Implementation**: User perspective, even for technical requirements.
1157
+ - ❌ "When the app requests that Twilio send Casey an OTP from /email/POST endpoint..."
1158
+ - ✅ "When Casey requests an email OTP..."
1159
+ - Note: Even technical requirements can be user-centric: "95 percent of users experience under 1 second of delay"
1160
+
1161
+ **11. Decompose Large Requirements**: If it can't be validated with a single test, break it down.
1162
+
1163
+ ## Example Requirements
1164
+
1165
+ **Unlabeled (recommended default):**
1166
+ \`\`\`dotrequirements
1167
+ AUTH-LOGIN-1: A registered user, Jamie, can log in to their account
1168
+ 0. → When Jamie provides their registered email and correct password, they are authenticated and brought to their dashboard
1169
+ 1. → When Jamie provides an incorrect password, they see an error message and remain on the login page
1170
+ 2. → When Jamie's account has been deactivated, they see a message explaining their account status
1171
+ \`\`\`
1172
+
1173
+ **With labels (example only - DO NOT use opinionated formats like Given/When/Then without asking the user first):**
1174
+ \`\`\`dotrequirements
1175
+ PAYMENT-REFUND-1: A customer, Alex, receives a refund after returning an item
1176
+ 0. Given → Alex purchased a laptop from the store 10 days ago
1177
+ 1. Given → Alex initiates a return through their order history
1178
+ 2. When → Alex's returned laptop is received and inspected at the warehouse
1179
+ 3. Then → Alex receives a refund to their original payment method within 5 business days
1180
+ 4. Then → Alex receives an email confirmation with the refund amount and expected timeline
1181
+ \`\`\`
1182
+
1183
+ ## Referencing Requirements in Tests
1184
+
1185
+ **Use \`requirement()\` as the test description** - it returns a string:
1186
+
1187
+ \`\`\`typescript
1188
+ import { describe, it, expect } from 'vitest';
1189
+ import { requirement } from '@popoverai/dotrequirements/test';
1190
+
1191
+ describe(requirement('AUTH-LOGIN-1'), () => {
1192
+ describe(requirement('AUTH-LOGIN-1.given'), () => {
1193
+ // Arrange: Create Jamie's account
1194
+ });
1195
+
1196
+ describe(requirement('AUTH-LOGIN-1.when'), () => {
1197
+ // Act: Submit login with valid credentials
1198
+
1199
+ it(requirement('AUTH-LOGIN-1.then'), () => {
1200
+ // Assert: Jamie is authenticated
1201
+ });
1202
+ });
1203
+ });
1204
+ \`\`\`
1205
+
1206
+ **Test Style Principles:**
1207
+
1208
+ **1. Use requirement() AS the description**: Don't put requirement() inside test body or in comments.
1209
+ - ✅ \`test(requirement('AUTH-LOGIN-1'), () => { /* test code */ })\`
1210
+ - ✅ \`it(requirement('LOGIN-1.then'), () => { /* assert */ })\`
1211
+ - ❌ \`test("user can log in", () => { requirement('AUTH-LOGIN-1'); /* test code */ })\`
1212
+ - ❌ \`// LOGIN-1: User can log in\` (comment instead of using requirement() as description)
1213
+
1214
+ **2. Comments describe the TEST, not the requirement**: Don't copy requirement text verbatim.
1215
+ - ✅ \`describe(requirement('REQ-1.0'), () => { // registered user, valid credentials\`
1216
+ - ❌ \`describe(requirement('REQ-1.0'), () => { // 0. When a registered user provides valid credentials, they are authenticated\` (verbatim copy is a red flag)
1217
+ - Note: Comments should reflect what the test actually does, not just repeat what the requirement says
1218
+
1219
+ **3. Structure tests to match requirements**: Nest describe/it blocks for structured requirements.
1220
+ - ✅ For structured requirements: \`describe(requirement('LOGIN-1.given'))\` nested with \`describe(requirement('LOGIN-1.when'))\` and \`it(requirement('LOGIN-1.then'))\`
1221
+ - ✅ For simple requirements: \`test(requirement('AUTH-LOGIN-1'), () => { /* arrange, act, assert all in one */ })\`
1222
+
1223
+ **Path formats:**
1224
+ - \`requirement('AUTH-LOGIN-1')\` - root requirement
1225
+ - \`requirement('AUTH-LOGIN-1.0')\` - by numeric position
1226
+ - \`requirement('AUTH-LOGIN-1.given')\` - by label (case-insensitive)
1227
+ - \`requirement('AUTH-LOGIN-1.given#1')\` - disambiguate duplicate labels`;
1228
+ return {
1229
+ content: [
1230
+ {
1231
+ type: 'text',
1232
+ text: `# Requirements File Template\n\nHere's a comprehensive template for \`${filePath}\` with format and style guidance:\n\n\`\`\`markdown\n${template}\`\`\`\n\n## Next Steps\n\n1. **Create file**: Use Write tool to create \`${filePath}\` based on this template\n2. **Refine style** (optional): Call \`style_check\` for AI feedback on style and best practices\n3. **Validate syntax**: Call \`validate_requirements\` to verify format is correct\n4. **Push to Convex**: Call \`push_requirements\` to sync requirements to data layer\n\n**Note**: Requirements files can be colocated with code (\`src/auth.requirements.md\`) or centralized in \`.requirements/\` directory.`,
1233
+ },
1234
+ ],
1235
+ };
1236
+ }
1237
+ case 'validate_requirements': {
1238
+ const { filePath } = args;
1239
+ const { parseRequirementsFromFile, validateForPush } = await import('../schema/index.js');
1240
+ const fs = await import('fs');
1241
+ const path = await import('path');
1242
+ const fullPath = path.resolve(WORKSPACE_ROOT, filePath);
1243
+ if (!fs.existsSync(fullPath)) {
1244
+ return {
1245
+ content: [
1246
+ {
1247
+ type: 'text',
1248
+ text: `File not found: ${filePath}`,
1249
+ },
1250
+ ],
1251
+ isError: true,
1252
+ };
1253
+ }
1254
+ try {
1255
+ const parsed = parseRequirementsFromFile(fullPath);
1256
+ const reqCount = Object.keys(parsed.requirements).length;
1257
+ // Check push readiness
1258
+ const pushValidation = validateForPush(parsed.metadata);
1259
+ let headline;
1260
+ let pushDetails = '';
1261
+ if (!pushValidation.valid) {
1262
+ headline = `❌ \`${filePath}\` - Not push-ready\n- ${pushValidation.reason}`;
1263
+ }
1264
+ else {
1265
+ const actionLabel = pushValidation.action === 'create' ? 'Will create new document' : 'Will update existing document';
1266
+ headline = `✓ \`${filePath}\` - ${actionLabel}`;
1267
+ if (pushValidation.warning) {
1268
+ pushDetails = `\n- ⚠️ ${pushValidation.warning}`;
1269
+ }
1270
+ }
1271
+ return {
1272
+ content: [
1273
+ {
1274
+ type: 'text',
1275
+ text: `${headline}${pushDetails}\n\n**Metadata:**\n- Project ID: ${parsed.metadata.projectId}\n- Version: ${parsed.metadata.version}\n- Document: ${parsed.metadata.document?.title || '(none)'}\n- Document ID: ${parsed.metadata.document?.id || '(none)'}\n\n**Requirements:** ${reqCount} root requirement(s) found`,
1276
+ },
1277
+ ],
1278
+ };
1279
+ }
1280
+ catch (error) {
1281
+ return {
1282
+ content: [
1283
+ {
1284
+ type: 'text',
1285
+ text: `✗ Validation failed for \`${filePath}\`:\n\n${error instanceof Error ? error.message : String(error)}`,
1286
+ },
1287
+ ],
1288
+ isError: true,
1289
+ };
1290
+ }
1291
+ }
1292
+ case 'push_requirements': {
1293
+ const { filePath, confirmed = false, projectId: projectIdParam } = args;
1294
+ const fs = await import('fs');
1295
+ const path = await import('path');
1296
+ const { parseRequirementsFromFile, getAllRequirements } = await import('../schema/index.js');
1297
+ const { ConvexHttpClient } = await import('convex/browser');
1298
+ // Get project
1299
+ const project = await getProjectFromDiscovery(projectIdParam);
1300
+ const config = loadConvexConfig(project.path);
1301
+ if (!config) {
1302
+ return {
1303
+ content: [
1304
+ {
1305
+ type: 'text',
1306
+ text: 'Push requires environment variables:\n- DOTREQUIREMENTS_PROJECT_ID\n- DOTREQUIREMENTS_PROJECT_SECRET',
1307
+ },
1308
+ ],
1309
+ isError: true,
1310
+ };
1311
+ }
1312
+ const convexUrl = config.convexUrl;
1313
+ const projectId = config.projectId;
1314
+ const projectSecret = config.projectSecret;
1315
+ // AUTHZ-2.1: Check if project is local-only
1316
+ if (isLocalOnlyProject(projectId)) {
1317
+ return {
1318
+ content: [
1319
+ {
1320
+ type: 'text',
1321
+ text: CLOUD_FEATURES_REQUIRE_AUTH_MESSAGE,
1322
+ },
1323
+ ],
1324
+ isError: true,
1325
+ };
1326
+ }
1327
+ // Determine files to push
1328
+ const requirementsDir = path.resolve(project.path, '.requirements');
1329
+ let filesToPush;
1330
+ if (filePath) {
1331
+ const fullPath = path.resolve(project.path, filePath);
1332
+ if (!fs.existsSync(fullPath)) {
1333
+ return {
1334
+ content: [
1335
+ {
1336
+ type: 'text',
1337
+ text: `File not found: ${filePath}`,
1338
+ },
1339
+ ],
1340
+ isError: true,
1341
+ };
1342
+ }
1343
+ filesToPush = [fullPath];
1344
+ }
1345
+ else {
1346
+ if (!fs.existsSync(requirementsDir)) {
1347
+ return {
1348
+ content: [
1349
+ {
1350
+ type: 'text',
1351
+ text: 'No .requirements/ directory found',
1352
+ },
1353
+ ],
1354
+ isError: true,
1355
+ };
1356
+ }
1357
+ const files = fs.readdirSync(requirementsDir);
1358
+ filesToPush = files
1359
+ .filter((f) => f.endsWith('.requirements.md'))
1360
+ .map((f) => path.join(requirementsDir, f));
1361
+ if (filesToPush.length === 0) {
1362
+ return {
1363
+ content: [
1364
+ {
1365
+ type: 'text',
1366
+ text: 'No Markdown files (*.requirements.md) found in .requirements/',
1367
+ },
1368
+ ],
1369
+ isError: true,
1370
+ };
1371
+ }
1372
+ }
1373
+ // Parse local requirements
1374
+ const allLocalRequirements = [];
1375
+ for (const file of filesToPush) {
1376
+ const parsed = parseRequirementsFromFile(file);
1377
+ const flatReqs = getAllRequirements(parsed.requirements);
1378
+ for (const req of flatReqs) {
1379
+ allLocalRequirements.push({
1380
+ key: req.id,
1381
+ label: req.label,
1382
+ content: req.content,
1383
+ rootId: req.metadata?.rootId,
1384
+ position: req.metadata?.position,
1385
+ });
1386
+ }
1387
+ }
1388
+ if (!confirmed) {
1389
+ // First call: show diff
1390
+ const client = new ConvexHttpClient(convexUrl);
1391
+ // Note: We need a public query to fetch requirements
1392
+ // For now, just show what would be pushed
1393
+ return {
1394
+ content: [
1395
+ {
1396
+ type: 'text',
1397
+ text: `# Push Preview\n\n**Files:** ${filesToPush.length}\n**Requirements:** ${allLocalRequirements.length}\n\n**Requirements to push:**\n${allLocalRequirements.map((r) => `- ${r.key}: ${r.label} → ${r.content}`).join('\n')}\n\n⚠️ This will **replace all** requirements in the project.\n\n**To proceed:** Call this tool again with \`confirmed: true\``,
1398
+ },
1399
+ ],
1400
+ };
1401
+ }
1402
+ // Second call: execute push
1403
+ const client = new ConvexHttpClient(convexUrl);
1404
+ try {
1405
+ const result = await client.mutation('requirements/mutations:bulkReplace', {
1406
+ projectAuth: {
1407
+ projectSlug: projectId,
1408
+ projectSecret,
1409
+ },
1410
+ target: { type: 'project', slug: projectId },
1411
+ requirements: allLocalRequirements,
1412
+ });
1413
+ return {
1414
+ content: [
1415
+ {
1416
+ type: 'text',
1417
+ text: `✓ Push complete!\n\n**Inserted:** ${result.inserted}\n**Deleted:** ${result.deleted}`,
1418
+ },
1419
+ ],
1420
+ };
1421
+ }
1422
+ catch (error) {
1423
+ return {
1424
+ content: [
1425
+ {
1426
+ type: 'text',
1427
+ text: `✗ Push failed:\n\n${error instanceof Error ? error.message : String(error)}`,
1428
+ },
1429
+ ],
1430
+ isError: true,
1431
+ };
1432
+ }
1433
+ }
1434
+ case 'style_check': {
1435
+ const { filePath, model } = args;
1436
+ const fs = await import('fs');
1437
+ const path = await import('path');
1438
+ const fullPath = path.resolve(WORKSPACE_ROOT, filePath);
1439
+ if (!fs.existsSync(fullPath)) {
1440
+ return {
1441
+ content: [
1442
+ {
1443
+ type: 'text',
1444
+ text: `File not found: ${filePath}`,
1445
+ },
1446
+ ],
1447
+ isError: true,
1448
+ };
1449
+ }
1450
+ // Read file contents
1451
+ const fileContents = fs.readFileSync(fullPath, 'utf-8');
1452
+ // Detect file type
1453
+ const isRequirementsFile = filePath.endsWith('.requirements.md');
1454
+ const isTestFile = /\.(test|spec)\.(js|jsx|ts|tsx)$/.test(filePath);
1455
+ if (!isRequirementsFile && !isTestFile) {
1456
+ return {
1457
+ content: [
1458
+ {
1459
+ type: 'text',
1460
+ text: `Unsupported file type. File must be:\n- Requirements file: *.requirements.md\n- Test file: *.test.{js,jsx,ts,tsx} or *.spec.{js,jsx,ts,tsx}`,
1461
+ },
1462
+ ],
1463
+ isError: true,
1464
+ };
1465
+ }
1466
+ const fileType = isRequirementsFile ? 'requirements' : 'test';
1467
+ // Get Convex config for credentials - walk up from file's directory to find project
1468
+ const fileDir = path.dirname(fullPath);
1469
+ let project;
1470
+ try {
1471
+ // Try to find a project starting from the file's directory
1472
+ const result = await discoverProjects(fileDir);
1473
+ if (result.type === 'none') {
1474
+ // If no project found from file dir, try from WORKSPACE_ROOT
1475
+ project = await getProjectFromDiscovery();
1476
+ }
1477
+ else if (result.type === 'single') {
1478
+ project = result.project;
1479
+ }
1480
+ else {
1481
+ // Multiple projects - can't auto-detect which one to use
1482
+ throw new Error('Multiple projects found - cannot auto-detect for this file');
1483
+ }
1484
+ }
1485
+ catch (error) {
1486
+ return {
1487
+ content: [
1488
+ {
1489
+ type: 'text',
1490
+ text: `Style check requires DOTREQUIREMENTS_PROJECT_ID and DOTREQUIREMENTS_PROJECT_SECRET to be configured in your .env or .env.local file.\n\nError: ${error instanceof Error ? error.message : String(error)}`,
1491
+ },
1492
+ ],
1493
+ isError: true,
1494
+ };
1495
+ }
1496
+ const config = loadConvexConfig(project.path);
1497
+ if (!config) {
1498
+ return {
1499
+ content: [
1500
+ {
1501
+ type: 'text',
1502
+ text: 'Style check requires DOTREQUIREMENTS_PROJECT_ID and DOTREQUIREMENTS_PROJECT_SECRET to be configured in your .env or .env.local file.',
1503
+ },
1504
+ ],
1505
+ isError: true,
1506
+ };
1507
+ }
1508
+ // AUTHZ-2.1: Check if project is local-only
1509
+ if (isLocalOnlyProject(config.projectId)) {
1510
+ return {
1511
+ content: [
1512
+ {
1513
+ type: 'text',
1514
+ text: CLOUD_FEATURES_REQUIRE_AUTH_MESSAGE,
1515
+ },
1516
+ ],
1517
+ isError: true,
1518
+ };
1519
+ }
1520
+ // Call Vercel API endpoint for style checking
1521
+ // Default to production, allow override via env var for local dev
1522
+ const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL || 'https://app.dotrequirements.io';
1523
+ try {
1524
+ const response = await fetch(`${apiBaseUrl}/api/style-check`, {
1525
+ method: 'POST',
1526
+ headers: {
1527
+ 'Content-Type': 'application/json',
1528
+ },
1529
+ body: JSON.stringify({
1530
+ projectId: config.projectId,
1531
+ projectSecret: config.projectSecret,
1532
+ fileContents,
1533
+ fileType,
1534
+ model,
1535
+ }),
1536
+ });
1537
+ if (!response.ok) {
1538
+ const errorData = await response.json();
1539
+ return {
1540
+ content: [
1541
+ {
1542
+ type: 'text',
1543
+ text: `Style check failed: ${errorData.error || response.statusText}`,
1544
+ },
1545
+ ],
1546
+ isError: true,
1547
+ };
1548
+ }
1549
+ const data = await response.json();
1550
+ return {
1551
+ content: [
1552
+ {
1553
+ type: 'text',
1554
+ text: `# Style Check Results for \`${filePath}\`\n\n${data.feedback}`,
1555
+ },
1556
+ ],
1557
+ };
1558
+ }
1559
+ catch (error) {
1560
+ return {
1561
+ content: [
1562
+ {
1563
+ type: 'text',
1564
+ text: `Style check error: ${error instanceof Error ? error.message : String(error)}`,
1565
+ },
1566
+ ],
1567
+ isError: true,
1568
+ };
1569
+ }
1570
+ }
1571
+ case 'review_test': {
1572
+ const { testFilePath, projectId } = args;
1573
+ const fs = await import('fs');
1574
+ const path = await import('path');
1575
+ const fullPath = path.resolve(WORKSPACE_ROOT, testFilePath);
1576
+ if (!fs.existsSync(fullPath)) {
1577
+ return {
1578
+ content: [
1579
+ {
1580
+ type: 'text',
1581
+ text: `File not found: ${testFilePath}`,
1582
+ },
1583
+ ],
1584
+ isError: true,
1585
+ };
1586
+ }
1587
+ // Verify it's a test file
1588
+ const isTestFile = /\.(test|spec)\.(js|jsx|ts|tsx)$/.test(testFilePath);
1589
+ if (!isTestFile) {
1590
+ return {
1591
+ content: [
1592
+ {
1593
+ type: 'text',
1594
+ text: `File must be a test file: *.test.{js,jsx,ts,tsx} or *.spec.{js,jsx,ts,tsx}`,
1595
+ },
1596
+ ],
1597
+ isError: true,
1598
+ };
1599
+ }
1600
+ // Read test file contents
1601
+ const testFileContents = fs.readFileSync(fullPath, 'utf-8');
1602
+ // Extract requirement IDs from the test file using regex
1603
+ const requirementIdMatches = testFileContents.matchAll(/requirement\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g);
1604
+ const requirementIds = new Set();
1605
+ for (const match of requirementIdMatches) {
1606
+ requirementIds.add(match[1]);
1607
+ }
1608
+ // Get project
1609
+ let project;
1610
+ try {
1611
+ project = await getProjectFromDiscovery(projectId);
1612
+ }
1613
+ catch (error) {
1614
+ return {
1615
+ content: [
1616
+ {
1617
+ type: 'text',
1618
+ text: `Test review requires a configured dotrequirements project.\n\nError: ${error instanceof Error ? error.message : String(error)}`,
1619
+ },
1620
+ ],
1621
+ isError: true,
1622
+ };
1623
+ }
1624
+ // Load all requirements for this project
1625
+ const allRequirements = await getRequirements(projectId);
1626
+ // Group requirement IDs by their root to avoid sending duplicate trees
1627
+ // e.g., NAV-3.0, NAV-3.1, NAV-3.2 all belong to root NAV-3
1628
+ const rootToTestedIds = new Map();
1629
+ for (const reqId of requirementIds) {
1630
+ const req = allRequirements.find(r => r.id === reqId || r.rootId === reqId);
1631
+ if (req) {
1632
+ if (!rootToTestedIds.has(req.rootId)) {
1633
+ rootToTestedIds.set(req.rootId, new Set());
1634
+ }
1635
+ rootToTestedIds.get(req.rootId).add(reqId);
1636
+ }
1637
+ }
1638
+ // Build requirements array with one entry per unique root tree
1639
+ const requirements = [];
1640
+ for (const [rootId, testedIds] of rootToTestedIds) {
1641
+ const tree = getRequirementTree(allRequirements, rootId);
1642
+ const formattedTree = formatRequirementTree(tree);
1643
+ requirements.push({
1644
+ id: rootId,
1645
+ content: formattedTree,
1646
+ testedIds: Array.from(testedIds).sort(),
1647
+ });
1648
+ }
1649
+ // Get Convex config
1650
+ const config = loadConvexConfig(project.path);
1651
+ if (!config) {
1652
+ return {
1653
+ content: [
1654
+ {
1655
+ type: 'text',
1656
+ text: 'Test review requires DOTREQUIREMENTS_PROJECT_ID and DOTREQUIREMENTS_PROJECT_SECRET to be configured in your .env or .env.local file.',
1657
+ },
1658
+ ],
1659
+ isError: true,
1660
+ };
1661
+ }
1662
+ // AUTHZ-2.1: Check if project is local-only
1663
+ if (isLocalOnlyProject(config.projectId)) {
1664
+ return {
1665
+ content: [
1666
+ {
1667
+ type: 'text',
1668
+ text: CLOUD_FEATURES_REQUIRE_AUTH_MESSAGE,
1669
+ },
1670
+ ],
1671
+ isError: true,
1672
+ };
1673
+ }
1674
+ // Call Vercel API endpoint for test review
1675
+ // Default to production, allow override via env var for local dev
1676
+ const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL || 'https://app.dotrequirements.io';
1677
+ try {
1678
+ const response = await fetch(`${apiBaseUrl}/api/review-test`, {
1679
+ method: 'POST',
1680
+ headers: {
1681
+ 'Content-Type': 'application/json',
1682
+ },
1683
+ body: JSON.stringify({
1684
+ projectId: config.projectId,
1685
+ projectSecret: config.projectSecret,
1686
+ testFileContents,
1687
+ requirements,
1688
+ }),
1689
+ });
1690
+ if (!response.ok) {
1691
+ const errorData = await response.json();
1692
+ return {
1693
+ content: [
1694
+ {
1695
+ type: 'text',
1696
+ text: `Test review failed: ${errorData.error || response.statusText}`,
1697
+ },
1698
+ ],
1699
+ isError: true,
1700
+ };
1701
+ }
1702
+ const data = await response.json();
1703
+ return {
1704
+ content: [
1705
+ {
1706
+ type: 'text',
1707
+ text: `# Test Review Results for \`${testFilePath}\`\n\n${data.feedback}`,
1708
+ },
1709
+ ],
1710
+ };
1711
+ }
1712
+ catch (error) {
1713
+ return {
1714
+ content: [
1715
+ {
1716
+ type: 'text',
1717
+ text: `Test review error: ${error instanceof Error ? error.message : String(error)}`,
1718
+ },
1719
+ ],
1720
+ isError: true,
1721
+ };
1722
+ }
1723
+ }
1724
+ default:
1725
+ return {
1726
+ content: [
1727
+ {
1728
+ type: 'text',
1729
+ text: `Unknown tool: ${name}`,
1730
+ },
1731
+ ],
1732
+ isError: true,
1733
+ };
1734
+ }
1735
+ }
1736
+ catch (error) {
1737
+ return {
1738
+ content: [
1739
+ {
1740
+ type: 'text',
1741
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
1742
+ },
1743
+ ],
1744
+ isError: true,
1745
+ };
1746
+ }
1747
+ });
1748
+ // Start server
1749
+ async function main() {
1750
+ const transport = new StdioServerTransport();
1751
+ await server.connect(transport);
1752
+ console.error('dot•requirements MCP server running');
1753
+ console.error('WORKSPACE_ROOT:', WORKSPACE_ROOT);
1754
+ console.error('process.cwd():', process.cwd());
1755
+ console.error('REQUIREMENTS_DIR env:', process.env.REQUIREMENTS_DIR);
1756
+ }
1757
+ main().catch(console.error);
1758
+ //# sourceMappingURL=index.js.map