@openwop/openwop-conformance 2.44.2 → 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,13 @@
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
+
3
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
4
12
 
5
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.
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.2 @openwop/spec-artifacts@2.44.2
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.2",
4
- "stampSha256": "66d1059ab9f4e5799bbb84c8b66e64033bc08f4daf6016c22c71164cca948c4c"
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.2",
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.2"
59
+ "@openwop/spec-artifacts": "2.44.3"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -34355,7 +34355,7 @@
34355
34355
  {
34356
34356
  "id": "openwop.it.v2-webhook-durable-delivery.a-failed-attempt-is-retried-and-the-event-is-delivered-at-least-once",
34357
34357
  "file": "v2-webhook-durable-delivery.test.ts",
34358
- "line": 263,
34358
+ "line": 279,
34359
34359
  "title": "a failed attempt is retried and the event is delivered at least once",
34360
34360
  "explicitId": "openwop.requirement.0173.webhook-durable-delivery",
34361
34361
  "citations": [
@@ -34392,7 +34392,7 @@
34392
34392
  {
34393
34393
  "id": "openwop.it.v2-webhook-durable-delivery.an-exhausted-delivery-is-dead-lettered-never-dropped",
34394
34394
  "file": "v2-webhook-durable-delivery.test.ts",
34395
- "line": 383,
34395
+ "line": 400,
34396
34396
  "title": "an exhausted delivery is dead-lettered, never dropped",
34397
34397
  "explicitId": "openwop.requirement.0173.webhook-durable-delivery.dead-letter",
34398
34398
  "citations": [
@@ -34431,7 +34431,7 @@
34431
34431
  {
34432
34432
  "id": "openwop.it.v2-webhook-durable-delivery.the-dead-letter-read-is-served-and-its-records-carry-no-payload",
34433
34433
  "file": "v2-webhook-durable-delivery.test.ts",
34434
- "line": 491,
34434
+ "line": 509,
34435
34435
  "title": "the dead-letter read is served and its records carry no payload",
34436
34436
  "explicitId": "openwop.requirement.0188.dead-letter-read",
34437
34437
  "citations": [
@@ -34444,7 +34444,7 @@
34444
34444
  {
34445
34445
  "id": "openwop.it.v2-webhook-durable-delivery.after-unregister-the-dead-letter-read-answers-404-not-found-as-for-a-subscriptio",
34446
34446
  "file": "v2-webhook-durable-delivery.test.ts",
34447
- "line": 512,
34447
+ "line": 530,
34448
34448
  "title": "after unregister, the dead-letter read answers 404 not_found, as for a subscription that never existed",
34449
34449
  "explicitId": "openwop.requirement.0217.dead-letter-read-after-unregister",
34450
34450
  "citations": [
@@ -34471,7 +34471,7 @@
34471
34471
  {
34472
34472
  "id": "openwop.it.v2-webhook-durable-delivery.a-dead-letter-record-carries-no-delivered-payload",
34473
34473
  "file": "v2-webhook-durable-delivery.test.ts",
34474
- "line": 545,
34474
+ "line": 563,
34475
34475
  "title": "a dead-letter record carries no delivered payload",
34476
34476
  "explicitId": "openwop.requirement.0188.dead-letter-content-free",
34477
34477
  "citations": [
@@ -34615,7 +34615,7 @@
34615
34615
  {
34616
34616
  "id": "openwop.it.v2-webhook-message-id-stable.every-attempt-of-one-delivery-carries-one-webhook-id-and-distinct-deliveries-car",
34617
34617
  "file": "v2-webhook-message-id-stable.test.ts",
34618
- "line": 69,
34618
+ "line": 70,
34619
34619
  "title": "every attempt of one delivery carries one webhook-id, and distinct deliveries carry distinct ids",
34620
34620
  "explicitId": "openwop.requirement.0201.message-id-stable",
34621
34621
  "citations": [
@@ -34854,7 +34854,7 @@
34854
34854
  {
34855
34855
  "id": "openwop.it.v2-webhook-unregister-stops-delivery.no-attempt-for-an-unregistered-subscription-starts-after-the-204-while-its-contr",
34856
34856
  "file": "v2-webhook-unregister-stops-delivery.test.ts",
34857
- "line": 86,
34857
+ "line": 87,
34858
34858
  "title": "no attempt for an unregistered subscription starts after the 204, while its control keeps being retried",
34859
34859
  "explicitId": "openwop.requirement.0215.unregister-stops-delivery",
34860
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.2",
5
- "corpusTag": "v2.44.2",
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": "f24127f3cd544f1c5a942859ffc492b438d37b1b7d7a660c631fb87f70cd24f6",
13
- "api/v2/asyncapi.yaml": "3283ff1bbe962dc75e6d66d2b755d4eb7d53770ce8511f2d671b923d6ca0e51a",
14
- "api/v2/openapi.yaml": "4a455ba22c81718e2842adce60e5d3c5970e6f8a55b301d9154ccc8060f6cf91",
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": "3d7fd99cd02153094723c2bf383d757c282e653526aa3b199d9bdb4b9cfd7c80",
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": "28b407019929da55701a718110d440f08899646af0b74d62f6adc5542c9c1c91",
303
+ "spec/v2/release.json": "f84bac4d5066e20022a2f9011a4f7441e74b261f3e41da59990cf52214896478",
303
304
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
304
- "spec/v2/surface-baseline.json": "d0540276e74001f5058dd81909100ea6ee3c1925c6ac02918418f5ec941d0661"
305
+ "spec/v2/surface-baseline.json": "8acb9fab0aca69abfe40dc56fcf48729aaf0d56a41467962e5c7247f3548eea3"
305
306
  },
306
- "corpusCommit": "c486eafcfa16908a7b57c98b33cfd66fdf8e599d"
307
+ "corpusCommit": "e8eda2ffa2042f1ab81232cfe4fe5657c9576032"
307
308
  }
@@ -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;
@@ -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