@popoverai/dotrequirements 0.26.1 → 0.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/codebase-to-spec/present.js +4 -5
  2. package/dist/codebase-to-spec/validate.js +3 -2
  3. package/dist/commands/acceptance-test.js +4 -2
  4. package/dist/commands/ai-setup.js +97 -59
  5. package/dist/commands/get.js +6 -2
  6. package/dist/commands/init.js +7 -5
  7. package/dist/commands/link-resolution.d.ts +3 -1
  8. package/dist/commands/link-resolution.js +4 -2
  9. package/dist/commands/pull.js +36 -3
  10. package/dist/commands/push.js +54 -16
  11. package/dist/commands/report.js +18 -3
  12. package/dist/commands/review-test.js +16 -8
  13. package/dist/commands/tests-for.js +13 -13
  14. package/dist/commands/validate.js +14 -14
  15. package/dist/harness/cache.d.ts +19 -3
  16. package/dist/harness/cache.js +38 -12
  17. package/dist/harness/finalize.js +33 -1
  18. package/dist/harness/index.js +16 -9
  19. package/dist/harness/requirementsLoader.js +12 -0
  20. package/dist/harness/tracking.d.ts +17 -2
  21. package/dist/harness/tracking.js +83 -9
  22. package/dist/mcp/handlers/authoring.js +13 -4
  23. package/dist/mcp/handlers/get.js +7 -3
  24. package/dist/mcp/handlers/push.js +59 -13
  25. package/dist/mcp/handlers/review.d.ts +1 -0
  26. package/dist/mcp/handlers/review.js +58 -15
  27. package/dist/mcp/handlers/test-mapping.js +47 -12
  28. package/dist/mcp/handlers/types.d.ts +14 -0
  29. package/dist/mcp/handlers/types.js +27 -0
  30. package/dist/mcp/index.js +4 -0
  31. package/dist/push/core.d.ts +50 -0
  32. package/dist/push/core.js +149 -11
  33. package/dist/push/index.d.ts +1 -1
  34. package/dist/push/index.js +1 -1
  35. package/dist/requirements/cloud-coverage.d.ts +12 -2
  36. package/dist/requirements/cloud-coverage.js +30 -3
  37. package/dist/requirements/grep.d.ts +7 -2
  38. package/dist/requirements/grep.js +75 -47
  39. package/dist/schema/builder.d.ts +1 -1
  40. package/dist/schema/builder.js +13 -0
  41. package/dist/schema/conversions.d.ts +7 -2
  42. package/dist/schema/conversions.js +13 -4
  43. package/dist/schema/parser-core.d.ts +28 -0
  44. package/dist/schema/parser-core.js +80 -9
  45. package/dist/schema/parser.d.ts +8 -26
  46. package/dist/schema/parser.js +23 -251
  47. package/dist/schema/resolver.js +18 -8
  48. package/dist/utils/env.js +17 -1
  49. package/dist/utils/oauth-flow.js +8 -0
  50. package/dist/utils/project-settings.d.ts +4 -0
  51. package/dist/utils/project-settings.js +14 -1
  52. package/package.json +1 -1
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { existsSync } from "node:fs";
8
8
  import { basename, resolve } from "node:path";
9
- import { dryRunPush, executePush, parseFilesForPush, } from "../../push/index.js";
9
+ import { dryRunPush, executePush, parseFilesForPushIndividually, } from "../../push/index.js";
10
10
  import { findRequirementsFiles } from "../../requirements/index.js";
11
11
  import { CONVEX_URL } from "../convexClient.js";
12
12
  import { errorResponse, textResponse } from "./types.js";
@@ -46,24 +46,60 @@ export async function handlePushRequirements(args, context) {
46
46
  return textResponse("No *.requirements.md files found. Nothing to push.");
47
47
  }
48
48
  }
49
- // Parse files
50
- const { parsedFiles, totalRequirements } = parseFilesForPush(filesToPush);
49
+ // Parse files. #45/SYNC-FAIL-4: one invalid file does not abort the whole
50
+ // push — its failure is surfaced in the preview's "Skipped - Invalid"
51
+ // section while valid files still push. Same helper as the CLI command.
52
+ const { parsedFiles, totalRequirements, parseFailures } = parseFilesForPushIndividually(filesToPush);
51
53
  // Build credentials
