@ggui-ai/protocol-conformance 0.25.0 → 0.27.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.
Files changed (40) hide show
  1. package/README.md +45 -25
  2. package/dist/cli.d.ts +4 -2
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +13 -8
  5. package/dist/dispatch-idempotency-conformance/index.d.ts +78 -0
  6. package/dist/dispatch-idempotency-conformance/index.d.ts.map +1 -0
  7. package/dist/dispatch-idempotency-conformance/index.js +173 -0
  8. package/dist/fixtures/index.d.ts +22 -4
  9. package/dist/fixtures/index.d.ts.map +1 -1
  10. package/dist/fixtures/index.js +23 -3
  11. package/dist/fixtures/reserved-channel-replay/fresh-subscribe-replays-reserved-preview.json +30 -0
  12. package/dist/fixtures/reserved-channel-replay/index.d.ts +4 -0
  13. package/dist/fixtures/reserved-channel-replay/index.d.ts.map +1 -0
  14. package/dist/fixtures/reserved-channel-replay/index.js +18 -0
  15. package/dist/host-helper-conformance/index.d.ts +144 -2
  16. package/dist/host-helper-conformance/index.d.ts.map +1 -1
  17. package/dist/host-helper-conformance/index.js +309 -1
  18. package/dist/index.d.ts +4 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +4 -2
  21. package/dist/match-behavior.d.ts.map +1 -1
  22. package/dist/match-behavior.js +47 -8
  23. package/dist/n1-compat-conformance/cases/forward-app-theme-carry-uninterpretable.json +21 -0
  24. package/dist/n1-compat-conformance/cases/forward-ops-list-blueprints-cloned.json +88 -0
  25. package/dist/n1-compat-conformance/cases/forward-ops-list-blueprints-stamped.json +77 -0
  26. package/dist/n1-compat-conformance/cases/forward-view-proof-later-version.json +14 -0
  27. package/dist/n1-compat-conformance/cases/forward-view-proof-root-later-claim.json +14 -0
  28. package/dist/n1-compat-conformance/cases/release-14-ops-list-blueprints.json +41 -0
  29. package/dist/n1-compat-conformance/index.d.ts +1 -1
  30. package/dist/n1-compat-conformance/index.d.ts.map +1 -1
  31. package/dist/n1-compat-conformance/index.js +128 -22
  32. package/dist/reporter.d.ts +5 -0
  33. package/dist/reporter.d.ts.map +1 -1
  34. package/dist/reporter.js +30 -4
  35. package/dist/run-conformance.d.ts +37 -1
  36. package/dist/run-conformance.d.ts.map +1 -1
  37. package/dist/run-conformance.js +32 -17
  38. package/dist/types.d.ts +16 -0
  39. package/dist/types.d.ts.map +1 -1
  40. package/package.json +8 -3
@@ -69,6 +69,27 @@
69
69
  * view-issued and a model-issued call are indistinguishable on the wire
70
70
  * — so the kit is where a host proves it. Absent ⇒ `skip`.
71
71
  *
