@ggui-ai/protocol-conformance 0.15.0 → 0.17.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 (62) hide show
  1. package/README.md +26 -16
  2. package/dist/cli.d.ts +6 -2
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +52 -6
  5. package/dist/domain-error-conformance/cases/amend-unknown-session.json +9 -0
  6. package/dist/domain-error-conformance/cases/consume-unknown-session.json +9 -0
  7. package/dist/domain-error-conformance/cases/emit-unknown-session.json +9 -0
  8. package/dist/domain-error-conformance/cases/get-session-unknown-session.json +6 -0
  9. package/dist/domain-error-conformance/cases/render-unknown-handshake.json +6 -0
  10. package/dist/domain-error-conformance/cases/update-unknown-session.json +9 -0
  11. package/dist/domain-error-conformance/index.d.ts +97 -0
  12. package/dist/domain-error-conformance/index.d.ts.map +1 -0
  13. package/dist/domain-error-conformance/index.js +97 -0
  14. package/dist/fixtures/index.d.ts +2 -2
  15. package/dist/fixtures/index.d.ts.map +1 -1
  16. package/dist/fixtures/index.js +3 -3
  17. package/dist/fixtures/{subscribe-tenancy → subscribe-app-scope}/absent-appid-defaults.json +1 -1
  18. package/dist/fixtures/{subscribe-tenancy → subscribe-app-scope}/app-mismatch.json +1 -1
  19. package/dist/fixtures/subscribe-app-scope/index.d.ts +6 -0
  20. package/dist/fixtures/subscribe-app-scope/index.d.ts.map +1 -0
  21. package/dist/fixtures/{subscribe-tenancy → subscribe-app-scope}/index.js +5 -5
  22. package/dist/index.d.ts +7 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +7 -1
  25. package/dist/n1-compat-conformance/cases/forward-app-theme-unknown-member.json +27 -0
  26. package/dist/n1-compat-conformance/cases/release-2-app-theme-v2.json +24 -0
  27. package/dist/n1-compat-conformance/cases/release-2-generation-profile.json +16 -0
  28. package/dist/n1-compat-conformance/cases/release-2-ops-generate-blueprint.json +33 -0
  29. package/dist/n1-compat-conformance/cases/release-2-render-meta.json +40 -0
  30. package/dist/n1-compat-conformance/index.d.ts +36 -0
  31. package/dist/n1-compat-conformance/index.d.ts.map +1 -0
  32. package/dist/n1-compat-conformance/index.js +140 -0
  33. package/dist/refusal-envelope-conformance/cases/refuse-after-fix-caller.json +3 -3
  34. package/dist/refusal-envelope-conformance/cases/refuse-non-render-surface.json +4 -4
  35. package/dist/run-conformance.d.ts +13 -3
  36. package/dist/run-conformance.d.ts.map +1 -1
  37. package/dist/run-conformance.js +83 -7
  38. package/dist/theme-binding-conformance/cases/app-theme-v1-base.json +18 -0
  39. package/dist/theme-binding-conformance/cases/app-theme-v1-one-palette.json +11 -0
  40. package/dist/theme-binding-conformance/cases/app-theme-v2-accepted.json +18 -0
  41. package/dist/theme-binding-conformance/cases/app-theme-v2-keyframes-accepted.json +16 -0
  42. package/dist/theme-binding-conformance/cases/app-theme-v2-keyframes-not-keyframes.json +15 -0
  43. package/dist/theme-binding-conformance/cases/app-theme-v2-label-too-long.json +17 -0
  44. package/dist/theme-binding-conformance/cases/app-theme-v2-missing-dark.json +13 -0
  45. package/dist/theme-binding-conformance/cases/app-theme-v2-no-hash.json +15 -0
  46. package/dist/theme-binding-conformance/cases/app-theme-v2-platform-pin.json +17 -0
  47. package/dist/theme-binding-conformance/cases/overlay-hash-canonical.json +17 -0
  48. package/dist/theme-binding-conformance/cases/theme-mode-absence.json +10 -0
  49. package/dist/theme-binding-conformance/cases/theme-mode-default-when-silent-host.json +10 -0
  50. package/dist/theme-binding-conformance/cases/theme-mode-order.json +10 -0
  51. package/dist/theme-binding-conformance/index.d.ts +59 -0
  52. package/dist/theme-binding-conformance/index.d.ts.map +1 -0
  53. package/dist/theme-binding-conformance/index.js +110 -0
  54. package/dist/transport-refusal-conformance/cases/refuse-deprovisioned-endpoint.json +7 -5
  55. package/dist/transport-refusal-conformance/cases/refuse-render-only-code.json +3 -2
  56. package/dist/transport-refusal-conformance/index.d.ts +2 -0
  57. package/dist/transport-refusal-conformance/index.d.ts.map +1 -1
  58. package/dist/transport-refusal-conformance/index.js +2 -2
  59. package/dist/types.d.ts +1 -1
  60. package/package.json +17 -2
  61. package/dist/fixtures/subscribe-tenancy/index.d.ts +0 -6
  62. package/dist/fixtures/subscribe-tenancy/index.d.ts.map +0 -1
