@popoverai/dotrequirements 0.29.1 → 0.29.3

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.
Files changed (42) hide show
  1. package/dist/codebase-to-spec/compose.js +0 -4
  2. package/dist/codebase-to-spec/renumber.d.ts +7 -5
  3. package/dist/codebase-to-spec/renumber.js +17 -15
  4. package/dist/commands/report.js +15 -1
  5. package/dist/commands/sync.js +7 -1
  6. package/dist/harness/cache.d.ts +83 -2
  7. package/dist/harness/cache.js +94 -8
  8. package/dist/harness/finalize.js +238 -78
  9. package/dist/harness/reportingStatus.d.ts +44 -0
  10. package/dist/harness/reportingStatus.js +123 -0
  11. package/dist/push/core.d.ts +0 -12
  12. package/dist/push/core.js +8 -58
  13. package/dist/push/index.d.ts +1 -1
  14. package/dist/push/index.js +1 -1
  15. package/dist/requirements/cloud-coverage.js +7 -2
  16. package/dist/schema/browser.d.ts +2 -2
  17. package/dist/schema/browser.js +2 -2
  18. package/dist/schema/builder.d.ts +6 -1
  19. package/dist/schema/builder.js +6 -1
  20. package/dist/schema/conversions.js +12 -3
  21. package/dist/schema/file-writer.d.ts +49 -0
  22. package/dist/schema/file-writer.js +138 -0
  23. package/dist/schema/index.d.ts +5 -2
  24. package/dist/schema/index.js +3 -2
  25. package/dist/schema/parser-core.d.ts +32 -5
  26. package/dist/schema/parser-core.js +136 -31
  27. package/dist/schema/parser.d.ts +2 -1
  28. package/dist/schema/parser.js +1 -1
  29. package/dist/schema/schemas.d.ts +11 -0
  30. package/dist/schema/schemas.js +14 -0
  31. package/dist/sync/compare.js +16 -1
  32. package/dist/sync/execute.d.ts +10 -2
  33. package/dist/sync/execute.js +44 -18
  34. package/dist/sync/local-files.d.ts +2 -12
  35. package/dist/sync/local-files.js +2 -62
  36. package/dist/sync/segment.d.ts +2 -2
  37. package/dist/sync/segment.js +45 -11
  38. package/package.json +2 -2
  39. package/dist/harness/convexReporting.d.ts +0 -15
  40. package/dist/harness/convexReporting.js +0 -131
  41. package/dist/harness/coverageCache.d.ts +0 -30
  42. package/dist/harness/coverageCache.js +0 -70
@@ -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; each fence becomes a
30
- * requirement unit keyed by its first-line KEY.
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
- const blockContent = match[1].trim();
43
- const keyMatch = blockContent.match(/^([\w-]+):/);
44
- // A block whose first line isn't KEY: is malformed; parsing rejects it
45
- // upstream, so segmentation never sees it in the compare path. Fall back to
46
- // the leading text as a key so segmentation stays total regardless.
47
- const key = (keyMatch ? keyMatch[1] : blockContent.slice(0, 24)).toUpperCase();
48
- units.push({ kind: "requirement", key, blockContent });
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.toUpperCase(), `${node.label}\u0000${node.content}`);
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.1",
3
+ "version": "0.29.3",
4
4
  "description": "Requirements tracking CLI and test harness",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,7 +54,7 @@
54
54
  "@workos-inc/node": "^7.77.0",
55
55
  "chalk": "^5.6.2",
56
56
  "commander": "^12.1.0",
57
- "convex": "^1.29.3",
57
+ "convex": "^1.42.3",
58
58
  "find-up": "^8.0.0",
59
59
  "glob": "^13.0.0",
60
60
  "open": "^11.0.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