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
package/dist/pyjson.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export { pyFloatRepr } from './memory/pyfs.js';
|
|
2
|
+
/**
|
|
3
|
+
* A JSON document as CPython's decoder builds it.
|
|
4
|
+
*
|
|
5
|
+
* Tagged rather than native because two of the tags are indistinguishable in JS: `int` and
|
|
6
|
+
* `float` are one type there, and `1` and `1.0` must not print the same. `int` carries a
|
|
7
|
+
* `bigint` because Python's does not overflow.
|
|
8
|
+
*/
|
|
9
|
+
export type PyValue = {
|
|
10
|
+
readonly t: 'null';
|
|
11
|
+
} | {
|
|
12
|
+
readonly t: 'bool';
|
|
13
|
+
readonly v: boolean;
|
|
14
|
+
} | {
|
|
15
|
+
readonly t: 'int';
|
|
16
|
+
readonly v: bigint;
|
|
17
|
+
} | {
|
|
18
|
+
readonly t: 'float';
|
|
19
|
+
readonly v: number;
|
|
20
|
+
} | {
|
|
21
|
+
readonly t: 'str';
|
|
22
|
+
readonly v: string;
|
|
23
|
+
} | {
|
|
24
|
+
readonly t: 'list';
|
|
25
|
+
readonly v: PyValue[];
|
|
26
|
+
} | {
|
|
27
|
+
readonly t: 'dict';
|
|
28
|
+
readonly v: Map<string, PyValue>;
|
|
29
|
+
};
|
|
30
|
+
export declare const PY_NONE: PyValue;
|
|
31
|
+
/**
|
|
32
|
+
* Lift a plain JS value into the tagged form.
|
|
33
|
+
*
|
|
34
|
+
* RULING — the one place this port cannot recover Python's answer. A JS number carries no
|
|
35
|
+
* record of whether its JSON text had a decimal point, so `1` and `1.0` arrive identical and
|
|
36
|
+
* this maps every integral, safe-range number to `int`. That is exact for every schema
|
|
37
|
+
* bantamkit ships (measured: the whole asset pack holds no non-integral number, and the only
|
|
38
|
+
* numeric keywords in it are `minimum`/`maximum` with integer bounds). It is NOT exact for a
|
|
39
|
+
* caller-supplied schema written `{"minimum": 1.0}`, where Python says
|
|
40
|
+
* `is less than the minimum of 1.0` and this says `1`. `parseJson` is the exact route and
|
|
41
|
+
* the MCP server should use it wherever it still holds the wire bytes; `fromJs` is the
|
|
42
|
+
* fallback for a value that has already been through an SDK's parser. Carried as a ruled
|
|
43
|
+
* case in `tools/conformance/suites/validate.mjs`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function fromJs(value: unknown): PyValue;
|
|
46
|
+
/** Back to plain JS, for a caller that only wants the data. Precision may be lost. */
|
|
47
|
+
export declare function toJs(value: PyValue): unknown;
|
|
48
|
+
/** `repr()` of a decoded JSON document, exactly as an error sentence interpolates it. */
|
|
49
|
+
export declare function reprValue(value: PyValue): string;
|
|
50
|
+
/**
|
|
51
|
+
* `json.dumps`, which is the OTHER half of this seam and the one that writes files.
|
|
52
|
+
*
|
|
53
|
+
* `JSON.stringify` differs from it in four ways, every one of which silently corrupts a
|
|
54
|
+
* shift-work checkpoint rather than failing:
|
|
55
|
+
*
|
|
56
|
+
* 1. **`ensure_ascii=True`.** Python escapes every codepoint outside ` `..`~` as
|
|
57
|
+
* `\uXXXX`, astral ones as a surrogate PAIR. `JSON.stringify` emits them raw. The
|
|
58
|
+
* document still round-trips to the same string, so nothing errors — the file just
|
|
59
|
+
* stops being byte-comparable with the one Python writes, and the whole 29 KB of a
|
|
60
|
+
* real checkpoint gets rewritten on the first successful clock-out.
|
|
61
|
+
* 2. **separators.** `(', ', ': ')` with no indent, `(',', ': ')` with one.
|
|
62
|
+
* `JSON.stringify` uses neither.
|
|
63
|
+
* 3. **`sort_keys` is a codepoint sort.** JS `Array.sort` on strings is UTF-16 order,
|
|
64
|
+
* which puts every astral key before U+E000 instead of after it.
|
|
65
|
+
* 4. **`5.0` is a float.** `JSON.stringify({n: 5.0})` is `{"n":5}`. Python keeps the
|
|
66
|
+
* decimal point because it kept the type, which is why this module carries a tagged
|
|
67
|
+
* value model at all.
|
|
68
|
+
*
|
|
69
|
+
* Also faithful: `allow_nan=True` emits the bare words `NaN`, `Infinity` and `-Infinity`
|
|
70
|
+
* that no JSON parser accepts, and `/` is NOT escaped.
|
|
71
|
+
*/
|
|
72
|
+
export interface DumpOptions {
|
|
73
|
+
/** Spaces per level. `null`/omitted is Python's `indent=None` — everything on one line. */
|
|
74
|
+
indent?: number | null;
|
|
75
|
+
/** Python's `sort_keys`. The comparison is by CODEPOINT, not by UTF-16 unit. */
|
|
76
|
+
sortKeys?: boolean;
|
|
77
|
+
}
|
|
78
|
+
export declare function dumpJson(value: PyValue, options?: DumpOptions): string;
|
|
79
|
+
/** `json.JSONDecodeError`. `message` is what `str(e)` gives, which is what the model reads. */
|
|
80
|
+
export declare class PyJSONDecodeError extends Error {
|
|
81
|
+
readonly msg: string;
|
|
82
|
+
readonly pos: number;
|
|
83
|
+
readonly lineno: number;
|
|
84
|
+
readonly colno: number;
|
|
85
|
+
constructor(msg: string, doc: string, utf16Pos: number);
|
|
86
|
+
}
|
|
87
|
+
/** `ValueError`, the base `extract_json` raises and `schema_error` catches. */
|
|
88
|
+
export declare class PyValueError extends Error {
|
|
89
|
+
constructor(message: string);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* `JSONDecoder().raw_decode(s, idx)` — decode ONE value and return it with the offset just
|
|
93
|
+
* past it. Trailing text is not an error here; that is the whole reason `extract_json` uses
|
|
94
|
+
* this and not `json.loads`. Note there is no leading-whitespace skip: `raw_decode` has
|
|
95
|
+
* none either, and `extract_json` always enters on a `{` or a `[`.
|
|
96
|
+
*/
|
|
97
|
+
export declare function rawDecode(s: string, idx?: number): [PyValue, number];
|
|
98
|
+
/**
|
|
99
|
+
* `json.loads`, for the places that want the whole document and nothing after it.
|
|
100
|
+
*
|
|
101
|
+
* THE BOM CHECK IS `loads`'s OWN, and it is not in `raw_decode`. `json/__init__.py` tests
|
|
102
|
+
* `s.startswith('\ufeff')` before it hands anything to a decoder and refuses with its own
|
|
103
|
+
* sentence; `JSONDecoder().raw_decode` — which is what `contract.extract_json` calls — has
|
|
104
|
+
* no such test and would report `Expecting value` instead. The two are kept apart here for
|
|
105
|
+
* the same reason. Reachable: `shiftwork._read_checkpoint` is `json.loads(read_text(...))`,
|
|
106
|
+
* and `read_text` KEEPS the U+FEFF (`utf-8-sig` is the codec that strips it), so a
|
|
107
|
+
* BOM-prefixed checkpoint is refused by the reference and must be refused here. N8 measured
|
|
108
|
+
* this port parsing one and then WRITING to it.
|
|
109
|
+
*/
|
|
110
|
+
export declare function parseJson(s: string): PyValue;
|
package/dist/pyjson.js
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CPython `json` seam: `JSONDecoder().raw_decode`, `json.dumps`, and `repr()` of what
|
|
3
|
+
* the decoder returns.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS NOT `JSON.parse`. `contract.extract_json` calls `raw_decode`, and every
|
|
6
|
+
* validation sentence downstream interpolates `{instance!r}` — Python's `repr` of the value
|
|
7
|
+
* the decoder built. Four things separate that from `JSON.parse`:
|
|
8
|
+
*
|
|
9
|
+
* 1. **int is not float.** `{"n": 1.0}` gives Python a `float` whose repr is `1.0`;
|
|
10
|
+
* `JSON.parse` gives `1`, and the sentence would name a value the model never wrote.
|
|
11
|
+
* 2. **int is arbitrary precision.** `12345678901234567890` survives Python exactly and
|
|
12
|
+
* rounds to `12345678901234567000` through a JS number.
|
|
13
|
+
* 3. **`NaN`, `Infinity` and `-Infinity` are accepted** by CPython's decoder and rejected
|
|
14
|
+
* by `JSON.parse`; their reprs are `nan`, `inf`, `-inf`.
|
|
15
|
+
* 4. **the failure text is the product.** `parse_error_message` interpolates the decoder's
|
|
16
|
+
* own exception, so `Expecting ':' delimiter: line 1 column 6 (char 5)` reaches the
|
|
17
|
+
* model verbatim, character offsets included.
|
|
18
|
+
*
|
|
19
|
+
* Ported against the **C** scanner (`_json.c`), which is what a stock CPython uses and whose
|
|
20
|
+
* messages differ from the pure-Python fallback in `json/decoder.py`: the C scanner writes
|
|
21
|
+
* `Invalid \escape` and `Invalid control character at` with NO repr of the offending
|
|
22
|
+
* character, and reports the position of the backslash rather than the position after it.
|
|
23
|
+
* That was measured, not read — see `tools/conformance/suites/validate.mjs`.
|
|
24
|
+
*
|
|
25
|
+
* POSITIONS ARE IN CODEPOINTS. Python indexes `str` by codepoint and JS indexes by UTF-16
|
|
26
|
+
* unit, so `{"\u{1F600}":1,}` is `char 7` in Python and would be `char 8` from a naive
|
|
27
|
+
* `.length`. Parsing runs on the JS string with UTF-16 indices — that is what the regexes
|
|
28
|
+
* and `slice` want — and every index that reaches a MESSAGE is converted on the way out.
|
|
29
|
+
*/
|
|
30
|
+
import { cmpCodepoint, pyFloatRepr, pyRepr as pyReprString } from './memory/pyfs.js';
|
|
31
|
+
// `pyFloatRepr` lives beside `pyRepr` in `pyfs.ts` — the two are one question (how does
|
|
32
|
+
// CPython spell a value in a sentence) and `memory/pyyaml.ts` needs the float half to
|
|
33
|
+
// re-emit a frontmatter scalar PyYAML resolved to a float. Re-exported here because this
|
|
34
|
+
// is where the rest of the package has always imported it from.
|
|
35
|
+
export { pyFloatRepr } from './memory/pyfs.js';
|
|
36
|
+
export const PY_NONE = { t: 'null' };
|
|
37
|
+
/**
|
|
38
|
+
* Lift a plain JS value into the tagged form.
|
|
39
|
+
*
|
|
40
|
+
* RULING — the one place this port cannot recover Python's answer. A JS number carries no
|
|
41
|
+
* record of whether its JSON text had a decimal point, so `1` and `1.0` arrive identical and
|
|
42
|
+
* this maps every integral, safe-range number to `int`. That is exact for every schema
|
|
43
|
+
* bantamkit ships (measured: the whole asset pack holds no non-integral number, and the only
|
|
44
|
+
* numeric keywords in it are `minimum`/`maximum` with integer bounds). It is NOT exact for a
|
|
45
|
+
* caller-supplied schema written `{"minimum": 1.0}`, where Python says
|
|
46
|
+
* `is less than the minimum of 1.0` and this says `1`. `parseJson` is the exact route and
|
|
47
|
+
* the MCP server should use it wherever it still holds the wire bytes; `fromJs` is the
|
|
48
|
+
* fallback for a value that has already been through an SDK's parser. Carried as a ruled
|
|
49
|
+
* case in `tools/conformance/suites/validate.mjs`.
|
|
50
|
+
*/
|
|
51
|
+
export function fromJs(value) {
|
|
52
|
+
if (value === null || value === undefined)
|
|
53
|
+
return PY_NONE;
|
|
54
|
+
if (typeof value === 'boolean')
|
|
55
|
+
return { t: 'bool', v: value };
|
|
56
|
+
if (typeof value === 'bigint')
|
|
57
|
+
return { t: 'int', v: value };
|
|
58
|
+
if (typeof value === 'number') {
|
|
59
|
+
return Number.isSafeInteger(value) && !Object.is(value, -0)
|
|
60
|
+
? { t: 'int', v: BigInt(value) }
|
|
61
|
+
: { t: 'float', v: value };
|
|
62
|
+
}
|
|
63
|
+
if (typeof value === 'string')
|
|
64
|
+
return { t: 'str', v: value };
|
|
65
|
+
if (Array.isArray(value))
|
|
66
|
+
return { t: 'list', v: value.map(fromJs) };
|
|
67
|
+
if (value instanceof Map) {
|
|
68
|
+
const out = new Map();
|
|
69
|
+
for (const [k, v] of value)
|
|
70
|
+
out.set(String(k), fromJs(v));
|
|
71
|
+
return { t: 'dict', v: out };
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === 'object') {
|
|
74
|
+
const out = new Map();
|
|
75
|
+
for (const [k, v] of Object.entries(value))
|
|
76
|
+
out.set(k, fromJs(v));
|
|
77
|
+
return { t: 'dict', v: out };
|
|
78
|
+
}
|
|
79
|
+
throw new TypeError(`cannot lift ${typeof value} into a PyValue`);
|
|
80
|
+
}
|
|
81
|
+
/** Back to plain JS, for a caller that only wants the data. Precision may be lost. */
|
|
82
|
+
export function toJs(value) {
|
|
83
|
+
switch (value.t) {
|
|
84
|
+
case 'null':
|
|
85
|
+
return null;
|
|
86
|
+
case 'bool':
|
|
87
|
+
case 'float':
|
|
88
|
+
case 'str':
|
|
89
|
+
return value.v;
|
|
90
|
+
case 'int':
|
|
91
|
+
return Number(value.v);
|
|
92
|
+
case 'list':
|
|
93
|
+
return value.v.map(toJs);
|
|
94
|
+
case 'dict': {
|
|
95
|
+
const out = {};
|
|
96
|
+
for (const [k, v] of value.v)
|
|
97
|
+
out[k] = toJs(v);
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// -------------------------------------------------------------------------------- repr
|
|
103
|
+
/** `repr()` of a decoded JSON document, exactly as an error sentence interpolates it. */
|
|
104
|
+
export function reprValue(value) {
|
|
105
|
+
switch (value.t) {
|
|
106
|
+
case 'null':
|
|
107
|
+
return 'None';
|
|
108
|
+
case 'bool':
|
|
109
|
+
return value.v ? 'True' : 'False';
|
|
110
|
+
case 'int':
|
|
111
|
+
return value.v.toString();
|
|
112
|
+
case 'float':
|
|
113
|
+
return pyFloatRepr(value.v);
|
|
114
|
+
case 'str':
|
|
115
|
+
return pyReprString(value.v);
|
|
116
|
+
case 'list':
|
|
117
|
+
return `[${value.v.map(reprValue).join(', ')}]`;
|
|
118
|
+
case 'dict':
|
|
119
|
+
return `{${[...value.v].map(([k, v]) => `${pyReprString(k)}: ${reprValue(v)}`).join(', ')}}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* `json/encoder.py`'s `ESCAPE_DCT`: the seven short escapes, then `\u00xx` for the rest of
|
|
124
|
+
* C0. Everything from ` ` (0x20) through `~` (0x7e) that is not `"` or `\` is literal, and
|
|
125
|
+
* everything else — DEL included — is `\uXXXX`.
|
|
126
|
+
*/
|
|
127
|
+
const ESCAPE_DCT = {
|
|
128
|
+
'\\': '\\\\',
|
|
129
|
+
'"': '\\"',
|
|
130
|
+
'\b': '\\b',
|
|
131
|
+
'\f': '\\f',
|
|
132
|
+
'\n': '\\n',
|
|
133
|
+
'\r': '\\r',
|
|
134
|
+
'\t': '\\t',
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* `py_encode_basestring_ascii`.
|
|
138
|
+
*
|
|
139
|
+
* The loop walks UTF-16 units on purpose: a lone surrogate half is exactly what Python
|
|
140
|
+
* emits for an astral character under `ensure_ascii`, so iterating by codepoint would only
|
|
141
|
+
* have to split every pair back apart.
|
|
142
|
+
*/
|
|
143
|
+
function encodeBasestringAscii(s) {
|
|
144
|
+
let out = '"';
|
|
145
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
146
|
+
const ch = s[i];
|
|
147
|
+
const short = ESCAPE_DCT[ch];
|
|
148
|
+
if (short !== undefined) {
|
|
149
|
+
out += short;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const unit = s.charCodeAt(i);
|
|
153
|
+
out += unit >= 0x20 && unit <= 0x7e ? ch : `\\u${unit.toString(16).padStart(4, '0')}`;
|
|
154
|
+
}
|
|
155
|
+
return `${out}"`;
|
|
156
|
+
}
|
|
157
|
+
/** `floatstr` under `allow_nan=True`: `repr` for the finite ones, bare words for the rest. */
|
|
158
|
+
function encodeFloat(x) {
|
|
159
|
+
if (Number.isNaN(x))
|
|
160
|
+
return 'NaN';
|
|
161
|
+
if (x === Infinity)
|
|
162
|
+
return 'Infinity';
|
|
163
|
+
if (x === -Infinity)
|
|
164
|
+
return '-Infinity';
|
|
165
|
+
return pyFloatRepr(x);
|
|
166
|
+
}
|
|
167
|
+
export function dumpJson(value, options = {}) {
|
|
168
|
+
const indent = options.indent ?? null;
|
|
169
|
+
const sortKeys = options.sortKeys ?? false;
|
|
170
|
+
const pad = indent === null ? '' : ' '.repeat(indent);
|
|
171
|
+
// `indent is not None` changes the ITEM separator and leaves the key separator alone.
|
|
172
|
+
const itemSep = indent === null ? ', ' : ',';
|
|
173
|
+
const keySep = ': ';
|
|
174
|
+
const encode = (v, level) => {
|
|
175
|
+
switch (v.t) {
|
|
176
|
+
case 'null':
|
|
177
|
+
return 'null';
|
|
178
|
+
case 'bool':
|
|
179
|
+
return v.v ? 'true' : 'false';
|
|
180
|
+
case 'int':
|
|
181
|
+
return v.v.toString();
|
|
182
|
+
case 'float':
|
|
183
|
+
return encodeFloat(v.v);
|
|
184
|
+
case 'str':
|
|
185
|
+
return encodeBasestringAscii(v.v);
|
|
186
|
+
case 'list': {
|
|
187
|
+
if (v.v.length === 0)
|
|
188
|
+
return '[]';
|
|
189
|
+
const parts = v.v.map((item) => encode(item, level + 1));
|
|
190
|
+
if (indent === null)
|
|
191
|
+
return `[${parts.join(itemSep)}]`;
|
|
192
|
+
const nl = `\n${pad.repeat(level + 1)}`;
|
|
193
|
+
return `[${nl}${parts.join(itemSep + nl)}\n${pad.repeat(level)}]`;
|
|
194
|
+
}
|
|
195
|
+
case 'dict': {
|
|
196
|
+
if (v.v.size === 0)
|
|
197
|
+
return '{}';
|
|
198
|
+
const entries = [...v.v];
|
|
199
|
+
// `sorted(dct.items())` — keys are unique, so the value never enters the compare.
|
|
200
|
+
if (sortKeys)
|
|
201
|
+
entries.sort((a, b) => cmpCodepoint(a[0], b[0]));
|
|
202
|
+
const parts = entries.map(([k, item]) => `${encodeBasestringAscii(k)}${keySep}${encode(item, level + 1)}`);
|
|
203
|
+
if (indent === null)
|
|
204
|
+
return `{${parts.join(itemSep)}}`;
|
|
205
|
+
const nl = `\n${pad.repeat(level + 1)}`;
|
|
206
|
+
return `{${nl}${parts.join(itemSep + nl)}\n${pad.repeat(level)}}`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
return encode(value, 0);
|
|
211
|
+
}
|
|
212
|
+
// ------------------------------------------------------------------------- the decoder
|
|
213
|
+
/** `json.JSONDecodeError`. `message` is what `str(e)` gives, which is what the model reads. */
|
|
214
|
+
export class PyJSONDecodeError extends Error {
|
|
215
|
+
msg;
|
|
216
|
+
pos;
|
|
217
|
+
lineno;
|
|
218
|
+
colno;
|
|
219
|
+
constructor(msg, doc, utf16Pos) {
|
|
220
|
+
const head = [...doc.slice(0, utf16Pos)];
|
|
221
|
+
const pos = head.length;
|
|
222
|
+
let lastNewline = -1;
|
|
223
|
+
let lines = 1;
|
|
224
|
+
for (let i = 0; i < head.length; i += 1) {
|
|
225
|
+
if (head[i] === '\n') {
|
|
226
|
+
lastNewline = i;
|
|
227
|
+
lines += 1;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
super(`${msg}: line ${lines} column ${pos - lastNewline} (char ${pos})`);
|
|
231
|
+
this.name = 'PyJSONDecodeError';
|
|
232
|
+
this.msg = msg;
|
|
233
|
+
this.pos = pos;
|
|
234
|
+
this.lineno = lines;
|
|
235
|
+
this.colno = pos - lastNewline;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** `ValueError`, the base `extract_json` raises and `schema_error` catches. */
|
|
239
|
+
export class PyValueError extends Error {
|
|
240
|
+
constructor(message) {
|
|
241
|
+
super(message);
|
|
242
|
+
this.name = 'PyValueError';
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const WS = ' \t\n\r';
|
|
246
|
+
/**
|
|
247
|
+
* `nextchar in _ws` where `nextchar` came from `s[end:end+1]`.
|
|
248
|
+
*
|
|
249
|
+
* Past the end that slice is `''`, and `'' in ' \t\n\r'` is TRUE in Python — the empty
|
|
250
|
+
* string is a substring of everything. Two of the decoder's branches depend on it.
|
|
251
|
+
*/
|
|
252
|
+
const isWs = (ch) => ch === '' || WS.includes(ch);
|
|
253
|
+
const skipWs = (s, i) => {
|
|
254
|
+
let at = i;
|
|
255
|
+
while (at < s.length && WS.includes(s[at]))
|
|
256
|
+
at += 1;
|
|
257
|
+
return at;
|
|
258
|
+
};
|
|
259
|
+
const BACKSLASH = {
|
|
260
|
+
'"': '"',
|
|
261
|
+
'\\': '\\',
|
|
262
|
+
'/': '/',
|
|
263
|
+
b: '\b',
|
|
264
|
+
f: '\f',
|
|
265
|
+
n: '\n',
|
|
266
|
+
r: '\r',
|
|
267
|
+
t: '\t',
|
|
268
|
+
};
|
|
269
|
+
/** `c_scanstring`, entered just past the opening quote. Returns [value, end]. */
|
|
270
|
+
function scanString(s, start) {
|
|
271
|
+
const begin = start - 1;
|
|
272
|
+
let end = start;
|
|
273
|
+
let out = '';
|
|
274
|
+
for (;;) {
|
|
275
|
+
let next = end;
|
|
276
|
+
let terminator = '';
|
|
277
|
+
while (next < s.length) {
|
|
278
|
+
const c = s[next];
|
|
279
|
+
if (c === '"' || c === '\\') {
|
|
280
|
+
terminator = c;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
if (c <= '\u001f')
|
|
284
|
+
throw new PyJSONDecodeError('Invalid control character at', s, next);
|
|
285
|
+
next += 1;
|
|
286
|
+
}
|
|
287
|
+
if (terminator === '')
|
|
288
|
+
throw new PyJSONDecodeError('Unterminated string starting at', s, begin);
|
|
289
|
+
if (next !== end)
|
|
290
|
+
out += s.slice(end, next);
|
|
291
|
+
next += 1;
|
|
292
|
+
if (terminator === '"')
|
|
293
|
+
return [out, next];
|
|
294
|
+
if (next === s.length)
|
|
295
|
+
throw new PyJSONDecodeError('Unterminated string starting at', s, begin);
|
|
296
|
+
const esc = s[next];
|
|
297
|
+
if (esc !== 'u') {
|
|
298
|
+
const ch = BACKSLASH[esc];
|
|
299
|
+
// The C scanner reports the BACKSLASH, not the chunk start and not the escape letter.
|
|
300
|
+
// Measured: `"a\\q"` is char 2 and `"\\q"` is char 1.
|
|
301
|
+
if (ch === undefined)
|
|
302
|
+
throw new PyJSONDecodeError('Invalid \\escape', s, next - 1);
|
|
303
|
+
out += ch;
|
|
304
|
+
end = next + 1;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
let uni = decodeUXXXX(s, next);
|
|
308
|
+
let after = next + 5;
|
|
309
|
+
if (uni >= 0xd800 && uni <= 0xdbff && s.slice(after, after + 2) === '\\u') {
|
|
310
|
+
const uni2 = decodeUXXXX(s, after + 1);
|
|
311
|
+
if (uni2 >= 0xdc00 && uni2 <= 0xdfff) {
|
|
312
|
+
uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00));
|
|
313
|
+
after += 6;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// A lone surrogate is a legal Python `str` character and a legal JS one; `chr(uni)` and
|
|
317
|
+
// `String.fromCodePoint` agree on every value the branch above can produce.
|
|
318
|
+
out += String.fromCodePoint(uni);
|
|
319
|
+
end = after;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* `\uXXXX`, reported at the position of the `u`.
|
|
324
|
+
*
|
|
325
|
+
* The C scanner needs FIVE characters after the `u`, not four: `"\u0041` is
|
|
326
|
+
* `Invalid \uXXXX escape` while `"\u0041 ` gets past the escape and fails as an
|
|
327
|
+
* unterminated string. Measured on both sides of the boundary — the pure-Python
|
|
328
|
+
* `_decode_uXXXX` needs only four, so a port written from `json/decoder.py` is wrong here.
|
|
329
|
+
*/
|
|
330
|
+
function decodeUXXXX(s, uAt) {
|
|
331
|
+
if (uAt + 5 >= s.length)
|
|
332
|
+
throw new PyJSONDecodeError('Invalid \\uXXXX escape', s, uAt);
|
|
333
|
+
const esc = s.slice(uAt + 1, uAt + 5);
|
|
334
|
+
if (esc[1] !== 'x' && esc[1] !== 'X' && /^[0-9a-fA-F]{4}$/.test(esc)) {
|
|
335
|
+
return Number.parseInt(esc, 16);
|
|
336
|
+
}
|
|
337
|
+
throw new PyJSONDecodeError('Invalid \\uXXXX escape', s, uAt);
|
|
338
|
+
}
|
|
339
|
+
const NUMBER_RE = /(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?/y;
|
|
340
|
+
/** A `StopIteration(idx)` out of `scan_once`, which `raw_decode` turns into a message. */
|
|
341
|
+
class StopScan {
|
|
342
|
+
at;
|
|
343
|
+
constructor(at) {
|
|
344
|
+
this.at = at;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function scanOnce(s, idx) {
|
|
348
|
+
if (idx >= s.length)
|
|
349
|
+
throw new StopScan(idx);
|
|
350
|
+
const nextchar = s[idx];
|
|
351
|
+
if (nextchar === '"') {
|
|
352
|
+
const [value, end] = scanString(s, idx + 1);
|
|
353
|
+
return [{ t: 'str', v: value }, end];
|
|
354
|
+
}
|
|
355
|
+
if (nextchar === '{')
|
|
356
|
+
return scanObject(s, idx + 1);
|
|
357
|
+
if (nextchar === '[')
|
|
358
|
+
return scanArray(s, idx + 1);
|
|
359
|
+
if (nextchar === 'n' && s.slice(idx, idx + 4) === 'null')
|
|
360
|
+
return [PY_NONE, idx + 4];
|
|
361
|
+
if (nextchar === 't' && s.slice(idx, idx + 4) === 'true')
|
|
362
|
+
return [{ t: 'bool', v: true }, idx + 4];
|
|
363
|
+
if (nextchar === 'f' && s.slice(idx, idx + 5) === 'false')
|
|
364
|
+
return [{ t: 'bool', v: false }, idx + 5];
|
|
365
|
+
NUMBER_RE.lastIndex = idx;
|
|
366
|
+
const m = NUMBER_RE.exec(s);
|
|
367
|
+
if (m) {
|
|
368
|
+
const integer = m[1];
|
|
369
|
+
const frac = m[2];
|
|
370
|
+
const exp = m[3];
|
|
371
|
+
if (frac || exp) {
|
|
372
|
+
return [{ t: 'float', v: Number(`${integer}${frac ?? ''}${exp ?? ''}`) }, NUMBER_RE.lastIndex];
|
|
373
|
+
}
|
|
374
|
+
return [{ t: 'int', v: BigInt(integer) }, NUMBER_RE.lastIndex];
|
|
375
|
+
}
|
|
376
|
+
if (nextchar === 'N' && s.slice(idx, idx + 3) === 'NaN')
|
|
377
|
+
return [{ t: 'float', v: NaN }, idx + 3];
|
|
378
|
+
if (nextchar === 'I' && s.slice(idx, idx + 8) === 'Infinity') {
|
|
379
|
+
return [{ t: 'float', v: Infinity }, idx + 8];
|
|
380
|
+
}
|
|
381
|
+
if (nextchar === '-' && s.slice(idx, idx + 9) === '-Infinity') {
|
|
382
|
+
return [{ t: 'float', v: -Infinity }, idx + 9];
|
|
383
|
+
}
|
|
384
|
+
throw new StopScan(idx);
|
|
385
|
+
}
|
|
386
|
+
/** `scan_once` with `StopIteration` already turned into the decoder's own message. */
|
|
387
|
+
function scanValue(s, idx) {
|
|
388
|
+
try {
|
|
389
|
+
return scanOnce(s, idx);
|
|
390
|
+
}
|
|
391
|
+
catch (e) {
|
|
392
|
+
if (e instanceof StopScan)
|
|
393
|
+
throw new PyJSONDecodeError('Expecting value', s, e.at);
|
|
394
|
+
throw e;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/** `JSONObject`, entered just past the `{`. */
|
|
398
|
+
function scanObject(s, start) {
|
|
399
|
+
const pairs = new Map();
|
|
400
|
+
let end = start;
|
|
401
|
+
let nextchar = s.slice(end, end + 1);
|
|
402
|
+
if (nextchar !== '"') {
|
|
403
|
+
if (isWs(nextchar)) {
|
|
404
|
+
end = skipWs(s, end);
|
|
405
|
+
nextchar = s.slice(end, end + 1);
|
|
406
|
+
}
|
|
407
|
+
if (nextchar === '}')
|
|
408
|
+
return [{ t: 'dict', v: pairs }, end + 1];
|
|
409
|
+
if (nextchar !== '"') {
|
|
410
|
+
throw new PyJSONDecodeError('Expecting property name enclosed in double quotes', s, end);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
end += 1;
|
|
414
|
+
for (;;) {
|
|
415
|
+
const [key, afterKey] = scanString(s, end);
|
|
416
|
+
end = afterKey;
|
|
417
|
+
if (s.slice(end, end + 1) !== ':') {
|
|
418
|
+
end = skipWs(s, end);
|
|
419
|
+
if (s.slice(end, end + 1) !== ':') {
|
|
420
|
+
throw new PyJSONDecodeError("Expecting ':' delimiter", s, end);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
end += 1;
|
|
424
|
+
end = skipWs(s, end);
|
|
425
|
+
const [value, afterValue] = scanValue(s, end);
|
|
426
|
+
end = afterValue;
|
|
427
|
+
// `dict(pairs)`: a repeated key keeps its FIRST position and its LAST value, which is
|
|
428
|
+
// what `Map.set` does too.
|
|
429
|
+
pairs.set(key, value);
|
|
430
|
+
let sep = end < s.length ? s[end] : '';
|
|
431
|
+
if (isWs(sep)) {
|
|
432
|
+
end = skipWs(s, end + 1);
|
|
433
|
+
sep = end < s.length ? s[end] : '';
|
|
434
|
+
}
|
|
435
|
+
end += 1;
|
|
436
|
+
if (sep === '}')
|
|
437
|
+
break;
|
|
438
|
+
if (sep !== ',')
|
|
439
|
+
throw new PyJSONDecodeError("Expecting ',' delimiter", s, end - 1);
|
|
440
|
+
end = skipWs(s, end);
|
|
441
|
+
const quote = s.slice(end, end + 1);
|
|
442
|
+
end += 1;
|
|
443
|
+
if (quote !== '"') {
|
|
444
|
+
throw new PyJSONDecodeError('Expecting property name enclosed in double quotes', s, end - 1);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return [{ t: 'dict', v: pairs }, end];
|
|
448
|
+
}
|
|
449
|
+
/** `JSONArray`, entered just past the `[`. */
|
|
450
|
+
function scanArray(s, start) {
|
|
451
|
+
const values = [];
|
|
452
|
+
let end = start;
|
|
453
|
+
let nextchar = s.slice(end, end + 1);
|
|
454
|
+
if (isWs(nextchar)) {
|
|
455
|
+
end = skipWs(s, end + 1);
|
|
456
|
+
nextchar = s.slice(end, end + 1);
|
|
457
|
+
}
|
|
458
|
+
if (nextchar === ']')
|
|
459
|
+
return [{ t: 'list', v: values }, end + 1];
|
|
460
|
+
for (;;) {
|
|
461
|
+
const [value, afterValue] = scanValue(s, end);
|
|
462
|
+
end = afterValue;
|
|
463
|
+
values.push(value);
|
|
464
|
+
let sep = s.slice(end, end + 1);
|
|
465
|
+
if (isWs(sep)) {
|
|
466
|
+
end = skipWs(s, end + 1);
|
|
467
|
+
sep = s.slice(end, end + 1);
|
|
468
|
+
}
|
|
469
|
+
end += 1;
|
|
470
|
+
if (sep === ']')
|
|
471
|
+
break;
|
|
472
|
+
if (sep !== ',')
|
|
473
|
+
throw new PyJSONDecodeError("Expecting ',' delimiter", s, end - 1);
|
|
474
|
+
end = skipWs(s, end);
|
|
475
|
+
}
|
|
476
|
+
return [{ t: 'list', v: values }, end];
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* `JSONDecoder().raw_decode(s, idx)` — decode ONE value and return it with the offset just
|
|
480
|
+
* past it. Trailing text is not an error here; that is the whole reason `extract_json` uses
|
|
481
|
+
* this and not `json.loads`. Note there is no leading-whitespace skip: `raw_decode` has
|
|
482
|
+
* none either, and `extract_json` always enters on a `{` or a `[`.
|
|
483
|
+
*/
|
|
484
|
+
export function rawDecode(s, idx = 0) {
|
|
485
|
+
try {
|
|
486
|
+
const [value, end] = scanOnce(s, idx);
|
|
487
|
+
// Python's `end` counts CODEPOINTS. Converting here rather than threading a codepoint
|
|
488
|
+
// index through the scanner keeps the hot path on UTF-16 slices, which is what the
|
|
489
|
+
// regexes want, and pays one linear pass only on success.
|
|
490
|
+
return [value, [...s.slice(0, end)].length];
|
|
491
|
+
}
|
|
492
|
+
catch (e) {
|
|
493
|
+
if (e instanceof StopScan)
|
|
494
|
+
throw new PyJSONDecodeError('Expecting value', s, e.at);
|
|
495
|
+
throw e;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* `json.loads`, for the places that want the whole document and nothing after it.
|
|
500
|
+
*
|
|
501
|
+
* THE BOM CHECK IS `loads`'s OWN, and it is not in `raw_decode`. `json/__init__.py` tests
|
|
502
|
+
* `s.startswith('\ufeff')` before it hands anything to a decoder and refuses with its own
|
|
503
|
+
* sentence; `JSONDecoder().raw_decode` — which is what `contract.extract_json` calls — has
|
|
504
|
+
* no such test and would report `Expecting value` instead. The two are kept apart here for
|
|
505
|
+
* the same reason. Reachable: `shiftwork._read_checkpoint` is `json.loads(read_text(...))`,
|
|
506
|
+
* and `read_text` KEEPS the U+FEFF (`utf-8-sig` is the codec that strips it), so a
|
|
507
|
+
* BOM-prefixed checkpoint is refused by the reference and must be refused here. N8 measured
|
|
508
|
+
* this port parsing one and then WRITING to it.
|
|
509
|
+
*/
|
|
510
|
+
export function parseJson(s) {
|
|
511
|
+
if (s.startsWith('\ufeff')) {
|
|
512
|
+
throw new PyJSONDecodeError('Unexpected UTF-8 BOM (decode using utf-8-sig)', s, 0);
|
|
513
|
+
}
|
|
514
|
+
const start = skipWs(s, 0);
|
|
515
|
+
const [value, end] = scanValueTop(s, start);
|
|
516
|
+
const rest = skipWs(s, end);
|
|
517
|
+
if (rest !== s.length)
|
|
518
|
+
throw new PyJSONDecodeError('Extra data', s, rest);
|
|
519
|
+
return value;
|
|
520
|
+
}
|
|
521
|
+
/** `raw_decode` without the codepoint conversion, for callers that slice with the offset. */
|
|
522
|
+
function scanValueTop(s, idx) {
|
|
523
|
+
return scanValue(s, idx);
|
|
524
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type PyValue } from './pyjson.js';
|
|
2
|
+
export declare class PyJsonSchemaUnsupported extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* `jsonschema.exceptions.ValidationError`, narrowed to the fields the relevance heuristic
|
|
7
|
+
* and `schema_error` read.
|
|
8
|
+
*
|
|
9
|
+
* `path` is the RELATIVE path, which is what `relevance` sorts on, and `parent` is set only
|
|
10
|
+
* for the context errors an `anyOf`/`oneOf` carries. `absolutePath` walks the chain, which
|
|
11
|
+
* is what `schema_error` interpolates. Keeping the two apart is not pedantry: `best_match`
|
|
12
|
+
* ranks context errors by their relative paths and then `schema_error` prints an absolute
|
|
13
|
+
* one, and a port that conflated them would name the wrong field inside every `anyOf`.
|
|
14
|
+
*/
|
|
15
|
+
export interface PyValidationError {
|
|
16
|
+
message: string;
|
|
17
|
+
validator: string | null;
|
|
18
|
+
instance: PyValue;
|
|
19
|
+
/** The (sub)schema the keyword lives in. `null` stands for a `false` schema. */
|
|
20
|
+
schema: PyValue | boolean;
|
|
21
|
+
path: (string | number)[];
|
|
22
|
+
context: PyValidationError[];
|
|
23
|
+
parent: PyValidationError | null;
|
|
24
|
+
}
|
|
25
|
+
export declare function absolutePath(error: PyValidationError): (string | number)[];
|
|
26
|
+
/** `jsonschema._utils.equal`: JSON equality that refuses `True == 1`. */
|
|
27
|
+
export declare function pyEqual(a: PyValue, b: PyValue): boolean;
|
|
28
|
+
declare function iterErrors(instance: PyValue, schema: PyValue | boolean): PyValidationError[];
|
|
29
|
+
/**
|
|
30
|
+
* `jsonschema.exceptions.best_match`.
|
|
31
|
+
*
|
|
32
|
+
* THE CHOICE IS THE PRODUCT, not just the formatting. One invalid document usually produces
|
|
33
|
+
* several errors and Python raises exactly one of them, so a port that reports the first
|
|
34
|
+
* error it finds emits a correct-looking sentence about the wrong field. Two rules do the
|
|
35
|
+
* work: `max` prefers the SHALLOWEST error (`-len(path)`) and returns the FIRST maximum on a
|
|
36
|
+
* tie; and once a winner is found, the loop descends into its `context` — the sub-errors an
|
|
37
|
+
* `anyOf`/`oneOf` carries — unless the two most relevant children tie, in which case the
|
|
38
|
+
* parent stands.
|
|
39
|
+
*/
|
|
40
|
+
export declare function bestMatch(errors: PyValidationError[]): PyValidationError | null;
|
|
41
|
+
export declare function checkSchema(schema: PyValue | boolean, where?: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* `jsonschema.validate(instance, schema)` reduced to what `contract.schema_error` needs:
|
|
44
|
+
* the single error `best_match` chooses, or `null`.
|
|
45
|
+
*
|
|
46
|
+
* `check_schema` runs FIRST, as it does in the reference. See `checkSchema` for what that
|
|
47
|
+
* means here and for the five wrong answers it replaced.
|
|
48
|
+
*/
|
|
49
|
+
export declare function validate(instance: PyValue, schema: PyValue): PyValidationError | null;
|
|
50
|
+
export { iterErrors };
|