@popoverai/dotrequirements 0.14.0 → 0.16.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.
@@ -2,7 +2,7 @@ import { getProjectCredentials } from '../utils/project-settings.js';
2
2
  /**
3
3
  * Production Convex deployment URL
4
4
  */
5
- const CONVEX_URL = 'https://data.dotrequirements.io';
5
+ export const CONVEX_URL = 'https://data.dotrequirements.io';
6
6
  /**
7
7
  * Load Convex configuration from project settings.
8
8
  * Returns null if credentials are not configured.
@@ -47,7 +47,11 @@ export async function getRequirementCoverage(requirementKey, projectId, projectS
47
47
  if (!response.ok) {
48
48
  throw new Error(`Failed to query coverage: ${response.statusText}`);
49
49
  }
50
- return await response.json();
50
+ const json = await response.json();
51
+ if (json.status === 'error') {
52
+ throw new Error(`Convex query failed: ${json.errorMessage}`);
53
+ }
54
+ return json.value;
51
55
  }
52
56
  /**
53
57
  * Query Convex for project coverage summary
@@ -76,6 +80,50 @@ export async function getProjectCoverage(projectId, projectSecret, convexUrl, op
76
80
  if (!response.ok) {
77
81
  throw new Error(`Failed to query coverage: ${response.statusText}`);
78
82
  }
79
- return await response.json();
83
+ const json = await response.json();
84
+ if (json.status === 'error') {
85
+ throw new Error(`Convex query failed: ${json.errorMessage}`);
86
+ }
87
+ return json.value;
88
+ }
89
+ /**
90
+ * PROJ-CONTEXT-7: Query Convex for project context (AI style guidelines)
91
+ */
92
+ export async function getProjectContext(projectId, projectSecret, convexUrl) {
93
+ try {
94
+ const response = await fetch(`${convexUrl}/api/query`, {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({
98
+ path: 'projects/queries:get',
99
+ args: {
100
+ projectAuth: {
101
+ projectSlug: projectId,
102
+ projectSecret,
103
+ },
104
+ target: {
105
+ type: 'project',
106
+ slug: projectId,
107
+ },
108
+ },
109
+ format: 'json',
110
+ }),
111
+ });
112
+ if (!response.ok) {
113
+ return null;
114
+ }
115
+ const json = await response.json();
116
+ if (json.status === 'error' || !json.value) {
117
+ return null;
118
+ }
119
+ return {
120
+ projectContext: json.value.projectContext ?? null,
121
+ requirementsStyleContext: json.value.requirementsStyleContext ?? null,
122
+ };
123
+ }
124
+ catch {
125
+ // Non-fatal: return null if query fails
126
+ return null;
127
+ }
80
128
  }
81
129
  //# sourceMappingURL=convexClient.js.map
package/dist/mcp/index.js CHANGED
@@ -2,15 +2,18 @@
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
- import { loadAllRequirements, searchRequirements, getRequirementById, getRequirementTree, formatRequirementTree, } from './requirements.js';
5
+ import { loadAllRequirements, searchRequirements, getRequirementById, getRequirementTree, formatRequirementTree, filterRequirementsByKeys, } from './requirements.js';
6
6
  import { findAllTestReferences, getReferencedRequirementIds, } from './grep.js';
7
7
  import { findTestCodeForRequirement, findFilesWithRequirement, } from './testCodeExtractor.js';
8
8
  import { glob } from 'glob';
9
- import { loadConvexConfig, getRequirementCoverage as queryRequirementCoverage, getProjectCoverage as queryProjectCoverage, } from './convexClient.js';
9
+ import { CONVEX_URL, getRequirementCoverage as queryRequirementCoverage, getProjectCoverage as queryProjectCoverage, getProjectContext, } from './convexClient.js';
10
10
  import { discoverProjects, resolveProject, } from '../utils/project-discovery.js';
11
- import { readFileSync } from 'fs';
11
+ import { getCredentialsFromEnv } from '../utils/project-settings.js';
12
+ import { readFileSync, existsSync } from 'fs';
12
13
  import { fileURLToPath } from 'url';
13
- import { dirname, join } from 'path';
14
+ import { dirname, join, resolve, basename } from 'path';
15
+ import { parseFilesForPush, dryRunPush, executePush, } from '../push/index.js';
16
+ import { findRequirementsFiles } from './requirements.js';
14
17
  // Read version from package.json
