@openwop/openwop-conformance 2.44.1 → 2.44.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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.44.3] — 2026-09-28 — fifteen more core families get v2 normative homes, and webhook retry rows record the wait path they measured
4
+
5
+ - **Webhook retry rows say which wait path ran and what they measured** (RFC 0225 witness). A passing dead-letter row carried no detail, so a bundle could not show whether its wait came from the host's advertised `retryPolicy.maxElapsedMs`, an operator-raised cap or the 20 s floor, or when the dead-letter arrived. MyndHyve's 2.44.1 cut was the case: it advertises 600000, and it could not be counted as RFC 0225's witness. Rows driven by `retryWaitFor` now carry an informational detail, on passes too. `0173.webhook-durable-delivery.dead-letter` and `0188.dead-letter-content-free` record, for example, `observed: advertised-bound (maxElapsedMs 600000): waited ≤630000ms; 5 attempt(s), last attempt after 225031ms, sink after 231412ms`. The last attempt is a lower bound on the dead-letter time, and the sink read an upper bound. The retry leg, `v2-webhook-unregister-stops-delivery` and `v2-webhook-message-id-stable` record the path and window. The detail is prefixed `observed: `, never `partial-witness: `, so `check-accepted-predicate` counts the row as a full witness. The bundle already carries `detail` on every row inside the witness digest, so the note is signed. New `lib/row-observation.ts`, attached in `setup.ts`; `retryWaitSelection` / `waitObservation` in `lib/webhook-retry-window.ts`; server-free tests in `row-observation.test.ts` and `webhook-retry-window.test.ts`, each sabotage-checked.
6
+ - **The 2.44.3 cycle opens.** Fifteen core families get a v2 normative home (RFC 0189), which changes the packed `@openwop/spec-artifacts` tree. v1-dependent core families fall from 23 to 8. No scenario changes: the rules are restated, none added.
7
+ - `events.md`: `feedback`, `providerUsage`, `envelopes`, and the rest of `heartbeat`.
8
+ - `host-services.md`: `secrets`, `modelCapabilities`, `scheduling`, `queueBus`, `toolHooks`, `httpClient`; `runs.md`: `deadLetter`.
9
+ - New `execution.md`: `selfHostedRunner`, `subWorkflow`, `multiAgent` (the family claim and `executionModel` facet); `identity.md` §1.5: `anonymousActor`.
10
+
11
+ ## [2.44.2] — 2026-09-28 — RFC 0215's no-head-of-line leg does not convict a late fan-out, and says when it sampled each count
12
+
13
+ - **The 2.44.2 cycle opens.** A patch: the requirement is unchanged, and one verdict branch that convicted without the contention it names becomes unjudged. `@openwop/spec-artifacts` moves in lockstep at the same exact pin.
14
+ - **`0215.no-head-of-line` no longer fails a host whose `run.started` fan-out is late.** The last branch failed the row as "a bounded dispatcher below the floor" whenever fewer than 8 held attempts had arrived by the time the run was terminal. MyndHyve `00821-qec` hit it twice. In the first cut no held attempt had arrived and the healthy one was delivered. In the second, 2 held attempts had arrived at terminal and 7 later, and the healthy attempt started with 7 held open and none closed. §A forbids an attempt waiting for another subscription's attempt to *finish*. None had finished, so the healthy attempt waited for nothing, and the contention §A.2 names was never established. The row is now `blocked` (unjudged) in that case. A bounded pool still fails: its healthy attempt starts only after a held attempt closes, or never while they stay open. So does a serial dispatcher, through the peak assertion.
15
+ - **The detail names each sampling time.** It read "2 held attempt(s) arrived (at most 7 open at once)", with the 2 sampled at terminal and the 7 at the verdict. It now reports what had arrived and was open at terminal, what had arrived by the verdict and the peak, when the healthy attempt arrived, and whether the host had closed any held attempt. It no longer names a cause the data does not show.
16
+ - **The verdict is a pure function with self-tests** (`lib/delivery-isolation-verdict.ts`). The tests cover pass, the contention-present fail, a 7-slot pool with a 15 s timeout (fail), a serial dispatcher (peak assertion), and both MyndHyve cuts (unjudged). Sabotage-checked: removing the new branch fails the two MyndHyve cases.
17
+
3
18
  ## [2.44.1] — 2026-09-28 — the webhook retry legs outlast an advertised maxElapsedMs, and four families get v2 normative homes
4
19
 
5
20
  - **The 2.44.1 cycle opens.** `workspace`, `content`, `triggerBridge` and `uiPlugins` get v2 normative homes (RFC 0189), which changes the packed `@openwop/spec-artifacts` tree (`spec/v2/core/host-services.md`, `i18n.md`, `webhooks.md`, `packs.md`, `spec/v2/declaration.json`) after `v2.44.0` was tagged. No scenario changes: the rules are restated from the v1 text, none added or strengthened.
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  # --legacy-peer-deps is REQUIRED, not optional: the exact peer pin is what npm's
12
12
  # default resolver refuses. npm 10.9 fails outright with
13
13
  # "Cannot read properties of null (reading 'edgesOut')" — use npm >= 11.
14
- npm install --legacy-peer-deps @openwop/openwop-conformance@2.44.1 @openwop/spec-artifacts@2.44.1
14
+ npm install --legacy-peer-deps @openwop/openwop-conformance@2.44.3 @openwop/spec-artifacts@2.44.3
15
15
  # or run without install:
16
16
  npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
