@openwop/openwop-conformance 2.0.4 → 2.0.6

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.
@@ -3,7 +3,7 @@
3
3
  "counts": {
4
4
  "files": 516,
5
5
  "v1": 445,
6
- "v2": 72
6
+ "v2": 73
7
7
  },
8
8
  "majors": {
9
9
  "a2a-1-0-agent-card.test.ts": [
@@ -773,7 +773,8 @@
773
773
  1
774
774
  ],
775
775
  "memory-attribution-replay-stable.test.ts": [
776
- 1
776
+ 1,
777
+ 2
777
778
  ],
778
779
  "memory-attribution-shape.test.ts": [
779
780
  1
@@ -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.0.4",
5
- "corpusTag": "v2.0.4",
4
+ "version": "2.0.6",
5
+ "corpusTag": "v2.0.6",
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": "39081c59fb696159806b0f2f9a42e7e9ff830d622fcf2ad4159b21357580a955",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "7298eee975d901d9356e8893c39d43261d74a76a07955fffa5b21fb9ffdadf1f",
13
- "api/v2/asyncapi.yaml": "98f304626ae95d8fef4de49ace6a4422104faf6c5348bff56ce5aafe511c1381",
14
- "api/v2/openapi.yaml": "a6ace2a7b2f9adce2374a6a76fe8b62c6cb2530be5689b2343d8876733e9a54c",
12
+ "api/seams-v2.yaml": "594aad07baceb9c8e00dc9fe91dd8f6f99bc6624c64eaf5be91dbb5f22a35c48",
13
+ "api/v2/asyncapi.yaml": "5839f6cb16ed8997a335491a6700ddc2f4166c994ca2838863d4029c1ef6d58e",
14
+ "api/v2/openapi.yaml": "f1288076f774dbb46b885519b94719df84b31b4535fe2a5bccb9881d2e09b0ea",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
@@ -116,7 +116,7 @@
116
116
  "schemas/v2/audit-verify-result.schema.json": "cb54c4d89ba5dfb73f49aa8959a980be65c2dc2a87a12f71ba36d3e836fe85da",
117
117
  "schemas/v2/budget-policy.schema.json": "c7449daeb6e1d95e7a047b8c2814d3d54748697c27046972834874867d9b5f4c",
118
118
  "schemas/v2/capabilities.schema.json": "792669b2cc7b1fd44ce9a4fd3e79696ad1277734e29299a4504b39eda15913ed",
119
- "schemas/v2/certification-bundle.schema.json": "b893ffa4de998b8b2b69a1038ebba0d7d50c8a32cde91ccae2b7c653fe459aa6",
119
+ "schemas/v2/certification-bundle.schema.json": "d2cbc5cadddff13630a528dc9673162f582c035c6a6663ca02c1ac53e8fec40c",
120
120
  "schemas/v2/channel-presence-payload.schema.json": "9475d46f358d21c19126e78204987a5633979917a2293653354ef5c5e044d54e",
121
121
  "schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
122
122
  "schemas/v2/chat-card-pack-manifest.schema.json": "8df0d9d273ae22efc6876d5586833f1d4c2e551edc3ac6cf6e8f0719329f03dd",
@@ -199,7 +199,7 @@
199
199
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
200
200
  "spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
201
201
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
202
- "spec/v1/core-standard-manifest.json": "04f279931a6749086cf24131671a48f9d119eca4c3a611d2c1e19cc909ac4101",
202
+ "spec/v1/core-standard-manifest.json": "0018230eb426fb0af82545b9f63920cc9504a9eed2a03c44ef61cf73f429e82b",
203
203
  "spec/v1/deprecations.json": "1d5acb69a9b8ccb57275a95605f74aef1d920685f8407c9d382a46b59dc803bb",
204
204
  "spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
205
205
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -211,12 +211,50 @@
211
211
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
212
212
  "spec/v1/operation-path-manifest.json": "5f5f4e3842669371730ebbd1aace3fb794192615f0018dc144f484ba5db82ac3",
213
213
  "spec/v1/spec-gaps.json": "6cc9962c6b969f632e07a78b52a4f61447ff579e2990cbae989866f584a86042",
214
- "spec/v2/declaration.json": "a624cfa6a46d0b10e0dac40fa49abbe05190aff87a8e852ce125d19379067b00",
215
- "spec/v2/declaration.schema.json": "b0881a76228dfbd1b85a2ec7b7259a60197f768344535cdb475db6ff719b0e54",
214
+ "spec/v2/README.md": "f56bd8d58be4ac04543c9f7b2fe92b294738d6121a4f21cbd1ce751e72b94c83",
215
+ "spec/v2/core/capabilities.md": "0f222434bbe58c881362a626ab9db6bce0ae2e54cb858e3d71b662dd1fc13150",
216
+ "spec/v2/core/conformance.md": "91418b882190e116ab9dac14527f52b8582b0462f41e96beeceb07e3fcc5f99a",
217
+ "spec/v2/core/connection-packs.md": "34e519dbe5b091c9758729787d398b35b1dc41f1e01ebd028e7e6b838ce2f6ee",
218
+ "spec/v2/core/errors.md": "671895de1b7c987a1dfd53fe0a814159efadae0ffd4c8c06bf1749d345270580",
219
+ "spec/v2/core/events.md": "7b9207079cede7ec335126b98bcb95d655ae3b7322b18febd1b4df51750b0a33",
220
+ "spec/v2/core/form-content-packs.md": "976f6e9b05a015f3bfe0748bc4f8bb11bbbd71460d12769f900bcde006b7cbb1",
221
+ "spec/v2/core/headers.md": "b7fc4f77904653ad47360b20302e3f406973821fb6d74ac86d6e81b674f52c4a",
222
+ "spec/v2/core/idempotency.md": "0ab68c1e41e81005b560421a3d22f985dda2f7537b2369bff67e40b79c8a581f",
223
+ "spec/v2/core/identity.md": "e3fa8d2d87c1dec2673e4bf4aec5f2257c684eaf64161c1a1016a634a87509c2",
224
+ "spec/v2/core/interop.md": "f701fddf15343d878faea31401e25e6ffb3672c2c8a87f30adc13b1e3c5c300c",
225
+ "spec/v2/core/interrupt.md": "4bd345bccdb8e2762ca95833cc67086158640c27ffa5ef84915a4ab3de348440",
226
+ "spec/v2/core/overview.md": "8ece6667535d24ed4013a046cb11e1a649f70e39625fc55e09ceb2c02505ed9c",
227
+ "spec/v2/core/packs.md": "90f32947816b4ec2b045b063ad2914126c73353883514a270ad200205dc8e51b",
228
+ "spec/v2/core/persistence.md": "e18195e078d7b0b799055005567e8d86a2ea7518c87809cfaf43dfe9f5d5c8ee",
229
+ "spec/v2/core/replay.md": "19b2fcbda4693895d39b99593f498b76376d77472a813bba6c22fca02e106332",
230
+ "spec/v2/core/runs.md": "f5f24df6da76b1d6e2cf655af3bbe3340195c36ec8d9ce0ca6e357385b1a8771",
231
+ "spec/v2/core/security-defaults.md": "53a1bc549aaa356a4fcb56cdb1c4f5880d3fb65d5d49dd570a413bb29aff82e5",
232
+ "spec/v2/core/versioning.md": "2eb4e8e5a1208a026a3d497d1e0fb76a4f7cb97e514d6f41f45dd66b89405abb",
233
+ "spec/v2/core/webhooks.md": "1d2719335f8fb46cd31dbb836b44ea7d78ed54f98ea181927502814603c5b935",
234
+ "spec/v2/core/workflow-chain-packs.md": "99ad4aeeeeb05521720dc2ae18ae63e6d604361780ed1b76778ca6d44da70eed",
235
+ "spec/v2/declaration.json": "9e191dffae9f95e622b3fc7087b8c17a2f42177bb4db3c7ee5fdf650443e036d",
236
+ "spec/v2/declaration.schema.json": "eac5f8080bd572f147bd57d0c4ac8ad73b14536d9236222490ae29a33b21e184",
216
237
  "spec/v2/errors.json": "f179414a92f30b5dadf26e9e57699649a67324bd7186d5287647d17605d137d3",
217
238
  "spec/v2/event-codemap.json": "37869a03cdfd8758b57e6f350641a6ebbd383231cebda51f5473ad85f94ab74c",
218
239
  "spec/v2/event-codemap.schema.json": "ce2d0ca085d7668d7ef745d2560b385ec6881d24a6d4b47c49931986b2ba642d",
240
+ "spec/v2/ext/a2uiSurface/README.md": "176cb0542e1c236ffcd6d682536ba0081242684444e2dec790abd480a44a571e",
241
+ "spec/v2/ext/brand/README.md": "1a80102a9cd766dcb6098c999e552af88f5b83c88951acde18d17cc0b29aa09d",
242
+ "spec/v2/ext/canvas/README.md": "a2d5f28523753e8e85cc9042db39faf9207a2eebce1a8553be22e341f7ea58d9",
243
+ "spec/v2/ext/chat/README.md": "39fe2961502d2937fe487cf52acfffeeb4a396731f0b2bf737b109eac2752bca",
244
+ "spec/v2/ext/coordination/README.md": "a193aa9a59a2d9ac8d2e9775e5be40c29b6f7476ffef35231dbab49377af9207",
245
+ "spec/v2/ext/dataIntegration/README.md": "8361c626ed9f77dc48a3bcaa0c9f769cb6706632941014700f7683efe0918bd1",
246
+ "spec/v2/ext/entities/README.md": "c3bf6c59ddf2255c22a522e689f6e7a11646fbcd309b17699b6cf69a4e2e0f91",
247
+ "spec/v2/ext/grpc-transport/README.md": "7fa692715744d6573443f994c521cbff0bf0be2e81ffdbb01e42f58f32a3ffaf",
248
+ "spec/v2/ext/kanban/README.md": "6792926f7f0cb8a4dcb0acdedb86a1988574fb0b5de164b37159dfe3ba35f908",
249
+ "spec/v2/ext/knowledge/README.md": "d2615320c7de4aec279f276381376a8881e962dd4e86b1435462d742dd00a52f",
250
+ "spec/v2/ext/launchStudio/README.md": "bfe417e774689773f3bbced9fdfa0c18809577aa6040a1312d914c821beed026",
251
+ "spec/v2/ext/messaging/README.md": "8a4702935483ce7900c0ee14793f9dfbeeb09d5e43f8943c946f8f94de2efa6f",
252
+ "spec/v2/ext/portability/README.md": "d9caf3dd723212676a0dd7f847284ce8aa3f0f5ebd4b273580c34a260ece96d7",
253
+ "spec/v2/ext/provider-idempotency/README.md": "c7620aadaf4e7ded997487c917e1583c8bc9ba20aa5353476d66f229207dbd1a",
219
254
  "spec/v2/ext/provider-idempotency/registry.json": "5b5fecad604fb4ab38da39f5abe697a4e57b05406c398f8afeb377331b2bd965",
255
+ "spec/v2/ext/restTransport/README.md": "b39f4d1ddbabd72dd945a39e90218d8413e46baea1269798b96dffb4e429cb4f",
256
+ "spec/v2/ext/sandbox-runtime-notes/README.md": "36c5517082b1cce0f1ccec5153e4bea10144bba88503b3b023f920444f3c15e6",
257
+ "spec/v2/ext/webResearch/README.md": "941dfe8323fd536791167f9fc7d119dd4e165bb7937e3382a19f15cda3d9f19a",
220
258
  "spec/v2/facets/a2a.schema.json": "6b0bceddec6d18d5115aa7d04720a0cc1ad7a9a0a78f53c208b34becaba5d16e",
221
259
  "spec/v2/facets/aiProviders.schema.json": "0c76e48e5d487b5be3b3ab1f6512a4e206c840ee5087d1336c7d7e2853b58d03",
222
260
  "spec/v2/facets/auth.schema.json": "73440c944ad12c41aab0618aeb799a90a0b59e97fc685527f248afb7d015381a",
@@ -230,8 +268,8 @@
230
268
  "spec/v2/path-manifest.json": "034152e09b1458c66810d4050e20a273b2b9b8fe2d92b66e8b819477de58a1be",
231
269
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
232
270
  "spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
233
- "spec/v2/release.json": "2c18f3bb246203a2de3eef9c1e433e10f548ae74d4689c2cd8eb86a2ae14b39b",
271
+ "spec/v2/release.json": "7735a176082e42a30d7884654d79ac7affd7fc50f4b7346085f7d841769c6819",
234
272
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
235
273
  },
236
- "corpusCommit": "87038a29abb5a19f8eb01bece101fe74af81a7e5"
274
+ "corpusCommit": "0ebdbc9ad741d49c44731e2bd255499a7da6c4a7"
237
275
  }
