@hyperscale0/adl 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORS +8 -0
- package/CHANGELOG.md +47 -0
- package/LICENSE +661 -0
- package/LICENSING.md +51 -0
- package/README.md +165 -0
- package/SECURITY.md +29 -0
- package/TRADEMARKS.md +30 -0
- package/conformance/cases.json +835 -0
- package/dist/conformance.d.ts +41 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +486 -0
- package/dist/conformance.js.map +1 -0
- package/dist/index.d.ts +219 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +144 -0
- package/dist/index.js.map +1 -0
- package/dist/vocabulary.d.ts +94 -0
- package/dist/vocabulary.d.ts.map +1 -0
- package/dist/vocabulary.js +139 -0
- package/dist/vocabulary.js.map +1 -0
- package/docs/authoring-guide.md +431 -0
- package/examples/meridian-bank/README.md +65 -0
- package/examples/meridian-bank/example.spec.ts +162 -0
- package/examples/meridian-bank/fixtures/payout-status-replay.json +19 -0
- package/examples/meridian-bank/fixtures/payout-status-settled.json +19 -0
- package/examples/meridian-bank/fixtures/payout-submit-accepted.json +26 -0
- package/examples/meridian-bank/fixtures/payout-submit-declined.json +25 -0
- package/examples/meridian-bank/fixtures/statement-camt053.json +45 -0
- package/examples/meridian-bank/fixtures/webhook-payout-settled.json +11 -0
- package/examples/meridian-bank/index.ts +262 -0
- package/package.json +79 -0
- package/spec/manifest.schema.json +757 -0
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "The conformance corpus. Each case takes the certified base declaration, breaks exactly one fact, and records what each gate says about the result: `load` is registry validation (createProviderAdapterRegistry), `conformance` is partnerBankConformanceFindings, `schema` is spec/manifest.schema.json. Where two gates disagree, `note` says why. Read this file to see every rule and what trips it. test/conformance-cases.spec.ts runs it and fails if any conformance code goes unexercised.",
|
|
3
|
+
"base": "examples/meridian-bank -- meridianAdapters[0], the payout_execution adapter",
|
|
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.",
|
|
5
|
+
"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
|
+
{
|
|
40
|
+
"id": "operation-map-empty",
|
|
41
|
+
"title": "no operations are bound",
|
|
42
|
+
"why": "An adapter with no commands serves nothing, and every command-level rule below would be vacuously green.",
|
|
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.",
|
|
349
|
+
"mutate": {
|
|
350
|
+
"set": [
|
|
351
|
+
{
|
|
352
|
+
"path": ["profile", "windows", "statements", "camt_053"],
|
|
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
|
+
}
|
|
409
|
+
}
|
|
410
|
+
]
|
|
411
|
+
},
|
|
412
|
+
"load": "accepts",
|
|
413
|
+
"conformance": ["vat_narrative_unestablished"],
|
|
414
|
+
"schema": "accepts"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
"id": "charges-netted-need-no-narrative",
|
|
418
|
+
"title": "charges netted at instruction, tax folded in",
|
|
419
|
+
"why": "Nothing posts separately, so there is no debit to recognize and no narrative to declare.",
|
|
420
|
+
"mutate": {
|
|
421
|
+
"set": [
|
|
422
|
+
{
|
|
423
|
+
"path": ["profile", "charges"],
|
|
424
|
+
"value": {
|
|
425
|
+
"posting": "at_instruction",
|
|
426
|
+
"vat": "included_in_charge"
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
]
|
|
430
|
+
},
|
|
431
|
+
"load": "accepts",
|
|
432
|
+
"conformance": [],
|
|
433
|
+
"schema": "accepts"
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
"id": "value-date-offset-fractional",
|
|
437
|
+
"title": "a value date offset of a day and a half",
|
|
438
|
+
"why": "Value dates are whole banking days; a fraction would be NaN in every comparison that follows.",
|
|
439
|
+
"mutate": {
|
|
440
|
+
"set": [
|
|
441
|
+
{
|
|
442
|
+
"path": [
|
|
443
|
+
"profile",
|
|
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"
|
|
488
|
+
}
|
|
489
|
+
]
|
|
490
|
+
},
|
|
491
|
+
"load": {
|
|
492
|
+
"throws": "windows.paymentCutoff.timeZone must be a valid IANA time zone"
|
|
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
|
+
]
|
|
556
|
+
},
|
|
557
|
+
"load": "accepts",
|
|
558
|
+
"conformance": ["cutoff_window_regression", "cutoff_window_regression"],
|
|
559
|
+
"schema": "accepts"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
"id": "value-date-window-not-positive",
|
|
563
|
+
"title": "a forward window of zero days",
|
|
564
|
+
"why": "The bank must be able to honor the after-cutoff value date it declares.",
|
|
565
|
+
"mutate": {
|
|
566
|
+
"set": [
|
|
567
|
+
{
|
|
568
|
+
"path": ["profile", "windows", "valueDateMaxDaysAhead"],
|
|
569
|
+
"value": 0
|
|
570
|
+
}
|
|
571
|
+
]
|
|
572
|
+
},
|
|
573
|
+
"load": {
|
|
574
|
+
"throws": "windows.valueDateMaxDaysAhead must be a positive integer"
|
|
575
|
+
},
|
|
576
|
+
"conformance": ["cutoff_window_regression"],
|
|
577
|
+
"schema": "rejects"
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
"id": "resource-binding-missing",
|
|
581
|
+
"title": "a settled resource kind with no binding mode",
|
|
582
|
+
"why": "Provider facts commit onto platform resources through a declared match mode; without one the bank's answer has no lawful way to land.",
|
|
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.",
|
|
592
|
+
"mutate": {
|
|
593
|
+
"set": [
|
|
594
|
+
{
|
|
595
|
+
"path": ["webhookMap"],
|
|
596
|
+
"value": {
|
|
597
|
+
"deposit.received": {
|
|
598
|
+
"eventKind": "deposit.received",
|
|
599
|
+
"operationName": "deposit.record",
|
|
600
|
+
"requiredPayloadFields": ["amount"],
|
|
601
|
+
"optionalPayloadFields": [],
|
|
602
|
+
"requiredFieldGroups": [],
|
|
603
|
+
"resourceIdField": "depositId",
|
|
604
|
+
"resourceKind": "deposit",
|
|
605
|
+
"timestampField": "receivedAt"
|
|
606
|
+
}
|
|
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 }
|
|
668
|
+
}
|
|
669
|
+
]
|
|
670
|
+
},
|
|
671
|
+
"load": {
|
|
672
|
+
"throws": "idempotencySpine.instructionId.maxLength must be a positive integer"
|
|
673
|
+
},
|
|
674
|
+
"conformance": [],
|
|
675
|
+
"schema": "rejects"
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
"id": "notification-mechanisms-without-poll",
|
|
679
|
+
"title": "a bank we would only hear from by callback",
|
|
680
|
+
"why": "Poll-first is the portable floor: a callback is at best a hint that a poll is due, never a fact to post from.",
|
|
681
|
+
"mutate": {
|
|
682
|
+
"set": [
|
|
683
|
+
{
|
|
684
|
+
"path": ["profile", "notificationMechanisms"],
|
|
685
|
+
"value": ["webhook"]
|
|
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
|
+
}
|
|
771
|
+
}
|
|
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
|
+
}
|
|
825
|
+
}
|
|
826
|
+
]
|
|
827
|
+
},
|
|
828
|
+
"load": {
|
|
829
|
+
"throws": "payout.submit.statusEnquiry.pathTemplate must include {providerReference}"
|
|
830
|
+
},
|
|
831
|
+
"conformance": [],
|
|
832
|
+
"schema": "rejects"
|
|
833
|
+
}
|
|
834
|
+
]
|
|
835
|
+
}
|