@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,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache management for the test harness.
|
|
3
|
+
*
|
|
4
|
+
* Manages the .requirements/.cache/ directory with:
|
|
5
|
+
* - lookup.json: Pre-resolved requirements for fast path resolution
|
|
6
|
+
* - tracking.jsonl: Append-only tracking of requirement() calls (cross-process safe)
|
|
7
|
+
* - coverage.json: Deduplication cache for cloud reporting
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { globSync } from 'glob';
|
|
12
|
+
import { findUpSync } from 'find-up';
|
|
13
|
+
import { brandPlain } from '../utils/brand.js';
|
|
14
|
+
// Cache directory structure
|
|
15
|
+
const CACHE_DIR = '.cache';
|
|
16
|
+
const LOOKUP_FILE = 'lookup.json';
|
|
17
|
+
const TRACKING_FILE = 'tracking.jsonl';
|
|
18
|
+
const COVERAGE_FILE = 'coverage.json';
|
|
19
|
+
const PROJECT_ROOT_FILE = 'project-root';
|
|
20
|
+
// Test run ID file (outside cache, in .requirements/)
|
|
21
|
+
const TEST_RUN_ID_FILE = '.test-run-id';
|
|
22
|
+
/**
|
|
23
|
+
* Find the nearest .requirements directory by walking up from startDir
|
|
24
|
+
*/
|
|
25
|
+
export function findRequirementsDir(startDir = process.cwd()) {
|
|
26
|
+
let currentDir = path.resolve(startDir);
|
|
27
|
+
while (true) {
|
|
28
|
+
const requirementsDir = path.join(currentDir, '.requirements');
|
|
29
|
+
if (fs.existsSync(requirementsDir) && fs.statSync(requirementsDir).isDirectory()) {
|
|
30
|
+
return requirementsDir;
|
|
31
|
+
}
|
|
32
|
+
const parentDir = path.dirname(currentDir);
|
|
33
|
+
if (parentDir === currentDir) {
|
|
34
|
+
// Reached filesystem root without finding .requirements
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
currentDir = parentDir;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Find the project root (directory containing .requirements)
|
|
42
|
+
*/
|
|
43
|
+
export function findProjectRoot(startDir = process.cwd()) {
|
|
44
|
+
const requirementsDir = findRequirementsDir(startDir);
|
|
45
|
+
return requirementsDir ? path.dirname(requirementsDir) : null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the cache directory path, creating it if necessary
|
|
49
|
+
*/
|
|
50
|
+
export function getCacheDir(requirementsDir, create = false) {
|
|
51
|
+
const cacheDir = path.join(requirementsDir, CACHE_DIR);
|
|
52
|
+
if (create && !fs.existsSync(cacheDir)) {
|
|
53
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
return cacheDir;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if an .env.local file has dotrequirements credentials.
|
|
59
|
+
*/
|
|
60
|
+
function hasDotrequirementsCredentials(envPath) {
|
|
61
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
62
|
+
const hasProjectId = envContent.includes('DOTREQUIREMENTS_PROJECT_ID=');
|
|
63
|
+
const hasProjectSecret = envContent.includes('DOTREQUIREMENTS_PROJECT_SECRET=');
|
|
64
|
+
return hasProjectId && hasProjectSecret;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Find the dotrequirements project root by looking for .env.local with credentials.
|
|
68
|
+
* Falls back to finding .requirements directory if no credentials found.
|
|
69
|
+
*/
|
|
70
|
+
export function findProjectRootWithCredentials(startDir = process.cwd()) {
|
|
71
|
+
let searchDir = startDir;
|
|
72
|
+
while (true) {
|
|
73
|
+
const envPath = findUpSync('.env.local', { cwd: searchDir });
|
|
74
|
+
if (!envPath) {
|
|
75
|
+
// No .env.local found - try to find project root by .requirements directory
|
|
76
|
+
const projectRoot = findProjectRoot(startDir);
|
|
77
|
+
if (projectRoot) {
|
|
78
|
+
return projectRoot;
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`Could not find ${brandPlain} project root. ` +
|
|
81
|
+
`Expected to find .env.local with credentials or a .requirements directory.`);
|
|
82
|
+
}
|
|
83
|
+
// Check if this .env.local has dotrequirements credentials
|
|
84
|
+
if (hasDotrequirementsCredentials(envPath)) {
|
|
85
|
+
return path.dirname(envPath);
|
|
86
|
+
}
|
|
87
|
+
// No credentials - continue searching from parent directory
|
|
88
|
+
const envDir = path.dirname(envPath);
|
|
89
|
+
const parentDir = path.dirname(envDir);
|
|
90
|
+
// If we've reached the root, stop
|
|
91
|
+
if (parentDir === envDir) {
|
|
92
|
+
// Try .requirements as fallback
|
|
93
|
+
const projectRoot = findProjectRoot(startDir);
|
|
94
|
+
if (projectRoot) {
|
|
95
|
+
return projectRoot;
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`Could not find ${brandPlain} project root. ` +
|
|
98
|
+
`Found .env.local files but none contained credentials.`);
|
|
99
|
+
}
|
|
100
|
+
searchDir = parentDir;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Find all *.requirements.md files under project root
|
|
105
|
+
*/
|
|
106
|
+
export function findRequirementsFiles(projectRoot) {
|
|
107
|
+
const pattern = '**/*.requirements.md';
|
|
108
|
+
const matches = globSync(pattern, {
|
|
109
|
+
cwd: projectRoot,
|
|
110
|
+
ignore: [
|
|
111
|
+
'**/node_modules/**',
|
|
112
|
+
'**/dist/**',
|
|
113
|
+
'**/.git/**',
|
|
114
|
+
'**/build/**',
|
|
115
|
+
'**/example/**',
|
|
116
|
+
'**/examples/**',
|
|
117
|
+
'**/__tests__/**',
|
|
118
|
+
'**/fixtures/**',
|
|
119
|
+
'**/.fixtures/**',
|
|
120
|
+
],
|
|
121
|
+
dot: true, // Include dotfiles/dotdirs like .requirements/
|
|
122
|
+
});
|
|
123
|
+
return matches.map(m => path.join(projectRoot, m));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Write the lookup cache with all pre-parsed requirements
|
|
127
|
+
*/
|
|
128
|
+
export function writeLookupCache(requirementsDir, requirements) {
|
|
129
|
+
const cacheDir = getCacheDir(requirementsDir, true);
|
|
130
|
+
const lookupPath = path.join(cacheDir, LOOKUP_FILE);
|
|
131
|
+
const lookup = {
|
|
132
|
+
generatedAt: new Date().toISOString(),
|
|
133
|
+
requirements: {},
|
|
134
|
+
};
|
|
135
|
+
// Recursively flatten the requirement trees
|
|
136
|
+
function flatten(node) {
|
|
137
|
+
lookup.requirements[node.id] = {
|
|
138
|
+
id: node.id,
|
|
139
|
+
label: node.label,
|
|
140
|
+
content: node.content,
|
|
141
|
+
};
|
|
142
|
+
for (const child of node.children) {
|
|
143
|
+
flatten(child);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const req of requirements) {
|
|
147
|
+
flatten(req);
|
|
148
|
+
}
|
|
149
|
+
fs.writeFileSync(lookupPath, JSON.stringify(lookup, null, 2));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Read the lookup cache, returning null if not found or invalid
|
|
153
|
+
*/
|
|
154
|
+
export function readLookupCache(requirementsDir) {
|
|
155
|
+
const cacheDir = getCacheDir(requirementsDir);
|
|
156
|
+
const lookupPath = path.join(cacheDir, LOOKUP_FILE);
|
|
157
|
+
if (!fs.existsSync(lookupPath)) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const content = fs.readFileSync(lookupPath, 'utf-8');
|
|
162
|
+
return JSON.parse(content);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Generate and store a unique test run ID
|
|
170
|
+
*/
|
|
171
|
+
export function initTestRunId(requirementsDir) {
|
|
172
|
+
const testRunId = `${Date.now()}-${process.pid}`;
|
|
173
|
+
const testRunIdPath = path.join(requirementsDir, TEST_RUN_ID_FILE);
|
|
174
|
+
fs.writeFileSync(testRunIdPath, testRunId);
|
|
175
|
+
return testRunId;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Get the current test run ID, or null if not initialized
|
|
179
|
+
*/
|
|
180
|
+
export function getTestRunId(requirementsDir) {
|
|
181
|
+
const testRunIdPath = path.join(requirementsDir, TEST_RUN_ID_FILE);
|
|
182
|
+
if (!fs.existsSync(testRunIdPath)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return fs.readFileSync(testRunIdPath, 'utf-8').trim();
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Clean up the test run ID file
|
|
189
|
+
*/
|
|
190
|
+
export function cleanupTestRunId(requirementsDir) {
|
|
191
|
+
const testRunIdPath = path.join(requirementsDir, TEST_RUN_ID_FILE);
|
|
192
|
+
if (fs.existsSync(testRunIdPath)) {
|
|
193
|
+
fs.unlinkSync(testRunIdPath);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Write the project root to cache (for cross-process persistence).
|
|
198
|
+
* This allows test workers to find the lookup cache even after cwd changes.
|
|
199
|
+
*/
|
|
200
|
+
export function writeProjectRoot(requirementsDir, projectRoot) {
|
|
201
|
+
const cacheDir = getCacheDir(requirementsDir, true);
|
|
202
|
+
const projectRootPath = path.join(cacheDir, PROJECT_ROOT_FILE);
|
|
203
|
+
fs.writeFileSync(projectRootPath, projectRoot);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Read the project root from cache, returning null if not found.
|
|
207
|
+
*/
|
|
208
|
+
export function readProjectRoot(requirementsDir) {
|
|
209
|
+
const cacheDir = getCacheDir(requirementsDir);
|
|
210
|
+
const projectRootPath = path.join(cacheDir, PROJECT_ROOT_FILE);
|
|
211
|
+
if (!fs.existsSync(projectRootPath)) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return fs.readFileSync(projectRootPath, 'utf-8').trim();
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Try to find the project root from any known .requirements cache directory.
|
|
218
|
+
* Walks up from cwd looking for .requirements/.cache/project-root file.
|
|
219
|
+
*/
|
|
220
|
+
export function findCachedProjectRoot(startDir = process.cwd()) {
|
|
221
|
+
let currentDir = path.resolve(startDir);
|
|
222
|
+
while (true) {
|
|
223
|
+
const requirementsDir = path.join(currentDir, '.requirements');
|
|
224
|
+
const projectRootPath = path.join(requirementsDir, CACHE_DIR, PROJECT_ROOT_FILE);
|
|
225
|
+
if (fs.existsSync(projectRootPath)) {
|
|
226
|
+
return fs.readFileSync(projectRootPath, 'utf-8').trim();
|
|
227
|
+
}
|
|
228
|
+
const parentDir = path.dirname(currentDir);
|
|
229
|
+
if (parentDir === currentDir) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
currentDir = parentDir;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Clear tracking data for a new test run
|
|
237
|
+
*/
|
|
238
|
+
export function clearTrackingFile(requirementsDir) {
|
|
239
|
+
const cacheDir = getCacheDir(requirementsDir, true);
|
|
240
|
+
const trackingPath = path.join(cacheDir, TRACKING_FILE);
|
|
241
|
+
if (fs.existsSync(trackingPath)) {
|
|
242
|
+
fs.unlinkSync(trackingPath);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Append a tracking entry to the JSONL file (cross-process safe)
|
|
247
|
+
*/
|
|
248
|
+
export function appendTrackingEntry(requirementsDir, entry) {
|
|
249
|
+
const cacheDir = getCacheDir(requirementsDir, true);
|
|
250
|
+
const trackingPath = path.join(cacheDir, TRACKING_FILE);
|
|
251
|
+
const line = JSON.stringify(entry) + '\n';
|
|
252
|
+
fs.appendFileSync(trackingPath, line);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Read all tracking entries from the JSONL file
|
|
256
|
+
*/
|
|
257
|
+
export function readTrackingEntries(requirementsDir) {
|
|
258
|
+
const cacheDir = getCacheDir(requirementsDir);
|
|
259
|
+
const trackingPath = path.join(cacheDir, TRACKING_FILE);
|
|
260
|
+
if (!fs.existsSync(trackingPath)) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
const content = fs.readFileSync(trackingPath, 'utf-8');
|
|
264
|
+
const entries = [];
|
|
265
|
+
for (const line of content.split('\n')) {
|
|
266
|
+
if (line.trim()) {
|
|
267
|
+
try {
|
|
268
|
+
entries.push(JSON.parse(line));
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Skip invalid lines
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return entries;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Delete the tracking file after finalize
|
|
279
|
+
*/
|
|
280
|
+
export function deleteTrackingFile(requirementsDir) {
|
|
281
|
+
const cacheDir = getCacheDir(requirementsDir);
|
|
282
|
+
const trackingPath = path.join(cacheDir, TRACKING_FILE);
|
|
283
|
+
if (fs.existsSync(trackingPath)) {
|
|
284
|
+
fs.unlinkSync(trackingPath);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Read the coverage cache for cloud deduplication
|
|
289
|
+
*/
|
|
290
|
+
export function readCoverageCache(requirementsDir) {
|
|
291
|
+
const cacheDir = getCacheDir(requirementsDir);
|
|
292
|
+
const coveragePath = path.join(cacheDir, COVERAGE_FILE);
|
|
293
|
+
if (!fs.existsSync(coveragePath)) {
|
|
294
|
+
return { current: null, previous: null };
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const content = fs.readFileSync(coveragePath, 'utf-8');
|
|
298
|
+
return JSON.parse(content);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return { current: null, previous: null };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Update the coverage cache after reporting
|
|
306
|
+
*/
|
|
307
|
+
export function updateCoverageCache(requirementsDir, testRunId, requirementKeys) {
|
|
308
|
+
const cacheDir = getCacheDir(requirementsDir, true);
|
|
309
|
+
const coveragePath = path.join(cacheDir, COVERAGE_FILE);
|
|
310
|
+
const existing = readCoverageCache(requirementsDir);
|
|
311
|
+
const newCache = {
|
|
312
|
+
current: {
|
|
313
|
+
testRunId,
|
|
314
|
+
timestamp: Date.now(),
|
|
315
|
+
requirementKeys,
|
|
316
|
+
},
|
|
317
|
+
previous: existing.current,
|
|
318
|
+
};
|
|
319
|
+
fs.writeFileSync(coveragePath, JSON.stringify(newCache, null, 2));
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Check if a requirement needs reporting based on coverage cache
|
|
323
|
+
*/
|
|
324
|
+
export function needsReporting(requirementKey, cache) {
|
|
325
|
+
const STALENESS_THRESHOLD_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
326
|
+
// If no current run, everything needs reporting
|
|
327
|
+
if (!cache.current) {
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
// If requirement wasn't in current run, it needs reporting
|
|
331
|
+
if (!cache.current.requirementKeys.includes(requirementKey)) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
// If current run is stale (>4 hours old), needs reporting
|
|
335
|
+
const age = Date.now() - cache.current.timestamp;
|
|
336
|
+
if (age > STALENESS_THRESHOLD_MS) {
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
// Otherwise, skip reporting (already reported recently)
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TrackedRequirement } from "./tracking.js";
|
|
2
|
+
/**
|
|
3
|
+
* Get the current git branch, or null if not in a git repo
|
|
4
|
+
*/
|
|
5
|
+
export declare function getCurrentBranch(cwd?: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Report coverage to Convex (async, fire-and-forget)
|
|
8
|
+
*
|
|
9
|
+
* @param cwd - Working directory to find .env.local and write cache
|
|
10
|
+
* @param trackedReqs - Optional tracked requirements map. If not provided, falls back to in-memory tracking.
|
|
11
|
+
* When called from finalizeTestRun (globalTeardown), this should be passed explicitly
|
|
12
|
+
* since the in-memory map is empty in a separate process context.
|
|
13
|
+
*/
|
|
14
|
+
export declare function reportCoverageToConvex(cwd?: string, trackedReqs?: Map<string, TrackedRequirement>): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=convexReporting.d.ts.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { getTrackedRequirements } from "./tracking.js";
|
|
3
|
+
import { saveCache, getRequirementsToReport } from "./coverageCache.js";
|
|
4
|
+
import { isLocalOnlyProject } from "../utils/local-project.js";
|
|
5
|
+
import { loadEnvFile, getProjectCredentials } from "../utils/env.js";
|
|
6
|
+
/**
|
|
7
|
+
* Get the current git branch, or null if not in a git repo
|
|
8
|
+
*/
|
|
9
|
+
export function getCurrentBranch(cwd = process.cwd()) {
|
|
10
|
+
try {
|
|
11
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
12
|
+
cwd,
|
|
13
|
+
encoding: "utf-8",
|
|
14
|
+
stdio: ["pipe", "pipe", "ignore"], // Suppress stderr
|
|
15
|
+
}).trim();
|
|
16
|
+
return branch;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
// Not in a git repo or git not available
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Production Convex deployment URL
|
|
25
|
+
*/
|
|
26
|
+
const CONVEX_URL = 'https://data.dotrequirements.io';
|
|
27
|
+
/**
|
|
28
|
+
* Extract file path and line number from caller location string
|
|
29
|
+
*/
|
|
30
|
+
function parseCallerLocation(location) {
|
|
31
|
+
// Location format: "filename.ts:123" or "unknown"
|
|
32
|
+
if (location === "unknown") {
|
|
33
|
+
return { testFile: undefined, testLine: undefined };
|
|
34
|
+
}
|
|
35
|
+
const match = location.match(/^(.+):(\d+)$/);
|
|
36
|
+
if (match) {
|
|
37
|
+
return {
|
|
38
|
+
testFile: match[1],
|
|
39
|
+
testLine: parseInt(match[2], 10),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { testFile: location, testLine: undefined };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Report coverage to Convex (async, fire-and-forget)
|
|
46
|
+
*
|
|
47
|
+
* @param cwd - Working directory to find .env.local and write cache
|
|
48
|
+
* @param trackedReqs - Optional tracked requirements map. If not provided, falls back to in-memory tracking.
|
|
49
|
+
* When called from finalizeTestRun (globalTeardown), this should be passed explicitly
|
|
50
|
+
* since the in-memory map is empty in a separate process context.
|
|
51
|
+
*/
|
|
52
|
+
export async function reportCoverageToConvex(cwd = process.cwd(), trackedReqs) {
|
|
53
|
+
try {
|
|
54
|
+
// Load credentials from .env.local (standard credential discovery)
|
|
55
|
+
loadEnvFile(cwd);
|
|
56
|
+
const credentials = getProjectCredentials();
|
|
57
|
+
// Skip if credentials are missing
|
|
58
|
+
if (!credentials) {
|
|
59
|
+
console.log("\nℹ️ Skipping cloud coverage reporting (DOTREQUIREMENTS_PROJECT_ID or DOTREQUIREMENTS_PROJECT_SECRET not configured)");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const { projectId, projectSecret } = credentials;
|
|
63
|
+
// AUTHZ-1.2: Skip cloud reporting for local-only projects (silent)
|
|
64
|
+
if (isLocalOnlyProject(projectId)) {
|
|
65
|
+
// Silently skip - local-only projects track coverage in-memory only
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Use provided trackedReqs or fall back to in-memory tracking
|
|
69
|
+
const reqs = trackedReqs ?? getTrackedRequirements();
|
|
70
|
+
const requirementKeys = Array.from(reqs.keys());
|
|
71
|
+
// Use caching to filter out requirements that don't need reporting
|
|
72
|
+
const keysToReport = getRequirementsToReport(requirementKeys, cwd);
|
|
73
|
+
if (keysToReport.length === 0) {
|
|
74
|
+
console.log("\n✓ Coverage unchanged since last run (skipping cloud report)");
|
|
75
|
+
// Still update the cache with current timestamp
|
|
76
|
+
saveCache({
|
|
77
|
+
timestamp: Date.now(),
|
|
78
|
+
requirementKeys,
|
|
79
|
+
}, cwd);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const branch = getCurrentBranch(cwd);
|
|
83
|
+
// Build coverage payload
|
|
84
|
+
const coverage = keysToReport.map((key) => {
|
|
85
|
+
const tracked = reqs.get(key);
|
|
86
|
+
// Use the first access location (most likely the test that called requirement())
|
|
87
|
+
const firstAccess = tracked.accessedAt[0] || "unknown";
|
|
88
|
+
const { testFile, testLine } = parseCallerLocation(firstAccess);
|
|
89
|
+
return {
|
|
90
|
+
requirementKey: key,
|
|
91
|
+
testFile,
|
|
92
|
+
testLine,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
// Send to Convex
|
|
96
|
+
const response = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
path: "testCoverage/mutations:recordCoverage",
|
|
103
|
+
args: {
|
|
104
|
+
projectAuth: {
|
|
105
|
+
projectSlug: projectId,
|
|
106
|
+
projectSecret,
|
|
107
|
+
},
|
|
108
|
+
target: {
|
|
109
|
+
type: "project",
|
|
110
|
+
slug: projectId,
|
|
111
|
+
},
|
|
112
|
+
branch,
|
|
113
|
+
coverage,
|
|
114
|
+
},
|
|
115
|
+
format: "json",
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const error = await response.text();
|
|
120
|
+
console.warn(`\n⚠️ Failed to report coverage to cloud: ${response.status} ${error}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const result = await response.json();
|
|
124
|
+
console.log(`\n✓ Reported ${keysToReport.length} requirement(s) to cloud (branch: ${branch})`);
|
|
125
|
+
// Update cache with current test run
|
|
126
|
+
saveCache({
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
requirementKeys,
|
|
129
|
+
}, cwd);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// Don't throw - reporting failures shouldn't break tests
|
|
133
|
+
console.warn(`\n⚠️ Error reporting coverage to cloud: ${error instanceof Error ? error.message : String(error)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=convexReporting.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface TestRun {
|
|
2
|
+
testRunId: string;
|
|
3
|
+
timestamp: number;
|
|
4
|
+
requirementKeys: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface CoverageCache {
|
|
7
|
+
current: TestRun | null;
|
|
8
|
+
previous: TestRun | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Load the coverage cache from disk
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadCache(cwd?: string): CoverageCache;
|
|
14
|
+
/**
|
|
15
|
+
* Save a new test run to the cache, rotating current → previous
|
|
16
|
+
*/
|
|
17
|
+
export declare function saveCache(testRun: Omit<TestRun, "testRunId">, cwd?: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Check if a requirement needs reporting based on cache state
|
|
20
|
+
* Returns true if:
|
|
21
|
+
* - Requirement was not covered in previous run, OR
|
|
22
|
+
* - Previous run was more than 4 hours ago
|
|
23
|
+
*/
|
|
24
|
+
export declare function needsReporting(requirementKey: string, cache: CoverageCache): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Get the list of requirements that need reporting
|
|
27
|
+
* (new or changed since last run, or stale)
|
|
28
|
+
*/
|
|
29
|
+
export declare function getRequirementsToReport(currentRequirementKeys: string[], cwd?: string): string[];
|
|
30
|
+
//# sourceMappingURL=coverageCache.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { v4 as uuidv4 } from "uuid";
|
|
4
|
+
const CACHE_FILE = ".requirements/.coverage-cache.json";
|
|
5
|
+
const STALENESS_THRESHOLD_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
6
|
+
/**
|
|
7
|
+
* Load the coverage cache from disk
|
|
8
|
+
*/
|
|
9
|
+
export function loadCache(cwd = process.cwd()) {
|
|
10
|
+
const cachePath = join(cwd, CACHE_FILE);
|
|
11
|
+
if (!existsSync(cachePath)) {
|
|
12
|
+
return { current: null, previous: null };
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const content = readFileSync(cachePath, "utf-8");
|
|
16
|
+
return JSON.parse(content);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
// If cache is corrupt, start fresh
|
|
20
|
+
console.warn("Warning: Coverage cache is corrupt, starting fresh");
|
|
21
|
+
return { current: null, previous: null };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Save a new test run to the cache, rotating current → previous
|
|
26
|
+
*/
|
|
27
|
+
export function saveCache(testRun, cwd = process.cwd()) {
|
|
28
|
+
const cache = loadCache(cwd);
|
|
29
|
+
const cachePath = join(cwd, CACHE_FILE);
|
|
30
|
+
const newCache = {
|
|
31
|
+
current: {
|
|
32
|
+
testRunId: uuidv4(),
|
|
33
|
+
...testRun,
|
|
34
|
+
},
|
|
35
|
+
previous: cache.current,
|
|
36
|
+
};
|
|
37
|
+
writeFileSync(cachePath, JSON.stringify(newCache, null, 2), "utf-8");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if a requirement needs reporting based on cache state
|
|
41
|
+
* Returns true if:
|
|
42
|
+
* - Requirement was not covered in previous run, OR
|
|
43
|
+
* - Previous run was more than 4 hours ago
|
|
44
|
+
*/
|
|
45
|
+
export function needsReporting(requirementKey, cache) {
|
|
46
|
+
// If no previous run, everything needs reporting
|
|
47
|
+
if (!cache.current) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
// If requirement wasn't in current run, it needs reporting
|
|
51
|
+
if (!cache.current.requirementKeys.includes(requirementKey)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
// If current run is stale (>4 hours old), needs reporting
|
|
55
|
+
const age = Date.now() - cache.current.timestamp;
|
|
56
|
+
if (age > STALENESS_THRESHOLD_MS) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
// Otherwise, skip reporting (already reported recently)
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get the list of requirements that need reporting
|
|
64
|
+
* (new or changed since last run, or stale)
|
|
65
|
+
*/
|
|
66
|
+
export function getRequirementsToReport(currentRequirementKeys, cwd = process.cwd()) {
|
|
67
|
+
const cache = loadCache(cwd);
|
|
68
|
+
return currentRequirementKeys.filter((key) => needsReporting(key, cache));
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=coverageCache.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
export interface FinalizeOptions {
|
|
13
|
+
/** Working directory to start search from (defaults to cwd) */
|
|
14
|
+
cwd?: string;
|
|
15
|
+
/** Whether to report to cloud (defaults to true) */
|
|
16
|
+
reportToCloud?: boolean;
|
|
17
|
+
/** Whether to print local report (defaults to true) */
|
|
18
|
+
printLocalReport?: boolean;
|
|
19
|
+
/** Whether to clean up tracking data on success (defaults to true) */
|
|
20
|
+
cleanup?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface FinalizeResult {
|
|
23
|
+
/** Number of unique requirements exercised */
|
|
24
|
+
requirementsTested: number;
|
|
25
|
+
/** Total requirements available */
|
|
26
|
+
totalRequirements: number;
|
|
27
|
+
/** Coverage percentage */
|
|
28
|
+
coveragePercent: number;
|
|
29
|
+
/** Whether cloud report was sent */
|
|
30
|
+
cloudReportSent: boolean;
|
|
31
|
+
/** Number of requirements reported to cloud (may be less due to deduplication) */
|
|
32
|
+
requirementsReportedToCloud: number;
|
|
33
|
+
/** Error if cloud reporting failed */
|
|
34
|
+
cloudError?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Finalize the test run after all tests complete.
|
|
38
|
+
*
|
|
39
|
+
* This function should be called once after tests complete (e.g., in globalTeardown).
|
|
40
|
+
* It reads tracking data, generates reports, and cleans up.
|
|
41
|
+
*
|
|
42
|
+
* HARNESS-FINALIZE-1: After finalize() runs, developers see which requirements were tested
|
|
43
|
+
* HARNESS-FINALIZE-2: After finalize() runs, coverage data is available in the cloud
|
|
44
|
+
* HARNESS-FINALIZE-3: Cloud coverage records identify which requirement was tested and where
|
|
45
|
+
* HARNESS-FINALIZE-4: Finalize cleans up after itself
|
|
46
|
+
*/
|
|
47
|
+
export declare function finalize(options?: FinalizeOptions): Promise<FinalizeResult>;
|
|
48
|
+
//# sourceMappingURL=finalize.d.ts.map
|