bantamkit-mcp 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +398 -0
- package/assets/contracts/default.yaml +90 -0
- package/assets/evals/devteam/manifest.yaml +351 -0
- package/assets/evals/devteam/repo/HISTORY.md +18 -0
- package/assets/evals/devteam/repo/README.md +12 -0
- package/assets/evals/devteam/repo/docs/architecture.md +17 -0
- package/assets/evals/devteam/repo/docs/runbook.md +10 -0
- package/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
- package/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
- package/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
- package/assets/evals/devteam/repo/src/ledger/config.py +35 -0
- package/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
- package/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
- package/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
- package/assets/evals/devteam/repo/src/ledger/report.py +9 -0
- package/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
- package/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
- package/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
- package/assets/evals/devteam/repo/tests/test_posting.py +13 -0
- package/assets/evals/devteam/repo/tests/test_settle.py +9 -0
- package/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
- package/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
- package/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
- package/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
- package/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
- package/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
- package/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
- package/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
- package/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
- package/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
- package/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
- package/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
- package/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
- package/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
- package/assets/evals/document/tasks/doc-small-137.yaml +37 -0
- package/assets/evals/document/tasks/doc-small-261.yaml +37 -0
- package/assets/evals/document/tasks/doc-small-388.yaml +37 -0
- package/assets/evals/fixtures/.gitkeep +0 -0
- package/assets/evals/fixtures/catalog.json +6 -0
- package/assets/evals/perturbations/task-completion.yaml +576 -0
- package/assets/evals/tasks/.gitkeep +0 -0
- package/assets/evals/tasks/extract-contact.yaml +14 -0
- package/assets/evals/tasks/extract-invoice.yaml +14 -0
- package/assets/evals/tasks/extract-order.yaml +15 -0
- package/assets/evals/tasks/extract-schedule.yaml +14 -0
- package/assets/evals/tasks/extract-versions.yaml +17 -0
- package/assets/evals/tasks/nav-prod-port.yaml +84 -0
- package/assets/evals/tasks/nav-release-bundle.yaml +87 -0
- package/assets/evals/tasks/recall-audit-retention.yaml +17 -0
- package/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
- package/assets/evals/tasks/recall-db-port.yaml +17 -0
- package/assets/evals/tasks/recall-deploy.yaml +13 -0
- package/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
- package/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
- package/assets/evals/tasks/recall-oncall.yaml +13 -0
- package/assets/evals/tasks/recall-org-quota.yaml +18 -0
- package/assets/evals/tasks/recall-owner.yaml +13 -0
- package/assets/evals/tasks/shop-basket-total.yaml +10 -0
- package/assets/evals/tasks/shop-cheapest.yaml +9 -0
- package/assets/evals/tasks/shop-compare.yaml +9 -0
- package/assets/evals/tasks/shop-gadget-value.yaml +9 -0
- package/assets/evals/tasks/shop-stock-total.yaml +9 -0
- package/assets/evals/tasks/shop-total.yaml +9 -0
- package/assets/profiles/default.yaml +31 -0
- package/assets/profiles/patient.yaml +31 -0
- package/assets/rubrics/.gitkeep +0 -0
- package/assets/rubrics/code-quality.yaml +20 -0
- package/assets/rubrics/grounded-completion.yaml +37 -0
- package/assets/rubrics/task-completion.yaml +28 -0
- package/assets/schemas/shiftwork-checkpoint.json +188 -0
- package/assets/skills/.gitkeep +0 -0
- package/assets/skills/file-graph.md +7 -0
- package/assets/skills/memory.md +35 -0
- package/assets/tools/.gitkeep +0 -0
- package/assets/tools/bantamkit_read.json +48 -0
- package/assets/tools/bantamkit_status.json +25 -0
- package/assets/tools/build_identity.json +17 -0
- package/assets/tools/document_list.json +12 -0
- package/assets/tools/document_read.json +31 -0
- package/assets/tools/file_graph.json +12 -0
- package/assets/tools/memory_compact.json +31 -0
- package/assets/tools/memory_recall.json +38 -0
- package/assets/tools/memory_save.json +61 -0
- package/assets/tools/shiftwork_clock_in.json +25 -0
- package/assets/tools/shiftwork_clock_out.json +60 -0
- package/assets/tools/shiftwork_status.json +25 -0
- package/assets/tools/skill_audit.json +70 -0
- package/assets/tools/validate_json.json +31 -0
- package/dist/assets.d.ts +48 -0
- package/dist/assets.js +109 -0
- package/dist/charsets.d.ts +17 -0
- package/dist/charsets.js +580 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +280 -0
- package/dist/contract.d.ts +145 -0
- package/dist/contract.js +608 -0
- package/dist/docread.d.ts +357 -0
- package/dist/docread.js +3315 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +14 -0
- package/dist/eventlog.d.ts +137 -0
- package/dist/eventlog.js +294 -0
- package/dist/htmlentities.d.ts +2 -0
- package/dist/htmlentities.js +16 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +13 -0
- package/dist/mcp/identity.d.ts +25 -0
- package/dist/mcp/identity.js +275 -0
- package/dist/mcp/pyargs.d.ts +80 -0
- package/dist/mcp/pyargs.js +362 -0
- package/dist/mcp/sdkjson.d.ts +56 -0
- package/dist/mcp/sdkjson.js +91 -0
- package/dist/mcp/server.d.ts +45 -0
- package/dist/mcp/server.js +740 -0
- package/dist/mcp/status.d.ts +210 -0
- package/dist/mcp/status.js +357 -0
- package/dist/mcp/transport.d.ts +68 -0
- package/dist/mcp/transport.js +111 -0
- package/dist/mcpreport.d.ts +214 -0
- package/dist/mcpreport.js +758 -0
- package/dist/memory/cli.d.ts +2 -0
- package/dist/memory/cli.js +396 -0
- package/dist/memory/component.d.ts +256 -0
- package/dist/memory/component.js +497 -0
- package/dist/memory/factfile.d.ts +120 -0
- package/dist/memory/factfile.js +406 -0
- package/dist/memory/layers.d.ts +92 -0
- package/dist/memory/layers.js +410 -0
- package/dist/memory/pyfs.d.ts +521 -0
- package/dist/memory/pyfs.js +1608 -0
- package/dist/memory/pyyaml.d.ts +155 -0
- package/dist/memory/pyyaml.js +868 -0
- package/dist/memory/store.d.ts +478 -0
- package/dist/memory/store.js +1034 -0
- package/dist/pyargparse.d.ts +141 -0
- package/dist/pyargparse.js +813 -0
- package/dist/pyjson.d.ts +110 -0
- package/dist/pyjson.js +524 -0
- package/dist/pyjsonschema.d.ts +50 -0
- package/dist/pyjsonschema.js +951 -0
- package/dist/shiftwork.d.ts +60 -0
- package/dist/shiftwork.js +324 -0
- package/dist/skillaudit.d.ts +172 -0
- package/dist/skillaudit.js +914 -0
- package/dist/statusline.d.ts +106 -0
- package/dist/statusline.js +281 -0
- package/package.json +45 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `jsonschema` seam: `Draft202012Validator.iter_errors` + `exceptions.best_match`.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS NOT AJV — measured, not assumed. Over the 288-case corpus in
|
|
5
|
+
* `tools/conformance/suites/validate.mjs`, ajv 8.20.0 and jsonschema 4.26.0 agree on
|
|
6
|
+
* WHETHER a document is valid in 288/288 cases, and on WHAT TO SAY ABOUT IT in **0 of 281**
|
|
7
|
+
* invalid ones. Not one message matches; ajv's own error list does not even CONTAIN
|
|
8
|
+
* Python's sentence anywhere in 281/281. Beyond the wording, ajv's error count differs in
|
|
9
|
+
* 28/281 (`additionalProperties` fires once per extra key where jsonschema fires once and
|
|
10
|
+
* names them all), its first error points at a different instance path in 11/281 and at a
|
|
11
|
+
* different KEYWORD in 19/281 (`anyOf`, `oneOf` and `contains` become `type`). The one bit
|
|
12
|
+
* ajv would contribute — valid or not — is the one bit that is not the product, and it is
|
|
13
|
+
* a bit this file has to compute anyway on the way to the sentence. So ajv buys nothing and
|
|
14
|
+
* costs a runtime dependency; the whole keyword vocabulary is 286 lines of Python
|
|
15
|
+
* (`jsonschema/_keywords.py`), which is smaller than the translation layer would be.
|
|
16
|
+
*
|
|
17
|
+
* WHAT IS IMPLEMENTED. Draft 2020-12, which is the checkpoint schema's dialect and also
|
|
18
|
+
* jsonschema's default for a schema with no `$schema` — the shape `validate_json` receives.
|
|
19
|
+
* 30 of the 36 keywords in `Draft202012Validator.VALIDATORS`. The other six RAISE rather
|
|
20
|
+
* than answer plausibly: `$ref` and `$dynamicRef` need a reference resolver,
|
|
21
|
+
* `unevaluatedItems`/`unevaluatedProperties` need annotation collection, and neither appears
|
|
22
|
+
* anywhere in the asset pack (measured: `assets/**\/*.json`). `format` is implemented as the
|
|
23
|
+
* no-op it is — `jsonschema.validate` passes no `format_checker`, so the keyword can never
|
|
24
|
+
* produce an error on this path.
|
|
25
|
+
*
|
|
26
|
+
* THE CALLER SCHEMA IS CHECKED FIRST, as it is in the reference. `jsonschema.validate` calls
|
|
27
|
+
* `cls.check_schema(schema)` before it looks at the instance, so an invalid schema raises
|
|
28
|
+
* `SchemaError` — and `contract.schema_error` catches only `ValidationError`, so that
|
|
29
|
+
* exception escapes `validate_json` to the caller. This port used NOT to run it, and the
|
|
30
|
+
* consequence was not a different sentence but a WRONG ANSWER: the keywords below are
|
|
31
|
+
* written to skip a schema value they do not recognise, which is correct inside
|
|
32
|
+
* `iter_errors` and is `{"valid": true}` at a tool boundary. Measured by N8 on the wire,
|
|
33
|
+
* `{"required": "a"}`, `{"required": 3}`, `{"properties": ["a"]}` and
|
|
34
|
+
* `{"additionalProperties": [1]}` all answered `{"valid": true}`, and `{"type": 3}` answered
|
|
35
|
+
* `{"valid": false}` with a sentence about a schema that is not a schema.
|
|
36
|
+
*
|
|
37
|
+
* `checkSchema` below is the fix and it is NOT the metaschema — running that would need
|
|
38
|
+
* `$ref` and `$dynamicRef`, which is the vocabulary this file refuses to implement. It is
|
|
39
|
+
* the keyword SHAPE table, applied recursively. Measured over 50 malformed schemas: 50 of 50
|
|
40
|
+
* now refuse on both sides, and only the WORDS differ. Each is a ruled case in
|
|
41
|
+
* `tools/conformance/suites/validate.mjs`. Registered and NOT fixed here: `validate_json`
|
|
42
|
+
* letting an exception out on caller input is a `runtime-py` defect (invariant 8).
|
|
43
|
+
*/
|
|
44
|
+
import { cmpCodepoint } from './memory/pyfs.js';
|
|
45
|
+
import { reprValue } from './pyjson.js';
|
|
46
|
+
export class PyJsonSchemaUnsupported extends Error {
|
|
47
|
+
constructor(message) {
|
|
48
|
+
super(`jsonschema seam: ${message}`);
|
|
49
|
+
this.name = 'PyJsonSchemaUnsupported';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function absolutePath(error) {
|
|
53
|
+
return error.parent === null ? error.path : [...absolutePath(error.parent), ...error.path];
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------- py helpers
|
|
56
|
+
const isDict = (v) => typeof v !== 'boolean' && v.t === 'dict';
|
|
57
|
+
const get = (schema, key) => isDict(schema) ? schema.v.get(key) : undefined;
|
|
58
|
+
const cpLength = (s) => [...s].length;
|
|
59
|
+
const asNumber = (v) => (v.t === 'int' ? Number(v.v) : v.v);
|
|
60
|
+
/** `jsonschema._utils.equal`: JSON equality that refuses `True == 1`. */
|
|
61
|
+
export function pyEqual(a, b) {
|
|
62
|
+
if (a.t === 'str' || b.t === 'str')
|
|
63
|
+
return a.t === 'str' && b.t === 'str' && a.v === b.v;
|
|
64
|
+
if (a.t === 'list' && b.t === 'list') {
|
|
65
|
+
return a.v.length === b.v.length && a.v.every((x, i) => pyEqual(x, b.v[i]));
|
|
66
|
+
}
|
|
67
|
+
if (a.t === 'dict' && b.t === 'dict') {
|
|
68
|
+
if (a.v.size !== b.v.size)
|
|
69
|
+
return false;
|
|
70
|
+
for (const [k, v] of a.v) {
|
|
71
|
+
const other = b.v.get(k);
|
|
72
|
+
if (other === undefined || !pyEqual(v, other))
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
// `unbool`: True and False become unique sentinels, so they equal nothing but themselves.
|
|
78
|
+
if (a.t === 'bool' || b.t === 'bool')
|
|
79
|
+
return a.t === 'bool' && b.t === 'bool' && a.v === b.v;
|
|
80
|
+
if (a.t === 'null' || b.t === 'null')
|
|
81
|
+
return a.t === 'null' && b.t === 'null';
|
|
82
|
+
if (a.t === 'int' && b.t === 'int')
|
|
83
|
+
return a.v === b.v;
|
|
84
|
+
if ((a.t === 'int' || a.t === 'float') && (b.t === 'int' || b.t === 'float')) {
|
|
85
|
+
return asNumber(a) === asNumber(b);
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* `draft202012_type_checker`.
|
|
91
|
+
*
|
|
92
|
+
* Two Python-isms carry: `bool` is not an `int`, so `true` is neither `integer` nor
|
|
93
|
+
* `number`; and draft 6 onward redefines `integer` to accept a float with no fractional
|
|
94
|
+
* part, so `1.0` IS an integer.
|
|
95
|
+
*/
|
|
96
|
+
function isType(instance, type) {
|
|
97
|
+
switch (type) {
|
|
98
|
+
case 'array':
|
|
99
|
+
return instance.t === 'list';
|
|
100
|
+
case 'boolean':
|
|
101
|
+
return instance.t === 'bool';
|
|
102
|
+
case 'integer':
|
|
103
|
+
return instance.t === 'int' || (instance.t === 'float' && Number.isInteger(instance.v));
|
|
104
|
+
case 'null':
|
|
105
|
+
return instance.t === 'null';
|
|
106
|
+
case 'number':
|
|
107
|
+
return instance.t === 'int' || instance.t === 'float';
|
|
108
|
+
case 'object':
|
|
109
|
+
return instance.t === 'dict';
|
|
110
|
+
case 'string':
|
|
111
|
+
return instance.t === 'str';
|
|
112
|
+
default:
|
|
113
|
+
// Python never reaches this: `check_schema` rejects the schema first. See the module
|
|
114
|
+
// header — both runtimes refuse, with different words.
|
|
115
|
+
throw new PyJsonSchemaUnsupported(`undefined type check ${JSON.stringify(type)}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function iterErrors(instance, schema) {
|
|
119
|
+
if (schema === true)
|
|
120
|
+
return [];
|
|
121
|
+
if (schema === false) {
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
message: `False schema does not allow ${reprValue(instance)}`,
|
|
125
|
+
validator: null,
|
|
126
|
+
instance,
|
|
127
|
+
schema,
|
|
128
|
+
path: [],
|
|
129
|
+
context: [],
|
|
130
|
+
parent: null,
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
if (schema.t === 'bool')
|
|
135
|
+
return iterErrors(instance, schema.v);
|
|
136
|
+
if (schema.t !== 'dict') {
|
|
137
|
+
throw new PyJsonSchemaUnsupported(`a schema must be an object or a boolean, got ${schema.t}`);
|
|
138
|
+
}
|
|
139
|
+
const out = [];
|
|
140
|
+
// `applicable_validators` for draft 2020-12 is `methodcaller("items")` — plain dict order,
|
|
141
|
+
// which for a JSON-parsed schema is DOCUMENT order. That order decides which error comes
|
|
142
|
+
// first, and `best_match` breaks ties by taking the first maximum, so it is load-bearing.
|
|
143
|
+
for (const [keyword, value] of schema.v) {
|
|
144
|
+
const fn = KEYWORDS[keyword];
|
|
145
|
+
if (fn === undefined) {
|
|
146
|
+
if (UNSUPPORTED.has(keyword)) {
|
|
147
|
+
throw new PyJsonSchemaUnsupported(`${keyword} is not implemented by this port`);
|
|
148
|
+
}
|
|
149
|
+
continue; // an annotation (`title`, `$comment`, `description`, …) — jsonschema skips it too
|
|
150
|
+
}
|
|
151
|
+
const emit = (message, extra = {}) => ({
|
|
152
|
+
message,
|
|
153
|
+
validator: extra.validator ?? keyword,
|
|
154
|
+
instance,
|
|
155
|
+
schema,
|
|
156
|
+
path: [],
|
|
157
|
+
context: extra.context ?? [],
|
|
158
|
+
parent: null,
|
|
159
|
+
});
|
|
160
|
+
const descend = (childInstance, childSchema, path) => {
|
|
161
|
+
if (childSchema === undefined)
|
|
162
|
+
return [];
|
|
163
|
+
// `Validator.descend` handles a boolean subschema and RETURNS, before the loop that
|
|
164
|
+
// prepends `path`. So `{"properties": {"k": false}}` reports at `root` and not at `k`.
|
|
165
|
+
// Measured against Python; a port that prepended here would name a field Python does
|
|
166
|
+
// not name.
|
|
167
|
+
if (childSchema.t === 'bool')
|
|
168
|
+
return iterErrors(childInstance, childSchema.v);
|
|
169
|
+
const errors = iterErrors(childInstance, childSchema);
|
|
170
|
+
if (path !== undefined)
|
|
171
|
+
for (const e of errors)
|
|
172
|
+
e.path.unshift(path);
|
|
173
|
+
return errors;
|
|
174
|
+
};
|
|
175
|
+
out.push(...fn(value, instance, schema, emit, descend));
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
const isValid = (instance, schema) => iterErrors(instance, schema).length === 0;
|
|
180
|
+
const UNSUPPORTED = new Set([
|
|
181
|
+
'$ref',
|
|
182
|
+
'$dynamicRef',
|
|
183
|
+
'$recursiveRef',
|
|
184
|
+
'unevaluatedItems',
|
|
185
|
+
'unevaluatedProperties',
|
|
186
|
+
]);
|
|
187
|
+
/** `extras_msg`: `"'a', 'b'", "were"`. */
|
|
188
|
+
function extrasMsg(extras) {
|
|
189
|
+
return [extras.map((e) => reprValue({ t: 'str', v: e })).join(', '), extras.length === 1 ? 'was' : 'were'];
|
|
190
|
+
}
|
|
191
|
+
/** `find_additional_properties`, in instance order. */
|
|
192
|
+
function findAdditionalProperties(instance, schema) {
|
|
193
|
+
const properties = get(schema, 'properties');
|
|
194
|
+
const known = properties && properties.t === 'dict' ? properties.v : new Map();
|
|
195
|
+
const pp = get(schema, 'patternProperties');
|
|
196
|
+
const patterns = pp && pp.t === 'dict' ? [...pp.v.keys()].join('|') : '';
|
|
197
|
+
const out = [];
|
|
198
|
+
for (const property of instance.keys()) {
|
|
199
|
+
if (known.has(property))
|
|
200
|
+
continue;
|
|
201
|
+
if (patterns && reSearch(patterns, property))
|
|
202
|
+
continue;
|
|
203
|
+
out.push(property);
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* `re.search(pattern, string)`.
|
|
209
|
+
*
|
|
210
|
+
* RULING. Python's `re` and JS's `RegExp` are different languages past the common core:
|
|
211
|
+
* `\d` is Unicode-wide in Python and ASCII-only in JS, `$` matches before a trailing newline
|
|
212
|
+
* in Python and not in JS, and `(?P<x>…)`/`\Z`/`\A` are Python-only syntax. The asset pack
|
|
213
|
+
* holds exactly one `pattern` (`assets/tools/memory_save.json`), which is inside the common
|
|
214
|
+
* core; a caller-supplied schema is not guaranteed to be. Carried as a ruled case rather
|
|
215
|
+
* than fixed, because fixing it means porting `sre_compile`.
|
|
216
|
+
*/
|
|
217
|
+
function reSearch(pattern, text) {
|
|
218
|
+
let re;
|
|
219
|
+
try {
|
|
220
|
+
re = new RegExp(pattern, 'u');
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
try {
|
|
224
|
+
re = new RegExp(pattern);
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
throw new PyJsonSchemaUnsupported(`pattern ${JSON.stringify(pattern)} is not a JS regular expression: ${e.message}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return re.test(text);
|
|
231
|
+
}
|
|
232
|
+
const sortedByStr = (names) => [...names].sort(cmpCodepoint);
|
|
233
|
+
/** Python truthiness for a decoded JSON value: `not aP`, `if uI`. */
|
|
234
|
+
function pyTruthy(value) {
|
|
235
|
+
switch (value.t) {
|
|
236
|
+
case 'null':
|
|
237
|
+
return false;
|
|
238
|
+
case 'bool':
|
|
239
|
+
return value.v;
|
|
240
|
+
case 'int':
|
|
241
|
+
return value.v !== 0n;
|
|
242
|
+
case 'float':
|
|
243
|
+
return value.v !== 0;
|
|
244
|
+
case 'str':
|
|
245
|
+
return value.v !== '';
|
|
246
|
+
case 'list':
|
|
247
|
+
return value.v.length > 0;
|
|
248
|
+
case 'dict':
|
|
249
|
+
return value.v.size > 0;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const KEYWORDS = {
|
|
253
|
+
type(value, instance, _schema, emit) {
|
|
254
|
+
// `ensure_list`: a bare string becomes a one-element list, anything else passes through.
|
|
255
|
+
const types = value.t === 'str' ? [value] : value.t === 'list' ? value.v : [value];
|
|
256
|
+
if (types.some((t) => t.t === 'str' && isType(instance, t.v)))
|
|
257
|
+
return [];
|
|
258
|
+
const reprs = types.map(reprValue).join(', ');
|
|
259
|
+
return [emit(`${reprValue(instance)} is not of type ${reprs}`)];
|
|
260
|
+
},
|
|
261
|
+
properties(value, instance, _schema, _emit, descend) {
|
|
262
|
+
if (instance.t !== 'dict' || value.t !== 'dict')
|
|
263
|
+
return [];
|
|
264
|
+
const out = [];
|
|
265
|
+
for (const [property, subschema] of value.v) {
|
|
266
|
+
const child = instance.v.get(property);
|
|
267
|
+
if (instance.v.has(property))
|
|
268
|
+
out.push(...descend(child, subschema, property));
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
},
|
|
272
|
+
required(value, instance, _schema, emit) {
|
|
273
|
+
if (instance.t !== 'dict' || value.t !== 'list')
|
|
274
|
+
return [];
|
|
275
|
+
const out = [];
|
|
276
|
+
for (const property of value.v) {
|
|
277
|
+
const name = property.v;
|
|
278
|
+
if (!instance.v.has(name)) {
|
|
279
|
+
out.push(emit(`${reprValue(property)} is a required property`));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
},
|
|
284
|
+
additionalProperties(value, instance, schema, emit, descend) {
|
|
285
|
+
if (instance.t !== 'dict')
|
|
286
|
+
return [];
|
|
287
|
+
const extras = findAdditionalProperties(instance.v, schema);
|
|
288
|
+
if (value.t === 'dict') {
|
|
289
|
+
const out = [];
|
|
290
|
+
// `set(...)` in Python — iteration order is a hash order this port cannot reproduce.
|
|
291
|
+
// Every error here descends into a subschema and carries its own `path`, so the ORDER
|
|
292
|
+
// is the only thing that differs and `best_match` sorts on `path` before order. Sorted
|
|
293
|
+
// for determinism; see the ruled case.
|
|
294
|
+
for (const extra of sortedByStr(extras))
|
|
295
|
+
out.push(...descend(instance.v.get(extra), value, extra));
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
if (!pyTruthy(value) && extras.length > 0) {
|
|
299
|
+
const pp = get(schema, 'patternProperties');
|
|
300
|
+
if (pp !== undefined) {
|
|
301
|
+
const verb = extras.length === 1 ? 'does' : 'do';
|
|
302
|
+
const joined = sortedByStr(extras).map((e) => reprValue({ t: 'str', v: e })).join(', ');
|
|
303
|
+
const patterns = sortedByStr(pp.t === 'dict' ? [...pp.v.keys()] : [])
|
|
304
|
+
.map((e) => reprValue({ t: 'str', v: e }))
|
|
305
|
+
.join(', ');
|
|
306
|
+
return [emit(`${joined} ${verb} not match any of the regexes: ${patterns}`)];
|
|
307
|
+
}
|
|
308
|
+
const [joined, verb] = extrasMsg(sortedByStr(extras));
|
|
309
|
+
return [emit(`Additional properties are not allowed (${joined} ${verb} unexpected)`)];
|
|
310
|
+
}
|
|
311
|
+
return [];
|
|
312
|
+
},
|
|
313
|
+
patternProperties(value, instance, _schema, _emit, descend) {
|
|
314
|
+
if (instance.t !== 'dict' || value.t !== 'dict')
|
|
315
|
+
return [];
|
|
316
|
+
const out = [];
|
|
317
|
+
for (const [pattern, subschema] of value.v) {
|
|
318
|
+
for (const [k, v] of instance.v) {
|
|
319
|
+
if (reSearch(pattern, k))
|
|
320
|
+
out.push(...descend(v, subschema, k));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
},
|
|
325
|
+
propertyNames(value, instance, _schema, _emit, descend) {
|
|
326
|
+
if (instance.t !== 'dict')
|
|
327
|
+
return [];
|
|
328
|
+
const out = [];
|
|
329
|
+
// No `path`: jsonschema descends on the KEY with no path component, so the error points
|
|
330
|
+
// at the object and not at a property that does not exist.
|
|
331
|
+
for (const property of instance.v.keys())
|
|
332
|
+
out.push(...descend({ t: 'str', v: property }, value));
|
|
333
|
+
return out;
|
|
334
|
+
},
|
|
335
|
+
prefixItems(value, instance, _schema, _emit, descend) {
|
|
336
|
+
if (instance.t !== 'list' || value.t !== 'list')
|
|
337
|
+
return [];
|
|
338
|
+
const out = [];
|
|
339
|
+
const n = Math.min(instance.v.length, value.v.length);
|
|
340
|
+
for (let i = 0; i < n; i += 1)
|
|
341
|
+
out.push(...descend(instance.v[i], value.v[i], i));
|
|
342
|
+
return out;
|
|
343
|
+
},
|
|
344
|
+
items(value, instance, schema, emit, descend) {
|
|
345
|
+
if (instance.t !== 'list')
|
|
346
|
+
return [];
|
|
347
|
+
const pi = get(schema, 'prefixItems');
|
|
348
|
+
const prefix = pi && pi.t === 'list' ? pi.v.length : 0;
|
|
349
|
+
const total = instance.v.length;
|
|
350
|
+
const extra = total - prefix;
|
|
351
|
+
if (extra <= 0)
|
|
352
|
+
return [];
|
|
353
|
+
if (value.t === 'bool' && value.v === false) {
|
|
354
|
+
const rest = extra !== 1 ? { t: 'list', v: instance.v.slice(prefix) } : instance.v[prefix];
|
|
355
|
+
const item = prefix !== 1 ? 'items' : 'item';
|
|
356
|
+
return [emit(`Expected at most ${prefix} ${item} but found ${extra} extra: ${reprValue(rest)}`)];
|
|
357
|
+
}
|
|
358
|
+
const out = [];
|
|
359
|
+
for (let i = prefix; i < total; i += 1)
|
|
360
|
+
out.push(...descend(instance.v[i], value, i));
|
|
361
|
+
return out;
|
|
362
|
+
},
|
|
363
|
+
contains(value, instance, schema, emit) {
|
|
364
|
+
if (instance.t !== 'list')
|
|
365
|
+
return [];
|
|
366
|
+
const minRaw = get(schema, 'minContains');
|
|
367
|
+
const maxRaw = get(schema, 'maxContains');
|
|
368
|
+
const minContains = minRaw === undefined ? 1 : asNumber(minRaw);
|
|
369
|
+
const maxContains = maxRaw === undefined ? instance.v.length : asNumber(maxRaw);
|
|
370
|
+
let matches = 0;
|
|
371
|
+
for (const each of instance.v) {
|
|
372
|
+
if (isValid(each, value)) {
|
|
373
|
+
matches += 1;
|
|
374
|
+
if (matches > maxContains) {
|
|
375
|
+
return [
|
|
376
|
+
emit(`Too many items match the given schema (expected at most ${maxContains})`, {
|
|
377
|
+
validator: 'maxContains',
|
|
378
|
+
}),
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (matches < minContains) {
|
|
384
|
+
if (matches === 0) {
|
|
385
|
+
return [emit(`${reprValue(instance)} does not contain items matching the given schema`)];
|
|
386
|
+
}
|
|
387
|
+
return [
|
|
388
|
+
emit(`Too few items match the given schema (expected at least ${minContains} but only ` +
|
|
389
|
+
`${matches} matched)`, { validator: 'minContains' }),
|
|
390
|
+
];
|
|
391
|
+
}
|
|
392
|
+
return [];
|
|
393
|
+
},
|
|
394
|
+
const(value, instance, _schema, emit) {
|
|
395
|
+
return pyEqual(instance, value) ? [] : [emit(`${reprValue(value)} was expected`)];
|
|
396
|
+
},
|
|
397
|
+
enum(value, instance, _schema, emit) {
|
|
398
|
+
if (value.t !== 'list')
|
|
399
|
+
return [];
|
|
400
|
+
if (value.v.some((each) => pyEqual(each, instance)))
|
|
401
|
+
return [];
|
|
402
|
+
return [emit(`${reprValue(instance)} is not one of ${reprValue(value)}`)];
|
|
403
|
+
},
|
|
404
|
+
minimum(value, instance, _schema, emit) {
|
|
405
|
+
if (!isType(instance, 'number'))
|
|
406
|
+
return [];
|
|
407
|
+
if (numLess(instance, value)) {
|
|
408
|
+
return [emit(`${reprValue(instance)} is less than the minimum of ${reprValue(value)}`)];
|
|
409
|
+
}
|
|
410
|
+
return [];
|
|
411
|
+
},
|
|
412
|
+
maximum(value, instance, _schema, emit) {
|
|
413
|
+
if (!isType(instance, 'number'))
|
|
414
|
+
return [];
|
|
415
|
+
if (numLess(value, instance)) {
|
|
416
|
+
return [emit(`${reprValue(instance)} is greater than the maximum of ${reprValue(value)}`)];
|
|
417
|
+
}
|
|
418
|
+
return [];
|
|
419
|
+
},
|
|
420
|
+
exclusiveMinimum(value, instance, _schema, emit) {
|
|
421
|
+
if (!isType(instance, 'number'))
|
|
422
|
+
return [];
|
|
423
|
+
if (!numLess(value, instance)) {
|
|
424
|
+
return [
|
|
425
|
+
emit(`${reprValue(instance)} is less than or equal to the minimum of ${reprValue(value)}`),
|
|
426
|
+
];
|
|
427
|
+
}
|
|
428
|
+
return [];
|
|
429
|
+
},
|
|
430
|
+
exclusiveMaximum(value, instance, _schema, emit) {
|
|
431
|
+
if (!isType(instance, 'number'))
|
|
432
|
+
return [];
|
|
433
|
+
if (!numLess(instance, value)) {
|
|
434
|
+
return [
|
|
435
|
+
emit(`${reprValue(instance)} is greater than or equal to the maximum of ${reprValue(value)}`),
|
|
436
|
+
];
|
|
437
|
+
}
|
|
438
|
+
return [];
|
|
439
|
+
},
|
|
440
|
+
multipleOf(value, instance, _schema, emit) {
|
|
441
|
+
if (!isType(instance, 'number'))
|
|
442
|
+
return [];
|
|
443
|
+
let failed;
|
|
444
|
+
if (value.t === 'float') {
|
|
445
|
+
const quotient = asNumber(instance) / value.v;
|
|
446
|
+
if (!Number.isFinite(quotient)) {
|
|
447
|
+
// `int(quotient)` raises OverflowError in Python and it falls back to exact Fraction
|
|
448
|
+
// arithmetic. Reached only for an enormous instance over a tiny divisor.
|
|
449
|
+
throw new PyJsonSchemaUnsupported('multipleOf overflowed; the Fraction fallback is not ported');
|
|
450
|
+
}
|
|
451
|
+
failed = Math.trunc(quotient) !== quotient;
|
|
452
|
+
}
|
|
453
|
+
else if (value.t === 'int') {
|
|
454
|
+
if (value.v === 0n)
|
|
455
|
+
throw new PyJsonSchemaUnsupported('multipleOf 0 divides by zero');
|
|
456
|
+
failed =
|
|
457
|
+
instance.t === 'int'
|
|
458
|
+
? instance.v % value.v !== 0n
|
|
459
|
+
: asNumber(instance) % Number(value.v) !== 0;
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
throw new PyJsonSchemaUnsupported(`multipleOf must be a number, got ${value.t}`);
|
|
463
|
+
}
|
|
464
|
+
// `{dB}` is str(), not repr(): an int prints the same either way and a float does too
|
|
465
|
+
// for every value `str` and `repr` share, which since 3.1 is all of them.
|
|
466
|
+
return failed ? [emit(`${reprValue(instance)} is not a multiple of ${reprValue(value)}`)] : [];
|
|
467
|
+
},
|
|
468
|
+
minItems(value, instance, _schema, emit) {
|
|
469
|
+
if (instance.t !== 'list' || instance.v.length >= asNumber(value))
|
|
470
|
+
return [];
|
|
471
|
+
const message = asNumber(value) === 1 ? 'should be non-empty' : 'is too short';
|
|
472
|
+
return [emit(`${reprValue(instance)} ${message}`)];
|
|
473
|
+
},
|
|
474
|
+
maxItems(value, instance, _schema, emit) {
|
|
475
|
+
if (instance.t !== 'list' || instance.v.length <= asNumber(value))
|
|
476
|
+
return [];
|
|
477
|
+
const message = asNumber(value) === 0 ? 'is expected to be empty' : 'is too long';
|
|
478
|
+
return [emit(`${reprValue(instance)} ${message}`)];
|
|
479
|
+
},
|
|
480
|
+
minLength(value, instance, _schema, emit) {
|
|
481
|
+
if (instance.t !== 'str' || cpLength(instance.v) >= asNumber(value))
|
|
482
|
+
return [];
|
|
483
|
+
const message = asNumber(value) === 1 ? 'should be non-empty' : 'is too short';
|
|
484
|
+
return [emit(`${reprValue(instance)} ${message}`)];
|
|
485
|
+
},
|
|
486
|
+
maxLength(value, instance, _schema, emit) {
|
|
487
|
+
if (instance.t !== 'str' || cpLength(instance.v) <= asNumber(value))
|
|
488
|
+
return [];
|
|
489
|
+
const message = asNumber(value) === 0 ? 'is expected to be empty' : 'is too long';
|
|
490
|
+
return [emit(`${reprValue(instance)} ${message}`)];
|
|
491
|
+
},
|
|
492
|
+
minProperties(value, instance, _schema, emit) {
|
|
493
|
+
if (instance.t !== 'dict' || instance.v.size >= asNumber(value))
|
|
494
|
+
return [];
|
|
495
|
+
const message = asNumber(value) === 1 ? 'should be non-empty' : 'does not have enough properties';
|
|
496
|
+
return [emit(`${reprValue(instance)} ${message}`)];
|
|
497
|
+
},
|
|
498
|
+
maxProperties(value, instance, _schema, emit) {
|
|
499
|
+
if (instance.t !== 'dict' || instance.v.size <= asNumber(value))
|
|
500
|
+
return [];
|
|
501
|
+
const message = asNumber(value) === 0 ? 'is expected to be empty' : 'has too many properties';
|
|
502
|
+
return [emit(`${reprValue(instance)} ${message}`)];
|
|
503
|
+
},
|
|
504
|
+
uniqueItems(value, instance, _schema, emit) {
|
|
505
|
+
if (!pyTruthy(value) || instance.t !== 'list')
|
|
506
|
+
return [];
|
|
507
|
+
const seen = [];
|
|
508
|
+
for (const each of instance.v) {
|
|
509
|
+
if (seen.some((i) => pyEqual(i, each))) {
|
|
510
|
+
return [emit(`${reprValue(instance)} has non-unique elements`)];
|
|
511
|
+
}
|
|
512
|
+
seen.push(each);
|
|
513
|
+
}
|
|
514
|
+
return [];
|
|
515
|
+
},
|
|
516
|
+
pattern(value, instance, _schema, emit) {
|
|
517
|
+
if (instance.t !== 'str' || value.t !== 'str')
|
|
518
|
+
return [];
|
|
519
|
+
if (reSearch(value.v, instance.v))
|
|
520
|
+
return [];
|
|
521
|
+
return [emit(`${reprValue(instance)} does not match ${reprValue(value)}`)];
|
|
522
|
+
},
|
|
523
|
+
/** `jsonschema.validate` passes no `format_checker`, so this keyword cannot ever fire. */
|
|
524
|
+
format() {
|
|
525
|
+
return [];
|
|
526
|
+
},
|
|
527
|
+
dependentRequired(value, instance, _schema, emit) {
|
|
528
|
+
if (instance.t !== 'dict' || value.t !== 'dict')
|
|
529
|
+
return [];
|
|
530
|
+
const out = [];
|
|
531
|
+
for (const [property, dependency] of value.v) {
|
|
532
|
+
if (!instance.v.has(property))
|
|
533
|
+
continue;
|
|
534
|
+
if (dependency.t !== 'list')
|
|
535
|
+
continue;
|
|
536
|
+
for (const each of dependency.v) {
|
|
537
|
+
const name = each.v;
|
|
538
|
+
if (!instance.v.has(name)) {
|
|
539
|
+
out.push(emit(`${reprValue(each)} is a dependency of ${reprValue({ t: 'str', v: property })}`));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return out;
|
|
544
|
+
},
|
|
545
|
+
dependentSchemas(value, instance, _schema, _emit, descend) {
|
|
546
|
+
if (instance.t !== 'dict' || value.t !== 'dict')
|
|
547
|
+
return [];
|
|
548
|
+
const out = [];
|
|
549
|
+
for (const [property, dependency] of value.v) {
|
|
550
|
+
if (!instance.v.has(property))
|
|
551
|
+
continue;
|
|
552
|
+
out.push(...descend(instance, dependency));
|
|
553
|
+
}
|
|
554
|
+
return out;
|
|
555
|
+
},
|
|
556
|
+
allOf(value, instance, _schema, _emit, descend) {
|
|
557
|
+
if (value.t !== 'list')
|
|
558
|
+
return [];
|
|
559
|
+
const out = [];
|
|
560
|
+
for (const subschema of value.v)
|
|
561
|
+
out.push(...descend(instance, subschema));
|
|
562
|
+
return out;
|
|
563
|
+
},
|
|
564
|
+
anyOf(value, instance, _schema, emit) {
|
|
565
|
+
if (value.t !== 'list')
|
|
566
|
+
return [];
|
|
567
|
+
const allErrors = [];
|
|
568
|
+
for (const subschema of value.v) {
|
|
569
|
+
const errs = iterErrors(instance, subschema);
|
|
570
|
+
if (errs.length === 0)
|
|
571
|
+
return [];
|
|
572
|
+
allErrors.push(...errs);
|
|
573
|
+
}
|
|
574
|
+
const error = emit(`${reprValue(instance)} is not valid under any of the given schemas`, {
|
|
575
|
+
context: allErrors,
|
|
576
|
+
});
|
|
577
|
+
for (const child of allErrors)
|
|
578
|
+
child.parent = error;
|
|
579
|
+
return [error];
|
|
580
|
+
},
|
|
581
|
+
oneOf(value, instance, _schema, emit) {
|
|
582
|
+
if (value.t !== 'list')
|
|
583
|
+
return [];
|
|
584
|
+
const out = [];
|
|
585
|
+
const allErrors = [];
|
|
586
|
+
let firstValid = null;
|
|
587
|
+
let index = 0;
|
|
588
|
+
for (; index < value.v.length; index += 1) {
|
|
589
|
+
const errs = iterErrors(instance, value.v[index]);
|
|
590
|
+
if (errs.length === 0) {
|
|
591
|
+
firstValid = value.v[index];
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
allErrors.push(...errs);
|
|
595
|
+
}
|
|
596
|
+
if (firstValid === null) {
|
|
597
|
+
const error = emit(`${reprValue(instance)} is not valid under any of the given schemas`, {
|
|
598
|
+
context: allErrors,
|
|
599
|
+
});
|
|
600
|
+
for (const child of allErrors)
|
|
601
|
+
child.parent = error;
|
|
602
|
+
out.push(error);
|
|
603
|
+
}
|
|
604
|
+
// `subschemas` is one shared iterator in Python: the comprehension below resumes where
|
|
605
|
+
// the loop above broke, so the schemas already tried are never re-checked.
|
|
606
|
+
const moreValid = value.v.slice(index + 1).filter((each) => isValid(instance, each));
|
|
607
|
+
if (moreValid.length > 0) {
|
|
608
|
+
const reprs = [...moreValid, firstValid].map(reprValue).join(', ');
|
|
609
|
+
out.push(emit(`${reprValue(instance)} is valid under each of ${reprs}`));
|
|
610
|
+
}
|
|
611
|
+
return out;
|
|
612
|
+
},
|
|
613
|
+
not(value, instance, _schema, emit) {
|
|
614
|
+
if (!isValid(instance, value))
|
|
615
|
+
return [];
|
|
616
|
+
return [emit(`${reprValue(instance)} should not be valid under ${reprValue(value)}`)];
|
|
617
|
+
},
|
|
618
|
+
if(value, instance, schema, _emit, descend) {
|
|
619
|
+
if (isValid(instance, value)) {
|
|
620
|
+
const then = get(schema, 'then');
|
|
621
|
+
return then === undefined ? [] : descend(instance, then);
|
|
622
|
+
}
|
|
623
|
+
const otherwise = get(schema, 'else');
|
|
624
|
+
return otherwise === undefined ? [] : descend(instance, otherwise);
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
/** `a < b` over Python numbers, exact for two ints of any size. */
|
|
628
|
+
function numLess(a, b) {
|
|
629
|
+
if (a.t === 'int' && b.t === 'int')
|
|
630
|
+
return a.v < b.v;
|
|
631
|
+
return asNumber(a) < asNumber(b);
|
|
632
|
+
}
|
|
633
|
+
// ------------------------------------------------------------------------- the heuristic
|
|
634
|
+
const WEAK_MATCHES = new Set(['anyOf', 'oneOf']);
|
|
635
|
+
// `STRONG_MATCHES` is `frozenset()` in 4.26 — the third tuple slot is always False. Kept
|
|
636
|
+
// visible rather than folded away, so a jsonschema release that fills it is a one-line edit.
|
|
637
|
+
const STRONG_MATCHES = new Set();
|
|
638
|
+
/** `error._matches_type()`. A `false` schema has no `["type"]`, so it answers False. */
|
|
639
|
+
function matchesType(error) {
|
|
640
|
+
const expected = get(error.schema, 'type');
|
|
641
|
+
if (expected === undefined)
|
|
642
|
+
return false;
|
|
643
|
+
if (expected.t === 'str')
|
|
644
|
+
return isType(error.instance, expected.v);
|
|
645
|
+
if (expected.t === 'list') {
|
|
646
|
+
return expected.v.some((t) => isType(error.instance, t.v));
|
|
647
|
+
}
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
/** `jsonschema.exceptions.relevance`, the default key of `by_relevance()`. */
|
|
651
|
+
function relevance(error) {
|
|
652
|
+
const validator = error.validator;
|
|
653
|
+
return [
|
|
654
|
+
-error.path.length,
|
|
655
|
+
error.path,
|
|
656
|
+
validator === null || !WEAK_MATCHES.has(validator),
|
|
657
|
+
validator !== null && STRONG_MATCHES.has(validator),
|
|
658
|
+
!matchesType(error),
|
|
659
|
+
];
|
|
660
|
+
}
|
|
661
|
+
/** Python tuple comparison, with `deque` lexicographic ordering for the path slot. */
|
|
662
|
+
function cmpRelevance(a, b) {
|
|
663
|
+
if (a[0] !== b[0])
|
|
664
|
+
return a[0] < b[0] ? -1 : 1;
|
|
665
|
+
const p = cmpPath(a[1], b[1]);
|
|
666
|
+
if (p !== 0)
|
|
667
|
+
return p;
|
|
668
|
+
for (const i of [2, 3, 4]) {
|
|
669
|
+
if (a[i] !== b[i])
|
|
670
|
+
return a[i] ? 1 : -1; // False < True
|
|
671
|
+
}
|
|
672
|
+
return 0;
|
|
673
|
+
}
|
|
674
|
+
function cmpPath(a, b) {
|
|
675
|
+
const n = Math.min(a.length, b.length);
|
|
676
|
+
for (let i = 0; i < n; i += 1) {
|
|
677
|
+
const x = a[i];
|
|
678
|
+
const y = b[i];
|
|
679
|
+
if (typeof x === 'number' && typeof y === 'number') {
|
|
680
|
+
if (x !== y)
|
|
681
|
+
return x < y ? -1 : 1;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (typeof x === 'string' && typeof y === 'string') {
|
|
685
|
+
const c = cmpCodepoint(x, y);
|
|
686
|
+
if (c !== 0)
|
|
687
|
+
return c;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
// Python raises TypeError comparing str with int. It cannot happen for errors from one
|
|
691
|
+
// instance — at each position the container is fixed by the shared prefix — and if it
|
|
692
|
+
// ever does, refusing beats inventing an order.
|
|
693
|
+
throw new PyJsonSchemaUnsupported(`cannot order path components ${String(x)} and ${String(y)}`);
|
|
694
|
+
}
|
|
695
|
+
return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* `jsonschema.exceptions.best_match`.
|
|
699
|
+
*
|
|
700
|
+
* THE CHOICE IS THE PRODUCT, not just the formatting. One invalid document usually produces
|
|
701
|
+
* several errors and Python raises exactly one of them, so a port that reports the first
|
|
702
|
+
* error it finds emits a correct-looking sentence about the wrong field. Two rules do the
|
|
703
|
+
* work: `max` prefers the SHALLOWEST error (`-len(path)`) and returns the FIRST maximum on a
|
|
704
|
+
* tie; and once a winner is found, the loop descends into its `context` — the sub-errors an
|
|
705
|
+
* `anyOf`/`oneOf` carries — unless the two most relevant children tie, in which case the
|
|
706
|
+
* parent stands.
|
|
707
|
+
*/
|
|
708
|
+
export function bestMatch(errors) {
|
|
709
|
+
if (errors.length === 0)
|
|
710
|
+
return null;
|
|
711
|
+
let best = errors[0];
|
|
712
|
+
let bestKey = relevance(best);
|
|
713
|
+
for (let i = 1; i < errors.length; i += 1) {
|
|
714
|
+
const key = relevance(errors[i]);
|
|
715
|
+
if (cmpRelevance(key, bestKey) > 0) {
|
|
716
|
+
best = errors[i];
|
|
717
|
+
bestKey = key;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
while (best.context.length > 0) {
|
|
721
|
+
// `heapq.nsmallest(2, ..., key)` is documented as `sorted(iterable, key=key)[:2]`, and
|
|
722
|
+
// its decoration by index makes it stable, so ties keep the earlier child.
|
|
723
|
+
const ordered = best.context
|
|
724
|
+
.map((e, i) => ({ e, i, k: relevance(e) }))
|
|
725
|
+
.sort((x, y) => cmpRelevance(x.k, y.k) || x.i - y.i);
|
|
726
|
+
const smallest = ordered.slice(0, 2);
|
|
727
|
+
if (smallest.length === 2 && cmpRelevance(smallest[0].k, smallest[1].k) === 0)
|
|
728
|
+
return best;
|
|
729
|
+
best = smallest[0].e;
|
|
730
|
+
}
|
|
731
|
+
return best;
|
|
732
|
+
}
|
|
733
|
+
// ------------------------------------------------------------------ the schema, checked
|
|
734
|
+
/**
|
|
735
|
+
* `cls.check_schema(schema)`, reduced to the SHAPE of each keyword's value.
|
|
736
|
+
*
|
|
737
|
+
* WHY THIS EXISTS, and why it is not the metaschema. `jsonschema.validate` runs
|
|
738
|
+
* `check_schema` BEFORE it looks at the instance, so a caller schema the 2020-12 metaschema
|
|
739
|
+
* refuses never reaches a keyword. Without it, this port ran the keywords anyway and the
|
|
740
|
+
* keywords are written to SKIP a value they do not recognise — which is correct behaviour
|
|
741
|
+
* inside `iter_errors` and a wrong ANSWER at the tool boundary. N8 measured it on the wire:
|
|
742
|
+
*
|
|
743
|
+
* {"required": "a"} python: SchemaError port: {"valid": true}
|
|
744
|
+
* {"required": 3} python: SchemaError port: {"valid": true}
|
|
745
|
+
* {"properties": ["a"]} python: SchemaError port: {"valid": true}
|
|
746
|
+
* {"additionalProperties": [1]} python: SchemaError port: {"valid": true}
|
|
747
|
+
* {"type": 3} python: SchemaError port: {"valid": false, …}
|
|
748
|
+
*
|
|
749
|
+
* The last one is the worst of the five: a sentence about a schema that is not a schema.
|
|
750
|
+
*
|
|
751
|
+
* WHAT IS CHECKED, AND THE RULING ON WHAT IS NOT. Running the real metaschema is not
|
|
752
|
+
* available to this port — the 2020-12 metaschema is eight documents wired together with
|
|
753
|
+
* `$ref` and `$dynamicRef`, which is exactly the vocabulary this file refuses to implement,
|
|
754
|
+
* so `check_schema` would have to be more capable than `validate`. What is here instead is
|
|
755
|
+
* the SHAPE TABLE: for every keyword this port implements, the type the metaschema demands
|
|
756
|
+
* of its value, applied recursively through the keywords whose values hold subschemas. That
|
|
757
|
+
* is narrower than the metaschema — it does not enforce `uniqueItems` on `required`, the
|
|
758
|
+
* `minLength: 1` on `allOf`, or that a `pattern` compiles — and every gap is a schema Python
|
|
759
|
+
* refuses and this port answers on. Each keyword listed below is MEASURED against the
|
|
760
|
+
* reference in `tools/conformance/suites/validate.mjs`; the gaps are listed there too, as
|
|
761
|
+
* cases that are deliberately absent rather than quietly ruled.
|
|
762
|
+
*
|
|
763
|
+
* BOTH RUNTIMES NOW REFUSE, AND THE SENTENCE DIFFERS. Python raises `SchemaError` with the
|
|
764
|
+
* metaschema's own wording and it ESCAPES `validate_json` to the caller (a registered
|
|
765
|
+
* `runtime-py` defect — a tool should not let an exception out on caller input; not fixed
|
|
766
|
+
* here, invariant 8). This raises `PyJsonSchemaUnsupported`, which escapes the same way,
|
|
767
|
+
* because agreeing that the tool raises is the behaviour and inventing an answer is not.
|
|
768
|
+
*/
|
|
769
|
+
const SIMPLE_TYPES = new Set(['array', 'boolean', 'integer', 'null', 'number', 'object', 'string']);
|
|
770
|
+
/** Keywords whose value is itself a schema. */
|
|
771
|
+
const SUBSCHEMA = [
|
|
772
|
+
'additionalProperties',
|
|
773
|
+
'contains',
|
|
774
|
+
'else',
|
|
775
|
+
'if',
|
|
776
|
+
'items',
|
|
777
|
+
'not',
|
|
778
|
+
'propertyNames',
|
|
779
|
+
'then',
|
|
780
|
+
'unevaluatedItems',
|
|
781
|
+
'unevaluatedProperties',
|
|
782
|
+
];
|
|
783
|
+
/** Keywords whose value is an object whose VALUES are schemas. */
|
|
784
|
+
const SUBSCHEMA_MAP = ['$defs', 'definitions', 'dependentSchemas', 'patternProperties', 'properties'];
|
|
785
|
+
/** Keywords whose value is an array of schemas. */
|
|
786
|
+
const SUBSCHEMA_LIST = ['allOf', 'anyOf', 'oneOf', 'prefixItems'];
|
|
787
|
+
/** Keywords whose value is any JSON number. */
|
|
788
|
+
const NUMBER_KEYWORDS = ['exclusiveMaximum', 'exclusiveMinimum', 'maximum', 'minimum'];
|
|
789
|
+
/** Keywords whose value is a non-negative integer. */
|
|
790
|
+
const COUNT_KEYWORDS = [
|
|
791
|
+
'maxContains',
|
|
792
|
+
'maxItems',
|
|
793
|
+
'maxLength',
|
|
794
|
+
'maxProperties',
|
|
795
|
+
'minContains',
|
|
796
|
+
'minItems',
|
|
797
|
+
'minLength',
|
|
798
|
+
'minProperties',
|
|
799
|
+
];
|
|
800
|
+
function refuse(where, expected, got) {
|
|
801
|
+
const kind = typeof got === 'boolean' ? 'boolean' : got.t;
|
|
802
|
+
throw new PyJsonSchemaUnsupported(`${where} must be ${expected}, got ${kind}`);
|
|
803
|
+
}
|
|
804
|
+
const isSchemaValue = (v) => v.t === 'dict' || v.t === 'bool';
|
|
805
|
+
export function checkSchema(schema, where = 'the schema') {
|
|
806
|
+
if (typeof schema === 'boolean' || schema.t === 'bool')
|
|
807
|
+
return;
|
|
808
|
+
if (schema.t !== 'dict')
|
|
809
|
+
refuse(where, 'an object or a boolean', schema);
|
|
810
|
+
for (const [keyword, value] of schema.v) {
|
|
811
|
+
const at = `${where}: ${keyword}`;
|
|
812
|
+
if (SUBSCHEMA_LIST.includes(keyword) && keyword !== 'prefixItems') {
|
|
813
|
+
// `allOf`/`anyOf`/`oneOf` carry `"minItems": 1` in the applicator vocabulary.
|
|
814
|
+
if (value.t === 'list' && value.v.length === 0) {
|
|
815
|
+
throw new PyJsonSchemaUnsupported(`${at} must be a non-empty array`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (SUBSCHEMA.includes(keyword)) {
|
|
819
|
+
if (!isSchemaValue(value))
|
|
820
|
+
refuse(at, 'an object or a boolean', value);
|
|
821
|
+
checkSchema(value, at);
|
|
822
|
+
}
|
|
823
|
+
else if (SUBSCHEMA_MAP.includes(keyword)) {
|
|
824
|
+
if (value.t !== 'dict')
|
|
825
|
+
refuse(at, 'an object', value);
|
|
826
|
+
for (const [k, v] of value.v) {
|
|
827
|
+
if (!isSchemaValue(v))
|
|
828
|
+
refuse(`${at}/${k}`, 'an object or a boolean', v);
|
|
829
|
+
checkSchema(v, `${at}/${k}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
else if (SUBSCHEMA_LIST.includes(keyword)) {
|
|
833
|
+
if (value.t !== 'list')
|
|
834
|
+
refuse(at, 'an array', value);
|
|
835
|
+
value.v.forEach((v, i) => {
|
|
836
|
+
if (!isSchemaValue(v))
|
|
837
|
+
refuse(`${at}/${i}`, 'an object or a boolean', v);
|
|
838
|
+
checkSchema(v, `${at}/${i}`);
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
else if (keyword === 'type') {
|
|
842
|
+
const each = (v, place) => {
|
|
843
|
+
if (v.t !== 'str')
|
|
844
|
+
refuse(place, 'a simple type name', v);
|
|
845
|
+
if (!SIMPLE_TYPES.has(v.v)) {
|
|
846
|
+
throw new PyJsonSchemaUnsupported(`${place} must be a simple type name, got ${reprValue(v)}`);
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
if (value.t === 'list') {
|
|
850
|
+
// `{"type": [...]}` is `"minItems": 1, "uniqueItems": true` in the validation
|
|
851
|
+
// vocabulary, and both are reachable: `{"type": []}` and `{"type": ["string",
|
|
852
|
+
// "string"]}` are schemas the reference refuses and this port used to answer on.
|
|
853
|
+
if (value.v.length === 0)
|
|
854
|
+
throw new PyJsonSchemaUnsupported(`${at} must be a non-empty array`);
|
|
855
|
+
const seen = new Set();
|
|
856
|
+
value.v.forEach((v, i) => {
|
|
857
|
+
each(v, `${at}/${i}`);
|
|
858
|
+
const name = v.v;
|
|
859
|
+
if (seen.has(name)) {
|
|
860
|
+
throw new PyJsonSchemaUnsupported(`${at} must have unique items, got ${reprValue(v)} twice`);
|
|
861
|
+
}
|
|
862
|
+
seen.add(name);
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
else
|
|
866
|
+
each(value, at);
|
|
867
|
+
}
|
|
868
|
+
else if (keyword === 'required') {
|
|
869
|
+
if (value.t !== 'list')
|
|
870
|
+
refuse(at, 'an array', value);
|
|
871
|
+
const seen = new Set();
|
|
872
|
+
value.v.forEach((v, i) => {
|
|
873
|
+
if (v.t !== 'str')
|
|
874
|
+
refuse(`${at}/${i}`, 'a string', v);
|
|
875
|
+
if (seen.has(v.v)) {
|
|
876
|
+
throw new PyJsonSchemaUnsupported(`${at} must have unique items, got ${reprValue(v)} twice`);
|
|
877
|
+
}
|
|
878
|
+
seen.add(v.v);
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
else if (keyword === 'dependentRequired') {
|
|
882
|
+
if (value.t !== 'dict')
|
|
883
|
+
refuse(at, 'an object', value);
|
|
884
|
+
for (const [k, v] of value.v) {
|
|
885
|
+
if (v.t !== 'list')
|
|
886
|
+
refuse(`${at}/${k}`, 'an array', v);
|
|
887
|
+
v.v.forEach((x, i) => {
|
|
888
|
+
if (x.t !== 'str')
|
|
889
|
+
refuse(`${at}/${k}/${i}`, 'a string', x);
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
else if (keyword === 'enum') {
|
|
894
|
+
if (value.t !== 'list')
|
|
895
|
+
refuse(at, 'an array', value);
|
|
896
|
+
}
|
|
897
|
+
else if (keyword === 'uniqueItems') {
|
|
898
|
+
if (value.t !== 'bool')
|
|
899
|
+
refuse(at, 'a boolean', value);
|
|
900
|
+
}
|
|
901
|
+
else if (keyword === 'pattern' || keyword === 'format') {
|
|
902
|
+
if (value.t !== 'str')
|
|
903
|
+
refuse(at, 'a string', value);
|
|
904
|
+
// `"format": "regex"` on `pattern` in the metaschema, and jsonschema's own format
|
|
905
|
+
// checker compiles it — `{"pattern": "("}` is a SchemaError there, and was
|
|
906
|
+
// `{"valid": true}` here. `reSearch` already refuses a pattern it cannot compile;
|
|
907
|
+
// this only moves the refusal to where Python's is, which is before the instance.
|
|
908
|
+
//
|
|
909
|
+
// THE PRICE, measured and accepted: `re` accepts patterns `RegExp` does not
|
|
910
|
+
// (`(?P<x>…)`, `\A`, `\Z`), so a schema carrying one now refuses HERE even when the
|
|
911
|
+
// keyword would never have fired. Before, it refused only when it fired, and answered
|
|
912
|
+
// otherwise. Both are the same ruled divergence — see the `python re and JS RegExp`
|
|
913
|
+
// ruling — and a refusal in an unreached branch is a smaller error than `{"valid":
|
|
914
|
+
// true}` for a pattern that is not a pattern. Both directions are pinned as ruled
|
|
915
|
+
// cases in `tools/conformance/suites/validate.mjs`.
|
|
916
|
+
if (keyword === 'pattern')
|
|
917
|
+
reSearch(value.v, '');
|
|
918
|
+
}
|
|
919
|
+
else if (keyword === 'multipleOf') {
|
|
920
|
+
if (value.t !== 'int' && value.t !== 'float')
|
|
921
|
+
refuse(at, 'a number', value);
|
|
922
|
+
if (value.t === 'int' ? value.v <= 0n : value.v <= 0) {
|
|
923
|
+
throw new PyJsonSchemaUnsupported(`${at} must be a number greater than 0, got ${reprValue(value)}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
else if (NUMBER_KEYWORDS.includes(keyword)) {
|
|
927
|
+
if (value.t !== 'int' && value.t !== 'float')
|
|
928
|
+
refuse(at, 'a number', value);
|
|
929
|
+
}
|
|
930
|
+
else if (COUNT_KEYWORDS.includes(keyword)) {
|
|
931
|
+
const integral = value.t === 'int' || (value.t === 'float' && Number.isInteger(value.v));
|
|
932
|
+
if (!integral)
|
|
933
|
+
refuse(at, 'a non-negative integer', value);
|
|
934
|
+
if (value.t === 'int' ? value.v < 0n : value.v < 0) {
|
|
935
|
+
throw new PyJsonSchemaUnsupported(`${at} must be a non-negative integer, got ${reprValue(value)}`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* `jsonschema.validate(instance, schema)` reduced to what `contract.schema_error` needs:
|
|
942
|
+
* the single error `best_match` chooses, or `null`.
|
|
943
|
+
*
|
|
944
|
+
* `check_schema` runs FIRST, as it does in the reference. See `checkSchema` for what that
|
|
945
|
+
* means here and for the five wrong answers it replaced.
|
|
946
|
+
*/
|
|
947
|
+
export function validate(instance, schema) {
|
|
948
|
+
checkSchema(schema);
|
|
949
|
+
return bestMatch(iterErrors(instance, schema));
|
|
950
|
+
}
|
|
951
|
+
export { iterErrors };
|