@ggui-ai/protocol-conformance 0.1.0-rc.1
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/LICENSE +201 -0
- package/README.md +89 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +193 -0
- package/dist/conformance-host.d.ts +155 -0
- package/dist/conformance-host.d.ts.map +1 -0
- package/dist/conformance-host.js +45 -0
- package/dist/fixtures/bootstrap-protocol/bootstrap-bundle-fetch-failed.json +26 -0
- package/dist/fixtures/bootstrap-protocol/bootstrap-meta-missing.json +32 -0
- package/dist/fixtures/bootstrap-protocol/bootstrap-success.json +17 -0
- package/dist/fixtures/bootstrap-protocol/index.d.ts +5 -0
- package/dist/fixtures/bootstrap-protocol/index.d.ts.map +1 -0
- package/dist/fixtures/bootstrap-protocol/index.js +24 -0
- package/dist/fixtures/canvas-mode-wire-shapes/canvas-bootstrap-mutual-exclusion.json +25 -0
- package/dist/fixtures/canvas-mode-wire-shapes/canvas-lifecycle-channel-emits-handshake-started.json +30 -0
- package/dist/fixtures/canvas-mode-wire-shapes/canvas-navigated-updates-active-stack-item.json +30 -0
- package/dist/fixtures/canvas-mode-wire-shapes/host-context-observed-persists.json +22 -0
- package/dist/fixtures/canvas-mode-wire-shapes/index.d.ts +5 -0
- package/dist/fixtures/canvas-mode-wire-shapes/index.d.ts.map +1 -0
- package/dist/fixtures/canvas-mode-wire-shapes/index.js +47 -0
- package/dist/fixtures/index.d.ts +78 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/index.js +87 -0
- package/dist/fixtures/observability-events/index.d.ts +5 -0
- package/dist/fixtures/observability-events/index.d.ts.map +1 -0
- package/dist/fixtures/observability-events/index.js +22 -0
- package/dist/fixtures/observability-events/observability-contract-error-emitted.json +29 -0
- package/dist/fixtures/observability-events/observability-wired-tool-invoked.json +28 -0
- package/dist/fixtures/refresh-semantics/index.d.ts +5 -0
- package/dist/fixtures/refresh-semantics/index.d.ts.map +1 -0
- package/dist/fixtures/refresh-semantics/index.js +19 -0
- package/dist/fixtures/refresh-semantics/stream-refresh-success.json +40 -0
- package/dist/fixtures/reserved-channel-authority/index.d.ts +4 -0
- package/dist/fixtures/reserved-channel-authority/index.d.ts.map +1 -0
- package/dist/fixtures/reserved-channel-authority/index.js +21 -0
- package/dist/fixtures/reserved-channel-authority/props-update-roundtrip.json +34 -0
- package/dist/fixtures/reserved-channel-authority/stream-schema-violation.json +45 -0
- package/dist/fixtures/schema-version-handshake/index.d.ts +4 -0
- package/dist/fixtures/schema-version-handshake/index.d.ts.map +1 -0
- package/dist/fixtures/schema-version-handshake/index.js +20 -0
- package/dist/fixtures/schema-version-handshake/version-match.json +16 -0
- package/dist/fixtures/schema-version-handshake/version-mismatch.json +26 -0
- package/dist/fixtures/wired-action-dispatch/index.d.ts +5 -0
- package/dist/fixtures/wired-action-dispatch/index.d.ts.map +1 -0
- package/dist/fixtures/wired-action-dispatch/index.js +29 -0
- package/dist/fixtures/wired-action-dispatch/wired-action-success.json +36 -0
- package/dist/fixtures/wired-action-dispatch/wired-action-tool-not-found.json +31 -0
- package/dist/fixtures/wired-action-dispatch/wired-action-tool-threw.json +32 -0
- package/dist/fixtures/wired-action-dispatch/wired-action-tool-timeout.json +32 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/loader.d.ts +19 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +46 -0
- package/dist/match-behavior.d.ts +121 -0
- package/dist/match-behavior.d.ts.map +1 -0
- package/dist/match-behavior.js +385 -0
- package/dist/registration-conformance/cases/gate-export-not-registered.json +19 -0
- package/dist/registration-conformance/cases/gate-no-gadgets-accept.json +13 -0
- package/dist/registration-conformance/cases/gate-package-mismatch.json +19 -0
- package/dist/registration-conformance/cases/gate-priority-not-registered-outranks-mismatch.json +20 -0
- package/dist/registration-conformance/cases/gate-registered-resolves-accept.json +25 -0
- package/dist/registration-conformance/cases/gate-stdlib-hook-accept.json +27 -0
- package/dist/registration-conformance/cases/gate-typo-classifies-as-not-registered.json +27 -0
- package/dist/registration-conformance/index.d.ts +114 -0
- package/dist/registration-conformance/index.d.ts.map +1 -0
- package/dist/registration-conformance/index.js +63 -0
- package/dist/reporter.d.ts +32 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +151 -0
- package/dist/resolution-conformance/cases/resolve-bundle-host-computed.json +13 -0
- package/dist/resolution-conformance/cases/resolve-default-host.json +12 -0
- package/dist/resolution-conformance/cases/resolve-explicit-bundle-and-style-url.json +14 -0
- package/dist/resolution-conformance/cases/resolve-explicit-bundle-url.json +12 -0
- package/dist/resolution-conformance/cases/resolve-loopback-host-http-scheme.json +13 -0
- package/dist/resolution-conformance/cases/resolve-package-only-no-version.json +8 -0
- package/dist/resolution-conformance/cases/resolve-stdlib-no-urls.json +10 -0
- package/dist/resolution-conformance/index.d.ts +76 -0
- package/dist/resolution-conformance/index.d.ts.map +1 -0
- package/dist/resolution-conformance/index.js +81 -0
- package/dist/run-conformance.d.ts +58 -0
- package/dist/run-conformance.d.ts.map +1 -0
- package/dist/run-conformance.js +286 -0
- package/dist/schema-conformance/cases/wire-bad-export-name-reject.json +11 -0
- package/dist/schema-conformance/cases/wire-bad-package-name-reject.json +11 -0
- package/dist/schema-conformance/cases/wire-component-export-accept.json +10 -0
- package/dist/schema-conformance/cases/wire-empty-package-reject.json +11 -0
- package/dist/schema-conformance/cases/wire-minimal-accept.json +10 -0
- package/dist/schema-conformance/cases/wire-multi-package-accept.json +14 -0
- package/dist/schema-conformance/cases/wire-prose-accept.json +15 -0
- package/dist/schema-conformance/cases/wire-registry-metadata-reject.json +13 -0
- package/dist/schema-conformance/cases/wire-stale-libraries-reject.json +14 -0
- package/dist/schema-conformance/cases/wire-stdlib-accept.json +10 -0
- package/dist/schema-conformance/cases/wire-transport-field-reject.json +15 -0
- package/dist/schema-conformance/cases/wire-version-field-reject.json +13 -0
- package/dist/schema-conformance/index.d.ts +71 -0
- package/dist/schema-conformance/index.d.ts.map +1 -0
- package/dist/schema-conformance/index.js +103 -0
- package/dist/types.d.ts +434 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +62 -0
- package/dist/ws-transport.d.ts +42 -0
- package/dist/ws-transport.d.ts.map +1 -0
- package/dist/ws-transport.js +135 -0
- package/package.json +83 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ConformanceHost } from './conformance-host.js';
|
|
2
|
+
import type { AuthConfig, ContractErrorBehavior, StreamUpdateBehavior, VersionMismatchBehavior } from './types.js';
|
|
3
|
+
export interface RunConformanceConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Base URL of the implementation under test. The runner appends
|
|
6
|
+
* `/ws` to reach the live-channel endpoint; provide `http://host:port`
|
|
7
|
+
* or `https://…` and the runner derives `ws://` / `wss://`.
|
|
8
|
+
*/
|
|
9
|
+
readonly serverUrl: string;
|
|
10
|
+
/** Auth carried on the WS upgrade. See {@link AuthConfig}. */
|
|
11
|
+
readonly auth: AuthConfig;
|
|
12
|
+
/** Optional abort signal — cancels the run between fixtures. */
|
|
13
|
+
readonly signal?: AbortSignal;
|
|
14
|
+
/** Filter fixtures by name. Useful for debugging. */
|
|
15
|
+
readonly only?: readonly string[];
|
|
16
|
+
/** Reporter callbacks. Default is a silent reporter. */
|
|
17
|
+
readonly reporter?: ConformanceReporter;
|
|
18
|
+
/**
|
|
19
|
+
* Conformance host the runner dispatches setup/teardown against.
|
|
20
|
+
* Fixtures with non-empty `setup` skip if `host` is undefined.
|
|
21
|
+
*/
|
|
22
|
+
readonly host?: ConformanceHost;
|
|
23
|
+
/** Per-fixture observation window. Default 2000ms. */
|
|
24
|
+
readonly observationTimeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface ConformanceResult {
|
|
27
|
+
readonly passed: readonly string[];
|
|
28
|
+
readonly failed: readonly ConformanceFailure[];
|
|
29
|
+
readonly skipped: readonly SkippedFixture[];
|
|
30
|
+
readonly totalMs: number;
|
|
31
|
+
}
|
|
32
|
+
export interface ConformanceFailure {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly criterion: string;
|
|
35
|
+
readonly expected: unknown;
|
|
36
|
+
readonly received: unknown;
|
|
37
|
+
readonly message: string;
|
|
38
|
+
}
|
|
39
|
+
export interface SkippedFixture {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly reason: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ConformanceReporter {
|
|
44
|
+
onStart?(totalFixtures: number): void;
|
|
45
|
+
onFixturePass?(name: string, elapsedMs: number): void;
|
|
46
|
+
onFixtureFail?(failure: ConformanceFailure): void;
|
|
47
|
+
onFixtureSkip?(name: string, reason: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* Invoked when a teardown step throws. Non-fatal — the fixture's
|
|
50
|
+
* pass/fail verdict is NOT affected. Surfaces cleanup gaps for
|
|
51
|
+
* operator attention.
|
|
52
|
+
*/
|
|
53
|
+
onTeardownWarning?(name: string, message: string): void;
|
|
54
|
+
onComplete?(result: ConformanceResult): void;
|
|
55
|
+
}
|
|
56
|
+
export declare function runConformance(config: RunConformanceConfig): Promise<ConformanceResult>;
|
|
57
|
+
export type { ContractErrorBehavior, StreamUpdateBehavior, VersionMismatchBehavior };
|
|
58
|
+
//# sourceMappingURL=run-conformance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-conformance.d.ts","sourceRoot":"","sources":["../src/run-conformance.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,OAAO,KAAK,EACV,UAAU,EACV,qBAAqB,EAErB,oBAAoB,EAGpB,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAOpB,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,qDAAqD;IACrD,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IACxC;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC;IAChC,sDAAsD;IACtD,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC/C,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,aAAa,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACtD,aAAa,CAAC,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAClD,aAAa,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACnD;;;;OAIG;IACH,iBAAiB,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACxD,UAAU,CAAC,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;CAC9C;AAMD,wBAAsB,cAAc,CAClC,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,iBAAiB,CAAC,CA6C5B;AAyPD,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,CAAC"}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runConformance()` — drive every fixture in the catalog against a
|
|
3
|
+
* live implementation and return a pass / fail / skip scorecard.
|
|
4
|
+
*
|
|
5
|
+
* Orchestration:
|
|
6
|
+
* 1. Invoke `reporter.onStart(totalFixtures)`.
|
|
7
|
+
* 2. For each fixture (filtered by `config.only` if provided):
|
|
8
|
+
* a. If `fixture.skipReason !== null` → skip with the reason.
|
|
9
|
+
* b. If `fixture.setup` non-empty AND `config.host` absent →
|
|
10
|
+
* skip with "no host provided".
|
|
11
|
+
* c. Dispatch every setup step via `host.dispatchSetup()`.
|
|
12
|
+
* Throw → skip with the error message (NOT a fail).
|
|
13
|
+
* d. Open a WS transport against `config.serverUrl`.
|
|
14
|
+
* e. Send the canonical `subscribe` frame (the runner knows the
|
|
15
|
+
* wire shape — fixture's `inputEnvelope` is NOT the subscribe
|
|
16
|
+
* frame; subscribe is always runner-owned).
|
|
17
|
+
* f. If the fixture's `inputEnvelope.type === 'action'`, send
|
|
18
|
+
* it as a live-channel action frame AFTER subscribe.
|
|
19
|
+
* g. Observe frames for `config.observationTimeoutMs` (default
|
|
20
|
+
* 2000ms).
|
|
21
|
+
* h. Match observed frames against `fixture.expectedBehavior`.
|
|
22
|
+
* i. Close the WS.
|
|
23
|
+
* j. Dispatch every teardown step via `host.dispatchTeardown()`.
|
|
24
|
+
* Throw → reporter warning; does NOT flip pass → fail.
|
|
25
|
+
* k. Record the outcome via reporter + accumulate in result.
|
|
26
|
+
* 3. Invoke `reporter.onComplete(result)` + return.
|
|
27
|
+
*
|
|
28
|
+
* Scope v1: pure WS transport. Browser-level fixtures (bootstrap-
|
|
29
|
+
* failure, props-update, observability-event) automatically skip via
|
|
30
|
+
* the matcher's `unmatchable-on-ws` arm.
|
|
31
|
+
*/
|
|
32
|
+
import { allFixtures, fixturesByContract } from './fixtures/index.js';
|
|
33
|
+
import { matchBehavior } from './match-behavior.js';
|
|
34
|
+
import { openWsTransport } from './ws-transport.js';
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// Runner
|
|
37
|
+
// =============================================================================
|
|
38
|
+
export async function runConformance(config) {
|
|
39
|
+
const started = Date.now();
|
|
40
|
+
const fixtures = filterFixtures(config.only);
|
|
41
|
+
const reporter = config.reporter ?? {};
|
|
42
|
+
reporter.onStart?.(fixtures.length);
|
|
43
|
+
const passed = [];
|
|
44
|
+
const failed = [];
|
|
45
|
+
const skipped = [];
|
|
46
|
+
for (const fixture of fixtures) {
|
|
47
|
+
if (config.signal?.aborted === true) {
|
|
48
|
+
skipped.push({
|
|
49
|
+
name: fixture.name,
|
|
50
|
+
reason: 'aborted by signal before dispatch',
|
|
51
|
+
});
|
|
52
|
+
reporter.onFixtureSkip?.(fixture.name, 'aborted by signal before dispatch');
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const outcome = await runOneFixture(fixture, config, reporter);
|
|
56
|
+
switch (outcome.kind) {
|
|
57
|
+
case 'pass':
|
|
58
|
+
passed.push(fixture.name);
|
|
59
|
+
reporter.onFixturePass?.(fixture.name, outcome.elapsedMs);
|
|
60
|
+
break;
|
|
61
|
+
case 'fail':
|
|
62
|
+
failed.push(outcome.failure);
|
|
63
|
+
reporter.onFixtureFail?.(outcome.failure);
|
|
64
|
+
break;
|
|
65
|
+
case 'skip':
|
|
66
|
+
skipped.push({ name: fixture.name, reason: outcome.reason });
|
|
67
|
+
reporter.onFixtureSkip?.(fixture.name, outcome.reason);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const result = {
|
|
72
|
+
passed,
|
|
73
|
+
failed,
|
|
74
|
+
skipped,
|
|
75
|
+
totalMs: Date.now() - started,
|
|
76
|
+
};
|
|
77
|
+
reporter.onComplete?.(result);
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
async function runOneFixture(fixture, config, _reporter) {
|
|
81
|
+
const fixtureStarted = Date.now();
|
|
82
|
+
if (fixture.skipReason !== null) {
|
|
83
|
+
return { kind: 'skip', reason: fixture.skipReason };
|
|
84
|
+
}
|
|
85
|
+
if (fixture.setup.length > 0 && config.host === undefined) {
|
|
86
|
+
return {
|
|
87
|
+
kind: 'skip',
|
|
88
|
+
reason: `fixture has ${fixture.setup.length} setup step(s) and no \`host\` was provided — supply a ConformanceHost to drive setup directives.`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Dispatch setup via host. A throw means "host doesn't implement
|
|
92
|
+
// this directive" — record as skip, NOT fail.
|
|
93
|
+
if (config.host !== undefined) {
|
|
94
|
+
for (const step of fixture.setup) {
|
|
95
|
+
try {
|
|
96
|
+
await config.host.dispatchSetup(narrowSetupStep(step));
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
return {
|
|
100
|
+
kind: 'skip',
|
|
101
|
+
reason: `host refused setup step type='${step.type ?? 'unknown'}': ${String(err.message ?? err)}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Open WS + subscribe + dispatch + observe.
|
|
107
|
+
let transport = null;
|
|
108
|
+
try {
|
|
109
|
+
const wsUrl = deriveWsUrl(config.serverUrl);
|
|
110
|
+
transport = await openWsTransport({ kind: 'ws', url: wsUrl, auth: config.auth });
|
|
111
|
+
const sessionId = extractSessionId(fixture);
|
|
112
|
+
transport.send({
|
|
113
|
+
type: 'subscribe',
|
|
114
|
+
payload: {
|
|
115
|
+
sessionId,
|
|
116
|
+
appId: 'conformance',
|
|
117
|
+
role: 'user',
|
|
118
|
+
...(maybeSupportedVersions(fixture.expectedBehavior) ?? {}),
|
|
119
|
+
},
|
|
120
|
+
requestId: `conformance-subscribe-${fixture.name}`,
|
|
121
|
+
});
|
|
122
|
+
if (shouldDispatchInputEnvelope(fixture)) {
|
|
123
|
+
transport.send(fixture.inputEnvelope);
|
|
124
|
+
}
|
|
125
|
+
const frames = await transport.observe({
|
|
126
|
+
timeoutMs: config.observationTimeoutMs ?? 2000,
|
|
127
|
+
});
|
|
128
|
+
const match = matchBehavior(fixture.expectedBehavior, frames);
|
|
129
|
+
// Dispatch teardown regardless of match outcome. Failures in
|
|
130
|
+
// teardown surface as warnings; they do not flip pass → fail.
|
|
131
|
+
if (config.host !== undefined) {
|
|
132
|
+
for (const step of fixture.teardown) {
|
|
133
|
+
try {
|
|
134
|
+
await config.host.dispatchTeardown(narrowTeardownStep(step));
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
_reporter.onTeardownWarning?.(fixture.name, `teardown step type='${step.type ?? 'unknown'}' threw: ${String(err.message ?? err)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (match.kind === 'pass') {
|
|
142
|
+
return { kind: 'pass', elapsedMs: Date.now() - fixtureStarted };
|
|
143
|
+
}
|
|
144
|
+
if (match.kind === 'fail') {
|
|
145
|
+
return {
|
|
146
|
+
kind: 'fail',
|
|
147
|
+
failure: {
|
|
148
|
+
name: fixture.name,
|
|
149
|
+
criterion: criterionForFixture(fixture.name),
|
|
150
|
+
expected: match.expected,
|
|
151
|
+
received: match.received,
|
|
152
|
+
message: match.message,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// match.kind === 'unmatchable-on-ws'
|
|
157
|
+
return { kind: 'skip', reason: match.reason };
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
return {
|
|
161
|
+
kind: 'fail',
|
|
162
|
+
failure: {
|
|
163
|
+
name: fixture.name,
|
|
164
|
+
criterion: criterionForFixture(fixture.name),
|
|
165
|
+
expected: 'transport reachable + fixture driveable',
|
|
166
|
+
received: String(err.message ?? err),
|
|
167
|
+
message: `runner-side error dispatching fixture: ${String(err.message ?? err)}`,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
if (transport !== null)
|
|
173
|
+
await transport.close();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// Helpers
|
|
178
|
+
// =============================================================================
|
|
179
|
+
function filterFixtures(only) {
|
|
180
|
+
if (only === undefined || only.length === 0)
|
|
181
|
+
return allFixtures;
|
|
182
|
+
const allow = new Set(only);
|
|
183
|
+
return allFixtures.filter((fixture) => allow.has(fixture.name));
|
|
184
|
+
}
|
|
185
|
+
function deriveWsUrl(serverUrl) {
|
|
186
|
+
const trimmed = serverUrl.replace(/\/+$/, '');
|
|
187
|
+
if (trimmed.startsWith('ws://') || trimmed.startsWith('wss://')) {
|
|
188
|
+
return `${trimmed}/ws`;
|
|
189
|
+
}
|
|
190
|
+
if (trimmed.startsWith('https://'))
|
|
191
|
+
return `wss://${trimmed.slice('https://'.length)}/ws`;
|
|
192
|
+
if (trimmed.startsWith('http://'))
|
|
193
|
+
return `ws://${trimmed.slice('http://'.length)}/ws`;
|
|
194
|
+
// No scheme provided — assume plain ws.
|
|
195
|
+
return `ws://${trimmed}/ws`;
|
|
196
|
+
}
|
|
197
|
+
function extractSessionId(fixture) {
|
|
198
|
+
// Prefer a sessionId from the first `create-session` setup step —
|
|
199
|
+
// this is the fixture's declared session identity. Fall back to
|
|
200
|
+
// inputEnvelope.sessionId, then to the fixture name.
|
|
201
|
+
for (const step of fixture.setup) {
|
|
202
|
+
if (step.type === 'create-session' &&
|
|
203
|
+
typeof step.sessionId === 'string') {
|
|
204
|
+
return step.sessionId;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const envelope = fixture.inputEnvelope;
|
|
208
|
+
if (envelope !== null &&
|
|
209
|
+
typeof envelope === 'object' &&
|
|
210
|
+
'sessionId' in envelope &&
|
|
211
|
+
typeof envelope.sessionId === 'string') {
|
|
212
|
+
return envelope.sessionId;
|
|
213
|
+
}
|
|
214
|
+
return fixture.name;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Fixtures with `inputEnvelope.type === 'action'` (or another channel-
|
|
218
|
+
* 3 dispatch shape) get sent verbatim after subscribe. Fixtures with
|
|
219
|
+
* `type === 'render'` are bootstrap-path — the subscribe itself is
|
|
220
|
+
* the dispatch, so we skip the explicit send.
|
|
221
|
+
*/
|
|
222
|
+
function shouldDispatchInputEnvelope(fixture) {
|
|
223
|
+
const envelope = fixture.inputEnvelope;
|
|
224
|
+
if (envelope === null || typeof envelope !== 'object')
|
|
225
|
+
return false;
|
|
226
|
+
const type = envelope.type;
|
|
227
|
+
return type === 'action' || type === 'pop' || type === 'close';
|
|
228
|
+
}
|
|
229
|
+
function maybeSupportedVersions(behavior) {
|
|
230
|
+
if (behavior.kind === 'version-mismatch') {
|
|
231
|
+
// Cast to the specific arm — the extensibly-closed `UnknownBehavior`
|
|
232
|
+
// in the union has `kind: string & {}` which widens narrowing and
|
|
233
|
+
// makes TS forget it already checked the discriminant literal.
|
|
234
|
+
const narrow = behavior;
|
|
235
|
+
return { supportedVersions: narrow.clientAccepts };
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Map fixture name → the bar criterion it exercises, for the
|
|
241
|
+
* `ConformanceFailure.criterion` field. Derived from the
|
|
242
|
+
* `fixturesByContract` classification so the runner stays in sync
|
|
243
|
+
* with the reporter's scorecard grouping.
|
|
244
|
+
*/
|
|
245
|
+
function criterionForFixture(name) {
|
|
246
|
+
for (const [slug, fixtures] of Object.entries(fixturesByContract)) {
|
|
247
|
+
if (fixtures.some((f) => f.name === name))
|
|
248
|
+
return slugToCriterion(slug);
|
|
249
|
+
}
|
|
250
|
+
return 'unknown criterion';
|
|
251
|
+
}
|
|
252
|
+
function slugToCriterion(slug) {
|
|
253
|
+
switch (slug) {
|
|
254
|
+
case 'bootstrap-protocol':
|
|
255
|
+
return 'Protocol #5 named failure modes — bootstrap contract';
|
|
256
|
+
case 'observability-events':
|
|
257
|
+
return 'Phase 2 C12 observability contract';
|
|
258
|
+
case 'refresh-semantics':
|
|
259
|
+
return 'SPEC §2.3 StreamSpec refresh triggers';
|
|
260
|
+
case 'reserved-channel-authority':
|
|
261
|
+
return 'SPEC §4.4 reserved-channel authority';
|
|
262
|
+
case 'schema-version-handshake':
|
|
263
|
+
return 'Protocol #3 version negotiation';
|
|
264
|
+
case 'wired-action-dispatch':
|
|
265
|
+
return 'Contract #3 defined failure modes (wiredActionRouter)';
|
|
266
|
+
default:
|
|
267
|
+
return slug;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The fixture-JSON `SetupStep` and the host's runtime `SetupStep` use
|
|
272
|
+
* slightly different shapes (`type` vs `kind` field, respectively).
|
|
273
|
+
* The runner accepts JSON-shape at the fixture boundary and narrows
|
|
274
|
+
* here — translating `{type,...}` → `{kind,...}` — before dispatching
|
|
275
|
+
* to the host.
|
|
276
|
+
*/
|
|
277
|
+
function narrowSetupStep(step) {
|
|
278
|
+
const raw = step;
|
|
279
|
+
const { type, ...rest } = raw;
|
|
280
|
+
return { kind: type ?? 'unknown', ...rest };
|
|
281
|
+
}
|
|
282
|
+
function narrowTeardownStep(step) {
|
|
283
|
+
const raw = step;
|
|
284
|
+
const { type, ...rest } = raw;
|
|
285
|
+
return { kind: type ?? 'unknown', ...rest };
|
|
286
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wire-bad-export-name-reject",
|
|
3
|
+
"description": "A package entry whose export-name KEY (`leafletmap`) is neither a `use`-prefixed hook (`HOOK_NAME_RE` — `/^use[A-Z][A-Za-z0-9]*$/`) nor a PascalCase component (`COMPONENT_NAME_RE` — `/^[A-Z][A-Za-z0-9]*$/`). The export name grammar is load-bearing — it is the only discriminator of hook vs component on the wire — so a key matching neither grammar MUST be rejected.",
|
|
4
|
+
"clientCapabilities": {
|
|
5
|
+
"gadgets": {
|
|
6
|
+
"@ggui-samples/leaflet": { "leafletmap": {} }
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"expect": "reject",
|
|
10
|
+
"rejectReason": "export-name key must match HOOK_NAME_RE (use-prefixed) or COMPONENT_NAME_RE (PascalCase); `leafletmap` matches neither."
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wire-bad-package-name-reject",
|
|
3
|
+
"description": "A `gadgets` map keyed by `Leaflet` — not a valid npm package name (uppercase first character). Package keys MUST match `NPM_PACKAGE_NAME_RE` (lowercase alphanumerics + `.`/`_`/`-`, an optional single `@scope/` prefix, no leading non-alphanumeric, no URL). A descriptor is looked up by npm package name, so a malformed key cannot resolve and MUST be rejected.",
|
|
4
|
+
"clientCapabilities": {
|
|
5
|
+
"gadgets": {
|
|
6
|
+
"Leaflet": { "useLeafletMap": {} }
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"expect": "reject",
|
|
10
|
+
"rejectReason": "package key must match NPM_PACKAGE_NAME_RE; `Leaflet` is uppercase."
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wire-component-export-accept",
|
|
3
|
+
"description": "A gadget export name is either a `use`-prefixed hook or a PascalCase component — the two grammars are disjoint, so the map KEY alone discriminates kind with no `hook`/`component` tag on the wire. A PascalCase key (`RevenueChart`) is a valid component export reference.",
|
|
4
|
+
"clientCapabilities": {
|
|
5
|
+
"gadgets": {
|
|
6
|
+
"@ggui-samples/charts": { "RevenueChart": {} }
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"expect": "accept"
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wire-empty-package-reject",
|
|
3
|
+
"description": "A package entry with an empty export map (`{}`). A `clientCapabilities.gadgets` package entry MUST declare at least one export — an entry that names a package but uses none of its exports carries no wire meaning. `gadgetPackageUseSchema` refines on `Object.keys(r).length > 0`, so an empty entry MUST be rejected.",
|
|
4
|
+
"clientCapabilities": {
|
|
5
|
+
"gadgets": {
|
|
6
|
+
"@ggui-samples/leaflet": {}
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"expect": "reject",
|
|
10
|
+
"rejectReason": "a clientCapabilities.gadgets package entry must declare at least one export (gadgetPackageUseSchema non-empty refine)."
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wire-minimal-accept",
|
|
3
|
+
"description": "Minimal well-formed gadget wire shape: `clientCapabilities.gadgets` keyed by an npm package name, each value a map of export name → entry. A bare `{}` export entry is valid — the wire carries identity only, `(package, export name)`, and both prose fields are optional.",
|
|
4
|
+
"clientCapabilities": {
|
|
5
|
+
"gadgets": {
|
|
6
|
+
"@ggui-samples/leaflet": { "useLeafletMap": {} }
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"expect": "accept"
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wire-multi-package-accept",
|
|
3
|
+
"description": "`clientCapabilities.gadgets` MAY reference multiple packages, and a single package MAY expose multiple exports — hooks and components mixed. Each package entry is its own export map; there is no package-level wrapper field on the wire.",
|
|
4
|
+
"clientCapabilities": {
|
|
5
|
+
"gadgets": {
|
|
6
|
+
"@ggui-samples/charts": {
|
|
7
|
+
"RevenueChart": {},
|
|
8
|
+
"useChartTheme": { "usage": "Match the chart palette to the app theme." }
|
|
9
|
+
},
|
|
10
|
+
"@ggui-ai/gadgets": { "useClipboardWrite": {} }
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"expect": "accept"
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wire-prose-accept",
|
|
3
|
+
"description": "A gadget export entry MAY carry the two optional intent-specific prose fields `description` and `usage`. When present the agent's prose wins over the registered export's text; when omitted, push-time resolution inherits the registered text verbatim. Both are the ONLY wire-authored payload on an export entry.",
|
|
4
|
+
"clientCapabilities": {
|
|
5
|
+
"gadgets": {
|
|
6
|
+
"@ggui-samples/leaflet": {
|
|
7
|
+
"useLeafletMap": {
|
|
8
|
+
"description": "Interactive map of the delivery route.",
|
|
9
|
+
"usage": "Render once the courier location stream is live."
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"expect": "accept"
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wire-registry-metadata-reject",
|
|
3
|
+
"description": "An export entry carrying per-export registry metadata (`permission`). The registry-side teaching + gating fields — `permission`, `example`, `gotchas`, `required` — live on each `exports[*]` of the registered descriptor, never on the wire. `gadgetExportUseSchema` admits only the optional prose pair `description`/`usage` and is `.strict()`, so a `permission` field MUST be rejected at parse time.",
|
|
4
|
+
"clientCapabilities": {
|
|
5
|
+
"gadgets": {
|
|
6
|
+
"@ggui-ai/gadgets": {
|
|
7
|
+
"useGeolocation": { "permission": "geolocation" }
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"expect": "reject",
|
|
12
|
+
"rejectReason": "per-export registry metadata (permission/example/gotchas/required) is descriptor-side, not a wire field; gadgetExportUseSchema is .strict()."
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wire-stale-libraries-reject",
|
|
3
|
+
"description": "A `clientCapabilities` object carrying the retired `libraries` sibling alongside `gadgets`. `libraries` was renamed to `gadgets`; `clientCapabilitiesSpecSchema` is `.strict()` (not `.passthrough()`) precisely so a stale `libraries` field — or any other unknown sibling — MUST fail loudly at parse time rather than being silently dropped.",
|
|
4
|
+
"clientCapabilities": {
|
|
5
|
+
"gadgets": {
|
|
6
|
+
"@ggui-samples/leaflet": { "useLeafletMap": {} }
|
|
7
|
+
},
|
|
8
|
+
"libraries": {
|
|
9
|
+
"@ggui-samples/leaflet": { "useLeafletMap": {} }
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"expect": "reject",
|
|
13
|
+
"rejectReason": "`libraries` is the retired pre-rename name of `gadgets`; clientCapabilitiesSpecSchema is .strict()."
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wire-stdlib-accept",
|
|
3
|
+
"description": "The first-party `@ggui-ai/gadgets` stdlib package is referenced on the wire exactly like any other gadget package — by its npm package name keying a map of export names. A stdlib browser-capability hook (`useGeolocation`) is a valid wire reference; the stdlib enjoys no special wire shape.",
|
|
4
|
+
"clientCapabilities": {
|
|
5
|
+
"gadgets": {
|
|
6
|
+
"@ggui-ai/gadgets": { "useGeolocation": {} }
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"expect": "accept"
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wire-transport-field-reject",
|
|
3
|
+
"description": "An export entry carrying a transport field (`bundleUrl`). Transport metadata — `bundleUrl`, `bundleHost`, `bundleSri`, `styleUrl`, `connect`, `requires`, `typesUrl`, `typesSri` — lives once on the registered `GadgetDescriptor`, never on the wire. `gadgetExportUseSchema` is `.strict()`, so a transport field MUST be rejected at parse time.",
|
|
4
|
+
"clientCapabilities": {
|
|
5
|
+
"gadgets": {
|
|
6
|
+
"@ggui-samples/leaflet": {
|
|
7
|
+
"useLeafletMap": {
|
|
8
|
+
"bundleUrl": "https://cdn.example.com/bundles/leaflet/bundle.js"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"expect": "reject",
|
|
14
|
+
"rejectReason": "transport metadata is descriptor-side (App.gadgets), not a wire field; gadgetExportUseSchema is .strict()."
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wire-version-field-reject",
|
|
3
|
+
"description": "An export entry carrying a `version` field. `version` is the operator's deployment pin in `App.gadgets`, never the agent's to author — it is resolved from the registry at push time, never carried on the wire. `gadgetExportUseSchema` is `.strict()`, so a `version` field MUST be rejected at parse time rather than silently dropped.",
|
|
4
|
+
"clientCapabilities": {
|
|
5
|
+
"gadgets": {
|
|
6
|
+
"@ggui-samples/leaflet": {
|
|
7
|
+
"useLeafletMap": { "version": "2.3.1" }
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"expect": "reject",
|
|
12
|
+
"rejectReason": "`version` is registry-side (App.gadgets), not a wire field; gadgetExportUseSchema is .strict()."
|
|
13
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One schema-conformance case. Authored as JSON under `./cases/`,
|
|
3
|
+
* consumed via {@link gadgetWireSchemaCases}, graded by
|
|
4
|
+
* {@link runSchemaConformance}.
|
|
5
|
+
*
|
|
6
|
+
* The shape IS the public API — additive changes only, mirroring the
|
|
7
|
+
* `TestCase` discipline in `../types.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export interface SchemaConformanceCase {
|
|
10
|
+
/** Unique case name. MUST match the JSON filename without `.json`. */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
/** What structural obligation this case proves. */
|
|
13
|
+
readonly description: string;
|
|
14
|
+
/**
|
|
15
|
+
* The `DataContract.clientCapabilities` value fed verbatim to the
|
|
16
|
+
* validator. `unknown` because reject cases are deliberately
|
|
17
|
+
* malformed — the catalog's whole point is feeding the parser
|
|
18
|
+
* payloads its type would not admit.
|
|
19
|
+
*/
|
|
20
|
+
readonly clientCapabilities: unknown;
|
|
21
|
+
/**
|
|
22
|
+
* Whether a spec-conformant `clientCapabilities` parser MUST accept
|
|
23
|
+
* or reject {@link clientCapabilities}.
|
|
24
|
+
*/
|
|
25
|
+
readonly expect: 'accept' | 'reject';
|
|
26
|
+
/**
|
|
27
|
+
* The normative reason a conformant parser rejects this case.
|
|
28
|
+
* Present iff `expect === 'reject'`.
|
|
29
|
+
*/
|
|
30
|
+
readonly rejectReason?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Every gadget-wire schema-conformance case the kit ships, in
|
|
34
|
+
* deterministic order — accepts first, then rejects, each group
|
|
35
|
+
* lexicographic by `name`.
|
|
36
|
+
*/
|
|
37
|
+
export declare const gadgetWireSchemaCases: readonly SchemaConformanceCase[];
|
|
38
|
+
/** One case the validator graded wrong. */
|
|
39
|
+
export interface SchemaConformanceMismatch {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
/** What the catalog says a conformant parser MUST do. */
|
|
42
|
+
readonly expected: 'accept' | 'reject';
|
|
43
|
+
/** What the validator under test actually did. */
|
|
44
|
+
readonly actual: 'accept' | 'reject';
|
|
45
|
+
/** The reject reason from the case, when `expected === 'reject'`. */
|
|
46
|
+
readonly rejectReason?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Outcome of grading a validator against the catalog. */
|
|
49
|
+
export interface SchemaConformanceResult {
|
|
50
|
+
/** Names of cases the validator graded correctly. */
|
|
51
|
+
readonly passed: readonly string[];
|
|
52
|
+
/** Cases the validator graded wrong — empty iff fully conformant. */
|
|
53
|
+
readonly failed: readonly SchemaConformanceMismatch[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Grade a `clientCapabilities` validator against the gadget-wire
|
|
57
|
+
* schema-conformance catalog.
|
|
58
|
+
*
|
|
59
|
+
* `validate` MUST return `true` iff its parser accepts the input as a
|
|
60
|
+
* well-formed `DataContract.clientCapabilities`, `false` otherwise.
|
|
61
|
+
* The kit deliberately does NOT import a concrete schema — adopters
|
|
62
|
+
* pass their own parser (a zod `safeParse`, a Go struct decode, a
|
|
63
|
+
* pydantic model) and the catalog grades it. A conformant validator
|
|
64
|
+
* produces an empty `failed` array.
|
|
65
|
+
*
|
|
66
|
+
* `validate` is invoked exactly once per case; it MUST be pure and
|
|
67
|
+
* MUST NOT throw (a throwing parser is itself non-conformant — wrap
|
|
68
|
+
* it). Cases are graded in {@link gadgetWireSchemaCases} order.
|
|
69
|
+
*/
|
|
70
|
+
export declare function runSchemaConformance(validate: (clientCapabilities: unknown) => boolean): SchemaConformanceResult;
|
|
71
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schema-conformance/index.ts"],"names":[],"mappings":"AA+CA;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB;IACpC,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,qBAAqB,EAajE,CAAC;AAEF,2CAA2C;AAC3C,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yDAAyD;IACzD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACvC,kDAAkD;IAClD,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,0DAA0D;AAC1D,MAAM,WAAW,uBAAuB;IACtC,qDAAqD;IACrD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,SAAS,yBAAyB,EAAE,CAAC;CACvD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,CAAC,kBAAkB,EAAE,OAAO,KAAK,OAAO,GACjD,uBAAuB,CAqBzB"}
|