@@ -47,7 +47,8 @@
47
47
  * loop is empty today.)
48
48
  * l. Record the outcome via reporter + accumulate in result.
49
49
  * 3. Fold in the PURE-FUNCTION catalogs (ggui#786) —
50
- * `refusal-envelope`, `registry-completeness` and `transport-refusal`. They grade a
50
+ * `refusal-envelope`, `registry-completeness`, `transport-refusal`,
51
+ * `domain-error` and `theme-binding` (ggui#987). They grade a
51
52
  * caller-supplied projector / registry rather than the wire, so
52
53
  * they need no transport; when the caller supplies neither, their
53
54
  * rows are reported SKIPPED with that reason so an ungraded
@@ -66,9 +67,14 @@ import { openWsTransport } from './ws-transport.js';
66
67
  import { refusalEnvelopeCases, runRefusalEnvelopeConformance, } from './refusal-envelope-conformance/index.js';
67
68
  import { registryCompletenessPins, runRegistryCompletenessConformance, } from './registry-completeness/index.js';
68
69
  import { runTransportRefusalConformance, transportRefusalCases, } from './transport-refusal-conformance/index.js';
70
+ import { runThemeBindingConformance } from './theme-binding-conformance/index.js';
71
+ import { runN1CompatConformance } from './n1-compat-conformance/index.js';
72
+ import { domainErrorCases, runDomainErrorConformance, } from './domain-error-conformance/index.js';
69
73
  /**
70
- * Row-name prefixes for the PURE-FUNCTION catalogs the runner folds
71
- * into a run. Prefixed so a catalog row can never collide with a
74
+ * Row-name prefixes for the catalogs the runner folds into a run beside
75
+ * the WebSocket fixtures: three grade an in-process function or a data
76
+ * table; `domain-error` (ggui#880) drives live `tools/call` scenarios
77
+ * through an adopter-supplied driver. Prefixed so a catalog row can never collide with a
72
78
  * WebSocket fixture name, and so the reporter can group them without a
73
79
  * second registry of names.
74
80
  */
@@ -76,6 +82,9 @@ export const PURE_FUNCTION_CATALOG_SLUGS = [
76
82
  'refusal-envelope',
77
83
  'registry-completeness',
78
84
  'transport-refusal',
85
+ 'domain-error',
86
+ 'theme-binding',
87
+ 'n1-compat',
79
88
  ];
80
89
  // =============================================================================
81
90
  // Runner
@@ -113,7 +122,7 @@ export async function runConformance(config) {
113
122
  break;
114
123
  }
115
124
  }
116
- runPureFunctionCatalogs(config, reporter, { passed, failed, skipped });
125
+ await runPureFunctionCatalogs(config, reporter, { passed, failed, skipped });
117
126
  const result = {
118
127
  passed,
119
128
  failed,
@@ -138,7 +147,7 @@ export async function runConformance(config) {
138
147
  * absent input produces a SKIP row naming what to supply rather than
139
148
  * nothing at all.
140
149
  */
141
- function runPureFunctionCatalogs(config, reporter, buckets) {
150
+ async function runPureFunctionCatalogs(config, reporter, buckets) {
142
151
  const only = config.only;
143
152
  const wanted = (name) => only === undefined || only.length === 0 || only.includes(name);
144
153
  const pass = (name) => {
@@ -201,6 +210,33 @@ function runPureFunctionCatalogs(config, reporter, buckets) {
201
210
  });
202
211
  }
203
212
  }
213
+ // ── domain-error (ggui#880) ──
214
+ const toolCallDriver = config.toolCallDriver;
215
+ if (toolCallDriver === undefined) {
216
+ for (const testCase of domainErrorCases) {
217
+ skip(`domain-error/${testCase.name}`, 'no `toolCallDriver` supplied — pass one to runConformance(), or `--tool-call-driver <module>` on the CLI, to grade SPEC §7.9\u2019s Plane-2 wire text');
218
+ }
219
+ }
220
+ else {
221
+ // A live driver is real calls: drive only the rows `only` selects.
222
+ const selected = domainErrorCases.filter((testCase) => wanted(`domain-error/${testCase.name}`));
223
+ const graded = selected.length === 0
224
+ ? { passed: [], failed: [], skipped: [] }
225
+ : await runDomainErrorConformance(toolCallDriver, { cases: selected });
226
+ for (const name of graded.passed)
227
+ pass(`domain-error/${name}`);
228
+ for (const skipped of graded.skipped)
229
+ skip(`domain-error/${skipped.name}`, skipped.reason);
230
+ for (const mismatch of graded.failed) {
231
+ fail({
232
+ name: `domain-error/${mismatch.name}`,
233
+ criterion: `Plane-2 wire text (SPEC §7.9): ${mismatch.criterion}`,
234
+ expected: mismatch.expected,
235
+ received: mismatch.actual,
236
+ message: 'the raw tools/call result does not carry the registered slug as the leading token of its text',
237
+ });
238
+ }
239
+ }
204
240
  // ── registry-completeness ──
205
241
  const registry = config.refusalRegistry;
206
242
  if (registry === undefined) {
@@ -222,6 +258,46 @@ function runPureFunctionCatalogs(config, reporter, buckets) {
222
258
  });
223
259
  }
224
260
  }
261
+ // ── theme-binding (ggui#987) ──
262
+ // The theming wave's arbiter: graded against the protocol's own functions
263
+ // (`appThemeSchema`, `effectiveThemeMode`, `canonicalOverlayHash`), so
264
+ // there is no adopter input to supply and no reason to skip — every run
265
+ // reports it. `only` still selects rows.
266
+ for (const graded of await runThemeBindingConformance()) {
267
+ const name = `theme-binding/${graded.name}`;
268
+ if (graded.pass) {
269
+ pass(name);
270
+ }
271
+ else {
272
+ fail({
273
+ name,
274
+ criterion: 'theme binding (SPEC "Theme binding — the normative total order", ggui#987)',
275
+ expected: 'the case\u2019s expectation',
276
+ received: graded.detail,
277
+ message: graded.detail,
278
+ });
279
+ }
280
+ }
281
+ // ── n1-compat (ggui#1014 §3.6) ──
282
+ // Previous-release payloads against today's protocol parsers: no adopter
283
+ // input, never skipped; a failing row is a receiver that broke N−1.
284
+ for (const graded of runN1CompatConformance()) {
285
+ const name = `n1-compat/${graded.name}`;
286
+ if (graded.pass) {
287
+ pass(name);
288
+ }
289
+ else {
290
+ fail({
291
+ name,
292
+ criterion: 'N−1 wire compatibility (VERSION-POLICY §3.6, ggui#1014)',
293
+ expected: graded.direction === 'forward'
294
+ ? 'a later release\u2019s payload KEPT by today\u2019s read door — the unknown top-level member stripped and named'
295
+ : 'the previous release\u2019s payload accepted by today\u2019s parser',
296
+ received: graded.detail,
297
+ message: graded.detail,
298
+ });
299
+ }
300
+ }
225
301
  }
