@openwop/openwop-conformance 2.33.2 → 2.34.1

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.
@@ -74,6 +74,7 @@ import { pollUntilTerminal, scaledTimeoutMs } from '../lib/polling.js';
74
74
  import { req } from '../lib/requirement-ids.js';
75
75
  import { receiverBinding, resolveRegistrationUrl } from '../lib/webhook-receiver.js';
76
76
  import { watchForResumption, type Observation, type Watch } from '../lib/durability-watch.js';
77
+ import { noteEvidence, parseRecoveryBounds, EVIDENCE_NAME_PATTERN } from '../lib/durability-evidence.js';
77
78
 
78
79
  const FIXTURE = 'conformance-noop';
79
80
  const FAILURE_FIXTURE = 'conformance-failure';
@@ -186,6 +187,32 @@ const KILL_ROW_TIMEOUT_MS = OBSERVATION_CEILING_MS + 120_000;
186
187
  /** Used only when the host serves no bound to read; named in the row's detail. */
187
188
  const UNDECLARED_BOUND_FALLBACK_MS = 60_000;
188
189
 
190
+ /**
191
+ * The declared recovery bounds, PER CLASS, read off the seam and normalised.
192
+ *
193
+ * Three response shapes are accepted, because the seam is non-normative and two
194
+ * hosts built it before this was written down:
195
+ * { classes: [{ class, bound, terms[] }] } — the preferred shape
196
+ * { classes: { <name>: { bound, terms[] } } } — a map keyed by class
197
+ * { bound, terms[], class? } — one class; named `class`
198
+ * or, absent that, `default`
199
+ * A top-level scalar `bound` beside `classes` is IGNORED: Unresolved Question 1
200
+ * resolved per class, and a convenience total is the aggregation it rejects.
201
+ * Only `{ name, ms }` survives from a term — free text a host attaches (the
202
+ * reference host's `enforcedBy`) never reaches a published bundle.
203
+ */
204
+ function readClasses(json: unknown): unknown[] | null {
205
+ const body = (json as { classes?: unknown; class?: unknown; bound?: unknown; terms?: unknown } | null) ?? {};
206
+ const strip = (cls: unknown, e: { bound?: unknown; terms?: unknown }): unknown => ({
207
+ class: cls, bound: e.bound,
208
+ terms: Array.isArray(e.terms) ? (e.terms as Array<{ name?: unknown; ms?: unknown }>).map((t) => ({ name: t?.name, ms: t?.ms })) : e.terms,
209
+ });
210
+ if (Array.isArray(body.classes)) return (body.classes as Array<{ class?: unknown; bound?: unknown; terms?: unknown }>).map((e) => strip(e?.class, e ?? {}));
211
+ if (body.classes !== null && typeof body.classes === 'object') return Object.entries(body.classes as Record<string, { bound?: unknown; terms?: unknown }>).map(([k, e]) => strip(k, e ?? {}));
212
+ if (body.bound !== undefined || body.terms !== undefined) return [strip(typeof body.class === 'string' ? body.class : 'default', body)];
213
+ return null;
214
+ }
215
+
189
216
  /**
190
217
  * How long to keep observing: the host's OWN declared recovery bound.
191
218
  *
@@ -199,19 +226,21 @@ const UNDECLARED_BOUND_FALLBACK_MS = 60_000;
199
226
  * bound is used as a CEILING FOR WAITING only; it is never asserted as a
200
227
  * scalar here (`bound-is-derived` owns the arithmetic).
201
228
  */