17
17
  ```
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.44.1",
4
- "stampSha256": "c9a9550d05910b204589704d1e2c83fac85656062d8374a94e5afedf00b011e3"
3
+ "version": "2.44.3",
4
+ "stampSha256": "bc74ca389abf79ec8f23a07b8bb2742a6a5bab984fd5a6a06c56299dcf8b7076"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.44.1",
3
+ "version": "2.44.3",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,6 +56,6 @@
56
56
  "@openwop/spec-artifacts": "file:../spec-artifacts"
57
57
  },
58
58
  "peerDependencies": {
59
- "@openwop/spec-artifacts": "2.44.1"
59
+ "@openwop/spec-artifacts": "2.44.3"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -34246,7 +34246,7 @@
34246
34246
  {
34247
34247
  "id": "openwop.requirement.0215.no-head-of-line",
34248
34248
  "file": "v2-webhook-delivery-isolation.test.ts",
34249
- "line": 113,
34249
+ "line": 119,
34250
34250
  "title": null,
34251
34251
  "explicitId": "openwop.requirement.0215.no-head-of-line",
34252
34252
  "citations": [
@@ -34273,11 +34273,6 @@
34273
34273
  "requirement": null,
34274
34274
  "interpolated": true
34275
34275
  },
34276
- {
34277
- "section": "webhooks.md §Durability (RFC 0215 §A.1, §A.2)",
34278
- "requirement": null,
34279
- "interpolated": true
34280
- },
34281
34276
  {
34282
34277
  "section": "webhooks.md §Durability (RFC 0215 §A.1, §A.2)",
34283
34278
  "requirement": null,
@@ -34360,7 +34355,7 @@
34360
34355
  {
34361
34356
  "id": "openwop.it.v2-webhook-durable-delivery.a-failed-attempt-is-retried-and-the-event-is-delivered-at-least-once",
34362
34357
  "file": "v2-webhook-durable-delivery.test.ts",
34363
- "line": 263,
34358
+ "line": 279,
34364
34359
  "title": "a failed attempt is retried and the event is delivered at least once",
34365
34360
  "explicitId": "openwop.requirement.0173.webhook-durable-delivery",
34366
34361
  "citations": [
@@ -34397,7 +34392,7 @@
34397
34392
  {
34398
34393
  "id": "openwop.it.v2-webhook-durable-delivery.an-exhausted-delivery-is-dead-lettered-never-dropped",
34399
34394
  "file": "v2-webhook-durable-delivery.test.ts",
34400
- "line": 383,
34395
+ "line": 400,
34401
34396
  "title": "an exhausted delivery is dead-lettered, never dropped",
34402
34397
  "explicitId": "openwop.requirement.0173.webhook-durable-delivery.dead-letter",
34403
34398
  "citations": [
@@ -34436,7 +34431,7 @@
34436
34431
  {
34437
34432
  "id": "openwop.it.v2-webhook-durable-delivery.the-dead-letter-read-is-served-and-its-records-carry-no-payload",
34438
34433
  "file": "v2-webhook-durable-delivery.test.ts",
34439
- "line": 491,
34434
+ "line": 509,
34440
34435
  "title": "the dead-letter read is served and its records carry no payload",
34441
34436
  "explicitId": "openwop.requirement.0188.dead-letter-read",
34442
34437
  "citations": [
@@ -34449,7 +34444,7 @@
34449
34444
  {
34450
34445
  "id": "openwop.it.v2-webhook-durable-delivery.after-unregister-the-dead-letter-read-answers-404-not-found-as-for-a-subscriptio",
34451
34446
  "file": "v2-webhook-durable-delivery.test.ts",
34452
- "line": 512,
34447
+ "line": 530,
34453
34448
  "title": "after unregister, the dead-letter read answers 404 not_found, as for a subscription that never existed",
34454
34449
  "explicitId": "openwop.requirement.0217.dead-letter-read-after-unregister",
34455
34450
  "citations": [
@@ -34476,7 +34471,7 @@
34476
34471
  {
34477
34472
  "id": "openwop.it.v2-webhook-durable-delivery.a-dead-letter-record-carries-no-delivered-payload",
34478
34473
  "file": "v2-webhook-durable-delivery.test.ts",
34479
- "line": 545,
34474
+ "line": 563,
34480
34475
  "title": "a dead-letter record carries no delivered payload",
34481
34476
  "explicitId": "openwop.requirement.0188.dead-letter-content-free",
34482
34477
  "citations": [
@@ -34620,7 +34615,7 @@
34620
34615
  {
34621
34616
  "id": "openwop.it.v2-webhook-message-id-stable.every-attempt-of-one-delivery-carries-one-webhook-id-and-distinct-deliveries-car",
34622
34617
  "file": "v2-webhook-message-id-stable.test.ts",
34623
- "line": 69,
34618
+ "line": 70,
34624
34619
  "title": "every attempt of one delivery carries one webhook-id, and distinct deliveries carry distinct ids",
34625
34620
  "explicitId": "openwop.requirement.0201.message-id-stable",
34626
34621
  "citations": [
@@ -34859,7 +34854,7 @@
34859
34854
  {
34860
34855
  "id": "openwop.it.v2-webhook-unregister-stops-delivery.no-attempt-for-an-unregistered-subscription-starts-after-the-204-while-its-contr",
34861
34856
  "file": "v2-webhook-unregister-stops-delivery.test.ts",
34862
- "line": 86,
34857
+ "line": 87,
34863
34858
  "title": "no attempt for an unregistered subscription starts after the 204, while its control keeps being retried",
34864
34859
  "explicitId": "openwop.requirement.0215.unregister-stops-delivery",
34865
34860
  "citations": [
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
3
3
  "package": "@openwop/spec-artifacts",
4
- "version": "2.44.1",
5
- "corpusTag": "v2.44.1",
4
+ "version": "2.44.3",
5
+ "corpusTag": "v2.44.3",
6
6
  "files": {
7
7
  "api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
8
8
  "api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
9
9
  "api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
10
10
  "api/openapi.yaml": "69464bc0e1e71ef3b543a73d4fa4f67202343d2603c80af8033f6c3619ef1b30",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "6f9780f7751804b6dd9f57cb5474728326de805fcca667979f6b4f39c9c97336",
13
- "api/v2/asyncapi.yaml": "ec6520792f7c4661403053003de7669430178597b5e0250ba5264b65dc82dbef",
14
- "api/v2/openapi.yaml": "46314e02c9aeb4cb7a4ca0cfd5997dc24bd11c3971341e505cd0b7bef3150cd8",
12
+ "api/seams-v2.yaml": "3319d19f95c90c82ad611d3698daa77062760aaed9c703000f0eaa93cdcb3ed3",
13
+ "api/v2/asyncapi.yaml": "fc977f137a3a40ac20c17911f9987e55f1b93ab5e73d039e94ab2749537ad4b3",
14
+ "api/v2/openapi.yaml": "353fe2c7b556dfa12f45f85783e0b1409ae3f6e59755719f08ba525b830189ac",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -204,50 +204,51 @@
204
204
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
205
205
  "spec/v1/alias-detectors.json": "29f21e0fb2c7a232a92a5c19b4cba3a86bc3b5e16f06780452f0421c66c6ec38",
206
206
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
207
- "spec/v1/core-standard-manifest.json": "b6efb5d51e1432088bfeff049f310ae0095f3852a59e877e9d628a30782f5580",
207
+ "spec/v1/core-standard-manifest.json": "8789a8c30013d9137db12e2cac6bd38b461a0f16147617366263b26bd265a1d8",
208
208
  "spec/v1/deprecations.json": "46df807fb4adf0aae24671c20f69975cf3496e3bc3a521cb7e6ea556bdce563c",
209
209
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
210
210
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
211
211
  "spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
212
212
  "spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
213
- "spec/v1/gaps.json": "07b0f5a206a015336307dd343b75adec647f06a9bae59ee08dbbfac851c503d5",
213
+ "spec/v1/gaps.json": "3eb4316a7895eeefd9bb918e496b145d8fc0e39d5cbc5dcc72e2fbefda6803ed",
214
214
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
215
215
  "spec/v1/migrations.json": "2efcd9064b2bc65a84a3b3d281aa86a91efd002690c1f4f14e8335c4a18194d1",
216
216
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
217
217
  "spec/v1/operation-path-manifest.json": "fada0342815fb129c1ae61f1829bd6e34417717408ce86fcc54280f373c19e74",
218
218
  "spec/v1/spec-gaps.json": "ea9612af55bea9dae3e82223d4f714243f8f590b0193f1188257b13491a38afc",
219
- "spec/v2/README.md": "377639de1dad1ed71aba6dae8d34fdb6ee954fb23106e89454fe8d29cdde6349",
219
+ "spec/v2/README.md": "ae7e9dceb3a6a1df75259de43c08244840c8ca8380a3eb4c727ae07dc4bdc4b1",
220
220
  "spec/v2/core/capabilities.md": "ad728e8a761153b55d036ece55043ff62d0982121e2e629e5e7cdf560fca3c03",
221
221
  "spec/v2/core/conformance.md": "0cfceb926eed87055550a5c23e640210c795fc8cf9dd1b2544db0e2b3842c564",
222
222
  "spec/v2/core/connection-packs.md": "1ee129d792c537bfe82393ccb92555d8e6ddc342b1aa781a13f101b27ec38171",
223
223
  "spec/v2/core/conversation.md": "354fa57c73d11062e523a44aa75a6353c5c81aefaf306d30c42416a228c82c86",
224
224
  "spec/v2/core/errors.md": "6d5a67389de84556f81e2558a30d83959c9283c8c5481ce7c759021e486d84fd",
225
- "spec/v2/core/events.md": "604abb6f9e2259a04103e1cb76d728712c3701c0d84dd915d4dc2949c912ee5e",
225
+ "spec/v2/core/events.md": "8a2767691f604b2adf76399edeeaccfc151c697fc9717dc17cf7dd57372a8c22",
226
+ "spec/v2/core/execution.md": "ec1afd30a2b7dab4a722f4502764cb752685e441ecd6ca61e96ed775163be139",
226
227
  "spec/v2/core/form-content-packs.md": "02931227899676fd4581a27017be2a6c4953904717be9b81c29d12f87ab3917d",
227
228
  "spec/v2/core/headers.md": "5cb1624fa63acc81992b6a6d842ec392cf7271aaf2f65d1f83c8c4fa286e6730",
228
- "spec/v2/core/host-services.md": "8b920bf2e9335ab04452068429b0c37d6eb8872b380a4d53368f2600d4afa241",
229
+ "spec/v2/core/host-services.md": "244e222ec6f7d4774751bd3cc124bbc9f548fdd84fabca65c19159ff00d49e84",
229
230
  "spec/v2/core/i18n.md": "2d5a67bd3b9b956d1144acab2fbc3646cdb50b3621f97d772087eaa953798286",
230
231
  "spec/v2/core/idempotency.md": "2084d756789c1faf5121593fb39e67ca36a4c227178e175004742e403178ea49",
231
- "spec/v2/core/identity.md": "2dceb1c2697015e4a4fa8a9b28f4fbd25fa44a76ca5a919c752e2a8163c819db",
232
+ "spec/v2/core/identity.md": "582eddbf2e1bad41911e886bd08524683837045404f7b8f6bceccf61792b2765",
232
233
  "spec/v2/core/interop.md": "e72e25dff864c83b2980d7366b34054cffa072af0225821199f260f6228d260e",
233
234
  "spec/v2/core/interrupt.md": "4c825634acac72900ebd3c5ac5941f55648fa33d2df92913ef16371dcb700a34",
234
235
  "spec/v2/core/node-pack-runtimes.md": "e1088cb69b8c3c67256c494063328916c2980ab1aa97a7063de25bab82fdb1e5",
235
236
  "spec/v2/core/oauth.md": "c96483a44b62978c596945fbb7d7c863cf6056cd980fdb4cf4e97555e3e10b72",
236
- "spec/v2/core/overview.md": "c8c3bedb8e02dd8ce244b8a7a75d23af1c30dc92a343af912489424a89674d70",
237
+ "spec/v2/core/overview.md": "76fdb48ea68de416d6a63ae3b3ec750cafda01d234ea22a1096426fded7b54f3",
237
238
  "spec/v2/core/packs.md": "5929e708bd4e79f6e1beea570af583c80f3b63bb21dec78a5df03c235e73364a",
238
239
  "spec/v2/core/persistence.md": "711874f74cb08b03cd118d683056db10a063517762fcea73ec70feb5d983e6a4",
239
240
  "spec/v2/core/portability.md": "87c4327f674f714fda6217fd79b99d2ba219ad47dc02f9751bb56c200fb2c17a",
240
241
  "spec/v2/core/replay.md": "4d379494057746c6883f6047972e93e621863b998ed291b9a691d3aa57c1ff43",
241
- "spec/v2/core/runs.md": "55e21292ae4b41d0d104199a6c2634ecb3f787c4a671f9ebc37084be38ceef03",
242
+ "spec/v2/core/runs.md": "d0e84ccf7cd7dfa657a8d9a90548dbe1d1f7033be30b405d56c41e36007af977",
242
243
  "spec/v2/core/security-defaults.md": "153884da7a2d54c30c81b5e649cfdef31e151b3a2a19dee1bc5a16c35d60a3aa",
243
244
  "spec/v2/core/storage.md": "598ad4f3775c297c698a20daf72a7d15a6836c1231ddbca5961865c704f765b5",
244
245
  "spec/v2/core/tool-catalog.md": "d16a9004418719c48301804aefa4d7ab6f41112a4ed83727f552aa1b92b418b0",
245
246
  "spec/v2/core/versioning.md": "e3b3ab6d76e51035039a32173e9b412628a52c8fe7b16687ff58a2d0d983faa0",
246
- "spec/v2/core/webhooks.md": "d2d33c6be777f217ade3e15dd369849e5c6ce67e1a4417216f0aa6ec5bdecc56",
247
+ "spec/v2/core/webhooks.md": "3f8730e0d15a5e4244e302adb910a0b6f745fbfa8e80920c14118117811ba881",
247
248
  "spec/v2/core/workflow-chain-packs.md": "fdce7a8477e269950a2bac05ee6eddbd66eb0b670b464d00054f7196ec05d29c",
248
249
  "spec/v2/corrections.json": "59f647f4d22477f6c2b06dcf913b682dde5d51dd5e607a5d0cf648ab60864d6e",
249
250
  "spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
250
- "spec/v2/declaration.json": "56c85c6d49f93d08d3bd48ccf1c049191cfbc4d4c22a1cd5202c1b6252759aca",
251
+ "spec/v2/declaration.json": "25ceb30f1b055afa529c2d822e768498e8c3e95bdc652b5edb2404bf75ac96ee",
251
252
  "spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
252
253
  "spec/v2/errors.json": "fa650cbbdf25a140fdaf82ec5269f95b4585ac4bdde1fa6b0b3ffe48a3f7943c",
253
254
  "spec/v2/errors.schema.json": "47404ad53e6147cf9017b813483712466573e419fb33e6c4b6c12d3e7deba1bc",
@@ -299,9 +300,9 @@
299
300
  "spec/v2/path-manifest.json": "17298a245c5da9a70aa7ab0b7f2db4277e06209d6fcdd7187a8b4cea179c08f3",
300
301
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
301
302
  "spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
302
- "spec/v2/release.json": "af448fd044ab5b990a21945905f109dbbb9b0df9ab7f1105e79aa7d1e0f35a33",
303
+ "spec/v2/release.json": "f84bac4d5066e20022a2f9011a4f7441e74b261f3e41da59990cf52214896478",
303
304
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
304
- "spec/v2/surface-baseline.json": "2f79e3283946dd8a401db039109d403b9c386e6b91a4780cbcb1440e8d3a6ffe"
305
+ "spec/v2/surface-baseline.json": "8acb9fab0aca69abfe40dc56fcf48729aaf0d56a41467962e5c7247f3548eea3"
305
306
  },
306
- "corpusCommit": "bf414c875dba212893f0230d44f2b4a2e226311d"
307
+ "corpusCommit": "e8eda2ffa2042f1ab81232cfe4fe5657c9576032"
307
308
  }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The verdict of RFC 0215's `no-head-of-line` leg, as a pure function of what
3
+ * the leg observed (suite 2.44.2), so the rule can be tested without a host.
4
+ *
5
+ * 2.44.2 fixes two defects that MyndHyve's 2.44.x cuts surfaced:
6
+ *
7
+ * 1. **A conviction without the contention it names.** The last branch failed
8
+ * the row as "a bounded dispatcher below the floor" whenever fewer than
9
+ * `floor` held attempts had arrived by the time the run was terminal. On a
10
+ * host whose `run.started` fan-out arrived late, that convicted a healthy
11
+ * attempt that started while every held attempt that HAD arrived was still
12
+ * open and none had finished (MyndHyve `00821-qec`: 0 held attempts, then 2
13
+ * at terminal and 7 later, the healthy attempt delivered with 7 open). §A
14
+ * forbids an attempt waiting for another subscription's attempt to FINISH; no
15
+ * attempt had finished, so there was nothing it could have waited for. The
16
+ * contention was never established, so the row is unjudged, not failed. A
17
+ * bounded pool still fails: its healthy attempt starts only after a held
18
+ * attempt closes, or never while they stay open.
19
+ *
20
+ * 2. **A detail string that mixed two sampling times.** It read "2 held
21
+ * attempt(s) arrived (at most 7 open at once)": the 2 was sampled at
22
+ * terminal, the 7 at the verdict. The detail now names each time.
23
+ */
24
+
25
+ export interface IsolationObservation {
26
+ /** §A.2's floor. */
27
+ readonly floor: number;
28
+ /** When the leg saw the run terminal (the healthy delivery is due by then). */
29
+ readonly terminalAt: number;
30
+ /** When the healthy attempt arrived, or null if it did not within the window. */
31
+ readonly healthyAt: number | null;
32
+ /** Held attempts open when the healthy attempt arrived (-1 if it did not). */
33
+ readonly openAtHealthy: number;
34
+ /** Held attempts that had arrived by `terminalAt`. */
35
+ readonly arrivedAtTerminal: number;
36
+ /** Held attempts still open at `terminalAt`. */
37
+ readonly openAtTerminal: number;
38
+ /** Held attempts that had arrived by the verdict. */
39
+ readonly arrivedByVerdict: number;
40
+ /** The most held attempts open at once, up to the verdict. */
41
+ readonly peakOpen: number;
42
+ /** When the first held attempt closed (the host abandoning it), or Infinity. */
43
+ readonly earliestClose: number;
44
+ readonly windowMs: number;
45
+ readonly delayMs: number;
46
+ }
47
+
48
+ export type IsolationVerdict =
49
+ /** The healthy attempt started while `floor` held attempts were open. */
50
+ | { readonly kind: 'pass'; readonly detail: string }
51
+ | { readonly kind: 'fail'; readonly message: string; readonly detail: string }
52
+ /** Unjudged, after asserting `peakOpen >= floor` (which fails a pool below the floor). */
53
+ | { readonly kind: 'blocked-after-peak'; readonly message: string; readonly detail: string }
54
+ | { readonly kind: 'blocked'; readonly message: string; readonly detail: string };
55
+
56
+ /** What the leg saw, each count against the time it was sampled. */
57
+ export function isolationDetail(o: IsolationObservation): string {
58
+ const at = (t: number): string => `${t - o.terminalAt}ms after the run was seen terminal`;
59
+ const healthy = o.healthyAt === null
60
+ ? `the healthy attempt did not arrive within ${o.windowMs}ms of that`
61
+ : `the healthy attempt arrived ${at(o.healthyAt)}, with ${o.openAtHealthy} held open`;
62
+ const closed = Number.isFinite(o.earliestClose) ? `the first held attempt was closed by the host ${at(o.earliestClose)}` : 'no held attempt was closed by the host';
63
+ return `at terminal, ${o.arrivedAtTerminal} held attempt(s) had arrived and ${o.openAtTerminal} were open; by the verdict, ${o.arrivedByVerdict} had arrived, at most ${o.peakOpen} open at once; ${healthy}; ${closed}`;
64
+ }
65
+
66
+ export function isolationVerdict(o: IsolationObservation): IsolationVerdict {
67
+ const detail = isolationDetail(o);
68
+ const F = o.floor;
69
+ if (o.healthyAt !== null && o.openAtHealthy >= F) return { kind: 'pass', detail };
70
+ if (o.openAtTerminal >= F) {
71
+ // The healthy attempt waited (or never came), and the contention §A.2
72
+ // names was present when it fell due.
73
+ return { kind: 'fail', message: `with ${F} subscriptions' attempts unanswered, a further subscription's attempt MUST still start; it waited for the host to release one`, detail };
74
+ }
75
+ if (o.arrivedAtTerminal >= F || o.earliestClose < o.terminalAt) {
76
+ // Contention was not sustained to the due time. Either the host's own
77
+ // delivery timeout released the held attempts early (its choice), or it
78
+ // never opened F of them at once. peakOpen separates the two: below F the
79
+ // host never had F attempts outstanding at once, which is the defect.
80
+ const released = Number.isFinite(o.earliestClose) ? `${o.terminalAt - o.earliestClose}ms ` : '';
81
+ return { kind: 'blocked-after-peak', message: `unjudged: ${F} held attempts were open at once, but the host closed held attempts ${released}before the run was terminal (its delivery timeout is shorter than this leg's ${o.delayMs}ms delay), so ${F} attempts were not outstanding when the healthy delivery fell due`, detail };
82
+ }
83
+ if (o.healthyAt !== null && !(o.earliestClose < o.healthyAt)) {
84
+ // Fewer than F held attempts had arrived when the healthy delivery fell
85
+ // due, and it started before any held attempt finished: it cannot have
86
+ // waited for one. The held fan-out was late, so the contention the floor
87
+ // names never existed when it mattered.
88
+ return { kind: 'blocked', message: `unjudged: only ${o.arrivedAtTerminal} of the ${F} held attempts had arrived when the healthy delivery fell due, and the healthy attempt started before any held attempt finished, so it did not wait for one; the held run.started deliveries arrived late, and the contention §A.2 names was not established`, detail };
89
+ }
90
+ return {
91
+ kind: 'fail',
92
+ message: o.healthyAt === null
93
+ ? `the host never had ${F} subscriptions' attempts outstanding at once, and the healthy attempt did not start within ${o.windowMs}ms while the held attempts stayed open`
94
+ : `the host never had ${F} subscriptions' attempts outstanding at once, and the healthy attempt started only after a held attempt finished`,
95
+ detail,
96
+ };
97
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * An informational note for the CURRENT `it`'s row (suite 2.44.3, RFC 0225).
3
+ *
4
+ * `setup.ts` attaches it to the row's `detail` in `afterEach`, whatever the
5
+ * disposition, so a PASSING row can say what it observed. The bundle writer
6
+ * already carries `detail` on every row and the witness digest covers it, so the
7
+ * note is signed with the row. It is prefixed `observed: `, never
8
+ * `partial-witness: `: it describes how a witness was obtained and does not
9
+ * qualify it (`check-accepted-predicate` counts a pass as partial only on that
10
+ * prefix). Same shape as `noteEvidence` (lib/durability-evidence.ts).
11
+ */
12
+ export const OBSERVATION_PREFIX = 'observed: ';
13
+
14
+ let pending: string | null = null;
15
+
16
+ /** Note what this `it` observed. A second note in the same `it` is appended. */
17
+ export function noteObservation(text: string): void {
18
+ const t = text.trim();
19
+ if (t === '') return;
20
+ pending = pending === null ? t : `${pending} · ${t}`;
21
+ }
22
+
23
+ /** Take (and clear) the pending note. */
24
+ export function takeNotedObservation(): string | null {
25
+ const n = pending;
26
+ pending = null;
27
+ return n;
28
+ }
29
+
30
+ /**
31
+ * The detail a row carries once an observation is attached. A row with no
32
+ * detail gets `observed: …`; a row that already has one (a non-pass reason, or
33
+ * a partial-witness marker) keeps it FIRST and gains `· observed: …`, so no
34
+ * prefix a reader filters on ever moves.
35
+ */
36
+ export function attachObservation(detail: string | undefined, observation: string | null): string | undefined {
37
+ if (observation === null || observation.trim() === '') return detail;
38
+ if (detail === undefined || detail.trim() === '') return `${OBSERVATION_PREFIX}${observation}`;
39
+ return `${detail} · ${OBSERVATION_PREFIX}${observation}`;
40
+ }
@@ -90,6 +90,62 @@ export function retryTestTimeoutMs(waits: number, slackMs: number): number {
90
90
  return waits * MAX_RETRY_WAIT_MS + slackMs;
91
91
  }