72
+ * - `V1-view-material-withheld` — OPTIONAL (ggui#1415, SPEC §4.7): the
73
+ * host keeps view-delivered material out of the model's context. The
74
+ * kit feeds the host's own context rules a render result and a render
75
+ * read body whose `_meta["ai.ggui/render"]` slice carries a unique
76
+ * marker in every string value, and requires none of them (raw,
77
+ * JSON-escaped, percent-encoded, or inside a base64 / base64url run) in
78
+ * what the model sees, while the
79
+ * model still sees the result's model-visible marker; and the host's
80
+ * model-facing resource list, when it has one, must withhold
81
+ * `ui://ggui/render/*` while keeping an ordinary resource. Absent ⇒
82
+ * `skip`.
83
+ * - `L1-view-locator-binding` — OPTIONAL (ggui#1415, SPEC §4.7): the host
84
+ * binds a view's calls and reads to its own locator. The kit feeds the
85
+ * host's own relay decision a view mounted for one session, and reads
86
+ * (every locator form) and every session-naming call a view may make,
87
+ * for that session (relay) and another (refuse). Absent ⇒ `skip`.
88
+ *
89
+ * The three host-rule grades (M1, V1, L1) grade the host's RULE, fed
90
+ * fixtures: they prove the rule is right, not that a running host applies
91
+ * it (self-certification; the V1 and L1 pass details say so).
92
+ *
72
93
  * A helper that refuses the relay honestly is graded **tier
73
94
  * `read-only`** — a LEGAL grade, with the R cases skipped, never
74
95
  * failed. `nonconforming` means a dishonesty case failed.
@@ -79,7 +100,7 @@ import { renderLocatorUri } from '../resource-read-conformance/index.js';
79
100
  * A served-equivalent shape the M1 grade feeds a host's filter (ggui#1414):
80
101
  * agent tools as ggui declares them — `ggui_render` and `ggui_update` stamp
81
102
  * `visibility: ['model']`, the others carry no marker (the spec's default)
82
- * — and ggui's six app-only `ggui_runtime_*` tools. The list is static and
103
+ * — and ggui's seven app-only `ggui_runtime_*` tools. The list is static and
83
104
  * the names are the protocol's own, so it grades the FILTER a host applies,
84
105
  * never the host's live `tools/list`: a host that filters by name rather
85
106
  * than by visibility passes only as long as this list matches what its
@@ -97,8 +118,221 @@ export const MODEL_TOOL_SET_FIXTURE = [
97
118
  { name: 'ggui_runtime_sync_context', _meta: { ui: { visibility: ['app'] } } },
98
119
  { name: 'ggui_runtime_refresh_ws_token', _meta: { ui: { visibility: ['app'] } } },
99
120
  { name: 'ggui_runtime_telemetry', _meta: { ui: { visibility: ['app'] } } },
121
+ { name: 'ggui_runtime_report_render_failure', _meta: { ui: { visibility: ['app'] } } },
100
122
  { name: 'ggui_runtime_declare_tool_catalog', _meta: { ui: { visibility: ['app'] } } },
101
123
  ];
124
+ /**
125
+ * The render result V1 feeds a host's context builder (ggui#1415). Every
126
+ * string value in the `_meta["ai.ggui/render"]` slice is a unique marker
127
+ * ({@link VIEW_MATERIAL_CANARIES}); the model-visible parts carry values of
128
+ * their own, so a host that shows them cannot trip a canary, and one marker
129
+ * ({@link VIEW_MATERIAL_VISIBLE_MARKER}) the model must still see.
130
+ */
131
+ export const VIEW_MATERIAL_RESULT_FIXTURE = {
132
+ structuredContent: { sessionId: 'vmx-visible-session-2a7d', status: 'rendered', note: 'vmx-visible-marker-5e1b' },
133
+ content: [{ type: 'text', text: 'Rendered the card (vmx-visible-marker-5e1b).' }],
134
+ _meta: {
135
+ 'ai.ggui/render': {
136
+ sessionId: 'vmx-slice-session-3f9a1c',
137
+ appId: 'vmx-slice-app-8b2e',
138
+ runtimeUrl: 'https://vmx-slice-runtime.invalid/iframe-runtime.js',
139
+ wsUrl: 'wss://vmx-slice-wsurl-4c2d.invalid/ws',
140
+ wsToken: 'vmx-slice-wstoken-6d41e0a7',
141
+ viewKey: 'vmx-slice-viewkey-Q2FuYXJ5S2V5',
142
+ expiresAt: '2099-07-07T07:07:07.707Z',
143
+ propsJson: '{"vmxSliceProp":"vmx-slice-props-91c2"}',
144
+ },
145
+ },
146
+ };
147
+ /**
148
+ * The marker V1 requires in the model's context: the result itself is not
149
+ * withheld. Both model-visible parts carry it, so a host that shows either
150
+ * one passes.
151
+ */
152
+ export const VIEW_MATERIAL_VISIBLE_MARKER = 'vmx-visible-marker-5e1b';
153
+ /**
154
+ * Every string a value holds, walked deep: object values and keys, arrays,
155
+ * Map entries, Set members, and byte views read as UTF-8 text. Cycles are
156
+ * walked once.
157
+ */
158
+ function stringValuesIn(value, out = [], seen = new Set()) {
159
+ if (typeof value === 'string') {
160
+ out.push(value);
161
+ return out;
162
+ }
163
+ if (typeof value !== 'object' || value === null || seen.has(value))
164
+ return out;
165
+ seen.add(value);
166
+ if (ArrayBuffer.isView(value)) {
167
+ out.push(new TextDecoder().decode(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)));
168
+ }
169
+ else if (value instanceof ArrayBuffer) {
170
+ out.push(new TextDecoder().decode(new Uint8Array(value)));
171
+ }
172
+ else if (value instanceof Map) {
173
+ for (const [k, v] of value) {
174
+ stringValuesIn(k, out, seen);
175
+ stringValuesIn(v, out, seen);
176
+ }
177
+ }
178
+ else if (value instanceof Set || Array.isArray(value)) {
179
+ for (const item of value)
180
+ stringValuesIn(item, out, seen);
181
+ }
182
+ else {
183
+ for (const [k, v] of Object.entries(value)) {
184
+ out.push(k);
185
+ stringValuesIn(v, out, seen);
186
+ }
187
+ }
188
+ return out;
189
+ }
190
+ /**
191
+ * One canary per string value of the fixture's `ai.ggui/render` slice: the
192
+ * value's unique `vmx-slice-…` mark, which survives JSON escaping and
193
+ * encoding, or the whole value when it carries none. None may reach the model.
194
+ */
195
+ export const VIEW_MATERIAL_CANARIES = Object.values(VIEW_MATERIAL_RESULT_FIXTURE._meta['ai.ggui/render'])
196
+ .filter((value) => typeof value === 'string')
197
+ .map((value) => /vmx-slice-[A-Za-z0-9-]+/.exec(value)?.[0] ?? value);
198
+ /** The resources V1 feeds a host's model-facing resource list: the render template and one ordinary resource. */
199
+ export const MODEL_RESOURCE_FIXTURE = [
200
+ { name: 'ggui render', uriTemplate: 'ui://ggui/render/{sessionId}' },
201
+ { name: 'notes', uri: 'file:///vmx-notes.md' },
202
+ ];
203
+ /**
204
+ * A render locator's `resources/read` result as a server answers it: the
205
+ * self-contained shell, with the view's slice inlined, canaries included.
206
+ */
207
+ export const VIEW_MATERIAL_READ_FIXTURE = {
208
+ contents: [
209
+ {
210
+ uri: 'ui://ggui/render/vmx-slice-session-3f9a1c',
211
+ mimeType: 'text/html;profile=mcp-app',
212
+ text: `<!doctype html><html><body><script>globalThis.__GGUI_META__ = ${JSON.stringify(VIEW_MATERIAL_RESULT_FIXTURE._meta)};</script></body></html>`,
213
+ },
214
+ ],
215
+ };
216
+ /** The view L1's requests come from. */
217
+ export const VIEW_BINDING_MOUNT = { locator: 'ui://ggui/render/vmx-s1/bk-vmx1', sessionId: 'vmx-s1' };
218
+ const OTHER_SESSION = 'vmx-s2';
219
+ /**
220
+ * Every tool a view may call (no `model`-only marker) that names a session in
221
+ * its arguments, with arguments its input schema accepts. Left out:
222
+ * `ggui_runtime_declare_tool_catalog` (app-scoped, names none),
223
+ * `ggui_runtime_refresh_ws_token` (its session is inside the envelope, and
224
+ * the server admits the caller to it), `ggui_handshake` and
225
+ * `ggui_list_sessions` (name none), and `ggui_render` / `ggui_update`
226
+ * (model-only).
227
+ */
228
+ export const VIEW_SESSION_BOUND_CALLS = [
229
+ {
230
+ name: 'ggui_runtime_submit_action',
231
+ args: (sessionId) => ({
232
+ kind: 'dispatch',
233
+ payload: { intent: 'confirm', actionData: null, uiContext: {} },
234
+ sessionId,
235
+ appId: 'vmx-app',
236
+ actionId: 'a3f2b1d4',
237
+ firedAt: '2026-09-29T00:00:00.000Z',
238
+ }),
239
+ },
240
+ { name: 'ggui_runtime_sync_context', args: (sessionId) => ({ sessionId, appId: 'vmx-app', snapshot: {} }) },
241
+ { name: 'ggui_runtime_pull', args: (sessionId) => ({ sessionId }) },
242
+ { name: 'ggui_runtime_telemetry', args: (sessionId) => ({ sessionId, events: [{ at: 0, kind: 'boot.path' }] }) },
243
+ {
244
+ name: 'ggui_runtime_report_render_failure',
245
+ args: (sessionId) => ({ sessionId, appId: 'vmx-app', phase: 'mount', errorName: 'TypeError', catches: 2 }),
246
+ },
247
+ { name: 'ggui_consume', args: (sessionId) => ({ sessionId }) },
248
+ { name: 'ggui_amend', args: (sessionId) => ({ sessionId, kind: 'merge', patch: { count: 1 } }) },
249
+ { name: 'ggui_emit', args: (sessionId) => ({ sessionId, channel: 'status', payload: { text: 'hi' } }) },
250
+ { name: 'ggui_get_session', args: (sessionId) => ({ sessionId }) },
251
+ { name: 'ggui_get_render_source', args: (sessionId) => ({ sessionId }) },
252
+ ];
253
+ /**
254
+ * L1's requests and the decision each must get (ggui#1415). A view's own
255
+ * session relays and another's refuses, for a read of either locator form
256
+ * (`{sessionId}`, `{sessionId}/{blueprintKey}`, and an epoch pin `#N`) and
257
+ * for every call in {@link VIEW_SESSION_BOUND_CALLS}.
258
+ */
259
+ export const VIEW_BINDING_CASES = [
260
+ { request: { kind: 'resources/read', uri: VIEW_BINDING_MOUNT.locator }, expect: 'relay' },
261
+ { request: { kind: 'resources/read', uri: `ui://ggui/render/${VIEW_BINDING_MOUNT.sessionId}` }, expect: 'relay' },
262
+ { request: { kind: 'resources/read', uri: `ui://ggui/render/${VIEW_BINDING_MOUNT.sessionId}#2` }, expect: 'relay' },
263
+ { request: { kind: 'resources/read', uri: `ui://ggui/render/${OTHER_SESSION}` }, expect: 'refuse' },
264
+ { request: { kind: 'resources/read', uri: `ui://ggui/render/${OTHER_SESSION}/bk-vmx2` }, expect: 'refuse' },
265
+ { request: { kind: 'resources/read', uri: `ui://ggui/render/${OTHER_SESSION}/bk-vmx2#1` }, expect: 'refuse' },
266
+ ...VIEW_SESSION_BOUND_CALLS.flatMap((call) => [
267
+ { request: { kind: 'tools/call', name: call.name, arguments: call.args(VIEW_BINDING_MOUNT.sessionId) }, expect: 'relay' },
268
+ { request: { kind: 'tools/call', name: call.name, arguments: call.args(OTHER_SESSION) }, expect: 'refuse' },
269
+ ]),
270
+ ];
271
+ /** The base64 / base64url runs inside a string, decoded; a run that does not decode yields nothing. */
272
+ function decodedBase64Runs(text) {
273
+ const runs = text.match(/[A-Za-z0-9+/_-]{16,}={0,2}/g) ?? [];
274
+ // A blob can start anywhere in a run (after a URL path, say), and base64
275
+ // decodes in 4-character groups, so each run is decoded from all four
276
+ // offsets: one of them lines up with the blob.
277
+ return runs.flatMap((run) => [0, 1, 2, 3].flatMap((offset) => {
278
+ const normal = run.slice(offset).replace(/-/g, '+').replace(/_/g, '/').replace(/=+$/, '');
279
+ const usable = normal.slice(0, normal.length - (normal.length % 4 === 1 ? 1 : 0));
280
+ try {
281
+ const binary = atob(usable + '==='.slice((usable.length + 3) % 4));
282
+ return [new TextDecoder().decode(Uint8Array.from(binary, (ch) => ch.charCodeAt(0)))];
283
+ }
284
+ catch {
285
+ // Not base64 from this offset: the run is searched raw already.
286
+ return [];
287
+ }
288
+ }));
289
+ }
290
+ /**
291
+ * A string with its `%XX` escapes decoded, where it carries any: the form
292
+ * state takes in a URL's query string. A malformed escape is left as is,
293
+ * one escape at a time, so a stray `%` never hides the rest.
294
+ */
295
+ function percentDecoded(text) {
296
+ if (!/%[0-9A-Fa-f]{2}/.test(text))
297
+ return [];
298
+ try {
299
+ return [decodeURIComponent(text)];
300
+ }
301
+ catch {
302
+ // A malformed escape somewhere: decode the well-formed ones in place.
303
+ return [text.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {
304
+ try {
305
+ return decodeURIComponent(run);
306
+ }
307
+ catch {
308
+ // Bytes that are not UTF-8 stay escaped; the rest still decode.
309
+ return run;
310
+ }
311
+ })];
312
+ }
313
+ }
314
+ /**
315
+ * Which canaries appear in a model context: raw (JSON-escaped values
316
+ * included, since every mark is escape-free), or inside a percent-encoded
317
+ * string or a base64 / base64url run, decoded (the first match per canary).
318
+ */
319
+ function leakedCanaries(context) {
320
+ const strings = stringValuesIn(context);
321
+ const unescaped = strings.flatMap(percentDecoded);
322
+ const decoded = [...unescaped, ...[...strings, ...unescaped].flatMap(decodedBase64Runs)];
323
+ const raw = VIEW_MATERIAL_CANARIES.filter((c) => strings.some((s) => s.includes(c)));
324
+ const encoded = VIEW_MATERIAL_CANARIES.filter((c) => !raw.includes(c) && decoded.some((s) => s.includes(c)));
325
+ return { raw, encoded };
326
+ }
327
+ /** The slice field a canary is the value of, for a failure detail. */
328
+ function canaryField(canary) {
329
+ const slice = VIEW_MATERIAL_RESULT_FIXTURE._meta['ai.ggui/render'];
330
+ const entry = typeof slice === 'object' && slice !== null
331
+ ? Object.entries(slice).find(([, v]) => typeof v === 'string' && /vmx-slice-[A-Za-z0-9-]+/.exec(v)?.[0] === canary) ??
332
+ Object.entries(slice).find(([, v]) => v === canary)
333
+ : undefined;
334
+ return entry !== undefined ? entry[0] : canary;
335
+ }
102
336
  const METHOD_NOT_SUPPORTED = -32601;
103
337
  const DEFAULT_REFUSAL_TIMEOUT_MS = 2_000;
104
338
  /** A method no ggui helper answers — the honest-refusal probe. */
@@ -511,6 +745,80 @@ export async function runHostHelperConformance(port, options = {}) {
511
745
  });
