@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.
- package/dist/mcp/handlers/authoring.d.ts +41 -0
- package/dist/mcp/handlers/authoring.js +308 -0
- package/dist/mcp/handlers/coverage.d.ts +44 -0
- package/dist/mcp/handlers/coverage.js +105 -0
- package/dist/mcp/handlers/debug.d.ts +17 -0
- package/dist/mcp/handlers/debug.js +37 -0
- package/dist/mcp/handlers/get.d.ts +24 -0
- package/dist/mcp/handlers/get.js +64 -0
- package/dist/mcp/handlers/index.d.ts +28 -0
- package/dist/mcp/handlers/index.js +19 -0
- package/dist/mcp/handlers/list.d.ts +36 -0
- package/dist/mcp/handlers/list.js +65 -0
- package/dist/mcp/handlers/push.d.ts +26 -0
- package/dist/mcp/handlers/push.js +143 -0
- package/dist/mcp/handlers/review.d.ts +51 -0
- package/dist/mcp/handlers/review.js +219 -0
- package/dist/mcp/handlers/search.d.ts +30 -0
- package/dist/mcp/handlers/search.js +60 -0
- package/dist/mcp/handlers/test-mapping.d.ts +39 -0
- package/dist/mcp/handlers/test-mapping.js +133 -0
- package/dist/mcp/handlers/types.d.ts +75 -0
- package/dist/mcp/handlers/types.js +25 -0
- package/dist/mcp/index.d.ts +42 -1
- package/dist/mcp/index.js +48 -1211
- package/dist/templates/requirements-readme.js +1 -1
- package/dist/templates/requirements-readme.ts +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -2,18 +2,13 @@
|
|
|
2
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
-
import { loadAllRequirements
|
|
6
|
-
import { findAllTestReferences, getReferencedRequirementIds, } from './grep.js';
|
|
7
|
-
import { findTestCodeForRequirement, findFilesWithRequirement, } from './testCodeExtractor.js';
|
|
8
|
-
import { glob } from 'glob';
|
|
9
|
-
import { CONVEX_URL, getRequirementCoverage as queryRequirementCoverage, getProjectCoverage as queryProjectCoverage, getProjectContext, } from './convexClient.js';
|
|
5
|
+
import { loadAllRequirements } from './requirements.js';
|
|
10
6
|
import { discoverProjects, resolveProject, } from '../utils/project-discovery.js';
|
|
11
7
|
import { getCredentialsFromEnv } from '../utils/project-settings.js';
|
|
12
|
-
import { readFileSync
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
13
9
|
import { fileURLToPath } from 'url';
|
|
14
|
-
import { dirname, join
|
|
15
|
-
import {
|
|
16
|
-
import { findRequirementsFiles } from './requirements.js';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
import { handleDebugMcpEnvironment, handleSearchRequirements, handleListAllRequirements, handleListUntestedRequirements, handleGetRequirement, handleGetRequirementsByTest, handleGetTestsByRequirement, handleGetRequirementCoverage, handleGetProjectCoverageSummary, handleCreateRequirementDocument, handleValidateRequirements, handlePushRequirements, handleStyleCheck, handleReviewTest, } from './handlers/index.js';
|
|
17
12
|
// Read version from package.json
|
|
18
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
14
|
const __dirname = dirname(__filename);
|
|
@@ -38,17 +33,6 @@ const PROJECT_PATHS = getProjectPathsFromEnv();
|
|
|
38
33
|
// Parse --auth-from-env flag for CI/CD environments
|
|
39
34
|
// When set, credentials are read from DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET env vars
|
|
40
35
|
const USE_ENV_AUTH = process.argv.includes('--auth-from-env');
|
|
41
|
-
const STYLE_CHECK_GUIDANCE = `
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## How to use this feedback
|
|
46
|
-
|
|
47
|
-
1. **This check is stateless.** It is a "new set of eyes" from any previous feedback rounds — it has no memory of prior suggestions or changes you've already made.
|
|
48
|
-
|
|
49
|
-
2. **Act on feedback from the posture of "what will make the requirements better."** The severity categories (MUST FIX / SHOULD FIX / COULD IMPROVE) are prioritized for convenience, but unless the user has explicitly told you to ignore lower-priority feedback, go ahead and make any improvements — minor or otherwise. If you believe acting on a specific piece of feedback would make the requirements *worse*, surface that feedback to the user rather than silently ignoring it. Otherwise, just act.
|
|
50
|
-
|
|
51
|
-
3. **Next steps:** Always run \`validate_requirements\` again after making updates. If you make significant changes based on this feedback, you can run \`style_check\` again — but be aware that will be a fresh set of eyes, not a continued conversation.`;
|
|
52
36
|
// Cache for loaded requirements (refreshed on each tool call for now)
|
|
53
37
|
let cachedRequirements = new Map();
|
|
54
38
|
let cachedDiscoveryResult = null;
|
|
@@ -109,7 +93,8 @@ async function getProjectFromDiscovery(projectId) {
|
|
|
109
93
|
return resolveProject(cachedDiscoveryResult, projectId);
|
|
110
94
|
}
|
|
111
95
|
// Invalidate cache (call before operations that should see fresh data)
|
|
112
|
-
|
|
96
|
+
// Exported for testing
|
|
97
|
+
export function invalidateCache() {
|
|
113
98
|
cachedRequirements.clear();
|
|
114
99
|
cachedDiscoveryResult = null;
|
|
115
100
|
}
|
|
@@ -371,8 +356,8 @@ const prompts = [
|
|
|
371
356
|
description: 'Guide writing tests that reference and validate requirements',
|
|
372
357
|
},
|
|
373
358
|
];
|
|
374
|
-
// Create server
|
|
375
|
-
const server = new Server({
|
|
359
|
+
// Create server (exported for testing with InMemoryTransport)
|
|
360
|
+
export const server = new Server({
|
|
376
361
|
name: 'dotrequirements',
|
|
377
362
|
version: VERSION,
|
|
378
363
|
}, {
|
|
@@ -564,1195 +549,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
564
549
|
const { name, arguments: args } = request.params;
|
|
565
550
|
// Refresh cache for each request to pick up changes
|
|
566
551
|
invalidateCache();
|
|
552
|
+
// Create handler context for extracted handlers
|
|
553
|
+
const handlerContext = {
|
|
554
|
+
getRequirements,
|
|
555
|
+
getProjectFromDiscovery,
|
|
556
|
+
workspaceRoot: WORKSPACE_ROOT,
|
|
557
|
+
projectPaths: PROJECT_PATHS,
|
|
558
|
+
env: process.env,
|
|
559
|
+
};
|
|
567
560
|
try {
|
|
568
561
|
switch (name) {
|
|
569
|
-
case 'debug_mcp_environment':
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
},
|
|
598
|
-
],
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
case 'search_requirements': {
|
|
602
|
-
const { query, useRegex = false, projectId } = args;
|
|
603
|
-
const requirements = await getRequirements(projectId);
|
|
604
|
-
let results;
|
|
605
|
-
if (useRegex) {
|
|
606
|
-
try {
|
|
607
|
-
const regex = new RegExp(query, 'i');
|
|
608
|
-
results = requirements.filter((r) => regex.test(r.id) ||
|
|
609
|
-
regex.test(r.content) ||
|
|
610
|
-
regex.test(r.label));
|
|
611
|
-
}
|
|
612
|
-
catch (error) {
|
|
613
|
-
return {
|
|
614
|
-
content: [
|
|
615
|
-
{
|
|
616
|
-
type: 'text',
|
|
617
|
-
text: `Invalid regular expression: ${error instanceof Error ? error.message : String(error)}`,
|
|
618
|
-
},
|
|
619
|
-
],
|
|
620
|
-
isError: true,
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
else {
|
|
625
|
-
results = searchRequirements(requirements, query);
|
|
626
|
-
}
|
|
627
|
-
// Filter to root requirements only (path.length === 0)
|
|
628
|
-
const rootResults = results.filter((r) => r.path.length === 0);
|
|
629
|
-
if (rootResults.length === 0) {
|
|
630
|
-
return {
|
|
631
|
-
content: [
|
|
632
|
-
{
|
|
633
|
-
type: 'text',
|
|
634
|
-
text: `No requirements found matching "${query}"${useRegex ? ' (regex)' : ''}`,
|
|
635
|
-
},
|
|
636
|
-
],
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
// For each root result, get the full tree
|
|
640
|
-
const formatted = rootResults
|
|
641
|
-
.map((r) => {
|
|
642
|
-
const tree = getRequirementTree(requirements, r.id);
|
|
643
|
-
const treeFormatted = formatRequirementTree(tree);
|
|
644
|
-
return `**${r.id}** (${r.label})\n${r.content}\n_Source: ${r.documentTitle}_\n\n\`\`\`\n${treeFormatted}\n\`\`\``;
|
|
645
|
-
})
|
|
646
|
-
.join('\n\n---\n\n');
|
|
647
|
-
return {
|
|
648
|
-
content: [
|
|
649
|
-
{
|
|
650
|
-
type: 'text',
|
|
651
|
-
text: `Found ${rootResults.length} requirement(s) matching "${query}"${useRegex ? ' (regex)' : ''}:\n\n${formatted}`,
|
|
652
|
-
},
|
|
653
|
-
],
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
case 'get_requirement': {
|
|
657
|
-
const { id, projectId } = args;
|
|
658
|
-
const project = await getProjectFromDiscovery(projectId);
|
|
659
|
-
const requirements = await getRequirements(projectId);
|
|
660
|
-
// Get the tree starting from this ID
|
|
661
|
-
const tree = getRequirementTree(requirements, id);
|
|
662
|
-
if (tree.length === 0) {
|
|
663
|
-
return {
|
|
664
|
-
content: [
|
|
665
|
-
{
|
|
666
|
-
type: 'text',
|
|
667
|
-
text: `Requirement "${id}" not found`,
|
|
668
|
-
},
|
|
669
|
-
],
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
const root = tree[0];
|
|
673
|
-
const formatted = formatRequirementTree(tree);
|
|
674
|
-
// Find test files that reference this requirement
|
|
675
|
-
const testFiles = await glob('**/*.{test,spec}.{js,jsx,ts,tsx}', {
|
|
676
|
-
cwd: project.path,
|
|
677
|
-
absolute: true,
|
|
678
|
-
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
|
|
679
|
-
});
|
|
680
|
-
const matchingFiles = findFilesWithRequirement(testFiles, id);
|
|
681
|
-
let testCodeSections = [];
|
|
682
|
-
for (const file of matchingFiles) {
|
|
683
|
-
const testRefs = findTestCodeForRequirement(file, id);
|
|
684
|
-
// Deduplicate by removing nested blocks - keep only outermost blocks
|
|
685
|
-
const deduplicated = testRefs.filter((ref, i) => {
|
|
686
|
-
// Check if this ref is contained within any other ref
|
|
687
|
-
const isNested = testRefs.some((other, j) => {
|
|
688
|
-
if (i === j)
|
|
689
|
-
return false;
|
|
690
|
-
// other contains ref if it starts before or at the same line and ends after or at the same line
|
|
691
|
-
return other.startLine <= ref.startLine && other.endLine >= ref.endLine &&
|
|
692
|
-
(other.startLine < ref.startLine || other.endLine > ref.endLine);
|
|
693
|
-
});
|
|
694
|
-
return !isNested;
|
|
695
|
-
});
|
|
696
|
-
for (const ref of deduplicated) {
|
|
697
|
-
testCodeSections.push(`**${ref.requirementId}** tested in \`${ref.file}:${ref.startLine}-${ref.endLine}\`:\n\n\`\`\`typescript\n${ref.code}\n\`\`\``);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
const testSection = testCodeSections.length > 0
|
|
701
|
-
? `\n\n## Test Coverage (${testCodeSections.length} reference(s)):\n\n${testCodeSections.join('\n\n---\n\n')}`
|
|
702
|
-
: '\n\n## Test Coverage\n\nNo tests found referencing this requirement.';
|
|
703
|
-
return {
|
|
704
|
-
content: [
|
|
705
|
-
{
|
|
706
|
-
type: 'text',
|
|
707
|
-
text: `# ${id}\n\n**Document:** ${root.documentTitle}\n**Source:** ${root.sourceFile}\n\n## Requirement Tree (${tree.length} node(s)):\n\n\`\`\`\n${formatted}\n\`\`\`${testSection}`,
|
|
708
|
-
},
|
|
709
|
-
],
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
case 'list_untested_requirements': {
|
|
713
|
-
const { projectId } = args;
|
|
714
|
-
const project = await getProjectFromDiscovery(projectId);
|
|
715
|
-
const requirements = await getRequirements(projectId);
|
|
716
|
-
const referencedIds = await getReferencedRequirementIds(project.path);
|
|
717
|
-
// Get root requirements only for this comparison
|
|
718
|
-
const rootRequirements = requirements.filter((r) => r.path.length === 0);
|
|
719
|
-
const untested = rootRequirements.filter((r) => !referencedIds.has(r.id));
|
|
720
|
-
if (untested.length === 0) {
|
|
721
|
-
return {
|
|
722
|
-
content: [
|
|
723
|
-
{
|
|
724
|
-
type: 'text',
|
|
725
|
-
text: `All ${rootRequirements.length} requirements have test references!`,
|
|
726
|
-
},
|
|
727
|
-
],
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
const formatted = untested
|
|
731
|
-
.map((r) => `- **${r.id}**: ${r.content}`)
|
|
732
|
-
.join('\n');
|
|
733
|
-
return {
|
|
734
|
-
content: [
|
|
735
|
-
{
|
|
736
|
-
type: 'text',
|
|
737
|
-
text: `Found ${untested.length} untested requirement(s) out of ${rootRequirements.length} total:\n\n${formatted}`,
|
|
738
|
-
},
|
|
739
|
-
],
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
case 'list_all_requirements': {
|
|
743
|
-
const { projectId } = args;
|
|
744
|
-
const project = await getProjectFromDiscovery(projectId);
|
|
745
|
-
const requirements = await getRequirements(projectId);
|
|
746
|
-
if (requirements.length === 0) {
|
|
747
|
-
return {
|
|
748
|
-
content: [
|
|
749
|
-
{
|
|
750
|
-
type: 'text',
|
|
751
|
-
text: `No requirements found in project: ${project.path}\n\nMake sure .requirements/ directory exists with Markdown files (*.requirements.md).`,
|
|
752
|
-
},
|
|
753
|
-
],
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
// Group by root ID
|
|
757
|
-
const grouped = new Map();
|
|
758
|
-
for (const req of requirements) {
|
|
759
|
-
const existing = grouped.get(req.rootId) || [];
|
|
760
|
-
existing.push(req);
|
|
761
|
-
grouped.set(req.rootId, existing);
|
|
762
|
-
}
|
|
763
|
-
const formatted = Array.from(grouped.entries())
|
|
764
|
-
.map(([rootId, reqs]) => {
|
|
765
|
-
const root = reqs.find((r) => r.path.length === 0);
|
|
766
|
-
const childCount = reqs.length - 1;
|
|
767
|
-
return `- **${rootId}**: ${root?.content || '(no content)'} (${childCount} children)`;
|
|
768
|
-
})
|
|
769
|
-
.join('\n');
|
|
770
|
-
return {
|
|
771
|
-
content: [
|
|
772
|
-
{
|
|
773
|
-
type: 'text',
|
|
774
|
-
text: `Found ${grouped.size} requirement(s) with ${requirements.length} total nodes:\n\n${formatted}`,
|
|
775
|
-
},
|
|
776
|
-
],
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
case 'get_requirements_by_test': {
|
|
780
|
-
const { testFile, projectId } = args;
|
|
781
|
-
const requirements = await getRequirements(projectId);
|
|
782
|
-
// Find all requirement references in the test file
|
|
783
|
-
const { findRequirementsInFile } = await import('./grep.js');
|
|
784
|
-
const refs = await findRequirementsInFile(testFile);
|
|
785
|
-
if (refs.length === 0) {
|
|
786
|
-
return {
|
|
787
|
-
content: [
|
|
788
|
-
{
|
|
789
|
-
type: 'text',
|
|
790
|
-
text: `No requirement references found in "${testFile}"`,
|
|
791
|
-
},
|
|
792
|
-
],
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
// For each reference, get the requirement content
|
|
796
|
-
const entries = refs.map((ref) => {
|
|
797
|
-
const req = getRequirementById(requirements, ref.requirementId);
|
|
798
|
-
const reqContent = req
|
|
799
|
-
? `**${req.label}:** ${req.content}`
|
|
800
|
-
: `(requirement not found)`;
|
|
801
|
-
return `**Line ${ref.line}:** \`${ref.requirementId}\` - ${reqContent}`;
|
|
802
|
-
});
|
|
803
|
-
return {
|
|
804
|
-
content: [
|
|
805
|
-
{
|
|
806
|
-
type: 'text',
|
|
807
|
-
text: `Requirements referenced in "${testFile}":\n\n${entries.join('\n\n')}`,
|
|
808
|
-
},
|
|
809
|
-
],
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
case 'get_tests_by_requirement': {
|
|
813
|
-
const { requirementsFile, projectId } = args;
|
|
814
|
-
const fs = await import('fs');
|
|
815
|
-
const path = await import('path');
|
|
816
|
-
const fullPath = path.resolve(WORKSPACE_ROOT, requirementsFile);
|
|
817
|
-
// Verify file exists
|
|
818
|
-
if (!fs.existsSync(fullPath)) {
|
|
819
|
-
return {
|
|
820
|
-
content: [
|
|
821
|
-
{
|
|
822
|
-
type: 'text',
|
|
823
|
-
text: `Requirements file not found: ${requirementsFile}`,
|
|
824
|
-
},
|
|
825
|
-
],
|
|
826
|
-
isError: true,
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
// Verify it's a requirements file
|
|
830
|
-
if (!requirementsFile.endsWith('.requirements.md')) {
|
|
831
|
-
return {
|
|
832
|
-
content: [
|
|
833
|
-
{
|
|
834
|
-
type: 'text',
|
|
835
|
-
text: `File must be a requirements file: *.requirements.md`,
|
|
836
|
-
},
|
|
837
|
-
],
|
|
838
|
-
isError: true,
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
// Load all requirements from this file
|
|
842
|
-
const requirements = await getRequirements(projectId);
|
|
843
|
-
const fileRequirements = requirements.filter(req => path.resolve(WORKSPACE_ROOT, req.sourceFile) === fullPath);
|
|
844
|
-
if (fileRequirements.length === 0) {
|
|
845
|
-
return {
|
|
846
|
-
content: [
|
|
847
|
-
{
|
|
848
|
-
type: 'text',
|
|
849
|
-
text: `No requirements found in "${requirementsFile}"`,
|
|
850
|
-
},
|
|
851
|
-
],
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
// Get unique root requirement IDs from this file
|
|
855
|
-
const rootIds = new Set(fileRequirements.map(req => req.rootId));
|
|
856
|
-
// Find all test references in the workspace
|
|
857
|
-
const allTestRefs = await findAllTestReferences(WORKSPACE_ROOT);
|
|
858
|
-
// Build coverage map: requirement ID -> list of test references
|
|
859
|
-
const coverageMap = new Map();
|
|
860
|
-
for (const ref of allTestRefs) {
|
|
861
|
-
if (!coverageMap.has(ref.requirementId)) {
|
|
862
|
-
coverageMap.set(ref.requirementId, []);
|
|
863
|
-
}
|
|
864
|
-
coverageMap.get(ref.requirementId).push(ref);
|
|
865
|
-
}
|
|
866
|
-
// Categorize requirements as covered or not covered
|
|
867
|
-
const covered = [];
|
|
868
|
-
const notCovered = [];
|
|
869
|
-
for (const rootId of rootIds) {
|
|
870
|
-
// Check if this requirement ID or any of its children are referenced
|
|
871
|
-
const reqAndChildren = fileRequirements.filter(r => r.rootId === rootId);
|
|
872
|
-
const allIds = reqAndChildren.map(r => r.id);
|
|
873
|
-
const testsForThisReq = [];
|
|
874
|
-
for (const id of allIds) {
|
|
875
|
-
const refs = coverageMap.get(id);
|
|
876
|
-
if (refs) {
|
|
877
|
-
testsForThisReq.push(...refs);
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
if (testsForThisReq.length > 0) {
|
|
881
|
-
covered.push({ id: rootId, tests: testsForThisReq });
|
|
882
|
-
}
|
|
883
|
-
else {
|
|
884
|
-
notCovered.push(rootId);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
// Format output
|
|
888
|
-
let output = `# Test Coverage for \`${requirementsFile}\`\n\n`;
|
|
889
|
-
if (covered.length > 0) {
|
|
890
|
-
output += `## Covered (${covered.length})\n\n`;
|
|
891
|
-
for (const { id, tests } of covered) {
|
|
892
|
-
// Group tests by file
|
|
893
|
-
const testsByFile = new Map();
|
|
894
|
-
for (const test of tests) {
|
|
895
|
-
if (!testsByFile.has(test.file)) {
|
|
896
|
-
testsByFile.set(test.file, []);
|
|
897
|
-
}
|
|
898
|
-
testsByFile.get(test.file).push(test.line);
|
|
899
|
-
}
|
|
900
|
-
output += `- **${id}**: ${tests.length} test${tests.length === 1 ? '' : 's'}\n`;
|
|
901
|
-
for (const [file, lines] of testsByFile.entries()) {
|
|
902
|
-
const relPath = path.relative(WORKSPACE_ROOT, file);
|
|
903
|
-
const lineList = lines.sort((a, b) => a - b).join(', ');
|
|
904
|
-
output += ` - \`${relPath}\` (lines ${lineList})\n`;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
output += '\n';
|
|
908
|
-
}
|
|
909
|
-
if (notCovered.length > 0) {
|
|
910
|
-
output += `## Not Covered (${notCovered.length})\n\n`;
|
|
911
|
-
for (const id of notCovered) {
|
|
912
|
-
output += `- **${id}**: No tests found\n`;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
return {
|
|
916
|
-
content: [
|
|
917
|
-
{
|
|
918
|
-
type: 'text',
|
|
919
|
-
text: output,
|
|
920
|
-
},
|
|
921
|
-
],
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
case 'get_requirement_coverage': {
|
|
925
|
-
const { requirementKey, projectId } = args;
|
|
926
|
-
const project = await getProjectFromDiscovery(projectId);
|
|
927
|
-
const coverage = await queryRequirementCoverage(requirementKey, project.projectId, project.projectSecret, CONVEX_URL);
|
|
928
|
-
if (!coverage.lastTestedAt) {
|
|
929
|
-
return {
|
|
930
|
-
content: [
|
|
931
|
-
{
|
|
932
|
-
type: 'text',
|
|
933
|
-
text: `**${requirementKey}** has not been tested yet.`,
|
|
934
|
-
},
|
|
935
|
-
],
|
|
936
|
-
};
|
|
937
|
-
}
|
|
938
|
-
const lastTested = new Date(coverage.lastTestedAt);
|
|
939
|
-
const testLocation = coverage.testFile
|
|
940
|
-
? `\n**Test Location:** \`${coverage.testFile}${coverage.testLine ? `:${coverage.testLine}` : ''}\``
|
|
941
|
-
: '';
|
|
942
|
-
let branchInfo = `\n**Branch:** ${coverage.branch}`;
|
|
943
|
-
if (coverage.allBranches.length > 1) {
|
|
944
|
-
const otherBranches = coverage.allBranches
|
|
945
|
-
.filter((b) => b.branch !== coverage.branch)
|
|
946
|
-
.map((b) => `- ${b.branch}: ${new Date(b.lastTestedAt).toISOString()}`)
|
|
947
|
-
.join('\n');
|
|
948
|
-
branchInfo += `\n\n**Also tested on:**\n${otherBranches}`;
|
|
949
|
-
}
|
|
950
|
-
return {
|
|
951
|
-
content: [
|
|
952
|
-
{
|
|
953
|
-
type: 'text',
|
|
954
|
-
text: `# Coverage for ${requirementKey}\n\n**Last Tested:** ${lastTested.toISOString()}${branchInfo}${testLocation}`,
|
|
955
|
-
},
|
|
956
|
-
],
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
case 'get_project_coverage_summary': {
|
|
960
|
-
const { branch, sinceTimestamp, projectId } = args;
|
|
961
|
-
const project = await getProjectFromDiscovery(projectId);
|
|
962
|
-
const coverage = await queryProjectCoverage(project.projectId, project.projectSecret, CONVEX_URL, { branch, sinceTimestamp });
|
|
963
|
-
const total = coverage.tested.length + coverage.untested.length;
|
|
964
|
-
const percentage = total > 0 ? ((coverage.tested.length / total) * 100).toFixed(1) : '0.0';
|
|
965
|
-
let filterInfo = '';
|
|
966
|
-
if (branch) {
|
|
967
|
-
filterInfo += `\n**Branch:** ${branch}`;
|
|
968
|
-
}
|
|
969
|
-
if (sinceTimestamp) {
|
|
970
|
-
filterInfo += `\n**Since:** ${new Date(sinceTimestamp).toISOString()}`;
|
|
971
|
-
}
|
|
972
|
-
let testedSection = '';
|
|
973
|
-
if (coverage.tested.length > 0) {
|
|
974
|
-
const testedList = coverage.tested
|
|
975
|
-
.map((t) => {
|
|
976
|
-
const date = new Date(t.lastTestedAt).toISOString();
|
|
977
|
-
const location = t.testFile
|
|
978
|
-
? ` (\`${t.testFile}${t.testLine ? `:${t.testLine}` : ''}\`)`
|
|
979
|
-
: '';
|
|
980
|
-
return `- **${t.requirementKey}** [${t.branch}]: ${date}${location}`;
|
|
981
|
-
})
|
|
982
|
-
.join('\n');
|
|
983
|
-
testedSection = `\n\n## Tested Requirements (${coverage.tested.length})\n\n${testedList}`;
|
|
984
|
-
}
|
|
985
|
-
let untestedSection = '';
|
|
986
|
-
if (coverage.untested.length > 0) {
|
|
987
|
-
const untestedList = coverage.untested
|
|
988
|
-
.map((key) => `- ${key}`)
|
|
989
|
-
.join('\n');
|
|
990
|
-
untestedSection = `\n\n## Untested Requirements (${coverage.untested.length})\n\n${untestedList}`;
|
|
991
|
-
}
|
|
992
|
-
return {
|
|
993
|
-
content: [
|
|
994
|
-
{
|
|
995
|
-
type: 'text',
|
|
996
|
-
text: `# Project Coverage Summary\n\n**Total Requirements:** ${total}\n**Coverage:** ${percentage}%${filterInfo}${testedSection}${untestedSection}`,
|
|
997
|
-
},
|
|
998
|
-
],
|
|
999
|
-
};
|
|
1000
|
-
}
|
|
1001
|
-
case 'create_requirement_document': {
|
|
1002
|
-
const { filePath = '.requirements/example.requirements.md' } = args;
|
|
1003
|
-
// Discover existing label and key patterns in the codebase
|
|
1004
|
-
// Try to get requirements, but don't fail if no project is configured
|
|
1005
|
-
let requirements = [];
|
|
1006
|
-
try {
|
|
1007
|
-
requirements = await getRequirements();
|
|
1008
|
-
}
|
|
1009
|
-
catch {
|
|
1010
|
-
// No project configured - that's okay, we'll just not have discovered patterns
|
|
1011
|
-
}
|
|
1012
|
-
const labels = new Set();
|
|
1013
|
-
const keyPrefixes = new Set();
|
|
1014
|
-
for (const req of requirements) {
|
|
1015
|
-
// findRequirementsFiles now excludes example/fixture files automatically
|
|
1016
|
-
// Collect labels (empty string means unlabeled, so skip those)
|
|
1017
|
-
if (req.label && req.label.trim()) {
|
|
1018
|
-
labels.add(req.label);
|
|
1019
|
-
}
|
|
1020
|
-
// Collect requirement key prefixes (root IDs only)
|
|
1021
|
-
// Extract prefix from keys like "AUTH-1" -> "AUTH", "LOGIN-FLOW-2" -> "LOGIN-FLOW"
|
|
1022
|
-
if (req.path.length === 0 && req.id) {
|
|
1023
|
-
const lastDashIndex = req.id.lastIndexOf('-');
|
|
1024
|
-
if (lastDashIndex > 0) {
|
|
1025
|
-
const prefix = req.id.substring(0, lastDashIndex);
|
|
1026
|
-
keyPrefixes.add(prefix);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
const discoveredLabels = Array.from(labels).sort();
|
|
1031
|
-
const discoveredPrefixes = Array.from(keyPrefixes).sort();
|
|
1032
|
-
let labelGuidance = '';
|
|
1033
|
-
let keyGuidance = '';
|
|
1034
|
-
// Generate label guidance
|
|
1035
|
-
if (discoveredLabels.length > 0) {
|
|
1036
|
-
const labelList = discoveredLabels.slice(0, 10).map(l => `"${l}"`).join(', ');
|
|
1037
|
-
const more = discoveredLabels.length > 10 ? ` (and ${discoveredLabels.length - 10} more)` : '';
|
|
1038
|
-
labelGuidance = `**Existing labels in this codebase:** ${labelList}${more}
|
|
1039
|
-
|
|
1040
|
-
**Use these existing labels** to maintain consistency. If you're unsure which labels to use for a new requirement, ask the user.`;
|
|
1041
|
-
}
|
|
1042
|
-
else {
|
|
1043
|
-
labelGuidance = `**No existing requirements found in this codebase.**
|
|
1044
|
-
|
|
1045
|
-
**Default to unlabeled requirements** (\`0. → content\`). If the user wants labels, ask them which format they prefer. Do not choose an opinionated framework like Given/When/Then without explicit user consent.`;
|
|
1046
|
-
}
|
|
1047
|
-
// Generate key guidance
|
|
1048
|
-
if (discoveredPrefixes.length > 0) {
|
|
1049
|
-
const prefixList = discoveredPrefixes.map(p => `"${p}"`).join(', ');
|
|
1050
|
-
keyGuidance = `**Existing requirement key prefixes in this codebase:** ${prefixList}
|
|
1051
|
-
|
|
1052
|
-
**Match the existing pattern** when creating new requirement keys. Use the same domain prefixes and sequential numbering style.`;
|
|
1053
|
-
}
|
|
1054
|
-
else {
|
|
1055
|
-
keyGuidance = `**No existing requirements found in this codebase.**
|
|
1056
|
-
|
|
1057
|
-
**Use concise domain prefixes** like \`AUTH-1\`, \`LOGIN-1\`, etc. Start numbering at 1 and increment sequentially.`;
|
|
1058
|
-
}
|
|
1059
|
-
// PROJ-CONTEXT-7: Try to fetch user-provided style context from cloud
|
|
1060
|
-
let userStyleGuidance = '';
|
|
1061
|
-
try {
|
|
1062
|
-
const project = await getProjectFromDiscovery();
|
|
1063
|
-
const context = await getProjectContext(project.projectId, project.projectSecret, CONVEX_URL);
|
|
1064
|
-
if (context?.requirementsStyleContext?.trim()) {
|
|
1065
|
-
userStyleGuidance = `
|
|
1066
|
-
|
|
1067
|
-
## User-Provided Style Guidelines
|
|
1068
|
-
|
|
1069
|
-
The following style guidelines were provided by the project owner. When these conflict with the defaults above, prioritize the user's guidelines.
|
|
1070
|
-
|
|
1071
|
-
${context.requirementsStyleContext.trim()}`;
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
catch {
|
|
1075
|
-
// No credentials or cloud unavailable - continue without user context
|
|
1076
|
-
}
|
|
1077
|
-
const template = `---
|
|
1078
|
-
document:
|
|
1079
|
-
title: "Example Requirements"
|
|
1080
|
-
---
|
|
1081
|
-
|
|
1082
|
-
# Example Requirements
|
|
1083
|
-
|
|
1084
|
-
This template demonstrates the dotrequirements Markdown format and style guidelines.
|
|
1085
|
-
|
|
1086
|
-
## Syntax Overview
|
|
1087
|
-
|
|
1088
|
-
**File naming:** Use \`*.requirements.md\` pattern (colocated: \`auth.requirements.md\` or centralized: \`.requirements/auth.requirements.md\`)
|
|
1089
|
-
|
|
1090
|
-
**Block format:**
|
|
1091
|
-
\`\`\`dotrequirements
|
|
1092
|
-
KEY: Root requirement content
|
|
1093
|
-
0. → First criterion (unlabeled)
|
|
1094
|
-
1. Label → Second criterion (with label)
|
|
1095
|
-
1.0. → Nested criterion (unlabeled)
|
|
1096
|
-
\`\`\`
|
|
1097
|
-
|
|
1098
|
-
- First line: \`KEY: content\` (requirement key and description)
|
|
1099
|
-
- Criteria: \`position. Label → content\` or \`position. → content\` (unlabeled)
|
|
1100
|
-
- Position: \`0\`, \`1\`, \`2\` (top-level) or \`0.0\`, \`1.0\` (nested) - defines hierarchy
|
|
1101
|
-
- Delimiter: \`→\` or \`->\` separates optional label from content
|
|
1102
|
-
|
|
1103
|
-
## Requirement Keys: Concise and Sequential
|
|
1104
|
-
|
|
1105
|
-
${keyGuidance}
|
|
1106
|
-
|
|
1107
|
-
**Key format:** \`DOMAIN-FEATURE-N\` where N is sequential (1, 2, 3...)
|
|
1108
|
-
|
|
1109
|
-
**Best practices:**
|
|
1110
|
-
1. **Concise domains** - Use short, clear prefixes
|
|
1111
|
-
- ✅ \`AUTHZ-1\` (authorization)
|
|
1112
|
-
- ✅ \`AUTH-1\` (authentication)
|
|
1113
|
-
- ❌ \`AUTHORIZATION-1\` (too verbose)
|
|
1114
|
-
- ❌ \`REQ-IDENTITY-ACCESS-AUTHZ-1\` (too nested)
|
|
1115
|
-
|
|
1116
|
-
2. **Sequential, 1-indexed numbering** - Start at 1, no padding
|
|
1117
|
-
- ✅ \`LOGIN-1\`, \`LOGIN-2\`, \`LOGIN-3\`
|
|
1118
|
-
- ❌ \`LOGIN-0\` (don't use 0-indexing for requirement IDs)
|
|
1119
|
-
- ❌ \`LOGIN-001\` (no zero-padding)
|
|
1120
|
-
|
|
1121
|
-
3. **Unique across project** - Each key must be unique in the entire project
|
|
1122
|
-
|
|
1123
|
-
4. **Match existing patterns** - Check existing requirements first and follow their convention
|
|
1124
|
-
|
|
1125
|
-
## Labels: Match Your Codebase or Ask the User
|
|
1126
|
-
|
|
1127
|
-
${labelGuidance}
|
|
1128
|
-
|
|
1129
|
-
## Style Principles for Requirements
|
|
1130
|
-
|
|
1131
|
-
**1. Use Concrete Examples**: Replace vague language with specific, testable conditions.
|
|
1132
|
-
- ❌ "users can log in" or "works properly"
|
|
1133
|
-
- ✅ "When a registered user provides valid credentials, they are authenticated"
|
|
1134
|
-
|
|
1135
|
-
**2. Write Natural, Concise Prose**: Avoid terseness and verbosity. Use declarative style (not "should").
|
|
1136
|
-
- ❌ "registered user with valid credentials is authenticated" (too terse)
|
|
1137
|
-
- ❌ "A registered user, whose account was created on Tuesday and whose life story is as follows..." (too verbose)
|
|
1138
|
-
- ✅ "When a registered user provides valid credentials, they are authenticated"
|
|
1139
|
-
|
|
1140
|
-
**3. Keep Arrange/Act/Assert in Mind**: Well-written requirements describe preconditions, trigger, and result.
|
|
1141
|
-
- ✅ "When [preconditions:] a registered user [trigger:] provides valid credentials, [result:] they are authenticated"
|
|
1142
|
-
|
|
1143
|
-
**4. Be Framework Neutral**: Don't prescribe Given/When/Then vs AC vs other formats - focus on content quality.
|
|
1144
|
-
|
|
1145
|
-
**5. Use Named Personas**: Establish personas in parent requirements, reuse in children.
|
|
1146
|
-
- ✅ Parent: "A registered user, Jamie, can log in normally" → Child: "When Jamie provides valid credentials, they are authenticated"
|
|
1147
|
-
|
|
1148
|
-
**6. Use User-Centric Language**: Describe user experience, not technical internals.
|
|
1149
|
-
- ❌ "they are redirected to app.dotrequirements.io/redirect/dashboard"
|
|
1150
|
-
- ✅ "they are automatically brought to the dashboard"
|
|
1151
|
-
|
|
1152
|
-
**7. Single Action Per Requirement**: Don't chain multiple actions with "and then".
|
|
1153
|
-
- ❌ "When Robin provides credentials, requests an OTP, then provides the OTP..."
|
|
1154
|
-
- ✅ Break into separate requirements for each action
|
|
1155
|
-
|
|
1156
|
-
**8. Each Requirement Should Be Independent**: Requirements within a block should be independently testable. If they share preconditions or form a sequence, either nest them or restate context.
|
|
1157
|
-
- ❌ "0. → When Jordan submits signup, an account is created" / "1. → Welcome email is sent" / "2. → Dashboard appears"
|
|
1158
|
-
- ✅ Option A: Restate context - "1. → When Jordan submits signup, a welcome email is sent"
|
|
1159
|
-
- ✅ Option B: Use nesting - "0. When → Jordan submits signup" / " 0.0. Then → an account is created"
|
|
1160
|
-
- Test: Can you understand what's being tested by reading just one requirement, or do you need to read its siblings?
|
|
1161
|
-
|
|
1162
|
-
**9. Focus on Behavior, Not Design**: Describe what happens, not UI specifics.
|
|
1163
|
-
- ❌ "enters valid credentials into two single-line input fields and presses a green button"
|
|
1164
|
-
- ✅ "provides valid credentials"
|
|
1165
|
-
|
|
1166
|
-
**10. Focus on Outcomes, Not Implementation**: User perspective, even for technical requirements.
|
|
1167
|
-
- ❌ "When the app requests that Twilio send Casey an OTP from /email/POST endpoint..."
|
|
1168
|
-
- ✅ "When Casey requests an email OTP..."
|
|
1169
|
-
- Note: Even technical requirements can be user-centric: "95 percent of users experience under 1 second of delay"
|
|
1170
|
-
|
|
1171
|
-
**11. Decompose Large Requirements**: If it can't be validated with a single test, break it down.
|
|
1172
|
-
${userStyleGuidance}
|
|
1173
|
-
|
|
1174
|
-
## Example Requirements
|
|
1175
|
-
|
|
1176
|
-
**Unlabeled (recommended default):**
|
|
1177
|
-
\`\`\`dotrequirements
|
|
1178
|
-
AUTH-LOGIN-1: A registered user, Jamie, can log in to their account
|
|
1179
|
-
0. → When Jamie provides their registered email and correct password, they are authenticated and brought to their dashboard
|
|
1180
|
-
1. → When Jamie provides an incorrect password, they see an error message and remain on the login page
|
|
1181
|
-
2. → When Jamie's account has been deactivated, they see a message explaining their account status
|
|
1182
|
-
\`\`\`
|
|
1183
|
-
|
|
1184
|
-
**With labels (example only - DO NOT use opinionated formats like Given/When/Then without asking the user first):**
|
|
1185
|
-
\`\`\`dotrequirements
|
|
1186
|
-
PAYMENT-REFUND-1: A customer, Alex, receives a refund after returning an item
|
|
1187
|
-
0. Given → Alex purchased a laptop from the store 10 days ago
|
|
1188
|
-
1. Given → Alex initiates a return through their order history
|
|
1189
|
-
2. When → Alex's returned laptop is received and inspected at the warehouse
|
|
1190
|
-
3. Then → Alex receives a refund to their original payment method within 5 business days
|
|
1191
|
-
4. Then → Alex receives an email confirmation with the refund amount and expected timeline
|
|
1192
|
-
\`\`\`
|
|
1193
|
-
|
|
1194
|
-
## Referencing Requirements in Tests
|
|
1195
|
-
|
|
1196
|
-
**Use \`requirement()\` as the test description** - it returns a string:
|
|
1197
|
-
|
|
1198
|
-
\`\`\`typescript
|
|
1199
|
-
import { describe, it, expect } from 'vitest';
|
|
1200
|
-
import { requirement } from '@popoverai/dotrequirements/test';
|
|
1201
|
-
|
|
1202
|
-
describe(requirement('AUTH-LOGIN-1'), () => {
|
|
1203
|
-
describe(requirement('AUTH-LOGIN-1.given'), () => {
|
|
1204
|
-
// Arrange: Create Jamie's account
|
|
1205
|
-
});
|
|
1206
|
-
|
|
1207
|
-
describe(requirement('AUTH-LOGIN-1.when'), () => {
|
|
1208
|
-
// Act: Submit login with valid credentials
|
|
1209
|
-
|
|
1210
|
-
it(requirement('AUTH-LOGIN-1.then'), () => {
|
|
1211
|
-
// Assert: Jamie is authenticated
|
|
1212
|
-
});
|
|
1213
|
-
});
|
|
1214
|
-
});
|
|
1215
|
-
\`\`\`
|
|
1216
|
-
|
|
1217
|
-
**Test Style Principles:**
|
|
1218
|
-
|
|
1219
|
-
**1. Use requirement() AS the description**: Don't put requirement() inside test body or in comments.
|
|
1220
|
-
- ✅ \`test(requirement('AUTH-LOGIN-1'), () => { /* test code */ })\`
|
|
1221
|
-
- ✅ \`it(requirement('LOGIN-1.then'), () => { /* assert */ })\`
|
|
1222
|
-
- ❌ \`test("user can log in", () => { requirement('AUTH-LOGIN-1'); /* test code */ })\`
|
|
1223
|
-
- ❌ \`// LOGIN-1: User can log in\` (comment instead of using requirement() as description)
|
|
1224
|
-
|
|
1225
|
-
**2. Comments describe the TEST, not the requirement**: Don't copy requirement text verbatim.
|
|
1226
|
-
- ✅ \`describe(requirement('REQ-1.0'), () => { // registered user, valid credentials\`
|
|
1227
|
-
- ❌ \`describe(requirement('REQ-1.0'), () => { // 0. When a registered user provides valid credentials, they are authenticated\` (verbatim copy is a red flag)
|
|
1228
|
-
- Note: Comments should reflect what the test actually does, not just repeat what the requirement says
|
|
1229
|
-
|
|
1230
|
-
**3. Structure tests to match requirements**: Nest describe/it blocks for structured requirements.
|
|
1231
|
-
- ✅ For structured requirements: \`describe(requirement('LOGIN-1.given'))\` nested with \`describe(requirement('LOGIN-1.when'))\` and \`it(requirement('LOGIN-1.then'))\`
|
|
1232
|
-
- ✅ For simple requirements: \`test(requirement('AUTH-LOGIN-1'), () => { /* arrange, act, assert all in one */ })\`
|
|
1233
|
-
|
|
1234
|
-
**Path formats:**
|
|
1235
|
-
- \`requirement('AUTH-LOGIN-1')\` - root requirement
|
|
1236
|
-
- \`requirement('AUTH-LOGIN-1.0')\` - by numeric position
|
|
1237
|
-
- \`requirement('AUTH-LOGIN-1.given')\` - by label (case-insensitive)
|
|
1238
|
-
- \`requirement('AUTH-LOGIN-1.given#1')\` - disambiguate duplicate labels`;
|
|
1239
|
-
return {
|
|
1240
|
-
content: [
|
|
1241
|
-
{
|
|
1242
|
-
type: 'text',
|
|
1243
|
-
text: `# Requirements File Template\n\nHere's a comprehensive template for \`${filePath}\` with format and style guidance:\n\n\`\`\`markdown\n${template}\`\`\`\n\n## Next Steps\n\n1. **Create file**: Use Write tool to create \`${filePath}\` based on this template\n2. **Refine style** (optional): Call \`style_check\` for AI feedback on style and best practices\n3. **Validate syntax**: Call \`validate_requirements\` to verify format is correct\n4. **Push to Convex**: Call \`push_requirements\` to sync requirements to data layer\n\n**Note**: Requirements files can be colocated with code (\`src/auth.requirements.md\`) or centralized in \`.requirements/\` directory.`,
|
|
1244
|
-
},
|
|
1245
|
-
],
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
case 'validate_requirements': {
|
|
1249
|
-
const { filePath } = args;
|
|
1250
|
-
const { parseRequirementsFromFile, validateForPush } = await import('../schema/index.js');
|
|
1251
|
-
const fs = await import('fs');
|
|
1252
|
-
const path = await import('path');
|
|
1253
|
-
const fullPath = path.resolve(WORKSPACE_ROOT, filePath);
|
|
1254
|
-
if (!fs.existsSync(fullPath)) {
|
|
1255
|
-
return {
|
|
1256
|
-
content: [
|
|
1257
|
-
{
|
|
1258
|
-
type: 'text',
|
|
1259
|
-
text: `File not found: ${filePath}`,
|
|
1260
|
-
},
|
|
1261
|
-
],
|
|
1262
|
-
isError: true,
|
|
1263
|
-
};
|
|
1264
|
-
}
|
|
1265
|
-
try {
|
|
1266
|
-
const parsed = parseRequirementsFromFile(fullPath);
|
|
1267
|
-
const reqCount = Object.keys(parsed.requirements).length;
|
|
1268
|
-
// Check push readiness
|
|
1269
|
-
const pushValidation = validateForPush(parsed.metadata);
|
|
1270
|
-
let headline;
|
|
1271
|
-
let pushDetails = '';
|
|
1272
|
-
if (!pushValidation.valid) {
|
|
1273
|
-
headline = `❌ \`${filePath}\` - Not push-ready\n- ${pushValidation.reason}`;
|
|
1274
|
-
}
|
|
1275
|
-
else {
|
|
1276
|
-
const actionLabel = pushValidation.action === 'create' ? 'Will create new document' : 'Will update existing document';
|
|
1277
|
-
headline = `✓ \`${filePath}\` - ${actionLabel}`;
|
|
1278
|
-
if (pushValidation.warning) {
|
|
1279
|
-
pushDetails = `\n- ⚠️ ${pushValidation.warning}`;
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
return {
|
|
1283
|
-
content: [
|
|
1284
|
-
{
|
|
1285
|
-
type: 'text',
|
|
1286
|
-
text: `${headline}${pushDetails}\n\n**Metadata:**\n- Version: ${parsed.metadata.version ?? '(none)'}\n- Document: ${parsed.metadata.document?.title || '(none)'}\n- Document ID: ${parsed.metadata.document?.id || '(none)'}\n\n**Requirements:** ${reqCount} root requirement(s) found`,
|
|
1287
|
-
},
|
|
1288
|
-
],
|
|
1289
|
-
};
|
|
1290
|
-
}
|
|
1291
|
-
catch (error) {
|
|
1292
|
-
return {
|
|
1293
|
-
content: [
|
|
1294
|
-
{
|
|
1295
|
-
type: 'text',
|
|
1296
|
-
text: `✗ Validation failed for \`${filePath}\`:\n\n${error instanceof Error ? error.message : String(error)}`,
|
|
1297
|
-
},
|
|
1298
|
-
],
|
|
1299
|
-
isError: true,
|
|
1300
|
-
};
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
case 'push_requirements': {
|
|
1304
|
-
const { filePath, confirmed = false, projectId: projectIdParam } = args;
|
|
1305
|
-
// Get project (credentials come from project object, works with both file and env auth)
|
|
1306
|
-
const project = await getProjectFromDiscovery(projectIdParam);
|
|
1307
|
-
// Determine files to push
|
|
1308
|
-
let filesToPush;
|
|
1309
|
-
if (filePath) {
|
|
1310
|
-
const fullPath = resolve(project.path, filePath);
|
|
1311
|
-
if (!existsSync(fullPath)) {
|
|
1312
|
-
return {
|
|
1313
|
-
content: [
|
|
1314
|
-
{
|
|
1315
|
-
type: 'text',
|
|
1316
|
-
text: `File not found: ${filePath}`,
|
|
1317
|
-
},
|
|
1318
|
-
],
|
|
1319
|
-
isError: true,
|
|
1320
|
-
};
|
|
1321
|
-
}
|
|
1322
|
-
filesToPush = [fullPath];
|
|
1323
|
-
}
|
|
1324
|
-
else {
|
|
1325
|
-
// Use same file discovery as CLI - searches entire workspace
|
|
1326
|
-
filesToPush = await findRequirementsFiles(project.path);
|
|
1327
|
-
if (filesToPush.length === 0) {
|
|
1328
|
-
return {
|
|
1329
|
-
content: [
|
|
1330
|
-
{
|
|
1331
|
-
type: 'text',
|
|
1332
|
-
text: 'No *.requirements.md files found. Nothing to push.',
|
|
1333
|
-
},
|
|
1334
|
-
],
|
|
1335
|
-
};
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
// Parse files
|
|
1339
|
-
const { parsedFiles, totalRequirements } = parseFilesForPush(filesToPush);
|
|
1340
|
-
// Build credentials
|
|
1341
|
-
const credentials = {
|
|
1342
|
-
projectId: project.projectId,
|
|
1343
|
-
projectSecret: project.projectSecret,
|
|
1344
|
-
convexUrl: CONVEX_URL,
|
|
1345
|
-
};
|
|
1346
|
-
// Run dry run (always, even when confirmed - ensures fresh state)
|
|
1347
|
-
const dryRunResult = await dryRunPush(parsedFiles, credentials);
|
|
1348
|
-
// Check if there's anything to push
|
|
1349
|
-
const pushableCount = dryRunResult.updates.length +
|
|
1350
|
-
dryRunResult.creates.length +
|
|
1351
|
-
dryRunResult.notFound.length;
|
|
1352
|
-
if (pushableCount === 0 && dryRunResult.invalid.length > 0) {
|
|
1353
|
-
// Only invalid files
|
|
1354
|
-
const invalidList = dryRunResult.invalid
|
|
1355
|
-
.map(({ file, result }) => `- ${basename(file.filePath)}: ${result.error}`)
|
|
1356
|
-
.join('\n');
|
|
1357
|
-
return {
|
|
1358
|
-
content: [
|
|
1359
|
-
{
|
|
1360
|
-
type: 'text',
|
|
1361
|
-
text: `✗ No valid documents to push.\n\n**Invalid files:**\n${invalidList}`,
|
|
1362
|
-
},
|
|
1363
|
-
],
|
|
1364
|
-
isError: true,
|
|
1365
|
-
};
|
|
1366
|
-
}
|
|
1367
|
-
if (!confirmed) {
|
|
1368
|
-
// First call: show preview with real dry run results
|
|
1369
|
-
let summary = `# Push Preview\n\n**Files:** ${parsedFiles.length}\n**Requirements:** ${totalRequirements}\n\n`;
|
|
1370
|
-
if (dryRunResult.updates.length > 0) {
|
|
1371
|
-
summary += `## Updates (${dryRunResult.updates.length})\n`;
|
|
1372
|
-
for (const { file, result } of dryRunResult.updates) {
|
|
1373
|
-
const fileName = basename(file.filePath);
|
|
1374
|
-
const hasConflict = dryRunResult.conflicts.some((c) => c.item.file === file);
|
|
1375
|
-
const conflictNote = hasConflict ? ' ⚠️ [cloud changed since pull]' : '';
|
|
1376
|
-
const warningNote = result.warning ? ` (${result.warning})` : '';
|
|
1377
|
-
summary += `- ~ ${fileName}${conflictNote}${warningNote}\n`;
|
|
1378
|
-
}
|
|
1379
|
-
summary += '\n';
|
|
1380
|
-
}
|
|
1381
|
-
if (dryRunResult.creates.length > 0) {
|
|
1382
|
-
summary += `## New Documents (${dryRunResult.creates.length})\n`;
|
|
1383
|
-
for (const { file, result } of dryRunResult.creates) {
|
|
1384
|
-
const fileName = basename(file.filePath);
|
|
1385
|
-
const warningNote = result.warning ? ` (${result.warning})` : '';
|
|
1386
|
-
summary += `- + ${fileName}${warningNote}\n`;
|
|
1387
|
-
}
|
|
1388
|
-
summary += '\n';
|
|
1389
|
-
}
|
|
1390
|
-
if (dryRunResult.notFound.length > 0) {
|
|
1391
|
-
summary += `## Not Found in Cloud (${dryRunResult.notFound.length}) - will create new\n`;
|
|
1392
|
-
for (const { file, result } of dryRunResult.notFound) {
|
|
1393
|
-
const fileName = basename(file.filePath);
|
|
1394
|
-
summary += `- ! ${fileName} (ID: ${result.documentId})\n`;
|
|
1395
|
-
}
|
|
1396
|
-
summary += '\n';
|
|
1397
|
-
}
|
|
1398
|
-
if (dryRunResult.invalid.length > 0) {
|
|
1399
|
-
summary += `## Skipped - Invalid (${dryRunResult.invalid.length})\n`;
|
|
1400
|
-
for (const { file, result } of dryRunResult.invalid) {
|
|
1401
|
-
const fileName = basename(file.filePath);
|
|
1402
|
-
summary += `- ✗ ${fileName}: ${result.error}\n`;
|
|
1403
|
-
}
|
|
1404
|
-
summary += '\n';
|
|
1405
|
-
}
|
|
1406
|
-
if (dryRunResult.conflicts.length > 0) {
|
|
1407
|
-
summary += `⚠️ **Warning:** ${dryRunResult.conflicts.length} file(s) have cloud changes since last pull. Pushing will overwrite those changes.\n\n`;
|
|
1408
|
-
}
|
|
1409
|
-
summary += `**To proceed:** Call this tool again with \`confirmed: true\``;
|
|
1410
|
-
return {
|
|
1411
|
-
content: [
|
|
1412
|
-
{
|
|
1413
|
-
type: 'text',
|
|
1414
|
-
text: summary,
|
|
1415
|
-
},
|
|
1416
|
-
],
|
|
1417
|
-
};
|
|
1418
|
-
}
|
|
1419
|
-
// Second call: execute push
|
|
1420
|
-
try {
|
|
1421
|
-
const result = await executePush(dryRunResult, credentials);
|
|
1422
|
-
let output = '✓ Push complete!\n\n';
|
|
1423
|
-
if (result.created > 0) {
|
|
1424
|
-
output += `**Created:** ${result.created} document(s)\n`;
|
|
1425
|
-
}
|
|
1426
|
-
if (result.updated > 0) {
|
|
1427
|
-
output += `**Updated:** ${result.updated} document(s)\n`;
|
|
1428
|
-
}
|
|
1429
|
-
if (result.errors.length > 0) {
|
|
1430
|
-
output += `\n**Errors:**\n`;
|
|
1431
|
-
for (const { fileName, error } of result.errors) {
|
|
1432
|
-
output += `- ${fileName}: ${error}\n`;
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
return {
|
|
1436
|
-
content: [
|
|
1437
|
-
{
|
|
1438
|
-
type: 'text',
|
|
1439
|
-
text: output,
|
|
1440
|
-
},
|
|
1441
|
-
],
|
|
1442
|
-
};
|
|
1443
|
-
}
|
|
1444
|
-
catch (error) {
|
|
1445
|
-
return {
|
|
1446
|
-
content: [
|
|
1447
|
-
{
|
|
1448
|
-
type: 'text',
|
|
1449
|
-
text: `✗ Push failed:\n\n${error instanceof Error ? error.message : String(error)}`,
|
|
1450
|
-
},
|
|
1451
|
-
],
|
|
1452
|
-
isError: true,
|
|
1453
|
-
};
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
case 'style_check': {
|
|
1457
|
-
const { filePath, model, requirementKeys } = args;
|
|
1458
|
-
const fs = await import('fs');
|
|
1459
|
-
const path = await import('path');
|
|
1460
|
-
const fullPath = path.resolve(WORKSPACE_ROOT, filePath);
|
|
1461
|
-
if (!fs.existsSync(fullPath)) {
|
|
1462
|
-
return {
|
|
1463
|
-
content: [
|
|
1464
|
-
{
|
|
1465
|
-
type: 'text',
|
|
1466
|
-
text: `File not found: ${filePath}`,
|
|
1467
|
-
},
|
|
1468
|
-
],
|
|
1469
|
-
isError: true,
|
|
1470
|
-
};
|
|
1471
|
-
}
|
|
1472
|
-
// Read file contents
|
|
1473
|
-
const fileContents = fs.readFileSync(fullPath, 'utf-8');
|
|
1474
|
-
// Detect file type
|
|
1475
|
-
const isRequirementsFile = filePath.endsWith('.requirements.md');
|
|
1476
|
-
const isTestFile = /\.(test|spec)\.(js|jsx|ts|tsx)$/.test(filePath);
|
|
1477
|
-
if (!isRequirementsFile && !isTestFile) {
|
|
1478
|
-
return {
|
|
1479
|
-
content: [
|
|
1480
|
-
{
|
|
1481
|
-
type: 'text',
|
|
1482
|
-
text: `Unsupported file type. File must be:\n- Requirements file: *.requirements.md\n- Test file: *.test.{js,jsx,ts,tsx} or *.spec.{js,jsx,ts,tsx}`,
|
|
1483
|
-
},
|
|
1484
|
-
],
|
|
1485
|
-
isError: true,
|
|
1486
|
-
};
|
|
1487
|
-
}
|
|
1488
|
-
const fileType = isRequirementsFile ? 'requirements' : 'test';
|
|
1489
|
-
// If requirementKeys provided, filter the file to only those requirements
|
|
1490
|
-
let fileContentsToCheck = fileContents;
|
|
1491
|
-
let scopeNote = '';
|
|
1492
|
-
if (requirementKeys && requirementKeys.length > 0) {
|
|
1493
|
-
if (!isRequirementsFile) {
|
|
1494
|
-
return {
|
|
1495
|
-
content: [
|
|
1496
|
-
{
|
|
1497
|
-
type: 'text',
|
|
1498
|
-
text: `requirementKeys can only be used with requirements files (*.requirements.md), not test files.`,
|
|
1499
|
-
},
|
|
1500
|
-
],
|
|
1501
|
-
isError: true,
|
|
1502
|
-
};
|
|
1503
|
-
}
|
|
1504
|
-
try {
|
|
1505
|
-
const { filteredContent, foundKeys, missingKeys } = filterRequirementsByKeys(fileContents, requirementKeys);
|
|
1506
|
-
if (foundKeys.length === 0) {
|
|
1507
|
-
return {
|
|
1508
|
-
content: [
|
|
1509
|
-
{
|
|
1510
|
-
type: 'text',
|
|
1511
|
-
text: `None of the specified requirement keys were found in ${filePath}: ${requirementKeys.join(', ')}`,
|
|
1512
|
-
},
|
|
1513
|
-
],
|
|
1514
|
-
isError: true,
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
fileContentsToCheck = filteredContent;
|
|
1518
|
-
if (missingKeys.length > 0) {
|
|
1519
|
-
scopeNote = `\n\n> **Note:** Some specified keys were not found in the file: ${missingKeys.join(', ')}`;
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
catch (error) {
|
|
1523
|
-
return {
|
|
1524
|
-
content: [
|
|
1525
|
-
{
|
|
1526
|
-
type: 'text',
|
|
1527
|
-
text: `Failed to filter requirements: ${error instanceof Error ? error.message : String(error)}`,
|
|
1528
|
-
},
|
|
1529
|
-
],
|
|
1530
|
-
isError: true,
|
|
1531
|
-
};
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
// Get Convex config for credentials - walk up from file's directory to find project
|
|
1535
|
-
const fileDir = path.dirname(fullPath);
|
|
1536
|
-
let project;
|
|
1537
|
-
try {
|
|
1538
|
-
// If using env auth, go straight to getProjectFromDiscovery (handles env credentials)
|
|
1539
|
-
if (USE_ENV_AUTH) {
|
|
1540
|
-
project = await getProjectFromDiscovery();
|
|
1541
|
-
}
|
|
1542
|
-
else {
|
|
1543
|
-
// Try to find a project starting from the file's directory
|
|
1544
|
-
const result = await discoverProjects(fileDir);
|
|
1545
|
-
if (result.type === 'none') {
|
|
1546
|
-
// If no project found from file dir, try from WORKSPACE_ROOT
|
|
1547
|
-
project = await getProjectFromDiscovery();
|
|
1548
|
-
}
|
|
1549
|
-
else if (result.type === 'single') {
|
|
1550
|
-
project = result.project;
|
|
1551
|
-
}
|
|
1552
|
-
else {
|
|
1553
|
-
// Multiple projects - can't auto-detect which one to use
|
|
1554
|
-
throw new Error('Multiple projects found - cannot auto-detect for this file');
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
catch (error) {
|
|
1559
|
-
return {
|
|
1560
|
-
content: [
|
|
1561
|
-
{
|
|
1562
|
-
type: 'text',
|
|
1563
|
-
text: `Style check requires project credentials. Run \`dotrequirements link\` to connect to cloud.\n\nError: ${error instanceof Error ? error.message : String(error)}`,
|
|
1564
|
-
},
|
|
1565
|
-
],
|
|
1566
|
-
isError: true,
|
|
1567
|
-
};
|
|
1568
|
-
}
|
|
1569
|
-
// Use credentials from project (works with both file-based and env-based auth)
|
|
1570
|
-
const { projectId: projId, projectSecret: projSecret } = project;
|
|
1571
|
-
// Call Vercel API endpoint for style checking
|
|
1572
|
-
// Default to production, allow override via env var for local dev
|
|
1573
|
-
const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL || 'https://app.dotrequirements.io';
|
|
1574
|
-
try {
|
|
1575
|
-
const response = await fetch(`${apiBaseUrl}/api/style-check`, {
|
|
1576
|
-
method: 'POST',
|
|
1577
|
-
headers: {
|
|
1578
|
-
'Content-Type': 'application/json',
|
|
1579
|
-
},
|
|
1580
|
-
body: JSON.stringify({
|
|
1581
|
-
projectId: projId,
|
|
1582
|
-
projectSecret: projSecret,
|
|
1583
|
-
fileContents: fileContentsToCheck,
|
|
1584
|
-
fileType,
|
|
1585
|
-
model,
|
|
1586
|
-
}),
|
|
1587
|
-
});
|
|
1588
|
-
if (!response.ok) {
|
|
1589
|
-
const errorData = await response.json();
|
|
1590
|
-
return {
|
|
1591
|
-
content: [
|
|
1592
|
-
{
|
|
1593
|
-
type: 'text',
|
|
1594
|
-
text: `Style check failed: ${errorData.error || response.statusText}`,
|
|
1595
|
-
},
|
|
1596
|
-
],
|
|
1597
|
-
isError: true,
|
|
1598
|
-
};
|
|
1599
|
-
}
|
|
1600
|
-
const data = await response.json();
|
|
1601
|
-
const scopeLabel = requirementKeys && requirementKeys.length > 0
|
|
1602
|
-
? ` (${requirementKeys.join(', ')})`
|
|
1603
|
-
: '';
|
|
1604
|
-
return {
|
|
1605
|
-
content: [
|
|
1606
|
-
{
|
|
1607
|
-
type: 'text',
|
|
1608
|
-
text: `# Style Check Results for \`${filePath}\`${scopeLabel}\n\n${data.feedback}${scopeNote}${STYLE_CHECK_GUIDANCE}`,
|
|
1609
|
-
},
|
|
1610
|
-
],
|
|
1611
|
-
};
|
|
1612
|
-
}
|
|
1613
|
-
catch (error) {
|
|
1614
|
-
return {
|
|
1615
|
-
content: [
|
|
1616
|
-
{
|
|
1617
|
-
type: 'text',
|
|
1618
|
-
text: `Style check error: ${error instanceof Error ? error.message : String(error)}`,
|
|
1619
|
-
},
|
|
1620
|
-
],
|
|
1621
|
-
isError: true,
|
|
1622
|
-
};
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
case 'review_test': {
|
|
1626
|
-
const { testFilePath, projectId } = args;
|
|
1627
|
-
const fs = await import('fs');
|
|
1628
|
-
const path = await import('path');
|
|
1629
|
-
const fullPath = path.resolve(WORKSPACE_ROOT, testFilePath);
|
|
1630
|
-
if (!fs.existsSync(fullPath)) {
|
|
1631
|
-
return {
|
|
1632
|
-
content: [
|
|
1633
|
-
{
|
|
1634
|
-
type: 'text',
|
|
1635
|
-
text: `File not found: ${testFilePath}`,
|
|
1636
|
-
},
|
|
1637
|
-
],
|
|
1638
|
-
isError: true,
|
|
1639
|
-
};
|
|
1640
|
-
}
|
|
1641
|
-
// Verify it's a test file
|
|
1642
|
-
const isTestFile = /\.(test|spec)\.(js|jsx|ts|tsx)$/.test(testFilePath);
|
|
1643
|
-
if (!isTestFile) {
|
|
1644
|
-
return {
|
|
1645
|
-
content: [
|
|
1646
|
-
{
|
|
1647
|
-
type: 'text',
|
|
1648
|
-
text: `File must be a test file: *.test.{js,jsx,ts,tsx} or *.spec.{js,jsx,ts,tsx}`,
|
|
1649
|
-
},
|
|
1650
|
-
],
|
|
1651
|
-
isError: true,
|
|
1652
|
-
};
|
|
1653
|
-
}
|
|
1654
|
-
// Read test file contents
|
|
1655
|
-
const testFileContents = fs.readFileSync(fullPath, 'utf-8');
|
|
1656
|
-
// Extract requirement IDs from the test file using regex
|
|
1657
|
-
const requirementIdMatches = testFileContents.matchAll(/requirement\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g);
|
|
1658
|
-
const requirementIds = new Set();
|
|
1659
|
-
for (const match of requirementIdMatches) {
|
|
1660
|
-
requirementIds.add(match[1]);
|
|
1661
|
-
}
|
|
1662
|
-
// Get project
|
|
1663
|
-
let project;
|
|
1664
|
-
try {
|
|
1665
|
-
project = await getProjectFromDiscovery(projectId);
|
|
1666
|
-
}
|
|
1667
|
-
catch (error) {
|
|
1668
|
-
return {
|
|
1669
|
-
content: [
|
|
1670
|
-
{
|
|
1671
|
-
type: 'text',
|
|
1672
|
-
text: `Test review requires a configured dotrequirements project.\n\nError: ${error instanceof Error ? error.message : String(error)}`,
|
|
1673
|
-
},
|
|
1674
|
-
],
|
|
1675
|
-
isError: true,
|
|
1676
|
-
};
|
|
1677
|
-
}
|
|
1678
|
-
// Load all requirements for this project
|
|
1679
|
-
const allRequirements = await getRequirements(projectId);
|
|
1680
|
-
// Group requirement IDs by their root to avoid sending duplicate trees
|
|
1681
|
-
// e.g., NAV-3.0, NAV-3.1, NAV-3.2 all belong to root NAV-3
|
|
1682
|
-
const rootToTestedIds = new Map();
|
|
1683
|
-
for (const reqId of requirementIds) {
|
|
1684
|
-
const req = allRequirements.find(r => r.id === reqId || r.rootId === reqId);
|
|
1685
|
-
if (req) {
|
|
1686
|
-
if (!rootToTestedIds.has(req.rootId)) {
|
|
1687
|
-
rootToTestedIds.set(req.rootId, new Set());
|
|
1688
|
-
}
|
|
1689
|
-
rootToTestedIds.get(req.rootId).add(reqId);
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
// Build requirements array with one entry per unique root tree
|
|
1693
|
-
const requirements = [];
|
|
1694
|
-
for (const [rootId, testedIds] of rootToTestedIds) {
|
|
1695
|
-
const tree = getRequirementTree(allRequirements, rootId);
|
|
1696
|
-
const formattedTree = formatRequirementTree(tree);
|
|
1697
|
-
requirements.push({
|
|
1698
|
-
id: rootId,
|
|
1699
|
-
content: formattedTree,
|
|
1700
|
-
testedIds: Array.from(testedIds).sort(),
|
|
1701
|
-
});
|
|
1702
|
-
}
|
|
1703
|
-
// Get Convex config
|
|
1704
|
-
// Use credentials from project (works with both file-based and env-based auth)
|
|
1705
|
-
const { projectId: projId, projectSecret: projSecret } = project;
|
|
1706
|
-
// Call Vercel API endpoint for test review
|
|
1707
|
-
// Default to production, allow override via env var for local dev
|
|
1708
|
-
const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL || 'https://app.dotrequirements.io';
|
|
1709
|
-
try {
|
|
1710
|
-
const response = await fetch(`${apiBaseUrl}/api/review-test`, {
|
|
1711
|
-
method: 'POST',
|
|
1712
|
-
headers: {
|
|
1713
|
-
'Content-Type': 'application/json',
|
|
1714
|
-
},
|
|
1715
|
-
body: JSON.stringify({
|
|
1716
|
-
projectId: projId,
|
|
1717
|
-
projectSecret: projSecret,
|
|
1718
|
-
testFileContents,
|
|
1719
|
-
requirements,
|
|
1720
|
-
}),
|
|
1721
|
-
});
|
|
1722
|
-
if (!response.ok) {
|
|
1723
|
-
const errorData = await response.json();
|
|
1724
|
-
return {
|
|
1725
|
-
content: [
|
|
1726
|
-
{
|
|
1727
|
-
type: 'text',
|
|
1728
|
-
text: `Test review failed: ${errorData.error || response.statusText}`,
|
|
1729
|
-
},
|
|
1730
|
-
],
|
|
1731
|
-
isError: true,
|
|
1732
|
-
};
|
|
1733
|
-
}
|
|
1734
|
-
const data = await response.json();
|
|
1735
|
-
return {
|
|
1736
|
-
content: [
|
|
1737
|
-
{
|
|
1738
|
-
type: 'text',
|
|
1739
|
-
text: `# Test Review Results for \`${testFilePath}\`\n\n${data.feedback}`,
|
|
1740
|
-
},
|
|
1741
|
-
],
|
|
1742
|
-
};
|
|
1743
|
-
}
|
|
1744
|
-
catch (error) {
|
|
1745
|
-
return {
|
|
1746
|
-
content: [
|
|
1747
|
-
{
|
|
1748
|
-
type: 'text',
|
|
1749
|
-
text: `Test review error: ${error instanceof Error ? error.message : String(error)}`,
|
|
1750
|
-
},
|
|
1751
|
-
],
|
|
1752
|
-
isError: true,
|
|
1753
|
-
};
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
562
|
+
case 'debug_mcp_environment':
|
|
563
|
+
return handleDebugMcpEnvironment(args, handlerContext);
|
|
564
|
+
case 'search_requirements':
|
|
565
|
+
return handleSearchRequirements(args, handlerContext);
|
|
566
|
+
case 'get_requirement':
|
|
567
|
+
return handleGetRequirement(args, handlerContext);
|
|
568
|
+
case 'list_untested_requirements':
|
|
569
|
+
return handleListUntestedRequirements(args, handlerContext);
|
|
570
|
+
case 'list_all_requirements':
|
|
571
|
+
return handleListAllRequirements(args, handlerContext);
|
|
572
|
+
case 'get_requirements_by_test':
|
|
573
|
+
return handleGetRequirementsByTest(args, handlerContext);
|
|
574
|
+
case 'get_tests_by_requirement':
|
|
575
|
+
return handleGetTestsByRequirement(args, handlerContext);
|
|
576
|
+
case 'get_requirement_coverage':
|
|
577
|
+
return handleGetRequirementCoverage(args, handlerContext);
|
|
578
|
+
case 'get_project_coverage_summary':
|
|
579
|
+
return handleGetProjectCoverageSummary(args, handlerContext);
|
|
580
|
+
case 'create_requirement_document':
|
|
581
|
+
return handleCreateRequirementDocument(args, handlerContext);
|
|
582
|
+
case 'validate_requirements':
|
|
583
|
+
return handleValidateRequirements(args, handlerContext);
|
|
584
|
+
case 'push_requirements':
|
|
585
|
+
return handlePushRequirements(args, handlerContext);
|
|
586
|
+
case 'style_check':
|
|
587
|
+
return handleStyleCheck(args, handlerContext, { useEnvAuth: USE_ENV_AUTH, apiBaseUrl: process.env.DOTREQUIREMENTS_API_URL });
|
|
588
|
+
case 'review_test':
|
|
589
|
+
return handleReviewTest(args, handlerContext, { apiBaseUrl: process.env.DOTREQUIREMENTS_API_URL });
|
|
1756
590
|
default:
|
|
1757
591
|
return {
|
|
1758
592
|
content: [
|
|
@@ -1786,5 +620,8 @@ async function main() {
|
|
|
1786
620
|
console.error('process.cwd():', process.cwd());
|
|
1787
621
|
console.error('REQUIREMENTS_DIR env:', process.env.REQUIREMENTS_DIR);
|
|
1788
622
|
}
|
|
1789
|
-
|
|
623
|
+
// Only run when executed directly, not when imported (enables InMemoryTransport testing)
|
|
624
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
625
|
+
main().catch(console.error);
|
|
626
|
+
}
|
|
1790
627
|
//# sourceMappingURL=index.js.map
|