@popoverai/dotrequirements 0.25.0 → 0.26.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 (53) hide show
  1. package/dist/cli.js +8 -1
  2. package/dist/codebase-to-spec/present.d.ts +9 -0
  3. package/dist/codebase-to-spec/present.js +23 -2
  4. package/dist/codebase-to-spec/skill-install.d.ts +16 -6
  5. package/dist/codebase-to-spec/skill-install.js +109 -47
  6. package/dist/codebase-to-spec/version-check.d.ts +31 -0
  7. package/dist/codebase-to-spec/version-check.js +56 -0
  8. package/dist/commands/ai-setup.d.ts +12 -1
  9. package/dist/commands/ai-setup.js +65 -33
  10. package/dist/commands/codebase-to-spec/pack.d.ts +4 -0
  11. package/dist/commands/codebase-to-spec/pack.js +17 -0
  12. package/dist/commands/init.js +6 -1
  13. package/dist/commands/link-resolution.d.ts +79 -0
  14. package/dist/commands/link-resolution.js +141 -0
  15. package/dist/commands/link.d.ts +14 -4
  16. package/dist/commands/link.js +369 -16
  17. package/dist/commands/pull.js +19 -2
  18. package/dist/commands/push.js +36 -2
  19. package/dist/convex.d.ts +5 -3
  20. package/dist/convex.js +5 -3
  21. package/dist/harness/cache.d.ts +0 -14
  22. package/dist/harness/cache.js +1 -41
  23. package/dist/harness/finalize.js +2 -2
  24. package/dist/harness/prepare.js +1 -3
  25. package/dist/harness/requirementsLoader.d.ts +3 -3
  26. package/dist/harness/requirementsLoader.js +13 -8
  27. package/dist/mcp/handlers/authoring.d.ts +5 -5
  28. package/dist/mcp/handlers/authoring.js +9 -9
  29. package/dist/mcp/handlers/push.d.ts +2 -2
  30. package/dist/mcp/handlers/push.js +36 -3
  31. package/dist/mcp/handlers/review.d.ts +4 -4
  32. package/dist/mcp/handlers/review.js +4 -4
  33. package/dist/mcp/handlers/search.d.ts +1 -1
  34. package/dist/mcp/handlers/search.js +1 -1
  35. package/dist/mcp/index.js +29 -0
  36. package/dist/push/core.d.ts +18 -0
  37. package/dist/push/core.js +70 -3
  38. package/dist/push/index.d.ts +1 -1
  39. package/dist/push/index.js +1 -1
  40. package/dist/schema/parser-core.js +5 -1
  41. package/dist/schema/parser.js +5 -1
  42. package/dist/schema/run-marker.d.ts +38 -0
  43. package/dist/schema/run-marker.js +138 -0
  44. package/dist/schema/schemas.d.ts +12 -0
  45. package/dist/schema/schemas.js +1 -0
  46. package/dist/templates/agents/cts-worker.md +1 -1
  47. package/dist/templates/skills/codebase-to-spec/SKILL.md +33 -11
  48. package/dist/templates/workflows/specify-codebase.js +4 -2
  49. package/dist/utils/own-package.d.ts +10 -0
  50. package/dist/utils/own-package.js +13 -0
  51. package/dist/utils/project-selector.d.ts +5 -0
  52. package/dist/utils/project-selector.js +4 -0
  53. package/package.json +2 -2
@@ -14,7 +14,6 @@ const CACHE_DIR = ".cache";
14
14
  const LOOKUP_FILE = "lookup.json";
15
15
  const TRACKING_FILE = "tracking.jsonl";
16
16
  const COVERAGE_FILE = "coverage.json";
17
- const PROJECT_ROOT_FILE = "project-root";
18
17
  // Test run ID file (outside cache, in .requirements/)
19
18
  const TEST_RUN_ID_FILE = ".test-run-id";
