@popoverai/dotrequirements 0.13.0 → 0.15.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.
- package/README.md +92 -48
- package/dist/cli.js +1 -10
- package/dist/commands/init.js +166 -226
- package/dist/commands/link.d.ts +9 -10
- package/dist/commands/link.js +81 -106
- package/dist/commands/mcp-setup.js +77 -94
- package/dist/commands/pull.d.ts +1 -0
- package/dist/commands/pull.js +70 -53
- package/dist/commands/push.js +4 -13
- package/dist/config.js +0 -4
- package/dist/harness/cache.d.ts +0 -5
- package/dist/harness/cache.js +0 -48
- package/dist/harness/convexReporting.js +7 -14
- package/dist/harness/finalize.js +7 -12
- package/dist/harness/prepare.js +7 -9
- package/dist/mcp/convexClient.d.ts +9 -1
- package/dist/mcp/convexClient.js +15 -35
- package/dist/mcp/index.js +112 -152
- package/dist/mcp/requirements.d.ts +10 -0
- package/dist/mcp/requirements.js +15 -1
- package/dist/templates/context-file-section.md +59 -0
- package/dist/utils/context-file.d.ts +38 -0
- package/dist/utils/context-file.js +94 -0
- package/dist/utils/env.d.ts +0 -13
- package/dist/utils/env.js +0 -19
- package/dist/utils/gitignore.d.ts +2 -2
- package/dist/utils/gitignore.js +4 -4
- package/dist/utils/oauth-flow.d.ts +0 -1
- package/dist/utils/oauth-flow.js +0 -9
- package/dist/utils/project-discovery.d.ts +3 -5
- package/dist/utils/project-discovery.js +18 -42
- package/dist/utils/project-selector.d.ts +17 -3
- package/dist/utils/project-selector.js +35 -3
- package/dist/utils/project-settings.d.ts +56 -0
- package/dist/utils/project-settings.js +126 -0
- package/dist/utils/templates.d.ts +0 -24
- package/dist/utils/templates.js +0 -39
- package/package.json +1 -1
- package/dist/harness/localReporting.d.ts +0 -6
- package/dist/harness/localReporting.js +0 -49
- package/dist/templates/antigravity-gemini.md +0 -3
- package/dist/templates/antigravity-overview-rule.md +0 -3
- package/dist/templates/antigravity-test-rule.md +0 -3
- package/dist/templates/behavioral-core.md +0 -25
- package/dist/templates/claude-code-overview-skill.md +0 -6
- package/dist/templates/claude-code-skill.md +0 -6
- package/dist/templates/claude-code-test-skill.md +0 -6
- package/dist/templates/codex-agents.md +0 -3
- package/dist/templates/codex-overview-agents.md +0 -3
- package/dist/templates/codex-test-agents.md +0 -3
- package/dist/templates/cursor-overview-rule.mdc +0 -5
- package/dist/templates/cursor-rule.mdc +0 -5
- package/dist/templates/cursor-test-rule.mdc +0 -5
- package/dist/templates/overview-core.md +0 -27
- package/dist/templates/test-writing-core.md +0 -72
- package/dist/utils/detect-existing-project.d.ts +0 -5
- package/dist/utils/detect-existing-project.js +0 -34
package/dist/mcp/index.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
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 {
|
|
9
|
+
import { CONVEX_URL, getRequirementCoverage as queryRequirementCoverage, getProjectCoverage as queryProjectCoverage, } from './convexClient.js';
|
|
10
10
|
import { discoverProjects, resolveProject, } from '../utils/project-discovery.js';
|
|
11
|
-
import {
|
|
11
|
+
import { getCredentialsFromEnv } from '../utils/project-settings.js';
|
|
12
12
|
import { readFileSync } from 'fs';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
import { dirname, join } from 'path';
|
|
@@ -33,6 +33,9 @@ function getProjectPathsFromEnv() {
|
|
|
33
33
|
// Get workspace root from environment or default to cwd
|
|
34
34
|
const WORKSPACE_ROOT = process.env.REQUIREMENTS_DIR || process.cwd();
|
|
35
35
|
const PROJECT_PATHS = getProjectPathsFromEnv();
|
|
36
|
+
// Parse --auth-from-env flag for CI/CD environments
|
|
37
|
+
// When set, credentials are read from DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET env vars
|
|
38
|
+
const USE_ENV_AUTH = process.argv.includes('--auth-from-env');
|
|
36
39
|
const STYLE_CHECK_GUIDANCE = `
|
|
37
40
|
|
|
38
41
|
---
|
|
@@ -58,6 +61,20 @@ async function getRequirements(projectId) {
|
|
|
58
61
|
}
|
|
59
62
|
async function getProjectFromDiscovery(projectId) {
|
|
60
63
|
const { isConfiguredProject } = await import('../utils/project-discovery.js');
|
|
64
|
+
// If --auth-from-env flag is set, use credentials from environment variables
|
|
65
|
+
// This is the explicit opt-in for CI/CD environments
|
|
66
|
+
if (USE_ENV_AUTH) {
|
|
67
|
+
const envCredentials = getCredentialsFromEnv();
|
|
68
|
+
if (!envCredentials) {
|
|
69
|
+
throw new Error('--auth-from-env flag requires DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET environment variables to be set');
|
|
70
|
+
}
|
|
71
|
+
// Return a synthetic project using env credentials and workspace root
|
|
72
|
+
return {
|
|
73
|
+
path: WORKSPACE_ROOT,
|
|
74
|
+
projectId: envCredentials.projectId,
|
|
75
|
+
projectSecret: envCredentials.projectSecret,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
61
78
|
// If we have PROJ_* env vars, use those instead of filesystem discovery
|
|
62
79
|
if (PROJECT_PATHS.size > 0) {
|
|
63
80
|
const projects = [];
|
|
@@ -211,7 +228,7 @@ const tools = [
|
|
|
211
228
|
},
|
|
212
229
|
{
|
|
213
230
|
name: 'get_requirement_coverage',
|
|
214
|
-
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
|
|
231
|
+
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 project to be linked to cloud (run `dotrequirements link`).',
|
|
215
232
|
inputSchema: {
|
|
216
233
|
type: 'object',
|
|
217
234
|
properties: {
|
|
@@ -229,7 +246,7 @@ const tools = [
|
|
|
229
246
|
},
|
|
230
247
|
{
|
|
231
248
|
name: 'get_project_coverage_summary',
|
|
232
|
-
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
|
|
249
|
+
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 project to be linked to cloud (run `dotrequirements link`).',
|
|
233
250
|
inputSchema: {
|
|
234
251
|
type: 'object',
|
|
235
252
|
properties: {
|
|
@@ -279,7 +296,7 @@ const tools = [
|
|
|
279
296
|
},
|
|
280
297
|
{
|
|
281
298
|
name: 'push_requirements',
|
|
282
|
-
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
|
|
299
|
+
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 project to be linked to cloud (run `dotrequirements link`).',
|
|
283
300
|
inputSchema: {
|
|
284
301
|
type: 'object',
|
|
285
302
|
properties: {
|
|
@@ -301,7 +318,7 @@ const tools = [
|
|
|
301
318
|
},
|
|
302
319
|
{
|
|
303
320
|
name: 'style_check',
|
|
304
|
-
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.*).
|
|
321
|
+
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`).',
|
|
305
322
|
inputSchema: {
|
|
306
323
|
type: 'object',
|
|
307
324
|
properties: {
|
|
@@ -309,6 +326,11 @@ const tools = [
|
|
|
309
326
|
type: 'string',
|
|
310
327
|
description: 'Path to the file to check (e.g., ".requirements/auth.requirements.md" or "src/auth.test.ts")',
|
|
311
328
|
},
|
|
329
|
+
requirementKeys: {
|
|
330
|
+
type: 'array',
|
|
331
|
+
items: { type: 'string' },
|
|
332
|
+
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.',
|
|
333
|
+
},
|
|
312
334
|
model: {
|
|
313
335
|
type: 'string',
|
|
314
336
|
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"',
|
|
@@ -319,7 +341,7 @@ const tools = [
|
|
|
319
341
|
},
|
|
320
342
|
{
|
|
321
343
|
name: 'review_test',
|
|
322
|
-
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
|
|
344
|
+
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 project to be linked to cloud (run `dotrequirements link`).',
|
|
323
345
|
inputSchema: {
|
|
324
346
|
type: 'object',
|
|
325
347
|
properties: {
|
|
@@ -416,7 +438,7 @@ Fill in:
|
|
|
416
438
|
After capturing requirements:
|
|
417
439
|
1. Implement the feature
|
|
418
440
|
2. Write tests that reference requirements using \`requirement('REQ-ID')\`
|
|
419
|
-
3.
|
|
441
|
+
3. See your project's context file (CLAUDE.md/AGENTS.md) for test structure guidance`,
|
|
420
442
|
},
|
|
421
443
|
},
|
|
422
444
|
],
|
|
@@ -900,31 +922,7 @@ ${projectList || '(none found)'}`,
|
|
|
900
922
|
case 'get_requirement_coverage': {
|
|
901
923
|
const { requirementKey, projectId } = args;
|
|
902
924
|
const project = await getProjectFromDiscovery(projectId);
|
|
903
|
-
const
|
|
904
|
-
if (!config) {
|
|
905
|
-
return {
|
|
906
|
-
content: [
|
|
907
|
-
{
|
|
908
|
-
type: 'text',
|
|
909
|
-
text: 'Coverage queries require DOTREQUIREMENTS_PROJECT_ID and DOTREQUIREMENTS_PROJECT_SECRET to be configured in your .env file.',
|
|
910
|
-
},
|
|
911
|
-
],
|
|
912
|
-
isError: true,
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
// AUTHZ-2.1: Check if project is local-only
|
|
916
|
-
if (isLocalOnlyProject(config.projectId)) {
|
|
917
|
-
return {
|
|
918
|
-
content: [
|
|
919
|
-
{
|
|
920
|
-
type: 'text',
|
|
921
|
-
text: CLOUD_FEATURES_REQUIRE_AUTH_MESSAGE,
|
|
922
|
-
},
|
|
923
|
-
],
|
|
924
|
-
isError: true,
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
const coverage = await queryRequirementCoverage(requirementKey, config.projectId, config.projectSecret, config.convexUrl);
|
|
925
|
+
const coverage = await queryRequirementCoverage(requirementKey, project.projectId, project.projectSecret, CONVEX_URL);
|
|
928
926
|
if (!coverage.lastTestedAt) {
|
|
929
927
|
return {
|
|
930
928
|
content: [
|
|
@@ -959,31 +957,7 @@ ${projectList || '(none found)'}`,
|
|
|
959
957
|
case 'get_project_coverage_summary': {
|
|
960
958
|
const { branch, sinceTimestamp, projectId } = args;
|
|
961
959
|
const project = await getProjectFromDiscovery(projectId);
|
|
962
|
-
const
|
|
963
|
-
if (!config) {
|
|
964
|
-
return {
|
|
965
|
-
content: [
|
|
966
|
-
{
|
|
967
|
-
type: 'text',
|
|
968
|
-
text: 'Coverage queries require DOTREQUIREMENTS_PROJECT_ID and DOTREQUIREMENTS_PROJECT_SECRET to be configured in your .env file.',
|
|
969
|
-
},
|
|
970
|
-
],
|
|
971
|
-
isError: true,
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
// AUTHZ-2.1: Check if project is local-only
|
|
975
|
-
if (isLocalOnlyProject(config.projectId)) {
|
|
976
|
-
return {
|
|
977
|
-
content: [
|
|
978
|
-
{
|
|
979
|
-
type: 'text',
|
|
980
|
-
text: CLOUD_FEATURES_REQUIRE_AUTH_MESSAGE,
|
|
981
|
-
},
|
|
982
|
-
],
|
|
983
|
-
isError: true,
|
|
984
|
-
};
|
|
985
|
-
}
|
|
986
|
-
const coverage = await queryProjectCoverage(config.projectId, config.projectSecret, config.convexUrl, { branch, sinceTimestamp });
|
|
960
|
+
const coverage = await queryProjectCoverage(project.projectId, project.projectSecret, CONVEX_URL, { branch, sinceTimestamp });
|
|
987
961
|
const total = coverage.tested.length + coverage.untested.length;
|
|
988
962
|
const percentage = total > 0 ? ((coverage.tested.length / total) * 100).toFixed(1) : '0.0';
|
|
989
963
|
let filterInfo = '';
|
|
@@ -1311,35 +1285,11 @@ describe(requirement('AUTH-LOGIN-1'), () => {
|
|
|
1311
1285
|
const path = await import('path');
|
|
1312
1286
|
const { parseRequirementsFromFile, getAllRequirements } = await import('../schema/index.js');
|
|
1313
1287
|
const { ConvexHttpClient } = await import('convex/browser');
|
|
1314
|
-
// Get project
|
|
1288
|
+
// Get project (credentials come from project object, works with both file and env auth)
|
|
1315
1289
|
const project = await getProjectFromDiscovery(projectIdParam);
|
|
1316
|
-
const
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
content: [
|
|
1320
|
-
{
|
|
1321
|
-
type: 'text',
|
|
1322
|
-
text: 'Push requires environment variables:\n- DOTREQUIREMENTS_PROJECT_ID\n- DOTREQUIREMENTS_PROJECT_SECRET',
|
|
1323
|
-
},
|
|
1324
|
-
],
|
|
1325
|
-
isError: true,
|
|
1326
|
-
};
|
|
1327
|
-
}
|
|
1328
|
-
const convexUrl = config.convexUrl;
|
|
1329
|
-
const projectId = config.projectId;
|
|
1330
|
-
const projectSecret = config.projectSecret;
|
|
1331
|
-
// AUTHZ-2.1: Check if project is local-only
|
|
1332
|
-
if (isLocalOnlyProject(projectId)) {
|
|
1333
|
-
return {
|
|
1334
|
-
content: [
|
|
1335
|
-
{
|
|
1336
|
-
type: 'text',
|
|
1337
|
-
text: CLOUD_FEATURES_REQUIRE_AUTH_MESSAGE,
|
|
1338
|
-
},
|
|
1339
|
-
],
|
|
1340
|
-
isError: true,
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1290
|
+
const convexUrl = CONVEX_URL;
|
|
1291
|
+
const projectId = project.projectId;
|
|
1292
|
+
const projectSecret = project.projectSecret;
|
|
1343
1293
|
// Determine files to push
|
|
1344
1294
|
const requirementsDir = path.resolve(project.path, '.requirements');
|
|
1345
1295
|
let filesToPush;
|
|
@@ -1448,7 +1398,7 @@ describe(requirement('AUTH-LOGIN-1'), () => {
|
|
|
1448
1398
|
}
|
|
1449
1399
|
}
|
|
1450
1400
|
case 'style_check': {
|
|
1451
|
-
const { filePath, model } = args;
|
|
1401
|
+
const { filePath, model, requirementKeys } = args;
|
|
1452
1402
|
const fs = await import('fs');
|
|
1453
1403
|
const path = await import('path');
|
|
1454
1404
|
const fullPath = path.resolve(WORKSPACE_ROOT, filePath);
|
|
@@ -1480,22 +1430,73 @@ describe(requirement('AUTH-LOGIN-1'), () => {
|
|
|
1480
1430
|
};
|
|
1481
1431
|
}
|
|
1482
1432
|
const fileType = isRequirementsFile ? 'requirements' : 'test';
|
|
1433
|
+
// If requirementKeys provided, filter the file to only those requirements
|
|
1434
|
+
let fileContentsToCheck = fileContents;
|
|
1435
|
+
let scopeNote = '';
|
|
1436
|
+
if (requirementKeys && requirementKeys.length > 0) {
|
|
1437
|
+
if (!isRequirementsFile) {
|
|
1438
|
+
return {
|
|
1439
|
+
content: [
|
|
1440
|
+
{
|
|
1441
|
+
type: 'text',
|
|
1442
|
+
text: `requirementKeys can only be used with requirements files (*.requirements.md), not test files.`,
|
|
1443
|
+
},
|
|
1444
|
+
],
|
|
1445
|
+
isError: true,
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
try {
|
|
1449
|
+
const { filteredContent, foundKeys, missingKeys } = filterRequirementsByKeys(fileContents, requirementKeys);
|
|
1450
|
+
if (foundKeys.length === 0) {
|
|
1451
|
+
return {
|
|
1452
|
+
content: [
|
|
1453
|
+
{
|
|
1454
|
+
type: 'text',
|
|
1455
|
+
text: `None of the specified requirement keys were found in ${filePath}: ${requirementKeys.join(', ')}`,
|
|
1456
|
+
},
|
|
1457
|
+
],
|
|
1458
|
+
isError: true,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
fileContentsToCheck = filteredContent;
|
|
1462
|
+
if (missingKeys.length > 0) {
|
|
1463
|
+
scopeNote = `\n\n> **Note:** Some specified keys were not found in the file: ${missingKeys.join(', ')}`;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
catch (error) {
|
|
1467
|
+
return {
|
|
1468
|
+
content: [
|
|
1469
|
+
{
|
|
1470
|
+
type: 'text',
|
|
1471
|
+
text: `Failed to filter requirements: ${error instanceof Error ? error.message : String(error)}`,
|
|
1472
|
+
},
|
|
1473
|
+
],
|
|
1474
|
+
isError: true,
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1483
1478
|
// Get Convex config for credentials - walk up from file's directory to find project
|
|
1484
1479
|
const fileDir = path.dirname(fullPath);
|
|
1485
1480
|
let project;
|
|
1486
1481
|
try {
|
|
1487
|
-
//
|
|
1488
|
-
|
|
1489
|
-
if (result.type === 'none') {
|
|
1490
|
-
// If no project found from file dir, try from WORKSPACE_ROOT
|
|
1482
|
+
// If using env auth, go straight to getProjectFromDiscovery (handles env credentials)
|
|
1483
|
+
if (USE_ENV_AUTH) {
|
|
1491
1484
|
project = await getProjectFromDiscovery();
|
|
1492
1485
|
}
|
|
1493
|
-
else if (result.type === 'single') {
|
|
1494
|
-
project = result.project;
|
|
1495
|
-
}
|
|
1496
1486
|
else {
|
|
1497
|
-
//
|
|
1498
|
-
|
|
1487
|
+
// Try to find a project starting from the file's directory
|
|
1488
|
+
const result = await discoverProjects(fileDir);
|
|
1489
|
+
if (result.type === 'none') {
|
|
1490
|
+
// If no project found from file dir, try from WORKSPACE_ROOT
|
|
1491
|
+
project = await getProjectFromDiscovery();
|
|
1492
|
+
}
|
|
1493
|
+
else if (result.type === 'single') {
|
|
1494
|
+
project = result.project;
|
|
1495
|
+
}
|
|
1496
|
+
else {
|
|
1497
|
+
// Multiple projects - can't auto-detect which one to use
|
|
1498
|
+
throw new Error('Multiple projects found - cannot auto-detect for this file');
|
|
1499
|
+
}
|
|
1499
1500
|
}
|
|
1500
1501
|
}
|
|
1501
1502
|
catch (error) {
|
|
@@ -1503,36 +1504,14 @@ describe(requirement('AUTH-LOGIN-1'), () => {
|
|
|
1503
1504
|
content: [
|
|
1504
1505
|
{
|
|
1505
1506
|
type: 'text',
|
|
1506
|
-
text: `Style check requires
|
|
1507
|
-
},
|
|
1508
|
-
],
|
|
1509
|
-
isError: true,
|
|
1510
|
-
};
|
|
1511
|
-
}
|
|
1512
|
-
const config = loadConvexConfig(project.path);
|
|
1513
|
-
if (!config) {
|
|
1514
|
-
return {
|
|
1515
|
-
content: [
|
|
1516
|
-
{
|
|
1517
|
-
type: 'text',
|
|
1518
|
-
text: 'Style check requires DOTREQUIREMENTS_PROJECT_ID and DOTREQUIREMENTS_PROJECT_SECRET to be configured in your .env or .env.local file.',
|
|
1519
|
-
},
|
|
1520
|
-
],
|
|
1521
|
-
isError: true,
|
|
1522
|
-
};
|
|
1523
|
-
}
|
|
1524
|
-
// AUTHZ-2.1: Check if project is local-only
|
|
1525
|
-
if (isLocalOnlyProject(config.projectId)) {
|
|
1526
|
-
return {
|
|
1527
|
-
content: [
|
|
1528
|
-
{
|
|
1529
|
-
type: 'text',
|
|
1530
|
-
text: CLOUD_FEATURES_REQUIRE_AUTH_MESSAGE,
|
|
1507
|
+
text: `Style check requires project credentials. Run \`dotrequirements link\` to connect to cloud.\n\nError: ${error instanceof Error ? error.message : String(error)}`,
|
|
1531
1508
|
},
|
|
1532
1509
|
],
|
|
1533
1510
|
isError: true,
|
|
1534
1511
|
};
|
|
1535
1512
|
}
|
|
1513
|
+
// Use credentials from project (works with both file-based and env-based auth)
|
|
1514
|
+
const { projectId: projId, projectSecret: projSecret } = project;
|
|
1536
1515
|
// Call Vercel API endpoint for style checking
|
|
1537
1516
|
// Default to production, allow override via env var for local dev
|
|
1538
1517
|
const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL || 'https://app.dotrequirements.io';
|
|
@@ -1543,9 +1522,9 @@ describe(requirement('AUTH-LOGIN-1'), () => {
|
|
|
1543
1522
|
'Content-Type': 'application/json',
|
|
1544
1523
|
},
|
|
1545
1524
|
body: JSON.stringify({
|
|
1546
|
-
projectId:
|
|
1547
|
-
projectSecret:
|
|
1548
|
-
fileContents,
|
|
1525
|
+
projectId: projId,
|
|
1526
|
+
projectSecret: projSecret,
|
|
1527
|
+
fileContents: fileContentsToCheck,
|
|
1549
1528
|
fileType,
|
|
1550
1529
|
model,
|
|
1551
1530
|
}),
|
|
@@ -1563,11 +1542,14 @@ describe(requirement('AUTH-LOGIN-1'), () => {
|
|
|
1563
1542
|
};
|
|
1564
1543
|
}
|
|
1565
1544
|
const data = await response.json();
|
|
1545
|
+
const scopeLabel = requirementKeys && requirementKeys.length > 0
|
|
1546
|
+
? ` (${requirementKeys.join(', ')})`
|
|
1547
|
+
: '';
|
|
1566
1548
|
return {
|
|
1567
1549
|
content: [
|
|
1568
1550
|
{
|
|
1569
1551
|
type: 'text',
|
|
1570
|
-
text: `# Style Check Results for \`${filePath}
|
|
1552
|
+
text: `# Style Check Results for \`${filePath}\`${scopeLabel}\n\n${data.feedback}${scopeNote}${STYLE_CHECK_GUIDANCE}`,
|
|
1571
1553
|
},
|
|
1572
1554
|
],
|
|
1573
1555
|
};
|
|
@@ -1663,30 +1645,8 @@ describe(requirement('AUTH-LOGIN-1'), () => {
|
|
|
1663
1645
|
});
|
|
1664
1646
|
}
|
|
1665
1647
|
// Get Convex config
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
return {
|
|
1669
|
-
content: [
|
|
1670
|
-
{
|
|
1671
|
-
type: 'text',
|
|
1672
|
-
text: 'Test review requires DOTREQUIREMENTS_PROJECT_ID and DOTREQUIREMENTS_PROJECT_SECRET to be configured in your .env or .env.local file.',
|
|
1673
|
-
},
|
|
1674
|
-
],
|
|
1675
|
-
isError: true,
|
|
1676
|
-
};
|
|
1677
|
-
}
|
|
1678
|
-
// AUTHZ-2.1: Check if project is local-only
|
|
1679
|
-
if (isLocalOnlyProject(config.projectId)) {
|
|
1680
|
-
return {
|
|
1681
|
-
content: [
|
|
1682
|
-
{
|
|
1683
|
-
type: 'text',
|
|
1684
|
-
text: CLOUD_FEATURES_REQUIRE_AUTH_MESSAGE,
|
|
1685
|
-
},
|
|
1686
|
-
],
|
|
1687
|
-
isError: true,
|
|
1688
|
-
};
|
|
1689
|
-
}
|
|
1648
|
+
// Use credentials from project (works with both file-based and env-based auth)
|
|
1649
|
+
const { projectId: projId, projectSecret: projSecret } = project;
|
|
1690
1650
|
// Call Vercel API endpoint for test review
|
|
1691
1651
|
// Default to production, allow override via env var for local dev
|
|
1692
1652
|
const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL || 'https://app.dotrequirements.io';
|
|
@@ -1697,8 +1657,8 @@ describe(requirement('AUTH-LOGIN-1'), () => {
|
|
|
1697
1657
|
'Content-Type': 'application/json',
|
|
1698
1658
|
},
|
|
1699
1659
|
body: JSON.stringify({
|
|
1700
|
-
projectId:
|
|
1701
|
-
projectSecret:
|
|
1660
|
+
projectId: projId,
|
|
1661
|
+
projectSecret: projSecret,
|
|
1702
1662
|
testFileContents,
|
|
1703
1663
|
requirements,
|
|
1704
1664
|
}),
|
|
@@ -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
|
*/
|
package/dist/mcp/requirements.js
CHANGED
|
@@ -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
|
*/
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
## Requirements-First Development
|
|
2
|
+
|
|
3
|
+
This project uses dotrequirements for requirements tracking. Requirements live in `.requirements/*.requirements.md`.
|
|
4
|
+
|
|
5
|
+
### Workflow
|
|
6
|
+
|
|
7
|
+
When adding new functionality (not bug fixes or refactoring):
|
|
8
|
+
|
|
9
|
+
1. **Draft requirements** - Write in `.requirements/*.requirements.md`
|
|
10
|
+
2. **Style-check** - Run `mcp__dotrequirements__style_check` on the file
|
|
11
|
+
3. **Get approval** - Present requirements, wait for go-ahead
|
|
12
|
+
4. **Implement** - Build the feature
|
|
13
|
+
5. **Write tests** - Reference requirements with `requirement()`
|
|
14
|
+
6. **Run tests** - Verify everything passes
|
|
15
|
+
7. **Review tests** - Run `mcp__dotrequirements__review_test` to validate coverage
|
|
16
|
+
|
|
17
|
+
### Requirements Syntax
|
|
18
|
+
|
|
19
|
+
```dotrequirements
|
|
20
|
+
REQ-ID: Short description of expected behavior
|
|
21
|
+
0. Given -> Precondition that must be true
|
|
22
|
+
1. When -> Action or trigger
|
|
23
|
+
2. Then -> Expected outcome
|
|
24
|
+
2.0. And -> Additional outcome detail
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- First line: `KEY: description`
|
|
28
|
+
- Criteria: `position. Label -> content` (Given/When/Then structure)
|
|
29
|
+
- Nesting: Indent with 2 spaces, use `x.y` position paths
|
|
30
|
+
- Delimiter: `->` or the arrow character
|
|
31
|
+
|
|
32
|
+
### Test Usage
|
|
33
|
+
|
|
34
|
+
Use `requirement()` AS the test description:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { requirement } from '@popoverai/dotrequirements/test';
|
|
38
|
+
|
|
39
|
+
test(requirement('REQ-ID'), () => { /* test the requirement */ });
|
|
40
|
+
test(requirement('REQ-ID.0'), () => { /* test specific criterion */ });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### MCP Tools
|
|
44
|
+
|
|
45
|
+
**Exploration:**
|
|
46
|
+
- `list_all_requirements` - Overview of all requirements
|
|
47
|
+
- `get_requirement` - Requirement tree with test coverage
|
|
48
|
+
- `search_requirements` - Search by text/regex
|
|
49
|
+
|
|
50
|
+
**Authoring:**
|
|
51
|
+
- `create_requirement_document` - Get template with format guidance
|
|
52
|
+
- `validate_requirements` - Check syntax (works offline)
|
|
53
|
+
- `style_check` - AI feedback on clarity
|
|
54
|
+
- `push_requirements` - Sync to cloud
|
|
55
|
+
|
|
56
|
+
**Testing:**
|
|
57
|
+
- `get_requirements_by_test` - See requirements a test file covers
|
|
58
|
+
- `list_untested_requirements` - Find gaps in coverage
|
|
59
|
+
- `review_test` - Validate tests match requirement intent
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the appropriate context file name for a platform
|
|
3
|
+
*/
|
|
4
|
+
export declare function getContextFileName(platform: string): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Find the git root directory
|
|
7
|
+
*/
|
|
8
|
+
export declare function findGitRoot(): Promise<string | null>;
|
|
9
|
+
/**
|
|
10
|
+
* Find the dotrequirements section in file content
|
|
11
|
+
* Returns the start/end indices and content, or null if not found
|
|
12
|
+
*/
|
|
13
|
+
export declare function findDotrequirementsSection(content: string): {
|
|
14
|
+
start: number;
|
|
15
|
+
end: number;
|
|
16
|
+
content: string;
|
|
17
|
+
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* Wrap content in section markers
|
|
20
|
+
*/
|
|
21
|
+
export declare function wrapInSectionMarkers(content: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Append or update the dotrequirements section in a context file
|
|
24
|
+
* - If file doesn't exist, creates it with just the section
|
|
25
|
+
* - If file exists without section, appends section at end
|
|
26
|
+
* - If file exists with section, replaces the section
|
|
27
|
+
*
|
|
28
|
+
* Returns: { action: 'created' | 'appended' | 'updated', existingContent?: string }
|
|
29
|
+
*/
|
|
30
|
+
export declare function appendOrUpdateSection(filePath: string, sectionContent: string): {
|
|
31
|
+
action: 'created' | 'appended' | 'updated';
|
|
32
|
+
existingContent?: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Get the full path to the context file for a platform
|
|
36
|
+
*/
|
|
37
|
+
export declare function getContextFilePath(platform: string): Promise<string | null>;
|
|
38
|
+
//# sourceMappingURL=context-file.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { findUp } from 'find-up';
|
|
4
|
+
const SECTION_START = '<!-- dotrequirements:start -->';
|
|
5
|
+
const SECTION_END = '<!-- dotrequirements:end -->';
|
|
6
|
+
/**
|
|
7
|
+
* Platform to context file mapping
|
|
8
|
+
*/
|
|
9
|
+
const PLATFORM_CONTEXT_FILES = {
|
|
10
|
+
'claude-code': 'CLAUDE.md',
|
|
11
|
+
'cursor': 'AGENTS.md',
|
|
12
|
+
'codex': 'AGENTS.md',
|
|
13
|
+
'github-copilot': 'AGENTS.md',
|
|
14
|
+
'antigravity': 'GEMINI.md',
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Get the appropriate context file name for a platform
|
|
18
|
+
*/
|
|
19
|
+
export function getContextFileName(platform) {
|
|
20
|
+
return PLATFORM_CONTEXT_FILES[platform] ?? null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Find the git root directory
|
|
24
|
+
*/
|
|
25
|
+
export async function findGitRoot() {
|
|
26
|
+
const gitDir = await findUp('.git', { type: 'directory' });
|
|
27
|
+
return gitDir ? dirname(gitDir) : null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Find the dotrequirements section in file content
|
|
31
|
+
* Returns the start/end indices and content, or null if not found
|
|
32
|
+
*/
|
|
33
|
+
export function findDotrequirementsSection(content) {
|
|
34
|
+
const startIdx = content.indexOf(SECTION_START);
|
|
35
|
+
if (startIdx === -1)
|
|
36
|
+
return null;
|
|
37
|
+
const endIdx = content.indexOf(SECTION_END, startIdx);
|
|
38
|
+
if (endIdx === -1)
|
|
39
|
+
return null;
|
|
40
|
+
return {
|
|
41
|
+
start: startIdx,
|
|
42
|
+
end: endIdx + SECTION_END.length,
|
|
43
|
+
content: content.slice(startIdx, endIdx + SECTION_END.length),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Wrap content in section markers
|
|
48
|
+
*/
|
|
49
|
+
export function wrapInSectionMarkers(content) {
|
|
50
|
+
return `${SECTION_START}\n${content}\n${SECTION_END}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Append or update the dotrequirements section in a context file
|
|
54
|
+
* - If file doesn't exist, creates it with just the section
|
|
55
|
+
* - If file exists without section, appends section at end
|
|
56
|
+
* - If file exists with section, replaces the section
|
|
57
|
+
*
|
|
58
|
+
* Returns: { action: 'created' | 'appended' | 'updated', existingContent?: string }
|
|
59
|
+
*/
|
|
60
|
+
export function appendOrUpdateSection(filePath, sectionContent) {
|
|
61
|
+
const wrappedContent = wrapInSectionMarkers(sectionContent);
|
|
62
|
+
if (!existsSync(filePath)) {
|
|
63
|
+
// Create new file with just the section
|
|
64
|
+
writeFileSync(filePath, wrappedContent + '\n', 'utf-8');
|
|
65
|
+
return { action: 'created' };
|
|
66
|
+
}
|
|
67
|
+
const existingFile = readFileSync(filePath, 'utf-8');
|
|
68
|
+
const existingSection = findDotrequirementsSection(existingFile);
|
|
69
|
+
if (!existingSection) {
|
|
70
|
+
// Append section to end of file
|
|
71
|
+
const separator = existingFile.endsWith('\n') ? '\n' : '\n\n';
|
|
72
|
+
writeFileSync(filePath, existingFile + separator + wrappedContent + '\n', 'utf-8');
|
|
73
|
+
return { action: 'appended' };
|
|
74
|
+
}
|
|
75
|
+
// Replace existing section
|
|
76
|
+
const newContent = existingFile.slice(0, existingSection.start) +
|
|
77
|
+
wrappedContent +
|
|
78
|
+
existingFile.slice(existingSection.end);
|
|
79
|
+
writeFileSync(filePath, newContent, 'utf-8');
|
|
80
|
+
return { action: 'updated', existingContent: existingSection.content };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get the full path to the context file for a platform
|
|
84
|
+
*/
|
|
85
|
+
export async function getContextFilePath(platform) {
|
|
86
|
+
const fileName = getContextFileName(platform);
|
|
87
|
+
if (!fileName)
|
|
88
|
+
return null;
|
|
89
|
+
const gitRoot = await findGitRoot();
|
|
90
|
+
if (!gitRoot)
|
|
91
|
+
return null;
|
|
92
|
+
return join(gitRoot, fileName);
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=context-file.js.map
|