@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 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,7 @@
1
+ interface FinalizeOptions {
2
+ push?: boolean;
3
+ quiet?: boolean;
4
+ }
5
+ export declare function finalizeCommand(options: FinalizeOptions): Promise<void>;
6
+ export {};
7
+ //# sourceMappingURL=finalize.d.ts.map
@@ -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, getContextFilePath, findDotrequirementsSection, appendOrUpdateSection, } from '../utils/context-file.js';
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 contextFilePath = await getContextFilePath(platform);
20
- if (!contextFilePath) {
21
- console.error(`\n❌ Could not determine context file path for ${platform}`);
19
+ const fileName = getContextFileName(platform);
20
+ if (!fileName) {
21
+ console.error(`\n❌ Unknown platform: ${platform}`);
22
22
  return;
23
23
  }
24
- const fileName = getContextFileName(platform);
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,6 @@
1
+ interface PrepareOptions {
2
+ quiet?: boolean;
3
+ }
4
+ export declare function prepareCommand(options: PrepareOptions): Promise<void>;
5
+ export {};
6
+ //# sourceMappingURL=prepare.d.ts.map
@@ -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,7 @@
1
+ interface ReportOptions {
2
+ format?: 'console' | 'json' | 'markdown';
3
+ requirement?: string;
4
+ }
5
+ export declare function reportCommand(options: ReportOptions): Promise<void>;
6
+ export {};
7
+ //# sourceMappingURL=report.d.ts.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
@@ -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.
@@ -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
- lookup.requirements[node.id] = {
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
- flatten(child);
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) {
@@ -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
- const totalRequirements = lookup ? Object.keys(lookup.requirements).length : 0;
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
- REQ-ID: Short description of expected behavior
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
- ## HOW-TO: How to use dotrequirements
21
+ ## HOWTO-1: How to use dotrequirements
22
22
 
23
23
  \`\`\`dotrequirements
24
- HOW-TO: How to use dotrequirements
25
- 0. → Requirements live here and are referenced by index, like requirement('HOW-TO.0')
26
- 1. Labels → Anything before the arrow is a label. You can reference a requirement by its index or its label: requirement('HOW-TO.1') == requirement('HOW-TO.labels')
27
- 2. Labels → Repeated labels can be referenced sequentially: requirement('HOW-TO.labels#2')
28
- 2.0. Nesting → You can keep nesting. Label references don't need to be nested: requirement('HOW-TO.nesting').
29
- 2.1. Several words → Reference multi-word labels using kebab-case. Label references are never case sensitive: requirement('HOW-TO.several-words')
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
- ## USER-AUTH: User authentication flow
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
- USER-AUTH: User authentication flow
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('USER-AUTH.0');
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('USER-AUTH.edge-case');
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
- ## HOW-TO: How to use dotrequirements
22
+ ## HOWTO-1: How to use dotrequirements
23
23
 
24
24
  \`\`\`dotrequirements
25
- HOW-TO: How to use dotrequirements
26
- 0. → Requirements live here and are referenced by index, like requirement('HOW-TO.0')
27
- 1. Labels → Anything before the arrow is a label. You can reference a requirement by its index or its label: requirement('HOW-TO.1') == requirement('HOW-TO.labels')
28
- 2. Labels → Repeated labels can be referenced sequentially: requirement('HOW-TO.labels#2')
29
- 2.0. Nesting → You can keep nesting. Label references don't need to be nested: requirement('HOW-TO.nesting').
30
- 2.1. Several words → Reference multi-word labels using kebab-case. Label references are never case sensitive: requirement('HOW-TO.several-words')
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
- ## USER-AUTH: User authentication flow
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
- USER-AUTH: User authentication flow
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('USER-AUTH.0');
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('USER-AUTH.edge-case');
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popoverai/dotrequirements",
3
- "version": "0.20.4",
3
+ "version": "0.21.1",
4
4
  "description": "Requirements tracking CLI, test harness, and MCP server",
5
5
  "type": "module",
6
6
  "bin": {