20
19
  /**
@@ -70,7 +69,7 @@ export function writeLookupCache(requirementsDir, requirements) {
70
69
  label: node.label,
71
70
  content: node.content,
72
71
  };
73
- // Always add the numeric path (e.g. "AUTH-LOGIN.0")
72
+ // Always add the numeric path (e.g. "AUTH-LOGIN-1.0")
74
73
  lookup.requirements[node.id] = entry;
75
74
  // Add label path as an alias if it differs from the numeric path
76
75
  if (labelPath && labelPath !== node.id) {
@@ -174,45 +173,6 @@ export function cleanupTestRunId(requirementsDir) {
174
173
  fs.unlinkSync(testRunIdPath);
175
174
  }
176
175
  }
177
- /**
178
- * Write the project root to cache (for cross-process persistence).
179
- * This allows test workers to find the lookup cache even after cwd changes.
180
- */
181
- export function writeProjectRoot(requirementsDir, projectRoot) {
182
- const cacheDir = getCacheDir(requirementsDir, true);
183
- const projectRootPath = path.join(cacheDir, PROJECT_ROOT_FILE);
184
- fs.writeFileSync(projectRootPath, projectRoot);
185
- }
186
- /**
187
- * Read the project root from cache, returning null if not found.
188
- */
189
- export function readProjectRoot(requirementsDir) {
190
- const cacheDir = getCacheDir(requirementsDir);
191
- const projectRootPath = path.join(cacheDir, PROJECT_ROOT_FILE);
192
- if (!fs.existsSync(projectRootPath)) {
193
- return null;
194
- }
195
- return fs.readFileSync(projectRootPath, "utf-8").trim();
196
- }
197
- /**
198
- * Try to find the project root from any known .requirements cache directory.
199
- * Walks up from cwd looking for .requirements/.cache/project-root file.
200
- */
201
- export function findCachedProjectRoot(startDir = process.cwd()) {
202
- let currentDir = path.resolve(startDir);
203
- while (true) {
204
- const requirementsDir = path.join(currentDir, ".requirements");
205
- const projectRootPath = path.join(requirementsDir, CACHE_DIR, PROJECT_ROOT_FILE);
206
- if (fs.existsSync(projectRootPath)) {
207
- return fs.readFileSync(projectRootPath, "utf-8").trim();
208
- }
209
- const parentDir = path.dirname(currentDir);
210
- if (parentDir === currentDir) {
211
- return null;
212
- }
213
- currentDir = parentDir;
214
- }
215
- }
216
176
  /**
217
177
  * Clear tracking data for a new test run
218
178
  */
