@popoverai/dotrequirements 0.26.1 → 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/codebase-to-spec/present.js +4 -5
- package/dist/codebase-to-spec/validate.js +3 -2
- package/dist/commands/acceptance-test.js +4 -2
- package/dist/commands/ai-setup.d.ts +8 -2
- package/dist/commands/ai-setup.js +154 -310
- package/dist/commands/get.js +6 -2
- package/dist/commands/init.js +8 -6
- package/dist/commands/link-resolution.d.ts +3 -1
- package/dist/commands/link-resolution.js +4 -2
- package/dist/commands/mcp.d.ts +8 -2
- package/dist/commands/mcp.js +17 -6
- package/dist/commands/pull.js +36 -3
- package/dist/commands/push.js +54 -16
- package/dist/commands/report.js +18 -3
- package/dist/commands/review-test.d.ts +5 -1
- package/dist/commands/review-test.js +117 -15
- package/dist/commands/style-check.d.ts +1 -0
- package/dist/commands/style-check.js +137 -13
- package/dist/commands/tests-for.js +13 -13
- package/dist/commands/validate.js +14 -14
- package/dist/convex.d.ts +1 -3
- package/dist/convex.js +3 -3
- package/dist/harness/cache.d.ts +19 -3
- package/dist/harness/cache.js +38 -12
- package/dist/harness/finalize.js +33 -1
- package/dist/harness/index.js +16 -9
- package/dist/harness/requirementsLoader.js +12 -0
- package/dist/harness/tracking.d.ts +17 -2
- package/dist/harness/tracking.js +83 -9
- package/dist/push/core.d.ts +50 -0
- package/dist/push/core.js +149 -11
- package/dist/push/index.d.ts +1 -1
- package/dist/push/index.js +1 -1
- package/dist/requirements/cloud-ai.d.ts +21 -8
- package/dist/requirements/cloud-ai.js +10 -8
- package/dist/requirements/cloud-coverage.d.ts +12 -2
- package/dist/requirements/cloud-coverage.js +30 -3
- package/dist/requirements/grep.d.ts +7 -2
- package/dist/requirements/grep.js +75 -47
- package/dist/schema/builder.d.ts +1 -1
- package/dist/schema/builder.js +13 -0
- package/dist/schema/conversions.d.ts +7 -2
- package/dist/schema/conversions.js +13 -4
- package/dist/schema/parser-core.d.ts +41 -0
- package/dist/schema/parser-core.js +113 -18
- package/dist/schema/parser.d.ts +8 -26
- package/dist/schema/parser.js +23 -251
- package/dist/schema/resolver.js +18 -8
- 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/env.js +17 -1
- package/dist/utils/oauth-flow.js +8 -0
- package/dist/utils/project-settings.d.ts +5 -0
- package/dist/utils/project-settings.js +36 -1
- 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 -104
- 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 -65
- 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 -186
- 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 -51
- package/dist/mcp/handlers/review.js +0 -200
- 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 -133
- package/dist/mcp/handlers/types.d.ts +0 -75
- package/dist/mcp/handlers/types.js +0 -25
- package/dist/mcp/index.d.ts +0 -45
- package/dist/mcp/index.js +0 -634
package/dist/commands/pull.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getConvexUrl } from "../config.js";
|
|
|
5
5
|
import { api } from "../convex.js";
|
|
6
6
|
import { findRequirementsFiles } from "../requirements/index.js";
|
|
7
7
|
import { buildRequirementsFile } from "../schema/index.js";
|
|
8
|
+
import { extractFrontmatterBlock } from "../schema/parser-core.js";
|
|
8
9
|
import { brand } from "../utils/brand.js";
|
|
9
10
|
import { getProjectCredentials } from "../utils/project-settings.js";
|
|
10
11
|
export async function pullCommand(options) {
|
|
@@ -94,12 +95,33 @@ export async function pullCommand(options) {
|
|
|
94
95
|
}
|
|
95
96
|
// Build index of existing files by document ID (search entire workspace)
|
|
96
97
|
const existingFilesByDocId = await buildDocumentIdIndex(process.cwd());
|
|
98
|
+
// SYNC-WEB-CREATE-2.0: track paths claimed during this pull so colliding
|
|
99
|
+
// titles don't silently overwrite each other within one operation
|
|
100
|
+
const usedPaths = new Set(existingFilesByDocId.values());
|
|
97
101
|
// Write each document as a Markdown file
|
|
98
102
|
for (const doc of documents) {
|
|
99
103
|
// Check if an existing file has this document ID
|
|
100
104
|
const existingFilePath = existingFilesByDocId.get(doc.documentId);
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
let filePath;
|
|
106
|
+
if (existingFilePath) {
|
|
107
|
+
filePath = existingFilePath;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// SYNC-WEB-CREATE-2.1: an all-symbols title sanitizes to nothing —
|
|
111
|
+
// fall back to the document ID rather than a hidden ".requirements.md"
|
|
112
|
+
const baseName = sanitizeFileName(doc.title) || doc.documentId;
|
|
113
|
+
let candidate = path.join(requirementsDir, `${baseName}.requirements.md`);
|
|
114
|
+
// SYNC-WEB-CREATE-2.0: disambiguate later collisions with a numeric
|
|
115
|
+
// suffix; also avoid clobbering an on-disk file that belongs to a
|
|
116
|
+
// different (or no) document
|
|
117
|
+
let suffix = 2;
|
|
118
|
+
while (usedPaths.has(candidate) || fs.existsSync(candidate)) {
|
|
119
|
+
candidate = path.join(requirementsDir, `${baseName}-${suffix}.requirements.md`);
|
|
120
|
+
suffix++;
|
|
121
|
+
}
|
|
122
|
+
filePath = candidate;
|
|
123
|
+
}
|
|
124
|
+
usedPaths.add(filePath);
|
|
103
125
|
const fileName = path.basename(filePath);
|
|
104
126
|
// IMPORT-1: carry the CTS run marker forward from the existing local
|
|
105
127
|
// file — pull rebuilds frontmatter from cloud data, and silently dropping
|
|
@@ -168,10 +190,21 @@ async function buildDocumentIdIndex(workspaceRoot) {
|
|
|
168
190
|
/**
|
|
169
191
|
* Extract document.id from a requirements file's frontmatter.
|
|
170
192
|
* Returns undefined if the file can't be read or doesn't have a document ID.
|
|
193
|
+
* SYNC-DISCOVERY-3: only the leading YAML frontmatter block is consulted —
|
|
194
|
+
* a document id quoted in body prose or a fenced example must never mark
|
|
195
|
+
* the file as owning that document.
|
|
171
196
|
*/
|
|
172
197
|
function extractDocumentIdFromFile(filePath) {
|
|
173
198
|
try {
|
|
174
|
-
const
|
|
199
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
200
|
+
// Isolate the leading ---...--- frontmatter block; no frontmatter means
|
|
201
|
+
// the file is unlinked to any cloud document (SYNC-DISCOVERY-3.2).
|
|
202
|
+
// Shared helper normalizes CRLF (SYNC-FORMAT-1) so a Windows-saved file
|
|
203
|
+
// keeps matching its document and is updated in place (SYNC-DISCOVERY-2.1).
|
|
204
|
+
const content = extractFrontmatterBlock(fileContent);
|
|
205
|
+
if (content === undefined) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
175
208
|
// Match document.id in YAML frontmatter - handles both inline and nested formats
|
|
176
209
|
// Inline: document: { id: "abc123", ... }
|
|
177
210
|
// Nested (id: can appear at any position within the indented document block):
|
package/dist/commands/push.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as readline from "node:readline";
|
|
4
4
|
import { getConvexUrl } from "../config.js";
|
|
5
|
-
import { dryRunPush, executePush,
|
|
5
|
+
import { dryRunPush, executePush, parseFilesForPushIndividually, } from "../push/index.js";
|
|
6
6
|
import { findRequirementsFiles } from "../requirements/index.js";
|
|
7
7
|
import { brand } from "../utils/brand.js";
|
|
8
8
|
import { getProjectCredentials } from "../utils/project-settings.js";
|
|
@@ -30,20 +30,30 @@ export async function pushCommand(file, options) {
|
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
// Parse
|
|
33
|
+
// Parse local files. SYNC-FAIL-4: one invalid file is skipped while the
|
|
34
|
+
// rest of the batch still pushes — same helper as the MCP push handler.
|
|
34
35
|
console.log("Parsing local requirements...");
|
|
35
|
-
const { parsedFiles, totalRequirements } =
|
|
36
|
+
const { parsedFiles, totalRequirements, parseFailures } = parseFilesForPushIndividually(filesToPush);
|
|
36
37
|
// Log file parsing progress
|
|
37
38
|
for (const file of parsedFiles) {
|
|
38
39
|
const fileName = path.basename(file.filePath);
|
|
39
40
|
const doc = file.metadata.document;
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
console.log(` Reading ${fileName}...`);
|
|
42
|
+
// DOC-HEADER-11.4: parseFilesForPush mutates the metadata when it infers
|
|
43
|
+
// the prefix, so the flag it returns is the only record of the inference
|
|
44
|
+
if (doc?.defaultPrefix && file.inferredDefaultPrefix) {
|
|
42
45
|
console.log(` Inferred defaultPrefix "${doc.defaultPrefix}" from first requirement`);
|
|
43
46
|
}
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
}
|
|
48
|
+
// SYNC-FAIL-4.1: every file in the push failed to parse — report each
|
|
49
|
+
// skipped file, then fail honestly instead of dry-running nothing
|
|
50
|
+
if (parsedFiles.length === 0) {
|
|
51
|
+
for (const failure of parseFailures) {
|
|
52
|
+
console.log(` ✗ Skipped ${path.basename(failure.filePath)}: ${failure.error}`);
|
|
46
53
|
}
|
|
54
|
+
console.log("\nNo valid documents to push.");
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
47
57
|
}
|
|
48
58
|
console.log(`\nFound ${totalRequirements} requirement(s) in ${parsedFiles.length} document(s).\n`);
|
|
49
59
|
// Build credentials
|
|
@@ -56,13 +66,19 @@ export async function pushCommand(file, options) {
|
|
|
56
66
|
console.log("Validating documents...");
|
|
57
67
|
const dryRunResult = await dryRunPush(parsedFiles, credentials);
|
|
58
68
|
// Display unified summary
|
|
59
|
-
displayDryRunSummary(dryRunResult);
|
|
69
|
+
displayDryRunSummary(dryRunResult, parseFailures);
|
|
60
70
|
// Check if there's anything to push
|
|
61
71
|
const pushableCount = dryRunResult.updates.length +
|
|
62
72
|
dryRunResult.creates.length +
|
|
63
73
|
dryRunResult.notFound.length;
|
|
64
74
|
if (pushableCount === 0) {
|
|
65
75
|
console.log("No valid documents to push.");
|
|
76
|
+
// SYNC-FAIL-4.1: every file in the push was invalid — whether it failed
|
|
77
|
+
// local parse or dry-run validation — so the command must fail honestly,
|
|
78
|
+
// matching the MCP handler's merged rule
|
|
79
|
+
if (parseFailures.length > 0 || dryRunResult.invalid.length > 0) {
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
}
|
|
66
82
|
return;
|
|
67
83
|
}
|
|
68
84
|
// Single confirmation prompt
|
|
@@ -88,8 +104,9 @@ export async function pushCommand(file, options) {
|
|
|
88
104
|
]) {
|
|
89
105
|
const fileName = path.basename(file.filePath);
|
|
90
106
|
const isCreate = dryResult.action === "create" || dryResult.action === "not_found";
|
|
91
|
-
// Check if this file had an error
|
|
92
|
-
|
|
107
|
+
// Check if this file had an error — match by full path, since two pushed
|
|
108
|
+
// files can share a basename
|
|
109
|
+
const error = result.errors.find((e) => e.filePath === file.filePath);
|
|
93
110
|
if (error) {
|
|
94
111
|
console.log(` ✗ Failed: ${fileName} - ${error.error}`);
|
|
95
112
|
}
|
|
@@ -100,6 +117,16 @@ export async function pushCommand(file, options) {
|
|
|
100
117
|
console.log(` ✓ Updated: ${fileName}`);
|
|
101
118
|
}
|
|
102
119
|
}
|
|
120
|
+
// SYNC-FAIL-2.1: the cloud save succeeded but the local file couldn't be
|
|
121
|
+
// updated — name the file, say the cloud is fine, and give the recovery
|
|
122
|
+
// step that re-links the file instead of minting a duplicate on retry
|
|
123
|
+
if (result.writeBackWarnings?.length > 0) {
|
|
124
|
+
console.log();
|
|
125
|
+
for (const warning of result.writeBackWarnings) {
|
|
126
|
+
console.log(`⚠ ${warning.fileName}: saved to the cloud, but the local file could not be updated (${warning.error}). ` +
|
|
127
|
+
`To avoid creating a duplicate, add "id: ${warning.documentId}" under "document:" in the frontmatter of ${warning.filePath}, then push again.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
103
130
|
// IMPORT-3: marker failures are loud but never fatal
|
|
104
131
|
if (result.importWarnings.length > 0) {
|
|
105
132
|
console.log();
|
|
@@ -148,7 +175,7 @@ export async function pushCommand(file, options) {
|
|
|
148
175
|
/**
|
|
149
176
|
* Display the dry run summary.
|
|
150
177
|
*/
|
|
151
|
-
function displayDryRunSummary(dryRunResult) {
|
|
178
|
+
function displayDryRunSummary(dryRunResult, parseFailures = []) {
|
|
152
179
|
const { updates, creates, notFound, invalid, conflicts } = dryRunResult;
|
|
153
180
|
console.log("\n=== Push Summary ===\n");
|
|
154
181
|
if (updates.length > 0) {
|
|
@@ -179,11 +206,22 @@ function displayDryRunSummary(dryRunResult) {
|
|
|
179
206
|
}
|
|
180
207
|
console.log();
|
|
181
208
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
209
|
+
// SYNC-FAIL-4.0: local parse failures surface alongside dry-run invalids,
|
|
210
|
+
// each named by file, while the rest of the batch proceeds
|
|
211
|
+
const skipped = [
|
|
212
|
+
...parseFailures.map((failure) => ({
|
|
213
|
+
fileName: path.basename(failure.filePath),
|
|
214
|
+
error: failure.error,
|
|
215
|
+
})),
|
|
216
|
+
...invalid.map(({ file, result }) => ({
|
|
217
|
+
fileName: path.basename(file.filePath),
|
|
218
|
+
error: result.error,
|
|
219
|
+
})),
|
|
220
|
+
];
|
|
221
|
+
if (skipped.length > 0) {
|
|
222
|
+
console.log(`Skipped - invalid files (${skipped.length}):`);
|
|
223
|
+
for (const { fileName, error } of skipped) {
|
|
224
|
+
console.log(` ✗ ${fileName}: ${error}`);
|
|
187
225
|
}
|
|
188
226
|
console.log();
|
|
189
227
|
}
|
package/dist/commands/report.js
CHANGED
|
@@ -36,8 +36,16 @@ export async function reportCommand(options) {
|
|
|
36
36
|
`(${error instanceof Error ? error.message : String(error)})`);
|
|
37
37
|
}
|
|
38
38
|
if (options.requirement) {
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// REPORT-CLOUD-1.7: --branch/--since apply to the requirement-scoped
|
|
40
|
+
// output too, not just the project-wide report
|
|
41
|
+
const record = await getRequirementCoverage(options.requirement, projectId, projectSecret, CONVEX_URL, {
|
|
42
|
+
branch: options.branch,
|
|
43
|
+
sinceTimestamp: options.since,
|
|
44
|
+
});
|
|
45
|
+
console.log(printCloudRequirement(record, format, {
|
|
46
|
+
branch: options.branch,
|
|
47
|
+
since: options.since,
|
|
48
|
+
}));
|
|
41
49
|
return;
|
|
42
50
|
}
|
|
43
51
|
const record = await getProjectCoverage(projectId, projectSecret, CONVEX_URL, {
|
|
@@ -100,11 +108,18 @@ function formatLocalMarkdown(report) {
|
|
|
100
108
|
}
|
|
101
109
|
return out;
|
|
102
110
|
}
|
|
103
|
-
function printCloudRequirement(record, format) {
|
|
111
|
+
function printCloudRequirement(record, format, filters) {
|
|
104
112
|
if (format === "json") {
|
|
105
113
|
return JSON.stringify({ source: "cloud", requirement: record }, null, 2);
|
|
106
114
|
}
|
|
107
115
|
if (!record.lastTestedAt) {
|
|
116
|
+
// REPORT-CLOUD-1.7: with filters in play, an empty record means nothing
|
|
117
|
+
// matched them — say so rather than implying the requirement was never
|
|
118
|
+
// tested at all
|
|
119
|
+
const filterNote = formatCloudFilters(filters);
|
|
120
|
+
if (filterNote) {
|
|
121
|
+
return `\n${record.requirementKey}: no coverage records match the requested filters (${filterNote.replace(/^Filters: /, "")}).\n`;
|
|
122
|
+
}
|
|
108
123
|
return `\n${record.requirementKey}: never tested on the cloud-persisted record.\n`;
|
|
109
124
|
}
|
|
110
125
|
const lastTested = new Date(record.lastTestedAt).toISOString();
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
interface ReviewTestOptions {
|
|
2
|
+
source?: string;
|
|
3
|
+
}
|
|
4
|
+
export declare function reviewTestCommand(testFilePath: string, options?: ReviewTestOptions): Promise<void>;
|
|
5
|
+
export {};
|
|
2
6
|
//# sourceMappingURL=review-test.d.ts.map
|
|
@@ -1,38 +1,71 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { DEFAULT_API_BASE_URL, fetchReviewTestFeedback, } from "../requirements/cloud-ai.js";
|
|
4
|
+
import { findRequirementsInFile } from "../requirements/grep.js";
|
|
4
5
|
import { formatRequirementTree, getRequirementTree, loadAllRequirements, } from "../requirements/index.js";
|
|
5
6
|
import { getProjectCredentials } from "../utils/project-settings.js";
|
|
6
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Judgment instructions for local-mode test review. Mirrors the hosted
|
|
9
|
+
* review's semantic framing: test anatomy (setup, actions, assertions)
|
|
10
|
+
* checked against requirement anatomy (preconditions, triggers, outcomes).
|
|
11
|
+
*/
|
|
12
|
+
const REVIEW_INSTRUCTIONS = `You are the reviewer. Judge whether the test file below validates what its referenced requirements specify, then report findings under the headings MUST FIX / SHOULD FIX / COULD IMPROVE (semantic gaps are MUST FIX; categorize other findings by their own severity). Focus on gaps and give actionable suggestions.
|
|
13
|
+
|
|
14
|
+
For each requirement reference, check:
|
|
15
|
+
- **Setup vs preconditions** — does the test establish the state the requirement describes (e.g. "a registered user" backed by actual data, not mocked away)?
|
|
16
|
+
- **Actions vs triggers** — does the test exercise the specified behavior with the specified inputs?
|
|
17
|
+
- **Assertions vs outcomes** — does the test verify the specified outcomes (no tautologies), and does it cover every distinct outcome the requirement lists?
|
|
18
|
+
- Note over-testing (validating behavior the requirement doesn't specify) without treating it as an error.
|
|
19
|
+
- When a test's level doesn't fit its requirement (e.g. a unit test referencing an end-to-end requirement), suggest scoping the reference or a different test level — with specific guidance.`;
|
|
20
|
+
export async function reviewTestCommand(testFilePath, options = {}) {
|
|
7
21
|
const workspaceRoot = process.cwd();
|
|
8
22
|
const fullPath = resolve(workspaceRoot, testFilePath);
|
|
9
|
-
// CLI-REVIEW-1.5:
|
|
23
|
+
// CLI-REVIEW-1.5: --source accepts only local|cloud
|
|
24
|
+
const source = options.source ?? "local";
|
|
25
|
+
if (source !== "local" && source !== "cloud") {
|
|
26
|
+
throw new Error(`Invalid --source value: ${options.source}. Expected "local" or "cloud".`);
|
|
27
|
+
}
|
|
28
|
+
// CLI-REVIEW-1.3: missing file → error + non-zero exit
|
|
10
29
|
if (!existsSync(fullPath)) {
|
|
11
30
|
throw new Error(`File not found: ${testFilePath}`);
|
|
12
31
|
}
|
|
13
|
-
// CLI-REVIEW-1.
|
|
32
|
+
// CLI-REVIEW-1.4: must be a test file
|
|
14
33
|
if (!/\.(test|spec)\.(js|jsx|ts|tsx)$/.test(testFilePath)) {
|
|
15
34
|
throw new Error(`File must be a test file: *.{test,spec}.{js,jsx,ts,tsx} — got ${testFilePath}`);
|
|
16
35
|
}
|
|
17
36
|
const testFileContents = readFileSync(fullPath, "utf-8");
|
|
18
|
-
// CLI-REVIEW-1.0: collect every requirement(...) call site
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
// CLI-REVIEW-1.0: collect every requirement(...) call site via the shared
|
|
38
|
+
// AST-based extractor (as the MCP review handler does post-#43) so that
|
|
39
|
+
// multi-arg calls (`requirement("A", "B")`) and options-bearing calls
|
|
40
|
+
// (`requirement("A", { ... })`) are captured — the old single-string regex
|
|
41
|
+
// matched neither.
|
|
42
|
+
const references = await findRequirementsInFile(fullPath);
|
|
43
|
+
const requirementIds = new Set(references.map((ref) => ref.requirementId));
|
|
44
|
+
// CLI-REVIEW-1.1: resolve references against the workspace and bundle trees.
|
|
45
|
+
// A reference may be a bare root (`AUTH-1`), an index child (`AUTH-1.0`), or
|
|
46
|
+
// a label path (`AUTH-1.given`). Normalize each reference to its root id
|
|
47
|
+
// (the substring before the first dot) before matching so label-path refs
|
|
48
|
+
// resolve instead of being dropped.
|
|
25
49
|
const { flattened } = await loadAllRequirements(workspaceRoot);
|
|
26
50
|
const rootToTestedIds = new Map();
|
|
51
|
+
// CLI-REVIEW-1.2: references whose root resolves to no workspace
|
|
52
|
+
// requirement are reported as missing (in every mode)
|
|
53
|
+
const missingIds = [];
|
|
27
54
|
for (const reqId of requirementIds) {
|
|
28
|
-
const
|
|
55
|
+
const dotIndex = reqId.indexOf(".");
|
|
56
|
+
const refRootId = dotIndex > 0 ? reqId.substring(0, dotIndex) : reqId;
|
|
57
|
+
const req = flattened.find((r) => r.rootId === refRootId);
|
|
29
58
|
if (req) {
|
|
30
59
|
if (!rootToTestedIds.has(req.rootId)) {
|
|
31
60
|
rootToTestedIds.set(req.rootId, new Set());
|
|
32
61
|
}
|
|
33
62
|
rootToTestedIds.get(req.rootId).add(reqId);
|
|
34
63
|
}
|
|
64
|
+
else {
|
|
65
|
+
missingIds.push(reqId);
|
|
66
|
+
}
|
|
35
67
|
}
|
|
68
|
+
missingIds.sort();
|
|
36
69
|
const requirements = [];
|
|
37
70
|
for (const [rootId, testedIds] of rootToTestedIds) {
|
|
38
71
|
const tree = getRequirementTree(flattened, rootId);
|
|
@@ -42,7 +75,64 @@ export async function reviewTestCommand(testFilePath) {
|
|
|
42
75
|
testedIds: Array.from(testedIds).sort(),
|
|
43
76
|
});
|
|
44
77
|
}
|
|
45
|
-
|
|
78
|
+
if (source === "cloud") {
|
|
79
|
+
await runCloudReview({
|
|
80
|
+
workspaceRoot,
|
|
81
|
+
testFilePath,
|
|
82
|
+
testFileContents,
|
|
83
|
+
requirements,
|
|
84
|
+
missingIds,
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
emitReviewMaterials({
|
|
89
|
+
testFilePath,
|
|
90
|
+
testFileContents,
|
|
91
|
+
requirements,
|
|
92
|
+
missingIds,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* CLI-REVIEW-2: local (default) mode — emit judgment-ready review materials
|
|
97
|
+
* for the calling agent (typically a dispatched review subagent). No cloud
|
|
98
|
+
* credentials required.
|
|
99
|
+
*/
|
|
100
|
+
function emitReviewMaterials(params) {
|
|
101
|
+
const { testFilePath, testFileContents, requirements, missingIds } = params;
|
|
102
|
+
console.log(`# Test Review Materials for ${testFilePath}\n`);
|
|
103
|
+
console.log(`## Judgment instructions\n`);
|
|
104
|
+
console.log(REVIEW_INSTRUCTIONS);
|
|
105
|
+
// CLI-REVIEW-2.0: resolved requirement trees bundled with the test contents
|
|
106
|
+
console.log(`\n## Referenced requirements\n`);
|
|
107
|
+
if (requirements.length === 0) {
|
|
108
|
+
console.log("(none resolved — the test file references no workspace requirements)");
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
for (const req of requirements) {
|
|
112
|
+
console.log(`### ${req.id} (tested: ${req.testedIds.join(", ")})\n`);
|
|
113
|
+
console.log(req.content);
|
|
114
|
+
console.log("");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// CLI-REVIEW-1.2: missing references reported in the output
|
|
118
|
+
if (missingIds.length > 0) {
|
|
119
|
+
console.log(`## Missing requirement references\n`);
|
|
120
|
+
console.log(`These references resolve to no requirement in this workspace — flag them in your findings:`);
|
|
121
|
+
for (const id of missingIds) {
|
|
122
|
+
console.log(`- ${id}`);
|
|
123
|
+
}
|
|
124
|
+
console.log("");
|
|
125
|
+
}
|
|
126
|
+
console.log(`## Test file under review (${testFilePath})\n`);
|
|
127
|
+
console.log(testFileContents);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* CLI-REVIEW-3: --source cloud — send the bundle to the hosted review
|
|
131
|
+
* endpoint and print the returned feedback. Requires cloud credentials.
|
|
132
|
+
*/
|
|
133
|
+
async function runCloudReview(params) {
|
|
134
|
+
const { workspaceRoot, testFilePath, testFileContents, requirements, missingIds, } = params;
|
|
135
|
+
// CLI-REVIEW-3.2: cloud credentials required
|
|
46
136
|
let projectId;
|
|
47
137
|
let projectSecret;
|
|
48
138
|
try {
|
|
@@ -54,22 +144,34 @@ export async function reviewTestCommand(testFilePath) {
|
|
|
54
144
|
throw new Error(`Test review requires cloud credentials. Run \`dotrequirements link\` to connect this project to the cloud.\n` +
|
|
55
145
|
`(${error instanceof Error ? error.message : String(error)})`);
|
|
56
146
|
}
|
|
57
|
-
// CLI-REVIEW-
|
|
147
|
+
// CLI-REVIEW-3.0 / .1 / .3: dispatch to the hosted endpoint
|
|
58
148
|
const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL ?? DEFAULT_API_BASE_URL;
|
|
59
149
|
let feedback;
|
|
150
|
+
let quotaWarning;
|
|
60
151
|
try {
|
|
61
|
-
feedback = await fetchReviewTestFeedback({
|
|
152
|
+
({ feedback, quotaWarning } = await fetchReviewTestFeedback({
|
|
62
153
|
apiBaseUrl,
|
|
63
154
|
projectId,
|
|
64
155
|
projectSecret,
|
|
65
156
|
testFileContents,
|
|
66
157
|
requirements,
|
|
67
|
-
});
|
|
158
|
+
}));
|
|
68
159
|
}
|
|
69
160
|
catch (error) {
|
|
70
161
|
throw new Error(`Test review failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
162
|
}
|
|
72
163
|
console.log(`Test Review Results for ${testFilePath}\n`);
|
|
73
164
|
console.log(feedback);
|
|
165
|
+
// LIMITS-5.2 / 5.2.0: the 80–99% quota warning prints with the result
|
|
166
|
+
if (quotaWarning) {
|
|
167
|
+
console.log(`\n⚠ ${quotaWarning}`);
|
|
168
|
+
}
|
|
169
|
+
// CLI-REVIEW-1.2: missing references reported in every mode
|
|
170
|
+
if (missingIds.length > 0) {
|
|
171
|
+
console.log(`\nMissing requirement references (not in this workspace):`);
|
|
172
|
+
for (const id of missingIds) {
|
|
173
|
+
console.log(`- ${id}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
74
176
|
}
|
|
75
177
|
//# sourceMappingURL=review-test.js.map
|
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { DEFAULT_API_BASE_URL, fetchStyleCheckFeedback, } from "../requirements/cloud-ai.js";
|
|
4
|
-
import { filterRequirementsByKeys } from "../requirements/index.js";
|
|
5
|
-
import {
|
|
3
|
+
import { DEFAULT_API_BASE_URL, fetchStyleCheckFeedback, getProjectContext, } from "../requirements/cloud-ai.js";
|
|
4
|
+
import { filterRequirementsByKeys, loadAllRequirements, } from "../requirements/index.js";
|
|
5
|
+
import { generateStyleGuideBody, readLocalStyleGuide, } from "../requirements/style-guide.js";
|
|
6
|
+
import { findProjectRoot, getProjectCredentials, } from "../utils/project-settings.js";
|
|
7
|
+
const CONVEX_URL = "https://data.dotrequirements.io";
|
|
8
|
+
/**
|
|
9
|
+
* Judgment instructions for test-file style review in local mode. Mirrors the
|
|
10
|
+
* hosted test-style rubric (PROMPT-STYLE-TESTS-*): requirement() usage,
|
|
11
|
+
* comment discipline, structure, coverage-comment red flags, and semantic
|
|
12
|
+
* alignment between test anatomy and requirement anatomy.
|
|
13
|
+
*/
|
|
14
|
+
const TEST_STYLE_INSTRUCTIONS = `You are the reviewer. Judge the test file below against these test-style conventions, then report findings under the headings MUST FIX / SHOULD FIX / COULD IMPROVE (categorize each finding by its own severity; semantic gaps are MUST FIX). Focus on gaps and give actionable suggestions.
|
|
15
|
+
|
|
16
|
+
- \`requirement()\` is used AS the description parameter of \`test()\`, \`describe()\`, or \`it()\` — flag requirement() calls placed in test bodies, and plain-string descriptions that should reference a requirement.
|
|
17
|
+
- Comments describe what the test implementation does — flag comments that copy requirement text verbatim (the requirement() reference already carries that meaning). Tests need no comments at all.
|
|
18
|
+
- Structured requirements (Given/When/Then trees) are tested with nested describe/it blocks; simple requirements need no extra nesting.
|
|
19
|
+
- Flag comments that document requirement coverage (e.g. "Requirements coverage: AUTH-9.0 ✓") — coverage lives in the harness, not comments; a second source of truth drifts.
|
|
20
|
+
- Test setup should establish the preconditions the referenced requirement describes; actions should exercise the specified behavior with the specified inputs; assertions should verify the specified outcomes (not tautologies like expect(true).toBe(true)). Flag requirements whose distinct outcomes are only partially validated; note over-testing without treating it as an error.
|
|
21
|
+
- When a test's level doesn't fit its requirement (e.g. a unit test referencing an end-to-end requirement), suggest scoping the reference or a different test level — with specific guidance, not generic observations.
|
|
22
|
+
|
|
23
|
+
The referenced requirement trees are not bundled here; if you need one, read it from \`.requirements/\` or run \`dotreq get <KEY>\`. For a full semantic review of tests against their requirements, \`dotreq review-test <file>\` is the dedicated verb.`;
|
|
24
|
+
const REQUIREMENTS_STYLE_INSTRUCTIONS = `You are the reviewer. Judge the requirements below against the style guide above, then report findings under the headings MUST FIX / SHOULD FIX / COULD IMPROVE (categorize each finding by its own severity). Focus on gaps and give actionable suggestions rather than just listing problems.`;
|
|
6
25
|
export async function styleCheckCommand(filePath, options) {
|
|
7
26
|
const workspaceRoot = process.cwd();
|
|
8
27
|
const fullPath = resolve(workspaceRoot, filePath);
|
|
9
|
-
// CLI-STYLE-1.
|
|
28
|
+
// CLI-STYLE-1.6: --source accepts only local|cloud
|
|
29
|
+
const source = options.source ?? "local";
|
|
30
|
+
if (source !== "local" && source !== "cloud") {
|
|
31
|
+
throw new Error(`Invalid --source value: ${options.source}. Expected "local" or "cloud".`);
|
|
32
|
+
}
|
|
33
|
+
// CLI-STYLE-1.7: --model configures the hosted review only
|
|
34
|
+
if (options.model && source !== "cloud") {
|
|
35
|
+
throw new Error(`--model applies to cloud review only. Add --source cloud to choose a review model.`);
|
|
36
|
+
}
|
|
37
|
+
// CLI-STYLE-1.4: missing file → error + non-zero exit
|
|
10
38
|
if (!existsSync(fullPath)) {
|
|
11
39
|
throw new Error(`File not found: ${filePath}`);
|
|
12
40
|
}
|
|
13
|
-
// CLI-STYLE-1.0 / .1 / .
|
|
41
|
+
// CLI-STYLE-1.0 / .1 / .5: file-type detection
|
|
14
42
|
const isRequirementsFile = filePath.endsWith(".requirements.md");
|
|
15
43
|
const isTestFile = /\.(test|spec)\.(js|jsx|ts|tsx)$/.test(filePath);
|
|
16
44
|
if (!isRequirementsFile && !isTestFile) {
|
|
@@ -35,7 +63,100 @@ export async function styleCheckCommand(filePath, options) {
|
|
|
35
63
|
scopeNote = `\nNote: Some specified keys were not found in the file: ${missingKeys.join(", ")}`;
|
|
36
64
|
}
|
|
37
65
|
}
|
|
38
|
-
|
|
66
|
+
const scopeLabel = options.keys && options.keys.length > 0
|
|
67
|
+
? ` (${options.keys.join(", ")})`
|
|
68
|
+
: "";
|
|
69
|
+
if (source === "cloud") {
|
|
70
|
+
await runCloudStyleCheck({
|
|
71
|
+
workspaceRoot,
|
|
72
|
+
filePath,
|
|
73
|
+
fileContentsToCheck,
|
|
74
|
+
fileType,
|
|
75
|
+
model: options.model,
|
|
76
|
+
scopeLabel,
|
|
77
|
+
scopeNote,
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await emitStyleMaterials({
|
|
82
|
+
workspaceRoot,
|
|
83
|
+
filePath,
|
|
84
|
+
fileContentsToCheck,
|
|
85
|
+
fileType,
|
|
86
|
+
scopeLabel,
|
|
87
|
+
scopeNote,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* CLI-STYLE-2: local (default) mode — emit judgment-ready review materials
|
|
92
|
+
* for the calling agent (typically a dispatched review subagent). No cloud
|
|
93
|
+
* credentials required.
|
|
94
|
+
*/
|
|
95
|
+
async function emitStyleMaterials(params) {
|
|
96
|
+
const { workspaceRoot, filePath, fileContentsToCheck, fileType, scopeLabel, scopeNote, } = params;
|
|
97
|
+
console.log(`# Style Review Materials for ${filePath}${scopeLabel}\n`);
|
|
98
|
+
if (fileType === "requirements") {
|
|
99
|
+
// CLI-STYLE-2.0: compose the style guide the same way
|
|
100
|
+
// create-requirement-document does.
|
|
101
|
+
// CLI-STYLE-2.0.0: a project STYLE.md, when present, IS the guide.
|
|
102
|
+
const localStyleGuide = readLocalStyleGuide(workspaceRoot);
|
|
103
|
+
let guide;
|
|
104
|
+
if (localStyleGuide?.trim()) {
|
|
105
|
+
guide = localStyleGuide;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// CLI-STYLE-2.0.1: otherwise generate from bundled defaults, workspace
|
|
109
|
+
// patterns, and cloud custom guidance when linked.
|
|
110
|
+
let requirements = [];
|
|
111
|
+
try {
|
|
112
|
+
const result = await loadAllRequirements(workspaceRoot);
|
|
113
|
+
requirements = result.flattened;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// No requirements in the workspace yet — defaults still apply
|
|
117
|
+
}
|
|
118
|
+
// CLI-STYLE-2.3 / .4: credentials are optional; cloud custom guidance is
|
|
119
|
+
// silently omitted when unlinked or unreachable.
|
|
120
|
+
let customStyleGuidance = null;
|
|
121
|
+
const projectRoot = findProjectRoot(workspaceRoot);
|
|
122
|
+
if (projectRoot) {
|
|
123
|
+
try {
|
|
124
|
+
const { projectId, projectSecret } = getProjectCredentials(workspaceRoot);
|
|
125
|
+
const contextData = await getProjectContext(projectId, projectSecret, CONVEX_URL);
|
|
126
|
+
customStyleGuidance = contextData?.requirementsStyleContext ?? null;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Unlinked or cloud unavailable — fall through with defaults
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
guide = generateStyleGuideBody({ requirements, customStyleGuidance });
|
|
133
|
+
}
|
|
134
|
+
console.log(`## Style guide\n`);
|
|
135
|
+
console.log(guide);
|
|
136
|
+
console.log(`\n## Judgment instructions\n`);
|
|
137
|
+
console.log(REQUIREMENTS_STYLE_INSTRUCTIONS);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
console.log(`## Judgment instructions\n`);
|
|
141
|
+
console.log(TEST_STYLE_INSTRUCTIONS);
|
|
142
|
+
}
|
|
143
|
+
// CLI-STYLE-2.1: the content under review (keys-filtered when --keys given)
|
|
144
|
+
const heading = fileType === "requirements"
|
|
145
|
+
? "Requirements under review"
|
|
146
|
+
: "Test file under review";
|
|
147
|
+
console.log(`\n## ${heading} (${filePath})\n`);
|
|
148
|
+
console.log(fileContentsToCheck);
|
|
149
|
+
if (scopeNote) {
|
|
150
|
+
console.log(scopeNote);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* CLI-STYLE-3: --source cloud — send the file to the hosted review endpoint
|
|
155
|
+
* and print the returned feedback. Requires cloud credentials.
|
|
156
|
+
*/
|
|
157
|
+
async function runCloudStyleCheck(params) {
|
|
158
|
+
const { workspaceRoot, filePath, fileContentsToCheck, fileType, model, scopeLabel, scopeNote, } = params;
|
|
159
|
+
// CLI-STYLE-3.2: credentials are required
|
|
39
160
|
let projectId;
|
|
40
161
|
let projectSecret;
|
|
41
162
|
try {
|
|
@@ -47,29 +168,32 @@ export async function styleCheckCommand(filePath, options) {
|
|
|
47
168
|
throw new Error(`Style check requires cloud credentials. Run \`dotrequirements link\` to connect this project to the cloud.\n` +
|
|
48
169
|
`(${error instanceof Error ? error.message : String(error)})`);
|
|
49
170
|
}
|
|
50
|
-
// CLI-STYLE-
|
|
171
|
+
// CLI-STYLE-3.0 / .1 / .3: dispatch to the hosted endpoint
|
|
51
172
|
const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL ?? DEFAULT_API_BASE_URL;
|
|
52
173
|
let feedback;
|
|
174
|
+
let quotaWarning;
|
|
53
175
|
try {
|
|
54
|
-
feedback = await fetchStyleCheckFeedback({
|
|
176
|
+
({ feedback, quotaWarning } = await fetchStyleCheckFeedback({
|
|
55
177
|
apiBaseUrl,
|
|
56
178
|
projectId,
|
|
57
179
|
projectSecret,
|
|
58
180
|
fileContents: fileContentsToCheck,
|
|
59
181
|
fileType,
|
|
60
|
-
model
|
|
61
|
-
});
|
|
182
|
+
model,
|
|
183
|
+
}));
|
|
62
184
|
}
|
|
63
185
|
catch (error) {
|
|
64
186
|
throw new Error(`Style check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
187
|
}
|
|
66
|
-
|
|
67
|
-
? ` (${options.keys.join(", ")})`
|
|
68
|
-
: "";
|
|
188
|
+
// CLI-STYLE-3.4: hosted feedback arrives severity-categorized
|
|
69
189
|
console.log(`Style Check Results for ${filePath}${scopeLabel}\n`);
|
|
70
190
|
console.log(feedback);
|
|
71
191
|
if (scopeNote) {
|
|
72
192
|
console.log(scopeNote);
|
|
73
193
|
}
|
|
194
|
+
// LIMITS-5.2 / 5.2.0: the 80–99% quota warning prints with the result
|
|
195
|
+
if (quotaWarning) {
|
|
196
|
+
console.log(`\n⚠ ${quotaWarning}`);
|
|
197
|
+
}
|
|
74
198
|
}
|
|
75
199
|
//# sourceMappingURL=style-check.js.map
|