@aventara/testing 0.0.0-stage → 0.1.0-pilot.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.
@@ -0,0 +1,998 @@
1
+ import { CORPUS_AUTHOR, CORPUS_CLIENT_CONTRACT_DOCUMENT, CORPUS_CLIENT_CONTRACT_HASH, CORPUS_NOBODY, CORPUS_NONE_CLIENT_CONTRACT_HASH, CORPUS_TRIGGER, } from "./corpus-contract.fixture.js";
2
+ /**
3
+ * The Phase 10 protocol fixture corpus: passive data, read by the coverage
4
+ * gate (`protocol-coverage.spec.ts`) and, from later slices on, driven
5
+ * through each host driver. Each slice adds the rows it settles and removes
6
+ * what they cover from the gate's awaiting ledger.
7
+ */
8
+ /**
9
+ * A well-formed generated-client request. A family-11 row's answer does not
10
+ * depend on it: the answer is what arrived instead of a framework envelope.
11
+ */
12
+ const OPERATION_REQUEST = {
13
+ method: "POST",
14
+ path: "/_resources/authors/find/many",
15
+ headers: {
16
+ "Content-Type": "application/json",
17
+ "Aventara-Protocol-Version": "1",
18
+ "Aventara-Contract-Hash": `sha256:${"0".repeat(64)}`,
19
+ },
20
+ body: { kind: "raw", content: "{}" },
21
+ };
22
+ /**
23
+ * Family 11 — transport errors (§13.5; R5). Each row is a response that is NOT
24
+ * a framework envelope — produced by a proxy, a broken host or the network,
25
+ * never by the framework — so a client raises `TransportError`, never a
26
+ * `FrameworkError`. `AvProtocol.isOperationResponse` refuses every row's body
27
+ * (`envelope-guard.spec.ts`), and the generated client's emitted check is held
28
+ * to agree with it row by row (`packages/client`, the envelope pin).
29
+ *
30
+ * Every label begins `transport-error:`. These rows describe what a client
31
+ * receives, not what a host must answer, so a host driver does not drive them:
32
+ * it selects its rows by `client.outcome`, skipping every row whose outcome is
33
+ * `"transport-error"` (the director, 2026-10-05) — never by label or by
34
+ * `server.code === null`. `adapterCalls` is `0` because the framework produced
35
+ * none of these answers.
36
+ */
37
+ const transportError = (label, status, bodyBytes) => ({
38
+ label: `transport-error: ${label}`,
39
+ request: OPERATION_REQUEST,
40
+ server: {
41
+ status,
42
+ code: null,
43
+ adapterCalls: 0,
44
+ ...(bodyBytes === undefined ? {} : { bodyBytes }),
45
+ },
46
+ client: { outcome: "transport-error" },
47
+ });
48
+ const envelopeText = (code, data, cause) => JSON.stringify({ data, code, cause });
49
+ const TRANSPORT_ERRORS = [
50
+ transportError("502 with an HTML page", 502, "<html><body><h1>502 Bad Gateway</h1></body></html>"),
51
+ transportError("200 with JSON of another shape", 200, '{"ok":true}'),
52
+ transportError("connection failure — no response arrived", 0),
53
+ transportError("a truncated body", 200, '{"data":null,"code":"A10'),
54
+ transportError("an empty body", 204, ""),
55
+ transportError("a JSON array", 200, "[]"),
56
+ transportError("JSON null", 200, "null"),
57
+ transportError("a JSON string", 200, '"A1000"'),
58
+ transportError("a code this protocol does not allocate", 400, envelopeText("A9999", null, { message: "x" })),
59
+ transportError("a code that is not a string", 200, '{"data":null,"code":1000,"cause":null}'),
60
+ transportError("no data member", 200, '{"code":"A1000","cause":null}'),
61
+ transportError("no cause member", 200, '{"data":1,"code":"A1000"}'),
62
+ transportError("a success code with a cause", 200, envelopeText("A1000", 1, { message: "x" })),
63
+ transportError("A1001 with data", 200, envelopeText("A1001", { id: 1 }, null)),
64
+ transportError("an error code with a null cause", 404, envelopeText("A2003", null, null)),
65
+ transportError("an error code with data", 404, envelopeText("A2003", { id: 1 }, { message: "x" })),
66
+ transportError("a cause whose message is not a string", 422, envelopeText("A2004", null, { message: 5 })),
67
+ transportError("a cause that is a string", 422, envelopeText("A2004", null, "Invalid.")),
68
+ transportError("issues that are not an array", 422, envelopeText("A2004", null, { message: "x", issues: {} })),
69
+ transportError("an issue whose code is not a validation code", 422, envelopeText("A2004", null, {
70
+ message: "x",
71
+ issues: [{ code: "V9999", message: "y" }],
72
+ })),
73
+ transportError("an issue whose message is not a string", 422, envelopeText("A2004", null, { message: "x", issues: [{ code: "V1000" }] })),
74
+ transportError("an issue whose path is not an array", 422, envelopeText("A2004", null, {
75
+ message: "x",
76
+ issues: [{ path: "a.b", code: "V1000", message: "y" }],
77
+ })),
78
+ transportError("an issue whose path holds an object", 422, envelopeText("A2004", null, {
79
+ message: "x",
80
+ issues: [{ path: [{}], code: "V1000", message: "y" }],
81
+ })),
82
+ transportError("an issue whose path holds a fraction", 422, envelopeText("A2004", null, {
83
+ message: "x",
84
+ issues: [{ path: [0.5], code: "V1000", message: "y" }],
85
+ })),
86
+ transportError("an operation index that is a string", 409, envelopeText("A2008", null, { message: "x", operation: "1" })),
87
+ transportError("a negative operation index", 409, envelopeText("A2008", null, { message: "x", operation: -1 })),
88
+ transportError("a fractional operation index", 409, envelopeText("A2008", null, { message: "x", operation: 0.5 })),
89
+ ];
90
+ /**
91
+ * A generated client's request to the corpus Framework for one operation of
92
+ * `authors` (`corpus-contract.fixture.ts`): its identity headers name the
93
+ * corpus ClientContract, and it sends its own `Aventara-Request-Id`, so the
94
+ * echo every framework answer carries (Q13) is a fixed fact of the row.
95
+ */
96
+ const corpusRequest = (operation, body, requestId, overrides = {}) => ({
97
+ method: "POST",
98
+ path: `/_resources/authors/${operation}`,
99
+ headers: {
100
+ "Content-Type": "application/json",
101
+ "Aventara-Protocol-Version": "1",
102
+ "Aventara-Contract-Hash": CORPUS_CLIENT_CONTRACT_HASH,
103
+ "Aventara-Request-Id": requestId,
104
+ },
105
+ body: { kind: "raw", content: JSON.stringify(body) },
106
+ ...overrides,
107
+ });
108
+ /** The framework's own headers on an answer to a request that sent `requestId`. */
109
+ const answeredHeaders = (requestId) => ({
110
+ "Content-Type": "application/json",
111
+ "Aventara-Request-Id": requestId,
112
+ });
113
+ /**
114
+ * Family 2 — success statuses (§13.2; plan S8). Every standard operation
115
+ * through a found route: its success code at its status — `201` exactly for
116
+ * `A1002` and `A1003`, `200` otherwise, `upsert.unique`'s `A1008` included —
117
+ * with `cause: null`, the result in wire form, one Adapter call, and no
118
+ * `Location` (P2: the framework's two headers are all it writes). The label
119
+ * begins with the operation's name, which is how the coverage gate reads a
120
+ * success row.
121
+ */
122
+ const success = (operation, body, status, code, data, label = "") => {
123
+ const requestId = `corpus-${operation.replace(".", "-")}${label === "" ? "" : "-miss"}`;
124
+ return {
125
+ label: `${operation}${label === "" ? "" : ` ${label}`}`,
126
+ request: corpusRequest(operation.replace(".", "/"), body, requestId),
127
+ server: {
128
+ status,
129
+ code,
130
+ adapterCalls: 1,
131
+ responseHeaders: answeredHeaders(requestId),
132
+ bodyBytes: JSON.stringify({ data, code, cause: null }),
133
+ },
134
+ client: { outcome: "resolve" },
135
+ };
136
+ };
137
+ const WHERE = { where: { id: CORPUS_AUTHOR.id } };
138
+ const DATA = { name: CORPUS_AUTHOR.name };
139
+ const SUCCESS_STATUSES = [
140
+ success("find.first", WHERE, 200, "A1000", CORPUS_AUTHOR),
141
+ success("find.first", { where: { id: CORPUS_NOBODY } }, 200, "A1001", null, "matching nothing → 200 A1001 with null data"),
142
+ success("find.unique", WHERE, 200, "A1000", CORPUS_AUTHOR),
143
+ success("find.many", {}, 200, "A1000", [CORPUS_AUTHOR]),
144
+ success("find.count", {}, 200, "A1000", 2),
145
+ success("create.one", { data: DATA }, 201, "A1002", CORPUS_AUTHOR),
146
+ success("create.many", { data: [DATA] }, 201, "A1003", [CORPUS_AUTHOR]),
147
+ success("create.count", { data: [DATA, DATA] }, 201, "A1003", 2),
148
+ success("update.first", { ...WHERE, data: DATA }, 200, "A1004", CORPUS_AUTHOR),
149
+ success("update.unique", { ...WHERE, data: DATA }, 200, "A1004", CORPUS_AUTHOR),
150
+ success("update.many", { ...WHERE, data: DATA }, 200, "A1005", [
151
+ CORPUS_AUTHOR,
152
+ ]),
153
+ success("update.count", { ...WHERE, data: DATA }, 200, "A1005", 2),
154
+ success("delete.first", WHERE, 200, "A1006", CORPUS_AUTHOR),
155
+ success("delete.unique", WHERE, 200, "A1006", CORPUS_AUTHOR),
156
+ success("delete.many", WHERE, 200, "A1007", [CORPUS_AUTHOR]),
157
+ success("delete.count", WHERE, 200, "A1007", 2),
158
+ success("upsert.unique", { ...WHERE, create: DATA, update: DATA }, 200, "A1008", CORPUS_AUTHOR),
159
+ ];
160
+ /**
161
+ * F-828 (plan S12) — a computed field added under `client.fields` is read
162
+ * through the HTTP surface like one added at root: `books.label`, VIRTUAL,
163
+ * computed from `id` (`corpus-contract.fixture.ts`). It is in the default read,
164
+ * and an explicit `select` of it answers it alone; the Adapter is asked only for
165
+ * stored columns (core's `computed-read-runtime.scope-added-field.spec.ts`).
166
+ */
167
+ const scopeAdded = (label, body, requestId, data) => ({
168
+ label: `scope-added: ${label}`,
169
+ request: corpusRequest("find/many", body, requestId, {
170
+ path: "/_resources/books/find/many",
171
+ }),
172
+ server: {
173
+ status: 200,
174
+ code: "A1000",
175
+ adapterCalls: 1,
176
+ responseHeaders: answeredHeaders(requestId),
177
+ bodyBytes: JSON.stringify({ data, code: "A1000", cause: null }),
178
+ },
179
+ client: { outcome: "resolve" },
180
+ });
181
+ const SCOPE_ADDED = [
182
+ scopeAdded("a computed field added under client.fields is in the default read (F-828)", {}, "corpus-scope-added-default",
183
+ // The Contract orders the record's fields, not the Adapter.
184
+ [{ authorId: CORPUS_AUTHOR.id, id: "b1", label: "book:b1" }]),
185
+ scopeAdded("an explicit select of it answers it alone (F-828)", { select: ["label"] }, "corpus-scope-added-select", [{ label: "book:b1" }]),
186
+ ];
187
+ /**
188
+ * A request the protocol answers itself, before any Adapter call: the
189
+ * framework's own envelope, exactly, at its code's status.
190
+ */
191
+ const refused = (label, request, status, code, message, headers = {}) => ({
192
+ label,
193
+ request,
194
+ server: {
195
+ status,
196
+ code,
197
+ causeKeys: ["message"],
198
+ adapterCalls: 0,
199
+ responseHeaders: {
200
+ ...answeredHeaders(String(request.headers["Aventara-Request-Id"])),
201
+ ...headers,
202
+ },
203
+ bodyBytes: JSON.stringify({ data: null, code, cause: { message } }),
204
+ },
205
+ client: { outcome: "framework-error", code },
206
+ });
207
+ const MALFORMED = "The request is malformed and cannot be interpreted.";
208
+ /** `{"pad":"x…"}`, one byte over the default 1 MiB `maxRequestBytes` (§20.1). */
209
+ const OVERSIZED = `{"pad":"${"x".repeat(1024 * 1024 + 1 - 10)}"}`;
210
+ const rawBody = (requestId, content) => corpusRequest("find/many", {}, requestId, {
211
+ body: { kind: "raw", content },
212
+ });
213
+ /**
214
+ * Family 7 — body and media (§12.3, §12.6, Q11; plan S7, driven from S8). On
215
+ * the raw arm: `{}` is valid (`find.many` above), and every other shape — a
216
+ * list, a scalar, `null`, no body, text that is not JSON — is `A2000`; a
217
+ * content type other than `application/json` is `A2011`; a decoded body over
218
+ * the limit is `A2010`. On the parsed arm only the shape is the decoder's.
219
+ * Routing metadata in the body is an argument like any other, refused by
220
+ * core's own validation (N16), so it reaches the Framework but no Adapter.
221
+ */
222
+ const BODY_AND_MEDIA = [
223
+ refused("body: a JSON list → 400 A2000", rawBody("corpus-body-list", "[]"), 400, "A2000", MALFORMED),
224
+ refused("body: a JSON number → 400 A2000", rawBody("corpus-body-number", "3"), 400, "A2000", MALFORMED),
225
+ refused("body: JSON null → 400 A2000", rawBody("corpus-body-null", "null"), 400, "A2000", MALFORMED),
226
+ refused("body: none (zero bytes) → 400 A2000", rawBody("corpus-body-none", ""), 400, "A2000", MALFORMED),
227
+ refused("body: text that is not JSON → 400 A2000", rawBody("corpus-body-text", "{"), 400, "A2000", MALFORMED),
228
+ refused("body: parsed by the host, null → 400 A2000", corpusRequest("find/many", {}, "corpus-body-parsed-null", {
229
+ body: { kind: "parsed", value: null },
230
+ }), 400, "A2000", MALFORMED),
231
+ refused("media: text/plain → 415 A2011", corpusRequest("find/many", {}, "corpus-media-text", {
232
+ headers: {
233
+ "Content-Type": "text/plain",
234
+ "Aventara-Protocol-Version": "1",
235
+ "Aventara-Contract-Hash": CORPUS_CLIENT_CONTRACT_HASH,
236
+ "Aventara-Request-Id": "corpus-media-text",
237
+ },
238
+ }), 415, "A2011", "The request content type is not supported; send application/json."),
239
+ refused("body: one byte over maxRequestBytes → 413 A2010", rawBody("corpus-body-oversized", OVERSIZED), 413, "A2010", "The request body exceeds the maximum request size."),
240
+ {
241
+ label: "body: routing metadata is an argument, refused by core → 422 A2004",
242
+ request: corpusRequest("find/many", { resource: "authors", family: "find", variant: "many" }, "corpus-body-routing"),
243
+ server: {
244
+ status: 422,
245
+ code: "A2004",
246
+ causeKeys: ["issues", "message"],
247
+ issues: [
248
+ { code: "V1001", path: ["arguments", "resource"] },
249
+ { code: "V1001", path: ["arguments", "family"] },
250
+ { code: "V1001", path: ["arguments", "variant"] },
251
+ ],
252
+ adapterCalls: 0,
253
+ responseHeaders: answeredHeaders("corpus-body-routing"),
254
+ },
255
+ client: { outcome: "framework-error", code: "A2004" },
256
+ },
257
+ ];
258
+ /**
259
+ * Family 8 — method (§12.1). A framework route accepts `POST` only; anything
260
+ * else is `405 A2012` carrying `Allow: POST` (RFC 9110 §15.5.6; the
261
+ * director, 2026-10-05).
262
+ */
263
+ const METHOD = [
264
+ refused("method: GET on a resource route → 405 A2012 with Allow: POST", corpusRequest("find/many", {}, "corpus-method-get", { method: "GET" }), 405, "A2012", "The HTTP method is not allowed on this framework route.", { Allow: "POST" }),
265
+ ];
266
+ /** A generator's `GET /_contract`, with `headers` beside its own request id. */
267
+ const contractRequest = (requestId, headers = {}, method = "GET") => ({
268
+ method,
269
+ path: "/_contract",
270
+ headers: { "Aventara-Request-Id": requestId, ...headers },
271
+ body: { kind: "raw", content: "" },
272
+ });
273
+ /** The corpus document's entity tag: the quoted contract hash (§12.5). */
274
+ const CORPUS_ETAG = `"${CORPUS_CLIENT_CONTRACT_HASH}"`;
275
+ /** `200` with the document: its exact bytes, `ETag`, `Cache-Control: no-cache`. */
276
+ const contractServed = (label, requestId, headers = {}) => ({
277
+ label: `contract: ${label}`,
278
+ request: contractRequest(requestId, headers),
279
+ server: {
280
+ status: 200,
281
+ code: null,
282
+ adapterCalls: 0,
283
+ responseHeaders: {
284
+ ...answeredHeaders(requestId),
285
+ ETag: CORPUS_ETAG,
286
+ "Cache-Control": "no-cache",
287
+ },
288
+ bodyBytes: CORPUS_CLIENT_CONTRACT_DOCUMENT,
289
+ },
290
+ client: { outcome: "resolve" },
291
+ });
292
+ /** `304 Not Modified`: no body, `ETag` and `Cache-Control` kept (RFC 9110 §15.4.5). */
293
+ const contractNotModified = (label, requestId, ifNoneMatch) => ({
294
+ label: `contract: ${label}`,
295
+ request: contractRequest(requestId, { "If-None-Match": ifNoneMatch }),
296
+ server: {
297
+ status: 304,
298
+ code: null,
299
+ adapterCalls: 0,
300
+ responseHeaders: {
301
+ "Aventara-Request-Id": requestId,
302
+ ETag: CORPUS_ETAG,
303
+ "Cache-Control": "no-cache",
304
+ },
305
+ bodyBytes: "",
306
+ },
307
+ client: { outcome: "resolve" },
308
+ });
309
+ /**
310
+ * Family 1 — contract discovery (§12.5, Q8; plan S11). `GET /_contract`
311
+ * serves the ClientContract's JCS bytes with `ETag` the quoted hash and
312
+ * `Cache-Control: no-cache`, needs no identity header, and answers `304` with
313
+ * no body when `If-None-Match` matches — weakly, in a list, or `*`. Any other
314
+ * method is `405 A2012` with `Allow: GET`. A generator resolves both `200`
315
+ * and `304` ("up to date", Phase 12).
316
+ */
317
+ const CONTRACT_DISCOVERY = [
318
+ contractServed("GET → 200 with the JCS bytes, ETag and no-cache", "corpus-contract"),
319
+ contractServed("no identity header is required, nor judged when sent", "corpus-contract-identity", {
320
+ "Aventara-Protocol-Version": "2",
321
+ "Aventara-Contract-Hash": `sha256:${"0".repeat(64)}`,
322
+ }),
323
+ contractNotModified("If-None-Match the quoted hash → 304", "corpus-contract-304", CORPUS_ETAG),
324
+ contractNotModified("If-None-Match weak W/ → 304", "corpus-contract-304-weak", `W/${CORPUS_ETAG}`),
325
+ contractNotModified("If-None-Match a list holding it → 304", "corpus-contract-304-list", `"sha256:old", ${CORPUS_ETAG}`),
326
+ contractNotModified("If-None-Match * → 304", "corpus-contract-304-star", "*"),
327
+ contractServed("If-None-Match another hash → 200", "corpus-contract-stale", {
328
+ "If-None-Match": '"sha256:old"',
329
+ }),
330
+ refused("method: POST on /_contract → 405 A2012 with Allow: GET", contractRequest("corpus-contract-post", {}, "POST"), 405, "A2012", "The HTTP method is not allowed on this framework route.", { Allow: "GET" }),
331
+ ];
332
+ /**
333
+ * One node of a generated client's plan (§14, M25): the operation, its
334
+ * arguments verbatim, and the fingerprint the client computed over them —
335
+ * a literal, as the client sends it; `corpus-contract.spec.ts` recomputes
336
+ * every one with core's `computeOperationFingerprint`.
337
+ */
338
+ const planNode = (resource, operation, args, fingerprint) => {
339
+ const [family, variant] = operation.split(".");
340
+ return { resource, family, variant, args, fingerprint };
341
+ };
342
+ const AUTHORS_FIND_MANY = planNode("authors", "find.many", {}, "fp1:1aLMnUbKspahv3fbsV7pKg");
343
+ /** `POST /_transactions` with a plan of `nodes`, under the corpus identity. */
344
+ const planRequest = (requestId, nodes, overrides = {}) => ({
345
+ ...corpusRequest("", { operations: nodes }, requestId),
346
+ path: "/_transactions",
347
+ ...overrides,
348
+ });
349
+ /** A row refused by core's validation layer: its issues and cause keys, no Adapter call. */
350
+ const invalid = (label, request, status, code, issues, operation) => ({
351
+ label,
352
+ request,
353
+ server: {
354
+ status,
355
+ code,
356
+ causeKeys: operation === undefined
357
+ ? ["issues", "message"]
358
+ : ["issues", "message", "operation"],
359
+ issues,
360
+ adapterCalls: 0,
361
+ responseHeaders: answeredHeaders(String(request.headers["Aventara-Request-Id"])),
362
+ },
363
+ client: { outcome: "framework-error", code },
364
+ });
365
+ /**
366
+ * Family 9 — transactions (§14; plan S9), through `/_transactions` under
367
+ * ClientContract rules only (R1). A committed plan is `200 A1009` with each
368
+ * step's data in plan order, a create included (P3); a rolled-back plan
369
+ * answers its failing step's own code at that code's status, cause keys
370
+ * `message` and `operation` (Q4, M15); a step that depends on rows an earlier
371
+ * step's cascade removed is refused before anything runs (`V1019` → `422`);
372
+ * and a plan node cannot carry an origin (N11).
373
+ *
374
+ * The R1 rows come in pairs, labelled `R1: <what> — single route` and
375
+ * `R1: <what> — /_transactions`: the same refusal through both routes, the
376
+ * plan's differing only by `cause.operation` (asserted pair by pair in
377
+ * `corpus-contract.spec.ts`).
378
+ */
379
+ const TRANSACTIONS = [
380
+ {
381
+ label: "transaction — committed, a create included → 200 A1009 in plan order",
382
+ request: planRequest("corpus-tx-committed", [
383
+ planNode("authors", "create.one", { data: { name: CORPUS_AUTHOR.name } }, "fp1:ivp4epVXIrdpENiusGWg3w"),
384
+ AUTHORS_FIND_MANY,
385
+ ]),
386
+ server: {
387
+ status: 200,
388
+ code: "A1009",
389
+ adapterCalls: 2,
390
+ responseHeaders: answeredHeaders("corpus-tx-committed"),
391
+ bodyBytes: JSON.stringify({
392
+ data: [CORPUS_AUTHOR, [CORPUS_AUTHOR]],
393
+ code: "A1009",
394
+ cause: null,
395
+ }),
396
+ },
397
+ client: { outcome: "resolve" },
398
+ },
399
+ {
400
+ label: "transaction — rolled back at step 1, a find.unique miss → 404 A2003 (its own status)",
401
+ request: planRequest("corpus-tx-rolled-back", [
402
+ AUTHORS_FIND_MANY,
403
+ planNode("authors", "find.unique", { where: { id: CORPUS_NOBODY } }, "fp1:WY0DNK9cOeveQr7rjKOdzA"),
404
+ ]),
405
+ server: {
406
+ status: 404,
407
+ code: "A2003",
408
+ causeKeys: ["message", "operation"],
409
+ adapterCalls: 2,
410
+ responseHeaders: answeredHeaders("corpus-tx-rolled-back"),
411
+ bodyBytes: JSON.stringify({
412
+ data: null,
413
+ code: "A2003",
414
+ cause: {
415
+ message: "No record matched the unique selector.",
416
+ operation: 1,
417
+ },
418
+ }),
419
+ },
420
+ client: { outcome: "framework-error", code: "A2003" },
421
+ },
422
+ invalid("transaction — a step that reads what an earlier delete cascaded into → 422 A2004 / V1019", planRequest("corpus-tx-cascade", [
423
+ planNode("authors", "delete.unique", { where: { id: CORPUS_AUTHOR.id } }, "fp1:ImWDaYNUDpIsjiuBSGBesg"),
424
+ planNode("books", "find.many", {}, "fp1:0vpQhJUZvleqWv_xMBvQug"),
425
+ ]), 422, "A2004", [{ code: "V1019", path: ["operations", 1, "resource"] }], 1),
426
+ ...["scope", "origin"].map((key) => invalid(`transaction — a plan node carrying "${key}" cannot smuggle an origin → 422 A2004 / V1001`, planRequest(`corpus-tx-smuggle-${key}`, [
427
+ { ...AUTHORS_FIND_MANY, [key]: "application" },
428
+ ]), 422, "A2004", [{ code: "V1001", path: [key] }], 0)),
429
+ invalid("R1: a client-hidden field — single route", corpusRequest("find/many", { select: ["id", "secret"] }, "corpus-r1-field"), 422, "A2004", [{ code: "V1005", path: ["arguments", "select", 1] }]),
430
+ invalid("R1: a client-hidden field — /_transactions", planRequest("corpus-r1-field-tx", [
431
+ AUTHORS_FIND_MANY,
432
+ planNode("authors", "find.many", { select: ["id", "secret"] }, "fp1:1t8vSfPuohv1N5MIX8ccNw"),
433
+ ]), 422, "A2004", [{ code: "V1005", path: ["arguments", "select", 1] }], 1),
434
+ invalid("R1: a client-restricted variant — single route", {
435
+ ...corpusRequest("", {}, "corpus-r1-variant"),
436
+ path: "/_resources/secrets/find/many",
437
+ }, 404, "A2002", [{ code: "V1006", path: ["operation"] }]),
438
+ invalid("R1: a client-restricted variant — /_transactions", planRequest("corpus-r1-variant-tx", [
439
+ AUTHORS_FIND_MANY,
440
+ planNode("secrets", "find.many", {}, "fp1:KegI5OcquDHi8z39VH11EQ"),
441
+ ]), 404, "A2002", [{ code: "V1006", path: ["operation"] }], 1),
442
+ invalid("R1: a client-hidden Resource — single route", {
443
+ ...corpusRequest("", {}, "corpus-r1-resource"),
444
+ path: "/_resources/hiddenRes/find/many",
445
+ }, 404, "A2001", [{ code: "V1005", path: ["resource"] }]),
446
+ invalid("R1: a client-hidden Resource — /_transactions", planRequest("corpus-r1-resource-tx", [
447
+ AUTHORS_FIND_MANY,
448
+ planNode("hiddenRes", "find.many", {}, "fp1:UY6pLVLKM2LaIMnfQa0zGw"),
449
+ ]), 404, "A2001", [{ code: "V1005", path: ["resource"] }], 1),
450
+ refused("method: GET on /_transactions → 405 A2012 with Allow: POST", planRequest("corpus-tx-get", [AUTHORS_FIND_MANY], { method: "GET" }), 405, "A2012", "The HTTP method is not allowed on this framework route.", { Allow: "POST" }),
451
+ ];
452
+ /** A hash no corpus ClientContract has: a stale generated client's. */
453
+ const STALE_HASH = `sha256:${"0".repeat(64)}`;
454
+ /** The FVL's validation-failure text, which every A2001/A2002 envelope carries. */
455
+ const VALIDATION_FAILED = "Operation arguments failed framework validation.";
456
+ /**
457
+ * An absence answer (Q2's O3): core's own envelope for the same request,
458
+ * byte for byte (`corpus-contract.spec.ts` recomputes each operation row's
459
+ * through `framework.execute`), with zero Adapter calls.
460
+ */
461
+ const absent = (label, request, status, code, cause) => ({
462
+ label: `absent: ${label}`,
463
+ request,
464
+ server: {
465
+ status,
466
+ code,
467
+ causeKeys: Object.keys(cause).sort(),
468
+ ...(Array.isArray(cause.issues)
469
+ ? {
470
+ issues: cause.issues.map(({ code: issue, path }) => ({ code: issue, path })),
471
+ }
472
+ : {}),
473
+ adapterCalls: 0,
474
+ responseHeaders: answeredHeaders(String(request.headers["Aventara-Request-Id"])),
475
+ bodyBytes: JSON.stringify({ data: null, code, cause }),
476
+ },
477
+ client: { outcome: "framework-error", code },
478
+ });
479
+ /** A corpus request to `path`, its identity headers overridden by `headers`. */
480
+ const absentRequest = (path, requestId, headers = {}, method = "POST") => {
481
+ const base = corpusRequest("", {}, requestId);
482
+ return { ...base, method, path, headers: { ...base.headers, ...headers } };
483
+ };
484
+ const unavailableOperation = (resource, operation) => ({
485
+ message: VALIDATION_FAILED,
486
+ issues: [
487
+ {
488
+ path: ["operation"],
489
+ code: "V1006",
490
+ message: `Operation "${operation}" is not available on Resource "${resource}".`,
491
+ },
492
+ ],
493
+ });
494
+ const unavailableResource = (resource) => ({
495
+ message: VALIDATION_FAILED,
496
+ issues: [
497
+ {
498
+ code: "V1005",
499
+ path: ["resource"],
500
+ message: `Resource "${resource}" is not available in the active Contract.`,
501
+ },
502
+ ],
503
+ });
504
+ /**
505
+ * Family 6 — the absence responder (Q2's O3, Q3; plan S10). What a request
506
+ * for a route the ClientContract does not advertise answers, identically
507
+ * whether the host mounts one route per `protocol.surface()` entry with
508
+ * `protocol.answerAbsent` as its fallback or a catch-all whose decoder answers
509
+ * absence (Q12) — both styles drive these rows. Identity first, so a stale
510
+ * client hears `A2005` (F-716's server half), then the Resource, then the
511
+ * operation; zero Adapter calls. `_transactions` under `"none"` is asked of
512
+ * the corpus's second ClientContract, named by its hash.
513
+ */
514
+ const ABSENCE = [
515
+ absent("an unadvertised operation, current hash → 404 A2002", absentRequest("/_resources/books/delete/many", "corpus-absent-op"), 404, "A2002", unavailableOperation("books", "delete.many")),
516
+ absent("an unadvertised operation, stale hash → 409 A2005 (regenerate)", absentRequest("/_resources/books/delete/many", "corpus-absent-stale", {
517
+ "Aventara-Contract-Hash": STALE_HASH,
518
+ }), 409, "A2005", {
519
+ message: "Generated client contract does not match the server. Regenerate the client.",
520
+ }),
521
+ absent("a stale hash before the method → 409 A2005", absentRequest("/_resources/authors/find/many", "corpus-absent-stale-get", { "Aventara-Contract-Hash": STALE_HASH }, "GET"), 409, "A2005", {
522
+ message: "Generated client contract does not match the server. Regenerate the client.",
523
+ }),
524
+ absent("another protocol version → 400 A2006", absentRequest("/_resources/books/delete/many", "corpus-absent-version", {
525
+ "Aventara-Protocol-Version": "2",
526
+ }), 400, "A2006", { message: "Aventara protocol version does not match the server." }),
527
+ absent("an unknown Resource → 404 A2001", absentRequest("/_resources/nope/find/many", "corpus-absent-resource"), 404, "A2001", unavailableResource("nope")),
528
+ absent("a Resource with operations: {} → 404 A2002 (M6)", absentRequest("/_resources/secrets/find/many", "corpus-absent-empty"), 404, "A2002", unavailableOperation("secrets", "find.many")),
529
+ absent("a client-hidden Resource → 404 A2001 (M7)", absentRequest("/_resources/hiddenRes/find/many", "corpus-absent-hidden"), 404, "A2001", unavailableResource("hiddenRes")),
530
+ absent('/_transactions under transactions "none" → 404 A2002 at ["operations"] (M17)', {
531
+ ...planRequest("corpus-absent-tx-none", [AUTHORS_FIND_MANY]),
532
+ headers: {
533
+ ...planRequest("corpus-absent-tx-none", [AUTHORS_FIND_MANY]).headers,
534
+ "Aventara-Contract-Hash": CORPUS_NONE_CLIENT_CONTRACT_HASH,
535
+ },
536
+ }, 404, "A2002", {
537
+ message: "Transaction plans are not available on the active Contract.",
538
+ issues: [
539
+ {
540
+ code: "V1006",
541
+ path: ["operations"],
542
+ message: "Transaction plans are not available on the active Contract.",
543
+ },
544
+ ],
545
+ }),
546
+ ];
547
+ /**
548
+ * A row whose whole answer is stated: its status, its envelope byte for byte
549
+ * (each object's keys in the order core writes them, as measured), its cause
550
+ * keys and issues read off that envelope, and its Adapter calls. A success
551
+ * code resolves; any other code is a framework error.
552
+ */
553
+ const answered = (label, request, status, adapterCalls, envelope, responseHeaders = answeredHeaders(String(request.headers["Aventara-Request-Id"]))) => {
554
+ const { cause } = envelope;
555
+ const issues = Array.isArray(cause?.issues)
556
+ ? cause.issues.map(({ code, path }) => path === undefined ? { code } : { code, path })
557
+ : undefined;
558
+ return {
559
+ label,
560
+ request,
561
+ server: {
562
+ status,
563
+ code: envelope.code,
564
+ ...(cause === null ? {} : { causeKeys: Object.keys(cause).sort() }),
565
+ ...(issues === undefined ? {} : { issues }),
566
+ adapterCalls,
567
+ responseHeaders,
568
+ bodyBytes: JSON.stringify({
569
+ data: envelope.data,
570
+ code: envelope.code,
571
+ cause,
572
+ }),
573
+ },
574
+ client: envelope.code.startsWith("A1")
575
+ ? { outcome: "resolve" }
576
+ : { outcome: "framework-error", code: envelope.code },
577
+ };
578
+ };
579
+ /** A corpus request to any Resource's operation route. */
580
+ const routeRequest = (resource, operation, body, requestId, overrides = {}) => corpusRequest("", body, requestId, {
581
+ path: `/_resources/${resource}/${operation.replace(".", "/")}`,
582
+ ...overrides,
583
+ });
584
+ const NO_MATCH = "No record matched the unique selector.";
585
+ /**
586
+ * Family 3 — absence semantics (§13.4): what a request that matches nothing
587
+ * answers, operation by operation. `find.first`'s `A1001` is family 2's.
588
+ */
589
+ const ABSENCE_SEMANTICS = [
590
+ answered("absence: find.unique matching nothing → 404 A2003", routeRequest("authors", "find.unique", { where: { id: CORPUS_NOBODY } }, "corpus-none-find-unique"), 404, 1, { data: null, code: "A2003", cause: { message: NO_MATCH } }),
591
+ answered("absence: find.many matching nothing → 200 A1000 with []", routeRequest("authors", "find.many", { where: { id: CORPUS_NOBODY } }, "corpus-none-find-many"), 200, 1, { data: [], code: "A1000", cause: null }),
592
+ answered("absence: find.count matching nothing → 200 A1000 with 0", routeRequest("authors", "find.count", { where: { id: CORPUS_NOBODY } }, "corpus-none-find-count"), 200, 1, { data: 0, code: "A1000", cause: null }),
593
+ answered("absence: update.first matching nothing → 200 A1001 with null", routeRequest("authors", "update.first", { where: { id: CORPUS_NOBODY }, data: DATA }, "corpus-none-update-first"), 200, 1, { data: null, code: "A1001", cause: null }),
594
+ answered("absence: delete.first matching nothing → 200 A1001 with null", routeRequest("authors", "delete.first", { where: { id: CORPUS_NOBODY } }, "corpus-none-delete-first"), 200, 1, { data: null, code: "A1001", cause: null }),
595
+ answered("absence: update.unique with no target → 404 A2003", routeRequest("authors", "update.unique", { where: { id: CORPUS_NOBODY }, data: DATA }, "corpus-none-update-unique"), 404, 1, { data: null, code: "A2003", cause: { message: NO_MATCH } }),
596
+ answered("absence: delete.unique with no target → 404 A2003", routeRequest("authors", "delete.unique", { where: { id: CORPUS_NOBODY } }, "corpus-none-delete-unique"), 404, 1, { data: null, code: "A2003", cause: { message: NO_MATCH } }),
597
+ ];
598
+ /** `{ id: { equals: "a1" } }` wrapped in `depth` single-member `AND`s. */
599
+ const nested = (depth) => depth === 0
600
+ ? { id: { equals: CORPUS_AUTHOR.id } }
601
+ : { AND: [nested(depth - 1)] };
602
+ /** 51 filter nodes under one `OR`: one over the default `maxBooleanNodes` (50). */
603
+ const TOO_MANY_NODES = {
604
+ OR: Array.from({ length: 51 }, () => ({ id: { equals: CORPUS_AUTHOR.id } })),
605
+ };
606
+ /** A plan reference to `path` of step `operation`, whose fingerprint is `fingerprint`. */
607
+ const ref = (operation, fingerprint, path) => ({
608
+ $ref: { operation, fingerprint, path: [path] },
609
+ });
610
+ /** Step 0 of every reference row: create an author, selecting its id. */
611
+ const CREATE_ID = planNode("authors", "create.one", { data: { name: CORPUS_AUTHOR.name }, select: ["id"] }, "fp1:aMKNYrUwcifOeY8ZYk4pow");
612
+ const CREATE_ID_PRINT = CREATE_ID.fingerprint;
613
+ /** A `find.first` that matches nothing: a source with no value to refer to. */
614
+ const FIRST_NOBODY = planNode("authors", "find.first", { where: { id: CORPUS_NOBODY } }, "fp1:qNR9mD7_iVygNtmuuXjymw");
615
+ const ARGUMENTS_FAILED = "Operation arguments failed framework validation.";
616
+ /**
617
+ * Family 4 — argument validation (§13.3; ADR 0008), core's own answers through
618
+ * the HTTP surface: an unknown field (`V1005`), an unadvertised operator
619
+ * (`V1006`), an invalid projection (`V1008`) → `A2004`; the three limits
620
+ * (`V1013` nesting, `V1014` a plan's operation count, `V1015` filter nodes) →
621
+ * `A2009`; a plan reference's index, fingerprint, path, type and value
622
+ * (`V1016`, `V1017`, `V1010`, `V1011`, `V1018`) → `A2007`. Rollup precedence
623
+ * (ADR 0008 decision 2), behaviourally: two `A2004` issues roll up together,
624
+ * and a limit refuses alone, before a field defect is read.
625
+ */
626
+ const ARGUMENT_VALIDATION = [
627
+ answered("argument: an unknown field in where → 422 A2004 / V1005", corpusRequest("find/many", { where: { nope: { equals: "x" } } }, "corpus-arg-field"), 422, 0, {
628
+ data: null,
629
+ code: "A2004",
630
+ cause: {
631
+ message: ARGUMENTS_FAILED,
632
+ issues: [
633
+ {
634
+ code: "V1005",
635
+ path: ["arguments", "where", "nope"],
636
+ message: 'Field "nope" is not available on Resource "authors".',
637
+ },
638
+ ],
639
+ },
640
+ }),
641
+ answered("argument: a filter operator the field does not advertise → 422 A2004 / V1006", corpusRequest("find/many", { where: { name: { contains: "A" } } }, "corpus-arg-operator"), 422, 0, {
642
+ data: null,
643
+ code: "A2004",
644
+ cause: {
645
+ message: ARGUMENTS_FAILED,
646
+ issues: [
647
+ {
648
+ code: "V1006",
649
+ path: ["arguments", "where", "name", "contains"],
650
+ message: 'Filter operator "contains" is not available on field "name".',
651
+ },
652
+ ],
653
+ },
654
+ }),
655
+ answered("argument: select and include together → 422 A2004 / V1008", corpusRequest("find/many", { select: ["id"], include: [] }, "corpus-arg-projection"), 422, 0, {
656
+ data: null,
657
+ code: "A2004",
658
+ cause: {
659
+ message: ARGUMENTS_FAILED,
660
+ issues: [
661
+ {
662
+ code: "V1008",
663
+ path: ["arguments"],
664
+ message: "select and include are mutually exclusive.",
665
+ },
666
+ ],
667
+ },
668
+ }),
669
+ answered("argument: two defects of one family roll up together → 422 A2004 / V1001 + V1005", corpusRequest("find/many", { take: 251, where: { nope: { equals: 1 } } }, "corpus-arg-rollup"), 422, 0, {
670
+ data: null,
671
+ code: "A2004",
672
+ cause: {
673
+ message: ARGUMENTS_FAILED,
674
+ issues: [
675
+ {
676
+ code: "V1001",
677
+ path: ["arguments", "take"],
678
+ message: 'Argument "take" is not valid for find.many.',
679
+ },
680
+ {
681
+ code: "V1005",
682
+ path: ["arguments", "where", "nope"],
683
+ message: 'Field "nope" is not available on Resource "authors".',
684
+ },
685
+ ],
686
+ },
687
+ }),
688
+ answered("argument: nesting over maxNestingDepth → 422 A2009 / V1013", corpusRequest("find/many", { where: nested(13) }, "corpus-arg-nesting"), 422, 0, {
689
+ data: null,
690
+ code: "A2009",
691
+ cause: {
692
+ message: ARGUMENTS_FAILED,
693
+ issues: [
694
+ {
695
+ path: ["arguments", "where"],
696
+ code: "V1013",
697
+ message: "Request nesting exceeds maxNestingDepth (12).",
698
+ },
699
+ ],
700
+ },
701
+ }),
702
+ answered("argument: filter nodes over maxBooleanNodes → 422 A2009 / V1015", corpusRequest("find/many", { where: TOO_MANY_NODES }, "corpus-arg-nodes"), 422, 0, {
703
+ data: null,
704
+ code: "A2009",
705
+ cause: {
706
+ message: ARGUMENTS_FAILED,
707
+ issues: [
708
+ {
709
+ path: ["arguments", "where"],
710
+ code: "V1015",
711
+ message: "Boolean/filter node count exceeds maxBooleanNodes (50).",
712
+ },
713
+ ],
714
+ },
715
+ }),
716
+ answered("argument: a limit refuses alone, before a field defect is read → 422 A2009 / V1015", corpusRequest("find/many", { where: TOO_MANY_NODES, select: ["nope"] }, "corpus-arg-precedence"), 422, 0, {
717
+ data: null,
718
+ code: "A2009",
719
+ cause: {
720
+ message: ARGUMENTS_FAILED,
721
+ issues: [
722
+ {
723
+ path: ["arguments", "where"],
724
+ code: "V1015",
725
+ message: "Boolean/filter node count exceeds maxBooleanNodes (50).",
726
+ },
727
+ ],
728
+ },
729
+ }),
730
+ answered("argument: a plan over maxTransactionOperations → 422 A2009 / V1014", planRequest("corpus-arg-plan-size", Array.from({ length: 21 }, () => AUTHORS_FIND_MANY)), 422, 0, {
731
+ data: null,
732
+ code: "A2009",
733
+ cause: {
734
+ message: "Transaction operation count exceeds the active Contract limit.",
735
+ issues: [
736
+ {
737
+ code: "V1014",
738
+ path: ["operations"],
739
+ message: "Transaction operation count exceeds maxTransactionOperations (20).",
740
+ },
741
+ ],
742
+ },
743
+ }),
744
+ ...[
745
+ [
746
+ "an operation index no earlier step has → 422 A2007 / V1016",
747
+ "corpus-arg-ref-index",
748
+ planNode("authors", "create.one", { data: { name: ref(5, CREATE_ID_PRINT, "id") } }, "fp1:NOPvPjrK18ki-rp5JekbCg"),
749
+ "V1016",
750
+ "name",
751
+ "operation",
752
+ "Transaction reference operation index is invalid.",
753
+ 0,
754
+ ],
755
+ [
756
+ "a fingerprint that is not its source's → 422 A2007 / V1017",
757
+ "corpus-arg-ref-fingerprint",
758
+ planNode("authors", "create.one", { data: { name: ref(0, "fp1:AAAAAAAAAAAAAAAAAAAAAA", "id") } }, "fp1:LMKG9r39fh8Q4FsS2EbmSQ"),
759
+ "V1017",
760
+ "name",
761
+ "fingerprint",
762
+ "Transaction reference fingerprint does not match its source operation.",
763
+ 0,
764
+ ],
765
+ [
766
+ "a path its source does not select → 422 A2007 / V1010",
767
+ "corpus-arg-ref-path",
768
+ planNode("authors", "create.one", { data: { name: ref(0, CREATE_ID_PRINT, "name") } }, "fp1:z-msTPG2tGrnDfhbp7OMNw"),
769
+ "V1010",
770
+ "name",
771
+ "path",
772
+ "Transaction reference path is not present in the source projection.",
773
+ 0,
774
+ ],
775
+ [
776
+ "a source type its destination cannot take → 422 A2007 / V1011",
777
+ "corpus-arg-ref-type",
778
+ planNode("samples", "create.one", {
779
+ data: {
780
+ count: "1",
781
+ at: "2026-10-05T00:00:00.000Z",
782
+ amount: "1",
783
+ blob: ref(0, CREATE_ID_PRINT, "id"),
784
+ },
785
+ }, "fp1:kFeNIIPUkzjgT905qlgNUg"),
786
+ "V1011",
787
+ "blob",
788
+ "path",
789
+ "Transaction reference source type is not assignable to its destination.",
790
+ 0,
791
+ ],
792
+ ].map(([what, requestId, step, code, field, member, message, calls]) => answered(`argument: a plan reference to ${what}`, planRequest(requestId, [CREATE_ID, step]), 422, calls, {
793
+ data: null,
794
+ code: "A2007",
795
+ cause: {
796
+ message: ARGUMENTS_FAILED,
797
+ issues: [
798
+ {
799
+ code,
800
+ path: ["operations", 1, "args", "data", field, "$ref", member],
801
+ message,
802
+ },
803
+ ],
804
+ operation: 1,
805
+ },
806
+ })),
807
+ answered("argument: a plan reference that resolves to no value → 422 A2007 / V1018, after its source ran", planRequest("corpus-arg-ref-value", [
808
+ FIRST_NOBODY,
809
+ planNode("authors", "create.one", { data: { name: ref(0, "fp1:qNR9mD7_iVygNtmuuXjymw", "id") } }, "fp1:4EFGvMTB6fmhpeV65U9J9Q"),
810
+ ]), 422, 1, {
811
+ data: null,
812
+ code: "A2007",
813
+ cause: {
814
+ message: ARGUMENTS_FAILED,
815
+ issues: [
816
+ {
817
+ code: "V1018",
818
+ path: ["operations", 1, "args", "data", "name", "$ref", "path"],
819
+ message: "Transaction reference resolved to no value.",
820
+ },
821
+ ],
822
+ operation: 1,
823
+ },
824
+ }),
825
+ ];
826
+ /** The corpus identity headers, with `overrides` applied and `omit` left out. */
827
+ const identityHeaders = (requestId, overrides = {}, omit) => Object.fromEntries(Object.entries({
828
+ "Content-Type": "application/json",
829
+ "Aventara-Protocol-Version": "1",
830
+ "Aventara-Contract-Hash": CORPUS_CLIENT_CONTRACT_HASH,
831
+ "Aventara-Request-Id": requestId,
832
+ ...overrides,
833
+ }).filter(([name]) => name !== omit));
834
+ const STALE_CLIENT = "Generated client contract does not match the server. Regenerate the client.";
835
+ /**
836
+ * Family 5 — identity (§12.4; S7 follow-ups): a request missing either
837
+ * identity header is malformed (`A2000`), another protocol version is
838
+ * `A2006`, a stale or malformed hash is `A2005` with Appendix B.3's text (Q3),
839
+ * all on a route that exists; header names are matched without regard to case.
840
+ */
841
+ const IDENTITY = [
842
+ refused("identity: no Aventara-Protocol-Version → 400 A2000", corpusRequest("find/many", {}, "corpus-id-no-version", {
843
+ headers: identityHeaders("corpus-id-no-version", {}, "Aventara-Protocol-Version"),
844
+ }), 400, "A2000", MALFORMED),
845
+ refused("identity: no Aventara-Contract-Hash → 400 A2000", corpusRequest("find/many", {}, "corpus-id-no-hash", {
846
+ headers: identityHeaders("corpus-id-no-hash", {}, "Aventara-Contract-Hash"),
847
+ }), 400, "A2000", MALFORMED),
848
+ refused("identity: another protocol version → 400 A2006", corpusRequest("find/many", {}, "corpus-id-version", {
849
+ headers: identityHeaders("corpus-id-version", {
850
+ "Aventara-Protocol-Version": "2",
851
+ }),
852
+ }), 400, "A2006", "Aventara protocol version does not match the server."),
853
+ refused("identity: a stale contract hash on a route that exists → 409 A2005", corpusRequest("find/many", {}, "corpus-id-stale", {
854
+ headers: identityHeaders("corpus-id-stale", {
855
+ "Aventara-Contract-Hash": `sha256:${"0".repeat(64)}`,
856
+ }),
857
+ }), 409, "A2005", STALE_CLIENT),
858
+ refused("identity: a malformed contract hash → 409 A2005", corpusRequest("find/many", {}, "corpus-id-malformed", {
859
+ headers: identityHeaders("corpus-id-malformed", {
860
+ "Aventara-Contract-Hash": "sha256:xyz",
861
+ }),
862
+ }), 409, "A2005", STALE_CLIENT),
863
+ answered("identity: header names in lower case → 200 A1000", corpusRequest("find/many", {}, "corpus-id-case", {
864
+ headers: Object.fromEntries(Object.entries(identityHeaders("corpus-id-case")).map(([name, value]) => [name.toLowerCase(), value])),
865
+ }), 200, 1, { data: [CORPUS_AUTHOR], code: "A1000", cause: null }, answeredHeaders("corpus-id-case")),
866
+ ];
867
+ /** `authors.find.unique` naming the trigger `id` — what the guard or Adapter fails on. */
868
+ const triggered = (id, requestId) => corpusRequest("find/unique", { where: { id } }, requestId);
869
+ /**
870
+ * Family 10 — internal and authorization (§13.2; Q5, Q6), from the corpus's
871
+ * guard and Adapter failing on purpose (labelled by trigger,
872
+ * `CORPUS_TRIGGER`): `A3000`–`A3003` → 500 with `{ message }` only — no
873
+ * sub-code reaches the wire (Q5); `A4000` → 401, `A4001`/`A4002` → 403, with
874
+ * no reason member (Q6). Every one is a framework error, never a transport
875
+ * error: each body is an envelope.
876
+ */
877
+ const INTERNAL = [
878
+ answered("internal: a guard that throws → 500 A3000", triggered(CORPUS_TRIGGER.guardThrows, "corpus-internal-a3000"), 500, 0, {
879
+ data: null,
880
+ code: "A3000",
881
+ cause: { message: "Internal framework error." },
882
+ }),
883
+ answered("internal: an Adapter that throws → 500 A3001", triggered(CORPUS_TRIGGER.adapterThrows, "corpus-internal-a3001"), 500, 1, {
884
+ data: null,
885
+ code: "A3001",
886
+ cause: { message: "Adapter execution failed." },
887
+ }),
888
+ answered("internal: a plan whose commit fails after the work → 500 A3002", planRequest("corpus-internal-a3002", [
889
+ planNode("authors", "update.unique", { where: { id: CORPUS_TRIGGER.commitFails }, data: DATA }, "fp1:8gs-RmzsqGlD_FFmk6dlQA"),
890
+ ]), 500, 1, { data: null, code: "A3002", cause: { message: "Transaction failed." } }),
891
+ answered("internal: a result the wire cannot spell → 500 A3003", routeRequest("samples", "find.unique", { where: { id: CORPUS_TRIGGER.unencodable } }, "corpus-internal-a3003"), 500, 1, {
892
+ data: null,
893
+ code: "A3003",
894
+ cause: { message: "Result serialization failed." },
895
+ }),
896
+ answered("internal: a guard that requires authentication → 401 A4000", triggered(CORPUS_TRIGGER.unauthenticated, "corpus-internal-a4000"), 401, 0, {
897
+ data: null,
898
+ code: "A4000",
899
+ cause: { message: "Authentication is required." },
900
+ }),
901
+ answered("internal: a guard that denies → 403 A4001", triggered(CORPUS_TRIGGER.guardDenies, "corpus-internal-a4001"), 403, 0, {
902
+ data: null,
903
+ code: "A4001",
904
+ cause: { message: "Operation denied by a framework guard." },
905
+ }),
906
+ answered("internal: a policy denial, no reason member → 403 A4002", triggered(CORPUS_TRIGGER.policyDenies, "corpus-internal-a4002"), 403, 0, {
907
+ data: null,
908
+ code: "A4002",
909
+ cause: { message: "Denied by the corpus policy." },
910
+ }),
911
+ ];
912
+ /**
913
+ * Family 12 — conflict codes (§13.2): the corpus Adapter raising `A2008`,
914
+ * `A2013` and `A2014` from an `update.unique` → 409, `cause.issues` absent.
915
+ */
916
+ const CONFLICT = [
917
+ [CORPUS_TRIGGER.conflict, "A2008"],
918
+ [CORPUS_TRIGGER.concurrentModification, "A2013"],
919
+ [CORPUS_TRIGGER.staleSelection, "A2014"],
920
+ ].map(([trigger, code]) => answered(`conflict: the Adapter answers ${code} → 409, no issues`, corpusRequest("update/unique", { where: { id: trigger }, data: DATA }, `corpus-conflict-${code}`), 409, 1, {
921
+ data: null,
922
+ code,
923
+ cause: { message: `The corpus Adapter answered ${code}.` },
924
+ }));
925
+ /** The one `samples` record in wire form (P1): what both encoders must write. */
926
+ const SAMPLE_WIRE = {
927
+ id: "s1",
928
+ count: "9007199254740993",
929
+ at: "2026-10-05T12:34:56.789Z",
930
+ amount: "12.50",
931
+ blob: "/wAQ",
932
+ };
933
+ /** A `samples` create whose `blob` is `blob`. */
934
+ const sampleCreate = (blob, requestId) => routeRequest("samples", "create.one", {
935
+ data: {
936
+ count: "1",
937
+ at: "2026-10-05T00:00:00.000Z",
938
+ amount: "1.5",
939
+ blob,
940
+ },
941
+ }, requestId);
942
+ /**
943
+ * Family 13 — wire scalars (P1, C1): a result carrying a `bigint`, a `Date`, a
944
+ * `Decimal` and a `Uint8Array` is written as their exact wire strings; a
945
+ * request `bytes` value whose padding carries non-zero bits is refused at the
946
+ * field, and its canonical spelling accepted. (The unencodable result is
947
+ * family 10's `A3003`.)
948
+ */
949
+ const WIRE_SCALARS = [
950
+ answered("wire: a result's bigint, datetime, decimal and bytes in wire form → 200 A1000", routeRequest("samples", "find.unique", { where: { id: "s1" } }, "corpus-wire-result"), 200, 1, { data: SAMPLE_WIRE, code: "A1000", cause: null }),
951
+ answered('wire: bytes "AB==" (non-zero pad bits) → 422 A2004 / V1003, no Adapter call', sampleCreate("AB==", "corpus-wire-pad-bits"), 422, 0, {
952
+ data: null,
953
+ code: "A2004",
954
+ cause: {
955
+ message: ARGUMENTS_FAILED,
956
+ issues: [
957
+ {
958
+ code: "V1003",
959
+ path: ["arguments", "data", "blob"],
960
+ message: "Value must use RFC 4648 base64 encoding.",
961
+ },
962
+ ],
963
+ },
964
+ }),
965
+ answered('wire: bytes "AA==" (canonical) → 201 A1002', sampleCreate("AA==", "corpus-wire-canonical"), 201, 1, { data: SAMPLE_WIRE, code: "A1002", cause: null }),
966
+ ];
967
+ /**
968
+ * Family 14 — correlation (Q13): `Aventara-Request-Id` is adopted when it is
969
+ * 1–128 visible ASCII characters and echoed (every row above sends one and
970
+ * states its echo, refusals included); otherwise it is ignored — a fresh id
971
+ * is minted, whose value no row can state — and the request is never refused
972
+ * over it.
973
+ */
974
+ const CORRELATION = [
975
+ answered("correlation: a 128-character id is adopted and echoed", corpusRequest("find/many", {}, "x".repeat(128)), 200, 1, { data: [CORPUS_AUTHOR], code: "A1000", cause: null }),
976
+ ...[
977
+ ["a 129-character id", "x".repeat(129)],
978
+ ["an empty id", ""],
979
+ ["an id with a space", "a b"],
980
+ ].map(([what, id]) => answered(`correlation: ${what} is not adopted, and never refused → 200 A1000`, corpusRequest("find/many", {}, id), 200, 1, { data: [CORPUS_AUTHOR], code: "A1000", cause: null }, { "Content-Type": "application/json" })),
981
+ ];
982
+ export const AvProtocolFixtures = [
983
+ ...CONTRACT_DISCOVERY,
984
+ ...SUCCESS_STATUSES,
985
+ ...ABSENCE_SEMANTICS,
986
+ ...SCOPE_ADDED,
987
+ ...ARGUMENT_VALIDATION,
988
+ ...IDENTITY,
989
+ ...BODY_AND_MEDIA,
990
+ ...METHOD,
991
+ ...ABSENCE,
992
+ ...TRANSACTIONS,
993
+ ...INTERNAL,
994
+ ...CONFLICT,
995
+ ...WIRE_SCALARS,
996
+ ...CORRELATION,
997
+ ...TRANSPORT_ERRORS,
998
+ ];