@hyperscale0/adl 1.0.0-beta.5 → 1.0.0-beta.7
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/CHANGELOG.md +10 -1
- package/README.md +8 -79
- package/conformance/cases.json +50 -764
- package/conformance/subject-adapter.ts +31 -0
- package/dist/boundary-fixture.d.ts +24 -0
- package/dist/boundary-fixture.d.ts.map +1 -0
- package/dist/boundary-fixture.js +34 -0
- package/dist/boundary-fixture.js.map +1 -0
- package/dist/index.d.ts +68 -156
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +100 -77
- package/dist/index.js.map +1 -1
- package/dist/vocabulary.d.ts +3 -57
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +12 -84
- package/dist/vocabulary.js.map +1 -1
- package/docs/authoring-guide.md +37 -417
- package/package.json +11 -6
- package/spec/manifest.schema.json +511 -446
- package/dist/conformance.d.ts +0 -41
- package/dist/conformance.d.ts.map +0 -1
- package/dist/conformance.js +0 -486
- package/dist/conformance.js.map +0 -1
- package/examples/meridian-bank/README.md +0 -65
- package/examples/meridian-bank/fixtures/payout-status-replay.json +0 -19
- package/examples/meridian-bank/fixtures/payout-status-settled.json +0 -19
- package/examples/meridian-bank/fixtures/payout-submit-accepted.json +0 -26
- package/examples/meridian-bank/fixtures/payout-submit-declined.json +0 -25
- package/examples/meridian-bank/fixtures/statement-camt053.json +0 -45
- package/examples/meridian-bank/fixtures/webhook-payout-settled.json +0 -11
- package/examples/meridian-bank/index.ts +0 -262
package/conformance/cases.json
CHANGED
|
@@ -1,834 +1,120 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "
|
|
3
|
-
"base": "
|
|
4
|
-
"mutations": "`set` writes a value at a path (array of keys, because operation names contain dots) and runs before `delete`, which removes a path.",
|
|
2
|
+
"description": "Generic declaration refusals. Boundary evidence shape checks are in conformance.spec.ts.",
|
|
3
|
+
"base": "../src/boundary-fixture.ts",
|
|
5
4
|
"cases": [
|
|
6
|
-
{
|
|
7
|
-
"id": "base-certifies",
|
|
8
|
-
"title": "the base declaration certifies with no findings",
|
|
9
|
-
"why": "The corpus is worthless if the base is not clean: every case below reads as one broken fact against a green baseline.",
|
|
10
|
-
"mutate": {},
|
|
11
|
-
"load": "accepts",
|
|
12
|
-
"conformance": [],
|
|
13
|
-
"schema": "accepts"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"id": "profile-missing",
|
|
17
|
-
"title": "no partner-bank profile at all",
|
|
18
|
-
"why": "Without the profile none of the bank facts exist, so the runtime has nothing to poll, classify, or reconcile from.",
|
|
19
|
-
"mutate": { "delete": [["profile"]] },
|
|
20
|
-
"load": "accepts",
|
|
21
|
-
"conformance": ["partner_bank_profile_missing"],
|
|
22
|
-
"schema": "accepts"
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"id": "profile-missing-still-checks-the-declaration",
|
|
26
|
-
"title": "a missing profile leads, and the declaration checks still run",
|
|
27
|
-
"why": "One run must report everything it can see; a missing profile is not a licence to stop reading the rest.",
|
|
28
|
-
"mutate": {
|
|
29
|
-
"set": [{ "path": ["bindings"], "value": {} }],
|
|
30
|
-
"delete": [["profile"]]
|
|
31
|
-
},
|
|
32
|
-
"load": "accepts",
|
|
33
|
-
"conformance": [
|
|
34
|
-
"partner_bank_profile_missing",
|
|
35
|
-
"resource_binding_missing"
|
|
36
|
-
],
|
|
37
|
-
"schema": "accepts"
|
|
38
|
-
},
|
|
39
5
|
{
|
|
40
6
|
"id": "operation-map-empty",
|
|
41
|
-
"title": "
|
|
42
|
-
"why": "
|
|
43
|
-
"mutate": { "set": [{ "path": ["operationMap"], "value": {} }] },
|
|
44
|
-
"load": "accepts",
|
|
45
|
-
"conformance": ["operation_map_empty", "required_operation_missing"],
|
|
46
|
-
"schema": "accepts"
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"id": "envelope-unclassified",
|
|
50
|
-
"title": "a live command with no response envelope class",
|
|
51
|
-
"why": "Without a class the runtime cannot tell acceptance from rejection, and a rejection read as silence strands money in flight.",
|
|
52
|
-
"mutate": {
|
|
53
|
-
"delete": [["operationMap", "payout.submit", "envelope"]]
|
|
54
|
-
},
|
|
55
|
-
"load": "accepts",
|
|
56
|
-
"conformance": ["response_envelope_unclassified"],
|
|
57
|
-
"schema": "accepts"
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
"id": "envelope-unconfirmed",
|
|
61
|
-
"title": "the envelope class is declared unconfirmed",
|
|
62
|
-
"why": "`unconfirmed_until_live` is the honest authoring state for a provider that publishes no error sample, and certification is the licence to serve live.",
|
|
63
|
-
"mutate": {
|
|
64
|
-
"set": [
|
|
65
|
-
{
|
|
66
|
-
"path": ["operationMap", "payout.submit", "envelope"],
|
|
67
|
-
"value": "unconfirmed_until_live"
|
|
68
|
-
}
|
|
69
|
-
]
|
|
70
|
-
},
|
|
71
|
-
"load": "accepts",
|
|
72
|
-
"conformance": ["response_envelope_unconfirmed"],
|
|
73
|
-
"schema": "accepts"
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
"id": "status-enquiry-missing",
|
|
77
|
-
"title": "a tenant-initiated REST command with no status enquiry",
|
|
78
|
-
"why": "A submit whose response is lost can then only be resolved by guessing or resubmitting, and resubmitting pays twice.",
|
|
79
|
-
"mutate": {
|
|
80
|
-
"delete": [["operationMap", "payout.submit", "statusEnquiry"]]
|
|
81
|
-
},
|
|
82
|
-
"load": "accepts",
|
|
83
|
-
"conformance": ["status_enquiry_missing"],
|
|
84
|
-
"schema": "accepts"
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
"id": "status-enquiry-exempt-under-sync-clearance",
|
|
88
|
-
"title": "sync clearance carries its truth in band",
|
|
89
|
-
"why": "The exemption is earned by the egress mode: a synchronous clearance answers in the same exchange, so there is no lost response to re-read.",
|
|
90
|
-
"mutate": {
|
|
91
|
-
"set": [{ "path": ["egress"], "value": "sync_clearance" }],
|
|
92
|
-
"delete": [["operationMap", "payout.submit", "statusEnquiry"]]
|
|
93
|
-
},
|
|
94
|
-
"load": "accepts",
|
|
95
|
-
"conformance": [],
|
|
96
|
-
"schema": "accepts"
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
"id": "egress-incoherent-batch-reconcile",
|
|
100
|
-
"title": "a tenant-initiated command on a file-reconciling adapter",
|
|
101
|
-
"why": "batch_reconcile performs no per-command outbound call, so the binding promises a call the runtime cannot make.",
|
|
102
|
-
"mutate": {
|
|
103
|
-
"set": [{ "path": ["egress"], "value": "batch_reconcile" }],
|
|
104
|
-
"delete": [["operationMap", "payout.submit", "statusEnquiry"]]
|
|
105
|
-
},
|
|
106
|
-
"load": "accepts",
|
|
107
|
-
"conformance": ["egress_direction_incoherent"],
|
|
108
|
-
"schema": "accepts"
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
"id": "egress-incoherent-none",
|
|
112
|
-
"title": "a tenant-initiated command on an observation-only adapter",
|
|
113
|
-
"why": "Same rule, the other silent egress mode: `none` sends the provider nothing at all.",
|
|
114
|
-
"mutate": {
|
|
115
|
-
"set": [{ "path": ["egress"], "value": "none" }],
|
|
116
|
-
"delete": [["operationMap", "payout.submit", "statusEnquiry"]]
|
|
117
|
-
},
|
|
118
|
-
"load": "accepts",
|
|
119
|
-
"conformance": ["egress_direction_incoherent"],
|
|
120
|
-
"schema": "accepts"
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
"id": "status-enquiry-without-rest-egress",
|
|
124
|
-
"title": "an enquiry plan an adapter has no transport to execute",
|
|
125
|
-
"why": "An enquiry path on an adapter that performs no REST egress is a plan the runtime can never run.",
|
|
126
|
-
"mutate": {
|
|
127
|
-
"set": [
|
|
128
|
-
{ "path": ["egress"], "value": "none" },
|
|
129
|
-
{
|
|
130
|
-
"path": ["operationMap", "payout.submit", "direction"],
|
|
131
|
-
"value": "system_settlement"
|
|
132
|
-
}
|
|
133
|
-
]
|
|
134
|
-
},
|
|
135
|
-
"load": "accepts",
|
|
136
|
-
"conformance": ["status_enquiry_without_rest_egress"],
|
|
137
|
-
"schema": "accepts"
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
"id": "lifecycle-terminals-incomplete",
|
|
141
|
-
"title": "no spelling maps to returned",
|
|
142
|
-
"why": "A payment ending in a return would be unclassifiable, so the payout polls forever.",
|
|
143
|
-
"mutate": {
|
|
144
|
-
"set": [
|
|
145
|
-
{
|
|
146
|
-
"path": ["profile", "lifecycleVocabulary"],
|
|
147
|
-
"value": {
|
|
148
|
-
"QUEUED": "received",
|
|
149
|
-
"AUTHORISED": "accepted",
|
|
150
|
-
"SETTLED": "processed",
|
|
151
|
-
"DECLINED": "failed",
|
|
152
|
-
"REPLAY_SETTLED": "duplicate_original_succeeded",
|
|
153
|
-
"REPLAY_DECLINED": "duplicate_original_rejected"
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
]
|
|
157
|
-
},
|
|
158
|
-
"load": "accepts",
|
|
159
|
-
"conformance": ["lifecycle_terminals_incomplete"],
|
|
160
|
-
"schema": "accepts"
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
"id": "lifecycle-intake-missing",
|
|
164
|
-
"title": "no spelling maps to received or accepted",
|
|
165
|
-
"why": "An acknowledgement proves the bank took the instruction, never that money moved; with no intake state every ack would have to be graded terminal or dropped.",
|
|
166
|
-
"mutate": {
|
|
167
|
-
"set": [
|
|
168
|
-
{
|
|
169
|
-
"path": ["profile", "lifecycleVocabulary"],
|
|
170
|
-
"value": {
|
|
171
|
-
"SETTLED": "processed",
|
|
172
|
-
"DECLINED": "failed",
|
|
173
|
-
"RECALLED": "returned",
|
|
174
|
-
"REPLAY_SETTLED": "duplicate_original_succeeded",
|
|
175
|
-
"REPLAY_DECLINED": "duplicate_original_rejected"
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
]
|
|
179
|
-
},
|
|
180
|
-
"load": "accepts",
|
|
181
|
-
"conformance": ["lifecycle_intake_missing"],
|
|
182
|
-
"schema": "accepts"
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
"id": "lifecycle-spelling-blank",
|
|
186
|
-
"title": "a spelling that trims to nothing",
|
|
187
|
-
"why": "Runtimes match after trimming and upper-casing, so an empty token can never match anything the bank sends.",
|
|
188
|
-
"mutate": {
|
|
189
|
-
"set": [
|
|
190
|
-
{
|
|
191
|
-
"path": ["profile", "lifecycleVocabulary"],
|
|
192
|
-
"value": {
|
|
193
|
-
"QUEUED": "received",
|
|
194
|
-
"IN_REVIEW": "received",
|
|
195
|
-
"AUTHORISED": "accepted",
|
|
196
|
-
"SETTLED": "processed",
|
|
197
|
-
"DECLINED": "failed",
|
|
198
|
-
"RECALLED": "returned",
|
|
199
|
-
"REPLAY_SETTLED": "duplicate_original_succeeded",
|
|
200
|
-
"REPLAY_DECLINED": "duplicate_original_rejected",
|
|
201
|
-
" ": "failed"
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
]
|
|
205
|
-
},
|
|
206
|
-
"load": "accepts",
|
|
207
|
-
"conformance": ["lifecycle_spelling_blank"],
|
|
208
|
-
"schema": "accepts"
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
"id": "lifecycle-spelling-conflict",
|
|
212
|
-
"title": "two spellings that normalize to one token and disagree",
|
|
213
|
-
"why": "Whichever the runtime matched first would decide the payment's fate, which makes classification order-dependent.",
|
|
214
|
-
"mutate": {
|
|
215
|
-
"set": [
|
|
216
|
-
{
|
|
217
|
-
"path": ["profile", "lifecycleVocabulary"],
|
|
218
|
-
"value": {
|
|
219
|
-
"QUEUED": "received",
|
|
220
|
-
"AUTHORISED": "accepted",
|
|
221
|
-
"SETTLED": "processed",
|
|
222
|
-
"DECLINED": "failed",
|
|
223
|
-
"RECALLED": "returned",
|
|
224
|
-
"REPLAY_SETTLED": "duplicate_original_succeeded",
|
|
225
|
-
"replay_settled ": "duplicate_original_rejected",
|
|
226
|
-
"REPLAY_DECLINED": "duplicate_original_rejected"
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
]
|
|
230
|
-
},
|
|
231
|
-
"load": "accepts",
|
|
232
|
-
"conformance": ["lifecycle_spelling_conflict"],
|
|
233
|
-
"schema": "accepts"
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
"id": "duplicate-verdicts-unmapped",
|
|
237
|
-
"title": "the replay verdicts have no spelling",
|
|
238
|
-
"why": "The dedupe spine is reference-based, so a crash-recovery resubmission comes back as a duplicate verdict; unclassified, it reads as a fresh failure.",
|
|
239
|
-
"mutate": {
|
|
240
|
-
"set": [
|
|
241
|
-
{
|
|
242
|
-
"path": ["profile", "lifecycleVocabulary"],
|
|
243
|
-
"value": {
|
|
244
|
-
"QUEUED": "received",
|
|
245
|
-
"AUTHORISED": "accepted",
|
|
246
|
-
"SETTLED": "processed",
|
|
247
|
-
"DECLINED": "failed",
|
|
248
|
-
"RECALLED": "returned"
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
]
|
|
252
|
-
},
|
|
253
|
-
"load": "accepts",
|
|
254
|
-
"conformance": ["duplicate_verdicts_unmapped"],
|
|
255
|
-
"schema": "accepts"
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
"id": "statement-formats-empty",
|
|
259
|
-
"title": "no statement format is declared",
|
|
260
|
-
"why": "Statements are bank truth; without one, reconciliation has nothing to prove the ledger against.",
|
|
261
|
-
"mutate": {
|
|
262
|
-
"set": [{ "path": ["profile", "statementFormats"], "value": [] }]
|
|
263
|
-
},
|
|
264
|
-
"load": "accepts",
|
|
265
|
-
"conformance": ["statement_formats_empty"],
|
|
266
|
-
"schema": "accepts"
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
"id": "statement-debit-reference-unestablished",
|
|
270
|
-
"title": "which reference comes back on a debit is unknown",
|
|
271
|
-
"why": "Pick the wrong one and every payout the platform instructed reads as an unexplained debit.",
|
|
272
|
-
"mutate": { "delete": [["profile", "statementDebitReference"]] },
|
|
273
|
-
"load": "accepts",
|
|
274
|
-
"conformance": ["statement_debit_reference_unestablished"],
|
|
275
|
-
"schema": "accepts"
|
|
276
|
-
},
|
|
277
|
-
{
|
|
278
|
-
"id": "statement-window-missing",
|
|
279
|
-
"title": "a declared format with no fetch window",
|
|
280
|
-
"why": "The runtime cannot schedule a retrieval it has no window for.",
|
|
281
|
-
"mutate": {
|
|
282
|
-
"set": [
|
|
283
|
-
{
|
|
284
|
-
"path": ["profile", "statementFormats"],
|
|
285
|
-
"value": ["camt_053", "camt_052", "mt940"]
|
|
286
|
-
}
|
|
287
|
-
]
|
|
288
|
-
},
|
|
289
|
-
"load": "accepts",
|
|
290
|
-
"conformance": ["statement_window_missing"],
|
|
291
|
-
"schema": "accepts"
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
"id": "statement-window-without-paging",
|
|
295
|
-
"title": "an end-of-day format with no paging facts",
|
|
296
|
-
"why": "T-1 pages over history, so how far back the archive goes and how many days one request covers are both required.",
|
|
297
|
-
"mutate": {
|
|
298
|
-
"set": [
|
|
299
|
-
{
|
|
300
|
-
"path": ["profile", "windows", "statements", "camt_053"],
|
|
301
|
-
"value": { "availability": "T-1" }
|
|
302
|
-
}
|
|
303
|
-
]
|
|
304
|
-
},
|
|
305
|
-
"load": "accepts",
|
|
306
|
-
"conformance": ["statement_window_incomplete"],
|
|
307
|
-
"schema": "accepts"
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
"id": "statement-window-availability-unrecognized",
|
|
311
|
-
"title": "an availability outside the vocabulary",
|
|
312
|
-
"why": "Every window rule branches on availability, so an unrecognized value must not slide through green.",
|
|
313
|
-
"mutate": {
|
|
314
|
-
"set": [
|
|
315
|
-
{
|
|
316
|
-
"path": ["profile", "windows", "statements", "camt_053"],
|
|
317
|
-
"value": { "availability": "whenever", "lookbackDays": 5 }
|
|
318
|
-
}
|
|
319
|
-
]
|
|
320
|
-
},
|
|
321
|
-
"load": {
|
|
322
|
-
"throws": "windows.statements.camt_053.availability must be one of T-1, today_only"
|
|
323
|
-
},
|
|
324
|
-
"conformance": ["statement_window_incomplete"],
|
|
325
|
-
"schema": "rejects"
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
"id": "statement-window-availability-missing",
|
|
329
|
-
"title": "a truthy window with no availability at all",
|
|
330
|
-
"why": "TypeScript requires the field; a plain-JSON manifest can omit it, and an availability-less window is unschedulable.",
|
|
331
|
-
"mutate": {
|
|
332
|
-
"set": [
|
|
333
|
-
{
|
|
334
|
-
"path": ["profile", "windows", "statements", "camt_053"],
|
|
335
|
-
"value": { "lookbackDays": 5 }
|
|
336
|
-
}
|
|
337
|
-
]
|
|
338
|
-
},
|
|
339
|
-
"load": {
|
|
340
|
-
"throws": "windows.statements.camt_053.availability must be one of T-1, today_only"
|
|
341
|
-
},
|
|
342
|
-
"conformance": ["statement_window_incomplete"],
|
|
343
|
-
"schema": "rejects"
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
"id": "statement-window-lookback-not-positive",
|
|
347
|
-
"title": "a lookback of zero days",
|
|
348
|
-
"why": "A window that pages back nothing cannot fetch the history it claims to.",
|
|
7
|
+
"title": "operation-map-empty",
|
|
8
|
+
"why": "operation-map-empty",
|
|
349
9
|
"mutate": {
|
|
350
10
|
"set": [
|
|
351
11
|
{
|
|
352
|
-
"path": ["
|
|
353
|
-
"value": {
|
|
354
|
-
"availability": "T-1",
|
|
355
|
-
"lookbackDays": 0,
|
|
356
|
-
"statementDaysPerRequest": 7
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
]
|
|
360
|
-
},
|
|
361
|
-
"load": {
|
|
362
|
-
"throws": "windows.statements.camt_053.lookbackDays must be a positive integer"
|
|
363
|
-
},
|
|
364
|
-
"conformance": ["statement_window_incomplete"],
|
|
365
|
-
"schema": "rejects"
|
|
366
|
-
},
|
|
367
|
-
{
|
|
368
|
-
"id": "charges-unestablished",
|
|
369
|
-
"title": "the bank's charge billing is unmodeled",
|
|
370
|
-
"why": "The bank's own charge debits would land as unexplained statement lines.",
|
|
371
|
-
"mutate": { "delete": [["profile", "charges"]] },
|
|
372
|
-
"load": "accepts",
|
|
373
|
-
"conformance": ["charges_unestablished"],
|
|
374
|
-
"schema": "accepts"
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
"id": "charge-narrative-unestablished",
|
|
378
|
-
"title": "post-hoc charges with no wording to recognize them by",
|
|
379
|
-
"why": "A post-hoc charge arrives as a bank-initiated debit recognizable only by its narrative.",
|
|
380
|
-
"mutate": {
|
|
381
|
-
"set": [
|
|
382
|
-
{
|
|
383
|
-
"path": ["profile", "charges"],
|
|
384
|
-
"value": {
|
|
385
|
-
"narratives": { "charge": [], "vat": ["MERIDIAN TAX"] },
|
|
386
|
-
"posting": "post_hoc",
|
|
387
|
-
"vat": "separate_line"
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
]
|
|
391
|
-
},
|
|
392
|
-
"load": "accepts",
|
|
393
|
-
"conformance": ["charge_narrative_unestablished"],
|
|
394
|
-
"schema": "accepts"
|
|
395
|
-
},
|
|
396
|
-
{
|
|
397
|
-
"id": "vat-narrative-unestablished",
|
|
398
|
-
"title": "a separate tax line with no wording to recognize it by",
|
|
399
|
-
"why": "A separate VAT line is its own debit with its own wording.",
|
|
400
|
-
"mutate": {
|
|
401
|
-
"set": [
|
|
402
|
-
{
|
|
403
|
-
"path": ["profile", "charges"],
|
|
404
|
-
"value": {
|
|
405
|
-
"narratives": { "charge": ["MERIDIAN FEE"], "vat": [] },
|
|
406
|
-
"posting": "post_hoc",
|
|
407
|
-
"vat": "separate_line"
|
|
408
|
-
}
|
|
12
|
+
"path": ["operationMap"],
|
|
13
|
+
"value": {}
|
|
409
14
|
}
|
|
410
15
|
]
|
|
411
16
|
},
|
|
412
17
|
"load": "accepts",
|
|
413
|
-
"conformance": ["
|
|
18
|
+
"conformance": ["operation_map_empty"],
|
|
414
19
|
"schema": "accepts"
|
|
415
20
|
},
|
|
416
21
|
{
|
|
417
|
-
"id": "
|
|
418
|
-
"title": "
|
|
419
|
-
"why": "
|
|
22
|
+
"id": "resource-binding-missing",
|
|
23
|
+
"title": "resource-binding-missing",
|
|
24
|
+
"why": "resource-binding-missing",
|
|
420
25
|
"mutate": {
|
|
421
26
|
"set": [
|
|
422
27
|
{
|
|
423
|
-
"path": ["
|
|
424
|
-
"value": {
|
|
425
|
-
"posting": "at_instruction",
|
|
426
|
-
"vat": "included_in_charge"
|
|
427
|
-
}
|
|
28
|
+
"path": ["bindings"],
|
|
29
|
+
"value": {}
|
|
428
30
|
}
|
|
429
31
|
]
|
|
430
32
|
},
|
|
431
33
|
"load": "accepts",
|
|
432
|
-
"conformance": [],
|
|
34
|
+
"conformance": ["resource_binding_missing"],
|
|
433
35
|
"schema": "accepts"
|
|
434
36
|
},
|
|
435
37
|
{
|
|
436
|
-
"id": "
|
|
437
|
-
"title": "
|
|
438
|
-
"why": "
|
|
38
|
+
"id": "operation-key-mismatch",
|
|
39
|
+
"title": "operation-key-mismatch",
|
|
40
|
+
"why": "operation-key-mismatch",
|
|
439
41
|
"mutate": {
|
|
440
42
|
"set": [
|
|
441
43
|
{
|
|
442
|
-
"path": [
|
|
443
|
-
|
|
444
|
-
"windows",
|
|
445
|
-
"paymentCutoff",
|
|
446
|
-
"afterCutoffValueDate"
|
|
447
|
-
],
|
|
448
|
-
"value": "D+1.5"
|
|
449
|
-
}
|
|
450
|
-
]
|
|
451
|
-
},
|
|
452
|
-
"load": "accepts",
|
|
453
|
-
"conformance": ["value_date_offset_invalid"],
|
|
454
|
-
"schema": "rejects",
|
|
455
|
-
"note": "The schema encodes the D+<days> shape as a pattern; registry load does not read the offsets at all, so conformance is the gate that catches this in TypeScript."
|
|
456
|
-
},
|
|
457
|
-
{
|
|
458
|
-
"id": "value-date-offset-wrong-prefix",
|
|
459
|
-
"title": "a T+1 offset where D+ is the vocabulary",
|
|
460
|
-
"why": "The template literal type stops this in TypeScript and nothing stops it in a JSON manifest.",
|
|
461
|
-
"mutate": {
|
|
462
|
-
"set": [
|
|
463
|
-
{
|
|
464
|
-
"path": [
|
|
465
|
-
"profile",
|
|
466
|
-
"windows",
|
|
467
|
-
"paymentCutoff",
|
|
468
|
-
"beforeCutoffValueDate"
|
|
469
|
-
],
|
|
470
|
-
"value": "T+1"
|
|
471
|
-
}
|
|
472
|
-
]
|
|
473
|
-
},
|
|
474
|
-
"load": "accepts",
|
|
475
|
-
"conformance": ["value_date_offset_invalid"],
|
|
476
|
-
"schema": "rejects",
|
|
477
|
-
"note": "Same split as value-date-offset-fractional: pattern in the schema, rule in conformance, silence at registry load."
|
|
478
|
-
},
|
|
479
|
-
{
|
|
480
|
-
"id": "cutoff-timezone-invalid",
|
|
481
|
-
"title": "a cutoff read on a zone that does not exist",
|
|
482
|
-
"why": "The cutoff is the bank's wall clock; an unreadable zone means every value date is computed somewhere else.",
|
|
483
|
-
"mutate": {
|
|
484
|
-
"set": [
|
|
485
|
-
{
|
|
486
|
-
"path": ["profile", "windows", "paymentCutoff", "timeZone"],
|
|
487
|
-
"value": "Mars/Phobos"
|
|
44
|
+
"path": ["operationMap", "boundary.observe", "operation"],
|
|
45
|
+
"value": "wrong"
|
|
488
46
|
}
|
|
489
47
|
]
|
|
490
48
|
},
|
|
491
49
|
"load": {
|
|
492
|
-
"throws": "
|
|
493
|
-
},
|
|
494
|
-
"conformance": ["cutoff_clock_invalid"],
|
|
495
|
-
"schema": "accepts",
|
|
496
|
-
"note": "JSON Schema cannot check an IANA zone name; the schema only requires a string. Both runtime gates resolve it through Intl.DateTimeFormat."
|
|
497
|
-
},
|
|
498
|
-
{
|
|
499
|
-
"id": "cutoff-timezone-missing",
|
|
500
|
-
"title": "no time zone at all",
|
|
501
|
-
"why": "Intl.DateTimeFormat reads an undefined zone as the host's, so absence would silently schedule the cutoff on the platform's clock.",
|
|
502
|
-
"mutate": {
|
|
503
|
-
"delete": [["profile", "windows", "paymentCutoff", "timeZone"]]
|
|
504
|
-
},
|
|
505
|
-
"load": {
|
|
506
|
-
"throws": "windows.paymentCutoff.timeZone must be a valid IANA time zone"
|
|
507
|
-
},
|
|
508
|
-
"conformance": ["cutoff_clock_invalid"],
|
|
509
|
-
"schema": "rejects"
|
|
510
|
-
},
|
|
511
|
-
{
|
|
512
|
-
"id": "cutoff-time-not-a-clock",
|
|
513
|
-
"title": "a cutoff time written in words",
|
|
514
|
-
"why": "The runtime compares HH:MM; prose compares as never.",
|
|
515
|
-
"mutate": {
|
|
516
|
-
"set": [
|
|
517
|
-
{
|
|
518
|
-
"path": ["profile", "windows", "paymentCutoff", "time"],
|
|
519
|
-
"value": "half past nine"
|
|
520
|
-
}
|
|
521
|
-
]
|
|
522
|
-
},
|
|
523
|
-
"load": { "throws": "windows.paymentCutoff.time must be HH:MM" },
|
|
524
|
-
"conformance": ["cutoff_clock_invalid"],
|
|
525
|
-
"schema": "rejects"
|
|
526
|
-
},
|
|
527
|
-
{
|
|
528
|
-
"id": "cutoff-window-regressions",
|
|
529
|
-
"title": "missing the cutoff moves value earlier, and past the forward window",
|
|
530
|
-
"why": "Two independent facts, reported independently on one run: a later cutoff can only push value later, and the bank must be able to honor its own after-cutoff date.",
|
|
531
|
-
"mutate": {
|
|
532
|
-
"set": [
|
|
533
|
-
{
|
|
534
|
-
"path": [
|
|
535
|
-
"profile",
|
|
536
|
-
"windows",
|
|
537
|
-
"paymentCutoff",
|
|
538
|
-
"beforeCutoffValueDate"
|
|
539
|
-
],
|
|
540
|
-
"value": "D+5"
|
|
541
|
-
},
|
|
542
|
-
{
|
|
543
|
-
"path": [
|
|
544
|
-
"profile",
|
|
545
|
-
"windows",
|
|
546
|
-
"paymentCutoff",
|
|
547
|
-
"afterCutoffValueDate"
|
|
548
|
-
],
|
|
549
|
-
"value": "D+2"
|
|
550
|
-
},
|
|
551
|
-
{
|
|
552
|
-
"path": ["profile", "windows", "valueDateMaxDaysAhead"],
|
|
553
|
-
"value": 1
|
|
554
|
-
}
|
|
555
|
-
]
|
|
50
|
+
"throws": "must match operation"
|
|
556
51
|
},
|
|
557
|
-
"
|
|
558
|
-
"conformance": ["cutoff_window_regression", "cutoff_window_regression"],
|
|
52
|
+
"conformance": ["declaration_invalid"],
|
|
559
53
|
"schema": "accepts"
|
|
560
54
|
},
|
|
561
55
|
{
|
|
562
|
-
"id": "
|
|
563
|
-
"title": "
|
|
564
|
-
"why": "
|
|
56
|
+
"id": "invalid-provider",
|
|
57
|
+
"title": "invalid-provider",
|
|
58
|
+
"why": "invalid-provider",
|
|
565
59
|
"mutate": {
|
|
566
60
|
"set": [
|
|
567
61
|
{
|
|
568
|
-
"path": ["
|
|
569
|
-
"value":
|
|
62
|
+
"path": ["provider"],
|
|
63
|
+
"value": "Invalid Adapter"
|
|
570
64
|
}
|
|
571
65
|
]
|
|
572
66
|
},
|
|
573
67
|
"load": {
|
|
574
|
-
"throws": "
|
|
68
|
+
"throws": "invalid provider key"
|
|
575
69
|
},
|
|
576
|
-
"conformance": ["
|
|
70
|
+
"conformance": ["declaration_invalid"],
|
|
577
71
|
"schema": "rejects"
|
|
578
72
|
},
|
|
579
73
|
{
|
|
580
|
-
"id": "
|
|
581
|
-
"title": "
|
|
582
|
-
"why": "
|
|
583
|
-
"mutate": { "set": [{ "path": ["bindings"], "value": {} }] },
|
|
584
|
-
"load": "accepts",
|
|
585
|
-
"conformance": ["resource_binding_missing"],
|
|
586
|
-
"schema": "accepts"
|
|
587
|
-
},
|
|
588
|
-
{
|
|
589
|
-
"id": "webhook-resource-binding-missing",
|
|
590
|
-
"title": "a webhook plan for a resource kind the adapter does not bind",
|
|
591
|
-
"why": "The webhook arm of the same rule: a plan names a resource, so that resource needs a binding mode too.",
|
|
74
|
+
"id": "optional-subject-requirement",
|
|
75
|
+
"title": "optional-subject-requirement",
|
|
76
|
+
"why": "optional-subject-requirement",
|
|
592
77
|
"mutate": {
|
|
593
78
|
"set": [
|
|
594
79
|
{
|
|
595
|
-
"path": ["
|
|
596
|
-
"value":
|
|
597
|
-
|
|
598
|
-
"
|
|
599
|
-
"
|
|
600
|
-
"
|
|
601
|
-
"optionalPayloadFields": [],
|
|
602
|
-
"requiredFieldGroups": [],
|
|
603
|
-
"resourceIdField": "depositId",
|
|
604
|
-
"resourceKind": "deposit",
|
|
605
|
-
"timestampField": "receivedAt"
|
|
80
|
+
"path": ["operationMap", "boundary.observe", "subjectRequirements"],
|
|
81
|
+
"value": [
|
|
82
|
+
{
|
|
83
|
+
"name": "reference",
|
|
84
|
+
"type": "text",
|
|
85
|
+
"optional": true
|
|
606
86
|
}
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
]
|
|
610
|
-
},
|
|
611
|
-
"load": "accepts",
|
|
612
|
-
"conformance": ["resource_binding_missing"],
|
|
613
|
-
"schema": "accepts"
|
|
614
|
-
},
|
|
615
|
-
{
|
|
616
|
-
"id": "lifecycle-vocabulary-empty",
|
|
617
|
-
"title": "a profile with no lifecycle vocabulary",
|
|
618
|
-
"why": "Nothing the bank says could be classified at all.",
|
|
619
|
-
"mutate": {
|
|
620
|
-
"set": [{ "path": ["profile", "lifecycleVocabulary"], "value": {} }]
|
|
621
|
-
},
|
|
622
|
-
"load": { "throws": "partner-bank profile has no lifecycle vocabulary" },
|
|
623
|
-
"conformance": [
|
|
624
|
-
"lifecycle_terminals_incomplete",
|
|
625
|
-
"lifecycle_intake_missing",
|
|
626
|
-
"duplicate_verdicts_unmapped"
|
|
627
|
-
],
|
|
628
|
-
"schema": "rejects"
|
|
629
|
-
},
|
|
630
|
-
{
|
|
631
|
-
"id": "dedupe-keys-empty",
|
|
632
|
-
"title": "an idempotency spine with no dedupe keys",
|
|
633
|
-
"why": "The spine is what makes a resubmission answerable; empty, every recovery is a fresh payment.",
|
|
634
|
-
"mutate": {
|
|
635
|
-
"set": [
|
|
636
|
-
{ "path": ["profile", "idempotencySpine", "dedupeKeys"], "value": [] }
|
|
637
|
-
]
|
|
638
|
-
},
|
|
639
|
-
"load": { "throws": "idempotencySpine.dedupeKeys is empty" },
|
|
640
|
-
"conformance": [],
|
|
641
|
-
"schema": "rejects"
|
|
642
|
-
},
|
|
643
|
-
{
|
|
644
|
-
"id": "dedupe-keys-duplicated",
|
|
645
|
-
"title": "the same dedupe key listed twice",
|
|
646
|
-
"why": "The list widened from a fixed triple, so it now carries the guarantees the tuple used to give for free.",
|
|
647
|
-
"mutate": {
|
|
648
|
-
"set": [
|
|
649
|
-
{
|
|
650
|
-
"path": ["profile", "idempotencySpine", "dedupeKeys"],
|
|
651
|
-
"value": ["instructionId", "instructionId"]
|
|
652
|
-
}
|
|
653
|
-
]
|
|
654
|
-
},
|
|
655
|
-
"load": { "throws": "idempotencySpine.dedupeKeys contains duplicates" },
|
|
656
|
-
"conformance": [],
|
|
657
|
-
"schema": "rejects"
|
|
658
|
-
},
|
|
659
|
-
{
|
|
660
|
-
"id": "spine-max-length-not-positive",
|
|
661
|
-
"title": "a reference length of zero",
|
|
662
|
-
"why": "A reference that cannot hold a character cannot carry the spine.",
|
|
663
|
-
"mutate": {
|
|
664
|
-
"set": [
|
|
665
|
-
{
|
|
666
|
-
"path": ["profile", "idempotencySpine", "instructionId"],
|
|
667
|
-
"value": { "maxLength": 0 }
|
|
87
|
+
]
|
|
668
88
|
}
|
|
669
89
|
]
|
|
670
90
|
},
|
|
671
91
|
"load": {
|
|
672
|
-
"throws": "
|
|
92
|
+
"throws": "action requirements cannot be optional"
|
|
673
93
|
},
|
|
674
|
-
"conformance": [],
|
|
94
|
+
"conformance": ["declaration_invalid"],
|
|
675
95
|
"schema": "rejects"
|
|
676
96
|
},
|
|
677
97
|
{
|
|
678
|
-
"id": "
|
|
679
|
-
"title": "
|
|
680
|
-
"why": "
|
|
98
|
+
"id": "invalid-subject-field",
|
|
99
|
+
"title": "invalid-subject-field",
|
|
100
|
+
"why": "invalid-subject-field",
|
|
681
101
|
"mutate": {
|
|
682
102
|
"set": [
|
|
683
103
|
{
|
|
684
|
-
"path": ["
|
|
685
|
-
"value": [
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
"load": { "throws": "partner-bank profile must support polling" },
|
|
690
|
-
"conformance": [],
|
|
691
|
-
"schema": "rejects"
|
|
692
|
-
},
|
|
693
|
-
{
|
|
694
|
-
"id": "credential-locator-blank",
|
|
695
|
-
"title": "an environment locator that names nothing",
|
|
696
|
-
"why": "ADL stores locators, never values, so a blank locator is the whole credential gone.",
|
|
697
|
-
"mutate": {
|
|
698
|
-
"set": [
|
|
699
|
-
{
|
|
700
|
-
"path": ["config", "credentialRef"],
|
|
701
|
-
"value": { "source": "environment", "name": " " }
|
|
702
|
-
}
|
|
703
|
-
]
|
|
704
|
-
},
|
|
705
|
-
"load": { "throws": "config.credentialRef.name must not be blank" },
|
|
706
|
-
"conformance": [],
|
|
707
|
-
"schema": "rejects"
|
|
708
|
-
},
|
|
709
|
-
{
|
|
710
|
-
"id": "keychain-locator-blank",
|
|
711
|
-
"title": "a keychain locator missing its account",
|
|
712
|
-
"why": "Same rule on the other locator shape.",
|
|
713
|
-
"mutate": {
|
|
714
|
-
"set": [
|
|
715
|
-
{
|
|
716
|
-
"path": ["config", "credentialRef"],
|
|
717
|
-
"value": {
|
|
718
|
-
"source": "keychain",
|
|
719
|
-
"account": " ",
|
|
720
|
-
"service": "meridian"
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
]
|
|
724
|
-
},
|
|
725
|
-
"load": { "throws": "config.credentialRef.account must not be blank" },
|
|
726
|
-
"conformance": [],
|
|
727
|
-
"schema": "rejects"
|
|
728
|
-
},
|
|
729
|
-
{
|
|
730
|
-
"id": "base-url-not-absolute",
|
|
731
|
-
"title": "a base URL that is only a path",
|
|
732
|
-
"why": "The runtime joins operation paths onto this; a relative base resolves against nothing.",
|
|
733
|
-
"mutate": {
|
|
734
|
-
"set": [{ "path": ["config", "baseUrl"], "value": "/v2" }]
|
|
735
|
-
},
|
|
736
|
-
"load": { "throws": "config.baseUrl must be an absolute URL" },
|
|
737
|
-
"conformance": [],
|
|
738
|
-
"schema": "rejects"
|
|
739
|
-
},
|
|
740
|
-
{
|
|
741
|
-
"id": "provider-key-invalid",
|
|
742
|
-
"title": "a provider key with spaces and capitals",
|
|
743
|
-
"why": "The key is an identity used in logs, metrics, and the registry; it is lowercase and machine-shaped.",
|
|
744
|
-
"mutate": {
|
|
745
|
-
"set": [{ "path": ["provider"], "value": "Meridian Bank" }]
|
|
746
|
-
},
|
|
747
|
-
"load": { "throws": "invalid provider key Meridian Bank" },
|
|
748
|
-
"conformance": [],
|
|
749
|
-
"schema": "rejects"
|
|
750
|
-
},
|
|
751
|
-
{
|
|
752
|
-
"id": "operation-map-key-drift",
|
|
753
|
-
"title": "an operation map key that does not match its binding",
|
|
754
|
-
"why": "The key is how the runtime looks the binding up; drift means the runtime finds nothing under the name it holds.",
|
|
755
|
-
"mutate": {
|
|
756
|
-
"set": [
|
|
757
|
-
{
|
|
758
|
-
"path": ["operationMap"],
|
|
759
|
-
"value": {
|
|
760
|
-
"submit": {
|
|
761
|
-
"operation": "payout.submit",
|
|
762
|
-
"direction": "tenant_initiated",
|
|
763
|
-
"obligationKind": "payout_submission",
|
|
764
|
-
"resourceIdPath": "payoutId",
|
|
765
|
-
"resourceKind": "payout",
|
|
766
|
-
"envelope": "http_status_error_body",
|
|
767
|
-
"statusEnquiry": {
|
|
768
|
-
"keys": "provider_reference",
|
|
769
|
-
"pathTemplate": "/v2/payments/{providerReference}"
|
|
770
|
-
}
|
|
104
|
+
"path": ["operationMap", "boundary.observe", "subjectRequirements"],
|
|
105
|
+
"value": [
|
|
106
|
+
{
|
|
107
|
+
"name": "reference",
|
|
108
|
+
"type": "unknown"
|
|
771
109
|
}
|
|
772
|
-
|
|
773
|
-
}
|
|
774
|
-
]
|
|
775
|
-
},
|
|
776
|
-
"load": {
|
|
777
|
-
"throws": "operationMap key submit must match operation payout.submit"
|
|
778
|
-
},
|
|
779
|
-
"conformance": ["required_operation_missing"],
|
|
780
|
-
"schema": "accepts",
|
|
781
|
-
"note": "JSON Schema has no way to require a property's key to equal a value inside it, so this one is registry-load only."
|
|
782
|
-
},
|
|
783
|
-
{
|
|
784
|
-
"id": "webhook-map-key-drift",
|
|
785
|
-
"title": "a webhook map key that does not match its event kind",
|
|
786
|
-
"why": "Same reason as the operation map: the key is the lookup.",
|
|
787
|
-
"mutate": {
|
|
788
|
-
"set": [
|
|
789
|
-
{
|
|
790
|
-
"path": ["webhookMap"],
|
|
791
|
-
"value": {
|
|
792
|
-
"wrong": {
|
|
793
|
-
"eventKind": "payout.settled",
|
|
794
|
-
"operationName": "payout.complete",
|
|
795
|
-
"requiredPayloadFields": ["providerReference"],
|
|
796
|
-
"optionalPayloadFields": [],
|
|
797
|
-
"requiredFieldGroups": [],
|
|
798
|
-
"resourceIdField": "payoutId",
|
|
799
|
-
"resourceKind": "payout",
|
|
800
|
-
"timestampField": "completedAt"
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
]
|
|
805
|
-
},
|
|
806
|
-
"load": {
|
|
807
|
-
"throws": "webhookMap key wrong must match eventKind payout.settled"
|
|
808
|
-
},
|
|
809
|
-
"conformance": [],
|
|
810
|
-
"schema": "accepts",
|
|
811
|
-
"note": "Key-equals-value again: expressible in the validator, not in the schema."
|
|
812
|
-
},
|
|
813
|
-
{
|
|
814
|
-
"id": "status-enquiry-path-without-placeholder",
|
|
815
|
-
"title": "an enquiry path with nowhere to put the reference",
|
|
816
|
-
"why": "The runtime substitutes the declared key into the template; without the placeholder it would call the same URL for every payment.",
|
|
817
|
-
"mutate": {
|
|
818
|
-
"set": [
|
|
819
|
-
{
|
|
820
|
-
"path": ["operationMap", "payout.submit", "statusEnquiry"],
|
|
821
|
-
"value": {
|
|
822
|
-
"keys": "provider_reference",
|
|
823
|
-
"pathTemplate": "/v2/payments/{id}"
|
|
824
|
-
}
|
|
110
|
+
]
|
|
825
111
|
}
|
|
826
112
|
]
|
|
827
113
|
},
|
|
828
114
|
"load": {
|
|
829
|
-
"throws": "
|
|
115
|
+
"throws": "does not match UDL"
|
|
830
116
|
},
|
|
831
|
-
"conformance": [],
|
|
117
|
+
"conformance": ["declaration_invalid"],
|
|
832
118
|
"schema": "rejects"
|
|
833
119
|
}
|
|
834
120
|
]
|