@adhd/apigen-conformance 0.1.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/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { seg, makeOp, ROUND_TRIP_OP, DESCRIPTOR_VECTORS, roundTripOperation, assertOperationEqual, OP_UNSAFE_ACTION, OP_SAFE_QUERY, OP_COLLISION_A, OP_COLLISION_B, OP_DISTINCT_A, OP_DISTINCT_B, NAMING_VECTORS, assertUnsafeIsPost, assertSafeIsGet, assertCollisionDetected, assertNoCollision, ENVELOPE_CASES, ENVELOPE_VECTORS, assertEnvelopeBinding, ERROR_MAPPING_VECTORS, assertErrorMapping, assertApiErrorCodeSurvivesSerialize, VALIDATION_VECTORS, minimalSchemaValidate, runAllVectors, } from './lib/vectors';
2
+ export type { ValidationCase, VectorResult } from './lib/vectors';
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function C(t){return t.words.join("-")}function x(t){return t.words.map(ie).join("")}function V(t){return t.words.join("_")}function _(t,e={}){var u,m;const n=[t.namespace,...t.path],r=t.safe?"GET":"POST",a=((m=(u=e.http)==null?void 0:u.verb)==null?void 0:m[t.id])??r,s="/"+n.map(d=>C(d)).join("/"),p=n.map(d=>V(d)).join("_"),c=n.map(d=>V(d)).slice(0,-1).join("."),f=x(n[n.length-2]??n[0]),h=x(n[n.length-1]),g=n.map(d=>C(d));return{http:{verb:a,route:s},mcp:{name:p},grpc:{package:c,service:f,method:h},cli:{path:g}}}class M extends Error{constructor(e){const n=e.map(r=>` [${r.transport}] "${r.target}" ← ${r.ids[0]} vs ${r.ids[1]}`);super(`apigen-naming: ${e.length} projection collision(s) detected:
2
+ ${n.join(`
3
+ `)}`),this.name="CollisionDetectedError",this.collisions=e}}function z(t,e={}){const n=new Map,r=new Map,a=new Map,s=new Map,p=[];for(const i of t){const c=_(i,e),f=`${c.http.verb} ${c.http.route}`,h=n.get(f);h!==void 0&&h!==i.id?p.push({transport:"http",target:f,ids:[h,i.id]}):n.set(f,i.id);const g=r.get(c.mcp.name);g!==void 0&&g!==i.id?p.push({transport:"mcp",target:c.mcp.name,ids:[g,i.id]}):r.set(c.mcp.name,i.id);const u=`${c.grpc.package}.${c.grpc.service}/${c.grpc.method}`,m=a.get(u);m!==void 0&&m!==i.id?p.push({transport:"grpc",target:u,ids:[m,i.id]}):a.set(u,i.id);const d=c.cli.path.join(" "),N=s.get(d);N!==void 0&&N!==i.id?p.push({transport:"cli",target:d,ids:[N,i.id]}):s.set(d,i.id)}if(p.length>0)throw new M(p)}function L(t,e){return`x-${t}-${e}`}function te(t,e){return`--${t}-${e}`}function ne(t,e){const n=r=>r.toUpperCase().replace(/-/g,"_");return t==="adhd"?`APIGEN_${n(e)}`:`APIGEN_${n(t)}_${n(e)}`}const re=L;function ie(t){return t.length===0?t:t[0].toUpperCase()+t.slice(1)}const R={invalid_argument:400,unauthenticated:401,permission_denied:403,not_found:404,internal:500},D={invalid_argument:"INVALID_ARGUMENT",unauthenticated:"UNAUTHENTICATED",permission_denied:"PERMISSION_DENIED",not_found:"NOT_FOUND",internal:"INTERNAL"},j={invalid_argument:2,unauthenticated:3,permission_denied:3,not_found:4,internal:1},G={invalid_argument:"error",unauthenticated:"error",permission_denied:"error",not_found:"error",internal:"error"};class se extends Error{constructor(e,n,r){super(n),this.name="ApiError",this.code=e,this.details=r,Object.setPrototypeOf(this,new.target.prototype)}toJSON(){const e={code:this.code,message:this.message};return this.details!==void 0&&(e.details=this.details),e}}function o(t,e){return{raw:t,words:e}}function l(t){return{host:"ts",kind:"action",async:!1,streaming:!1,safe:!1,input:{type:"object",properties:{}},output:{type:"object"},envelope:{},typeText:null,...t}}const U=l({id:"transform/humanize/humanize-bytes",host:"ts",namespace:o("transform",["transform"]),path:[o("humanize",["humanize"]),o("humanizeBytes",["humanize","bytes"])],kind:"action",async:!0,streaming:!1,safe:!1,input:{type:"object",properties:{bytes:{type:"number"},precision:{type:"number"}},required:["bytes"]},output:{type:"string"},envelope:{type:"object",properties:{session:{type:"string"}},required:["session"]},typeText:{lang:"ts",input:"{ bytes: number; precision?: number }",output:"string"}});function k(t){return JSON.parse(JSON.stringify(t))}function K(t,e){const n=JSON.stringify(t),r=JSON.stringify(e);if(n===r)return null;const a=Object.keys(t);for(const s of a)if(JSON.stringify(t[s])!==JSON.stringify(e[s]))return`field "${s}" differs: ${JSON.stringify(t[s])} !== ${JSON.stringify(e[s])}`;return"unexpected structural diff (key set mismatch)"}const w=[{id:"descriptor.roundtrip.1",description:"A complete Operation round-trips through JSON serialization losslessly",input:U},{id:"descriptor.roundtrip.2",description:"typeText: null is preserved (not dropped or coerced)",input:l({id:"transform/ping",namespace:o("transform",["transform"]),path:[o("ping",["ping"])],typeText:null})},{id:"descriptor.roundtrip.3",description:"Empty $defs in input schema is preserved",input:l({id:"transform/noop",namespace:o("transform",["transform"]),path:[o("noop",["noop"])],input:{type:"object",$defs:{},properties:{}}})}],T=o("auth",["auth"]),J=o("transform",["transform"]),F=o("session",["session"]),oe=o("login",["login"]),H=o("humanize",["humanize"]),q=o("humanizeBytes",["humanize","bytes"]),E=l({id:"transform/humanize/humanize-bytes",namespace:J,path:[H,q],safe:!1}),b=l({id:"transform/humanize/humanize-bytes-safe",namespace:J,path:[H,q],safe:!0}),A=l({id:"auth/session",namespace:T,path:[F],safe:!1}),P=l({id:"auth/login",namespace:T,path:[o("session",["session"])],safe:!1}),v=l({id:"auth/session",namespace:T,path:[F],safe:!1}),O=l({id:"auth/login",namespace:T,path:[oe],safe:!1});function B(t){const e=_(t);return e.http.verb!=="POST"?`expected verb POST for safe=false, got ${e.http.verb}`:null}function X(t){const e=_(t);return e.http.verb!=="GET"?`expected verb GET for safe=true, got ${e.http.verb}`:null}function Q(t){try{return z(t),"expected CollisionDetectedError to be thrown, but checkCollisions succeeded"}catch(e){return e instanceof M?null:`expected CollisionDetectedError, got: ${String(e)}`}}function S(t){try{return z(t),null}catch(e){return`expected no collision, but got: ${String(e)}`}}const ae=[{id:"naming.verb.1",description:"safe=false → HTTP verb is POST",op:E,expected:{httpVerb:"POST"}},{id:"naming.verb.2",description:"safe=true → HTTP verb is GET",op:b,expected:{httpVerb:"GET"}},{id:"naming.verb.NEGATIVE",description:"safe=false does NOT produce GET (negative control)",op:E,expected:{httpVerbIsNot:"GET"}},{id:"naming.collision.1",description:"Two ids with identical path tokenization → CollisionDetectedError",ops:[A,P],expected:{collision:!0}},{id:"naming.collision.2",description:"Distinct ops do NOT trigger collision",ops:[v,O],expected:{collision:!1}},{id:"naming.collision.NEGATIVE",description:"Non-colliding set must NOT throw (negative control)",ops:[v,O],expected:{collision:!1}}],$=[{pluginId:"auth",field:"session",expectedKey:"x-auth-session",expectedFlag:"--auth-session",expectedEnv:"APIGEN_AUTH_SESSION"},{pluginId:"adhd",field:"trace-id",expectedKey:"x-adhd-trace-id",expectedFlag:"--adhd-trace-id",expectedEnv:"APIGEN_TRACE_ID"},{pluginId:"rate",field:"limit",expectedKey:"x-rate-limit",expectedFlag:"--rate-limit",expectedEnv:"APIGEN_RATE_LIMIT"}];function Y(t,e,n,r,a){const s=L(t,e),p=te(t,e),i=ne(t,e),c=re(t,e);return s!==n?`envelopeKey: expected "${n}", got "${s}"`:p!==r?`envelopeCliFlag: expected "${r}", got "${p}"`:i!==a?`envelopeEnvVar: expected "${a}", got "${i}"`:c!==s?`envelopeMetaKey !== envelopeKey: "${c}" !== "${s}"`:null}const ce=$.map(t=>({id:`envelope.binding.${t.pluginId}.${t.field.replace(/-/g,"_")}`,description:`(${t.pluginId}, ${t.field}) → correct carrier key for all transports`,pluginId:t.pluginId,field:t.field,expected:{httpHeader:t.expectedKey,grpcMeta:t.expectedKey,mcpMetaKey:t.expectedKey,cliFlag:t.expectedFlag,cliEnvVar:t.expectedEnv}})),y=[{id:"error.map.invalid_argument",code:"invalid_argument",expected:{http:400,grpc:"INVALID_ARGUMENT",cli:2,mcp:"error"}},{id:"error.map.unauthenticated",code:"unauthenticated",expected:{http:401,grpc:"UNAUTHENTICATED",cli:3,mcp:"error"}},{id:"error.map.permission_denied",code:"permission_denied",expected:{http:403,grpc:"PERMISSION_DENIED",cli:3,mcp:"error"}},{id:"error.map.not_found",code:"not_found",expected:{http:404,grpc:"NOT_FOUND",cli:4,mcp:"error"}},{id:"error.map.internal",code:"internal",expected:{http:500,grpc:"INTERNAL",cli:1,mcp:"error"}}];function Z(t,e){return R[t]!==e.http?`HTTP_STATUS[${t}]: expected ${e.http}, got ${R[t]}`:D[t]!==e.grpc?`GRPC_CODE[${t}]: expected "${e.grpc}", got "${D[t]}"`:j[t]!==e.cli?`CLI_EXIT_CODE[${t}]: expected ${e.cli}, got ${j[t]}`:G[t]!==e.mcp?`MCP_ERROR_KIND[${t}]: expected "${e.mcp}", got "${G[t]}"`:null}function W(t){const n=new se(t,"test message").toJSON();return n.code!==t?`ApiError.toJSON().code: expected "${t}", got "${n.code}"`:null}const ee=[{id:"validation.extra-properties",description:"additionalProperties not restricted — extra keys pass schema but may cause dispatch errors",schema:{type:"object",properties:{userId:{type:"string"}},required:["userId"]},schemaValidValue:{userId:"abc",unexpectedField:"injection"},domainNote:"An unexpected field passes the schema but may be ignored or cause a typed dispatch error in strict hosts (Rust/Go)."},{id:"validation.number-precision",description:"A JSON number within the schema range but beyond JS safe integer boundary",schema:{type:"object",properties:{amount:{type:"integer"}},required:["amount"]},schemaValidValue:{amount:9007199254740992},domainNote:"Exceeds Number.MAX_SAFE_INTEGER; JSON.parse silently rounds it. SPEC §4 mandates int64 as string-encoded for 64-bit integers; this proves the validator alone cannot catch the precision loss."},{id:"validation.date-string",description:"A date-format string is schema-valid but semantically wrong",schema:{type:"object",properties:{at:{type:"string",format:"date-time"}},required:["at"]},schemaValidValue:{at:"2099-02-30T00:00:00Z"},domainNote:"Standard AJV does not validate date-time values by default; Feb 30 passes schema validation but is a semantically invalid date."},{id:"validation.option-vs-missing",description:"An Option/nullable field being null passes where a domain function expects presence",schema:{type:"object",properties:{user:{oneOf:[{type:"object",properties:{id:{type:"string"}}},{type:"null"}]}},required:["user"]},schemaValidValue:{user:null},domainNote:"null satisfies the oneOf schema, but a domain function that destructures user.id will throw at runtime — validation passed, dispatch fails."}];function I(t,e){if(t.type==="object"){if(typeof e!="object"||e===null)return!1;const n=t.required;if(n){const r=e;for(const a of n)if(!(a in r))return!1}return!0}return e!==void 0}function pe(){const t=[];for(const e of w){const n=k(e.input),r=K(e.input,n);t.push({id:e.id,pass:r===null,error:r??void 0})}{const e=B(E);t.push({id:"naming.verb.1",pass:e===null,error:e??void 0})}{const e=X(b);t.push({id:"naming.verb.2",pass:e===null,error:e??void 0})}{const n=_(E).http.verb!=="GET";t.push({id:"naming.verb.NEGATIVE",pass:n,error:n?void 0:"safe=false produced GET (should be POST)"})}{const e=Q([A,P]);t.push({id:"naming.collision.1",pass:e===null,error:e??void 0})}{const e=S([v,O]);t.push({id:"naming.collision.2",pass:e===null,error:e??void 0})}{const e=S([v,O]);t.push({id:"naming.collision.NEGATIVE",pass:e===null,error:e??void 0})}for(const e of $){const n=`envelope.binding.${e.pluginId}.${e.field.replace(/-/g,"_")}`,r=Y(e.pluginId,e.field,e.expectedKey,e.expectedFlag,e.expectedEnv);t.push({id:n,pass:r===null,error:r??void 0})}for(const e of y){const n=Z(e.code,e.expected);t.push({id:e.id,pass:n===null,error:n??void 0})}for(const e of y){const n=`${e.id}.serialize`,r=W(e.code);t.push({id:n,pass:r===null,error:r??void 0})}for(const e of ee){const n=I(e.schema,e.schemaValidValue);t.push({id:e.id,pass:n,error:n?void 0:`schema validator rejected a schema-valid value (should have accepted): ${JSON.stringify(e.schemaValidValue)}`});const r=`${e.id}.NEGATIVE`,a=!I(e.schema,"not-an-object");t.push({id:r,pass:a,error:a?void 0:'schema validator accepted "not-an-object" as valid for an object schema'})}return t}exports.DESCRIPTOR_VECTORS=w;exports.ENVELOPE_CASES=$;exports.ENVELOPE_VECTORS=ce;exports.ERROR_MAPPING_VECTORS=y;exports.NAMING_VECTORS=ae;exports.OP_COLLISION_A=A;exports.OP_COLLISION_B=P;exports.OP_DISTINCT_A=v;exports.OP_DISTINCT_B=O;exports.OP_SAFE_QUERY=b;exports.OP_UNSAFE_ACTION=E;exports.ROUND_TRIP_OP=U;exports.VALIDATION_VECTORS=ee;exports.assertApiErrorCodeSurvivesSerialize=W;exports.assertCollisionDetected=Q;exports.assertEnvelopeBinding=Y;exports.assertErrorMapping=Z;exports.assertNoCollision=S;exports.assertOperationEqual=K;exports.assertSafeIsGet=X;exports.assertUnsafeIsPost=B;exports.makeOp=l;exports.minimalSchemaValidate=I;exports.roundTripOperation=k;exports.runAllVectors=pe;exports.seg=o;
package/index.mjs ADDED
@@ -0,0 +1,456 @@
1
+ function _(t) {
2
+ return t.words.join("-");
3
+ }
4
+ function O(t) {
5
+ return t.words.map(q).join("");
6
+ }
7
+ function S(t) {
8
+ return t.words.join("_");
9
+ }
10
+ function b(t, e = {}) {
11
+ var u, m;
12
+ const n = [t.namespace, ...t.path], r = t.safe ? "GET" : "POST", o = ((m = (u = e.http) == null ? void 0 : u.verb) == null ? void 0 : m[t.id]) ?? r, s = "/" + n.map((d) => _(d)).join("/"), p = n.map((d) => S(d)).join("_"), a = n.map((d) => S(d)).slice(0, -1).join("."), f = O(n[n.length - 2] ?? n[0]), h = O(n[n.length - 1]), g = n.map((d) => _(d));
13
+ return {
14
+ http: { verb: o, route: s },
15
+ mcp: { name: p },
16
+ grpc: { package: a, service: f, method: h },
17
+ cli: { path: g }
18
+ };
19
+ }
20
+ class j extends Error {
21
+ constructor(e) {
22
+ const n = e.map(
23
+ (r) => ` [${r.transport}] "${r.target}" ← ${r.ids[0]} vs ${r.ids[1]}`
24
+ );
25
+ super(`apigen-naming: ${e.length} projection collision(s) detected:
26
+ ${n.join(`
27
+ `)}`), this.name = "CollisionDetectedError", this.collisions = e;
28
+ }
29
+ }
30
+ function R(t, e = {}) {
31
+ const n = /* @__PURE__ */ new Map(), r = /* @__PURE__ */ new Map(), o = /* @__PURE__ */ new Map(), s = /* @__PURE__ */ new Map(), p = [];
32
+ for (const i of t) {
33
+ const a = b(i, e), f = `${a.http.verb} ${a.http.route}`, h = n.get(f);
34
+ h !== void 0 && h !== i.id ? p.push({ transport: "http", target: f, ids: [h, i.id] }) : n.set(f, i.id);
35
+ const g = r.get(a.mcp.name);
36
+ g !== void 0 && g !== i.id ? p.push({ transport: "mcp", target: a.mcp.name, ids: [g, i.id] }) : r.set(a.mcp.name, i.id);
37
+ const u = `${a.grpc.package}.${a.grpc.service}/${a.grpc.method}`, m = o.get(u);
38
+ m !== void 0 && m !== i.id ? p.push({ transport: "grpc", target: u, ids: [m, i.id] }) : o.set(u, i.id);
39
+ const d = a.cli.path.join(" "), N = s.get(d);
40
+ N !== void 0 && N !== i.id ? p.push({ transport: "cli", target: d, ids: [N, i.id] }) : s.set(d, i.id);
41
+ }
42
+ if (p.length > 0)
43
+ throw new j(p);
44
+ }
45
+ function D(t, e) {
46
+ return `x-${t}-${e}`;
47
+ }
48
+ function L(t, e) {
49
+ return `--${t}-${e}`;
50
+ }
51
+ function F(t, e) {
52
+ const n = (r) => r.toUpperCase().replace(/-/g, "_");
53
+ return t === "adhd" ? `APIGEN_${n(e)}` : `APIGEN_${n(t)}_${n(e)}`;
54
+ }
55
+ const H = D;
56
+ function q(t) {
57
+ return t.length === 0 ? t : t[0].toUpperCase() + t.slice(1);
58
+ }
59
+ const I = {
60
+ invalid_argument: 400,
61
+ unauthenticated: 401,
62
+ permission_denied: 403,
63
+ not_found: 404,
64
+ internal: 500
65
+ }, $ = {
66
+ invalid_argument: "INVALID_ARGUMENT",
67
+ unauthenticated: "UNAUTHENTICATED",
68
+ permission_denied: "PERMISSION_DENIED",
69
+ not_found: "NOT_FOUND",
70
+ internal: "INTERNAL"
71
+ }, x = {
72
+ invalid_argument: 2,
73
+ unauthenticated: 3,
74
+ permission_denied: 3,
75
+ not_found: 4,
76
+ internal: 1
77
+ }, A = {
78
+ invalid_argument: "error",
79
+ unauthenticated: "error",
80
+ permission_denied: "error",
81
+ not_found: "error",
82
+ internal: "error"
83
+ };
84
+ class B extends Error {
85
+ constructor(e, n, r) {
86
+ super(n), this.name = "ApiError", this.code = e, this.details = r, Object.setPrototypeOf(this, new.target.prototype);
87
+ }
88
+ /** Serialise to a plain object suitable for JSON transport. */
89
+ toJSON() {
90
+ const e = {
91
+ code: this.code,
92
+ message: this.message
93
+ };
94
+ return this.details !== void 0 && (e.details = this.details), e;
95
+ }
96
+ }
97
+ function c(t, e) {
98
+ return { raw: t, words: e };
99
+ }
100
+ function l(t) {
101
+ return {
102
+ host: "ts",
103
+ kind: "action",
104
+ async: !1,
105
+ streaming: !1,
106
+ safe: !1,
107
+ input: { type: "object", properties: {} },
108
+ output: { type: "object" },
109
+ envelope: {},
110
+ typeText: null,
111
+ ...t
112
+ };
113
+ }
114
+ const X = l({
115
+ id: "transform/humanize/humanize-bytes",
116
+ host: "ts",
117
+ namespace: c("transform", ["transform"]),
118
+ path: [c("humanize", ["humanize"]), c("humanizeBytes", ["humanize", "bytes"])],
119
+ kind: "action",
120
+ async: !0,
121
+ streaming: !1,
122
+ safe: !1,
123
+ input: {
124
+ type: "object",
125
+ properties: {
126
+ bytes: { type: "number" },
127
+ precision: { type: "number" }
128
+ },
129
+ required: ["bytes"]
130
+ },
131
+ output: { type: "string" },
132
+ envelope: {
133
+ type: "object",
134
+ properties: {
135
+ session: { type: "string" }
136
+ },
137
+ required: ["session"]
138
+ },
139
+ typeText: { lang: "ts", input: "{ bytes: number; precision?: number }", output: "string" }
140
+ });
141
+ function Q(t) {
142
+ return JSON.parse(JSON.stringify(t));
143
+ }
144
+ function Y(t, e) {
145
+ const n = JSON.stringify(t), r = JSON.stringify(e);
146
+ if (n === r)
147
+ return null;
148
+ const o = Object.keys(t);
149
+ for (const s of o)
150
+ if (JSON.stringify(t[s]) !== JSON.stringify(e[s]))
151
+ return `field "${s}" differs: ${JSON.stringify(t[s])} !== ${JSON.stringify(e[s])}`;
152
+ return "unexpected structural diff (key set mismatch)";
153
+ }
154
+ const Z = [
155
+ {
156
+ id: "descriptor.roundtrip.1",
157
+ description: "A complete Operation round-trips through JSON serialization losslessly",
158
+ input: X
159
+ },
160
+ {
161
+ id: "descriptor.roundtrip.2",
162
+ description: "typeText: null is preserved (not dropped or coerced)",
163
+ input: l({
164
+ id: "transform/ping",
165
+ namespace: c("transform", ["transform"]),
166
+ path: [c("ping", ["ping"])],
167
+ typeText: null
168
+ })
169
+ },
170
+ {
171
+ id: "descriptor.roundtrip.3",
172
+ description: "Empty $defs in input schema is preserved",
173
+ input: l({
174
+ id: "transform/noop",
175
+ namespace: c("transform", ["transform"]),
176
+ path: [c("noop", ["noop"])],
177
+ input: { type: "object", $defs: {}, properties: {} }
178
+ })
179
+ }
180
+ ], T = c("auth", ["auth"]), G = c("transform", ["transform"]), z = c("session", ["session"]), W = c("login", ["login"]), M = c("humanize", ["humanize"]), K = c("humanizeBytes", ["humanize", "bytes"]), v = l({
181
+ id: "transform/humanize/humanize-bytes",
182
+ namespace: G,
183
+ path: [M, K],
184
+ safe: !1
185
+ }), U = l({
186
+ id: "transform/humanize/humanize-bytes-safe",
187
+ namespace: G,
188
+ path: [M, K],
189
+ safe: !0
190
+ }), k = l({
191
+ id: "auth/session",
192
+ namespace: T,
193
+ path: [z],
194
+ safe: !1
195
+ }), w = l({
196
+ id: "auth/login",
197
+ namespace: T,
198
+ path: [c("session", ["session"])],
199
+ // same words as segSession → collision
200
+ safe: !1
201
+ }), E = l({
202
+ id: "auth/session",
203
+ namespace: T,
204
+ path: [z],
205
+ safe: !1
206
+ }), y = l({
207
+ id: "auth/login",
208
+ namespace: T,
209
+ path: [W],
210
+ safe: !1
211
+ });
212
+ function ee(t) {
213
+ const e = b(t);
214
+ return e.http.verb !== "POST" ? `expected verb POST for safe=false, got ${e.http.verb}` : null;
215
+ }
216
+ function te(t) {
217
+ const e = b(t);
218
+ return e.http.verb !== "GET" ? `expected verb GET for safe=true, got ${e.http.verb}` : null;
219
+ }
220
+ function ne(t) {
221
+ try {
222
+ return R(t), "expected CollisionDetectedError to be thrown, but checkCollisions succeeded";
223
+ } catch (e) {
224
+ return e instanceof j ? null : `expected CollisionDetectedError, got: ${String(e)}`;
225
+ }
226
+ }
227
+ function P(t) {
228
+ try {
229
+ return R(t), null;
230
+ } catch (e) {
231
+ return `expected no collision, but got: ${String(e)}`;
232
+ }
233
+ }
234
+ const ae = [
235
+ {
236
+ id: "naming.verb.1",
237
+ description: "safe=false → HTTP verb is POST",
238
+ op: v,
239
+ expected: { httpVerb: "POST" }
240
+ },
241
+ {
242
+ id: "naming.verb.2",
243
+ description: "safe=true → HTTP verb is GET",
244
+ op: U,
245
+ expected: { httpVerb: "GET" }
246
+ },
247
+ {
248
+ id: "naming.verb.NEGATIVE",
249
+ description: "safe=false does NOT produce GET (negative control)",
250
+ op: v,
251
+ expected: { httpVerbIsNot: "GET" }
252
+ },
253
+ {
254
+ id: "naming.collision.1",
255
+ description: "Two ids with identical path tokenization → CollisionDetectedError",
256
+ ops: [k, w],
257
+ expected: { collision: !0 }
258
+ },
259
+ {
260
+ id: "naming.collision.2",
261
+ description: "Distinct ops do NOT trigger collision",
262
+ ops: [E, y],
263
+ expected: { collision: !1 }
264
+ },
265
+ {
266
+ id: "naming.collision.NEGATIVE",
267
+ description: "Non-colliding set must NOT throw (negative control)",
268
+ ops: [E, y],
269
+ expected: { collision: !1 }
270
+ }
271
+ ], J = [
272
+ { pluginId: "auth", field: "session", expectedKey: "x-auth-session", expectedFlag: "--auth-session", expectedEnv: "APIGEN_AUTH_SESSION" },
273
+ { pluginId: "adhd", field: "trace-id", expectedKey: "x-adhd-trace-id", expectedFlag: "--adhd-trace-id", expectedEnv: "APIGEN_TRACE_ID" },
274
+ { pluginId: "rate", field: "limit", expectedKey: "x-rate-limit", expectedFlag: "--rate-limit", expectedEnv: "APIGEN_RATE_LIMIT" }
275
+ ];
276
+ function re(t, e, n, r, o) {
277
+ const s = D(t, e), p = L(t, e), i = F(t, e), a = H(t, e);
278
+ return s !== n ? `envelopeKey: expected "${n}", got "${s}"` : p !== r ? `envelopeCliFlag: expected "${r}", got "${p}"` : i !== o ? `envelopeEnvVar: expected "${o}", got "${i}"` : a !== s ? `envelopeMetaKey !== envelopeKey: "${a}" !== "${s}"` : null;
279
+ }
280
+ const ce = J.map((t) => ({
281
+ id: `envelope.binding.${t.pluginId}.${t.field.replace(/-/g, "_")}`,
282
+ description: `(${t.pluginId}, ${t.field}) → correct carrier key for all transports`,
283
+ pluginId: t.pluginId,
284
+ field: t.field,
285
+ expected: {
286
+ httpHeader: t.expectedKey,
287
+ grpcMeta: t.expectedKey,
288
+ mcpMetaKey: t.expectedKey,
289
+ cliFlag: t.expectedFlag,
290
+ cliEnvVar: t.expectedEnv
291
+ }
292
+ })), C = [
293
+ { id: "error.map.invalid_argument", code: "invalid_argument", expected: { http: 400, grpc: "INVALID_ARGUMENT", cli: 2, mcp: "error" } },
294
+ { id: "error.map.unauthenticated", code: "unauthenticated", expected: { http: 401, grpc: "UNAUTHENTICATED", cli: 3, mcp: "error" } },
295
+ { id: "error.map.permission_denied", code: "permission_denied", expected: { http: 403, grpc: "PERMISSION_DENIED", cli: 3, mcp: "error" } },
296
+ { id: "error.map.not_found", code: "not_found", expected: { http: 404, grpc: "NOT_FOUND", cli: 4, mcp: "error" } },
297
+ { id: "error.map.internal", code: "internal", expected: { http: 500, grpc: "INTERNAL", cli: 1, mcp: "error" } }
298
+ ];
299
+ function ie(t, e) {
300
+ return I[t] !== e.http ? `HTTP_STATUS[${t}]: expected ${e.http}, got ${I[t]}` : $[t] !== e.grpc ? `GRPC_CODE[${t}]: expected "${e.grpc}", got "${$[t]}"` : x[t] !== e.cli ? `CLI_EXIT_CODE[${t}]: expected ${e.cli}, got ${x[t]}` : A[t] !== e.mcp ? `MCP_ERROR_KIND[${t}]: expected "${e.mcp}", got "${A[t]}"` : null;
301
+ }
302
+ function se(t) {
303
+ const n = new B(t, "test message").toJSON();
304
+ return n.code !== t ? `ApiError.toJSON().code: expected "${t}", got "${n.code}"` : null;
305
+ }
306
+ const oe = [
307
+ {
308
+ id: "validation.extra-properties",
309
+ description: "additionalProperties not restricted — extra keys pass schema but may cause dispatch errors",
310
+ schema: {
311
+ type: "object",
312
+ properties: { userId: { type: "string" } },
313
+ required: ["userId"]
314
+ },
315
+ schemaValidValue: { userId: "abc", unexpectedField: "injection" },
316
+ domainNote: "An unexpected field passes the schema but may be ignored or cause a typed dispatch error in strict hosts (Rust/Go)."
317
+ },
318
+ {
319
+ id: "validation.number-precision",
320
+ description: "A JSON number within the schema range but beyond JS safe integer boundary",
321
+ schema: {
322
+ type: "object",
323
+ properties: { amount: { type: "integer" } },
324
+ required: ["amount"]
325
+ },
326
+ // This exceeds Number.MAX_SAFE_INTEGER — JSON.parse will silently corrupt it.
327
+ schemaValidValue: { amount: 9007199254740992 },
328
+ domainNote: "Exceeds Number.MAX_SAFE_INTEGER; JSON.parse silently rounds it. SPEC §4 mandates int64 as string-encoded for 64-bit integers; this proves the validator alone cannot catch the precision loss."
329
+ },
330
+ {
331
+ id: "validation.date-string",
332
+ description: "A date-format string is schema-valid but semantically wrong",
333
+ schema: {
334
+ type: "object",
335
+ properties: { at: { type: "string", format: "date-time" } },
336
+ required: ["at"]
337
+ },
338
+ schemaValidValue: { at: "2099-02-30T00:00:00Z" },
339
+ // Feb 30 does not exist
340
+ domainNote: "Standard AJV does not validate date-time values by default; Feb 30 passes schema validation but is a semantically invalid date."
341
+ },
342
+ {
343
+ id: "validation.option-vs-missing",
344
+ description: "An Option/nullable field being null passes where a domain function expects presence",
345
+ schema: {
346
+ type: "object",
347
+ properties: {
348
+ user: { oneOf: [{ type: "object", properties: { id: { type: "string" } } }, { type: "null" }] }
349
+ },
350
+ required: ["user"]
351
+ },
352
+ schemaValidValue: { user: null },
353
+ domainNote: "null satisfies the oneOf schema, but a domain function that destructures user.id will throw at runtime — validation passed, dispatch fails."
354
+ }
355
+ ];
356
+ function V(t, e) {
357
+ if (t.type === "object") {
358
+ if (typeof e != "object" || e === null)
359
+ return !1;
360
+ const n = t.required;
361
+ if (n) {
362
+ const r = e;
363
+ for (const o of n)
364
+ if (!(o in r))
365
+ return !1;
366
+ }
367
+ return !0;
368
+ }
369
+ return e !== void 0;
370
+ }
371
+ function pe() {
372
+ const t = [];
373
+ for (const e of Z) {
374
+ const n = Q(e.input), r = Y(e.input, n);
375
+ t.push({ id: e.id, pass: r === null, error: r ?? void 0 });
376
+ }
377
+ {
378
+ const e = ee(v);
379
+ t.push({ id: "naming.verb.1", pass: e === null, error: e ?? void 0 });
380
+ }
381
+ {
382
+ const e = te(U);
383
+ t.push({ id: "naming.verb.2", pass: e === null, error: e ?? void 0 });
384
+ }
385
+ {
386
+ const n = b(v).http.verb !== "GET";
387
+ t.push({ id: "naming.verb.NEGATIVE", pass: n, error: n ? void 0 : "safe=false produced GET (should be POST)" });
388
+ }
389
+ {
390
+ const e = ne([k, w]);
391
+ t.push({ id: "naming.collision.1", pass: e === null, error: e ?? void 0 });
392
+ }
393
+ {
394
+ const e = P([E, y]);
395
+ t.push({ id: "naming.collision.2", pass: e === null, error: e ?? void 0 });
396
+ }
397
+ {
398
+ const e = P([E, y]);
399
+ t.push({ id: "naming.collision.NEGATIVE", pass: e === null, error: e ?? void 0 });
400
+ }
401
+ for (const e of J) {
402
+ const n = `envelope.binding.${e.pluginId}.${e.field.replace(/-/g, "_")}`, r = re(e.pluginId, e.field, e.expectedKey, e.expectedFlag, e.expectedEnv);
403
+ t.push({ id: n, pass: r === null, error: r ?? void 0 });
404
+ }
405
+ for (const e of C) {
406
+ const n = ie(e.code, e.expected);
407
+ t.push({ id: e.id, pass: n === null, error: n ?? void 0 });
408
+ }
409
+ for (const e of C) {
410
+ const n = `${e.id}.serialize`, r = se(e.code);
411
+ t.push({ id: n, pass: r === null, error: r ?? void 0 });
412
+ }
413
+ for (const e of oe) {
414
+ const n = V(e.schema, e.schemaValidValue);
415
+ t.push({
416
+ id: e.id,
417
+ pass: n,
418
+ error: n ? void 0 : `schema validator rejected a schema-valid value (should have accepted): ${JSON.stringify(e.schemaValidValue)}`
419
+ });
420
+ const r = `${e.id}.NEGATIVE`, o = !V(e.schema, "not-an-object");
421
+ t.push({
422
+ id: r,
423
+ pass: o,
424
+ error: o ? void 0 : 'schema validator accepted "not-an-object" as valid for an object schema'
425
+ });
426
+ }
427
+ return t;
428
+ }
429
+ export {
430
+ Z as DESCRIPTOR_VECTORS,
431
+ J as ENVELOPE_CASES,
432
+ ce as ENVELOPE_VECTORS,
433
+ C as ERROR_MAPPING_VECTORS,
434
+ ae as NAMING_VECTORS,
435
+ k as OP_COLLISION_A,
436
+ w as OP_COLLISION_B,
437
+ E as OP_DISTINCT_A,
438
+ y as OP_DISTINCT_B,
439
+ U as OP_SAFE_QUERY,
440
+ v as OP_UNSAFE_ACTION,
441
+ X as ROUND_TRIP_OP,
442
+ oe as VALIDATION_VECTORS,
443
+ se as assertApiErrorCodeSurvivesSerialize,
444
+ ne as assertCollisionDetected,
445
+ re as assertEnvelopeBinding,
446
+ ie as assertErrorMapping,
447
+ P as assertNoCollision,
448
+ Y as assertOperationEqual,
449
+ te as assertSafeIsGet,
450
+ ee as assertUnsafeIsPost,
451
+ l as makeOp,
452
+ V as minimalSchemaValidate,
453
+ Q as roundTripOperation,
454
+ pe as runAllVectors,
455
+ c as seg
456
+ };
@@ -0,0 +1,221 @@
1
+ import { ApiErrorCode } from '@adhd/apigen-errors';
2
+ import { Operation, Segment } from '@adhd/apigen-core';
3
+
4
+ /** Build a casing-neutral Segment from a raw token and its word list. */
5
+ export declare function seg(raw: string, words: string[]): Segment;
6
+ /**
7
+ * Build a minimal but complete Operation.
8
+ *
9
+ * Only the fields tested by a given vector are semantically meaningful;
10
+ * the rest are set to neutral defaults. This mirrors the approach used in
11
+ * `packages/apigen/naming/src/test/naming.spec.ts`.
12
+ */
13
+ export declare function makeOp(overrides: Partial<Operation> & Pick<Operation, 'id' | 'namespace' | 'path'>): Operation;
14
+ /** The reference Operation used across descriptor round-trip vectors. */
15
+ export declare const ROUND_TRIP_OP: Operation;
16
+ /**
17
+ * Serialize an Operation to a JSON string and deserialize it back.
18
+ *
19
+ * A host runner uses this to prove the descriptor survives its IPC wire
20
+ * (§14: extractor subprocess emits JSON; the CLI merges it). The round-trip
21
+ * MUST be lossless for all spec-defined fields.
22
+ */
23
+ export declare function roundTripOperation(op: Operation): Operation;
24
+ /**
25
+ * Assert that two Operations are deep-equal on every SPEC §4 field.
26
+ *
27
+ * Returns `null` on success; returns a string describing the first mismatch
28
+ * on failure (so non-TS hosts can surface useful diagnostics).
29
+ */
30
+ export declare function assertOperationEqual(a: Operation, b: Operation): null | string;
31
+ export declare const DESCRIPTOR_VECTORS: readonly [{
32
+ readonly id: "descriptor.roundtrip.1";
33
+ readonly description: "A complete Operation round-trips through JSON serialization losslessly";
34
+ readonly input: Operation;
35
+ }, {
36
+ readonly id: "descriptor.roundtrip.2";
37
+ readonly description: "typeText: null is preserved (not dropped or coerced)";
38
+ readonly input: Operation;
39
+ }, {
40
+ readonly id: "descriptor.roundtrip.3";
41
+ readonly description: "Empty $defs in input schema is preserved";
42
+ readonly input: Operation;
43
+ }];
44
+ /** unsafe action (safe=false): POST /transform/humanize/humanize-bytes */
45
+ export declare const OP_UNSAFE_ACTION: Operation;
46
+ /** safe query (safe=true): GET /transform/humanize/humanize-bytes */
47
+ export declare const OP_SAFE_QUERY: Operation;
48
+ /**
49
+ * Two operations whose `path` tokenization is identical — they will collide
50
+ * in every transport. Used for the collision negative-control.
51
+ *
52
+ * Op A: id='auth/session' path=[session]
53
+ * Op B: id='auth/login' path=[session] ← same segment words despite different id
54
+ */
55
+ export declare const OP_COLLISION_A: Operation;
56
+ export declare const OP_COLLISION_B: Operation;
57
+ export declare const OP_DISTINCT_A: Operation;
58
+ export declare const OP_DISTINCT_B: Operation;
59
+ /** Assert that an Operation with safe=false projects to POST over HTTP. */
60
+ export declare function assertUnsafeIsPost(op: Operation): null | string;
61
+ /** Assert that an Operation with safe=true projects to GET over HTTP. */
62
+ export declare function assertSafeIsGet(op: Operation): null | string;
63
+ /**
64
+ * Assert that checkCollisions throws CollisionDetectedError for a colliding pair.
65
+ *
66
+ * Returns `null` on success (i.e. the error WAS thrown); returns a diagnostic
67
+ * string if the expected error was NOT thrown.
68
+ */
69
+ export declare function assertCollisionDetected(ops: Operation[]): null | string;
70
+ /**
71
+ * Assert that checkCollisions does NOT throw for a non-colliding set.
72
+ */
73
+ export declare function assertNoCollision(ops: Operation[]): null | string;
74
+ export declare const NAMING_VECTORS: readonly [{
75
+ readonly id: "naming.verb.1";
76
+ readonly description: "safe=false → HTTP verb is POST";
77
+ readonly op: Operation;
78
+ readonly expected: {
79
+ readonly httpVerb: "POST";
80
+ };
81
+ }, {
82
+ readonly id: "naming.verb.2";
83
+ readonly description: "safe=true → HTTP verb is GET";
84
+ readonly op: Operation;
85
+ readonly expected: {
86
+ readonly httpVerb: "GET";
87
+ };
88
+ }, {
89
+ readonly id: "naming.verb.NEGATIVE";
90
+ readonly description: "safe=false does NOT produce GET (negative control)";
91
+ readonly op: Operation;
92
+ readonly expected: {
93
+ readonly httpVerbIsNot: "GET";
94
+ };
95
+ }, {
96
+ readonly id: "naming.collision.1";
97
+ readonly description: "Two ids with identical path tokenization → CollisionDetectedError";
98
+ readonly ops: readonly [Operation, Operation];
99
+ readonly expected: {
100
+ readonly collision: true;
101
+ };
102
+ }, {
103
+ readonly id: "naming.collision.2";
104
+ readonly description: "Distinct ops do NOT trigger collision";
105
+ readonly ops: readonly [Operation, Operation];
106
+ readonly expected: {
107
+ readonly collision: false;
108
+ };
109
+ }, {
110
+ readonly id: "naming.collision.NEGATIVE";
111
+ readonly description: "Non-colliding set must NOT throw (negative control)";
112
+ readonly ops: readonly [Operation, Operation];
113
+ readonly expected: {
114
+ readonly collision: false;
115
+ };
116
+ }];
117
+ /** Representative envelope field cases. */
118
+ export declare const ENVELOPE_CASES: readonly [{
119
+ readonly pluginId: "auth";
120
+ readonly field: "session";
121
+ readonly expectedKey: "x-auth-session";
122
+ readonly expectedFlag: "--auth-session";
123
+ readonly expectedEnv: "APIGEN_AUTH_SESSION";
124
+ }, {
125
+ readonly pluginId: "adhd";
126
+ readonly field: "trace-id";
127
+ readonly expectedKey: "x-adhd-trace-id";
128
+ readonly expectedFlag: "--adhd-trace-id";
129
+ readonly expectedEnv: "APIGEN_TRACE_ID";
130
+ }, {
131
+ readonly pluginId: "rate";
132
+ readonly field: "limit";
133
+ readonly expectedKey: "x-rate-limit";
134
+ readonly expectedFlag: "--rate-limit";
135
+ readonly expectedEnv: "APIGEN_RATE_LIMIT";
136
+ }];
137
+ /**
138
+ * Assert envelope binding for a single (pluginId, field) pair.
139
+ *
140
+ * Returns `null` on full success; returns a diagnostic string on the first failure.
141
+ */
142
+ export declare function assertEnvelopeBinding(pluginId: string, field: string, expectedKey: string, expectedFlag: string, expectedEnv: string): null | string;
143
+ export declare const ENVELOPE_VECTORS: {
144
+ id: string;
145
+ description: string;
146
+ pluginId: "adhd" | "auth" | "rate";
147
+ field: "session" | "trace-id" | "limit";
148
+ expected: {
149
+ httpHeader: "x-auth-session" | "x-adhd-trace-id" | "x-rate-limit";
150
+ grpcMeta: "x-auth-session" | "x-adhd-trace-id" | "x-rate-limit";
151
+ mcpMetaKey: "x-auth-session" | "x-adhd-trace-id" | "x-rate-limit";
152
+ cliFlag: "--auth-session" | "--adhd-trace-id" | "--rate-limit";
153
+ cliEnvVar: "APIGEN_AUTH_SESSION" | "APIGEN_TRACE_ID" | "APIGEN_RATE_LIMIT";
154
+ };
155
+ }[];
156
+ /** The normative §9 mapping table expressed as plain data. */
157
+ export declare const ERROR_MAPPING_VECTORS: Array<{
158
+ id: string;
159
+ code: ApiErrorCode;
160
+ expected: {
161
+ http: number;
162
+ grpc: string;
163
+ cli: number;
164
+ mcp: 'error';
165
+ };
166
+ }>;
167
+ /**
168
+ * Assert that a given ApiErrorCode maps to the expected transport statuses.
169
+ *
170
+ * Returns `null` on success; diagnostic string on first failure.
171
+ */
172
+ export declare function assertErrorMapping(code: ApiErrorCode, expected: {
173
+ http: number;
174
+ grpc: string;
175
+ cli: number;
176
+ mcp: 'error';
177
+ }): null | string;
178
+ /**
179
+ * Assert that ApiError is constructible with the given code and that its code
180
+ * field survives serialization (toJSON) — the carrier shape shipped over every
181
+ * transport must round-trip the code.
182
+ */
183
+ export declare function assertApiErrorCodeSurvivesSerialize(code: ApiErrorCode): null | string;
184
+ /**
185
+ * A validation case: a JSON Schema + a value that is schema-valid.
186
+ * A host validator MUST accept this value without error.
187
+ *
188
+ * The `domainNote` explains why the value is still "wrong" at the domain level —
189
+ * this is the "necessary-not-sufficient" proof.
190
+ */
191
+ export interface ValidationCase {
192
+ id: string;
193
+ description: string;
194
+ schema: Record<string, unknown>;
195
+ /** A value that satisfies the JSON Schema but may be wrong at domain level. */
196
+ schemaValidValue: unknown;
197
+ /** Why the value is still problematic despite passing schema validation. */
198
+ domainNote: string;
199
+ }
200
+ export declare const VALIDATION_VECTORS: ValidationCase[];
201
+ /**
202
+ * A minimal JSON-Schema validator that checks the type keyword and required
203
+ * fields only — sufficient to prove "schema-valid-but-domain-wrong" for the
204
+ * vectors above without pulling in a full AJV dependency.
205
+ *
206
+ * Returns `true` if the value passes this minimal check, `false` otherwise.
207
+ *
208
+ * HOST RUNNERS: replace this with your host's real schema validator (AJV /
209
+ * pydantic / schemars). The point is that the real validator ALSO accepts
210
+ * these values, so the host's typed dispatch is the authoritative gate.
211
+ */
212
+ export declare function minimalSchemaValidate(schema: Record<string, unknown>, value: unknown): boolean;
213
+ /** A single vector result. */
214
+ export interface VectorResult {
215
+ id: string;
216
+ pass: boolean;
217
+ /** Populated on failure with a diagnostic string. */
218
+ error?: string;
219
+ }
220
+ /** Execute all conformance vectors and return per-vector results. */
221
+ export declare function runAllVectors(): VectorResult[];
package/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@adhd/apigen-conformance",
3
+ "version": "0.1.0",
4
+ "main": "./index.js",
5
+ "module": "./index.mjs",
6
+ "typings": "./index.d.ts",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ }
10
+ }