512
746
  }
513
747
  }
748
+ // ── V1: view-delivered material stays out of the model's context ──
749
+ if (options.modelContext === undefined) {
750
+ cases.push({
751
+ id: 'V1-view-material-withheld',
752
+ outcome: 'skip',
753
+ detail: "no model-context rule supplied: keeping view material from the model is still the host's obligation (SPEC §4.7), just not graded here (supply modelContext to grade)",
754
+ });
755
+ }
756
+ else {
757
+ const rules = options.modelContext;
758
+ try {
759
+ const context = await rules.modelContextOf(VIEW_MATERIAL_RESULT_FIXTURE);
760
+ const readContext = await rules.modelContextOfRead(VIEW_MATERIAL_READ_FIXTURE);
761
+ const fromResult = leakedCanaries(context);
762
+ const fromRead = leakedCanaries(readContext);
763
+ const seesResult = stringValuesIn(context).some((s) => s.includes(VIEW_MATERIAL_VISIBLE_MARKER));
764
+ const offered = rules.resourcesOfferedToModel?.(MODEL_RESOURCE_FIXTURE);
765
+ const offersRender = (offered ?? []).filter((u) => u.startsWith('ui://ggui/render'));
766
+ const keepsOrdinary = offered === undefined || offered.includes('file:///vmx-notes.md');
767
+ const parts = [
768
+ fromResult.raw.length > 0 ? `view material from a tool result in the model's context: ${fromResult.raw.map(canaryField).join(', ')}` : undefined,
769
+ fromResult.encoded.length > 0 ? `view material from a tool result inside a percent-encoded or base64 run: ${fromResult.encoded.map(canaryField).join(', ')}` : undefined,
770
+ fromRead.raw.length > 0 ? `a render read body in the model's context: ${fromRead.raw.map(canaryField).join(', ')}` : undefined,
771
+ fromRead.encoded.length > 0 ? `a render read body inside a percent-encoded or base64 run: ${fromRead.encoded.map(canaryField).join(', ')}` : undefined,
772
+ seesResult ? undefined : `the model does not see the result's own marker ${VIEW_MATERIAL_VISIBLE_MARKER}: withholding the result is not withholding the view material`,
773
+ offersRender.length > 0 ? `a render read offered to the model: ${offersRender.join(', ')}` : undefined,
774
+ keepsOrdinary ? undefined : 'the ordinary resource was not offered: a list that hides everything is not a rule (omit resourcesOfferedToModel when the model reads no resources)',
775
+ ].filter((part) => part !== undefined);
776
+ cases.push(parts.length === 0
777
+ ? {
778
+ id: 'V1-view-material-withheld',
779
+ outcome: 'pass',
780
+ detail: `none of the slice's ${VIEW_MATERIAL_CANARIES.length} values reaches the model from a tool result or a render read body, raw, JSON-escaped, percent-encoded, base64 or base64url; the result itself does; ${offered === undefined ? 'the host offers the model no resource reads' : 'no render read is offered'} (the host's rule, self-certified)`,
781
+ }
782
+ : { id: 'V1-view-material-withheld', outcome: 'fail', detail: parts.join('; ') });
783
+ }
784
+ catch (err) {
785
+ cases.push({ id: 'V1-view-material-withheld', outcome: 'fail', detail: `the host's rule threw: ${err instanceof Error ? err.message : String(err)}` });
786
+ }
787
+ }
788
+ // ── L1: a view's calls and reads are bound to its own locator ─────
789
+ if (options.viewBinding === undefined) {
790
+ cases.push({
791
+ id: 'L1-view-locator-binding',
792
+ outcome: 'skip',
793
+ detail: "no view-binding rule supplied: binding a view to its own locator is still the host's obligation (SPEC §4.7), just not graded here (supply viewBinding to grade)",
794
+ });
795
+ }
796
+ else {
797
+ const describe = (r) => r.kind === 'resources/read' ? `resources/read ${r.uri}` : `${r.name} for session ${String(r.arguments['sessionId'])}`;
798
+ const decide = options.viewBinding.decide;
799
+ const decided = VIEW_BINDING_CASES.map((c) => {
800
+ try {
801
+ return { ...c, got: decide(VIEW_BINDING_MOUNT, c.request) };
802
+ }
803
+ catch (err) {
804
+ // A rule that throws decides nothing: the request is neither relayed nor refused, and the case says so.
805
+ return { ...c, got: `threw: ${err instanceof Error ? err.message : String(err)}` };
806
+ }
807
+ });
808
+ const relayedForeign = decided.filter((c) => c.expect === 'refuse' && c.got !== 'refuse').map((c) => describe(c.request));
809
+ const refusedOwn = decided.filter((c) => c.expect === 'relay' && c.got !== 'relay').map((c) => describe(c.request));
810
+ const parts = [
811
+ relayedForeign.length > 0 ? `relayed another view's request (${relayedForeign.length}): ${relayedForeign.join('; ')}` : undefined,
812
+ refusedOwn.length > 0 ? `refused its own view's request (${refusedOwn.length}): ${refusedOwn.join('; ')}` : undefined,
813
+ ].filter((part) => part !== undefined);
814
+ cases.push(parts.length === 0
815
+ ? {
816
+ id: 'L1-view-locator-binding',
817
+ outcome: 'pass',
818
+ detail: `the view's own reads and session-bound calls relay and another session's refuse, ${VIEW_BINDING_CASES.length} requests (the host's rule, self-certified)`,
819
+ }
820
+ : { id: 'L1-view-locator-binding', outcome: 'fail', detail: parts.join('; ') });
821
+ }
514
822
  const failures = cases