226
302
  async function runOneFixture(fixture, config, _reporter) {
227
303
  const fixtureStarted = Date.now();
@@ -684,8 +760,8 @@ function slugToCriterion(slug) {
684
760
  return 'SPEC §4.4 reserved-channel authority';
685
761
  case 'schema-version-handshake':
686
762
  return 'Protocol #3 version negotiation';
687
- case 'subscribe-tenancy':
688
- return 'SPEC §12.2 subscribe tenancy — appId MUST match the bound app (§12.2.3 APP_MISMATCH)';
763
+ case 'subscribe-app-scope':
764
+ return 'SPEC §12.2 subscribe app scope — appId MUST match the bound app (§12.2.3 APP_MISMATCH)';
689
765
  default:
690
766
  return slug;
691
767
  }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "app-theme-v1-base",
3
+ "description": "A v1 overlay carrying a registered `base` ladder is REFUSED since ggui#987 (D2 = B): the registration tier and `base` are gone. Accepted before #987 (promoted, then failed \u2014 the \u00a71.1 receipt).",
4
+ "theme": {
5
+ "mode": "dark",
6
+ "cssVariables": {},
7
+ "base": {
8
+ "documentHash": "0000000000000000000000000000000000000000000000000000000000000000",
9
+ "light": {
10
+ "--ggui-color-primary-500": "#3355ff"
11
+ },
12
+ "dark": {
13
+ "--ggui-color-primary-500": "#99aaff"
14
+ }
15
+ }
16
+ },
17
+ "expect": "refused"
18
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "app-theme-v1-one-palette",
3
+ "description": "The v1 overlay wire \u2014 a pinned mode and ONE cssVariables palette \u2014 is REFUSED since ggui#987: an overlay that could not follow the host's mode is not accepted; both projections are required. Accepted before #987 (promoted, then failed \u2014 the \u00a71.1 receipt).",
4
+ "theme": {
5
+ "mode": "light",
6
+ "cssVariables": {
7
+ "--ggui-color-primary-500": "#3355ff"
8
+ }
9
+ },
10
+ "expect": "refused"
11
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "app-theme-v2-accepted",
3
+ "description": "The v2 overlay: both projections, the attestation, an optional default mode and a label. Accepted.",
4
+ "theme": {
5
+ "overlayHash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
6
+ "overlays": {
7
+ "light": {
8
+ "--ggui-color-primary-500": "#3355ff"
9
+ },
10
+ "dark": {
11
+ "--ggui-color-primary-500": "#99aaff"
12
+ }
13
+ },
14
+ "mode": "light",
15
+ "name": "ocean"
16
+ },
17
+ "expect": "accepted"
18
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "app-theme-v2-keyframes-accepted",
3
+ "description": "Per-mode keyframes text that is exactly `@keyframes <ident> { … }` blocks (two blocks, a comment, frame selectors of every kind) is ACCEPTED by appThemeSchema — the slot carries what the card injects verbatim after the variables (ggui#987 §3.3, follower D).",
4
+ "theme": {
5
+ "overlays": {
6
+ "light": { "--ggui-color-primary-500": "#3355ff" },
7
+ "dark": { "--ggui-color-primary-500": "#99aaff" }
8
+ },
9
+ "overlayHash": "0000000000000000000000000000000000000000000000000000000000000000",
10
+ "keyframes": {
11
+ "light": "/* entrance */ @keyframes entrance { 0% { opacity: 0; transform: translateY(4px) } 100% { opacity: 1; transform: none } }\n@keyframes accent-pulse{0%,100%{opacity:1}50%{opacity:.7}}",
12
+ "dark": "@keyframes spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
13
+ }
14
+ },
15
+ "expect": "accepted"
16
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "app-theme-v2-keyframes-not-keyframes",
3
+ "description": "Keyframes text that carries anything other than `@keyframes` blocks — here a bare `body { … }` rule after a valid block — is REFUSED by appThemeSchema: the slot is bounded to what it is for, so the theme principal cannot ship arbitrary same-document CSS through it (ggui#987 §3.4, follower D).",
4
+ "theme": {
5
+ "overlays": {
6
+ "light": { "--ggui-color-primary-500": "#3355ff" },
7
+ "dark": { "--ggui-color-primary-500": "#99aaff" }
8
+ },
9
+ "overlayHash": "0000000000000000000000000000000000000000000000000000000000000000",
10
+ "keyframes": {
11
+ "light": "@keyframes spin { to { transform: rotate(360deg) } } body { display: none }"
12
+ }
13
+ },
14
+ "expect": "refused"
15
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "app-theme-v2-label-too-long",
3
+ "description": "A stamped label carrying a FULL sha256 after the prefix is 77 chars and exceeds appThemeSchema's 64-char `name` cap \u2014 REFUSED at the door. The seam ggui#987 flagged for the platform's pin shape.",
4
+ "theme": {
5
+ "overlays": {
6
+ "light": {
7
+ "--ggui-color-primary-500": "#3355ff"
8
+ },
9
+ "dark": {
10
+ "--ggui-color-primary-500": "#99aaff"
11
+ }
12
+ },
13
+ "overlayHash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
14
+ "name": "guuey-app-v2.dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
15
+ },
16
+ "expect": "refused"
17
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "app-theme-v2-missing-dark",
3
+ "description": "An overlay with only one projection cannot follow the host's mode \u2014 REFUSED.",
4
+ "theme": {
5
+ "overlayHash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
6
+ "overlays": {
7
+ "light": {
8
+ "--ggui-color-primary-500": "#3355ff"
9
+ }
10
+ }
11
+ },
12
+ "expect": "refused"
13
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "app-theme-v2-no-hash",
3
+ "description": "The attestation is REQUIRED: an overlay without `overlayHash` is refused at the door.",
4
+ "theme": {
5
+ "overlays": {
6
+ "light": {
7
+ "--ggui-color-primary-500": "#3355ff"
8
+ },
9
+ "dark": {
10
+ "--ggui-color-primary-500": "#99aaff"
11
+ }
12
+ }
13
+ },
14
+ "expect": "refused"
15
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "app-theme-v2-platform-pin",
3
+ "description": "The exact overlay shape a platform's -v2 composer sends (ggui#987 \u00a79; guuey#1128): both projections, the attestation, and a label of the form `<prefix>.<hash-prefix>`. `name` is capped at 64 chars by appThemeSchema, so a stamped label must keep its hash segment to 51 chars after a 13-char prefix. Accepted.",
4
+ "theme": {
5
+ "overlays": {
6
+ "light": {
7
+ "--ggui-color-primary-500": "#3355ff"
8
+ },
9
+ "dark": {
10
+ "--ggui-color-primary-500": "#99aaff"
11
+ }
12
+ },
13
+ "overlayHash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
14
+ "name": "guuey-app-v2.ccccccccccccccccccccccccccccccccccccccccccccccccccc"
15
+ },
16
+ "expect": "accepted"
17
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "overlay-hash-canonical",
3
+ "description": "canonicalOverlayHash is ONE function on every minter and door: keys sorted recursively, no whitespace, absent fields omitted, sha256 lowercase hex. The expected value was computed independently with node:crypto over the canonical JSON; the input's keys are deliberately unsorted.",
4
+ "input": {
5
+ "overlays": {
6
+ "light": {
7
+ "--ggui-color-primary-500": "#3355ff",
8
+ "--ggui-color-ground": "#ffffff"
9
+ },
10
+ "dark": {
11
+ "--ggui-color-primary-500": "#99aaff",
12
+ "--ggui-color-ground": "#0e1014"
13
+ }
14
+ }
15
+ },
16
+ "expect": "7cc8c947beeae43de91a8e3eedc182a739ae134e3a3957958ebee4ec6cf25e96"
17
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "theme-mode-absence",
3
+ "description": "The absence law (ggui#551): when no layer resolves, the projection is undefined \u2014 never a light default.",
4
+ "sources": {
5
+ "stamped": null,
6
+ "sessionSidecar": null,
7
+ "hostAnnounced": null
8
+ },
9
+ "expect": null
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "theme-mode-default-when-silent-host",
3
+ "description": "A host that announces nothing leaves the stamp (then the sidecar) to fill the mode \u2014 the defaults still apply.",
4
+ "sources": {
5
+ "stamped": "light",
6
+ "sessionSidecar": "dark",
7
+ "hostAnnounced": null
8
+ },
9
+ "expect": "light"
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "theme-mode-order",
3
+ "description": "The client projection of the themeMode total order since ggui#987 (D4 = A, 'follow the widget'): with a stamped mode and a host announce present, the HOST wins \u2014 the embedding host owns runtime mode; the stamp and the sidecar are defaults. Before #987 this case expected 'light' (the stamp); it was promoted into the kit at that version and FAILED when the order flipped \u2014 the VERSION-POLICY \u00a71.1 receipt that names the flip breaking.",
4
+ "sources": {
5
+ "stamped": "light",
6
+ "sessionSidecar": null,
7
+ "hostAnnounced": "dark"
8
+ },
9
+ "expect": "dark"
10
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Theme-binding conformance — a pure-function catalog (ggui#987).
3
+ *
4
+ * Three contracts the kit grades directly against `@ggui-ai/protocol`, so
5
+ * their breaking-ness is decided by a fixture and not by prose
6
+ * (VERSION-POLICY §2 — the kit is the arbiter):
7
+ *
8
+ * - `theme-mode-*` — the CLIENT projection of the `themeMode` total
9
+ * order, `effectiveThemeMode` (`integrations/theme-binding.ts`): the
10
+ * embedding host owns runtime mode (D4).
11
+ * - `app-theme-*` — which overlay shapes `appThemeSchema` accepts: both
12
+ * projections and the attestation required; the v1 one-palette wire and
13
+ * `base` refused.
14
+ * - `overlay-hash-*` — `canonicalOverlayHash` against an independently
15
+ * computed vector.
16
+ *
17
+ * History (the §1.1 receipt): `theme-mode-order`, `app-theme-v1-one-palette`
18
+ * and `app-theme-v1-base` were promoted from the unit pins at the version
19
+ * BEFORE the revision, passed there, and failed when it landed — that is
20
+ * what named the revision breaking. Like `../refusal-envelope-conformance`,
21
+ * the cases are raw JSON under `./cases/` and are NOT registered in
22
+ * `fixturesByContract`; the entry point is the runner below and its test.
23
+ */
24
+ import { canonicalOverlayHash } from '@ggui-ai/protocol';
25
+ type Opinion = 'light' | 'dark';
26
+ export interface ThemeModeCase {
27
+ readonly name: string;
28
+ readonly description: string;
29
+ readonly sources: {
30
+ readonly stamped: Opinion | null;
31
+ readonly sessionSidecar: Opinion | null;
32
+ readonly hostAnnounced: Opinion | null;
33
+ };
34
+ readonly expect: Opinion | null;
35
+ }
36
+ export interface AppThemeCase {
37
+ readonly name: string;
38
+ readonly description: string;
39
+ readonly theme: unknown;
40
+ readonly expect: 'accepted' | 'refused';
41
+ }
42
+ export interface OverlayHashCase {
43
+ readonly name: string;
44
+ readonly description: string;
45
+ readonly input: Parameters<typeof canonicalOverlayHash>[0];
46
+ readonly expect: string;
47
+ }
48
+ export declare const THEME_MODE_CASES: readonly ThemeModeCase[];
49
+ export declare const APP_THEME_CASES: readonly AppThemeCase[];
50
+ export declare const OVERLAY_HASH_CASES: readonly OverlayHashCase[];
51
+ export interface ThemeBindingResult {
52
+ readonly name: string;
53
+ readonly pass: boolean;
54
+ readonly detail: string;
55
+ }
56
+ /** Grade every theme-binding case against the protocol's own functions. */
57
+ export declare function runThemeBindingConformance(): Promise<readonly ThemeBindingResult[]>;
58
+ export {};
59
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/theme-binding-conformance/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAkB,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAiBzE,KAAK,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAEhC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE;QAChB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;QACjC,QAAQ,CAAC,cAAc,EAAE,OAAO,GAAG,IAAI,CAAC;QACxC,QAAQ,CAAC,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC;KACxC,CAAC;IACF,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;CACzC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAkDD,eAAO,MAAM,gBAAgB,EAAE,SAAS,aAAa,EAA0F,CAAC;AAChJ,eAAO,MAAM,eAAe,EAAE,SAAS,YAAY,EAAuO,CAAC;AAC3R,eAAO,MAAM,kBAAkB,EAAE,SAAS,eAAe,EAAgD,CAAC;AAE1G,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAID,2EAA2E;AAC3E,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAqBzF"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Theme-binding conformance — a pure-function catalog (ggui#987).
3
+ *
4
+ * Three contracts the kit grades directly against `@ggui-ai/protocol`, so
5
+ * their breaking-ness is decided by a fixture and not by prose
6
+ * (VERSION-POLICY §2 — the kit is the arbiter):
7
+ *
8
+ * - `theme-mode-*` — the CLIENT projection of the `themeMode` total
9
+ * order, `effectiveThemeMode` (`integrations/theme-binding.ts`): the
10
+ * embedding host owns runtime mode (D4).
11
+ * - `app-theme-*` — which overlay shapes `appThemeSchema` accepts: both
12
+ * projections and the attestation required; the v1 one-palette wire and
13
+ * `base` refused.
14
+ * - `overlay-hash-*` — `canonicalOverlayHash` against an independently
15
+ * computed vector.
16
+ *
17
+ * History (the §1.1 receipt): `theme-mode-order`, `app-theme-v1-one-palette`
18
+ * and `app-theme-v1-base` were promoted from the unit pins at the version
19
+ * BEFORE the revision, passed there, and failed when it landed — that is
20
+ * what named the revision breaking. Like `../refusal-envelope-conformance`,
21
+ * the cases are raw JSON under `./cases/` and are NOT registered in
22
+ * `fixturesByContract`; the entry point is the runner below and its test.
23
+ */
24
+ import { appThemeSchema, canonicalOverlayHash } from '@ggui-ai/protocol';
25
+ import { effectiveThemeMode } from '@ggui-ai/protocol/integrations/theme-binding';
26
+ import appThemeV1Base from './cases/app-theme-v1-base.json' with { type: 'json' };
27
+ import appThemeV1OnePalette from './cases/app-theme-v1-one-palette.json' with { type: 'json' };
28
+ import appThemeV2Accepted from './cases/app-theme-v2-accepted.json' with { type: 'json' };
29
+ import appThemeV2MissingDark from './cases/app-theme-v2-missing-dark.json' with { type: 'json' };
30
+ import appThemeV2LabelTooLong from './cases/app-theme-v2-label-too-long.json' with { type: 'json' };
31
+ import appThemeV2KeyframesAccepted from './cases/app-theme-v2-keyframes-accepted.json' with { type: 'json' };
32
+ import appThemeV2KeyframesNotKeyframes from './cases/app-theme-v2-keyframes-not-keyframes.json' with { type: 'json' };
33
+ import appThemeV2NoHash from './cases/app-theme-v2-no-hash.json' with { type: 'json' };
34
+ import appThemeV2PlatformPin from './cases/app-theme-v2-platform-pin.json' with { type: 'json' };
35
+ import overlayHashCanonical from './cases/overlay-hash-canonical.json' with { type: 'json' };
36
+ import themeModeAbsence from './cases/theme-mode-absence.json' with { type: 'json' };
37
+ import themeModeDefaultWhenSilentHost from './cases/theme-mode-default-when-silent-host.json' with { type: 'json' };
38
+ import themeModeOrder from './cases/theme-mode-order.json' with { type: 'json' };
39
+ const isOpinion = (v) => v === 'light' || v === 'dark';
40
+ const opinionOrNull = (v, where) => {
41
+ if (v === null || isOpinion(v))
42
+ return v;
43
+ throw new Error(`theme-binding case: ${where} must be 'light' | 'dark' | null, got ${JSON.stringify(v)}`);
44
+ };
45
+ const isRecord = (v) => typeof v === 'object' && v !== null;
46
+ const isVarMap = (v) => isRecord(v) && Object.values(v).every((x) => typeof x === 'string');
47
+ function themeModeCase(raw) {
48
+ if (!isRecord(raw) || typeof raw['name'] !== 'string' || typeof raw['description'] !== 'string' || !isRecord(raw['sources']))
49
+ throw new Error('theme-binding case: malformed theme-mode case');
50
+ const src = raw['sources'];
51
+ return {
52
+ name: raw['name'],
53
+ description: raw['description'],
54
+ sources: {
55
+ stamped: opinionOrNull(src['stamped'], 'sources.stamped'),
56
+ sessionSidecar: opinionOrNull(src['sessionSidecar'], 'sources.sessionSidecar'),
57
+ hostAnnounced: opinionOrNull(src['hostAnnounced'], 'sources.hostAnnounced'),
58
+ },
59
+ expect: opinionOrNull(raw['expect'], 'expect'),
60
+ };
61
+ }
62
+ function appThemeCase(raw) {
63
+ if (!isRecord(raw) || typeof raw['name'] !== 'string' || typeof raw['description'] !== 'string' || !('theme' in raw))
64
+ throw new Error('theme-binding case: malformed app-theme case');
65
+ const expect = raw['expect'];
66
+ if (expect !== 'accepted' && expect !== 'refused')
67
+ throw new Error(`theme-binding case: expect must be accepted|refused, got ${JSON.stringify(expect)}`);
68
+ return { name: raw['name'], description: raw['description'], theme: raw['theme'], expect };
69
+ }
70
+ function overlayHashCase(raw) {
71
+ if (!isRecord(raw) || typeof raw['name'] !== 'string' || typeof raw['description'] !== 'string' || typeof raw['expect'] !== 'string' || !isRecord(raw['input']))
72
+ throw new Error('theme-binding case: malformed overlay-hash case');
73
+ const input = raw['input'];
74
+ const overlays = input['overlays'];
75
+ if (!isRecord(overlays) || !isVarMap(overlays['light']) || !isVarMap(overlays['dark']))
76
+ throw new Error('theme-binding case: overlay-hash input needs overlays.light and overlays.dark');
77
+ return {
78
+ name: raw['name'],
79
+ description: raw['description'],
80
+ input: { overlays: { light: overlays['light'], dark: overlays['dark'] } },
81
+ expect: raw['expect'],
82
+ };
83
+ }
84
+ export const THEME_MODE_CASES = [themeModeOrder, themeModeAbsence, themeModeDefaultWhenSilentHost].map(themeModeCase);
85
+ export const APP_THEME_CASES = [appThemeV1OnePalette, appThemeV1Base, appThemeV2Accepted, appThemeV2MissingDark, appThemeV2NoHash, appThemeV2PlatformPin, appThemeV2LabelTooLong, appThemeV2KeyframesAccepted, appThemeV2KeyframesNotKeyframes].map(appThemeCase);
86
+ export const OVERLAY_HASH_CASES = [overlayHashCanonical].map(overlayHashCase);
87
+ const nullable = (v) => (v === null ? undefined : v);
88
+ /** Grade every theme-binding case against the protocol's own functions. */
89
+ export async function runThemeBindingConformance() {
90
+ const results = [];
91
+ for (const c of THEME_MODE_CASES) {
92
+ const got = effectiveThemeMode({
93
+ stamped: nullable(c.sources.stamped),
94
+ sessionSidecar: nullable(c.sources.sessionSidecar),
95
+ hostAnnounced: nullable(c.sources.hostAnnounced),
96
+ });
97
+ const want = nullable(c.expect);
98
+ results.push({ name: c.name, pass: got === want, detail: `effectiveThemeMode(${JSON.stringify(c.sources)}) = ${String(got)}; expected ${String(want)}` });
99
+ }
100
+ for (const c of APP_THEME_CASES) {
101
+ const parsed = appThemeSchema.safeParse(c.theme);
102
+ const got = parsed.success ? 'accepted' : 'refused';
103
+ results.push({ name: c.name, pass: got === c.expect, detail: `appThemeSchema: ${got}; expected ${c.expect}${parsed.success ? '' : ` (${parsed.error.issues.map((i) => i.message).join('; ')})`}` });
104
+ }
105
+ for (const c of OVERLAY_HASH_CASES) {
106
+ const got = await canonicalOverlayHash(c.input);
107
+ results.push({ name: c.name, pass: got === c.expect, detail: `canonicalOverlayHash = ${got}; expected ${c.expect}` });
108
+ }
109
+ return results;
110
+ }
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "refuse-deprovisioned-endpoint",
3
- "description": "A deprovisioned app answers EVERY JSON-RPC request on its per-app MCP endpoint (initialize included) with HTTP 403 and a JSON-RPC error whose data.refusal carries the registry projection \u2014 code, message, fix, retry \u2014 so the client reads app_deprovisioned instead of guessing between a deleted app and a bad credential. error.code is -32003 (APP_NOT_FOUND \u2014 the endpoint no longer serves this app) and error.message is App not found; an untyped authorization failure answers -32001 (UNAUTHORIZED) with no data, so a client can tell the two apart by code and can tell neither from a dropped socket by mistake \u2014 -32000 is the SDK client's ConnectionClosed and a first-party server never chooses it (ggui#836).",
3
+ "description": "A deprovisioned app answers EVERY JSON-RPC request on its per-app MCP endpoint (initialize included) with HTTP 403 and a JSON-RPC error whose data.refusal carries the registry projection — code, message, fix, retry, appId — so the client reads app_deprovisioned instead of guessing between a deleted app and a bad credential. error.code is -32003 (APP_NOT_FOUND — the endpoint no longer serves this app) and error.message is App not found; an untyped authorization failure answers -32007 (UNAUTHORIZED) with no data, so a client can tell the two apart by code and can tell neither from a dropped socket by mistake — -32000 is the SDK client's ConnectionClosed and a first-party server never chooses it (ggui#836).",
4
4
  "refusal": {
5
5
  "code": "app_deprovisioned",
6
- "message": "the app record has no owner claim any more \u2014 the app was deprovisioned",
6
+ "message": "the app record has no owner claim any more — the app was deprovisioned",
7
7
  "fix": "provision the app again; this app id never renders again",
8
- "retry": "never"
8
+ "retry": "never",
9
+ "appId": "app_deprov_1"
9
10
  },
10
11
  "expect": {
11
12
  "httpStatus": 403,
@@ -15,9 +16,10 @@
15
16
  "data": {
16
17
  "refusal": {
17
18
  "code": "app_deprovisioned",
18
- "message": "the app record has no owner claim any more \u2014 the app was deprovisioned",
19
+ "message": "the app record has no owner claim any more — the app was deprovisioned",
19
20
  "fix": "provision the app again; this app id never renders again",
20
- "retry": "never"
21
+ "retry": "never",
22
+ "appId": "app_deprov_1"
21
23
  }
22
24
  }
23
25
  }
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "refuse-render-only-code",
3
- "description": "A REGISTERED code whose surfaces exclude mcp-endpoint has no transport envelope: the per-app endpoint's authorization never speaks it, and a projector that types it anyway would put a render-gate state on a surface the registry does not list. The three auth arms (no issuer, foreign issuer, denied subject) stay the bare 403 by the same rule \u2014 naming any of them would say which is true.",
3
+ "description": "A REGISTERED code whose surfaces exclude mcp-endpoint has no transport envelope: the per-app endpoint's authorization never speaks it, and a projector that types it anyway would put a render-gate state on a surface the registry does not list. The three auth arms (no issuer, foreign issuer, denied subject) stay the bare 403 by the same rule — naming any of them would say which is true.",
4
4
  "refusal": {
5
5
  "code": "insufficient_credit",
6
6
  "message": "the configured allowance for this app is exhausted",
7
7
  "fix": "top up the allowance for this app, then retry",
8
- "retry": "after-fix"
8
+ "retry": "after-fix",
9
+ "appId": "app_live_1"
9
10
  },
10
11
  "expect": null
11
12
  }
@@ -14,6 +14,8 @@ export interface TransportRefusalInput {
14
14
  readonly fix: string;
15
15
  /** Retry class (`after-fix` | `next-period` | `later` | `never`). */
16
16
  readonly retry: string;
17
+ /** The app id the refused endpoint serves — equals the path's `{appId}` (ggui#870). */
18
+ readonly appId: string;
17
19
  }
18
20
  /** What the endpoint answers: the HTTP status and the JSON-RPC error object. */
19
21
  export interface ProjectedTransportRefusal {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transport-refusal-conformance/index.ts"],"names":[],"mappings":"AAyBA;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0BAA0B;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,6BAA6B;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,gFAAgF;AAChF,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,IAAI,EAAE;YAAE,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAA;SAAE,CAAC;KAC5D,CAAC;CACH;AAED,qEAAqE;AACrE,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,wFAAwF;IACxF,QAAQ,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,CAAC;CACnD;AAED,eAAO,MAAM,qBAAqB,EAAE,SAAS,+BAA+B,EAG3E,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,yBAAyB,GAAG,IAAI,CAAC;IACpD,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,GAAG,MAAM,CAAC;CAC5D;AAED,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,SAAS,wBAAwB,EAAE,CAAC;CACtD;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,yBAAyB,GAAG,IAAI,GAC5E,iCAAiC,CAiBnC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transport-refusal-conformance/index.ts"],"names":[],"mappings":"AAyBA;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0BAA0B;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,6BAA6B;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uFAAuF;IACvF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,gFAAgF;AAChF,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,IAAI,EAAE;YAAE,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAA;SAAE,CAAC;KAC5D,CAAC;CACH;AAED,qEAAqE;AACrE,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,wFAAwF;IACxF,QAAQ,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,CAAC;CACnD;AAED,eAAO,MAAM,qBAAqB,EAAE,SAAS,+BAA+B,EAG3E,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,yBAAyB,GAAG,IAAI,CAAC;IACpD,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,GAAG,MAAM,CAAC;CAC5D;AAED,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,SAAS,wBAAwB,EAAE,CAAC;CACtD;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,yBAAyB,GAAG,IAAI,GAC5E,iCAAiC,CAiBnC"}
@@ -8,8 +8,8 @@
8
8
  * The contract it makes observable: a deprovisioned app answers HTTP 403
9
9
  * with `{ code: -32003, message: "App not found", data: { refusal } }` where
10
10
  * `refusal` is the registry projection without the render-only fields
11
- * (`code`, `message`, `fix`, `retry`); an authorization failure that is
12
- * NOT a registry state answers 403 with `-32001` and no `data` — the untyped
11
+ * (`code`, `message`, `fix`, `retry`, `appId`); an authorization failure that is
12
+ * NOT a registry state answers 403 with `-32007` and no `data` — the untyped
13
13
  * arms deliberately indistinguishable among themselves, so a client never
14
14
  * learns which one it hit (codes ruled in ggui#836).
15
15
  *
package/dist/types.d.ts CHANGED
@@ -167,7 +167,7 @@ export interface CreateGguiSessionStep {
167
167
  /** Opaque render id the host allocates. Downstream steps + the
168
168
  * fixture's `inputEnvelope` reference it by value. */
169
169
  readonly sessionId: string;
170
- /** Optional app (tenant) id the render scopes to. Defaults to the
170
+ /** Optional app id the render scopes to. Defaults to the
171
171
  * host's implementation-defined "default app". */
172
172
  readonly appId?: string;
173
173
  /**