15
18
  const __filename = fileURLToPath(import.meta.url);
16
19
  const __dirname = dirname(__filename);
@@ -32,6 +35,9 @@ function getProjectPathsFromEnv() {
32
35
  // Get workspace root from environment or default to cwd
33
36
  const WORKSPACE_ROOT = process.env.REQUIREMENTS_DIR || process.cwd();
34
37
  const PROJECT_PATHS = getProjectPathsFromEnv();
38
+ // Parse --auth-from-env flag for CI/CD environments
39
+ // When set, credentials are read from DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET env vars
40
+ const USE_ENV_AUTH = process.argv.includes('--auth-from-env');
35
41
  const STYLE_CHECK_GUIDANCE = `
36
42
 
37
43
  ---
@@ -57,6 +63,20 @@ async function getRequirements(projectId) {
57
63
  }
58
64
  async function getProjectFromDiscovery(projectId) {
59
65
  const { isConfiguredProject } = await import('../utils/project-discovery.js');
66
+ // If --auth-from-env flag is set, use credentials from environment variables
67
+ // This is the explicit opt-in for CI/CD environments
68
+ if (USE_ENV_AUTH) {
69
+ const envCredentials = getCredentialsFromEnv();
70
+ if (!envCredentials) {
71
+ throw new Error('--auth-from-env flag requires DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET environment variables to be set');
72
+ }
73
+ // Return a synthetic project using env credentials and workspace root
74
+ return {
75
+ path: WORKSPACE_ROOT,
76
+ projectId: envCredentials.projectId,
77
+ projectSecret: envCredentials.projectSecret,
78
+ };
79
+ }
60
80
  // If we have PROJ_* env vars, use those instead of filesystem discovery
61
81
  if (PROJECT_PATHS.size > 0) {
62
82
  const projects = [];
@@ -300,7 +320,7 @@ const tools = [
300
320
  },
301
321
  {
302
322
  name: 'style_check',
303
- 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 project to be linked to cloud (run `dotrequirements link`).',
323
+ 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.*). For requirements files, you can optionally specify requirement keys to check only those requirements instead of the entire file. Requires project to be linked to cloud (run `dotrequirements link`).',
304
324
  inputSchema: {
305
325
  type: 'object',
306
326
  properties: {
@@ -308,6 +328,11 @@ const tools = [
308
328
  type: 'string',
309
329
  description: 'Path to the file to check (e.g., ".requirements/auth.requirements.md" or "src/auth.test.ts")',
310
330
  },
331
+ requirementKeys: {
332
+ type: 'array',
333
+ items: { type: 'string' },
334
+ description: 'Optional: Array of requirement keys to check (e.g., ["AUTH-1", "AUTH-2"]). Only valid for requirements files (*.requirements.md). When provided, only these requirements are checked instead of the entire file.',
335
+ },
311
336
  model: {
312
337
  type: 'string',
313
338
  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"',
@@ -899,19 +924,7 @@ ${projectList || '(none found)'}`,
899
924
  case 'get_requirement_coverage': {
900
925
  const { requirementKey, projectId } = args;
901
926
  const project = await getProjectFromDiscovery(projectId);
902
- const config = loadConvexConfig(project.path);
903
- if (!config) {
904
- return {
905
- content: [
906
- {
907
- type: 'text',
908
- text: 'Coverage queries require project credentials. Run `dotrequirements link` to connect to cloud.',
909
- },
910
- ],
911
- isError: true,
912
- };
913
- }
914
- const coverage = await queryRequirementCoverage(requirementKey, config.projectId, config.projectSecret, config.convexUrl);
927
+ const coverage = await queryRequirementCoverage(requirementKey, project.projectId, project.projectSecret, CONVEX_URL);
915
928
  if (!coverage.lastTestedAt) {
916
929
  return {
917
930
  content: [
@@ -946,19 +959,7 @@ ${projectList || '(none found)'}`,
946
959
  case 'get_project_coverage_summary': {
947
960
  const { branch, sinceTimestamp, projectId } = args;
948
961
  const project = await getProjectFromDiscovery(projectId);
949
- const config = loadConvexConfig(project.path);
950
- if (!config) {
951
- return {
952
- content: [
953
- {
954
- type: 'text',
955
- text: 'Coverage queries require project credentials. Run `dotrequirements link` to connect to cloud.',
956
- },
957
- ],
958
- isError: true,
959
- };
960
- }
961
- const coverage = await queryProjectCoverage(config.projectId, config.projectSecret, config.convexUrl, { branch, sinceTimestamp });
962
+ const coverage = await queryProjectCoverage(project.projectId, project.projectSecret, CONVEX_URL, { branch, sinceTimestamp });
962
963
  const total = coverage.tested.length + coverage.untested.length;
963
964
  const percentage = total > 0 ? ((coverage.tested.length / total) * 100).toFixed(1) : '0.0';
964
965
  let filterInfo = '';
@@ -1055,6 +1056,24 @@ ${projectList || '(none found)'}`,
1055
1056
 
1056
1057
  **Use concise domain prefixes** like \`AUTH-1\`, \`LOGIN-1\`, etc. Start numbering at 1 and increment sequentially.`;
1057
1058
  }
1059
+ // PROJ-CONTEXT-7: Try to fetch user-provided style context from cloud
1060
+ let userStyleGuidance = '';
1061
+ try {
1062
+ const project = await getProjectFromDiscovery();
1063
+ const context = await getProjectContext(project.projectId, project.projectSecret, CONVEX_URL);
1064
+ if (context?.requirementsStyleContext?.trim()) {
1065
+ userStyleGuidance = `
1066
+
1067
+ ## User-Provided Style Guidelines
1068
+
1069
+ The following style guidelines were provided by the project owner. When these conflict with the defaults above, prioritize the user's guidelines.
1070
+
1071
+ ${context.requirementsStyleContext.trim()}`;
1072
+ }
1073
+ }
1074
+ catch {
1075
+ // No credentials or cloud unavailable - continue without user context
1076
+ }
1058
1077
  const template = `---
1059
1078
  document:
1060
1079
  title: "Example Requirements"
@@ -1150,6 +1169,7 @@ ${labelGuidance}
1150
1169
  - Note: Even technical requirements can be user-centric: "95 percent of users experience under 1 second of delay"
1151
1170
 
1152
1171
  **11. Decompose Large Requirements**: If it can't be validated with a single test, break it down.
1172
+ ${userStyleGuidance}
1153
1173
 
1154
1174
  ## Example Requirements
1155
1175
 
@@ -1282,33 +1302,13 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1282
1302
  }
1283
1303
  case 'push_requirements': {
1284
1304
  const { filePath, confirmed = false, projectId: projectIdParam } = args;
1285
- const fs = await import('fs');
1286
- const path = await import('path');
1287
- const { parseRequirementsFromFile, getAllRequirements } = await import('../schema/index.js');
1288
- const { ConvexHttpClient } = await import('convex/browser');
1289
- // Get project
1305
+ // Get project (credentials come from project object, works with both file and env auth)
1290
1306
  const project = await getProjectFromDiscovery(projectIdParam);
1291
- const config = loadConvexConfig(project.path);
1292
- if (!config) {
1293
- return {
1294
- content: [
1295
- {
1296
- type: 'text',
1297
- text: 'Push requires project credentials. Run `dotrequirements link` to connect to cloud.',
1298
- },
1299
- ],
1300
- isError: true,
1301
- };
1302
- }
1303
- const convexUrl = config.convexUrl;
1304
- const projectId = config.projectId;
1305
- const projectSecret = config.projectSecret;
1306
1307
  // Determine files to push
1307
- const requirementsDir = path.resolve(project.path, '.requirements');
1308
1308
  let filesToPush;
1309
1309
  if (filePath) {
1310
- const fullPath = path.resolve(project.path, filePath);
1311
- if (!fs.existsSync(fullPath)) {
1310
+ const fullPath = resolve(project.path, filePath);
1311
+ if (!existsSync(fullPath)) {
1312
1312
  return {
1313
1313
  content: [
1314
1314
  {
@@ -1322,78 +1322,121 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1322
1322
  filesToPush = [fullPath];
1323
1323
  }
1324
1324
  else {
1325
- if (!fs.existsSync(requirementsDir)) {
1326
- return {
1327
- content: [
1328
- {
1329
- type: 'text',
1330
- text: 'No .requirements/ directory found',
1331
- },
1332
- ],
1333
- isError: true,
1334
- };
1335
- }
1336
- const files = fs.readdirSync(requirementsDir);
1337
- filesToPush = files
1338
- .filter((f) => f.endsWith('.requirements.md'))
1339
- .map((f) => path.join(requirementsDir, f));
1325
+ // Use same file discovery as CLI - searches entire workspace
1326
+ filesToPush = await findRequirementsFiles(project.path);
1340
1327
  if (filesToPush.length === 0) {
1341
1328
  return {
1342
1329
  content: [
1343
1330
  {
1344
1331
  type: 'text',
1345
- text: 'No Markdown files (*.requirements.md) found in .requirements/',
1332
+ text: 'No *.requirements.md files found. Nothing to push.',
1346
1333
  },
1347
1334
  ],
1348
- isError: true,
1349
1335
  };
1350
1336
  }
1351
1337
  }
1352
- // Parse local requirements
1353
- const allLocalRequirements = [];
1354
- for (const file of filesToPush) {
1355
- const parsed = parseRequirementsFromFile(file);
1356
- const flatReqs = getAllRequirements(parsed.requirements);
1357
- for (const req of flatReqs) {
1358
- allLocalRequirements.push({
1359
- key: req.id,
1360
- label: req.label,
1361
- content: req.content,
1362
- rootId: req.metadata?.rootId,
1363
- position: req.metadata?.position,
1364
- });
1365
- }
1338
+ // Parse files
1339
+ const { parsedFiles, totalRequirements } = parseFilesForPush(filesToPush);
1340
+ // Build credentials
1341
+ const credentials = {
1342
+ projectId: project.projectId,
1343
+ projectSecret: project.projectSecret,
1344
+ convexUrl: CONVEX_URL,
1345
+ };
1346
+ // Run dry run (always, even when confirmed - ensures fresh state)
1347
+ const dryRunResult = await dryRunPush(parsedFiles, credentials);
1348
+ // Check if there's anything to push
1349
+ const pushableCount = dryRunResult.updates.length +
1350
+ dryRunResult.creates.length +
1351
+ dryRunResult.notFound.length;
1352
+ if (pushableCount === 0 && dryRunResult.invalid.length > 0) {
1353
+ // Only invalid files
1354
+ const invalidList = dryRunResult.invalid
1355
+ .map(({ file, result }) => `- ${basename(file.filePath)}: ${result.error}`)
1356
+ .join('\n');
1357
+ return {
1358
+ content: [
1359
+ {
1360
+ type: 'text',
1361
+ text: `✗ No valid documents to push.\n\n**Invalid files:**\n${invalidList}`,
1362
+ },
1363
+ ],
1364
+ isError: true,
1365
+ };
1366
1366
  }
1367
1367
  if (!confirmed) {
1368
- // First call: show diff
1369
- const client = new ConvexHttpClient(convexUrl);
1370
- // Note: We need a public query to fetch requirements
1371
- // For now, just show what would be pushed
1368
+ // First call: show preview with real dry run results
1369
+ let summary = `# Push Preview\n\n**Files:** ${parsedFiles.length}\n**Requirements:** ${totalRequirements}\n\n`;
1370
+ if (dryRunResult.updates.length > 0) {
1371
+ summary += `## Updates (${dryRunResult.updates.length})\n`;
1372
+ for (const { file, result } of dryRunResult.updates) {
1373
+ const fileName = basename(file.filePath);
1374
+ const hasConflict = dryRunResult.conflicts.some((c) => c.item.file === file);
1375
+ const conflictNote = hasConflict ? ' ⚠️ [cloud changed since pull]' : '';
1376
+ const warningNote = result.warning ? ` (${result.warning})` : '';
1377
+ summary += `- ~ ${fileName}${conflictNote}${warningNote}\n`;
1378
+ }
1379
+ summary += '\n';
1380
+ }
1381
+ if (dryRunResult.creates.length > 0) {
1382
+ summary += `## New Documents (${dryRunResult.creates.length})\n`;
1383
+ for (const { file, result } of dryRunResult.creates) {
1384
+ const fileName = basename(file.filePath);
1385
+ const warningNote = result.warning ? ` (${result.warning})` : '';
1386
+ summary += `- + ${fileName}${warningNote}\n`;
1387
+ }
1388
+ summary += '\n';
1389
+ }
1390
+ if (dryRunResult.notFound.length > 0) {
1391
+ summary += `## Not Found in Cloud (${dryRunResult.notFound.length}) - will create new\n`;
1392
+ for (const { file, result } of dryRunResult.notFound) {
1393
+ const fileName = basename(file.filePath);
1394
+ summary += `- ! ${fileName} (ID: ${result.documentId})\n`;
1395
+ }
1396
+ summary += '\n';
1397
+ }
1398
+ if (dryRunResult.invalid.length > 0) {
1399
+ summary += `## Skipped - Invalid (${dryRunResult.invalid.length})\n`;
1400
+ for (const { file, result } of dryRunResult.invalid) {
1401
+ const fileName = basename(file.filePath);
1402
+ summary += `- ✗ ${fileName}: ${result.error}\n`;
1403
+ }
1404
+ summary += '\n';
1405
+ }
1406
+ if (dryRunResult.conflicts.length > 0) {
1407
+ summary += `⚠️ **Warning:** ${dryRunResult.conflicts.length} file(s) have cloud changes since last pull. Pushing will overwrite those changes.\n\n`;
1408
+ }
1409
+ summary += `**To proceed:** Call this tool again with \`confirmed: true\``;
1372
1410
  return {
1373
1411
  content: [
1374
1412
  {
1375
1413
  type: 'text',
1376
- 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\``,
1414
+ text: summary,
1377
1415
  },
1378
1416
  ],
1379
1417
  };
1380
1418
  }
1381
1419
  // Second call: execute push
1382
- const client = new ConvexHttpClient(convexUrl);
1383
1420
  try {
1384
- const result = await client.mutation('requirements/mutations:bulkReplace', {
1385
- projectAuth: {
1386
- projectSlug: projectId,
1387
- projectSecret,
1388
- },
1389
- target: { type: 'project', slug: projectId },
1390
- requirements: allLocalRequirements,
1391
- });
1421
+ const result = await executePush(dryRunResult, credentials);
1422
+ let output = '✓ Push complete!\n\n';
1423
+ if (result.created > 0) {
1424
+ output += `**Created:** ${result.created} document(s)\n`;
1425
+ }
1426
+ if (result.updated > 0) {
1427
+ output += `**Updated:** ${result.updated} document(s)\n`;
1428
+ }
1429
+ if (result.errors.length > 0) {
1430
+ output += `\n**Errors:**\n`;
1431
+ for (const { fileName, error } of result.errors) {
1432
+ output += `- ${fileName}: ${error}\n`;
1433
+ }
1434
+ }
1392
1435
  return {
1393
1436
  content: [
1394
1437
  {
1395
1438
  type: 'text',
1396
- text: `✓ Push complete!\n\n**Inserted:** ${result.inserted}\n**Deleted:** ${result.deleted}`,
1439
+ text: output,
1397
1440
  },
1398
1441
  ],
1399
1442
  };
@@ -1411,7 +1454,7 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1411
1454
  }
1412
1455
  }
1413
1456
  case 'style_check': {
1414
- const { filePath, model } = args;
1457
+ const { filePath, model, requirementKeys } = args;
1415
1458
  const fs = await import('fs');
1416
1459
  const path = await import('path');
1417
1460
  const fullPath = path.resolve(WORKSPACE_ROOT, filePath);
@@ -1443,22 +1486,73 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1443
1486
  };
1444
1487
  }
1445
1488
  const fileType = isRequirementsFile ? 'requirements' : 'test';
1489
+ // If requirementKeys provided, filter the file to only those requirements
1490
+ let fileContentsToCheck = fileContents;
1491
+ let scopeNote = '';
1492
+ if (requirementKeys && requirementKeys.length > 0) {
1493
+ if (!isRequirementsFile) {
1494
+ return {
1495
+ content: [
1496
+ {
1497
+ type: 'text',
1498
+ text: `requirementKeys can only be used with requirements files (*.requirements.md), not test files.`,
1499
+ },
1500
+ ],
1501
+ isError: true,
1502
+ };
1503
+ }
1504
+ try {
1505
+ const { filteredContent, foundKeys, missingKeys } = filterRequirementsByKeys(fileContents, requirementKeys);
1506
+ if (foundKeys.length === 0) {
1507
+ return {
1508
+ content: [
1509
+ {
1510
+ type: 'text',
1511
+ text: `None of the specified requirement keys were found in ${filePath}: ${requirementKeys.join(', ')}`,
1512
+ },
1513
+ ],
1514
+ isError: true,
1515
+ };
1516
+ }
1517
+ fileContentsToCheck = filteredContent;
1518
+ if (missingKeys.length > 0) {
1519
+ scopeNote = `\n\n> **Note:** Some specified keys were not found in the file: ${missingKeys.join(', ')}`;
1520
+ }
1521
+ }
1522
+ catch (error) {
1523
+ return {
1524
+ content: [
1525
+ {
1526
+ type: 'text',
1527
+ text: `Failed to filter requirements: ${error instanceof Error ? error.message : String(error)}`,
1528
+ },
1529
+ ],
1530
+ isError: true,
1531
+ };
1532
+ }
1533
+ }
1446
1534
  // Get Convex config for credentials - walk up from file's directory to find project
1447
1535
  const fileDir = path.dirname(fullPath);
1448
1536
  let project;
1449
1537
  try {
1450
- // Try to find a project starting from the file's directory
1451
- const result = await discoverProjects(fileDir);
1452
- if (result.type === 'none') {
1453
- // If no project found from file dir, try from WORKSPACE_ROOT
1538
+ // If using env auth, go straight to getProjectFromDiscovery (handles env credentials)
1539
+ if (USE_ENV_AUTH) {
1454
1540
  project = await getProjectFromDiscovery();
1455
1541
  }
1456
- else if (result.type === 'single') {
1457
- project = result.project;
1458
- }
1459
1542
  else {
1460
- // Multiple projects - can't auto-detect which one to use
1461
- throw new Error('Multiple projects found - cannot auto-detect for this file');
1543
+ // Try to find a project starting from the file's directory
1544
+ const result = await discoverProjects(fileDir);
1545
+ if (result.type === 'none') {
1546
+ // If no project found from file dir, try from WORKSPACE_ROOT
1547
+ project = await getProjectFromDiscovery();
1548
+ }
1549
+ else if (result.type === 'single') {
1550
+ project = result.project;
1551
+ }
1552
+ else {
1553
+ // Multiple projects - can't auto-detect which one to use
1554
+ throw new Error('Multiple projects found - cannot auto-detect for this file');
1555
+ }
1462
1556
  }
1463
1557
  }
1464
1558
  catch (error) {
@@ -1472,18 +1566,8 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1472
1566
  isError: true,
1473
1567
  };
1474
1568
  }
1475
- const config = loadConvexConfig(project.path);
1476
- if (!config) {
1477
- return {
1478
- content: [
1479
- {
1480
- type: 'text',
1481
- text: 'Style check requires project credentials. Run `dotrequirements link` to connect to cloud.',
1482
- },
1483
- ],
1484
- isError: true,
1485
- };
1486
- }
1569
+ // Use credentials from project (works with both file-based and env-based auth)
1570
+ const { projectId: projId, projectSecret: projSecret } = project;
1487
1571
  // Call Vercel API endpoint for style checking
1488
1572
  // Default to production, allow override via env var for local dev
1489
1573
  const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL || 'https://app.dotrequirements.io';
@@ -1494,9 +1578,9 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1494
1578
  'Content-Type': 'application/json',
1495
1579
  },