92
92
 
93
+ /**
94
+ * WHICH branch `retryWaitFor` took, so a row can say so (RFC 0225 witness, suite
95
+ * 2.44.3). A passing dead-letter row used to carry no detail at all, so a
96
+ * bundle could not show whether the wait was derived from the host's advertised
97
+ * `maxElapsedMs` (the RFC 0225 path), from an operator-raised cap, or from the
98
+ * 20 s floor, and a certifier could not tell a witness of the bound from a pass
99
+ * the old default would have produced anyway.
100
+ *
101
+ * advertised-bound the window is the advertised maxElapsedMs + the grace
102
+ * cap the window is the cap (default or operator-raised); when
103
+ * a bound is advertised this means the operator raised the
104
+ * cap ABOVE it, so the bound did not set the wait
105
+ * floor no policy, or backoff none: the 20 s floor
106
+ */
107
+ export type RetryWaitPath = 'advertised-bound' | 'cap' | 'floor';
108
+ export interface RetryWaitSelection { readonly path: RetryWaitPath; readonly windowMs: number; readonly maxElapsedMs: number | null; readonly capMs: number }
109
+
110
+ export function retryWaitSelection(policy: AdvertisedRetryPolicy | null, capMs: number): RetryWaitSelection {
111
+ const windowMs = retryWaitFor(policy, capMs);
112
+ const bound = advertisedMaxElapsedMs(policy);
113
+ if (policy === null) return { path: 'floor', windowMs, maxElapsedMs: null, capMs };
114
+ if (bound !== null) return { path: bound + ADVERTISED_BOUND_GRACE_MS >= capMs ? 'advertised-bound' : 'cap', windowMs, maxElapsedMs: bound, capMs };
115
+ const backoff = String(policy.backoff ?? '');
116
+ return { path: backoff === 'exponential' || backoff === 'fixed' ? 'cap' : 'floor', windowMs, maxElapsedMs: null, capMs };
117
+ }
118
+
119
+ /** What the leg measured, in ms since the delivery's FIRST attempt reached the receiver. */
120
+ export interface RetryWaitMeasurement { readonly attempts: number; readonly lastAttemptAfterMs: number | null; /** undefined when the leg does not read the sink */ readonly sinkSeenAfterMs?: number | null }
121
+
122
+ /**
123
+ * The informational row detail: the path, the window, and what was measured.
124
+ * The last attempt is a LOWER bound on the dead-lettering time (the host cannot
125
+ * dead-letter before its last attempt); the sink observation is an UPPER bound
126
+ * (the read that first found the record). Never starts with the partial-witness
127
+ * prefix: this describes a witness, it does not qualify one.
128
+ */
129
+ /** The path and window alone, for a leg that measures no dead-lettering. */
130
+ export function waitPathNote(sel: RetryWaitSelection): string {
131
+ return `${pathLabel(sel)}: waited ≤${sel.windowMs}ms`;
132
+ }
133
+
134
+ function pathLabel(sel: RetryWaitSelection): string {
135
+ return sel.path === 'advertised-bound'
136
+ ? `advertised-bound (maxElapsedMs ${sel.maxElapsedMs})`
137
+ : sel.path === 'cap'
138
+ ? `cap (${sel.capMs > DEFAULT_RETRY_WAIT_CAP_MS ? `${RETRY_WAIT_ENV} raised to ${sel.capMs}` : `default ${sel.capMs}`}ms; advertised maxElapsedMs ${sel.maxElapsedMs ?? 'none'})`
139
+ : 'floor';
140
+ }
141
+
142
+ export function waitObservation(sel: RetryWaitSelection, m: RetryWaitMeasurement): string {
143
+ const how = pathLabel(sel);
144
+ const last = m.lastAttemptAfterMs === null ? 'no attempt' : `last attempt after ${m.lastAttemptAfterMs}ms`;
145
+ const sink = m.sinkSeenAfterMs === undefined ? '' : m.sinkSeenAfterMs === null ? ', sink not observed' : `, sink after ${m.sinkSeenAfterMs}ms`;
146
+ return `${how}: waited ≤${sel.windowMs}ms; ${m.attempts} attempt(s), ${last}${sink}`;
147
+ }
148
+
93
149
  /** What a row says when its window closed before the host's schedule did. Computed, so the numbers a host reads are the ones the run used. */
