json-schema-engine 0.0.2__tar.gz → 0.0.3__tar.gz

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 (88) hide show
  1. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/CHANGELOG.md +31 -1
  2. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/PKG-INFO +56 -41
  3. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/README.md +55 -40
  4. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/pyproject.toml +1 -1
  5. json_schema_engine-0.0.3/src/json_schema_engine/compiler/__init__.py +250 -0
  6. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/emit.py +77 -0
  7. json_schema_engine-0.0.3/src/json_schema_engine/compiler/plan.py +780 -0
  8. json_schema_engine-0.0.3/src/json_schema_engine/compiler/runtime.py +227 -0
  9. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/runtime_compile.py +9 -1
  10. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/serialize/__init__.py +114 -8
  11. json_schema_engine-0.0.3/src/json_schema_engine/compiler/serialize/body.py +1116 -0
  12. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/serialize/context.py +77 -2
  13. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/serialize/units.py +49 -9
  14. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/standalone.py +4 -0
  15. json_schema_engine-0.0.3/src/json_schema_engine/core/channel_ops.py +152 -0
  16. json_schema_engine-0.0.3/src/json_schema_engine/core/coverage.py +58 -0
  17. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/dialect.py +9 -1
  18. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/engine.py +84 -58
  19. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/evaluator.py +34 -16
  20. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/json_model.py +7 -2
  21. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/applicator.py +11 -4
  22. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/applicator_array.py +64 -19
  23. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/applicator_object.py +50 -14
  24. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/content.py +8 -4
  25. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/core.py +58 -6
  26. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/format.py +7 -2
  27. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/legacy.py +37 -7
  28. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/unevaluated.py +93 -40
  29. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/validation.py +21 -7
  30. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/lowering.py +243 -18
  31. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/registry.py +70 -0
  32. json_schema_engine-0.0.2/src/json_schema_engine/compiler/__init__.py +0 -113
  33. json_schema_engine-0.0.2/src/json_schema_engine/compiler/plan.py +0 -415
  34. json_schema_engine-0.0.2/src/json_schema_engine/compiler/runtime.py +0 -114
  35. json_schema_engine-0.0.2/src/json_schema_engine/compiler/serialize/body.py +0 -524
  36. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/.gitignore +0 -0
  37. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/LICENSE +0 -0
  38. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/errors.py +0 -0
  39. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/py.typed +0 -0
  40. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/__init__.py +0 -0
  41. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/channel.py +0 -0
  42. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/cursor.py +0 -0
  43. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/errors.py +0 -0
  44. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/formats.py +0 -0
  45. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/__init__.py +0 -0
  46. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/_ids.py +0 -0
  47. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/dialect2019.py +0 -0
  48. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/dialect2020.py +0 -0
  49. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/dialect7.py +0 -0
  50. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/dialects.py +0 -0
  51. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/meta_data.py +0 -0
  52. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/loader.py +0 -0
  53. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2019-09/applicator.json +0 -0
  54. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2019-09/content.json +0 -0
  55. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2019-09/core.json +0 -0
  56. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2019-09/format.json +0 -0
  57. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2019-09/meta-data.json +0 -0
  58. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2019-09/schema.json +0 -0
  59. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2019-09/validation.json +0 -0
  60. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/applicator.json +0 -0
  61. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/content.json +0 -0
  62. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/core.json +0 -0
  63. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/format-annotation.json +0 -0
  64. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/format-assertion.json +0 -0
  65. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/meta-data.json +0 -0
  66. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/schema.json +0 -0
  67. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/unevaluated.json +0 -0
  68. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/validation.json +0 -0
  69. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/__init__.py +0 -0
  70. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/draft-06/schema.json +0 -0
  71. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/draft-07/schema.json +0 -0
  72. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/output.py +0 -0
  73. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/positions.py +0 -0
  74. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/py.typed +0 -0
  75. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/records.py +0 -0
  76. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/ref.py +0 -0
  77. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/regex.py +0 -0
  78. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/result.py +0 -0
  79. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/uri.py +0 -0
  80. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/__init__.py +0 -0
  81. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/_abnf.py +0 -0
  82. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/datetime_.py +0 -0
  83. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/idna_.py +0 -0
  84. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/misc.py +0 -0
  85. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/net.py +0 -0
  86. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/pointer.py +0 -0
  87. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/py.typed +0 -0
  88. {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/uri.py +0 -0
@@ -5,7 +5,37 @@ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
5
5
  versions follow [SemVer](https://semver.org/) with the 0.x caveat that
6
6
  minor versions may change public API.
7
7
 
8
- ## [0.0.2] - unreleased
8
+ ## [0.0.3] - 2026-09-21
9
+
10
+ ### Added
11
+
12
+ - `compile_evaluator`: compiles a registered root schema into an evaluator
13
+ serving every output format the interpreter does (errors, annotations,
14
+ dropped records, the application trace), not only the verdict `flag`
15
+ level.
16
+ - Plan-time resolution of `$dynamicRef`/`$recursiveRef`: a reference site
17
+ whose target is the same on every path that can reach it compiles as an
18
+ ordinary static edge instead of falling back to the interpreter;
19
+ `explain_compilation` reports such sites through `resolved_dynamic_sites`.
20
+ - Runtime coverage tracking: an `unevaluated*` consumer whose evaluated
21
+ coverage depends on runtime branching (an `anyOf`/`oneOf` alternative, an
22
+ `if`'s condition) compiles directly instead of islanding, folding a
23
+ runtime coverage channel instead of a static licence.
24
+ - Public `json_schema_engine.core.lowering`: the compiler lowering IR a
25
+ custom keyword's `lower()` is built from, previously private to the
26
+ engine's own keyword modules.
27
+
28
+ ### Changed
29
+
30
+ - `first_duplicate_pair` returns a list.
31
+ - Compiled error messages now match the interpreter's for `oneOf`,
32
+ `contains`, `uniqueItems`, and `type`.
33
+ - A dialect refusing unknown keywords no longer compiles those units
34
+ silently: an unknown keyword under such a dialect now falls back to the
35
+ interpreter like any other unlowerable case, rather than being planned
36
+ as if the keyword were absent.
37
+
38
+ ## [0.0.2] - 2026-09-21
9
39
 
10
40
  The first functional release. Everything below is new relative to the
11
41
  0.0.1 name reservation.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: json-schema-engine
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: A spec-complete, annotation-first JSON Schema engine for Python: interpreter, compiler, formats, every standard output format.
5
5
  Project-URL: Homepage, https://github.com/handrews/py-json-schema-engine
6
6
  Project-URL: Repository, https://github.com/handrews/py-json-schema-engine
@@ -39,20 +39,16 @@ relevance model.
39
39
 
40
40
  Produced by Henry Andrews via Claude Code.
41
41
 
42
- **Status: `0.0.2` is the first functional release.**
42
+ **Status: `0.0.3` is fully compliant except for draft-04 support.**
43
+
44
+ See [CHANGELOG.md](CHANGELOG.md) for the current release's contents.
45
+ [DESIGN.md](DESIGN.md) is the design contract and carries the milestone
46
+ status.
43
47
 
44
48
  All `0.0.x` releases will have AI-written documentation. Version
45
49
  `0.1.0` will indicate that the documentation has been audited and revised
46
50
  by a human.
47
51
 
48
- As of `0.0.2`, this package carries the
49
- interpreter core for 2020-12, 2019-09, draft-07, and draft-06 with every
50
- standard output format, the compiler tier (a flag validator and standalone
51
- modules, M6–M7), and the `json_schema_engine.formats` package — green on
52
- every official test-suite file for those drafts through both tiers, on the
53
- official output-tests, and on Bowtie. [DESIGN.md](DESIGN.md) is the design
54
- contract and carries the milestone status.
55
-
56
52
  The regular-expression translator lives in its own package,
57
53
  [`ecma-regex`](packages/ecma-regex/README.md) (`0.1.0`): ECMA-262 patterns
58
54
  for Python, with JavaScript semantics, no dependency on this engine.
@@ -93,12 +89,14 @@ one. Each keeps its own semantics, so a draft-07 `$ref` ignores its
93
89
  siblings while a 2019-09 one does not.
94
90
 
95
91
  Every standard output format is available by name: `flag` (the default),
96
- `basic`, `detailed`, and `verbose` from the IETF draft-03 output spec, and
97
- `list` and `hierarchical` from the machines-oriented proposal. Annotations
98
- are a separate control (`annotations=True`, or an `AnnotationSelection`),
99
- `verbose=True` asks `list`/`hierarchical` for the verbose level with
100
- irrelevant records marked as dropped, and `trace=True` adds the application
101
- tree with error indexes into `result.errors`.
92
+ `basic`, `detailed`, and `verbose` from the
93
+ [IETF working group draft-03](https://www.ietf.org/archive/id/draft-ietf-jsonschema-json-schema-03.html)
94
+ output sections, and `list` and `hierarchical` from the
95
+ [machines-oriented proposal](https://github.com/json-schema-org/json-schema-spec/blob/main/specs/output/jsonschema-validation-output-machines.md).
96
+ Annotations are a separate control (`annotations=True`, or an
97
+ `AnnotationSelection`), `verbose=True` asks `list`/`hierarchical` for the
98
+ verbose level with irrelevant records marked as dropped, and `trace=True`
99
+ adds the application tree with error indexes into `result.errors`.
102
100
 
103
101
  ```python
104
102
  result = engine.evaluate(uri, {"name": 3}, output="hierarchical")
@@ -129,9 +127,9 @@ attach a `source` location to every error and annotation, and
129
127
  ## Compile
130
128
 
131
129
  The compiler tier turns a registered schema into a Python function. It is
132
- not a second implementation: any subschema it cannot emit (a `$dynamicRef`,
133
- an `unevaluated*` whose coverage is only known at runtime, an in-place
134
- cycle) calls back into the interpreter, so a compiled validator is exactly
130
+ not a second implementation: any subschema it cannot emit (a `$dynamicRef`
131
+ whose target differs by path, an in-place cycle) calls back into the
132
+ interpreter, so a compiled validator is exactly
135
133
  as correct as `Engine.evaluate` and never less complete. Tier choice is a
136
134
  performance decision, not a semantic one.
137
135
 
@@ -154,9 +152,25 @@ only the standard library and this package's pure helpers; it refuses,
154
152
  with `StandaloneUnsupportedError`, a schema that would need the
155
153
  interpreter at evaluation time. Compiled code assumes plain data as
156
154
  `json.loads` produces it (`dict`, `list`, `str`, `int`, `float`, `bool`,
157
- `None`); subclasses of those types belong to the interpreter. Errors,
158
- annotations, and the output formats are interpreter features today;
159
- compiled output beyond the verdict is a later milestone.
155
+ `None`); subclasses of those types belong to the interpreter.
156
+
157
+ `compile_evaluator` compiles a schema into an evaluator serving every
158
+ output format the interpreter does — errors, annotations, dropped
159
+ records, and the application trace, not only the verdict. The annotation
160
+ selection is fixed at compile time; every other control (`output`,
161
+ `error_params`, `verbose`, `trace`, `positions`) is chosen per call, same
162
+ as `Engine.evaluate`.
163
+
164
+ ```python
165
+ from json_schema_engine.compiler import compile_evaluator
166
+
167
+ evaluator = compile_evaluator(engine, uri, annotations=True)
168
+ compiled_result = evaluator.evaluate({}, output="list", error_params=True)
169
+ interpreted_result = engine.evaluate(
170
+ uri, {}, output="list", error_params=True, annotations=True
171
+ )
172
+ assert compiled_result.errors == interpreted_result.errors
173
+ ```
160
174
 
161
175
  ## Formats
162
176
 
@@ -258,10 +272,6 @@ large arrays of distinct values do not incur quadratic cost, while genuine
258
272
  duplicates — including numbers equal across `int`/`float` and objects that
259
273
  differ only in member order — are still reported.
260
274
 
261
- **No prototype hazard.** Python dicts have no prototype chain, so there is
262
- nothing for a hostile property name to pollute: `__proto__`, `constructor`,
263
- and similar reserved-looking names evaluate as ordinary properties.
264
-
265
275
  ## Development
266
276
 
267
277
  ```sh
@@ -290,21 +300,26 @@ uv run python scripts/bowtie_check.py
290
300
  uv run python scripts/bench.py --budget-ms 250 --filter user
291
301
  ```
292
302
 
293
- `scripts/bench.py` times the compiler tier's flag and standalone artifacts
294
- against the interpreter and two competitors (fastjsonschema, jsonschema)
295
- over seven corpora in `packages/bench`: three small hand-authored schemas,
296
- the official OpenAPI 3.1 schema against a real document, a generated
297
- API-payload corpus, and two 2000-record corpora. It is report-only (it
298
- enforces no performance threshold) and, per the IP policy below, runs the
299
- competitors only, never reading or porting their source. `--filter` takes
300
- a regex over corpus/subject/partition names; omit `--out` to skip writing
301
- JSON; `--compare BEFORE AFTER` prints a before/after comparison of two
302
- results files. The committed run lives at
303
- `packages/bench/results/results.json` (`--budget-ms 250`). The interpreter
304
- is the reference semantics, so ratios are informational, not a
305
- compatibility claim. [`packages/bench/README.md`](packages/bench/README.md)
306
- covers corpus provenance and licensing, methodology, and the recorded
307
- exclusions.
303
+ `scripts/bench.py` times every jse tier (the interpreter, the compiler's
304
+ flag validator and evaluator, and the standalone artifact) against two
305
+ competitors (fastjsonschema, jsonschema) over seven corpora in
306
+ `packages/bench`: three small hand-authored schemas, the official OpenAPI
307
+ 3.1 schema against a real document, a generated API-payload corpus, and
308
+ two 2000-record corpora. Every jse tier is timed against every corpus,
309
+ `jse standalone` against `oas-document` included, since its `$dynamicRef`
310
+ sites resolve at plan time and leave no interpreted unit. Two subjects
311
+ measure the record-producing tier against the verdict-only tiers:
312
+ `jse interpreter list` and `jse compiled evaluator (list)`, both timing
313
+ `output="list"`. The bench is report-only (it enforces no performance
314
+ threshold) and, per the IP policy below, runs the competitors only, never
315
+ reading or porting their source. `--filter` takes a regex over
316
+ corpus/subject/partition names; omit `--out` to skip writing JSON;
317
+ `--compare BEFORE AFTER` prints a before/after comparison of two results
318
+ files. The committed run lives at `packages/bench/results/results.json`
319
+ (`--budget-ms 250`). The interpreter is the reference semantics, so ratios
320
+ are informational, not a compatibility claim.
321
+ [`packages/bench/README.md`](packages/bench/README.md) covers corpus
322
+ provenance and licensing, methodology, and the recorded exclusions.
308
323
 
309
324
  ## IP policy
310
325
 
@@ -12,20 +12,16 @@ relevance model.
12
12
 
13
13
  Produced by Henry Andrews via Claude Code.
14
14
 
15
- **Status: `0.0.2` is the first functional release.**
15
+ **Status: `0.0.3` is fully compliant except for draft-04 support.**
16
+
17
+ See [CHANGELOG.md](CHANGELOG.md) for the current release's contents.
18
+ [DESIGN.md](DESIGN.md) is the design contract and carries the milestone
19
+ status.
16
20
 
17
21
  All `0.0.x` releases will have AI-written documentation. Version
18
22
  `0.1.0` will indicate that the documentation has been audited and revised
19
23
  by a human.
20
24
 
21
- As of `0.0.2`, this package carries the
22
- interpreter core for 2020-12, 2019-09, draft-07, and draft-06 with every
23
- standard output format, the compiler tier (a flag validator and standalone
24
- modules, M6–M7), and the `json_schema_engine.formats` package — green on
25
- every official test-suite file for those drafts through both tiers, on the
26
- official output-tests, and on Bowtie. [DESIGN.md](DESIGN.md) is the design
27
- contract and carries the milestone status.
28
-
29
25
  The regular-expression translator lives in its own package,
30
26
  [`ecma-regex`](packages/ecma-regex/README.md) (`0.1.0`): ECMA-262 patterns
31
27
  for Python, with JavaScript semantics, no dependency on this engine.
@@ -66,12 +62,14 @@ one. Each keeps its own semantics, so a draft-07 `$ref` ignores its
66
62
  siblings while a 2019-09 one does not.
67
63
 
68
64
  Every standard output format is available by name: `flag` (the default),
69
- `basic`, `detailed`, and `verbose` from the IETF draft-03 output spec, and
70
- `list` and `hierarchical` from the machines-oriented proposal. Annotations
71
- are a separate control (`annotations=True`, or an `AnnotationSelection`),
72
- `verbose=True` asks `list`/`hierarchical` for the verbose level with
73
- irrelevant records marked as dropped, and `trace=True` adds the application
74
- tree with error indexes into `result.errors`.
65
+ `basic`, `detailed`, and `verbose` from the
66
+ [IETF working group draft-03](https://www.ietf.org/archive/id/draft-ietf-jsonschema-json-schema-03.html)
67
+ output sections, and `list` and `hierarchical` from the
68
+ [machines-oriented proposal](https://github.com/json-schema-org/json-schema-spec/blob/main/specs/output/jsonschema-validation-output-machines.md).
69
+ Annotations are a separate control (`annotations=True`, or an
70
+ `AnnotationSelection`), `verbose=True` asks `list`/`hierarchical` for the
71
+ verbose level with irrelevant records marked as dropped, and `trace=True`
72
+ adds the application tree with error indexes into `result.errors`.
75
73
 
76
74
  ```python
77
75
  result = engine.evaluate(uri, {"name": 3}, output="hierarchical")
@@ -102,9 +100,9 @@ attach a `source` location to every error and annotation, and
102
100
  ## Compile
103
101
 
104
102
  The compiler tier turns a registered schema into a Python function. It is
105
- not a second implementation: any subschema it cannot emit (a `$dynamicRef`,
106
- an `unevaluated*` whose coverage is only known at runtime, an in-place
107
- cycle) calls back into the interpreter, so a compiled validator is exactly
103
+ not a second implementation: any subschema it cannot emit (a `$dynamicRef`
104
+ whose target differs by path, an in-place cycle) calls back into the
105
+ interpreter, so a compiled validator is exactly
108
106
  as correct as `Engine.evaluate` and never less complete. Tier choice is a
109
107
  performance decision, not a semantic one.
110
108
 
@@ -127,9 +125,25 @@ only the standard library and this package's pure helpers; it refuses,
127
125
  with `StandaloneUnsupportedError`, a schema that would need the
128
126
  interpreter at evaluation time. Compiled code assumes plain data as
129
127
  `json.loads` produces it (`dict`, `list`, `str`, `int`, `float`, `bool`,
130
- `None`); subclasses of those types belong to the interpreter. Errors,
131
- annotations, and the output formats are interpreter features today;
132
- compiled output beyond the verdict is a later milestone.
128
+ `None`); subclasses of those types belong to the interpreter.
129
+
130
+ `compile_evaluator` compiles a schema into an evaluator serving every
131
+ output format the interpreter does — errors, annotations, dropped
132
+ records, and the application trace, not only the verdict. The annotation
133
+ selection is fixed at compile time; every other control (`output`,
134
+ `error_params`, `verbose`, `trace`, `positions`) is chosen per call, same
135
+ as `Engine.evaluate`.
136
+
137
+ ```python
138
+ from json_schema_engine.compiler import compile_evaluator
139
+
140
+ evaluator = compile_evaluator(engine, uri, annotations=True)
141
+ compiled_result = evaluator.evaluate({}, output="list", error_params=True)
142
+ interpreted_result = engine.evaluate(
143
+ uri, {}, output="list", error_params=True, annotations=True
144
+ )
145
+ assert compiled_result.errors == interpreted_result.errors
146
+ ```
133
147
 
134
148
  ## Formats
135
149
 
@@ -231,10 +245,6 @@ large arrays of distinct values do not incur quadratic cost, while genuine
231
245
  duplicates — including numbers equal across `int`/`float` and objects that
232
246
  differ only in member order — are still reported.
233
247
 
234
- **No prototype hazard.** Python dicts have no prototype chain, so there is
235
- nothing for a hostile property name to pollute: `__proto__`, `constructor`,
236
- and similar reserved-looking names evaluate as ordinary properties.
237
-
238
248
  ## Development
239
249
 
240
250
  ```sh
@@ -263,21 +273,26 @@ uv run python scripts/bowtie_check.py
263
273
  uv run python scripts/bench.py --budget-ms 250 --filter user
264
274
  ```
265
275
 
266
- `scripts/bench.py` times the compiler tier's flag and standalone artifacts
267
- against the interpreter and two competitors (fastjsonschema, jsonschema)
268
- over seven corpora in `packages/bench`: three small hand-authored schemas,
269
- the official OpenAPI 3.1 schema against a real document, a generated
270
- API-payload corpus, and two 2000-record corpora. It is report-only (it
271
- enforces no performance threshold) and, per the IP policy below, runs the
272
- competitors only, never reading or porting their source. `--filter` takes
273
- a regex over corpus/subject/partition names; omit `--out` to skip writing
274
- JSON; `--compare BEFORE AFTER` prints a before/after comparison of two
275
- results files. The committed run lives at
276
- `packages/bench/results/results.json` (`--budget-ms 250`). The interpreter
277
- is the reference semantics, so ratios are informational, not a
278
- compatibility claim. [`packages/bench/README.md`](packages/bench/README.md)
279
- covers corpus provenance and licensing, methodology, and the recorded
280
- exclusions.
276
+ `scripts/bench.py` times every jse tier (the interpreter, the compiler's
277
+ flag validator and evaluator, and the standalone artifact) against two
278
+ competitors (fastjsonschema, jsonschema) over seven corpora in
279
+ `packages/bench`: three small hand-authored schemas, the official OpenAPI
280
+ 3.1 schema against a real document, a generated API-payload corpus, and
281
+ two 2000-record corpora. Every jse tier is timed against every corpus,
282
+ `jse standalone` against `oas-document` included, since its `$dynamicRef`
283
+ sites resolve at plan time and leave no interpreted unit. Two subjects
284
+ measure the record-producing tier against the verdict-only tiers:
285
+ `jse interpreter list` and `jse compiled evaluator (list)`, both timing
286
+ `output="list"`. The bench is report-only (it enforces no performance
287
+ threshold) and, per the IP policy below, runs the competitors only, never
288
+ reading or porting their source. `--filter` takes a regex over
289
+ corpus/subject/partition names; omit `--out` to skip writing JSON;
290
+ `--compare BEFORE AFTER` prints a before/after comparison of two results
291
+ files. The committed run lives at `packages/bench/results/results.json`
292
+ (`--budget-ms 250`). The interpreter is the reference semantics, so ratios
293
+ are informational, not a compatibility claim.
294
+ [`packages/bench/README.md`](packages/bench/README.md) covers corpus
295
+ provenance and licensing, methodology, and the recorded exclusions.
281
296
 
282
297
  ## IP policy
283
298
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "json-schema-engine"
7
- version = "0.0.2"
7
+ version = "0.0.3"
8
8
  description = "A spec-complete, annotation-first JSON Schema engine for Python: interpreter, compiler, formats, every standard output format."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -0,0 +1,250 @@
1
+ # The compiler tier's public surface (DESIGN.md D1, D9, D10; M6): compile a
2
+ # registered schema into a flag validator, or emit it as a standalone
3
+ # module. The interpreter is the reference semantics; any subschema the
4
+ # compiler cannot emit trampolines back into it, so an artifact is exactly
5
+ # as correct as `Engine.evaluate` and never less complete — tier choice is
6
+ # a performance decision, not a semantic one.
7
+ #
8
+ # An artifact binds a snapshot of the schema and dialect registries taken
9
+ # at compile time: compile after registration is complete.
10
+ #
11
+ # Dependency direction: imports the planner, serializer, runtime, and
12
+ # instantiation modules, plus core's engine façade. `json_schema_engine.core`
13
+ # never imports this package (P5).
14
+
15
+ import ast
16
+ from collections.abc import Callable
17
+ from dataclasses import dataclass
18
+
19
+ from json_schema_engine.compiler import emit as e
20
+ from json_schema_engine.compiler.errors import (
21
+ FormatTableError,
22
+ StandaloneUnsupportedError,
23
+ )
24
+ from json_schema_engine.compiler.plan import (
25
+ CompilationExplanation,
26
+ CompilationPlan,
27
+ DynamicResolution,
28
+ FallbackCause,
29
+ PlannedApplication,
30
+ PlannedUnit,
31
+ ResolvedDynamicSite,
32
+ build_plan,
33
+ build_plan_over,
34
+ explain_compilation,
35
+ )
36
+ from json_schema_engine.compiler.runtime import make_namespace, make_runtime
37
+ from json_schema_engine.compiler.runtime_compile import instantiate, instantiate_entry
38
+ from json_schema_engine.compiler.serialize import Flags, assemble, serialize_plan
39
+ from json_schema_engine.compiler.standalone import emit_standalone
40
+ from json_schema_engine.core.channel_ops import cut_annotations
41
+ from json_schema_engine.core.engine import Engine, assemble_evaluation
42
+ from json_schema_engine.core.evaluator import EvalState
43
+ from json_schema_engine.core.json_model import JsonValue
44
+ from json_schema_engine.core.output import AnnotationsOption, make_record_predicate
45
+ from json_schema_engine.core.result import OutputFormat, Result, resolve_output_demand
46
+
47
+ __all__ = [
48
+ "CompilationExplanation",
49
+ "CompilationPlan",
50
+ "CompiledEvaluator",
51
+ "CompiledValidator",
52
+ "DynamicResolution",
53
+ "FallbackCause",
54
+ "FormatTableError",
55
+ "PlannedApplication",
56
+ "PlannedUnit",
57
+ "ResolvedDynamicSite",
58
+ "StandaloneUnsupportedError",
59
+ "build_plan",
60
+ "compile_evaluator",
61
+ "compile_validator",
62
+ "emit_standalone",
63
+ "explain_compilation",
64
+ ]
65
+
66
+
67
+ @dataclass(frozen=True, slots=True)
68
+ class CompiledValidator:
69
+ """A compiled flag validator: `validate(instance) -> bool`, the plan it
70
+ was built from, the emitted module, and its source (for diagnostics
71
+ and goldens; `ast.unparse` of `module`)."""
72
+
73
+ validate: Callable[[JsonValue], bool]
74
+ plan: CompilationPlan
75
+ module: ast.Module
76
+ source: str
77
+
78
+
79
+ def compile_validator(
80
+ engine: Engine,
81
+ schema_uri: str,
82
+ *,
83
+ max_depth: int | None = None,
84
+ conservative: bool = False,
85
+ ) -> CompiledValidator:
86
+ """Compile a registered root schema into a verdict-only validator.
87
+
88
+ `max_depth` defaults to the engine's; `conservative` turns the
89
+ emitter's optimizations off (no inlining, no set specialization) — the
90
+ differential fuzzer referees both configurations.
91
+ """
92
+ registry = engine.schemas.snapshot()
93
+ plan = build_plan_over(registry, schema_uri)
94
+ flags = Flags(inline=not conservative, specialize_sets=not conservative)
95
+ serialized = serialize_plan(plan, registry, flags)
96
+ prologue = [
97
+ e.assign(
98
+ name,
99
+ e.subscript(e.attr(e.load(e.RUNTIME), "re"), e.const(source)),
100
+ )
101
+ for name, source in serialized.regexes
102
+ ]
103
+ prologue.extend(
104
+ e.assign(
105
+ name,
106
+ e.subscript(e.attr(e.load(e.RUNTIME), "formats"), e.const(format_name)),
107
+ )
108
+ for name, format_name in serialized.formats
109
+ )
110
+ module = assemble(prologue, serialized)
111
+ budget = engine.max_depth if max_depth is None else max_depth
112
+ runtime = make_runtime(
113
+ registry,
114
+ engine.regex_cache,
115
+ plan.patterns,
116
+ budget,
117
+ formats=plan.formats,
118
+ format_table=engine.formats,
119
+ coverage_ids=plan.coverage_ids,
120
+ )
121
+ validate = instantiate(
122
+ module, make_namespace(runtime, [t.ref for t in plan.targets])
123
+ )
124
+ return CompiledValidator(validate, plan, module, ast.unparse(module))
125
+
126
+
127
+ def _record_nothing(keyword_name: str, vocabulary_uri: str | None) -> bool:
128
+ return False
129
+
130
+
131
+ @dataclass(frozen=True, slots=True)
132
+ class CompiledEvaluator:
133
+ """A compiled evaluator (M9): `evaluate(instance, *, output, error_params,
134
+ verbose, trace, positions) -> Result`, the same result `Engine.evaluate`
135
+ returns for that demand. The annotation selection is fixed at compile
136
+ time (ruled-out annotations are never recorded); every other output
137
+ control is chosen per evaluation. Records and the application trace
138
+ are written to a core `EvalState` the artifact shares with any
139
+ interpreted island, and rendered by the interpreter's own renderers.
140
+ """
141
+
142
+ evaluate: Callable[..., Result]
143
+ plan: CompilationPlan
144
+ module: ast.Module
145
+ source: str
146
+ annotations: AnnotationsOption
147
+
148
+
149
+ def compile_evaluator(
150
+ engine: Engine,
151
+ schema_uri: str,
152
+ *,
153
+ annotations: AnnotationsOption = False,
154
+ max_depth: int | None = None,
155
+ conservative: bool = False,
156
+ ) -> CompiledEvaluator:
157
+ """Compile a registered root schema into an evaluator serving every
158
+ output format but the verdict-only `flag`.
159
+
160
+ Every consumer is tracked at runtime (a static coverage models only
161
+ the parent-success path, and an evaluator continues past a failed
162
+ sibling), nothing inlines, and every branch runs (§4 rule 7), so the
163
+ errors, annotations, dropped records, and trace equal the interpreter's.
164
+ """
165
+ registry = engine.schemas.snapshot()
166
+ plan = build_plan_over(registry, schema_uri, track_all=True)
167
+ flags = Flags(inline=False, specialize_sets=not conservative, mode="evaluator")
168
+ record = make_record_predicate(annotations)
169
+ serialized = serialize_plan(plan, registry, flags, record=record)
170
+ prologue = [
171
+ e.assign(
172
+ name,
173
+ e.subscript(e.attr(e.load(e.RUNTIME), "re"), e.const(source)),
174
+ )
175
+ for name, source in serialized.regexes
176
+ ]
177
+ prologue.extend(
178
+ e.assign(
179
+ name,
180
+ e.subscript(e.attr(e.load(e.RUNTIME), "formats"), e.const(format_name)),
181
+ )
182
+ for name, format_name in serialized.formats
183
+ )
184
+ prologue.extend(
185
+ e.assign(name, e.subscript(e.load(e.SITES), e.const(index)))
186
+ for index, (name, _) in enumerate(serialized.sites)
187
+ )
188
+ module = assemble(prologue, serialized)
189
+ budget = engine.max_depth if max_depth is None else max_depth
190
+ runtime = make_runtime(
191
+ registry,
192
+ engine.regex_cache,
193
+ plan.patterns,
194
+ budget,
195
+ formats=plan.formats,
196
+ format_table=engine.formats,
197
+ coverage_ids=plan.coverage_ids,
198
+ )
199
+ entry = instantiate_entry(
200
+ module,
201
+ make_namespace(
202
+ runtime,
203
+ [t.ref for t in plan.targets],
204
+ [site for _, site in serialized.sites],
205
+ ),
206
+ e.EVALUATE,
207
+ )
208
+ root_location = registry.root_ref(schema_uri).location
209
+ compile_regex = engine.regex_cache.compile
210
+ should_record = record if record is not None else _record_nothing
211
+
212
+ def evaluate(
213
+ instance: JsonValue,
214
+ *,
215
+ output: str | OutputFormat = OutputFormat.LIST,
216
+ error_params: bool = False,
217
+ verbose: bool | None = None,
218
+ trace: bool = False,
219
+ positions: bool = False,
220
+ ) -> Result:
221
+ demand = resolve_output_demand(
222
+ output=output,
223
+ annotations=annotations,
224
+ error_params=error_params,
225
+ verbose=verbose,
226
+ trace=trace,
227
+ positions=positions,
228
+ )
229
+ state = EvalState(
230
+ registry,
231
+ compile_regex,
232
+ should_record=should_record,
233
+ max_depth=budget,
234
+ tracing=demand.tracing,
235
+ )
236
+ valid = bool(entry(instance, state))
237
+ if not valid:
238
+ # The root application's records never merge (rule 3).
239
+ cut_annotations(state, 0)
240
+ return assemble_evaluation(
241
+ state,
242
+ valid,
243
+ demand,
244
+ annotations,
245
+ root_location,
246
+ trace,
247
+ locate=engine.locate if positions else None,
248
+ )
249
+
250
+ return CompiledEvaluator(evaluate, plan, module, ast.unparse(module), annotations)