@popoverai/dotrequirements 0.26.1 → 0.27.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 +13 -69
- package/dist/cli.js +19 -7
- package/dist/codebase-to-spec/present.js +4 -5
- package/dist/codebase-to-spec/validate.js +3 -2
- package/dist/commands/acceptance-test.js +4 -2
- package/dist/commands/ai-setup.d.ts +8 -2
- package/dist/commands/ai-setup.js +154 -310
- package/dist/commands/get.js +6 -2
- package/dist/commands/init.js +8 -6
- package/dist/commands/link-resolution.d.ts +3 -1
- package/dist/commands/link-resolution.js +4 -2
- package/dist/commands/mcp.d.ts +8 -2
- package/dist/commands/mcp.js +17 -6
- package/dist/commands/pull.js +36 -3
- package/dist/commands/push.js +54 -16
- package/dist/commands/report.js +18 -3
- package/dist/commands/review-test.d.ts +5 -1
- package/dist/commands/review-test.js +117 -15
- package/dist/commands/style-check.d.ts +1 -0
- package/dist/commands/style-check.js +137 -13
- package/dist/commands/tests-for.js +13 -13
- package/dist/commands/validate.js +14 -14
- package/dist/convex.d.ts +1 -3
- package/dist/convex.js +3 -3
- package/dist/harness/cache.d.ts +19 -3
- package/dist/harness/cache.js +38 -12
- package/dist/harness/finalize.js +33 -1
- package/dist/harness/index.js +16 -9
- package/dist/harness/requirementsLoader.js +12 -0
- package/dist/harness/tracking.d.ts +17 -2
- package/dist/harness/tracking.js +83 -9
- package/dist/push/core.d.ts +50 -0
- package/dist/push/core.js +149 -11
- package/dist/push/index.d.ts +1 -1
- package/dist/push/index.js +1 -1
- package/dist/requirements/cloud-ai.d.ts +21 -8
- package/dist/requirements/cloud-ai.js +10 -8
- package/dist/requirements/cloud-coverage.d.ts +12 -2
- package/dist/requirements/cloud-coverage.js +30 -3
- package/dist/requirements/grep.d.ts +7 -2
- package/dist/requirements/grep.js +75 -47
- package/dist/schema/builder.d.ts +1 -1
- package/dist/schema/builder.js +13 -0
- package/dist/schema/conversions.d.ts +7 -2
- package/dist/schema/conversions.js +13 -4
- package/dist/schema/parser-core.d.ts +41 -0
- package/dist/schema/parser-core.js +113 -18
- package/dist/schema/parser.d.ts +8 -26
- package/dist/schema/parser.js +23 -251
- package/dist/schema/resolver.js +18 -8
- package/dist/templates/context-file-section.md +25 -22
- package/dist/utils/context-file.d.ts +7 -3
- package/dist/utils/context-file.js +10 -7
- package/dist/utils/env.js +17 -1
- package/dist/utils/oauth-flow.js +8 -0
- package/dist/utils/project-settings.d.ts +5 -0
- package/dist/utils/project-settings.js +36 -1
- package/package.json +3 -5
- package/dist/mcp/convexClient.d.ts +0 -19
- package/dist/mcp/convexClient.js +0 -24
- package/dist/mcp/handlers/authoring.d.ts +0 -41
- package/dist/mcp/handlers/authoring.js +0 -104
- package/dist/mcp/handlers/debug.d.ts +0 -16
- package/dist/mcp/handlers/debug.js +0 -37
- package/dist/mcp/handlers/get.d.ts +0 -24
- package/dist/mcp/handlers/get.js +0 -65
- package/dist/mcp/handlers/index.d.ts +0 -28
- package/dist/mcp/handlers/index.js +0 -19
- package/dist/mcp/handlers/list.d.ts +0 -7
- package/dist/mcp/handlers/list.js +0 -43
- package/dist/mcp/handlers/push.d.ts +0 -26
- package/dist/mcp/handlers/push.js +0 -186
- package/dist/mcp/handlers/report.d.ts +0 -16
- package/dist/mcp/handlers/report.js +0 -134
- package/dist/mcp/handlers/review.d.ts +0 -51
- package/dist/mcp/handlers/review.js +0 -200
- package/dist/mcp/handlers/search.d.ts +0 -30
- package/dist/mcp/handlers/search.js +0 -58
- package/dist/mcp/handlers/test-mapping.d.ts +0 -39
- package/dist/mcp/handlers/test-mapping.js +0 -133
- package/dist/mcp/handlers/types.d.ts +0 -75
- package/dist/mcp/handlers/types.js +0 -25
- package/dist/mcp/index.d.ts +0 -45
- package/dist/mcp/index.js +0 -634
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Convert between Convex flat representation and hierarchical RequirementNode trees.
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
4
|
+
import { type Metadata, type RequirementNode } from "./schemas.js";
|
|
5
5
|
/**
|
|
6
6
|
* Convex requirement type (flat structure with position paths).
|
|
7
7
|
* This mirrors the Convex database schema.
|
|
@@ -31,7 +31,12 @@ export declare function constructKey(prefix: string, index: number): string;
|
|
|
31
31
|
/**
|
|
32
32
|
* Parse a requirement key into prefix and index.
|
|
33
33
|
* E.g., "REQ-123" -> { prefix: "REQ", index: 123 }
|
|
34
|
-
*
|
|
34
|
+
* E.g., "SYNC-CLI-CREATE-1" -> { prefix: "SYNC-CLI-CREATE", index: 1 }
|
|
35
|
+
* Returns null if key doesn't match REQUIREMENT_KEY_PATTERN, which is the
|
|
36
|
+
* sole grammar authority here: the split below is mechanical. (Deliberately
|
|
37
|
+
* NOT delegated to parseRequirementKey — it case-normalizes and enforces a
|
|
38
|
+
* 30-char prefix cap this pattern doesn't, and pattern-passing keys must
|
|
39
|
+
* never fall back to the caller's prefix=node.id path.)
|
|
35
40
|
*/
|
|
36
41
|
export declare function parseKey(key: string): {
|
|
37
42
|
prefix: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Convert between Convex flat representation and hierarchical RequirementNode trees.
|
|
3
3
|
*/
|
|
4
|
+
import { REQUIREMENT_KEY_PATTERN, } from "./schemas.js";
|
|
4
5
|
/**
|
|
5
6
|
* Construct a requirement key from prefix and index.
|
|
6
7
|
* E.g., ("REQ", 123) -> "REQ-123"
|
|
@@ -11,13 +12,21 @@ export function constructKey(prefix, index) {
|
|
|
11
12
|
/**
|
|
12
13
|
* Parse a requirement key into prefix and index.
|
|
13
14
|
* E.g., "REQ-123" -> { prefix: "REQ", index: 123 }
|
|
14
|
-
*
|
|
15
|
+
* E.g., "SYNC-CLI-CREATE-1" -> { prefix: "SYNC-CLI-CREATE", index: 1 }
|
|
16
|
+
* Returns null if key doesn't match REQUIREMENT_KEY_PATTERN, which is the
|
|
17
|
+
* sole grammar authority here: the split below is mechanical. (Deliberately
|
|
18
|
+
* NOT delegated to parseRequirementKey — it case-normalizes and enforces a
|
|
19
|
+
* 30-char prefix cap this pattern doesn't, and pattern-passing keys must
|
|
20
|
+
* never fall back to the caller's prefix=node.id path.)
|
|
15
21
|
*/
|
|
16
22
|
export function parseKey(key) {
|
|
17
|
-
|
|
18
|
-
if (!match)
|
|
23
|
+
if (!REQUIREMENT_KEY_PATTERN.test(key))
|
|
19
24
|
return null;
|
|
20
|
-
|
|
25
|
+
const splitAt = key.lastIndexOf("-");
|
|
26
|
+
return {
|
|
27
|
+
prefix: key.slice(0, splitAt),
|
|
28
|
+
index: parseInt(key.slice(splitAt + 1), 10),
|
|
29
|
+
};
|
|
21
30
|
}
|
|
22
31
|
/**
|
|
23
32
|
* Build a hierarchical tree from flat Convex requirements.
|
|
@@ -15,6 +15,19 @@ export declare const DELIMITER_PATTERN = "(?:\u2192|->)";
|
|
|
15
15
|
* Parse a criterion line in "position. Label → content" format.
|
|
16
16
|
* Example: "0. Given → user has valid credentials"
|
|
17
17
|
* Also supports optional label: "0. → user has valid credentials"
|
|
18
|
+
*
|
|
19
|
+
* This is the canonical criterion grammar (SYNC-KEY-1.4). The web editor's
|
|
20
|
+
* markdown-conversion parser mirrors this exact behavior; keep them in sync.
|
|
21
|
+
*
|
|
22
|
+
* Two behaviors are load-bearing:
|
|
23
|
+
* - Split on the FIRST delimiter only, so an arrow inside the content
|
|
24
|
+
* ("maps A -> B") is preserved verbatim rather than being re-consumed as a
|
|
25
|
+
* label boundary. Everything before the first delimiter is the (optional,
|
|
26
|
+
* freeform — custom labels are supported, see MARKDOWN_SCHEMA) label;
|
|
27
|
+
* everything after is content.
|
|
28
|
+
* - An empty criterion ("0. → ") is a valid criterion with content "", not a
|
|
29
|
+
* parse failure. Returning null here would make parseRequirementBlocksFromMarkdown
|
|
30
|
+
* reject the whole block, and would drop the criterion (and its subtree) on load.
|
|
18
31
|
*/
|
|
19
32
|
export declare function parseCriterionLine(line: string, delimiter?: string): ParsedCriterion | null;
|
|
20
33
|
/**
|
|
@@ -40,6 +53,34 @@ export declare function extractRequirementBlocks(body: string): Array<{
|
|
|
40
53
|
key: string;
|
|
41
54
|
blockContent: string;
|
|
42
55
|
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Split raw file content into its leading YAML frontmatter and body — both
|
|
58
|
+
* derived from ONE fence match, so the two halves are complementary by
|
|
59
|
+
* construction and can never disagree about where the fence closes.
|
|
60
|
+
*
|
|
61
|
+
* Normalizes CRLF first (SYNC-FORMAT-1): frontmatter isolation must accept
|
|
62
|
+
* the same line-ending styles the parser does. Callers must use this (or the
|
|
63
|
+
* wrappers below) instead of hand-rolled regexes so every consumer agrees on
|
|
64
|
+
* what "has frontmatter" means.
|
|
65
|
+
*
|
|
66
|
+
* Returns undefined when the content has no leading frontmatter fence.
|
|
67
|
+
*/
|
|
68
|
+
export declare function splitFrontmatter(content: string): {
|
|
69
|
+
yaml: string;
|
|
70
|
+
body: string;
|
|
71
|
+
} | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* The frontmatter half of splitFrontmatter: the inner YAML text, or
|
|
74
|
+
* undefined when the file has no leading fence.
|
|
75
|
+
*/
|
|
76
|
+
export declare function extractFrontmatterBlock(content: string): string | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* The body half of splitFrontmatter: the (CRLF-normalized) content with the
|
|
79
|
+
* leading frontmatter fence removed, or the whole normalized content when
|
|
80
|
+
* there is no fence. Blank lines directly after the fence are consumed so
|
|
81
|
+
* the body starts at its first real line.
|
|
82
|
+
*/
|
|
83
|
+
export declare function stripFrontmatterBlock(content: string): string;
|
|
43
84
|
/**
|
|
44
85
|
* Parse requirement blocks from markdown content (without frontmatter).
|
|
45
86
|
* Use this for web editor content that doesn't have YAML frontmatter.
|
|
@@ -11,24 +11,48 @@ import { ValidationError, validateKey, } from "./schemas.js";
|
|
|
11
11
|
*/
|
|
12
12
|
export const DEFAULT_DELIMITER = "→";
|
|
13
13
|
export const DELIMITER_PATTERN = "(?:→|->)"; // Non-capturing group for both Unicode and ASCII
|
|
14
|
+
// Compiled once for the common (default-delimiter) path so parseCriterionLine
|
|
15
|
+
// doesn't rebuild the same RegExp on every line.
|
|
16
|
+
const DEFAULT_DELIMITER_REGEX = new RegExp(DELIMITER_PATTERN);
|
|
14
17
|
/**
|
|
15
18
|
* Parse a criterion line in "position. Label → content" format.
|
|
16
19
|
* Example: "0. Given → user has valid credentials"
|
|
17
20
|
* Also supports optional label: "0. → user has valid credentials"
|
|
21
|
+
*
|
|
22
|
+
* This is the canonical criterion grammar (SYNC-KEY-1.4). The web editor's
|
|
23
|
+
* markdown-conversion parser mirrors this exact behavior; keep them in sync.
|
|
24
|
+
*
|
|
25
|
+
* Two behaviors are load-bearing:
|
|
26
|
+
* - Split on the FIRST delimiter only, so an arrow inside the content
|
|
27
|
+
* ("maps A -> B") is preserved verbatim rather than being re-consumed as a
|
|
28
|
+
* label boundary. Everything before the first delimiter is the (optional,
|
|
29
|
+
* freeform — custom labels are supported, see MARKDOWN_SCHEMA) label;
|
|
30
|
+
* everything after is content.
|
|
31
|
+
* - An empty criterion ("0. → ") is a valid criterion with content "", not a
|
|
32
|
+
* parse failure. Returning null here would make parseRequirementBlocksFromMarkdown
|
|
33
|
+
* reject the whole block, and would drop the criterion (and its subtree) on load.
|
|
18
34
|
*/
|
|
19
35
|
export function parseCriterionLine(line, delimiter = DELIMITER_PATTERN) {
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const pattern = new RegExp(`^\\s*(\\d+(?:\\.\\d+)*)\\.\\s*(?:(.+?)\\s+)?${delimiter}\\s*(.+)$`);
|
|
24
|
-
const match = line.match(pattern);
|
|
25
|
-
if (!match) {
|
|
36
|
+
// Position prefix: "0.", "1.0.", "2.3.1." — required for a criterion line.
|
|
37
|
+
const positionMatch = line.match(/^\s*(\d+(?:\.\d+)*)\.\s*/);
|
|
38
|
+
if (!positionMatch) {
|
|
26
39
|
return null;
|
|
27
40
|
}
|
|
41
|
+
const rest = line.slice(positionMatch[0].length);
|
|
42
|
+
const delimiterMatch = rest.match(delimiter === DELIMITER_PATTERN
|
|
43
|
+
? DEFAULT_DELIMITER_REGEX
|
|
44
|
+
: new RegExp(delimiter));
|
|
45
|
+
if (!delimiterMatch || delimiterMatch.index === undefined) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const label = rest.slice(0, delimiterMatch.index).trim();
|
|
49
|
+
const content = rest
|
|
50
|
+
.slice(delimiterMatch.index + delimiterMatch[0].length)
|
|
51
|
+
.trim();
|
|
28
52
|
return {
|
|
29
|
-
position:
|
|
30
|
-
label
|
|
31
|
-
content
|
|
53
|
+
position: positionMatch[1],
|
|
54
|
+
label, // Preserve original case; freeform (empty when no label)
|
|
55
|
+
content, // May be "" for an empty criterion
|
|
32
56
|
};
|
|
33
57
|
}
|
|
34
58
|
/**
|
|
@@ -95,30 +119,48 @@ export function parseRequirementBlock(key, blockContent) {
|
|
|
95
119
|
}
|
|
96
120
|
const criterion = parseCriterionLine(line);
|
|
97
121
|
if (!criterion) {
|
|
98
|
-
//
|
|
122
|
+
// Only a line whose prefix is shaped like a requirement KEY (uppercase
|
|
123
|
+
// prefix + "-<digits>", per REQUIREMENT_KEY_PATTERN) suggests a second
|
|
124
|
+
// requirement in the block; a stray "https://..." must not be
|
|
125
|
+
// misdiagnosed as one.
|
|
99
126
|
const trimmedLine = line.trim();
|
|
100
|
-
if (/^[
|
|
127
|
+
if (/^[A-Z][A-Z-]*-\d+:\s*.+/.test(trimmedLine)) {
|
|
101
128
|
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
129
|
}
|
|
103
130
|
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
131
|
}
|
|
132
|
+
// SYNC-FORMAT-2.1: a duplicate position is structural ambiguity — reject
|
|
133
|
+
// it instead of silently dropping the earlier criterion.
|
|
134
|
+
if (criteriaByPosition.has(criterion.position)) {
|
|
135
|
+
throw new ValidationError(`Duplicate criterion position "${criterion.position}" at line ${i + 1} — each position may appear only once per requirement`, key);
|
|
136
|
+
}
|
|
105
137
|
criteriaByPosition.set(criterion.position, criterion);
|
|
106
138
|
}
|
|
107
139
|
// Build tree from flat position paths
|
|
108
|
-
|
|
140
|
+
const consumed = new Set();
|
|
141
|
+
buildTreeFromPositions(root, key, criteriaByPosition, consumed);
|
|
142
|
+
// SYNC-FORMAT-2.2: any criterion not attached to the tree references a
|
|
143
|
+
// missing parent position — reject it instead of silently dropping it.
|
|
144
|
+
for (const position of criteriaByPosition.keys()) {
|
|
145
|
+
if (!consumed.has(position)) {
|
|
146
|
+
const parentPosition = position.split(".").slice(0, -1).join(".");
|
|
147
|
+
throw new ValidationError(`Orphaned criterion position "${position}" — parent position "${parentPosition}" does not exist`, key);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
109
150
|
return root;
|
|
110
151
|
}
|
|
111
152
|
/**
|
|
112
153
|
* Build a tree structure from flat position paths.
|
|
113
154
|
* Positions like "0", "1", "1.0", "1.1", "1.1.0" define the hierarchy.
|
|
114
155
|
*/
|
|
115
|
-
function buildTreeFromPositions(root, rootKey, criteriaByPosition) {
|
|
156
|
+
function buildTreeFromPositions(root, rootKey, criteriaByPosition, consumed) {
|
|
116
157
|
// Get all top-level children (single digit positions: "0", "1", "2")
|
|
117
158
|
const topLevelPositions = Array.from(criteriaByPosition.keys())
|
|
118
159
|
.filter((pos) => !pos.includes("."))
|
|
119
160
|
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
|
|
120
161
|
for (const position of topLevelPositions) {
|
|
121
162
|
const criterion = criteriaByPosition.get(position);
|
|
163
|
+
consumed.add(position);
|
|
122
164
|
const childNode = {
|
|
123
165
|
id: `${rootKey}.${position}`,
|
|
124
166
|
label: criterion.label,
|
|
@@ -126,14 +168,14 @@ function buildTreeFromPositions(root, rootKey, criteriaByPosition) {
|
|
|
126
168
|
children: [],
|
|
127
169
|
};
|
|
128
170
|
// Recursively build children
|
|
129
|
-
buildChildrenFromPositions(childNode, position, rootKey, criteriaByPosition);
|
|
171
|
+
buildChildrenFromPositions(childNode, position, rootKey, criteriaByPosition, consumed);
|
|
130
172
|
root.children.push(childNode);
|
|
131
173
|
}
|
|
132
174
|
}
|
|
133
175
|
/**
|
|
134
176
|
* Recursively build children for a given position.
|
|
135
177
|
*/
|
|
136
|
-
function buildChildrenFromPositions(parent, parentPosition, rootKey, criteriaByPosition) {
|
|
178
|
+
function buildChildrenFromPositions(parent, parentPosition, rootKey, criteriaByPosition, consumed) {
|
|
137
179
|
// Find direct children (e.g., if parent is "1", find "1.0", "1.1", etc.)
|
|
138
180
|
const childPositions = Array.from(criteriaByPosition.keys())
|
|
139
181
|
.filter((pos) => {
|
|
@@ -158,6 +200,7 @@ function buildChildrenFromPositions(parent, parentPosition, rootKey, criteriaByP
|
|
|
158
200
|
});
|
|
159
201
|
for (const position of childPositions) {
|
|
160
202
|
const criterion = criteriaByPosition.get(position);
|
|
203
|
+
consumed.add(position);
|
|
161
204
|
const childNode = {
|
|
162
205
|
id: `${rootKey}.${position}`,
|
|
163
206
|
label: criterion.label,
|
|
@@ -165,7 +208,7 @@ function buildChildrenFromPositions(parent, parentPosition, rootKey, criteriaByP
|
|
|
165
208
|
children: [],
|
|
166
209
|
};
|
|
167
210
|
// Recursively build grandchildren
|
|
168
|
-
buildChildrenFromPositions(childNode, position, rootKey, criteriaByPosition);
|
|
211
|
+
buildChildrenFromPositions(childNode, position, rootKey, criteriaByPosition, consumed);
|
|
169
212
|
parent.children.push(childNode);
|
|
170
213
|
}
|
|
171
214
|
}
|
|
@@ -175,6 +218,7 @@ function buildChildrenFromPositions(parent, parentPosition, rootKey, criteriaByP
|
|
|
175
218
|
*/
|
|
176
219
|
export function extractRequirementBlocks(body) {
|
|
177
220
|
const blocks = [];
|
|
221
|
+
const seenKeys = new Set();
|
|
178
222
|
// Find all ```dotrequirements code blocks
|
|
179
223
|
const blockRegex = /```dotrequirements\n([\s\S]*?)```/gm;
|
|
180
224
|
let match = blockRegex.exec(body);
|
|
@@ -190,17 +234,65 @@ export function extractRequirementBlocks(body) {
|
|
|
190
234
|
// locally instead of erroring server-side. Validation only — the original
|
|
191
235
|
// key is preserved (normalization happens downstream).
|
|
192
236
|
validateKey(key);
|
|
237
|
+
// SYNC-FORMAT-2.0: a duplicate key is structural ambiguity — reject it
|
|
238
|
+
// instead of producing two requirements with the same key.
|
|
239
|
+
if (seenKeys.has(key)) {
|
|
240
|
+
throw new ValidationError(`Duplicate requirement key "${key}" — each requirement key may appear in only one block`, key);
|
|
241
|
+
}
|
|
242
|
+
seenKeys.add(key);
|
|
193
243
|
blocks.push({ key, blockContent: blockContent.trim() });
|
|
194
244
|
match = blockRegex.exec(body);
|
|
195
245
|
}
|
|
196
246
|
return blocks;
|
|
197
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Split raw file content into its leading YAML frontmatter and body — both
|
|
250
|
+
* derived from ONE fence match, so the two halves are complementary by
|
|
251
|
+
* construction and can never disagree about where the fence closes.
|
|
252
|
+
*
|
|
253
|
+
* Normalizes CRLF first (SYNC-FORMAT-1): frontmatter isolation must accept
|
|
254
|
+
* the same line-ending styles the parser does. Callers must use this (or the
|
|
255
|
+
* wrappers below) instead of hand-rolled regexes so every consumer agrees on
|
|
256
|
+
* what "has frontmatter" means.
|
|
257
|
+
*
|
|
258
|
+
* Returns undefined when the content has no leading frontmatter fence.
|
|
259
|
+
*/
|
|
260
|
+
export function splitFrontmatter(content) {
|
|
261
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
262
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
263
|
+
if (!match) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
return { yaml: match[1], body: normalized.slice(match[0].length) };
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The frontmatter half of splitFrontmatter: the inner YAML text, or
|
|
270
|
+
* undefined when the file has no leading fence.
|
|
271
|
+
*/
|
|
272
|
+
export function extractFrontmatterBlock(content) {
|
|
273
|
+
return splitFrontmatter(content)?.yaml;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* The body half of splitFrontmatter: the (CRLF-normalized) content with the
|
|
277
|
+
* leading frontmatter fence removed, or the whole normalized content when
|
|
278
|
+
* there is no fence. Blank lines directly after the fence are consumed so
|
|
279
|
+
* the body starts at its first real line.
|
|
280
|
+
*/
|
|
281
|
+
export function stripFrontmatterBlock(content) {
|
|
282
|
+
const split = splitFrontmatter(content);
|
|
283
|
+
if (split === undefined) {
|
|
284
|
+
return content.replace(/\r\n/g, "\n");
|
|
285
|
+
}
|
|
286
|
+
return split.body.replace(/^\n+/, "");
|
|
287
|
+
}
|
|
198
288
|
/**
|
|
199
289
|
* Parse requirement blocks from markdown content (without frontmatter).
|
|
200
290
|
* Use this for web editor content that doesn't have YAML frontmatter.
|
|
201
291
|
*/
|
|
202
292
|
export function parseRequirementBlocksFromMarkdown(markdownContent) {
|
|
203
|
-
|
|
293
|
+
// SYNC-FORMAT-1: normalize Windows (CRLF) line endings so content parses
|
|
294
|
+
// identically regardless of line-ending style.
|
|
295
|
+
const blocks = extractRequirementBlocks(markdownContent.replace(/\r\n/g, "\n"));
|
|
204
296
|
const requirements = [];
|
|
205
297
|
for (const { key, blockContent } of blocks) {
|
|
206
298
|
try {
|
|
@@ -208,7 +300,10 @@ export function parseRequirementBlocksFromMarkdown(markdownContent) {
|
|
|
208
300
|
requirements.push(reqNode);
|
|
209
301
|
}
|
|
210
302
|
catch (error) {
|
|
211
|
-
|
|
303
|
+
// Include the inner message so structural errors (duplicate position,
|
|
304
|
+
// orphaned position, ...) stay identifiable at the top level.
|
|
305
|
+
const detail = error instanceof Error && error.message ? `: ${error.message}` : "";
|
|
306
|
+
throw new ValidationError(`Failed to parse requirement ${key}${detail}`, key, error);
|
|
212
307
|
}
|
|
213
308
|
}
|
|
214
309
|
return requirements;
|
package/dist/schema/parser.d.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parse Markdown requirements files into typed structures.
|
|
3
|
+
*
|
|
4
|
+
* File-level concerns only: frontmatter isolation, metadata validation, and
|
|
5
|
+
* disk IO. All block and criterion parsing delegates to the
|
|
6
|
+
* environment-agnostic parser-core module, which the web editor and Convex
|
|
7
|
+
* also consume — one parser, so the CLI and the cloud cannot drift on what a
|
|
8
|
+
* requirements file means (SYNC-FORMAT-2.3).
|
|
3
9
|
*/
|
|
4
|
-
import { type
|
|
5
|
-
|
|
6
|
-
* Parse a criterion line in "position. Label → content" format.
|
|
7
|
-
* Example: "0. Given → user has valid credentials"
|
|
8
|
-
* Also supports optional label: "0. → user has valid credentials"
|
|
9
|
-
*/
|
|
10
|
-
export declare function parseCriterionLine(line: string, delimiter?: string): ParsedCriterion | null;
|
|
11
|
-
/**
|
|
12
|
-
* Parse a dotrequirements fenced block into a RequirementNode tree.
|
|
13
|
-
* First line is the requirement content (with optional arrow format).
|
|
14
|
-
* Subsequent lines are criteria with position paths.
|
|
15
|
-
*/
|
|
16
|
-
export declare function parseRequirementBlock(key: string, blockContent: string): RequirementNode;
|
|
10
|
+
import { type RequirementNode, type RequirementsFile } from "./schemas.js";
|
|
11
|
+
export { findRequirementById, flattenRequirementTree, getAllRequirements, parseCriterionLine, parseRequirementBlock, } from "./parser-core.js";
|
|
17
12
|
/**
|
|
18
13
|
* Parse a complete requirements Markdown file.
|
|
19
14
|
*/
|
|
@@ -28,17 +23,4 @@ export declare function parseRequirementsFromFile(filePath: string): {
|
|
|
28
23
|
metadata: RequirementsFile["_meta"];
|
|
29
24
|
requirements: RequirementNode[];
|
|
30
25
|
};
|
|
31
|
-
/**
|
|
32
|
-
* Flatten a requirement tree into a list of all nodes.
|
|
33
|
-
* Useful for searching or displaying all requirements.
|
|
34
|
-
*/
|
|
35
|
-
export declare function flattenRequirementTree(node: RequirementNode): RequirementNode[];
|
|
36
|
-
/**
|
|
37
|
-
* Find a requirement by its ID in a tree.
|
|
38
|
-
*/
|
|
39
|
-
export declare function findRequirementById(nodes: RequirementNode[], id: string): RequirementNode | undefined;
|
|
40
|
-
/**
|
|
41
|
-
* Get all requirements from multiple files as a flat list.
|
|
42
|
-
*/
|
|
43
|
-
export declare function getAllRequirements(requirements: RequirementNode[]): RequirementNode[];
|
|
44
26
|
//# sourceMappingURL=parser.d.ts.map
|