94
150
  export function windowClosedNote(seen: number, maxAttempts: number, waitedMs: number, capMs: number): string {
95
151
  const raised = capMs > DEFAULT_RETRY_WAIT_CAP_MS;
@@ -26,6 +26,11 @@
26
26
  * host released one to make room), or never within the window, AND the held
27
27
  * attempts had stayed open until the run was terminal: the contention the
28
28
  * floor names existed when the healthy delivery fell due, and it waited.
29
+ * - blocked (unjudged, suite 2.44.2) — fewer than 8 held attempts had arrived
30
+ * when the healthy delivery fell due, and the healthy attempt started before
31
+ * any held attempt finished: it waited for nothing, and the contention was
32
+ * never established (a late `run.started` fan-out). The rule and its tests
33
+ * are in `lib/delivery-isolation-verdict.ts`.
29
34
  * - blocked (unjudged) — the host had all 8 held attempts open at once, but
30
35
  * its own delivery timeout closed them before the run was terminal.
31
36
  * Contention was not sustained when the healthy delivery fell due, so this
@@ -56,6 +61,7 @@ import { readErrorCode } from '../lib/error-envelope.js';
56
61
  import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
57
62
  import { req } from '../lib/requirement-ids.js';
58
63
  import { RETRY_WAIT_FLOOR_MS } from '../lib/webhook-retry-window.js';
