@ggui-ai/protocol-conformance 0.13.0 → 0.15.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/README.md +28 -14
- package/dist/cli.d.ts +23 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +228 -19
- package/dist/fixtures/index.d.ts +9 -0
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +9 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/json-equal.d.ts +6 -0
- package/dist/json-equal.d.ts.map +1 -0
- package/dist/json-equal.js +23 -0
- package/dist/refusal-envelope-conformance/cases/refuse-after-fix-caller.json +27 -0
- package/dist/refusal-envelope-conformance/cases/refuse-after-fix-owner-with-balance.json +29 -0
- package/dist/refusal-envelope-conformance/cases/refuse-later.json +27 -0
- package/dist/refusal-envelope-conformance/cases/refuse-never.json +27 -0
- package/dist/refusal-envelope-conformance/cases/refuse-next-period.json +27 -0
- package/dist/refusal-envelope-conformance/cases/refuse-non-render-surface.json +12 -0
- package/dist/refusal-envelope-conformance/index.d.ts +108 -0
- package/dist/refusal-envelope-conformance/index.d.ts.map +1 -0
- package/dist/refusal-envelope-conformance/index.js +119 -0
- package/dist/registry-completeness/index.d.ts +105 -0
- package/dist/registry-completeness/index.d.ts.map +1 -0
- package/dist/registry-completeness/index.js +110 -0
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +26 -9
- package/dist/run-conformance.d.ts +38 -0
- package/dist/run-conformance.d.ts.map +1 -1
- package/dist/run-conformance.js +125 -4
- package/dist/transport-refusal-conformance/cases/refuse-deprovisioned-endpoint.json +25 -0
- package/dist/transport-refusal-conformance/cases/refuse-render-only-code.json +11 -0
- package/dist/transport-refusal-conformance/index.d.ts +54 -0
- package/dist/transport-refusal-conformance/index.d.ts.map +1 -0
- package/dist/transport-refusal-conformance/index.js +52 -0
- package/package.json +20 -5
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "refuse-after-fix-caller",
|
|
3
|
+
"description": "An after-fix refusal the CALLER can act on itself: the projector emits an isError result whose text leads with the code, carries outcome 'refused' plus the refusal and nothing else, and exposes no _meta. This is the only fixBy class an agent may retry on its own.",
|
|
4
|
+
"refusal": {
|
|
5
|
+
"code": "model_not_in_tier",
|
|
6
|
+
"message": "the requested model is not among those this app may use",
|
|
7
|
+
"fix": "pick a model the app is allowed to use and call again with the same handshakeId",
|
|
8
|
+
"retry": "after-fix",
|
|
9
|
+
"handshake": "intact"
|
|
10
|
+
},
|
|
11
|
+
"expect": {
|
|
12
|
+
"isError": true,
|
|
13
|
+
"text": "model_not_in_tier: the requested model is not among those this app may use pick a model the app is allowed to use and call again with the same handshakeId",
|
|
14
|
+
"structuredContent": {
|
|
15
|
+
"outcome": "refused",
|
|
16
|
+
"refusal": {
|
|
17
|
+
"code": "model_not_in_tier",
|
|
18
|
+
"message": "the requested model is not among those this app may use",
|
|
19
|
+
"fix": "pick a model the app is allowed to use and call again with the same handshakeId",
|
|
20
|
+
"retry": "after-fix",
|
|
21
|
+
"handshake": "intact"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"hasMeta": false,
|
|
25
|
+
"identityFields": []
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "refuse-after-fix-owner-with-balance",
|
|
3
|
+
"description": "An after-fix refusal whose fix belongs to the app OWNER, carrying the optional balance the refusing check read. The optional field rides the envelope unchanged \u2014 a projector must not drop it, and an agent must not auto-retry a fix it cannot perform.",
|
|
4
|
+
"refusal": {
|
|
5
|
+
"code": "insufficient_credit",
|
|
6
|
+
"message": "the balance this deployment funds the app from is exhausted",
|
|
7
|
+
"fix": "the app owner adds funds; the same handshakeId is still valid afterwards",
|
|
8
|
+
"retry": "after-fix",
|
|
9
|
+
"handshake": "intact",
|
|
10
|
+
"balanceCentsAtCheck": 0
|
|
11
|
+
},
|
|
12
|
+
"expect": {
|
|
13
|
+
"isError": true,
|
|
14
|
+
"text": "insufficient_credit: the balance this deployment funds the app from is exhausted the app owner adds funds; the same handshakeId is still valid afterwards",
|
|
15
|
+
"structuredContent": {
|
|
16
|
+
"outcome": "refused",
|
|
17
|
+
"refusal": {
|
|
18
|
+
"code": "insufficient_credit",
|
|
19
|
+
"message": "the balance this deployment funds the app from is exhausted",
|
|
20
|
+
"fix": "the app owner adds funds; the same handshakeId is still valid afterwards",
|
|
21
|
+
"retry": "after-fix",
|
|
22
|
+
"handshake": "intact",
|
|
23
|
+
"balanceCentsAtCheck": 0
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"hasMeta": false,
|
|
27
|
+
"identityFields": []
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "refuse-later",
|
|
3
|
+
"description": "A transient refusal: unbounded time rather than a period boundary. Handshake intact, so the SAME id is valid on the retry the fix invites \u2014 the case exists so a projector cannot special-case the retryable class.",
|
|
4
|
+
"refusal": {
|
|
5
|
+
"code": "issuer_rate_limited",
|
|
6
|
+
"message": "calls under this issuing identity are over the rate this deployment allows",
|
|
7
|
+
"fix": "retry shortly \u2014 time restores it",
|
|
8
|
+
"retry": "later",
|
|
9
|
+
"handshake": "intact"
|
|
10
|
+
},
|
|
11
|
+
"expect": {
|
|
12
|
+
"isError": true,
|
|
13
|
+
"text": "issuer_rate_limited: calls under this issuing identity are over the rate this deployment allows retry shortly \u2014 time restores it",
|
|
14
|
+
"structuredContent": {
|
|
15
|
+
"outcome": "refused",
|
|
16
|
+
"refusal": {
|
|
17
|
+
"code": "issuer_rate_limited",
|
|
18
|
+
"message": "calls under this issuing identity are over the rate this deployment allows",
|
|
19
|
+
"fix": "retry shortly \u2014 time restores it",
|
|
20
|
+
"retry": "later",
|
|
21
|
+
"handshake": "intact"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"hasMeta": false,
|
|
25
|
+
"identityFields": []
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "refuse-never",
|
|
3
|
+
"description": "A refusal no caller action can lift. It still projects to a normal isError tool result — a terminal refusal is a wire state, never a thrown error. Carried on `billing_mode_anomaly` since registry v9 retired `tier_unrecognized`; the graded property is the `never` retry class, not the particular code.",
|
|
4
|
+
"refusal": {
|
|
5
|
+
"code": "billing_mode_anomaly",
|
|
6
|
+
"message": "the app record declares a funding mode this deployment does not recognise",
|
|
7
|
+
"fix": "no caller or owner action restores rendering under this identity; the operator reconciles the record",
|
|
8
|
+
"retry": "never",
|
|
9
|
+
"handshake": "intact"
|
|
10
|
+
},
|
|
11
|
+
"expect": {
|
|
12
|
+
"isError": true,
|
|
13
|
+
"text": "billing_mode_anomaly: the app record declares a funding mode this deployment does not recognise no caller or owner action restores rendering under this identity; the operator reconciles the record",
|
|
14
|
+
"structuredContent": {
|
|
15
|
+
"outcome": "refused",
|
|
16
|
+
"refusal": {
|
|
17
|
+
"code": "billing_mode_anomaly",
|
|
18
|
+
"message": "the app record declares a funding mode this deployment does not recognise",
|
|
19
|
+
"fix": "no caller or owner action restores rendering under this identity; the operator reconciles the record",
|
|
20
|
+
"retry": "never",
|
|
21
|
+
"handshake": "intact"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"hasMeta": false,
|
|
25
|
+
"identityFields": []
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "refuse-next-period",
|
|
3
|
+
"description": "A next-period refusal names no party at all \u2014 time restores it. The envelope is otherwise identical, which is the point: the retry class is data on the wire, not a different shape.",
|
|
4
|
+
"refusal": {
|
|
5
|
+
"code": "hard_cap_exceeded",
|
|
6
|
+
"message": "the app reached the hard ceiling set for the current period",
|
|
7
|
+
"fix": "the ceiling resets at the start of the next period; no action restores it sooner",
|
|
8
|
+
"retry": "next-period",
|
|
9
|
+
"handshake": "intact"
|
|
10
|
+
},
|
|
11
|
+
"expect": {
|
|
12
|
+
"isError": true,
|
|
13
|
+
"text": "hard_cap_exceeded: the app reached the hard ceiling set for the current period the ceiling resets at the start of the next period; no action restores it sooner",
|
|
14
|
+
"structuredContent": {
|
|
15
|
+
"outcome": "refused",
|
|
16
|
+
"refusal": {
|
|
17
|
+
"code": "hard_cap_exceeded",
|
|
18
|
+
"message": "the app reached the hard ceiling set for the current period",
|
|
19
|
+
"fix": "the ceiling resets at the start of the next period; no action restores it sooner",
|
|
20
|
+
"retry": "next-period",
|
|
21
|
+
"handshake": "intact"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"hasMeta": false,
|
|
25
|
+
"identityFields": []
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "refuse-non-render-surface",
|
|
3
|
+
"description": "A REGISTERED code whose surfaces exclude the render gate. It has no render envelope at all, so a conformant projector produces nothing for it. Projecting it anyway is the drift the per-surface namespace rule forbids \u2014 this case is what makes that rule observable rather than prose.",
|
|
4
|
+
"refusal": {
|
|
5
|
+
"code": "subscription_exists",
|
|
6
|
+
"message": "an active plan already exists on this app",
|
|
7
|
+
"fix": "cancel the existing plan first, then call again",
|
|
8
|
+
"retry": "after-fix",
|
|
9
|
+
"handshake": "intact"
|
|
10
|
+
},
|
|
11
|
+
"expect": null
|
|
12
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The refusal a deployment's gate returned, as the projector receives
|
|
3
|
+
* it. Authored here rather than imported from `@ggui-ai/protocol`:
|
|
4
|
+
* this is the kit's own vocabulary, and `code` is typed loosely on
|
|
5
|
+
* purpose so a catalog case can carry a registered code from ANY
|
|
6
|
+
* surface — including one that must not project.
|
|
7
|
+
*/
|
|
8
|
+
export interface PreGenerationRefusalInput {
|
|
9
|
+
/** A code from the deployment's refusal registry. */
|
|
10
|
+
readonly code: string;
|
|
11
|
+
/** Precise diagnostic — what was checked, against what. */
|
|
12
|
+
readonly message: string;
|
|
13
|
+
/** The one recovery step, addressed to the party that can take it. */
|
|
14
|
+
readonly fix: string;
|
|
15
|
+
/** How the call becomes possible again. */
|
|
16
|
+
readonly retry: string;
|
|
17
|
+
/** Always `intact` — a refusal reads nothing, so it consumes nothing. */
|
|
18
|
+
readonly handshake: string;
|
|
19
|
+
/** Present only when the refusing check read a balance. */
|
|
20
|
+
readonly balanceCentsAtCheck?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The observable shape of the tool result a conformant server emits for
|
|
24
|
+
* a refusal. Deliberately flattened to the four facts the obligation is
|
|
25
|
+
* about, so an implementation in any language can report them without
|
|
26
|
+
* reproducing an MCP result object.
|
|
27
|
+
*/
|
|
28
|
+
export interface ProjectedRefusalResult {
|
|
29
|
+
/** MUST be true — a refusal is an in-result error, never a throw. */
|
|
30
|
+
readonly isError: boolean;
|
|
31
|
+
/** `content[0].text` — leads with the code, then message and fix. */
|
|
32
|
+
readonly text: string;
|
|
33
|
+
/** The whole `structuredContent`. */
|
|
34
|
+
readonly structuredContent: {
|
|
35
|
+
readonly outcome: string;
|
|
36
|
+
readonly refusal: PreGenerationRefusalInput;
|
|
37
|
+
};
|
|
38
|
+
/** Whether the result carries any `_meta`. MUST be false. */
|
|
39
|
+
readonly hasMeta: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Identity fields the projector put on the envelope. MUST be empty —
|
|
42
|
+
* a refusal commits nothing, so any identity field means the
|
|
43
|
+
* projection leaked state that does not exist.
|
|
44
|
+
*/
|
|
45
|
+
readonly identityFields: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* One refusal-envelope case. Authored as JSON under `./cases/`,
|
|
49
|
+
* consumed via {@link refusalEnvelopeCases}, graded by
|
|
50
|
+
* {@link runRefusalEnvelopeConformance}.
|
|
51
|
+
*
|
|
52
|
+
* The shape IS the public API — additive changes only.
|
|
53
|
+
*/
|
|
54
|
+
export interface RefusalEnvelopeConformanceCase {
|
|
55
|
+
/** Unique case name. MUST match the JSON filename without `.json`. */
|
|
56
|
+
readonly name: string;
|
|
57
|
+
/** Which projection obligation this case proves. */
|
|
58
|
+
readonly description: string;
|
|
59
|
+
/** The refusal fed to the projector. */
|
|
60
|
+
readonly refusal: PreGenerationRefusalInput;
|
|
61
|
+
/**
|
|
62
|
+
* The result a spec-conformant projector MUST produce — or `null`
|
|
63
|
+
* when the code has no render envelope at all (its registered
|
|
64
|
+
* surfaces exclude the render gate).
|
|
65
|
+
*/
|
|
66
|
+
readonly expect: ProjectedRefusalResult | null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Every refusal-envelope case the kit ships, in deterministic order:
|
|
70
|
+
* the projectable cases by retry class (after-fix, next-period, later,
|
|
71
|
+
* never), then the non-projectable surface case.
|
|
72
|
+
*/
|
|
73
|
+
export declare const refusalEnvelopeCases: readonly RefusalEnvelopeConformanceCase[];
|
|
74
|
+
/** One case the projector under test graded wrong. */
|
|
75
|
+
export interface RefusalEnvelopeMismatch {
|
|
76
|
+
readonly name: string;
|
|
77
|
+
/** What the catalog says a conformant projector MUST produce. */
|
|
78
|
+
readonly expected: ProjectedRefusalResult | null;
|
|
79
|
+
/**
|
|
80
|
+
* What the projector produced — or, when it THREW instead of
|
|
81
|
+
* returning, a one-line report of the throw. A gate that throws to
|
|
82
|
+
* refuse is non-conformant, so the throw is graded, never rethrown.
|
|
83
|
+
*/
|
|
84
|
+
readonly actual: ProjectedRefusalResult | null | string;
|
|
85
|
+
}
|
|
86
|
+
/** Outcome of grading a projector against the catalog. */
|
|
87
|
+
export interface RefusalEnvelopeConformanceResult {
|
|
88
|
+
/** Names of cases the projector graded correctly. */
|
|
89
|
+
readonly passed: readonly string[];
|
|
90
|
+
/** Cases the projector graded wrong — empty iff fully conformant. */
|
|
91
|
+
readonly failed: readonly RefusalEnvelopeMismatch[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Grade a refusal projector against the catalog.
|
|
95
|
+
*
|
|
96
|
+
* `project` MUST be a pure computation of `(refusal) → result | null`,
|
|
97
|
+
* where `null` means "this code has no render envelope". The kit
|
|
98
|
+
* deliberately does NOT import a concrete projector — adopters pass
|
|
99
|
+
* their own. A conformant projector produces an empty `failed` array.
|
|
100
|
+
*
|
|
101
|
+
* `project` is invoked exactly once per case, in
|
|
102
|
+
* {@link refusalEnvelopeCases} order. It MUST NOT throw: throwing to
|
|
103
|
+
* refuse is precisely the non-conformance this catalog exists to
|
|
104
|
+
* catch, so a throw is recorded as a graded FAILURE on that case
|
|
105
|
+
* rather than escaping the run.
|
|
106
|
+
*/
|
|
107
|
+
export declare function runRefusalEnvelopeConformance(project: (refusal: PreGenerationRefusalInput) => ProjectedRefusalResult | null): RefusalEnvelopeConformanceResult;
|
|
108
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/refusal-envelope-conformance/index.ts"],"names":[],"mappings":"AA4DA;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,2DAA2D;IAC3D,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,QAAQ,CAAC,iBAAiB,EAAE;QAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,OAAO,EAAE,yBAAyB,CAAC;KAC7C,CAAC;IACF,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,8BAA8B;IAC7C,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,wCAAwC;IACxC,QAAQ,CAAC,OAAO,EAAE,yBAAyB,CAAC;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAAC;CAChD;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,EAAE,SAAS,8BAA8B,EAOzE,CAAC;AAEF,sDAAsD;AACtD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACjD;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI,GAAG,MAAM,CAAC;CACzD;AAED,0DAA0D;AAC1D,MAAM,WAAW,gCAAgC;IAC/C,qDAAqD;IACrD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,SAAS,uBAAuB,EAAE,CAAC;CACrD;AAgBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,CACP,OAAO,EAAE,yBAAyB,KAC/B,sBAAsB,GAAG,IAAI,GACjC,gCAAgC,CAwBlC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refusal-envelope conformance catalog — the projection obligation for a
|
|
3
|
+
* PRE-GENERATION refusal (SPEC §7.1's refused arm, ggui#786).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is a pure-function catalog
|
|
6
|
+
*
|
|
7
|
+
* Given a refusal a deployment's gate returned, the tool result a
|
|
8
|
+
* conformant server emits is a pure, deterministic projection — no
|
|
9
|
+
* render, no transport, no wire frame. So it gets the same treatment as
|
|
10
|
+
* the wire schema (`../schema-conformance`) and the gadget URL resolver
|
|
11
|
+
* (`../resolution-conformance`): a catalog of cases graded against a
|
|
12
|
+
* caller-supplied projector.
|
|
13
|
+
*
|
|
14
|
+
* The obligation, restated from SPEC §7.1 so an implementer never has
|
|
15
|
+
* to guess:
|
|
16
|
+
*
|
|
17
|
+
* - The result is an `isError: true` TOOL RESULT — never a thrown
|
|
18
|
+
* error, never a JSON-RPC error. A gate that throws instead of
|
|
19
|
+
* returning a refusal is non-conformant, which is why the runner
|
|
20
|
+
* grades a throwing projector as a failure on every case rather
|
|
21
|
+
* than letting the exception escape.
|
|
22
|
+
* - `content[0].text` LEADS with the code, then the message and the
|
|
23
|
+
* fix. A courtesy line for the model; the machine-readable code is
|
|
24
|
+
* the mechanism.
|
|
25
|
+
* - `structuredContent` is `{outcome: 'refused', refusal}` and
|
|
26
|
+
* NOTHING else. A refusal commits nothing, so no identity field
|
|
27
|
+
* may appear on it.
|
|
28
|
+
* - No `_meta`. A refusal exposes no mount affordance.
|
|
29
|
+
* - A code whose registered surfaces exclude the render gate has NO
|
|
30
|
+
* render envelope; a conformant projector produces none.
|
|
31
|
+
*
|
|
32
|
+
* ## Polyglot, decoupled
|
|
33
|
+
*
|
|
34
|
+
* Each case ships as raw JSON under `./cases/`.
|
|
35
|
+
* {@link runRefusalEnvelopeConformance} takes the projector as a
|
|
36
|
+
* callback — the kit never imports a concrete server here, so adopters
|
|
37
|
+
* drive their OWN. The reference binding
|
|
38
|
+
* (`./refusal-envelope-conformance.test.ts`) builds a faithful in-test
|
|
39
|
+
* projector from `@ggui-ai/protocol` primitives only; the drift-catch
|
|
40
|
+
* against a SHIPPING projector belongs implementation-side, in the
|
|
41
|
+
* package that owns it.
|
|
42
|
+
*
|
|
43
|
+
* ## Not a WS fixture
|
|
44
|
+
*
|
|
45
|
+
* These cases are deliberately NOT registered in `fixturesByContract`.
|
|
46
|
+
* The obligation is a projection, not a WebSocket-observable behavior,
|
|
47
|
+
* so registering it there would be a permanent skip on every run —
|
|
48
|
+
* exactly the false gate the kit's exact skip-set pinning prevents.
|
|
49
|
+
* `runConformance()` / the CLI / the reporter are untouched; the entry
|
|
50
|
+
* point is the programmatic runner below.
|
|
51
|
+
*/
|
|
52
|
+
import refuseAfterFixCaller from './cases/refuse-after-fix-caller.json' with { type: 'json' };
|
|
53
|
+
import refuseAfterFixOwnerWithBalance from './cases/refuse-after-fix-owner-with-balance.json' with { type: 'json' };
|
|
54
|
+
import refuseLater from './cases/refuse-later.json' with { type: 'json' };
|
|
55
|
+
import refuseNever from './cases/refuse-never.json' with { type: 'json' };
|
|
56
|
+
import refuseNextPeriod from './cases/refuse-next-period.json' with { type: 'json' };
|
|
57
|
+
import refuseNonRenderSurface from './cases/refuse-non-render-surface.json' with { type: 'json' };
|
|
58
|
+
import { jsonEqual } from '../json-equal.js';
|
|
59
|
+
/**
|
|
60
|
+
* Every refusal-envelope case the kit ships, in deterministic order:
|
|
61
|
+
* the projectable cases by retry class (after-fix, next-period, later,
|
|
62
|
+
* never), then the non-projectable surface case.
|
|
63
|
+
*/
|
|
64
|
+
export const refusalEnvelopeCases = [
|
|
65
|
+
refuseAfterFixCaller,
|
|
66
|
+
refuseAfterFixOwnerWithBalance,
|
|
67
|
+
refuseNextPeriod,
|
|
68
|
+
refuseLater,
|
|
69
|
+
refuseNever,
|
|
70
|
+
refuseNonRenderSurface,
|
|
71
|
+
];
|
|
72
|
+
function projectionEqual(a, b) {
|
|
73
|
+
if (a === null || b === null)
|
|
74
|
+
return a === b;
|
|
75
|
+
return (a.isError === b.isError &&
|
|
76
|
+
a.text === b.text &&
|
|
77
|
+
a.hasMeta === b.hasMeta &&
|
|
78
|
+
jsonEqual([...a.identityFields], [...b.identityFields]) &&
|
|
79
|
+
jsonEqual(a.structuredContent, b.structuredContent));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Grade a refusal projector against the catalog.
|
|
83
|
+
*
|
|
84
|
+
* `project` MUST be a pure computation of `(refusal) → result | null`,
|
|
85
|
+
* where `null` means "this code has no render envelope". The kit
|
|
86
|
+
* deliberately does NOT import a concrete projector — adopters pass
|
|
87
|
+
* their own. A conformant projector produces an empty `failed` array.
|
|
88
|
+
*
|
|
89
|
+
* `project` is invoked exactly once per case, in
|
|
90
|
+
* {@link refusalEnvelopeCases} order. It MUST NOT throw: throwing to
|
|
91
|
+
* refuse is precisely the non-conformance this catalog exists to
|
|
92
|
+
* catch, so a throw is recorded as a graded FAILURE on that case
|
|
93
|
+
* rather than escaping the run.
|
|
94
|
+
*/
|
|
95
|
+
export function runRefusalEnvelopeConformance(project) {
|
|
96
|
+
const passed = [];
|
|
97
|
+
const failed = [];
|
|
98
|
+
for (const testCase of refusalEnvelopeCases) {
|
|
99
|
+
let actual;
|
|
100
|
+
try {
|
|
101
|
+
actual = project(testCase.refusal);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
failed.push({
|
|
105
|
+
name: testCase.name,
|
|
106
|
+
expected: testCase.expect,
|
|
107
|
+
actual: `threw instead of returning a refusal envelope: ${err instanceof Error ? err.message : String(err)}`,
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (projectionEqual(actual, testCase.expect)) {
|
|
112
|
+
passed.push(testCase.name);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
failed.push({ name: testCase.name, expected: testCase.expect, actual });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { passed, failed };
|
|
119
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry-completeness catalog — the structural obligations a
|
|
3
|
+
* deployment's refusal-code registry MUST satisfy (ggui#786, ruling
|
|
4
|
+
* 5b).
|
|
5
|
+
*
|
|
6
|
+
* ## Why this is a pure-function catalog, graded differently
|
|
7
|
+
*
|
|
8
|
+
* Unlike every other catalog here, this one grades DATA rather than a
|
|
9
|
+
* function: the closed set of refusal codes a deployment declares. The
|
|
10
|
+
* caller passes the registry; each pin inspects every row and reports
|
|
11
|
+
* the rows that violate it.
|
|
12
|
+
*
|
|
13
|
+
* ## The four PORTABLE pins, and the two that are deliberately absent
|
|
14
|
+
*
|
|
15
|
+
* Ruling 5b names six checks. Four are portable — a third-party adopter
|
|
16
|
+
* with their own registry can run them, and they are the four below.
|
|
17
|
+
*
|
|
18
|
+
* Two read repo SOURCE and therefore cannot ship as conformance: the
|
|
19
|
+
* docstring rule ("an agent MUST NOT auto-retry an after-fix refusal
|
|
20
|
+
* whose fixBy is not caller") and the grep for code literals minted
|
|
21
|
+
* outside the registry file. This package publishes only `dist` +
|
|
22
|
+
* `README.md`, so a source walk could never run for an adopter, and the
|
|
23
|
+
* files that rule names live outside this tree entirely. Both live in
|
|
24
|
+
* `@ggui-ai/protocol`'s own suite, where the invariant is owned and the
|
|
25
|
+
* paths are stable. The omission is DECLARED here rather than hidden.
|
|
26
|
+
*
|
|
27
|
+
* ## Why the `fixBy` pin is one-directional
|
|
28
|
+
*
|
|
29
|
+
* The pin below asserts only that an `after-fix` row NAMES who acts. It
|
|
30
|
+
* does not assert `fixBy` is absent elsewhere, because that is a naming
|
|
31
|
+
* discipline rather than a reader-observable obligation: a reader
|
|
32
|
+
* branches on `retry`, and a `fixBy` beside a `never` misleads a human
|
|
33
|
+
* without changing what any implementation does.
|
|
34
|
+
*
|
|
35
|
+
* ggui's own registry is stricter — `fixBy` is REQUIRED on `after-fix`,
|
|
36
|
+
* PERMITTED on `later` (the operator who restores a transiently
|
|
37
|
+
* unavailable surface) and FORBIDDEN on `next-period` and `never` — and
|
|
38
|
+
* enforces it at the type level in its definer, pinned in
|
|
39
|
+
* `@ggui-ai/protocol`'s own suite. Adopters are welcome to that rule;
|
|
40
|
+
* the kit does not make it a conformance obligation. The one thing that
|
|
41
|
+
* must never happen, and IS graded here, is an `after-fix` row that
|
|
42
|
+
* does not say WHO acts — the one an agent could misread as its own to
|
|
43
|
+
* retry.
|
|
44
|
+
*/
|
|
45
|
+
/** One registry row, in the kit's own loose vocabulary. */
|
|
46
|
+
export interface RefusalRegistryRow {
|
|
47
|
+
/** MUST equal the row's key in the registry. */
|
|
48
|
+
readonly code: string;
|
|
49
|
+
/** MUST be non-empty — the surfaces this state is refused on. */
|
|
50
|
+
readonly surfaces: readonly string[];
|
|
51
|
+
/** MUST be one of the four recognized retry classes. */
|
|
52
|
+
readonly retry: string;
|
|
53
|
+
/** Who acts. REQUIRED when `retry` is `after-fix`. */
|
|
54
|
+
readonly fixBy?: string | undefined;
|
|
55
|
+
/** Where the code is produced. */
|
|
56
|
+
readonly emitter: string;
|
|
57
|
+
/** What state the code names. */
|
|
58
|
+
readonly description: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A registry as the kit reads it: code → row. Decoupled from
|
|
62
|
+
* `@ggui-ai/protocol`'s own types on purpose — an adopter's registry is
|
|
63
|
+
* graded through this view, and a deliberately broken registry must be
|
|
64
|
+
* expressible for the negative cases.
|
|
65
|
+
*/
|
|
66
|
+
export type RefusalRegistryView = {
|
|
67
|
+
readonly [code: string]: RefusalRegistryRow;
|
|
68
|
+
};
|
|
69
|
+
/** One structural pin, applied to every row of a registry. */
|
|
70
|
+
export interface RegistryCompletenessPin {
|
|
71
|
+
/** Unique pin name. */
|
|
72
|
+
readonly name: string;
|
|
73
|
+
/** The obligation this pin fixes in place. */
|
|
74
|
+
readonly description: string;
|
|
75
|
+
/**
|
|
76
|
+
* Report why `row` (declared under `code`) violates the pin, or
|
|
77
|
+
* `null` when it satisfies it.
|
|
78
|
+
*/
|
|
79
|
+
readonly violation: (code: string, row: RefusalRegistryRow) => string | null;
|
|
80
|
+
}
|
|
81
|
+
/** Every portable pin the kit ships, in deterministic order. */
|
|
82
|
+
export declare const registryCompletenessPins: readonly RegistryCompletenessPin[];
|
|
83
|
+
/** One pin a registry failed, with every offending row named. */
|
|
84
|
+
export interface RegistryCompletenessMismatch {
|
|
85
|
+
readonly name: string;
|
|
86
|
+
readonly description: string;
|
|
87
|
+
/** One entry per violating row: `<code>: <why>`. */
|
|
88
|
+
readonly violations: readonly string[];
|
|
89
|
+
}
|
|
90
|
+
/** Outcome of grading a registry against the pins. */
|
|
91
|
+
export interface RegistryCompletenessResult {
|
|
92
|
+
/** Names of pins the registry satisfies. */
|
|
93
|
+
readonly passed: readonly string[];
|
|
94
|
+
/** Pins the registry violates — empty iff fully conformant. */
|
|
95
|
+
readonly failed: readonly RegistryCompletenessMismatch[];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Grade a refusal registry against {@link registryCompletenessPins}.
|
|
99
|
+
*
|
|
100
|
+
* Every pin is applied to every row, in declaration order; a pin passes
|
|
101
|
+
* only when NO row violates it. A conformant registry produces an empty
|
|
102
|
+
* `failed` array.
|
|
103
|
+
*/
|
|
104
|
+
export declare function runRegistryCompletenessConformance(registry: RefusalRegistryView): RegistryCompletenessResult;
|
|
105
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/registry-completeness/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,2DAA2D;AAC3D,MAAM,WAAW,kBAAkB;IACjC,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,sDAAsD;IACtD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,kCAAkC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,iCAAiC;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,kBAAkB,CAAC;CAC7C,CAAC;AAUF,8DAA8D;AAC9D,MAAM,WAAW,uBAAuB;IACtC,uBAAuB;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,kBAAkB,KAAK,MAAM,GAAG,IAAI,CAAC;CAC9E;AAED,gEAAgE;AAChE,eAAO,MAAM,wBAAwB,EAAE,SAAS,uBAAuB,EAoCtE,CAAC;AAEF,iEAAiE;AACjE,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,oDAAoD;IACpD,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,sDAAsD;AACtD,MAAM,WAAW,0BAA0B;IACzC,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,SAAS,4BAA4B,EAAE,CAAC;CAC1D;AAED;;;;;;GAMG;AACH,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,mBAAmB,GAC5B,0BAA0B,CAiB5B"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry-completeness catalog — the structural obligations a
|
|
3
|
+
* deployment's refusal-code registry MUST satisfy (ggui#786, ruling
|
|
4
|
+
* 5b).
|
|
5
|
+
*
|
|
6
|
+
* ## Why this is a pure-function catalog, graded differently
|
|
7
|
+
*
|
|
8
|
+
* Unlike every other catalog here, this one grades DATA rather than a
|
|
9
|
+
* function: the closed set of refusal codes a deployment declares. The
|
|
10
|
+
* caller passes the registry; each pin inspects every row and reports
|
|
11
|
+
* the rows that violate it.
|
|
12
|
+
*
|
|
13
|
+
* ## The four PORTABLE pins, and the two that are deliberately absent
|
|
14
|
+
*
|
|
15
|
+
* Ruling 5b names six checks. Four are portable — a third-party adopter
|
|
16
|
+
* with their own registry can run them, and they are the four below.
|
|
17
|
+
*
|
|
18
|
+
* Two read repo SOURCE and therefore cannot ship as conformance: the
|
|
19
|
+
* docstring rule ("an agent MUST NOT auto-retry an after-fix refusal
|
|
20
|
+
* whose fixBy is not caller") and the grep for code literals minted
|
|
21
|
+
* outside the registry file. This package publishes only `dist` +
|
|
22
|
+
* `README.md`, so a source walk could never run for an adopter, and the
|
|
23
|
+
* files that rule names live outside this tree entirely. Both live in
|
|
24
|
+
* `@ggui-ai/protocol`'s own suite, where the invariant is owned and the
|
|
25
|
+
* paths are stable. The omission is DECLARED here rather than hidden.
|
|
26
|
+
*
|
|
27
|
+
* ## Why the `fixBy` pin is one-directional
|
|
28
|
+
*
|
|
29
|
+
* The pin below asserts only that an `after-fix` row NAMES who acts. It
|
|
30
|
+
* does not assert `fixBy` is absent elsewhere, because that is a naming
|
|
31
|
+
* discipline rather than a reader-observable obligation: a reader
|
|
32
|
+
* branches on `retry`, and a `fixBy` beside a `never` misleads a human
|
|
33
|
+
* without changing what any implementation does.
|
|
34
|
+
*
|
|
35
|
+
* ggui's own registry is stricter — `fixBy` is REQUIRED on `after-fix`,
|
|
36
|
+
* PERMITTED on `later` (the operator who restores a transiently
|
|
37
|
+
* unavailable surface) and FORBIDDEN on `next-period` and `never` — and
|
|
38
|
+
* enforces it at the type level in its definer, pinned in
|
|
39
|
+
* `@ggui-ai/protocol`'s own suite. Adopters are welcome to that rule;
|
|
40
|
+
* the kit does not make it a conformance obligation. The one thing that
|
|
41
|
+
* must never happen, and IS graded here, is an `after-fix` row that
|
|
42
|
+
* does not say WHO acts — the one an agent could misread as its own to
|
|
43
|
+
* retry.
|
|
44
|
+
*/
|
|
45
|
+
/** The four recognized retry classes (ggui#786 rulings v2 + v4). */
|
|
46
|
+
const RECOGNIZED_RETRIES = [
|
|
47
|
+
'after-fix',
|
|
48
|
+
'next-period',
|
|
49
|
+
'later',
|
|
50
|
+
'never',
|
|
51
|
+
];
|
|
52
|
+
/** Every portable pin the kit ships, in deterministic order. */
|
|
53
|
+
export const registryCompletenessPins = [
|
|
54
|
+
{
|
|
55
|
+
name: 'surfaces-non-empty',
|
|
56
|
+
description: 'Every entry lists at least one surface it is emitted on. A row nobody emits anywhere is a name reserved with no obligation behind it, which is the drift a closed registry exists to prevent. One code MAY list several surfaces — one state can surface in more than one place.',
|
|
57
|
+
violation: (_code, row) => row.surfaces.length > 0 ? null : 'surfaces is empty',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'code-equals-key',
|
|
61
|
+
description: "Every entry's `code` equals the key it is declared under. The key is what emitters look a row up by and the code is what reaches the wire; if they disagree, the registry stops being the single namespace it claims to be.",
|
|
62
|
+
violation: (code, row) => row.code === code ? null : `code '${row.code}' does not equal key '${code}'`,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'after-fix-names-fixby',
|
|
66
|
+
description: "Every `after-fix` entry names WHO acts, in `fixBy`. Without it, 'retry after the fix' cannot be told apart from 'retry now' — and an agent may only act on a fix that is its own. A row whose `retry` is not a recognized class ALSO fails this pin: the obligation is undecidable for it, and a pin that silently passes a row it could not evaluate is a false gate.",
|
|
67
|
+
violation: (_code, row) => {
|
|
68
|
+
if (!RECOGNIZED_RETRIES.includes(row.retry)) {
|
|
69
|
+
return `retry '${row.retry}' is not a recognized class, so the fixBy obligation cannot be evaluated`;
|
|
70
|
+
}
|
|
71
|
+
if (row.retry !== 'after-fix')
|
|
72
|
+
return null;
|
|
73
|
+
return row.fixBy === undefined ? 'after-fix entry carries no fixBy' : null;
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'retry-in-closed-set',
|
|
78
|
+
description: "Every entry's `retry` is one of `after-fix`, `next-period`, `later`, `never`. The class is what a reader branches on to decide whether to wait, act, or stop; an unrecognized value leaves it with no defined behaviour.",
|
|
79
|
+
violation: (_code, row) => RECOGNIZED_RETRIES.includes(row.retry)
|
|
80
|
+
? null
|
|
81
|
+
: `retry '${row.retry}' is outside the closed set`,
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
/**
|
|
85
|
+
* Grade a refusal registry against {@link registryCompletenessPins}.
|
|
86
|
+
*
|
|
87
|
+
* Every pin is applied to every row, in declaration order; a pin passes
|
|
88
|
+
* only when NO row violates it. A conformant registry produces an empty
|
|
89
|
+
* `failed` array.
|
|
90
|
+
*/
|
|
91
|
+
export function runRegistryCompletenessConformance(registry) {
|
|
92
|
+
const entries = Object.entries(registry);
|
|
93
|
+
const passed = [];
|
|
94
|
+
const failed = [];
|
|
95
|
+
for (const pin of registryCompletenessPins) {
|
|
96
|
+
const violations = [];
|
|
97
|
+
for (const [code, row] of entries) {
|
|
98
|
+
const why = pin.violation(code, row);
|
|
99
|
+
if (why !== null)
|
|
100
|
+
violations.push(`${code}: ${why}`);
|
|
101
|
+
}
|
|
102
|
+
if (violations.length === 0) {
|
|
103
|
+
passed.push(pin.name);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
failed.push({ name: pin.name, description: pin.description, violations });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { passed, failed };
|
|
110
|
+
}
|
package/dist/reporter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACf,MAAM,sBAAsB,CAAC;AAM9B,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,sBAA2B,GACnC,mBAAmB,CA0CrB;AAMD,wBAAgB,eAAe,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAejE;AAyBD,wBAAgB,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAW/D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,GAAG,MAAM,CAW9E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,CAStE"}
|