@popoverai/dotrequirements 0.29.0 → 0.29.2
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/dist/codebase-to-spec/compose.js +0 -4
- package/dist/codebase-to-spec/renumber.d.ts +7 -5
- package/dist/codebase-to-spec/renumber.js +17 -15
- package/dist/commands/report.js +15 -1
- package/dist/commands/sync.js +7 -1
- package/dist/harness/cache.d.ts +83 -2
- package/dist/harness/cache.js +94 -8
- package/dist/harness/finalize.js +238 -78
- package/dist/harness/reportingStatus.d.ts +44 -0
- package/dist/harness/reportingStatus.js +123 -0
- package/dist/push/core.d.ts +0 -12
- package/dist/push/core.js +8 -58
- package/dist/push/index.d.ts +1 -1
- package/dist/push/index.js +1 -1
- package/dist/requirements/cloud-coverage.js +7 -2
- package/dist/schema/browser.d.ts +2 -2
- package/dist/schema/browser.js +2 -2
- package/dist/schema/builder.d.ts +6 -1
- package/dist/schema/builder.js +6 -1
- package/dist/schema/conversions.js +12 -3
- package/dist/schema/file-writer.d.ts +49 -0
- package/dist/schema/file-writer.js +138 -0
- package/dist/schema/index.d.ts +5 -2
- package/dist/schema/index.js +3 -2
- package/dist/schema/parser-core.d.ts +32 -5
- package/dist/schema/parser-core.js +136 -31
- package/dist/schema/parser.d.ts +2 -1
- package/dist/schema/parser.js +1 -1
- package/dist/schema/schemas.d.ts +11 -0
- package/dist/schema/schemas.js +14 -0
- package/dist/sync/compare.js +16 -1
- package/dist/sync/execute.d.ts +10 -2
- package/dist/sync/execute.js +44 -18
- package/dist/sync/local-files.d.ts +2 -12
- package/dist/sync/local-files.js +2 -62
- package/dist/sync/segment.d.ts +2 -2
- package/dist/sync/segment.js +45 -11
- package/package.json +2 -2
- package/dist/harness/convexReporting.d.ts +0 -15
- package/dist/harness/convexReporting.js +0 -131
- package/dist/harness/coverageCache.d.ts +0 -30
- package/dist/harness/coverageCache.js +0 -70
package/dist/sync/segment.js
CHANGED
|
@@ -6,7 +6,19 @@
|
|
|
6
6
|
* normalized text (prose has no stable id, so a reworded passage reads as a
|
|
7
7
|
* removal-plus-addition — a conflict, per DIFF-4.3).
|
|
8
8
|
*/
|
|
9
|
-
import { getAllRequirements, parseRequirementBlock } from "../schema/index.js";
|
|
9
|
+
import { canonicalRequirementKey, getAllRequirements, parseRequirementBlock, splitRequirementFenceContent, } from "../schema/index.js";
|
|
10
|
+
/**
|
|
11
|
+
* A node's comparison id: the canonical root key plus its position tail, so the
|
|
12
|
+
* two sides agree on identity even when they spell the key differently
|
|
13
|
+
* (DIFF-3.3). Splitting on the first "." keeps the tail untouched — position
|
|
14
|
+
* paths are already canonical.
|
|
15
|
+
*/
|
|
16
|
+
function canonicalNodeId(id) {
|
|
17
|
+
const dot = id.indexOf(".");
|
|
18
|
+
if (dot === -1)
|
|
19
|
+
return canonicalRequirementKey(id);
|
|
20
|
+
return `${canonicalRequirementKey(id.slice(0, dot))}${id.slice(dot)}`;
|
|
21
|
+
}
|
|
10
22
|
/** Matches a fenced dotrequirements block; group 1 is its inner content. */
|
|
11
23
|
const FENCE = /```dotrequirements\n([\s\S]*?)```/gm;
|
|
12
24
|
/**
|
|
@@ -26,8 +38,8 @@ export function normalizeProse(text) {
|
|
|
26
38
|
}
|
|
27
39
|
/**
|
|
28
40
|
* Split a body into its ordered units. Prose between (and around) fences
|
|
29
|
-
* becomes prose units when non-empty after normalization;
|
|
30
|
-
* requirement unit keyed by its
|
|
41
|
+
* becomes prose units when non-empty after normalization; every root inside a
|
|
42
|
+
* fence becomes a requirement unit keyed by its KEY.
|
|
31
43
|
*/
|
|
32
44
|
export function segmentBody(body) {
|
|
33
45
|
const normalized = body.replace(/\r\n/g, "\n");
|
|
@@ -39,13 +51,35 @@ export function segmentBody(body) {
|
|
|
39
51
|
const prose = normalizeProse(normalized.slice(lastIndex, match.index));
|
|
40
52
|
if (prose)
|
|
41
53
|
units.push({ kind: "prose", text: prose });
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
// the
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
// Segmentation must stay total. Cloud bodies arrive as stored markdown,
|
|
55
|
+
// never re-parsed, so a fence that predates today's grammar still shows up
|
|
56
|
+
// here — and a throw would take down the comparison for every document in
|
|
57
|
+
// the run. A fence we cannot split becomes one unit keyed by its leading
|
|
58
|
+
// text, so the pair still classifies (as a conflict) and `--repo-wins` can
|
|
59
|
+
// still repair it (SYNC-FAIL-4.2).
|
|
60
|
+
const fenceBlocks = (() => {
|
|
61
|
+
try {
|
|
62
|
+
return splitRequirementFenceContent(match[1]);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
const blockContent = match[1].trim();
|
|
66
|
+
const leading = blockContent.match(/^([\w-]+):/)?.[1];
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
69
|
+
key: leading ?? blockContent.slice(0, 24),
|
|
70
|
+
blockContent,
|
|
71
|
+
startLine: 0,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
})();
|
|
76
|
+
for (const { key, blockContent } of fenceBlocks) {
|
|
77
|
+
units.push({
|
|
78
|
+
kind: "requirement",
|
|
79
|
+
key: canonicalRequirementKey(key),
|
|
80
|
+
blockContent,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
49
83
|
lastIndex = FENCE.lastIndex;
|
|
50
84
|
match = FENCE.exec(normalized);
|
|
51
85
|
}
|
|
@@ -69,7 +103,7 @@ export function requirementUnitMap(blockContent) {
|
|
|
69
103
|
const map = new Map();
|
|
70
104
|
for (const node of getAllRequirements([tree])) {
|
|
71
105
|
// A NUL (\u0000) separates the label from content so a label change reads as a change.
|
|
72
|
-
map.set(node.id
|
|
106
|
+
map.set(canonicalNodeId(node.id), `${node.label}\u0000${node.content}`);
|
|
73
107
|
}
|
|
74
108
|
return map;
|
|
75
109
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popoverai/dotrequirements",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.2",
|
|
4
4
|
"description": "Requirements tracking CLI and test harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"commander": "^12.1.0",
|
|
57
57
|
"convex": "^1.29.3",
|
|
58
58
|
"find-up": "^8.0.0",
|
|
59
|
-
"glob": "^
|
|
59
|
+
"glob": "^13.0.0",
|
|
60
60
|
"open": "^11.0.0",
|
|
61
61
|
"prompts": "^2.4.2",
|
|
62
62
|
"repomix": "^1.14.0",
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { type TrackedRequirement } from "./tracking.js";
|
|
2
|
-
/**
|
|
3
|
-
* Get the current git branch, or null if not in a git repo
|
|
4
|
-
*/
|
|
5
|
-
export declare function getCurrentBranch(cwd?: string): string;
|
|
6
|
-
/**
|
|
7
|
-
* Report coverage to Convex (async, fire-and-forget)
|
|
8
|
-
*
|
|
9
|
-
* @param cwd - Working directory to find .env.local and write cache
|
|
10
|
-
* @param trackedReqs - Optional tracked requirements map. If not provided, falls back to in-memory tracking.
|
|
11
|
-
* When called from finalizeTestRun (globalTeardown), this should be passed explicitly
|
|
12
|
-
* since the in-memory map is empty in a separate process context.
|
|
13
|
-
*/
|
|
14
|
-
export declare function reportCoverageToConvex(cwd?: string, trackedReqs?: Map<string, TrackedRequirement>): Promise<void>;
|
|
15
|
-
//# sourceMappingURL=convexReporting.d.ts.map
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import { getProjectInfo } from "../utils/project-settings.js";
|
|
3
|
-
import { getRequirementsToReport, saveCache } from "./coverageCache.js";
|
|
4
|
-
import { getTrackedRequirements } from "./tracking.js";
|
|
5
|
-
/**
|
|
6
|
-
* Get the current git branch, or null if not in a git repo
|
|
7
|
-
*/
|
|
8
|
-
export function getCurrentBranch(cwd = process.cwd()) {
|
|
9
|
-
try {
|
|
10
|
-
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
11
|
-
cwd,
|
|
12
|
-
encoding: "utf-8",
|
|
13
|
-
stdio: ["pipe", "pipe", "ignore"], // Suppress stderr
|
|
14
|
-
}).trim();
|
|
15
|
-
return branch;
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
// Not in a git repo or git not available
|
|
19
|
-
return "unknown";
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Production Convex deployment URL
|
|
24
|
-
*/
|
|
25
|
-
const CONVEX_URL = "https://data.dotrequirements.io";
|
|
26
|
-
/**
|
|
27
|
-
* Extract file path and line number from caller location string
|
|
28
|
-
*/
|
|
29
|
-
function parseCallerLocation(location) {
|
|
30
|
-
// Location format: "filename.ts:123" or "unknown"
|
|
31
|
-
if (location === "unknown") {
|
|
32
|
-
return { testFile: undefined, testLine: undefined };
|
|
33
|
-
}
|
|
34
|
-
const match = location.match(/^(.+):(\d+)$/);
|
|
35
|
-
if (match) {
|
|
36
|
-
return {
|
|
37
|
-
testFile: match[1],
|
|
38
|
-
testLine: parseInt(match[2], 10),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
return { testFile: location, testLine: undefined };
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Report coverage to Convex (async, fire-and-forget)
|
|
45
|
-
*
|
|
46
|
-
* @param cwd - Working directory to find .env.local and write cache
|
|
47
|
-
* @param trackedReqs - Optional tracked requirements map. If not provided, falls back to in-memory tracking.
|
|
48
|
-
* When called from finalizeTestRun (globalTeardown), this should be passed explicitly
|
|
49
|
-
* since the in-memory map is empty in a separate process context.
|
|
50
|
-
*/
|
|
51
|
-
export async function reportCoverageToConvex(cwd = process.cwd(), trackedReqs) {
|
|
52
|
-
try {
|
|
53
|
-
// Get project info from .requirements/project-settings.json
|
|
54
|
-
const projectInfo = getProjectInfo(cwd);
|
|
55
|
-
// Skip if no project found or not connected to cloud
|
|
56
|
-
if (!projectInfo ||
|
|
57
|
-
projectInfo.status !== "cloud-connected" ||
|
|
58
|
-
!projectInfo.credentials) {
|
|
59
|
-
console.log("\nℹ️ Skipping cloud coverage reporting (project not connected to cloud)");
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const { projectId, projectSecret } = projectInfo.credentials;
|
|
63
|
-
// Use provided trackedReqs or fall back to in-memory tracking
|
|
64
|
-
const reqs = trackedReqs ?? getTrackedRequirements();
|
|
65
|
-
const requirementKeys = Array.from(reqs.keys());
|
|
66
|
-
// Use caching to filter out requirements that don't need reporting
|
|
67
|
-
const keysToReport = getRequirementsToReport(requirementKeys, cwd);
|
|
68
|
-
if (keysToReport.length === 0) {
|
|
69
|
-
console.log("\n✓ Coverage unchanged since last run (skipping cloud report)");
|
|
70
|
-
// Still update the cache with current timestamp
|
|
71
|
-
saveCache({
|
|
72
|
-
timestamp: Date.now(),
|
|
73
|
-
requirementKeys,
|
|
74
|
-
}, cwd);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const branch = getCurrentBranch(cwd);
|
|
78
|
-
// Build coverage payload
|
|
79
|
-
const coverage = keysToReport.map((key) => {
|
|
80
|
-
const tracked = reqs.get(key);
|
|
81
|
-
// Use the first access location (most likely the test that called requirement())
|
|
82
|
-
const firstAccess = tracked.accessedAt[0] || "unknown";
|
|
83
|
-
const { testFile, testLine } = parseCallerLocation(firstAccess);
|
|
84
|
-
return {
|
|
85
|
-
requirementKey: key,
|
|
86
|
-
testFile,
|
|
87
|
-
testLine,
|
|
88
|
-
};
|
|
89
|
-
});
|
|
90
|
-
// Send to Convex
|
|
91
|
-
const response = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
92
|
-
method: "POST",
|
|
93
|
-
headers: {
|
|
94
|
-
"Content-Type": "application/json",
|
|
95
|
-
},
|
|
96
|
-
body: JSON.stringify({
|
|
97
|
-
path: "testCoverage/mutations:recordCoverage",
|
|
98
|
-
args: {
|
|
99
|
-
projectAuth: {
|
|
100
|
-
projectSlug: projectId,
|
|
101
|
-
projectSecret,
|
|
102
|
-
},
|
|
103
|
-
target: {
|
|
104
|
-
type: "project",
|
|
105
|
-
slug: projectId,
|
|
106
|
-
},
|
|
107
|
-
branch,
|
|
108
|
-
coverage,
|
|
109
|
-
},
|
|
110
|
-
format: "json",
|
|
111
|
-
}),
|
|
112
|
-
});
|
|
113
|
-
if (!response.ok) {
|
|
114
|
-
const error = await response.text();
|
|
115
|
-
console.warn(`\n⚠️ Failed to report coverage to cloud: ${response.status} ${error}`);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
await response.json();
|
|
119
|
-
console.log(`\n✓ Reported ${keysToReport.length} requirement(s) to cloud (branch: ${branch})`);
|
|
120
|
-
// Update cache with current test run
|
|
121
|
-
saveCache({
|
|
122
|
-
timestamp: Date.now(),
|
|
123
|
-
requirementKeys,
|
|
124
|
-
}, cwd);
|
|
125
|
-
}
|
|
126
|
-
catch (error) {
|
|
127
|
-
// Don't throw - reporting failures shouldn't break tests
|
|
128
|
-
console.warn(`\n⚠️ Error reporting coverage to cloud: ${error instanceof Error ? error.message : String(error)}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
//# sourceMappingURL=convexReporting.js.map
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
export interface TestRun {
|
|
2
|
-
testRunId: string;
|
|
3
|
-
timestamp: number;
|
|
4
|
-
requirementKeys: string[];
|
|
5
|
-
}
|
|
6
|
-
export interface CoverageCache {
|
|
7
|
-
current: TestRun | null;
|
|
8
|
-
previous: TestRun | null;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Load the coverage cache from disk
|
|
12
|
-
*/
|
|
13
|
-
export declare function loadCache(cwd?: string): CoverageCache;
|
|
14
|
-
/**
|
|
15
|
-
* Save a new test run to the cache, rotating current → previous
|
|
16
|
-
*/
|
|
17
|
-
export declare function saveCache(testRun: Omit<TestRun, "testRunId">, cwd?: string): void;
|
|
18
|
-
/**
|
|
19
|
-
* Check if a requirement needs reporting based on cache state
|
|
20
|
-
* Returns true if:
|
|
21
|
-
* - Requirement was not covered in previous run, OR
|
|
22
|
-
* - Previous run was more than 4 hours ago
|
|
23
|
-
*/
|
|
24
|
-
export declare function needsReporting(requirementKey: string, cache: CoverageCache): boolean;
|
|
25
|
-
/**
|
|
26
|
-
* Get the list of requirements that need reporting
|
|
27
|
-
* (new or changed since last run, or stale)
|
|
28
|
-
*/
|
|
29
|
-
export declare function getRequirementsToReport(currentRequirementKeys: string[], cwd?: string): string[];
|
|
30
|
-
//# sourceMappingURL=coverageCache.d.ts.map
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { v4 as uuidv4 } from "uuid";
|
|
4
|
-
const CACHE_FILE = ".requirements/.coverage-cache.json";
|
|
5
|
-
const STALENESS_THRESHOLD_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
6
|
-
/**
|
|
7
|
-
* Load the coverage cache from disk
|
|
8
|
-
*/
|
|
9
|
-
export function loadCache(cwd = process.cwd()) {
|
|
10
|
-
const cachePath = join(cwd, CACHE_FILE);
|
|
11
|
-
if (!existsSync(cachePath)) {
|
|
12
|
-
return { current: null, previous: null };
|
|
13
|
-
}
|
|
14
|
-
try {
|
|
15
|
-
const content = readFileSync(cachePath, "utf-8");
|
|
16
|
-
return JSON.parse(content);
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
// If cache is corrupt, start fresh
|
|
20
|
-
console.warn("Warning: Coverage cache is corrupt, starting fresh");
|
|
21
|
-
return { current: null, previous: null };
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Save a new test run to the cache, rotating current → previous
|
|
26
|
-
*/
|
|
27
|
-
export function saveCache(testRun, cwd = process.cwd()) {
|
|
28
|
-
const cache = loadCache(cwd);
|
|
29
|
-
const cachePath = join(cwd, CACHE_FILE);
|
|
30
|
-
const newCache = {
|
|
31
|
-
current: {
|
|
32
|
-
testRunId: uuidv4(),
|
|
33
|
-
...testRun,
|
|
34
|
-
},
|
|
35
|
-
previous: cache.current,
|
|
36
|
-
};
|
|
37
|
-
writeFileSync(cachePath, JSON.stringify(newCache, null, 2), "utf-8");
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Check if a requirement needs reporting based on cache state
|
|
41
|
-
* Returns true if:
|
|
42
|
-
* - Requirement was not covered in previous run, OR
|
|
43
|
-
* - Previous run was more than 4 hours ago
|
|
44
|
-
*/
|
|
45
|
-
export function needsReporting(requirementKey, cache) {
|
|
46
|
-
// If no previous run, everything needs reporting
|
|
47
|
-
if (!cache.current) {
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
// If requirement wasn't in current run, it needs reporting
|
|
51
|
-
if (!cache.current.requirementKeys.includes(requirementKey)) {
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
// If current run is stale (>4 hours old), needs reporting
|
|
55
|
-
const age = Date.now() - cache.current.timestamp;
|
|
56
|
-
if (age > STALENESS_THRESHOLD_MS) {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
// Otherwise, skip reporting (already reported recently)
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Get the list of requirements that need reporting
|
|
64
|
-
* (new or changed since last run, or stale)
|
|
65
|
-
*/
|
|
66
|
-
export function getRequirementsToReport(currentRequirementKeys, cwd = process.cwd()) {
|
|
67
|
-
const cache = loadCache(cwd);
|
|
68
|
-
return currentRequirementKeys.filter((key) => needsReporting(key, cache));
|
|
69
|
-
}
|
|
70
|
-
//# sourceMappingURL=coverageCache.js.map
|