@popoverai/dotrequirements 0.26.0 → 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/area-name.d.ts +13 -0
- package/dist/codebase-to-spec/area-name.js +18 -0
- package/dist/codebase-to-spec/cache.d.ts +31 -0
- package/dist/codebase-to-spec/cache.js +29 -1
- package/dist/codebase-to-spec/compose.d.ts +7 -4
- package/dist/codebase-to-spec/compose.js +10 -21
- package/dist/codebase-to-spec/dispatch.js +1 -1
- package/dist/codebase-to-spec/present.js +11 -7
- package/dist/codebase-to-spec/prompts/planner-initial.d.ts +3 -2
- package/dist/codebase-to-spec/prompts/planner-initial.js +3 -2
- package/dist/codebase-to-spec/renumber.d.ts +52 -0
- package/dist/codebase-to-spec/renumber.js +105 -0
- package/dist/codebase-to-spec/schemas.d.ts +35 -240
- package/dist/codebase-to-spec/schemas.js +5 -173
- 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/codebase-to-spec/dispatch-editor.js +1 -1
- package/dist/commands/codebase-to-spec/dispatch-spec.js +1 -1
- package/dist/commands/codebase-to-spec/index.js +7 -103
- package/dist/commands/codebase-to-spec/pack.js +8 -1
- package/dist/commands/codebase-to-spec/present-orchestrator.d.ts +3 -1
- package/dist/commands/codebase-to-spec/present-orchestrator.js +11 -4
- 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/templates/skills/codebase-to-spec/SKILL.md +10 -4
- 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
- package/dist/codebase-to-spec/edit-loop.d.ts +0 -54
- package/dist/codebase-to-spec/edit-loop.js +0 -195
- package/dist/codebase-to-spec/editor.d.ts +0 -54
- package/dist/codebase-to-spec/editor.js +0 -74
- package/dist/codebase-to-spec/fan-out.d.ts +0 -63
- package/dist/codebase-to-spec/fan-out.js +0 -215
- package/dist/codebase-to-spec/outline-review-loop.d.ts +0 -51
- package/dist/codebase-to-spec/outline-review-loop.js +0 -187
- package/dist/codebase-to-spec/planner.d.ts +0 -41
- package/dist/codebase-to-spec/planner.js +0 -76
- package/dist/codebase-to-spec/prompts/outline-reviewer.d.ts +0 -12
- package/dist/codebase-to-spec/prompts/outline-reviewer.js +0 -89
- package/dist/codebase-to-spec/slice.d.ts +0 -49
- package/dist/codebase-to-spec/slice.js +0 -111
- package/dist/codebase-to-spec/specifier.d.ts +0 -60
- package/dist/codebase-to-spec/specifier.js +0 -85
- package/dist/codebase-to-spec/summary.d.ts +0 -51
- package/dist/codebase-to-spec/summary.js +0 -183
- package/dist/commands/codebase-to-spec/compose.d.ts +0 -14
- package/dist/commands/codebase-to-spec/compose.js +0 -57
- package/dist/commands/codebase-to-spec/edit-loop.d.ts +0 -16
- package/dist/commands/codebase-to-spec/edit-loop.js +0 -83
- package/dist/commands/codebase-to-spec/fan-out.d.ts +0 -19
- package/dist/commands/codebase-to-spec/fan-out.js +0 -77
- package/dist/commands/codebase-to-spec/plan-loop.d.ts +0 -26
- package/dist/commands/codebase-to-spec/plan-loop.js +0 -105
- package/dist/commands/codebase-to-spec/present.d.ts +0 -26
- package/dist/commands/codebase-to-spec/present.js +0 -97
- package/dist/commands/codebase-to-spec/run.d.ts +0 -20
- package/dist/commands/codebase-to-spec/run.js +0 -86
- package/dist/commands/codebase-to-spec/specify-area.d.ts +0 -18
- package/dist/commands/codebase-to-spec/specify-area.js +0 -82
|
@@ -11,8 +11,21 @@ export interface TrackedRequirement {
|
|
|
11
11
|
fieldsAccessed: string[];
|
|
12
12
|
accessedAt: string[];
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Initialize (or reuse) a test run.
|
|
16
|
+
*
|
|
17
|
+
* HARNESS-REQUIREMENT-6.0: when an explicit projectRoot is provided, the run
|
|
18
|
+
* is keyed to that project's .requirements/ so tracking writes land in the
|
|
19
|
+
* resolving project's cache, not the enclosing project's.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensureTestRun(projectRoot?: string): string;
|
|
15
22
|
export declare function getCallerLocation(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Normalize an absolute path inside the project to a project-relative one
|
|
25
|
+
* (COVERAGE-CONTEXT-2: stable, blame-able from the project root). Paths that
|
|
26
|
+
* are already relative or fall outside the project are returned unchanged.
|
|
27
|
+
*/
|
|
28
|
+
export declare function toProjectRelativePath(filePath: string, projectRoot: string): string;
|
|
16
29
|
/**
|
|
17
30
|
* Track a requirement access.
|
|
18
31
|
*
|
|
@@ -21,8 +34,10 @@ export declare function getCallerLocation(): string;
|
|
|
21
34
|
* HARNESS-REQUIREMENT-3.2: Works correctly when tests run in parallel processes
|
|
22
35
|
*
|
|
23
36
|
* @param reqId - Full requirement path (e.g., 'REQ-123.given' or 'REQ-123.0.1')
|
|
37
|
+
* @param projectRoot - Explicit project root; tracking is written to this
|
|
38
|
+
* project's cache when provided (HARNESS-REQUIREMENT-6.0)
|
|
24
39
|
*/
|
|
25
|
-
export declare function trackRequirement(reqId: string): void;
|
|
40
|
+
export declare function trackRequirement(reqId: string, projectRoot?: string): void;
|
|
26
41
|
/**
|
|
27
42
|
* Save current tracking data.
|
|
28
43
|
*
|
package/dist/harness/tracking.js
CHANGED
|
@@ -7,13 +7,33 @@
|
|
|
7
7
|
* HARNESS-REQUIREMENT-3: Each requirement() call is recorded for coverage reporting
|
|
8
8
|
*/
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
10
11
|
import { appendTrackingEntry, clearTrackingFile, findProjectRoot, findRequirementsDir, getCacheDir, getTestRunId, initTestRunId, } from "./cache.js";
|
|
11
12
|
const trackedRequirements = new Map();
|
|
12
13
|
// Test run tracking
|
|
13
14
|
let testRunId = null;
|
|
14
15
|
let requirementsDirCached = null;
|
|
15
|
-
//
|
|
16
|
-
|
|
16
|
+
// Per-project run ids for explicit projectRoot tracking (HARNESS-REQUIREMENT-6)
|
|
17
|
+
const runIdsByRequirementsDir = new Map();
|
|
18
|
+
/**
|
|
19
|
+
* Initialize (or reuse) a test run.
|
|
20
|
+
*
|
|
21
|
+
* HARNESS-REQUIREMENT-6.0: when an explicit projectRoot is provided, the run
|
|
22
|
+
* is keyed to that project's .requirements/ so tracking writes land in the
|
|
23
|
+
* resolving project's cache, not the enclosing project's.
|
|
24
|
+
*/
|
|
25
|
+
export function ensureTestRun(projectRoot) {
|
|
26
|
+
if (projectRoot) {
|
|
27
|
+
const requirementsDir = path.join(projectRoot, ".requirements");
|
|
28
|
+
let runId = runIdsByRequirementsDir.get(requirementsDir);
|
|
29
|
+
if (!runId) {
|
|
30
|
+
// Reuse a run id written by prepare() in that project, else mint one
|
|
31
|
+
runId = getTestRunId(requirementsDir) ?? `${Date.now()}-${process.pid}`;
|
|
32
|
+
runIdsByRequirementsDir.set(requirementsDir, runId);
|
|
33
|
+
getCacheDir(requirementsDir, true);
|
|
34
|
+
}
|
|
35
|
+
return runId;
|
|
36
|
+
}
|
|
17
37
|
if (!testRunId) {
|
|
18
38
|
// Priority 1: Use environment variable (cross-process persistence from globalSetup)
|
|
19
39
|
if (process.env.DOTREQUIREMENTS_PROJECT_ROOT) {
|
|
@@ -61,14 +81,62 @@ export function getCallerLocation() {
|
|
|
61
81
|
const regex2 = /at (.+):(\d+):(\d+)/;
|
|
62
82
|
const match = regex1.exec(frame) || regex2.exec(frame);
|
|
63
83
|
if (match) {
|
|
64
|
-
|
|
84
|
+
let filePath = match[1];
|
|
65
85
|
const lineNumber = match[2];
|
|
66
|
-
|
|
86
|
+
// ESM stack frames use file:// URLs
|
|
87
|
+
if (filePath.startsWith("file://")) {
|
|
88
|
+
try {
|
|
89
|
+
filePath = fileURLToPath(filePath);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Keep the raw frame path if the URL can't be converted
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// COVERAGE-CONTEXT-2: keep the full path (not just the basename) so
|
|
96
|
+
// finalize() can git-blame the call site for nested test files.
|
|
97
|
+
return `${filePath}:${lineNumber}`;
|
|
67
98
|
}
|
|
68
99
|
}
|
|
69
100
|
}
|
|
70
101
|
return "unknown";
|
|
71
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Convert an absolute caller location to a project-relative one when the
|
|
105
|
+
* caller file lives inside the project. Keeps locations stable and blame-able
|
|
106
|
+
* from the project root (COVERAGE-CONTEXT-2); callers outside the project
|
|
107
|
+
* stay absolute.
|
|
108
|
+
*/
|
|
109
|
+
function toProjectRelativeLocation(location, requirementsDir) {
|
|
110
|
+
if (!requirementsDir) {
|
|
111
|
+
return location;
|
|
112
|
+
}
|
|
113
|
+
const match = /^(.+):(\d+)$/.exec(location);
|
|
114
|
+
if (!match) {
|
|
115
|
+
return location;
|
|
116
|
+
}
|
|
117
|
+
const [, filePath, lineNumber] = match;
|
|
118
|
+
const projectRoot = path.dirname(requirementsDir);
|
|
119
|
+
const relative = toProjectRelativePath(filePath, projectRoot);
|
|
120
|
+
if (relative === filePath) {
|
|
121
|
+
return location;
|
|
122
|
+
}
|
|
123
|
+
return `${relative}:${lineNumber}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Normalize an absolute path inside the project to a project-relative one
|
|
127
|
+
* (COVERAGE-CONTEXT-2: stable, blame-able from the project root). Paths that
|
|
128
|
+
* are already relative or fall outside the project are returned unchanged.
|
|
129
|
+
*/
|
|
130
|
+
export function toProjectRelativePath(filePath, projectRoot) {
|
|
131
|
+
if (!path.isAbsolute(filePath)) {
|
|
132
|
+
return filePath;
|
|
133
|
+
}
|
|
134
|
+
const relative = path.relative(projectRoot, filePath);
|
|
135
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
136
|
+
return filePath;
|
|
137
|
+
}
|
|
138
|
+
return relative;
|
|
139
|
+
}
|
|
72
140
|
// Get caller's directory (full path) for finding .requirements
|
|
73
141
|
function getCallerDirectory() {
|
|
74
142
|
const error = new Error();
|
|
@@ -100,10 +168,15 @@ function getCallerDirectory() {
|
|
|
100
168
|
* HARNESS-REQUIREMENT-3.2: Works correctly when tests run in parallel processes
|
|
101
169
|
*
|
|
102
170
|
* @param reqId - Full requirement path (e.g., 'REQ-123.given' or 'REQ-123.0.1')
|
|
171
|
+
* @param projectRoot - Explicit project root; tracking is written to this
|
|
172
|
+
* project's cache when provided (HARNESS-REQUIREMENT-6.0)
|
|
103
173
|
*/
|
|
104
|
-
export function trackRequirement(reqId) {
|
|
105
|
-
const
|
|
106
|
-
const
|
|
174
|
+
export function trackRequirement(reqId, projectRoot) {
|
|
175
|
+
const runId = ensureTestRun(projectRoot);
|
|
176
|
+
const targetRequirementsDir = projectRoot
|
|
177
|
+
? path.join(projectRoot, ".requirements")
|
|
178
|
+
: requirementsDirCached;
|
|
179
|
+
const callerLocation = toProjectRelativeLocation(getCallerLocation(), targetRequirementsDir);
|
|
107
180
|
// Track in memory for backward compatibility
|
|
108
181
|
if (!trackedRequirements.has(reqId)) {
|
|
109
182
|
trackedRequirements.set(reqId, {
|
|
@@ -116,14 +189,14 @@ export function trackRequirement(reqId) {
|
|
|
116
189
|
tracked.accessedAt.push(callerLocation);
|
|
117
190
|
// Append to JSONL file for cross-process tracking
|
|
118
191
|
// HARNESS-REQUIREMENT-3.2: JSONL is append-only, safe for parallel workers
|
|
119
|
-
if (
|
|
192
|
+
if (targetRequirementsDir) {
|
|
120
193
|
const entry = {
|
|
121
194
|
requirementKey: reqId,
|
|
122
195
|
callerLocation,
|
|
123
196
|
timestamp: Date.now(),
|
|
124
197
|
testRunId: runId,
|
|
125
198
|
};
|
|
126
|
-
appendTrackingEntry(
|
|
199
|
+
appendTrackingEntry(targetRequirementsDir, entry);
|
|
127
200
|
}
|
|
128
201
|
}
|
|
129
202
|
/**
|
|
@@ -144,6 +217,7 @@ export function clearTracking() {
|
|
|
144
217
|
trackedRequirements.clear();
|
|
145
218
|
testRunId = null;
|
|
146
219
|
requirementsDirCached = null;
|
|
220
|
+
runIdsByRequirementsDir.clear();
|
|
147
221
|
}
|
|
148
222
|
/**
|
|
149
223
|
* Initialize a test run (called by globalSetup).
|
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
* - validate_requirements: Validate requirements file syntax offline
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
|
-
import { resolve } from "node:path";
|
|
10
9
|
import { getProjectContext } from "../../requirements/cloud-ai.js";
|
|
11
10
|
import { generateStyleGuide, readLocalStyleGuide, } from "../../requirements/style-guide.js";
|
|
12
11
|
import { parseRequirementsFromFile, validateForPush, } from "../../schema/index.js";
|
|
13
12
|
import { CONVEX_URL } from "../convexClient.js";
|
|
14
|
-
import { errorResponse, textResponse } from "./types.js";
|
|
13
|
+
import { errorResponse, resolveProjectFilePath, textResponse, } from "./types.js";
|
|
15
14
|
/**
|
|
16
15
|
* Handler for create_requirement_document tool
|
|
17
16
|
*
|
|
@@ -60,8 +59,18 @@ export async function handleCreateRequirementDocument(args, context) {
|
|
|
60
59
|
*/
|
|
61
60
|
export async function handleValidateRequirements(args, context) {
|
|
62
61
|
const { filePath } = args;
|
|
63
|
-
//
|
|
64
|
-
|
|
62
|
+
// #49: Resolve against the discovered project.path (matching push) with a
|
|
63
|
+
// workspaceRoot fallback. MCP-AUTHOR-2.3: validation stays offline — if
|
|
64
|
+
// discovery has no credentials it simply throws and we fall back to
|
|
65
|
+
// workspaceRoot resolution.
|
|
66
|
+
let projectPath;
|
|
67
|
+
try {
|
|
68
|
+
projectPath = (await context.getProjectFromDiscovery()).path;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
projectPath = undefined;
|
|
72
|
+
}
|
|
73
|
+
const fullPath = resolveProjectFilePath(filePath, projectPath, context.workspaceRoot);
|
|
65
74
|
// MCP-AUTHOR-2.4: Check if file exists
|
|
66
75
|
if (!existsSync(fullPath)) {
|
|
67
76
|
return errorResponse(`File not found: ${filePath}`);
|
package/dist/mcp/handlers/get.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Retrieves a specific requirement by ID with its full tree and test coverage.
|
|
5
5
|
*/
|
|
6
6
|
import { glob } from "glob";
|
|
7
|
-
import { formatRequirementTree
|
|
7
|
+
import { formatRequirementTree } from "../../requirements/index.js";
|
|
8
8
|
import { findFilesWithRequirement, findTestCodeForRequirement, } from "../../requirements/testCodeExtractor.js";
|
|
9
9
|
import { textResponse } from "./types.js";
|
|
10
10
|
/**
|
|
@@ -20,10 +20,14 @@ export async function handleGetRequirement(args, context) {
|
|
|
20
20
|
const { id, projectId } = args;
|
|
21
21
|
const project = await context.getProjectFromDiscovery(projectId);
|
|
22
22
|
const requirements = await context.getRequirements(projectId);
|
|
23
|
-
// Get the tree starting from this ID
|
|
23
|
+
// Get the tree starting from this ID.
|
|
24
24
|
// MCP-GET-1.0: Full tree for root requirement
|
|
25
25
|
// MCP-GET-1.1: Subtree for child requirement
|
|
26
|
-
|
|
26
|
+
//
|
|
27
|
+
// #46: Match the node itself plus all of its descendants by id prefix. The
|
|
28
|
+
// shared getRequirementTree only matches on rootId/id equality, which drops
|
|
29
|
+
// grandchildren (e.g. REQ-123.0.0) when a child id (REQ-123.0) is requested.
|
|
30
|
+
const tree = requirements.filter((r) => r.id === id || r.id.startsWith(`${id}.`));
|
|
27
31
|
// MCP-GET-1.2: Return error if not found
|
|
28
32
|
if (tree.length === 0) {
|
|
29
33
|
return textResponse(`Requirement "${id}" not found`);
|
|
@@ -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
|
*/
|