dynamic-systems-testing-framework 1.0.1__py3-none-any.whl
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.
- dstf/__init__.py +41 -0
- dstf/__main__.py +10 -0
- dstf/authoring/__init__.py +13 -0
- dstf/authoring/builtin/__init__.py +32 -0
- dstf/authoring/builtin/accept.py +205 -0
- dstf/authoring/builtin/edit_criteria.py +479 -0
- dstf/authoring/builtin/edit_family.py +228 -0
- dstf/authoring/builtin/edit_on_plot.py +1354 -0
- dstf/authoring/builtin/edit_tree.py +1399 -0
- dstf/authoring/builtin/enqueue.py +144 -0
- dstf/authoring/capabilities.py +463 -0
- dstf/authoring/errors.py +49 -0
- dstf/authoring/family_rows.py +526 -0
- dstf/authoring/forms.py +631 -0
- dstf/authoring/layout.py +104 -0
- dstf/authoring/live_rows.py +337 -0
- dstf/authoring/live_runs.py +385 -0
- dstf/authoring/needle.py +127 -0
- dstf/authoring/picker_rows.py +92 -0
- dstf/authoring/queue_rows.py +354 -0
- dstf/authoring/session.py +3016 -0
- dstf/authoring/shell/__init__.py +9 -0
- dstf/authoring/shell/actions.py +702 -0
- dstf/authoring/shell/app.py +53 -0
- dstf/authoring/shell/drawer_controls.py +190 -0
- dstf/authoring/shell/family_fold.py +468 -0
- dstf/authoring/shell/figure_hook.py +249 -0
- dstf/authoring/shell/grips.js +1000 -0
- dstf/authoring/shell/header.py +783 -0
- dstf/authoring/shell/live_view.py +93 -0
- dstf/authoring/shell/picker.py +267 -0
- dstf/authoring/shell/placing.py +122 -0
- dstf/authoring/shell/plot_card.py +1171 -0
- dstf/authoring/shell/queue_panel.py +433 -0
- dstf/authoring/shell/rail.py +593 -0
- dstf/authoring/shell/shell.py +528 -0
- dstf/authoring/shell/stats_card.py +501 -0
- dstf/authoring/shell/strip.py +204 -0
- dstf/authoring/shell/style.py +721 -0
- dstf/authoring/shell/suite.py +277 -0
- dstf/authoring/shell/suite_grid.js +414 -0
- dstf/authoring/shell/suite_grid.py +373 -0
- dstf/authoring/shell/tab_theme.py +110 -0
- dstf/authoring/shell/tabstate.py +146 -0
- dstf/authoring/shell/test_list.py +179 -0
- dstf/authoring/shell/test_page.py +1404 -0
- dstf/authoring/shell/tree_card.py +1171 -0
- dstf/authoring/shell/variables_drawer.py +589 -0
- dstf/authoring/shell/widgets.py +188 -0
- dstf/authoring/situation.py +287 -0
- dstf/authoring/spec_document.py +215 -0
- dstf/authoring/spec_preview.py +83 -0
- dstf/authoring/suite_rows.py +99 -0
- dstf/authoring/tree_move.py +516 -0
- dstf/authoring/tree_pointer.py +33 -0
- dstf/authoring/tree_text.py +61 -0
- dstf/authoring/typed_number.js +64 -0
- dstf/authoring/typed_number.py +76 -0
- dstf/authoring/unscorable.py +141 -0
- dstf/authoring/variable_rows.py +216 -0
- dstf/backends/__init__.py +283 -0
- dstf/backends/base.py +1117 -0
- dstf/backends/common/__init__.py +1 -0
- dstf/backends/common/json_driver.py +484 -0
- dstf/backends/common/live_mat.py +369 -0
- dstf/backends/common/mat_reader.py +632 -0
- dstf/backends/common/proc_kill.py +238 -0
- dstf/backends/common/proc_output.py +20 -0
- dstf/backends/common/result_files.py +109 -0
- dstf/backends/common/result_variables.py +329 -0
- dstf/backends/common/statistics.py +39 -0
- dstf/backends/cross_backend.py +193 -0
- dstf/backends/dymola/__init__.py +5 -0
- dstf/backends/dymola/backend.py +776 -0
- dstf/backends/dymola/interface_loader.py +241 -0
- dstf/backends/dymola/log_parser.py +396 -0
- dstf/backends/dymola/persistent_backend.py +949 -0
- dstf/backends/fmpy/__init__.py +11 -0
- dstf/backends/fmpy/backend.py +443 -0
- dstf/backends/julia/__init__.py +27 -0
- dstf/backends/julia/backend.py +156 -0
- dstf/backends/julia/persistent_backend.py +541 -0
- dstf/backends/julia/run_persistent.jl +167 -0
- dstf/backends/julia/run_test.jl +146 -0
- dstf/backends/openmodelica/__init__.py +21 -0
- dstf/backends/openmodelica/backend.py +368 -0
- dstf/backends/openmodelica/init_xml.py +76 -0
- dstf/backends/openmodelica/log_parser.py +119 -0
- dstf/backends/openmodelica/mos_generator.py +228 -0
- dstf/backends/openmodelica/persistent_backend.py +1045 -0
- dstf/backends/openmodelica/session_loader.py +63 -0
- dstf/backends/persistent.py +1811 -0
- dstf/backends/pool.py +602 -0
- dstf/backends/progress.py +395 -0
- dstf/backends/python/__init__.py +11 -0
- dstf/backends/python/backend.py +141 -0
- dstf/backends/python/run_test.py +141 -0
- dstf/backends/run_metadata.py +103 -0
- dstf/backends/types.py +310 -0
- dstf/cli/__init__.py +12 -0
- dstf/cli/author_cmd.py +61 -0
- dstf/cli/baseline_cmds.py +341 -0
- dstf/cli/common.py +231 -0
- dstf/cli/diag_cmds.py +350 -0
- dstf/cli/main.py +561 -0
- dstf/cli/review.py +398 -0
- dstf/cli/run_cmds.py +589 -0
- dstf/cli/spec_cmds.py +123 -0
- dstf/comparison/__init__.py +13 -0
- dstf/comparison/algorithms.py +2127 -0
- dstf/comparison/builtins.py +291 -0
- dstf/comparison/comparator.py +1189 -0
- dstf/comparison/geometry.py +164 -0
- dstf/comparison/handles.py +1194 -0
- dstf/comparison/metric_tree.py +332 -0
- dstf/comparison/mode_schema.py +176 -0
- dstf/comparison/modes.py +788 -0
- dstf/comparison/registry.py +230 -0
- dstf/comparison/seeds.py +281 -0
- dstf/comparison/sides.py +158 -0
- dstf/comparison/snap.py +62 -0
- dstf/comparison/strict_keys.py +246 -0
- dstf/comparison/summaries.py +354 -0
- dstf/comparison/tree_eval.py +868 -0
- dstf/comparison/tree_spec.py +678 -0
- dstf/comparison/types.py +194 -0
- dstf/comparison/validator.py +232 -0
- dstf/config.py +815 -0
- dstf/discovery/__init__.py +0 -0
- dstf/discovery/json_recognizer.py +702 -0
- dstf/discovery/library_state.py +248 -0
- dstf/discovery/mo_parser.py +794 -0
- dstf/discovery/patch_apply.py +308 -0
- dstf/discovery/patch_envelope.py +63 -0
- dstf/discovery/plots.py +318 -0
- dstf/discovery/recognizer.py +103 -0
- dstf/discovery/scan_cache.py +154 -0
- dstf/discovery/spec_parser.py +538 -0
- dstf/discovery/spec_shape.py +122 -0
- dstf/discovery/spec_update.py +493 -0
- dstf/discovery/test_registry.py +488 -0
- dstf/discovery/unit_tests.py +95 -0
- dstf/figures/__init__.py +14 -0
- dstf/figures/assemble.py +48 -0
- dstf/figures/decimate.py +150 -0
- dstf/figures/families.py +289 -0
- dstf/figures/palette.py +170 -0
- dstf/figures/plotly.py +1413 -0
- dstf/figures/view.py +233 -0
- dstf/pipeline.py +435 -0
- dstf/plugins.py +104 -0
- dstf/reporting/__init__.py +0 -0
- dstf/reporting/console_report.py +187 -0
- dstf/reporting/context_builder.py +979 -0
- dstf/reporting/dashboard_render.py +534 -0
- dstf/reporting/junit_report.py +192 -0
- dstf/reporting/overlay_loader.py +708 -0
- dstf/reporting/report_suite.py +1059 -0
- dstf/reporting/rows.py +458 -0
- dstf/reporting/schema_export.py +413 -0
- dstf/reporting/stats_rows.py +321 -0
- dstf/reporting/status.py +388 -0
- dstf/reporting/templates/axes.js +61 -0
- dstf/reporting/templates/dashboard.html +284 -0
- dstf/reporting/templates/dashboard.js +604 -0
- dstf/reporting/templates/filters.js +100 -0
- dstf/reporting/templates/interactive.html +420 -0
- dstf/reporting/templates/report.js +116 -0
- dstf/reporting/templates/vendor/plotly.min.js +8 -0
- dstf/reporting/theme.py +324 -0
- dstf/reporting/tree_rows.py +73 -0
- dstf/reporting/verdicts.py +124 -0
- dstf/storage/__init__.py +0 -0
- dstf/storage/accept.py +216 -0
- dstf/storage/baseline.py +43 -0
- dstf/storage/batch_manifest.py +204 -0
- dstf/storage/ref_index.py +295 -0
- dstf/storage/reference_store.py +661 -0
- dstf/storage/roles.py +576 -0
- dstf/storage/run_stats.py +85 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""JSON-Schema export for ``test_spec.json`` (PHASE_6_PLAN 6.4.5).
|
|
2
|
+
|
|
3
|
+
Derives a JSON-Schema document from:
|
|
4
|
+
* every registered metric's Config dataclass (reusing the 6.1.1
|
|
5
|
+
introspection — one definition per mode);
|
|
6
|
+
* the :class:`LeafSpec` / :class:`CombinatorSpec` grammar (the
|
|
7
|
+
MetricTree shape);
|
|
8
|
+
* the per-test entry shape (simulation block, comparison block,
|
|
9
|
+
variables, metrics tree, window, baselines).
|
|
10
|
+
|
|
11
|
+
Emits JSON-Schema draft 2020-12 shape. Thin layer — no validation
|
|
12
|
+
logic here (that lives in ``comparison/validator.py`` + parse-time).
|
|
13
|
+
Downstream uses: IDE autocomplete, LLM authoring, tool handoff.
|
|
14
|
+
|
|
15
|
+
The schema is as strict as the parser: wherever a key nobody reads is a
|
|
16
|
+
spec error (``comparison/strict_keys.py``) the object says
|
|
17
|
+
``additionalProperties: false``, and its property names come from the
|
|
18
|
+
same data the parser checks against — a metric's config dataclass, the
|
|
19
|
+
tree parser's key tables, ``discovery/spec_shape.py`` for the entry.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import copy
|
|
25
|
+
import dataclasses
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from ..comparison.mode_schema import derive_schema
|
|
29
|
+
from ..comparison.modes import QUANTITY_KEYS
|
|
30
|
+
from ..comparison.registry import (
|
|
31
|
+
combinator_names,
|
|
32
|
+
metric_names,
|
|
33
|
+
metrics,
|
|
34
|
+
unnamed_metrics,
|
|
35
|
+
)
|
|
36
|
+
from ..comparison.strict_keys import EVERY_METRIC, declares_quantity, row_fields
|
|
37
|
+
from ..comparison.tree_spec import COMBINATOR_KIND_KEYS
|
|
38
|
+
from ..discovery import spec_shape
|
|
39
|
+
|
|
40
|
+
_JSON_SCHEMA_URI = "https://json-schema.org/draft/2020-12/schema"
|
|
41
|
+
|
|
42
|
+
#: A tolerance with two sides: one number (both sides), or exactly
|
|
43
|
+
#: ``lower`` and ``upper`` — what :func:`~dstf.comparison.sides.parse_sides`
|
|
44
|
+
#: takes. A pair with one side, or with a third key, is neither.
|
|
45
|
+
_SIDES = [
|
|
46
|
+
{"type": "number"},
|
|
47
|
+
{
|
|
48
|
+
"type": "object",
|
|
49
|
+
"properties": {"lower": {"type": "number"}, "upper": {"type": "number"}},
|
|
50
|
+
"required": ["lower", "upper"],
|
|
51
|
+
"additionalProperties": False,
|
|
52
|
+
},
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _field_to_schema(field) -> dict:
|
|
57
|
+
"""Translate a 6.1.1 FieldSpec into a JSON-Schema property dict."""
|
|
58
|
+
node: dict[str, Any] = {}
|
|
59
|
+
if field.type == "float":
|
|
60
|
+
node["type"] = ["number", "null"] if field.optional else "number"
|
|
61
|
+
elif field.type == "int":
|
|
62
|
+
node["type"] = ["integer", "null"] if field.optional else "integer"
|
|
63
|
+
elif field.type == "bool":
|
|
64
|
+
node["type"] = ["boolean", "null"] if field.optional else "boolean"
|
|
65
|
+
elif field.type == "str":
|
|
66
|
+
node["type"] = ["string", "null"] if field.optional else "string"
|
|
67
|
+
elif field.type == "enum":
|
|
68
|
+
base: dict[str, Any] = {"type": "string", "enum": list(field.choices or [])}
|
|
69
|
+
if field.optional:
|
|
70
|
+
node["oneOf"] = [base, {"type": "null"}]
|
|
71
|
+
else:
|
|
72
|
+
node.update(base)
|
|
73
|
+
elif field.type == "sides":
|
|
74
|
+
node["oneOf"] = copy.deepcopy(_SIDES)
|
|
75
|
+
elif field.type == "passthrough":
|
|
76
|
+
# Complex / unknown — allow any JSON shape. Documentation hints
|
|
77
|
+
# via description if we have one.
|
|
78
|
+
node = {}
|
|
79
|
+
if field.default is not None:
|
|
80
|
+
node["default"] = field.default
|
|
81
|
+
if field.label:
|
|
82
|
+
node["title"] = field.label
|
|
83
|
+
if field.help:
|
|
84
|
+
node["description"] = field.help
|
|
85
|
+
return node
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _modes() -> list[tuple[str, type]]:
|
|
89
|
+
"""Registered metrics in registration order, as ``(name, config_cls)``.
|
|
90
|
+
|
|
91
|
+
Single source for both the ``$defs`` emission and the leaf's if/then
|
|
92
|
+
wiring (finding 72) — and the reason a registered metric shows up in
|
|
93
|
+
the exported schema without editing this module.
|
|
94
|
+
"""
|
|
95
|
+
return [(desc.name, desc.config_cls) for desc in metrics()]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _mode_def_name(mode_name: str) -> str:
|
|
99
|
+
return f"mode_{mode_name.replace('-', '_')}"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _quantity_metric_names() -> list[str]:
|
|
103
|
+
"""Registered metrics whose ``quantity`` may stand in for ``variable``.
|
|
104
|
+
|
|
105
|
+
Mirrors the parser rule in :func:`~dstf.comparison.tree_spec._parse_leaf`
|
|
106
|
+
— both read ``MetricDescriptor.accepts_quantity``, so the exported
|
|
107
|
+
grammar and the one the CLI enforces can't drift apart.
|
|
108
|
+
"""
|
|
109
|
+
return [desc.name for desc in metrics() if desc.accepts_quantity]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _leaf_identity_rule() -> dict:
|
|
113
|
+
"""The "how a leaf names its data" requirement, as JSON-Schema.
|
|
114
|
+
|
|
115
|
+
Every leaf needs ``metric``. It then needs either a ``variable`` (the
|
|
116
|
+
tracked series it scores) or — only for a metric that declares
|
|
117
|
+
``accepts_quantity`` — a string ``quantity`` naming a stored scalar.
|
|
118
|
+
"""
|
|
119
|
+
alternatives: list[dict] = [{"required": ["variable"]}]
|
|
120
|
+
quantity_metrics = _quantity_metric_names()
|
|
121
|
+
if quantity_metrics:
|
|
122
|
+
alternatives.append(
|
|
123
|
+
{
|
|
124
|
+
"properties": {
|
|
125
|
+
"metric": {"enum": quantity_metrics},
|
|
126
|
+
"quantity": {"type": "string", "minLength": 1},
|
|
127
|
+
},
|
|
128
|
+
"required": ["metric", "quantity"],
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
return {"anyOf": alternatives}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _leaf_common_properties() -> dict[str, Any]:
|
|
135
|
+
"""Leaf keys that co-reside with mode-specific knobs in the same object.
|
|
136
|
+
|
|
137
|
+
Shared between the leaf spec and every mode ``$def`` — the mode defs are
|
|
138
|
+
``$ref``'d from inside the leaf (if metric == <mode> then $ref), so they
|
|
139
|
+
describe the same flat object and must list these keys for editor/LLM
|
|
140
|
+
autocomplete (review 2026-07-06 finding 72).
|
|
141
|
+
"""
|
|
142
|
+
return {
|
|
143
|
+
"metric": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"enum": sorted(metric_names()),
|
|
146
|
+
"description": "The per-variable metric to score with.",
|
|
147
|
+
},
|
|
148
|
+
"variable": {
|
|
149
|
+
"type": "string",
|
|
150
|
+
"description": "Variable name as reported by the backend.",
|
|
151
|
+
},
|
|
152
|
+
"against": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"default": "primary",
|
|
155
|
+
"description": (
|
|
156
|
+
"Which baseline to score against — 'primary' or the name "
|
|
157
|
+
"of a registered soft_check. Companions are not scorable."
|
|
158
|
+
),
|
|
159
|
+
},
|
|
160
|
+
"window": {
|
|
161
|
+
"type": "object",
|
|
162
|
+
"description": "Scope the leaf to [start, end] (idea #46).",
|
|
163
|
+
"properties": {
|
|
164
|
+
"start": {"type": "number"},
|
|
165
|
+
"end": {"type": "number"},
|
|
166
|
+
},
|
|
167
|
+
"additionalProperties": False,
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _param_properties(config_cls: type, mode_name: str) -> dict[str, Any] | None:
|
|
173
|
+
"""One metric's params as schema properties: its config's fields — a
|
|
174
|
+
row table's rows closed to its ``columns`` — and the two params read
|
|
175
|
+
off every leaf (``strict_keys.EVERY_METRIC``). None for a metric whose
|
|
176
|
+
config is not a dataclass: nothing says what it reads, the parser
|
|
177
|
+
does not check it, and neither does the schema."""
|
|
178
|
+
if not dataclasses.is_dataclass(config_cls):
|
|
179
|
+
return None
|
|
180
|
+
props: dict[str, Any] = {
|
|
181
|
+
# A leaf's own tolerance, a number or a pair, where the metric's
|
|
182
|
+
# config does not type it itself; `quantity` of any shape.
|
|
183
|
+
"tolerance": {"oneOf": copy.deepcopy(_SIDES)},
|
|
184
|
+
"quantity": {},
|
|
185
|
+
}
|
|
186
|
+
assert set(props) == set(EVERY_METRIC)
|
|
187
|
+
for f in derive_schema(config_cls, mode=mode_name).fields:
|
|
188
|
+
props[f.name] = _field_to_schema(f)
|
|
189
|
+
if declares_quantity(mode_name):
|
|
190
|
+
# Read here: a path, or an object closed to the keys the parser
|
|
191
|
+
# takes inside one (`strict_keys`).
|
|
192
|
+
props["quantity"] = {
|
|
193
|
+
**props["quantity"],
|
|
194
|
+
"oneOf": [
|
|
195
|
+
{"type": "string"},
|
|
196
|
+
{
|
|
197
|
+
"type": "object",
|
|
198
|
+
"properties": {key: {} for key in QUANTITY_KEYS},
|
|
199
|
+
"additionalProperties": False,
|
|
200
|
+
},
|
|
201
|
+
{"type": "null"},
|
|
202
|
+
],
|
|
203
|
+
}
|
|
204
|
+
for name, columns in row_fields(mode_name).items():
|
|
205
|
+
props[name] = {
|
|
206
|
+
**props[name],
|
|
207
|
+
"type": ["array", "null"],
|
|
208
|
+
"items": {
|
|
209
|
+
"type": "object",
|
|
210
|
+
"properties": {column: {} for column in sorted(columns)},
|
|
211
|
+
"additionalProperties": False,
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
return props
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _mode_def(config_cls: type, mode_name: str) -> dict:
|
|
218
|
+
"""A leaf of one metric, whole: the leaf's own keys and the metric's
|
|
219
|
+
params, and nothing else — the parser refuses any other key (it was
|
|
220
|
+
lenient until a misspelled bound scored as no bound at all)."""
|
|
221
|
+
params = _param_properties(config_cls, mode_name)
|
|
222
|
+
node: dict[str, Any] = {
|
|
223
|
+
"type": "object",
|
|
224
|
+
"title": f"{mode_name} mode config",
|
|
225
|
+
"properties": {**_leaf_common_properties(), **(params or {})},
|
|
226
|
+
}
|
|
227
|
+
if params is not None:
|
|
228
|
+
node["additionalProperties"] = False
|
|
229
|
+
return node
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _override_def_name(mode_name: str) -> str:
|
|
233
|
+
return f"override_{mode_name.replace('-', '_')}"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _override_def(config_clss: list[tuple[str, type]], title: str) -> dict:
|
|
237
|
+
"""A flat ``comparison.variable_overrides`` entry scored by one of
|
|
238
|
+
``config_clss`` (more than one for an entry with no ``mode``): ``mode``
|
|
239
|
+
and those metrics' params, and nothing else."""
|
|
240
|
+
props: dict[str, Any] = {"mode": {"type": "string", "enum": sorted(metric_names())}}
|
|
241
|
+
closed = True
|
|
242
|
+
for name, config_cls in config_clss:
|
|
243
|
+
params = _param_properties(config_cls, name)
|
|
244
|
+
closed = closed and params is not None
|
|
245
|
+
props.update(params or {})
|
|
246
|
+
node: dict[str, Any] = {"type": "object", "title": title, "properties": props}
|
|
247
|
+
if closed:
|
|
248
|
+
node["additionalProperties"] = False
|
|
249
|
+
return node
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _override_defs(modes: list[tuple[str, type]]) -> dict[str, dict]:
|
|
253
|
+
"""``override`` — one entry of ``variable_overrides`` — and the def per
|
|
254
|
+
mode it selects by its ``mode`` key. With no ``mode`` the entry is
|
|
255
|
+
scored by the default metric, or by ``points`` under
|
|
256
|
+
``--default-points`` (``registry.unnamed_metrics``): it may carry
|
|
257
|
+
what either reads."""
|
|
258
|
+
by_name = dict(modes)
|
|
259
|
+
defs = {
|
|
260
|
+
_override_def_name(name): _override_def([(name, cls)], f"{name} override")
|
|
261
|
+
for name, cls in modes
|
|
262
|
+
}
|
|
263
|
+
defs["override_without_mode"] = _override_def(
|
|
264
|
+
[(name, by_name[name]) for name in unnamed_metrics() if name in by_name],
|
|
265
|
+
"override with no mode",
|
|
266
|
+
)
|
|
267
|
+
defs["override"] = {
|
|
268
|
+
"type": "object",
|
|
269
|
+
"title": "Flat per-variable override",
|
|
270
|
+
"allOf": [
|
|
271
|
+
{
|
|
272
|
+
"if": {"required": ["mode"]},
|
|
273
|
+
"else": {"$ref": "#/$defs/override_without_mode"},
|
|
274
|
+
},
|
|
275
|
+
*(
|
|
276
|
+
{
|
|
277
|
+
"if": {
|
|
278
|
+
"properties": {"mode": {"const": name}},
|
|
279
|
+
"required": ["mode"],
|
|
280
|
+
},
|
|
281
|
+
"then": {"$ref": f"#/$defs/{_override_def_name(name)}"},
|
|
282
|
+
}
|
|
283
|
+
for name, _ in modes
|
|
284
|
+
),
|
|
285
|
+
],
|
|
286
|
+
}
|
|
287
|
+
return defs
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _combinator_kind_rules() -> list[dict]:
|
|
291
|
+
"""Each key only some kinds read (``k``, ``weights``, ...) is forbidden
|
|
292
|
+
on every other kind — the parser's own table, so the two agree."""
|
|
293
|
+
keys = sorted({key for kind in COMBINATOR_KIND_KEYS.values() for key in kind})
|
|
294
|
+
return [
|
|
295
|
+
{
|
|
296
|
+
"if": {
|
|
297
|
+
"properties": {
|
|
298
|
+
"combinator": {
|
|
299
|
+
"enum": sorted(
|
|
300
|
+
kind
|
|
301
|
+
for kind, takes in COMBINATOR_KIND_KEYS.items()
|
|
302
|
+
if key in takes
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
"required": ["combinator"],
|
|
307
|
+
},
|
|
308
|
+
"else": {"properties": {key: False}},
|
|
309
|
+
}
|
|
310
|
+
for key in keys
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def build_schema() -> dict:
|
|
315
|
+
"""Build the top-level JSON-Schema document."""
|
|
316
|
+
modes = _modes()
|
|
317
|
+
mode_defs = {_mode_def_name(name): _mode_def(cls, name) for name, cls in modes}
|
|
318
|
+
|
|
319
|
+
leaf_spec = {
|
|
320
|
+
"type": "object",
|
|
321
|
+
"title": "Metric tree leaf",
|
|
322
|
+
# ``variable`` is not unconditionally required: a metric that
|
|
323
|
+
# declares ``accepts_quantity`` may name its data with a string
|
|
324
|
+
# ``quantity`` instead (see _leaf_identity_rule, folded into allOf
|
|
325
|
+
# below so it composes with the per-mode if/then clauses).
|
|
326
|
+
"required": ["metric"],
|
|
327
|
+
# Mode-specific knobs live inline alongside metric/variable per the
|
|
328
|
+
# historical override shape — e.g., a leaf with metric=tube may set
|
|
329
|
+
# tube_rel, tube_width_mode, etc. (typed via the allOf below).
|
|
330
|
+
"properties": _leaf_common_properties(),
|
|
331
|
+
# review 2026-07-06 finding 72: make the mode $defs real — the
|
|
332
|
+
# ``metric`` discriminator selects the matching mode definition, so
|
|
333
|
+
# validators type-check mode knobs and editors get autocomplete.
|
|
334
|
+
# Each of those defs is closed (``additionalProperties: false``):
|
|
335
|
+
# that is where a leaf's unknown key is refused.
|
|
336
|
+
"allOf": [
|
|
337
|
+
_leaf_identity_rule(),
|
|
338
|
+
*(
|
|
339
|
+
{
|
|
340
|
+
"if": {
|
|
341
|
+
"properties": {"metric": {"const": name}},
|
|
342
|
+
"required": ["metric"],
|
|
343
|
+
},
|
|
344
|
+
"then": {"$ref": f"#/$defs/{_mode_def_name(name)}"},
|
|
345
|
+
}
|
|
346
|
+
for name, _ in modes
|
|
347
|
+
),
|
|
348
|
+
],
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
combinator_spec = {
|
|
352
|
+
"type": "object",
|
|
353
|
+
"title": "Metric tree combinator",
|
|
354
|
+
"required": ["combinator", "children"],
|
|
355
|
+
"properties": {
|
|
356
|
+
"combinator": {
|
|
357
|
+
"type": "string",
|
|
358
|
+
"enum": sorted(combinator_names()),
|
|
359
|
+
},
|
|
360
|
+
"children": {
|
|
361
|
+
"type": "array",
|
|
362
|
+
"items": {"$ref": "#/$defs/tree_node"},
|
|
363
|
+
"minItems": 1,
|
|
364
|
+
},
|
|
365
|
+
"k": {
|
|
366
|
+
"type": "integer",
|
|
367
|
+
"minimum": 1,
|
|
368
|
+
"description": "Required when combinator = 'k-of-n'.",
|
|
369
|
+
},
|
|
370
|
+
"weights": {
|
|
371
|
+
"type": "array",
|
|
372
|
+
"items": {"type": "number"},
|
|
373
|
+
"description": "Weights for 'weighted' combinator; parallel to children.",
|
|
374
|
+
},
|
|
375
|
+
"threshold": {"type": "number"},
|
|
376
|
+
"direction": {"type": "string", "enum": ["less", "greater"]},
|
|
377
|
+
},
|
|
378
|
+
"additionalProperties": False,
|
|
379
|
+
"allOf": _combinator_kind_rules(),
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
test_entry = {
|
|
383
|
+
"type": "object",
|
|
384
|
+
"required": ["model"],
|
|
385
|
+
# The entry's keys, and its simulation and comparison blocks': the
|
|
386
|
+
# one table the spec parser checks an entry against.
|
|
387
|
+
"properties": copy.deepcopy(spec_shape.ENTRY),
|
|
388
|
+
"additionalProperties": False,
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
"$schema": _JSON_SCHEMA_URI,
|
|
393
|
+
"title": "DSTF test_spec.json",
|
|
394
|
+
"type": "object",
|
|
395
|
+
"required": ["tests"],
|
|
396
|
+
# The root's own keys — `tests`, and the `$schema` that points an
|
|
397
|
+
# editor here — and no other: the parser refuses one (`spec_shape`).
|
|
398
|
+
"properties": copy.deepcopy(spec_shape.ROOT),
|
|
399
|
+
"additionalProperties": False,
|
|
400
|
+
"$defs": {
|
|
401
|
+
"test_entry": test_entry,
|
|
402
|
+
"tree_node": {
|
|
403
|
+
"oneOf": [
|
|
404
|
+
{"$ref": "#/$defs/leaf"},
|
|
405
|
+
{"$ref": "#/$defs/combinator"},
|
|
406
|
+
]
|
|
407
|
+
},
|
|
408
|
+
"leaf": leaf_spec,
|
|
409
|
+
"combinator": combinator_spec,
|
|
410
|
+
**mode_defs,
|
|
411
|
+
**_override_defs(modes),
|
|
412
|
+
},
|
|
413
|
+
}
|