@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.
- package/dist/codebase-to-spec/present.js +4 -5
- package/dist/codebase-to-spec/validate.js +3 -2
- package/dist/commands/acceptance-test.js +4 -2
- package/dist/commands/ai-setup.js +97 -59
- package/dist/commands/get.js +6 -2
- package/dist/commands/init.js +7 -5
- package/dist/commands/link-resolution.d.ts +3 -1
- package/dist/commands/link-resolution.js +4 -2
- package/dist/commands/pull.js +36 -3
- package/dist/commands/push.js +54 -16
- package/dist/commands/report.js +18 -3
- package/dist/commands/review-test.js +16 -8
- package/dist/commands/tests-for.js +13 -13
- package/dist/commands/validate.js +14 -14
- package/dist/harness/cache.d.ts +19 -3
- package/dist/harness/cache.js +38 -12
- package/dist/harness/finalize.js +33 -1
- package/dist/harness/index.js +16 -9
- package/dist/harness/requirementsLoader.js +12 -0
- package/dist/harness/tracking.d.ts +17 -2
- package/dist/harness/tracking.js +83 -9
- package/dist/mcp/handlers/authoring.js +13 -4
- package/dist/mcp/handlers/get.js +7 -3
- package/dist/mcp/handlers/push.js +59 -13
- package/dist/mcp/handlers/review.d.ts +1 -0
- package/dist/mcp/handlers/review.js +58 -15
- package/dist/mcp/handlers/test-mapping.js +47 -12
- package/dist/mcp/handlers/types.d.ts +14 -0
- package/dist/mcp/handlers/types.js +27 -0
- package/dist/mcp/index.js +4 -0
- package/dist/push/core.d.ts +50 -0
- package/dist/push/core.js +149 -11
- package/dist/push/index.d.ts +1 -1
- package/dist/push/index.js +1 -1
- package/dist/requirements/cloud-coverage.d.ts +12 -2
- package/dist/requirements/cloud-coverage.js +30 -3
- package/dist/requirements/grep.d.ts +7 -2
- package/dist/requirements/grep.js +75 -47
- package/dist/schema/builder.d.ts +1 -1
- package/dist/schema/builder.js +13 -0
- package/dist/schema/conversions.d.ts +7 -2
- package/dist/schema/conversions.js +13 -4
- package/dist/schema/parser-core.d.ts +28 -0
- package/dist/schema/parser-core.js +80 -9
- package/dist/schema/parser.d.ts +8 -26
- package/dist/schema/parser.js +23 -251
- package/dist/schema/resolver.js +18 -8
- package/dist/utils/env.js +17 -1
- package/dist/utils/oauth-flow.js +8 -0
- package/dist/utils/project-settings.d.ts +4 -0
- package/dist/utils/project-settings.js +14 -1
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
99
|
+
if (pushableCount === 0 && allInvalid.length > 0) {
|
|
64
100
|
// Only invalid files
|
|
65
|
-
const invalidList =
|
|
66
|
-
.map(({
|
|
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 (
|
|
110
|
-
summary += `## Skipped - Invalid (${
|
|
111
|
-
for (const {
|
|
112
|
-
|
|
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`;
|
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
72
|
-
// Build coverage map
|
|
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
|
-
|
|
76
|
-
|
|
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(
|
|
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(
|
|
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
|
},
|
package/dist/push/core.d.ts
CHANGED
|
@@ -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
|
*/
|