@popoverai/dotrequirements 0.29.1 → 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 +1 -1
- 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
|
@@ -82,10 +82,6 @@ export function assembleComposedDocument(outline, partialPathFor) {
|
|
|
82
82
|
partialsMissing,
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
-
/** Escape a string for safe YAML scalar use. */
|
|
86
|
-
function escapeYamlString(s) {
|
|
87
|
-
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
88
|
-
}
|
|
89
85
|
/**
|
|
90
86
|
* Compose the spec and write it to disk. Renumbers requirement IDs into a
|
|
91
87
|
* clean sequence (CTS-COMPOSE-3), then validates the result.
|
|
@@ -34,11 +34,13 @@ export interface Rename {
|
|
|
34
34
|
* a contiguous 1..N sequence. Returns the rename list (including identity
|
|
35
35
|
* renames for already-clean IDs, so callers can treat the result uniformly).
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
37
|
+
* A fence may hold several adjacent requirements (SYNC-FORMAT-4), so every
|
|
38
|
+
* header line in a block counts, not just the first. HEADER_RE is anchored at
|
|
39
|
+
* column zero and criteria are indented, so criterion text never reads as a
|
|
40
|
+
* header. Exact-duplicate keys are a distinct validation error and out of scope
|
|
41
|
+
* here: a repeated key does not consume a sequence slot (so it introduces no
|
|
42
|
+
* phantom gap) and keeps its first mapping, leaving the downstream validator to
|
|
43
|
+
* flag the duplicate.
|
|
42
44
|
*/
|
|
43
45
|
export declare function computeRenames(content: string): Rename[];
|
|
44
46
|
/**
|
|
@@ -41,11 +41,13 @@ function escapeRegExp(s) {
|
|
|
41
41
|
* a contiguous 1..N sequence. Returns the rename list (including identity
|
|
42
42
|
* renames for already-clean IDs, so callers can treat the result uniformly).
|
|
43
43
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* A fence may hold several adjacent requirements (SYNC-FORMAT-4), so every
|
|
45
|
+
* header line in a block counts, not just the first. HEADER_RE is anchored at
|
|
46
|
+
* column zero and criteria are indented, so criterion text never reads as a
|
|
47
|
+
* header. Exact-duplicate keys are a distinct validation error and out of scope
|
|
48
|
+
* here: a repeated key does not consume a sequence slot (so it introduces no
|
|
49
|
+
* phantom gap) and keeps its first mapping, leaving the downstream validator to
|
|
50
|
+
* flag the duplicate.
|
|
49
51
|
*/
|
|
50
52
|
export function computeRenames(content) {
|
|
51
53
|
const counters = new Map();
|
|
@@ -54,18 +56,18 @@ export function computeRenames(content) {
|
|
|
54
56
|
BLOCK_RE.lastIndex = 0;
|
|
55
57
|
let block = BLOCK_RE.exec(content);
|
|
56
58
|
while (block !== null) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
for (const line of block[1].split("\n")) {
|
|
60
|
+
const m = line.match(HEADER_RE);
|
|
61
|
+
if (!m)
|
|
62
|
+
continue;
|
|
61
63
|
const [, prefix, num, suffix] = m;
|
|
62
64
|
const from = `${prefix}-${num}${suffix}`;
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
65
|
+
if (seen.has(from))
|
|
66
|
+
continue;
|
|
67
|
+
seen.add(from);
|
|
68
|
+
const next = (counters.get(prefix) ?? 0) + 1;
|
|
69
|
+
counters.set(prefix, next);
|
|
70
|
+
renames.push({ from, to: `${prefix}-${next}` });
|
|
69
71
|
}
|
|
70
72
|
block = BLOCK_RE.exec(content);
|
|
71
73
|
}
|
package/dist/commands/report.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { findRequirementsDir, readCoverageCache } from "../harness/cache.js";
|
|
2
|
+
import { formatUnrecordedNotice } from "../harness/reportingStatus.js";
|
|
1
3
|
import { getProjectCoverage, getRequirementCoverage, } from "../requirements/cloud-coverage.js";
|
|
2
4
|
import { buildLocalReport, } from "../requirements/coverage.js";
|
|
3
5
|
import { findProjectRoot, getProjectCredentials, } from "../utils/project-settings.js";
|
|
@@ -5,11 +7,23 @@ const CONVEX_URL = "https://data.dotrequirements.io";
|
|
|
5
7
|
export async function reportCommand(options) {
|
|
6
8
|
const source = options.source ?? "local";
|
|
7
9
|
const format = options.format ?? "console";
|
|
10
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
11
|
+
// COVERAGE-REPORT-3.1: someone asking about coverage learns first that
|
|
12
|
+
// coverage stopped recording. On stderr so machine-readable formats stay
|
|
13
|
+
// parseable.
|
|
14
|
+
if (projectRoot) {
|
|
15
|
+
const requirementsDir = findRequirementsDir(projectRoot);
|
|
16
|
+
const notice = requirementsDir
|
|
17
|
+
? formatUnrecordedNotice(readCoverageCache(requirementsDir))
|
|
18
|
+
: null;
|
|
19
|
+
if (notice) {
|
|
20
|
+
console.error(notice);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
8
23
|
if (source === "local" && (options.branch || options.since !== undefined)) {
|
|
9
24
|
throw new Error("--branch and --since require --source cloud. Local reports cover only the most recent test run on this machine.");
|
|
10
25
|
}
|
|
11
26
|
if (source === "local") {
|
|
12
|
-
const projectRoot = findProjectRoot(process.cwd());
|
|
13
27
|
if (!projectRoot) {
|
|
14
28
|
throw new Error('Could not find .requirements/ directory. Run "dotrequirements init" to initialize your project.');
|
|
15
29
|
}
|
package/dist/commands/sync.js
CHANGED
|
@@ -93,7 +93,7 @@ export async function syncCommand(scope, options) {
|
|
|
93
93
|
return "clean";
|
|
94
94
|
}
|
|
95
95
|
console.log(`\nSyncing with ${brand} cloud...`);
|
|
96
|
-
const outcome = await executePlan(plan, cloud, auth, workspaceRoot);
|
|
96
|
+
const outcome = await executePlan(plan, cloud, auth, workspaceRoot, mode);
|
|
97
97
|
// Report.
|
|
98
98
|
if (outcome.cloudCreated)
|
|
99
99
|
console.log(` Created in cloud: ${outcome.cloudCreated}`);
|
|
@@ -119,6 +119,12 @@ export async function syncCommand(scope, options) {
|
|
|
119
119
|
for (const r of outcome.renames) {
|
|
120
120
|
console.log(` renamed "${r.from}" → "${r.to}"`);
|
|
121
121
|
}
|
|
122
|
+
// DOC-HEADER-11.4: a prefix nobody wrote down was read off the first
|
|
123
|
+
// requirement key — name it rather than letting it appear in the frontmatter
|
|
124
|
+
// unannounced.
|
|
125
|
+
for (const { name, prefix } of outcome.inferredPrefixes) {
|
|
126
|
+
console.log(` ${name}: no prefix in frontmatter — using "${prefix}"`);
|
|
127
|
+
}
|
|
122
128
|
// IMPORT-3: marker failures are loud but never fatal.
|
|
123
129
|
for (const { name, status } of outcome.importWarnings) {
|
|
124
130
|
if (status === "already_used") {
|
package/dist/harness/cache.d.ts
CHANGED
|
@@ -64,9 +64,55 @@ export interface CoverageCacheEntry {
|
|
|
64
64
|
/** Legacy field retained for backward-compat reads of pre-attribution caches. */
|
|
65
65
|
requirementKeys?: string[];
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The most recent reporting attempt that did not reach the cloud.
|
|
69
|
+
*
|
|
70
|
+
* Written on every failed attempt and cleared the moment one succeeds, so its
|
|
71
|
+
* presence alone answers "is coverage recording right now?" (COVERAGE-REPORT-3).
|
|
72
|
+
*/
|
|
73
|
+
/** One requirement's coverage as it is sent to the cloud. */
|
|
74
|
+
export interface CoverageRow {
|
|
75
|
+
requirementKey: string;
|
|
76
|
+
testFile?: string;
|
|
77
|
+
testLine?: number;
|
|
78
|
+
user?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A report that did not land, kept verbatim so a later run can replay it.
|
|
82
|
+
*
|
|
83
|
+
* Stored whole rather than as bare tuples for two reasons. A later run only
|
|
84
|
+
* knows the requirements *it* exercised, so anything narrower can never retry
|
|
85
|
+
* what a partial run (`vitest run one.test.ts`, a CI shard) doesn't touch. And
|
|
86
|
+
* branch and attribution belong to the original run — replaying them under a
|
|
87
|
+
* later run's branch would file the coverage against the wrong one.
|
|
88
|
+
*/
|
|
89
|
+
export interface PendingCoverageReport {
|
|
90
|
+
branch?: string;
|
|
91
|
+
context?: string;
|
|
92
|
+
runId?: string;
|
|
93
|
+
coverage: CoverageRow[];
|
|
94
|
+
}
|
|
95
|
+
export interface CoverageReportFailure {
|
|
96
|
+
timestamp: number;
|
|
97
|
+
/** The reason as it will be shown to the developer. */
|
|
98
|
+
reason: string;
|
|
99
|
+
/** The report to replay. Absent on caches written before replay existed. */
|
|
100
|
+
pending?: PendingCoverageReport;
|
|
101
|
+
}
|
|
67
102
|
export interface CoverageCache {
|
|
68
103
|
current: CoverageCacheEntry | null;
|
|
69
104
|
previous: CoverageCacheEntry | null;
|
|
105
|
+
/**
|
|
106
|
+
* When a report last actually reached the cloud.
|
|
107
|
+
*
|
|
108
|
+
* Deliberately separate from `current.timestamp`, which the debounce path
|
|
109
|
+
* also refreshes without contacting anything — anchoring "coverage last
|
|
110
|
+
* recorded …" on that would let a daily-driver suite roll the claim forward
|
|
111
|
+
* indefinitely while the cloud went stale (COVERAGE-REPORT-3.0).
|
|
112
|
+
*/
|
|
113
|
+
lastRecordedAt?: number;
|
|
114
|
+
/** Absent while reporting is healthy. */
|
|
115
|
+
lastFailure?: CoverageReportFailure;
|
|
70
116
|
}
|
|
71
117
|
/**
|
|
72
118
|
* Find the nearest .requirements directory by walking up from startDir
|
|
@@ -128,10 +174,45 @@ export declare function readTrackingEntries(requirementsDir: string): TrackingEn
|
|
|
128
174
|
export declare function deleteTrackingFile(requirementsDir: string): void;
|
|
129
175
|
export declare function readCoverageCache(requirementsDir: string): CoverageCache;
|
|
130
176
|
/**
|
|
131
|
-
* Update the
|
|
132
|
-
* (new) or bare requirement keys (legacy; converted to
|
|
177
|
+
* Update the debounce window after a reporting attempt. Accepts either
|
|
178
|
+
* attribution tuples (new) or bare requirement keys (legacy; converted to
|
|
179
|
+
* tuples with no context/branch).
|
|
180
|
+
*
|
|
181
|
+
* Says nothing about whether anything reached the cloud — the debounce path
|
|
182
|
+
* calls this having contacted nothing. Whether coverage is recording is
|
|
183
|
+
* `lastRecordedAt` / `lastFailure`, and both are carried through untouched.
|
|
133
184
|
*/
|
|
134
185
|
export declare function updateCoverageCache(requirementsDir: string, testRunId: string, tuples: CoverageTuple[] | string[]): void;
|
|
186
|
+
/**
|
|
187
|
+
* Record that coverage actually reached the cloud: refresh the debounce window,
|
|
188
|
+
* stamp when it landed, and drop any standing failure (COVERAGE-REPORT-3.3).
|
|
189
|
+
*
|
|
190
|
+
* This is the only path that may clear `lastFailure` on the strength of a
|
|
191
|
+
* successful report.
|
|
192
|
+
*/
|
|
193
|
+
export declare function recordCoverageReported(requirementsDir: string, testRunId: string, tuples: CoverageTuple[] | string[], timestamp?: number): void;
|
|
194
|
+
/**
|
|
195
|
+
* Record that a reporting attempt did not reach the cloud, preserving when
|
|
196
|
+
* coverage last actually recorded — that is what "coverage last recorded …" is
|
|
197
|
+
* measured from (COVERAGE-REPORT-3.0).
|
|
198
|
+
*/
|
|
199
|
+
export declare function recordCoverageReportFailure(requirementsDir: string, reason: string, timestamp?: number, pending?: PendingCoverageReport): void;
|
|
200
|
+
/**
|
|
201
|
+
* Drop a standing failure without claiming a report landed.
|
|
202
|
+
*
|
|
203
|
+
* For when reporting has stopped being applicable at all — an unlinked project
|
|
204
|
+
* is not failing to record, so the notice must not outlive the link
|
|
205
|
+
* (COVERAGE-REPORT-3.3).
|
|
206
|
+
*/
|
|
207
|
+
/**
|
|
208
|
+
* Record that a previously failed report has now landed on replay.
|
|
209
|
+
*
|
|
210
|
+
* Stamps when coverage recorded and drops the failure, leaving the debounce
|
|
211
|
+
* window alone — the replay carries an earlier run's tuples, which say nothing
|
|
212
|
+
* about what this run should send (COVERAGE-REPORT-3.4).
|
|
213
|
+
*/
|
|
214
|
+
export declare function recordCoverageReplayed(requirementsDir: string, timestamp?: number): void;
|
|
215
|
+
export declare function clearCoverageReportFailure(requirementsDir: string): void;
|
|
135
216
|
export declare const COVERAGE_DEBOUNCE_WINDOW_MS: number;
|
|
136
217
|
/**
|
|
137
218
|
* Check if an attribution tuple needs reporting based on the coverage cache.
|
package/dist/harness/cache.js
CHANGED
|
@@ -286,6 +286,26 @@ function normalizeEntry(raw) {
|
|
|
286
286
|
}
|
|
287
287
|
return null;
|
|
288
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Normalize the last-failure record read from disk. Caches written before
|
|
291
|
+
* COVERAGE-REPORT-3 have no such field, which reads as "reporting is healthy".
|
|
292
|
+
*/
|
|
293
|
+
function normalizeFailure(raw) {
|
|
294
|
+
if (!raw || typeof raw !== "object")
|
|
295
|
+
return undefined;
|
|
296
|
+
const failure = raw;
|
|
297
|
+
if (typeof failure.timestamp !== "number")
|
|
298
|
+
return undefined;
|
|
299
|
+
if (typeof failure.reason !== "string")
|
|
300
|
+
return undefined;
|
|
301
|
+
const pending = failure.pending;
|
|
302
|
+
const replayable = pending && typeof pending === "object" && Array.isArray(pending.coverage);
|
|
303
|
+
return {
|
|
304
|
+
timestamp: failure.timestamp,
|
|
305
|
+
reason: failure.reason,
|
|
306
|
+
...(replayable ? { pending } : {}),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
289
309
|
export function readCoverageCache(requirementsDir) {
|
|
290
310
|
const cacheDir = getCacheDir(requirementsDir);
|
|
291
311
|
const coveragePath = path.join(cacheDir, COVERAGE_FILE);
|
|
@@ -295,9 +315,13 @@ export function readCoverageCache(requirementsDir) {
|
|
|
295
315
|
try {
|
|
296
316
|
const content = fs.readFileSync(coveragePath, "utf-8");
|
|
297
317
|
const raw = JSON.parse(content);
|
|
318
|
+
const lastFailure = normalizeFailure(raw?.lastFailure);
|
|
319
|
+
const lastRecordedAt = typeof raw?.lastRecordedAt === "number" ? raw.lastRecordedAt : undefined;
|
|
298
320
|
return {
|
|
299
321
|
current: normalizeEntry(raw?.current),
|
|
300
322
|
previous: normalizeEntry(raw?.previous),
|
|
323
|
+
...(lastRecordedAt !== undefined ? { lastRecordedAt } : {}),
|
|
324
|
+
...(lastFailure ? { lastFailure } : {}),
|
|
301
325
|
};
|
|
302
326
|
}
|
|
303
327
|
catch {
|
|
@@ -305,25 +329,87 @@ export function readCoverageCache(requirementsDir) {
|
|
|
305
329
|
}
|
|
306
330
|
}
|
|
307
331
|
/**
|
|
308
|
-
* Update the
|
|
309
|
-
* (new) or bare requirement keys (legacy; converted to
|
|
332
|
+
* Update the debounce window after a reporting attempt. Accepts either
|
|
333
|
+
* attribution tuples (new) or bare requirement keys (legacy; converted to
|
|
334
|
+
* tuples with no context/branch).
|
|
335
|
+
*
|
|
336
|
+
* Says nothing about whether anything reached the cloud — the debounce path
|
|
337
|
+
* calls this having contacted nothing. Whether coverage is recording is
|
|
338
|
+
* `lastRecordedAt` / `lastFailure`, and both are carried through untouched.
|
|
310
339
|
*/
|
|
311
340
|
export function updateCoverageCache(requirementsDir, testRunId, tuples) {
|
|
312
|
-
const cacheDir = getCacheDir(requirementsDir, true);
|
|
313
|
-
const coveragePath = path.join(cacheDir, COVERAGE_FILE);
|
|
314
341
|
const normalized = tuples.map((t) => typeof t === "string" ? { requirementKey: t } : t);
|
|
315
342
|
const existing = readCoverageCache(requirementsDir);
|
|
316
|
-
|
|
343
|
+
writeCoverageCache(requirementsDir, {
|
|
344
|
+
...existing,
|
|
317
345
|
current: {
|
|
318
346
|
testRunId,
|
|
319
347
|
timestamp: Date.now(),
|
|
320
348
|
tuples: normalized,
|
|
321
349
|
},
|
|
322
350
|
previous: existing.current,
|
|
323
|
-
};
|
|
324
|
-
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Record that coverage actually reached the cloud: refresh the debounce window,
|
|
355
|
+
* stamp when it landed, and drop any standing failure (COVERAGE-REPORT-3.3).
|
|
356
|
+
*
|
|
357
|
+
* This is the only path that may clear `lastFailure` on the strength of a
|
|
358
|
+
* successful report.
|
|
359
|
+
*/
|
|
360
|
+
export function recordCoverageReported(requirementsDir, testRunId, tuples, timestamp = Date.now()) {
|
|
361
|
+
updateCoverageCache(requirementsDir, testRunId, tuples);
|
|
362
|
+
const existing = readCoverageCache(requirementsDir);
|
|
363
|
+
writeCoverageCache(requirementsDir, {
|
|
364
|
+
current: existing.current,
|
|
365
|
+
previous: existing.previous,
|
|
366
|
+
lastRecordedAt: timestamp,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Record that a reporting attempt did not reach the cloud, preserving when
|
|
371
|
+
* coverage last actually recorded — that is what "coverage last recorded …" is
|
|
372
|
+
* measured from (COVERAGE-REPORT-3.0).
|
|
373
|
+
*/
|
|
374
|
+
export function recordCoverageReportFailure(requirementsDir, reason, timestamp = Date.now(), pending) {
|
|
375
|
+
const existing = readCoverageCache(requirementsDir);
|
|
376
|
+
writeCoverageCache(requirementsDir, {
|
|
377
|
+
...existing,
|
|
378
|
+
lastFailure: { timestamp, reason, ...(pending ? { pending } : {}) },
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Drop a standing failure without claiming a report landed.
|
|
383
|
+
*
|
|
384
|
+
* For when reporting has stopped being applicable at all — an unlinked project
|
|
385
|
+
* is not failing to record, so the notice must not outlive the link
|
|
386
|
+
* (COVERAGE-REPORT-3.3).
|
|
387
|
+
*/
|
|
388
|
+
/**
|
|
389
|
+
* Record that a previously failed report has now landed on replay.
|
|
390
|
+
*
|
|
391
|
+
* Stamps when coverage recorded and drops the failure, leaving the debounce
|
|
392
|
+
* window alone — the replay carries an earlier run's tuples, which say nothing
|
|
393
|
+
* about what this run should send (COVERAGE-REPORT-3.4).
|
|
394
|
+
*/
|
|
395
|
+
export function recordCoverageReplayed(requirementsDir, timestamp = Date.now()) {
|
|
396
|
+
const existing = readCoverageCache(requirementsDir);
|
|
397
|
+
const { lastFailure: _cleared, ...rest } = existing;
|
|
398
|
+
writeCoverageCache(requirementsDir, { ...rest, lastRecordedAt: timestamp });
|
|
399
|
+
}
|
|
400
|
+
export function clearCoverageReportFailure(requirementsDir) {
|
|
401
|
+
const existing = readCoverageCache(requirementsDir);
|
|
402
|
+
if (!existing.lastFailure)
|
|
403
|
+
return;
|
|
404
|
+
const { lastFailure: _dropped, ...rest } = existing;
|
|
405
|
+
writeCoverageCache(requirementsDir, rest);
|
|
406
|
+
}
|
|
407
|
+
function writeCoverageCache(requirementsDir, cache) {
|
|
408
|
+
const cacheDir = getCacheDir(requirementsDir, true);
|
|
409
|
+
const coveragePath = path.join(cacheDir, COVERAGE_FILE);
|
|
410
|
+
fs.writeFileSync(coveragePath, JSON.stringify(cache, null, 2));
|
|
325
411
|
}
|
|
326
|
-
export const COVERAGE_DEBOUNCE_WINDOW_MS = 4 * 60 * 60 * 1000; // 4 hours — must match
|
|
412
|
+
export const COVERAGE_DEBOUNCE_WINDOW_MS = 4 * 60 * 60 * 1000; // 4 hours — must match DEBOUNCE_WINDOW_MS in testCoverage/helpers.ts
|
|
327
413
|
function tuplesMatch(a, b) {
|
|
328
414
|
return (a.requirementKey === b.requirementKey &&
|
|
329
415
|
a.context === b.context &&
|