@openwop/openwop-conformance 2.46.44 → 2.47.0

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,3 +1,9 @@
1
+ ## [2.47.0] — 2026-11-13 — Cache-key recipe 2, telemetry export, sabotage ledger
2
+
3
+ - **RFC 0260.** `v2-replay-llm-cache-key`: a server-free shape leg, an unaided implies-idempotency leg, and four host legs on `GET /runs/{runId}/effects` (key recorded, parameters in the key, stable, portable), gated on `replay.llmCacheKeyRecipe` and the `conformance-llm-cache-key` fixture. Judges in `lib/llm-cache-key-2.ts`.
4
+ - **RFC 0262.** `v2-telemetry-export`: a server-free shape leg and five legs against the suite OTLP collector (run span, node span, `traceparent`, no secret in spans, GenAI attributes), gated on the `telemetry` family.
5
+ - **RFC 0263.** `v2-sabotage-ledger` (coherence) drives `check-accepted-predicate` rules 5 and 6 on fixture trees, with positive and negative controls.
6
+
1
7
  ## [2.46.44] — 2026-11-08 — Bounded loopback and resolvedVia go Active
2
8
 
3
9
  - **RFC 0258.** `v2-bounded-loopback`: a server-free shape leg and six host legs (re-arm, history from the log, `ApprovalData.loopback`, withdraw, fail on exhaustion, replay re-arm), gated on the `loopback` family and the `conformance-loopback-refine` / `-fail` fixtures. Judges in `lib/bounded-loopback-witness.ts`.
package/README.md CHANGED
@@ -137,7 +137,7 @@ Each scenario file records its disposition and assertion count in `<report-dir>/
137
137
 
138
138
  ## What's Covered
139
139
 
140
- The current suite has 668 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 228 files run against a v2 host and 455 against a v1 host; 15 of them run at both.
140
+ The current suite has 670 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 230 files run against a v2 host and 455 against a v1 host; 15 of them run at both.
141
141
 
142
142
  The v2 scenarios (`src/scenarios/v2-*.test.ts`) cover, by area:
143
143
 
@@ -166,7 +166,7 @@ Three kinds of scenario share the tree:
166
166
  - **Host** checks drive a live host over HTTP.
167
167
  - **Gated** checks run only when the host advertises the family, profile or fixture they need, and otherwise record `inapplicable` or `blocked` with a reason. `--require-behavior` turns an advertised-but-unwitnessable behaviour into a failure.
168
168
 
169
- Current source tree: 668 scenario files. [`coverage.md`](./coverage.md) maps each spec document and every OpenAPI operation to its scenarios. [`CHANGELOG.md`](./CHANGELOG.md) records when each scenario landed.
169
+ Current source tree: 670 scenario files. [`coverage.md`](./coverage.md) maps each spec document and every OpenAPI operation to its scenarios. [`CHANGELOG.md`](./CHANGELOG.md) records when each scenario landed.
170
170
 
171
171
  ## How the 2.x suite is built
172
172
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.46.44",
4
- "stampSha256": "e300a0ad34ed84f4615a9c50caeb9d25598c83c66924ad027cd8cfb9ee8b2651"
3
+ "version": "2.47.0",
4
+ "stampSha256": "f03caded5ddba2a0fd12bf7b6a93995789afdd4ae5ffd56d0cfcde8701f7a657"
5
5
  }
@@ -0,0 +1,23 @@
1
+ {
2
+ "id": "conformance-llm-cache-key",
3
+ "name": "Conformance: LLM cache key recipe 2 (RFC 0260)",
4
+ "version": "1.0",
5
+ "description": "Fixture for `v2-replay-llm-cache-key.test.ts` (RFC 0260; the rules are the `replay.llmCacheKeyRecipe` facet description and the effect-ledger `cacheKey` description). One node, `conformance.callai.cache-key` (conformance-RESERVED). It makes exactly one `ctx.callAI` call through the host's conformance mock provider, whose provider id is `conformance-mock` and model id `conformance-mock-1`. The call sends one message `{role: \"user\", content: inputs.request.prompt}`, no tools, no toolChoice, no responseSchema, and exactly the parameters in `inputs.request.params` (keys from recipe 2's named set: `temperature`, `maxTokens`, `stop`, `seed`, `topP`, `topK`, `frequencyPenalty`, `presencePenalty`), nothing more. The mock replies `ok`. The host records the call on `GET /runs/{runId}/effects` under this node's id, carrying `cacheKey`. The node completes with `outputs.text`, the reply. A host that does not advertise `replay.llmCacheKeyRecipe` MUST NOT advertise this fixture.",
6
+ "nodes": [
7
+ {
8
+ "id": "llm-call",
9
+ "typeId": "conformance.callai.cache-key",
10
+ "name": "One ctx.callAI call through the conformance mock",
11
+ "position": { "x": 0, "y": 0 },
12
+ "config": {},
13
+ "inputs": {}
14
+ }
15
+ ],
16
+ "edges": [],
17
+ "triggers": [{ "id": "manual", "type": "manual", "enabled": true }],
18
+ "variables": [
19
+ { "name": "request", "type": "object" }
20
+ ],
21
+ "metadata": { "tags": ["conformance", "replay", "llmCacheKeyRecipe"] },
22
+ "settings": { "timeout": 0 }
23
+ }
package/fixtures.md CHANGED
@@ -150,6 +150,7 @@ All fixtures MUST advertise:
150
150
  | v2 fs Sandbox Probe | `conformance-fs-probe` | `storage.md` §`fs` — one `core.conformance.fs-probe` node (conformance-RESERVED) calls the host's own `ctx.fs.read` / `write` with the run's `path`, unchanged; a rejection fails the node with its code and details. Operator symlink `<sandboxRoot>/conformance/escape-link` points outside the root. See §"The storage probe fixtures" | `completed` inside the root; `failed` (`forbidden`, `details.reason: path-outside-sandbox`) for an escape | ≤ 30s |
151
151
  | v2 tableStorage Schema Probe | `conformance-table-schema-probe` | `storage.md` §`tableStorage` — one `core.conformance.table-schema-probe` node (conformance-RESERVED) creates a table `{ k: string, n: number }` with the host's own `ctx.storage.table`, then inserts a well-typed row (`control`), a row whose `n` is a string (`insert-mistyped`), or updates `n` to a string (`update-mistyped`). See §"The tableStorage schema probe fixture" | `completed` for `control`; `failed` (`validation_error`, `details.service: tableStorage`) for a mistyped write | ≤ 30s |
152
152
  | v2 ctx.callAI Attempt Accounting | `conformance-callai-attempt-accounting` | RFC 0248 — one `conformance.callai.echo-attempts` node (conformance-RESERVED) that makes one `ctx.callAI` call through the host's conformance mock, serves the run's `script`, and echoes the result's `attempts` (`v2-callai-attempt-accounting`) | each case run completes or fails as its script dictates | ≤ 30s |
153
+ | v2 LLM Cache Key | `conformance-llm-cache-key` | RFC 0260 — one `conformance.callai.cache-key` node (conformance-RESERVED) making one `ctx.callAI` call through the suite's mock provider with the run's prompt and params unchanged (`v2-replay-llm-cache-key`) | completes; the call's effect record carries `cacheKey` | ≤ 30s |
153
154
  | v2 safeFetch SSRF Probe | `conformance-safefetch-probe` | `host-services.md` §`httpClient` — one `core.conformance.safefetch-probe` node (conformance-RESERVED) calls the host's own `ctx.http.safeFetch(url)` with the run's `url`, unchanged; completes with the response `status` only; a rejection fails the node with its code and details. See §"The safeFetch probe fixture" | `failed` (`egress_denied`, `details.reason: ssrf-blocked`) for a refused target | ≤ 60s |
154
155
  | A2A Task Roundtrip | `conformance-a2a-task-roundtrip` | Track 6 — host consumes the conformance suite's synthetic A2A peer; covers drift points #3 (`AUTH_REQUIRED`) and #4 (`REJECTED`). Node `core.conformance.a2a-invoke` is a **conformance-RESERVED** typeId (renamed 2026-08-16 from `core.a2a.invoke`, a multi-segment `core.*` id no host shipped and the corpus never defined): a host that consumes A2A peers MUST map it to its A2A bridge node (a host-extension node — `a2a-integration.md` §"State projection"); a host that does not consume A2A MUST NOT advertise this fixture | `failed` or `waiting-input` (per `driftScenario` input) | ≤ 30s |
155
156
  | WASM Pack Roundtrip | `conformance-wasm-pack-roundtrip` | RFC 0008 — invokes `vendor.openwop.rust-hello.greet` (loaded WASM pack); exercises required exports + at least one import | `completed` | ≤ 10s |
@@ -924,6 +925,8 @@ For `v2-tool-catalog-annotations`' unclassified-MCP leg, a host that advertises
924
925
 
925
926
  `conformance-callai-attempt-accounting` is what `v2-callai-attempt-accounting` drives. Its node, `conformance.callai.echo-attempts`, is conformance-RESERVED. A host that advertises `aiProviders.callAccounting` maps it to a node that makes exactly one `ctx.callAI` call through the host's conformance mock provider. It does no counting of its own. A host that does not advertise `callAccounting` MUST NOT advertise the fixture.
926
927
 
