@popoverai/dotrequirements 0.16.0 → 0.17.1

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.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * search_requirements handler
3
+ *
4
+ * Searches requirements by text or regex query across IDs, content, and labels.
5
+ * Returns matching requirements with their full tree context.
6
+ */
7
+ import { textResponse, errorResponse } from './types.js';
8
+ import { searchRequirements, getRequirementTree, formatRequirementTree, } from '../requirements.js';
9
+ /**
10
+ * Handler for search_requirements tool
11
+ *
12
+ * Requirements covered:
13
+ * - MCP-SEARCH-1.0: Query matches requirement ID
14
+ * - MCP-SEARCH-1.1: Query matches requirement content
15
+ * - MCP-SEARCH-1.2: Query matches requirement label
16
+ * - MCP-SEARCH-1.3: useRegex interprets query as case-insensitive regex
17
+ * - MCP-SEARCH-1.4: Invalid regex returns error
18
+ * - MCP-SEARCH-1.5: No matches returns informative message
19
+ * - MCP-SEARCH-1.6: Nested match returns root requirement
20
+ * - MCP-SEARCH-1.7: Results include full tree as code block
21
+ */
22
+ export async function handleSearchRequirements(args, context) {
23
+ const { query, useRegex = false, projectId } = args;
24
+ const requirements = await context.getRequirements(projectId);
25
+ let results;
26
+ if (useRegex) {
27
+ // MCP-SEARCH-1.3: Interpret query as case-insensitive regex
28
+ // MCP-SEARCH-1.4: Return error for invalid regex
29
+ try {
30
+ const regex = new RegExp(query, 'i');
31
+ results = requirements.filter((r) => regex.test(r.id) ||
32
+ regex.test(r.content) ||
33
+ regex.test(r.label));
34
+ }
35
+ catch (error) {
36
+ return errorResponse(`Invalid regular expression: ${error instanceof Error ? error.message : String(error)}`);
37
+ }
38
+ }
39
+ else {
40
+ // MCP-SEARCH-1.0, MCP-SEARCH-1.1, MCP-SEARCH-1.2: Text search across ID, content, label
41
+ results = searchRequirements(requirements, query);
42
+ }
43
+ // MCP-SEARCH-1.6: Collect unique root IDs from all matches (nested matches bubble up to their root)
44
+ const matchedRootIds = new Set(results.map((r) => r.rootId));
45
+ const rootResults = requirements.filter((r) => r.path.length === 0 && matchedRootIds.has(r.rootId));
46
+ // MCP-SEARCH-1.5: No matches returns informative message
47
+ if (rootResults.length === 0) {
48
+ return textResponse(`No requirements found matching "${query}"${useRegex ? ' (regex)' : ''}`);
49
+ }
50
+ // MCP-SEARCH-1.7: Each result includes full tree as code block
51
+ const formatted = rootResults
52
+ .map((r) => {
53
+ const tree = getRequirementTree(requirements, r.id);
54
+ const treeFormatted = formatRequirementTree(tree);
55
+ return `**${r.id}** (${r.label})\n${r.content}\n_Source: ${r.documentTitle}_\n\n\`\`\`\n${treeFormatted}\n\`\`\``;
56
+ })
57
+ .join('\n\n---\n\n');
58
+ return textResponse(`Found ${rootResults.length} requirement(s) matching "${query}"${useRegex ? ' (regex)' : ''}:\n\n${formatted}`);
59
+ }
60
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * get_requirements_by_test and get_tests_by_requirement handlers
3
+ *
4
+ * Maps between test files and requirements in both directions.
5
+ */
6
+ import type { HandlerContext, ToolResponse } from './types.js';
7
+ /**
8
+ * Arguments for get_requirements_by_test tool
9
+ */
10
+ export interface GetRequirementsByTestArgs {
11
+ testFile: string;
12
+ projectId?: string;
13
+ }
14
+ /**
15
+ * Handler for get_requirements_by_test tool
16
+ *
17
+ * Requirements covered:
18
+ * - MCP-MAP-1.0: The response lists each requirement() call found in the test file
19
+ * - MCP-MAP-1.1: For each reference, the requirement content and line number are shown
20
+ * - MCP-MAP-1.2: When the test file does not exist, an error is returned
21
+ */
22
+ export declare function handleGetRequirementsByTest(args: GetRequirementsByTestArgs, context: HandlerContext): Promise<ToolResponse>;
23
+ /**
24
+ * Arguments for get_tests_by_requirement tool
25
+ */
26
+ export interface GetTestsByRequirementArgs {
27
+ requirementsFile: string;
28
+ projectId?: string;
29
+ }
30
+ /**
31
+ * Handler for get_tests_by_requirement tool
32
+ *
33
+ * Requirements covered:
34
+ * - MCP-MAP-2.0: The response lists which requirements have test references
35
+ * - MCP-MAP-2.1: For each covered requirement, the test file and line number are shown
36
+ * - MCP-MAP-2.2: Requirements without test references are listed separately
37
+ */
38
+ export declare function handleGetTestsByRequirement(args: GetTestsByRequirementArgs, context: HandlerContext): Promise<ToolResponse>;
39
+ //# sourceMappingURL=test-mapping.d.ts.map
@@ -0,0 +1,133 @@
1
+ /**
2
+ * get_requirements_by_test and get_tests_by_requirement handlers
3
+ *
4
+ * Maps between test files and requirements in both directions.
5
+ */
6
+ import { existsSync } from 'fs';
7
+ import { resolve, relative } from 'path';
8
+ import { textResponse, errorResponse } from './types.js';
9
+ import { getRequirementById } from '../requirements.js';
10
+ import { findRequirementsInFile, findAllTestReferences, } from '../grep.js';
11
+ /**
12
+ * Handler for get_requirements_by_test tool
13
+ *
14
+ * Requirements covered:
15
+ * - MCP-MAP-1.0: The response lists each requirement() call found in the test file
16
+ * - MCP-MAP-1.1: For each reference, the requirement content and line number are shown
17
+ * - MCP-MAP-1.2: When the test file does not exist, an error is returned
18
+ */
19
+ export async function handleGetRequirementsByTest(args, context) {
20
+ const { testFile, projectId } = args;
21
+ // MCP-MAP-1.2: Check if file exists before processing
22
+ const fullPath = resolve(context.workspaceRoot, testFile);
23
+ if (!existsSync(fullPath)) {
24
+ return errorResponse(`Test file not found: ${testFile}`);
25
+ }
26
+ 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);
29
+ if (refs.length === 0) {
30
+ return textResponse(`No requirement references found in "${testFile}"`);
31
+ }
32
+ // MCP-MAP-1.1: For each reference, get the requirement content and show line number
33
+ const entries = refs.map((ref) => {
34
+ const req = getRequirementById(requirements, ref.requirementId);
35
+ const reqContent = req
36
+ ? `**${req.label}:** ${req.content}`
37
+ : `(requirement not found)`;
38
+ return `**Line ${ref.line}:** \`${ref.requirementId}\` - ${reqContent}`;
39
+ });
40
+ return textResponse(`Requirements referenced in "${testFile}":\n\n${entries.join('\n\n')}`);
41
+ }
42
+ /**
43
+ * Handler for get_tests_by_requirement tool
44
+ *
45
+ * Requirements covered:
46
+ * - MCP-MAP-2.0: The response lists which requirements have test references
47
+ * - MCP-MAP-2.1: For each covered requirement, the test file and line number are shown
48
+ * - MCP-MAP-2.2: Requirements without test references are listed separately
49
+ */
50
+ export async function handleGetTestsByRequirement(args, context) {
51
+ const { requirementsFile, projectId } = args;
52
+ const { workspaceRoot } = context;
53
+ const fullPath = resolve(workspaceRoot, requirementsFile);
54
+ // Verify file exists
55
+ if (!existsSync(fullPath)) {
56
+ return errorResponse(`Requirements file not found: ${requirementsFile}`);
57
+ }
58
+ // Verify it's a requirements file
59
+ if (!requirementsFile.endsWith('.requirements.md')) {
60
+ return errorResponse(`File must be a requirements file: *.requirements.md`);
61
+ }
62
+ // Load all requirements from this file
63
+ const requirements = await context.getRequirements(projectId);
64
+ const fileRequirements = requirements.filter(req => resolve(workspaceRoot, req.sourceFile) === fullPath);
65
+ if (fileRequirements.length === 0) {
66
+ return textResponse(`No requirements found in "${requirementsFile}"`);
67
+ }
68
+ // Get unique root requirement IDs from this file
69
+ const rootIds = new Set(fileRequirements.map(req => req.rootId));
70
+ // Find all test references in the workspace
71
+ const allTestRefs = await findAllTestReferences(workspaceRoot);
72
+ // Build coverage map: requirement ID -> list of test references
73
+ const coverageMap = new Map();
74
+ for (const ref of allTestRefs) {
75
+ if (!coverageMap.has(ref.requirementId)) {
76
+ coverageMap.set(ref.requirementId, []);
77
+ }
78
+ coverageMap.get(ref.requirementId).push(ref);
79
+ }
80
+ // MCP-MAP-2.0: Categorize requirements as covered or not covered
81
+ const covered = [];
82
+ const notCovered = [];
83
+ for (const rootId of rootIds) {
84
+ // Check if this requirement ID or any of its children are referenced
85
+ const reqAndChildren = fileRequirements.filter(r => r.rootId === rootId);
86
+ const allIds = reqAndChildren.map(r => r.id);
87
+ const testsForThisReq = [];
88
+ for (const id of allIds) {
89
+ const refs = coverageMap.get(id);
90
+ if (refs) {
91
+ testsForThisReq.push(...refs);
92
+ }
93
+ }
94
+ if (testsForThisReq.length > 0) {
95
+ covered.push({ id: rootId, tests: testsForThisReq });
96
+ }
97
+ else {
98
+ notCovered.push(rootId);
99
+ }
100
+ }
101
+ // Format output
102
+ let output = `# Test Coverage for \`${requirementsFile}\`\n\n`;
103
+ // MCP-MAP-2.1: For each covered requirement, show test file and line number
104
+ if (covered.length > 0) {
105
+ output += `## Covered (${covered.length})\n\n`;
106
+ for (const { id, tests } of covered) {
107
+ // Group tests by file
108
+ const testsByFile = new Map();
109
+ for (const test of tests) {
110
+ if (!testsByFile.has(test.file)) {
111
+ testsByFile.set(test.file, []);
112
+ }
113
+ testsByFile.get(test.file).push(test.line);
114
+ }
115
+ output += `- **${id}**: ${tests.length} test${tests.length === 1 ? '' : 's'}\n`;
116
+ for (const [file, lines] of testsByFile.entries()) {
117
+ const relPath = relative(workspaceRoot, file);
118
+ const lineList = lines.sort((a, b) => a - b).join(', ');
119
+ output += ` - \`${relPath}\` (lines ${lineList})\n`;
120
+ }
121
+ }
122
+ output += '\n';
123
+ }
124
+ // MCP-MAP-2.2: Requirements without test references are listed separately
125
+ if (notCovered.length > 0) {
126
+ output += `## Not Covered (${notCovered.length})\n\n`;
127
+ for (const id of notCovered) {
128
+ output += `- **${id}**: No tests found\n`;
129
+ }
130
+ }
131
+ return textResponse(output);
132
+ }
133
+ //# sourceMappingURL=test-mapping.js.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Handler types for MCP tool handlers
3
+ *
4
+ * Each handler is a pure function that takes arguments and context,
5
+ * and returns an MCP tool response. This enables unit testing without
6
+ * the full MCP server infrastructure.
7
+ */
8
+ import type { FlattenedRequirement } from '../types.js';
9
+ import type { DotreqProject } from '../../utils/project-discovery.js';
10
+ /**
11
+ * MCP tool response content item
12
+ */
13
+ export interface ToolContentItem {
14
+ type: 'text';
15
+ text: string;
16
+ }
17
+ /**
18
+ * MCP tool response
19
+ *
20
+ * Compatible with MCP SDK's CallToolResult type.
21
+ * The index signature allows for SDK extensibility (e.g., _meta, task).
22
+ */
23
+ export interface ToolResponse {
24
+ [key: string]: unknown;
25
+ content: ToolContentItem[];
26
+ isError?: boolean;
27
+ }
28
+ /**
29
+ * Context provided to all handlers
30
+ *
31
+ * This abstraction enables:
32
+ * - Unit testing with mock context
33
+ * - Dependency injection
34
+ * - Isolation from global state
35
+ */
36
+ export interface HandlerContext {
37
+ /**
38
+ * Get flattened requirements for a project
39
+ * @param projectId - Optional project ID for multi-project scenarios
40
+ */
41
+ getRequirements: (projectId?: string) => Promise<FlattenedRequirement[]>;
42
+ /**
43
+ * Get project from discovery (path, credentials)
44
+ * @param projectId - Optional project ID for multi-project scenarios
45
+ */
46
+ getProjectFromDiscovery: (projectId?: string) => Promise<DotreqProject>;
47
+ /**
48
+ * Workspace root directory
49
+ */
50
+ workspaceRoot: string;
51
+ /**
52
+ * Project paths from environment variables (PROJ_* pattern)
53
+ */
54
+ projectPaths: Map<string, string>;
55
+ /**
56
+ * Environment variables (for debug_mcp_environment)
57
+ */
58
+ env: NodeJS.ProcessEnv;
59
+ }
60
+ /**
61
+ * Handler function signature
62
+ *
63
+ * All MCP tool handlers implement this interface.
64
+ * The args parameter contains tool-specific arguments.
65
+ */
66
+ export type ToolHandler<TArgs = Record<string, unknown>> = (args: TArgs, context: HandlerContext) => Promise<ToolResponse>;
67
+ /**
68
+ * Helper to create a text response
69
+ */
70
+ export declare function textResponse(text: string): ToolResponse;
71
+ /**
72
+ * Helper to create an error response
73
+ */
74
+ export declare function errorResponse(message: string): ToolResponse;
75
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Handler types for MCP tool handlers
3
+ *
4
+ * Each handler is a pure function that takes arguments and context,
5
+ * and returns an MCP tool response. This enables unit testing without
6
+ * the full MCP server infrastructure.
7
+ */
8
+ /**
9
+ * Helper to create a text response
10
+ */
11
+ export function textResponse(text) {
12
+ return {
13
+ content: [{ type: 'text', text }],
14
+ };
15
+ }
16
+ /**
17
+ * Helper to create an error response
18
+ */
19
+ export function errorResponse(message) {
20
+ return {
21
+ content: [{ type: 'text', text: message }],
22
+ isError: true,
23
+ };
24
+ }
25
+ //# sourceMappingURL=types.js.map
@@ -1,3 +1,44 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ export declare function invalidateCache(): void;
4
+ export declare const server: Server<{
5
+ method: string;
6
+ params?: {
7
+ [x: string]: unknown;
8
+ task?: {
9
+ [x: string]: unknown;
10
+ ttl?: number | null | undefined;
11
+ pollInterval?: number | undefined;
12
+ } | undefined;
13
+ _meta?: {
14
+ [x: string]: unknown;
15
+ progressToken?: string | number | undefined;
16
+ "io.modelcontextprotocol/related-task"?: {
17
+ [x: string]: unknown;
18
+ taskId: string;
19
+ } | undefined;
20
+ } | undefined;
21
+ } | undefined;
22
+ }, {
23
+ method: string;
24
+ params?: {
25
+ [x: string]: unknown;
26
+ _meta?: {
27
+ [x: string]: unknown;
28
+ "io.modelcontextprotocol/related-task"?: {
29
+ [x: string]: unknown;
30
+ taskId: string;
31
+ } | undefined;
32
+ } | undefined;
33
+ } | undefined;
34
+ }, {
35
+ [x: string]: unknown;
36
+ _meta?: {
37
+ [x: string]: unknown;
38
+ "io.modelcontextprotocol/related-task"?: {
39
+ [x: string]: unknown;
40
+ taskId: string;
41
+ } | undefined;
42
+ } | undefined;
43
+ }>;
3
44
  //# sourceMappingURL=index.d.ts.map