@popoverai/dotrequirements 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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