64
+ import { isolationVerdict } from '../lib/delivery-isolation-verdict.js';
59
65
 
60
66
  export const REQUIRES_HOST_CALLBACK = 'the host POSTs webhook deliveries to nine suite-owned subscriptions on the scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL, eight of which the suite holds open';
61
67
 
@@ -179,44 +185,30 @@ describe('RFC 0215 §A — one subscription\'s receiver does not hold another\'s
179
185
  const terminalAt = await waitTerminal(runId, DELAY_MS + 30_000);
180
186
  if (terminalAt === null) return blockedDespiteAssertions(`${FIXTURE} run ${runId} did not reach a terminal status — the healthy delivery never fell due`);
181
187
  const openAtTerminal = openCount();
182
- const everOpened = held.size;
188
+ const arrivedAtTerminal = held.size;
183
189
  await waitFor(() => healthyAt !== null, WINDOW_MS);
184
190
  const earliestClose = Math.min(...[...held.values()].map((h) => h.closedAt ?? Infinity));
185
- const detail = `${everOpened} held attempt(s) arrived (at most ${peakOpen} open at once); ${openAtTerminal} were still open when the run was seen terminal; `
186
- + (healthyAt === null ? `the healthy attempt did not arrive within ${WINDOW_MS}ms of that` : `the healthy attempt arrived ${healthyAt - terminalAt}ms after it, with ${openAtHealthy} held open`);
187
191
 
188
- if (healthyAt === null && everOpened === 0) {
192
+ if (healthyAt === null && held.size === 0) {
189
193
  if (absenceIsUnmeasured(rx)) return blockedDespiteAssertions(noDeliveryCause(rx, 'webhook attempt'));
190
194
  return blockedDespiteAssertions(`no attempt for any of the ${FLOOR + 1} subscriptions arrived — ${noDeliveryCause(rx, 'webhook attempt')}`);
191
195
  }
192
- if (healthyAt !== null && openAtHealthy >= FLOOR) {
193
- expect(openAtHealthy, req(ID, DOC, `an attempt to one subscription MUST NOT wait for attempts to other subscriptions — ${detail}`)).toBeGreaterThanOrEqual(FLOOR);
194
- } else if (openAtTerminal >= FLOOR) {
195
- // The healthy attempt waited (or never came), and the contention §A.2
196
- // names was present when it fell due.
197
- expect.fail(req(ID, DOC, `with ${FLOOR} subscriptions' attempts unanswered, a further subscription's attempt MUST still start; it waited for the host to release one — ${detail}`));
198
- } else if (everOpened >= FLOOR || earliestClose < terminalAt) {
199
- // Contention was not sustained to the due time. Either the host's own
200
- // delivery timeout released the held attempts early (its choice), or it
201
- // never opened FLOOR of them at once. The second is the defect, the first
202
- // is not, and they differ in WHEN the first held attempt closed.
203
- //
204
- // Unfailable-leg audit (2026-09-26): this branch asserted everOpened > 0
205
- // and then `softSkip('skipped')`, which records `executed-pass` with a
206
- // `partial-witness:` detail. A serial dispatcher (pool of 1) with a
207
- // sub-2s delivery timeout landed here and passed — every held attempt
208
- // arrived eventually (everOpened reached FLOOR), one at a time, while the
209
- // healthy attempt queued behind them: head-of-line blocking. peakOpen
210
- // separates the two cases the comment above names: below FLOOR the host
211
- // never had FLOOR attempts outstanding at once, which is the defect; at
212
- // FLOOR the host's timeout released them early, which is unjudged.
196
+ const v = isolationVerdict({
197
+ floor: FLOOR, terminalAt, healthyAt, openAtHealthy, arrivedAtTerminal, openAtTerminal,
198
+ arrivedByVerdict: held.size, peakOpen, earliestClose, windowMs: WINDOW_MS, delayMs: DELAY_MS,
199
+ });
200
+ if (v.kind === 'blocked') return blockedDespiteAssertions(`${v.message} — ${v.detail}`);
201
+ if (v.kind === 'blocked-after-peak') {
202
+ // Unfailable-leg audit (2026-09-26): a serial dispatcher (pool of 1)
203
+ // with a sub-2s delivery timeout lands here; below FLOOR at once it
204
+ // fails, at FLOOR the host's timeout released them early (unjudged).
213
205
  expect(
214
206
  peakOpen,
215
- req(ID, DOC, `a host MUST sustain ${FLOOR} subscriptions' attempts outstanding at once — at most ${peakOpen} were ever open together, so the held attempts were dispatched below the floor — ${detail}`),
207
+ req(ID, DOC, `a host MUST sustain ${FLOOR} subscriptions' attempts outstanding at once — at most ${peakOpen} were ever open together, so the held attempts were dispatched below the floor — ${v.detail}`),
216
208
  ).toBeGreaterThanOrEqual(FLOOR);
217
- return blockedDespiteAssertions(`unjudged: ${FLOOR} held attempts were open at once, but the host closed held attempts ${Number.isFinite(earliestClose) ? `${terminalAt - earliestClose}ms ` : ''}before the run was terminal (its delivery timeout is shorter than this leg's ${DELAY_MS}ms delay), so ${FLOOR} attempts were not outstanding when the healthy delivery fell due — ${detail}`);
218
- } else {
219
- expect.fail(req(ID, DOC, `the host never had ${FLOOR} subscriptions' attempts outstanding at once, and the healthy attempt waited — a bounded dispatcher below the floor — ${detail}`));
209
+ return blockedDespiteAssertions(`${v.message} — ${v.detail}`);
220
210
  }
211
+ if (v.kind === 'fail') expect.fail(req(ID, DOC, `${v.message} — ${v.detail}`));
212
+ expect(openAtHealthy, req(ID, DOC, `an attempt to one subscription MUST NOT wait for attempts to other subscriptions — ${v.detail}`)).toBeGreaterThanOrEqual(FLOOR);
221
213
  }, DELAY_MS + 30_000 + RETRY_WAIT_FLOOR_MS + 30_000);
222
214
  });
