@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
|
@@ -20,27 +20,27 @@ export async function testsForCommand(requirementsFile) {
|
|
|
20
20
|
}
|
|
21
21
|
const rootIds = new Set(fileRequirements.map((req) => req.rootId));
|
|
22
22
|
const allTestRefs = await findAllTestReferences(workspaceRoot);
|
|
23
|
-
// Build coverage map
|
|
23
|
+
// Build coverage map keyed by ROOT id. A reference may be a bare root
|
|
24
|
+
// (`AUTH-1`), an index child (`AUTH-1.0`), or a label path (`AUTH-1.given`) —
|
|
25
|
+
// and every string arg of a multi-arg `requirement("A", "B")` call is its own
|
|
26
|
+
// ref. Normalizing each ref to its root id (the first dotted segment) lets a
|
|
27
|
+
// covered descendant count toward its root requirement.
|
|
24
28
|
const coverageMap = new Map();
|
|
25
29
|
for (const ref of allTestRefs) {
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
const dotIndex = ref.requirementId.indexOf(".");
|
|
31
|
+
const refRootId = dotIndex > 0
|
|
32
|
+
? ref.requirementId.substring(0, dotIndex)
|
|
33
|
+
: ref.requirementId;
|
|
34
|
+
if (!coverageMap.has(refRootId)) {
|
|
35
|
+
coverageMap.set(refRootId, []);
|
|
28
36
|
}
|
|
29
|
-
coverageMap.get(
|
|
37
|
+
coverageMap.get(refRootId).push(ref);
|
|
30
38
|
}
|
|
31
39
|
// CLI-FIND-MAP-2.0: group by coverage status
|
|
32
40
|
const covered = [];
|
|
33
41
|
const notCovered = [];
|
|
34
42
|
for (const rootId of rootIds) {
|
|
35
|
-
const
|
|
36
|
-
const allIds = reqAndChildren.map((r) => r.id);
|
|
37
|
-
const testsForThisReq = [];
|
|
38
|
-
for (const id of allIds) {
|
|
39
|
-
const refs = coverageMap.get(id);
|
|
40
|
-
if (refs) {
|
|
41
|
-
testsForThisReq.push(...refs);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
43
|
+
const testsForThisReq = coverageMap.get(rootId) ?? [];
|
|
44
44
|
if (testsForThisReq.length > 0) {
|
|
45
45
|
covered.push({ id: rootId, tests: testsForThisReq });
|
|
46
46
|
}
|
|
@@ -1,24 +1,18 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
1
|
import * as path from "node:path";
|
|
2
|
+
import { findRequirementsFiles } from "../requirements/index.js";
|
|
3
3
|
import { parseRequirementsFromFile, } from "../schema/index.js";
|
|
4
4
|
export async function validateCommand(options) {
|
|
5
5
|
console.log("Parsing and validating requirements...");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
process.exit(1);
|
|
11
|
-
}
|
|
12
|
-
// Get files to test
|
|
6
|
+
// Validation parses the file in front of it — it never requires a
|
|
7
|
+
// .requirements/ directory (VALIDATE-1.0/.1). The no-flag path uses the
|
|
8
|
+
// shared recursive discovery so colocated files at any level and files in
|
|
9
|
+
// subdirectories of .requirements/ are all found (VALIDATE-2.0).
|
|
13
10
|
let files;
|
|
14
11
|
if (options.file) {
|
|
15
12
|
files = [options.file];
|
|
16
13
|
}
|
|
17
14
|
else {
|
|
18
|
-
files =
|
|
19
|
-
.readdirSync(requirementsDir)
|
|
20
|
-
.filter((f) => f.endsWith(".requirements.md"))
|
|
21
|
-
.map((f) => path.join(requirementsDir, f));
|
|
15
|
+
files = await findRequirementsFiles(process.cwd());
|
|
22
16
|
}
|
|
23
17
|
if (files.length === 0) {
|
|
24
18
|
console.log("No Markdown requirements files found to test.");
|
|
@@ -40,8 +34,14 @@ export async function validateCommand(options) {
|
|
|
40
34
|
}
|
|
41
35
|
totalRequirements += count;
|
|
42
36
|
console.log(` ✓ Valid structure (${count} requirement(s))`);
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
// Version/sync metadata is optional (hand-authored files have neither);
|
|
38
|
+
// only report fields that are present (VALIDATE-3.0/.1)
|
|
39
|
+
if (metadata.version !== undefined) {
|
|
40
|
+
console.log(` ✓ Version: ${metadata.version}`);
|
|
41
|
+
}
|
|
42
|
+
if (metadata.pulledAt !== undefined) {
|
|
43
|
+
console.log(` ✓ Last sync: ${metadata.pulledAt}`);
|
|
44
|
+
}
|
|
45
45
|
passCount++;
|
|
46
46
|
}
|
|
47
47
|
catch (error) {
|
package/dist/convex.d.ts
CHANGED
|
@@ -55,11 +55,9 @@ export declare const api: {
|
|
|
55
55
|
};
|
|
56
56
|
mutations: {
|
|
57
57
|
createInvite: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
58
|
+
acceptInvite: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
58
59
|
};
|
|
59
60
|
};
|
|
60
|
-
polar: {
|
|
61
|
-
acceptTeamInvite: import("convex/server").FunctionReference<"action", "public", any, any, string | undefined>;
|
|
62
|
-
};
|
|
63
61
|
projectSecrets: {
|
|
64
62
|
queries: {
|
|
65
63
|
getOwnSecret: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
package/dist/convex.js
CHANGED
|
@@ -61,11 +61,11 @@ export const api = {
|
|
|
61
61
|
},
|
|
62
62
|
mutations: {
|
|
63
63
|
createInvite: mutation("teamInvites/mutations:createInvite"),
|
|
64
|
+
// Joins the team and reconciles paid-team seats via a scheduled sync
|
|
65
|
+
// (POLAR-SEATS-3). Replaced the deleted polar:acceptTeamInvite action.
|
|
66
|
+
acceptInvite: mutation("teamInvites/mutations:acceptInvite"),
|
|
64
67
|
},
|
|
65
68
|
},
|
|
66
|
-
polar: {
|
|
67
|
-
acceptTeamInvite: action("polar:acceptTeamInvite"),
|
|
68
|
-
},
|
|
69
69
|
projectSecrets: {
|
|
70
70
|
queries: {
|
|
71
71
|
getOwnSecret: query("projectSecrets/queries:getOwnSecret"),
|
package/dist/harness/cache.d.ts
CHANGED
|
@@ -36,6 +36,18 @@ export interface LookupCache {
|
|
|
36
36
|
isAlias?: boolean;
|
|
37
37
|
}>;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Staleness metadata for the lookup cache, stored in a sibling file.
|
|
41
|
+
*
|
|
42
|
+
* lookup.json's shape is a cross-language contract: the shipped Python/Java
|
|
43
|
+
* helpers parse it with minimal JSON parsers (MLANG-6/7), so new fields —
|
|
44
|
+
* especially arrays — must not be added to it. Deletion detection
|
|
45
|
+
* (HARNESS-REQUIREMENT-4) needs the source-file set, so that lives here.
|
|
46
|
+
*/
|
|
47
|
+
export interface LookupCacheMeta {
|
|
48
|
+
/** Project-relative paths of the source files the cache was built from. */
|
|
49
|
+
sourceFiles: string[];
|
|
50
|
+
}
|
|
39
51
|
/**
|
|
40
52
|
* Structure of the coverage.json cache (for cloud deduplication)
|
|
41
53
|
*/
|
|
@@ -73,10 +85,14 @@ export declare function getCacheDir(requirementsDir: string, create?: boolean):
|
|
|
73
85
|
*/
|
|
74
86
|
export declare function writeLookupCache(requirementsDir: string, requirements: RequirementNode[]): void;
|
|
75
87
|
/**
|
|
76
|
-
* Check if
|
|
77
|
-
*
|
|
88
|
+
* Check if the lookup cache is stale and should be invalidated.
|
|
89
|
+
*
|
|
90
|
+
* A cache is stale when any source file is newer than the cache file, or —
|
|
91
|
+
* when the cache's recorded source-file set is provided — when the current
|
|
92
|
+
* set of source files differs from the recorded one (HARNESS-REQUIREMENT-4:
|
|
93
|
+
* deletions don't touch surviving files' mtimes, so the set must be compared).
|
|
78
94
|
*/
|
|
79
|
-
export declare function isCacheStale(requirementsDir: string, cacheMtime: number): boolean;
|
|
95
|
+
export declare function isCacheStale(requirementsDir: string, cacheMtime: number, cachedSourceFiles?: string[]): boolean;
|
|
80
96
|
/**
|
|
81
97
|
* Read the lookup cache, returning null if not found, invalid, or stale.
|
|
82
98
|
* Cache is considered stale if any .requirements.md file is newer than the cache.
|
package/dist/harness/cache.js
CHANGED
|
@@ -12,6 +12,7 @@ import { findRequirementsFilesSync } from "../requirements/index.js";
|
|
|
12
12
|
// Cache directory structure
|
|
13
13
|
const CACHE_DIR = ".cache";
|
|
14
14
|
const LOOKUP_FILE = "lookup.json";
|
|
15
|
+
const LOOKUP_META_FILE = "lookup-meta.json";
|
|
15
16
|
const TRACKING_FILE = "tracking.jsonl";
|
|
16
17
|
const COVERAGE_FILE = "coverage.json";
|
|
17
18
|
// Test run ID file (outside cache, in .requirements/)
|
|
@@ -58,6 +59,10 @@ export function getCacheDir(requirementsDir, create = false) {
|
|
|
58
59
|
export function writeLookupCache(requirementsDir, requirements) {
|
|
59
60
|
const cacheDir = getCacheDir(requirementsDir, true);
|
|
60
61
|
const lookupPath = path.join(cacheDir, LOOKUP_FILE);
|
|
62
|
+
const projectRoot = path.dirname(requirementsDir);
|
|
63
|
+
const sourceFiles = findRequirementsFilesSync(projectRoot)
|
|
64
|
+
.map((file) => path.relative(projectRoot, file))
|
|
65
|
+
.sort();
|
|
61
66
|
const lookup = {
|
|
62
67
|
generatedAt: new Date().toISOString(),
|
|
63
68
|
requirements: {},
|
|
@@ -96,14 +101,30 @@ export function writeLookupCache(requirementsDir, requirements) {
|
|
|
96
101
|
flatten(req, null);
|
|
97
102
|
}
|
|
98
103
|
fs.writeFileSync(lookupPath, JSON.stringify(lookup, null, 2));
|
|
104
|
+
const meta = { sourceFiles };
|
|
105
|
+
fs.writeFileSync(path.join(cacheDir, LOOKUP_META_FILE), JSON.stringify(meta, null, 2));
|
|
99
106
|
}
|
|
100
107
|
/**
|
|
101
|
-
* Check if
|
|
102
|
-
*
|
|
108
|
+
* Check if the lookup cache is stale and should be invalidated.
|
|
109
|
+
*
|
|
110
|
+
* A cache is stale when any source file is newer than the cache file, or —
|
|
111
|
+
* when the cache's recorded source-file set is provided — when the current
|
|
112
|
+
* set of source files differs from the recorded one (HARNESS-REQUIREMENT-4:
|
|
113
|
+
* deletions don't touch surviving files' mtimes, so the set must be compared).
|
|
103
114
|
*/
|
|
104
|
-
export function isCacheStale(requirementsDir, cacheMtime) {
|
|
115
|
+
export function isCacheStale(requirementsDir, cacheMtime, cachedSourceFiles) {
|
|
105
116
|
const projectRoot = path.dirname(requirementsDir);
|
|
106
117
|
const sourceFiles = findRequirementsFilesSync(projectRoot);
|
|
118
|
+
if (cachedSourceFiles) {
|
|
119
|
+
const current = sourceFiles
|
|
120
|
+
.map((file) => path.relative(projectRoot, file))
|
|
121
|
+
.sort();
|
|
122
|
+
const cached = [...cachedSourceFiles].sort();
|
|
123
|
+
if (current.length !== cached.length ||
|
|
124
|
+
current.some((file, i) => file !== cached[i])) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
107
128
|
for (const file of sourceFiles) {
|
|
108
129
|
try {
|
|
109
130
|
const stat = fs.statSync(file);
|
|
@@ -127,23 +148,28 @@ export function readLookupCache(requirementsDir) {
|
|
|
127
148
|
if (!fs.existsSync(lookupPath)) {
|
|
128
149
|
return null;
|
|
129
150
|
}
|
|
130
|
-
|
|
151
|
+
let cache;
|
|
152
|
+
let meta;
|
|
153
|
+
let cacheMtime;
|
|
131
154
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
155
|
+
cacheMtime = fs.statSync(lookupPath).mtimeMs;
|
|
156
|
+
cache = JSON.parse(fs.readFileSync(lookupPath, "utf-8"));
|
|
157
|
+
meta = JSON.parse(fs.readFileSync(path.join(cacheDir, LOOKUP_META_FILE), "utf-8"));
|
|
136
158
|
}
|
|
137
159
|
catch {
|
|
160
|
+
// A cache without its meta file (legacy layout) can't detect deletions —
|
|
161
|
+
// treat it as stale and let prepare() rebuild both files.
|
|
138
162
|
return null;
|
|
139
163
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return JSON.parse(content);
|
|
164
|
+
if (!Array.isArray(meta.sourceFiles)) {
|
|
165
|
+
return null;
|
|
143
166
|
}
|
|
144
|
-
|
|
167
|
+
// Stale if the source-file set changed (deletions included) or any source
|
|
168
|
+
// file is newer than the cache
|
|
169
|
+
if (isCacheStale(requirementsDir, cacheMtime, meta.sourceFiles)) {
|
|
145
170
|
return null;
|
|
146
171
|
}
|
|
172
|
+
return cache;
|
|
147
173
|
}
|
|
148
174
|
/**
|
|
149
175
|
* Generate and store a unique test run ID
|
package/dist/harness/finalize.js
CHANGED
|
@@ -13,6 +13,7 @@ import { execSync } from "node:child_process";
|
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
14
|
import { getProjectInfo } from "../utils/project-settings.js";
|
|
15
15
|
import { cleanupTestRunId, deleteTrackingFile, findProjectRoot, findRequirementsDir, getTestRunId, needsReporting, readCoverageCache, readLookupCache, readTrackingEntries, updateCoverageCache, } from "./cache.js";
|
|
16
|
+
import { toProjectRelativePath } from "./tracking.js";
|
|
16
17
|
/**
|
|
17
18
|
* Aggregate tracking entries by requirement key
|
|
18
19
|
*/
|
|
@@ -181,7 +182,7 @@ async function reportToConvex(projectRoot, testRunId, aggregated, showCloudStatu
|
|
|
181
182
|
// Use the first access location
|
|
182
183
|
const firstEntry = entries[0];
|
|
183
184
|
const location = firstEntry.callerLocation;
|
|
184
|
-
// Parse "
|
|
185
|
+
// Parse "path/to/file.ts:42" format
|
|
185
186
|
let testFile;
|
|
186
187
|
let testLine;
|
|
187
188
|
if (location !== "unknown") {
|
|
@@ -193,6 +194,12 @@ async function reportToConvex(projectRoot, testRunId, aggregated, showCloudStatu
|
|
|
193
194
|
else {
|
|
194
195
|
testFile = location;
|
|
195
196
|
}
|
|
197
|
+
// COVERAGE-CONTEXT-2: locations inside the project are recorded
|
|
198
|
+
// project-relative; normalize any absolute path that falls inside
|
|
199
|
+
// the project so records and git blame use a stable relative path.
|
|
200
|
+
if (testFile) {
|
|
201
|
+
testFile = toProjectRelativePath(testFile, projectRoot);
|
|
202
|
+
}
|
|
196
203
|
}
|
|
197
204
|
// COVERAGE-CONTEXT-2: git blame on the requirement() call line
|
|
198
205
|
let user;
|
|
@@ -337,6 +344,23 @@ export async function finalize(options = {}) {
|
|
|
337
344
|
// We resolve those to their canonical numeric key (e.g. "AUTH-LOGIN-1.0") so that
|
|
338
345
|
// coverage counting, display, and cloud reporting all use consistent keys.
|
|
339
346
|
if (lookup) {
|
|
347
|
+
// HARNESS-RESOLVE-2.1: label paths resolve case-insensitively, but
|
|
348
|
+
// tracking preserves the test's original casing while the cache stores
|
|
349
|
+
// alias keys lowercased. Fold unmatched keys onto their lowercased-label
|
|
350
|
+
// alias first, so a ref like "REQ-1.Given" isn't dropped as foreign below.
|
|
351
|
+
for (const [key, trackingEntries] of Array.from(aggregated.entries())) {
|
|
352
|
+
if (lookup.requirements[key])
|
|
353
|
+
continue;
|
|
354
|
+
const m = /^([A-Z][A-Z0-9_-]*-\d+)(\..+)$/.exec(key);
|
|
355
|
+
if (!m)
|
|
356
|
+
continue;
|
|
357
|
+
const folded = m[1] + m[2].toLowerCase();
|
|
358
|
+
if (folded !== key && lookup.requirements[folded]) {
|
|
359
|
+
const existing = aggregated.get(folded) || [];
|
|
360
|
+
aggregated.set(folded, [...existing, ...trackingEntries]);
|
|
361
|
+
aggregated.delete(key);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
340
364
|
for (const [key, trackingEntries] of Array.from(aggregated.entries())) {
|
|
341
365
|
const entry = lookup.requirements[key];
|
|
342
366
|
if (entry?.isAlias) {
|
|
@@ -347,6 +371,14 @@ export async function finalize(options = {}) {
|
|
|
347
371
|
aggregated.delete(key);
|
|
348
372
|
}
|
|
349
373
|
}
|
|
374
|
+
// HARNESS-REQUIREMENT-6.1: a project's coverage report contains only
|
|
375
|
+
// requirement keys that belong to that project. Tracking entries for
|
|
376
|
+
// foreign keys (e.g. resolved against another project) are dropped.
|
|
377
|
+
for (const key of Array.from(aggregated.keys())) {
|
|
378
|
+
if (!lookup.requirements[key]) {
|
|
379
|
+
aggregated.delete(key);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
350
382
|
}
|
|
351
383
|
const testedKeys = Array.from(aggregated.keys());
|
|
352
384
|
const totalRequirements = lookup
|
package/dist/harness/index.js
CHANGED
|
@@ -70,21 +70,28 @@ export function requirement(...args) {
|
|
|
70
70
|
if (options.projectRoot) {
|
|
71
71
|
loadRequirements({ projectRoot: options.projectRoot });
|
|
72
72
|
}
|
|
73
|
-
//
|
|
74
|
-
|
|
73
|
+
// HARNESS-REQUIREMENT-2: Invalid requirement paths fail the individual test.
|
|
74
|
+
// Every ref is validated — a typo in any ref throws, naming the missing path.
|
|
75
|
+
// Validation happens BEFORE tracking so that refs that fail lookup are never
|
|
76
|
+
// recorded as covered (HARNESS-REQUIREMENT-3).
|
|
77
|
+
const resolved = requirementRefs.map((ref) => {
|
|
78
|
+
const found = getRequirement(ref);
|
|
79
|
+
if (!found) {
|
|
80
|
+
throw new Error(`Requirement ${ref} not found`);
|
|
81
|
+
}
|
|
82
|
+
return found;
|
|
83
|
+
});
|
|
84
|
+
// Ensure test run is initialized against the resolving project
|
|
85
|
+
// (HARNESS-REQUIREMENT-6: explicit projectRoot wins over the enclosing one)
|
|
86
|
+
ensureTestRun(options.projectRoot);
|
|
75
87
|
// Track all requirements for coverage at the full path level
|
|
76
88
|
// e.g., 'REQ-123.given' is tracked as 'REQ-123.given', not just 'REQ-123'
|
|
77
89
|
for (const ref of requirementRefs) {
|
|
78
|
-
trackRequirement(ref);
|
|
90
|
+
trackRequirement(ref, options.projectRoot);
|
|
79
91
|
}
|
|
80
92
|
// Save after tracking all requirements (no-op in new JSONL approach)
|
|
81
93
|
saveTrackingData();
|
|
82
|
-
|
|
83
|
-
const firstRef = requirementRefs[0];
|
|
84
|
-
const req = getRequirement(firstRef);
|
|
85
|
-
if (!req) {
|
|
86
|
-
throw new Error(`Requirement ${firstRef} not found`);
|
|
87
|
-
}
|
|
94
|
+
const req = resolved[0];
|
|
88
95
|
// HARNESS-REQUIREMENT-1: Format output as human-readable string
|
|
89
96
|
// Root requirements use 'requirementHeader' label which should not be shown in output
|
|
90
97
|
if (!req.label || req.label === "requirementHeader") {
|
|
@@ -151,6 +151,18 @@ export function getRequirement(reqId) {
|
|
|
151
151
|
if (direct) {
|
|
152
152
|
return direct;
|
|
153
153
|
}
|
|
154
|
+
// HARNESS-RESOLVE-2.1: label matching is case-insensitive. The lookup
|
|
155
|
+
// cache stores label-path aliases lowercased (e.g. "REQ-123.given"), and
|
|
156
|
+
// in cache mode nodes carry no children for the fallback resolver to walk,
|
|
157
|
+
// so retry with the label segments lowercased (root key stays as written).
|
|
158
|
+
const firstDot = reqId.indexOf(".");
|
|
159
|
+
if (firstDot !== -1) {
|
|
160
|
+
const normalized = reqId.slice(0, firstDot) + reqId.slice(firstDot).toLowerCase();
|
|
161
|
+
const aliased = loadedRequirements.get(normalized);
|
|
162
|
+
if (aliased) {
|
|
163
|
+
return aliased;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
154
166
|
// If not found, try resolving label-based paths
|
|
155
167
|
// resolveRequirementPath expects an array of root requirements and the full path
|
|
156
168
|
const allTrees = Array.from(requirementTrees.values());
|
|
@@ -11,8 +11,21 @@ export interface TrackedRequirement {
|
|
|
11
11
|
fieldsAccessed: string[];
|
|
12
12
|
accessedAt: string[];
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Initialize (or reuse) a test run.
|
|
16
|
+
*
|
|
17
|
+
* HARNESS-REQUIREMENT-6.0: when an explicit projectRoot is provided, the run
|
|
18
|
+
* is keyed to that project's .requirements/ so tracking writes land in the
|
|
19
|
+
* resolving project's cache, not the enclosing project's.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensureTestRun(projectRoot?: string): string;
|
|
15
22
|
export declare function getCallerLocation(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Normalize an absolute path inside the project to a project-relative one
|
|
25
|
+
* (COVERAGE-CONTEXT-2: stable, blame-able from the project root). Paths that
|
|
26
|
+
* are already relative or fall outside the project are returned unchanged.
|
|
27
|
+
*/
|
|
28
|
+
export declare function toProjectRelativePath(filePath: string, projectRoot: string): string;
|
|
16
29
|
/**
|
|
17
30
|
* Track a requirement access.
|
|
18
31
|
*
|
|
@@ -21,8 +34,10 @@ export declare function getCallerLocation(): string;
|
|
|
21
34
|
* HARNESS-REQUIREMENT-3.2: Works correctly when tests run in parallel processes
|
|
22
35
|
*
|
|
23
36
|
* @param reqId - Full requirement path (e.g., 'REQ-123.given' or 'REQ-123.0.1')
|
|
37
|
+
* @param projectRoot - Explicit project root; tracking is written to this
|
|
38
|
+
* project's cache when provided (HARNESS-REQUIREMENT-6.0)
|
|
24
39
|
*/
|
|
25
|
-
export declare function trackRequirement(reqId: string): void;
|
|
40
|
+
export declare function trackRequirement(reqId: string, projectRoot?: string): void;
|
|
26
41
|
/**
|
|
27
42
|
* Save current tracking data.
|
|
28
43
|
*
|
package/dist/harness/tracking.js
CHANGED
|
@@ -7,13 +7,33 @@
|
|
|
7
7
|
* HARNESS-REQUIREMENT-3: Each requirement() call is recorded for coverage reporting
|
|
8
8
|
*/
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
10
11
|
import { appendTrackingEntry, clearTrackingFile, findProjectRoot, findRequirementsDir, getCacheDir, getTestRunId, initTestRunId, } from "./cache.js";
|
|
11
12
|
const trackedRequirements = new Map();
|
|
12
13
|
// Test run tracking
|
|
13
14
|
let testRunId = null;
|
|
14
15
|
let requirementsDirCached = null;
|
|
15
|
-
//
|
|
16
|
-
|
|
16
|
+
// Per-project run ids for explicit projectRoot tracking (HARNESS-REQUIREMENT-6)
|
|
17
|
+
const runIdsByRequirementsDir = new Map();
|
|
18
|
+
/**
|
|
19
|
+
* Initialize (or reuse) a test run.
|
|
20
|
+
*
|
|
21
|
+
* HARNESS-REQUIREMENT-6.0: when an explicit projectRoot is provided, the run
|
|
22
|
+
* is keyed to that project's .requirements/ so tracking writes land in the
|
|
23
|
+
* resolving project's cache, not the enclosing project's.
|
|
24
|
+
*/
|
|
25
|
+
export function ensureTestRun(projectRoot) {
|
|
26
|
+
if (projectRoot) {
|
|
27
|
+
const requirementsDir = path.join(projectRoot, ".requirements");
|
|
28
|
+
let runId = runIdsByRequirementsDir.get(requirementsDir);
|
|
29
|
+
if (!runId) {
|
|
30
|
+
// Reuse a run id written by prepare() in that project, else mint one
|
|
31
|
+
runId = getTestRunId(requirementsDir) ?? `${Date.now()}-${process.pid}`;
|
|
32
|
+
runIdsByRequirementsDir.set(requirementsDir, runId);
|
|
33
|
+
getCacheDir(requirementsDir, true);
|
|
34
|
+
}
|
|
35
|
+
return runId;
|
|
36
|
+
}
|
|
17
37
|
if (!testRunId) {
|
|
18
38
|
// Priority 1: Use environment variable (cross-process persistence from globalSetup)
|
|
19
39
|
if (process.env.DOTREQUIREMENTS_PROJECT_ROOT) {
|
|
@@ -61,14 +81,62 @@ export function getCallerLocation() {
|
|
|
61
81
|
const regex2 = /at (.+):(\d+):(\d+)/;
|
|
62
82
|
const match = regex1.exec(frame) || regex2.exec(frame);
|
|
63
83
|
if (match) {
|
|
64
|
-
|
|
84
|
+
let filePath = match[1];
|
|
65
85
|
const lineNumber = match[2];
|
|
66
|
-
|
|
86
|
+
// ESM stack frames use file:// URLs
|
|
87
|
+
if (filePath.startsWith("file://")) {
|
|
88
|
+
try {
|
|
89
|
+
filePath = fileURLToPath(filePath);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Keep the raw frame path if the URL can't be converted
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// COVERAGE-CONTEXT-2: keep the full path (not just the basename) so
|
|
96
|
+
// finalize() can git-blame the call site for nested test files.
|
|
97
|
+
return `${filePath}:${lineNumber}`;
|
|
67
98
|
}
|
|
68
99
|
}
|
|
69
100
|
}
|
|
70
101
|
return "unknown";
|
|
71
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Convert an absolute caller location to a project-relative one when the
|
|
105
|
+
* caller file lives inside the project. Keeps locations stable and blame-able
|
|
106
|
+
* from the project root (COVERAGE-CONTEXT-2); callers outside the project
|
|
107
|
+
* stay absolute.
|
|
108
|
+
*/
|
|
109
|
+
function toProjectRelativeLocation(location, requirementsDir) {
|
|
110
|
+
if (!requirementsDir) {
|
|
111
|
+
return location;
|
|
112
|
+
}
|
|
113
|
+
const match = /^(.+):(\d+)$/.exec(location);
|
|
114
|
+
if (!match) {
|
|
115
|
+
return location;
|
|
116
|
+
}
|
|
117
|
+
const [, filePath, lineNumber] = match;
|
|
118
|
+
const projectRoot = path.dirname(requirementsDir);
|
|
119
|
+
const relative = toProjectRelativePath(filePath, projectRoot);
|
|
120
|
+
if (relative === filePath) {
|
|
121
|
+
return location;
|
|
122
|
+
}
|
|
123
|
+
return `${relative}:${lineNumber}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Normalize an absolute path inside the project to a project-relative one
|
|
127
|
+
* (COVERAGE-CONTEXT-2: stable, blame-able from the project root). Paths that
|
|
128
|
+
* are already relative or fall outside the project are returned unchanged.
|
|
129
|
+
*/
|
|
130
|
+
export function toProjectRelativePath(filePath, projectRoot) {
|
|
131
|
+
if (!path.isAbsolute(filePath)) {
|
|
132
|
+
return filePath;
|
|
133
|
+
}
|
|
134
|
+
const relative = path.relative(projectRoot, filePath);
|
|
135
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
136
|
+
return filePath;
|
|
137
|
+
}
|
|
138
|
+
return relative;
|
|
139
|
+
}
|
|
72
140
|
// Get caller's directory (full path) for finding .requirements
|
|
73
141
|
function getCallerDirectory() {
|
|
74
142
|
const error = new Error();
|
|
@@ -100,10 +168,15 @@ function getCallerDirectory() {
|
|
|
100
168
|
* HARNESS-REQUIREMENT-3.2: Works correctly when tests run in parallel processes
|
|
101
169
|
*
|
|
102
170
|
* @param reqId - Full requirement path (e.g., 'REQ-123.given' or 'REQ-123.0.1')
|
|
171
|
+
* @param projectRoot - Explicit project root; tracking is written to this
|
|
172
|
+
* project's cache when provided (HARNESS-REQUIREMENT-6.0)
|
|
103
173
|
*/
|
|
104
|
-
export function trackRequirement(reqId) {
|
|
105
|
-
const
|
|
106
|
-
const
|
|
174
|
+
export function trackRequirement(reqId, projectRoot) {
|
|
175
|
+
const runId = ensureTestRun(projectRoot);
|
|
176
|
+
const targetRequirementsDir = projectRoot
|
|
177
|
+
? path.join(projectRoot, ".requirements")
|
|
178
|
+
: requirementsDirCached;
|
|
179
|
+
const callerLocation = toProjectRelativeLocation(getCallerLocation(), targetRequirementsDir);
|
|
107
180
|
// Track in memory for backward compatibility
|
|
108
181
|
if (!trackedRequirements.has(reqId)) {
|
|
109
182
|
trackedRequirements.set(reqId, {
|
|
@@ -116,14 +189,14 @@ export function trackRequirement(reqId) {
|
|
|
116
189
|
tracked.accessedAt.push(callerLocation);
|
|
117
190
|
// Append to JSONL file for cross-process tracking
|
|
118
191
|
// HARNESS-REQUIREMENT-3.2: JSONL is append-only, safe for parallel workers
|
|
119
|
-
if (
|
|
192
|
+
if (targetRequirementsDir) {
|
|
120
193
|
const entry = {
|
|
121
194
|
requirementKey: reqId,
|
|
122
195
|
callerLocation,
|
|
123
196
|
timestamp: Date.now(),
|
|
124
197
|
testRunId: runId,
|
|
125
198
|
};
|
|
126
|
-
appendTrackingEntry(
|
|
199
|
+
appendTrackingEntry(targetRequirementsDir, entry);
|
|
127
200
|
}
|
|
128
201
|
}
|
|
129
202
|
/**
|
|
@@ -144,6 +217,7 @@ export function clearTracking() {
|
|
|
144
217
|
trackedRequirements.clear();
|
|
145
218
|
testRunId = null;
|
|
146
219
|
requirementsDirCached = null;
|
|
220
|
+
runIdsByRequirementsDir.clear();
|
|
147
221
|
}
|
|
148
222
|
/**
|
|
149
223
|
* Initialize a test run (called by globalSetup).
|
package/dist/push/core.d.ts
CHANGED
|
@@ -26,6 +26,14 @@ export interface ParsedFile {
|
|
|
26
26
|
markdownContent: string;
|
|
27
27
|
/** Requirement count for display */
|
|
28
28
|
requirementCount: number;
|
|
29
|
+
/**
|
|
30
|
+
* DOC-HEADER-14: the frontmatter exactly as the user wrote it, including
|
|
31
|
+
* keys the schema doesn't recognize — merged back on the post-push rewrite
|
|
32
|
+
* so those keys survive.
|
|
33
|
+
*/
|
|
34
|
+
rawFrontmatter?: Record<string, unknown>;
|
|
35
|
+
/** DOC-HEADER-11.4: true when defaultPrefix was inferred from the first requirement rather than read from frontmatter */
|
|
36
|
+
inferredDefaultPrefix?: boolean;
|
|
29
37
|
}
|
|
30
38
|
/**
|
|
31
39
|
* Cloud document metadata for conflict detection.
|
|
@@ -85,8 +93,10 @@ export declare const WEB_APP_URL = "https://app.dotrequirements.io";
|
|
|
85
93
|
export interface PushResult {
|
|
86
94
|
created: number;
|
|
87
95
|
updated: number;
|
|
96
|
+
/** filePath disambiguates when two pushed files share a basename */
|
|
88
97
|
errors: Array<{
|
|
89
98
|
fileName: string;
|
|
99
|
+
filePath: string;
|
|
90
100
|
error: string;
|
|
91
101
|
}>;
|
|
92
102
|
/** IMPORT-3: marker failures that must be surfaced to the user, per file */
|
|
@@ -100,6 +110,18 @@ export interface PushResult {
|
|
|
100
110
|
documentId: string;
|
|
101
111
|
url: string;
|
|
102
112
|
}>;
|
|
113
|
+
/**
|
|
114
|
+
* SYNC-FAIL-2: cloud save succeeded but the local file write-back failed.
|
|
115
|
+
* These documents are synced (counted in created/updated and listed in
|
|
116
|
+
* `synced`) — the warning carries what the user needs to re-link the file
|
|
117
|
+
* without creating a duplicate.
|
|
118
|
+
*/
|
|
119
|
+
writeBackWarnings: Array<{
|
|
120
|
+
fileName: string;
|
|
121
|
+
filePath: string;
|
|
122
|
+
documentId: string;
|
|
123
|
+
error: string;
|
|
124
|
+
}>;
|
|
103
125
|
}
|
|
104
126
|
/**
|
|
105
127
|
* Extract markdown content from a file, stripping YAML frontmatter.
|
|
@@ -107,11 +129,39 @@ export interface PushResult {
|
|
|
107
129
|
export declare function extractMarkdownContent(rawContent: string): string;
|
|
108
130
|
/**
|
|
109
131
|
* Parse files for push. Returns parsed files with metadata and content.
|
|
132
|
+
*
|
|
133
|
+
* Throws (via `parseRequirementsFromFile`) if any file is syntactically
|
|
134
|
+
* invalid. Callers that need one bad file not to abort the batch should parse
|
|
135
|
+
* files individually and collect failures (see the MCP push handler, #45).
|
|
110
136
|
*/
|
|
111
137
|
export declare function parseFilesForPush(filePaths: string[]): {
|
|
112
138
|
parsedFiles: ParsedFile[];
|
|
113
139
|
totalRequirements: number;
|
|
114
140
|
};
|
|
141
|
+
export interface ParseFailure {
|
|
142
|
+
filePath: string;
|
|
143
|
+
error: string;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* SYNC-FAIL-4.0: parse files one at a time so a single invalid file cannot
|
|
147
|
+
* abort the batch — its failure is collected per file while the valid files
|
|
148
|
+
* still parse. Shared by the CLI push command and the MCP push handler (#45)
|
|
149
|
+
* so their isolation semantics cannot drift.
|
|
150
|
+
*
|
|
151
|
+
* Note: cross-file checks that need the whole batch (e.g. SYNC-FAIL-3
|
|
152
|
+
* duplicate document IDs) are enforced downstream in dryRunPush, not here.
|
|
153
|
+
*/
|
|
154
|
+
export declare function parseFilesForPushIndividually(filePaths: string[]): {
|
|
155
|
+
parsedFiles: ParsedFile[];
|
|
156
|
+
totalRequirements: number;
|
|
157
|
+
parseFailures: ParseFailure[];
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* SYNC-FAIL-3: two ParsedFiles carrying the same document.id would both push
|
|
161
|
+
* as updates to one cloud document — last writer wins and the first spec is
|
|
162
|
+
* silently destroyed. Abort instead, naming both files and the shared id.
|
|
163
|
+
*/
|
|
164
|
+
export declare function assertNoDuplicateDocumentIds(parsedFiles: ParsedFile[]): void;
|
|
115
165
|
/**
|
|
116
166
|
* Execute dry run phase: validate all files against Convex and detect conflicts.
|
|
117
167
|
*/
|