@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.
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 +1 -1
  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
@@ -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 += "\n=== Requirements Coverage Report ===\n";
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 { projectId, projectSecret } = projectInfo.credentials;
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 coverageCache = readCoverageCache(requirementsDir);
166
- const tuplesToReport = allTuples.filter((tuple) => needsReporting(tuple, coverageCache));
332
+ const tuplesToReport = strandedLegacyFailure
333
+ ? allTuples
334
+ : allTuples.filter((tuple) => needsReporting(tuple, coverageCache));
167
335
  if (tuplesToReport.length === 0) {
168
- if (showCloudStatus) {
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
- updateCoverageCache(requirementsDir, testRunId, allTuples);
173
- return { sent: false, count: 0 };
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
- // Send to Convex
224
- // Note: projectId from env is always a slug (e.g., "reduced-cephalopod-288"),
225
- // not a Convex ID. Use projectSlug field for authentication and slug for target.
226
- const response = await fetch(`${CONVEX_URL}/api/mutation`, {
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 reported this run)
283
- updateCoverageCache(requirementsDir, testRunId, allTuples);
284
- return { sent: true, count: tuplesToReport.length };
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
- if (showCloudStatus) {
289
- console.warn(`\n⚠️ Error reporting coverage to cloud: ${errorMessage}`);
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-1: Print local report
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
- // HARNESS-FINALIZE-2, HARNESS-FINALIZE-3: Report to cloud
398
- let cloudResult = {
399
- sent: false,
400
- count: 0,
401
- };
402
- if (shouldReportToCloud) {
403
- cloudResult = await reportToConvex(projectRoot, testRunId, aggregated, showCloudStatus, context);
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
@@ -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
  *