@popoverai/dotrequirements 0.20.4 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +20 -0
- package/dist/commands/finalize.d.ts +7 -0
- package/dist/commands/finalize.js +40 -0
- package/dist/commands/mcp-setup.js +10 -5
- package/dist/commands/prepare.d.ts +6 -0
- package/dist/commands/prepare.js +20 -0
- package/dist/commands/report.d.ts +7 -0
- package/dist/commands/report.js +152 -0
- package/dist/harness/cache.d.ts +7 -0
- package/dist/harness/cache.js +24 -6
- package/dist/harness/finalize.js +19 -3
- package/dist/templates/context-file-section.md +2 -1
- package/dist/templates/example-requirements.js +11 -11
- package/dist/templates/example-requirements.ts +11 -11
- package/dist/utils/context-file.d.ts +8 -0
- package/dist/utils/context-file.js +19 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,9 @@ import { pullCommand } from './commands/pull.js';
|
|
|
6
6
|
import { pushCommand } from './commands/push.js';
|
|
7
7
|
import { testCommand } from './commands/test.js';
|
|
8
8
|
import { browserTestCommand } from './commands/browsertest.js';
|
|
9
|
+
import { prepareCommand } from './commands/prepare.js';
|
|
10
|
+
import { finalizeCommand } from './commands/finalize.js';
|
|
11
|
+
import { reportCommand } from './commands/report.js';
|
|
9
12
|
import { mcpCommand } from './commands/mcp.js';
|
|
10
13
|
import { mcpSetupCommand } from './commands/mcp-setup.js';
|
|
11
14
|
import { loadEnvFile } from './utils/env.js';
|
|
@@ -77,6 +80,23 @@ program
|
|
|
77
80
|
.option('--json', 'Output results as JSON')
|
|
78
81
|
.option('--useAgent', 'Encourage Claude to use the Agent tool for multi-step tasks')
|
|
79
82
|
.action(wrapCommand(browserTestCommand));
|
|
83
|
+
program
|
|
84
|
+
.command('prepare')
|
|
85
|
+
.description('Parse requirements and build lookup cache for multi-language test tracking')
|
|
86
|
+
.option('-q, --quiet', 'Suppress output (for scripting)')
|
|
87
|
+
.action(wrapCommand(prepareCommand));
|
|
88
|
+
program
|
|
89
|
+
.command('finalize')
|
|
90
|
+
.description('Aggregate test tracking data and generate coverage report')
|
|
91
|
+
.option('--push', 'Push coverage to DotRequirements Cloud')
|
|
92
|
+
.option('-q, --quiet', 'Output only coverage percentage (for scripting)')
|
|
93
|
+
.action(wrapCommand(finalizeCommand));
|
|
94
|
+
program
|
|
95
|
+
.command('report')
|
|
96
|
+
.description('Display coverage report from most recent test run')
|
|
97
|
+
.option('-f, --format <format>', 'Output format: console, json, markdown', 'console')
|
|
98
|
+
.option('-r, --requirement <id>', 'Filter to specific requirement and its children')
|
|
99
|
+
.action(wrapCommand(reportCommand));
|
|
80
100
|
program
|
|
81
101
|
.command('mcp')
|
|
82
102
|
.description('Start the MCP (Model Context Protocol) server for AI assistant integration')
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { finalize } from '../harness/finalize.js';
|
|
4
|
+
import { findRequirementsDir, isCacheStale } from '../harness/cache.js';
|
|
5
|
+
import { findProjectRoot } from '../utils/project-settings.js';
|
|
6
|
+
export async function finalizeCommand(options) {
|
|
7
|
+
// Warn if cache is stale before finalizing
|
|
8
|
+
if (!options.quiet) {
|
|
9
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
10
|
+
if (projectRoot) {
|
|
11
|
+
const requirementsDir = findRequirementsDir(projectRoot);
|
|
12
|
+
if (requirementsDir) {
|
|
13
|
+
const lookupPath = path.join(requirementsDir, '.cache', 'lookup.json');
|
|
14
|
+
try {
|
|
15
|
+
const cacheStat = fs.statSync(lookupPath);
|
|
16
|
+
if (isCacheStale(requirementsDir, cacheStat.mtimeMs)) {
|
|
17
|
+
console.error('Warning: Cache may be stale — requirements have changed since last prepare. Run "dotrequirements prepare" to rebuild.\n');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Ignore — finalize will handle missing cache
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const result = await finalize({
|
|
27
|
+
cwd: process.cwd(),
|
|
28
|
+
reportToCloud: options.push ?? false,
|
|
29
|
+
cleanup: true,
|
|
30
|
+
showSummary: !options.quiet,
|
|
31
|
+
showTestedList: true,
|
|
32
|
+
showUntestedList: true,
|
|
33
|
+
showCloudStatus: !options.quiet,
|
|
34
|
+
});
|
|
35
|
+
if (options.quiet) {
|
|
36
|
+
// In quiet mode, just output the coverage percent for scripting
|
|
37
|
+
console.log(result.coveragePercent.toFixed(1));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=finalize.js.map
|
|
@@ -5,7 +5,7 @@ import { join } from 'path';
|
|
|
5
5
|
import prompts from 'prompts';
|
|
6
6
|
import { loadTemplate } from '../utils/templates.js';
|
|
7
7
|
import { brand } from '../utils/brand.js';
|
|
8
|
-
import { getContextFileName,
|
|
8
|
+
import { getContextFileName, findGitRoot, findDotrequirementsSection, appendOrUpdateSection, buildNoGitRepoMessage, } from '../utils/context-file.js';
|
|
9
9
|
/**
|
|
10
10
|
* Load the context file section template
|
|
11
11
|
*/
|
|
@@ -16,12 +16,17 @@ function loadContextFileSection() {
|
|
|
16
16
|
* Install workflow guidance to the appropriate context file for a platform
|
|
17
17
|
*/
|
|
18
18
|
async function installContextFileSection(platform) {
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
21
|
-
console.error(`\n❌
|
|
19
|
+
const fileName = getContextFileName(platform);
|
|
20
|
+
if (!fileName) {
|
|
21
|
+
console.error(`\n❌ Unknown platform: ${platform}`);
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
|
-
const
|
|
24
|
+
const gitRoot = await findGitRoot();
|
|
25
|
+
if (!gitRoot) {
|
|
26
|
+
console.log('\n' + buildNoGitRepoMessage(fileName) + '\n');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const contextFilePath = join(gitRoot, fileName);
|
|
25
30
|
const sectionContent = loadContextFileSection();
|
|
26
31
|
// Check if file exists and has existing section
|
|
27
32
|
let existingSection = null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { prepare } from '../harness/prepare.js';
|
|
2
|
+
export async function prepareCommand(options) {
|
|
3
|
+
const result = prepare({
|
|
4
|
+
cwd: process.cwd(),
|
|
5
|
+
logWarnings: !options.quiet,
|
|
6
|
+
});
|
|
7
|
+
if (!options.quiet) {
|
|
8
|
+
console.log(`Parsed ${result.filesProcessed} file(s), loaded ${result.requirementsLoaded} requirement(s) into cache.`);
|
|
9
|
+
if (result.parseErrors.length > 0) {
|
|
10
|
+
console.log(`\n⚠️ ${result.parseErrors.length} file(s) had parse errors (skipped):`);
|
|
11
|
+
for (const err of result.parseErrors) {
|
|
12
|
+
console.log(` - ${err.file}: ${err.error}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
console.log('\nCache written to .requirements/.cache/lookup.json');
|
|
16
|
+
console.log('Tracking initialized at .requirements/.cache/tracking.jsonl');
|
|
17
|
+
console.log('\nRun your tests, then run "dotrequirements finalize" to generate coverage report.');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=prepare.js.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { findProjectRoot } from '../utils/project-settings.js';
|
|
4
|
+
import { findRequirementsDir, readTrackingEntries, isCacheStale, } from '../harness/cache.js';
|
|
5
|
+
function buildCoverageEntries(lookup, entries, filterKey) {
|
|
6
|
+
// Aggregate tracking by key
|
|
7
|
+
const locationsByKey = new Map();
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
if (!locationsByKey.has(entry.requirementKey)) {
|
|
10
|
+
locationsByKey.set(entry.requirementKey, new Set());
|
|
11
|
+
}
|
|
12
|
+
locationsByKey.get(entry.requirementKey).add(entry.callerLocation);
|
|
13
|
+
}
|
|
14
|
+
const results = [];
|
|
15
|
+
for (const [key, req] of Object.entries(lookup.requirements)) {
|
|
16
|
+
// Skip alias entries to avoid double-counting
|
|
17
|
+
if (req.isAlias)
|
|
18
|
+
continue;
|
|
19
|
+
// Filter if requested
|
|
20
|
+
if (filterKey && key !== filterKey && !key.startsWith(filterKey + '.')) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
// Collect locations from both the numeric key and any alias keys that map to the same id
|
|
24
|
+
const locations = new Set();
|
|
25
|
+
const keyLocations = locationsByKey.get(key);
|
|
26
|
+
if (keyLocations)
|
|
27
|
+
keyLocations.forEach(l => locations.add(l));
|
|
28
|
+
// Also check if tracking data was written using a label path
|
|
29
|
+
for (const [aliasKey, aliasReq] of Object.entries(lookup.requirements)) {
|
|
30
|
+
if (aliasReq.isAlias && aliasReq.id === req.id) {
|
|
31
|
+
const aliasLocations = locationsByKey.get(aliasKey);
|
|
32
|
+
if (aliasLocations)
|
|
33
|
+
aliasLocations.forEach(l => locations.add(l));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
results.push({
|
|
37
|
+
key,
|
|
38
|
+
label: req.label || null,
|
|
39
|
+
content: req.content,
|
|
40
|
+
tested: locations.size > 0,
|
|
41
|
+
locations: Array.from(locations),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return results;
|
|
45
|
+
}
|
|
46
|
+
function formatConsole(coverage) {
|
|
47
|
+
const tested = coverage.filter(c => c.tested).length;
|
|
48
|
+
const total = coverage.length;
|
|
49
|
+
const pct = total > 0 ? ((tested / total) * 100).toFixed(1) : '0.0';
|
|
50
|
+
let out = `\n=== Requirements Coverage Report ===\n`;
|
|
51
|
+
out += `\nTotal: ${total} | Tested: ${tested} | Untested: ${total - tested} | Coverage: ${pct}%\n`;
|
|
52
|
+
const testedEntries = coverage.filter(c => c.tested);
|
|
53
|
+
if (testedEntries.length > 0) {
|
|
54
|
+
out += '\n✓ Tested:\n';
|
|
55
|
+
for (const c of testedEntries) {
|
|
56
|
+
const preview = c.content.length > 60 ? c.content.substring(0, 60) + '...' : c.content;
|
|
57
|
+
out += ` ${c.key}: ${preview}\n`;
|
|
58
|
+
for (const loc of c.locations) {
|
|
59
|
+
out += ` ← ${loc}\n`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const untestedEntries = coverage.filter(c => !c.tested);
|
|
64
|
+
if (untestedEntries.length > 0) {
|
|
65
|
+
out += '\n✗ Untested:\n';
|
|
66
|
+
for (const c of untestedEntries) {
|
|
67
|
+
const preview = c.content.length > 60 ? c.content.substring(0, 60) + '...' : c.content;
|
|
68
|
+
out += ` ${c.key}: ${preview}\n`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
out += '\n====================================\n';
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function formatJson(coverage) {
|
|
75
|
+
const tested = coverage.filter(c => c.tested).length;
|
|
76
|
+
const total = coverage.length;
|
|
77
|
+
return JSON.stringify({
|
|
78
|
+
summary: {
|
|
79
|
+
total,
|
|
80
|
+
tested,
|
|
81
|
+
untested: total - tested,
|
|
82
|
+
coveragePercent: total > 0 ? parseFloat(((tested / total) * 100).toFixed(1)) : 0,
|
|
83
|
+
},
|
|
84
|
+
requirements: coverage,
|
|
85
|
+
}, null, 2);
|
|
86
|
+
}
|
|
87
|
+
function formatMarkdown(coverage) {
|
|
88
|
+
const tested = coverage.filter(c => c.tested).length;
|
|
89
|
+
const total = coverage.length;
|
|
90
|
+
const pct = total > 0 ? ((tested / total) * 100).toFixed(1) : '0.0';
|
|
91
|
+
let out = `# Requirements Coverage Report\n\n`;
|
|
92
|
+
out += `**Coverage:** ${tested}/${total} (${pct}%)\n\n`;
|
|
93
|
+
out += `| Requirement | Status | Locations |\n`;
|
|
94
|
+
out += `|---|---|---|\n`;
|
|
95
|
+
for (const c of coverage) {
|
|
96
|
+
const status = c.tested ? '✓' : '✗';
|
|
97
|
+
const locs = c.locations.join(', ') || '-';
|
|
98
|
+
const preview = c.content.length > 40 ? c.content.substring(0, 40) + '...' : c.content;
|
|
99
|
+
out += `| ${c.key}: ${preview} | ${status} | ${locs} |\n`;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
export async function reportCommand(options) {
|
|
104
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
105
|
+
if (!projectRoot) {
|
|
106
|
+
throw new Error('Could not find .requirements directory. Run "dotrequirements init" to initialize your project.');
|
|
107
|
+
}
|
|
108
|
+
const requirementsDir = findRequirementsDir(projectRoot);
|
|
109
|
+
const cacheDir = path.join(requirementsDir, '.cache');
|
|
110
|
+
const lookupPath = path.join(cacheDir, 'lookup.json');
|
|
111
|
+
if (!fs.existsSync(lookupPath)) {
|
|
112
|
+
throw new Error('No lookup cache found. Run "dotrequirements prepare" first to build the cache.');
|
|
113
|
+
}
|
|
114
|
+
// Read lookup cache
|
|
115
|
+
const lookupContent = fs.readFileSync(lookupPath, 'utf-8');
|
|
116
|
+
const lookup = JSON.parse(lookupContent);
|
|
117
|
+
// Warn if cache is stale (requirements changed since last prepare)
|
|
118
|
+
try {
|
|
119
|
+
const cacheStat = fs.statSync(lookupPath);
|
|
120
|
+
if (isCacheStale(requirementsDir, cacheStat.mtimeMs)) {
|
|
121
|
+
console.error('Warning: Cache may be stale — requirements have changed since last prepare. Run "dotrequirements prepare" to rebuild.\n');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Ignore stat errors
|
|
126
|
+
}
|
|
127
|
+
// Read tracking entries
|
|
128
|
+
const trackingPath = path.join(cacheDir, 'tracking.jsonl');
|
|
129
|
+
const trackingExists = fs.existsSync(trackingPath);
|
|
130
|
+
const entries = readTrackingEntries(requirementsDir);
|
|
131
|
+
if (!trackingExists) {
|
|
132
|
+
console.error('Warning: No tracking data found. Run your tests first, or run "dotrequirements prepare" to start a new tracking session.\n');
|
|
133
|
+
}
|
|
134
|
+
else if (entries.length === 0) {
|
|
135
|
+
console.error('Warning: Tracking file is empty — no requirements were tracked during tests.\n');
|
|
136
|
+
}
|
|
137
|
+
const coverage = buildCoverageEntries(lookup, entries, options.requirement);
|
|
138
|
+
const format = options.format || 'console';
|
|
139
|
+
switch (format) {
|
|
140
|
+
case 'json':
|
|
141
|
+
console.log(formatJson(coverage));
|
|
142
|
+
break;
|
|
143
|
+
case 'markdown':
|
|
144
|
+
console.log(formatMarkdown(coverage));
|
|
145
|
+
break;
|
|
146
|
+
case 'console':
|
|
147
|
+
default:
|
|
148
|
+
console.log(formatConsole(coverage));
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=report.js.map
|
package/dist/harness/cache.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface LookupCache {
|
|
|
32
32
|
id: string;
|
|
33
33
|
label: string;
|
|
34
34
|
content: string;
|
|
35
|
+
/** True if this key is a label-path alias for another entry (e.g. "REQ.given" → "REQ.0") */
|
|
36
|
+
isAlias?: boolean;
|
|
35
37
|
}>;
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
@@ -69,6 +71,11 @@ export declare function findRequirementsFiles(projectRoot: string): string[];
|
|
|
69
71
|
* Write the lookup cache with all pre-parsed requirements
|
|
70
72
|
*/
|
|
71
73
|
export declare function writeLookupCache(requirementsDir: string, requirements: RequirementNode[]): void;
|
|
74
|
+
/**
|
|
75
|
+
* Check if any source file is newer than the cache file.
|
|
76
|
+
* Returns true if cache is stale and should be invalidated.
|
|
77
|
+
*/
|
|
78
|
+
export declare function isCacheStale(requirementsDir: string, cacheMtime: number): boolean;
|
|
72
79
|
/**
|
|
73
80
|
* Read the lookup cache, returning null if not found, invalid, or stale.
|
|
74
81
|
* Cache is considered stale if any .requirements.md file is newer than the cache.
|
package/dist/harness/cache.js
CHANGED
|
@@ -84,19 +84,37 @@ export function writeLookupCache(requirementsDir, requirements) {
|
|
|
84
84
|
generatedAt: new Date().toISOString(),
|
|
85
85
|
requirements: {},
|
|
86
86
|
};
|
|
87
|
-
// Recursively flatten the requirement trees
|
|
88
|
-
function flatten(node) {
|
|
89
|
-
|
|
87
|
+
// Recursively flatten the requirement trees, adding both numeric and label paths
|
|
88
|
+
function flatten(node, labelPath) {
|
|
89
|
+
const entry = {
|
|
90
90
|
id: node.id,
|
|
91
91
|
label: node.label,
|
|
92
92
|
content: node.content,
|
|
93
93
|
};
|
|
94
|
+
// Always add the numeric path (e.g. "AUTH-LOGIN.0")
|
|
95
|
+
lookup.requirements[node.id] = entry;
|
|
96
|
+
// Add label path as an alias if it differs from the numeric path
|
|
97
|
+
if (labelPath && labelPath !== node.id) {
|
|
98
|
+
lookup.requirements[labelPath] = { ...entry, isAlias: true };
|
|
99
|
+
}
|
|
100
|
+
// Track label occurrences for disambiguation (e.g. given#0, given#1)
|
|
101
|
+
const labelCounts = new Map();
|
|
94
102
|
for (const child of node.children) {
|
|
95
|
-
|
|
103
|
+
let childLabelPath = null;
|
|
104
|
+
if (child.label && child.label !== 'requirementHeader') {
|
|
105
|
+
const normalizedLabel = child.label.toLowerCase().replace(/\s+/g, '-');
|
|
106
|
+
const count = labelCounts.get(normalizedLabel) || 0;
|
|
107
|
+
labelCounts.set(normalizedLabel, count + 1);
|
|
108
|
+
const parentPath = labelPath || node.id;
|
|
109
|
+
childLabelPath = count === 0
|
|
110
|
+
? `${parentPath}.${normalizedLabel}`
|
|
111
|
+
: `${parentPath}.${normalizedLabel}#${count}`;
|
|
112
|
+
}
|
|
113
|
+
flatten(child, childLabelPath);
|
|
96
114
|
}
|
|
97
115
|
}
|
|
98
116
|
for (const req of requirements) {
|
|
99
|
-
flatten(req);
|
|
117
|
+
flatten(req, null);
|
|
100
118
|
}
|
|
101
119
|
fs.writeFileSync(lookupPath, JSON.stringify(lookup, null, 2));
|
|
102
120
|
}
|
|
@@ -104,7 +122,7 @@ export function writeLookupCache(requirementsDir, requirements) {
|
|
|
104
122
|
* Check if any source file is newer than the cache file.
|
|
105
123
|
* Returns true if cache is stale and should be invalidated.
|
|
106
124
|
*/
|
|
107
|
-
function isCacheStale(requirementsDir, cacheMtime) {
|
|
125
|
+
export function isCacheStale(requirementsDir, cacheMtime) {
|
|
108
126
|
const projectRoot = path.dirname(requirementsDir);
|
|
109
127
|
const sourceFiles = findRequirementsFiles(projectRoot);
|
|
110
128
|
for (const file of sourceFiles) {
|
package/dist/harness/finalize.js
CHANGED
|
@@ -47,7 +47,7 @@ function getCurrentBranch(cwd) {
|
|
|
47
47
|
*/
|
|
48
48
|
function printLocalReport(testedKeys, lookup, options) {
|
|
49
49
|
const { showSummary, showTestedList, showUntestedList } = options;
|
|
50
|
-
const allKeys = lookup ? Object.keys(lookup.requirements) : [];
|
|
50
|
+
const allKeys = lookup ? Object.keys(lookup.requirements).filter(k => !lookup.requirements[k].isAlias) : [];
|
|
51
51
|
const untestedKeys = allKeys.filter(key => !testedKeys.includes(key));
|
|
52
52
|
const total = allKeys.length;
|
|
53
53
|
const tested = testedKeys.length;
|
|
@@ -278,10 +278,26 @@ export async function finalize(options = {}) {
|
|
|
278
278
|
const entries = readTrackingEntries(requirementsDir);
|
|
279
279
|
// Aggregate by requirement key
|
|
280
280
|
const aggregated = aggregateEntries(entries);
|
|
281
|
-
const testedKeys = Array.from(aggregated.keys());
|
|
282
281
|
// Read lookup cache for report
|
|
283
282
|
const lookup = readLookupCache(requirementsDir);
|
|
284
|
-
|
|
283
|
+
// Normalize alias keys to canonical numeric keys.
|
|
284
|
+
// Non-JS consumers may write label paths (e.g. "AUTH-LOGIN.given") to tracking.jsonl.
|
|
285
|
+
// We resolve those to their canonical numeric key (e.g. "AUTH-LOGIN.0") so that
|
|
286
|
+
// coverage counting, display, and cloud reporting all use consistent keys.
|
|
287
|
+
if (lookup) {
|
|
288
|
+
for (const [key, trackingEntries] of Array.from(aggregated.entries())) {
|
|
289
|
+
const entry = lookup.requirements[key];
|
|
290
|
+
if (entry?.isAlias) {
|
|
291
|
+
const canonicalKey = entry.id;
|
|
292
|
+
// Merge into canonical key's entries
|
|
293
|
+
const existing = aggregated.get(canonicalKey) || [];
|
|
294
|
+
aggregated.set(canonicalKey, [...existing, ...trackingEntries]);
|
|
295
|
+
aggregated.delete(key);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const testedKeys = Array.from(aggregated.keys());
|
|
300
|
+
const totalRequirements = lookup ? Object.keys(lookup.requirements).filter(k => !lookup.requirements[k].isAlias).length : 0;
|
|
285
301
|
const coveragePercent = totalRequirements > 0
|
|
286
302
|
? (testedKeys.length / totalRequirements) * 100
|
|
287
303
|
: 0;
|
|
@@ -21,7 +21,7 @@ requirements, so your plan should too.
|
|
|
21
21
|
### Requirements Syntax
|
|
22
22
|
|
|
23
23
|
```dotrequirements
|
|
24
|
-
|
|
24
|
+
DOMAIN-1: Short description of expected behavior
|
|
25
25
|
0. -> First criterion or condition
|
|
26
26
|
1. -> Second criterion
|
|
27
27
|
1.0. -> Nested detail under second criterion
|
|
@@ -31,6 +31,7 @@ REQ-ID: Short description of expected behavior
|
|
|
31
31
|
- Criteria: `position. -> content` (optional label before the arrow)
|
|
32
32
|
- Nesting: Indent with 2 spaces, use `x.y` position paths
|
|
33
33
|
- Delimiter: `->` or `→`
|
|
34
|
+
- **Key style**: Use sequential keys with a short domain prefix (`ORCHESTRATOR-1`, `ORCHESTRATOR-2`, ...) rather than semantic keys (`AUTONOMOUS-ADVANCE`). Sequential keys stay stable when a requirement gets reworded, so test references don't break. Call `create_requirement_document` for full key-naming guidance.
|
|
34
35
|
|
|
35
36
|
### Test Usage
|
|
36
37
|
|
|
@@ -18,15 +18,15 @@ document:
|
|
|
18
18
|
|
|
19
19
|
This file demonstrates the dotrequirements format. Feel free to edit or delete it.
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## HOWTO-1: How to use dotrequirements
|
|
22
22
|
|
|
23
23
|
\`\`\`dotrequirements
|
|
24
|
-
|
|
25
|
-
0. → Requirements live here and are referenced by index, like requirement('
|
|
26
|
-
1. Labels → Anything before the arrow is a label. You can reference a requirement by its index or its label: requirement('
|
|
27
|
-
2. Labels → Repeated labels can be referenced sequentially: requirement('
|
|
28
|
-
2.0. Nesting → You can keep nesting. Label references don't need to be nested: requirement('
|
|
29
|
-
2.1. Several words → Reference multi-word labels using kebab-case. Label references are never case sensitive: requirement('
|
|
24
|
+
HOWTO-1: How to use dotrequirements
|
|
25
|
+
0. → Requirements live here and are referenced by index, like requirement('HOWTO-1.0')
|
|
26
|
+
1. Labels → Anything before the arrow is a label. You can reference a requirement by its index or its label: requirement('HOWTO-1.1') == requirement('HOWTO-1.labels')
|
|
27
|
+
2. Labels → Repeated labels can be referenced sequentially: requirement('HOWTO-1.labels#2')
|
|
28
|
+
2.0. Nesting → You can keep nesting. Label references don't need to be nested: requirement('HOWTO-1.nesting').
|
|
29
|
+
2.1. Several words → Reference multi-word labels using kebab-case. Label references are never case sensitive: requirement('HOWTO-1.several-words')
|
|
30
30
|
3. → While you _can_ write your requirements longhand in this format, there are a number of better ways.
|
|
31
31
|
3.0. MCP → The dotrequirements MCP equips an AI assistant to develop requirements with you
|
|
32
32
|
3.1. Your current tools → Dotrequirements has a composer for Jira, Confluence, and Notion
|
|
@@ -36,12 +36,12 @@ HOW-TO: How to use dotrequirements
|
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## AUTH-1: User authentication flow
|
|
40
40
|
|
|
41
41
|
**Implementation notes:** Use bcrypt for password hashing with work factor >= 12.
|
|
42
42
|
|
|
43
43
|
\`\`\`dotrequirements
|
|
44
|
-
|
|
44
|
+
AUTH-1: User authentication flow
|
|
45
45
|
0. AC → Login form accepts email and password
|
|
46
46
|
1. AC → Invalid credentials show error message
|
|
47
47
|
2. Edge-case → Rate limiting after 5 failed attempts
|
|
@@ -71,11 +71,11 @@ import { requirement } from '@popoverai/dotrequirements/test';
|
|
|
71
71
|
|
|
72
72
|
test('login with valid credentials', () => {
|
|
73
73
|
// Reference by numeric path
|
|
74
|
-
const ac = requirement('
|
|
74
|
+
const ac = requirement('AUTH-1.0');
|
|
75
75
|
// Returns: "AC: Login form accepts email and password"
|
|
76
76
|
|
|
77
77
|
// Reference by label
|
|
78
|
-
const edgeCase = requirement('
|
|
78
|
+
const edgeCase = requirement('AUTH-1.edge-case');
|
|
79
79
|
// Returns: "Edge-case: Rate limiting after 5 failed attempts"
|
|
80
80
|
|
|
81
81
|
// Your test implementation...
|
|
@@ -19,15 +19,15 @@ document:
|
|
|
19
19
|
|
|
20
20
|
This file demonstrates the dotrequirements format. Feel free to edit or delete it.
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## HOWTO-1: How to use dotrequirements
|
|
23
23
|
|
|
24
24
|
\`\`\`dotrequirements
|
|
25
|
-
|
|
26
|
-
0. → Requirements live here and are referenced by index, like requirement('
|
|
27
|
-
1. Labels → Anything before the arrow is a label. You can reference a requirement by its index or its label: requirement('
|
|
28
|
-
2. Labels → Repeated labels can be referenced sequentially: requirement('
|
|
29
|
-
2.0. Nesting → You can keep nesting. Label references don't need to be nested: requirement('
|
|
30
|
-
2.1. Several words → Reference multi-word labels using kebab-case. Label references are never case sensitive: requirement('
|
|
25
|
+
HOWTO-1: How to use dotrequirements
|
|
26
|
+
0. → Requirements live here and are referenced by index, like requirement('HOWTO-1.0')
|
|
27
|
+
1. Labels → Anything before the arrow is a label. You can reference a requirement by its index or its label: requirement('HOWTO-1.1') == requirement('HOWTO-1.labels')
|
|
28
|
+
2. Labels → Repeated labels can be referenced sequentially: requirement('HOWTO-1.labels#2')
|
|
29
|
+
2.0. Nesting → You can keep nesting. Label references don't need to be nested: requirement('HOWTO-1.nesting').
|
|
30
|
+
2.1. Several words → Reference multi-word labels using kebab-case. Label references are never case sensitive: requirement('HOWTO-1.several-words')
|
|
31
31
|
3. → While you _can_ write your requirements longhand in this format, there are a number of better ways.
|
|
32
32
|
3.0. MCP → The dotrequirements MCP equips an AI assistant to develop requirements with you
|
|
33
33
|
3.1. Your current tools → Dotrequirements has a composer for Jira, Confluence, and Notion
|
|
@@ -37,12 +37,12 @@ HOW-TO: How to use dotrequirements
|
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## AUTH-1: User authentication flow
|
|
41
41
|
|
|
42
42
|
**Implementation notes:** Use bcrypt for password hashing with work factor >= 12.
|
|
43
43
|
|
|
44
44
|
\`\`\`dotrequirements
|
|
45
|
-
|
|
45
|
+
AUTH-1: User authentication flow
|
|
46
46
|
0. AC → Login form accepts email and password
|
|
47
47
|
1. AC → Invalid credentials show error message
|
|
48
48
|
2. Edge-case → Rate limiting after 5 failed attempts
|
|
@@ -72,11 +72,11 @@ import { requirement } from '@popoverai/dotrequirements/test';
|
|
|
72
72
|
|
|
73
73
|
test('login with valid credentials', () => {
|
|
74
74
|
// Reference by numeric path
|
|
75
|
-
const ac = requirement('
|
|
75
|
+
const ac = requirement('AUTH-1.0');
|
|
76
76
|
// Returns: "AC: Login form accepts email and password"
|
|
77
77
|
|
|
78
78
|
// Reference by label
|
|
79
|
-
const edgeCase = requirement('
|
|
79
|
+
const edgeCase = requirement('AUTH-1.edge-case');
|
|
80
80
|
// Returns: "Edge-case: Rate limiting after 5 failed attempts"
|
|
81
81
|
|
|
82
82
|
// Your test implementation...
|
|
@@ -35,4 +35,12 @@ export declare function appendOrUpdateSection(filePath: string, sectionContent:
|
|
|
35
35
|
* Get the full path to the context file for a platform
|
|
36
36
|
*/
|
|
37
37
|
export declare function getContextFilePath(platform: string): Promise<string | null>;
|
|
38
|
+
/**
|
|
39
|
+
* Build a user-facing message explaining that context file installation
|
|
40
|
+
* was skipped because the current directory is not inside a git repository.
|
|
41
|
+
*
|
|
42
|
+
* The MCP server itself is configured separately, so this message is only
|
|
43
|
+
* about the second step (writing the platform's context file).
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildNoGitRepoMessage(fileName: string): string;
|
|
38
46
|
//# sourceMappingURL=context-file.d.ts.map
|
|
@@ -91,4 +91,23 @@ export async function getContextFilePath(platform) {
|
|
|
91
91
|
return null;
|
|
92
92
|
return join(gitRoot, fileName);
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Build a user-facing message explaining that context file installation
|
|
96
|
+
* was skipped because the current directory is not inside a git repository.
|
|
97
|
+
*
|
|
98
|
+
* The MCP server itself is configured separately, so this message is only
|
|
99
|
+
* about the second step (writing the platform's context file).
|
|
100
|
+
*/
|
|
101
|
+
export function buildNoGitRepoMessage(fileName) {
|
|
102
|
+
return [
|
|
103
|
+
`⚠️ Skipped writing ${fileName}: not inside a git repository.`,
|
|
104
|
+
` ${fileName} would have been created at the git repo root, but no .git directory was found.`,
|
|
105
|
+
'',
|
|
106
|
+
' To finish setup, either:',
|
|
107
|
+
' • run `git init` here, then re-run `dotreq mcp-setup`, or',
|
|
108
|
+
' • `cd` into an existing project directory and run `dotreq mcp-setup` there.',
|
|
109
|
+
'',
|
|
110
|
+
' Note: the MCP server itself was configured successfully — only the context file step was skipped.',
|
|
111
|
+
].join('\n');
|
|
112
|
+
}
|
|
94
113
|
//# sourceMappingURL=context-file.js.map
|