928
+ `conformance-llm-cache-key` is what `v2-replay-llm-cache-key` drives. Its node, `conformance.callai.cache-key`, is conformance-RESERVED. A host that advertises `replay.llmCacheKeyRecipe` maps it to a node that makes exactly one `ctx.callAI` call through the host's conformance mock provider, passing the run's prompt and parameters unchanged, so the suite can recompute the recipe-2 key from the inputs it sent. A host that does not advertise `llmCacheKeyRecipe` MUST NOT advertise the fixture.
929
+
927
930
  **Inputs.** `request` is passed to `ctx.callAI` unchanged: `prompt`, and optionally `responseSchema`, `maxTokens` and `retry`. `script` is the mock's replies.
928
931
 
929
932
  **The mock.** It serves `script` to the call's provider requests in order, one entry per request, and repeats the last entry past the end. Each entry is `{ kind, outputTokens?, usage?, structuredMode? }`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.46.44",
3
+ "version": "2.47.0",
4
4
  "description": "Black-box conformance suite for OpenWOP hosts (v2 current; v1 scenarios retained for the frozen v1 tree).",
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.46.44"
59
+ "@openwop/spec-artifacts": "2.47.0"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "$comment": "GENERATED by conformance/scripts/generate-requirement-registry.mjs — do not edit. One record per it()/test() in src/scenarios. Ids: openwop.it.<file-stem>.<title-slug>[~n] (src/lib/requirement-ids.ts). A record with id null has an interpolated title; its run-time row is keyed by the rendered title and maps here by file+line only. Renamed ids need a row in requirement-aliases.json.",
3
3
  "generatedFrom": "src/scenarios/*.test.ts",
4
4
  "counts": {
5
- "files": 751,
6
- "tests": 2888,
7
- "withStableId": 2888,
5
+ "files": 754,
6
+ "tests": 2903,
7
+ "withStableId": 2903,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2744
9
+ "explicitIds": 2747
10
10
  },
11
11
  "records": [
12
12
  {
@@ -37052,6 +37052,84 @@
37052
37052
  }
37053
37053
  ]
37054
37054
  },
37055
+ {
37056
+ "id": "openwop.it.v2-replay-llm-cache-key.the-effect-ledger-record-admits-cachekey-recipe-digest-and-refuses-anything-else",
37057
+ "file": "v2-replay-llm-cache-key.test.ts",
37058
+ "line": 102,
37059
+ "title": "the effect-ledger record admits cacheKey {recipe, digest} and refuses anything else",
37060
+ "explicitId": null,
37061
+ "citations": [
37062
+ {
37063
+ "section": "replay facet llmCacheKeyRecipe + effect-ledger-projection cacheKey (RFC 0260)",
37064
+ "requirement": "a record without cacheKey MUST validate (the facet is opt-in)"
37065
+ },
37066
+ {
37067
+ "section": "replay facet llmCacheKeyRecipe + effect-ledger-projection cacheKey (RFC 0260)",
37068
+ "requirement": "cacheKey {recipe: openwop-llm-key-2, digest: 64 hex} MUST validate"
37069
+ },
37070
+ {
37071
+ "section": "replay facet llmCacheKeyRecipe + effect-ledger-projection cacheKey (RFC 0260)",
37072
+ "requirement": "an unregistered recipe MUST fail"
37073
+ },
37074
+ {
37075
+ "section": "replay facet llmCacheKeyRecipe + effect-ledger-projection cacheKey (RFC 0260)",
37076
+ "requirement": "an uppercase or short digest MUST fail"
37077
+ },
37078
+ {
37079
+ "section": "replay facet llmCacheKeyRecipe + effect-ledger-projection cacheKey (RFC 0260)",
37080
+ "requirement": "digest is REQUIRED"
37081
+ },
37082
+ {
37083
+ "section": "replay facet llmCacheKeyRecipe + effect-ledger-projection cacheKey (RFC 0260)",
37084
+ "requirement": "cacheKey is closed: a preimage member MUST fail"
37085
+ }
37086
+ ]
37087
+ },
37088
+ {
37089
+ "id": "openwop.it.v2-replay-llm-cache-key.a-host-advertising-replay-llmcachekeyrecipe-also-advertises-idempotency",
37090
+ "file": "v2-replay-llm-cache-key.test.ts",
37091
+ "line": 117,
37092
+ "title": "a host advertising replay.llmCacheKeyRecipe also advertises idempotency",
37093
+ "explicitId": null,
37094
+ "citations": [
37095
+ {
37096
+ "section": "replay facet llmCacheKeyRecipe + effect-ledger-projection cacheKey (RFC 0260)",
37097
+ "requirement": "a host advertising replay.llmCacheKeyRecipe MUST advertise idempotency: the effect ledger is where the key is recorded"
37098
+ }
37099
+ ]
37100
+ },
37101
+ {
37102
+ "id": "openwop.it.v2-replay-llm-cache-key.the-provider-call-record-carries-cachekey-with-recipe-openwop-llm-key-2",
37103
+ "file": "v2-replay-llm-cache-key.test.ts",
37104
+ "line": 125,
37105
+ "title": "the provider-call record carries cacheKey with recipe openwop-llm-key-2",
37106
+ "explicitId": null,
37107
+ "citations": []
37108
+ },
37109
+ {
37110
+ "id": "openwop.it.v2-replay-llm-cache-key.two-runs-that-differ-only-in-maxtokens-have-different-keys",
37111
+ "file": "v2-replay-llm-cache-key.test.ts",
37112
+ "line": 133,
37113
+ "title": "two runs that differ only in maxTokens have different keys",
37114
+ "explicitId": null,
37115
+ "citations": []
37116
+ },
37117
+ {
37118
+ "id": "openwop.it.v2-replay-llm-cache-key.two-identical-runs-have-the-same-key",
37119
+ "file": "v2-replay-llm-cache-key.test.ts",
37120
+ "line": 143,
37121
+ "title": "two identical runs have the same key",
37122
+ "explicitId": null,
37123
+ "citations": []
37124
+ },
37125
+ {
37126
+ "id": "openwop.it.v2-replay-llm-cache-key.the-key-equals-recipe-2-recomputed-over-the-inputs-the-fixture-pins",
37127
+ "file": "v2-replay-llm-cache-key.test.ts",
37128
+ "line": 153,
37129
+ "title": "the key equals recipe 2 recomputed over the inputs the fixture pins",
37130
+ "explicitId": null,
37131
+ "citations": []
37132
+ },
37055
37133
  {
37056
37134
  "id": "openwop.it.v2-replay-suppression-ordinal.a-replay-fork-resolves-each-execution-of-a-looped-side-effecting-node-by-its-own",
37057
37135
  "file": "v2-replay-suppression-ordinal.test.ts",
@@ -38710,6 +38788,133 @@
38710
38788
  }
38711
38789
  ]
38712
38790
  },
