@popoverai/dotrequirements 0.24.0 → 0.24.2
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 +4 -3
- package/dist/cli.js +0 -0
- package/dist/codebase-to-spec/claude.d.ts +1 -0
- package/dist/codebase-to-spec/claude.js +9 -0
- package/dist/codebase-to-spec/pack.d.ts +16 -0
- package/dist/codebase-to-spec/pack.js +17 -3
- package/dist/codebase-to-spec/present.d.ts +8 -1
- package/dist/codebase-to-spec/present.js +7 -4
- package/dist/codebase-to-spec/progress.d.ts +6 -0
- package/dist/codebase-to-spec/progress.js +34 -0
- package/dist/codebase-to-spec/prompts/outline-reviewer.d.ts +1 -1
- package/dist/codebase-to-spec/prompts/outline-reviewer.js +3 -1
- package/dist/codebase-to-spec/prompts/planner-initial.d.ts +1 -1
- package/dist/codebase-to-spec/prompts/planner-initial.js +4 -0
- package/dist/codebase-to-spec/prompts/planner-revise.d.ts +1 -1
- package/dist/codebase-to-spec/prompts/planner-revise.js +2 -2
- package/dist/codebase-to-spec/prompts/spec-reviewer.d.ts +1 -1
- package/dist/codebase-to-spec/prompts/spec-reviewer.js +6 -1
- package/dist/codebase-to-spec/prompts/specifier.d.ts +1 -1
- package/dist/codebase-to-spec/prompts/specifier.js +6 -4
- package/dist/codebase-to-spec/prompts/style-check.d.ts +10 -2
- package/dist/codebase-to-spec/prompts/style-check.js +76 -46
- package/dist/codebase-to-spec/schemas.d.ts +85 -1
- package/dist/codebase-to-spec/schemas.js +25 -1
- package/dist/codebase-to-spec/specifier.js +6 -0
- package/dist/commands/codebase-to-spec/index.js +13 -0
- package/dist/commands/codebase-to-spec/pack.d.ts +6 -0
- package/dist/commands/codebase-to-spec/pack.js +1 -0
- package/dist/commands/codebase-to-spec/present.d.ts +5 -0
- package/dist/commands/codebase-to-spec/present.js +6 -1
- package/dist/commands/codebase-to-spec/run.js +1 -0
- package/package.json +5 -6
- package/dist/codebase-to-spec/prompts/planner-apply.d.ts +0 -12
- package/dist/codebase-to-spec/prompts/planner-apply.js +0 -32
- package/dist/commands/browsertest.d.ts +0 -6
- package/dist/commands/browsertest.js +0 -212
- package/dist/commands/login.d.ts +0 -12
- package/dist/commands/login.js +0 -117
- package/dist/commands/logout.d.ts +0 -5
- package/dist/commands/logout.js +0 -17
- package/dist/commands/mcp-setup.d.ts +0 -5
- package/dist/commands/mcp-setup.js +0 -441
- package/dist/commands/test.d.ts +0 -6
- package/dist/commands/test.js +0 -72
- package/dist/mcp/grep.d.ts +0 -24
- package/dist/mcp/grep.js +0 -306
- package/dist/mcp/handlers/coverage.d.ts +0 -44
- package/dist/mcp/handlers/coverage.js +0 -103
- package/dist/mcp/requirements.d.ts +0 -57
- package/dist/mcp/requirements.js +0 -155
- package/dist/mcp/testCodeExtractor.d.ts +0 -22
- package/dist/mcp/testCodeExtractor.js +0 -150
- package/dist/mcp/types.d.ts +0 -27
- package/dist/mcp/types.js +0 -2
- package/dist/utils/local-project.d.ts +0 -31
- package/dist/utils/local-project.js +0 -33
- package/dist/utils/token-refresh.d.ts +0 -24
- package/dist/utils/token-refresh.js +0 -69
- package/dist/utils/token-storage.d.ts +0 -31
- package/dist/utils/token-storage.js +0 -57
package/dist/mcp/grep.js
DELETED
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
const execFileAsync = promisify(execFile);
|
|
4
|
-
/**
|
|
5
|
-
* Check if ripgrep is available
|
|
6
|
-
*/
|
|
7
|
-
async function hasRipgrep() {
|
|
8
|
-
try {
|
|
9
|
-
await execFileAsync("rg", ["--version"]);
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Find all test files that reference a specific requirement ID
|
|
18
|
-
*
|
|
19
|
-
* Uses execFile with array arguments to avoid command injection vulnerabilities.
|
|
20
|
-
*/
|
|
21
|
-
export async function findTestsForRequirement(workspaceRoot, requirementId) {
|
|
22
|
-
const useRg = await hasRipgrep();
|
|
23
|
-
// Escape special regex characters in the ID
|
|
24
|
-
const escapedId = requirementId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
-
// Pattern to match requirement('ID'), requirement("ID"), or requirement(`ID`)
|
|
26
|
-
const pattern = `requirement\\s*\\(\\s*['"\`]${escapedId}`;
|
|
27
|
-
try {
|
|
28
|
-
let output;
|
|
29
|
-
if (useRg) {
|
|
30
|
-
// Use ripgrep for better performance
|
|
31
|
-
// Using execFile with array args to avoid shell injection
|
|
32
|
-
const { stdout } = await execFileAsync("rg", [
|
|
33
|
-
"-n",
|
|
34
|
-
"--column",
|
|
35
|
-
pattern,
|
|
36
|
-
"--type",
|
|
37
|
-
"ts",
|
|
38
|
-
"--type",
|
|
39
|
-
"js",
|
|
40
|
-
"--type",
|
|
41
|
-
"tsx",
|
|
42
|
-
"--type",
|
|
43
|
-
"jsx",
|
|
44
|
-
workspaceRoot,
|
|
45
|
-
], { maxBuffer: 10 * 1024 * 1024 }).catch(() => ({ stdout: "" })); // ripgrep returns non-zero on no matches
|
|
46
|
-
output = stdout;
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
// Fall back to grep using execFile
|
|
50
|
-
const { stdout } = await execFileAsync("grep", [
|
|
51
|
-
"-rn",
|
|
52
|
-
"--include=*.ts",
|
|
53
|
-
"--include=*.tsx",
|
|
54
|
-
"--include=*.js",
|
|
55
|
-
"--include=*.jsx",
|
|
56
|
-
"-E",
|
|
57
|
-
pattern,
|
|
58
|
-
workspaceRoot,
|
|
59
|
-
], { maxBuffer: 10 * 1024 * 1024 }).catch(() => ({ stdout: "" })); // grep returns non-zero on no matches
|
|
60
|
-
output = stdout;
|
|
61
|
-
}
|
|
62
|
-
return parseGrepOutput(output, requirementId, useRg);
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
// Grep returns exit code 1 when no matches found
|
|
66
|
-
return [];
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Find all requirement references in a specific file using AST parsing
|
|
71
|
-
*/
|
|
72
|
-
export async function findRequirementsInFile(filePath) {
|
|
73
|
-
const { parse } = await import("@babel/parser");
|
|
74
|
-
const traverse = await import("@babel/traverse");
|
|
75
|
-
const t = await import("@babel/types");
|
|
76
|
-
const fs = await import("node:fs");
|
|
77
|
-
const pathModule = await import("node:path");
|
|
78
|
-
const results = [];
|
|
79
|
-
// Resolve relative paths from current working directory
|
|
80
|
-
let resolvedPath = pathModule.isAbsolute(filePath)
|
|
81
|
-
? filePath
|
|
82
|
-
: pathModule.resolve(process.cwd(), filePath);
|
|
83
|
-
// If file doesn't exist and path is relative, try resolving from CLI package root
|
|
84
|
-
if (!pathModule.isAbsolute(filePath) && !fs.existsSync(resolvedPath)) {
|
|
85
|
-
const cliPackageRoot = pathModule.resolve(__dirname, "../..");
|
|
86
|
-
const alternativePath = pathModule.resolve(cliPackageRoot, filePath);
|
|
87
|
-
if (fs.existsSync(alternativePath)) {
|
|
88
|
-
resolvedPath = alternativePath;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
const code = fs.readFileSync(resolvedPath, "utf-8");
|
|
93
|
-
const ast = parse(code, {
|
|
94
|
-
sourceType: "module",
|
|
95
|
-
plugins: ["typescript", "jsx"],
|
|
96
|
-
errorRecovery: true,
|
|
97
|
-
});
|
|
98
|
-
// Handle both ES module and CommonJS exports for traverse
|
|
99
|
-
// biome-ignore lint/suspicious/noExplicitAny: @babel/traverse ships a CJS default-export that's a callable, with .default property nested 1-2 levels. The interop dance requires escaping the function-vs-namespace type wall.
|
|
100
|
-
let traverseFunc = traverse;
|
|
101
|
-
if (typeof traverseFunc !== "function") {
|
|
102
|
-
traverseFunc = traverseFunc.default;
|
|
103
|
-
}
|
|
104
|
-
if (typeof traverseFunc !== "function") {
|
|
105
|
-
traverseFunc = traverseFunc.default;
|
|
106
|
-
}
|
|
107
|
-
traverseFunc(ast, {
|
|
108
|
-
// biome-ignore lint/suspicious/noExplicitAny: babel-traverse visitor `path` is a deeply-generic NodePath whose precise type depends on what handler you're inside; `any` is the documented practice for plugin code.
|
|
109
|
-
CallExpression(path) {
|
|
110
|
-
const callee = path.node.callee;
|
|
111
|
-
// Check if this is a requirement() call
|
|
112
|
-
if (t.isIdentifier(callee) && callee.name === "requirement") {
|
|
113
|
-
const args = path.node.arguments;
|
|
114
|
-
const loc = path.node.loc;
|
|
115
|
-
// Extract all string literal arguments (supports multi-requirement calls)
|
|
116
|
-
for (const arg of args) {
|
|
117
|
-
if (t.isStringLiteral(arg)) {
|
|
118
|
-
const requirementId = arg.value;
|
|
119
|
-
if (loc) {
|
|
120
|
-
results.push({
|
|
121
|
-
file: resolvedPath,
|
|
122
|
-
line: loc.start.line,
|
|
123
|
-
column: loc.start.column + 1, // 1-indexed
|
|
124
|
-
requirementId,
|
|
125
|
-
context: code.split("\n")[loc.start.line - 1] || "",
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
return results;
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
return [];
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Find all requirement references in the entire workspace
|
|
141
|
-
*
|
|
142
|
-
* Uses execFile with array arguments to avoid command injection vulnerabilities.
|
|
143
|
-
*/
|
|
144
|
-
export async function findAllTestReferences(workspaceRoot) {
|
|
145
|
-
const useRg = await hasRipgrep();
|
|
146
|
-
// Pattern to match any requirement() call
|
|
147
|
-
const pattern = `requirement\\s*\\(\\s*['"\`][^'"\`]+`;
|
|
148
|
-
try {
|
|
149
|
-
let output;
|
|
150
|
-
if (useRg) {
|
|
151
|
-
// Using execFile with array args to avoid shell injection
|
|
152
|
-
const { stdout } = await execFileAsync("rg", [
|
|
153
|
-
"-n",
|
|
154
|
-
"--column",
|
|
155
|
-
pattern,
|
|
156
|
-
"--type",
|
|
157
|
-
"ts",
|
|
158
|
-
"--type",
|
|
159
|
-
"js",
|
|
160
|
-
"--type",
|
|
161
|
-
"tsx",
|
|
162
|
-
"--type",
|
|
163
|
-
"jsx",
|
|
164
|
-
workspaceRoot,
|
|
165
|
-
], { maxBuffer: 10 * 1024 * 1024 }).catch(() => ({ stdout: "" })); // ripgrep returns non-zero on no matches
|
|
166
|
-
output = stdout;
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
const { stdout } = await execFileAsync("grep", [
|
|
170
|
-
"-rn",
|
|
171
|
-
"--include=*.ts",
|
|
172
|
-
"--include=*.tsx",
|
|
173
|
-
"--include=*.js",
|
|
174
|
-
"--include=*.jsx",
|
|
175
|
-
"-E",
|
|
176
|
-
pattern,
|
|
177
|
-
workspaceRoot,
|
|
178
|
-
], { maxBuffer: 10 * 1024 * 1024 }).catch(() => ({ stdout: "" })); // grep returns non-zero on no matches
|
|
179
|
-
output = stdout;
|
|
180
|
-
}
|
|
181
|
-
return parseGrepOutput(output, "", useRg);
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
return [];
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Parse grep/ripgrep output into TestReference objects
|
|
189
|
-
*/
|
|
190
|
-
function parseGrepOutput(output, expectedId, isRipgrep, singleFilePath) {
|
|
191
|
-
const results = [];
|
|
192
|
-
const lines = output.trim().split("\n").filter(Boolean);
|
|
193
|
-
for (const line of lines) {
|
|
194
|
-
let match;
|
|
195
|
-
let file;
|
|
196
|
-
let lineNum;
|
|
197
|
-
let column;
|
|
198
|
-
let context;
|
|
199
|
-
if (isRipgrep) {
|
|
200
|
-
// ripgrep format: file:line:column:content
|
|
201
|
-
match = line.match(/^(.+?):(\d+):(\d+):(.*)$/);
|
|
202
|
-
if (!match)
|
|
203
|
-
continue;
|
|
204
|
-
file = match[1];
|
|
205
|
-
lineNum = parseInt(match[2], 10);
|
|
206
|
-
column = parseInt(match[3], 10);
|
|
207
|
-
context = match[4];
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
// grep format: file:line:content (no column)
|
|
211
|
-
if (singleFilePath) {
|
|
212
|
-
match = line.match(/^(\d+):(.*)$/);
|
|
213
|
-
if (!match)
|
|
214
|
-
continue;
|
|
215
|
-
file = singleFilePath;
|
|
216
|
-
lineNum = parseInt(match[1], 10);
|
|
217
|
-
context = match[2];
|
|
218
|
-
column = 1;
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
match = line.match(/^(.+?):(\d+):(.*)$/);
|
|
222
|
-
if (!match)
|
|
223
|
-
continue;
|
|
224
|
-
file = match[1];
|
|
225
|
-
lineNum = parseInt(match[2], 10);
|
|
226
|
-
context = match[3];
|
|
227
|
-
column = 1;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
// Extract the requirement ID from the context
|
|
231
|
-
const idMatch = context.match(/requirement\s*\(\s*['"`]([^'"`]+)/);
|
|
232
|
-
const requirementId = idMatch ? idMatch[1] : expectedId;
|
|
233
|
-
results.push({
|
|
234
|
-
file,
|
|
235
|
-
line: lineNum,
|
|
236
|
-
column,
|
|
237
|
-
requirementId,
|
|
238
|
-
context: context.trim(),
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
return results;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Get all unique requirement IDs referenced in tests
|
|
245
|
-
* Normalizes child IDs (e.g., AUTH-VALID-LOGIN.0) to root IDs (e.g., AUTH-VALID-LOGIN)
|
|
246
|
-
* Uses AST parsing instead of grep for reliability across environments
|
|
247
|
-
*/
|
|
248
|
-
export async function getReferencedRequirementIds(workspaceRoot) {
|
|
249
|
-
const { glob } = await import("glob");
|
|
250
|
-
const { parse } = await import("@babel/parser");
|
|
251
|
-
const traverse = await import("@babel/traverse");
|
|
252
|
-
const t = await import("@babel/types");
|
|
253
|
-
const fs = await import("node:fs");
|
|
254
|
-
// Find all test files using glob (same pattern as get_requirement tool)
|
|
255
|
-
const testFiles = await glob("**/*.{test,spec}.{js,jsx,ts,tsx}", {
|
|
256
|
-
cwd: workspaceRoot,
|
|
257
|
-
absolute: true,
|
|
258
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"],
|
|
259
|
-
});
|
|
260
|
-
const allRequirementIds = [];
|
|
261
|
-
// For each test file, parse AST and extract requirement IDs
|
|
262
|
-
for (const file of testFiles) {
|
|
263
|
-
try {
|
|
264
|
-
const code = fs.readFileSync(file, "utf-8");
|
|
265
|
-
const ast = parse(code, {
|
|
266
|
-
sourceType: "module",
|
|
267
|
-
plugins: ["typescript", "jsx"],
|
|
268
|
-
errorRecovery: true,
|
|
269
|
-
});
|
|
270
|
-
// Handle both ES module and CommonJS exports for traverse
|
|
271
|
-
// @babel/traverse can be imported as: traverse.default.default in compiled code
|
|
272
|
-
// biome-ignore lint/suspicious/noExplicitAny: see earlier comment on babel/traverse CJS/ESM interop
|
|
273
|
-
let traverseFunc = traverse;
|
|
274
|
-
if (typeof traverseFunc !== "function") {
|
|
275
|
-
traverseFunc = traverseFunc.default;
|
|
276
|
-
}
|
|
277
|
-
if (typeof traverseFunc !== "function") {
|
|
278
|
-
traverseFunc = traverseFunc.default;
|
|
279
|
-
}
|
|
280
|
-
traverseFunc(ast, {
|
|
281
|
-
// biome-ignore lint/suspicious/noExplicitAny: babel-traverse visitor `path` is a deeply-generic NodePath whose precise type depends on what handler you're inside; `any` is the documented practice for plugin code.
|
|
282
|
-
CallExpression(path) {
|
|
283
|
-
const callee = path.node.callee;
|
|
284
|
-
// Check if this is a requirement() call
|
|
285
|
-
if (t.isIdentifier(callee) && callee.name === "requirement") {
|
|
286
|
-
const args = path.node.arguments;
|
|
287
|
-
// Extract all string literal arguments (supports multi-requirement calls)
|
|
288
|
-
for (const arg of args) {
|
|
289
|
-
if (t.isStringLiteral(arg)) {
|
|
290
|
-
allRequirementIds.push(arg.value);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
},
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
catch (error) { }
|
|
298
|
-
}
|
|
299
|
-
// Extract root IDs by removing the path suffix (everything after the first dot)
|
|
300
|
-
const rootIds = allRequirementIds.map((id) => {
|
|
301
|
-
const dotIndex = id.indexOf(".");
|
|
302
|
-
return dotIndex > 0 ? id.substring(0, dotIndex) : id;
|
|
303
|
-
});
|
|
304
|
-
return new Set(rootIds);
|
|
305
|
-
}
|
|
306
|
-
//# sourceMappingURL=grep.js.map
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Coverage handlers for MCP tools
|
|
3
|
-
*
|
|
4
|
-
* Provides cloud coverage query functionality:
|
|
5
|
-
* - get_requirement_coverage: Coverage for a specific requirement
|
|
6
|
-
* - get_project_coverage_summary: Project-wide coverage summary
|
|
7
|
-
*/
|
|
8
|
-
import type { HandlerContext, ToolResponse } from "./types.js";
|
|
9
|
-
/**
|
|
10
|
-
* Arguments for get_requirement_coverage tool
|
|
11
|
-
*/
|
|
12
|
-
export interface GetRequirementCoverageArgs {
|
|
13
|
-
requirementKey: string;
|
|
14
|
-
projectId?: string;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Arguments for get_project_coverage_summary tool
|
|
18
|
-
*/
|
|
19
|
-
export interface GetProjectCoverageSummaryArgs {
|
|
20
|
-
branch?: string;
|
|
21
|
-
sinceTimestamp?: number;
|
|
22
|
-
projectId?: string;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Handler for get_requirement_coverage tool
|
|
26
|
-
*
|
|
27
|
-
* Requirements covered:
|
|
28
|
-
* - MCP-COVERAGE-1.0: When credentials are valid, the response shows when the requirement was last tested
|
|
29
|
-
* - MCP-COVERAGE-1.1: The response includes the most recent branch and test file where coverage was recorded
|
|
30
|
-
* - MCP-COVERAGE-1.2: When the requirement has no coverage records, the response indicates this
|
|
31
|
-
* - MCP-COVERAGE-1.3: When credentials are missing or invalid, an error explains how to authenticate
|
|
32
|
-
*/
|
|
33
|
-
export declare function handleGetRequirementCoverage(args: GetRequirementCoverageArgs, context: HandlerContext): Promise<ToolResponse>;
|
|
34
|
-
/**
|
|
35
|
-
* Handler for get_project_coverage_summary tool
|
|
36
|
-
*
|
|
37
|
-
* Requirements covered:
|
|
38
|
-
* - MCP-COVERAGE-2.0: The response summarizes which requirements have been tested and which have not
|
|
39
|
-
* - MCP-COVERAGE-2.1: When a branch filter is provided, coverage results include only records from that branch
|
|
40
|
-
* - MCP-COVERAGE-2.2: When sinceTimestamp is provided, only coverage recorded after that time is included
|
|
41
|
-
* - MCP-COVERAGE-2.3: When credentials are missing or invalid, an error explains how to authenticate
|
|
42
|
-
*/
|
|
43
|
-
export declare function handleGetProjectCoverageSummary(args: GetProjectCoverageSummaryArgs, context: HandlerContext): Promise<ToolResponse>;
|
|
44
|
-
//# sourceMappingURL=coverage.d.ts.map
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Coverage handlers for MCP tools
|
|
3
|
-
*
|
|
4
|
-
* Provides cloud coverage query functionality:
|
|
5
|
-
* - get_requirement_coverage: Coverage for a specific requirement
|
|
6
|
-
* - get_project_coverage_summary: Project-wide coverage summary
|
|
7
|
-
*/
|
|
8
|
-
import { CONVEX_URL, getProjectCoverage as queryProjectCoverage, getRequirementCoverage as queryRequirementCoverage, } from "../convexClient.js";
|
|
9
|
-
import { errorResponse, textResponse } from "./types.js";
|
|
10
|
-
/**
|
|
11
|
-
* Handler for get_requirement_coverage tool
|
|
12
|
-
*
|
|
13
|
-
* Requirements covered:
|
|
14
|
-
* - MCP-COVERAGE-1.0: When credentials are valid, the response shows when the requirement was last tested
|
|
15
|
-
* - MCP-COVERAGE-1.1: The response includes the most recent branch and test file where coverage was recorded
|
|
16
|
-
* - MCP-COVERAGE-1.2: When the requirement has no coverage records, the response indicates this
|
|
17
|
-
* - MCP-COVERAGE-1.3: When credentials are missing or invalid, an error explains how to authenticate
|
|
18
|
-
*/
|
|
19
|
-
export async function handleGetRequirementCoverage(args, context) {
|
|
20
|
-
const { requirementKey, projectId } = args;
|
|
21
|
-
// MCP-COVERAGE-1.3: Get project credentials (throws if not configured)
|
|
22
|
-
let project;
|
|
23
|
-
try {
|
|
24
|
-
project = await context.getProjectFromDiscovery(projectId);
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
return errorResponse(`Coverage query requires project credentials. Run \`dotrequirements link\` to connect to cloud.\n\nError: ${error instanceof Error ? error.message : String(error)}`);
|
|
28
|
-
}
|
|
29
|
-
// Query cloud for coverage
|
|
30
|
-
const coverage = await queryRequirementCoverage(requirementKey, project.projectId, project.projectSecret, CONVEX_URL);
|
|
31
|
-
// MCP-COVERAGE-1.2: No coverage records
|
|
32
|
-
if (!coverage.lastTestedAt) {
|
|
33
|
-
return textResponse(`**${requirementKey}** has not been tested yet.`);
|
|
34
|
-
}
|
|
35
|
-
// MCP-COVERAGE-1.0: Show when requirement was last tested
|
|
36
|
-
const lastTested = new Date(coverage.lastTestedAt);
|
|
37
|
-
// MCP-COVERAGE-1.1: Include branch and test file
|
|
38
|
-
const testLocation = coverage.testFile
|
|
39
|
-
? `\n**Test Location:** \`${coverage.testFile}${coverage.testLine ? `:${coverage.testLine}` : ""}\``
|
|
40
|
-
: "";
|
|
41
|
-
let branchInfo = `\n**Branch:** ${coverage.branch}`;
|
|
42
|
-
if (coverage.allBranches.length > 1) {
|
|
43
|
-
const otherBranches = coverage.allBranches
|
|
44
|
-
.filter((b) => b.branch !== coverage.branch)
|
|
45
|
-
.map((b) => `- ${b.branch}: ${new Date(b.lastTestedAt).toISOString()}`)
|
|
46
|
-
.join("\n");
|
|
47
|
-
branchInfo += `\n\n**Also tested on:**\n${otherBranches}`;
|
|
48
|
-
}
|
|
49
|
-
return textResponse(`# Coverage for ${requirementKey}\n\n**Last Tested:** ${lastTested.toISOString()}${branchInfo}${testLocation}`);
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Handler for get_project_coverage_summary tool
|
|
53
|
-
*
|
|
54
|
-
* Requirements covered:
|
|
55
|
-
* - MCP-COVERAGE-2.0: The response summarizes which requirements have been tested and which have not
|
|
56
|
-
* - MCP-COVERAGE-2.1: When a branch filter is provided, coverage results include only records from that branch
|
|
57
|
-
* - MCP-COVERAGE-2.2: When sinceTimestamp is provided, only coverage recorded after that time is included
|
|
58
|
-
* - MCP-COVERAGE-2.3: When credentials are missing or invalid, an error explains how to authenticate
|
|
59
|
-
*/
|
|
60
|
-
export async function handleGetProjectCoverageSummary(args, context) {
|
|
61
|
-
const { branch, sinceTimestamp, projectId } = args;
|
|
62
|
-
// MCP-COVERAGE-2.3: Get project credentials (throws if not configured)
|
|
63
|
-
let project;
|
|
64
|
-
try {
|
|
65
|
-
project = await context.getProjectFromDiscovery(projectId);
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
return errorResponse(`Coverage query requires project credentials. Run \`dotrequirements link\` to connect to cloud.\n\nError: ${error instanceof Error ? error.message : String(error)}`);
|
|
69
|
-
}
|
|
70
|
-
// Query cloud for project coverage with optional filters
|
|
71
|
-
const coverage = await queryProjectCoverage(project.projectId, project.projectSecret, CONVEX_URL, { branch, sinceTimestamp });
|
|
72
|
-
// MCP-COVERAGE-2.0: Calculate and display coverage summary
|
|
73
|
-
const total = coverage.tested.length + coverage.untested.length;
|
|
74
|
-
const percentage = total > 0 ? ((coverage.tested.length / total) * 100).toFixed(1) : "0.0";
|
|
75
|
-
// MCP-COVERAGE-2.1 & MCP-COVERAGE-2.2: Show filter info if provided
|
|
76
|
-
let filterInfo = "";
|
|
77
|
-
if (branch) {
|
|
78
|
-
filterInfo += `\n**Branch:** ${branch}`;
|
|
79
|
-
}
|
|
80
|
-
if (sinceTimestamp) {
|
|
81
|
-
filterInfo += `\n**Since:** ${new Date(sinceTimestamp).toISOString()}`;
|
|
82
|
-
}
|
|
83
|
-
let testedSection = "";
|
|
84
|
-
if (coverage.tested.length > 0) {
|
|
85
|
-
const testedList = coverage.tested
|
|
86
|
-
.map((t) => {
|
|
87
|
-
const date = new Date(t.lastTestedAt).toISOString();
|
|
88
|
-
const location = t.testFile
|
|
89
|
-
? ` (\`${t.testFile}${t.testLine ? `:${t.testLine}` : ""}\`)`
|
|
90
|
-
: "";
|
|
91
|
-
return `- **${t.requirementKey}** [${t.branch}]: ${date}${location}`;
|
|
92
|
-
})
|
|
93
|
-
.join("\n");
|
|
94
|
-
testedSection = `\n\n## Tested Requirements (${coverage.tested.length})\n\n${testedList}`;
|
|
95
|
-
}
|
|
96
|
-
let untestedSection = "";
|
|
97
|
-
if (coverage.untested.length > 0) {
|
|
98
|
-
const untestedList = coverage.untested.map((key) => `- ${key}`).join("\n");
|
|
99
|
-
untestedSection = `\n\n## Untested Requirements (${coverage.untested.length})\n\n${untestedList}`;
|
|
100
|
-
}
|
|
101
|
-
return textResponse(`# Project Coverage Summary\n\n**Total Requirements:** ${total}\n**Coverage:** ${percentage}%${filterInfo}${testedSection}${untestedSection}`);
|
|
102
|
-
}
|
|
103
|
-
//# sourceMappingURL=coverage.js.map
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { type Metadata, type RequirementNode } from "../schema/index.js";
|
|
2
|
-
import type { FlattenedRequirement } from "./types.js";
|
|
3
|
-
/**
|
|
4
|
-
* Parsed requirements file
|
|
5
|
-
*/
|
|
6
|
-
export interface ParsedRequirementsFile {
|
|
7
|
-
metadata: Metadata;
|
|
8
|
-
requirements: RequirementNode[];
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Find all *.requirements.md files in the workspace.
|
|
12
|
-
* Supports both .requirements/ directories and colocated files.
|
|
13
|
-
* Excludes example files, test fixtures, and build artifacts.
|
|
14
|
-
*/
|
|
15
|
-
export declare function findRequirementsFiles(workspaceRoot: string): Promise<string[]>;
|
|
16
|
-
/**
|
|
17
|
-
* Load all requirements from the workspace
|
|
18
|
-
*/
|
|
19
|
-
export declare function loadAllRequirements(workspaceRoot: string): Promise<{
|
|
20
|
-
files: ParsedRequirementsFile[];
|
|
21
|
-
flattened: FlattenedRequirement[];
|
|
22
|
-
}>;
|
|
23
|
-
/**
|
|
24
|
-
* Flatten a requirements file into searchable requirements
|
|
25
|
-
*/
|
|
26
|
-
export declare function flattenRequirementsFile(file: ParsedRequirementsFile, sourceFile: string): FlattenedRequirement[];
|
|
27
|
-
/**
|
|
28
|
-
* Search requirements by text query
|
|
29
|
-
*/
|
|
30
|
-
export declare function searchRequirements(requirements: FlattenedRequirement[], query: string): FlattenedRequirement[];
|
|
31
|
-
/**
|
|
32
|
-
* Get a specific requirement by ID (supports partial matching)
|
|
33
|
-
*/
|
|
34
|
-
export declare function getRequirementById(requirements: FlattenedRequirement[], id: string): FlattenedRequirement | undefined;
|
|
35
|
-
/**
|
|
36
|
-
* Get a requirement and all its children
|
|
37
|
-
*/
|
|
38
|
-
export declare function getRequirementTree(requirements: FlattenedRequirement[], rootId: string): FlattenedRequirement[];
|
|
39
|
-
/**
|
|
40
|
-
* Format a requirement for display
|
|
41
|
-
*/
|
|
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
|
-
};
|
|
53
|
-
/**
|
|
54
|
-
* Format a requirement tree for display
|
|
55
|
-
*/
|
|
56
|
-
export declare function formatRequirementTree(requirements: FlattenedRequirement[]): string;
|
|
57
|
-
//# sourceMappingURL=requirements.d.ts.map
|
package/dist/mcp/requirements.js
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import * as path from "node:path";
|
|
2
|
-
import { glob } from "glob";
|
|
3
|
-
import { buildRequirementMarkdown, getAllRequirements, parseRequirementsFile, parseRequirementsFromFile, } from "../schema/index.js";
|
|
4
|
-
/**
|
|
5
|
-
* Find all *.requirements.md files in the workspace.
|
|
6
|
-
* Supports both .requirements/ directories and colocated files.
|
|
7
|
-
* Excludes example files, test fixtures, and build artifacts.
|
|
8
|
-
*/
|
|
9
|
-
export async function findRequirementsFiles(workspaceRoot) {
|
|
10
|
-
const pattern = "**/*.requirements.md";
|
|
11
|
-
const matches = await glob(pattern, {
|
|
12
|
-
cwd: workspaceRoot,
|
|
13
|
-
ignore: [
|
|
14
|
-
"**/node_modules/**",
|
|
15
|
-
"**/dist/**",
|
|
16
|
-
"**/.git/**",
|
|
17
|
-
"**/build/**",
|
|
18
|
-
// Exclude example and test fixture directories
|
|
19
|
-
"**/example/**",
|
|
20
|
-
"**/examples/**",
|
|
21
|
-
"**/__tests__/**",
|
|
22
|
-
"**/fixtures/**",
|
|
23
|
-
"**/.fixtures/**",
|
|
24
|
-
],
|
|
25
|
-
dot: true, // Include dotfiles/dotdirs like .requirements/
|
|
26
|
-
});
|
|
27
|
-
return matches.map((m) => path.join(workspaceRoot, m));
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Load all requirements from the workspace
|
|
31
|
-
*/
|
|
32
|
-
export async function loadAllRequirements(workspaceRoot) {
|
|
33
|
-
const filePaths = await findRequirementsFiles(workspaceRoot);
|
|
34
|
-
const allFiles = [];
|
|
35
|
-
const allFlattened = [];
|
|
36
|
-
for (const filePath of filePaths) {
|
|
37
|
-
try {
|
|
38
|
-
const parsed = parseRequirementsFromFile(filePath);
|
|
39
|
-
allFiles.push(parsed);
|
|
40
|
-
const flattened = flattenRequirementsFile(parsed, filePath);
|
|
41
|
-
allFlattened.push(...flattened);
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
// Skip invalid files
|
|
45
|
-
console.error(`Failed to parse ${filePath}:`, error);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return { files: allFiles, flattened: allFlattened };
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Flatten a requirements file into searchable requirements
|
|
52
|
-
*/
|
|
53
|
-
export function flattenRequirementsFile(file, sourceFile) {
|
|
54
|
-
const results = [];
|
|
55
|
-
// Use shared utility to get all requirements (flattened)
|
|
56
|
-
const allReqs = getAllRequirements(file.requirements);
|
|
57
|
-
for (const req of allReqs) {
|
|
58
|
-
// Extract position from ID (e.g., "REQ123.0.1" -> "0.1")
|
|
59
|
-
const idParts = req.id.split(".");
|
|
60
|
-
const rootId = idParts[0];
|
|
61
|
-
const position = idParts.slice(1).join(".");
|
|
62
|
-
results.push({
|
|
63
|
-
id: req.id,
|
|
64
|
-
rootId: rootId,
|
|
65
|
-
label: req.label,
|
|
66
|
-
content: req.content,
|
|
67
|
-
path: idParts.slice(1), // ["0", "1"] for "REQ123.0.1"
|
|
68
|
-
sourceFile: sourceFile,
|
|
69
|
-
documentTitle: file.metadata.document?.title || rootId,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
return results;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Search requirements by text query
|
|
76
|
-
*/
|
|
77
|
-
export function searchRequirements(requirements, query) {
|
|
78
|
-
const lowerQuery = query.toLowerCase();
|
|
79
|
-
return requirements.filter((req) => {
|
|
80
|
-
// Search in ID
|
|
81
|
-
if (req.id.toLowerCase().includes(lowerQuery))
|
|
82
|
-
return true;
|
|
83
|
-
// Search in content
|
|
84
|
-
if (req.content.toLowerCase().includes(lowerQuery))
|
|
85
|
-
return true;
|
|
86
|
-
// Search in label
|
|
87
|
-
if (req.label.toLowerCase().includes(lowerQuery))
|
|
88
|
-
return true;
|
|
89
|
-
return false;
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Get a specific requirement by ID (supports partial matching)
|
|
94
|
-
*/
|
|
95
|
-
export function getRequirementById(requirements, id) {
|
|
96
|
-
// Exact match first
|
|
97
|
-
const exact = requirements.find((r) => r.id === id);
|
|
98
|
-
if (exact)
|
|
99
|
-
return exact;
|
|
100
|
-
// Try partial match (just the root ID)
|
|
101
|
-
return requirements.find((r) => r.rootId === id || r.id.startsWith(`${id}.`));
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Get a requirement and all its children
|
|
105
|
-
*/
|
|
106
|
-
export function getRequirementTree(requirements, rootId) {
|
|
107
|
-
return requirements.filter((r) => r.rootId === rootId || r.id === rootId);
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Format a requirement for display
|
|
111
|
-
*/
|
|
112
|
-
export function formatRequirement(req) {
|
|
113
|
-
const indent = " ".repeat(req.path.length);
|
|
114
|
-
const label = req.label ? ` (${req.label})` : "";
|
|
115
|
-
return `${indent}${req.id}${label}: ${req.content}`;
|
|
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
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Format a requirement tree for display
|
|
133
|
-
*/
|
|
134
|
-
export function formatRequirementTree(requirements) {
|
|
135
|
-
// Sort by path to maintain hierarchy - children appear immediately after parent
|
|
136
|
-
const sorted = [...requirements].sort((a, b) => {
|
|
137
|
-
// Compare paths element by element for proper tree order
|
|
138
|
-
const maxLen = Math.max(a.path.length, b.path.length);
|
|
139
|
-
for (let i = 0; i < maxLen; i++) {
|
|
140
|
-
const aVal = i < a.path.length ? parseInt(a.path[i], 10) : -1;
|
|
141
|
-
const bVal = i < b.path.length ? parseInt(b.path[i], 10) : -1;
|
|
142
|
-
// If one path is a prefix of the other, the shorter one comes first
|
|
143
|
-
if (aVal === -1)
|
|
144
|
-
return -1;
|
|
145
|
-
if (bVal === -1)
|
|
146
|
-
return 1;
|
|
147
|
-
if (aVal !== bVal) {
|
|
148
|
-
return aVal - bVal;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return 0;
|
|
152
|
-
});
|
|
153
|
-
return sorted.map(formatRequirement).join("\n");
|
|
154
|
-
}
|
|
155
|
-
//# sourceMappingURL=requirements.js.map
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extract test code context using @babel/parser AST traversal.
|
|
3
|
-
*
|
|
4
|
-
* Finds requirement() call references and extracts the enclosing
|
|
5
|
-
* meaningful code block (function, describe, test, etc.)
|
|
6
|
-
*/
|
|
7
|
-
export interface TestCodeReference {
|
|
8
|
-
file: string;
|
|
9
|
-
startLine: number;
|
|
10
|
-
endLine: number;
|
|
11
|
-
code: string;
|
|
12
|
-
requirementId: string;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Find all test code blocks that reference a specific requirement
|
|
16
|
-
*/
|
|
17
|
-
export declare function findTestCodeForRequirement(filePath: string, requirementId: string): TestCodeReference[];
|
|
18
|
-
/**
|
|
19
|
-
* Find all files that reference a requirement
|
|
20
|
-
*/
|
|
21
|
-
export declare function findFilesWithRequirement(files: string[], requirementId: string): string[];
|
|
22
|
-
//# sourceMappingURL=testCodeExtractor.d.ts.map
|