package/src/cli.ts CHANGED
@@ -677,7 +677,11 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
677
677
  generatedAt: new Date().toISOString(),
678
678
  suite: { name: '@openwop/openwop-conformance', version, targetMajor: target.major, specArtifactsVersion: lock?.version ?? 'repo-layout', ...(lock ? { stampSha256: lock.stampSha256 } : {}) },
679
679
  host: { name: host.name, version: host.version, ...(host.vendor ? { vendor: host.vendor } : {}), build, signingKeyId: keyId, ...(relaxations && relaxations.length ? { relaxations } : {}) },
680
- discovery: { url: discoveryUrl, sha256, protocolVersions, preferredVersion },
680
+ // `document` is what makes `claimedProfiles[].certified` checkable by
681
+ // someone other than this process (RFC 0148 §B(1)); v2 carried it and v3
682
+ // dropped it. `sha256` is a digest of `canonicalJSON(document)`, so the
683
+ // two are consistent by construction and the verifier re-derives it.
684
+ discovery: { url: discoveryUrl, sha256, protocolVersions, preferredVersion, document },
681
685
  claimedProfiles: claimed3,
682
686
  results: { totals: totals3, requirements: rows3 },
683
687
  witnessSha256: witnessDigest(rows3),
@@ -18,6 +18,7 @@
18
18
  * in cli.ts) and pure: it never reads the environment.
