@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.
- package/README.md +45 -1
- package/dist/cli.js +1 -0
- package/dist/commands/pull.d.ts +1 -0
- package/dist/commands/pull.js +70 -31
- package/dist/commands/push.d.ts +0 -5
- package/dist/commands/push.js +87 -204
- package/dist/config.js +0 -4
- package/dist/harness/cache.d.ts +2 -1
- package/dist/harness/cache.js +32 -1
- package/dist/mcp/convexClient.d.ts +11 -0
- package/dist/mcp/convexClient.js +51 -3
- package/dist/mcp/index.js +217 -140
- package/dist/mcp/requirements.d.ts +10 -0
- package/dist/mcp/requirements.js +15 -1
- package/dist/push/core.d.ts +105 -0
- package/dist/push/core.js +216 -0
- package/dist/push/index.d.ts +8 -0
- package/dist/push/index.js +10 -0
- package/dist/utils/project-settings.d.ts +9 -0
- package/dist/utils/project-settings.js +16 -0
- package/package.json +1 -1
package/dist/mcp/convexClient.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
1311
|
-
if (!
|
|
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
|
-
|
|
1326
|
-
|
|
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
|
|
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
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
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
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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:
|
|
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
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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:
|
|
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
|
-
//
|
|
1451
|
-
|
|
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
|
-
//
|
|
1461
|
-
|
|
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
|
-
|
|
1476
|
-
|
|
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:
|
|
1498
|
-
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}
|
|
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
|
-
|
|
1618
|
-
|
|
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:
|
|
1640
|
-
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
|
*/
|
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
|
*/
|