515
823
  .filter((c) => c.outcome === 'fail')
516
824
  .map((c) => c.id);
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  */
13
13
  export type { TransportConfig, WebSocketTransportConfig, UnknownTransportConfig, AuthConfig, TestCase, SubscribeFrameShaping, SetupStep, CreateGguiSessionStep, ActionSpecEntryDecl, JsonSchemaDecl, RendererUrlOverrideStep, ServerVersionOverrideStep, UiInitializeResponseOverrideStep, EmitEnvelopeStep, TeardownStep, ExpectedBehavior, ActionAckBehavior, ErrorFrameBehavior, StreamUpdateBehavior, BootstrapFailureBehavior, BootstrapSuccessBehavior, VersionMismatchBehavior, PropsUpdateBehavior, SessionStateBehavior, NoOpBehavior, UnknownBehavior, ProtocolError, BootstrapFailureReason, } from './types.js';
14
14
  export type { ConformanceHost, SetupStep as HostSetupStep, CreateGguiSessionSetup, RendererUrlOverrideSetup, ServerVersionOverrideSetup, UiInitializeResponseOverrideSetup, EmitEnvelopeSetup, TeardownStep as HostTeardownStep, } from './conformance-host.js';
15
- export { allFixtures, fixturesByContract, bootstrapProtocolFixtures, consumeBufferFixtures, hostContextFixtures, reservedChannelAuthorityFixtures, schemaVersionHandshakeFixtures, subscribeAppScopeFixtures, } from './fixtures/index.js';
15
+ export { allFixtures, fixturesByContract, bootstrapProtocolFixtures, consumeBufferFixtures, hostContextFixtures, reservedChannelAuthorityFixtures, reservedChannelReplayFixtures, schemaVersionHandshakeFixtures, subscribeAppScopeFixtures, } from './fixtures/index.js';
16
16
  export type { ContractSlug } from './fixtures/index.js';