19
19
  */
20
20
  import { createHash, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify, type KeyObject } from 'node:crypto';
21
+ import { profileDerivable, type DiscoveryPayload } from './profiles.js';
21
22
 
22
23
  export type BundleV3Result = 'executed-pass' | 'executed-fail' | 'skipped' | 'inapplicable' | 'blocked';
23
24
 
@@ -41,7 +42,25 @@ export interface BundleV3 {
41
42
  generatedAt: string;
42
43
  suite: { name: '@openwop/openwop-conformance'; version: string; targetMajor: 1 | 2; specArtifactsVersion: string; stampSha256?: string };
43
44
  host: { name: string; version: string; vendor?: string; build: { kind: 'image-digest' | 'commit' | 'artifact-sha256'; id: string }; signingKeyId?: string; relaxations?: BundleV3Relaxation[] };
44
- discovery: { url: string; sha256: string; protocolVersions: string[]; preferredVersion: string };
45
+ /**
46
+ * `document` is the captured `/.well-known/openwop` payload, OPTIONAL.
47
+ *
48
+ * v2 bundles carried it (`discovery: { url, sha256, document }`) and
49
+ * `verifyBundleV2` read it to decide whether each claimed profile was
50
+ * DERIVABLE from what the host actually advertised. v3 shipped
51
+ * `{ url, sha256, protocolVersions, preferredVersion }` and `verifyBundleV3`
52
+ * dropped the check to match — so a v3 bundle's `certified: true` became a
53
+ * claim only its emitter could evaluate. `sha256` does not help a reader: it
54
+ * is a digest of `canonicalJSON(document)`, and a digest of a document you do
55
+ * not have proves nothing about its contents. Re-fetching is not a
56
+ * substitute, because the host redeploys and the bundle is supposed to
57
+ * attest to the build it names.
58
+ *
59
+ * Optional, not required: every bundle cut before this revision lacks it and
60
+ * stays verifiable. Present-and-inconsistent is a rejection; absent is a
61
+ * stated gap (`V3Verdict.derivabilityChecked === false`).
62
+ */
63
+ discovery: { url: string; sha256: string; protocolVersions: string[]; preferredVersion: string; document?: Record<string, unknown> };
45
64
  claimedProfiles: BundleV3Profile[];
46
65
  results: { totals: Record<'executedPass' | 'executedFail' | 'skipped' | 'inapplicable' | 'blocked', number>; requirements: BundleV3Requirement[] };
47
66
  witnessSha256: string;
@@ -104,6 +123,13 @@ export interface V3Verdict {
104
123
  readonly signatureVerified: boolean;
105
124
  readonly verifierSignatureVerified: boolean;
106
125
  readonly certifiedProfiles: string[];
126
+ /**
127
+ * Whether every `certified` profile was re-derived from the bundle's own
128
+ * `discovery.document`. `false` means the bundle did not carry one, so the
129
+ * certified list is the emitter's word — not wrong, but not independently
130
+ * checked. Read it before quoting a verdict.
131
+ */
132
+ readonly derivabilityChecked: boolean;
107
133
  }
108
134
 
109
135
  export function verifyBundleV3(bundle: BundleV3, opts: VerifyV3Options = {}): V3Verdict {
@@ -158,6 +184,32 @@ export function verifyBundleV3(bundle: BundleV3, opts: VerifyV3Options = {}): V3
158
184
  else if (p.certified) certifiedProfiles.push(p.id);
159
185
  }
160
186
  if (expected.blocked > 0 && certifiedProfiles.length > 0) rejections.push({ kind: 'blocked-certified', detail: `${expected.blocked} blocked row(s): a bundle with blocked > 0 does not certify (RFC 0168 §E.1)` });
187
+
188
+ // Derivability, restored. RFC 0148 §B(1): a profile certifies only if it is
189
+ // DERIVABLE from what the host advertised. `verifyBundleV2` checked exactly
190
+ // that against `discovery.document`; v3 stopped carrying the document and the
191
+ // check went with it, leaving `certified: true` a claim only its emitter
192
+ // could evaluate. When the document is present the check runs again — and it
193
+ // runs on the SAME bytes the signature covers, because `discovery.sha256` is
194
+ // a digest of `canonicalJSON(document)` and is re-derived here first. A
195
+ // document that does not hash to the signed digest is a substituted
196
+ // document, and rejecting it is the whole point of checking.
197
+ const document = bundle.discovery?.document;
198
+ let derivabilityChecked = false;
199
+ if (document !== undefined) {
200
+ const digest = createHash('sha256').update(canonicalJSON(document)).digest('hex');
201
+ if (digest !== bundle.discovery?.sha256) {
202
+ rejections.push({ kind: 'discovery-digest', detail: `discovery.document hashes to ${digest.slice(0, 12)} but discovery.sha256 is ${String(bundle.discovery?.sha256).slice(0, 12)} — the captured document is not the one the signature attests to` });
203
+ } else {
204
+ derivabilityChecked = true;
205
+ for (const p of bundle.claimedProfiles ?? []) {
206
+ if (!p.certified) continue;
207
+ if (!profileDerivable(document as DiscoveryPayload, p.id)) {
208
+ rejections.push({ kind: 'profile-not-derivable', profile: p.id, detail: `${p.id} is marked certified, but the captured discovery document does not derive it (RFC 0148 §B(1)) — evidence cannot certify a profile the host does not advertise` });
209
+ }
210
+ }
211
+ }
212
+ }
161
213
  // RFC 0168 §E.2: the verifier REFUSES, it does not warn — but a refusal is
162
214
  // scoped to what it names. A rejection carrying a `profile` removes that
163
215
  // profile only (a relaxation on one obligation does not poison an unrelated
@@ -171,5 +223,6 @@ export function verifyBundleV3(bundle: BundleV3, opts: VerifyV3Options = {}): V3
171
223
  signatureVerified,
172
224
  verifierSignatureVerified,
173
225
  certifiedProfiles: bundleWide.length > 0 ? [] : certifiedProfiles.filter((p) => !scoped.has(p)),
226
+ derivabilityChecked,
174
227
  };
