@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,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse Markdown requirements files into typed structures.
|
|
3
|
+
*/
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import { ValidationError, validateMetadata, } from './schemas.js';
|
|
7
|
+
/**
|
|
8
|
+
* Default delimiter for requirements.
|
|
9
|
+
* Can be overridden for organization-specific preferences.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_DELIMITER = '→';
|
|
12
|
+
const DELIMITER_PATTERN = '(?:→|->)'; // Non-capturing group for both Unicode and ASCII
|
|
13
|
+
/**
|
|
14
|
+
* Parse a criterion line in "position. Label → content" format.
|
|
15
|
+
* Example: "0. Given → user has valid credentials"
|
|
16
|
+
* Also supports optional label: "0. → user has valid credentials"
|
|
17
|
+
*/
|
|
18
|
+
export function parseCriterionLine(line, delimiter = DELIMITER_PATTERN) {
|
|
19
|
+
// Match: position + period + space + optional(label + space) + delimiter + space + content
|
|
20
|
+
// Position can be: 0, 1.0, 2.3.1, etc.
|
|
21
|
+
// Label can contain any characters except the delimiter
|
|
22
|
+
const pattern = new RegExp(`^\\s*(\\d+(?:\\.\\d+)*)\\.\\s*(?:(.+?)\\s+)?${delimiter}\\s*(.+)$`);
|
|
23
|
+
const match = line.match(pattern);
|
|
24
|
+
if (!match) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
position: match[1],
|
|
29
|
+
label: match[2] ? match[2].trim() : '', // Preserve original case, empty if no label
|
|
30
|
+
content: match[3].trim(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse the first line of a dotrequirements block (the root requirement).
|
|
35
|
+
* Format: "KEY: content" or "Label → content" or "→ content"
|
|
36
|
+
*/
|
|
37
|
+
function parseRootLine(line, delimiter = DELIMITER_PATTERN) {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
// Try to match: KEY + colon + content (explicit key format)
|
|
40
|
+
// KEY must be followed by colon (not arrow) to distinguish from "Label → content"
|
|
41
|
+
const keyMatch = trimmed.match(/^([\w-]+):\s*(.+)$/);
|
|
42
|
+
if (keyMatch) {
|
|
43
|
+
return {
|
|
44
|
+
label: '', // Keys use empty string for unlabeled requirements
|
|
45
|
+
content: keyMatch[2].trim(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// Match: optional(label + space) + delimiter + space + content
|
|
49
|
+
const pattern = new RegExp(`^(?:(.+?)\\s+)?${delimiter}\\s*(.+)$`);
|
|
50
|
+
const match = trimmed.match(pattern);
|
|
51
|
+
if (!match) {
|
|
52
|
+
// If no delimiter found, treat entire line as content with empty label
|
|
53
|
+
return {
|
|
54
|
+
label: '',
|
|
55
|
+
content: trimmed,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
label: match[1] ? match[1].trim() : '',
|
|
60
|
+
content: match[2].trim(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse a dotrequirements fenced block into a RequirementNode tree.
|
|
65
|
+
* First line is the requirement content (with optional arrow format).
|
|
66
|
+
* Subsequent lines are criteria with position paths.
|
|
67
|
+
*/
|
|
68
|
+
export function parseRequirementBlock(key, blockContent) {
|
|
69
|
+
const lines = blockContent.trim().split('\n');
|
|
70
|
+
if (lines.length === 0) {
|
|
71
|
+
throw new ValidationError('Empty requirement block', key);
|
|
72
|
+
}
|
|
73
|
+
// First line is the requirement content
|
|
74
|
+
const rootParsed = parseRootLine(lines[0]);
|
|
75
|
+
if (!rootParsed.content) {
|
|
76
|
+
throw new ValidationError('Requirement content (first line) cannot be empty', key);
|
|
77
|
+
}
|
|
78
|
+
const root = {
|
|
79
|
+
id: key,
|
|
80
|
+
// Root requirements always use "requirementHeader" label for indexing/querying
|
|
81
|
+
// The markdown format doesn't preserve root labels, but we standardize on this value
|
|
82
|
+
label: 'requirementHeader',
|
|
83
|
+
content: rootParsed.content,
|
|
84
|
+
children: [],
|
|
85
|
+
};
|
|
86
|
+
// Parse criteria lines
|
|
87
|
+
const criteriaByPosition = new Map();
|
|
88
|
+
for (let i = 1; i < lines.length; i++) {
|
|
89
|
+
const line = lines[i];
|
|
90
|
+
// Skip empty lines and comments
|
|
91
|
+
if (!line.trim() || line.trim().startsWith('#')) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const criterion = parseCriterionLine(line);
|
|
95
|
+
if (!criterion) {
|
|
96
|
+
throw new ValidationError(`Invalid criterion format at line ${i + 1}: "${line.trim()}"`, key);
|
|
97
|
+
}
|
|
98
|
+
criteriaByPosition.set(criterion.position, criterion);
|
|
99
|
+
}
|
|
100
|
+
// Build tree from flat position paths
|
|
101
|
+
buildTreeFromPositions(root, key, criteriaByPosition);
|
|
102
|
+
return root;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build a tree structure from flat position paths.
|
|
106
|
+
* Positions like "0", "1", "1.0", "1.1", "1.1.0" define the hierarchy.
|
|
107
|
+
*/
|
|
108
|
+
function buildTreeFromPositions(root, rootKey, criteriaByPosition) {
|
|
109
|
+
// Get all top-level children (single digit positions: "0", "1", "2")
|
|
110
|
+
const topLevelPositions = Array.from(criteriaByPosition.keys())
|
|
111
|
+
.filter(pos => !pos.includes('.'))
|
|
112
|
+
.sort((a, b) => parseInt(a) - parseInt(b));
|
|
113
|
+
for (const position of topLevelPositions) {
|
|
114
|
+
const criterion = criteriaByPosition.get(position);
|
|
115
|
+
const childNode = {
|
|
116
|
+
id: `${rootKey}.${position}`,
|
|
117
|
+
label: criterion.label,
|
|
118
|
+
content: criterion.content,
|
|
119
|
+
children: [],
|
|
120
|
+
};
|
|
121
|
+
// Recursively build children
|
|
122
|
+
buildChildrenFromPositions(childNode, position, rootKey, criteriaByPosition);
|
|
123
|
+
root.children.push(childNode);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Recursively build children for a given position.
|
|
128
|
+
*/
|
|
129
|
+
function buildChildrenFromPositions(parent, parentPosition, rootKey, criteriaByPosition) {
|
|
130
|
+
// Find direct children (e.g., if parent is "1", find "1.0", "1.1", etc.)
|
|
131
|
+
const childPositions = Array.from(criteriaByPosition.keys())
|
|
132
|
+
.filter(pos => {
|
|
133
|
+
const parts = pos.split('.');
|
|
134
|
+
const parentParts = parentPosition.split('.');
|
|
135
|
+
// Must be exactly one level deeper
|
|
136
|
+
if (parts.length !== parentParts.length + 1) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
// All parent parts must match
|
|
140
|
+
for (let i = 0; i < parentParts.length; i++) {
|
|
141
|
+
if (parts[i] !== parentParts[i]) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
})
|
|
147
|
+
.sort((a, b) => {
|
|
148
|
+
const aLast = parseInt(a.split('.').pop());
|
|
149
|
+
const bLast = parseInt(b.split('.').pop());
|
|
150
|
+
return aLast - bLast;
|
|
151
|
+
});
|
|
152
|
+
for (const position of childPositions) {
|
|
153
|
+
const criterion = criteriaByPosition.get(position);
|
|
154
|
+
const childNode = {
|
|
155
|
+
id: `${rootKey}.${position}`,
|
|
156
|
+
label: criterion.label,
|
|
157
|
+
content: criterion.content,
|
|
158
|
+
children: [],
|
|
159
|
+
};
|
|
160
|
+
// Recursively build grandchildren
|
|
161
|
+
buildChildrenFromPositions(childNode, position, rootKey, criteriaByPosition);
|
|
162
|
+
parent.children.push(childNode);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Extract YAML frontmatter from Markdown content.
|
|
167
|
+
* Returns { frontmatter, body } where frontmatter is the parsed YAML object.
|
|
168
|
+
*/
|
|
169
|
+
function extractFrontmatter(content) {
|
|
170
|
+
// Match YAML frontmatter: ---\n...\n---
|
|
171
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
172
|
+
const match = content.match(frontmatterRegex);
|
|
173
|
+
if (!match) {
|
|
174
|
+
throw new ValidationError('Missing YAML frontmatter (should start with ---)');
|
|
175
|
+
}
|
|
176
|
+
const [, frontmatterYaml, body] = match;
|
|
177
|
+
let frontmatter;
|
|
178
|
+
try {
|
|
179
|
+
frontmatter = YAML.parse(frontmatterYaml);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
throw new ValidationError('Failed to parse YAML frontmatter', undefined, error);
|
|
183
|
+
}
|
|
184
|
+
return { frontmatter, body: body.trim() };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Extract requirement blocks from Markdown body.
|
|
188
|
+
* Returns a map of requirement key -> { title, blockContent }
|
|
189
|
+
*
|
|
190
|
+
* The KEY is extracted from the first line inside the code block.
|
|
191
|
+
* Headings are optional and only used for human readability.
|
|
192
|
+
*/
|
|
193
|
+
function extractRequirementBlocks(body) {
|
|
194
|
+
const blocks = new Map();
|
|
195
|
+
// Find all ```dotrequirements code blocks
|
|
196
|
+
const blockRegex = /```dotrequirements\n([\s\S]*?)```/gm;
|
|
197
|
+
let match;
|
|
198
|
+
while ((match = blockRegex.exec(body)) !== null) {
|
|
199
|
+
const blockContent = match[1];
|
|
200
|
+
// Extract key from first line of block (format: "KEY: content")
|
|
201
|
+
const firstLineMatch = blockContent.match(/^([\w-]+):\s*(.+)/);
|
|
202
|
+
if (!firstLineMatch) {
|
|
203
|
+
throw new ValidationError(`Invalid requirement block format - first line must be "KEY: content"`, blockContent.substring(0, 50));
|
|
204
|
+
}
|
|
205
|
+
const key = firstLineMatch[1];
|
|
206
|
+
// Try to find a heading immediately before this block for the title
|
|
207
|
+
// Look backwards from the match position to find the nearest heading
|
|
208
|
+
const textBeforeBlock = body.substring(0, match.index);
|
|
209
|
+
const headingMatch = textBeforeBlock.match(/^#{2,6}\s+([^\n]+)\s*$/m);
|
|
210
|
+
const title = headingMatch ? headingMatch[1].trim() : key;
|
|
211
|
+
blocks.set(key, { title, blockContent: blockContent.trim() });
|
|
212
|
+
}
|
|
213
|
+
return blocks;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Parse a complete requirements Markdown file.
|
|
217
|
+
*/
|
|
218
|
+
export function parseRequirementsFile(markdownContent) {
|
|
219
|
+
// Extract frontmatter and body
|
|
220
|
+
const { frontmatter, body } = extractFrontmatter(markdownContent);
|
|
221
|
+
// Validate metadata from frontmatter
|
|
222
|
+
const metadata = validateMetadata(frontmatter);
|
|
223
|
+
// Extract requirement blocks
|
|
224
|
+
const blocks = extractRequirementBlocks(body);
|
|
225
|
+
if (blocks.size === 0) {
|
|
226
|
+
throw new ValidationError('No requirement blocks found in document');
|
|
227
|
+
}
|
|
228
|
+
// Parse each requirement block
|
|
229
|
+
const requirements = [];
|
|
230
|
+
for (const [key, { blockContent }] of blocks) {
|
|
231
|
+
try {
|
|
232
|
+
const reqNode = parseRequirementBlock(key, blockContent);
|
|
233
|
+
requirements.push(reqNode);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
throw new ValidationError(`Failed to parse requirement ${key}`, key, error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
metadata,
|
|
241
|
+
requirements,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Parse requirements file from disk.
|
|
246
|
+
*/
|
|
247
|
+
export function parseRequirementsFromFile(filePath) {
|
|
248
|
+
try {
|
|
249
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
250
|
+
return parseRequirementsFile(content);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (error instanceof ValidationError) {
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
throw new ValidationError(`Failed to read file: ${filePath}`, undefined, error);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Flatten a requirement tree into a list of all nodes.
|
|
261
|
+
* Useful for searching or displaying all requirements.
|
|
262
|
+
*/
|
|
263
|
+
export function flattenRequirementTree(node) {
|
|
264
|
+
const result = [node];
|
|
265
|
+
for (const child of node.children) {
|
|
266
|
+
result.push(...flattenRequirementTree(child));
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Find a requirement by its ID in a tree.
|
|
272
|
+
*/
|
|
273
|
+
export function findRequirementById(nodes, id) {
|
|
274
|
+
for (const node of nodes) {
|
|
275
|
+
if (node.id === id) {
|
|
276
|
+
return node;
|
|
277
|
+
}
|
|
278
|
+
const found = findRequirementById(node.children, id);
|
|
279
|
+
if (found) {
|
|
280
|
+
return found;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Get all requirements from multiple files as a flat list.
|
|
287
|
+
*/
|
|
288
|
+
export function getAllRequirements(requirements) {
|
|
289
|
+
const all = [];
|
|
290
|
+
for (const req of requirements) {
|
|
291
|
+
all.push(...flattenRequirementTree(req));
|
|
292
|
+
}
|
|
293
|
+
return all;
|
|
294
|
+
}
|
|
295
|
+
//# sourceMappingURL=parser.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve label-based requirement paths to numeric paths.
|
|
3
|
+
* Supports: REQ123.given, REQ123.when.then, REQ123.given#1
|
|
4
|
+
*/
|
|
5
|
+
import { RequirementNode } from './schemas.js';
|
|
6
|
+
/**
|
|
7
|
+
* Parse a requirement path into segments.
|
|
8
|
+
* E.g., "REQ123.given.then" → ["REQ123", "given", "then"]
|
|
9
|
+
* E.g., "REQ123.given#1" → ["REQ123", "given#1"]
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseRequirementPath(path: string): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Parse a path segment that may include disambiguation.
|
|
14
|
+
* E.g., "given" → { label: "given", index: 0 }
|
|
15
|
+
* E.g., "given#2" → { label: "given", index: 2 }
|
|
16
|
+
*/
|
|
17
|
+
export declare function parsePathSegment(segment: string): {
|
|
18
|
+
label: string;
|
|
19
|
+
index: number;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Find children matching a label.
|
|
23
|
+
* Returns all children with the given label.
|
|
24
|
+
* Label matching is case-insensitive and kebab-case is normalized to spaces.
|
|
25
|
+
*/
|
|
26
|
+
export declare function findChildrenByLabel(node: RequirementNode, label: string): RequirementNode[];
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a label-based path segment to a numeric index.
|
|
29
|
+
* Returns the child node and its numeric index.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolvePathSegment(node: RequirementNode, segment: string): {
|
|
32
|
+
child: RequirementNode;
|
|
33
|
+
index: number;
|
|
34
|
+
} | null;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a complete requirement path to a node.
|
|
37
|
+
*
|
|
38
|
+
* Examples:
|
|
39
|
+
* - "REQ123" → Root requirement
|
|
40
|
+
* - "REQ123.0" → First child (numeric)
|
|
41
|
+
* - "REQ123.given" → First child with label "given"
|
|
42
|
+
* - "REQ123.given#1" → Second child with label "given"
|
|
43
|
+
* - "REQ123.when.then" → First "then" under first "when"
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveRequirementPath(requirements: RequirementNode[], path: string): RequirementNode | null;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve a path and return the full numeric path.
|
|
48
|
+
*
|
|
49
|
+
* Examples:
|
|
50
|
+
* - "REQ123.given" → "REQ123.0" (if "given" is first child)
|
|
51
|
+
* - "REQ123.when.then" → "REQ123.2.1" (numeric path)
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveToNumericPath(requirements: RequirementNode[], path: string): string | null;
|
|
54
|
+
/**
|
|
55
|
+
* Get all possible label-based paths for a requirement.
|
|
56
|
+
* Useful for display/debugging.
|
|
57
|
+
*
|
|
58
|
+
* Returns paths like: ["REQ123", "REQ123.given", "REQ123.when", ...]
|
|
59
|
+
*/
|
|
60
|
+
export declare function getAllLabelPaths(node: RequirementNode, prefix?: string): string[];
|
|
61
|
+
/**
|
|
62
|
+
* Check if a path is ambiguous (multiple matches for a label).
|
|
63
|
+
* Returns the number of matches, or 0 if unambiguous.
|
|
64
|
+
*/
|
|
65
|
+
export declare function checkPathAmbiguity(requirements: RequirementNode[], path: string): number;
|
|
66
|
+
//# sourceMappingURL=resolver.d.ts.map
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve label-based requirement paths to numeric paths.
|
|
3
|
+
* Supports: REQ123.given, REQ123.when.then, REQ123.given#1
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Parse a requirement path into segments.
|
|
7
|
+
* E.g., "REQ123.given.then" → ["REQ123", "given", "then"]
|
|
8
|
+
* E.g., "REQ123.given#1" → ["REQ123", "given#1"]
|
|
9
|
+
*/
|
|
10
|
+
export function parseRequirementPath(path) {
|
|
11
|
+
return path.split('.');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parse a path segment that may include disambiguation.
|
|
15
|
+
* E.g., "given" → { label: "given", index: 0 }
|
|
16
|
+
* E.g., "given#2" → { label: "given", index: 2 }
|
|
17
|
+
*/
|
|
18
|
+
export function parsePathSegment(segment) {
|
|
19
|
+
const match = segment.match(/^(\w+)(?:#(\d+))?$/);
|
|
20
|
+
if (!match) {
|
|
21
|
+
throw new Error(`Invalid path segment: ${segment}`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
label: match[1],
|
|
25
|
+
index: match[2] ? parseInt(match[2]) : 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Normalize a label for comparison.
|
|
30
|
+
* Converts kebab-case to spaces and lowercases.
|
|
31
|
+
* Examples: "Several-words" -> "several words", "AC" -> "ac"
|
|
32
|
+
*/
|
|
33
|
+
function normalizeLabel(label) {
|
|
34
|
+
return label.replace(/-/g, ' ').toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Find children matching a label.
|
|
38
|
+
* Returns all children with the given label.
|
|
39
|
+
* Label matching is case-insensitive and kebab-case is normalized to spaces.
|
|
40
|
+
*/
|
|
41
|
+
export function findChildrenByLabel(node, label) {
|
|
42
|
+
const normalizedLabel = normalizeLabel(label);
|
|
43
|
+
return node.children.filter(child => normalizeLabel(child.label) === normalizedLabel);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a label-based path segment to a numeric index.
|
|
47
|
+
* Returns the child node and its numeric index.
|
|
48
|
+
*/
|
|
49
|
+
export function resolvePathSegment(node, segment) {
|
|
50
|
+
// Check if segment is numeric (direct index)
|
|
51
|
+
if (/^\d+$/.test(segment)) {
|
|
52
|
+
const index = parseInt(segment);
|
|
53
|
+
if (index < node.children.length) {
|
|
54
|
+
return {
|
|
55
|
+
child: node.children[index],
|
|
56
|
+
index,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
// Parse label with optional disambiguation
|
|
62
|
+
const { label, index: disambiguationIndex } = parsePathSegment(segment);
|
|
63
|
+
// Find all children with this label
|
|
64
|
+
const matches = findChildrenByLabel(node, label);
|
|
65
|
+
if (matches.length === 0) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (disambiguationIndex >= matches.length) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
// Get the Nth match
|
|
72
|
+
const targetChild = matches[disambiguationIndex];
|
|
73
|
+
// Find the actual numeric index of this child in the parent
|
|
74
|
+
const numericIndex = node.children.indexOf(targetChild);
|
|
75
|
+
return {
|
|
76
|
+
child: targetChild,
|
|
77
|
+
index: numericIndex,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a complete requirement path to a node.
|
|
82
|
+
*
|
|
83
|
+
* Examples:
|
|
84
|
+
* - "REQ123" → Root requirement
|
|
85
|
+
* - "REQ123.0" → First child (numeric)
|
|
86
|
+
* - "REQ123.given" → First child with label "given"
|
|
87
|
+
* - "REQ123.given#1" → Second child with label "given"
|
|
88
|
+
* - "REQ123.when.then" → First "then" under first "when"
|
|
89
|
+
*/
|
|
90
|
+
export function resolveRequirementPath(requirements, path) {
|
|
91
|
+
const segments = parseRequirementPath(path);
|
|
92
|
+
if (segments.length === 0) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
// First segment should be the root requirement ID
|
|
96
|
+
const rootId = segments[0];
|
|
97
|
+
let currentNode = requirements.find(r => r.id === rootId);
|
|
98
|
+
if (!currentNode) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
// If only one segment, return the root
|
|
102
|
+
if (segments.length === 1) {
|
|
103
|
+
return currentNode;
|
|
104
|
+
}
|
|
105
|
+
// Traverse the path
|
|
106
|
+
for (let i = 1; i < segments.length; i++) {
|
|
107
|
+
const segment = segments[i];
|
|
108
|
+
const resolved = resolvePathSegment(currentNode, segment);
|
|
109
|
+
if (!resolved) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
currentNode = resolved.child;
|
|
113
|
+
}
|
|
114
|
+
return currentNode;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a path and return the full numeric path.
|
|
118
|
+
*
|
|
119
|
+
* Examples:
|
|
120
|
+
* - "REQ123.given" → "REQ123.0" (if "given" is first child)
|
|
121
|
+
* - "REQ123.when.then" → "REQ123.2.1" (numeric path)
|
|
122
|
+
*/
|
|
123
|
+
export function resolveToNumericPath(requirements, path) {
|
|
124
|
+
const node = resolveRequirementPath(requirements, path);
|
|
125
|
+
return node ? node.id : null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get all possible label-based paths for a requirement.
|
|
129
|
+
* Useful for display/debugging.
|
|
130
|
+
*
|
|
131
|
+
* Returns paths like: ["REQ123", "REQ123.given", "REQ123.when", ...]
|
|
132
|
+
*/
|
|
133
|
+
export function getAllLabelPaths(node, prefix = '') {
|
|
134
|
+
const currentPath = prefix ? `${prefix}.${node.label}` : node.id;
|
|
135
|
+
const paths = [currentPath];
|
|
136
|
+
for (const child of node.children) {
|
|
137
|
+
paths.push(...getAllLabelPaths(child, currentPath));
|
|
138
|
+
}
|
|
139
|
+
return paths;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Check if a path is ambiguous (multiple matches for a label).
|
|
143
|
+
* Returns the number of matches, or 0 if unambiguous.
|
|
144
|
+
*/
|
|
145
|
+
export function checkPathAmbiguity(requirements, path) {
|
|
146
|
+
const segments = parseRequirementPath(path);
|
|
147
|
+
if (segments.length === 0) {
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
const rootId = segments[0];
|
|
151
|
+
let currentNode = requirements.find(r => r.id === rootId);
|
|
152
|
+
if (!currentNode) {
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
if (segments.length === 1) {
|
|
156
|
+
return 0; // Root is never ambiguous
|
|
157
|
+
}
|
|
158
|
+
// Check each segment for ambiguity
|
|
159
|
+
for (let i = 1; i < segments.length; i++) {
|
|
160
|
+
const segment = segments[i];
|
|
161
|
+
// Numeric segments are never ambiguous
|
|
162
|
+
if (/^\d+$/.test(segment)) {
|
|
163
|
+
const index = parseInt(segment);
|
|
164
|
+
if (index >= currentNode.children.length) {
|
|
165
|
+
return 0; // Invalid path
|
|
166
|
+
}
|
|
167
|
+
currentNode = currentNode.children[index];
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
// Check for label disambiguation (#N)
|
|
171
|
+
const { label, index } = parsePathSegment(segment);
|
|
172
|
+
const matches = findChildrenByLabel(currentNode, label);
|
|
173
|
+
// If this is the last segment and there are multiple matches, it's ambiguous
|
|
174
|
+
if (i === segments.length - 1 && matches.length > 1 && index === 0) {
|
|
175
|
+
return matches.length;
|
|
176
|
+
}
|
|
177
|
+
// Otherwise, continue traversing
|
|
178
|
+
if (index >= matches.length) {
|
|
179
|
+
return 0; // Invalid path
|
|
180
|
+
}
|
|
181
|
+
currentNode = matches[index];
|
|
182
|
+
}
|
|
183
|
+
return 0; // Path is unambiguous
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=resolver.js.map
|