@popoverai/dotrequirements 0.11.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.
- package/README.md +478 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +82 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +355 -0
- package/dist/commands/link.d.ts +15 -0
- package/dist/commands/link.js +156 -0
- package/dist/commands/login.d.ts +12 -0
- package/dist/commands/login.js +117 -0
- package/dist/commands/logout.d.ts +5 -0
- package/dist/commands/logout.js +17 -0
- package/dist/commands/mcp-setup.d.ts +5 -0
- package/dist/commands/mcp-setup.js +367 -0
- package/dist/commands/mcp.d.ts +6 -0
- package/dist/commands/mcp.js +10 -0
- package/dist/commands/pull.d.ts +7 -0
- package/dist/commands/pull.js +171 -0
- package/dist/commands/push.d.ts +11 -0
- package/dist/commands/push.js +310 -0
- package/dist/commands/test.d.ts +6 -0
- package/dist/commands/test.js +78 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +16 -0
- package/dist/convex.d.ts +56 -0
- package/dist/convex.js +58 -0
- package/dist/harness/cache.d.ts +135 -0
- package/dist/harness/cache.js +342 -0
- package/dist/harness/convexReporting.d.ts +15 -0
- package/dist/harness/convexReporting.js +136 -0
- package/dist/harness/coverageCache.d.ts +30 -0
- package/dist/harness/coverageCache.js +70 -0
- package/dist/harness/finalize.d.ts +48 -0
- package/dist/harness/finalize.js +299 -0
- package/dist/harness/index.d.ts +70 -0
- package/dist/harness/index.js +103 -0
- package/dist/harness/localReporting.d.ts +6 -0
- package/dist/harness/localReporting.js +49 -0
- package/dist/harness/prepare.d.ts +41 -0
- package/dist/harness/prepare.js +83 -0
- package/dist/harness/requirementsLoader.d.ts +45 -0
- package/dist/harness/requirementsLoader.js +201 -0
- package/dist/harness/tracking.d.ts +49 -0
- package/dist/harness/tracking.js +179 -0
- package/dist/harness/types.d.ts +12 -0
- package/dist/harness/types.js +6 -0
- package/dist/mcp/convexClient.d.ts +43 -0
- package/dist/mcp/convexClient.js +101 -0
- package/dist/mcp/grep.d.ts +24 -0
- package/dist/mcp/grep.js +261 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.js +1758 -0
- package/dist/mcp/requirements.d.ts +47 -0
- package/dist/mcp/requirements.js +141 -0
- package/dist/mcp/testCodeExtractor.d.ts +22 -0
- package/dist/mcp/testCodeExtractor.js +152 -0
- package/dist/mcp/types.d.ts +27 -0
- package/dist/mcp/types.js +2 -0
- package/dist/schema/browser.d.ts +12 -0
- package/dist/schema/browser.js +24 -0
- package/dist/schema/builder.d.ts +25 -0
- package/dist/schema/builder.js +125 -0
- package/dist/schema/conversions.d.ts +69 -0
- package/dist/schema/conversions.js +201 -0
- package/dist/schema/index.d.ts +14 -0
- package/dist/schema/index.js +24 -0
- package/dist/schema/parser-core.d.ts +61 -0
- package/dist/schema/parser-core.js +247 -0
- package/dist/schema/parser.d.ts +44 -0
- package/dist/schema/parser.js +295 -0
- package/dist/schema/resolver.d.ts +66 -0
- package/dist/schema/resolver.js +185 -0
- package/dist/schema/schemas.d.ts +312 -0
- package/dist/schema/schemas.js +258 -0
- package/dist/schema/test-schema.d.ts +5 -0
- package/dist/schema/test-schema.js +81 -0
- package/dist/templates/antigravity-gemini.md +3 -0
- package/dist/templates/antigravity-overview-rule.md +3 -0
- package/dist/templates/antigravity-test-rule.md +3 -0
- package/dist/templates/behavioral-core.md +25 -0
- package/dist/templates/claude-code-overview-skill.md +6 -0
- package/dist/templates/claude-code-skill.md +6 -0
- package/dist/templates/claude-code-test-skill.md +6 -0
- package/dist/templates/codex-agents.md +3 -0
- package/dist/templates/codex-overview-agents.md +3 -0
- package/dist/templates/codex-test-agents.md +3 -0
- package/dist/templates/cursor-overview-rule.mdc +5 -0
- package/dist/templates/cursor-rule.mdc +5 -0
- package/dist/templates/cursor-test-rule.mdc +5 -0
- package/dist/templates/example-requirements.d.ts +8 -0
- package/dist/templates/example-requirements.js +88 -0
- package/dist/templates/example-requirements.ts +88 -0
- package/dist/templates/overview-core.md +27 -0
- package/dist/templates/requirements-readme.d.ts +5 -0
- package/dist/templates/requirements-readme.js +31 -0
- package/dist/templates/requirements-readme.ts +30 -0
- package/dist/templates/test-writing-core.md +72 -0
- package/dist/utils/brand.d.ts +5 -0
- package/dist/utils/brand.js +8 -0
- package/dist/utils/browser-launch.d.ts +19 -0
- package/dist/utils/browser-launch.js +36 -0
- package/dist/utils/detect-existing-project.d.ts +5 -0
- package/dist/utils/detect-existing-project.js +34 -0
- package/dist/utils/env.d.ts +19 -0
- package/dist/utils/env.js +56 -0
- package/dist/utils/gitignore.d.ts +7 -0
- package/dist/utils/gitignore.js +29 -0
- package/dist/utils/local-project.d.ts +31 -0
- package/dist/utils/local-project.js +33 -0
- package/dist/utils/oauth-callback-server.d.ts +28 -0
- package/dist/utils/oauth-callback-server.js +156 -0
- package/dist/utils/oauth-flow.d.ts +22 -0
- package/dist/utils/oauth-flow.js +120 -0
- package/dist/utils/project-discovery.d.ts +57 -0
- package/dist/utils/project-discovery.js +146 -0
- package/dist/utils/project-name.d.ts +8 -0
- package/dist/utils/project-name.js +48 -0
- package/dist/utils/project-selector.d.ts +25 -0
- package/dist/utils/project-selector.js +69 -0
- package/dist/utils/prompts.d.ts +33 -0
- package/dist/utils/prompts.js +60 -0
- package/dist/utils/templates.d.ts +29 -0
- package/dist/utils/templates.js +67 -0
- package/dist/utils/token-refresh.d.ts +24 -0
- package/dist/utils/token-refresh.js +69 -0
- package/dist/utils/token-storage.d.ts +31 -0
- package/dist/utils/token-storage.js +57 -0
- package/package.json +82 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finalize function for the test harness.
|
|
3
|
+
*
|
|
4
|
+
* Called after tests complete (e.g., in Vitest globalTeardown) to:
|
|
5
|
+
* 1. Read tracking data
|
|
6
|
+
* 2. Generate local coverage report
|
|
7
|
+
* 3. Report to Convex cloud (if configured)
|
|
8
|
+
* 4. Clean up tracking data
|
|
9
|
+
*
|
|
10
|
+
* Implements: HARNESS-FINALIZE-1, HARNESS-FINALIZE-2, HARNESS-FINALIZE-3, HARNESS-FINALIZE-4
|
|
11
|
+
*/
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import { findRequirementsDir, findProjectRoot, getTestRunId, cleanupTestRunId, readTrackingEntries, deleteTrackingFile, readLookupCache, readCoverageCache, updateCoverageCache, needsReporting, } from './cache.js';
|
|
14
|
+
import { loadEnvFile, getProjectCredentials } from '../utils/env.js';
|
|
15
|
+
import { isLocalOnlyProject } from '../utils/local-project.js';
|
|
16
|
+
/**
|
|
17
|
+
* Aggregate tracking entries by requirement key
|
|
18
|
+
*/
|
|
19
|
+
function aggregateEntries(entries) {
|
|
20
|
+
const byKey = new Map();
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const existing = byKey.get(entry.requirementKey) || [];
|
|
23
|
+
existing.push(entry);
|
|
24
|
+
byKey.set(entry.requirementKey, existing);
|
|
25
|
+
}
|
|
26
|
+
return byKey;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get the current git branch
|
|
30
|
+
*/
|
|
31
|
+
function getCurrentBranch(cwd) {
|
|
32
|
+
try {
|
|
33
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
34
|
+
cwd,
|
|
35
|
+
encoding: 'utf-8',
|
|
36
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
37
|
+
}).trim();
|
|
38
|
+
return branch;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return 'unknown';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Print local coverage report to console
|
|
46
|
+
*
|
|
47
|
+
* HARNESS-FINALIZE-1: Shows which requirements were tested
|
|
48
|
+
*/
|
|
49
|
+
function printLocalReport(testedKeys, lookup) {
|
|
50
|
+
const allKeys = lookup ? Object.keys(lookup.requirements) : [];
|
|
51
|
+
const untestedKeys = allKeys.filter(key => !testedKeys.includes(key));
|
|
52
|
+
const total = allKeys.length;
|
|
53
|
+
const tested = testedKeys.length;
|
|
54
|
+
const coverage = total > 0 ? ((tested / total) * 100).toFixed(1) : '0.0';
|
|
55
|
+
let report = '\n=== Requirements Coverage Report ===\n';
|
|
56
|
+
report += `\nTotal Requirements: ${total}\n`;
|
|
57
|
+
report += `Tested Requirements: ${tested}\n`;
|
|
58
|
+
report += `Untested Requirements: ${untestedKeys.length}\n`;
|
|
59
|
+
report += `Coverage: ${coverage}%\n`;
|
|
60
|
+
// Show tested requirements
|
|
61
|
+
if (testedKeys.length > 0) {
|
|
62
|
+
report += '\n✓ Tested Requirements:\n';
|
|
63
|
+
for (const key of testedKeys) {
|
|
64
|
+
const req = lookup?.requirements[key];
|
|
65
|
+
if (req) {
|
|
66
|
+
const label = req.label || '';
|
|
67
|
+
const content = req.content || '';
|
|
68
|
+
const preview = content.length > 60 ? content.substring(0, 60) + '...' : content;
|
|
69
|
+
const labelPart = label ? ` (${label})` : '';
|
|
70
|
+
report += ` - ${key}${labelPart}: ${preview}\n`;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
report += ` - ${key}\n`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Show untested requirements
|
|
78
|
+
if (untestedKeys.length > 0) {
|
|
79
|
+
report += '\n✗ Untested Requirements:\n';
|
|
80
|
+
for (const key of untestedKeys) {
|
|
81
|
+
const req = lookup?.requirements[key];
|
|
82
|
+
if (req) {
|
|
83
|
+
const label = req.label || '';
|
|
84
|
+
const content = req.content || '';
|
|
85
|
+
const preview = content.length > 60 ? content.substring(0, 60) + '...' : content;
|
|
86
|
+
const labelPart = label ? ` (${label})` : '';
|
|
87
|
+
report += ` - ${key}${labelPart}: ${preview}\n`;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
report += ` - ${key}\n`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
report += '\n====================================\n';
|
|
95
|
+
console.log(report);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Production Convex deployment URL
|
|
99
|
+
*/
|
|
100
|
+
const CONVEX_URL = 'https://data.dotrequirements.io';
|
|
101
|
+
/**
|
|
102
|
+
* Report coverage to Convex cloud
|
|
103
|
+
*
|
|
104
|
+
* HARNESS-FINALIZE-2: Cloud reporting with error tolerance
|
|
105
|
+
* HARNESS-FINALIZE-3: Coverage records include requirement, file, line, branch
|
|
106
|
+
*/
|
|
107
|
+
async function reportToConvex(projectRoot, testRunId, aggregated) {
|
|
108
|
+
try {
|
|
109
|
+
// Load credentials
|
|
110
|
+
loadEnvFile(projectRoot);
|
|
111
|
+
const credentials = getProjectCredentials();
|
|
112
|
+
if (!credentials) {
|
|
113
|
+
console.log('\nℹ️ Skipping cloud coverage reporting (DOTREQUIREMENTS_PROJECT_ID or DOTREQUIREMENTS_PROJECT_SECRET not configured)');
|
|
114
|
+
return { sent: false, count: 0 };
|
|
115
|
+
}
|
|
116
|
+
const { projectId, projectSecret } = credentials;
|
|
117
|
+
// Skip for local-only projects
|
|
118
|
+
if (isLocalOnlyProject(projectId)) {
|
|
119
|
+
return { sent: false, count: 0 };
|
|
120
|
+
}
|
|
121
|
+
const requirementsDir = findRequirementsDir(projectRoot);
|
|
122
|
+
// Get all tracked requirement keys
|
|
123
|
+
const requirementKeys = Array.from(aggregated.keys());
|
|
124
|
+
// Use coverage cache for deduplication
|
|
125
|
+
const coverageCache = readCoverageCache(requirementsDir);
|
|
126
|
+
const keysToReport = requirementKeys.filter(key => needsReporting(key, coverageCache));
|
|
127
|
+
if (keysToReport.length === 0) {
|
|
128
|
+
console.log('\n✓ Coverage unchanged since last run (skipping cloud report)');
|
|
129
|
+
// Still update the cache
|
|
130
|
+
updateCoverageCache(requirementsDir, testRunId, requirementKeys);
|
|
131
|
+
return { sent: false, count: 0 };
|
|
132
|
+
}
|
|
133
|
+
const branch = getCurrentBranch(projectRoot);
|
|
134
|
+
// Build coverage payload
|
|
135
|
+
// HARNESS-FINALIZE-3: Include requirement path, test file, line, branch
|
|
136
|
+
const coverage = keysToReport.map(key => {
|
|
137
|
+
const entries = aggregated.get(key);
|
|
138
|
+
// Use the first access location
|
|
139
|
+
const firstEntry = entries[0];
|
|
140
|
+
const location = firstEntry.callerLocation;
|
|
141
|
+
// Parse "filename.ts:42" format
|
|
142
|
+
let testFile;
|
|
143
|
+
let testLine;
|
|
144
|
+
if (location !== 'unknown') {
|
|
145
|
+
const match = location.match(/^(.+):(\d+)$/);
|
|
146
|
+
if (match) {
|
|
147
|
+
testFile = match[1];
|
|
148
|
+
testLine = parseInt(match[2], 10);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
testFile = location;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
requirementKey: key,
|
|
156
|
+
testFile,
|
|
157
|
+
testLine,
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
// Send to Convex
|
|
161
|
+
// Note: projectId from env is always a slug (e.g., "reduced-cephalopod-288"),
|
|
162
|
+
// not a Convex ID. Use projectSlug field for authentication and slug for target.
|
|
163
|
+
const response = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
},
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
path: 'testCoverage/mutations:recordCoverage',
|
|
170
|
+
args: {
|
|
171
|
+
projectAuth: {
|
|
172
|
+
projectSlug: projectId,
|
|
173
|
+
projectSecret,
|
|
174
|
+
},
|
|
175
|
+
target: {
|
|
176
|
+
type: 'project',
|
|
177
|
+
slug: projectId,
|
|
178
|
+
},
|
|
179
|
+
branch,
|
|
180
|
+
coverage,
|
|
181
|
+
},
|
|
182
|
+
format: 'json',
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
const error = await response.text();
|
|
187
|
+
console.warn(`\n⚠️ Failed to report coverage to cloud: ${response.status} ${error}`);
|
|
188
|
+
return { sent: false, count: 0, error: `${response.status} ${error}` };
|
|
189
|
+
}
|
|
190
|
+
// Check for Convex-level errors in the response body
|
|
191
|
+
// Convex returns 200 OK even for validation errors, with the error in the body
|
|
192
|
+
const responseText = await response.text();
|
|
193
|
+
let responseData;
|
|
194
|
+
try {
|
|
195
|
+
responseData = JSON.parse(responseText);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Not JSON, treat as success
|
|
199
|
+
}
|
|
200
|
+
if (responseData && typeof responseData === 'object' && 'status' in responseData) {
|
|
201
|
+
const convexResponse = responseData;
|
|
202
|
+
if (convexResponse.status === 'error') {
|
|
203
|
+
const errorMsg = convexResponse.errorMessage || 'Unknown Convex error';
|
|
204
|
+
console.warn(`\n⚠️ Failed to report coverage to cloud: ${errorMsg}`);
|
|
205
|
+
return { sent: false, count: 0, error: errorMsg };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
console.log(`\n✓ Reported ${keysToReport.length} requirement(s) to cloud (branch: ${branch})`);
|
|
209
|
+
// Update coverage cache
|
|
210
|
+
updateCoverageCache(requirementsDir, testRunId, requirementKeys);
|
|
211
|
+
return { sent: true, count: keysToReport.length };
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
215
|
+
console.warn(`\n⚠️ Error reporting coverage to cloud: ${errorMessage}`);
|
|
216
|
+
return { sent: false, count: 0, error: errorMessage };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Finalize the test run after all tests complete.
|
|
221
|
+
*
|
|
222
|
+
* This function should be called once after tests complete (e.g., in globalTeardown).
|
|
223
|
+
* It reads tracking data, generates reports, and cleans up.
|
|
224
|
+
*
|
|
225
|
+
* HARNESS-FINALIZE-1: After finalize() runs, developers see which requirements were tested
|
|
226
|
+
* HARNESS-FINALIZE-2: After finalize() runs, coverage data is available in the cloud
|
|
227
|
+
* HARNESS-FINALIZE-3: Cloud coverage records identify which requirement was tested and where
|
|
228
|
+
* HARNESS-FINALIZE-4: Finalize cleans up after itself
|
|
229
|
+
*/
|
|
230
|
+
export async function finalize(options = {}) {
|
|
231
|
+
const { cwd = process.cwd(), reportToCloud: shouldReportToCloud = true, printLocalReport: shouldPrintLocal = true, cleanup: shouldCleanup = true, } = options;
|
|
232
|
+
// Priority 1: Use environment variable (cross-process persistence from globalSetup)
|
|
233
|
+
let projectRoot = process.env.DOTREQUIREMENTS_PROJECT_ROOT || null;
|
|
234
|
+
// Priority 2: Find from cwd
|
|
235
|
+
if (!projectRoot) {
|
|
236
|
+
projectRoot = findProjectRoot(cwd);
|
|
237
|
+
}
|
|
238
|
+
if (!projectRoot) {
|
|
239
|
+
console.log('Could not find .requirements directory');
|
|
240
|
+
return {
|
|
241
|
+
requirementsTested: 0,
|
|
242
|
+
totalRequirements: 0,
|
|
243
|
+
coveragePercent: 0,
|
|
244
|
+
cloudReportSent: false,
|
|
245
|
+
requirementsReportedToCloud: 0,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const requirementsDir = findRequirementsDir(projectRoot);
|
|
249
|
+
// Get test run ID
|
|
250
|
+
const testRunId = getTestRunId(requirementsDir);
|
|
251
|
+
if (!testRunId) {
|
|
252
|
+
console.log('No test run ID found - was prepare() called?');
|
|
253
|
+
return {
|
|
254
|
+
requirementsTested: 0,
|
|
255
|
+
totalRequirements: 0,
|
|
256
|
+
coveragePercent: 0,
|
|
257
|
+
cloudReportSent: false,
|
|
258
|
+
requirementsReportedToCloud: 0,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
// Read tracking entries
|
|
262
|
+
const entries = readTrackingEntries(requirementsDir);
|
|
263
|
+
// Aggregate by requirement key
|
|
264
|
+
const aggregated = aggregateEntries(entries);
|
|
265
|
+
const testedKeys = Array.from(aggregated.keys());
|
|
266
|
+
// Read lookup cache for report
|
|
267
|
+
const lookup = readLookupCache(requirementsDir);
|
|
268
|
+
const totalRequirements = lookup ? Object.keys(lookup.requirements).length : 0;
|
|
269
|
+
const coveragePercent = totalRequirements > 0
|
|
270
|
+
? (testedKeys.length / totalRequirements) * 100
|
|
271
|
+
: 0;
|
|
272
|
+
// HARNESS-FINALIZE-1: Print local report
|
|
273
|
+
if (shouldPrintLocal) {
|
|
274
|
+
printLocalReport(testedKeys, lookup);
|
|
275
|
+
}
|
|
276
|
+
// HARNESS-FINALIZE-2, HARNESS-FINALIZE-3: Report to cloud
|
|
277
|
+
let cloudResult = { sent: false, count: 0 };
|
|
278
|
+
if (shouldReportToCloud) {
|
|
279
|
+
cloudResult = await reportToConvex(projectRoot, testRunId, aggregated);
|
|
280
|
+
}
|
|
281
|
+
// HARNESS-FINALIZE-4: Clean up tracking data
|
|
282
|
+
if (shouldCleanup && !cloudResult.error) {
|
|
283
|
+
// HARNESS-FINALIZE-4.0: Remove tracking data from this test run
|
|
284
|
+
deleteTrackingFile(requirementsDir);
|
|
285
|
+
// Clean up test run ID file
|
|
286
|
+
cleanupTestRunId(requirementsDir);
|
|
287
|
+
// HARNESS-FINALIZE-4.1: Lookup cache is preserved (no deletion)
|
|
288
|
+
}
|
|
289
|
+
// HARNESS-FINALIZE-4.2: On error, tracking data is preserved for debugging
|
|
290
|
+
return {
|
|
291
|
+
requirementsTested: testedKeys.length,
|
|
292
|
+
totalRequirements,
|
|
293
|
+
coveragePercent,
|
|
294
|
+
cloudReportSent: cloudResult.sent,
|
|
295
|
+
requirementsReportedToCloud: cloudResult.count,
|
|
296
|
+
cloudError: cloudResult.error,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=finalize.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Harness for dot•requirements.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the test harness for tracking requirement coverage.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
*
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // In globalSetup (e.g., vitest.setup.ts)
|
|
10
|
+
* import { prepare } from '@popoverai/dotrequirements/test';
|
|
11
|
+
* export default function() {
|
|
12
|
+
* prepare();
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* // In tests
|
|
16
|
+
* import { requirement } from '@popoverai/dotrequirements/test';
|
|
17
|
+
* describe(requirement('REQ-123'), () => {
|
|
18
|
+
* it(requirement('REQ-123.0'), () => {
|
|
19
|
+
* // test implementation
|
|
20
|
+
* });
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* // In globalTeardown
|
|
24
|
+
* import { finalize } from '@popoverai/dotrequirements/test';
|
|
25
|
+
* export default async function() {
|
|
26
|
+
* await finalize();
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Options for the requirement() function.
|
|
32
|
+
*/
|
|
33
|
+
export interface RequirementOptions {
|
|
34
|
+
/** Explicit project root path for loading requirements (useful when cwd is changed) */
|
|
35
|
+
projectRoot?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Main requirements function for test harness.
|
|
39
|
+
*
|
|
40
|
+
* HARNESS-REQUIREMENT-1: Test frameworks can use requirement() as test descriptions
|
|
41
|
+
* HARNESS-REQUIREMENT-2: Invalid requirement paths fail the individual test
|
|
42
|
+
* HARNESS-REQUIREMENT-3: Each requirement() call is recorded for coverage reporting
|
|
43
|
+
* HARNESS-REQUIREMENT-4: Developers can run tests without calling prepare() first
|
|
44
|
+
* HARNESS-REQUIREMENT-5: A single test can exercise multiple requirements
|
|
45
|
+
*
|
|
46
|
+
* Supports both numeric and label-based paths.
|
|
47
|
+
* Accepts multiple requirement refs to track coverage for multiple requirements in one test.
|
|
48
|
+
*
|
|
49
|
+
* Examples:
|
|
50
|
+
* - requirement('REQ-123') - Returns root requirement content
|
|
51
|
+
* - requirement('REQ-123.0') - Returns first child (numeric path)
|
|
52
|
+
* - requirement('REQ-123.given') - Returns first "given" child (label path)
|
|
53
|
+
* - requirement('REQ-123.given#1') - Returns second "given" child
|
|
54
|
+
* - requirement('REQ-123.0.1') - Returns nested child
|
|
55
|
+
* - requirement('REQ-123.then.and') - Returns first "and" under first "then"
|
|
56
|
+
* - requirement('REQ-AUTH', 'REQ-SECURITY') - Tracks both, returns first
|
|
57
|
+
* - requirement('REQ-123', { projectRoot: '/path/to/project' }) - Explicit project root
|
|
58
|
+
*
|
|
59
|
+
* @param args - One or more requirement reference paths, optionally followed by options
|
|
60
|
+
* @returns A formatted string with the first requirement's label and content
|
|
61
|
+
*/
|
|
62
|
+
export declare function requirement(...args: (string | RequirementOptions)[]): string;
|
|
63
|
+
export { prepare } from './prepare.js';
|
|
64
|
+
export type { PrepareOptions, PrepareResult } from './prepare.js';
|
|
65
|
+
export { finalize } from './finalize.js';
|
|
66
|
+
export type { FinalizeOptions, FinalizeResult } from './finalize.js';
|
|
67
|
+
export { clearTracking } from './tracking.js';
|
|
68
|
+
export { initTestRun, finalizeTestRun, } from './tracking.js';
|
|
69
|
+
export type { Requirement } from './types.js';
|
|
70
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Harness for dot•requirements.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the test harness for tracking requirement coverage.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
*
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // In globalSetup (e.g., vitest.setup.ts)
|
|
10
|
+
* import { prepare } from '@popoverai/dotrequirements/test';
|
|
11
|
+
* export default function() {
|
|
12
|
+
* prepare();
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* // In tests
|
|
16
|
+
* import { requirement } from '@popoverai/dotrequirements/test';
|
|
17
|
+
* describe(requirement('REQ-123'), () => {
|
|
18
|
+
* it(requirement('REQ-123.0'), () => {
|
|
19
|
+
* // test implementation
|
|
20
|
+
* });
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* // In globalTeardown
|
|
24
|
+
* import { finalize } from '@popoverai/dotrequirements/test';
|
|
25
|
+
* export default async function() {
|
|
26
|
+
* await finalize();
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
import { getRequirement, loadRequirements } from './requirementsLoader.js';
|
|
31
|
+
import { ensureTestRun, trackRequirement, saveTrackingData } from './tracking.js';
|
|
32
|
+
/**
|
|
33
|
+
* Main requirements function for test harness.
|
|
34
|
+
*
|
|
35
|
+
* HARNESS-REQUIREMENT-1: Test frameworks can use requirement() as test descriptions
|
|
36
|
+
* HARNESS-REQUIREMENT-2: Invalid requirement paths fail the individual test
|
|
37
|
+
* HARNESS-REQUIREMENT-3: Each requirement() call is recorded for coverage reporting
|
|
38
|
+
* HARNESS-REQUIREMENT-4: Developers can run tests without calling prepare() first
|
|
39
|
+
* HARNESS-REQUIREMENT-5: A single test can exercise multiple requirements
|
|
40
|
+
*
|
|
41
|
+
* Supports both numeric and label-based paths.
|
|
42
|
+
* Accepts multiple requirement refs to track coverage for multiple requirements in one test.
|
|
43
|
+
*
|
|
44
|
+
* Examples:
|
|
45
|
+
* - requirement('REQ-123') - Returns root requirement content
|
|
46
|
+
* - requirement('REQ-123.0') - Returns first child (numeric path)
|
|
47
|
+
* - requirement('REQ-123.given') - Returns first "given" child (label path)
|
|
48
|
+
* - requirement('REQ-123.given#1') - Returns second "given" child
|
|
49
|
+
* - requirement('REQ-123.0.1') - Returns nested child
|
|
50
|
+
* - requirement('REQ-123.then.and') - Returns first "and" under first "then"
|
|
51
|
+
* - requirement('REQ-AUTH', 'REQ-SECURITY') - Tracks both, returns first
|
|
52
|
+
* - requirement('REQ-123', { projectRoot: '/path/to/project' }) - Explicit project root
|
|
53
|
+
*
|
|
54
|
+
* @param args - One or more requirement reference paths, optionally followed by options
|
|
55
|
+
* @returns A formatted string with the first requirement's label and content
|
|
56
|
+
*/
|
|
57
|
+
export function requirement(...args) {
|
|
58
|
+
if (args.length === 0) {
|
|
59
|
+
throw new Error('requirement() requires at least one argument');
|
|
60
|
+
}
|
|
61
|
+
// Check if last argument is options object
|
|
62
|
+
const lastArg = args[args.length - 1];
|
|
63
|
+
const hasOptions = typeof lastArg === 'object' && lastArg !== null;
|
|
64
|
+
const options = hasOptions ? lastArg : {};
|
|
65
|
+
const requirementRefs = (hasOptions ? args.slice(0, -1) : args);
|
|
66
|
+
if (requirementRefs.length === 0) {
|
|
67
|
+
throw new Error('requirement() requires at least one requirement reference');
|
|
68
|
+
}
|
|
69
|
+
// If projectRoot provided, reload requirements from that path
|
|
70
|
+
if (options.projectRoot) {
|
|
71
|
+
loadRequirements({ projectRoot: options.projectRoot });
|
|
72
|
+
}
|
|
73
|
+
// Ensure test run is initialized
|
|
74
|
+
ensureTestRun();
|
|
75
|
+
// Track all requirements for coverage at the full path level
|
|
76
|
+
// e.g., 'REQ-123.given' is tracked as 'REQ-123.given', not just 'REQ-123'
|
|
77
|
+
for (const ref of requirementRefs) {
|
|
78
|
+
trackRequirement(ref);
|
|
79
|
+
}
|
|
80
|
+
// Save after tracking all requirements (no-op in new JSONL approach)
|
|
81
|
+
saveTrackingData();
|
|
82
|
+
// HARNESS-REQUIREMENT-2: Invalid requirement paths fail the individual test
|
|
83
|
+
const firstRef = requirementRefs[0];
|
|
84
|
+
const req = getRequirement(firstRef);
|
|
85
|
+
if (!req) {
|
|
86
|
+
throw new Error(`Requirement ${firstRef} not found`);
|
|
87
|
+
}
|
|
88
|
+
// HARNESS-REQUIREMENT-1: Format output as human-readable string
|
|
89
|
+
// Root requirements use 'requirementHeader' label which should not be shown in output
|
|
90
|
+
if (!req.label || req.label === 'requirementHeader') {
|
|
91
|
+
return req.content;
|
|
92
|
+
}
|
|
93
|
+
const label = req.label.charAt(0).toUpperCase() + req.label.slice(1);
|
|
94
|
+
return `${label}: ${req.content}`;
|
|
95
|
+
}
|
|
96
|
+
// Export new lifecycle functions
|
|
97
|
+
export { prepare } from './prepare.js';
|
|
98
|
+
export { finalize } from './finalize.js';
|
|
99
|
+
// Export tracking utilities
|
|
100
|
+
export { clearTracking } from './tracking.js';
|
|
101
|
+
// Deprecated exports - use prepare() and finalize() instead
|
|
102
|
+
export { initTestRun, finalizeTestRun, } from './tracking.js';
|
|
103
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { TrackedRequirement } from "./tracking.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generate a simple console report showing which requirements were tested
|
|
4
|
+
*/
|
|
5
|
+
export declare function reportLocalCoverage(trackedReqs?: Map<string, TrackedRequirement>): void;
|
|
6
|
+
//# sourceMappingURL=localReporting.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { getAllRequirements } from "./requirementsLoader.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generate a simple console report showing which requirements were tested
|
|
4
|
+
*/
|
|
5
|
+
export function reportLocalCoverage(trackedReqs) {
|
|
6
|
+
const allRequirements = getAllRequirements();
|
|
7
|
+
// If no tracked requirements provided, return empty report
|
|
8
|
+
if (!trackedReqs) {
|
|
9
|
+
trackedReqs = new Map();
|
|
10
|
+
}
|
|
11
|
+
const allRequirementKeys = Array.from(allRequirements.keys());
|
|
12
|
+
const testedKeys = Array.from(trackedReqs.keys());
|
|
13
|
+
const untestedKeys = allRequirementKeys.filter((key) => !trackedReqs.has(key));
|
|
14
|
+
const coverage = allRequirementKeys.length > 0
|
|
15
|
+
? ((testedKeys.length / allRequirementKeys.length) * 100).toFixed(1)
|
|
16
|
+
: "0.0";
|
|
17
|
+
let report = "\n=== Requirements Coverage Report ===\n";
|
|
18
|
+
report += `\nTotal Requirements: ${allRequirementKeys.length}\n`;
|
|
19
|
+
report += `Tested Requirements: ${testedKeys.length}\n`;
|
|
20
|
+
report += `Untested Requirements: ${untestedKeys.length}\n`;
|
|
21
|
+
report += `Coverage: ${coverage}%\n`;
|
|
22
|
+
// Show tested requirements
|
|
23
|
+
if (testedKeys.length > 0) {
|
|
24
|
+
report += "\n✓ Tested Requirements:\n";
|
|
25
|
+
testedKeys.forEach((key) => {
|
|
26
|
+
const req = allRequirements.get(key);
|
|
27
|
+
const label = req?.label || "";
|
|
28
|
+
const content = req?.content || "";
|
|
29
|
+
const preview = content.length > 60 ? content.substring(0, 60) + "..." : content;
|
|
30
|
+
const labelPart = label ? ` (${label})` : "";
|
|
31
|
+
report += ` - ${key}${labelPart}: ${preview}\n`;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// Show untested requirements
|
|
35
|
+
if (untestedKeys.length > 0) {
|
|
36
|
+
report += "\n✗ Untested Requirements:\n";
|
|
37
|
+
untestedKeys.forEach((key) => {
|
|
38
|
+
const req = allRequirements.get(key);
|
|
39
|
+
const label = req?.label || "";
|
|
40
|
+
const content = req?.content || "";
|
|
41
|
+
const preview = content.length > 60 ? content.substring(0, 60) + "..." : content;
|
|
42
|
+
const labelPart = label ? ` (${label})` : "";
|
|
43
|
+
report += ` - ${key}${labelPart}: ${preview}\n`;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
report += "\n====================================\n";
|
|
47
|
+
console.log(report);
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=localReporting.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepare function for the test harness.
|
|
3
|
+
*
|
|
4
|
+
* Called before tests run (e.g., in Vitest globalSetup) to:
|
|
5
|
+
* 1. Parse all .requirements.md files
|
|
6
|
+
* 2. Build lookup cache for fast path resolution
|
|
7
|
+
* 3. Initialize tracking for the test run
|
|
8
|
+
*
|
|
9
|
+
* Implements: HARNESS-PREPARE-1, HARNESS-PREPARE-2, HARNESS-PREPARE-3
|
|
10
|
+
*/
|
|
11
|
+
export interface PrepareOptions {
|
|
12
|
+
/** Working directory to start search from (defaults to cwd) */
|
|
13
|
+
cwd?: string;
|
|
14
|
+
/** Whether to log warnings (defaults to true) */
|
|
15
|
+
logWarnings?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface PrepareResult {
|
|
18
|
+
/** Number of requirements files processed */
|
|
19
|
+
filesProcessed: number;
|
|
20
|
+
/** Number of requirements loaded into cache */
|
|
21
|
+
requirementsLoaded: number;
|
|
22
|
+
/** Files that had parse errors */
|
|
23
|
+
parseErrors: Array<{
|
|
24
|
+
file: string;
|
|
25
|
+
error: string;
|
|
26
|
+
}>;
|
|
27
|
+
/** The test run ID for this run */
|
|
28
|
+
testRunId: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Prepare the test harness for a test run.
|
|
32
|
+
*
|
|
33
|
+
* This function should be called once before tests run (e.g., in globalSetup).
|
|
34
|
+
* It parses all requirements files and builds a lookup cache for fast resolution.
|
|
35
|
+
*
|
|
36
|
+
* HARNESS-PREPARE-1: After prepare() runs, requirement paths resolve quickly
|
|
37
|
+
* HARNESS-PREPARE-2: When prepare() encounters invalid requirements, it warns but continues
|
|
38
|
+
* HARNESS-PREPARE-3: prepare() initializes the cache directory for tracking
|
|
39
|
+
*/
|
|
40
|
+
export declare function prepare(options?: PrepareOptions): PrepareResult;
|
|
41
|
+
//# sourceMappingURL=prepare.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepare function for the test harness.
|
|
3
|
+
*
|
|
4
|
+
* Called before tests run (e.g., in Vitest globalSetup) to:
|
|
5
|
+
* 1. Parse all .requirements.md files
|
|
6
|
+
* 2. Build lookup cache for fast path resolution
|
|
7
|
+
* 3. Initialize tracking for the test run
|
|
8
|
+
*
|
|
9
|
+
* Implements: HARNESS-PREPARE-1, HARNESS-PREPARE-2, HARNESS-PREPARE-3
|
|
10
|
+
*/
|
|
11
|
+
import { findProjectRootWithCredentials, findRequirementsDir, findRequirementsFiles, writeLookupCache, initTestRunId, clearTrackingFile, writeProjectRoot, } from './cache.js';
|
|
12
|
+
import { parseRequirementsFromFile, } from '../schema/index.js';
|
|
13
|
+
/**
|
|
14
|
+
* Prepare the test harness for a test run.
|
|
15
|
+
*
|
|
16
|
+
* This function should be called once before tests run (e.g., in globalSetup).
|
|
17
|
+
* It parses all requirements files and builds a lookup cache for fast resolution.
|
|
18
|
+
*
|
|
19
|
+
* HARNESS-PREPARE-1: After prepare() runs, requirement paths resolve quickly
|
|
20
|
+
* HARNESS-PREPARE-2: When prepare() encounters invalid requirements, it warns but continues
|
|
21
|
+
* HARNESS-PREPARE-3: prepare() initializes the cache directory for tracking
|
|
22
|
+
*/
|
|
23
|
+
export function prepare(options = {}) {
|
|
24
|
+
const { cwd = process.cwd(), logWarnings = true } = options;
|
|
25
|
+
// Find the project root - prefer root with credentials (.env.local) for monorepo support
|
|
26
|
+
let projectRoot;
|
|
27
|
+
try {
|
|
28
|
+
projectRoot = findProjectRootWithCredentials(cwd);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new Error('Could not find .requirements directory or .env.local with credentials. ' +
|
|
32
|
+
'Please create a .requirements directory in your project root.');
|
|
33
|
+
}
|
|
34
|
+
// Set environment variable for cross-process persistence (globalSetup → test workers)
|
|
35
|
+
process.env.DOTREQUIREMENTS_PROJECT_ROOT = projectRoot;
|
|
36
|
+
const requirementsDir = findRequirementsDir(projectRoot);
|
|
37
|
+
// Find all requirements files
|
|
38
|
+
const files = findRequirementsFiles(projectRoot);
|
|
39
|
+
// Parse all files, collecting requirements and errors
|
|
40
|
+
const allRequirements = [];
|
|
41
|
+
const parseErrors = [];
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
try {
|
|
44
|
+
const { requirements } = parseRequirementsFromFile(file);
|
|
45
|
+
allRequirements.push(...requirements);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
49
|
+
parseErrors.push({ file, error: errorMessage });
|
|
50
|
+
// HARNESS-PREPARE-2.0: Log warning with file path and error details
|
|
51
|
+
if (logWarnings) {
|
|
52
|
+
console.warn(`⚠️ Warning: Failed to parse ${file}: ${errorMessage}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// HARNESS-PREPARE-1: Write lookup cache for fast resolution
|
|
57
|
+
writeLookupCache(requirementsDir, allRequirements);
|
|
58
|
+
// Write project root to cache (for cross-process persistence in test workers)
|
|
59
|
+
writeProjectRoot(requirementsDir, projectRoot);
|
|
60
|
+
// HARNESS-PREPARE-3.0: Initialize cache directory (done by writeLookupCache)
|
|
61
|
+
// HARNESS-PREPARE-3.1: Clear any previous tracking data
|
|
62
|
+
clearTrackingFile(requirementsDir);
|
|
63
|
+
// Initialize test run ID
|
|
64
|
+
const testRunId = initTestRunId(requirementsDir);
|
|
65
|
+
// Count total requirements (including children)
|
|
66
|
+
let requirementsLoaded = 0;
|
|
67
|
+
function countRequirements(node) {
|
|
68
|
+
requirementsLoaded++;
|
|
69
|
+
for (const child of node.children) {
|
|
70
|
+
countRequirements(child);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const req of allRequirements) {
|
|
74
|
+
countRequirements(req);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
filesProcessed: files.length,
|
|
78
|
+
requirementsLoaded,
|
|
79
|
+
parseErrors,
|
|
80
|
+
testRunId,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=prepare.js.map
|