175
228
  }
@@ -35,7 +35,7 @@ import { driver, type OpenWOPResponse } from './driver.js';
35
35
  import { loadEnv } from './env.js';
36
36
  import { seamPath, seamsProfileAdvertised } from './seams.js';
37
37
  import { readErrorCode } from './error-envelope.js';
38
- import { V1_DIR } from './paths.js';
38
+ import { SPEC_V2_DIR } from './paths.js';
39
39
 
40
40
  export const SEED_PATH_V1 = '/v1/host/sample/event-log/seed';
41
41
  export const SEED_PATH = seamPath(SEED_PATH_V1);
@@ -171,7 +171,12 @@ let codemap: Map<string, string> | undefined;
171
171
  export function codemapV1toV2(): Map<string, string> {
172
172
  if (codemap) return codemap;
173
173
  const m = new Map<string, string>(FALLBACK);
174
- const candidate = V1_DIR ? join(V1_DIR, '..', 'v2', 'event-codemap.json') : null;
174
+ // Same wrong anchor as `registeredOrgs` carried, found while fixing that one:
175
+ // v1-anchored, so null on every published layout. It fails less loudly here
176
+ // because FALLBACK stands — but 7 rows are not 117, and a scenario asserting
177
+ // `got.type === map.get(s.type)` for a type outside the fallback was checking
178
+ // against a name the truncated map never had.
179
+ const candidate = SPEC_V2_DIR ? join(SPEC_V2_DIR, 'event-codemap.json') : null;
175
180
  if (candidate && existsSync(candidate)) {
176
181
  try {
177
182
  const doc = JSON.parse(readFileSync(candidate, 'utf8')) as { rows?: Array<{ v1?: unknown; v2?: unknown }> };
@@ -182,6 +187,82 @@ export function codemapV1toV2(): Map<string, string> {
182
187
  return m;
183
188
  }
184
189
 
190
+ /**
191
+ * The registered vendor orgs, from `spec/v2/declaration.json` `extensions`
192
+ * (events.md §Rules; RFC 0171 §A.1). A vendor event type's first segment MUST
193
+ * be a key here, which is what separates "read under its own name unchanged"
194
+ * from "fail the read with `event_type_unmapped`" (persistence.md §The codemap
195
+ * is data / §The era key).
196
+ *
197
+ * `undefined` means the declaration is not on disk in this layout — a scenario
198
+ * that needs the registry MUST record that as a soft-skip rather than guess.
199
+ * There is no fallback list on purpose: a hard-coded org would make the suite
200
+ * the registry, and the whole defect this closes was a rule citing a registry
201
+ * that did not exist.
202
+ */
203
+ let orgs: ReadonlySet<string> | null | undefined;
204
+ export function registeredOrgs(): ReadonlySet<string> | undefined {
205
+ if (orgs !== undefined) return orgs ?? undefined;
206
+ // SPEC_V2_DIR, not `V1_DIR/../v2`. The v1-anchored form shipped in 2.0.5 and
207
+ // resolved to null for every consumer of the published package — `spec/v1/`
208
+ // exists only in a repo checkout, so a v2 lookup routed through a v1 probe
209
+ // lost the registry the moment anyone installed the suite. The declaration is
210
+ // reachable in both layouts; only the anchor was wrong.
211
+ const candidate = SPEC_V2_DIR ? join(SPEC_V2_DIR, 'declaration.json') : null;
212
+ orgs = null;
213
+ if (candidate && existsSync(candidate)) {
214
+ try {
215
+ const doc = JSON.parse(readFileSync(candidate, 'utf8')) as { extensions?: Record<string, unknown> };
216
+ if (doc.extensions && typeof doc.extensions === 'object') orgs = new Set(Object.keys(doc.extensions));
217
+ } catch { /* left null: unreadable is indistinguishable from absent, and both are soft-skips */ }
218
+ }
219
+ return orgs ?? undefined;
220
+ }
221
+
222
+ export type Gate = { ok: true } | { ok: false; kind: 'blocked' | 'inapplicable'; reason: string };
223
+
224
+ const orgOf = (type: string): string => type.split('.')[0] ?? '';
225
+
226
+ /**
227
+ * Preconditions for the REFUSAL half of the reader rule (`v2-unmapped-type-
228
+ * refused`): a type the codemap does not name, whose org is not registered,
229
+ * MUST fail the read.
230
+ *
231
+ * `registered === undefined` (no resolvable registry) is NOT a blocker here.
232
+ * An unreadable registry registers nothing, so the driven org is unregistered
233
+ * either way and the refusal is still the required outcome — the fail-CLOSED
234
+ * reading. Only a registry that resolves AND names the org invalidates the leg.
235
+ *
236
+ * Suite 2.0.5 got this wrong by sharing one precondition with the control leg,
237
+ * which does need the registry. On published layouts the registry did not
238
+ * resolve, so the refusal leg soft-skipped `inapplicable` on exactly the hosts
239
+ * it exists to catch: one still answering `200` to an unmapped type and one
240
+ * correctly answering `500` were both green. A precondition belongs to the leg
241
+ * that needs it, never to the file.
242
+ */
243
+ export function unmappedRefusalGate(registered: ReadonlySet<string> | undefined, map: ReadonlyMap<string, string>, type: string): Gate {
244
+ if (registered?.has(orgOf(type)) === true) {
245
+ return { ok: false, kind: 'blocked', reason: `the refusal leg drives ${type}, whose org '${orgOf(type)}' is NOW REGISTERED in spec/v2/declaration.json extensions — it is a vendor type that must pass through, not an unmapped one that must be refused; pick an unregistered org for this leg` };
246
+ }
247
+ if (map.has(type)) return { ok: false, kind: 'blocked', reason: `${type} now has a codemap row (→ ${String(map.get(type))}) — the refusal leg requires a type the codemap does not name` };
248
+ return { ok: true };
249
+ }
250
+
251
+ /**
252
+ * Preconditions for the CONTROL half: a registered vendor org the codemap does
253
+ * not name MUST be read under its own name unchanged.
254
+ *
255
+ * This leg genuinely needs the registry to resolve — it asserts that an org IS
256
+ * registered, and an absent registry cannot establish that. `inapplicable` here
257
+ * is honest rather than over-gating.
258
+ */
259
+ export function vendorControlGate(registered: ReadonlySet<string> | undefined, map: ReadonlyMap<string, string>, type: string): Gate {
260
+ if (registered === undefined) return { ok: false, kind: 'inapplicable', reason: 'spec/v2/declaration.json is not resolvable in this layout — the control leg asserts that a REGISTERED org passes through, and guessing which orgs are registered would make the suite the registry' };
261
+ if (!registered.has(orgOf(type))) return { ok: false, kind: 'blocked', reason: `the control leg needs org '${orgOf(type)}' registered in spec/v2/declaration.json extensions (registered: ${[...registered].join(', ') || 'none'}) — without a registered org the positive half of the vendor rule cannot be driven at all` };
262
+ if (map.has(type)) return { ok: false, kind: 'blocked', reason: `${type} now has a codemap row (→ ${String(map.get(type))}) — the control leg requires a type the codemap does not name` };
263
+ return { ok: true };
264
+ }
265
+
185
266
  /** A minimal era-2 log in v1 vocabulary: two renamed rows between run.started and run.completed. */
186
267
  export function v1FixtureLog(workflowId = 'conformance-noop'): SeedEvent[] {
187
268
  const t0 = Date.parse('2026-01-15T10:00:00.000Z');
@@ -5,9 +5,36 @@
5
5
  import { driver } from './driver.js';
6
6
  import { discoveryFamilies } from './discovery-capabilities.js';
7
7
  import { isFixtureAdvertised } from './fixtures.js';
8
+ import { targetMajor } from './seams.js';
9
+ import { gateFamily } from './v2.js';
8
10
 
9
- /** Reads `capabilities.memory.attribution` from discovery; null when unadvertised. */
11
+ /**
12
+ * The runs collection under the major in play. `/v1/runs` is a v1 address, not
13
+ * a seam, so `driver`'s seam rewrite does not touch it and a v1-shaped helper
14
+ * simply 404s on a v2 host. Every path in this file goes through here so a
15
+ * scenario can be made dual-major without editing three call sites and missing
16
+ * a fourth.
17
+ */
18
+ export const runsPath = (): string => (targetMajor() === 2 ? '/runs' : '/v1/runs');
19
+
20
+ /**
21
+ * `memory.attribution`, read the way the major in play advertises it, with the
22
+ * gate RECORDED at major 2.
23
+ *
24
+ * At major 1 this is the v1 capability read it always was. At major 2 it goes
25
+ * through `gateFamily('memory')`, which registers `openwop.family.memory` in
26
+ * the behaviour ledger — so a bundle distinguishes "the host does not
27
+ * advertise `memory`" from "the suite's reader did not understand the shape".
28
+ * An unrecorded early return looks identical to both, and to a scenario that
29
+ * was never selected; that is the whole reason a v1 behavioural file cannot
30
+ * just be pointed at a v2 host and trusted.
31
+ */
10
32
  export async function readMemoryAttributionCap(): Promise<Record<string, unknown> | null> {
33
+ if (targetMajor() === 2) {
34
+ const family = await gateFamily('memory');
35
+ const attr = family?.['attribution'];
36
+ return attr && typeof attr === 'object' && !Array.isArray(attr) ? (attr as Record<string, unknown>) : null;
37
+ }
11
38
  const res = await driver.get('/.well-known/openwop');
12
39
  const caps = discoveryFamilies(res.json);
13
40
  const mem = caps && typeof caps === 'object' ? (caps as Record<string, unknown>)['memory'] : undefined;
@@ -33,7 +60,7 @@ export async function seedRun(_label: string): Promise<string | null> {
33
60
  // certifying. No caller ever needed a SECOND tenant: every leg reads back its own run.
34
61
  // The tenant is the credential's own tenant (the host defaults it from the API key —
35
62
  // rest-endpoints.md), which is what "a run the bearer provably owns" means.
36
- const r = await driver.post('/v1/runs', { workflowId: SEED_FIXTURE, inputs: {} });
63
+ const r = await driver.post(runsPath(), { workflowId: SEED_FIXTURE, inputs: {} });
37
64
  if (r.status !== 200 && r.status !== 201) return null;
38
65
  return (r.json as { runId?: string } | undefined)?.runId ?? null;
39
66
  }
@@ -46,7 +73,7 @@ interface RunEventLike {
46
73
 
47
74
  /** Fetches a run's events and returns only the `memory.written` ones. */
48
75
  export async function memoryWrittenEvents(runId: string): Promise<RunEventLike[]> {
49
- const res = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events`);
76
+ const res = await driver.get(`${runsPath()}/${encodeURIComponent(runId)}/events`);
50
77
  const events = (res.json as { events?: RunEventLike[] } | undefined)?.events ?? [];
51
78
  return events.filter((e) => e.type === 'memory.written');
52
79
  }
package/src/lib/paths.ts CHANGED
@@ -66,6 +66,21 @@ interface ResolvedLayout {
66
66
  readonly coverageDocPath: string | null;
67
67
  /** Directory containing v1 prose docs (`*.md`), if present in this layout. */
68
68
  readonly v1Dir: string | null;
69
+ /**
70
+ * Directory containing the v2 corpus data (`declaration.json`, the codemap,
71
+ * `spec/v2/core/*.md`), if present in this layout.
72
+ *
73
+ * Anchored on the CONTRACT root, not the layout root, because that is the
74
+ * only anchor that holds in both shapes: in a repo checkout the contract
75
+ * root is the repo and `spec/v2/` sits inside it; in a published install it
76
+ * is the `@openwop/spec-artifacts` peer, which ships `spec/` while the
77
+ * conformance package ships none. A resolver anchored on `v1Dir` instead
78
+ * (the shape before 2.0.6) returned null for every consumer of the published
79
+ * package, because `spec/v1/` is a repo-only directory — so a v2 lookup was
80
+ * routed through a v1 probe and every published layout silently lost the
81
+ * data. See `era2-seed.registeredOrgs`.
82
+ */
83
+ readonly specV2Dir: string | null;
69
84
  /** Path to repository README.md, if present in this layout. */
70
85
  readonly readmePath: string | null;
71
86
  /** Path to the TypeScript SDK run-helper source, if present in this layout. */
@@ -137,6 +152,8 @@ function resolveFromRoot(root: string, layout: ResolvedLayout['layout'], contrac
137
152
  : null;
138
153
  const v1Probe = join(root, 'spec', 'v1');
139
154
  const v1Dir = existsSync(v1Probe) ? v1Probe : null;
155
+ const specV2Probe = join(contractRoot, 'spec', 'v2');
156
+ const specV2Dir = existsSync(specV2Probe) ? specV2Probe : null;
140
157
  const readmeProbe = join(root, 'README.md');
141
158
  const readmePath = existsSync(readmeProbe) ? readmeProbe : null;
142
159
  const typescriptRunHelpersProbe = join(root, 'sdk', 'typescript', 'src', 'run-helpers.ts');
@@ -155,6 +172,7 @@ function resolveFromRoot(root: string, layout: ResolvedLayout['layout'], contrac
155
172
  fixturesDocPath,
156
173
  coverageDocPath,
157
174
  v1Dir,
175
+ specV2Dir,
158
176
  readmePath,
159
177
  typescriptRunHelpersPath,
160
178
  pythonTypesPath,
@@ -208,6 +226,7 @@ export const CONFORMANCE_README_PATH: string | null = _layout.conformanceReadmeP
208
226
  export const FIXTURES_DOC_PATH: string | null = _layout.fixturesDocPath;
209
227
  export const COVERAGE_DOC_PATH: string | null = _layout.coverageDocPath;
210
228
  export const V1_DIR: string | null = _layout.v1Dir;
229
+ export const SPEC_V2_DIR: string | null = _layout.specV2Dir;
211
230
  export const README_PATH: string | null = _layout.readmePath;
212
231
  export const TYPESCRIPT_RUN_HELPERS_PATH: string | null = _layout.typescriptRunHelpersPath;
213
232
  export const PYTHON_TYPES_PATH: string | null = _layout.pythonTypesPath;
@@ -29,18 +29,42 @@
29
29
  * should have: nothing normative changed, the instrument just could not see
30
30
  * it before.
31
31
  *
32
- * Gated on `capabilities.memory.attribution.emitsWriteEvents`; soft-skips
33
- * when unadvertised, when the seeded run wrote no memory, or when the host
34
- * doesn't support `:fork` in `replay` mode.
32
+ * Gated on `memory.attribution.emitsWriteEvents`; soft-skips when unadvertised,
33
+ * when the seeded run wrote no memory, or when the host doesn't support `:fork`
34
+ * in `replay` mode.
35
+ *
36
+ * MAJORS [1, 2] (suite 2.0.5), and why this file is the first one moved.
37
+ * The rule it checks — a replay MUST re-emit recorded-fact events and MUST NOT
38
+ * regenerate their ids — is `replay.md` §Determinism caveat 5 in BOTH majors,
39
+ * word for word. It was nevertheless `majors: [1]`, so no v2 host had ever been
40
+ * measured on it, for two reasons that have nothing to do with the rule:
41
+ *
42
+ * 1. Its gate read the v1 capability shape directly and returned early with
43
+ * NOTHING RECORDED. An unrecorded early return is indistinguishable, in a
44
+ * bundle, from a host that does not advertise `memory`, from a suite
45
+ * reader that does not understand the v2 shape, and from a file that was
46
+ * never selected. It now goes through `gateFamily('memory')` at major 2,
47
+ * which registers `openwop.family.memory` — the gate becomes evidence.
48
+ * 2. Its three paths were hard-coded `/v1/…`. That is a v1 address and not a
49
+ * seam, so the driver's seam rewrite never touched it; on a v2 host the
50
+ * helper would simply 404 and the file would report a host defect that was
51
+ * really a suite defect. `runsPath()` resolves the major.
52
+ *
53
+ * Both are properties of the INSTRUMENT, not of the obligation. That is the
54
+ * shape to look for in the remaining v1 behavioural files: a rule that holds at
55
+ * major 2, held back by a v1-shaped gate and a v1-shaped path. Neither is a
56
+ * reason for a host to go unmeasured, and neither announces itself — the file
57
+ * was green at major 1 the whole time.
35
58
  *
36
59
  * @see RFCS/0057-memory-write-attribution-event.md §D
60
+ * @see spec/v2/core/replay.md §Determinism caveats
37
61
  */
38
62
 
39
63
  import { describe, it, expect } from 'vitest';
40
64
  import { softSkip } from '../lib/soft-skip.js';
41
65
  import { driver } from '../lib/driver.js';
42
66
  import { pollUntilTerminal } from '../lib/polling.js';
43
- import { readMemoryAttributionCap, emitsWriteEvents, seedRun, memoryWrittenEvents } from '../lib/memoryAttribution.js';
67
+ import { readMemoryAttributionCap, emitsWriteEvents, seedRun, memoryWrittenEvents, runsPath } from '../lib/memoryAttribution.js';
44
68
  import { req } from '../lib/requirement-ids.js';
45
69
 
46
70
  function memoryIdOf(payload: Record<string, unknown> | undefined): string | null {
@@ -51,7 +75,7 @@ function memoryIdOf(payload: Record<string, unknown> | undefined): string | null
51
75
  describe('memory-attribution-replay-stable (RFC 0057 §D)', () => {
52
76
  it('a replay-mode fork introduces no memory.written with a new memoryId', async () => {
53
77
  const cap = await readMemoryAttributionCap();
54
- if (!emitsWriteEvents(cap)) return softSkip('inapplicable', 'capability or profile not advertised by this host gate `!emitsWriteEvents(cap)` returned early');
78
+ if (!emitsWriteEvents(cap)) return softSkip('inapplicable', 'memory.attribution.emitsWriteEvents not advertised at major 2 the gate is recorded under openwop.family.memory, so an unadvertised family and an unreadable one are distinguishable in the bundle');
55
79
  const runId = await seedRun('mem-attr-replay');
56
80
  if (!runId) return softSkip('blocked', 'precondition not met — `!runId` returned early (seam, prior step, or fixture unavailable)');
57
81
  try {
@@ -63,7 +87,7 @@ describe('memory-attribution-replay-stable (RFC 0057 §D)', () => {
63
87
  if (original.length === 0) return softSkip('blocked', 'run wrote no memory — nothing to test');
64
88
  const recordedIds = new Set(original.map((e) => memoryIdOf(e.payload)).filter((x): x is string => x !== null));
65
89
 
66
- const fork = await driver.post(`/v1/runs/${runId}:fork`, { fromSeq: 0, mode: 'replay' });
90
+ const fork = await driver.post(`${runsPath()}/${encodeURIComponent(runId)}:fork`, { fromSeq: 0, mode: 'replay' });
67
91
  if (fork.status !== 200 && fork.status !== 201) return softSkip('inapplicable', 'replay fork unsupported — soft-skip (fork.status !== 200 && fork.status !== 201)');
68
92
  const forkId = (fork.json as { runId?: string } | undefined)?.runId;
69
93
  if (!forkId) return softSkip('blocked', 'precondition not met — `!forkId` returned early (seam, prior step, or fixture unavailable)');
@@ -52,13 +52,57 @@
52
52
  * file's worth: the boundary it checks is real and a tier-1 host shipped the
53
53
  * bug it catches.
54
54
  *
55
+ * The payload-equivalence leg below does NOT move that verdict. It strengthens
56
+ * the same single `it` behind the same fixture gate, so the file still cannot
57
+ * witness for a predicate holder that advertises no optional fixture. Promotion
58
+ * stays blocked on a prior condition anyway: the strengthened leg has not been
59
+ * MEASURED on the reference host, and a floor is not a place to find out.
60
+ *
55
61
  * What this file deliberately does NOT assert: the CONTENT of the re-executed
56
62
  * tail, determinism across two forks, timing, and effect re-fire suppression.
57
63
  * Those are `replay.md` §Replay determinism and the seams floor's, witnessed in
58
64
  * `replay-fork-arbitrary` (major 1) and `v2-effect-seam-no-refire` (seams).
59
65
  *
66
+ * PAYLOAD EQUIVALENCE, and the honest size of what it witnesses.
67
+ * `replay.md` §Byte-equivalence of the prefix opens: "The events at indices
68
+ * `[0, fromSeq)` MUST be byte-equivalent between source and replay, modulo
69
+ * per-region clock fields (RFC 0036 §E) and ULID time-component entropy when
70
+ * ULIDs are minted fresh." Until this revision the only witness of that MUST
71
+ * compared `${sequence}:${type}` — sequence-and-TYPE equality. A host could
72
+ * inherit a prefix whose event PAYLOADS differed from the source, in every
73
+ * field, and pass. "Byte-equivalent" was checked by comparing two of an event's
74
+ * ten fields, and the gap was invisible because the leg was green.
75
+ *
76
+ * The exclusion set is NOT invented here. `runs.md` §Diff and ancestry already
77
+ * states, normatively, what two logs are compared on: "`eventId`, `runId`,
78
+ * `timestamp` and other run-scoped fields MUST be excluded from comparison."
79
+ * That is the spec's own definition of the same event on two runs, and a fork
80
+ * IS a new run with its own `runId` and its own `eventId`s. So:
81
+ *
82
+ * compared `type`, `nodeId`, `payload` (deep)
83
+ * excluded `eventId` run-scoped (runs.md §Diff) + ULID entropy (§36)
84
+ * `runId` the fork is a different run, by construction
85
+ * `timestamp` a per-region clock field (RFC 0036 §E, named in §36)
86
+ * `causationId` names an `eventId` of this run, so run-scoped
87
+ *
88
+ * NAMED RESIDUE, not silently dropped. `schemaVersion` and `engineVersion` are
89
+ * not asserted: §36's carve-out list does not excuse them, but neither does any
90
+ * clause say whether a host re-stamps them when copying fixed history into a
91
+ * replay running against CURRENT code, and a scenario should not settle a
92
+ * question the prose left open. `replay.md` line 37 — "`variables`, `channels`,
93
+ * and `status` of the run snapshot at each index in that range MUST be
94
+ * byte-equivalent" — has no witness at any major and gets none here: there is
95
+ * no wire surface that returns a run snapshot AS OF an index, only at head, so
96
+ * an unaided scenario cannot reach it. Both are gaps in the evidence, recorded
97
+ * as gaps. This leg witnesses a NAMED SUBSET of §36 and says which.
98
+ *
99
+ * The leg runs LAST, after the boundary assertion, so a host that fails this
100
+ * stricter new check still reports the boundary verdict the file was cut for.
101
+ *
60
102
  * @see spec/v2/core/replay.md §Endpoint
103
+ * @see spec/v2/core/replay.md §Byte-equivalence of the prefix
61
104
  * @see spec/v2/core/runs.md §Fork
105
+ * @see spec/v2/core/runs.md §Diff and ancestry
62
106
  */
63
107
 
64
108
  import { describe, it, expect } from 'vitest';
@@ -71,10 +115,32 @@ import { req } from '../lib/requirement-ids.js';
71
115
 
72
116
  const ID = 'openwop.requirement.0170.fork-prefix-boundary';
73
117
  const DOC = 'spec/v2/core/replay.md §Endpoint';
118
+ const BYTES = 'spec/v2/core/replay.md §Byte-equivalence of the prefix';
74
119
  const MULTI = 'conformance-multi-node';
75
120
  const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
76
121
 
77
- interface Ev { readonly sequence?: unknown; readonly type?: unknown }
122
+ interface Ev { readonly sequence?: unknown; readonly type?: unknown; readonly nodeId?: unknown; readonly payload?: unknown }
123
+
124
+ /**
125
+ * Stable JSON for deep comparison: object keys sorted at every depth, so two
126
+ * payloads differing only in serialization order compare equal. Array order is
127
+ * preserved — it is content.
128
+ */
129
+ function canonical(v: unknown): string {
130
+ const norm = (x: unknown): unknown => {
131
+ if (Array.isArray(x)) return x.map(norm);
132
+ if (x !== null && typeof x === 'object') {
133
+ const o: Record<string, unknown> = {};
134
+ for (const k of Object.keys(x as Record<string, unknown>).sort()) o[k] = norm((x as Record<string, unknown>)[k]);
135
+ return o;
136
+ }
137
+ return x;
138
+ };
139
+ return JSON.stringify(norm(v)) ?? 'undefined';
140
+ }
141
+
142
+ /** The compared projection of an event: everything §36 binds that is not run-scoped. */
143
+ const projection = (e: Ev): string => canonical({ type: e.type ?? null, nodeId: e.nodeId ?? null, payload: e.payload ?? null });
78
144
 
79
145
  async function discovery(): Promise<Record<string, unknown> | null> { try { return await v2Discovery(); } catch { return null; } }
80
146
  async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> { try { return await fn(); } catch { return null; } }
@@ -156,5 +222,23 @@ describe('v2 run-fork-prefix (replay.md §Endpoint — the boundary is exclusive
156
222
  String(at.type),
157
223
  req(ID, DOC, `the event at sequence ${fromSeq} is re-executed, not fixed history: the fork MUST NOT carry the source's ${String(point.type)} at that sequence — seeing it means the prefix was copied inclusively ([0, ${fromSeq}] instead of [0, ${fromSeq}))`),
158
224
  ).not.toBe(String(point.type));
225
+
226
+ // Byte-equivalence of the prefix, on the named subset (see the docblock).
227
+ // Last, so a host that fails this stricter check still reports the boundary
228
+ // verdict above. The exclusion set is runs.md §Diff and ancestry's, not
229
+ // this file's: eventId, runId, timestamp and causationId are run-scoped,
230
+ // and a fork is a different run.
231
+ const src = source.filter((e) => (e.sequence as number) < fromSeq);
232
+ const differing = inherited
233
+ .map((e, i) => ({ seq: e.sequence as number, fork: projection(e), from: src[i] === undefined ? '<missing>' : projection(src[i]) }))
234
+ .filter((r) => r.fork !== r.from);
235
+ expect(
236
+ differing.map((r) => r.seq),
237
+ req(
238
+ ID,
239
+ BYTES,
240
+ `every event in [0, ${fromSeq}) MUST be byte-equivalent to the source's, modulo per-region clock fields and freshly minted ULIDs — compared on {type, nodeId, payload}, excluding eventId/runId/timestamp/causationId as run-scoped (runs.md §Diff and ancestry). ${differing.length} row(s) differ${differing.length ? `: ${differing.slice(0, 3).map((r) => `seq ${r.seq} fork ${r.fork.slice(0, 160)} vs source ${r.from.slice(0, 160)}`).join(' | ')}` : ''}. Inherited history is copied, not recomputed: a differing payload means the fork re-derived a row the spec calls fixed`,
241
+ ),
242
+ ).toEqual([]);
159
243
  }, 60_000);
160
244
  });