38791
+ {
38792
+ "id": "openwop.it.v2-telemetry-export.the-telemetry-record-requires-otlp-protocols-from-the-closed-transports-genaisem",
38793
+ "file": "v2-telemetry-export.test.ts",
38794
+ "line": 102,
38795
+ "title": "the telemetry record requires otlp.protocols from the closed transports; genAiSemconv is semver",
38796
+ "explicitId": null,
38797
+ "citations": [
38798
+ {
38799
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38800
+ "requirement": "a record with otlp.protocols MUST validate"
38801
+ },
38802
+ {
38803
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38804
+ "requirement": "genAiSemconv as semver MUST validate"
38805
+ },
38806
+ {
38807
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38808
+ "requirement": "otlp is REQUIRED"
38809
+ },
38810
+ {
38811
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38812
+ "requirement": "protocols MUST be non-empty"
38813
+ },
38814
+ {
38815
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38816
+ "requirement": "an unknown transport MUST fail"
38817
+ },
38818
+ {
38819
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38820
+ "requirement": "a non-semver genAiSemconv MUST fail"
38821
+ }
38822
+ ]
38823
+ },
38824
+ {
38825
+ "id": "openwop.it.v2-telemetry-export.a-run-exports-an-openwop-run-span-with-its-run-and-workflow-ids",
38826
+ "file": "v2-telemetry-export.test.ts",
38827
+ "line": 115,
38828
+ "title": "a run exports an openwop.run span with its run and workflow ids",
38829
+ "explicitId": null,
38830
+ "citations": [
38831
+ {
38832
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38833
+ "requirement": "every run MUST produce a span named openwop.run carrying openwop.run_id"
38834
+ },
38835
+ {
38836
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38837
+ "requirement": "the run span MUST carry openwop.workflow_id"
38838
+ }
38839
+ ]
38840
+ },
38841
+ {
38842
+ "id": "openwop.it.v2-telemetry-export.each-node-execution-exports-an-openwop-node-typeid-span-with-its-node-attributes",
38843
+ "file": "v2-telemetry-export.test.ts",
38844
+ "line": 125,
38845
+ "title": "each node execution exports an openwop.node.<typeId> span with its node attributes",
38846
+ "explicitId": null,
38847
+ "citations": [
38848
+ {
38849
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38850
+ "requirement": "every node execution MUST produce a span named openwop.node.<typeId>"
38851
+ },
38852
+ {
38853
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38854
+ "requirement": "the node span MUST carry openwop.node_id"
38855
+ },
38856
+ {
38857
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38858
+ "requirement": "the node span MUST carry openwop.node_type, and its name MUST be openwop.node.<typeId>"
38859
+ },
38860
+ {
38861
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38862
+ "requirement": "openwop.node_attempt MUST be a non-negative integer"
38863
+ }
38864
+ ]
38865
+ },
38866
+ {
38867
+ "id": "openwop.it.v2-telemetry-export.a-run-created-with-a-traceparent-exports-its-spans-in-that-trace",
38868
+ "file": "v2-telemetry-export.test.ts",
38869
+ "line": 139,
38870
+ "title": "a run created with a traceparent exports its spans in that trace",
38871
+ "explicitId": null,
38872
+ "citations": [
38873
+ {
38874
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38875
+ "requirement": "a run created with a valid W3C traceparent MUST emit its spans in that trace"
38876
+ }
38877
+ ]
38878
+ },
38879
+ {
38880
+ "id": "openwop.it.v2-telemetry-export.no-exported-span-carries-the-resolved-store-secret",
38881
+ "file": "v2-telemetry-export.test.ts",
38882
+ "line": 149,
38883
+ "title": "no exported span carries the resolved store secret",
38884
+ "explicitId": null,
38885
+ "citations": [
38886
+ {
38887
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38888
+ "requirement": "a span name, attribute, event or resource attribute MUST NOT carry a resolved secret"
38889
+ }
38890
+ ]
38891
+ },
38892
+ {
38893
+ "id": "openwop.it.v2-telemetry-export.a-host-advertising-genaisemconv-puts-the-gen-ai-attributes-on-the-model-call-spa",
38894
+ "file": "v2-telemetry-export.test.ts",
38895
+ "line": 167,
38896
+ "title": "a host advertising genAiSemconv puts the gen_ai attributes on the model-call span",
38897
+ "explicitId": null,
38898
+ "citations": [
38899
+ {
38900
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38901
+ "requirement": null,
38902
+ "interpolated": true
38903
+ },
38904
+ {
38905
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38906
+ "requirement": "the model-call span MUST carry the provider attribute its version names"
38907
+ },
38908
+ {
38909
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38910
+ "requirement": "the model-call span MUST carry gen_ai.request.model"
38911
+ },
38912
+ {
38913
+ "section": "spec/v2/core/telemetry.md (RFC 0262)",
38914
+ "requirement": "the model-call span MUST carry input and output token usage"
38915
+ }
38916
+ ]
38917
+ },
38713
38918
  {
38714
38919
  "id": "openwop.it.v2-terminal-event-once.every-run-observed-to-a-terminal-state-has-exactly-one-terminal-event-and-no-for",
38715
38920
  "file": "v2-terminal-event-once.test.ts",
@@ -48642,7 +48847,7 @@
48642
48847
  {
48643
48848
  "id": "openwop.it.v2-front-door-counts.every-stated-count-of-spec-v2-core-documents-equals-the-tree",
48644
48849
  "file": "v2-front-door-counts.test.ts",
48645
- "line": 51,
48850
+ "line": 52,
48646
48851
  "title": "every stated count of spec/v2/core/ documents equals the tree",
48647
48852
  "explicitId": "openwop.requirement.front-door.core-doc-count",
48648
48853
  "citations": [
@@ -48656,7 +48861,7 @@
48656
48861
  {
48657
48862
  "id": "openwop.it.v2-front-door-counts.capabilities-md-5-names-as-many-core-families-as-the-declaration-declares",
48658
48863
  "file": "v2-front-door-counts.test.ts",
48659
- "line": 74,
48864
+ "line": 75,
48660
48865
  "title": "capabilities.md §5 names as many core families as the declaration declares",
48661
48866
  "explicitId": "openwop.requirement.front-door.core-family-count",
48662
48867
  "citations": [
@@ -49428,6 +49633,130 @@
49428
49633
  }
49429
49634
  ]
49430
49635
  },
49636
+ {
49637
+ "id": "openwop.it.v2-sabotage-ledger.a-every-witnessable-falsifiability-row-names-an-id-or-says-planned",
49638
+ "file": "v2-sabotage-ledger.test.ts",
49639
+ "line": 85,
49640
+ "title": "§A every witnessable Falsifiability row names an id or says planned",
49641
+ "explicitId": "openwop.requirement.0263.falsifiability-names-ids",
49642
+ "citations": [
49643
+ {
49644
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49645
+ "requirement": null,
49646
+ "interpolated": true
49647
+ },
49648
+ {
49649
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49650
+ "requirement": "a table naming an id, a planned row and a claims-check row MUST pass at Active"
49651
+ },
49652
+ {
49653
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49654
+ "requirement": "a witnessable row with no id and no `planned` MUST fail"
49655
+ },
49656
+ {
49657
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49658
+ "requirement": "the refusal MUST name §A"
49659
+ },
49660
+ {
49661
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49662
+ "requirement": "an RFC before 0258 is not in scope"
49663
+ },
49664
+ {
49665
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49666
+ "requirement": "a Draft RFC is not in scope"
49667
+ },
49668
+ {
49669
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49670
+ "requirement": "from RFC 0260 a planned row MUST block Accepted"
49671
+ },
49672
+ {
49673
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49674
+ "requirement": "the refusal MUST name the planned row"
49675
+ },
49676
+ {
49677
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49678
+ "requirement": "before RFC 0260 a planned row does not block Accepted"
49679
+ }
49680
+ ]
49681
+ },
49682
+ {
49683
+ "id": "openwop.it.v2-sabotage-ledger.c-an-accepted-rfc-from-0260-has-a-red-ledger-row-for-every-id-it-cites",
49684
+ "file": "v2-sabotage-ledger.test.ts",
49685
+ "line": 101,
49686
+ "title": "§C an Accepted RFC from 0260 has a red ledger row for every id it cites",
49687
+ "explicitId": "openwop.requirement.0263.sabotage-recorded-before-accepted",
49688
+ "citations": [
49689
+ {
49690
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49691
+ "requirement": "an executed-fail row for the cited id MUST satisfy §C"
49692
+ },
49693
+ {
49694
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49695
+ "requirement": "a red row on a leg under the cited id MUST satisfy §C"
49696
+ },
49697
+ {
49698
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49699
+ "requirement": "with no ledger row the flip MUST fail"
49700
+ },
49701
+ {
49702
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49703
+ "requirement": "the refusal MUST name the id"
49704
+ },
49705
+ {
49706
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49707
+ "requirement": "a red row for a different id MUST NOT satisfy §C"
49708
+ },
49709
+ {
49710
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49711
+ "requirement": "§C binds the Accepted flip, not Active"
49712
+ },
49713
+ {
49714
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49715
+ "requirement": "an RFC before 0260 is not retroactively invalidated"
49716
+ }
49717
+ ]
49718
+ },
49719
+ {
49720
+ "id": "openwop.it.v2-sabotage-ledger.b-the-ledger-records-red-runs-in-a-closed-shape-never-on-a-certified-bundle",
49721
+ "file": "v2-sabotage-ledger.test.ts",
49722
+ "line": 114,
49723
+ "title": "§B the ledger records red runs, in a closed shape, never on a certified bundle",
49724
+ "explicitId": "openwop.requirement.0263.ledger-shape",
49725
+ "citations": [
49726
+ {
49727
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49728
+ "requirement": "well-formed rows, one with a red digest, MUST pass"
49729
+ },
49730
+ {
49731
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49732
+ "requirement": "a sabotage that passed MUST fail the shape"
49733
+ },
49734
+ {
49735
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49736
+ "requirement": "the refusal MUST say why"
49737
+ },
49738
+ {
49739
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49740
+ "requirement": "an unknown member MUST fail"
49741
+ },
49742
+ {
49743
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49744
+ "requirement": "a sabotage that does not say what was broken MUST fail"
49745
+ },
49746
+ {
49747
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49748
+ "requirement": "a subject without a build MUST fail"
49749
+ },
49750
+ {
49751
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49752
+ "requirement": "a row naming a certified bundle's digest MUST fail — that bundle recorded the pass"
49753
+ },
49754
+ {
49755
+ "section": "RFC 0263 · scripts/check-accepted-predicate.mjs rules 5 and 6",
49756
+ "requirement": "the refusal MUST name the certified bundle"
49757
+ }
49758
+ ]
49759
+ },
49431
49760
  {
49432
49761
  "id": "openwop.it.v2-seams-not-in-canonical.the-corpus-gate-script-exits-0",
49433
49762
  "file": "v2-seams-not-in-canonical.test.ts",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "$comment": "GENERATED by conformance/scripts/generate-scenario-majors.mjs (RFC 0168 §D.3). Do not edit; add a file to BOTH_MAJORS in the generator to target both majors.",
3
3
  "counts": {
4
- "files": 668,
4
+ "files": 670,
5
5
  "v1": 455,
6
- "v2": 228
6
+ "v2": 230
7
7
  },
8
8
  "majors": {
9
9
  "a2a-1-0-agent-card.test.ts": [
@@ -1740,6 +1740,9 @@
1740
1740
  "v2-relaxation-recorded.test.ts": [
1741
1741
  2
1742
1742
  ],
1743
+ "v2-replay-llm-cache-key.test.ts": [
1744
+ 2
1745
+ ],
1743
1746
  "v2-replay-suppression-ordinal.test.ts": [
1744
1747
  2
1745
1748
  ],
@@ -1833,6 +1836,9 @@
1833
1836
  "v2-table-schema-enforcement.test.ts": [
1834
1837
  2
1835
1838
  ],
1839
+ "v2-telemetry-export.test.ts": [
1840
+ 2
1841
+ ],
1836
1842
  "v2-terminal-event-once.test.ts": [
1837
1843
  2
1838
1844
  ],
@@ -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.46.44",
5
- "corpusTag": "v2.46.44",
4
+ "version": "2.47.0",
5
+ "corpusTag": "v2.47.0",
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": "ac6de5303872664b78ff419c035915593e01599e02835e777a0123de1baeb398",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "55b4467b4ca8278dc8c5944cc0d11e69a2a92fcc6f8608269d7cac2c77626833",
13
- "api/v2/asyncapi.yaml": "bbd713d6b5e254622342e21f5a7331514d0901094d8d7daffb3ccac4e15b5558",
14
- "api/v2/openapi.yaml": "d82512b61f6b7eeda8a9efeeebc987e5702f452250ad0000623c826a3e5c5aaf",
12
+ "api/seams-v2.yaml": "dffb86d9287e72ee6261bd830b6920068ac78c1a0947f8d5087b47ecc19f11e5",
13
+ "api/v2/asyncapi.yaml": "7a8c4937c9b4f8387cc8d7cd6a5b890449b751ac2254b991f3434774798452d9",
14
+ "api/v2/openapi.yaml": "0c3ba86687c60e7b65add574b011c306c1ad132e1cc371c954ac1190c1f3781e",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "276fc1d51eb08e14e9127b9fd27761d7046ee33c12000f3a64a801322db8bf58",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -115,8 +115,8 @@
115
115
  "schemas/v2/artifact.schema.json": "85b9dc284d39b99d2a66fa8cbf47f2dd3d8526d3bd6cc411ad315b372de38975",
116
116
  "schemas/v2/audit-verify-result.schema.json": "14af8e9283510f0a40e48a2059c929ee44e0fc9fa3cc66a95bfbe90b17c48297",
117
117
  "schemas/v2/budget-policy.schema.json": "1de68095db0beb5c7a4be629de26e79f62ebbe748a88d46f36e768edfd1f0d68",
118
- "schemas/v2/capabilities.schema.json": "c912974988bb56d3d1457cbbd09ff4ffe64533ca7de0953efa5a1d8144ac0b43",
119
- "schemas/v2/certification-bundle.schema.json": "29d2ff33d8256d8729cd4914e2ecff5ff07afc310264e2db483a9c0ae13df2a6",
118
+ "schemas/v2/capabilities.schema.json": "609aa5998cafb549badbae7a6d94100ee32d130ade03dc6dfa40005f40154ac1",
119
+ "schemas/v2/certification-bundle.schema.json": "29d0be495dfaa2265f30394de92a96ca95058f1a89f663ece97c67ea6fae94c0",
120
120
  "schemas/v2/channel-presence-payload.schema.json": "fc03df0c20cc12a25f7ee6a47503a4c5931d79fe3d5ff24815e8fd7c1bcb9a87",
121
121
  "schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
122
122
  "schemas/v2/chat-card-pack-manifest.schema.json": "9cf235f5d0fe662f34f7e72bfbf2a82e0743046059a9d5af7aec948e228fdb0e",
@@ -132,7 +132,7 @@
132
132
  "schemas/v2/credential-reference.schema.json": "ea699ce30f58fb2c32d4279158b3f730f70be7b2dc8c3037693a76e66eb3e001",
133
133
  "schemas/v2/debug-bundle.schema.json": "0ec1e0bca9c0a359cc6cf3448e5200a1c3d3d5b1e5efaa6605231477daaf3fee",
134
134
  "schemas/v2/dispatch-config.schema.json": "27cb203d2a5cec90d8eb398569acbd8314ad9b8e231d1acfb8db4aa76f8d8f8b",
135
- "schemas/v2/effect-ledger-projection.schema.json": "0d118d385a43921118a76888449c5c3c5618eab1b80914b84a0a1b48d18822c1",
135
+ "schemas/v2/effect-ledger-projection.schema.json": "ac1f577ec420ece9a5fbe74a33a54aa5bbe4d8259f3c5e9c2bd9b8901ed1c289",
136
136
  "schemas/v2/effect-seam-manifest.schema.json": "197ef9944ab027a50d1b9352f317cbbc12c8ad955fdb14650fb90477052ffef0",
137
137
  "schemas/v2/error-envelope.schema.json": "5d566a910b715452e99b32876426cd490ef803f2177e3ecbbd1051170d4d4462",
138
138
  "schemas/v2/eval-summary.schema.json": "d07f4d116c5410997a81d0bc5387545fb61933a6333c360a26ab8a71c749336b",
@@ -203,14 +203,14 @@
203
203
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
204
204
  "spec/v1/alias-detectors.json": "202a443678e592a515e33aad7f6ab679a07d8408809f2d4b0f59d156c84ffc65",
205
205
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
206
- "spec/v1/core-standard-manifest.json": "561a3f85b9d00c3f2d6031428b2ca28e95805ed18eb3b069c6c983da62e8f6cc",
206
+ "spec/v1/core-standard-manifest.json": "9c3d53b164244ae9f238d5047681e01701c0df244172489a50509e62265514bc",
207
207
  "spec/v1/deprecations.json": "901fdab0a7212627f1838cbb06ae7b7204f0dff528299e56d3d938deecea27c1",
208
208
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
209
209
  "spec/v1/end-of-support.json": "35f0bf3b70fe78b16e2a2fd81a153fc27e703f6c16376c6fe7cdf1f4f2c4e6d4",
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": "871b4b7f12412609c65df1dee40af4cea9dc46996480a157d7863337572d1cb2",
213
+ "spec/v1/gaps.json": "08579ba52f384764f8b5849ee7d0bc97a0cb49318f6c6ccd0561707ab5f3be80",
214
214
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
215
215
  "spec/v1/migrations.json": "4c50760be70cb644d02427bcaf5749606700edf4513f423903cf3b1d12f5c300",
216
216
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
@@ -218,7 +218,7 @@
218
218
  "spec/v1/spec-gaps.json": "ea9612af55bea9dae3e82223d4f714243f8f590b0193f1188257b13491a38afc",
219
219
  "spec/v2/README.md": "c58acd84bb67c69043d9f777cbaa16d03e4f0f0a8158d2984526b69981067a14",
220
220
  "spec/v2/core/artifact-type-packs.md": "ba5a258ac93b1aa4d74a4464c63a8f72dbbc5d24fd56f34a2c2ce9f464892799",
221
- "spec/v2/core/capabilities.md": "316dc256259968e74f24a5529015ad7b78d5866b914aa0c108cb8e7abb724d4d",
221
+ "spec/v2/core/capabilities.md": "7ab4c1ab5b0b0a95a5e35ff2db9b45c5fb6bf23844c97ffeed6d6ebda3d17c7e",
222
222
  "spec/v2/core/conformance.md": "2e12b5fceb9cbd1bbf4a536310fe92a4cd44947e48d0466013327c5de202a2d9",
223
223
  "spec/v2/core/connection-packs.md": "c80f95bd6a03698f35c72cdb2b46bd973f49203bbd786c26760e35fa86e6f23d",
224
224
  "spec/v2/core/conversation.md": "8d6abedf6af408a74577753bfca24719d318e2632e10b45ff5d63bdcf31a8aaa",
@@ -239,17 +239,18 @@
239
239
  "spec/v2/core/packs.md": "c46a3bbd8a3a8cfbb6476b88f90d234a45f724aa1f640c9ce6430ba05bacb069",
240
240
  "spec/v2/core/persistence.md": "d1767bde0db8038bf6c4f8dc31c4900c67dbd77b4ccc382e99b9f29501b472a3",
241
241
  "spec/v2/core/portability.md": "93b9cb8bb85448669df716effb7627236b2bb069f8c5ca7306e8971b9355abfd",
242
- "spec/v2/core/replay.md": "cf992b8c7f646a79bd0bdd39451cc9d015f7150a3c342c312b2a89d95abe49de",
242
+ "spec/v2/core/replay.md": "de705ad72e4878def9a0e1d215df9020c129eabae5f224b8c9b3330bd3b48c47",
243
243
  "spec/v2/core/runs.md": "f9c26490e52fa69a10c8e98c8a489e3bc8dfbe3effe334bb1793bc9b575f5efe",
244
244
  "spec/v2/core/security-defaults.md": "18c9956cae51c3da1def80ce94741ad1fd664a8150701160c6cd0ef367c79749",
245
245
  "spec/v2/core/storage.md": "bfcc74526d0f24539188b1c5d1567d8510e80d0b6c39fdc4d19204fcd6fd1f85",
246
+ "spec/v2/core/telemetry.md": "5e2f30919c51195b57566a2a277663e72dffd1a56a68c19fb8b0d3260629ff7f",
246
247
  "spec/v2/core/tool-catalog.md": "d16a9004418719c48301804aefa4d7ab6f41112a4ed83727f552aa1b92b418b0",
247
248
  "spec/v2/core/versioning.md": "f25c64849e9fa7e3ddc976f33c502bbf0e3fdd158c5c2d5730d524b1f8a19bfe",
248
249
  "spec/v2/core/webhooks.md": "861284396eb4f445f4b7bec210f07dd2c96d18084f8511371235b98617bf2b90",
249
250
  "spec/v2/core/workflow-chain-packs.md": "fdce7a8477e269950a2bac05ee6eddbd66eb0b670b464d00054f7196ec05d29c",
250
251
  "spec/v2/corrections.json": "5ee76d1ee87677a86d5d9ee2f477b88fc29f9f9d37611040b9ccb08f8f30fb2d",
251
252
  "spec/v2/corrections.schema.json": "02f6a0a9531e657693be311af0f59d6432f3efcf645f56ccbf39e5b141e6b641",
252
- "spec/v2/declaration.json": "3c3953f30eddf26341ed7ffab7e64bc2807e1def47d57b7e408047e28aab7c0d",
253
+ "spec/v2/declaration.json": "7c8da768d45866b70544fad1e26c513f1dce8657c276ffa7148127d11b744811",
253
254
  "spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
254
255
  "spec/v2/eos-override.json": "4c72d2f6a658943b01c1de1643cf294642091933af7832f38ee5495df684cb2d",
255
256
  "spec/v2/errors.json": "8b4d2ebab569b26f608323ca4c9adaf805ab515286d26532db23fcb97541787f",
@@ -282,6 +283,7 @@
282
283
  "spec/v2/facets/connections.schema.json": "f998bb206a5814183bb4720cc34fa43b7ab64d61e39a79437b009c589ce8b78e",
283
284
  "spec/v2/facets/heartbeat.schema.json": "81d7fa1895a8b0241bd07cf00a7e7905bef0fd14eb3427325261c62eee5746a6",
284
285
  "spec/v2/facets/hostEvents.schema.json": "8834c2fde7bc2ba9eb433f22c9d6ec3f9739f44049eecdbf0a05baf2bfa0bb7d",
286
+ "spec/v2/facets/idempotency.schema.json": "f20c1e0b08749385b443a7e65293197cfe68cb51486b29659c6af6b96179fa98",
285
287
  "spec/v2/facets/interrupt.schema.json": "e89e008a06b0a53cebfeac500c1622ca871c920fc8856b31c019689c5df41b66",
286
288
  "spec/v2/facets/limits.schema.json": "e68327665def83007f2573b413757caa602e82ad347d1b85e9853fea153ffb48",
287
289
  "spec/v2/facets/loopback.schema.json": "577a34333d1633f8ab00f11e1aeaee2708022faa69a26cf316fc5bea00ee7d50",
@@ -289,9 +291,10 @@
289
291
  "spec/v2/facets/nondeterminismPolicy.schema.json": "e9dfaa32e0778a1a837fdad3b8a0673fccda9579ab08c8bfe9777362dd15e989",
290
292
  "spec/v2/facets/oauth.schema.json": "3a0cbe249d3bd5a5982349ae7ba2534776cc958e4a57e0fbeabf2bdab6e99e69",
291
293
  "spec/v2/facets/packs.schema.json": "d544a280419c0052ce0f3c3b378f7aa04085cadc31df52a3c8f43da600b131b1",
292
- "spec/v2/facets/replay.schema.json": "0a5aefb0108c708c6794fadd533cf256709704c4f16dbf44cd63fa8980c78018",
294
+ "spec/v2/facets/replay.schema.json": "ebb1ce04ed1e4fca237f448e565fe75c833e630320930accecba2ce4e660c8d9",
293
295
  "spec/v2/facets/runList.schema.json": "6223e1baaebbb9f4faa71bfe968f8d22b4aa88046d0e43e8d5f56409e7f86e90",
294
296
  "spec/v2/facets/sandbox.schema.json": "e75f54486d387b2d089791aa93b5093cd1450f1920753a0f97fc23acdfe978ed",
297
+ "spec/v2/facets/telemetry.schema.json": "56e96a26910ff1d891eabfc520c2d152fd9881449fdcea7e03df34793763849e",
295
298
  "spec/v2/facets/uiPlugins.schema.json": "1fd3f8440cfb6e4422553a7c4b5f091eb3e10a40da0d43e8b977a18929116758",
296
299
  "spec/v2/facets/webhooks.schema.json": "54b576f1d150e28e1642693d846ce471c118e9c521bdf3924185305fbd1d0de4",
297
300
  "spec/v2/facets/workflowChainPacks.schema.json": "ea625ce818fbeb746478f00f31f96ee9613d538e4409c258c1bfb517acdeaba9",
@@ -306,9 +309,9 @@
306
309
  "spec/v2/path-manifest.json": "3720e502614f301efc7556e0bfcbfa7b7ad8a37c579256ba6bca82417a2728f1",
307
310
  "spec/v2/peer-dependency-aliases.json": "cbf62f551f9295b0ac7ba6cc0466abb153563354a57b0622f3ec6e6689cba47e",
308
311
  "spec/v2/profiles.json": "b382b261c7844798575f7c1b37c934f0da19846016a2337d883d4f5ef6e05479",
309
- "spec/v2/release.json": "26d4c0eb733574959b9bc9a92a5cc1674f73c63b3792241156f628709dad3a1a",
312
+ "spec/v2/release.json": "6082692ec2549fcb02ca33f24336e7f9f417ec4ff08a14d23fc02533d8a00527",
310
313
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
311
- "spec/v2/surface-baseline.json": "fedf128175065f105c7abb1deb5173093aac64a7b0b7fda286e9697969dbbcb9"
314
+ "spec/v2/surface-baseline.json": "c67e5764105a35d5dc575942582b7b0784c0ad2e3739fe7b5b23de28ea58408f"
312
315
  },
313
- "corpusCommit": "2111a4fb43c27c18f080355eb0b1dc2e6653bc43"
316
+ "corpusCommit": "aeb6ca4f976b56668c3770acd128c08ec59cfb18"
314
317
  }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * RFC 0260 — LLM cache key recipe 2 (`openwop-llm-key-2`). A pure function: the
3
+ * suite recomputes the key a host recorded and compares (§B, `0260.key-portable`).
4
+ *
5
+ * key = hex(SHA-256(JCS({ recipe, model, provider, messages, tools, toolChoice,
6
+ * responseSchema, params })))
7
+ *
8
+ * `params` names every sampling or output-shaping parameter; one the host did not
9
+ * send is `null`, never omitted. `stop` is sorted (UTF-16 code-unit order, as JCS
10
+ * orders keys) and de-duplicated. Any other parameter sits under `params.extra`.
11
+ * A string message content is the one-part array `[{type: "text", text}]`. No
12
+ * Unicode normalisation: the bytes are as sent.
13
+ */
14
+ import { createHash } from 'node:crypto';
15
+ import { canonicalJSON, codeUnitCompare } from './jcs.js';
16
+
17
+ export const RECIPE_2 = 'openwop-llm-key-2';
18
+
19
+ /** The parameters recipe 2 names. A request that omits one puts `null` in the key. */
20
+ export const NAMED_PARAMS = ['temperature', 'maxTokens', 'stop', 'seed', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty'] as const;
21
+ export type NamedParam = (typeof NAMED_PARAMS)[number];
22
+
23
+ export interface KeyInputs {
24
+ readonly model: string;
25
+ readonly provider: string;
26
+ readonly messages: readonly { readonly role: string; readonly content: string | readonly Record<string, unknown>[] }[];
27
+ readonly tools?: readonly unknown[];
28
+ readonly toolChoice?: unknown;
29
+ readonly responseSchema?: unknown;
30
+ readonly params?: Partial<Record<NamedParam, unknown>> & { readonly extra?: Record<string, unknown> };
31
+ }
32
+
33
+ const nullIfAbsent = (v: unknown): unknown => (v === undefined ? null : v);
34
+
35
+ /** The object recipe 2 hashes, before canonicalization. Exported so a failure can show what was hashed. */
36
+ export function keyPreimage(i: KeyInputs): Record<string, unknown> {
37
+ const p = i.params ?? {};
38
+ const params: Record<string, unknown> = {};
39
+ for (const name of NAMED_PARAMS) params[name] = nullIfAbsent(p[name]);
40
+ if (Array.isArray(params['stop'])) params['stop'] = [...new Set((params['stop'] as unknown[]).map(String))].sort(codeUnitCompare);
41
+ if (p.extra !== undefined && Object.keys(p.extra).length > 0) params['extra'] = p.extra;
42
+ return {
43
+ recipe: RECIPE_2,
44
+ model: i.model,
45
+ provider: i.provider,
46
+ messages: i.messages.map((m) => ({ role: m.role, content: typeof m.content === 'string' ? [{ type: 'text', text: m.content }] : m.content })),
47
+ tools: i.tools ?? [],
48
+ toolChoice: nullIfAbsent(i.toolChoice),
49
+ responseSchema: nullIfAbsent(i.responseSchema),
50
+ params,
51
+ };
52
+ }
53
+
54
+ export function cacheKey2(i: KeyInputs): string {
55
+ return createHash('sha256').update(canonicalJSON(keyPreimage(i)), 'utf8').digest('hex');
56
+ }
57
+
58
+ /**
59
+ * The inputs the `conformance-llm-cache-key` fixture pins (conformance/fixtures.md
60
+ * §The LLM cache-key fixture): the host's conformance mock is provider
61
+ * `conformance-mock`, model `conformance-mock-1`; the node sends one user message,
62
+ * no tools, no toolChoice, no responseSchema, and exactly the parameters in the
63
+ * run's `inputs.request.params`.
64
+ */
65
+ export const FIXTURE = 'conformance-llm-cache-key';
66
+ export const NODE_ID = 'llm-call';
67
+ export const FIXTURE_PROVIDER = 'conformance-mock';
68
+ export const FIXTURE_MODEL = 'conformance-mock-1';
69
+ export function fixtureInputs(prompt: string, params: NonNullable<KeyInputs['params']>): KeyInputs {
70
+ return { model: FIXTURE_MODEL, provider: FIXTURE_PROVIDER, messages: [{ role: 'user', content: prompt }], params };
71
+ }
72
+
73
+ // ── Judges. Each returns findings and asserts nothing; the scenario frames them with req(). ──
74
+
75
+ export interface Finding { readonly ok: boolean; readonly message: string }
76
+ const f = (ok: boolean, message: string): Finding => ({ ok, message });
77
+
78
+ /** The `cacheKey` member of the fixture node's provider-call record on GET /runs/{runId}/effects, or undefined. */
79
+ export interface LedgerKey { readonly recipe?: unknown; readonly digest?: unknown }
80
+
81
+ export function readLedgerKey(projection: unknown, nodeId: string): LedgerKey | undefined | 'no-record' {
82
+ const effects = (projection as { effects?: unknown } | null)?.effects;
83
+ if (!Array.isArray(effects)) return 'no-record';
84
+ const mine = effects.filter((e) => (e as { nodeId?: unknown } | null)?.nodeId === nodeId);
85
+ if (mine.length === 0) return 'no-record';
86
+ const withKey = mine.find((e) => (e as { cacheKey?: unknown }).cacheKey !== undefined) as { cacheKey?: LedgerKey } | undefined;
87
+ return withKey?.cacheKey;
88
+ }
89
+
90
+ const HEX64 = /^[0-9a-f]{64}$/;
91
+
92
+ export function judgeRecorded(key: LedgerKey | undefined): Finding[] {
93
+ return [
94
+ f(key !== undefined, 'the provider-call record MUST carry cacheKey when replay.llmCacheKeyRecipe is advertised'),
95
+ f(key?.recipe === RECIPE_2, `cacheKey.recipe MUST be "${RECIPE_2}" (got ${JSON.stringify(key?.recipe)})`),
96
+ f(typeof key?.digest === 'string' && HEX64.test(key.digest), `cacheKey.digest MUST be 64 lowercase hex characters (got ${JSON.stringify(key?.digest)})`),
97
+ ];
98
+ }
99
+
100
+ /** Two invocations that differ only in one named parameter MUST NOT share a key. */
101
+ export function judgeParamsInKey(a: LedgerKey | undefined, b: LedgerKey | undefined): Finding[] {
102
+ return [f(typeof a?.digest === 'string' && typeof b?.digest === 'string' && a.digest !== b.digest, `invocations differing only in maxTokens MUST have different keys (got ${String(a?.digest)} twice)`)];
103
+ }
104
+
105
+ /** The same inputs MUST give the same key. */
106
+ export function judgeStable(a: LedgerKey | undefined, b: LedgerKey | undefined): Finding[] {
107
+ return [f(typeof a?.digest === 'string' && a.digest === b?.digest, `identical invocations MUST have the same key (got ${String(a?.digest)} and ${String(b?.digest)})`)];
108
+ }
109
+
110
+ /** The host's key MUST equal the recipe computed from the inputs the fixture pins. */
111
+ export function judgePortable(key: LedgerKey | undefined, expected: KeyInputs): Finding[] {
112
+ const want = cacheKey2(expected);
113
+ return [f(key?.digest === want, `cacheKey.digest MUST equal recipe 2 over the fixture's inputs: expected ${want}, got ${String(key?.digest)} (preimage ${canonicalJSON(keyPreimage(expected))})`)];
114
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * v2 — LLM cache key recipe 2 (RFC 0260; the rules are the
3
+ * `replay.llmCacheKeyRecipe` facet description in `spec/v2/facets/replay.schema.json`
4
+ * and the `cacheKey` description in `schemas/v2/effect-ledger-projection.schema.json`).
5
+ *
6
+ * Server-free (always on):
7
+ * 1. `0260.shape` — the effect-ledger record admits `cacheKey: {recipe, digest}`
8
+ * with a closed recipe vocabulary and a 64-hex digest, and refuses anything else.
9
+ *
10
+ * Host legs, unaided through the `conformance-llm-cache-key` fixture: its node makes
11
+ * one `ctx.callAI` call through the host's conformance mock with the run's
12
+ * `inputs.request` (prompt and params) unchanged. The suite reads the call's record
13
+ * on `GET /runs/{runId}/effects` (idempotency.md §Witness), which is a normative
14
+ * observation path, so no seam is needed:
15
+ * 2. `0260.key-recorded` — the record carries `cacheKey` with recipe `openwop-llm-key-2`
16
+ * 3. `0260.params-in-key` — runs differing only in `maxTokens` have different digests
17
+ * 4. `0260.key-stable` — two identical runs have the same digest
18
+ * 5. `0260.key-portable` — the digest equals recipe 2 recomputed by the suite
19
+ * (lib/llm-cache-key-2.ts) over the inputs the fixture pins
20
+ *
21
+ * Dispositions: `replay.llmCacheKeyRecipe` not advertised, `idempotency` absent, or the
22
+ * fixture not listed ⇒ `inapplicable`. A run that cannot be created, read or finished,
23
+ * or a ledger with no record for the node ⇒ `blocked`.
24
+ *
25
+ * @see RFCS/0260-llm-cache-key-recipe-2.md
26
+ * @see spec/v2/core/replay.md §Suppression (the cache key for LLM-calling nodes)
27
+ * @see conformance/fixtures.md §The LLM cache-key fixture
28
+ */
29
+
30
+ import { describe, it, expect } from 'vitest';
31
+ import { driver, type OpenWOPResponse } from '../lib/driver.js';
32
+ import { v2Discovery, v2RefValidator } from '../lib/v2.js';
33
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
34
+ import { projectBoundId } from '../lib/bound-id.js';
35
+ import { readErrorCode } from '../lib/error-envelope.js';
36
+ import { softSkip } from '../lib/soft-skip.js';
37
+ import { req } from '../lib/requirement-ids.js';
38
+ import { scaledTimeoutMs } from '../lib/polling.js';
39
+ import {
40
+ RECIPE_2, FIXTURE, NODE_ID, fixtureInputs, readLedgerKey,
41
+ judgeRecorded, judgeParamsInKey, judgeStable, judgePortable, type Finding, type LedgerKey, type NamedParam,
42
+ } from '../lib/llm-cache-key-2.js';
43
+
44
+ const DOC = 'replay facet llmCacheKeyRecipe + effect-ledger-projection cacheKey (RFC 0260)';
45
+ const R = (slug: string): string => `openwop.requirement.0260.${slug}`;
46
+ const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
47
+ const PROMPT = 'Say ok.';
48
+ const PARAMS_A: Partial<Record<NamedParam, unknown>> = { maxTokens: 16, temperature: 0 };
49
+ const PARAMS_B: Partial<Record<NamedParam, unknown>> = { maxTokens: 32, temperature: 0 };
50
+
51
+ type Skip = readonly ['inapplicable' | 'blocked', string];
52
+ const isRec = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
53
+ async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> { try { return await fn(); } catch { return null; } }
54
+ const assertAll = (id: string, fs: readonly Finding[]): void => { for (const x of fs) expect(x.ok, req(id, DOC, x.message)).toBe(true); };
55
+
56
+ let disc: Promise<Record<string, unknown> | null> | undefined;
57
+ const discovery = (): Promise<Record<string, unknown> | null> => (disc ??= v2Discovery().catch(() => null));
58
+
59
+ async function gate(): Promise<Skip | null> {
60
+ const doc = await discovery();
61
+ if (!doc) return ['blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0'];
62
+ const replay = doc['replay'];
63
+ if (!isRec(replay) || replay['llmCacheKeyRecipe'] !== RECIPE_2) return ['inapplicable', `the host does not advertise replay.llmCacheKeyRecipe "${RECIPE_2}" (absent ⇒ the RFC 0041 recipe, opt-in per RFC 0260)`];
64
+ if (!isRec(doc['idempotency'])) return ['inapplicable', 'the host does not advertise idempotency, so it serves no effect ledger to read the key from'];
65
+ if (!isFixtureAdvertised(FIXTURE)) return ['inapplicable', `the host does not advertise fixture ${FIXTURE}`];
66
+ return null;
67
+ }
68
+
69
+ async function terminal(runId: string): Promise<string | null> {
70
+ const deadline = Date.now() + scaledTimeoutMs(30_000);
71
+ for (;;) {
72
+ const snap = await http(() => driver.get(`/runs/${projectBoundId(runId)}`));
73
+ const status = String((snap?.json as { status?: unknown } | undefined)?.status ?? '');
74
+ if (TERMINAL.has(status)) return status;
75
+ if (Date.now() > deadline) return null;
76
+ await new Promise((r) => setTimeout(r, 250));
77
+ }
78
+ }
79
+
80
+ /** One fixture run with these params, and the key its provider-call record carries. Asserts nothing. */
81
+ async function keyed(params: Partial<Record<NamedParam, unknown>>): Promise<{ skip: Skip } | { key: LedgerKey | undefined }> {
82
+ const created = await http(() => driver.post('/runs', { workflowId: FIXTURE, inputs: { request: { prompt: PROMPT, params } } }));
83
+ const runId = (created?.json as { runId?: unknown } | undefined)?.runId;
84
+ if (created === null || created.status !== 201 || typeof runId !== 'string') return { skip: ['blocked', `POST /runs {workflowId: ${FIXTURE}} answered ${created?.status ?? 'nothing'} ${readErrorCode(created?.json) ?? ''}`.trim()] };
85
+ const status = await terminal(runId);
86
+ if (status !== 'completed') return { skip: ['blocked', `the ${FIXTURE} run ended ${status ?? 'not at all within the timeout'}, so its provider call is not on record`] };
87
+ const ledger = await http(() => driver.get(`/runs/${projectBoundId(runId)}/effects`));
88
+ if (ledger === null || ledger.status !== 200) return { skip: ['blocked', `GET /runs/{runId}/effects answered ${ledger?.status ?? 'nothing'} — idempotency.md §Witness owns that contract`] };
89
+ const key = readLedgerKey(ledger.json, NODE_ID);
90
+ if (key === 'no-record') return { skip: ['blocked', `the effect ledger has no record for node ${NODE_ID}: the host did not record the provider call as an effect (idempotency.md §Composition)`] };
91
+ return { key };
92
+ }
93
+
94
+ const memo = new Map<string, ReturnType<typeof keyed>>();
95
+ const once = (name: 'a' | 'a2' | 'b'): ReturnType<typeof keyed> => {
96
+ let p = memo.get(name);
97
+ if (!p) { p = keyed(name === 'b' ? PARAMS_B : PARAMS_A); memo.set(name, p); }
98
+ return p;
99
+ };
100
+
101
+ describe('v2 RFC 0260 — cacheKey wire shape (server-free)', () => {
102
+ it('the effect-ledger record admits cacheKey {recipe, digest} and refuses anything else', () => {
103
+ const v = v2RefValidator('effect-ledger-projection.schema.json');
104
+ const base = { effectId: 'acme/0123456789abcdef', nodeId: 'llm-call', attempt: 1, keying: 'activity-recipe', state: 'completed', at: '2026-11-13T00:00:00Z' };
105
+ const doc = (rec: Record<string, unknown>) => ({ runId: 'acme/0123456789abcdef', effects: [rec] });
106
+ const digest = 'f1714d7aa2abf5994dd375f66aac14514b1c2439f203a6171dc6053c59f54402';
107
+ expect(v(doc(base)).ok, req(R('shape'), DOC, 'a record without cacheKey MUST validate (the facet is opt-in)')).toBe(true);
108
+ expect(v(doc({ ...base, cacheKey: { recipe: RECIPE_2, digest } })).ok, req(R('shape'), DOC, 'cacheKey {recipe: openwop-llm-key-2, digest: 64 hex} MUST validate')).toBe(true);
109
+ expect(v(doc({ ...base, cacheKey: { recipe: 'spec-rfc-0041', digest } })).ok, req(R('shape'), DOC, 'an unregistered recipe MUST fail')).toBe(false);
110
+ expect(v(doc({ ...base, cacheKey: { recipe: RECIPE_2, digest: digest.toUpperCase() } })).ok, req(R('shape'), DOC, 'an uppercase or short digest MUST fail')).toBe(false);
111
+ expect(v(doc({ ...base, cacheKey: { recipe: RECIPE_2 } })).ok, req(R('shape'), DOC, 'digest is REQUIRED')).toBe(false);
112
+ expect(v(doc({ ...base, cacheKey: { recipe: RECIPE_2, digest, preimage: '{}' } })).ok, req(R('shape'), DOC, 'cacheKey is closed: a preimage member MUST fail')).toBe(false);
113
+ });
114
+ });
115
+
116
+ describe('v2 RFC 0260 — recipe 2 on the effect ledger (host)', () => {
117
+ it('a host advertising replay.llmCacheKeyRecipe also advertises idempotency', async () => {
118
+ const doc = await discovery();
119
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
120
+ const replay = doc['replay'];
121
+ if (!isRec(replay) || replay['llmCacheKeyRecipe'] === undefined) return softSkip('inapplicable', 'the host does not advertise replay.llmCacheKeyRecipe');
122
+ expect(isRec(doc['idempotency']), req(R('implies-idempotency'), DOC, 'a host advertising replay.llmCacheKeyRecipe MUST advertise idempotency: the effect ledger is where the key is recorded')).toBe(true);
123
+ });
124
+
125
+ it('the provider-call record carries cacheKey with recipe openwop-llm-key-2', async () => {
126
+ const g = await gate();
127
+ if (g) return softSkip(...g);
128
+ const a = await once('a');
129
+ if ('skip' in a) return softSkip(...a.skip);
130
+ assertAll(R('key-recorded'), judgeRecorded(a.key));
131
+ }, 120_000);
132
+
133
+ it('two runs that differ only in maxTokens have different keys', async () => {
134
+ const g = await gate();
135
+ if (g) return softSkip(...g);
136
+ const a = await once('a');
137
+ if ('skip' in a) return softSkip(...a.skip);
138
+ const b = await once('b');
139
+ if ('skip' in b) return softSkip(...b.skip);
140
+ assertAll(R('params-in-key'), judgeParamsInKey(a.key, b.key));
141
+ }, 120_000);
142
+
143
+ it('two identical runs have the same key', async () => {
144
+ const g = await gate();
145
+ if (g) return softSkip(...g);
146
+ const a = await once('a');
147
+ if ('skip' in a) return softSkip(...a.skip);
148
+ const a2 = await once('a2');
149
+ if ('skip' in a2) return softSkip(...a2.skip);
150
+ assertAll(R('key-stable'), judgeStable(a.key, a2.key));
151
+ }, 120_000);
152
+
153
+ it('the key equals recipe 2 recomputed over the inputs the fixture pins', async () => {
154
+ const g = await gate();
155
+ if (g) return softSkip(...g);
156
+ const a = await once('a');
157
+ if ('skip' in a) return softSkip(...a.skip);
158
+ assertAll(R('key-portable'), judgePortable(a.key, fixtureInputs(PROMPT, PARAMS_A)));
159
+ }, 120_000);
160
+ });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * v2 — the `telemetry` family: a host exports OTLP traces (RFC 0262; the rules
3
+ * are `spec/v2/core/telemetry.md`, the facets `spec/v2/facets/telemetry.schema.json`).
4
+ *
5
+ * Server-free (always on):
6
+ * 1. `0262.shape` — the generated `telemetry` record requires `otlp` with a
7
+ * non-empty `protocols` from the closed transport vocabulary; the optional
8
+ * `genAiSemconv` is a semver string.
9
+ *
10
+ * Host legs — gated on the `telemetry` family record AND on the suite's OTLP
11
+ * collector (`OPENWOP_OTEL_COLLECTOR=true`, the host's exporter pointed at it
12
+ * with `OTEL_EXPORTER_OTLP_ENDPOINT`). The host calls the suite, so a host in
13
+ * another network namespace records `blocked`, not a failure.
14
+ * 2. `0262.run-span` — a `conformance-noop` run exports an `openwop.run` span
15
+ * carrying its `openwop.run_id` and `openwop.workflow_id`.
16
+ * 3. `0262.node-span` — the same run exports an `openwop.node.<typeId>` span
17
+ * carrying `openwop.node_id`, `openwop.node_type` and a non-negative
18
+ * `openwop.node_attempt`.
19
+ * 4. `0262.traceparent-continued` — a run created with a suite-minted
20
+ * `traceparent` exports its spans under that `traceId`.
21
+ * 5. `0262.no-secret-in-spans` — after an `openwop-smoke-byok-roundtrip` run,
22
+ * no captured span name, attribute or resource attribute carries the store
23
+ * canary (digest detector, plus the plaintext when the operator supplies
24
+ * `OPENWOP_CANARY_SECRET_VALUE`; `lib/secret-scan.ts`).
25
+ * 6. `0262.gen-ai-attributes` — gated on `telemetry.genAiSemconv` and the
26
+ * `conformance-callai-attempt-accounting` fixture: the model-call span
27
+ * carries `gen_ai.operation.name`, a provider attribute, `gen_ai.request.model`
28
+ * and both usage-token attributes.
29
+ *
30
+ * A host leg whose run exported NO span records `blocked`: an empty export is
31
+ * not a pass, so a host that advertises the family and exports nothing cannot
32
+ * certify (RFC 0262 R2).
33
+ *
34
+ * @see RFCS/0262-telemetry-family.md
35
+ * @see spec/v2/core/telemetry.md
36
+ */
37
+
38
+ import { describe, it, expect } from 'vitest';
39
+ import { driver, type OpenWOPResponse } from '../lib/driver.js';
40
+ import { familyAdvertised, v2Discovery, v2RefValidator } from '../lib/v2.js';
41
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
42
+ import { readErrorCode } from '../lib/error-envelope.js';
43
+ import { pollUntilTerminal } from '../lib/polling.js';
44
+ import { softSkip } from '../lib/soft-skip.js';
45
+ import { req } from '../lib/requirement-ids.js';
46
+ import { getCollector, waitForRunSpans, type CapturedSpan } from '../lib/otel-collector.js';
47
+ import { digestDetector, findSecretDigest, leaks, valueDetector, type SecretDetector, type Surface } from '../lib/secret-scan.js';
48
+
49
+ /**
50
+ * Callback-shaped: the host exports OTLP to the suite's collector.
51
+ * Unwitnessable when the host is in a separate network namespace — see
52
+ * `../lib/host-callback.ts`. Not host non-conformance.
53
+ */
54
+ export const REQUIRES_HOST_CALLBACK = "the host exports OTLP traces to the suite's collector";
55
+
56
+ const DOC = 'spec/v2/core/telemetry.md (RFC 0262)';
57
+ const R = (slug: string): string => `openwop.requirement.0262.${slug}`;
58
+ const NOOP = 'conformance-noop';
59
+ const BYOK = 'openwop-smoke-byok-roundtrip';
60
+ const CALLAI = 'conformance-callai-attempt-accounting';
61
+
62
+ type Skip = { skip: ['inapplicable' | 'blocked', string] };
63
+ const skipped = (s: Skip): undefined => softSkip(s.skip[0], s.skip[1]);
64
+ const isRec = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
65
+ async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> { try { return await fn(); } catch { return null; } }
66
+
67
+ /** The family record, or why the host legs do not bind. */
68
+ async function gate(fixture: string): Promise<Skip | { record: Record<string, unknown> }> {
69
+ if (!(await v2Discovery())) return { skip: ['blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 under OpenWOP-Version: 2.0'] };
70
+ const record = await familyAdvertised('telemetry');
71
+ if (record === null) return { skip: ['inapplicable', 'the telemetry family is not advertised — no export claim'] };
72
+ if (!getCollector()) return { skip: ['blocked', 'the suite OTLP collector is not running (set OPENWOP_OTEL_COLLECTOR=true and point the host exporter at it)'] };
73
+ if (!isFixtureAdvertised(fixture)) return { skip: ['inapplicable', `fixture ${fixture} is not advertised`] };
74
+ return { record };
75
+ }
76
+
77
+ /** Start a run, wait for it to settle, then wait for its exported spans. */
78
+ async function exportedRun(body: Record<string, unknown>, headers?: Record<string, string>): Promise<Skip | { runId: string; created: OpenWOPResponse; spans: readonly CapturedSpan[] }> {
79
+ const created = await http(() => driver.post('/runs', body, headers ? { headers } : {}));
80
+ const runId = isRec(created?.json) ? created.json['runId'] : undefined;
81
+ if (created === null || created.status !== 201 || typeof runId !== 'string') return { skip: ['blocked', `POST /runs (${String(body['workflowId'])}) answered ${created?.status ?? 'nothing'} ${readErrorCode(created?.json) ?? ''}`.trim()] };
82
+ try { await pollUntilTerminal(runId, { timeoutMs: 20_000 }); } catch { return { skip: ['blocked', `run ${runId} did not settle within 20 s`] }; }
83
+ const spans = await waitForRunSpans(runId, { timeoutMs: 8_000, minCount: 2 });
84
+ if (spans.length === 0) return { skip: ['blocked', `the collector captured no span carrying openwop.run_id ${runId} — the host advertised telemetry but nothing arrived (an empty export is not a pass)`] };
85
+ return { runId, created, spans };
86
+ }
87
+
88
+ let noopRun: ReturnType<typeof exportedRun> | undefined;
89
+ const noop = (): ReturnType<typeof exportedRun> => {
90
+ if (!noopRun) { getCollector()?.reset(); noopRun = exportedRun({ workflowId: NOOP }); }
91
+ return noopRun;
92
+ };
93
+
94
+ function spanSurfaces(spans: readonly CapturedSpan[]): Surface[] {
95
+ return spans.map((s) => ({
96
+ name: `span ${s.name}`,
97
+ text: JSON.stringify({ name: s.name, attributes: Object.fromEntries(s.attributes), resource: Object.fromEntries(s.resourceAttributes) }),
98
+ }));
99
+ }
100
+
101
+ describe('v2 RFC 0262 — telemetry facet shape (server-free)', () => {
102
+ it('the telemetry record requires otlp.protocols from the closed transports; genAiSemconv is semver', () => {
103
+ const v = v2RefValidator('capabilities.schema.json#/properties/telemetry');
104
+ const base = { status: 'experimental', since: '2.0', witness: 'witnessable-gated' };
105
+ expect(v({ ...base, otlp: { protocols: ['http/protobuf'] } }).ok, req(R('shape'), DOC, 'a record with otlp.protocols MUST validate')).toBe(true);
106
+ expect(v({ ...base, otlp: { protocols: ['grpc', 'http/json'] }, genAiSemconv: '1.37.0' }).ok, req(R('shape'), DOC, 'genAiSemconv as semver MUST validate')).toBe(true);
107
+ expect(v({ ...base }).ok, req(R('shape'), DOC, 'otlp is REQUIRED')).toBe(false);
108
+ expect(v({ ...base, otlp: { protocols: [] } }).ok, req(R('shape'), DOC, 'protocols MUST be non-empty')).toBe(false);
109
+ expect(v({ ...base, otlp: { protocols: ['zipkin'] } }).ok, req(R('shape'), DOC, 'an unknown transport MUST fail')).toBe(false);
110
+ expect(v({ ...base, otlp: { protocols: ['grpc'] }, genAiSemconv: 'latest' }).ok, req(R('shape'), DOC, 'a non-semver genAiSemconv MUST fail')).toBe(false);
111
+ });
112
+ });
113
+
114
+ describe('v2 RFC 0262 — OTLP export (host, telemetry-gated)', () => {
115
+ it('a run exports an openwop.run span with its run and workflow ids', async () => {
116
+ const g = await gate(NOOP);
117
+ if ('skip' in g) return skipped(g);
118
+ const r = await noop();
119
+ if ('skip' in r) return skipped(r);
120
+ const run = r.spans.find((s) => s.name === 'openwop.run' || s.name.startsWith('openwop.run.'));
121
+ expect(run, req(R('run-span'), DOC, 'every run MUST produce a span named openwop.run carrying openwop.run_id')).toBeDefined();
122
+ expect(run?.attributes.get('openwop.workflow_id'), req(R('run-span'), DOC, 'the run span MUST carry openwop.workflow_id')).toBe(NOOP);
123
+ });
124
+
125
+ it('each node execution exports an openwop.node.<typeId> span with its node attributes', async () => {
126
+ const g = await gate(NOOP);
127
+ if ('skip' in g) return skipped(g);
128
+ const r = await noop();
129
+ if ('skip' in r) return skipped(r);
130
+ const node = r.spans.find((s) => s.name.startsWith('openwop.node.'));
131
+ expect(node, req(R('node-span'), DOC, 'every node execution MUST produce a span named openwop.node.<typeId>')).toBeDefined();
132
+ expect(typeof node?.attributes.get('openwop.node_id'), req(R('node-span'), DOC, 'the node span MUST carry openwop.node_id')).toBe('string');
133
+ const type = node?.attributes.get('openwop.node_type');
134
+ expect(typeof type === 'string' && node?.name === `openwop.node.${type}`, req(R('node-span'), DOC, 'the node span MUST carry openwop.node_type, and its name MUST be openwop.node.<typeId>')).toBe(true);
135
+ const attempt = node?.attributes.get('openwop.node_attempt');
136
+ expect(typeof attempt === 'number' && Number.isInteger(attempt) && attempt >= 0, req(R('node-span'), DOC, 'openwop.node_attempt MUST be a non-negative integer')).toBe(true);
137
+ });
138
+
139
+ it('a run created with a traceparent exports its spans in that trace', async () => {
140
+ const g = await gate(NOOP);
141
+ if ('skip' in g) return skipped(g);
142
+ const traceId = '0af7651916cd43dd8448eb211c80319c';
143
+ const r = await exportedRun({ workflowId: NOOP }, { traceparent: `00-${traceId}-b7ad6b7169203331-01` });
144
+ if ('skip' in r) return skipped(r);
145
+ const inTrace = r.spans.filter((s) => s.traceId.toLowerCase() === traceId);
146
+ expect(inTrace.length, req(R('traceparent-continued'), DOC, 'a run created with a valid W3C traceparent MUST emit its spans in that trace')).toBe(r.spans.length);
147
+ });
148
+
149
+ it('no exported span carries the resolved store secret', async () => {
150
+ const g = await gate(BYOK);
151
+ if ('skip' in g) return skipped(g);
152
+ if (!(await familyAdvertised('secrets'))) return softSkip('inapplicable', 'the secrets family is not advertised, so the store-canary fixture binds nothing');
153
+ const r = await exportedRun({ workflowId: BYOK });
154
+ if ('skip' in r) return skipped(r);
155
+ const snap = await http(() => driver.get(`/runs/${encodeURIComponent(r.runId)}`));
156
+ const poll = await http(() => driver.get(`/runs/${encodeURIComponent(r.runId)}/events/poll?timeout=1&streamMode=debug`));
157
+ const detectors: SecretDetector[] = [];
158
+ const digest = findSecretDigest([r.created.json, snap?.json, poll?.json]);
159
+ if (digest !== null) detectors.push(digestDetector(digest.sha256, digest.length));
160
+ const operator = process.env['OPENWOP_CANARY_SECRET_VALUE'];
161
+ if (operator && operator.length > 0) detectors.push(valueDetector(operator));
162
+ if (detectors.length === 0) return softSkip('inapplicable', 'the run surfaced no {secretSha256, secretLength} and OPENWOP_CANARY_SECRET_VALUE is unset — nothing can be recognised as the canary');
163
+ const found = leaks(spanSurfaces(r.spans), detectors);
164
+ expect(found, req(R('no-secret-in-spans'), DOC, 'a span name, attribute, event or resource attribute MUST NOT carry a resolved secret')).toEqual([]);
165
+ });
166
+
167
+ it('a host advertising genAiSemconv puts the gen_ai attributes on the model-call span', async () => {
168
+ const g = await gate(CALLAI);
169
+ if ('skip' in g) return skipped(g);
170
+ const version = g.record['genAiSemconv'];
171
+ if (typeof version !== 'string') return softSkip('inapplicable', 'telemetry.genAiSemconv is not advertised — no GenAI mapping claim');
172
+ const r = await exportedRun({ workflowId: CALLAI, inputs: { request: { prompt: 'conformance', maxTokens: 64 }, script: [{ kind: 'valid', outputTokens: 5 }] } });
173
+ if ('skip' in r) return skipped(r);
174
+ const call = r.spans.find((s) => s.attributes.has('gen_ai.operation.name'));
175
+ expect(call, req(R('gen-ai-attributes'), DOC, `with genAiSemconv ${version}, each model-call span MUST carry gen_ai.operation.name`)).toBeDefined();
176
+ const has = (k: string): boolean => call?.attributes.get(k) !== undefined && call?.attributes.get(k) !== null;
177
+ expect(has('gen_ai.provider.name') || has('gen_ai.system'), req(R('gen-ai-attributes'), DOC, 'the model-call span MUST carry the provider attribute its version names')).toBe(true);
178
+ expect(has('gen_ai.request.model'), req(R('gen-ai-attributes'), DOC, 'the model-call span MUST carry gen_ai.request.model')).toBe(true);
179
+ expect(has('gen_ai.usage.input_tokens') && has('gen_ai.usage.output_tokens'), req(R('gen-ai-attributes'), DOC, 'the model-call span MUST carry input and output token usage')).toBe(true);
180
+ });
181
+ });