1496
1580
  body: JSON.stringify({
1497
- projectId: config.projectId,
1498
- projectSecret: config.projectSecret,
1499
- fileContents,
1581
+ projectId: projId,
1582
+ projectSecret: projSecret,
1583
+ fileContents: fileContentsToCheck,
1500
1584
  fileType,
1501
1585
  model,
1502
1586
  }),
@@ -1514,11 +1598,14 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1514
1598
  };
1515
1599
  }
1516
1600
  const data = await response.json();
1601
+ const scopeLabel = requirementKeys && requirementKeys.length > 0
1602
+ ? ` (${requirementKeys.join(', ')})`
1603
+ : '';
1517
1604
  return {
1518
1605
  content: [
1519
1606
  {
1520
1607
  type: 'text',
1521
- text: `# Style Check Results for \`${filePath}\`\n\n${data.feedback}${STYLE_CHECK_GUIDANCE}`,
1608
+ text: `# Style Check Results for \`${filePath}\`${scopeLabel}\n\n${data.feedback}${scopeNote}${STYLE_CHECK_GUIDANCE}`,
1522
1609
  },
1523
1610
  ],
1524
1611
  };
@@ -1614,18 +1701,8 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1614
1701
  });
1615
1702
  }
1616
1703
  // Get Convex config
