@popoverai/dotrequirements 0.23.0 → 0.24.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 +167 -20
- package/dist/cli.js +121 -60
- package/dist/codebase-to-spec/budget.d.ts +53 -0
- package/dist/codebase-to-spec/budget.js +80 -0
- package/dist/codebase-to-spec/cache.d.ts +49 -0
- package/dist/codebase-to-spec/cache.js +54 -0
- package/dist/codebase-to-spec/claude.d.ts +69 -0
- package/dist/codebase-to-spec/claude.js +126 -0
- package/dist/codebase-to-spec/compose.d.ts +49 -0
- package/dist/codebase-to-spec/compose.js +124 -0
- package/dist/codebase-to-spec/edit-loop.d.ts +54 -0
- package/dist/codebase-to-spec/edit-loop.js +195 -0
- package/dist/codebase-to-spec/editor.d.ts +54 -0
- package/dist/codebase-to-spec/editor.js +74 -0
- package/dist/codebase-to-spec/exit-codes.d.ts +40 -0
- package/dist/codebase-to-spec/exit-codes.js +58 -0
- package/dist/codebase-to-spec/fan-out.d.ts +63 -0
- package/dist/codebase-to-spec/fan-out.js +215 -0
- package/dist/codebase-to-spec/interactive.d.ts +30 -0
- package/dist/codebase-to-spec/interactive.js +48 -0
- package/dist/codebase-to-spec/outline-review-loop.d.ts +51 -0
- package/dist/codebase-to-spec/outline-review-loop.js +187 -0
- package/dist/codebase-to-spec/pack.d.ts +51 -0
- package/dist/codebase-to-spec/pack.js +127 -0
- package/dist/codebase-to-spec/planner.d.ts +41 -0
- package/dist/codebase-to-spec/planner.js +76 -0
- package/dist/codebase-to-spec/present.d.ts +94 -0
- package/dist/codebase-to-spec/present.js +288 -0
- package/dist/codebase-to-spec/progress.d.ts +33 -0
- package/dist/codebase-to-spec/progress.js +28 -0
- package/dist/codebase-to-spec/prompts/editor.d.ts +13 -0
- package/dist/codebase-to-spec/prompts/editor.js +57 -0
- package/dist/codebase-to-spec/prompts/outline-reviewer.d.ts +12 -0
- package/dist/codebase-to-spec/prompts/outline-reviewer.js +87 -0
- package/dist/codebase-to-spec/prompts/planner-apply.d.ts +12 -0
- package/dist/codebase-to-spec/prompts/planner-apply.js +32 -0
- package/dist/codebase-to-spec/prompts/planner-initial.d.ts +11 -0
- package/dist/codebase-to-spec/prompts/planner-initial.js +125 -0
- package/dist/codebase-to-spec/prompts/planner-revise.d.ts +14 -0
- package/dist/codebase-to-spec/prompts/planner-revise.js +60 -0
- package/dist/codebase-to-spec/prompts/spec-reviewer.d.ts +16 -0
- package/dist/codebase-to-spec/prompts/spec-reviewer.js +96 -0
- package/dist/codebase-to-spec/prompts/specifier.d.ts +12 -0
- package/dist/codebase-to-spec/prompts/specifier.js +100 -0
- package/dist/codebase-to-spec/prompts/style-check.d.ts +12 -0
- package/dist/codebase-to-spec/prompts/style-check.js +78 -0
- package/dist/codebase-to-spec/schemas.d.ts +257 -0
- package/dist/codebase-to-spec/schemas.js +183 -0
- package/dist/codebase-to-spec/skill-install.d.ts +57 -0
- package/dist/codebase-to-spec/skill-install.js +79 -0
- package/dist/codebase-to-spec/slice.d.ts +49 -0
- package/dist/codebase-to-spec/slice.js +111 -0
- package/dist/codebase-to-spec/specifier.d.ts +60 -0
- package/dist/codebase-to-spec/specifier.js +79 -0
- package/dist/codebase-to-spec/style-check.d.ts +29 -0
- package/dist/codebase-to-spec/style-check.js +33 -0
- package/dist/codebase-to-spec/summary.d.ts +51 -0
- package/dist/codebase-to-spec/summary.js +183 -0
- package/dist/codebase-to-spec/validate.d.ts +46 -0
- package/dist/codebase-to-spec/validate.js +130 -0
- package/dist/commands/acceptance-test.d.ts +6 -0
- package/dist/commands/acceptance-test.js +212 -0
- package/dist/commands/ai-setup.d.ts +5 -0
- package/dist/commands/ai-setup.js +441 -0
- package/dist/commands/browsertest.js +34 -27
- package/dist/commands/codebase-to-spec/compose.d.ts +14 -0
- package/dist/commands/codebase-to-spec/compose.js +57 -0
- package/dist/commands/codebase-to-spec/edit-loop.d.ts +16 -0
- package/dist/commands/codebase-to-spec/edit-loop.js +83 -0
- package/dist/commands/codebase-to-spec/fan-out.d.ts +19 -0
- package/dist/commands/codebase-to-spec/fan-out.js +77 -0
- package/dist/commands/codebase-to-spec/index.d.ts +9 -0
- package/dist/commands/codebase-to-spec/index.js +135 -0
- package/dist/commands/codebase-to-spec/pack.d.ts +22 -0
- package/dist/commands/codebase-to-spec/pack.js +76 -0
- package/dist/commands/codebase-to-spec/plan-loop.d.ts +26 -0
- package/dist/commands/codebase-to-spec/plan-loop.js +105 -0
- package/dist/commands/codebase-to-spec/present.d.ts +21 -0
- package/dist/commands/codebase-to-spec/present.js +92 -0
- package/dist/commands/codebase-to-spec/run.d.ts +20 -0
- package/dist/commands/codebase-to-spec/run.js +85 -0
- package/dist/commands/codebase-to-spec/skill-install.d.ts +20 -0
- package/dist/commands/codebase-to-spec/skill-install.js +51 -0
- package/dist/commands/codebase-to-spec/specify-area.d.ts +18 -0
- package/dist/commands/codebase-to-spec/specify-area.js +82 -0
- package/dist/commands/codebase-to-spec/style-check.d.ts +15 -0
- package/dist/commands/codebase-to-spec/style-check.js +42 -0
- package/dist/commands/codebase-to-spec/validate.d.ts +18 -0
- package/dist/commands/codebase-to-spec/validate.js +38 -0
- package/dist/commands/create-requirement-document.d.ts +2 -0
- package/dist/commands/create-requirement-document.js +41 -0
- package/dist/commands/finalize.js +7 -7
- package/dist/commands/get.d.ts +2 -0
- package/dist/commands/get.js +55 -0
- package/dist/commands/init.js +132 -117
- package/dist/commands/link.js +27 -27
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.js +43 -0
- package/dist/commands/mcp-setup.js +159 -149
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/prepare.js +4 -4
- package/dist/commands/pull.js +116 -121
- package/dist/commands/push.js +106 -112
- package/dist/commands/report.d.ts +6 -2
- package/dist/commands/report.js +177 -122
- package/dist/commands/requirements-for.d.ts +2 -0
- package/dist/commands/requirements-for.js +29 -0
- package/dist/commands/review-test.d.ts +2 -0
- package/dist/commands/review-test.js +75 -0
- package/dist/commands/search.d.ts +6 -0
- package/dist/commands/search.js +39 -0
- package/dist/commands/style-check.d.ts +7 -0
- package/dist/commands/style-check.js +75 -0
- package/dist/commands/test.js +53 -59
- package/dist/commands/tests-for.d.ts +2 -0
- package/dist/commands/tests-for.js +80 -0
- package/dist/commands/validate.d.ts +6 -0
- package/dist/commands/validate.js +72 -0
- package/dist/config.js +1 -1
- package/dist/convex.d.ts +34 -22
- package/dist/convex.js +38 -22
- package/dist/harness/cache.d.ts +1 -5
- package/dist/harness/cache.js +49 -59
- package/dist/harness/convexReporting.d.ts +1 -1
- package/dist/harness/convexReporting.js +9 -7
- package/dist/harness/coverageCache.js +3 -3
- package/dist/harness/finalize.js +59 -46
- package/dist/harness/index.d.ts +6 -7
- package/dist/harness/index.js +9 -10
- package/dist/harness/prepare.js +6 -5
- package/dist/harness/requirementsLoader.d.ts +2 -2
- package/dist/harness/requirementsLoader.js +13 -35
- package/dist/harness/tracking.js +18 -18
- package/dist/harness/types.d.ts +1 -1
- package/dist/mcp/convexClient.d.ts +0 -39
- package/dist/mcp/convexClient.js +2 -107
- package/dist/mcp/grep.d.ts +1 -1
- package/dist/mcp/grep.js +87 -42
- package/dist/mcp/handlers/authoring.d.ts +1 -1
- package/dist/mcp/handlers/authoring.js +30 -234
- package/dist/mcp/handlers/coverage.d.ts +1 -1
- package/dist/mcp/handlers/coverage.js +13 -15
- package/dist/mcp/handlers/debug.d.ts +2 -3
- package/dist/mcp/handlers/debug.js +10 -10
- package/dist/mcp/handlers/get.d.ts +1 -1
- package/dist/mcp/handlers/get.js +11 -10
- package/dist/mcp/handlers/index.d.ts +20 -20
- package/dist/mcp/handlers/index.js +10 -10
- package/dist/mcp/handlers/list.d.ts +4 -33
- package/dist/mcp/handlers/list.js +16 -38
- package/dist/mcp/handlers/push.d.ts +1 -1
- package/dist/mcp/handlers/push.js +28 -18
- package/dist/mcp/handlers/report.d.ts +16 -0
- package/dist/mcp/handlers/report.js +134 -0
- package/dist/mcp/handlers/review.d.ts +1 -1
- package/dist/mcp/handlers/review.js +40 -59
- package/dist/mcp/handlers/search.d.ts +1 -1
- package/dist/mcp/handlers/search.js +7 -9
- package/dist/mcp/handlers/test-mapping.d.ts +1 -1
- package/dist/mcp/handlers/test-mapping.js +14 -14
- package/dist/mcp/handlers/types.d.ts +3 -3
- package/dist/mcp/handlers/types.js +2 -2
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +147 -167
- package/dist/mcp/requirements.d.ts +2 -2
- package/dist/mcp/requirements.js +30 -30
- package/dist/mcp/testCodeExtractor.js +24 -26
- package/dist/mcp/types.d.ts +1 -1
- package/dist/push/core.d.ts +2 -2
- package/dist/push/core.js +20 -20
- package/dist/push/index.d.ts +1 -1
- package/dist/push/index.js +2 -2
- package/dist/requirements/cloud-ai.d.ts +57 -0
- package/dist/requirements/cloud-ai.js +104 -0
- package/dist/requirements/cloud-coverage.d.ts +41 -0
- package/dist/requirements/cloud-coverage.js +60 -0
- package/dist/requirements/coverage.d.ts +45 -0
- package/dist/requirements/coverage.js +114 -0
- package/dist/requirements/grep.d.ts +33 -0
- package/dist/requirements/grep.js +306 -0
- package/dist/requirements/index.d.ts +73 -0
- package/dist/requirements/index.js +174 -0
- package/dist/requirements/style-guide.d.ts +67 -0
- package/dist/requirements/style-guide.js +299 -0
- package/dist/requirements/testCodeExtractor.d.ts +22 -0
- package/dist/requirements/testCodeExtractor.js +150 -0
- package/dist/schema/browser.d.ts +8 -8
- package/dist/schema/browser.js +13 -15
- package/dist/schema/builder.d.ts +1 -1
- package/dist/schema/builder.js +13 -44
- package/dist/schema/conversions.d.ts +2 -2
- package/dist/schema/conversions.js +11 -11
- package/dist/schema/index.d.ts +9 -9
- package/dist/schema/index.js +15 -15
- package/dist/schema/parser-core.d.ts +1 -1
- package/dist/schema/parser-core.js +23 -22
- package/dist/schema/parser.d.ts +3 -3
- package/dist/schema/parser.js +27 -31
- package/dist/schema/resolver.d.ts +1 -1
- package/dist/schema/resolver.js +9 -9
- package/dist/schema/scenario.d.ts +1 -1
- package/dist/schema/scenario.js +1 -1
- package/dist/schema/schemas.d.ts +3 -3
- package/dist/schema/schemas.js +41 -28
- package/dist/schema/test-schema.js +27 -27
- package/dist/templates/context-file-section.md +3 -2
- package/dist/templates/example-requirements.js +1 -1
- package/dist/templates/example-requirements.ts +3 -1
- package/dist/templates/requirements-readme.js +1 -1
- package/dist/templates/requirements-readme.ts +1 -1
- package/dist/templates/skills/codebase-to-spec/SKILL.md +118 -0
- package/dist/utils/brand.js +3 -3
- package/dist/utils/browser-launch.js +4 -4
- package/dist/utils/context-file.d.ts +1 -1
- package/dist/utils/context-file.js +26 -26
- package/dist/utils/env.js +7 -7
- package/dist/utils/gitignore.js +7 -7
- package/dist/utils/oauth-callback-server.d.ts +1 -1
- package/dist/utils/oauth-callback-server.js +27 -25
- package/dist/utils/oauth-flow.js +32 -29
- package/dist/utils/project-discovery.d.ts +3 -3
- package/dist/utils/project-discovery.js +18 -17
- package/dist/utils/project-name.js +8 -8
- package/dist/utils/project-selector.d.ts +1 -1
- package/dist/utils/project-selector.js +24 -21
- package/dist/utils/project-settings.d.ts +1 -1
- package/dist/utils/project-settings.js +24 -22
- package/dist/utils/templates.js +6 -6
- package/package.json +2 -1
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* This module contains pure parsing functions with no Node.js dependencies,
|
|
5
5
|
* making it safe to import in Convex runtime or browser environments.
|
|
6
6
|
*/
|
|
7
|
-
import { ValidationError, } from
|
|
7
|
+
import { ValidationError, } from "./schemas.js";
|
|
8
8
|
/**
|
|
9
9
|
* Default delimiter for requirements.
|
|
10
10
|
* Can be overridden for organization-specific preferences.
|
|
11
11
|
*/
|
|
12
|
-
export const DEFAULT_DELIMITER =
|
|
13
|
-
export const DELIMITER_PATTERN =
|
|
12
|
+
export const DEFAULT_DELIMITER = "→";
|
|
13
|
+
export const DELIMITER_PATTERN = "(?:→|->)"; // Non-capturing group for both Unicode and ASCII
|
|
14
14
|
/**
|
|
15
15
|
* Parse a criterion line in "position. Label → content" format.
|
|
16
16
|
* Example: "0. Given → user has valid credentials"
|
|
@@ -27,7 +27,7 @@ export function parseCriterionLine(line, delimiter = DELIMITER_PATTERN) {
|
|
|
27
27
|
}
|
|
28
28
|
return {
|
|
29
29
|
position: match[1],
|
|
30
|
-
label: match[2] ? match[2].trim() :
|
|
30
|
+
label: match[2] ? match[2].trim() : "", // Preserve original case, empty if no label
|
|
31
31
|
content: match[3].trim(),
|
|
32
32
|
};
|
|
33
33
|
}
|
|
@@ -43,7 +43,7 @@ export function parseRootLine(line, delimiter = DELIMITER_PATTERN) {
|
|
|
43
43
|
if (keyMatch) {
|
|
44
44
|
return {
|
|
45
45
|
key: keyMatch[1],
|
|
46
|
-
label:
|
|
46
|
+
label: "", // Keys use empty string for unlabeled requirements
|
|
47
47
|
content: keyMatch[2].trim(),
|
|
48
48
|
};
|
|
49
49
|
}
|
|
@@ -53,12 +53,12 @@ export function parseRootLine(line, delimiter = DELIMITER_PATTERN) {
|
|
|
53
53
|
if (!match) {
|
|
54
54
|
// If no delimiter found, treat entire line as content with empty label
|
|
55
55
|
return {
|
|
56
|
-
label:
|
|
56
|
+
label: "",
|
|
57
57
|
content: trimmed,
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
return {
|
|
61
|
-
label: match[1] ? match[1].trim() :
|
|
61
|
+
label: match[1] ? match[1].trim() : "",
|
|
62
62
|
content: match[2].trim(),
|
|
63
63
|
};
|
|
64
64
|
}
|
|
@@ -68,20 +68,20 @@ export function parseRootLine(line, delimiter = DELIMITER_PATTERN) {
|
|
|
68
68
|
* Subsequent lines are criteria with position paths.
|
|
69
69
|
*/
|
|
70
70
|
export function parseRequirementBlock(key, blockContent) {
|
|
71
|
-
const lines = blockContent.trim().split(
|
|
71
|
+
const lines = blockContent.trim().split("\n");
|
|
72
72
|
if (lines.length === 0) {
|
|
73
|
-
throw new ValidationError(
|
|
73
|
+
throw new ValidationError("Empty requirement block", key);
|
|
74
74
|
}
|
|
75
75
|
// First line is the requirement content
|
|
76
76
|
const rootParsed = parseRootLine(lines[0]);
|
|
77
77
|
if (!rootParsed.content) {
|
|
78
|
-
throw new ValidationError(
|
|
78
|
+
throw new ValidationError("Requirement content (first line) cannot be empty", key);
|
|
79
79
|
}
|
|
80
80
|
const root = {
|
|
81
81
|
id: key,
|
|
82
82
|
// Root requirements always use "requirementHeader" label for indexing/querying
|
|
83
83
|
// The markdown format doesn't preserve root labels, but we standardize on this value
|
|
84
|
-
label:
|
|
84
|
+
label: "requirementHeader",
|
|
85
85
|
content: rootParsed.content,
|
|
86
86
|
children: [],
|
|
87
87
|
};
|
|
@@ -90,7 +90,7 @@ export function parseRequirementBlock(key, blockContent) {
|
|
|
90
90
|
for (let i = 1; i < lines.length; i++) {
|
|
91
91
|
const line = lines[i];
|
|
92
92
|
// Skip empty lines and comments
|
|
93
|
-
if (!line.trim() || line.trim().startsWith(
|
|
93
|
+
if (!line.trim() || line.trim().startsWith("#")) {
|
|
94
94
|
continue;
|
|
95
95
|
}
|
|
96
96
|
const criterion = parseCriterionLine(line);
|
|
@@ -98,7 +98,7 @@ export function parseRequirementBlock(key, blockContent) {
|
|
|
98
98
|
// Check if this looks like another requirement key (KEY: content format)
|
|
99
99
|
const trimmedLine = line.trim();
|
|
100
100
|
if (/^[\w-]+:\s*.+/.test(trimmedLine)) {
|
|
101
|
-
throw new ValidationError(`Multiple requirements in single block. Found "${trimmedLine.split(
|
|
101
|
+
throw new ValidationError(`Multiple requirements in single block. Found "${trimmedLine.split(":")[0]}" at line ${i + 1}, but each requirement must have its own \`\`\`dotrequirements code block.`, key);
|
|
102
102
|
}
|
|
103
103
|
throw new ValidationError(`Invalid criterion format at line ${i + 1}: "${trimmedLine}". Expected format: "N. Label → content" (e.g., "0. Given → user is logged in")`, key);
|
|
104
104
|
}
|
|
@@ -115,8 +115,8 @@ export function parseRequirementBlock(key, blockContent) {
|
|
|
115
115
|
function buildTreeFromPositions(root, rootKey, criteriaByPosition) {
|
|
116
116
|
// Get all top-level children (single digit positions: "0", "1", "2")
|
|
117
117
|
const topLevelPositions = Array.from(criteriaByPosition.keys())
|
|
118
|
-
.filter(pos => !pos.includes(
|
|
119
|
-
.sort((a, b) => parseInt(a) - parseInt(b));
|
|
118
|
+
.filter((pos) => !pos.includes("."))
|
|
119
|
+
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
|
|
120
120
|
for (const position of topLevelPositions) {
|
|
121
121
|
const criterion = criteriaByPosition.get(position);
|
|
122
122
|
const childNode = {
|
|
@@ -136,9 +136,9 @@ function buildTreeFromPositions(root, rootKey, criteriaByPosition) {
|
|
|
136
136
|
function buildChildrenFromPositions(parent, parentPosition, rootKey, criteriaByPosition) {
|
|
137
137
|
// Find direct children (e.g., if parent is "1", find "1.0", "1.1", etc.)
|
|
138
138
|
const childPositions = Array.from(criteriaByPosition.keys())
|
|
139
|
-
.filter(pos => {
|
|
140
|
-
const parts = pos.split(
|
|
141
|
-
const parentParts = parentPosition.split(
|
|
139
|
+
.filter((pos) => {
|
|
140
|
+
const parts = pos.split(".");
|
|
141
|
+
const parentParts = parentPosition.split(".");
|
|
142
142
|
// Must be exactly one level deeper
|
|
143
143
|
if (parts.length !== parentParts.length + 1) {
|
|
144
144
|
return false;
|
|
@@ -152,8 +152,8 @@ function buildChildrenFromPositions(parent, parentPosition, rootKey, criteriaByP
|
|
|
152
152
|
return true;
|
|
153
153
|
})
|
|
154
154
|
.sort((a, b) => {
|
|
155
|
-
const aLast = parseInt(a.split(
|
|
156
|
-
const bLast = parseInt(b.split(
|
|
155
|
+
const aLast = parseInt(a.split(".").pop(), 10);
|
|
156
|
+
const bLast = parseInt(b.split(".").pop(), 10);
|
|
157
157
|
return aLast - bLast;
|
|
158
158
|
});
|
|
159
159
|
for (const position of childPositions) {
|
|
@@ -177,8 +177,8 @@ export function extractRequirementBlocks(body) {
|
|
|
177
177
|
const blocks = [];
|
|
178
178
|
// Find all ```dotrequirements code blocks
|
|
179
179
|
const blockRegex = /```dotrequirements\n([\s\S]*?)```/gm;
|
|
180
|
-
let match;
|
|
181
|
-
while (
|
|
180
|
+
let match = blockRegex.exec(body);
|
|
181
|
+
while (match !== null) {
|
|
182
182
|
const blockContent = match[1];
|
|
183
183
|
// Extract key from first line of block (format: "KEY: content")
|
|
184
184
|
const firstLineMatch = blockContent.match(/^([\w-]+):\s*(.+)/);
|
|
@@ -187,6 +187,7 @@ export function extractRequirementBlocks(body) {
|
|
|
187
187
|
}
|
|
188
188
|
const key = firstLineMatch[1];
|
|
189
189
|
blocks.push({ key, blockContent: blockContent.trim() });
|
|
190
|
+
match = blockRegex.exec(body);
|
|
190
191
|
}
|
|
191
192
|
return blocks;
|
|
192
193
|
}
|
package/dist/schema/parser.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parse Markdown requirements files into typed structures.
|
|
3
3
|
*/
|
|
4
|
-
import { RequirementNode, RequirementsFile
|
|
4
|
+
import { type ParsedCriterion, type RequirementNode, type RequirementsFile } from "./schemas.js";
|
|
5
5
|
/**
|
|
6
6
|
* Parse a criterion line in "position. Label → content" format.
|
|
7
7
|
* Example: "0. Given → user has valid credentials"
|
|
@@ -18,14 +18,14 @@ export declare function parseRequirementBlock(key: string, blockContent: string)
|
|
|
18
18
|
* Parse a complete requirements Markdown file.
|
|
19
19
|
*/
|
|
20
20
|
export declare function parseRequirementsFile(markdownContent: string): {
|
|
21
|
-
metadata: RequirementsFile[
|
|
21
|
+
metadata: RequirementsFile["_meta"];
|
|
22
22
|
requirements: RequirementNode[];
|
|
23
23
|
};
|
|
24
24
|
/**
|
|
25
25
|
* Parse requirements file from disk.
|
|
26
26
|
*/
|
|
27
27
|
export declare function parseRequirementsFromFile(filePath: string): {
|
|
28
|
-
metadata: RequirementsFile[
|
|
28
|
+
metadata: RequirementsFile["_meta"];
|
|
29
29
|
requirements: RequirementNode[];
|
|
30
30
|
};
|
|
31
31
|
/**
|
package/dist/schema/parser.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parse Markdown requirements files into typed structures.
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import { ValidationError, validateMetadata, } from
|
|
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
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import YAML from "yaml";
|
|
6
|
+
import { ValidationError, validateMetadata, } from "./schemas.js";
|
|
7
|
+
const DELIMITER_PATTERN = "(?:→|->)"; // Non-capturing group for both Unicode and ASCII
|
|
13
8
|
/**
|
|
14
9
|
* Parse a criterion line in "position. Label → content" format.
|
|
15
10
|
* Example: "0. Given → user has valid credentials"
|
|
@@ -26,7 +21,7 @@ export function parseCriterionLine(line, delimiter = DELIMITER_PATTERN) {
|
|
|
26
21
|
}
|
|
27
22
|
return {
|
|
28
23
|
position: match[1],
|
|
29
|
-
label: match[2] ? match[2].trim() :
|
|
24
|
+
label: match[2] ? match[2].trim() : "", // Preserve original case, empty if no label
|
|
30
25
|
content: match[3].trim(),
|
|
31
26
|
};
|
|
32
27
|
}
|
|
@@ -41,7 +36,7 @@ function parseRootLine(line, delimiter = DELIMITER_PATTERN) {
|
|
|
41
36
|
const keyMatch = trimmed.match(/^([\w-]+):\s*(.+)$/);
|
|
42
37
|
if (keyMatch) {
|
|
43
38
|
return {
|
|
44
|
-
label:
|
|
39
|
+
label: "", // Keys use empty string for unlabeled requirements
|
|
45
40
|
content: keyMatch[2].trim(),
|
|
46
41
|
};
|
|
47
42
|
}
|
|
@@ -51,12 +46,12 @@ function parseRootLine(line, delimiter = DELIMITER_PATTERN) {
|
|
|
51
46
|
if (!match) {
|
|
52
47
|
// If no delimiter found, treat entire line as content with empty label
|
|
53
48
|
return {
|
|
54
|
-
label:
|
|
49
|
+
label: "",
|
|
55
50
|
content: trimmed,
|
|
56
51
|
};
|
|
57
52
|
}
|
|
58
53
|
return {
|
|
59
|
-
label: match[1] ? match[1].trim() :
|
|
54
|
+
label: match[1] ? match[1].trim() : "",
|
|
60
55
|
content: match[2].trim(),
|
|
61
56
|
};
|
|
62
57
|
}
|
|
@@ -66,20 +61,20 @@ function parseRootLine(line, delimiter = DELIMITER_PATTERN) {
|
|
|
66
61
|
* Subsequent lines are criteria with position paths.
|
|
67
62
|
*/
|
|
68
63
|
export function parseRequirementBlock(key, blockContent) {
|
|
69
|
-
const lines = blockContent.trim().split(
|
|
64
|
+
const lines = blockContent.trim().split("\n");
|
|
70
65
|
if (lines.length === 0) {
|
|
71
|
-
throw new ValidationError(
|
|
66
|
+
throw new ValidationError("Empty requirement block", key);
|
|
72
67
|
}
|
|
73
68
|
// First line is the requirement content
|
|
74
69
|
const rootParsed = parseRootLine(lines[0]);
|
|
75
70
|
if (!rootParsed.content) {
|
|
76
|
-
throw new ValidationError(
|
|
71
|
+
throw new ValidationError("Requirement content (first line) cannot be empty", key);
|
|
77
72
|
}
|
|
78
73
|
const root = {
|
|
79
74
|
id: key,
|
|
80
75
|
// Root requirements always use "requirementHeader" label for indexing/querying
|
|
81
76
|
// The markdown format doesn't preserve root labels, but we standardize on this value
|
|
82
|
-
label:
|
|
77
|
+
label: "requirementHeader",
|
|
83
78
|
content: rootParsed.content,
|
|
84
79
|
children: [],
|
|
85
80
|
};
|
|
@@ -88,7 +83,7 @@ export function parseRequirementBlock(key, blockContent) {
|
|
|
88
83
|
for (let i = 1; i < lines.length; i++) {
|
|
89
84
|
const line = lines[i];
|
|
90
85
|
// Skip empty lines and comments
|
|
91
|
-
if (!line.trim() || line.trim().startsWith(
|
|
86
|
+
if (!line.trim() || line.trim().startsWith("#")) {
|
|
92
87
|
continue;
|
|
93
88
|
}
|
|
94
89
|
const criterion = parseCriterionLine(line);
|
|
@@ -108,8 +103,8 @@ export function parseRequirementBlock(key, blockContent) {
|
|
|
108
103
|
function buildTreeFromPositions(root, rootKey, criteriaByPosition) {
|
|
109
104
|
// Get all top-level children (single digit positions: "0", "1", "2")
|
|
110
105
|
const topLevelPositions = Array.from(criteriaByPosition.keys())
|
|
111
|
-
.filter(pos => !pos.includes(
|
|
112
|
-
.sort((a, b) => parseInt(a) - parseInt(b));
|
|
106
|
+
.filter((pos) => !pos.includes("."))
|
|
107
|
+
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
|
|
113
108
|
for (const position of topLevelPositions) {
|
|
114
109
|
const criterion = criteriaByPosition.get(position);
|
|
115
110
|
const childNode = {
|
|
@@ -129,9 +124,9 @@ function buildTreeFromPositions(root, rootKey, criteriaByPosition) {
|
|
|
129
124
|
function buildChildrenFromPositions(parent, parentPosition, rootKey, criteriaByPosition) {
|
|
130
125
|
// Find direct children (e.g., if parent is "1", find "1.0", "1.1", etc.)
|
|
131
126
|
const childPositions = Array.from(criteriaByPosition.keys())
|
|
132
|
-
.filter(pos => {
|
|
133
|
-
const parts = pos.split(
|
|
134
|
-
const parentParts = parentPosition.split(
|
|
127
|
+
.filter((pos) => {
|
|
128
|
+
const parts = pos.split(".");
|
|
129
|
+
const parentParts = parentPosition.split(".");
|
|
135
130
|
// Must be exactly one level deeper
|
|
136
131
|
if (parts.length !== parentParts.length + 1) {
|
|
137
132
|
return false;
|
|
@@ -145,8 +140,8 @@ function buildChildrenFromPositions(parent, parentPosition, rootKey, criteriaByP
|
|
|
145
140
|
return true;
|
|
146
141
|
})
|
|
147
142
|
.sort((a, b) => {
|
|
148
|
-
const aLast = parseInt(a.split(
|
|
149
|
-
const bLast = parseInt(b.split(
|
|
143
|
+
const aLast = parseInt(a.split(".").pop(), 10);
|
|
144
|
+
const bLast = parseInt(b.split(".").pop(), 10);
|
|
150
145
|
return aLast - bLast;
|
|
151
146
|
});
|
|
152
147
|
for (const position of childPositions) {
|
|
@@ -171,7 +166,7 @@ function extractFrontmatter(content) {
|
|
|
171
166
|
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
172
167
|
const match = content.match(frontmatterRegex);
|
|
173
168
|
if (!match) {
|
|
174
|
-
throw new ValidationError(
|
|
169
|
+
throw new ValidationError("Missing YAML frontmatter (should start with ---)");
|
|
175
170
|
}
|
|
176
171
|
const [, frontmatterYaml, body] = match;
|
|
177
172
|
let frontmatter;
|
|
@@ -179,7 +174,7 @@ function extractFrontmatter(content) {
|
|
|
179
174
|
frontmatter = YAML.parse(frontmatterYaml);
|
|
180
175
|
}
|
|
181
176
|
catch (error) {
|
|
182
|
-
throw new ValidationError(
|
|
177
|
+
throw new ValidationError("Failed to parse YAML frontmatter", undefined, error);
|
|
183
178
|
}
|
|
184
179
|
return { frontmatter, body: body.trim() };
|
|
185
180
|
}
|
|
@@ -194,8 +189,8 @@ function extractRequirementBlocks(body) {
|
|
|
194
189
|
const blocks = new Map();
|
|
195
190
|
// Find all ```dotrequirements code blocks
|
|
196
191
|
const blockRegex = /```dotrequirements\n([\s\S]*?)```/gm;
|
|
197
|
-
let match;
|
|
198
|
-
while (
|
|
192
|
+
let match = blockRegex.exec(body);
|
|
193
|
+
while (match !== null) {
|
|
199
194
|
const blockContent = match[1];
|
|
200
195
|
// Extract key from first line of block (format: "KEY: content")
|
|
201
196
|
const firstLineMatch = blockContent.match(/^([\w-]+):\s*(.+)/);
|
|
@@ -209,6 +204,7 @@ function extractRequirementBlocks(body) {
|
|
|
209
204
|
const headingMatch = textBeforeBlock.match(/^#{2,6}\s+([^\n]+)\s*$/m);
|
|
210
205
|
const title = headingMatch ? headingMatch[1].trim() : key;
|
|
211
206
|
blocks.set(key, { title, blockContent: blockContent.trim() });
|
|
207
|
+
match = blockRegex.exec(body);
|
|
212
208
|
}
|
|
213
209
|
return blocks;
|
|
214
210
|
}
|
|
@@ -223,7 +219,7 @@ export function parseRequirementsFile(markdownContent) {
|
|
|
223
219
|
// Extract requirement blocks
|
|
224
220
|
const blocks = extractRequirementBlocks(body);
|
|
225
221
|
if (blocks.size === 0) {
|
|
226
|
-
throw new ValidationError(
|
|
222
|
+
throw new ValidationError("No requirement blocks found in document");
|
|
227
223
|
}
|
|
228
224
|
// Parse each requirement block
|
|
229
225
|
const requirements = [];
|
|
@@ -246,7 +242,7 @@ export function parseRequirementsFile(markdownContent) {
|
|
|
246
242
|
*/
|
|
247
243
|
export function parseRequirementsFromFile(filePath) {
|
|
248
244
|
try {
|
|
249
|
-
const content = fs.readFileSync(filePath,
|
|
245
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
250
246
|
return parseRequirementsFile(content);
|
|
251
247
|
}
|
|
252
248
|
catch (error) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Resolve label-based requirement paths to numeric paths.
|
|
3
3
|
* Supports: REQ123.given, REQ123.when.then, REQ123.given#1
|
|
4
4
|
*/
|
|
5
|
-
import { RequirementNode } from
|
|
5
|
+
import type { RequirementNode } from "./schemas.js";
|
|
6
6
|
/**
|
|
7
7
|
* Parse a requirement path into segments.
|
|
8
8
|
* E.g., "REQ123.given.then" → ["REQ123", "given", "then"]
|
package/dist/schema/resolver.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* E.g., "REQ123.given#1" → ["REQ123", "given#1"]
|
|
9
9
|
*/
|
|
10
10
|
export function parseRequirementPath(path) {
|
|
11
|
-
return path.split(
|
|
11
|
+
return path.split(".");
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
14
|
* Parse a path segment that may include disambiguation.
|
|
@@ -22,7 +22,7 @@ export function parsePathSegment(segment) {
|
|
|
22
22
|
}
|
|
23
23
|
return {
|
|
24
24
|
label: match[1],
|
|
25
|
-
index: match[2] ? parseInt(match[2]) : 0,
|
|
25
|
+
index: match[2] ? parseInt(match[2], 10) : 0,
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
@@ -31,7 +31,7 @@ export function parsePathSegment(segment) {
|
|
|
31
31
|
* Examples: "Several-words" -> "several words", "AC" -> "ac"
|
|
32
32
|
*/
|
|
33
33
|
function normalizeLabel(label) {
|
|
34
|
-
return label.replace(/-/g,
|
|
34
|
+
return label.replace(/-/g, " ").toLowerCase();
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* Find children matching a label.
|
|
@@ -40,7 +40,7 @@ function normalizeLabel(label) {
|
|
|
40
40
|
*/
|
|
41
41
|
export function findChildrenByLabel(node, label) {
|
|
42
42
|
const normalizedLabel = normalizeLabel(label);
|
|
43
|
-
return node.children.filter(child => normalizeLabel(child.label) === normalizedLabel);
|
|
43
|
+
return node.children.filter((child) => normalizeLabel(child.label) === normalizedLabel);
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
46
|
* Resolve a label-based path segment to a numeric index.
|
|
@@ -49,7 +49,7 @@ export function findChildrenByLabel(node, label) {
|
|
|
49
49
|
export function resolvePathSegment(node, segment) {
|
|
50
50
|
// Check if segment is numeric (direct index)
|
|
51
51
|
if (/^\d+$/.test(segment)) {
|
|
52
|
-
const index = parseInt(segment);
|
|
52
|
+
const index = parseInt(segment, 10);
|
|
53
53
|
if (index < node.children.length) {
|
|
54
54
|
return {
|
|
55
55
|
child: node.children[index],
|
|
@@ -94,7 +94,7 @@ export function resolveRequirementPath(requirements, path) {
|
|
|
94
94
|
}
|
|
95
95
|
// First segment should be the root requirement ID
|
|
96
96
|
const rootId = segments[0];
|
|
97
|
-
let currentNode = requirements.find(r => r.id === rootId);
|
|
97
|
+
let currentNode = requirements.find((r) => r.id === rootId);
|
|
98
98
|
if (!currentNode) {
|
|
99
99
|
return null;
|
|
100
100
|
}
|
|
@@ -130,7 +130,7 @@ export function resolveToNumericPath(requirements, path) {
|
|
|
130
130
|
*
|
|
131
131
|
* Returns paths like: ["REQ123", "REQ123.given", "REQ123.when", ...]
|
|
132
132
|
*/
|
|
133
|
-
export function getAllLabelPaths(node, prefix =
|
|
133
|
+
export function getAllLabelPaths(node, prefix = "") {
|
|
134
134
|
const currentPath = prefix ? `${prefix}.${node.label}` : node.id;
|
|
135
135
|
const paths = [currentPath];
|
|
136
136
|
for (const child of node.children) {
|
|
@@ -148,7 +148,7 @@ export function checkPathAmbiguity(requirements, path) {
|
|
|
148
148
|
return 0;
|
|
149
149
|
}
|
|
150
150
|
const rootId = segments[0];
|
|
151
|
-
let currentNode = requirements.find(r => r.id === rootId);
|
|
151
|
+
let currentNode = requirements.find((r) => r.id === rootId);
|
|
152
152
|
if (!currentNode) {
|
|
153
153
|
return 0;
|
|
154
154
|
}
|
|
@@ -160,7 +160,7 @@ export function checkPathAmbiguity(requirements, path) {
|
|
|
160
160
|
const segment = segments[i];
|
|
161
161
|
// Numeric segments are never ambiguous
|
|
162
162
|
if (/^\d+$/.test(segment)) {
|
|
163
|
-
const index = parseInt(segment);
|
|
163
|
+
const index = parseInt(segment, 10);
|
|
164
164
|
if (index >= currentNode.children.length) {
|
|
165
165
|
return 0; // Invalid path
|
|
166
166
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scenario builder — turns a requirement tree into a browser-automation Scenario.
|
|
3
3
|
*
|
|
4
|
-
* Used by both the CLI (via `dotreq
|
|
4
|
+
* Used by both the CLI (via `dotreq acceptance-test`) and the Convex Node action
|
|
5
5
|
* (`testCoverage/browserRun:run`) so the two paths produce identical scenarios
|
|
6
6
|
* for the same input.
|
|
7
7
|
*
|
package/dist/schema/scenario.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scenario builder — turns a requirement tree into a browser-automation Scenario.
|
|
3
3
|
*
|
|
4
|
-
* Used by both the CLI (via `dotreq
|
|
4
|
+
* Used by both the CLI (via `dotreq acceptance-test`) and the Convex Node action
|
|
5
5
|
* (`testCoverage/browserRun:run`) so the two paths produce identical scenarios
|
|
6
6
|
* for the same input.
|
|
7
7
|
*
|
package/dist/schema/schemas.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Zod schemas for Markdown requirements format.
|
|
3
3
|
* TypeScript types are inferred from these schemas using z.infer<>.
|
|
4
4
|
*/
|
|
5
|
-
import { z } from
|
|
5
|
+
import { z } from "zod";
|
|
6
6
|
/**
|
|
7
7
|
* Requirement prefix format: uppercase letters and hyphens, 1-30 chars.
|
|
8
8
|
* First char must be a letter.
|
|
@@ -100,7 +100,7 @@ export declare const CONVEX_ID_PATTERN: RegExp;
|
|
|
100
100
|
*/
|
|
101
101
|
export type PushValidationResult = {
|
|
102
102
|
valid: true;
|
|
103
|
-
action:
|
|
103
|
+
action: "create" | "update";
|
|
104
104
|
warning?: string;
|
|
105
105
|
} | {
|
|
106
106
|
valid: false;
|
|
@@ -246,7 +246,7 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
246
246
|
}>;
|
|
247
247
|
}, z.ZodTypeAny, "passthrough">>;
|
|
248
248
|
export type RequirementsFile = z.infer<typeof RequirementsFileSchema> & {
|
|
249
|
-
[key: string]:
|
|
249
|
+
[key: string]: unknown;
|
|
250
250
|
};
|
|
251
251
|
/**
|
|
252
252
|
* Parsed criterion from Markdown (position. Label: content format)
|