@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.
Files changed (84) hide show
  1. package/README.md +13 -69
  2. package/dist/cli.js +19 -7
  3. package/dist/codebase-to-spec/present.js +4 -5
  4. package/dist/codebase-to-spec/validate.js +3 -2
  5. package/dist/commands/acceptance-test.js +4 -2
  6. package/dist/commands/ai-setup.d.ts +8 -2
  7. package/dist/commands/ai-setup.js +154 -310
  8. package/dist/commands/get.js +6 -2
  9. package/dist/commands/init.js +8 -6
  10. package/dist/commands/link-resolution.d.ts +3 -1
  11. package/dist/commands/link-resolution.js +4 -2
  12. package/dist/commands/mcp.d.ts +8 -2
  13. package/dist/commands/mcp.js +17 -6
  14. package/dist/commands/pull.js +36 -3
  15. package/dist/commands/push.js +54 -16
  16. package/dist/commands/report.js +18 -3
  17. package/dist/commands/review-test.d.ts +5 -1
  18. package/dist/commands/review-test.js +117 -15
  19. package/dist/commands/style-check.d.ts +1 -0
  20. package/dist/commands/style-check.js +137 -13
  21. package/dist/commands/tests-for.js +13 -13
  22. package/dist/commands/validate.js +14 -14
  23. package/dist/convex.d.ts +1 -3
  24. package/dist/convex.js +3 -3
  25. package/dist/harness/cache.d.ts +19 -3
  26. package/dist/harness/cache.js +38 -12
  27. package/dist/harness/finalize.js +33 -1
  28. package/dist/harness/index.js +16 -9
  29. package/dist/harness/requirementsLoader.js +12 -0
  30. package/dist/harness/tracking.d.ts +17 -2
  31. package/dist/harness/tracking.js +83 -9
  32. package/dist/push/core.d.ts +50 -0
  33. package/dist/push/core.js +149 -11
  34. package/dist/push/index.d.ts +1 -1
  35. package/dist/push/index.js +1 -1
  36. package/dist/requirements/cloud-ai.d.ts +21 -8
  37. package/dist/requirements/cloud-ai.js +10 -8
  38. package/dist/requirements/cloud-coverage.d.ts +12 -2
  39. package/dist/requirements/cloud-coverage.js +30 -3
  40. package/dist/requirements/grep.d.ts +7 -2
  41. package/dist/requirements/grep.js +75 -47
  42. package/dist/schema/builder.d.ts +1 -1
  43. package/dist/schema/builder.js +13 -0
  44. package/dist/schema/conversions.d.ts +7 -2
  45. package/dist/schema/conversions.js +13 -4
  46. package/dist/schema/parser-core.d.ts +41 -0
  47. package/dist/schema/parser-core.js +113 -18
  48. package/dist/schema/parser.d.ts +8 -26
  49. package/dist/schema/parser.js +23 -251
  50. package/dist/schema/resolver.js +18 -8
  51. package/dist/templates/context-file-section.md +25 -22
  52. package/dist/utils/context-file.d.ts +7 -3
  53. package/dist/utils/context-file.js +10 -7
  54. package/dist/utils/env.js +17 -1
  55. package/dist/utils/oauth-flow.js +8 -0
  56. package/dist/utils/project-settings.d.ts +5 -0
  57. package/dist/utils/project-settings.js +36 -1
  58. package/package.json +3 -5
  59. package/dist/mcp/convexClient.d.ts +0 -19
  60. package/dist/mcp/convexClient.js +0 -24
  61. package/dist/mcp/handlers/authoring.d.ts +0 -41
  62. package/dist/mcp/handlers/authoring.js +0 -104
  63. package/dist/mcp/handlers/debug.d.ts +0 -16
  64. package/dist/mcp/handlers/debug.js +0 -37
  65. package/dist/mcp/handlers/get.d.ts +0 -24
  66. package/dist/mcp/handlers/get.js +0 -65
  67. package/dist/mcp/handlers/index.d.ts +0 -28
  68. package/dist/mcp/handlers/index.js +0 -19
  69. package/dist/mcp/handlers/list.d.ts +0 -7
  70. package/dist/mcp/handlers/list.js +0 -43
  71. package/dist/mcp/handlers/push.d.ts +0 -26
  72. package/dist/mcp/handlers/push.js +0 -186
  73. package/dist/mcp/handlers/report.d.ts +0 -16
  74. package/dist/mcp/handlers/report.js +0 -134
  75. package/dist/mcp/handlers/review.d.ts +0 -51
  76. package/dist/mcp/handlers/review.js +0 -200
  77. package/dist/mcp/handlers/search.d.ts +0 -30
  78. package/dist/mcp/handlers/search.js +0 -58
  79. package/dist/mcp/handlers/test-mapping.d.ts +0 -39
  80. package/dist/mcp/handlers/test-mapping.js +0 -133
  81. package/dist/mcp/handlers/types.d.ts +0 -75
  82. package/dist/mcp/handlers/types.js +0 -25
  83. package/dist/mcp/index.d.ts +0 -45
  84. package/dist/mcp/index.js +0 -634
@@ -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
- const filePath = existingFilePath ??
102
- path.join(requirementsDir, `${sanitizeFileName(doc.title)}.requirements.md`);
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 content = fs.readFileSync(filePath, "utf-8");
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):
@@ -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, parseFilesForPush, } from "../push/index.js";
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 all local files
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 } = parseFilesForPush(filesToPush);
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
- if (doc?.defaultPrefix && !file.metadata.document?.defaultPrefix) {
41
- console.log(` Reading ${fileName}...`);
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
- else {
45
- console.log(` Reading ${fileName}...`);
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
- const error = result.errors.find((e) => e.fileName === fileName);
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
- if (invalid.length > 0) {
183
- console.log(`Skipped - invalid files (${invalid.length}):`);
184
- for (const { file, result } of invalid) {
185
- const fileName = path.basename(file.filePath);
186
- console.log(` ✗ ${fileName}: ${result.error}`);
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
  }
@@ -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
- const record = await getRequirementCoverage(options.requirement, projectId, projectSecret, CONVEX_URL);
40
- console.log(printCloudRequirement(record, format));
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
- export declare function reviewTestCommand(testFilePath: string): Promise<void>;
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
- export async function reviewTestCommand(testFilePath) {
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: missing file → error + non-zero exit
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.6: must be a test file
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
- const requirementIdMatches = testFileContents.matchAll(/requirement\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g);
20
- const requirementIds = new Set();
21
- for (const match of requirementIdMatches) {
22
- requirementIds.add(match[1]);
23
- }
24
- // CLI-REVIEW-1.1: resolve references against the workspace and bundle trees
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 req = flattened.find((r) => r.id === reqId || r.rootId === reqId);
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
- // CLI-REVIEW-1.6: cloud credentials required
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-1.2 / .3 / .4 / .7 / .8: dispatch to the hosted endpoint
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,6 +1,7 @@
1
1
  interface StyleCheckOptions {
2
2
  keys?: string[];
3
3
  model?: string;
4
+ source?: string;
4
5
  }
5
6
  export declare function styleCheckCommand(filePath: string, options: StyleCheckOptions): Promise<void>;
6
7
  export {};
@@ -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 { getProjectCredentials } from "../utils/project-settings.js";
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.5: missing file → error + non-zero exit
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 / .6: file-type detection
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
- // CLI-STYLE-1.7: credentials are required
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-1.4 / .8 / .9: dispatch to the hosted endpoint
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: options.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
- const scopeLabel = options.keys && options.keys.length > 0
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