@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,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requirements loader for test harness.
|
|
3
|
+
* Loads requirements from .requirements/ directories using shared schema parsing.
|
|
4
|
+
*
|
|
5
|
+
* Project root discovery (in priority order):
|
|
6
|
+
* 1. Explicit projectRoot option passed to loadRequirements()
|
|
7
|
+
* 2. DOTREQUIREMENTS_PROJECT_ROOT environment variable
|
|
8
|
+
* 3. Cached project root file (.requirements/.cache/project-root) written by prepare()
|
|
9
|
+
*/
|
|
10
|
+
import { type RequirementNode } from '../schema/index.js';
|
|
11
|
+
import { Requirement } from './types.js';
|
|
12
|
+
export declare const loadedRequirements: Map<string, RequirementNode>;
|
|
13
|
+
export interface LoadOptions {
|
|
14
|
+
/** Explicit project root path. Takes precedence over env var and cached file. */
|
|
15
|
+
projectRoot?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Load all requirements from .requirements/ directories.
|
|
19
|
+
*
|
|
20
|
+
* Tries lookup cache first (fast path after prepare() was called).
|
|
21
|
+
* Falls back to parsing .requirements.md files on-demand.
|
|
22
|
+
*
|
|
23
|
+
* @param options.projectRoot - Explicit project root path (optional)
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadRequirements(options?: LoadOptions): void;
|
|
26
|
+
/**
|
|
27
|
+
* Get a requirement by ID (supports both numeric and label-based paths).
|
|
28
|
+
*
|
|
29
|
+
* Examples:
|
|
30
|
+
* - getRequirement('REQ-123') - Root requirement
|
|
31
|
+
* - getRequirement('REQ-123.0') - First child (numeric)
|
|
32
|
+
* - getRequirement('REQ-123.given') - First "given" child (label)
|
|
33
|
+
* - getRequirement('REQ-123.given#1') - Second "given" child (disambiguated)
|
|
34
|
+
* - getRequirement('REQ-123.0.1') - Nested child
|
|
35
|
+
*/
|
|
36
|
+
export declare function getRequirement(reqId: string): Requirement | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Get all loaded requirements (flat map)
|
|
39
|
+
*/
|
|
40
|
+
export declare function getAllRequirements(): Map<string, Requirement>;
|
|
41
|
+
/**
|
|
42
|
+
* Get all root requirement trees
|
|
43
|
+
*/
|
|
44
|
+
export declare function getAllRequirementTrees(): Map<string, Requirement>;
|
|
45
|
+
//# sourceMappingURL=requirementsLoader.d.ts.map
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requirements loader for test harness.
|
|
3
|
+
* Loads requirements from .requirements/ directories using shared schema parsing.
|
|
4
|
+
*
|
|
5
|
+
* Project root discovery (in priority order):
|
|
6
|
+
* 1. Explicit projectRoot option passed to loadRequirements()
|
|
7
|
+
* 2. DOTREQUIREMENTS_PROJECT_ROOT environment variable
|
|
8
|
+
* 3. Cached project root file (.requirements/.cache/project-root) written by prepare()
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { globSync } from 'glob';
|
|
13
|
+
import { parseRequirementsFromFile, resolveRequirementPath, } from '../schema/index.js';
|
|
14
|
+
import { findCachedProjectRoot, readLookupCache, } from './cache.js';
|
|
15
|
+
// Cache: Maps full requirement IDs (e.g., "REQ-123.0.1") to RequirementNode
|
|
16
|
+
export const loadedRequirements = new Map();
|
|
17
|
+
// Cache: Maps root requirement IDs (e.g., "REQ-123") to their tree
|
|
18
|
+
const requirementTrees = new Map();
|
|
19
|
+
// Flag to track if we've warned about fallback parsing
|
|
20
|
+
let warnedAboutFallback = false;
|
|
21
|
+
/**
|
|
22
|
+
* Find the project root using the priority order:
|
|
23
|
+
* 1. Explicit projectRoot option
|
|
24
|
+
* 2. DOTREQUIREMENTS_PROJECT_ROOT env var
|
|
25
|
+
* 3. Cached project root file
|
|
26
|
+
*/
|
|
27
|
+
function findProjectRoot(options = {}) {
|
|
28
|
+
// Priority 1: Explicit option
|
|
29
|
+
if (options.projectRoot) {
|
|
30
|
+
return options.projectRoot;
|
|
31
|
+
}
|
|
32
|
+
// Priority 2: Environment variable
|
|
33
|
+
if (process.env.DOTREQUIREMENTS_PROJECT_ROOT) {
|
|
34
|
+
return process.env.DOTREQUIREMENTS_PROJECT_ROOT;
|
|
35
|
+
}
|
|
36
|
+
// Priority 3: Cached file (written by prepare())
|
|
37
|
+
return findCachedProjectRoot(process.cwd());
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Find all *.requirements.md files under project root synchronously.
|
|
41
|
+
*/
|
|
42
|
+
function findRequirementsFilesSync(projectRoot) {
|
|
43
|
+
const pattern = '**/*.requirements.md';
|
|
44
|
+
const matches = globSync(pattern, {
|
|
45
|
+
cwd: projectRoot,
|
|
46
|
+
ignore: [
|
|
47
|
+
'**/node_modules/**',
|
|
48
|
+
'**/dist/**',
|
|
49
|
+
'**/.git/**',
|
|
50
|
+
'**/build/**',
|
|
51
|
+
'**/example/**',
|
|
52
|
+
'**/examples/**',
|
|
53
|
+
'**/__tests__/**',
|
|
54
|
+
'**/fixtures/**',
|
|
55
|
+
'**/.fixtures/**',
|
|
56
|
+
],
|
|
57
|
+
dot: true, // Include dotfiles/dotdirs like .requirements/
|
|
58
|
+
});
|
|
59
|
+
return matches.map(m => path.join(projectRoot, m));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Try to load requirements from lookup cache (fast path).
|
|
63
|
+
* Returns true if cache was loaded, false if not available.
|
|
64
|
+
*/
|
|
65
|
+
function tryLoadFromCache(projectRoot) {
|
|
66
|
+
const requirementsDir = path.join(projectRoot, '.requirements');
|
|
67
|
+
if (!fs.existsSync(requirementsDir)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const cache = readLookupCache(requirementsDir);
|
|
71
|
+
if (!cache) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
// Load all requirements from cache into the flat map
|
|
75
|
+
for (const [id, req] of Object.entries(cache.requirements)) {
|
|
76
|
+
// Create a minimal RequirementNode structure
|
|
77
|
+
const node = {
|
|
78
|
+
id: req.id,
|
|
79
|
+
label: req.label,
|
|
80
|
+
content: req.content,
|
|
81
|
+
children: [], // Children are loaded separately by ID
|
|
82
|
+
};
|
|
83
|
+
loadedRequirements.set(id, node);
|
|
84
|
+
// If this is a root node (no dot in ID), add to trees cache
|
|
85
|
+
if (!id.includes('.')) {
|
|
86
|
+
requirementTrees.set(id, node);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Recursively flatten a requirement tree into the cache.
|
|
93
|
+
* Adds both the root and all children with their full IDs.
|
|
94
|
+
*/
|
|
95
|
+
function flattenRequirementTree(node) {
|
|
96
|
+
// Add this node to the flat cache
|
|
97
|
+
loadedRequirements.set(node.id, node);
|
|
98
|
+
// If this is a root node (no dot in ID), also add to trees cache
|
|
99
|
+
if (!node.id.includes('.')) {
|
|
100
|
+
requirementTrees.set(node.id, node);
|
|
101
|
+
}
|
|
102
|
+
// Recursively add children
|
|
103
|
+
for (const child of node.children) {
|
|
104
|
+
flattenRequirementTree(child);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Load all requirements from .requirements/ directories.
|
|
109
|
+
*
|
|
110
|
+
* Tries lookup cache first (fast path after prepare() was called).
|
|
111
|
+
* Falls back to parsing .requirements.md files on-demand.
|
|
112
|
+
*
|
|
113
|
+
* @param options.projectRoot - Explicit project root path (optional)
|
|
114
|
+
*/
|
|
115
|
+
export function loadRequirements(options = {}) {
|
|
116
|
+
// Clear existing cache when loading
|
|
117
|
+
loadedRequirements.clear();
|
|
118
|
+
requirementTrees.clear();
|
|
119
|
+
// Find project root
|
|
120
|
+
const projectRoot = findProjectRoot(options);
|
|
121
|
+
if (!projectRoot) {
|
|
122
|
+
throw new Error('Could not find project root. ' +
|
|
123
|
+
'Set DOTREQUIREMENTS_PROJECT_ROOT environment variable or run prepare() in globalSetup.');
|
|
124
|
+
}
|
|
125
|
+
// Try cache first (fast path)
|
|
126
|
+
if (tryLoadFromCache(projectRoot)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Fallback: Parse .requirements.md files on-demand
|
|
130
|
+
if (!warnedAboutFallback && !options.projectRoot) {
|
|
131
|
+
console.warn('⚠️ Performance warning: prepare() was not called. ' +
|
|
132
|
+
'Parsing requirements on-demand. Run prepare() in globalSetup for faster tests.');
|
|
133
|
+
warnedAboutFallback = true;
|
|
134
|
+
}
|
|
135
|
+
// Find all .requirements.md files under the project root
|
|
136
|
+
const requirementsFiles = findRequirementsFilesSync(projectRoot);
|
|
137
|
+
// Parse and flatten each file
|
|
138
|
+
for (const file of requirementsFiles) {
|
|
139
|
+
try {
|
|
140
|
+
const { requirements } = parseRequirementsFromFile(file);
|
|
141
|
+
// Flatten each requirement tree into the cache
|
|
142
|
+
for (const req of requirements) {
|
|
143
|
+
flattenRequirementTree(req);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
148
|
+
console.error(`Error loading ${file}:`, errorMessage);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Get a requirement by ID (supports both numeric and label-based paths).
|
|
154
|
+
*
|
|
155
|
+
* Examples:
|
|
156
|
+
* - getRequirement('REQ-123') - Root requirement
|
|
157
|
+
* - getRequirement('REQ-123.0') - First child (numeric)
|
|
158
|
+
* - getRequirement('REQ-123.given') - First "given" child (label)
|
|
159
|
+
* - getRequirement('REQ-123.given#1') - Second "given" child (disambiguated)
|
|
160
|
+
* - getRequirement('REQ-123.0.1') - Nested child
|
|
161
|
+
*/
|
|
162
|
+
export function getRequirement(reqId) {
|
|
163
|
+
if (loadedRequirements.size === 0) {
|
|
164
|
+
loadRequirements();
|
|
165
|
+
}
|
|
166
|
+
// First try direct lookup (for numeric paths like "REQ-123.0.1")
|
|
167
|
+
const direct = loadedRequirements.get(reqId);
|
|
168
|
+
if (direct) {
|
|
169
|
+
return direct;
|
|
170
|
+
}
|
|
171
|
+
// If not found, try resolving label-based paths
|
|
172
|
+
// resolveRequirementPath expects an array of root requirements and the full path
|
|
173
|
+
const allTrees = Array.from(requirementTrees.values());
|
|
174
|
+
try {
|
|
175
|
+
const resolved = resolveRequirementPath(allTrees, reqId);
|
|
176
|
+
return resolved ?? undefined;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Path resolution failed
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get all loaded requirements (flat map)
|
|
185
|
+
*/
|
|
186
|
+
export function getAllRequirements() {
|
|
187
|
+
if (loadedRequirements.size === 0) {
|
|
188
|
+
loadRequirements();
|
|
189
|
+
}
|
|
190
|
+
return loadedRequirements;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get all root requirement trees
|
|
194
|
+
*/
|
|
195
|
+
export function getAllRequirementTrees() {
|
|
196
|
+
if (requirementTrees.size === 0) {
|
|
197
|
+
loadRequirements();
|
|
198
|
+
}
|
|
199
|
+
return requirementTrees;
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=requirementsLoader.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracking for the test harness.
|
|
3
|
+
*
|
|
4
|
+
* Tracks requirement() calls during test execution.
|
|
5
|
+
* Uses JSONL format for cross-process safety (parallel test workers).
|
|
6
|
+
*
|
|
7
|
+
* HARNESS-REQUIREMENT-3: Each requirement() call is recorded for coverage reporting
|
|
8
|
+
*/
|
|
9
|
+
export interface TrackedRequirement {
|
|
10
|
+
id: string;
|
|
11
|
+
fieldsAccessed: string[];
|
|
12
|
+
accessedAt: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function ensureTestRun(): string;
|
|
15
|
+
export declare function getCallerLocation(): string;
|
|
16
|
+
/**
|
|
17
|
+
* Track a requirement access.
|
|
18
|
+
*
|
|
19
|
+
* HARNESS-REQUIREMENT-3.0: After a test calls requirement(path), finalize() can report
|
|
20
|
+
* HARNESS-REQUIREMENT-3.1: Coverage record includes which file and line called requirement()
|
|
21
|
+
* HARNESS-REQUIREMENT-3.2: Works correctly when tests run in parallel processes
|
|
22
|
+
*
|
|
23
|
+
* @param reqId - Full requirement path (e.g., 'REQ-123.given' or 'REQ-123.0.1')
|
|
24
|
+
*/
|
|
25
|
+
export declare function trackRequirement(reqId: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* Save current tracking data.
|
|
28
|
+
*
|
|
29
|
+
* In the new JSONL-based approach, this is a no-op since tracking is
|
|
30
|
+
* written immediately via appendTrackingEntry. Kept for backward compatibility.
|
|
31
|
+
*/
|
|
32
|
+
export declare function saveTrackingData(): void;
|
|
33
|
+
export declare function getTrackedRequirements(): Map<string, TrackedRequirement>;
|
|
34
|
+
export declare function clearTracking(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Initialize a test run (called by globalSetup).
|
|
37
|
+
*
|
|
38
|
+
* @deprecated Use prepare() from './prepare.js' instead.
|
|
39
|
+
* This function is kept for backward compatibility.
|
|
40
|
+
*/
|
|
41
|
+
export declare function initTestRun(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Finalize test run and generate report (called by globalTeardown).
|
|
44
|
+
*
|
|
45
|
+
* @deprecated Use finalize() from './finalize.js' instead.
|
|
46
|
+
* This function is kept for backward compatibility.
|
|
47
|
+
*/
|
|
48
|
+
export declare function finalizeTestRun(): void;
|
|
49
|
+
//# sourceMappingURL=tracking.d.ts.map
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracking for the test harness.
|
|
3
|
+
*
|
|
4
|
+
* Tracks requirement() calls during test execution.
|
|
5
|
+
* Uses JSONL format for cross-process safety (parallel test workers).
|
|
6
|
+
*
|
|
7
|
+
* HARNESS-REQUIREMENT-3: Each requirement() call is recorded for coverage reporting
|
|
8
|
+
*/
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { findRequirementsDir, findProjectRoot, getTestRunId, appendTrackingEntry, getCacheDir, initTestRunId, clearTrackingFile, } from './cache.js';
|
|
11
|
+
const trackedRequirements = new Map();
|
|
12
|
+
// Test run tracking
|
|
13
|
+
let testRunId = null;
|
|
14
|
+
let requirementsDirCached = null;
|
|
15
|
+
// Initialize test run
|
|
16
|
+
export function ensureTestRun() {
|
|
17
|
+
if (!testRunId) {
|
|
18
|
+
// Priority 1: Use environment variable (cross-process persistence from globalSetup)
|
|
19
|
+
if (process.env.DOTREQUIREMENTS_PROJECT_ROOT) {
|
|
20
|
+
requirementsDirCached = path.join(process.env.DOTREQUIREMENTS_PROJECT_ROOT, '.requirements');
|
|
21
|
+
}
|
|
22
|
+
// Priority 2: Find from caller directory
|
|
23
|
+
if (!requirementsDirCached) {
|
|
24
|
+
const callerDir = getCallerDirectory();
|
|
25
|
+
requirementsDirCached = findRequirementsDir(callerDir || process.cwd());
|
|
26
|
+
// If not found from caller directory, try from cwd explicitly
|
|
27
|
+
if (!requirementsDirCached && callerDir) {
|
|
28
|
+
requirementsDirCached = findRequirementsDir(process.cwd());
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!requirementsDirCached) {
|
|
32
|
+
throw new Error('Could not find .requirements directory. Please create one in your project root.');
|
|
33
|
+
}
|
|
34
|
+
// Try to read from shared test run ID file (set by prepare())
|
|
35
|
+
const existingRunId = getTestRunId(requirementsDirCached);
|
|
36
|
+
if (existingRunId) {
|
|
37
|
+
testRunId = existingRunId;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// Fallback for standalone usage without prepare()
|
|
41
|
+
testRunId = `${Date.now()}-${process.pid}`;
|
|
42
|
+
}
|
|
43
|
+
// Ensure cache directory exists
|
|
44
|
+
getCacheDir(requirementsDirCached, true);
|
|
45
|
+
}
|
|
46
|
+
return testRunId;
|
|
47
|
+
}
|
|
48
|
+
// Get caller location for tracking
|
|
49
|
+
export function getCallerLocation() {
|
|
50
|
+
const error = new Error();
|
|
51
|
+
const stack = error.stack?.split('\n') || [];
|
|
52
|
+
// Find the first stack frame that's not in our library files
|
|
53
|
+
for (let i = 2; i < stack.length; i++) {
|
|
54
|
+
const frame = stack[i];
|
|
55
|
+
if (!frame.includes('/harness/index.ts') &&
|
|
56
|
+
!frame.includes('/harness/index.js') &&
|
|
57
|
+
!frame.includes('/harness/tracking.ts') &&
|
|
58
|
+
!frame.includes('/harness/tracking.js')) {
|
|
59
|
+
// Extract filename and line number
|
|
60
|
+
const regex1 = /\((.+):(\d+):(\d+)\)/;
|
|
61
|
+
const regex2 = /at (.+):(\d+):(\d+)/;
|
|
62
|
+
const match = regex1.exec(frame) || regex2.exec(frame);
|
|
63
|
+
if (match) {
|
|
64
|
+
const filePath = match[1];
|
|
65
|
+
const lineNumber = match[2];
|
|
66
|
+
return `${path.basename(filePath)}:${lineNumber}`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return 'unknown';
|
|
71
|
+
}
|
|
72
|
+
// Get caller's directory (full path) for finding .requirements
|
|
73
|
+
function getCallerDirectory() {
|
|
74
|
+
const error = new Error();
|
|
75
|
+
const stack = error.stack?.split('\n') || [];
|
|
76
|
+
// Find the first stack frame that's not in our library files
|
|
77
|
+
for (let i = 2; i < stack.length; i++) {
|
|
78
|
+
const frame = stack[i];
|
|
79
|
+
if (!frame.includes('/harness/index.ts') &&
|
|
80
|
+
!frame.includes('/harness/index.js') &&
|
|
81
|
+
!frame.includes('/harness/tracking.ts') &&
|
|
82
|
+
!frame.includes('/harness/tracking.js')) {
|
|
83
|
+
// Extract filename
|
|
84
|
+
const regex1 = /\((.+):(\d+):(\d+)\)/;
|
|
85
|
+
const regex2 = /at (.+):(\d+):(\d+)/;
|
|
86
|
+
const match = regex1.exec(frame) || regex2.exec(frame);
|
|
87
|
+
if (match) {
|
|
88
|
+
const filePath = match[1];
|
|
89
|
+
return path.dirname(filePath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Track a requirement access.
|
|
97
|
+
*
|
|
98
|
+
* HARNESS-REQUIREMENT-3.0: After a test calls requirement(path), finalize() can report
|
|
99
|
+
* HARNESS-REQUIREMENT-3.1: Coverage record includes which file and line called requirement()
|
|
100
|
+
* HARNESS-REQUIREMENT-3.2: Works correctly when tests run in parallel processes
|
|
101
|
+
*
|
|
102
|
+
* @param reqId - Full requirement path (e.g., 'REQ-123.given' or 'REQ-123.0.1')
|
|
103
|
+
*/
|
|
104
|
+
export function trackRequirement(reqId) {
|
|
105
|
+
const callerLocation = getCallerLocation();
|
|
106
|
+
const runId = ensureTestRun();
|
|
107
|
+
// Track in memory for backward compatibility
|
|
108
|
+
if (!trackedRequirements.has(reqId)) {
|
|
109
|
+
trackedRequirements.set(reqId, {
|
|
110
|
+
id: reqId,
|
|
111
|
+
fieldsAccessed: [], // Kept for backward compatibility
|
|
112
|
+
accessedAt: [],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const tracked = trackedRequirements.get(reqId);
|
|
116
|
+
tracked.accessedAt.push(callerLocation);
|
|
117
|
+
// Append to JSONL file for cross-process tracking
|
|
118
|
+
// HARNESS-REQUIREMENT-3.2: JSONL is append-only, safe for parallel workers
|
|
119
|
+
if (requirementsDirCached) {
|
|
120
|
+
const entry = {
|
|
121
|
+
requirementKey: reqId,
|
|
122
|
+
callerLocation,
|
|
123
|
+
timestamp: Date.now(),
|
|
124
|
+
testRunId: runId,
|
|
125
|
+
};
|
|
126
|
+
appendTrackingEntry(requirementsDirCached, entry);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Save current tracking data.
|
|
131
|
+
*
|
|
132
|
+
* In the new JSONL-based approach, this is a no-op since tracking is
|
|
133
|
+
* written immediately via appendTrackingEntry. Kept for backward compatibility.
|
|
134
|
+
*/
|
|
135
|
+
export function saveTrackingData() {
|
|
136
|
+
// No-op: JSONL entries are written immediately in trackRequirement()
|
|
137
|
+
}
|
|
138
|
+
// Get all tracked requirements (in-memory)
|
|
139
|
+
export function getTrackedRequirements() {
|
|
140
|
+
return trackedRequirements;
|
|
141
|
+
}
|
|
142
|
+
// Clear tracking (for testing)
|
|
143
|
+
export function clearTracking() {
|
|
144
|
+
trackedRequirements.clear();
|
|
145
|
+
testRunId = null;
|
|
146
|
+
requirementsDirCached = null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Initialize a test run (called by globalSetup).
|
|
150
|
+
*
|
|
151
|
+
* @deprecated Use prepare() from './prepare.js' instead.
|
|
152
|
+
* This function is kept for backward compatibility.
|
|
153
|
+
*/
|
|
154
|
+
export function initTestRun() {
|
|
155
|
+
const projectRoot = findProjectRoot();
|
|
156
|
+
if (!projectRoot) {
|
|
157
|
+
throw new Error('Could not find .requirements directory. Please create one in your project root.');
|
|
158
|
+
}
|
|
159
|
+
const reqDir = findRequirementsDir();
|
|
160
|
+
// Initialize test run ID
|
|
161
|
+
initTestRunId(reqDir);
|
|
162
|
+
// Clear previous tracking data
|
|
163
|
+
clearTrackingFile(reqDir);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Finalize test run and generate report (called by globalTeardown).
|
|
167
|
+
*
|
|
168
|
+
* @deprecated Use finalize() from './finalize.js' instead.
|
|
169
|
+
* This function is kept for backward compatibility.
|
|
170
|
+
*/
|
|
171
|
+
export function finalizeTestRun() {
|
|
172
|
+
// Import and call the new finalize function
|
|
173
|
+
// Using dynamic import to avoid circular dependencies
|
|
174
|
+
const { finalize } = require('./finalize.js');
|
|
175
|
+
finalize().catch((error) => {
|
|
176
|
+
console.error('Error in finalizeTestRun:', error.message);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=tracking.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for test harness.
|
|
3
|
+
* Uses shared schema types for consistency with CLI and MCP.
|
|
4
|
+
*/
|
|
5
|
+
import type { RequirementNode } from '../schema/index.js';
|
|
6
|
+
export type { RequirementNode as Requirement };
|
|
7
|
+
export interface CriteriaDetail {
|
|
8
|
+
reqId: string;
|
|
9
|
+
totalCriteria: number;
|
|
10
|
+
testedCriteria: number;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load Convex configuration from .env file or environment variables
|
|
3
|
+
*/
|
|
4
|
+
interface ConvexConfig {
|
|
5
|
+
convexUrl: string;
|
|
6
|
+
projectId: string;
|
|
7
|
+
projectSecret: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function loadConvexConfig(cwd?: string): ConvexConfig | null;
|
|
10
|
+
/**
|
|
11
|
+
* Query Convex for requirement coverage
|
|
12
|
+
*/
|
|
13
|
+
export declare function getRequirementCoverage(requirementKey: string, projectId: string, projectSecret: string, convexUrl: string): Promise<{
|
|
14
|
+
requirementKey: string;
|
|
15
|
+
lastTestedAt: number | null;
|
|
16
|
+
branch: string | null;
|
|
17
|
+
testFile: string | null;
|
|
18
|
+
testLine: number | null;
|
|
19
|
+
allBranches: Array<{
|
|
20
|
+
branch: string;
|
|
21
|
+
lastTestedAt: number;
|
|
22
|
+
testFile?: string;
|
|
23
|
+
testLine?: number;
|
|
24
|
+
}>;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Query Convex for project coverage summary
|
|
28
|
+
*/
|
|
29
|
+
export declare function getProjectCoverage(projectId: string, projectSecret: string, convexUrl: string, options?: {
|
|
30
|
+
branch?: string;
|
|
31
|
+
sinceTimestamp?: number;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
tested: Array<{
|
|
34
|
+
requirementKey: string;
|
|
35
|
+
lastTestedAt: number;
|
|
36
|
+
branch: string;
|
|
37
|
+
testFile?: string;
|
|
38
|
+
testLine?: number;
|
|
39
|
+
}>;
|
|
40
|
+
untested: string[];
|
|
41
|
+
}>;
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=convexClient.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Production Convex deployment URL
|
|
5
|
+
*/
|
|
6
|
+
const CONVEX_URL = 'https://data.dotrequirements.io';
|
|
7
|
+
export function loadConvexConfig(cwd = process.cwd()) {
|
|
8
|
+
let projectId;
|
|
9
|
+
let projectSecret;
|
|
10
|
+
// Try to load from .env files (.env.local takes precedence over .env)
|
|
11
|
+
const envFiles = ['.env.local', '.env'];
|
|
12
|
+
for (const envFile of envFiles) {
|
|
13
|
+
const envPath = join(cwd, envFile);
|
|
14
|
+
if (existsSync(envPath)) {
|
|
15
|
+
const envContent = readFileSync(envPath, 'utf-8');
|
|
16
|
+
const lines = envContent.split('\n');
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
if (trimmed.startsWith('DOTREQUIREMENTS_PROJECT_ID=')) {
|
|
20
|
+
projectId = trimmed.split('=')[1].trim().replace(/['"]/g, '');
|
|
21
|
+
}
|
|
22
|
+
if (trimmed.startsWith('DOTREQUIREMENTS_PROJECT_SECRET=')) {
|
|
23
|
+
projectSecret = trimmed.split('=')[1].trim().replace(/['"]/g, '');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// If we found values, stop looking (precedence: .env.local > .env)
|
|
27
|
+
if (projectId && projectSecret) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Environment variables take precedence
|
|
33
|
+
if (process.env.DOTREQUIREMENTS_PROJECT_ID) {
|
|
34
|
+
projectId = process.env.DOTREQUIREMENTS_PROJECT_ID;
|
|
35
|
+
}
|
|
36
|
+
if (process.env.DOTREQUIREMENTS_PROJECT_SECRET) {
|
|
37
|
+
projectSecret = process.env.DOTREQUIREMENTS_PROJECT_SECRET;
|
|
38
|
+
}
|
|
39
|
+
if (!projectId || !projectSecret) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return { convexUrl: CONVEX_URL, projectId, projectSecret };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Query Convex for requirement coverage
|
|
46
|
+
*/
|
|
47
|
+
export async function getRequirementCoverage(requirementKey, projectId, projectSecret, convexUrl) {
|
|
48
|
+
const response = await fetch(`${convexUrl}/api/query`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
path: 'testCoverage/queries:getRequirementCoverage',
|
|
53
|
+
args: {
|
|
54
|
+
projectAuth: {
|
|
55
|
+
projectSlug: projectId,
|
|
56
|
+
projectSecret,
|
|
57
|
+
},
|
|
58
|
+
target: {
|
|
59
|
+
type: 'project',
|
|
60
|
+
slug: projectId,
|
|
61
|
+
},
|
|
62
|
+
requirementKey,
|
|
63
|
+
},
|
|
64
|
+
format: 'json',
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(`Failed to query coverage: ${response.statusText}`);
|
|
69
|
+
}
|
|
70
|
+
return await response.json();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Query Convex for project coverage summary
|
|
74
|
+
*/
|
|
75
|
+
export async function getProjectCoverage(projectId, projectSecret, convexUrl, options) {
|
|
76
|
+
const response = await fetch(`${convexUrl}/api/query`, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
path: 'testCoverage/queries:getProjectCoverage',
|
|
81
|
+
args: {
|
|
82
|
+
projectAuth: {
|
|
83
|
+
projectSlug: projectId,
|
|
84
|
+
projectSecret,
|
|
85
|
+
},
|
|
86
|
+
target: {
|
|
87
|
+
type: 'project',
|
|
88
|
+
slug: projectId,
|
|
89
|
+
},
|
|
90
|
+
branch: options?.branch,
|
|
91
|
+
sinceTimestamp: options?.sinceTimestamp,
|
|
92
|
+
},
|
|
93
|
+
format: 'json',
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new Error(`Failed to query coverage: ${response.statusText}`);
|
|
98
|
+
}
|
|
99
|
+
return await response.json();
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=convexClient.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TestReference } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Find all test files that reference a specific requirement ID
|
|
4
|
+
*
|
|
5
|
+
* Uses execFile with array arguments to avoid command injection vulnerabilities.
|
|
6
|
+
*/
|
|
7
|
+
export declare function findTestsForRequirement(workspaceRoot: string, requirementId: string): Promise<TestReference[]>;
|
|
8
|
+
/**
|
|
9
|
+
* Find all requirement references in a specific file using AST parsing
|
|
10
|
+
*/
|
|
11
|
+
export declare function findRequirementsInFile(filePath: string): Promise<TestReference[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Find all requirement references in the entire workspace
|
|
14
|
+
*
|
|
15
|
+
* Uses execFile with array arguments to avoid command injection vulnerabilities.
|
|
16
|
+
*/
|
|
17
|
+
export declare function findAllTestReferences(workspaceRoot: string): Promise<TestReference[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Get all unique requirement IDs referenced in tests
|
|
20
|
+
* Normalizes child IDs (e.g., AUTH-VALID-LOGIN.0) to root IDs (e.g., AUTH-VALID-LOGIN)
|
|
21
|
+
* Uses AST parsing instead of grep for reliability across environments
|
|
22
|
+
*/
|
|
23
|
+
export declare function getReferencedRequirementIds(workspaceRoot: string): Promise<Set<string>>;
|
|
24
|
+
//# sourceMappingURL=grep.d.ts.map
|