@@ -35,7 +35,8 @@ import { absenceIsUnmeasured, noDeliveryCause, startScopedReceiver, type ScopedR
35
35
  import { readErrorCode } from '../lib/error-envelope.js';
36
36
  import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
37
37
  import { req } from '../lib/requirement-ids.js';
38
- import { pastAdvertisedBound, retryTestTimeoutMs, retryWaitCapMs, retryWaitFor, windowClosedNote } from '../lib/webhook-retry-window.js';
38
+ import { pastAdvertisedBound, retryTestTimeoutMs, retryWaitCapMs, retryWaitFor, retryWaitSelection, waitObservation, windowClosedNote } from '../lib/webhook-retry-window.js';
39
+ import { noteObservation } from '../lib/row-observation.js';
39
40
 
40
41
  export const REQUIRES_HOST_CALLBACK = 'the host POSTs and retries webhook deliveries to the suite-owned scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL';
41
42
 
@@ -180,6 +181,21 @@ function retryWaitMs(doc: Record<string, unknown>): number {
180
181
  return retryWaitFor(advertisedRetryPolicy(doc), RETRY_WAIT_CAP_MS);
181
182
  }
182
183
 
184
+ /**
185
+ * Suite 2.44.3 (RFC 0225 witness): say, on the row, which wait path ran and what
186
+ * was measured. The first attempt is t0; the last attempt lower-bounds the
187
+ * dead-lettering, and the sink read that found the record upper-bounds it.
188
+ */
189
+ function observeWait(doc: Record<string, unknown>, attempts: ReadonlyArray<{ readonly at: number }>, sinkSeenAt?: number | null): void {
190
+ const t0 = attempts[0]?.at;
191
+ const last = attempts[attempts.length - 1]?.at;
192
+ noteObservation(waitObservation(retryWaitSelection(advertisedRetryPolicy(doc), RETRY_WAIT_CAP_MS), {
193
+ attempts: attempts.length,
194
+ lastAttemptAfterMs: t0 === undefined || last === undefined ? null : last - t0,
195
+ ...(sinkSeenAt === undefined ? {} : { sinkSeenAfterMs: t0 === undefined || sinkSeenAt === null ? null : sinkSeenAt - t0 }),
196
+ }));
197
+ }
198
+
183
199
  /**
184
200
  * The per-test budget, DERIVED from the wait above (suite 2.0.2).
185
201
  *
@@ -287,6 +303,7 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
287
303
  const ours = () => receiver.attempts.filter((a) => a.runId === runId && a.webhookId === sub.webhookId);
288
304
  const retried = await waitFor(() => ours().some((a) => a.status === 204), retryWaitMs(doc));
289
305
  const attempts = ours();
306
+ observeWait(doc, attempts);
290
307
  // A zero that is PROVABLY not a verdict about the host records `blocked`
291
308
  // with its cause, not `executed-fail` (2.37.0). `absenceIsUnmeasured` is
292
309
  // true only when other traffic reached this listener — the path from the
@@ -440,6 +457,7 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
440
457
  const sink = await driver.get(`/webhooks/${projectBoundId(sub.webhookId)}/dead-letters`);
441
458
  inSink = sink.status === 200 && ((sink.json as { deliveries?: unknown[] } | null)?.deliveries ?? []).some((r) => (r as Record<string, unknown>)['runId'] === runId);
442
459
  }
460
+ observeWait(doc, ours(), inSink === true ? Date.now() : null);
443
461
  await driver.delete(`/webhooks/${encodeURIComponent(sub.webhookId)}`);
444
462
  // "Routed to the sink, not dropped" is a claim about an EXHAUSTED delivery,
445
463
  // and until 2.34.1 it was asserted whether or not exhaustion had been
@@ -589,9 +607,11 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
589
607
  await new Promise((r) => setTimeout(r, 1_000));
590
608
 
591
609
  const sink = await driver.get(`/webhooks/${projectBoundId(sub.webhookId)}/dead-letters`);
610
+ const sinkReadAt = Date.now();
592
611
  await driver.delete(`/webhooks/${encodeURIComponent(sub.webhookId)}`);
593
612
  if (sink.status !== 200) return blockedDespiteAssertions(`the dead-letter read answered ${sink.status}`);
594
613
  const rows = ((sink.json as { deliveries?: Array<Record<string, unknown>> } | null)?.deliveries ?? []);
614
+ observeWait(doc, ours(), rows.length > 0 ? sinkReadAt : null);
595
615
  // An empty sink is NOT a pass. Recording one as a pass is exactly the
596
616
  // vacuous witness this leg used to produce; say so instead.
597
617
  if (rows.length === 0) {
@@ -33,7 +33,8 @@ import { req } from '../lib/requirement-ids.js';
33
33
  import { readErrorCode } from '../lib/error-envelope.js';
34
34
  import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
35
35
  import { hitHeader, mintWhsec, startModalReceiver, STANDARD_WEBHOOKS_ID, type ModalHit } from '../lib/webhook-receiver.js';
36
- import { retryTestTimeoutMs, retryWaitCapMs, retryWaitFor } from '../lib/webhook-retry-window.js';
36
+ import { retryTestTimeoutMs, retryWaitCapMs, retryWaitFor, retryWaitSelection, waitPathNote } from '../lib/webhook-retry-window.js';
37
+ import { noteObservation } from '../lib/row-observation.js';
37
38
  import {
38
39
  STANDARD_WEBHOOKS_ALG,
39
40
  deliveriesFor,
@@ -89,7 +90,9 @@ describe('RFC 0201 §C.10 — webhook-id is stable across retries, distinct acro
89
90
  return m;
90
91
  };
91
92
  const expectedKeys = subs.length * EVENTS.length;
92
- const window = retryWaitFor((g.facet['retryPolicy'] as { backoff?: string; maxElapsedMs?: unknown } | undefined) ?? null, CAP_MS);
93
+ const advertised = (g.facet['retryPolicy'] as { backoff?: string; maxElapsedMs?: unknown } | undefined) ?? null;
94
+ const window = retryWaitFor(advertised, CAP_MS);
95
+ noteObservation(waitPathNote(retryWaitSelection(advertised, CAP_MS)));
93
96
  await waitFor(() => {
94
97
  const m = byKey();
95
98
  return m.size >= expectedKeys && [...m.values()].every((a) => a.length >= 2);
@@ -45,7 +45,8 @@ import { absenceIsUnmeasured, noDeliveryCause, startScopedReceiver, type ScopedR
45
45
  import { readErrorCode } from '../lib/error-envelope.js';
46
46
  import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
47
47
  import { req } from '../lib/requirement-ids.js';
48
- import { retryTestTimeoutMs, retryWaitCapMs, retryWaitFor } from '../lib/webhook-retry-window.js';
48
+ import { retryTestTimeoutMs, retryWaitCapMs, retryWaitFor, retryWaitSelection, waitPathNote } from '../lib/webhook-retry-window.js';
49
+ import { noteObservation } from '../lib/row-observation.js';
49
50
 
50
51
  export const REQUIRES_HOST_CALLBACK = 'the host POSTs and retries webhook deliveries to the suite-owned scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL';
51
52
 
@@ -94,6 +95,7 @@ describe('RFC 0215 §B — unregistering stops the attempts (gated on webhooks)'
94
95
  const policy = advertisedRetryPolicy(doc);
95
96
  if (policy?.maxAttempts === 1) return softSkip('inapplicable', 'webhooks.retryPolicy.maxAttempts is 1 — the host schedules no retry, so there is none for unregistering to stop');
96
97
  const windowMs = retryWaitFor(policy, CAP_MS);
98
+ noteObservation(waitPathNote(retryWaitSelection(policy, CAP_MS)));
97
99
 
98
100
  const arrivals = new Map<string, number[]>();
99
101
  let targetId: string | null = null;
package/src/setup.ts CHANGED
@@ -38,6 +38,7 @@ import { PKG_ROOT_PATH } from './lib/paths.js';
38
38
  import { recordRequirement, hasRequirement, journalLength, journalSince } from './lib/requirement-ledger.js';
39
39
  import { requirementIdForFile, resolveFileRecord, resolveItRecord, unrecordedTests, type FileTestState, type FinishedTest, type TestFailure } from './lib/scenario-disposition.js';
40
40
  import { softSkipDisposition, softSkipDispositionSince, softSkipMark } from './lib/soft-skip.js';
41
+ import { attachObservation, takeNotedObservation } from './lib/row-observation.js';
41
42
  import { ItIdAllocator, takeExplicitRequirementId } from './lib/requirement-ids.js';
42
43
  import { SPEC_COHERENCE_SCENARIOS, SPEC_COHERENCE_DETAIL } from './lib/spec-coherence.js';
43
44
  import type { DiscoveryPayload } from './lib/profiles.js';
@@ -472,6 +473,7 @@ afterEach(({ task }) => {
472
473
  // Suite 2.0.0: src/coherence/ rows feed the corpus ledger (scripts/check-spec-coherence.mjs), not a host bundle.
473
474
  if (!/[\\/]src[\\/](scenarios|coherence)[\\/]/.test(filepath)) {
474
475
  takeExplicitRequirementId();
476
+ takeNotedObservation();
475
477
  return;
476
478
  }
477
479
  // A leg that soft-skips before its first assertion never calls `req()`, so the
@@ -506,6 +508,11 @@ afterEach(({ task }) => {
506
508
  disposition = rec.disposition;
507
509
  detail = rec.detail;
508
510
  }
511
+ // Suite 2.44.3 (RFC 0225 witness): an informational note the `it` wrote
512
+ // (`noteObservation`) rides on the row's detail, on a pass too — the bundle
513
+ // writer carries it and the witness digest signs it. Taken whatever the
514
+ // disposition, so a note never leaks into the next `it`.
515
+ detail = attachObservation(detail, takeNotedObservation());
509
516
  try {
510
517
  const evidence = takeNotedEvidence();
511
518
  // `fold: true` — several `it` legs of one file legitimately share ONE