1617
- const config = loadConvexConfig(project.path);
1618
- if (!config) {
1619
- return {
1620
- content: [
1621
- {
1622
- type: 'text',
1623
- text: 'Test review requires project credentials. Run `dotrequirements link` to connect to cloud.',
1624
- },
1625
- ],
1626
- isError: true,
1627
- };
1628
- }
1704
+ // Use credentials from project (works with both file-based and env-based auth)
1705
+ const { projectId: projId, projectSecret: projSecret } = project;
1629
1706
  // Call Vercel API endpoint for test review
1630
1707
  // Default to production, allow override via env var for local dev
1631
1708
  const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL || 'https://app.dotrequirements.io';
@@ -1636,8 +1713,8 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1636
1713
  'Content-Type': 'application/json',
1637
1714
  },
1638
1715
  body: JSON.stringify({
1639
- projectId: config.projectId,
1640
- projectSecret: config.projectSecret,
1716
+ projectId: projId,
1717
+ projectSecret: projSecret,
1641
1718
  testFileContents,
1642
1719
  requirements,
1643
1720
  }),
@@ -40,6 +40,16 @@ export declare function getRequirementTree(requirements: FlattenedRequirement[],
40
40
  * Format a requirement for display
41
41
  */
42
42
  export declare function formatRequirement(req: FlattenedRequirement): string;
43
+ /**
44
+ * Filter a requirements file's content to only include specified requirement keys.
45
+ * Parses the file content, filters top-level requirements by key, and rebuilds as
46
+ * markdown requirement blocks (without frontmatter).
47
+ */
48
+ export declare function filterRequirementsByKeys(fileContents: string, keys: string[]): {
49
+ filteredContent: string;
50
+ foundKeys: string[];
51
+ missingKeys: string[];
52
+ };
43
53
  /**
44
54
  * Format a requirement tree for display
45
55
  */
@@ -1,6 +1,6 @@
1
1
  import * as path from 'path';
2
2
  import { glob } from 'glob';
3
- import { parseRequirementsFromFile, getAllRequirements, } from '../schema/index.js';
3
+ import { parseRequirementsFromFile, parseRequirementsFile, buildRequirementMarkdown, getAllRequirements, } from '../schema/index.js';
4
4
  /**
5
5
  * Find all *.requirements.md files in the workspace.
6
6
  * Supports both .requirements/ directories and colocated files.
@@ -114,6 +114,20 @@ export function formatRequirement(req) {
114
114
  const label = req.label ? ` (${req.label})` : '';
115
115
  return `${indent}${req.id}${label}: ${req.content}`;
116
116
  }
117
+ /**
118
+ * Filter a requirements file's content to only include specified requirement keys.
119
+ * Parses the file content, filters top-level requirements by key, and rebuilds as
120
+ * markdown requirement blocks (without frontmatter).
121
+ */
122
+ export function filterRequirementsByKeys(fileContents, keys) {
123
+ const { requirements } = parseRequirementsFile(fileContents);
124
+ const upperKeys = keys.map(k => k.toUpperCase());
125
+ const filtered = requirements.filter(r => upperKeys.includes(r.id.toUpperCase()));
126
+ const foundKeys = filtered.map(r => r.id);
127
+ const missingKeys = upperKeys.filter(k => !foundKeys.map(f => f.toUpperCase()).includes(k));
128
+ const filteredContent = filtered.map(buildRequirementMarkdown).join('\n');
129
+ return { filteredContent, foundKeys, missingKeys };
130
+ }
117
131
  /**
118
132
  * Format a requirement tree for display
119
133
  */