@popoverai/dotrequirements 0.21.0 → 0.22.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 +8 -0
- package/dist/cli.js +1 -0
- package/dist/commands/finalize.d.ts +1 -0
- package/dist/commands/finalize.js +1 -0
- package/dist/commands/mcp-setup.js +10 -5
- package/dist/harness/cache.d.ts +22 -17
- package/dist/harness/cache.js +47 -10
- package/dist/harness/finalize.d.ts +5 -0
- package/dist/harness/finalize.js +64 -18
- 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/README.md
CHANGED
|
@@ -276,6 +276,14 @@ await finalize({ showTestedList: true }); // Include tested requirements
|
|
|
276
276
|
await finalize({ showSummary: false, showUntestedList: false }); // Quiet mode
|
|
277
277
|
```
|
|
278
278
|
|
|
279
|
+
**Attribution:** tag cloud coverage rows with the name of the framework that produced them:
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
await finalize({ context: 'Vitest' });
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The Coverage tab uses this label to show which tool produced each result. Any string works — useful when you have multiple test suites (`"Vitest — unit"`, `"Vitest — integration"`).
|
|
286
|
+
|
|
279
287
|
By default, only untested requirements are shown. See the [test harness docs](https://docs.dotrequirements.io/tools/test-harness) for all options.
|
|
280
288
|
|
|
281
289
|
### Setup with Jest
|
package/dist/cli.js
CHANGED
|
@@ -90,6 +90,7 @@ program
|
|
|
90
90
|
.description('Aggregate test tracking data and generate coverage report')
|
|
91
91
|
.option('--push', 'Push coverage to DotRequirements Cloud')
|
|
92
92
|
.option('-q, --quiet', 'Output only coverage percentage (for scripting)')
|
|
93
|
+
.option('--context <label>', 'Attribution label identifying the reporter (e.g. "Vitest", "pytest")')
|
|
93
94
|
.action(wrapCommand(finalizeCommand));
|
|
94
95
|
program
|
|
95
96
|
.command('report')
|
|
@@ -31,6 +31,7 @@ export async function finalizeCommand(options) {
|
|
|
31
31
|
showTestedList: true,
|
|
32
32
|
showUntestedList: true,
|
|
33
33
|
showCloudStatus: !options.quiet,
|
|
34
|
+
context: options.context,
|
|
34
35
|
});
|
|
35
36
|
if (options.quiet) {
|
|
36
37
|
// In quiet mode, just output the coverage percent for scripting
|
|
@@ -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;
|
package/dist/harness/cache.d.ts
CHANGED
|
@@ -39,17 +39,22 @@ export interface LookupCache {
|
|
|
39
39
|
/**
|
|
40
40
|
* Structure of the coverage.json cache (for cloud deduplication)
|
|
41
41
|
*/
|
|
42
|
+
/** Attribution tuple used for debounce keying (COVERAGE-DEBOUNCE-2). */
|
|
43
|
+
export interface CoverageTuple {
|
|
44
|
+
requirementKey: string;
|
|
45
|
+
context?: string;
|
|
46
|
+
branch?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface CoverageCacheEntry {
|
|
49
|
+
testRunId: string;
|
|
50
|
+
timestamp: number;
|
|
51
|
+
tuples: CoverageTuple[];
|
|
52
|
+
/** Legacy field retained for backward-compat reads of pre-attribution caches. */
|
|
53
|
+
requirementKeys?: string[];
|
|
54
|
+
}
|
|
42
55
|
export interface CoverageCache {
|
|
43
|
-
current:
|
|
44
|
-
|
|
45
|
-
timestamp: number;
|
|
46
|
-
requirementKeys: string[];
|
|
47
|
-
} | null;
|
|
48
|
-
previous: {
|
|
49
|
-
testRunId: string;
|
|
50
|
-
timestamp: number;
|
|
51
|
-
requirementKeys: string[];
|
|
52
|
-
} | null;
|
|
56
|
+
current: CoverageCacheEntry | null;
|
|
57
|
+
previous: CoverageCacheEntry | null;
|
|
53
58
|
}
|
|
54
59
|
/**
|
|
55
60
|
* Find the nearest .requirements directory by walking up from startDir
|
|
@@ -123,16 +128,16 @@ export declare function readTrackingEntries(requirementsDir: string): TrackingEn
|
|
|
123
128
|
* Delete the tracking file after finalize
|
|
124
129
|
*/
|
|
125
130
|
export declare function deleteTrackingFile(requirementsDir: string): void;
|
|
126
|
-
/**
|
|
127
|
-
* Read the coverage cache for cloud deduplication
|
|
128
|
-
*/
|
|
129
131
|
export declare function readCoverageCache(requirementsDir: string): CoverageCache;
|
|
130
132
|
/**
|
|
131
|
-
* Update the coverage cache after reporting
|
|
133
|
+
* Update the coverage cache after reporting. Accepts either attribution tuples
|
|
134
|
+
* (new) or bare requirement keys (legacy; converted to tuples with no context/branch).
|
|
132
135
|
*/
|
|
133
|
-
export declare function updateCoverageCache(requirementsDir: string, testRunId: string,
|
|
136
|
+
export declare function updateCoverageCache(requirementsDir: string, testRunId: string, tuples: CoverageTuple[] | string[]): void;
|
|
137
|
+
export declare const COVERAGE_DEBOUNCE_WINDOW_MS: number;
|
|
134
138
|
/**
|
|
135
|
-
* Check if
|
|
139
|
+
* Check if an attribution tuple needs reporting based on the coverage cache.
|
|
140
|
+
* Returns true if no matching tuple was reported within the debounce window.
|
|
136
141
|
*/
|
|
137
|
-
export declare function needsReporting(
|
|
142
|
+
export declare function needsReporting(tupleOrKey: CoverageTuple | string, cache: CoverageCache): boolean;
|
|
138
143
|
//# sourceMappingURL=cache.d.ts.map
|
package/dist/harness/cache.js
CHANGED
|
@@ -288,6 +288,30 @@ export function deleteTrackingFile(requirementsDir) {
|
|
|
288
288
|
/**
|
|
289
289
|
* Read the coverage cache for cloud deduplication
|
|
290
290
|
*/
|
|
291
|
+
/**
|
|
292
|
+
* Normalize a cache entry read from disk. Migrates legacy `requirementKeys`
|
|
293
|
+
* arrays into the tuple shape (context/branch undefined) so downstream code
|
|
294
|
+
* can treat both shapes uniformly.
|
|
295
|
+
*/
|
|
296
|
+
function normalizeEntry(raw) {
|
|
297
|
+
if (!raw || typeof raw !== 'object')
|
|
298
|
+
return null;
|
|
299
|
+
if (Array.isArray(raw.tuples)) {
|
|
300
|
+
return {
|
|
301
|
+
testRunId: raw.testRunId,
|
|
302
|
+
timestamp: raw.timestamp,
|
|
303
|
+
tuples: raw.tuples,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (Array.isArray(raw.requirementKeys)) {
|
|
307
|
+
return {
|
|
308
|
+
testRunId: raw.testRunId,
|
|
309
|
+
timestamp: raw.timestamp,
|
|
310
|
+
tuples: raw.requirementKeys.map((requirementKey) => ({ requirementKey })),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
291
315
|
export function readCoverageCache(requirementsDir) {
|
|
292
316
|
const cacheDir = getCacheDir(requirementsDir);
|
|
293
317
|
const coveragePath = path.join(cacheDir, COVERAGE_FILE);
|
|
@@ -296,45 +320,58 @@ export function readCoverageCache(requirementsDir) {
|
|
|
296
320
|
}
|
|
297
321
|
try {
|
|
298
322
|
const content = fs.readFileSync(coveragePath, 'utf-8');
|
|
299
|
-
|
|
323
|
+
const raw = JSON.parse(content);
|
|
324
|
+
return {
|
|
325
|
+
current: normalizeEntry(raw?.current),
|
|
326
|
+
previous: normalizeEntry(raw?.previous),
|
|
327
|
+
};
|
|
300
328
|
}
|
|
301
329
|
catch {
|
|
302
330
|
return { current: null, previous: null };
|
|
303
331
|
}
|
|
304
332
|
}
|
|
305
333
|
/**
|
|
306
|
-
* Update the coverage cache after reporting
|
|
334
|
+
* Update the coverage cache after reporting. Accepts either attribution tuples
|
|
335
|
+
* (new) or bare requirement keys (legacy; converted to tuples with no context/branch).
|
|
307
336
|
*/
|
|
308
|
-
export function updateCoverageCache(requirementsDir, testRunId,
|
|
337
|
+
export function updateCoverageCache(requirementsDir, testRunId, tuples) {
|
|
309
338
|
const cacheDir = getCacheDir(requirementsDir, true);
|
|
310
339
|
const coveragePath = path.join(cacheDir, COVERAGE_FILE);
|
|
340
|
+
const normalized = tuples.map((t) => typeof t === 'string' ? { requirementKey: t } : t);
|
|
311
341
|
const existing = readCoverageCache(requirementsDir);
|
|
312
342
|
const newCache = {
|
|
313
343
|
current: {
|
|
314
344
|
testRunId,
|
|
315
345
|
timestamp: Date.now(),
|
|
316
|
-
|
|
346
|
+
tuples: normalized,
|
|
317
347
|
},
|
|
318
348
|
previous: existing.current,
|
|
319
349
|
};
|
|
320
350
|
fs.writeFileSync(coveragePath, JSON.stringify(newCache, null, 2));
|
|
321
351
|
}
|
|
352
|
+
export const COVERAGE_DEBOUNCE_WINDOW_MS = 4 * 60 * 60 * 1000; // 4 hours — must match server-side in testCoverage/mutations.ts
|
|
353
|
+
function tuplesMatch(a, b) {
|
|
354
|
+
return (a.requirementKey === b.requirementKey &&
|
|
355
|
+
a.context === b.context &&
|
|
356
|
+
a.branch === b.branch);
|
|
357
|
+
}
|
|
322
358
|
/**
|
|
323
|
-
* Check if
|
|
359
|
+
* Check if an attribution tuple needs reporting based on the coverage cache.
|
|
360
|
+
* Returns true if no matching tuple was reported within the debounce window.
|
|
324
361
|
*/
|
|
325
|
-
export function needsReporting(
|
|
326
|
-
const
|
|
362
|
+
export function needsReporting(tupleOrKey, cache) {
|
|
363
|
+
const tuple = typeof tupleOrKey === 'string' ? { requirementKey: tupleOrKey } : tupleOrKey;
|
|
327
364
|
// If no current run, everything needs reporting
|
|
328
365
|
if (!cache.current) {
|
|
329
366
|
return true;
|
|
330
367
|
}
|
|
331
|
-
// If
|
|
332
|
-
if (!cache.current.
|
|
368
|
+
// If the tuple wasn't in the current cached run, it needs reporting
|
|
369
|
+
if (!cache.current.tuples.some((t) => tuplesMatch(t, tuple))) {
|
|
333
370
|
return true;
|
|
334
371
|
}
|
|
335
372
|
// If current run is stale (>4 hours old), needs reporting
|
|
336
373
|
const age = Date.now() - cache.current.timestamp;
|
|
337
|
-
if (age >
|
|
374
|
+
if (age > COVERAGE_DEBOUNCE_WINDOW_MS) {
|
|
338
375
|
return true;
|
|
339
376
|
}
|
|
340
377
|
// Otherwise, skip reporting (already reported recently)
|
|
@@ -24,6 +24,11 @@ export interface FinalizeOptions {
|
|
|
24
24
|
showUntestedList?: boolean;
|
|
25
25
|
/** Whether to show cloud reporting status messages (defaults to true) */
|
|
26
26
|
showCloudStatus?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Attribution label identifying the reporter (e.g. "Vitest", "Jest", "pytest").
|
|
29
|
+
* When omitted, rows are recorded with context absent per COVERAGE-CONTEXT-1.2.
|
|
30
|
+
*/
|
|
31
|
+
context?: string;
|
|
27
32
|
}
|
|
28
33
|
export interface FinalizeResult {
|
|
29
34
|
/** Number of unique requirements exercised */
|
package/dist/harness/finalize.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Implements: HARNESS-FINALIZE-1, HARNESS-FINALIZE-2, HARNESS-FINALIZE-3, HARNESS-FINALIZE-4
|
|
11
11
|
*/
|
|
12
12
|
import { execSync } from 'child_process';
|
|
13
|
+
import { randomUUID } from 'crypto';
|
|
13
14
|
import { findRequirementsDir, findProjectRoot, getTestRunId, cleanupTestRunId, readTrackingEntries, deleteTrackingFile, readLookupCache, readCoverageCache, updateCoverageCache, needsReporting, } from './cache.js';
|
|
14
15
|
import { getProjectInfo } from '../utils/project-settings.js';
|
|
15
16
|
/**
|
|
@@ -40,6 +41,26 @@ function getCurrentBranch(cwd) {
|
|
|
40
41
|
return 'unknown';
|
|
41
42
|
}
|
|
42
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Return the git-blame author email for a specific file:line, or undefined
|
|
46
|
+
* if unavailable (not a git repo, file untracked, git unavailable, etc.).
|
|
47
|
+
*
|
|
48
|
+
* COVERAGE-CONTEXT-2: "last reported by" attribution.
|
|
49
|
+
*/
|
|
50
|
+
function getLineAuthor(cwd, file, line) {
|
|
51
|
+
try {
|
|
52
|
+
const output = execSync(`git blame -L ${line},${line} --porcelain -- "${file}"`, {
|
|
53
|
+
cwd,
|
|
54
|
+
encoding: 'utf-8',
|
|
55
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
56
|
+
});
|
|
57
|
+
const match = output.match(/^author-mail\s+<([^>]+)>/m);
|
|
58
|
+
return match?.[1];
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
43
64
|
/**
|
|
44
65
|
* Print local coverage report to console
|
|
45
66
|
*
|
|
@@ -112,7 +133,7 @@ const CONVEX_URL = 'https://data.dotrequirements.io';
|
|
|
112
133
|
* HARNESS-FINALIZE-2: Cloud reporting with error tolerance
|
|
113
134
|
* HARNESS-FINALIZE-3: Coverage records include requirement, file, line, branch
|
|
114
135
|
*/
|
|
115
|
-
async function reportToConvex(projectRoot, testRunId, aggregated, showCloudStatus) {
|
|
136
|
+
async function reportToConvex(projectRoot, testRunId, aggregated, showCloudStatus, context) {
|
|
116
137
|
try {
|
|
117
138
|
// Get project info from .requirements/project-settings.json
|
|
118
139
|
const projectInfo = getProjectInfo(projectRoot);
|
|
@@ -125,24 +146,34 @@ async function reportToConvex(projectRoot, testRunId, aggregated, showCloudStatu
|
|
|
125
146
|
}
|
|
126
147
|
const { projectId, projectSecret } = projectInfo.credentials;
|
|
127
148
|
const requirementsDir = findRequirementsDir(projectRoot);
|
|
128
|
-
|
|
149
|
+
const branch = getCurrentBranch(projectRoot);
|
|
150
|
+
// Build the full set of tuples for this run, one per tracked requirement
|
|
129
151
|
const requirementKeys = Array.from(aggregated.keys());
|
|
130
|
-
|
|
152
|
+
const allTuples = requirementKeys.map((key) => ({
|
|
153
|
+
requirementKey: key,
|
|
154
|
+
context,
|
|
155
|
+
branch,
|
|
156
|
+
}));
|
|
157
|
+
// Local debounce (COVERAGE-DEBOUNCE-1): only report tuples not recently cached.
|
|
158
|
+
// Server-side debounce is a backstop (COVERAGE-DEBOUNCE-4) — this layer just
|
|
159
|
+
// prevents unnecessary HTTP calls.
|
|
131
160
|
const coverageCache = readCoverageCache(requirementsDir);
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
161
|
+
const tuplesToReport = allTuples.filter((tuple) => needsReporting(tuple, coverageCache));
|
|
162
|
+
if (tuplesToReport.length === 0) {
|
|
134
163
|
if (showCloudStatus) {
|
|
135
164
|
console.log('\n✓ Coverage unchanged since last run (skipping cloud report)');
|
|
136
165
|
}
|
|
137
|
-
// Still update the cache
|
|
138
|
-
updateCoverageCache(requirementsDir, testRunId,
|
|
166
|
+
// Still update the cache so the window continues tracking
|
|
167
|
+
updateCoverageCache(requirementsDir, testRunId, allTuples);
|
|
139
168
|
return { sent: false, count: 0 };
|
|
140
169
|
}
|
|
141
|
-
|
|
170
|
+
// Deterministic runId for this finalize invocation (COVERAGE-CONTEXT-4)
|
|
171
|
+
const runId = randomUUID();
|
|
172
|
+
// Cache git-blame lookups per file:line to avoid redundant subprocess spawns
|
|
173
|
+
const userCache = new Map();
|
|
142
174
|
// Build coverage payload
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const entries = aggregated.get(key);
|
|
175
|
+
const coverage = tuplesToReport.map((tuple) => {
|
|
176
|
+
const entries = aggregated.get(tuple.requirementKey);
|
|
146
177
|
// Use the first access location
|
|
147
178
|
const firstEntry = entries[0];
|
|
148
179
|
const location = firstEntry.callerLocation;
|
|
@@ -159,10 +190,23 @@ async function reportToConvex(projectRoot, testRunId, aggregated, showCloudStatu
|
|
|
159
190
|
testFile = location;
|
|
160
191
|
}
|
|
161
192
|
}
|
|
193
|
+
// COVERAGE-CONTEXT-2: git blame on the requirement() call line
|
|
194
|
+
let user;
|
|
195
|
+
if (testFile && testLine !== undefined) {
|
|
196
|
+
const cacheKey = `${testFile}:${testLine}`;
|
|
197
|
+
if (userCache.has(cacheKey)) {
|
|
198
|
+
user = userCache.get(cacheKey);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
user = getLineAuthor(projectRoot, testFile, testLine);
|
|
202
|
+
userCache.set(cacheKey, user);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
162
205
|
return {
|
|
163
|
-
requirementKey:
|
|
206
|
+
requirementKey: tuple.requirementKey,
|
|
164
207
|
testFile,
|
|
165
208
|
testLine,
|
|
209
|
+
user,
|
|
166
210
|
};
|
|
167
211
|
});
|
|
168
212
|
// Send to Convex
|
|
@@ -185,6 +229,8 @@ async function reportToConvex(projectRoot, testRunId, aggregated, showCloudStatu
|
|
|
185
229
|
slug: projectId,
|
|
186
230
|
},
|
|
187
231
|
branch,
|
|
232
|
+
context,
|
|
233
|
+
runId,
|
|
188
234
|
coverage,
|
|
189
235
|
},
|
|
190
236
|
format: 'json',
|
|
@@ -218,11 +264,11 @@ async function reportToConvex(projectRoot, testRunId, aggregated, showCloudStatu
|
|
|
218
264
|
}
|
|
219
265
|
}
|
|
220
266
|
if (showCloudStatus) {
|
|
221
|
-
console.log(`\n✓ Reported ${
|
|
267
|
+
console.log(`\n✓ Reported ${tuplesToReport.length} requirement(s) to cloud (branch: ${branch})`);
|
|
222
268
|
}
|
|
223
|
-
// Update coverage cache
|
|
224
|
-
updateCoverageCache(requirementsDir, testRunId,
|
|
225
|
-
return { sent: true, count:
|
|
269
|
+
// Update coverage cache with the full tuple set (includes those not reported this run)
|
|
270
|
+
updateCoverageCache(requirementsDir, testRunId, allTuples);
|
|
271
|
+
return { sent: true, count: tuplesToReport.length };
|
|
226
272
|
}
|
|
227
273
|
catch (error) {
|
|
228
274
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -244,7 +290,7 @@ async function reportToConvex(projectRoot, testRunId, aggregated, showCloudStatu
|
|
|
244
290
|
* HARNESS-FINALIZE-4: Finalize cleans up after itself
|
|
245
291
|
*/
|
|
246
292
|
export async function finalize(options = {}) {
|
|
247
|
-
const { cwd = process.cwd(), reportToCloud: shouldReportToCloud = true, cleanup: shouldCleanup = true, showSummary = true, showTestedList = false, showUntestedList = true, showCloudStatus = true, } = options;
|
|
293
|
+
const { cwd = process.cwd(), reportToCloud: shouldReportToCloud = true, cleanup: shouldCleanup = true, showSummary = true, showTestedList = false, showUntestedList = true, showCloudStatus = true, context, } = options;
|
|
248
294
|
// Priority 1: Use environment variable (cross-process persistence from globalSetup)
|
|
249
295
|
let projectRoot = process.env.DOTREQUIREMENTS_PROJECT_ROOT || null;
|
|
250
296
|
// Priority 2: Find from cwd
|
|
@@ -309,7 +355,7 @@ export async function finalize(options = {}) {
|
|
|
309
355
|
// HARNESS-FINALIZE-2, HARNESS-FINALIZE-3: Report to cloud
|
|
310
356
|
let cloudResult = { sent: false, count: 0 };
|
|
311
357
|
if (shouldReportToCloud) {
|
|
312
|
-
cloudResult = await reportToConvex(projectRoot, testRunId, aggregated, showCloudStatus);
|
|
358
|
+
cloudResult = await reportToConvex(projectRoot, testRunId, aggregated, showCloudStatus, context);
|
|
313
359
|
}
|
|
314
360
|
// HARNESS-FINALIZE-4: Clean up tracking data
|
|
315
361
|
if (shouldCleanup && !cloudResult.error) {
|
|
@@ -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
|