@popoverai/dotrequirements 0.26.2 → 0.27.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 +13 -69
- package/dist/cli.js +19 -7
- package/dist/commands/ai-setup.d.ts +8 -2
- package/dist/commands/ai-setup.js +153 -347
- package/dist/commands/init.js +1 -1
- package/dist/commands/mcp.d.ts +8 -2
- package/dist/commands/mcp.js +17 -6
- package/dist/commands/review-test.d.ts +5 -1
- package/dist/commands/review-test.js +101 -7
- package/dist/commands/style-check.d.ts +1 -0
- package/dist/commands/style-check.js +137 -13
- package/dist/convex.d.ts +1 -3
- package/dist/convex.js +3 -3
- package/dist/requirements/cloud-ai.d.ts +21 -8
- package/dist/requirements/cloud-ai.js +10 -8
- package/dist/schema/parser-core.d.ts +13 -0
- package/dist/schema/parser-core.js +33 -9
- package/dist/templates/context-file-section.md +25 -22
- package/dist/utils/context-file.d.ts +7 -3
- package/dist/utils/context-file.js +10 -7
- package/dist/utils/project-settings.d.ts +1 -0
- package/dist/utils/project-settings.js +22 -0
- package/package.json +3 -5
- package/dist/mcp/convexClient.d.ts +0 -19
- package/dist/mcp/convexClient.js +0 -24
- package/dist/mcp/handlers/authoring.d.ts +0 -41
- package/dist/mcp/handlers/authoring.js +0 -113
- package/dist/mcp/handlers/debug.d.ts +0 -16
- package/dist/mcp/handlers/debug.js +0 -37
- package/dist/mcp/handlers/get.d.ts +0 -24
- package/dist/mcp/handlers/get.js +0 -69
- package/dist/mcp/handlers/index.d.ts +0 -28
- package/dist/mcp/handlers/index.js +0 -19
- package/dist/mcp/handlers/list.d.ts +0 -7
- package/dist/mcp/handlers/list.js +0 -43
- package/dist/mcp/handlers/push.d.ts +0 -26
- package/dist/mcp/handlers/push.js +0 -232
- package/dist/mcp/handlers/report.d.ts +0 -16
- package/dist/mcp/handlers/report.js +0 -134
- package/dist/mcp/handlers/review.d.ts +0 -52
- package/dist/mcp/handlers/review.js +0 -243
- package/dist/mcp/handlers/search.d.ts +0 -30
- package/dist/mcp/handlers/search.js +0 -58
- package/dist/mcp/handlers/test-mapping.d.ts +0 -39
- package/dist/mcp/handlers/test-mapping.js +0 -168
- package/dist/mcp/handlers/types.d.ts +0 -89
- package/dist/mcp/handlers/types.js +0 -52
- package/dist/mcp/index.d.ts +0 -45
- package/dist/mcp/index.js +0 -638
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Push handler for MCP tools
|
|
3
|
-
*
|
|
4
|
-
* Provides requirements push functionality:
|
|
5
|
-
* - push_requirements: Push local requirements to cloud
|
|
6
|
-
*/
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
8
|
-
import { basename, resolve } from "node:path";
|
|
9
|
-
import { dryRunPush, executePush, parseFilesForPushIndividually, } from "../../push/index.js";
|
|
10
|
-
import { findRequirementsFiles } from "../../requirements/index.js";
|
|
11
|
-
import { CONVEX_URL } from "../convexClient.js";
|
|
12
|
-
import { errorResponse, textResponse } from "./types.js";
|
|
13
|
-
/**
|
|
14
|
-
* Handler for push_requirements tool
|
|
15
|
-
*
|
|
16
|
-
* Requirements covered:
|
|
17
|
-
* - MCP-PUSH-1.0: When not confirmed, no changes are made and a preview of creates/updates is returned
|
|
18
|
-
* - MCP-PUSH-1.1: When confirmed is true, the push executes and returns success message
|
|
19
|
-
* - MCP-PUSH-1.2: When credentials are missing or invalid, an error explains how to authenticate
|
|
20
|
-
* - MCP-PUSH-1.3: When filePath is provided, only that file is pushed (MCP-PUSH-1.3.0: missing file is an error)
|
|
21
|
-
*/
|
|
22
|
-
export async function handlePushRequirements(args, context) {
|
|
23
|
-
const { filePath, confirmed = false, projectId } = args;
|
|
24
|
-
// MCP-PUSH-1.2: Get project credentials (throws if not configured)
|
|
25
|
-
let project;
|
|
26
|
-
try {
|
|
27
|
-
project = await context.getProjectFromDiscovery(projectId);
|
|
28
|
-
}
|
|
29
|
-
catch (error) {
|
|
30
|
-
return errorResponse(`Push requires project credentials. Run \`dotrequirements link\` to connect to cloud.\n\nError: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
-
}
|
|
32
|
-
// Determine files to push
|
|
33
|
-
let filesToPush;
|
|
34
|
-
// MCP-PUSH-1.3: When filePath is provided, only that file is pushed
|
|
35
|
-
if (filePath) {
|
|
36
|
-
const fullPath = resolve(project.path, filePath);
|
|
37
|
-
if (!existsSync(fullPath)) {
|
|
38
|
-
return errorResponse(`File not found: ${filePath}`);
|
|
39
|
-
}
|
|
40
|
-
filesToPush = [fullPath];
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
// Use same file discovery as CLI - searches entire workspace
|
|
44
|
-
filesToPush = await findRequirementsFiles(project.path);
|
|
45
|
-
if (filesToPush.length === 0) {
|
|
46
|
-
return textResponse("No *.requirements.md files found. Nothing to push.");
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// Parse files. #45/SYNC-FAIL-4: one invalid file does not abort the whole
|
|
50
|
-
// push — its failure is surfaced in the preview's "Skipped - Invalid"
|
|
51
|
-
// section while valid files still push. Same helper as the CLI command.
|
|
52
|
-
const { parsedFiles, totalRequirements, parseFailures } = parseFilesForPushIndividually(filesToPush);
|
|
53
|
-
// Build credentials
|
|
54
|
-
const credentials = {
|
|
55
|
-
projectId: project.projectId,
|
|
56
|
-
projectSecret: project.projectSecret,
|
|
57
|
-
convexUrl: CONVEX_URL,
|
|
58
|
-
};
|
|
59
|
-
// Run dry run (always, even when confirmed - ensures fresh state).
|
|
60
|
-
// SYNC-FAIL-3: two files claiming the same document ID abort the push
|
|
61
|
-
// before any cloud write — surface that as a clean error result naming
|
|
62
|
-
// both files rather than an unhandled throw.
|
|
63
|
-
let dryRunResult;
|
|
64
|
-
try {
|
|
65
|
-
dryRunResult = await dryRunPush(parsedFiles, credentials);
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
-
if (message.includes("claim the same document ID")) {
|
|
70
|
-
return {
|
|
71
|
-
content: [
|
|
72
|
-
{
|
|
73
|
-
type: "text",
|
|
74
|
-
text: `✗ Push aborted: ${message}`,
|
|
75
|
-
},
|
|
76
|
-
],
|
|
77
|
-
isError: true,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
82
|
-
// #45: Merge dry-run invalids (missing document section, cloud validation)
|
|
83
|
-
// with local parse failures so both are surfaced together, each named by
|
|
84
|
-
// filename.
|
|
85
|
-
const allInvalid = [
|
|
86
|
-
...dryRunResult.invalid.map(({ file, result }) => ({
|
|
87
|
-
fileName: basename(file.filePath),
|
|
88
|
-
error: result.error ?? "invalid",
|
|
89
|
-
})),
|
|
90
|
-
...parseFailures.map(({ filePath, error }) => ({
|
|
91
|
-
fileName: basename(filePath),
|
|
92
|
-
error,
|
|
93
|
-
})),
|
|
94
|
-
];
|
|
95
|
-
// Check if there's anything to push
|
|
96
|
-
const pushableCount = dryRunResult.updates.length +
|
|
97
|
-
dryRunResult.creates.length +
|
|
98
|
-
dryRunResult.notFound.length;
|
|
99
|
-
if (pushableCount === 0 && allInvalid.length > 0) {
|
|
100
|
-
// Only invalid files
|
|
101
|
-
const invalidList = allInvalid
|
|
102
|
-
.map(({ fileName, error }) => `- ${fileName}: ${error}`)
|
|
103
|
-
.join("\n");
|
|
104
|
-
return {
|
|
105
|
-
content: [
|
|
106
|
-
{
|
|
107
|
-
type: "text",
|
|
108
|
-
text: `✗ No valid documents to push.\n\n**Invalid files:**\n${invalidList}`,
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
isError: true,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
// MCP-PUSH-1.0: When not confirmed, show preview
|
|
115
|
-
if (!confirmed) {
|
|
116
|
-
let summary = `# Push Preview\n\n**Files:** ${parsedFiles.length}\n**Requirements:** ${totalRequirements}\n\n`;
|
|
117
|
-
if (dryRunResult.updates.length > 0) {
|
|
118
|
-
summary += `## Updates (${dryRunResult.updates.length})\n`;
|
|
119
|
-
for (const { file, result } of dryRunResult.updates) {
|
|
120
|
-
const fileName = basename(file.filePath);
|
|
121
|
-
const hasConflict = dryRunResult.conflicts.some((c) => c.item.file === file);
|
|
122
|
-
const conflictNote = hasConflict ? " ⚠️ [cloud changed since pull]" : "";
|
|
123
|
-
const warningNote = result.warning ? ` (${result.warning})` : "";
|
|
124
|
-
summary += `- ~ ${fileName}${conflictNote}${warningNote}\n`;
|
|
125
|
-
}
|
|
126
|
-
summary += "\n";
|
|
127
|
-
}
|
|
128
|
-
if (dryRunResult.creates.length > 0) {
|
|
129
|
-
summary += `## New Documents (${dryRunResult.creates.length})\n`;
|
|
130
|
-
for (const { file, result } of dryRunResult.creates) {
|
|
131
|
-
const fileName = basename(file.filePath);
|
|
132
|
-
const warningNote = result.warning ? ` (${result.warning})` : "";
|
|
133
|
-
summary += `- + ${fileName}${warningNote}\n`;
|
|
134
|
-
}
|
|
135
|
-
summary += "\n";
|
|
136
|
-
}
|
|
137
|
-
if (dryRunResult.notFound.length > 0) {
|
|
138
|
-
summary += `## Not Found in Cloud (${dryRunResult.notFound.length}) - will create new\n`;
|
|
139
|
-
for (const { file, result } of dryRunResult.notFound) {
|
|
140
|
-
const fileName = basename(file.filePath);
|
|
141
|
-
summary += `- ! ${fileName} (ID: ${result.documentId})\n`;
|
|
142
|
-
}
|
|
143
|
-
summary += "\n";
|
|
144
|
-
}
|
|
145
|
-
if (allInvalid.length > 0) {
|
|
146
|
-
summary += `## Skipped - Invalid (${allInvalid.length})\n`;
|
|
147
|
-
for (const { fileName, error } of allInvalid) {
|
|
148
|
-
summary += `- ✗ ${fileName}: ${error}\n`;
|
|
149
|
-
}
|
|
150
|
-
summary += "\n";
|
|
151
|
-
}
|
|
152
|
-
if (dryRunResult.conflicts.length > 0) {
|
|
153
|
-
summary += `⚠️ **Warning:** ${dryRunResult.conflicts.length} file(s) have cloud changes since last pull. Pushing will overwrite those changes.\n\n`;
|
|
154
|
-
}
|
|
155
|
-
summary += `**To proceed:** Call this tool again with \`confirmed: true\``;
|
|
156
|
-
return textResponse(summary);
|
|
157
|
-
}
|
|
158
|
-
// MCP-PUSH-1.1: When confirmed, execute push
|
|
159
|
-
try {
|
|
160
|
-
const result = await executePush(dryRunResult, credentials);
|
|
161
|
-
// SYNC-FAIL-1: a failed push must not masquerade as success
|
|
162
|
-
const failed = result.errors.length;
|
|
163
|
-
const syncedCount = result.created + result.updated;
|
|
164
|
-
let output;
|
|
165
|
-
if (failed > 0 && syncedCount === 0) {
|
|
166
|
-
output = `✗ Push failed: ${failed} document(s) could not be saved.\n\n`;
|
|
167
|
-
}
|
|
168
|
-
else if (failed > 0) {
|
|
169
|
-
output = `⚠ Push incomplete: ${syncedCount} document(s) synced, ${failed} failed.\n\n`;
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
output = "✓ Push complete!\n\n";
|
|
173
|
-
}
|
|
174
|
-
if (result.created > 0) {
|
|
175
|
-
output += `**Created:** ${result.created} document(s)\n`;
|
|
176
|
-
}
|
|
177
|
-
if (result.updated > 0) {
|
|
178
|
-
output += `**Updated:** ${result.updated} document(s)\n`;
|
|
179
|
-
}
|
|
180
|
-
// IMPORT-3: marker failures are loud but never fatal
|
|
181
|
-
if (result.importWarnings.length > 0) {
|
|
182
|
-
output += `\n**Import marker warnings:**\n`;
|
|
183
|
-
for (const { fileName, status } of result.importWarnings) {
|
|
184
|
-
output +=
|
|
185
|
-
status === "already_used"
|
|
186
|
-
? `- ⚠ ${fileName}: import marker already used — requirements were saved as natively authored (they count toward the plan's requirement limit). Re-running codebase-to-spec produces a fresh marker.\n`
|
|
187
|
-
: `- ⚠ ${fileName}: import marker not recognized — the CLI may need updating. Requirements were saved as natively authored (they count toward the plan's requirement limit).\n`;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (result.errors.length > 0) {
|
|
191
|
-
output += `\n**Errors:**\n`;
|
|
192
|
-
for (const { fileName, error } of result.errors) {
|
|
193
|
-
output += `- ${fileName}: ${error}\n`;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
// SYNC-FAIL-2.1: cloud save succeeded but the local file couldn't be
|
|
197
|
-
// updated — name the file, say the cloud is fine, and give the recovery
|
|
198
|
-
// step so a retry doesn't mint a duplicate document.
|
|
199
|
-
if (result.writeBackWarnings?.length > 0) {
|
|
200
|
-
output += `\n**Warnings:**\n`;
|
|
201
|
-
for (const warning of result.writeBackWarnings) {
|
|
202
|
-
output +=
|
|
203
|
-
`- ⚠ ${warning.fileName}: saved to the cloud, but the local file could not be updated (${warning.error}). ` +
|
|
204
|
-
`To avoid creating a duplicate, add "id: ${warning.documentId}" under "document:" in the frontmatter of ${warning.filePath}, then push again.\n`;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
// SYNC-LAND-1: where each synced document lives in the web app
|
|
208
|
-
if (result.synced.length > 0) {
|
|
209
|
-
output += `\n**Review in dot•requirements:**\n`;
|
|
210
|
-
for (const doc of result.synced) {
|
|
211
|
-
output += `- ${doc.fileName}: ${doc.url}\n`;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
// SYNC-FAIL-1.2: total failure is an error result, not a success story
|
|
215
|
-
if (failed > 0 && syncedCount === 0) {
|
|
216
|
-
return { content: [{ type: "text", text: output }], isError: true };
|
|
217
|
-
}
|
|
218
|
-
return textResponse(output);
|
|
219
|
-
}
|
|
220
|
-
catch (error) {
|
|
221
|
-
return {
|
|
222
|
-
content: [
|
|
223
|
-
{
|
|
224
|
-
type: "text",
|
|
225
|
-
text: `✗ Push failed:\n\n${error instanceof Error ? error.message : String(error)}`,
|
|
226
|
-
},
|
|
227
|
-
],
|
|
228
|
-
isError: true,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
//# sourceMappingURL=push.js.map
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified coverage handler for the MCP `report_coverage` tool. Replaces
|
|
3
|
-
* `get_requirement_coverage` and `get_project_coverage_summary` with a
|
|
4
|
-
* single tool that takes a `source: "local" | "cloud"` argument and
|
|
5
|
-
* mirrors the CLI `dotreq report` semantics.
|
|
6
|
-
*/
|
|
7
|
-
import type { HandlerContext, ToolResponse } from "./types.js";
|
|
8
|
-
export interface ReportArgs {
|
|
9
|
-
source?: "local" | "cloud";
|
|
10
|
-
requirementKey?: string;
|
|
11
|
-
branch?: string;
|
|
12
|
-
sinceTimestamp?: number;
|
|
13
|
-
projectId?: string;
|
|
14
|
-
}
|
|
15
|
-
export declare function handleReport(args: ReportArgs, context: HandlerContext): Promise<ToolResponse>;
|
|
16
|
-
//# sourceMappingURL=report.d.ts.map
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified coverage handler for the MCP `report_coverage` tool. Replaces
|
|
3
|
-
* `get_requirement_coverage` and `get_project_coverage_summary` with a
|
|
4
|
-
* single tool that takes a `source: "local" | "cloud"` argument and
|
|
5
|
-
* mirrors the CLI `dotreq report` semantics.
|
|
6
|
-
*/
|
|
7
|
-
import { getProjectCoverage as queryProjectCoverage, getRequirementCoverage as queryRequirementCoverage, } from "../../requirements/cloud-coverage.js";
|
|
8
|
-
import { buildLocalReport, LocalReportCacheMissingError, } from "../../requirements/coverage.js";
|
|
9
|
-
import { CONVEX_URL } from "../convexClient.js";
|
|
10
|
-
import { errorResponse, textResponse } from "./types.js";
|
|
11
|
-
export async function handleReport(args, context) {
|
|
12
|
-
const source = args.source ?? "local";
|
|
13
|
-
if (source === "local") {
|
|
14
|
-
if (args.branch || args.sinceTimestamp !== undefined) {
|
|
15
|
-
return errorResponse("branch and sinceTimestamp require source=cloud. Local reports cover only the most recent test run on this machine.");
|
|
16
|
-
}
|
|
17
|
-
try {
|
|
18
|
-
const report = buildLocalReport({
|
|
19
|
-
projectRoot: context.workspaceRoot,
|
|
20
|
-
requirementFilter: args.requirementKey,
|
|
21
|
-
});
|
|
22
|
-
return textResponse(formatLocalReport(report));
|
|
23
|
-
}
|
|
24
|
-
catch (error) {
|
|
25
|
-
if (error instanceof LocalReportCacheMissingError) {
|
|
26
|
-
return errorResponse(error.message);
|
|
27
|
-
}
|
|
28
|
-
throw error;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
// source === "cloud"
|
|
32
|
-
let project;
|
|
33
|
-
try {
|
|
34
|
-
project = await context.getProjectFromDiscovery(args.projectId);
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
return errorResponse(`Cloud report requires cloud credentials. Run \`dotrequirements link\` to connect to cloud.\n\nError: ${error instanceof Error ? error.message : String(error)}`);
|
|
38
|
-
}
|
|
39
|
-
if (args.requirementKey) {
|
|
40
|
-
const record = await queryRequirementCoverage(args.requirementKey, project.projectId, project.projectSecret, CONVEX_URL);
|
|
41
|
-
return textResponse(formatCloudRequirement(record));
|
|
42
|
-
}
|
|
43
|
-
const record = await queryProjectCoverage(project.projectId, project.projectSecret, CONVEX_URL, {
|
|
44
|
-
branch: args.branch,
|
|
45
|
-
sinceTimestamp: args.sinceTimestamp,
|
|
46
|
-
});
|
|
47
|
-
return textResponse(formatCloudProject(record, {
|
|
48
|
-
branch: args.branch,
|
|
49
|
-
sinceTimestamp: args.sinceTimestamp,
|
|
50
|
-
}));
|
|
51
|
-
}
|
|
52
|
-
function formatLocalReport(report) {
|
|
53
|
-
const { total, tested, untested, coveragePercent } = report.summary;
|
|
54
|
-
let out = `# Coverage Report (local)\n\n`;
|
|
55
|
-
if (report.warnings.length > 0) {
|
|
56
|
-
for (const w of report.warnings) {
|
|
57
|
-
out += `> ⚠️ ${w}\n`;
|
|
58
|
-
}
|
|
59
|
-
out += "\n";
|
|
60
|
-
}
|
|
61
|
-
out += `**Total:** ${total} | **Tested:** ${tested} | **Untested:** ${untested} | **Coverage:** ${coveragePercent}%\n\n`;
|
|
62
|
-
const testedEntries = report.entries.filter((c) => c.tested);
|
|
63
|
-
if (testedEntries.length > 0) {
|
|
64
|
-
out += `## Tested (${testedEntries.length})\n\n`;
|
|
65
|
-
for (const c of testedEntries) {
|
|
66
|
-
out += `- **${c.key}**: ${c.content}\n`;
|
|
67
|
-
for (const loc of c.locations) {
|
|
68
|
-
out += ` - \`${loc}\`\n`;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
out += "\n";
|
|
72
|
-
}
|
|
73
|
-
const untestedEntries = report.entries.filter((c) => !c.tested);
|
|
74
|
-
if (untestedEntries.length > 0) {
|
|
75
|
-
out += `## Untested (${untestedEntries.length})\n\n`;
|
|
76
|
-
for (const c of untestedEntries) {
|
|
77
|
-
out += `- **${c.key}**: ${c.content}\n`;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return out;
|
|
81
|
-
}
|
|
82
|
-
function formatCloudRequirement(record) {
|
|
83
|
-
if (!record.lastTestedAt) {
|
|
84
|
-
return `**${record.requirementKey}** has no cloud coverage records.`;
|
|
85
|
-
}
|
|
86
|
-
const lastTested = new Date(record.lastTestedAt).toISOString();
|
|
87
|
-
let out = `# Coverage for ${record.requirementKey} (cloud)\n\n`;
|
|
88
|
-
out += `**Last tested:** ${lastTested}\n`;
|
|
89
|
-
if (record.branch)
|
|
90
|
-
out += `**Branch:** ${record.branch}\n`;
|
|
91
|
-
if (record.testFile) {
|
|
92
|
-
out += `**Test location:** \`${record.testFile}${record.testLine ? `:${record.testLine}` : ""}\`\n`;
|
|
93
|
-
}
|
|
94
|
-
const otherBranches = record.allBranches.filter((b) => b.branch !== record.branch);
|
|
95
|
-
if (otherBranches.length > 0) {
|
|
96
|
-
out += `\n**Also tested on:**\n`;
|
|
97
|
-
for (const b of otherBranches) {
|
|
98
|
-
out += `- ${b.branch}: ${new Date(b.lastTestedAt).toISOString()}\n`;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return out;
|
|
102
|
-
}
|
|
103
|
-
function formatCloudProject(record, filters) {
|
|
104
|
-
const tested = record.tested.length;
|
|
105
|
-
const untested = record.untested.length;
|
|
106
|
-
const total = tested + untested;
|
|
107
|
-
const coveragePercent = total > 0 ? ((tested / total) * 100).toFixed(1) : "0.0";
|
|
108
|
-
let filterNote = "";
|
|
109
|
-
if (filters.branch)
|
|
110
|
-
filterNote += `\n**Branch:** ${filters.branch}`;
|
|
111
|
-
if (filters.sinceTimestamp) {
|
|
112
|
-
filterNote += `\n**Since:** ${new Date(filters.sinceTimestamp).toISOString()}`;
|
|
113
|
-
}
|
|
114
|
-
let out = `# Project Coverage Report (cloud)\n\n`;
|
|
115
|
-
out += `**Total Requirements:** ${total}\n**Coverage:** ${coveragePercent}%${filterNote}\n`;
|
|
116
|
-
if (record.tested.length > 0) {
|
|
117
|
-
out += `\n## Tested Requirements (${record.tested.length})\n\n`;
|
|
118
|
-
for (const t of record.tested) {
|
|
119
|
-
const date = new Date(t.lastTestedAt).toISOString();
|
|
120
|
-
const location = t.testFile
|
|
121
|
-
? ` (\`${t.testFile}${t.testLine ? `:${t.testLine}` : ""}\`)`
|
|
122
|
-
: "";
|
|
123
|
-
out += `- **${t.requirementKey}** [${t.branch}]: ${date}${location}\n`;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (record.untested.length > 0) {
|
|
127
|
-
out += `\n## Untested Requirements (${record.untested.length})\n\n`;
|
|
128
|
-
for (const key of record.untested) {
|
|
129
|
-
out += `- ${key}\n`;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return out;
|
|
133
|
-
}
|
|
134
|
-
//# sourceMappingURL=report.js.map
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Review handlers for MCP tools
|
|
3
|
-
*
|
|
4
|
-
* Provides AI-powered review functionality:
|
|
5
|
-
* - style_check: Check requirements or test files for style issues
|
|
6
|
-
* - review_test: Comprehensively review tests for semantic correctness
|
|
7
|
-
*/
|
|
8
|
-
import type { HandlerContext, ToolResponse } from "./types.js";
|
|
9
|
-
/**
|
|
10
|
-
* Arguments for style_check tool
|
|
11
|
-
*/
|
|
12
|
-
export interface StyleCheckArgs {
|
|
13
|
-
filePath: string;
|
|
14
|
-
requirementKeys?: string[];
|
|
15
|
-
model?: string;
|
|
16
|
-
projectId?: string;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Arguments for review_test tool
|
|
20
|
-
*/
|
|
21
|
-
export interface ReviewTestArgs {
|
|
22
|
-
testFilePath: string;
|
|
23
|
-
projectId?: string;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Handler for style_check tool
|
|
27
|
-
*
|
|
28
|
-
* Requirements covered:
|
|
29
|
-
* - MCP-REVIEW-1.0: For requirements files, feedback identifies clarity issues such as vague language and missing preconditions
|
|
30
|
-
* - MCP-REVIEW-1.1: For test files, feedback identifies incorrect requirement() usage and missing test coverage
|
|
31
|
-
* - MCP-REVIEW-1.2: Feedback is categorized by severity: must fix, should fix, could improve
|
|
32
|
-
* - MCP-REVIEW-1.3: When cloud credentials are unavailable, an error explains how to authenticate
|
|
33
|
-
*/
|
|
34
|
-
export declare function handleStyleCheck(args: StyleCheckArgs, context: HandlerContext, options?: {
|
|
35
|
-
useEnvAuth?: boolean;
|
|
36
|
-
apiBaseUrl?: string;
|
|
37
|
-
}): Promise<ToolResponse>;
|
|
38
|
-
/**
|
|
39
|
-
* Handler for review_test tool
|
|
40
|
-
*
|
|
41
|
-
* Requirements covered:
|
|
42
|
-
* - MCP-REVIEW-2.0: Feedback identifies test setup that does not match requirement preconditions
|
|
43
|
-
* - MCP-REVIEW-2.1: Feedback identifies test actions that do not match requirement triggers
|
|
44
|
-
* - MCP-REVIEW-2.2: Feedback identifies test assertions that do not match requirement outcomes
|
|
45
|
-
* - MCP-REVIEW-2.3: Feedback identifies requirements without test coverage
|
|
46
|
-
* - MCP-REVIEW-2.4: Feedback identifies tests that reference non-existent requirements
|
|
47
|
-
* - MCP-REVIEW-2.5: When cloud credentials are unavailable, an error explains how to authenticate
|
|
48
|
-
*/
|
|
49
|
-
export declare function handleReviewTest(args: ReviewTestArgs, context: HandlerContext, options?: {
|
|
50
|
-
apiBaseUrl?: string;
|
|
51
|
-
}): Promise<ToolResponse>;
|
|
52
|
-
//# sourceMappingURL=review.d.ts.map
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Review handlers for MCP tools
|
|
3
|
-
*
|
|
4
|
-
* Provides AI-powered review functionality:
|
|
5
|
-
* - style_check: Check requirements or test files for style issues
|
|
6
|
-
* - review_test: Comprehensively review tests for semantic correctness
|
|
7
|
-
*/
|
|
8
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
-
import { dirname } from "node:path";
|
|
10
|
-
import { fetchReviewTestFeedback, fetchStyleCheckFeedback, } from "../../requirements/cloud-ai.js";
|
|
11
|
-
import { findRequirementsInFile } from "../../requirements/grep.js";
|
|
12
|
-
import { filterRequirementsByKeys, formatRequirementTree, getRequirementTree, } from "../../requirements/index.js";
|
|
13
|
-
import { discoverProjects, } from "../../utils/project-discovery.js";
|
|
14
|
-
import { errorResponse, resolveProjectFilePath, textResponse, } from "./types.js";
|
|
15
|
-
const STYLE_CHECK_GUIDANCE = `
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## How to use this feedback
|
|
20
|
-
|
|
21
|
-
1. **This check is stateless.** It is a "new set of eyes" from any previous feedback rounds — it has no memory of prior suggestions or changes you've already made.
|
|
22
|
-
|
|
23
|
-
2. **Act on feedback from the posture of "what will make the requirements better."** The severity categories (MUST FIX / SHOULD FIX / COULD IMPROVE) are prioritized for convenience, but unless the user has explicitly told you to ignore lower-priority feedback, go ahead and make any improvements — minor or otherwise. If you believe acting on a specific piece of feedback would make the requirements *worse*, surface that feedback to the user rather than silently ignoring it. Otherwise, just act.
|
|
24
|
-
|
|
25
|
-
3. **Next steps:** Always run \`validate_requirements\` again after making updates. If you make significant changes based on this feedback, you can run \`style_check\` again — but be aware that will be a fresh set of eyes, not a continued conversation.`;
|
|
26
|
-
/**
|
|
27
|
-
* Handler for style_check tool
|
|
28
|
-
*
|
|
29
|
-
* Requirements covered:
|
|
30
|
-
* - MCP-REVIEW-1.0: For requirements files, feedback identifies clarity issues such as vague language and missing preconditions
|
|
31
|
-
* - MCP-REVIEW-1.1: For test files, feedback identifies incorrect requirement() usage and missing test coverage
|
|
32
|
-
* - MCP-REVIEW-1.2: Feedback is categorized by severity: must fix, should fix, could improve
|
|
33
|
-
* - MCP-REVIEW-1.3: When cloud credentials are unavailable, an error explains how to authenticate
|
|
34
|
-
*/
|
|
35
|
-
export async function handleStyleCheck(args, context, options) {
|
|
36
|
-
const { filePath, requirementKeys, model, projectId } = args;
|
|
37
|
-
const { useEnvAuth = false, apiBaseUrl = "https://app.dotrequirements.io" } = options ?? {};
|
|
38
|
-
// #49: Resolve against the discovered project.path (matching push) with a
|
|
39
|
-
// workspaceRoot fallback, so a relative path that push accepts also works
|
|
40
|
-
// here in PROJ_*-based setups. Discovery may throw without credentials — that
|
|
41
|
-
// is fine, we then fall back to workspaceRoot.
|
|
42
|
-
let styleProjectPath;
|
|
43
|
-
try {
|
|
44
|
-
styleProjectPath = (await context.getProjectFromDiscovery(projectId)).path;
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
styleProjectPath = undefined;
|
|
48
|
-
}
|
|
49
|
-
const fullPath = resolveProjectFilePath(filePath, styleProjectPath, context.workspaceRoot);
|
|
50
|
-
if (!existsSync(fullPath)) {
|
|
51
|
-
return errorResponse(`File not found: ${filePath}`);
|
|
52
|
-
}
|
|
53
|
-
// Read file contents
|
|
54
|
-
const fileContents = readFileSync(fullPath, "utf-8");
|
|
55
|
-
// Detect file type
|
|
56
|
-
// MCP-REVIEW-1.0 & MCP-REVIEW-1.1: Different feedback for different file types
|
|
57
|
-
const isRequirementsFile = filePath.endsWith(".requirements.md");
|
|
58
|
-
const isTestFile = /\.(test|spec)\.(js|jsx|ts|tsx)$/.test(filePath);
|
|
59
|
-
if (!isRequirementsFile && !isTestFile) {
|
|
60
|
-
return errorResponse(`Unsupported file type. File must be:\n- Requirements file: *.requirements.md\n- Test file: *.test.{js,jsx,ts,tsx} or *.spec.{js,jsx,ts,tsx}`);
|
|
61
|
-
}
|
|
62
|
-
const fileType = isRequirementsFile ? "requirements" : "test";
|
|
63
|
-
// If requirementKeys provided, filter the file to only those requirements
|
|
64
|
-
let fileContentsToCheck = fileContents;
|
|
65
|
-
let scopeNote = "";
|
|
66
|
-
if (requirementKeys && requirementKeys.length > 0) {
|
|
67
|
-
if (!isRequirementsFile) {
|
|
68
|
-
return errorResponse(`requirementKeys can only be used with requirements files (*.requirements.md), not test files.`);
|
|
69
|
-
}
|
|
70
|
-
try {
|
|
71
|
-
const { filteredContent, foundKeys, missingKeys } = filterRequirementsByKeys(fileContents, requirementKeys);
|
|
72
|
-
if (foundKeys.length === 0) {
|
|
73
|
-
return errorResponse(`None of the specified requirement keys were found in ${filePath}: ${requirementKeys.join(", ")}`);
|
|
74
|
-
}
|
|
75
|
-
fileContentsToCheck = filteredContent;
|
|
76
|
-
if (missingKeys.length > 0) {
|
|
77
|
-
scopeNote = `\n\n> **Note:** Some specified keys were not found in the file: ${missingKeys.join(", ")}`;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
return errorResponse(`Failed to filter requirements: ${error instanceof Error ? error.message : String(error)}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
// MCP-REVIEW-1.3: Get credentials - walk up from file's directory to find project
|
|
85
|
-
const fileDir = dirname(fullPath);
|
|
86
|
-
let project;
|
|
87
|
-
try {
|
|
88
|
-
if (projectId) {
|
|
89
|
-
// #48: PROJ-CONTEXT-6 — an explicit projectId disambiguates a
|
|
90
|
-
// multi-project workspace. Thread it straight through to discovery
|
|
91
|
-
// (matching review_test/push/report) instead of the file-directory
|
|
92
|
-
// auto-detect path, which cannot choose among multiple projects.
|
|
93
|
-
project = await context.getProjectFromDiscovery(projectId);
|
|
94
|
-
}
|
|
95
|
-
else if (useEnvAuth) {
|
|
96
|
-
// Using env auth - go straight to context
|
|
97
|
-
project = await context.getProjectFromDiscovery();
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
// Try to find a project starting from the file's directory
|
|
101
|
-
const result = await discoverProjects(fileDir);
|
|
102
|
-
if (result.type === "none") {
|
|
103
|
-
// If no project found from file dir, try from context
|
|
104
|
-
project = await context.getProjectFromDiscovery();
|
|
105
|
-
}
|
|
106
|
-
else if (result.type === "single") {
|
|
107
|
-
project = result.project;
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
// #48: Multiple projects — can't auto-detect. The remedy is to pass
|
|
111
|
-
// projectId (not `dotrequirements link`, which is already satisfied);
|
|
112
|
-
// list the available projects so the caller can choose.
|
|
113
|
-
const available = result.projects
|
|
114
|
-
.map((p) => p.projectId)
|
|
115
|
-
.filter(Boolean)
|
|
116
|
-
.join(", ");
|
|
117
|
-
return errorResponse(`Multiple projects found. Specify the \`projectId\` parameter to choose one${available ? ` (available: ${available})` : ""}.`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
return errorResponse(`Style check requires project credentials. Run \`dotrequirements link\` to connect to cloud.\n\nError: ${error instanceof Error ? error.message : String(error)}`);
|
|
123
|
-
}
|
|
124
|
-
// Call hosted endpoint via the shared library helper
|
|
125
|
-
try {
|
|
126
|
-
const feedback = await fetchStyleCheckFeedback({
|
|
127
|
-
apiBaseUrl,
|
|
128
|
-
projectId: project.projectId,
|
|
129
|
-
projectSecret: project.projectSecret,
|
|
130
|
-
fileContents: fileContentsToCheck,
|
|
131
|
-
fileType,
|
|
132
|
-
model,
|
|
133
|
-
});
|
|
134
|
-
const scopeLabel = requirementKeys && requirementKeys.length > 0
|
|
135
|
-
? ` (${requirementKeys.join(", ")})`
|
|
136
|
-
: "";
|
|
137
|
-
// MCP-REVIEW-1.2: Feedback includes severity categories (in the API response)
|
|
138
|
-
return textResponse(`# Style Check Results for \`${filePath}\`${scopeLabel}\n\n${feedback}${scopeNote}${STYLE_CHECK_GUIDANCE}`);
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
return errorResponse(`Style check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Handler for review_test tool
|
|
146
|
-
*
|
|
147
|
-
* Requirements covered:
|
|
148
|
-
* - MCP-REVIEW-2.0: Feedback identifies test setup that does not match requirement preconditions
|
|
149
|
-
* - MCP-REVIEW-2.1: Feedback identifies test actions that do not match requirement triggers
|
|
150
|
-
* - MCP-REVIEW-2.2: Feedback identifies test assertions that do not match requirement outcomes
|
|
151
|
-
* - MCP-REVIEW-2.3: Feedback identifies requirements without test coverage
|
|
152
|
-
* - MCP-REVIEW-2.4: Feedback identifies tests that reference non-existent requirements
|
|
153
|
-
* - MCP-REVIEW-2.5: When cloud credentials are unavailable, an error explains how to authenticate
|
|
154
|
-
*/
|
|
155
|
-
export async function handleReviewTest(args, context, options) {
|
|
156
|
-
const { testFilePath, projectId } = args;
|
|
157
|
-
const { apiBaseUrl = "https://app.dotrequirements.io" } = options ?? {};
|
|
158
|
-
// #49: Resolve against the discovered project.path (matching push) with a
|
|
159
|
-
// workspaceRoot fallback. Discovery may throw without credentials — fall back
|
|
160
|
-
// to workspaceRoot; the credential error is reported later.
|
|
161
|
-
let reviewProjectPath;
|
|
162
|
-
try {
|
|
163
|
-
reviewProjectPath = (await context.getProjectFromDiscovery(projectId)).path;
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
reviewProjectPath = undefined;
|
|
167
|
-
}
|
|
168
|
-
const fullPath = resolveProjectFilePath(testFilePath, reviewProjectPath, context.workspaceRoot);
|
|
169
|
-
if (!existsSync(fullPath)) {
|
|
170
|
-
return errorResponse(`File not found: ${testFilePath}`);
|
|
171
|
-
}
|
|
172
|
-
// Verify it's a test file
|
|
173
|
-
const isTestFile = /\.(test|spec)\.(js|jsx|ts|tsx)$/.test(testFilePath);
|
|
174
|
-
if (!isTestFile) {
|
|
175
|
-
return errorResponse(`File must be a test file: *.test.{js,jsx,ts,tsx} or *.spec.{js,jsx,ts,tsx}`);
|
|
176
|
-
}
|
|
177
|
-
// Read test file contents
|
|
178
|
-
const testFileContents = readFileSync(fullPath, "utf-8");
|
|
179
|
-
// #43: Extract requirement IDs via the shared AST-based extractor so that
|
|
180
|
-
// multi-arg calls (`requirement("A", "B")`) and options-bearing calls
|
|
181
|
-
// (`requirement("A", { ... })`) are captured — the old single-string regex
|
|
182
|
-
// matched neither.
|
|
183
|
-
const references = await findRequirementsInFile(fullPath);
|
|
184
|
-
const requirementIds = new Set(references.map((ref) => ref.requirementId));
|
|
185
|
-
// MCP-REVIEW-2.5: Get project credentials
|
|
186
|
-
let project;
|
|
187
|
-
try {
|
|
188
|
-
project = await context.getProjectFromDiscovery(projectId);
|
|
189
|
-
}
|
|
190
|
-
catch (error) {
|
|
191
|
-
return errorResponse(`Test review requires a configured dotrequirements project.\n\nError: ${error instanceof Error ? error.message : String(error)}`);
|
|
192
|
-
}
|
|
193
|
-
// Load all requirements for this project
|
|
194
|
-
const allRequirements = await context.getRequirements(projectId);
|
|
195
|
-
// Group requirement IDs by their root to avoid sending duplicate trees
|
|
196
|
-
// e.g., NAV-3.0, NAV-3.1, NAV-3.2 all belong to root NAV-3
|
|
197
|
-
//
|
|
198
|
-
// #42: A reference may be a bare root (`AUTH-1`), an index child (`AUTH-1.0`),
|
|
199
|
-
// or a label path (`AUTH-1.given`). Normalize each reference to its root id
|
|
200
|
-
// (the substring before the first dot) before matching — the same approach
|
|
201
|
-
// getReferencedRequirementIds uses — so label-path refs resolve instead of
|
|
202
|
-
// being reported as non-existent.
|
|
203
|
-
const rootToTestedIds = new Map();
|
|
204
|
-
for (const reqId of requirementIds) {
|
|
205
|
-
const dotIndex = reqId.indexOf(".");
|
|
206
|
-
const refRootId = dotIndex > 0 ? reqId.substring(0, dotIndex) : reqId;
|
|
207
|
-
const req = allRequirements.find((r) => r.rootId === refRootId);
|
|
208
|
-
if (req) {
|
|
209
|
-
if (!rootToTestedIds.has(req.rootId)) {
|
|
210
|
-
rootToTestedIds.set(req.rootId, new Set());
|
|
211
|
-
}
|
|
212
|
-
rootToTestedIds.get(req.rootId).add(reqId);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
// Build requirements array with one entry per unique root tree
|
|
216
|
-
// MCP-REVIEW-2.0, 2.1, 2.2: These validations happen in the API based on the tree structure
|
|
217
|
-
const requirements = [];
|
|
218
|
-
for (const [rootId, testedIds] of rootToTestedIds) {
|
|
219
|
-
const tree = getRequirementTree(allRequirements, rootId);
|
|
220
|
-
const formattedTree = formatRequirementTree(tree);
|
|
221
|
-
requirements.push({
|
|
222
|
-
id: rootId,
|
|
223
|
-
content: formattedTree,
|
|
224
|
-
testedIds: Array.from(testedIds).sort(),
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
// Call hosted endpoint via the shared library helper
|
|
228
|
-
// MCP-REVIEW-2.3 & 2.4: API identifies coverage gaps and non-existent references
|
|
229
|
-
try {
|
|
230
|
-
const feedback = await fetchReviewTestFeedback({
|
|
231
|
-
apiBaseUrl,
|
|
232
|
-
projectId: project.projectId,
|
|
233
|
-
projectSecret: project.projectSecret,
|
|
234
|
-
testFileContents,
|
|
235
|
-
requirements,
|
|
236
|
-
});
|
|
237
|
-
return textResponse(`# Test Review Results for \`${testFilePath}\`\n\n${feedback}`);
|
|
238
|
-
}
|
|
239
|
-
catch (error) {
|
|
240
|
-
return errorResponse(`Test review failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
//# sourceMappingURL=review.js.map
|