@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.
- 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/harness/finalize.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
import { execSync } from "node:child_process";
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
14
|
import { getProjectInfo } from "../utils/project-settings.js";
|
|
15
|
-
import { cleanupTestRunId, deleteTrackingFile, findProjectRoot, findRequirementsDir, getTestRunId, needsReporting, readCoverageCache, readLookupCache, readTrackingEntries, updateCoverageCache, } from "./cache.js";
|
|
15
|
+
import { cleanupTestRunId, clearCoverageReportFailure, deleteTrackingFile, findProjectRoot, findRequirementsDir, getTestRunId, needsReporting, readCoverageCache, readLookupCache, readTrackingEntries, recordCoverageReplayed, recordCoverageReported, recordCoverageReportFailure, updateCoverageCache, } from "./cache.js";
|
|
16
|
+
import { formatFailureNotice, formatUnrecordedNotice, refusalReason, } from "./reportingStatus.js";
|
|
16
17
|
import { toProjectRelativePath } from "./tracking.js";
|
|
17
18
|
/**
|
|
18
19
|
* Aggregate tracking entries by requirement key
|
|
@@ -68,7 +69,7 @@ function getLineAuthor(cwd, file, line) {
|
|
|
68
69
|
* HARNESS-FINALIZE-1: Shows which requirements were tested
|
|
69
70
|
*/
|
|
70
71
|
function printLocalReport(testedKeys, lookup, options) {
|
|
71
|
-
const { showSummary, showTestedList, showUntestedList } = options;
|
|
72
|
+
const { showSummary, showTestedList, showUntestedList, cloudUnrecorded } = options;
|
|
72
73
|
const allKeys = lookup
|
|
73
74
|
? Object.keys(lookup.requirements).filter((k) => !lookup.requirements[k].isAlias)
|
|
74
75
|
: [];
|
|
@@ -79,7 +80,9 @@ function printLocalReport(testedKeys, lookup, options) {
|
|
|
79
80
|
let report = "";
|
|
80
81
|
// Show summary stats
|
|
81
82
|
if (showSummary) {
|
|
82
|
-
report +=
|
|
83
|
+
report += cloudUnrecorded
|
|
84
|
+
? "\n=== Requirements Coverage Report (local only — not recorded to cloud) ===\n"
|
|
85
|
+
: "\n=== Requirements Coverage Report ===\n";
|
|
83
86
|
report += `\nTotal Requirements: ${total}\n`;
|
|
84
87
|
report += `Tested Requirements: ${tested}\n`;
|
|
85
88
|
report += `Untested Requirements: ${untestedKeys.length}\n`;
|
|
@@ -130,6 +133,122 @@ function printLocalReport(testedKeys, lookup, options) {
|
|
|
130
133
|
* Production Convex deployment URL
|
|
131
134
|
*/
|
|
132
135
|
const CONVEX_URL = "https://data.dotrequirements.io";
|
|
136
|
+
/**
|
|
137
|
+
* Record a reporting attempt that did not reach the cloud, so later runs and
|
|
138
|
+
* `dotreq report` keep saying so (COVERAGE-REPORT-3), and hand the reason back
|
|
139
|
+
* for this run's notice.
|
|
140
|
+
*/
|
|
141
|
+
function fail(requirementsDir, reason, pending) {
|
|
142
|
+
const stated = asReason(reason);
|
|
143
|
+
persist(() => recordCoverageReportFailure(requirementsDir, stated, Date.now(), pending));
|
|
144
|
+
return { sent: false, count: 0, error: stated };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Run a coverage-cache write, swallowing IO errors.
|
|
148
|
+
*
|
|
149
|
+
* A read-only or full `.requirements/.cache` must never change the outcome of
|
|
150
|
+
* a test run (COVERAGE-REPORT-2.0) — nor, on the success path, turn a report
|
|
151
|
+
* that reached the cloud into one reported as failed.
|
|
152
|
+
*
|
|
153
|
+
* Returns whether the write landed, because a swallowed success-path write
|
|
154
|
+
* leaves the cache disagreeing with what just happened.
|
|
155
|
+
*/
|
|
156
|
+
function persist(write) {
|
|
157
|
+
try {
|
|
158
|
+
write();
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// The run still says its piece; only the memory of it is lost.
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* How much of a server's response body to keep as a failure reason.
|
|
168
|
+
*
|
|
169
|
+
* The reason is persisted and reprinted on every later run, and the endpoint
|
|
170
|
+
* sits behind Cloudflare — an origin 5xx returns a multi-KB HTML page whose
|
|
171
|
+
* newlines would shred the notice.
|
|
172
|
+
*/
|
|
173
|
+
const MAX_REASON_LENGTH = 200;
|
|
174
|
+
/** Collapse to a single line and cap, so a reason stays readable forever. */
|
|
175
|
+
function asReason(body) {
|
|
176
|
+
const collapsed = body.replace(/\s+/g, " ").trim();
|
|
177
|
+
return collapsed.length > MAX_REASON_LENGTH
|
|
178
|
+
? `${collapsed.slice(0, MAX_REASON_LENGTH)}…`
|
|
179
|
+
: collapsed;
|
|
180
|
+
}
|
|
181
|
+
/** How long to wait on the cloud before giving up and saying so. */
|
|
182
|
+
const REPORT_TIMEOUT_MS = 15_000;
|
|
183
|
+
/**
|
|
184
|
+
* Send one coverage report and say plainly whether it landed.
|
|
185
|
+
*
|
|
186
|
+
* Shared by this run's report and the replay of a previous run's, so both are
|
|
187
|
+
* judged by the same rule: success has to be stated by the cloud, never
|
|
188
|
+
* inferred from the absence of an error.
|
|
189
|
+
*/
|
|
190
|
+
async function postCoverage(credentials, report) {
|
|
191
|
+
// Note: projectId from env is always a slug (e.g., "reduced-cephalopod-288"),
|
|
192
|
+
// not a Convex ID. Use projectSlug field for authentication and slug for target.
|
|
193
|
+
const response = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: { "Content-Type": "application/json" },
|
|
196
|
+
body: JSON.stringify({
|
|
197
|
+
path: "testCoverage/mutations:recordCoverage",
|
|
198
|
+
args: {
|
|
199
|
+
projectAuth: {
|
|
200
|
+
projectSlug: credentials.projectId,
|
|
201
|
+
projectSecret: credentials.projectSecret,
|
|
202
|
+
},
|
|
203
|
+
target: { type: "project", slug: credentials.projectId },
|
|
204
|
+
branch: report.branch,
|
|
205
|
+
context: report.context,
|
|
206
|
+
runId: report.runId,
|
|
207
|
+
coverage: report.coverage,
|
|
208
|
+
},
|
|
209
|
+
format: "json",
|
|
210
|
+
}),
|
|
211
|
+
// Without this a black-holed connection hangs teardown indefinitely,
|
|
212
|
+
// and the run never gets to say coverage didn't record.
|
|
213
|
+
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS),
|
|
214
|
+
});
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
// Convex answers a rejected function with 200 and an error body, so a
|
|
217
|
+
// non-ok status is an HTTP-level problem — but the body may still say what
|
|
218
|
+
// was wrong (a 413 on an oversized payload, say), so don't discard it in
|
|
219
|
+
// favour of a blanket "could not be reached".
|
|
220
|
+
const body = await response.text().catch(() => "");
|
|
221
|
+
const detail = body.trim() ? ` ${body}` : "";
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
reason: `The cloud refused the report (HTTP ${response.status}).${detail}`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const responseText = await response.text();
|
|
228
|
+
let responseData;
|
|
229
|
+
try {
|
|
230
|
+
responseData = JSON.parse(responseText);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Left undefined; handled as an unreadable answer below.
|
|
234
|
+
}
|
|
235
|
+
const convexResponse = responseData && typeof responseData === "object"
|
|
236
|
+
? responseData
|
|
237
|
+
: undefined;
|
|
238
|
+
if (convexResponse?.status === "error") {
|
|
239
|
+
// COVERAGE-REPORT-1: the reason lives in errorData, not the raw
|
|
240
|
+
// errorMessage that production redacts.
|
|
241
|
+
return { ok: false, reason: refusalReason(convexResponse) };
|
|
242
|
+
}
|
|
243
|
+
// COVERAGE-REPORT-1.4: only an affirmative success may clear a failure.
|
|
244
|
+
if (convexResponse?.status !== "success") {
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
reason: `The cloud's answer could not be read: ${responseText.trim() || "empty response"}`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return { ok: true };
|
|
251
|
+
}
|
|
133
252
|
/**
|
|
134
253
|
* Report coverage to Convex cloud
|
|
135
254
|
*
|
|
@@ -137,6 +256,9 @@ const CONVEX_URL = "https://data.dotrequirements.io";
|
|
|
137
256
|
* HARNESS-FINALIZE-3: Coverage records include requirement, file, line, branch
|
|
138
257
|
*/
|
|
139
258
|
async function reportToConvex(projectRoot, testRunId, aggregated, showCloudStatus, context) {
|
|
259
|
+
// Hoisted so a transport failure — DNS, TLS, the timeout — records the
|
|
260
|
+
// report it was carrying, and not an unreplayable failure (COVERAGE-REPORT-3.4).
|
|
261
|
+
let thisRun;
|
|
140
262
|
try {
|
|
141
263
|
// Get project info from .requirements/project-settings.json
|
|
142
264
|
const projectInfo = getProjectInfo(projectRoot);
|
|
@@ -147,9 +269,20 @@ async function reportToConvex(projectRoot, testRunId, aggregated, showCloudStatu
|
|
|
147
269
|
if (showCloudStatus) {
|
|
148
270
|
console.log("\nℹ️ Skipping cloud coverage reporting (project not connected to cloud)");
|
|
149
271
|
}
|
|
272
|
+
// An unlinked project is not failing to record — it has stopped trying.
|
|
273
|
+
// Without this the notice would outlive the link with no way out of it.
|
|
274
|
+
const dir = findRequirementsDir(projectRoot);
|
|
275
|
+
if (dir) {
|
|
276
|
+
try {
|
|
277
|
+
clearCoverageReportFailure(dir);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// Same reasoning as fail(): never let cache IO redden a run.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
150
283
|
return { sent: false, count: 0 };
|
|
151
284
|
}
|
|
152
|
-
const
|
|
285
|
+
const credentials = projectInfo.credentials;
|
|
153
286
|
const requirementsDir = findRequirementsDir(projectRoot);
|
|
154
287
|
const branch = getCurrentBranch(projectRoot);
|
|
155
288
|
// Build the full set of tuples for this run, one per tracked requirement
|
|
@@ -159,18 +292,59 @@ async function reportToConvex(projectRoot, testRunId, aggregated, showCloudStatu
|
|
|
159
292
|
context,
|
|
160
293
|
branch,
|
|
161
294
|
}));
|
|
295
|
+
const coverageCache = readCoverageCache(requirementsDir);
|
|
296
|
+
// COVERAGE-REPORT-3.4: replay a report that didn't land, before anything
|
|
297
|
+
// else. It has to be replayed rather than recomputed: this run only knows
|
|
298
|
+
// the requirements it exercised, so a partial run (`vitest run one.test.ts`,
|
|
299
|
+
// a CI shard, watch mode) would otherwise never retry the rest and the
|
|
300
|
+
// failure would stand forever. Replaying verbatim also keeps the original
|
|
301
|
+
// branch and attribution, which a later run cannot reconstruct.
|
|
302
|
+
const pending = coverageCache.lastFailure?.pending;
|
|
303
|
+
let replayedCount = 0;
|
|
304
|
+
if (pending) {
|
|
305
|
+
// Hold it as the report in flight before going near the network:
|
|
306
|
+
// postCoverage doesn't catch its own transport errors, and a DNS blip
|
|
307
|
+
// reaching the catch-all with nothing held would replace the stored
|
|
308
|
+
// report with a bare reason — losing the branch, runId and attribution
|
|
309
|
+
// that are the whole reason it's kept whole.
|
|
310
|
+
thisRun = pending;
|
|
311
|
+
const replay = await postCoverage(credentials, pending);
|
|
312
|
+
if (!replay.ok) {
|
|
313
|
+
// Same cause will greet this run's own report; don't ask twice.
|
|
314
|
+
return fail(requirementsDir, replay.reason, pending);
|
|
315
|
+
}
|
|
316
|
+
replayedCount = pending.coverage.length;
|
|
317
|
+
if (showCloudStatus) {
|
|
318
|
+
console.log(`\n✓ Reported ${replayedCount} previously unrecorded requirement(s) to cloud`);
|
|
319
|
+
}
|
|
320
|
+
// Coverage reached the cloud, so this stamps when — a replay is a
|
|
321
|
+
// recording like any other.
|
|
322
|
+
persist(() => recordCoverageReplayed(requirementsDir));
|
|
323
|
+
thisRun = undefined;
|
|
324
|
+
}
|
|
325
|
+
// A failure written before replay existed has nothing to replay, so the
|
|
326
|
+
// only way out is to report this run in full and let success clear it.
|
|
327
|
+
// One run's cost, once, rather than a notice with no exit.
|
|
328
|
+
const strandedLegacyFailure = Boolean(coverageCache.lastFailure && !coverageCache.lastFailure.pending);
|
|
162
329
|
// Local debounce (COVERAGE-DEBOUNCE-1): only report tuples not recently cached.
|
|
163
330
|
// Server-side debounce is a backstop (COVERAGE-DEBOUNCE-4) — this layer just
|
|
164
331
|
// prevents unnecessary HTTP calls.
|
|
165
|
-
const
|
|
166
|
-
|
|
332
|
+
const tuplesToReport = strandedLegacyFailure
|
|
333
|
+
? allTuples
|
|
334
|
+
: allTuples.filter((tuple) => needsReporting(tuple, coverageCache));
|
|
167
335
|
if (tuplesToReport.length === 0) {
|
|
168
|
-
if
|
|
336
|
+
// Only "unchanged" if nothing went to the cloud at all — saying it under
|
|
337
|
+
// a replay line that just reported N requirements contradicts it.
|
|
338
|
+
if (showCloudStatus && replayedCount === 0) {
|
|
169
339
|
console.log("\n✓ Coverage unchanged since last run (skipping cloud report)");
|
|
170
340
|
}
|
|
171
|
-
// Still update the cache so the window continues tracking
|
|
172
|
-
|
|
173
|
-
|
|
341
|
+
// Still update the cache so the window continues tracking. This branch
|
|
342
|
+
// contacts nothing, so it must not clear a standing failure — and it
|
|
343
|
+
// doesn't: updateCoverageCache carries lastFailure through untouched.
|
|
344
|
+
// Safe to reach with one standing: a pending report was already replayed
|
|
345
|
+
// above, and a legacy failure forced a full report rather than this path.
|
|
346
|
+
persist(() => updateCoverageCache(requirementsDir, testRunId, allTuples));
|
|
347
|
+
return { sent: replayedCount > 0, count: replayedCount };
|
|
174
348
|
}
|
|
175
349
|
// Deterministic runId for this finalize invocation (COVERAGE-CONTEXT-4)
|
|
176
350
|
const runId = randomUUID();
|
|
@@ -220,73 +394,33 @@ async function reportToConvex(projectRoot, testRunId, aggregated, showCloudStatu
|
|
|
220
394
|
user,
|
|
221
395
|
};
|
|
222
396
|
});
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
method: "POST",
|
|
228
|
-
headers: {
|
|
229
|
-
"Content-Type": "application/json",
|
|
230
|
-
},
|
|
231
|
-
body: JSON.stringify({
|
|
232
|
-
path: "testCoverage/mutations:recordCoverage",
|
|
233
|
-
args: {
|
|
234
|
-
projectAuth: {
|
|
235
|
-
projectSlug: projectId,
|
|
236
|
-
projectSecret,
|
|
237
|
-
},
|
|
238
|
-
target: {
|
|
239
|
-
type: "project",
|
|
240
|
-
slug: projectId,
|
|
241
|
-
},
|
|
242
|
-
branch,
|
|
243
|
-
context,
|
|
244
|
-
runId,
|
|
245
|
-
coverage,
|
|
246
|
-
},
|
|
247
|
-
format: "json",
|
|
248
|
-
}),
|
|
249
|
-
});
|
|
250
|
-
if (!response.ok) {
|
|
251
|
-
const error = await response.text();
|
|
252
|
-
if (showCloudStatus) {
|
|
253
|
-
console.warn(`\n⚠️ Failed to report coverage to cloud: ${response.status} ${error}`);
|
|
254
|
-
}
|
|
255
|
-
return { sent: false, count: 0, error: `${response.status} ${error}` };
|
|
256
|
-
}
|
|
257
|
-
// Check for Convex-level errors in the response body
|
|
258
|
-
// Convex returns 200 OK even for validation errors, with the error in the body
|
|
259
|
-
const responseText = await response.text();
|
|
260
|
-
let responseData;
|
|
261
|
-
try {
|
|
262
|
-
responseData = JSON.parse(responseText);
|
|
263
|
-
}
|
|
264
|
-
catch {
|
|
265
|
-
// Not JSON, treat as success
|
|
266
|
-
}
|
|
267
|
-
if (responseData &&
|
|
268
|
-
typeof responseData === "object" &&
|
|
269
|
-
"status" in responseData) {
|
|
270
|
-
const convexResponse = responseData;
|
|
271
|
-
if (convexResponse.status === "error") {
|
|
272
|
-
const errorMsg = convexResponse.errorMessage || "Unknown Convex error";
|
|
273
|
-
if (showCloudStatus) {
|
|
274
|
-
console.warn(`\n⚠️ Failed to report coverage to cloud: ${errorMsg}`);
|
|
275
|
-
}
|
|
276
|
-
return { sent: false, count: 0, error: errorMsg };
|
|
277
|
-
}
|
|
397
|
+
thisRun = { branch, context, runId, coverage };
|
|
398
|
+
const posted = await postCoverage(credentials, thisRun);
|
|
399
|
+
if (!posted.ok) {
|
|
400
|
+
return fail(requirementsDir, posted.reason, thisRun);
|
|
278
401
|
}
|
|
279
402
|
if (showCloudStatus) {
|
|
280
403
|
console.log(`\n✓ Reported ${tuplesToReport.length} requirement(s) to cloud (branch: ${branch})`);
|
|
281
404
|
}
|
|
282
|
-
// Update coverage cache with the full tuple set (includes those not
|
|
283
|
-
|
|
284
|
-
|
|
405
|
+
// Update coverage cache with the full tuple set (includes those not
|
|
406
|
+
// reported this run). The only path that may stamp lastRecordedAt and drop
|
|
407
|
+
// a standing failure — the cloud has affirmatively said success here.
|
|
408
|
+
//
|
|
409
|
+
// Guarded: the report already landed, so a full or read-only disk must not
|
|
410
|
+
// demote it to "Coverage did not record" under the success line. It does
|
|
411
|
+
// mean the cache still holds a failure this report just disproved — say so
|
|
412
|
+
// rather than let the next run repeat a cause Sam has already fixed.
|
|
413
|
+
const remembered = persist(() => recordCoverageReported(requirementsDir, testRunId, allTuples));
|
|
414
|
+
if (!remembered && showCloudStatus) {
|
|
415
|
+
console.error("\n⚠️ Coverage recorded, but .requirements/.cache could not be written — the next run will report it again.");
|
|
416
|
+
}
|
|
417
|
+
return { sent: true, count: tuplesToReport.length + replayedCount };
|
|
285
418
|
}
|
|
286
419
|
catch (error) {
|
|
287
420
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
288
|
-
|
|
289
|
-
|
|
421
|
+
const requirementsDir = findRequirementsDir(projectRoot);
|
|
422
|
+
if (requirementsDir) {
|
|
423
|
+
return fail(requirementsDir, errorMessage, thisRun);
|
|
290
424
|
}
|
|
291
425
|
return { sent: false, count: 0, error: errorMessage };
|
|
292
426
|
}
|
|
@@ -385,22 +519,48 @@ export async function finalize(options = {}) {
|
|
|
385
519
|
? Object.keys(lookup.requirements).filter((k) => !lookup.requirements[k].isAlias).length
|
|
386
520
|
: 0;
|
|
387
521
|
const coveragePercent = totalRequirements > 0 ? (testedKeys.length / totalRequirements) * 100 : 0;
|
|
388
|
-
// HARNESS-FINALIZE-
|
|
522
|
+
// HARNESS-FINALIZE-2, HARNESS-FINALIZE-3: Report to cloud.
|
|
523
|
+
// Runs before the local report so the report can say whether these figures
|
|
524
|
+
// reached the cloud (COVERAGE-REPORT-2.2).
|
|
525
|
+
let cloudResult = {
|
|
526
|
+
sent: false,
|
|
527
|
+
count: 0,
|
|
528
|
+
};
|
|
529
|
+
if (shouldReportToCloud) {
|
|
530
|
+
cloudResult = await reportToConvex(projectRoot, testRunId, aggregated, showCloudStatus, context);
|
|
531
|
+
}
|
|
532
|
+
// Read once, after reporting has had its say, for both the report header and
|
|
533
|
+
// the notice below.
|
|
534
|
+
const coverageCache = readCoverageCache(requirementsDir);
|
|
535
|
+
// This run's own report landing outranks anything the cache remembers: if
|
|
536
|
+
// the cache still holds a failure, this report just disproved it, and an
|
|
537
|
+
// unwritable cache must not make a landed report read as a failed one.
|
|
538
|
+
const recordedThisRun = cloudResult.sent;
|
|
539
|
+
// HARNESS-FINALIZE-1: Print local report.
|
|
540
|
+
// A run that didn't itself attempt a report (debounced, unlinked,
|
|
541
|
+
// reportToCloud off) can still be sitting on coverage that never landed —
|
|
542
|
+
// the header follows the cache, not just this run's outcome.
|
|
389
543
|
const shouldPrintLocal = showSummary || showTestedList || showUntestedList;
|
|
390
544
|
if (shouldPrintLocal) {
|
|
391
545
|
printLocalReport(testedKeys, lookup, {
|
|
392
546
|
showSummary,
|
|
393
547
|
showTestedList,
|
|
394
548
|
showUntestedList,
|
|
549
|
+
cloudUnrecorded: Boolean(cloudResult.error ??
|
|
550
|
+
(recordedThisRun ? null : coverageCache?.lastFailure)),
|
|
395
551
|
});
|
|
396
552
|
}
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
553
|
+
// COVERAGE-REPORT-2.1, -3.0: the run's last word is whether coverage
|
|
554
|
+
// recorded. Written to stderr so it cannot be silenced by a quiet run or
|
|
555
|
+
// swallowed by a caller parsing stdout — a failure nobody sees is the whole
|
|
556
|
+
// defect this replaces.
|
|
557
|
+
const unrecordedNotice = cloudResult.error
|
|
558
|
+
? formatFailureNotice(cloudResult.error, coverageCache)
|
|
559
|
+
: recordedThisRun
|
|
560
|
+
? null
|
|
561
|
+
: formatUnrecordedNotice(coverageCache);
|
|
562
|
+
if (unrecordedNotice) {
|
|
563
|
+
console.error(unrecordedNotice);
|
|
404
564
|
}
|
|
405
565
|
// HARNESS-FINALIZE-4: Clean up tracking data
|
|
406
566
|
if (shouldCleanup && !cloudResult.error) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a cloud refusal, and saying plainly that coverage did not record.
|
|
3
|
+
*
|
|
4
|
+
* Covers COVERAGE-REPORT-1 (a refusal keeps its reason) and the wording of
|
|
5
|
+
* COVERAGE-REPORT-2 / -3 (the run says coverage did not record, and keeps
|
|
6
|
+
* saying it until it does).
|
|
7
|
+
*/
|
|
8
|
+
import type { CoverageCache } from "./cache.js";
|
|
9
|
+
/**
|
|
10
|
+
* Shown when a refusal carries nothing a developer could act on
|
|
11
|
+
* (COVERAGE-REPORT-1.2). The raw `errorMessage` is deliberately not used in its
|
|
12
|
+
* place: production Convex redacts it to "[Request ID: …] Server Error", which
|
|
13
|
+
* tells the reader nothing and reads like a product defect.
|
|
14
|
+
*/
|
|
15
|
+
export declare const REFUSAL_WITHOUT_REASON = "the cloud refused the report without giving a reason";
|
|
16
|
+
/**
|
|
17
|
+
* A Convex HTTP API response body. A ConvexError's `{kind, message}` arrives in
|
|
18
|
+
* `errorData`; `errorMessage` is the raw (production-redacted) string.
|
|
19
|
+
*/
|
|
20
|
+
export interface ConvexErrorResponse {
|
|
21
|
+
status?: string;
|
|
22
|
+
errorMessage?: string;
|
|
23
|
+
errorData?: unknown;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Pull the developer-facing reason out of a refused Convex call.
|
|
27
|
+
*
|
|
28
|
+
* Mirrors the selection rules in `packages/web/lib/convex-error.ts`: a
|
|
29
|
+
* ConvexError's data is either a plain string or an object carrying `message`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function refusalReason(response: ConvexErrorResponse): string;
|
|
32
|
+
/**
|
|
33
|
+
* The block a failing run ends with (COVERAGE-REPORT-2.1): what happened, why,
|
|
34
|
+
* and how long coverage has been out of date.
|
|
35
|
+
*/
|
|
36
|
+
export declare function formatFailureNotice(reason: string, cache: CoverageCache | null, now?: number): string;
|
|
37
|
+
/**
|
|
38
|
+
* The standing notice for a run or report that did not itself attempt to
|
|
39
|
+
* report, but follows one that failed (COVERAGE-REPORT-3.0, -3.1).
|
|
40
|
+
*
|
|
41
|
+
* Returns null once a report has succeeded (COVERAGE-REPORT-3.3).
|
|
42
|
+
*/
|
|
43
|
+
export declare function formatUnrecordedNotice(cache: CoverageCache | null, now?: number): string | null;
|
|
44
|
+
//# sourceMappingURL=reportingStatus.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a cloud refusal, and saying plainly that coverage did not record.
|
|
3
|
+
*
|
|
4
|
+
* Covers COVERAGE-REPORT-1 (a refusal keeps its reason) and the wording of
|
|
5
|
+
* COVERAGE-REPORT-2 / -3 (the run says coverage did not record, and keeps
|
|
6
|
+
* saying it until it does).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Shown when a refusal carries nothing a developer could act on
|
|
10
|
+
* (COVERAGE-REPORT-1.2). The raw `errorMessage` is deliberately not used in its
|
|
11
|
+
* place: production Convex redacts it to "[Request ID: …] Server Error", which
|
|
12
|
+
* tells the reader nothing and reads like a product defect.
|
|
13
|
+
*/
|
|
14
|
+
export const REFUSAL_WITHOUT_REASON = "the cloud refused the report without giving a reason";
|
|
15
|
+
/**
|
|
16
|
+
* Pull the developer-facing reason out of a refused Convex call.
|
|
17
|
+
*
|
|
18
|
+
* Mirrors the selection rules in `packages/web/lib/convex-error.ts`: a
|
|
19
|
+
* ConvexError's data is either a plain string or an object carrying `message`.
|
|
20
|
+
*/
|
|
21
|
+
export function refusalReason(response) {
|
|
22
|
+
const data = response.errorData;
|
|
23
|
+
if (typeof data === "string" && data.trim()) {
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
if (data && typeof data === "object" && "message" in data) {
|
|
27
|
+
const { message } = data;
|
|
28
|
+
if (typeof message === "string" && message.trim()) {
|
|
29
|
+
return message;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return REFUSAL_WITHOUT_REASON;
|
|
33
|
+
}
|
|
34
|
+
const MONTHS = [
|
|
35
|
+
"January",
|
|
36
|
+
"February",
|
|
37
|
+
"March",
|
|
38
|
+
"April",
|
|
39
|
+
"May",
|
|
40
|
+
"June",
|
|
41
|
+
"July",
|
|
42
|
+
"August",
|
|
43
|
+
"September",
|
|
44
|
+
"October",
|
|
45
|
+
"November",
|
|
46
|
+
"December",
|
|
47
|
+
];
|
|
48
|
+
function formatDate(timestamp) {
|
|
49
|
+
const d = new Date(timestamp);
|
|
50
|
+
return `${d.getDate()} ${MONTHS[d.getMonth()]}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* How long ago, in the coarsest unit that still reads as a duration.
|
|
54
|
+
*/
|
|
55
|
+
function formatElapsed(ms) {
|
|
56
|
+
const minutes = Math.floor(ms / 60_000);
|
|
57
|
+
if (minutes < 1)
|
|
58
|
+
return "less than a minute";
|
|
59
|
+
if (minutes < 60)
|
|
60
|
+
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
61
|
+
const hours = Math.floor(minutes / 60);
|
|
62
|
+
if (hours < 24)
|
|
63
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
64
|
+
const days = Math.floor(hours / 24);
|
|
65
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* "Coverage last recorded 3 days ago (12 July)", or the never-recorded form
|
|
69
|
+
* for a checkout that has not had a successful report (COVERAGE-REPORT-3.2).
|
|
70
|
+
*/
|
|
71
|
+
function lastRecordedLine(cache, now) {
|
|
72
|
+
// Caches written before lastRecordedAt existed fall back to the debounce
|
|
73
|
+
// timestamp — telling someone who has reported for months that coverage has
|
|
74
|
+
// "never recorded" would be the worse lie. But that timestamp is refreshed
|
|
75
|
+
// by runs that contact nothing, so on its own it can claim coverage recorded
|
|
76
|
+
// a minute ago while reporting has been broken for days. A failure is proof
|
|
77
|
+
// coverage had not recorded by then: clamp to it.
|
|
78
|
+
const lastRecordedAt = cache?.lastRecordedAt ??
|
|
79
|
+
(cache?.current
|
|
80
|
+
? Math.min(cache.current.timestamp, cache.lastFailure?.timestamp ?? cache.current.timestamp)
|
|
81
|
+
: undefined);
|
|
82
|
+
if (lastRecordedAt === undefined) {
|
|
83
|
+
return "Coverage has never recorded from this checkout.";
|
|
84
|
+
}
|
|
85
|
+
return `Coverage last recorded ${formatElapsed(now - lastRecordedAt)} ago (${formatDate(lastRecordedAt)}).`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The block a failing run ends with (COVERAGE-REPORT-2.1): what happened, why,
|
|
89
|
+
* and how long coverage has been out of date.
|
|
90
|
+
*/
|
|
91
|
+
export function formatFailureNotice(reason, cache, now = Date.now()) {
|
|
92
|
+
return [
|
|
93
|
+
"",
|
|
94
|
+
"──────────────────────────────────────────────",
|
|
95
|
+
" Coverage did not record.",
|
|
96
|
+
` ${reason}`,
|
|
97
|
+
` ${lastRecordedLine(cache, now)}`,
|
|
98
|
+
" Your tests are unaffected.",
|
|
99
|
+
"──────────────────────────────────────────────",
|
|
100
|
+
"",
|
|
101
|
+
].join("\n");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The standing notice for a run or report that did not itself attempt to
|
|
105
|
+
* report, but follows one that failed (COVERAGE-REPORT-3.0, -3.1).
|
|
106
|
+
*
|
|
107
|
+
* Returns null once a report has succeeded (COVERAGE-REPORT-3.3).
|
|
108
|
+
*/
|
|
109
|
+
export function formatUnrecordedNotice(cache, now = Date.now()) {
|
|
110
|
+
const failure = cache?.lastFailure;
|
|
111
|
+
if (!failure)
|
|
112
|
+
return null;
|
|
113
|
+
return [
|
|
114
|
+
"",
|
|
115
|
+
"──────────────────────────────────────────────",
|
|
116
|
+
` ${lastRecordedLine(cache, now)}`,
|
|
117
|
+
` The last attempt, ${formatElapsed(now - failure.timestamp)} ago, failed:`,
|
|
118
|
+
` ${failure.reason}`,
|
|
119
|
+
"──────────────────────────────────────────────",
|
|
120
|
+
"",
|
|
121
|
+
].join("\n");
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=reportingStatus.js.map
|
package/dist/push/core.d.ts
CHANGED
|
@@ -16,12 +16,6 @@ export interface ParsedFile {
|
|
|
16
16
|
markdownContent: string;
|
|
17
17
|
/** Requirement count for display */
|
|
18
18
|
requirementCount: number;
|
|
19
|
-
/**
|
|
20
|
-
* DOC-HEADER-14: the frontmatter exactly as the user wrote it, including
|
|
21
|
-
* keys the schema doesn't recognize — merged back on the post-sync rewrite
|
|
22
|
-
* so those keys survive.
|
|
23
|
-
*/
|
|
24
|
-
rawFrontmatter?: Record<string, unknown>;
|
|
25
19
|
/** DOC-HEADER-11.4: true when defaultPrefix was inferred from the first requirement rather than read from frontmatter */
|
|
26
20
|
inferredDefaultPrefix?: boolean;
|
|
27
21
|
}
|
|
@@ -31,12 +25,6 @@ export declare const WEB_APP_URL = "https://app.dotrequirements.io";
|
|
|
31
25
|
* Extract markdown content from a file, stripping YAML frontmatter.
|
|
32
26
|
*/
|
|
33
27
|
export declare function extractMarkdownContent(rawContent: string): string;
|
|
34
|
-
/**
|
|
35
|
-
* DOC-HEADER-14: merge the validated (and sync-updated) metadata over the raw
|
|
36
|
-
* frontmatter so unrecognized keys survive the rewrite while the fields the
|
|
37
|
-
* sync owns (document ID, pulledAt, defaultPrefix, version) stay updated.
|
|
38
|
-
*/
|
|
39
|
-
export declare function mergeMetadataWithRawFrontmatter(metadata: Metadata, rawFrontmatter: Record<string, unknown> | undefined): Metadata;
|
|
40
28
|
/**
|
|
41
29
|
* Parse files for sync. Returns parsed files with metadata and content.
|
|
42
30
|
*
|