202
- async function declaredBoundMs(fired: unknown): Promise<{ ms: number; declared: boolean }> {
229
+ async function declaredBoundMs(fired: unknown): Promise<{ ms: number; declared: boolean; recoveryClass: string | null }> {
230
+ const named = (fired as { recoveryClass?: unknown } | null)?.recoveryClass;
231
+ const recoveryClass = typeof named === 'string' && EVIDENCE_NAME_PATTERN.test(named) ? named : null;
203
232
  // A host whose bound is PER CLASS (Unresolved Question 1: unleased work waits
204
233
  // out an outbox lease, leased work a dispatch lease — 65 s against 750 s on
205
234
  // one measured host) names the figure that governs THIS work on the seam's
206
235
  // own response. The bare read below returns one class and would report the
207
236
  // interval against a bound that does not govern the staged run.
208
237
  const governing = (fired as { recoveryBoundMs?: unknown } | null)?.recoveryBoundMs;
209
- if (typeof governing === 'number' && Number.isFinite(governing) && governing > 0) return { ms: governing, declared: true };
238
+ if (typeof governing === 'number' && Number.isFinite(governing) && governing > 0) return { ms: governing, declared: true, recoveryClass };
210
239
  const r = await driver.get('/host/durability/bound');
211
240
  const bound = (r.json as { bound?: unknown } | null)?.bound;
212
241
  return r.status === 200 && typeof bound === 'number' && Number.isFinite(bound) && bound > 0
213
- ? { ms: bound, declared: true }
214
- : { ms: UNDECLARED_BOUND_FALLBACK_MS, declared: false };
242
+ ? { ms: bound, declared: true, recoveryClass }
243
+ : { ms: UNDECLARED_BOUND_FALLBACK_MS, declared: false, recoveryClass };
215
244
  }
216
245
 
217
246
  /**
@@ -294,6 +323,15 @@ describe('v2-durability-recovery (RFC 0158 §B.4, §D.9, §E — the durable-sin
294
323
  w.resumedAfterMs !== null,
295
324
  req('openwop.requirement.0158.kill-after-accept', 'RFC 0158 §B.4', `work accepted before a real process death MUST dispatch on resume within the declared recovery bound — service answered again after ${backIn}ms, then observed for ${w.waitedMs}ms against a ${bound.declared ? `declared ${bound.ms}ms bound` : `${bound.ms}ms fallback (the host serves no /host/durability/bound)`}: ${w.last.runStarted} run.started, ${w.last.nodeStarted} node.started, ${w.last.restored} restored${w.resumedAfterMs !== null ? `; dispatch first observed ${backIn + w.resumedAfterMs}ms after the kill` : ''}`),
296
325
  ).toBe(true);
326
+ // RFC 0158 §E, 2.34.0: the interval this row MEASURED is evidence, and until
327
+ // now it existed only inside a failure message — a passing row recorded
328
+ // nothing, so a bundle could not show what was observed or against which
329
+ // class. Recorded only when the seam NAMED the class it exercised: without
330
+ // that the bundle cannot bind this exercise to a declared bound, the rung is
331
+ // not derivable, and saying so is more honest than guessing a class.
332
+ if (bound.declared && bound.recoveryClass !== null && w.resumedAfterMs !== null) {
333
+ noteEvidence({ recovery: { class: bound.recoveryClass, boundMs: Math.round(bound.ms), observedMs: backIn + w.resumedAfterMs } });
334
+ }
297
335
  }, KILL_ROW_TIMEOUT_MS);
298
336
 
299
337
  it('work executing at a real process death is never reported complete, and resumes', async () => {
@@ -341,6 +379,15 @@ describe('v2-durability-recovery (RFC 0158 §B.4, §D.9, §E — the durable-sin
341
379
  w.resumedAfterMs !== null,
342
380
  req('openwop.requirement.0158.kill-during-execution', 'RFC 0158 §B.4 / §E item 11', `work executing at a real process death MUST resume within the declared recovery bound; §B.4 measures kill → RESUMPTION, never kill → terminal — service answered again after ${backIn}ms, then observed for ${w.waitedMs}ms against a ${bound.declared ? `declared ${bound.ms}ms bound` : `${bound.ms}ms fallback (the host serves no /host/durability/bound)`}: ${w.last.runStarted} run.started, ${w.last.restored} restored${w.resumedAfterMs !== null ? `; resumption first observed ${backIn + w.resumedAfterMs}ms after the kill` : ''}`),
343
381
  ).toBe(true);
382
+ // RFC 0158 §E, 2.34.0: the interval this row MEASURED is evidence, and until
383
+ // now it existed only inside a failure message — a passing row recorded
384
+ // nothing, so a bundle could not show what was observed or against which
385
+ // class. Recorded only when the seam NAMED the class it exercised: without
386
+ // that the bundle cannot bind this exercise to a declared bound, the rung is
387
+ // not derivable, and saying so is more honest than guessing a class.
388
+ if (bound.declared && bound.recoveryClass !== null && w.resumedAfterMs !== null) {
389
+ noteEvidence({ recovery: { class: bound.recoveryClass, boundMs: Math.round(bound.ms), observedMs: backIn + w.resumedAfterMs } });
390
+ }
344
391
  }, KILL_ROW_TIMEOUT_MS);
345
392
 
346
393
  it('the same accepted work delivered twice fires each effect exactly once', async () => {
@@ -427,24 +474,25 @@ describe('v2-durability-recovery (RFC 0158 §B.4, §D.9, §E — the durable-sin
427
474
  if (terms.status === 404 || terms.status === 405) {
428
475
  return softSkip('blocked', 'the host exposes the durability seam but serves no recovery-bound terms — §E puts the per-class arithmetic in the RFC 0148 evidence bundle where a reader can recompute it, and there is nothing here to recompute');
429
476
  }
430
- const body = (terms.json as { bound?: unknown; terms?: unknown } | null) ?? {};
431
- const bound = typeof body.bound === 'number' ? body.bound : null;
432
- const parts = Array.isArray(body.terms) ? (body.terms as unknown[]) : null;
433
- if (bound === null || parts === null || parts.length === 0) {
434
- return softSkip('blocked', 'the recovery-bound response names no { bound, terms[] } — §E asks for the per-class arithmetic, not a single total (Unresolved Question 1), and a total alone cannot be recomputed');
477
+ const classes = readClasses(terms.json);
478
+ if (classes === null) {
479
+ return softSkip('blocked', 'the recovery-bound response names no classes and no { bound, terms[] } — §E asks for the per-class arithmetic, not a single total (Unresolved Question 1), and a total alone cannot be recomputed');
435
480
  }
436
- const summed = parts.reduce<number>((acc, t) => acc + (typeof (t as { ms?: unknown }).ms === 'number' ? (t as { ms: number }).ms : Number.NaN), 0);
437
481
  // THIS ROW IS A PAPER CHECK BY CONSTRUCTION AND THE RFC SAYS SO. It checks
438
- // that the declared number follows from the stated mechanism. It cannot
482
+ // that each declared number follows from the stated mechanism. It cannot
439
483
  // check that the mechanism RUNS: a host whose sweeper wedges has a
440
484
  // derivation that stays perfectly correct while the bound is not produced
441
485
  // at all — a run sat unclaimed for 16 minutes against a derived bound of
442
486
  // 12.5 with every isolated check of the mechanism passing. Only the kill
443
487
  // rows above witness liveness, and this row MUST NOT be cited for it.
488
+ const parsed = parseRecoveryBounds(classes);
444
489
  expect(
445
- Number.isFinite(summed) && Math.abs(summed - bound) <= 1,
446
- req('openwop.requirement.0158.bound-is-derived', 'RFC 0158 §B.5', `the declared recovery bound MUST follow from the per-class terms that produce it — declared ${bound}, terms sum to ${summed}. A host that states a bound it cannot produce fails; a host whose sweeper wedges still passes, which is why this row MUST NOT be read as evidence that the mechanism runs`),
490
+ parsed.ok,
491
+ req('openwop.requirement.0158.bound-is-derived', 'RFC 0158 §B.5', `every declared recovery bound MUST follow from the per-class terms that produce it, each term { name, ms } — ${parsed.ok ? '' : parsed.why}. A host that states a bound it cannot produce fails; this checks the ARITHMETIC only, never that the mechanism runs`),
447
492
  ).toBe(true);
493
+ // The derivation goes INTO THE BUNDLE, per class, where a reader can
494
+ // recompute it (§E) — until 2.34.0 it lived only on this seam route.
495
+ if (parsed.ok) noteEvidence({ recoveryBounds: parsed.bounds });
448
496
  }, 120_000);
449
497
 
450
498
  it('deterministically failing work reaches a terminal state and stops being retried', async () => {
@@ -34,8 +34,9 @@ import { v2Discovery, gateFamily } from '../lib/v2.js';
34
34
  import { projectBoundId } from '../lib/bound-id.js';
35
35
  import { receiverBinding, resolveRegistrationUrl } from '../lib/webhook-receiver.js';
36
36
  import { readErrorCode } from '../lib/error-envelope.js';
37
- import { softSkip } from '../lib/soft-skip.js';
37
+ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
38
38
  import { req } from '../lib/requirement-ids.js';
39
+ import { retryWaitCapMs, retryWaitFor, windowClosedNote } from '../lib/webhook-retry-window.js';
39
40
 
40
41
  const FIXTURE = 'conformance-noop';
41
42
  const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
@@ -167,14 +168,14 @@ function advertisedRetryPolicy(doc: Record<string, unknown>): { maxAttempts?: nu
167
168
  * which covers a 30 s first attempt with room for the second. The cap is
168
169
  * deliberate: unbounded waiting would let a host that never retries hold the
169
170
  * suite open instead of failing.
171
+ *
172
+ * 2.34.1: and 90 s was the same defect again, one schedule further out — see
173
+ * `lib/webhook-retry-window.ts`. The cap is now operator-RAISABLE through
174
+ * `OPENWOP_WEBHOOK_RETRY_WAIT_MS` and never lowerable; still bounded.
170
175
  */
171
- const RETRY_WAIT_FLOOR_MS = 20_000;
172
- const RETRY_WAIT_CAP_MS = 90_000;
176
+ const RETRY_WAIT_CAP_MS = retryWaitCapMs();
173
177
  function retryWaitMs(doc: Record<string, unknown>): number {
174
- const policy = advertisedRetryPolicy(doc);
175
- if (policy === null) return RETRY_WAIT_FLOOR_MS;
176
- const backoff = String(policy.backoff ?? '');
177
- return backoff === 'exponential' || backoff === 'fixed' ? RETRY_WAIT_CAP_MS : RETRY_WAIT_FLOOR_MS;
178
+ return retryWaitFor(advertisedRetryPolicy(doc), RETRY_WAIT_CAP_MS);
178
179
  }
179
180
 
180
181
  /**
@@ -330,8 +331,26 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
330
331
  const sub = await register(receiver.url);
331
332
  if (sub === null) return softSkip('blocked', 'registration refused (reason recorded above)');
332
333
 
334
+ // The row id is the FIRST thing this leg names: `register()` asserts under
335
+ // the base `0173.webhook-durable-delivery` id, and a leg that returned before
336
+ // any `.dead-letter` req() ran recorded its outcome under THAT id, leaving
337
+ // `.dead-letter` with no row in the bundle at all (measured, 2.34.1 draft).
338
+ req('openwop.requirement.0173.webhook-durable-delivery.dead-letter', 'webhooks.md §Durability', 'an exhausted delivery MUST be routed to the sink, not dropped');
333
339
  const create = await driver.post('/runs', { workflowId: FIXTURE });
334
- expect(create.status, req('openwop.requirement.0173.webhook-durable-delivery.dead-letter', 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
340
+ // 2.34.1 — NO OBLIGATION IS ASSERTED UNTIL THE WINDOW QUESTION IS ANSWERED. A leg
341
+ // that asserts and THEN soft-skips records `executed-pass` with a
342
+ // `partial-witness:` detail (`resolveItRecord`), which certifies. The first
343
+ // draft of this fix asserted `create.status` and the retry, then
344
+ // soft-skipped `blocked` on a closed window, and so turned a false FAIL into
345
+ // a pass that never looked at the sink — caught by measuring it: five rows
346
+ // `executed-pass`, 106 s, on a host whose first delivery exhausts at 225 s.
347
+ // So everything is OBSERVED first, the one inconclusive case returns with
348
+ // zero assertions (a real `blocked`, which denies certification), and only
349
+ // then are the obligations asserted, in their original order. A failure the
350
+ // observations already show still fails: the create check fires at once.
351
+ if (create.status !== 201) {
352
+ expect(create.status, req('openwop.requirement.0173.webhook-durable-delivery.dead-letter', 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
353
+ }
335
354
  const runId = (create.json as { runId: string }).runId;
336
355
  await waitTerminal(runId, 10_000);
337
356
  // Filter by the delivery's own SUBSCRIPTION, not just its run. A host that
@@ -344,19 +363,11 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
344
363
  const ours = () => receiver.attempts.filter((a) => a.runId === runId && a.webhookId === sub.webhookId);
345
364
  await waitFor(() => ours().length > 1, retryWaitMs(doc));
346
365
  const attempts = ours();
347
- expect(
348
- attempts.length,
349
- req('openwop.requirement.0173.webhook-durable-delivery.dead-letter', 'webhooks.md §Durability', 'a delivery that keeps failing MUST be retried before it can be exhausted (one attempt is a drop)'),
350
- ).toBeGreaterThan(1);
351
366
  const policy = advertisedRetryPolicy(doc);
352
- if (policy?.maxAttempts !== undefined) {
367
+ if (policy?.maxAttempts !== undefined && attempts.length > 1) {
353
368
  // Give the policy time to exhaust, then the host MUST stop.
354
369
  await waitFor(() => ours().length >= policy.maxAttempts!, retryWaitMs(doc));
355
370
  await new Promise((r) => setTimeout(r, 1_000));
356
- expect(
357
- ours().length,
358
- req('openwop.requirement.0173.webhook-durable-delivery.dead-letter', 'webhooks.md §Durability', `retries MUST stop at the advertised retryPolicy.maxAttempts (${policy.maxAttempts}) — exhaustion routes to the dead-letter sink, not to an unbounded loop`),
359
- ).toBeLessThanOrEqual(policy.maxAttempts);
360
371
  }
361
372
  // The sink itself. Until RFC 0188 this was an UNCONDITIONAL soft-skip on
362
373
  // every host — `webhooks.md` §Durability said the sink was "inspectable for
@@ -364,14 +375,41 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
364
375
  // every bundle recorded "exhaustion was observed, routing to the sink was
365
376
  // not". The read exists now; read it before deleting the subscription.
366
377
  const fam = await gateFamily('webhooks');
367
- if (!fam?.['deadLetter']) {
368
- await driver.delete(`/webhooks/${encodeURIComponent(sub.webhookId)}`);
369
- return softSkip('inapplicable', 'host does not advertise the webhooks.deadLetter facet — RFC 0188 §A.5 makes the read a 404 rather than an obligation, so the sink half of §Durability is unwitnessable here (the retry half above passed)');
378
+ let inSink: boolean | null = null;
379
+ if (fam?.['deadLetter']) {
380
+ const sink = await driver.get(`/webhooks/${projectBoundId(sub.webhookId)}/dead-letters`);
381
+ inSink = sink.status === 200 && ((sink.json as { deliveries?: unknown[] } | null)?.deliveries ?? []).some((r) => (r as Record<string, unknown>)['runId'] === runId);
370
382
  }
371
- const sink = await driver.get(`/webhooks/${projectBoundId(sub.webhookId)}/dead-letters`);
372
383
  await driver.delete(`/webhooks/${encodeURIComponent(sub.webhookId)}`);
384
+ // "Routed to the sink, not dropped" is a claim about an EXHAUSTED delivery,
385
+ // and until 2.34.1 it was asserted whether or not exhaustion had been
386
+ // observed: a host retrying at 15 / 30 / 60 / 120 s had made 4 of its 5
387
+ // attempts when the 90 s window closed, and this row said it had dropped a
388
+ // delivery still in flight. Retried, fewer attempts than advertised, and
389
+ // nothing in the sink is a window that closed early OR a host that stopped
390
+ // retrying — indistinguishable without an interval on the wire — so it is
391
+ // `blocked`, never a conviction. Reached maxAttempts with nothing in the
392
+ // sink still FAILS below; so does a delivery that was never retried.
393
+ const seen = ours().length;
394
+ if (inSink === false && policy?.maxAttempts !== undefined && attempts.length > 1 && seen < policy.maxAttempts) {
395
+ return blockedDespiteAssertions(windowClosedNote(seen, policy.maxAttempts, retryWaitMs(doc), RETRY_WAIT_CAP_MS));
396
+ }
397
+ expect(create.status, req('openwop.requirement.0173.webhook-durable-delivery.dead-letter', 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
398
+ expect(
399
+ attempts.length,
400
+ req('openwop.requirement.0173.webhook-durable-delivery.dead-letter', 'webhooks.md §Durability', 'a delivery that keeps failing MUST be retried before it can be exhausted (one attempt is a drop)'),
401
+ ).toBeGreaterThan(1);
402
+ if (policy?.maxAttempts !== undefined) {
403
+ expect(
404
+ ours().length,
405
+ req('openwop.requirement.0173.webhook-durable-delivery.dead-letter', 'webhooks.md §Durability', `retries MUST stop at the advertised retryPolicy.maxAttempts (${policy.maxAttempts}) — exhaustion routes to the dead-letter sink, not to an unbounded loop`),
406
+ ).toBeLessThanOrEqual(policy.maxAttempts);
407
+ }
408
+ if (inSink === null) {
409
+ return softSkip('inapplicable', 'host does not advertise the webhooks.deadLetter facet — RFC 0188 §A.5 makes the read a 404 rather than an obligation, so the sink half of §Durability is unwitnessable here (the retry half above passed)');
410
+ }
373
411
  expect(
374
- sink.status === 200 && ((sink.json as { deliveries?: unknown[] } | null)?.deliveries ?? []).some((r) => (r as Record<string, unknown>)['runId'] === runId),
412
+ inSink,
375
413
  req('openwop.requirement.0173.webhook-durable-delivery.dead-letter', 'webhooks.md §Durability', 'an exhausted delivery MUST be routed to the sink, not dropped — the half no bundle could witness before RFC 0188 served a read'),
376
414
  ).toBe(true);
377
415
  }, DEAD_LETTER_TEST_TIMEOUT_MS);
@@ -424,10 +462,14 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
424
462
  active = receiver.server;
425
463
  const sub = await register(receiver.url);
426
464
  if (sub === null) return softSkip('blocked', 'registration refused (reason recorded above)');
465
+ // Same two traps as the leg above (2.34.1): `register()` asserts under the base
466
+ // id, so name this row first; and every `blocked` after it must STAND rather
467
+ // than fold into a partial-witness pass that certifies.
468
+ req('openwop.requirement.0188.dead-letter-content-free', 'RFC 0188 §B.1', 'a dead-letter record MUST NOT carry the delivered body, headers or subscription secret');
427
469
  const create = await driver.post('/runs', { workflowId: FIXTURE });
428
470
  if (create.status !== 201) {
429
471
  await driver.delete(`/webhooks/${encodeURIComponent(sub.webhookId)}`);
430
- return softSkip('blocked', `POST /runs answered ${create.status} — no delivery to exhaust`);
472
+ return blockedDespiteAssertions(`POST /runs answered ${create.status} — no delivery to exhaust`);
431
473
  }
432
474
  const runId = (create.json as { runId: string }).runId;
433
475
  await waitTerminal(runId, 10_000);
@@ -441,12 +483,16 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
441
483
 
442
484
  const sink = await driver.get(`/webhooks/${projectBoundId(sub.webhookId)}/dead-letters`);
443
485
  await driver.delete(`/webhooks/${encodeURIComponent(sub.webhookId)}`);
444
- if (sink.status !== 200) return softSkip('blocked', `the dead-letter read answered ${sink.status}`);
486
+ if (sink.status !== 200) return blockedDespiteAssertions(`the dead-letter read answered ${sink.status}`);
445
487
  const rows = ((sink.json as { deliveries?: Array<Record<string, unknown>> } | null)?.deliveries ?? []);
446
488
  // An empty sink is NOT a pass. Recording one as a pass is exactly the
447
489
  // vacuous witness this leg used to produce; say so instead.
448
490
  if (rows.length === 0) {
449
- return softSkip('blocked', 'the exhausted delivery did not reach the sink inside the retry window — §B.1 is a claim about a real record and there is none here to read');
491
+ const seen = ours().length;
492
+ const why = policy?.maxAttempts !== undefined && seen < policy.maxAttempts
493
+ ? windowClosedNote(seen, policy.maxAttempts, retryWaitMs(doc), RETRY_WAIT_CAP_MS)
494
+ : `the delivery made ${seen} attempt(s) and is not in the sink`;
495
+ return blockedDespiteAssertions(`§B.1 is a claim about a real record and there is none here to read — ${why}`);
450
496
  }
451
497
  expect(
452
498
  rows.every((r) => r['body'] === undefined && r['headers'] === undefined && r['secret'] === undefined),
package/src/setup.ts CHANGED
@@ -43,6 +43,7 @@ import { SPEC_COHERENCE_SCENARIOS, SPEC_COHERENCE_DETAIL } from './lib/spec-cohe
43
43
  import type { DiscoveryPayload } from './lib/profiles.js';
44
44
  import { targetMajor } from './lib/seams.js';
45
45
  import { softSkip } from './lib/soft-skip.js';
46
+ import { takeNotedEvidence } from './lib/durability-evidence.js';
46
47
 
47
48
  // 20 s, not 5: a Cloud Run cold start routinely exceeds 5 s, and a discovery
48
49
  // fetch that aborted at init used to turn every fixture-gated scenario into a
@@ -457,7 +458,8 @@ afterEach(({ task }) => {
457
458
  detail = rec.detail;
458
459
  }
459
460
  try {
460
- recordRequirement(itId, disposition, detail, { assertionCount: calls, scenarioFile: file });
461
+ const evidence = takeNotedEvidence();
462
+ recordRequirement(itId, disposition, detail, { assertionCount: calls, scenarioFile: file, ...(evidence === null ? {} : { evidence }) });
461
463
  } catch {
462
464
  /* never fail a test for bookkeeping */
463
465
  }