52
54
  const credentials = {
53
55
  projectId: project.projectId,
54
56
  projectSecret: project.projectSecret,
55
57
  convexUrl: CONVEX_URL,
56
58
  };
57
- // Run dry run (always, even when confirmed - ensures fresh state)
58
- const dryRunResult = await dryRunPush(parsedFiles, credentials);
59
+ // Run dry run (always, even when confirmed - ensures fresh state).
60
+ // SYNC-FAIL-3: two files claiming the same document ID abort the push
61
+ // before any cloud write — surface that as a clean error result naming
62
+ // both files rather than an unhandled throw.
63
+ let dryRunResult;
64
+ try {
65
+ dryRunResult = await dryRunPush(parsedFiles, credentials);
66
+ }
67
+ catch (error) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ if (message.includes("claim the same document ID")) {
70
+ return {
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: `✗ Push aborted: ${message}`,
75
+ },
76
+ ],
77
+ isError: true,
78
+ };
79
+ }
80
+ throw error;
81
+ }
82
+ // #45: Merge dry-run invalids (missing document section, cloud validation)
83
+ // with local parse failures so both are surfaced together, each named by
84
+ // filename.
85
+ const allInvalid = [
86
+ ...dryRunResult.invalid.map(({ file, result }) => ({
87
+ fileName: basename(file.filePath),
88
+ error: result.error ?? "invalid",
89
+ })),
90
+ ...parseFailures.map(({ filePath, error }) => ({
91
+ fileName: basename(filePath),
92
+ error,
93
+ })),
94
+ ];
59
95
  // Check if there's anything to push
60
96
  const pushableCount = dryRunResult.updates.length +
61
97
  dryRunResult.creates.length +
62
98
  dryRunResult.notFound.length;
