@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
package/dist/schema/parser.js
CHANGED
|
@@ -1,245 +1,53 @@
|
|
|
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
10
|
import * as fs from "node:fs";
|
|
5
11
|
import YAML from "yaml";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* Also supports optional label: "0. → user has valid credentials"
|
|
12
|
-
*/
|
|
13
|
-
export function parseCriterionLine(line, delimiter = DELIMITER_PATTERN) {
|
|
14
|
-
// Match: position + period + space + optional(label + space) + delimiter + space + content
|
|
15
|
-
// Position can be: 0, 1.0, 2.3.1, etc.
|
|
16
|
-
// Label can contain any characters except the delimiter
|
|
17
|
-
const pattern = new RegExp(`^\\s*(\\d+(?:\\.\\d+)*)\\.\\s*(?:(.+?)\\s+)?${delimiter}\\s*(.+)$`);
|
|
18
|
-
const match = line.match(pattern);
|
|
19
|
-
if (!match) {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
position: match[1],
|
|
24
|
-
label: match[2] ? match[2].trim() : "", // Preserve original case, empty if no label
|
|
25
|
-
content: match[3].trim(),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Parse the first line of a dotrequirements block (the root requirement).
|
|
30
|
-
* Format: "KEY: content" or "Label → content" or "→ content"
|
|
31
|
-
*/
|
|
32
|
-
function parseRootLine(line, delimiter = DELIMITER_PATTERN) {
|
|
33
|
-
const trimmed = line.trim();
|
|
34
|
-
// Try to match: KEY + colon + content (explicit key format)
|
|
35
|
-
// KEY must be followed by colon (not arrow) to distinguish from "Label → content"
|
|
36
|
-
const keyMatch = trimmed.match(/^([\w-]+):\s*(.+)$/);
|
|
37
|
-
if (keyMatch) {
|
|
38
|
-
return {
|
|
39
|
-
label: "", // Keys use empty string for unlabeled requirements
|
|
40
|
-
content: keyMatch[2].trim(),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
// Match: optional(label + space) + delimiter + space + content
|
|
44
|
-
const pattern = new RegExp(`^(?:(.+?)\\s+)?${delimiter}\\s*(.+)$`);
|
|
45
|
-
const match = trimmed.match(pattern);
|
|
46
|
-
if (!match) {
|
|
47
|
-
// If no delimiter found, treat entire line as content with empty label
|
|
48
|
-
return {
|
|
49
|
-
label: "",
|
|
50
|
-
content: trimmed,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
label: match[1] ? match[1].trim() : "",
|
|
55
|
-
content: match[2].trim(),
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Parse a dotrequirements fenced block into a RequirementNode tree.
|
|
60
|
-
* First line is the requirement content (with optional arrow format).
|
|
61
|
-
* Subsequent lines are criteria with position paths.
|
|
62
|
-
*/
|
|
63
|
-
export function parseRequirementBlock(key, blockContent) {
|
|
64
|
-
const lines = blockContent.trim().split("\n");
|
|
65
|
-
if (lines.length === 0) {
|
|
66
|
-
throw new ValidationError("Empty requirement block", key);
|
|
67
|
-
}
|
|
68
|
-
// First line is the requirement content
|
|
69
|
-
const rootParsed = parseRootLine(lines[0]);
|
|
70
|
-
if (!rootParsed.content) {
|
|
71
|
-
throw new ValidationError("Requirement content (first line) cannot be empty", key);
|
|
72
|
-
}
|
|
73
|
-
const root = {
|
|
74
|
-
id: key,
|
|
75
|
-
// Root requirements always use "requirementHeader" label for indexing/querying
|
|
76
|
-
// The markdown format doesn't preserve root labels, but we standardize on this value
|
|
77
|
-
label: "requirementHeader",
|
|
78
|
-
content: rootParsed.content,
|
|
79
|
-
children: [],
|
|
80
|
-
};
|
|
81
|
-
// Parse criteria lines
|
|
82
|
-
const criteriaByPosition = new Map();
|
|
83
|
-
for (let i = 1; i < lines.length; i++) {
|
|
84
|
-
const line = lines[i];
|
|
85
|
-
// Skip empty lines and comments
|
|
86
|
-
if (!line.trim() || line.trim().startsWith("#")) {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
const criterion = parseCriterionLine(line);
|
|
90
|
-
if (!criterion) {
|
|
91
|
-
throw new ValidationError(`Invalid criterion format at line ${i + 1}: "${line.trim()}"`, key);
|
|
92
|
-
}
|
|
93
|
-
criteriaByPosition.set(criterion.position, criterion);
|
|
94
|
-
}
|
|
95
|
-
// Build tree from flat position paths
|
|
96
|
-
buildTreeFromPositions(root, key, criteriaByPosition);
|
|
97
|
-
return root;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Build a tree structure from flat position paths.
|
|
101
|
-
* Positions like "0", "1", "1.0", "1.1", "1.1.0" define the hierarchy.
|
|
102
|
-
*/
|
|
103
|
-
function buildTreeFromPositions(root, rootKey, criteriaByPosition) {
|
|
104
|
-
// Get all top-level children (single digit positions: "0", "1", "2")
|
|
105
|
-
const topLevelPositions = Array.from(criteriaByPosition.keys())
|
|
106
|
-
.filter((pos) => !pos.includes("."))
|
|
107
|
-
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
|
|
108
|
-
for (const position of topLevelPositions) {
|
|
109
|
-
const criterion = criteriaByPosition.get(position);
|
|
110
|
-
const childNode = {
|
|
111
|
-
id: `${rootKey}.${position}`,
|
|
112
|
-
label: criterion.label,
|
|
113
|
-
content: criterion.content,
|
|
114
|
-
children: [],
|
|
115
|
-
};
|
|
116
|
-
// Recursively build children
|
|
117
|
-
buildChildrenFromPositions(childNode, position, rootKey, criteriaByPosition);
|
|
118
|
-
root.children.push(childNode);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Recursively build children for a given position.
|
|
123
|
-
*/
|
|
124
|
-
function buildChildrenFromPositions(parent, parentPosition, rootKey, criteriaByPosition) {
|
|
125
|
-
// Find direct children (e.g., if parent is "1", find "1.0", "1.1", etc.)
|
|
126
|
-
const childPositions = Array.from(criteriaByPosition.keys())
|
|
127
|
-
.filter((pos) => {
|
|
128
|
-
const parts = pos.split(".");
|
|
129
|
-
const parentParts = parentPosition.split(".");
|
|
130
|
-
// Must be exactly one level deeper
|
|
131
|
-
if (parts.length !== parentParts.length + 1) {
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
// All parent parts must match
|
|
135
|
-
for (let i = 0; i < parentParts.length; i++) {
|
|
136
|
-
if (parts[i] !== parentParts[i]) {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return true;
|
|
141
|
-
})
|
|
142
|
-
.sort((a, b) => {
|
|
143
|
-
const aLast = parseInt(a.split(".").pop(), 10);
|
|
144
|
-
const bLast = parseInt(b.split(".").pop(), 10);
|
|
145
|
-
return aLast - bLast;
|
|
146
|
-
});
|
|
147
|
-
for (const position of childPositions) {
|
|
148
|
-
const criterion = criteriaByPosition.get(position);
|
|
149
|
-
const childNode = {
|
|
150
|
-
id: `${rootKey}.${position}`,
|
|
151
|
-
label: criterion.label,
|
|
152
|
-
content: criterion.content,
|
|
153
|
-
children: [],
|
|
154
|
-
};
|
|
155
|
-
// Recursively build grandchildren
|
|
156
|
-
buildChildrenFromPositions(childNode, position, rootKey, criteriaByPosition);
|
|
157
|
-
parent.children.push(childNode);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
12
|
+
import { parseRequirementBlocksFromMarkdown, splitFrontmatter, } from "./parser-core.js";
|
|
13
|
+
import { ValidationError, validateMetadata, } from "./schemas.js";
|
|
14
|
+
// Block/criterion parsing and tree utilities live in parser-core; re-exported
|
|
15
|
+
// here so the schema module's public surface is unchanged.
|
|
16
|
+
export { findRequirementById, flattenRequirementTree, getAllRequirements, parseCriterionLine, parseRequirementBlock, } from "./parser-core.js";
|
|
160
17
|
/**
|
|
161
18
|
* Extract YAML frontmatter from Markdown content.
|
|
162
19
|
* Returns { frontmatter, body } where frontmatter is the parsed YAML object.
|
|
163
20
|
*/
|
|
164
21
|
function extractFrontmatter(content) {
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
22
|
+
// Single fence match: yaml and body come from the same split, so they can
|
|
23
|
+
// never disagree about where the frontmatter closes.
|
|
24
|
+
const split = splitFrontmatter(content);
|
|
25
|
+
if (split === undefined) {
|
|
169
26
|
throw new ValidationError("Missing YAML frontmatter (should start with ---)");
|
|
170
27
|
}
|
|
171
|
-
const [, frontmatterYaml, body] = match;
|
|
172
28
|
let frontmatter;
|
|
173
29
|
try {
|
|
174
|
-
frontmatter = YAML.parse(
|
|
30
|
+
frontmatter = YAML.parse(split.yaml);
|
|
175
31
|
}
|
|
176
32
|
catch (error) {
|
|
177
33
|
throw new ValidationError("Failed to parse YAML frontmatter", undefined, error);
|
|
178
34
|
}
|
|
179
|
-
return { frontmatter, body: body.trim() };
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Extract requirement blocks from Markdown body.
|
|
183
|
-
* Returns a map of requirement key -> { title, blockContent }
|
|
184
|
-
*
|
|
185
|
-
* The KEY is extracted from the first line inside the code block.
|
|
186
|
-
* Headings are optional and only used for human readability.
|
|
187
|
-
*/
|
|
188
|
-
function extractRequirementBlocks(body) {
|
|
189
|
-
const blocks = new Map();
|
|
190
|
-
// Find all ```dotrequirements code blocks
|
|
191
|
-
const blockRegex = /```dotrequirements\n([\s\S]*?)```/gm;
|
|
192
|
-
let match = blockRegex.exec(body);
|
|
193
|
-
while (match !== null) {
|
|
194
|
-
const blockContent = match[1];
|
|
195
|
-
// Extract key from first line of block (format: "KEY: content")
|
|
196
|
-
const firstLineMatch = blockContent.match(/^([\w-]+):\s*(.+)/);
|
|
197
|
-
if (!firstLineMatch) {
|
|
198
|
-
throw new ValidationError(`Invalid requirement block format - first line must be "KEY: content"`, blockContent.substring(0, 50));
|
|
199
|
-
}
|
|
200
|
-
const key = firstLineMatch[1];
|
|
201
|
-
// SYNC-KEY-1: reject malformed keys at parse time so push/validate fail
|
|
202
|
-
// locally instead of erroring server-side. Validation only — the original
|
|
203
|
-
// key is preserved (normalization happens downstream).
|
|
204
|
-
validateKey(key);
|
|
205
|
-
// Try to find a heading immediately before this block for the title
|
|
206
|
-
// Look backwards from the match position to find the nearest heading
|
|
207
|
-
const textBeforeBlock = body.substring(0, match.index);
|
|
208
|
-
const headingMatch = textBeforeBlock.match(/^#{2,6}\s+([^\n]+)\s*$/m);
|
|
209
|
-
const title = headingMatch ? headingMatch[1].trim() : key;
|
|
210
|
-
blocks.set(key, { title, blockContent: blockContent.trim() });
|
|
211
|
-
match = blockRegex.exec(body);
|
|
212
|
-
}
|
|
213
|
-
return blocks;
|
|
35
|
+
return { frontmatter, body: split.body.trim() };
|
|
214
36
|
}
|
|
215
37
|
/**
|
|
216
38
|
* Parse a complete requirements Markdown file.
|
|
217
39
|
*/
|
|
218
40
|
export function parseRequirementsFile(markdownContent) {
|
|
219
|
-
//
|
|
41
|
+
// CRLF normalization (SYNC-FORMAT-1) happens inside the shared parser-core
|
|
42
|
+
// helpers (splitFrontmatter / parseRequirementBlocksFromMarkdown), so every
|
|
43
|
+
// entry point agrees.
|
|
220
44
|
const { frontmatter, body } = extractFrontmatter(markdownContent);
|
|
221
|
-
// Validate metadata from frontmatter
|
|
222
45
|
const metadata = validateMetadata(frontmatter);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (blocks.size === 0) {
|
|
46
|
+
const requirements = parseRequirementBlocksFromMarkdown(body);
|
|
47
|
+
if (requirements.length === 0) {
|
|
226
48
|
throw new ValidationError("No requirement blocks found in document");
|
|
227
49
|
}
|
|
228
|
-
|
|
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
|
-
};
|
|
50
|
+
return { metadata, requirements };
|
|
243
51
|
}
|
|
244
52
|
/**
|
|
245
53
|
* Parse requirements file from disk.
|
|
@@ -256,40 +64,4 @@ export function parseRequirementsFromFile(filePath) {
|
|
|
256
64
|
throw new ValidationError(`Failed to read file: ${filePath}`, undefined, error);
|
|
257
65
|
}
|
|
258
66
|
}
|
|
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
67
|
//# sourceMappingURL=parser.js.map
|
package/dist/schema/resolver.js
CHANGED
|
@@ -16,7 +16,9 @@ export function parseRequirementPath(path) {
|
|
|
16
16
|
* E.g., "given#2" → { label: "given", index: 2 }
|
|
17
17
|
*/
|
|
18
18
|
export function parsePathSegment(segment) {
|
|
19
|
-
|
|
19
|
+
// HARNESS-RESOLVE-2.0: multi-word labels are addressed by their kebab-case
|
|
20
|
+
// form (e.g. "Edge Case" → "edge-case"), so segments may contain hyphens.
|
|
21
|
+
const match = segment.match(/^([\w-]+)(?:#(\d+))?$/);
|
|
20
22
|
if (!match) {
|
|
21
23
|
throw new Error(`Invalid path segment: ${segment}`);
|
|
22
24
|
}
|
|
@@ -47,10 +49,18 @@ export function findChildrenByLabel(node, label) {
|
|
|
47
49
|
* Returns the child node and its numeric index.
|
|
48
50
|
*/
|
|
49
51
|
export function resolvePathSegment(node, segment) {
|
|
50
|
-
// Check if segment is numeric (
|
|
52
|
+
// Check if segment is numeric (written position)
|
|
53
|
+
// HARNESS-RESOLVE-1: a numeric segment means the position written in the
|
|
54
|
+
// file, not the array index. Node ids preserve written positions (which may
|
|
55
|
+
// have gaps), so match the child whose id's last position segment equals
|
|
56
|
+
// the requested position. When no child was written at that position,
|
|
57
|
+
// resolution fails rather than binding to a neighboring criterion.
|
|
51
58
|
if (/^\d+$/.test(segment)) {
|
|
52
|
-
const index =
|
|
53
|
-
|
|
59
|
+
const index = node.children.findIndex((child) => {
|
|
60
|
+
const lastDot = child.id.lastIndexOf(".");
|
|
61
|
+
return child.id.slice(lastDot + 1) === segment;
|
|
62
|
+
});
|
|
63
|
+
if (index !== -1) {
|
|
54
64
|
return {
|
|
55
65
|
child: node.children[index],
|
|
56
66
|
index,
|
|
@@ -158,13 +168,13 @@ export function checkPathAmbiguity(requirements, path) {
|
|
|
158
168
|
// Check each segment for ambiguity
|
|
159
169
|
for (let i = 1; i < segments.length; i++) {
|
|
160
170
|
const segment = segments[i];
|
|
161
|
-
// Numeric segments are never ambiguous
|
|
171
|
+
// Numeric segments are never ambiguous (resolved by written position)
|
|
162
172
|
if (/^\d+$/.test(segment)) {
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
173
|
+
const resolved = resolvePathSegment(currentNode, segment);
|
|
174
|
+
if (!resolved) {
|
|
165
175
|
return 0; // Invalid path
|
|
166
176
|
}
|
|
167
|
-
currentNode =
|
|
177
|
+
currentNode = resolved.child;
|
|
168
178
|
continue;
|
|
169
179
|
}
|
|
170
180
|
// Check for label disambiguation (#N)
|
|
@@ -10,13 +10,14 @@ requirements, so your plan should too.
|
|
|
10
10
|
|
|
11
11
|
**ALWAYS follow this workflow when changing system behavior** (new features, bug fixes, any behavioral change). Only pure refactoring (same behavior, different code) may skip requirements.
|
|
12
12
|
|
|
13
|
-
1. **Find or write requirements** - Check `.requirements/` for existing specs; write new ones if needed
|
|
14
|
-
2. **Style-
|
|
15
|
-
3. **
|
|
16
|
-
4. **
|
|
17
|
-
5. **
|
|
18
|
-
6. **
|
|
19
|
-
7. **
|
|
13
|
+
1. **Find or write requirements** - Check `.requirements/` for existing specs (`dotreq search`, `dotreq get`, `dotreq list`); write new ones if needed (`dotreq create-requirement-document` prints the template and style guide)
|
|
14
|
+
2. **Style-review** - Dispatch a subagent to run `dotreq style-check <file>` and judge the requirements against the style guide it emits. If this platform cannot dispatch subagents, run the command and perform the review in this conversation.
|
|
15
|
+
3. **Validate** - Run `dotreq validate` to check syntax
|
|
16
|
+
4. **Get approval** - Present requirements, wait for go-ahead
|
|
17
|
+
5. **Implement** - Build the feature
|
|
18
|
+
6. **Write tests** - Reference requirements with `requirement()`
|
|
19
|
+
7. **Run tests** - Verify everything passes
|
|
20
|
+
8. **Test-review** - Dispatch a subagent to run `dotreq review-test <test-file>` and judge the tests against the materials it emits. Same fallback: no subagent support, review in this conversation.
|
|
20
21
|
|
|
21
22
|
### Requirements Syntax
|
|
22
23
|
|
|
@@ -31,7 +32,7 @@ DOMAIN-1: Short description of expected behavior
|
|
|
31
32
|
- Criteria: `position. -> content` (optional label before the arrow)
|
|
32
33
|
- Nesting: Indent with 2 spaces, use `x.y` position paths
|
|
33
34
|
- 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.
|
|
35
|
+
- **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. Run `dotreq create-requirement-document` for full key-naming guidance.
|
|
35
36
|
|
|
36
37
|
### Test Usage
|
|
37
38
|
|
|
@@ -44,21 +45,23 @@ test(requirement('REQ-ID'), () => { /* test the requirement */ });
|
|
|
44
45
|
test(requirement('REQ-ID.0'), () => { /* test specific criterion */ });
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
###
|
|
48
|
+
### CLI Verbs
|
|
48
49
|
|
|
49
50
|
**Exploration:**
|
|
50
|
-
- `
|
|
51
|
-
- `
|
|
52
|
-
- `
|
|
51
|
+
- `dotreq list [--untested]` - Overview of all requirements (`--untested` filters to coverage gaps)
|
|
52
|
+
- `dotreq get <id>` - Requirement tree with test coverage
|
|
53
|
+
- `dotreq search <query> [--regex]` - Search by text/regex
|
|
54
|
+
- `dotreq requirements-for <test-file>` - See requirements a test file covers
|
|
55
|
+
- `dotreq tests-for <req-file>` - See test coverage for a requirements file
|
|
53
56
|
|
|
54
57
|
**Authoring:**
|
|
55
|
-
- `
|
|
56
|
-
- `
|
|
57
|
-
- `
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
- `
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
- `
|
|
58
|
+
- `dotreq create-requirement-document [path]` - Print the template and style guide
|
|
59
|
+
- `dotreq validate [glob]` - Check syntax (works offline)
|
|
60
|
+
- `dotreq push [file]` - Sync to cloud (shows diff preview, then confirms)
|
|
61
|
+
|
|
62
|
+
**Review (judgment runs in the dispatched subagent):**
|
|
63
|
+
- `dotreq style-check <file>` - Emits the style guide + content for the reviewer to judge (`--source cloud` for hosted review)
|
|
64
|
+
- `dotreq review-test <test-file>` - Emits referenced requirement trees + test content for the reviewer to judge (`--source cloud` for hosted review)
|
|
65
|
+
|
|
66
|
+
**Coverage:**
|
|
67
|
+
- `dotreq report [--source local|cloud]` - Test coverage report
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare function getContextFileName(platform: string): string | null;
|
|
5
5
|
/**
|
|
6
|
-
* Find the git root directory
|
|
6
|
+
* Find the git root directory.
|
|
7
|
+
*
|
|
8
|
+
* CONTEXT-FILE-6: in a worktree, .git is a FILE pointing at the main
|
|
9
|
+
* checkout — matching directories only would walk past it and resolve to
|
|
10
|
+
* the main checkout's root, landing the context file in the wrong tree.
|
|
7
11
|
*/
|
|
8
12
|
export declare function findGitRoot(): Promise<string | null>;
|
|
9
13
|
/**
|
|
@@ -39,8 +43,8 @@ export declare function getContextFilePath(platform: string): Promise<string | n
|
|
|
39
43
|
* Build a user-facing message explaining that context file installation
|
|
40
44
|
* was skipped because the current directory is not inside a git repository.
|
|
41
45
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
46
|
+
* Writing the platform's context file is the whole of setup now, so a missing
|
|
47
|
+
* git root means nothing was installed.
|
|
44
48
|
*/
|
|
45
49
|
export declare function buildNoGitRepoMessage(fileName: string): string;
|
|
46
50
|
//# sourceMappingURL=context-file.d.ts.map
|
|
@@ -11,7 +11,8 @@ const PLATFORM_CONTEXT_FILES = {
|
|
|
11
11
|
cursor: "AGENTS.md",
|
|
12
12
|
codex: "AGENTS.md",
|
|
13
13
|
"github-copilot": "AGENTS.md",
|
|
14
|
-
|
|
14
|
+
// Antigravity 2.0 retired the GEMINI.md/.gemini conventions for AGENTS.md
|
|
15
|
+
antigravity: "AGENTS.md",
|
|
15
16
|
};
|
|
16
17
|
/**
|
|
17
18
|
* Get the appropriate context file name for a platform
|
|
@@ -20,10 +21,14 @@ export function getContextFileName(platform) {
|
|
|
20
21
|
return PLATFORM_CONTEXT_FILES[platform] ?? null;
|
|
21
22
|
}
|
|
22
23
|
/**
|
|
23
|
-
* Find the git root directory
|
|
24
|
+
* Find the git root directory.
|
|
25
|
+
*
|
|
26
|
+
* CONTEXT-FILE-6: in a worktree, .git is a FILE pointing at the main
|
|
27
|
+
* checkout — matching directories only would walk past it and resolve to
|
|
28
|
+
* the main checkout's root, landing the context file in the wrong tree.
|
|
24
29
|
*/
|
|
25
30
|
export async function findGitRoot() {
|
|
26
|
-
const gitDir = await findUp(".git", { type: "
|
|
31
|
+
const gitDir = await findUp(".git", { type: "both" });
|
|
27
32
|
return gitDir ? dirname(gitDir) : null;
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
@@ -95,8 +100,8 @@ export async function getContextFilePath(platform) {
|
|
|
95
100
|
* Build a user-facing message explaining that context file installation
|
|
96
101
|
* was skipped because the current directory is not inside a git repository.
|
|
97
102
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
103
|
+
* Writing the platform's context file is the whole of setup now, so a missing
|
|
104
|
+
* git root means nothing was installed.
|
|
100
105
|
*/
|
|
101
106
|
export function buildNoGitRepoMessage(fileName) {
|
|
102
107
|
return [
|
|
@@ -106,8 +111,6 @@ export function buildNoGitRepoMessage(fileName) {
|
|
|
106
111
|
" To finish setup, either:",
|
|
107
112
|
" • run `git init` here, then re-run `dotreq ai-setup`, or",
|
|
108
113
|
" • `cd` into an existing project directory and run `dotreq ai-setup` there.",
|
|
109
|
-
"",
|
|
110
|
-
" Note: the MCP server itself was configured successfully — only the context file step was skipped.",
|
|
111
114
|
].join("\n");
|
|
112
115
|
}
|
|
113
116
|
//# sourceMappingURL=context-file.js.map
|
package/dist/utils/env.js
CHANGED
|
@@ -23,7 +23,7 @@ export function loadEnvFile(cwd = process.cwd(), force = false) {
|
|
|
23
23
|
const match = line.match(/^([^=]+)=(.*)$/);
|
|
24
24
|
if (match) {
|
|
25
25
|
const key = match[1].trim();
|
|
26
|
-
const value = match[2].trim();
|
|
26
|
+
const value = stripSurroundingQuotes(match[2].trim());
|
|
27
27
|
// Set if force=true, or if not already in process.env
|
|
28
28
|
if (force || !process.env[key]) {
|
|
29
29
|
process.env[key] = value;
|
|
@@ -36,4 +36,20 @@ export function loadEnvFile(cwd = process.cwd(), force = false) {
|
|
|
36
36
|
// The commands will error appropriately if env vars are missing
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Strip a single matching pair of surrounding single or double quotes from a
|
|
41
|
+
* value, matching dotenv behavior. Credentials written in standard
|
|
42
|
+
* dotenv/Next.js style (KEY="value") should not carry the quote characters
|
|
43
|
+
* into process.env. Mismatched or interior quotes are left untouched.
|
|
44
|
+
*/
|
|
45
|
+
function stripSurroundingQuotes(value) {
|
|
46
|
+
if (value.length >= 2) {
|
|
47
|
+
const first = value[0];
|
|
48
|
+
const last = value[value.length - 1];
|
|
49
|
+
if ((first === '"' || first === "'") && first === last) {
|
|
50
|
+
return value.slice(1, -1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
39
55
|
//# sourceMappingURL=env.js.map
|
package/dist/utils/oauth-flow.js
CHANGED
|
@@ -32,6 +32,14 @@ export async function executeOAuthFlow() {
|
|
|
32
32
|
port: 3010,
|
|
33
33
|
timeoutMs: 120000,
|
|
34
34
|
});
|
|
35
|
+
// Attach a no-op rejection handler immediately. startCallbackServer can
|
|
36
|
+
// reject synchronously-adjacent to creation (e.g. listen EADDRINUSE when the
|
|
37
|
+
// callback port is busy), but the real handler isn't awaited until after the
|
|
38
|
+
// browser launch below. Without this, an early rejection has no handler and
|
|
39
|
+
// Node escalates it to a fatal unhandledRejection, bypassing wrapCommand's
|
|
40
|
+
// clean CLI error formatting. The awaited `callbackPromise` below still sees
|
|
41
|
+
// the rejection and surfaces it as a normal thrown error.
|
|
42
|
+
callbackPromise.catch(() => { });
|
|
35
43
|
// Build authorization URL
|
|
36
44
|
const authUrl = buildAuthorizationUrl({
|
|
37
45
|
clientId: WORKOS_CLIENT_ID,
|
|
@@ -41,6 +41,7 @@ export interface ProjectInfo {
|
|
|
41
41
|
* Returns undefined if either is missing.
|
|
42
42
|
*/
|
|
43
43
|
export declare function getCredentialsFromEnv(): ProjectSettings | undefined;
|
|
44
|
+
export declare function setAuthFromEnv(enabled: boolean): void;
|
|
44
45
|
/**
|
|
45
46
|
* Find the project root by walking up from startDir looking for .requirements/ folder
|
|
46
47
|
* Returns the directory containing .requirements/, or undefined if not found
|
|
@@ -54,6 +55,10 @@ export declare function readProjectSettings(projectRoot: string): ProjectSetting
|
|
|
54
55
|
/**
|
|
55
56
|
* Write project settings to .requirements/project-settings.json
|
|
56
57
|
* Creates .requirements/ directory if it doesn't exist
|
|
58
|
+
*
|
|
59
|
+
* SETTINGS-2.3/2.4: fields being written replace the stored ones, but any
|
|
60
|
+
* other existing settings (defaultURL, browserTest, ...) survive — re-linking
|
|
61
|
+
* with fresh credentials must not erase the rest of the file.
|
|
57
62
|
*/
|
|
58
63
|
export declare function writeProjectSettings(projectRoot: string, settings: ProjectSettings): void;
|
|
59
64
|
/**
|
|
@@ -18,6 +18,19 @@ export function getCredentialsFromEnv() {
|
|
|
18
18
|
}
|
|
19
19
|
return undefined;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* AUTHZ-6: explicit CI/CD credential injection. Set by the global
|
|
23
|
+
* --auth-from-env flag (cli.ts preAction hook); when enabled,
|
|
24
|
+
* getProjectCredentials reads DOTREQ_PROJECT_ID / DOTREQ_PROJECT_SECRET
|
|
25
|
+
* instead of file-based discovery. Without the flag those variables are
|
|
26
|
+
* ignored entirely — the explicit opt-in prevents credential conflicts
|
|
27
|
+
* between local and CI environments. (Previously the retired MCP server's
|
|
28
|
+
* --auth-from-env; ported to the CLI with identical semantics.)
|
|
29
|
+
*/
|
|
30
|
+
let authFromEnv = false;
|
|
31
|
+
export function setAuthFromEnv(enabled) {
|
|
32
|
+
authFromEnv = enabled;
|
|
33
|
+
}
|
|
21
34
|
/**
|
|
22
35
|
* Find the project root by walking up from startDir looking for .requirements/ folder
|
|
23
36
|
* Returns the directory containing .requirements/, or undefined if not found
|
|
@@ -106,6 +119,10 @@ export function readProjectSettings(projectRoot) {
|
|
|
106
119
|
/**
|
|
107
120
|
* Write project settings to .requirements/project-settings.json
|
|
108
121
|
* Creates .requirements/ directory if it doesn't exist
|
|
122
|
+
*
|
|
123
|
+
* SETTINGS-2.3/2.4: fields being written replace the stored ones, but any
|
|
124
|
+
* other existing settings (defaultURL, browserTest, ...) survive — re-linking
|
|
125
|
+
* with fresh credentials must not erase the rest of the file.
|
|
109
126
|
*/
|
|
110
127
|
export function writeProjectSettings(projectRoot, settings) {
|
|
111
128
|
const requirementsDir = path.join(projectRoot, REQUIREMENTS_DIR);
|
|
@@ -114,7 +131,16 @@ export function writeProjectSettings(projectRoot, settings) {
|
|
|
114
131
|
if (!fs.existsSync(requirementsDir)) {
|
|
115
132
|
fs.mkdirSync(requirementsDir, { recursive: true });
|
|
116
133
|
}
|
|
117
|
-
|
|
134
|
+
let existing;
|
|
135
|
+
try {
|
|
136
|
+
existing = readProjectSettings(projectRoot);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Unreadable or invalid existing file: nothing preservable, write fresh
|
|
140
|
+
existing = undefined;
|
|
141
|
+
}
|
|
142
|
+
const merged = existing ? { ...existing, ...settings } : settings;
|
|
143
|
+
const content = `${JSON.stringify(merged, null, 2)}\n`;
|
|
118
144
|
fs.writeFileSync(settingsPath, content, "utf-8");
|
|
119
145
|
}
|
|
120
146
|
/**
|
|
@@ -144,6 +170,15 @@ export function getProjectInfo(startDir = process.cwd()) {
|
|
|
144
170
|
* Get project credentials, throwing helpful error if not available
|
|
145
171
|
*/
|
|
146
172
|
export function getProjectCredentials(startDir = process.cwd()) {
|
|
173
|
+
// AUTHZ-6.0 / 6.1: with the explicit flag, env credentials replace
|
|
174
|
+
// file-based discovery entirely
|
|
175
|
+
if (authFromEnv) {
|
|
176
|
+
const envCredentials = getCredentialsFromEnv();
|
|
177
|
+
if (!envCredentials) {
|
|
178
|
+
throw new Error("--auth-from-env requires both DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET environment variables to be set.");
|
|
179
|
+
}
|
|
180
|
+
return envCredentials;
|
|
181
|
+
}
|
|
147
182
|
const info = getProjectInfo(startDir);
|
|
148
183
|
if (!info) {
|
|
149
184
|
throw new Error('No dotrequirements project found. Run "dotrequirements init" to create one.');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popoverai/dotrequirements",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Requirements tracking CLI
|
|
3
|
+
"version": "0.27.0",
|
|
4
|
+
"description": "Requirements tracking CLI and test harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"dotrequirements": "./dist/cli.js",
|
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
".": "./dist/cli.js",
|
|
12
12
|
"./test": "./dist/harness/index.js",
|
|
13
13
|
"./schema": "./dist/schema/index.js",
|
|
14
|
-
"./schema/browser": "./dist/schema/browser.js"
|
|
15
|
-
"./mcp": "./dist/mcp/index.js"
|
|
14
|
+
"./schema/browser": "./dist/schema/browser.js"
|
|
16
15
|
},
|
|
17
16
|
"publishConfig": {
|
|
18
17
|
"access": "public"
|
|
@@ -34,7 +33,6 @@
|
|
|
34
33
|
"test-coverage",
|
|
35
34
|
"bdd",
|
|
36
35
|
"tdd",
|
|
37
|
-
"mcp",
|
|
38
36
|
"ai-assistant"
|
|
39
37
|
],
|
|
40
38
|
"author": "Will Raymer @Popover",
|