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.
Files changed (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/assets/contracts/default.yaml +90 -0
  4. package/assets/evals/devteam/manifest.yaml +351 -0
  5. package/assets/evals/devteam/repo/HISTORY.md +18 -0
  6. package/assets/evals/devteam/repo/README.md +12 -0
  7. package/assets/evals/devteam/repo/docs/architecture.md +17 -0
  8. package/assets/evals/devteam/repo/docs/runbook.md +10 -0
  9. package/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
  10. package/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
  11. package/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
  12. package/assets/evals/devteam/repo/src/ledger/config.py +35 -0
  13. package/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
  14. package/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
  15. package/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
  16. package/assets/evals/devteam/repo/src/ledger/report.py +9 -0
  17. package/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
  18. package/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
  19. package/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
  20. package/assets/evals/devteam/repo/tests/test_posting.py +13 -0
  21. package/assets/evals/devteam/repo/tests/test_settle.py +9 -0
  22. package/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
  23. package/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
  24. package/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
  25. package/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
  26. package/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
  27. package/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
  28. package/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
  29. package/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
  30. package/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
  31. package/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
  32. package/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
  33. package/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
  34. package/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
  35. package/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
  36. package/assets/evals/document/tasks/doc-small-137.yaml +37 -0
  37. package/assets/evals/document/tasks/doc-small-261.yaml +37 -0
  38. package/assets/evals/document/tasks/doc-small-388.yaml +37 -0
  39. package/assets/evals/fixtures/.gitkeep +0 -0
  40. package/assets/evals/fixtures/catalog.json +6 -0
  41. package/assets/evals/perturbations/task-completion.yaml +576 -0
  42. package/assets/evals/tasks/.gitkeep +0 -0
  43. package/assets/evals/tasks/extract-contact.yaml +14 -0
  44. package/assets/evals/tasks/extract-invoice.yaml +14 -0
  45. package/assets/evals/tasks/extract-order.yaml +15 -0
  46. package/assets/evals/tasks/extract-schedule.yaml +14 -0
  47. package/assets/evals/tasks/extract-versions.yaml +17 -0
  48. package/assets/evals/tasks/nav-prod-port.yaml +84 -0
  49. package/assets/evals/tasks/nav-release-bundle.yaml +87 -0
  50. package/assets/evals/tasks/recall-audit-retention.yaml +17 -0
  51. package/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
  52. package/assets/evals/tasks/recall-db-port.yaml +17 -0
  53. package/assets/evals/tasks/recall-deploy.yaml +13 -0
  54. package/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
  55. package/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
  56. package/assets/evals/tasks/recall-oncall.yaml +13 -0
  57. package/assets/evals/tasks/recall-org-quota.yaml +18 -0
  58. package/assets/evals/tasks/recall-owner.yaml +13 -0
  59. package/assets/evals/tasks/shop-basket-total.yaml +10 -0
  60. package/assets/evals/tasks/shop-cheapest.yaml +9 -0
  61. package/assets/evals/tasks/shop-compare.yaml +9 -0
  62. package/assets/evals/tasks/shop-gadget-value.yaml +9 -0
  63. package/assets/evals/tasks/shop-stock-total.yaml +9 -0
  64. package/assets/evals/tasks/shop-total.yaml +9 -0
  65. package/assets/profiles/default.yaml +31 -0
  66. package/assets/profiles/patient.yaml +31 -0
  67. package/assets/rubrics/.gitkeep +0 -0
  68. package/assets/rubrics/code-quality.yaml +20 -0
  69. package/assets/rubrics/grounded-completion.yaml +37 -0
  70. package/assets/rubrics/task-completion.yaml +28 -0
  71. package/assets/schemas/shiftwork-checkpoint.json +188 -0
  72. package/assets/skills/.gitkeep +0 -0
  73. package/assets/skills/file-graph.md +7 -0
  74. package/assets/skills/memory.md +35 -0
  75. package/assets/tools/.gitkeep +0 -0
  76. package/assets/tools/bantamkit_read.json +48 -0
  77. package/assets/tools/bantamkit_status.json +25 -0
  78. package/assets/tools/build_identity.json +17 -0
  79. package/assets/tools/document_list.json +12 -0
  80. package/assets/tools/document_read.json +31 -0
  81. package/assets/tools/file_graph.json +12 -0
  82. package/assets/tools/memory_compact.json +31 -0
  83. package/assets/tools/memory_recall.json +38 -0
  84. package/assets/tools/memory_save.json +61 -0
  85. package/assets/tools/shiftwork_clock_in.json +25 -0
  86. package/assets/tools/shiftwork_clock_out.json +60 -0
  87. package/assets/tools/shiftwork_status.json +25 -0
  88. package/assets/tools/skill_audit.json +70 -0
  89. package/assets/tools/validate_json.json +31 -0
  90. package/dist/assets.d.ts +48 -0
  91. package/dist/assets.js +109 -0
  92. package/dist/charsets.d.ts +17 -0
  93. package/dist/charsets.js +580 -0
  94. package/dist/cli.d.ts +12 -0
  95. package/dist/cli.js +280 -0
  96. package/dist/contract.d.ts +145 -0
  97. package/dist/contract.js +608 -0
  98. package/dist/docread.d.ts +357 -0
  99. package/dist/docread.js +3315 -0
  100. package/dist/errors.d.ts +11 -0
  101. package/dist/errors.js +14 -0
  102. package/dist/eventlog.d.ts +137 -0
  103. package/dist/eventlog.js +294 -0
  104. package/dist/htmlentities.d.ts +2 -0
  105. package/dist/htmlentities.js +16 -0
  106. package/dist/index.d.ts +23 -0
  107. package/dist/index.js +13 -0
  108. package/dist/mcp/identity.d.ts +25 -0
  109. package/dist/mcp/identity.js +275 -0
  110. package/dist/mcp/pyargs.d.ts +80 -0
  111. package/dist/mcp/pyargs.js +362 -0
  112. package/dist/mcp/sdkjson.d.ts +56 -0
  113. package/dist/mcp/sdkjson.js +91 -0
  114. package/dist/mcp/server.d.ts +45 -0
  115. package/dist/mcp/server.js +740 -0
  116. package/dist/mcp/status.d.ts +210 -0
  117. package/dist/mcp/status.js +357 -0
  118. package/dist/mcp/transport.d.ts +68 -0
  119. package/dist/mcp/transport.js +111 -0
  120. package/dist/mcpreport.d.ts +214 -0
  121. package/dist/mcpreport.js +758 -0
  122. package/dist/memory/cli.d.ts +2 -0
  123. package/dist/memory/cli.js +396 -0
  124. package/dist/memory/component.d.ts +256 -0
  125. package/dist/memory/component.js +497 -0
  126. package/dist/memory/factfile.d.ts +120 -0
  127. package/dist/memory/factfile.js +406 -0
  128. package/dist/memory/layers.d.ts +92 -0
  129. package/dist/memory/layers.js +410 -0
  130. package/dist/memory/pyfs.d.ts +521 -0
  131. package/dist/memory/pyfs.js +1608 -0
  132. package/dist/memory/pyyaml.d.ts +155 -0
  133. package/dist/memory/pyyaml.js +868 -0
  134. package/dist/memory/store.d.ts +478 -0
  135. package/dist/memory/store.js +1034 -0
  136. package/dist/pyargparse.d.ts +141 -0
  137. package/dist/pyargparse.js +813 -0
  138. package/dist/pyjson.d.ts +110 -0
  139. package/dist/pyjson.js +524 -0
  140. package/dist/pyjsonschema.d.ts +50 -0
  141. package/dist/pyjsonschema.js +951 -0
  142. package/dist/shiftwork.d.ts +60 -0
  143. package/dist/shiftwork.js +324 -0
  144. package/dist/skillaudit.d.ts +172 -0
  145. package/dist/skillaudit.js +914 -0
  146. package/dist/statusline.d.ts +106 -0
  147. package/dist/statusline.js +281 -0
  148. package/package.json +45 -0
@@ -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 };