@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.
Files changed (127) hide show
  1. package/README.md +478 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.js +82 -0
  4. package/dist/commands/init.d.ts +6 -0
  5. package/dist/commands/init.js +355 -0
  6. package/dist/commands/link.d.ts +15 -0
  7. package/dist/commands/link.js +156 -0
  8. package/dist/commands/login.d.ts +12 -0
  9. package/dist/commands/login.js +117 -0
  10. package/dist/commands/logout.d.ts +5 -0
  11. package/dist/commands/logout.js +17 -0
  12. package/dist/commands/mcp-setup.d.ts +5 -0
  13. package/dist/commands/mcp-setup.js +367 -0
  14. package/dist/commands/mcp.d.ts +6 -0
  15. package/dist/commands/mcp.js +10 -0
  16. package/dist/commands/pull.d.ts +7 -0
  17. package/dist/commands/pull.js +171 -0
  18. package/dist/commands/push.d.ts +11 -0
  19. package/dist/commands/push.js +310 -0
  20. package/dist/commands/test.d.ts +6 -0
  21. package/dist/commands/test.js +78 -0
  22. package/dist/config.d.ts +5 -0
  23. package/dist/config.js +16 -0
  24. package/dist/convex.d.ts +56 -0
  25. package/dist/convex.js +58 -0
  26. package/dist/harness/cache.d.ts +135 -0
  27. package/dist/harness/cache.js +342 -0
  28. package/dist/harness/convexReporting.d.ts +15 -0
  29. package/dist/harness/convexReporting.js +136 -0
  30. package/dist/harness/coverageCache.d.ts +30 -0
  31. package/dist/harness/coverageCache.js +70 -0
  32. package/dist/harness/finalize.d.ts +48 -0
  33. package/dist/harness/finalize.js +299 -0
  34. package/dist/harness/index.d.ts +70 -0
  35. package/dist/harness/index.js +103 -0
  36. package/dist/harness/localReporting.d.ts +6 -0
  37. package/dist/harness/localReporting.js +49 -0
  38. package/dist/harness/prepare.d.ts +41 -0
  39. package/dist/harness/prepare.js +83 -0
  40. package/dist/harness/requirementsLoader.d.ts +45 -0
  41. package/dist/harness/requirementsLoader.js +201 -0
  42. package/dist/harness/tracking.d.ts +49 -0
  43. package/dist/harness/tracking.js +179 -0
  44. package/dist/harness/types.d.ts +12 -0
  45. package/dist/harness/types.js +6 -0
  46. package/dist/mcp/convexClient.d.ts +43 -0
  47. package/dist/mcp/convexClient.js +101 -0
  48. package/dist/mcp/grep.d.ts +24 -0
  49. package/dist/mcp/grep.js +261 -0
  50. package/dist/mcp/index.d.ts +3 -0
  51. package/dist/mcp/index.js +1758 -0
  52. package/dist/mcp/requirements.d.ts +47 -0
  53. package/dist/mcp/requirements.js +141 -0
  54. package/dist/mcp/testCodeExtractor.d.ts +22 -0
  55. package/dist/mcp/testCodeExtractor.js +152 -0
  56. package/dist/mcp/types.d.ts +27 -0
  57. package/dist/mcp/types.js +2 -0
  58. package/dist/schema/browser.d.ts +12 -0
  59. package/dist/schema/browser.js +24 -0
  60. package/dist/schema/builder.d.ts +25 -0
  61. package/dist/schema/builder.js +125 -0
  62. package/dist/schema/conversions.d.ts +69 -0
  63. package/dist/schema/conversions.js +201 -0
  64. package/dist/schema/index.d.ts +14 -0
  65. package/dist/schema/index.js +24 -0
  66. package/dist/schema/parser-core.d.ts +61 -0
  67. package/dist/schema/parser-core.js +247 -0
  68. package/dist/schema/parser.d.ts +44 -0
  69. package/dist/schema/parser.js +295 -0
  70. package/dist/schema/resolver.d.ts +66 -0
  71. package/dist/schema/resolver.js +185 -0
  72. package/dist/schema/schemas.d.ts +312 -0
  73. package/dist/schema/schemas.js +258 -0
  74. package/dist/schema/test-schema.d.ts +5 -0
  75. package/dist/schema/test-schema.js +81 -0
  76. package/dist/templates/antigravity-gemini.md +3 -0
  77. package/dist/templates/antigravity-overview-rule.md +3 -0
  78. package/dist/templates/antigravity-test-rule.md +3 -0
  79. package/dist/templates/behavioral-core.md +25 -0
  80. package/dist/templates/claude-code-overview-skill.md +6 -0
  81. package/dist/templates/claude-code-skill.md +6 -0
  82. package/dist/templates/claude-code-test-skill.md +6 -0
  83. package/dist/templates/codex-agents.md +3 -0
  84. package/dist/templates/codex-overview-agents.md +3 -0
  85. package/dist/templates/codex-test-agents.md +3 -0
  86. package/dist/templates/cursor-overview-rule.mdc +5 -0
  87. package/dist/templates/cursor-rule.mdc +5 -0
  88. package/dist/templates/cursor-test-rule.mdc +5 -0
  89. package/dist/templates/example-requirements.d.ts +8 -0
  90. package/dist/templates/example-requirements.js +88 -0
  91. package/dist/templates/example-requirements.ts +88 -0
  92. package/dist/templates/overview-core.md +27 -0
  93. package/dist/templates/requirements-readme.d.ts +5 -0
  94. package/dist/templates/requirements-readme.js +31 -0
  95. package/dist/templates/requirements-readme.ts +30 -0
  96. package/dist/templates/test-writing-core.md +72 -0
  97. package/dist/utils/brand.d.ts +5 -0
  98. package/dist/utils/brand.js +8 -0
  99. package/dist/utils/browser-launch.d.ts +19 -0
  100. package/dist/utils/browser-launch.js +36 -0
  101. package/dist/utils/detect-existing-project.d.ts +5 -0
  102. package/dist/utils/detect-existing-project.js +34 -0
  103. package/dist/utils/env.d.ts +19 -0
  104. package/dist/utils/env.js +56 -0
  105. package/dist/utils/gitignore.d.ts +7 -0
  106. package/dist/utils/gitignore.js +29 -0
  107. package/dist/utils/local-project.d.ts +31 -0
  108. package/dist/utils/local-project.js +33 -0
  109. package/dist/utils/oauth-callback-server.d.ts +28 -0
  110. package/dist/utils/oauth-callback-server.js +156 -0
  111. package/dist/utils/oauth-flow.d.ts +22 -0
  112. package/dist/utils/oauth-flow.js +120 -0
  113. package/dist/utils/project-discovery.d.ts +57 -0
  114. package/dist/utils/project-discovery.js +146 -0
  115. package/dist/utils/project-name.d.ts +8 -0
  116. package/dist/utils/project-name.js +48 -0
  117. package/dist/utils/project-selector.d.ts +25 -0
  118. package/dist/utils/project-selector.js +69 -0
  119. package/dist/utils/prompts.d.ts +33 -0
  120. package/dist/utils/prompts.js +60 -0
  121. package/dist/utils/templates.d.ts +29 -0
  122. package/dist/utils/templates.js +67 -0
  123. package/dist/utils/token-refresh.d.ts +24 -0
  124. package/dist/utils/token-refresh.js +69 -0
  125. package/dist/utils/token-storage.d.ts +31 -0
  126. package/dist/utils/token-storage.js +57 -0
  127. 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