@ggui-ai/protocol-conformance 0.8.0 → 0.9.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/dist/resource-read-conformance/cases/pin-past-the-head-answers-not-found.json +29 -0
- package/dist/resource-read-conformance/cases/pinned-superseded-record-serves-its-reign-immutably.json +42 -0
- package/dist/resource-read-conformance/index.d.ts +44 -0
- package/dist/resource-read-conformance/index.d.ts.map +1 -1
- package/dist/resource-read-conformance/index.js +128 -6
- package/package.json +2 -2
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pin-past-the-head-answers-not-found",
|
|
3
|
+
"description": "Epoch history (#483, SPEC §7.1.2.2): a pinned locator whose epoch exceeds the session's head names a record that does not exist — the same NOT_FOUND on -32002, detail-free, that a missing session answers. Nothing about the head's value may leak through the error shape: a caller must not be able to binary-search the history depth off error variance.",
|
|
4
|
+
"server": { "durableSubstrate": "all", "liveChannel": true },
|
|
5
|
+
"caller": "owner",
|
|
6
|
+
"seeds": [
|
|
7
|
+
{
|
|
8
|
+
"kind": "epoch-history",
|
|
9
|
+
"session": "7a4d9c15-8e2f-4b06-a3c9-5d1f8e0b2c47",
|
|
10
|
+
"records": [{ "props": { "count": 1 } }, { "props": { "count": 5 } }]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"reads": [
|
|
14
|
+
{
|
|
15
|
+
"as": "past-head",
|
|
16
|
+
"locator": {
|
|
17
|
+
"kind": "render",
|
|
18
|
+
"session": "7a4d9c15-8e2f-4b06-a3c9-5d1f8e0b2c47",
|
|
19
|
+
"epoch": 7
|
|
20
|
+
},
|
|
21
|
+
"expect": {
|
|
22
|
+
"kind": "typed-error",
|
|
23
|
+
"jsonRpcCode": -32002,
|
|
24
|
+
"dataCode": "NOT_FOUND",
|
|
25
|
+
"detailAbsent": true
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pinned-superseded-record-serves-its-reign-immutably",
|
|
3
|
+
"description": "Epoch history (#483, SPEC §7.1.2.2): a session whose head advanced to epoch 1 still serves its superseded epoch-0 record on the pinned locator — with the props of ITS reign (the amended mint state), never the successor's — and serves them byte-identically on a second read. The bare locator meanwhile tracks the live head. Immutability is graded on propsJson only: live-channel tokens on the same meta legitimately differ per read; the RECORD is what may not.",
|
|
4
|
+
"server": { "durableSubstrate": "all", "liveChannel": true },
|
|
5
|
+
"caller": "owner",
|
|
6
|
+
"seeds": [
|
|
7
|
+
{
|
|
8
|
+
"kind": "epoch-history",
|
|
9
|
+
"session": "5e2c7a90-4f1b-4c3d-8a67-9b0e2d4f6a81",
|
|
10
|
+
"records": [{ "props": { "count": 1 } }, { "props": { "count": 5 } }]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"reads": [
|
|
14
|
+
{
|
|
15
|
+
"as": "pin0-first",
|
|
16
|
+
"locator": {
|
|
17
|
+
"kind": "render",
|
|
18
|
+
"session": "5e2c7a90-4f1b-4c3d-8a67-9b0e2d4f6a81",
|
|
19
|
+
"epoch": 0
|
|
20
|
+
},
|
|
21
|
+
"expect": { "kind": "pinned-mount", "propsInclude": { "count": 1 } }
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"as": "pin0-second",
|
|
25
|
+
"locator": {
|
|
26
|
+
"kind": "render",
|
|
27
|
+
"session": "5e2c7a90-4f1b-4c3d-8a67-9b0e2d4f6a81",
|
|
28
|
+
"epoch": 0
|
|
29
|
+
},
|
|
30
|
+
"expect": { "kind": "pinned-mount", "propsInclude": { "count": 1 } }
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"as": "head",
|
|
34
|
+
"locator": {
|
|
35
|
+
"kind": "render",
|
|
36
|
+
"session": "5e2c7a90-4f1b-4c3d-8a67-9b0e2d4f6a81"
|
|
37
|
+
},
|
|
38
|
+
"expect": { "kind": "pinned-mount", "propsInclude": { "count": 5 } }
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"identicalMounts": ["pin0-first", "pin0-second"]
|
|
42
|
+
}
|
|
@@ -98,6 +98,19 @@ export type ResourceReadSeed = {
|
|
|
98
98
|
*/
|
|
99
99
|
readonly kind: 'registered-blueprint';
|
|
100
100
|
readonly as: string;
|
|
101
|
+
} | {
|
|
102
|
+
/**
|
|
103
|
+
* A session with epoch HISTORY (#483): `records[0]` is the mint
|
|
104
|
+
* (epoch 0), each later entry one `ggui_update` (epoch N). The
|
|
105
|
+
* driver seeds the ledger so every superseded record is
|
|
106
|
+
* reconstructable (an in-reign props event per record) and the
|
|
107
|
+
* row carries the head epoch.
|
|
108
|
+
*/
|
|
109
|
+
readonly kind: 'epoch-history';
|
|
110
|
+
readonly session: string;
|
|
111
|
+
readonly records: ReadonlyArray<{
|
|
112
|
+
readonly props: Readonly<Record<string, unknown>>;
|
|
113
|
+
}>;
|
|
101
114
|
};
|
|
102
115
|
/**
|
|
103
116
|
* The blueprint-key segment of a locator.
|
|
@@ -127,6 +140,14 @@ export type ResourceReadLocator = {
|
|
|
127
140
|
readonly kind: 'render';
|
|
128
141
|
readonly session: string;
|
|
129
142
|
readonly key?: ResourceReadKey;
|
|
143
|
+
/**
|
|
144
|
+
* Epoch pin (#483, SPEC §7.1.2.2): present ⇒ the kit reads the
|
|
145
|
+
* PINNED history record (`…#N`); absent ⇒ the live head. The
|
|
146
|
+
* fragment encoding is pinned HERE deliberately — the kit is
|
|
147
|
+
* the arbiter of the wire grammar, so an encoding change is a
|
|
148
|
+
* conscious, visible kit change.
|
|
149
|
+
*/
|
|
150
|
+
readonly epoch?: number;
|
|
130
151
|
} | {
|
|
131
152
|
readonly kind: 'raw';
|
|
132
153
|
readonly uri: string;
|
|
@@ -169,6 +190,15 @@ export type ResourceReadExpectation = {
|
|
|
169
190
|
} | {
|
|
170
191
|
/** A result whose contents declare a delivery channel. */
|
|
171
192
|
readonly kind: 'live-mount';
|
|
193
|
+
} | {
|
|
194
|
+
/**
|
|
195
|
+
* A mount serving a HISTORY record (#483): still a real mount
|
|
196
|
+
* (delivery channel required), graded additionally on its
|
|
197
|
+
* props — every `propsInclude` entry must appear in the parsed
|
|
198
|
+
* `propsJson` with the authored value.
|
|
199
|
+
*/
|
|
200
|
+
readonly kind: 'pinned-mount';
|
|
201
|
+
readonly propsInclude?: Readonly<Record<string, unknown>>;
|
|
172
202
|
};
|
|
173
203
|
/** One read within a case. */
|
|
174
204
|
export interface ResourceReadProbe {
|
|
@@ -209,6 +239,14 @@ export interface ResourceReadConformanceCase {
|
|
|
209
239
|
* not excused here.
|
|
210
240
|
*/
|
|
211
241
|
readonly indistinguishable?: readonly string[];
|
|
242
|
+
/**
|
|
243
|
+
* Probe names whose MOUNT outcomes must carry byte-identical
|
|
244
|
+
* `propsJson` (#483 — pinned-record immutability). Deliberately
|
|
245
|
+
* compares ONLY the props: live-channel tokens and expiries on the
|
|
246
|
+
* same meta legitimately differ per read; the RECORD is what may
|
|
247
|
+
* not.
|
|
248
|
+
*/
|
|
249
|
+
readonly identicalMounts?: readonly string[];
|
|
212
250
|
/**
|
|
213
251
|
* Literal substrings that MUST NOT appear anywhere in any error frame
|
|
214
252
|
* this case produces. The values a refusal would leak if a diagnostic
|
|
@@ -244,6 +282,12 @@ export interface ResourceReadRenderMeta {
|
|
|
244
282
|
readonly wsToken?: string;
|
|
245
283
|
/** A server-emitted system card, which carries its own content. */
|
|
246
284
|
readonly kind?: string;
|
|
285
|
+
/**
|
|
286
|
+
* The mount's props as a JSON string (#483). Graded by the
|
|
287
|
+
* `pinned-mount` expectation and the `identicalMounts` group;
|
|
288
|
+
* ungraded elsewhere.
|
|
289
|
+
*/
|
|
290
|
+
readonly propsJson?: string;
|
|
247
291
|
}
|
|
248
292
|
/** The raw JSON-RPC error body a server put on the wire. */
|
|
249
293
|
export interface JsonRpcErrorFrame {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resource-read-conformance/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resource-read-conformance/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -111,6 +111,8 @@ import malformedLocator from './cases/malformed-locator-stays-outside-the-typed-
|
|
|
111
111
|
import liveRowMount from './cases/live-row-read-returns-mount-material.json' with { type: 'json' };
|
|
112
112
|
import remintMount from './cases/evicted-row-remints-from-the-durable-record.json' with { type: 'json' };
|
|
113
113
|
import inlineMount from './cases/channel-less-read-mounts-inline.json' with { type: 'json' };
|
|
114
|
+
import pinnedImmutable from './cases/pinned-superseded-record-serves-its-reign-immutably.json' with { type: 'json' };
|
|
115
|
+
import pinPastHead from './cases/pin-past-the-head-answers-not-found.json' with { type: 'json' };
|
|
114
116
|
// =============================================================================
|
|
115
117
|
// Catalog
|
|
116
118
|
// =============================================================================
|
|
@@ -134,6 +136,8 @@ export const resourceReadCases = [
|
|
|
134
136
|
liveRowMount,
|
|
135
137
|
remintMount,
|
|
136
138
|
inlineMount,
|
|
139
|
+
pinnedImmutable,
|
|
140
|
+
pinPastHead,
|
|
137
141
|
];
|
|
138
142
|
// =============================================================================
|
|
139
143
|
// Locator grammar + the mount predicate
|
|
@@ -148,17 +152,18 @@ export const GGUI_RENDER_RESOURCE_URI = 'ui://ggui/render';
|
|
|
148
152
|
export function renderLocatorUri(locator, registeredKeys = {}) {
|
|
149
153
|
if (locator.kind === 'raw')
|
|
150
154
|
return locator.uri;
|
|
155
|
+
const pin = locator.epoch === undefined ? '' : `#${locator.epoch}`;
|
|
151
156
|
const base = `${GGUI_RENDER_RESOURCE_URI}/${locator.session}`;
|
|
152
157
|
const keyRef = locator.key;
|
|
153
158
|
if (keyRef === undefined)
|
|
154
|
-
return base
|
|
159
|
+
return `${base}${pin}`;
|
|
155
160
|
if (keyRef.kind === 'literal')
|
|
156
|
-
return `${base}/${keyRef.value}`;
|
|
161
|
+
return `${base}/${keyRef.value}${pin}`;
|
|
157
162
|
const key = registeredKeys[keyRef.seed];
|
|
158
163
|
if (key === undefined) {
|
|
159
164
|
throw new Error(`no key was reported for registry seed '${keyRef.seed}'`);
|
|
160
165
|
}
|
|
161
|
-
return `${base}/${key}`;
|
|
166
|
+
return `${base}/${key}${pin}`;
|
|
162
167
|
}
|
|
163
168
|
/**
|
|
164
169
|
* Does this render meta carry enough to paint something? One of the
|
|
@@ -289,6 +294,9 @@ async function gradeCase(testCase, prepared) {
|
|
|
289
294
|
const fusion = gradeIndistinguishable(testCase, outcomes);
|
|
290
295
|
if (fusion !== null)
|
|
291
296
|
return { kind: 'fail', failure: fusion };
|
|
297
|
+
const pinned = gradeIdenticalMounts(testCase, outcomes);
|
|
298
|
+
if (pinned !== null)
|
|
299
|
+
return { kind: 'fail', failure: pinned };
|
|
292
300
|
return { kind: 'pass' };
|
|
293
301
|
}
|
|
294
302
|
function gradeProbe(testCase, probe, uri, outcome) {
|
|
@@ -309,6 +317,38 @@ function gradeProbe(testCase, probe, uri, outcome) {
|
|
|
309
317
|
}
|
|
310
318
|
return null;
|
|
311
319
|
}
|
|
320
|
+
if (probe.expect.kind === 'pinned-mount') {
|
|
321
|
+
if (outcome.kind !== 'mount') {
|
|
322
|
+
return fail('a pinned history record within the servable window mounts (SPEC §7.1.2.2)', 'a result whose contents declare a delivery channel and carry the record props', `reading '${uri}' failed, but this pinned record resolves on this server.`);
|
|
323
|
+
}
|
|
324
|
+
if (!declaresDeliveryChannel(outcome.renderMeta)) {
|
|
325
|
+
return fail('a pinned mount is still a mount', 'render meta declaring a delivery channel', `reading '${uri}' succeeded with contents that declare no delivery channel.`);
|
|
326
|
+
}
|
|
327
|
+
const include = probe.expect.propsInclude;
|
|
328
|
+
if (include !== undefined) {
|
|
329
|
+
const raw = outcome.renderMeta.propsJson;
|
|
330
|
+
let parsed = null;
|
|
331
|
+
if (typeof raw === 'string') {
|
|
332
|
+
try {
|
|
333
|
+
const candidate = JSON.parse(raw);
|
|
334
|
+
if (isRecord(candidate))
|
|
335
|
+
parsed = candidate;
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
parsed = null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (parsed === null) {
|
|
342
|
+
return fail('a pinned mount carries its record props (SPEC §7.1.2.2)', 'parseable propsJson on the render meta', `reading '${uri}' mounted, but its render meta carries no parseable propsJson to grade the record against.`);
|
|
343
|
+
}
|
|
344
|
+
for (const [key, expected] of Object.entries(include)) {
|
|
345
|
+
if (JSON.stringify(parsed[key]) !== JSON.stringify(expected)) {
|
|
346
|
+
return fail('a pinned record serves the props of ITS epoch, never a neighbor\'s', `propsJson entry '${key}' = ${JSON.stringify(expected)}`, `reading '${uri}' served '${key}' = ${JSON.stringify(parsed[key])}.`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
312
352
|
if (outcome.kind !== 'error') {
|
|
313
353
|
return fail('a read that cannot be mounted answers with a typed error, never a success-shaped result', probe.expect, `reading '${uri}' returned a result, but this locator cannot be mounted on this server.`);
|
|
314
354
|
}
|
|
@@ -376,6 +416,40 @@ function gradeDisclosure(testCase, outcomes) {
|
|
|
376
416
|
}
|
|
377
417
|
return null;
|
|
378
418
|
}
|
|
419
|
+
function gradeIdenticalMounts(testCase, outcomes) {
|
|
420
|
+
const group = testCase.identicalMounts ?? [];
|
|
421
|
+
if (group.length < 2)
|
|
422
|
+
return null;
|
|
423
|
+
let reference = null;
|
|
424
|
+
for (const probeName of group) {
|
|
425
|
+
const outcome = outcomes.get(probeName);
|
|
426
|
+
if (outcome === undefined || outcome.kind !== 'mount' || outcome.renderMeta.propsJson === undefined) {
|
|
427
|
+
return {
|
|
428
|
+
name: testCase.name,
|
|
429
|
+
probe: probeName,
|
|
430
|
+
obligation: 'a pinned history record serves identical props on every read (SPEC §7.1.2.2)',
|
|
431
|
+
expected: 'a mount outcome carrying propsJson to compare',
|
|
432
|
+
actual: outcome ?? '(no outcome recorded)',
|
|
433
|
+
message: `probe '${probeName}' is named in this case's identicalMounts group but did not produce a props-carrying mount, so immutability cannot even be compared.`,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
if (reference === null) {
|
|
437
|
+
reference = { probe: probeName, propsJson: outcome.renderMeta.propsJson };
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (outcome.renderMeta.propsJson !== reference.propsJson) {
|
|
441
|
+
return {
|
|
442
|
+
name: testCase.name,
|
|
443
|
+
probe: probeName,
|
|
444
|
+
obligation: 'a pinned history record serves identical props on every read (SPEC §7.1.2.2)',
|
|
445
|
+
expected: reference.propsJson,
|
|
446
|
+
actual: outcome.renderMeta.propsJson,
|
|
447
|
+
message: `probes '${reference.probe}' and '${probeName}' read the same pinned record and got different props — the record mutated between reads.`,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
379
453
|
function gradeIndistinguishable(testCase, outcomes) {
|
|
380
454
|
const group = testCase.indistinguishable ?? [];
|
|
381
455
|
if (group.length < 2)
|
|
@@ -531,6 +605,14 @@ export function parseCase(input) {
|
|
|
531
605
|
probeNames.add(parsed.as);
|
|
532
606
|
return parsed;
|
|
533
607
|
});
|
|
608
|
+
const mountGroup = input['identicalMounts'];
|
|
609
|
+
if (mountGroup !== undefined) {
|
|
610
|
+
if (!Array.isArray(mountGroup))
|
|
611
|
+
throw bad("'identicalMounts' must be an array of probe names");
|
|
612
|
+
if (mountGroup.length < 2) {
|
|
613
|
+
throw bad("'identicalMounts' names a single probe — a group of one asserts nothing, which reads as an obligation being graded when it is not");
|
|
614
|
+
}
|
|
615
|
+
}
|
|
534
616
|
const group = input['indistinguishable'];
|
|
535
617
|
if (group !== undefined) {
|
|
536
618
|
if (!Array.isArray(group))
|
|
@@ -565,6 +647,7 @@ export function parseCase(input) {
|
|
|
565
647
|
seeds,
|
|
566
648
|
reads,
|
|
567
649
|
...(group !== undefined ? { indistinguishable: group.map(String) } : {}),
|
|
650
|
+
...(mountGroup !== undefined ? { identicalMounts: mountGroup.map(String) } : {}),
|
|
568
651
|
...(secrets !== undefined ? { disclosesNothing: secrets.map(String) } : {}),
|
|
569
652
|
};
|
|
570
653
|
}
|
|
@@ -593,6 +676,7 @@ const CASE_KEYS = [
|
|
|
593
676
|
'seeds',
|
|
594
677
|
'reads',
|
|
595
678
|
'indistinguishable',
|
|
679
|
+
'identicalMounts',
|
|
596
680
|
'disclosesNothing',
|
|
597
681
|
];
|
|
598
682
|
const SERVER_KEYS = [
|
|
@@ -606,11 +690,12 @@ const SEED_KEYS = {
|
|
|
606
690
|
'identity-record': ['kind', 'session', 'key', 'blueprint'],
|
|
607
691
|
'durable-blueprint': ['kind', 'componentRef', 'body'],
|
|
608
692
|
'committed-render': ['kind', 'session', 'size'],
|
|
693
|
+
'epoch-history': ['kind', 'session', 'records'],
|
|
609
694
|
'uncommitted-render': ['kind', 'session'],
|
|
610
695
|
'registered-blueprint': ['kind', 'as'],
|
|
611
696
|
};
|
|
612
697
|
const LOCATOR_KEYS = {
|
|
613
|
-
render: ['kind', 'session', 'key'],
|
|
698
|
+
render: ['kind', 'session', 'key', 'epoch'],
|
|
614
699
|
raw: ['kind', 'uri'],
|
|
615
700
|
};
|
|
616
701
|
const KEY_KEYS = {
|
|
@@ -621,6 +706,7 @@ const EXPECTATION_KEYS = {
|
|
|
621
706
|
'typed-error': ['kind', 'jsonRpcCode', 'dataCode', 'detailAbsent'],
|
|
622
707
|
'outside-typed-set': ['kind'],
|
|
623
708
|
'live-mount': ['kind'],
|
|
709
|
+
'pinned-mount': ['kind', 'propsInclude'],
|
|
624
710
|
};
|
|
625
711
|
function parseSubstrate(bad, value) {
|
|
626
712
|
switch (value) {
|
|
@@ -686,6 +772,21 @@ function parseSeed(bad, seed) {
|
|
|
686
772
|
kind: 'uncommitted-render',
|
|
687
773
|
session: requireText(bad, seed['session'], "an uncommitted-render seed's 'session'"),
|
|
688
774
|
};
|
|
775
|
+
case 'epoch-history': {
|
|
776
|
+
const session = requireText(bad, seed['session'], "epoch-history seed 'session'");
|
|
777
|
+
const rawRecords = seed['records'];
|
|
778
|
+
if (!Array.isArray(rawRecords) || rawRecords.length < 2) {
|
|
779
|
+
throw bad("epoch-history seed 'records' must be an array of at least TWO entries — one mint plus one update; a single-record history has nothing superseded to pin");
|
|
780
|
+
}
|
|
781
|
+
const records = rawRecords.map((entry, i) => {
|
|
782
|
+
if (!isRecord(entry) || !isRecord(entry['props'])) {
|
|
783
|
+
throw bad(`epoch-history seed record ${i} authors no 'props' object`);
|
|
784
|
+
}
|
|
785
|
+
rejectUnknownKeys(bad, entry, ['props'], `epoch-history seed record ${i}`);
|
|
786
|
+
return { props: entry['props'] };
|
|
787
|
+
});
|
|
788
|
+
return { kind: 'epoch-history', session, records };
|
|
789
|
+
}
|
|
689
790
|
case 'registered-blueprint':
|
|
690
791
|
return {
|
|
691
792
|
kind: 'registered-blueprint',
|
|
@@ -716,10 +817,19 @@ function parseLocator(bad, as, locator) {
|
|
|
716
817
|
switch (locator['kind']) {
|
|
717
818
|
case 'render': {
|
|
718
819
|
const session = requireText(bad, locator['session'], `probe '${as}' locator 'session'`);
|
|
820
|
+
const rawEpoch = locator['epoch'];
|
|
821
|
+
let epoch;
|
|
822
|
+
if (rawEpoch !== undefined) {
|
|
823
|
+
if (typeof rawEpoch !== 'number' || !Number.isInteger(rawEpoch) || rawEpoch < 0) {
|
|
824
|
+
throw bad(`probe '${as}' locator 'epoch' must be a non-negative integer`);
|
|
825
|
+
}
|
|
826
|
+
epoch = rawEpoch;
|
|
827
|
+
}
|
|
719
828
|
const key = locator['key'];
|
|
829
|
+
const base = { kind: 'render', session, ...(epoch !== undefined ? { epoch } : {}) };
|
|
720
830
|
if (key === undefined)
|
|
721
|
-
return
|
|
722
|
-
return {
|
|
831
|
+
return base;
|
|
832
|
+
return { ...base, key: parseKey(bad, as, key) };
|
|
723
833
|
}
|
|
724
834
|
case 'raw':
|
|
725
835
|
return { kind: 'raw', uri: requireText(bad, locator['uri'], `probe '${as}' locator 'uri'`) };
|
|
@@ -755,6 +865,18 @@ function parseExpectation(bad, as, expect) {
|
|
|
755
865
|
switch (expect['kind']) {
|
|
756
866
|
case 'outside-typed-set':
|
|
757
867
|
return { kind: 'outside-typed-set' };
|
|
868
|
+
case 'pinned-mount': {
|
|
869
|
+
const rawInclude = expect['propsInclude'];
|
|
870
|
+
if (rawInclude !== undefined && !isRecord(rawInclude)) {
|
|
871
|
+
throw bad(`probe '${as}' 'propsInclude' must be an object of expected prop entries`);
|
|
872
|
+
}
|
|
873
|
+
return {
|
|
874
|
+
kind: 'pinned-mount',
|
|
875
|
+
...(rawInclude !== undefined
|
|
876
|
+
? { propsInclude: rawInclude }
|
|
877
|
+
: {}),
|
|
878
|
+
};
|
|
879
|
+
}
|
|
758
880
|
case 'live-mount':
|
|
759
881
|
return { kind: 'live-mount' };
|
|
760
882
|
case 'typed-error': {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/protocol-conformance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Conformance test kit for the ggui protocol. Drives a behavioral fixture catalog against a live implementation over WebSocket, a resources/read catalog against a live MCP server, and four pure-function catalogs (schema, registration, resolution, binding) for the gadget and tool-binding obligations, reporting a scorecard-style pass / fail / skip. MCP builders adopt this kit to prove their implementation is protocol-conformant.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"ws": "^8.20.1",
|
|
62
|
-
"@ggui-ai/protocol": "0.
|
|
62
|
+
"@ggui-ai/protocol": "0.9.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@types/node": "^22.0.0",
|