@@ -333,8 +333,8 @@ export async function finalize(options = {}) {
333
333
  // Read lookup cache for report
334
334
  const lookup = readLookupCache(requirementsDir);
335
335
  // Normalize alias keys to canonical numeric keys.
336
- // Non-JS consumers may write label paths (e.g. "AUTH-LOGIN.given") to tracking.jsonl.
337
- // We resolve those to their canonical numeric key (e.g. "AUTH-LOGIN.0") so that
336
+ // Non-JS consumers may write label paths (e.g. "AUTH-LOGIN-1.given") to tracking.jsonl.
337
+ // We resolve those to their canonical numeric key (e.g. "AUTH-LOGIN-1.0") so that
338
338
  // coverage counting, display, and cloud reporting all use consistent keys.
339
339
  if (lookup) {
340
340
  for (const [key, trackingEntries] of Array.from(aggregated.entries())) {
@@ -11,7 +11,7 @@
11
11
  import { findRequirementsFilesSync } from "../requirements/index.js";
12
12
  import { parseRequirementsFromFile, } from "../schema/index.js";
13
13
  import { findProjectRoot } from "../utils/project-settings.js";
14
- import { clearTrackingFile, findRequirementsDir, initTestRunId, writeLookupCache, writeProjectRoot, } from "./cache.js";
14
+ import { clearTrackingFile, findRequirementsDir, initTestRunId, writeLookupCache, } from "./cache.js";
15
15
  /**
16
16
  * Prepare the test harness for a test run.
17
17
  *
@@ -54,8 +54,6 @@ export function prepare(options = {}) {
54
54
  }
55
55
  // HARNESS-PREPARE-1: Write lookup cache for fast resolution
56
56
  writeLookupCache(requirementsDir, allRequirements);
57
- // Write project root to cache (for cross-process persistence in test workers)
58
- writeProjectRoot(requirementsDir, projectRoot);
59
57
  // HARNESS-PREPARE-3.0: Initialize cache directory (done by writeLookupCache)
60
58
  // HARNESS-PREPARE-3.1: Clear any previous tracking data
61
59
  clearTrackingFile(requirementsDir);
@@ -4,14 +4,14 @@
4
4
  *
5
5
  * Project root discovery (in priority order):
6
6
  * 1. Explicit projectRoot option passed to loadRequirements()
7
- * 2. DOTREQUIREMENTS_PROJECT_ROOT environment variable
8
- * 3. Cached project root file (.requirements/.cache/project-root) written by prepare()
7
+ * 2. DOTREQUIREMENTS_PROJECT_ROOT environment variable (set by prepare())
8
+ * 3. Walk up from the working directory to the nearest .requirements/ folder
9
9
  */
10
10
  import { type RequirementNode } from "../schema/index.js";
11
11
  import type { Requirement } from "./types.js";
12
12
  export declare const loadedRequirements: Map<string, RequirementNode>;
13
13
  export interface LoadOptions {
14
- /** Explicit project root path. Takes precedence over env var and cached file. */
14
+ /** Explicit project root path. Takes precedence over env var and walk-up discovery. */
15
15
  projectRoot?: string;
16
16
  }
17
17
  /**
@@ -4,14 +4,14 @@
4
4
  *
5
5
  * Project root discovery (in priority order):
6
6
  * 1. Explicit projectRoot option passed to loadRequirements()
7
- * 2. DOTREQUIREMENTS_PROJECT_ROOT environment variable
8
- * 3. Cached project root file (.requirements/.cache/project-root) written by prepare()
7
+ * 2. DOTREQUIREMENTS_PROJECT_ROOT environment variable (set by prepare())
8
+ * 3. Walk up from the working directory to the nearest .requirements/ folder
9
9
  */
10
10
  import * as fs from "node:fs";
11
11
  import * as path from "node:path";
12
12
  import { findRequirementsFilesSync } from "../requirements/index.js";
13
13
  import { parseRequirementsFromFile, resolveRequirementPath, } from "../schema/index.js";
14
- import { findCachedProjectRoot, readLookupCache } from "./cache.js";
14
+ import { findProjectRoot as findProjectRootByWalkUp, readLookupCache, } from "./cache.js";
15
15
  // Cache: Maps full requirement IDs (e.g., "REQ-123.0.1") to RequirementNode
16
16
  export const loadedRequirements = new Map();
17
17
  // Cache: Maps root requirement IDs (e.g., "REQ-123") to their tree
@@ -22,7 +22,12 @@ let warnedAboutFallback = false;
22
22
  * Find the project root using the priority order:
23
23
  * 1. Explicit projectRoot option
24
24
  * 2. DOTREQUIREMENTS_PROJECT_ROOT env var
25
- * 3. Cached project root file
25
+ * 3. Walk up from cwd to the nearest .requirements/ folder
26
+ *
27
+ * The walk-up means the nearest enclosing project wins: tests running in a
28
+ * worktree nested inside another checkout load that worktree's requirements,
29
+ * not the outer checkout's (HARNESS-REQUIREMENT-4.3). It also matches how
30
+ * tracking.ts discovers the requirements directory.
26
31
  */
27
32
  function findProjectRoot(options = {}) {
28
33
  // Priority 1: Explicit option
@@ -33,8 +38,8 @@ function findProjectRoot(options = {}) {
33
38
  if (process.env.DOTREQUIREMENTS_PROJECT_ROOT) {
34
39
  return process.env.DOTREQUIREMENTS_PROJECT_ROOT;
35
40
  }
36
- // Priority 3: Cached file (written by prepare())
37
- return findCachedProjectRoot(process.cwd());
41
+ // Priority 3: Nearest .requirements/ folder (HARNESS-REQUIREMENT-4.2)
42
+ return findProjectRootByWalkUp(process.cwd());
38
43
  }
39
44
  /**
40
45
  * Try to load requirements from lookup cache (fast path).
@@ -97,8 +102,8 @@ export function loadRequirements(options = {}) {
97
102
  // Find project root
98
103
  const projectRoot = findProjectRoot(options);
99
104
  if (!projectRoot) {
100
- throw new Error("Could not find project root. " +
101
- "Set DOTREQUIREMENTS_PROJECT_ROOT environment variable or run prepare() in globalSetup.");
105
+ throw new Error(`Could not find project root: no .requirements/ directory in ${process.cwd()} or any parent directory. ` +
106
+ "Create one with `dotrequirements init`, or set the DOTREQUIREMENTS_PROJECT_ROOT environment variable.");
102
107
  }
103
108
  // Try cache first (fast path)
104
109
  if (tryLoadFromCache(projectRoot)) {
@@ -23,7 +23,7 @@ export interface ValidateRequirementsArgs {
23
23
  *
24
24
  * Requirements covered:
25
25
  * - MCP-AUTHOR-1.0: The template includes format guidance with code block examples
26
- * - MCP-AUTHOR-1.1: The template includes guidance on concrete examples, concise prose, and testable conditions
26
+ * - MCP-AUTHOR-1.1: The template includes style guidance (concrete examples, concise prose, testable conditions)
27
27
  * - MCP-AUTHOR-1.2: When the project has requirementsStyleContext configured, it is included in the template
28
28
  * - MCP-AUTHOR-1.3: When cloud credentials are unavailable, the template works without the custom context
29
29
  */
@@ -32,10 +32,10 @@ export declare function handleCreateRequirementDocument(args: CreateRequirementD
32
32
  * Handler for validate_requirements tool
33
33
  *
34
34
  * Requirements covered:
35
- * - MCP-AUTHOR-2.0: When the file has valid syntax, the response confirms validation passed
36
- * - MCP-AUTHOR-2.1: When the file has syntax errors, the response lists each error with location
37
- * - MCP-AUTHOR-2.2: Validation does not require network access or cloud credentials
38
- * - MCP-AUTHOR-2.3: When the file does not exist, an error is returned
35
+ * - MCP-AUTHOR-2.1: When the file has valid syntax, the response confirms validation passed
36
+ * - MCP-AUTHOR-2.2: When the file has syntax errors, the response lists each error with location
37
+ * - MCP-AUTHOR-2.3: Validation does not require network access or cloud credentials
38
+ * - MCP-AUTHOR-2.4: When the file does not exist, an error is returned
39
39
  */
40
40
  export declare function handleValidateRequirements(args: ValidateRequirementsArgs, context: HandlerContext): Promise<ToolResponse>;
41
41
  //# sourceMappingURL=authoring.d.ts.map
@@ -17,7 +17,7 @@ import { errorResponse, textResponse } from "./types.js";
17
17
  *
18
18
  * Requirements covered:
19
19
  * - MCP-AUTHOR-1.0: The template includes format guidance with code block examples
20
- * - MCP-AUTHOR-1.1: The template includes guidance on concrete examples, concise prose, and testable conditions
20
+ * - MCP-AUTHOR-1.1: The template includes style guidance (concrete examples, concise prose, testable conditions)
21
21
  * - MCP-AUTHOR-1.2: When the project has requirementsStyleContext configured, it is included in the template
22
22
  * - MCP-AUTHOR-1.3: When cloud credentials are unavailable, the template works without the custom context
23
23
  */
@@ -53,21 +53,21 @@ export async function handleCreateRequirementDocument(args, context) {
53
53
  * Handler for validate_requirements tool
54
54
  *
55
55
  * Requirements covered:
56
- * - MCP-AUTHOR-2.0: When the file has valid syntax, the response confirms validation passed
57
- * - MCP-AUTHOR-2.1: When the file has syntax errors, the response lists each error with location
58
- * - MCP-AUTHOR-2.2: Validation does not require network access or cloud credentials
59
- * - MCP-AUTHOR-2.3: When the file does not exist, an error is returned
56
+ * - MCP-AUTHOR-2.1: When the file has valid syntax, the response confirms validation passed
57
+ * - MCP-AUTHOR-2.2: When the file has syntax errors, the response lists each error with location
58
+ * - MCP-AUTHOR-2.3: Validation does not require network access or cloud credentials
59
+ * - MCP-AUTHOR-2.4: When the file does not exist, an error is returned
60
60
  */
61
61
  export async function handleValidateRequirements(args, context) {
62
62
  const { filePath } = args;
63
- // MCP-AUTHOR-2.2: Validation works offline - just use workspaceRoot
63
+ // MCP-AUTHOR-2.3: Validation works offline - just use workspaceRoot
64
64
  const fullPath = resolve(context.workspaceRoot, filePath);
65
- // MCP-AUTHOR-2.3: Check if file exists
65
+ // MCP-AUTHOR-2.4: Check if file exists
66
66
  if (!existsSync(fullPath)) {
67
67
  return errorResponse(`File not found: ${filePath}`);
68
68
  }
69
69
  try {
70
- // MCP-AUTHOR-2.0 & MCP-AUTHOR-2.1: Parse and validate
70
+ // MCP-AUTHOR-2.1 & MCP-AUTHOR-2.2: Parse and validate
71
71
  const parsed = parseRequirementsFromFile(fullPath);
72
72
  const reqCount = Object.keys(parsed.requirements).length;
73
73
  // Check push readiness
@@ -89,7 +89,7 @@ export async function handleValidateRequirements(args, context) {
89
89
  return textResponse(`${headline}${pushDetails}\n\n**Metadata:**\n- Version: ${parsed.metadata.version ?? "(none)"}\n- Document: ${parsed.metadata.document?.title || "(none)"}\n- Document ID: ${parsed.metadata.document?.id || "(none)"}\n\n**Requirements:** ${reqCount} root requirement(s) found`);
90
90
  }
91
91
  catch (error) {
92
- // MCP-AUTHOR-2.1: Syntax errors are returned with details
92
+ // MCP-AUTHOR-2.2: Syntax errors are returned with details
93
93
  return {
94
94
  content: [
95
95
  {
@@ -17,10 +17,10 @@ export interface PushRequirementsArgs {
17
17
  * Handler for push_requirements tool
18
18
  *
19
19
  * Requirements covered:
20
- * - MCP-PUSH-1.0: When a push is requested without confirmed flag, a diff preview is returned
20
+ * - MCP-PUSH-1.0: When not confirmed, no changes are made and a preview of creates/updates is returned
21
21
  * - MCP-PUSH-1.1: When confirmed is true, the push executes and returns success message
22
22
  * - MCP-PUSH-1.2: When credentials are missing or invalid, an error explains how to authenticate
23
- * - MCP-PUSH-1.3: When filePath is provided, only that file is pushed
23
+ * - MCP-PUSH-1.3: When filePath is provided, only that file is pushed (MCP-PUSH-1.3.0: missing file is an error)
24
24
  */
25
25
  export declare function handlePushRequirements(args: PushRequirementsArgs, context: HandlerContext): Promise<ToolResponse>;
26
26
  //# sourceMappingURL=push.d.ts.map
@@ -14,10 +14,10 @@ import { errorResponse, textResponse } from "./types.js";
14
14
  * Handler for push_requirements tool
15
15
  *
16
16
  * Requirements covered:
17
- * - MCP-PUSH-1.0: When a push is requested without confirmed flag, a diff preview is returned
17
+ * - MCP-PUSH-1.0: When not confirmed, no changes are made and a preview of creates/updates is returned
18
18
  * - MCP-PUSH-1.1: When confirmed is true, the push executes and returns success message
19
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
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
21
  */
22
22
  export async function handlePushRequirements(args, context) {
23
23
  const { filePath, confirmed = false, projectId } = args;
@@ -123,19 +123,52 @@ export async function handlePushRequirements(args, context) {
123
123
  // MCP-PUSH-1.1: When confirmed, execute push
124
124
  try {
125
125
  const result = await executePush(dryRunResult, credentials);
126
- let output = "✓ Push complete!\n\n";
126
+ // SYNC-FAIL-1: a failed push must not masquerade as success
127
+ const failed = result.errors.length;
128
+ const syncedCount = result.created + result.updated;
129
+ let output;
130
+ if (failed > 0 && syncedCount === 0) {
131
+ output = `✗ Push failed: ${failed} document(s) could not be saved.\n\n`;
132
+ }
133
+ else if (failed > 0) {
134
+ output = `⚠ Push incomplete: ${syncedCount} document(s) synced, ${failed} failed.\n\n`;
135
+ }
136
+ else {
137
+ output = "✓ Push complete!\n\n";
138
+ }
127
139
  if (result.created > 0) {
128
140
  output += `**Created:** ${result.created} document(s)\n`;
129
141
  }
130
142
  if (result.updated > 0) {
131
143
  output += `**Updated:** ${result.updated} document(s)\n`;
132
144
  }
145
+ // IMPORT-3: marker failures are loud but never fatal
146
+ if (result.importWarnings.length > 0) {
147
+ output += `\n**Import marker warnings:**\n`;
148
+ for (const { fileName, status } of result.importWarnings) {
149
+ output +=
150
+ status === "already_used"
151
+ ? `- ⚠ ${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`
152
+ : `- ⚠ ${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`;
153
+ }
154
+ }
133
155
  if (result.errors.length > 0) {
134
156
  output += `\n**Errors:**\n`;
135
157
  for (const { fileName, error } of result.errors) {
136
158
  output += `- ${fileName}: ${error}\n`;
137
159
  }
138
160
  }
161
+ // SYNC-LAND-1: where each synced document lives in the web app
162
+ if (result.synced.length > 0) {
163
+ output += `\n**Review in dot•requirements:**\n`;
164
+ for (const doc of result.synced) {
165
+ output += `- ${doc.fileName}: ${doc.url}\n`;
166
+ }
167
+ }
168
+ // SYNC-FAIL-1.2: total failure is an error result, not a success story
169
+ if (failed > 0 && syncedCount === 0) {
170
+ return { content: [{ type: "text", text: output }], isError: true };
171
+ }
139
172
  return textResponse(output);
140
173
  }
141
174
  catch (error) {
@@ -25,7 +25,7 @@ export interface ReviewTestArgs {
25
25
  * Handler for style_check tool
26
26
  *
27
27
  * Requirements covered:
28
- * - MCP-REVIEW-1.0: For requirements files, feedback identifies vague language, missing preconditions, and style violations
28
+ * - MCP-REVIEW-1.0: For requirements files, feedback identifies clarity issues such as vague language and missing preconditions
29
29
  * - MCP-REVIEW-1.1: For test files, feedback identifies incorrect requirement() usage and missing test coverage
30
30
  * - MCP-REVIEW-1.2: Feedback is categorized by severity: must fix, should fix, could improve
31
31
  * - MCP-REVIEW-1.3: When cloud credentials are unavailable, an error explains how to authenticate
@@ -38,9 +38,9 @@ export declare function handleStyleCheck(args: StyleCheckArgs, context: HandlerC
38
38
  * Handler for review_test tool
39
39
  *
40
40
  * Requirements covered:
41
- * - MCP-REVIEW-2.0: The review validates that test setup matches requirement preconditions
42
- * - MCP-REVIEW-2.1: The review validates that test actions match requirement triggers
43
- * - MCP-REVIEW-2.2: The review validates that test assertions match requirement outcomes
41
+ * - MCP-REVIEW-2.0: Feedback identifies test setup that does not match requirement preconditions
42
+ * - MCP-REVIEW-2.1: Feedback identifies test actions that do not match requirement triggers
43
+ * - MCP-REVIEW-2.2: Feedback identifies test assertions that do not match requirement outcomes
44
44
  * - MCP-REVIEW-2.3: Feedback identifies requirements without test coverage
45
45
  * - MCP-REVIEW-2.4: Feedback identifies tests that reference non-existent requirements
46
46
  * - MCP-REVIEW-2.5: When cloud credentials are unavailable, an error explains how to authenticate
@@ -26,7 +26,7 @@ const STYLE_CHECK_GUIDANCE = `
26
26
  * Handler for style_check tool
27
27
  *
28
28
  * Requirements covered:
29
- * - MCP-REVIEW-1.0: For requirements files, feedback identifies vague language, missing preconditions, and style violations
29
+ * - MCP-REVIEW-1.0: For requirements files, feedback identifies clarity issues such as vague language and missing preconditions
30
30
  * - MCP-REVIEW-1.1: For test files, feedback identifies incorrect requirement() usage and missing test coverage
31
31
  * - MCP-REVIEW-1.2: Feedback is categorized by severity: must fix, should fix, could improve
32
32
  * - MCP-REVIEW-1.3: When cloud credentials are unavailable, an error explains how to authenticate
@@ -120,9 +120,9 @@ export async function handleStyleCheck(args, context, options) {
120
120
  * Handler for review_test tool
121
121
  *
122
122
  * Requirements covered:
123
- * - MCP-REVIEW-2.0: The review validates that test setup matches requirement preconditions
124
- * - MCP-REVIEW-2.1: The review validates that test actions match requirement triggers
125
- * - MCP-REVIEW-2.2: The review validates that test assertions match requirement outcomes
123
+ * - MCP-REVIEW-2.0: Feedback identifies test setup that does not match requirement preconditions
124
+ * - MCP-REVIEW-2.1: Feedback identifies test actions that do not match requirement triggers
125
+ * - MCP-REVIEW-2.2: Feedback identifies test assertions that do not match requirement outcomes
126
126
  * - MCP-REVIEW-2.3: Feedback identifies requirements without test coverage
127
127
  * - MCP-REVIEW-2.4: Feedback identifies tests that reference non-existent requirements
128
128
  * - MCP-REVIEW-2.5: When cloud credentials are unavailable, an error explains how to authenticate
@@ -23,7 +23,7 @@ export interface SearchRequirementsArgs {
23
23
  * - MCP-SEARCH-1.3: useRegex interprets query as case-insensitive regex
24
24
  * - MCP-SEARCH-1.4: Invalid regex returns error
25
25
  * - MCP-SEARCH-1.5: No matches returns informative message
26
- * - MCP-SEARCH-1.6: Nested match returns root requirement
26
+ * - MCP-SEARCH-1.6: Nested match returns the full root requirement tree
27
27
  * - MCP-SEARCH-1.7: Results include full tree as code block
28
28
  */
29
29
  export declare function handleSearchRequirements(args: SearchRequirementsArgs, context: HandlerContext): Promise<ToolResponse>;
@@ -16,7 +16,7 @@ import { errorResponse, textResponse } from "./types.js";
16
16
  * - MCP-SEARCH-1.3: useRegex interprets query as case-insensitive regex
17
17
  * - MCP-SEARCH-1.4: Invalid regex returns error
18
18
  * - MCP-SEARCH-1.5: No matches returns informative message
19
- * - MCP-SEARCH-1.6: Nested match returns root requirement
19
+ * - MCP-SEARCH-1.6: Nested match returns the full root requirement tree
20
20
  * - MCP-SEARCH-1.7: Results include full tree as code block
21
21
  */
22
22
  export async function handleSearchRequirements(args, context) {
package/dist/mcp/index.js CHANGED
@@ -523,11 +523,40 @@ After writing tests:
523
523
  }
524
524
  throw new Error(`Unknown prompt: ${name}`);
525
525
  });
526
+ /**
527
+ * MCP-ARGS-1: find required arguments (per the tool's inputSchema) that are
528
+ * missing from a call, so we can fail with a self-explaining error before the
529
+ * handler runs — a missing argument must never surface as an internal crash.
530
+ */
531
+ function findMissingRequiredArgs(toolName, args) {
532
+ const tool = tools.find((t) => t.name === toolName);
533
+ if (!tool)
534
+ return undefined; // unknown tools get their own error downstream
535
+ const schema = tool.inputSchema;
536
+ const required = schema.required ?? [];
537
+ const missing = required.filter((key) => args?.[key] === undefined || args?.[key] === null);
538
+ if (missing.length === 0)
539
+ return undefined;
540
+ return { missing, accepted: Object.keys(schema.properties ?? {}) };
541
+ }
526
542
  // Handle tool calls
527
543
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
528
544
  const { name, arguments: args } = request.params;
529
545
  // Refresh cache for each request to pick up changes
530
546
  invalidateCache();
547
+ // MCP-ARGS-1.2: reject calls missing required arguments before dispatch
548
+ const argCheck = findMissingRequiredArgs(name, args);
549
+ if (argCheck) {
550
+ return {
551
+ content: [
552
+ {
553
+ type: "text",
554
+ text: `Missing required argument(s) for ${name}: ${argCheck.missing.join(", ")}. Accepted arguments: ${argCheck.accepted.join(", ")}.`,
555
+ },
556
+ ],
557
+ isError: true,
558
+ };
559
+ }
531
560
  // Create handler context for extracted handlers
532
561
  const handlerContext = {
533
562
  getRequirements,
@@ -72,6 +72,13 @@ export interface DryRunResult {
72
72
  conflicts: ConflictInfo[];
73
73
  totalRequirements: number;
74
74
  }
75
+ /**
76
+ * Outcome of run-marker handling for one pushed file (IMPORT-2/3).
77
+ * Mirrors the server's ImportStatus.
78
+ */
79
+ export type ImportStatus = "imported" | "invalid_marker" | "unsupported_version" | "already_used" | "ignored_update" | "none";
80
+ /** Base URL of the web app, where synced documents are reviewed. */
81
+ export declare const WEB_APP_URL = "https://app.dotrequirements.io";
75
82
  /**
76
83
  * Result of the execute phase.
77
84
  */
@@ -82,6 +89,17 @@ export interface PushResult {
82
89
  fileName: string;
83
90
  error: string;
84
91
  }>;
92
+ /** IMPORT-3: marker failures that must be surfaced to the user, per file */
93
+ importWarnings: Array<{
94
+ fileName: string;
95
+ status: Extract<ImportStatus, "invalid_marker" | "unsupported_version" | "already_used">;
96
+ }>;
97
+ /** SYNC-LAND-1: each successfully synced document, with its web URL */
98
+ synced: Array<{
99
+ fileName: string;
100
+ documentId: string;
101
+ url: string;
102
+ }>;
85
103
  }
86
104
  /**
87
105
  * Extract markdown content from a file, stripping YAML frontmatter.
package/dist/push/core.js CHANGED
@@ -14,6 +14,8 @@ import * as path from "node:path";
14
14
  import { ConvexHttpClient } from "convex/browser";
15
15
  import { api } from "../convex.js";
16
16
  import { buildRequirementsFile, getAllRequirements, parseRequirementKey, parseRequirementsFromFile, } from "../schema/index.js";
17
+ /** Base URL of the web app, where synced documents are reviewed. */
18
+ export const WEB_APP_URL = "https://app.dotrequirements.io";
17
19
  // ============================================================================
18
20
  // Parsing
19
21
  // ============================================================================
@@ -170,13 +172,15 @@ export async function executePush(dryRunResult, credentials) {
170
172
  let created = 0;
171
173
  let updated = 0;
172
174
  const errors = [];
175
+ const importWarnings = [];
176
+ const synced = [];
173
177
  for (const { file, result } of pushableFiles) {
174
178
  const doc = file.metadata.document;
175
179
  const fileName = path.basename(file.filePath);
176
180
  // For not_found, clear the ID so we create a new document
177
181
  const effectiveDocId = result.action === "not_found" ? undefined : doc.id;
178
182
  try {
179
- const pushResult = (await client.mutation(api.documents.saveWithRequirements.saveWithRequirements, {
183
+ const rawResult = (await client.mutation(api.documents.saveWithRequirements.saveWithRequirements, {
180
184
  projectAuth: {
181
185
  projectSlug: credentials.projectId,
182
186
  projectSecret: credentials.projectSecret,
@@ -186,8 +190,21 @@ export async function executePush(dryRunResult, credentials) {
186
190
  title: doc.title,
187
191
  markdownContent: file.markdownContent,
188
192
  defaultPrefix: doc.defaultPrefix,
193
+ // IMPORT-2: forward the CTS run marker when the file is stamped
194
+ runMarker: file.metadata.ctsRun,
189
195
  dryRun: false,
190
196
  }));
197
+ // The server returns a structured result iff we sent a runMarker
198
+ const pushResult = typeof rawResult === "string" ? rawResult : rawResult.documentId;
199
+ if (typeof rawResult !== "string") {
200
+ const status = rawResult.importStatus;
201
+ if (status === "invalid_marker" ||
202
+ status === "unsupported_version" ||
203
+ status === "already_used") {
204
+ // IMPORT-3.2/3.3: loud, never fatal
205
+ importWarnings.push({ fileName, status });
206
+ }
207
+ }
191
208
  const isCreate = result.action === "create" || result.action === "not_found";
192
209
  if (isCreate) {
193
210
  // New document - write ID back to file
@@ -206,11 +223,61 @@ export async function executePush(dryRunResult, credentials) {
206
223
  fs.writeFileSync(file.filePath, updatedContent, "utf-8");
207
224
  updated++;
208
225
  }
226
+ // SYNC-LAND-1: every synced document gets its web URL in the result
227
+ synced.push({
228
+ fileName,
229
+ documentId: pushResult,
230
+ url: `${WEB_APP_URL}/documents/${pushResult}`,
231
+ });
209
232
  }
210
233
  catch (err) {
211
- errors.push({ fileName, error: err.message });
234
+ errors.push({ fileName, error: errorDisplayMessage(err) });
235
+ }
236
+ }
237
+ return { created, updated, errors, importWarnings, synced };
238
+ }
239
+ /**
240
+ * Servers throw ConvexError({kind, message}) for expected failures (e.g.
241
+ * LIMITS-4.2's limit error); the client-side Error message embeds that data
242
+ * as JSON. Surface the human-readable message it carries instead of the blob.
243
+ */
244
+ function errorDisplayMessage(err) {
245
+ const data = err.data;
246
+ const fromData = humanMessage(data);
247
+ if (fromData)
248
+ return fromData;
249
+ const message = err instanceof Error ? err.message : String(err);
250
+ // ConvexError messages may embed the data JSON directly or after a
251
+ // "ConvexError:" prefix — try the trailing {...} chunk
252
+ const jsonStart = message.indexOf("{");
253
+ if (jsonStart !== -1) {
254
+ try {
255
+ const parsed = JSON.parse(message.slice(jsonStart));
256
+ const fromMessage = humanMessage(parsed);
257
+ if (fromMessage)
258
+ return fromMessage;
259
+ }
260
+ catch {
261
+ // fall through to the raw message
262
+ }
263
+ }
264
+ return message;
265
+ }
266
+ function humanMessage(data) {
267
+ if (typeof data === "string") {
268
+ try {
269
+ return humanMessage(JSON.parse(data));
212
270
  }
271
+ catch {
272
+ return undefined;
273
+ }
274
+ }
275
+ if (data &&
276
+ typeof data === "object" &&
277
+ "message" in data &&
278
+ typeof data.message === "string") {
279
+ return data.message;
213
280
  }
214
- return { created, updated, errors };
281
+ return undefined;
215
282
  }
216
283
  //# sourceMappingURL=core.js.map
@@ -4,5 +4,5 @@
4
4
  * Shared push logic for syncing local requirements files to the cloud.
5
5
  * Used by both CLI push command and MCP push_requirements tool.
6
6
  */
7
- export { type CloudDocumentMetadata, type ConflictInfo, type DryRunResult, type DryRunResultItem, dryRunPush, executePush, extractMarkdownContent, type FileWithDryRun, type ParsedFile, type PushCredentials, type PushResult, parseFilesForPush, } from "./core.js";
7
+ export { type CloudDocumentMetadata, type ConflictInfo, type DryRunResult, type DryRunResultItem, dryRunPush, executePush, extractMarkdownContent, type FileWithDryRun, type ParsedFile, type PushCredentials, type PushResult, parseFilesForPush, WEB_APP_URL, } from "./core.js";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -6,5 +6,5 @@
6
6
  */
7
7
  export { dryRunPush, executePush,
8
8
  // Functions
9
- extractMarkdownContent, parseFilesForPush, } from "./core.js";
9
+ extractMarkdownContent, parseFilesForPush, WEB_APP_URL, } from "./core.js";
10
10
  //# sourceMappingURL=index.js.map
@@ -4,7 +4,7 @@
4
4
  * This module contains pure parsing functions with no Node.js dependencies,
5
5
  * making it safe to import in Convex runtime or browser environments.
6
6
  */
7
- import { ValidationError, } from "./schemas.js";
7
+ import { ValidationError, validateKey, } from "./schemas.js";
8
8
  /**
9
9
  * Default delimiter for requirements.
10
10
  * Can be overridden for organization-specific preferences.
@@ -186,6 +186,10 @@ export function extractRequirementBlocks(body) {
186
186
  throw new ValidationError(`Invalid requirement block format - first line must be "KEY: content"`, blockContent.substring(0, 50));
187
187
  }
188
188
  const key = firstLineMatch[1];
189
+ // SYNC-KEY-1: reject malformed keys at parse time so push/validate fail
190
+ // locally instead of erroring server-side. Validation only — the original
191
+ // key is preserved (normalization happens downstream).
192
+ validateKey(key);
189
193
  blocks.push({ key, blockContent: blockContent.trim() });
190
194
  match = blockRegex.exec(body);
191
195
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import * as fs from "node:fs";
5
5
  import YAML from "yaml";
6
- import { ValidationError, validateMetadata, } from "./schemas.js";
6
+ import { ValidationError, validateKey, validateMetadata, } from "./schemas.js";
7
7
  const DELIMITER_PATTERN = "(?:→|->)"; // Non-capturing group for both Unicode and ASCII
8
8
  /**
9
9
  * Parse a criterion line in "position. Label → content" format.
@@ -198,6 +198,10 @@ function extractRequirementBlocks(body) {
198
198
  throw new ValidationError(`Invalid requirement block format - first line must be "KEY: content"`, blockContent.substring(0, 50));
199
199
  }
200
200
  const key = firstLineMatch[1];
201
+ // SYNC-KEY-1: reject malformed keys at parse time so push/validate fail
202
+ // locally instead of erroring server-side. Validation only — the original
203
+ // key is preserved (normalization happens downstream).
204
+ validateKey(key);
201
205
  // Try to find a heading immediately before this block for the title
202
206
  // Look backwards from the match position to find the nearest heading
203
207
  const textBeforeBlock = body.substring(0, match.index);