63
- if (pushableCount === 0 && dryRunResult.invalid.length > 0) {
99
+ if (pushableCount === 0 && allInvalid.length > 0) {
64
100
  // Only invalid files
65
- const invalidList = dryRunResult.invalid
66
- .map(({ file, result }) => `- ${basename(file.filePath)}: ${result.error}`)
101
+ const invalidList = allInvalid
102
+ .map(({ fileName, error }) => `- ${fileName}: ${error}`)
67
103
  .join("\n");
68
104
  return {
69
105
  content: [
@@ -106,11 +142,10 @@ export async function handlePushRequirements(args, context) {
106
142
  }
107
143
  summary += "\n";
108
144
  }
109
- if (dryRunResult.invalid.length > 0) {
110
- summary += `## Skipped - Invalid (${dryRunResult.invalid.length})\n`;
111
- for (const { file, result } of dryRunResult.invalid) {
112
- const fileName = basename(file.filePath);
113
- summary += `- ✗ ${fileName}: ${result.error}\n`;
145
+ if (allInvalid.length > 0) {
146
+ summary += `## Skipped - Invalid (${allInvalid.length})\n`;
147
+ for (const { fileName, error } of allInvalid) {
148
+ summary += `- ✗ ${fileName}: ${error}\n`;
114
149
  }
115
150
  summary += "\n";
116
151
  }
@@ -158,6 +193,17 @@ export async function handlePushRequirements(args, context) {
158
193
  output += `- ${fileName}: ${error}\n`;
159
194
  }
160
195
  }
196
+ // SYNC-FAIL-2.1: cloud save succeeded but the local file couldn't be
197
+ // updated — name the file, say the cloud is fine, and give the recovery
198
+ // step so a retry doesn't mint a duplicate document.
199
+ if (result.writeBackWarnings?.length > 0) {
200
+ output += `\n**Warnings:**\n`;
201
+ for (const warning of result.writeBackWarnings) {
202
+ output +=
203
+ `- ⚠ ${warning.fileName}: saved to the cloud, but the local file could not be updated (${warning.error}). ` +
204
+ `To avoid creating a duplicate, add "id: ${warning.documentId}" under "document:" in the frontmatter of ${warning.filePath}, then push again.\n`;
205
+ }
206
+ }
161
207
  // SYNC-LAND-1: where each synced document lives in the web app
162
208
  if (result.synced.length > 0) {
163
209
  output += `\n**Review in dot•requirements:**\n`;
@@ -13,6 +13,7 @@ export interface StyleCheckArgs {
13
13
  filePath: string;
14
14
  requirementKeys?: string[];
15
15
  model?: string;
16
+ projectId?: string;
16
17
  }
17
18
  /**
18
19
  * Arguments for review_test tool
@@ -6,11 +6,12 @@
6
6
  * - review_test: Comprehensively review tests for semantic correctness
7
7
  */
8
8
  import { existsSync, readFileSync } from "node:fs";
9
- import { dirname, resolve } from "node:path";
9
+ import { dirname } from "node:path";
10
10
  import { fetchReviewTestFeedback, fetchStyleCheckFeedback, } from "../../requirements/cloud-ai.js";
11
+ import { findRequirementsInFile } from "../../requirements/grep.js";
11
12
  import { filterRequirementsByKeys, formatRequirementTree, getRequirementTree, } from "../../requirements/index.js";
12
13
  import { discoverProjects, } from "../../utils/project-discovery.js";
13
- import { errorResponse, textResponse } from "./types.js";
14
+ import { errorResponse, resolveProjectFilePath, textResponse, } from "./types.js";
14
15
  const STYLE_CHECK_GUIDANCE = `
15
16
 
16
17
  ---
@@ -32,9 +33,20 @@ const STYLE_CHECK_GUIDANCE = `
32
33
  * - MCP-REVIEW-1.3: When cloud credentials are unavailable, an error explains how to authenticate
33
34
  */
34
35
  export async function handleStyleCheck(args, context, options) {
35
- const { filePath, requirementKeys, model } = args;
36
+ const { filePath, requirementKeys, model, projectId } = args;
36
37
  const { useEnvAuth = false, apiBaseUrl = "https://app.dotrequirements.io" } = options ?? {};
37
- const fullPath = resolve(context.workspaceRoot, filePath);
38
+ // #49: Resolve against the discovered project.path (matching push) with a
39
+ // workspaceRoot fallback, so a relative path that push accepts also works
40
+ // here in PROJ_*-based setups. Discovery may throw without credentials — that
41
+ // is fine, we then fall back to workspaceRoot.
42
+ let styleProjectPath;
43
+ try {
44
+ styleProjectPath = (await context.getProjectFromDiscovery(projectId)).path;
45
+ }
46
+ catch {
47
+ styleProjectPath = undefined;
48
+ }
49
+ const fullPath = resolveProjectFilePath(filePath, styleProjectPath, context.workspaceRoot);
38
50
  if (!existsSync(fullPath)) {
39
51
  return errorResponse(`File not found: ${filePath}`);
40
52
  }
@@ -73,7 +85,14 @@ export async function handleStyleCheck(args, context, options) {
73
85
  const fileDir = dirname(fullPath);
74
86
  let project;
75
87
  try {
76
- if (useEnvAuth) {
88
+ if (projectId) {
89
+ // #48: PROJ-CONTEXT-6 — an explicit projectId disambiguates a
90
+ // multi-project workspace. Thread it straight through to discovery
91
+ // (matching review_test/push/report) instead of the file-directory
92
+ // auto-detect path, which cannot choose among multiple projects.
93
+ project = await context.getProjectFromDiscovery(projectId);
94
+ }
95
+ else if (useEnvAuth) {
77
96
  // Using env auth - go straight to context
78
97
  project = await context.getProjectFromDiscovery();
79
98
  }
@@ -88,8 +107,14 @@ export async function handleStyleCheck(args, context, options) {
88
107
  project = result.project;
89
108
  }
90
109
  else {
91
- // Multiple projects - can't auto-detect which one to use
92
- throw new Error("Multiple projects found - cannot auto-detect for this file");
110
+ // #48: Multiple projects — can't auto-detect. The remedy is to pass
111
+ // projectId (not `dotrequirements link`, which is already satisfied);
112
+ // list the available projects so the caller can choose.
113
+ const available = result.projects
114
+ .map((p) => p.projectId)
115
+ .filter(Boolean)
116
+ .join(", ");
117
+ return errorResponse(`Multiple projects found. Specify the \`projectId\` parameter to choose one${available ? ` (available: ${available})` : ""}.`);
93
118
  }
94
119
  }
95
120
  }
@@ -130,7 +155,17 @@ export async function handleStyleCheck(args, context, options) {
130
155
  export async function handleReviewTest(args, context, options) {
131
156
  const { testFilePath, projectId } = args;
132
157
  const { apiBaseUrl = "https://app.dotrequirements.io" } = options ?? {};
133
- const fullPath = resolve(context.workspaceRoot, testFilePath);
158
+ // #49: Resolve against the discovered project.path (matching push) with a
159
+ // workspaceRoot fallback. Discovery may throw without credentials — fall back
160
+ // to workspaceRoot; the credential error is reported later.
161
+ let reviewProjectPath;
162
+ try {
163
+ reviewProjectPath = (await context.getProjectFromDiscovery(projectId)).path;
164
+ }
165
+ catch {
166
+ reviewProjectPath = undefined;
167
+ }
168
+ const fullPath = resolveProjectFilePath(testFilePath, reviewProjectPath, context.workspaceRoot);
134
169
  if (!existsSync(fullPath)) {
135
170
  return errorResponse(`File not found: ${testFilePath}`);
136
171
  }
@@ -141,12 +176,12 @@ export async function handleReviewTest(args, context, options) {
141
176
  }
142
177
  // Read test file contents
143
178
  const testFileContents = readFileSync(fullPath, "utf-8");
144
- // Extract requirement IDs from the test file using regex
145
- const requirementIdMatches = testFileContents.matchAll(/requirement\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g);
146
- const requirementIds = new Set();
147
- for (const match of requirementIdMatches) {
148
- requirementIds.add(match[1]);
149
- }
179
+ // #43: Extract requirement IDs via the shared AST-based extractor so that
180
+ // multi-arg calls (`requirement("A", "B")`) and options-bearing calls
181
+ // (`requirement("A", { ... })`) are captured — the old single-string regex
182
+ // matched neither.
183
+ const references = await findRequirementsInFile(fullPath);
184
+ const requirementIds = new Set(references.map((ref) => ref.requirementId));
150
185
  // MCP-REVIEW-2.5: Get project credentials
151
186
  let project;
152
187
  try {
@@ -159,9 +194,17 @@ export async function handleReviewTest(args, context, options) {
159
194
  const allRequirements = await context.getRequirements(projectId);
160
195
  // Group requirement IDs by their root to avoid sending duplicate trees
161
196
  // e.g., NAV-3.0, NAV-3.1, NAV-3.2 all belong to root NAV-3
197
+ //
198
+ // #42: A reference may be a bare root (`AUTH-1`), an index child (`AUTH-1.0`),
199
+ // or a label path (`AUTH-1.given`). Normalize each reference to its root id
200
+ // (the substring before the first dot) before matching — the same approach
201
+ // getReferencedRequirementIds uses — so label-path refs resolve instead of
202
+ // being reported as non-existent.
162
203
  const rootToTestedIds = new Map();
163
204
  for (const reqId of requirementIds) {
164
- const req = allRequirements.find((r) => r.id === reqId || r.rootId === reqId);
205
+ const dotIndex = reqId.indexOf(".");
206
+ const refRootId = dotIndex > 0 ? reqId.substring(0, dotIndex) : reqId;
207
+ const req = allRequirements.find((r) => r.rootId === refRootId);
165
208
  if (req) {
166
209
  if (!rootToTestedIds.has(req.rootId)) {
167
210
  rootToTestedIds.set(req.rootId, new Set());
@@ -7,7 +7,7 @@ import { existsSync } from "node:fs";
7
7
  import { relative, resolve } from "node:path";
8
8
  import { findAllTestReferences, findRequirementsInFile, } from "../../requirements/grep.js";
9
9
  import { getRequirementById } from "../../requirements/index.js";
10
- import { errorResponse, textResponse } from "./types.js";
10
+ import { errorResponse, resolveProjectFilePath, textResponse, } from "./types.js";
11
11
  /**
12
12
  * Handler for get_requirements_by_test tool
13
13
  *
@@ -18,14 +18,26 @@ import { errorResponse, textResponse } from "./types.js";
18
18
  */
19
19
  export async function handleGetRequirementsByTest(args, context) {
20
20
  const { testFile, projectId } = args;
21
+ // #49: Resolve against the discovered project.path (matching push) with a
22
+ // workspaceRoot fallback, so a relative path works in PROJ_*-based setups.
23
+ let projectPath;
24
+ try {
25
+ projectPath = (await context.getProjectFromDiscovery(projectId)).path;
26
+ }
27
+ catch {
28
+ projectPath = undefined;
29
+ }
21
30
  // MCP-MAP-1.2: Check if file exists before processing
22
- const fullPath = resolve(context.workspaceRoot, testFile);
31
+ const fullPath = resolveProjectFilePath(testFile, projectPath, context.workspaceRoot);
23
32
  if (!existsSync(fullPath)) {
24
33
  return errorResponse(`Test file not found: ${testFile}`);
25
34
  }
26
35
  const requirements = await context.getRequirements(projectId);
27
- // MCP-MAP-1.0: Find all requirement references in the test file
28
- const refs = await findRequirementsInFile(testFile);
36
+ // MCP-MAP-1.0: Find all requirement references in the test file.
37
+ // #44: Pass the already-resolved absolute path — findRequirementsInFile
38
+ // resolves relative paths against process.cwd(), which is not the workspace
39
+ // root when the MCP server runs from a different directory.
40
+ const refs = await findRequirementsInFile(fullPath);
29
41
  if (refs.length === 0) {
30
42
  return textResponse(`No requirement references found in "${testFile}"`);
31
43
  }
@@ -50,7 +62,18 @@ export async function handleGetRequirementsByTest(args, context) {
50
62
  export async function handleGetTestsByRequirement(args, context) {
51
63
  const { requirementsFile, projectId } = args;
52
64
  const { workspaceRoot } = context;
53
- const fullPath = resolve(workspaceRoot, requirementsFile);
65
+ // #49: Resolve against the discovered project.path (matching push) with a
66
+ // workspaceRoot fallback, so a relative path works in PROJ_*-based setups.
67
+ // The same base root anchors sourceFile comparison, test scanning, and the
68
+ // relative paths in the output.
69
+ let projectPath;
70
+ try {
71
+ projectPath = (await context.getProjectFromDiscovery(projectId)).path;
72
+ }
73
+ catch {
74
+ projectPath = undefined;
75
+ }
76
+ const fullPath = resolveProjectFilePath(requirementsFile, projectPath, workspaceRoot);
54
77
  // Verify file exists
55
78
  if (!existsSync(fullPath)) {
56
79
  return errorResponse(`Requirements file not found: ${requirementsFile}`);
@@ -59,23 +82,35 @@ export async function handleGetTestsByRequirement(args, context) {
59
82
  if (!requirementsFile.endsWith(".requirements.md")) {
60
83
  return errorResponse(`File must be a requirements file: *.requirements.md`);
61
84
  }
85
+ // Anchor all subsequent path work to the root that actually contains the
86
+ // requirements file, so sourceFile comparison and test discovery agree.
87
+ const baseRoot = projectPath ?? workspaceRoot;
62
88
  // Load all requirements from this file
63
89
  const requirements = await context.getRequirements(projectId);
64
- const fileRequirements = requirements.filter((req) => resolve(workspaceRoot, req.sourceFile) === fullPath);
90
+ const fileRequirements = requirements.filter((req) => resolve(baseRoot, req.sourceFile) === fullPath);
65
91
  if (fileRequirements.length === 0) {
66
92
  return textResponse(`No requirements found in "${requirementsFile}"`);
67
93
  }
68
94
  // Get unique root requirement IDs from this file
69
95
  const rootIds = new Set(fileRequirements.map((req) => req.rootId));
70
96
  // Find all test references in the workspace
71
- const allTestRefs = await findAllTestReferences(workspaceRoot);
72
- // Build coverage map: requirement ID -> list of test references
97
+ const allTestRefs = await findAllTestReferences(baseRoot);
98
+ // Build coverage map keyed by ROOT id. A reference may be a bare root
99
+ // (`AUTH-1`), an index child (`AUTH-1.0`), or a label path (`AUTH-1.given`).
100
+ // #47: Normalize each reference to its root id (the substring before the
101
+ // first dot) before keying — the same normalization as the tests-for fix —
102
+ // so label-path refs match numeric requirement ids instead of rendering as
103
+ // "(requirement not found)".
73
104
  const coverageMap = new Map();
74
105
  for (const ref of allTestRefs) {
75
- if (!coverageMap.has(ref.requirementId)) {
76
- coverageMap.set(ref.requirementId, []);
106
+ const dotIndex = ref.requirementId.indexOf(".");
107
+ const refRootId = dotIndex > 0
108
+ ? ref.requirementId.substring(0, dotIndex)
109
+ : ref.requirementId;
110
+ if (!coverageMap.has(refRootId)) {
111
+ coverageMap.set(refRootId, []);
77
112
  }
78
- coverageMap.get(ref.requirementId).push(ref);
113
+ coverageMap.get(refRootId).push(ref);
79
114
  }
80
115
  // MCP-MAP-2.0: Categorize requirements as covered or not covered
81
116
  const covered = [];
@@ -114,7 +149,7 @@ export async function handleGetTestsByRequirement(args, context) {
114
149
  }
115
150
  output += `- **${id}**: ${tests.length} test${tests.length === 1 ? "" : "s"}\n`;
116
151
  for (const [file, lines] of testsByFile.entries()) {
117
- const relPath = relative(workspaceRoot, file);
152
+ const relPath = relative(baseRoot, file);
118
153
  const lineList = lines.sort((a, b) => a - b).join(", ");
119
154
  output += ` - \`${relPath}\` (lines ${lineList})\n`;
120
155
  }
@@ -64,6 +64,20 @@ export interface HandlerContext {
64
64
  * The args parameter contains tool-specific arguments.
65
65
  */
66
66
  export type ToolHandler<TArgs = Record<string, unknown>> = (args: TArgs, context: HandlerContext) => Promise<ToolResponse>;
67
+ /**
68
+ * Resolve a user-supplied relative file path to an absolute path.
69
+ *
70
+ * #49: The MCP server's workspaceRoot is its process cwd, but in PROJ_*-based
71
+ * (Antigravity) setups the discovered project.path is the real project root and
72
+ * differs from cwd. push_requirements resolves against project.path; the other
73
+ * file-taking handlers historically resolved against workspaceRoot, so the same
74
+ * relative path worked for push but not for validate/style/review/mapping.
75
+ *
76
+ * This resolves against project.path first (matching push) and falls back to
77
+ * workspaceRoot when the file is not present there, so both layouts work.
78
+ * Absolute paths are returned unchanged.
79
+ */
80
+ export declare function resolveProjectFilePath(filePath: string, projectPath: string | undefined, workspaceRoot: string): string;
67
81
  /**
68
82
  * Helper to create a text response
69
83
  */
@@ -5,6 +5,33 @@
5
5
  * and returns an MCP tool response. This enables unit testing without
6
6
  * the full MCP server infrastructure.
7
7
  */
8
+ import { existsSync } from "node:fs";
9
+ import { isAbsolute, resolve } from "node:path";
10
+ /**
11
+ * Resolve a user-supplied relative file path to an absolute path.
12
+ *
13
+ * #49: The MCP server's workspaceRoot is its process cwd, but in PROJ_*-based
14
+ * (Antigravity) setups the discovered project.path is the real project root and
15
+ * differs from cwd. push_requirements resolves against project.path; the other
16
+ * file-taking handlers historically resolved against workspaceRoot, so the same
17
+ * relative path worked for push but not for validate/style/review/mapping.
18
+ *
19
+ * This resolves against project.path first (matching push) and falls back to
20
+ * workspaceRoot when the file is not present there, so both layouts work.
21
+ * Absolute paths are returned unchanged.
22
+ */
23
+ export function resolveProjectFilePath(filePath, projectPath, workspaceRoot) {
24
+ if (isAbsolute(filePath)) {
25
+ return filePath;
26
+ }
27
+ if (projectPath) {
28
+ const fromProject = resolve(projectPath, filePath);
29
+ if (existsSync(fromProject)) {
30
+ return fromProject;
31
+ }
32
+ }
33
+ return resolve(workspaceRoot, filePath);
34
+ }
8
35
  /**
9
36
  * Helper to create a text response
10
37
  */
package/dist/mcp/index.js CHANGED
@@ -301,6 +301,10 @@ const tools = [
301
301
  type: "string",
302
302
  description: 'Optional: AI model to use for style checking (default: "anthropic/claude-haiku-4.5"). Supported models: "anthropic/claude-haiku-4.5", "google/gemini-3-flash"',
303
303
  },
304
+ projectId: {
305
+ type: "string",
306
+ description: "Optional: Specify which project to use (required when multiple projects exist)",
307
+ },
304
308
  },
305
309
  required: ["filePath"],
306
310
  },
@@ -26,6 +26,14 @@ export interface ParsedFile {
26
26
  markdownContent: string;
27
27
  /** Requirement count for display */
28
28
  requirementCount: number;
29
+ /**
30
+ * DOC-HEADER-14: the frontmatter exactly as the user wrote it, including
31
+ * keys the schema doesn't recognize — merged back on the post-push rewrite
32
+ * so those keys survive.
33
+ */
34
+ rawFrontmatter?: Record<string, unknown>;
35
+ /** DOC-HEADER-11.4: true when defaultPrefix was inferred from the first requirement rather than read from frontmatter */
36
+ inferredDefaultPrefix?: boolean;
29
37
  }
30
38
  /**
31
39
  * Cloud document metadata for conflict detection.
@@ -85,8 +93,10 @@ export declare const WEB_APP_URL = "https://app.dotrequirements.io";
85
93
  export interface PushResult {
86
94
  created: number;
87
95
  updated: number;
96
+ /** filePath disambiguates when two pushed files share a basename */
88
97
  errors: Array<{
89
98
  fileName: string;
99
+ filePath: string;
90
100
  error: string;
91
101
  }>;
92
102
  /** IMPORT-3: marker failures that must be surfaced to the user, per file */
@@ -100,6 +110,18 @@ export interface PushResult {
100
110
  documentId: string;
101
111
  url: string;
102
112
  }>;
113
+ /**
114
+ * SYNC-FAIL-2: cloud save succeeded but the local file write-back failed.
115
+ * These documents are synced (counted in created/updated and listed in
116
+ * `synced`) — the warning carries what the user needs to re-link the file
117
+ * without creating a duplicate.
118
+ */
119
+ writeBackWarnings: Array<{
120
+ fileName: string;
121
+ filePath: string;
122
+ documentId: string;
123
+ error: string;
124
+ }>;
103
125
  }
104
126
  /**
105
127
  * Extract markdown content from a file, stripping YAML frontmatter.
@@ -107,11 +129,39 @@ export interface PushResult {
107
129
  export declare function extractMarkdownContent(rawContent: string): string;
108
130
  /**
109
131
  * Parse files for push. Returns parsed files with metadata and content.
132
+ *
133
+ * Throws (via `parseRequirementsFromFile`) if any file is syntactically
134
+ * invalid. Callers that need one bad file not to abort the batch should parse
135
+ * files individually and collect failures (see the MCP push handler, #45).
110
136
  */
111
137
  export declare function parseFilesForPush(filePaths: string[]): {
112
138
  parsedFiles: ParsedFile[];
113
139
  totalRequirements: number;
114
140
  };
141
+ export interface ParseFailure {
142
+ filePath: string;
143
+ error: string;
144
+ }
145
+ /**
146
+ * SYNC-FAIL-4.0: parse files one at a time so a single invalid file cannot
147
+ * abort the batch — its failure is collected per file while the valid files
148
+ * still parse. Shared by the CLI push command and the MCP push handler (#45)
149
+ * so their isolation semantics cannot drift.
150
+ *
151
+ * Note: cross-file checks that need the whole batch (e.g. SYNC-FAIL-3
152
+ * duplicate document IDs) are enforced downstream in dryRunPush, not here.
153
+ */
154
+ export declare function parseFilesForPushIndividually(filePaths: string[]): {
155
+ parsedFiles: ParsedFile[];
156
+ totalRequirements: number;
157
+ parseFailures: ParseFailure[];
158
+ };
159
+ /**
160
+ * SYNC-FAIL-3: two ParsedFiles carrying the same document.id would both push
161
+ * as updates to one cloud document — last writer wins and the first spec is
162
+ * silently destroyed. Abort instead, naming both files and the shared id.
163
+ */
164
+ export declare function assertNoDuplicateDocumentIds(parsedFiles: ParsedFile[]): void;
115
165
  /**
116
166
  * Execute dry run phase: validate all files against Convex and detect conflicts.
117
167
  */