17
17
  export { gadgetWireSchemaCases, runSchemaConformance, } from './schema-conformance/index.js';
18
18
  export type { SchemaConformanceCase, SchemaConformanceMismatch, SchemaConformanceResult, } from './schema-conformance/index.js';
@@ -37,10 +37,12 @@ export type { PreparedReadPlaneOnlyScenario, ReadPlaneOnlyConformanceCase, ReadP
37
37
  export { loadFixture, listFixtures, loadAllFixtures } from './loader.js';
38
38
  export { runConformance } from './run-conformance.js';
39
39
  export type { RunConformanceConfig, ConformanceResult, ConformanceFailure, SkippedFixture, ConformanceReporter, InputEnvelopeDispatch, HostContextObservedInputEnvelope, } from './run-conformance.js';
40
- export { createDefaultReporter, formatScorecard, formatSummary, formatFailures, formatSkips, } from './reporter.js';
40
+ export { createDefaultReporter, formatScorecard, formatSummary, formatFailures, formatWarnings, formatSkips, } from './reporter.js';
41
41
  export type { DefaultReporterOptions } from './reporter.js';
42
42
  export { domainErrorCases, isRawToolCallResult, runDomainErrorConformance, } from './domain-error-conformance/index.js';
43
43
  export type { DomainErrorConformanceCase, DomainErrorConformanceResult, DomainErrorCriterion, DomainErrorMismatch, DomainErrorSkip, RawToolCallResult, RunDomainErrorConformanceOptions, ToolCallArgs, ToolCallContent, ToolCallDriver, ToolCallScenario, } from './domain-error-conformance/index.js';
44
+ export { DISPATCH_IDEMPOTENCY_ACTION, DISPATCH_IDEMPOTENCY_ACTION_SPEC, runDispatchIdempotencyConformance, } from './dispatch-idempotency-conformance/index.js';
45
+ export type { DispatchIdempotencyCaseName, DispatchIdempotencyCaseResult, DispatchIdempotencyHost, DispatchIdempotencyResult, DispatchIdempotencySession, } from './dispatch-idempotency-conformance/index.js';
44
46
  export { APP_THEME_CASES, OVERLAY_HASH_CASES, THEME_MODE_CASES, runThemeBindingConformance, } from './theme-binding-conformance/index.js';
45
47
  export type { AppThemeCase, OverlayHashCase, ThemeBindingResult, ThemeModeCase, } from './theme-binding-conformance/index.js';
46
48
  export { N1_COMPAT_CASES, N1_COMPAT_DIRECTIONS, N1_COMPAT_WIRES, runN1CompatConformance } from './n1-compat-conformance/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  // Fixture catalog — every authored conformance case, classified by
14
14
  // contract. Third-party runners + the kit's own `runConformance()`
15
15
  // consume this surface.
16
- export { allFixtures, fixturesByContract, bootstrapProtocolFixtures, consumeBufferFixtures, hostContextFixtures, reservedChannelAuthorityFixtures, schemaVersionHandshakeFixtures, subscribeAppScopeFixtures, } from './fixtures/index.js';
16
+ export { allFixtures, fixturesByContract, bootstrapProtocolFixtures, consumeBufferFixtures, hostContextFixtures, reservedChannelAuthorityFixtures, reservedChannelReplayFixtures, schemaVersionHandshakeFixtures, subscribeAppScopeFixtures, } from './fixtures/index.js';
17
17
  // Pure-function conformance catalogs — SPEC §7.7.2's gadget
18
18
  // obligations are deterministic validation functions, not transport-
19
19
  // observable behaviors. Each catalog grades a caller-supplied function
@@ -78,9 +78,11 @@ export { runConformance } from './run-conformance.js';
78
78
  // Default reporter — stdout-based bar-scorecard implementation +
79
79
  // formatters programmatic consumers can reuse when building their
80
80
  // own reporter (CI annotations, vitest integration, etc.).
81
- export { createDefaultReporter, formatScorecard, formatSummary, formatFailures, formatSkips, } from './reporter.js';
81
+ export { createDefaultReporter, formatScorecard, formatSummary, formatFailures, formatWarnings, formatSkips, } from './reporter.js';
82
82
  // ── domain-error (SPEC §7.9 Plane 2 on the tools/call wire, ggui#880) ──
83
83
  export { domainErrorCases, isRawToolCallResult, runDomainErrorConformance, } from './domain-error-conformance/index.js';
84
+ // ── dispatch-idempotency (SPEC §11.1 "A retried dispatch is one gesture", ggui#1519; SHOULD level) ──
85
+ export { DISPATCH_IDEMPOTENCY_ACTION, DISPATCH_IDEMPOTENCY_ACTION_SPEC, runDispatchIdempotencyConformance, } from './dispatch-idempotency-conformance/index.js';
84
86
  // ── theme binding (ggui#987 — the theming wave's arbiter; guuey 1.3 judges against it) ──
85
87
  export { APP_THEME_CASES, OVERLAY_HASH_CASES, THEME_MODE_CASES, runThemeBindingConformance, } from './theme-binding-conformance/index.js';
86
88
  // ── n1-compat (ggui#1014 §3.6 — previous-release payloads against today's parsers; ggui#1093 — a later release's payload against today's READ door) ──
@@ -1 +1 @@
1
- {"version":3,"file":"match-behavior.d.ts","sourceRoot":"","sources":["../src/match-behavior.ts"],"names":[],"mappings":"AA+FA,OAAO,KAAK,EAIV,gBAAgB,EAIjB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,gBAAgB,CAAC,MAAM,CAAC,EAC3D,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,CAAC,GACN,QAAQ,IAAI,OAAO,CAAC,gBAAgB,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC,CAE7D;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,SAAS,aAAa,EAAE,GAC/B,WAAW,CAkDb;AAqUD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAgBzD"}
1
+ {"version":3,"file":"match-behavior.d.ts","sourceRoot":"","sources":["../src/match-behavior.ts"],"names":[],"mappings":"AA+FA,OAAO,KAAK,EAIV,gBAAgB,EAIjB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,gBAAgB,CAAC,MAAM,CAAC,EAC3D,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,CAAC,GACN,QAAQ,IAAI,OAAO,CAAC,gBAAgB,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC,CAE7D;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,SAAS,aAAa,EAAE,GAC/B,WAAW,CAkDb;AAkXD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAgBzD"}
@@ -340,19 +340,21 @@ function matchContractViolationPayload(assertions, parsed, frames) {
340
340
  * non-conformant envelope must not satisfy the assertion.
341
341
  */
342
342
  function matchStreamUpdate(behavior, frames) {
343
- const envelopes = frames.flatMap((f) => {
344
- if (f.kind !== 'frame' || f.parsed['type'] !== 'data')
345
- return [];
346
- const body = f.parsed['payload'];
347
- return isStreamEnvelope(body) ? [body] : [];
348
- });
349
343
  // `valueMatch: 'subset'` relaxes the value comparison to "every
350
344
  // declared key is present + matching; extra observed keys ignored".
351
345
  // Default ('exact' / absent) is the exact deep-equal.
352
346
  const subset = behavior.valueMatch === 'subset';
353
347
  const valueMatches = (observed) => subset ? deepMatchSubset(behavior.value, observed) : deepEqual(observed, behavior.value);
354
- const match = envelopes.find((envelope) => envelope.channel === behavior.channel && valueMatches(envelope.payload));
355
- if (match === undefined) {
348
+ // Every matching envelope, with where it arrived among the frames.
349
+ const matches = frames.flatMap((f, index) => {
350
+ if (f.kind !== 'frame' || f.parsed['type'] !== 'data')
351
+ return [];
352
+ const body = f.parsed['payload'];
353
+ return isStreamEnvelope(body) && body.channel === behavior.channel && valueMatches(body.payload)
354
+ ? [{ index, envelope: body }]
355
+ : [];
356
+ });
357
+ if (matches.length === 0) {
356
358
  return {
357
359
  kind: 'fail',
358
360
  expected: {
@@ -366,6 +368,43 @@ function matchStreamUpdate(behavior, frames) {
366
368
  message: `expected a \`data\` frame whose StreamEnvelope names channel '${behavior.channel}' and carries the declared value as its payload (${subset ? 'subset match — every declared key present + matching' : 'exact match'}); none matched.`,
367
369
  };
368
370
  }
371
+ if (behavior.replayAfterAck !== true)
372
+ return { kind: 'pass' };
373
+ return matchReplayPlacement(behavior.channel, frames, matches);
374
+ }
375
+ /**
376
+ * The replay placement a `stream-update` with `replayAfterAck` grades
377
+ * (SPEC §12.2.1, ggui#1526): a matching frame that arrives after the
378
+ * subscribe's `ack` and carries a `seq` at or below that ack's
379
+ * `streamSeq`. The first placement failure found names what is wrong.
380
+ */
381
+ function matchReplayPlacement(channel, frames, matches) {
382
+ const ackIndex = frames.findIndex((f) => f.kind === 'frame' && f.parsed['type'] === 'ack');
383
+ const expected = { type: 'data', channel, after: 'the subscribe ack', seq: '<= ack.payload.streamSeq' };
384
+ const fail = (message) => ({
385
+ kind: 'fail',
386
+ expected,
387
+ received: frames.filter((f) => f.kind === 'frame').map((f) => (f.kind === 'frame' ? f.parsed : f)),
388
+ message,
389
+ });
390
+ if (ackIndex === -1) {
391
+ return fail(`a matching frame on '${channel}' was observed, but no subscribe \`ack\` was, so it cannot be shown to be the replay.`);
392
+ }
393
+ const ackFrame = frames[ackIndex];
394
+ const ackPayload = ackFrame !== undefined && ackFrame.kind === 'frame' ? ackFrame.parsed['payload'] : undefined;
395
+ const streamSeq = isRecord(ackPayload) ? ackPayload['streamSeq'] : undefined;
396
+ if (typeof streamSeq !== 'number') {
397
+ return fail(`the subscribe \`ack\` carries no numeric \`streamSeq\`, so a frame on '${channel}' cannot be shown to be at or below it.`);
398
+ }
399
+ const afterAck = matches.filter((m) => m.index > ackIndex);
400
+ if (afterAck.length === 0) {
401
+ return fail(`the only matching frames on '${channel}' arrived before the subscribe's \`ack\`; the replay belongs after it.`);
402
+ }
403
+ const replayed = afterAck.find((m) => typeof m.envelope.seq === 'number' && m.envelope.seq <= streamSeq);
404
+ if (replayed === undefined) {
405
+ const seqs = afterAck.map((m) => (typeof m.envelope.seq === 'number' ? m.envelope.seq : 'none')).join(', ');
406
+ return fail(`the matching frames on '${channel}' after the \`ack\` carry seq ${seqs}, none at or below the ack's \`streamSeq\` (${streamSeq}): each is above the ack's \`streamSeq\` (${streamSeq}) or unnumbered, so none is the replay.`);
407
+ }
369
408
  return { kind: 'pass' };
370
409
  }
371
410
  function matchNoOp(behavior, frames) {
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "forward-app-theme-carry-uninterpretable",
3
+ "description": "FORWARD tolerance on the theme CARRY read (ggui#1175, VERSION-POLICY §3.6): a theme GET response for a stored document that fails the carry shape, as a door that names it returns it — `theme: null` beside `uninterpretable`, whose issues carry a path and a protocol-owned code and never a stored value. Today's response schema must accept it, and must read it as a document that is not a theme, never as \"no theme\".",
4
+ "direction": "forward",
5
+ "release": {
6
+ "label": "a later ggui release (synthetic): the theme GET naming an uninterpretable stored document",
7
+ "sha": "synthetic"
8
+ },
9
+ "wire": "app-theme-carry",
10
+ "payload": {
11
+ "theme": null,
12
+ "uninterpretable": {
13
+ "issueCount": 2,
14
+ "issues": [
15
+ { "path": ["overlays", "light", "not-a-ggui-var"], "code": "unknown_key" },
16
+ { "path": ["mode"], "code": "value_not_allowed" }
17
+ ]
18
+ }
19
+ },
20
+ "expect": "accepted"
21
+ }
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "forward-ops-list-blueprints-cloned",
3
+ "description": "FORWARD (ggui#1570 emit, VERSION-POLICY §3.6): the ggui_ops_list_blueprints result the release after the declare step writes once a copied row marks itself — synthetic by construction, so it carries no landed sha. Two rows built on the stamped forward case's first row: the original, with no `clonedFrom`, and its copy, which carries the SAME bytes, `source` and `build` (true about the bytes) under its own id and variance, and `clonedFrom` naming the original. The reader MUST accept the result, keep `clonedFrom` VERBATIM on the copy and leave the original without one. A reader that dropped or rewrote the mark — the declare step's reader declares it, and a later change to `opsListBlueprintsOutputSchema` must not lose it — fails here, and so would a copy that reads as a second production of its generation.",
4
+ "direction": "forward",
5
+ "release": {
6
+ "label": "a later ggui release (synthetic): the stamped served row, and its copy",
7
+ "sha": "synthetic"
8
+ },
9
+ "wire": "ops-list-blueprints",
10
+ "payload": {
11
+ "blueprints": [
12
+ {
13
+ "blueprintId": "bp_00000000-0000-4000-8000-000000001280",
14
+ "contractHash": "a8b8be414e9e53f7",
15
+ "appId": "IMud3W0m",
16
+ "codeS3Url": "s3://blueprint-code/code/be997120c9425551b4db6c041c32d632255ba166e5d3746fe3c4cbbbf3a2edd9.js",
17
+ "codeHash": "be997120c9425551b4db6c041c32d632255ba166e5d3746fe3c4cbbbf3a2edd9",
18
+ "source": {
19
+ "kind": "llm",
20
+ "generator": "ui-gen-default",
21
+ "model": "openai/gpt-6-luna"
22
+ },
23
+ "variance": {},
24
+ "createdAt": "2026-09-27T16:40:27.386Z",
25
+ "createdBy": "agent",
26
+ "contract": {
27
+ "propsSpec": {
28
+ "properties": {
29
+ "harbour": {
30
+ "schema": {
31
+ "type": "string"
32
+ },
33
+ "required": true
34
+ }
35
+ }
36
+ }
37
+ },
38
+ "build": {
39
+ "version": "0.25.0",
40
+ "mode": "constrained",
41
+ "digests": {
42
+ "promptTemplateSha256": "fdf5de0bdc977b698772e337d552dbe27cbc984382e6c0f68b0f5a7d8a953f00",
43
+ "boilerplateTemplateSha256": "9b4393cd686244d9e06d480f477dc85aa5b4469d45dfb103b76b873e22e65c24"
44
+ }
45
+ }
46
+ },
47
+ {
48
+ "blueprintId": "bp_00000000-0000-4000-8000-000000001570",
49
+ "contractHash": "a8b8be414e9e53f7",
50
+ "appId": "IMud3W0m",
51
+ "codeS3Url": "s3://blueprint-code/code/be997120c9425551b4db6c041c32d632255ba166e5d3746fe3c4cbbbf3a2edd9.js",
52
+ "codeHash": "be997120c9425551b4db6c041c32d632255ba166e5d3746fe3c4cbbbf3a2edd9",
53
+ "source": {
54
+ "kind": "llm",
55
+ "generator": "ui-gen-default",
56
+ "model": "openai/gpt-6-luna"
57
+ },
58
+ "variance": {
59
+ "persona": "control"
60
+ },
61
+ "createdAt": "2026-09-30T09:00:00.000Z",
62
+ "createdBy": "operator",
63
+ "contract": {
64
+ "propsSpec": {
65
+ "properties": {
66
+ "harbour": {
67
+ "schema": {
68
+ "type": "string"
69
+ },
70
+ "required": true
71
+ }
72
+ }
73
+ }
74
+ },
75
+ "build": {
76
+ "version": "0.25.0",
77
+ "mode": "constrained",
78
+ "digests": {
79
+ "promptTemplateSha256": "fdf5de0bdc977b698772e337d552dbe27cbc984382e6c0f68b0f5a7d8a953f00",
80
+ "boilerplateTemplateSha256": "9b4393cd686244d9e06d480f477dc85aa5b4469d45dfb103b76b873e22e65c24"
81
+ }
82
+ },
83
+ "clonedFrom": "bp_00000000-0000-4000-8000-000000001280"
84
+ }
85
+ ]
86
+ },
87
+ "expect": "accepted"
88
+ }