reg-schema 2.0.0__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.
- reg_schema-2.0.0/.gitignore +129 -0
- reg_schema-2.0.0/DESIGN.md +317 -0
- reg_schema-2.0.0/PKG-INFO +54 -0
- reg_schema-2.0.0/README.md +36 -0
- reg_schema-2.0.0/pyproject.toml +36 -0
- reg_schema-2.0.0/src/reg_schema/__init__.py +54 -0
- reg_schema-2.0.0/src/reg_schema/project_data.py +318 -0
- reg_schema-2.0.0/src/reg_schema/structural.py +1593 -0
- reg_schema-2.0.0/src/reg_schema/validation.py +73 -0
- reg_schema-2.0.0/test_corpus/README.md +105 -0
- reg_schema-2.0.0/test_corpus/binding_version_suffix_rejected/expected_ValidationResult.json +10 -0
- reg_schema-2.0.0/test_corpus/binding_version_suffix_rejected/input.json +19 -0
- reg_schema-2.0.0/test_corpus/composite_entity_key/expected_ValidationResult.json +3 -0
- reg_schema-2.0.0/test_corpus/composite_entity_key/input.json +56 -0
- reg_schema-2.0.0/test_corpus/invalid_period/expected_ValidationResult.json +10 -0
- reg_schema-2.0.0/test_corpus/invalid_period/input.json +20 -0
- reg_schema-2.0.0/test_corpus/invalid_root_array/expected_ValidationResult.json +10 -0
- reg_schema-2.0.0/test_corpus/invalid_root_array/input.json +1 -0
- reg_schema-2.0.0/test_corpus/invalid_window/expected_ValidationResult.json +10 -0
- reg_schema-2.0.0/test_corpus/invalid_window/input.json +22 -0
- reg_schema-2.0.0/test_corpus/load_test_200col/build.py +167 -0
- reg_schema-2.0.0/test_corpus/load_test_200col/expected_ValidationResult.json +3 -0
- reg_schema-2.0.0/test_corpus/load_test_200col/input.json +1240 -0
- reg_schema-2.0.0/test_corpus/minimal/expected_ValidationResult.json +3 -0
- reg_schema-2.0.0/test_corpus/minimal/input.json +20 -0
- reg_schema-2.0.0/test_corpus/unexpected_field_on_binding/expected_ValidationResult.json +10 -0
- reg_schema-2.0.0/test_corpus/unexpected_field_on_binding/input.json +21 -0
- reg_schema-2.0.0/test_corpus/unexpected_fields_on_project/expected_ValidationResult.json +16 -0
- reg_schema-2.0.0/test_corpus/unexpected_fields_on_project/input.json +9 -0
- reg_schema-2.0.0/test_corpus/with_panel/expected_ValidationResult.json +3 -0
- reg_schema-2.0.0/test_corpus/with_panel/input.json +54 -0
- reg_schema-2.0.0/test_corpus/with_period_range/expected_ValidationResult.json +3 -0
- reg_schema-2.0.0/test_corpus/with_period_range/input.json +21 -0
- reg_schema-2.0.0/test_corpus/with_study_window/expected_ValidationResult.json +3 -0
- reg_schema-2.0.0/test_corpus/with_study_window/input.json +22 -0
- reg_schema-2.0.0/tests/test_corpus.py +140 -0
- reg_schema-2.0.0/tests/test_project_data.py +360 -0
- reg_schema-2.0.0/tests/test_structural.py +1315 -0
- reg_schema-2.0.0/tests/test_validation.py +75 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
# Root-anchored: only the repo-root packaging dir, NOT the real
|
|
7
|
+
# `reg_monabundle/src/reg_monabundle/build/` source package (the bundle
|
|
8
|
+
# amalgamator). `__pycache__/` above still ignores byte-code anywhere.
|
|
9
|
+
/build/
|
|
10
|
+
|
|
11
|
+
# Environments
|
|
12
|
+
.venv/
|
|
13
|
+
.env
|
|
14
|
+
.envrc
|
|
15
|
+
|
|
16
|
+
# Tools
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
.mypy_cache/
|
|
20
|
+
.hypothesis/
|
|
21
|
+
node_modules/
|
|
22
|
+
|
|
23
|
+
# IDE
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
26
|
+
*.swp
|
|
27
|
+
|
|
28
|
+
# OS
|
|
29
|
+
.DS_Store
|
|
30
|
+
Thumbs.db
|
|
31
|
+
|
|
32
|
+
# Project
|
|
33
|
+
.tmp/
|
|
34
|
+
|
|
35
|
+
# Input data (SCB CSVs, Socialstyrelsen metadata, source PDFs — not committed).
|
|
36
|
+
# Listed per-subdirectory so the maintainer-curated classifications/ folder
|
|
37
|
+
# can stay tracked without an exclude/re-include dance (git doesn't traverse
|
|
38
|
+
# into a parent dir that's been ignored at the directory level).
|
|
39
|
+
reg_meta_build/input_data/*
|
|
40
|
+
!reg_meta_build/input_data/classifications/
|
|
41
|
+
# Thin curated providers (#422): each agency's hand-authored <provider>.toml IS
|
|
42
|
+
# the committed source delivery (no machine export exists), so its dir stays
|
|
43
|
+
# tracked. One exception line per curated agency — NOT a broad `*/` exception,
|
|
44
|
+
# which would start tracking the untracked SCB/Socialstyrelsen/swecov seed dirs.
|
|
45
|
+
!reg_meta_build/input_data/Folkhalsomyndigheten/
|
|
46
|
+
!reg_meta_build/input_data/Forsakringskassan/
|
|
47
|
+
!reg_meta_build/input_data/Lakemedelsverket/
|
|
48
|
+
!reg_meta_build/input_data/Pliktverket/
|
|
49
|
+
!reg_meta_build/input_data/Riksarkivet/
|
|
50
|
+
!reg_meta_build/input_data/UMU/
|
|
51
|
+
# Canonical-SCB curated content (#444) — committed TOML + value-set CSVs, not an
|
|
52
|
+
# agency dir but the same tracked-committed-source rule.
|
|
53
|
+
!reg_meta_build/input_data/scb_canonical/
|
|
54
|
+
# Under classifications/ only the normalized CSVs (+ manifest.json) are tracked.
|
|
55
|
+
# The raw SOS source workbooks the fetch script downloads are not — they
|
|
56
|
+
# regenerate from scripts/fetch_sos_classifications.py.
|
|
57
|
+
reg_meta_build/input_data/classifications/sos/*.xls
|
|
58
|
+
reg_meta_build/input_data/classifications/sos/*.xlsx
|
|
59
|
+
# landskoder.csv is fetched but deliberately not seeded (see
|
|
60
|
+
# reg_meta_build/CLASSIFICATIONS.md); a bare fetch run regenerates it —
|
|
61
|
+
# keep it out of status noise.
|
|
62
|
+
reg_meta_build/input_data/classifications/sos/landskoder.csv
|
|
63
|
+
|
|
64
|
+
# mock-data-wizard generated output
|
|
65
|
+
mock_data/
|
|
66
|
+
mock_output/
|
|
67
|
+
mdw_runner.py
|
|
68
|
+
mdw_step1_discovery.json
|
|
69
|
+
mdw_step2_config.json
|
|
70
|
+
mdw_step3_stats.json
|
|
71
|
+
extract_stats*.R
|
|
72
|
+
|
|
73
|
+
# mock-data-wizard runtime artifacts (transient — fcntl sidecar, run logs)
|
|
74
|
+
.mock_data_config.lock
|
|
75
|
+
mdw_log_*.txt
|
|
76
|
+
|
|
77
|
+
# Local test workspaces (real user data, not part of the toolkit)
|
|
78
|
+
/covid-education-immigrants-test/
|
|
79
|
+
|
|
80
|
+
# Personal exploration scripts (not part of the toolkit's curated scripts/)
|
|
81
|
+
scripts/sample_*.py
|
|
82
|
+
|
|
83
|
+
# MONA probe artefacts -- can contain workspace metadata (paths,
|
|
84
|
+
# hostnames, DSNs); inspect before sharing. The findings that matter
|
|
85
|
+
# live in mock_data_wizard/DESIGN.md.
|
|
86
|
+
mdw_probe_*.log
|
|
87
|
+
mdw_python_probe_*.log
|
|
88
|
+
mdw_py_probe_*.csv
|
|
89
|
+
mdw_upload_probe_*.txt
|
|
90
|
+
|
|
91
|
+
# reg_meta database (built from SCB exports, not committed)
|
|
92
|
+
*.db
|
|
93
|
+
|
|
94
|
+
# Build-generated auto-slug TOMLs (variable slugs). A `churning` provider (the
|
|
95
|
+
# #470 default) regenerates these from scratch each build — only the curated
|
|
96
|
+
# <provider>.toml, the freeze.toml state map, and .snapshot.json are committed.
|
|
97
|
+
# Covers the global dir and per-steward subdirs (fqid_slugs/swecov/).
|
|
98
|
+
# See reg_meta_build/DESIGN.md → Slug immutability.
|
|
99
|
+
# To pin a provider (curating/frozen) its auto.toml MUST be committed: either
|
|
100
|
+
# `git add -f reg_meta_build/fqid_slugs/<provider>.auto.toml`, or add a
|
|
101
|
+
# per-provider negation here: `!reg_meta_build/fqid_slugs/<provider>.auto.toml`.
|
|
102
|
+
reg_meta_build/fqid_slugs/**/*.auto.toml
|
|
103
|
+
# Pinned providers (#759, curating): the committed auto.toml IS the curating
|
|
104
|
+
# baseline, so un-ignore exactly these. Steward/churning auto.toml stay ignored.
|
|
105
|
+
!reg_meta_build/fqid_slugs/scb.auto.toml
|
|
106
|
+
!reg_meta_build/fqid_slugs/sos.auto.toml
|
|
107
|
+
!reg_meta_build/fqid_slugs/fk.auto.toml
|
|
108
|
+
!reg_meta_build/fqid_slugs/fohm.auto.toml
|
|
109
|
+
!reg_meta_build/fqid_slugs/lakemedelsverket.auto.toml
|
|
110
|
+
!reg_meta_build/fqid_slugs/pliktverket.auto.toml
|
|
111
|
+
!reg_meta_build/fqid_slugs/riksarkivet.auto.toml
|
|
112
|
+
!reg_meta_build/fqid_slugs/umu.auto.toml
|
|
113
|
+
|
|
114
|
+
# SCB source PDFs (binary, copyrighted, not committed)
|
|
115
|
+
*.pdf
|
|
116
|
+
|
|
117
|
+
# Marker raw output (regenerable from PDFs + parser)
|
|
118
|
+
reg_meta_build/docs/_raw/
|
|
119
|
+
|
|
120
|
+
# Archive (concluded investigations, ad-hoc scripts, internal notes)
|
|
121
|
+
archive/
|
|
122
|
+
*.har
|
|
123
|
+
|
|
124
|
+
# Claude Code harness state (transient locks, per-user settings, worktree shims).
|
|
125
|
+
# `.claude/skills/` is intentionally tracked for shared skills, so ignore the
|
|
126
|
+
# transient pieces individually rather than blanket-ignoring `.claude/`.
|
|
127
|
+
.claude/scheduled_tasks.lock
|
|
128
|
+
.claude/worktrees/
|
|
129
|
+
.claude/settings.local.json
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# Design: reg_schema
|
|
2
|
+
|
|
3
|
+
Design rationale and constraints for the `project_data.json` schema and its structural
|
|
4
|
+
validator. The code (`project_data.py` / `structural.py` / `validation.py`) plus the
|
|
5
|
+
generated `model_json_schema()` are the field-level reference; this file is the WHY.
|
|
6
|
+
Cross-cutting topology (package tree, dependency graph, Pydantic policy) lives in the
|
|
7
|
+
root `ARCHITECTURE.md`; remaining/unbuilt schema work lives in `REFACTOR_SPEC.md`.
|
|
8
|
+
|
|
9
|
+
## Scope
|
|
10
|
+
|
|
11
|
+
`reg_schema` owns:
|
|
12
|
+
|
|
13
|
+
- The `project_data.json` v2 shape (Model A): Pydantic v2 models for `ProjectData`,
|
|
14
|
+
`Source`, `Binding`, `Panel`, `PanelMember`, `PeriodRange`, `LiteralPeriod`,
|
|
15
|
+
`TimeRange`, `StudyWindow`, and the `Period` / `EntityKey` / `TimeKey` / `TimePoint`
|
|
16
|
+
type aliases. Under Model A a `Source` carries a 3-part `register_variant` coordinate
|
|
17
|
+
plus a required `period`; bindings (renamed from the v0.x `columns`) name a 3-segment
|
|
18
|
+
binding FQID via `variable`. An optional top-level
|
|
19
|
+
`window: {"from": <year>, "to": <year>}` (`StudyWindow`) seeds the global study period
|
|
20
|
+
on the subject page (#613/#611); absent = full history; existing specs validate
|
|
21
|
+
unchanged. `Source.period` is a `PeriodSegment` (int year / period token /
|
|
22
|
+
`PeriodRange`), the `"_default"` sentinel, or — since #307 — a **list of segments**
|
|
23
|
+
(an interrupted series, e.g.
|
|
24
|
+
`[{"from": 2005, "to": 2010}, {"from": 2015, "to": 2020}]`): one source stays one
|
|
25
|
+
register extraction (panel keys / binding sets are not duplicated across
|
|
26
|
+
pseudo-sources). The structural list rules: non-empty, members are segments
|
|
27
|
+
(`_default` and nested lists are not), each member non-inverted, and the members
|
|
28
|
+
**sorted ascending and non-overlapping** (adjacency allowed — the list expresses
|
|
29
|
+
interruption; rejecting contiguity would need calendar adjacency math for no safety
|
|
30
|
+
gain). Sorted-and-disjoint keeps the comma-joined wire form (`2005..2010,2015..2020`)
|
|
31
|
+
canonical and per-segment resolution deterministic. Composite `entity_key` /
|
|
32
|
+
`time_key` arrays are part of the schema from day one (the validator enforces their
|
|
33
|
+
ordering/homogeneity rules). Remaining: composite-key runtime support in the extract
|
|
34
|
+
path — see `REFACTOR_SPEC.md`.
|
|
35
|
+
- The §6.8.1 **structural validator** — rules enforceable with only the spec payload, no
|
|
36
|
+
external state: required fields, type/subtype consistency, FQID well-formedness
|
|
37
|
+
(3-segment binding FQID / 2-segment `class/<slug>` value set / 3-part variant
|
|
38
|
+
coordinate), `Source.period` grammar, panel composite ordering, source-collision,
|
|
39
|
+
panel key-refs landing on the source's `display_name` strings, etc.
|
|
40
|
+
- The §6.8.0 cross-runtime contract: `ValidationIssue` / `ValidationResult`. Same shape
|
|
41
|
+
consumed by `reg_schema` (Python) and the SPA (TypeScript codegen'd from OpenAPI).
|
|
42
|
+
Composition just concatenates `issues`.
|
|
43
|
+
|
|
44
|
+
## Logical project selection vs. physical delivery
|
|
45
|
+
|
|
46
|
+
`project_data.json` records research intent, not steward storage topology. A `Source`
|
|
47
|
+
groups bindings by logical `register_variant` and requested period; `Source.name` is an
|
|
48
|
+
internal handle used by panels. It is never a physical filename or SQL-table identity.
|
|
49
|
+
One logical source can resolve to many edition-specific physical tables, and one
|
|
50
|
+
multi-period physical table can satisfy several requested periods, so adding `table` or
|
|
51
|
+
physical `edition` fields to `Source` would conflate two different grains.
|
|
52
|
+
|
|
53
|
+
The v1 target therefore keeps a separate, public, version-controlled steward delivery
|
|
54
|
+
inventory compiled into the released steward artifact. Each physical table has one
|
|
55
|
+
explicit finite edition and literal physical columns; each column has zero or more
|
|
56
|
+
mappings to `(register_variant, variable FQID, canonical representation)`. This
|
|
57
|
+
preserves unmapped columns for coverage and permits one table/column to serve several
|
|
58
|
+
logical variants. `reg_schema` remains independent of the inventory and of reg_meta;
|
|
59
|
+
shared `reg_meta` project code will join a structurally valid project, semantic
|
|
60
|
+
resolution, and the optional inventory in the v1 target. `reg_schema` validates the
|
|
61
|
+
requested-period shape only; the materializer must require the union of each matched
|
|
62
|
+
edition's overlap with its exact resolved representation slice to cover every requested
|
|
63
|
+
segment and report exact uncovered gaps.
|
|
64
|
+
|
|
65
|
+
Every v1 researcher project must carry an explicit requested period. `"_default"` exists
|
|
66
|
+
only for the provisional steward pseudo-project, so remove it from `Source.period` when
|
|
67
|
+
that filter migrates to the delivery inventory; do not retain a structurally valid but
|
|
68
|
+
non-orderable project state. The SPA may expose a common study window as an authoring
|
|
69
|
+
default, but each `Source` still persists its concrete period. Adding a source defaults
|
|
70
|
+
that period to the full available intersection, including disjoint segments. If the
|
|
71
|
+
intersection is empty, the add is blocked rather than inventing a period. A later
|
|
72
|
+
common-window edit does not mutate existing source periods; if it leaves an existing
|
|
73
|
+
source disjoint, the source keeps its explicit period and the project becomes blocking.
|
|
74
|
+
Divergence remains visible rather than being hidden as inheritance, and an explicit
|
|
75
|
+
apply-to-all action rewrites only sources with an overlap. An empty project is a valid
|
|
76
|
+
editable draft but cannot be materialized as an order. See `REFACTOR_SPEC.md` §12.
|
|
77
|
+
|
|
78
|
+
## Closed project root
|
|
79
|
+
|
|
80
|
+
**V1 decision (2026-07-14; implemented by #1134):** `ProjectData` is a closed object.
|
|
81
|
+
Unknown top-level keys receive `unexpected_field`, just like unknown keys on `Source`,
|
|
82
|
+
`Binding`, and `Panel`. V1 has neither arbitrary steward-namespaced blocks nor a
|
|
83
|
+
placeholder `extensions` field. The only current corpus use is the archived
|
|
84
|
+
`reg_monabundle` subsystem, so the cutover deletes that mechanism and its fixtures
|
|
85
|
+
without migration code. If a real future consumer needs extension data, add one explicit
|
|
86
|
+
`extensions` container with a defined owner and validation boundary then.
|
|
87
|
+
|
|
88
|
+
## Not in scope (intentionally)
|
|
89
|
+
|
|
90
|
+
- **Generic extension validation.** V1 has no extension surface to validate. A future
|
|
91
|
+
extension consumer must introduce its explicit container and owner-specific contract
|
|
92
|
+
rather than reopening the project root.
|
|
93
|
+
- **§6.8.3 semantic rules (reg_meta-backed).** FQID resolution against a live reg_meta
|
|
94
|
+
DB, classification existence, steward-inventory membership, drift detection. The
|
|
95
|
+
current web-only implementation lives in `reg_webapp/semantic.py`; the v1 target moves
|
|
96
|
+
it into shared `reg_meta` project code used by the webapp and CLI. The dependency
|
|
97
|
+
remains one-way, so `reg_schema` still ships reg_meta-free.
|
|
98
|
+
- The `project_data.codes.json` sibling file. Codes live alongside the spec and are
|
|
99
|
+
dereferenced from reg_meta at kit-build time; deferred to the MONA rebuild (see
|
|
100
|
+
REFACTOR_SPEC.md §8/9/10a — archived). It may grow a schema dataclass here later;
|
|
101
|
+
phase 1 keeps it out.
|
|
102
|
+
- **Per-source SQL filtering (`where`).** There is no `where` field in the v1 baseline
|
|
103
|
+
`Source`. Cohort/row filtering is a property of the MONA-side runner, not the order
|
|
104
|
+
spec. A future audit-filter use case requires a separately designed contract; it does
|
|
105
|
+
not reserve a generic v1 escape hatch.
|
|
106
|
+
|
|
107
|
+
## What this layer does NOT validate
|
|
108
|
+
|
|
109
|
+
`ValidationResult.__post_init__` coerces `issues` to a tuple but does **not** verify
|
|
110
|
+
each element is a `ValidationIssue` instance. JSON deserialization belongs at read/write
|
|
111
|
+
boundaries — the API ingress in `reg_webapp` — not in the contract module itself.
|
|
112
|
+
Python-internal callers are type-checked; cross-runtime callers own their decode step.
|
|
113
|
+
If `result.ok` ever crashes with `AttributeError` on `.level`, that is a boundary bug to
|
|
114
|
+
fix upstream, not a defensive check to add here.
|
|
115
|
+
|
|
116
|
+
The `level` allowlist *is* enforced at construction because the cost of a
|
|
117
|
+
silently-weakened `ok` (returning `True` for a result that should block) is higher than
|
|
118
|
+
the cost of one extra check on a 3-value frozenset.
|
|
119
|
+
|
|
120
|
+
**`schema_version` is not value-checked here.** The structural layer only requires
|
|
121
|
+
`schema_version` to be a present, non-null string — it does **not** reject a v0.x
|
|
122
|
+
(`"1.x.x"`) value. The "Model A files are `"2.0.0"`; v0.x is hard-rejected, no migration
|
|
123
|
+
code" policy is enforced by the consumer that loads the file (the SPA / CLI in
|
|
124
|
+
`reg_webapp`), not by `validate_structural`. Reason: the version-acceptance window is a
|
|
125
|
+
deployment concern (which schema a given app build understands), whereas this layer is
|
|
126
|
+
the version-agnostic shape checker shared by every runtime. Same split for
|
|
127
|
+
`reg_meta_version`: required as a non-null string here; drift against the
|
|
128
|
+
actually-loaded reg_meta DB is a §6.8.3 semantic concern.
|
|
129
|
+
|
|
130
|
+
## Dependency direction
|
|
131
|
+
|
|
132
|
+
`reg_schema` has **one runtime dependency: Pydantic v2**. The models are the canonical
|
|
133
|
+
project_data shape, double as FastAPI response models in `reg_webapp`, and feed the
|
|
134
|
+
SPA's TypeScript types via `model_json_schema()`; those three jobs make Pydantic's
|
|
135
|
+
declarative field/model validators the right tool here, and keeping `reg_schema` as the
|
|
136
|
+
only Pydantic surface kills the 1:1 wrapper-drift that a separate validation model would
|
|
137
|
+
create between the schema and the API.
|
|
138
|
+
|
|
139
|
+
The **structural validator** (`structural.py`) uses no Pydantic in its rule logic — it
|
|
140
|
+
operates on a parsed dict. (It does import the `Literal` type aliases from
|
|
141
|
+
`project_data.py`, which pulls Pydantic into the import chain.) The Pydantic models live
|
|
142
|
+
on the model surface (`project_data.py`).
|
|
143
|
+
|
|
144
|
+
Why the dep split matters: the spec is validated in two execution contexts with
|
|
145
|
+
different dependency availability — only the webapp backend has reg_meta. Keeping the
|
|
146
|
+
structural layer dep-free means:
|
|
147
|
+
|
|
148
|
+
- Confining Pydantic to `project_data.py` keeps the structural layer importable in
|
|
149
|
+
dep-light contexts without dragging in the model surface.
|
|
150
|
+
- The TypeScript SPA mirrors a small, stable surface.
|
|
151
|
+
|
|
152
|
+
The inbound dependency graph (who imports `reg_schema`) is part of the cross-cutting
|
|
153
|
+
package topology — see `ARCHITECTURE.md`. The constraint `reg_schema` itself imposes is
|
|
154
|
+
the one above: it pulls in **only** Pydantic, and only on the model surface.
|
|
155
|
+
|
|
156
|
+
## Two layers: models vs. validator
|
|
157
|
+
|
|
158
|
+
`reg_schema` is deliberately split into a **shape** layer and a **rule** layer, and the
|
|
159
|
+
two are kept apart on purpose:
|
|
160
|
+
|
|
161
|
+
- **Models** (`project_data.py`, Pydantic v2): `ProjectData`, `Source`, `Binding`,
|
|
162
|
+
`Panel`, `PanelMember`, `PeriodRange`, `LiteralPeriod`, `TimeRange`, `StudyWindow`,
|
|
163
|
+
and the `Period` / `EntityKey` / `TimeKey` / `TimePoint` type aliases. **Pure shape
|
|
164
|
+
definitions** — structural rules are *not* re-encoded as raising field validators.
|
|
165
|
+
Re-encoding them would replace the issue-accumulating contract with a fail-fast one,
|
|
166
|
+
and every runtime that shares the contract (SPA, webapp) needs the full issue list,
|
|
167
|
+
not the first exception.
|
|
168
|
+
- **Structural validator** (`structural.py`, §6.8.1): the entrypoint
|
|
169
|
+
`validate_structural(data: Mapping[str, object]) -> ValidationResult` operates on a
|
|
170
|
+
**parsed dict, not the Pydantic models**, for two reasons. First, rules like "`type` ∈
|
|
171
|
+
enum" must fire on raw JSON values *before* any `Literal` cast would coerce or reject
|
|
172
|
+
them — a wrong enum value has to surface as an accumulated `invalid_enum_value` issue,
|
|
173
|
+
not a constructor crash. Second, staying off the model surface keeps the validator
|
|
174
|
+
Pydantic-free so it ports cleanly to the TS SPA.
|
|
175
|
+
|
|
176
|
+
Models are constructed at boundaries (API ingress) only *after* `validate_structural`
|
|
177
|
+
has passed; a Pydantic raise at that point signals validator/model drift, not user
|
|
178
|
+
error.
|
|
179
|
+
|
|
180
|
+
## Shared validator corpus (`test_corpus/`)
|
|
181
|
+
|
|
182
|
+
`reg_schema/test_corpus/` is the single artifact that keeps the §6.8.1 structural rules
|
|
183
|
+
behaving identically in the two runtimes that carry a copy of them — the canonical
|
|
184
|
+
Python `validate_structural` and the SPA's TypeScript port. Each case is a directory
|
|
185
|
+
containing an `input.json` (a `project_data.json` payload) and an
|
|
186
|
+
`expected_ValidationResult.json` (the validator output the structural rules must
|
|
187
|
+
produce). See `test_corpus/README.md` for the directory layout, file formats, and the
|
|
188
|
+
rule for adding cases.
|
|
189
|
+
|
|
190
|
+
**Two** consumers run the structural corpus — the two runtimes that own a copy of the
|
|
191
|
+
§6.8.1 rules:
|
|
192
|
+
|
|
193
|
+
- `reg_schema/tests/test_corpus.py` runs `validate_structural(input)` against every case
|
|
194
|
+
and asserts an unordered-issue equality match with the decoded
|
|
195
|
+
`expected_ValidationResult.json` — the single Python source of truth that the SPA
|
|
196
|
+
mirrors.
|
|
197
|
+
- The SPA's TypeScript test suite imports the JSON as fixtures and runs its TS port of
|
|
198
|
+
the validator against them.
|
|
199
|
+
|
|
200
|
+
The corpus grows alongside the validator: at least one well-formed empty-issues case to
|
|
201
|
+
prove the format/harness/round-trip, plus one (or more) negative case per structural
|
|
202
|
+
rule. Negative cases for §6.8.3 (reg_meta-backed semantic) live in their owning
|
|
203
|
+
packages, not here — `reg_schema` only owns the structural layer's corpus.
|
|
204
|
+
|
|
205
|
+
## Structural rules and issue codes
|
|
206
|
+
|
|
207
|
+
Issue `code` values are stable across releases — tests pin them, the SPA maps codes to
|
|
208
|
+
UI affordances, new codes are additive (§6.8.0). Current codes:
|
|
209
|
+
|
|
210
|
+
| Code | Rule (§6.8.1) |
|
|
211
|
+
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
212
|
+
| `invalid_root` | Root must be an object. |
|
|
213
|
+
| `missing_required_field` | A required field (top-level, source, binding, panel, member) is absent. |
|
|
214
|
+
| `invalid_field_type` | A field's JSON type is wrong (e.g. `steward` is not a string; `members` is not an array; `period` is null). |
|
|
215
|
+
| `invalid_enum_value` | `steward`, `type`, `id_subtype`, `numeric_subtype` is outside its allowed set. |
|
|
216
|
+
| `unexpected_field` | An unrecognized key on a closed object: `ProjectData`, `Source`, `Binding`, `Panel`, or panel member. Unknown top-level values receive this code regardless of whether the value is an object, array, or scalar. |
|
|
217
|
+
| `invalid_fqid` | FQID segment count or per-segment characters are wrong: binding `variable` is not a 3-segment `<provider>/<register>/<slug>`, `value_set` is not a 2-segment `class/<slug>`, or `register_variant` is not a 3-part `<provider>/<register>/<variant>` coordinate. The binding leaf is a bare slug — the retired `@version` pin is now a stray `@` the per-segment grammar rejects (§6.8.3 resolves the value set from `(variable, variant, period)`). |
|
|
218
|
+
| `fqid_register_variant_mismatch` | A binding `variable`'s first **2** segments (provider/register) don't equal the owning source's `register_variant` prefix. The variant is not repeated on the binding — it lives once on the Source. |
|
|
219
|
+
| `invalid_period` | A `Source.period` is not an int year, a period-token string (`YYYY`, `YYYY-MM`, `YYYY-MM-DD`, `HTYYYY`, `VTYYYY`, `YYYY-Q[1-4]`, `YYYY-H[12]`), the snapshot sentinel `"_default"`, a `{"from","to"}` range object with valid endpoints, or a #307 segment LIST (interrupted series). The list rules raise the same code (member-pathed `/period/<i>`): empty list, a non-segment member (`_default` / nested list / junk), an inverted member range, or members not sorted-ascending / overlapping. A `YYYY-MM-DD` token that passes the syntactic 01-31 day envelope but names a calendar-impossible day (`2019-02-29` in a non-leap year, `2018-02-30`) also raises this code. |
|
|
220
|
+
| `invalid_window` | A top-level `window`'s `to` year is less than its `from` year. |
|
|
221
|
+
| `subtype_on_wrong_type` | A `*_subtype` or `*_format` field is set on a binding whose `type` doesn't own it (e.g. `id_subtype` on a categorical). |
|
|
222
|
+
| `empty_bindings` | A source has zero bindings. |
|
|
223
|
+
| `duplicate_source_name` | Two sources share a `name`. |
|
|
224
|
+
| `display_name_collision` | Two bindings on the same source share an explicit `display_name`. The implicit-resolution half — one explicit + one resolving to the same reg_meta default — needs reg_meta and lives in §6.8.3. |
|
|
225
|
+
| `duplicate_panel_id` | Two panels share a `panel_id`. |
|
|
226
|
+
| `empty_members` | A panel has zero members. |
|
|
227
|
+
| `literal_period_invalid` | The `{"period": ...}` or `{"range": {"from","to"}}` time_key object form is malformed (missing/extra keys, or non-period endpoints). |
|
|
228
|
+
| `composite_time_key_mixed_kinds` | A composite `time_key` array mixes column refs and literals on a single member. |
|
|
229
|
+
| `composite_key_inconsistent` | Composite `entity_key` / `time_key` tuples across members of a panel are not identically ordered. |
|
|
230
|
+
| `time_key_member_kind_mismatch` | A member-level composite `time_key` override has a different kind (literal vs ref) than the panel-level composite. |
|
|
231
|
+
| `literal_time_key_duplicate` | Two members of one panel resolve to the same literal `time_key`. |
|
|
232
|
+
| `entity_key_unknown_column` | A bare-string `entity_key` ref doesn't match any `display_name` on the member's source. Skipped on sources with any unset `display_name` (the ref may resolve to a reg_meta-derived default at runtime). |
|
|
233
|
+
| `time_key_unknown_column` | Same rule, for `time_key` column refs. |
|
|
234
|
+
| `source_referenced_by_multiple_panels` | One source appears in two panels. |
|
|
235
|
+
| `panel_member_unknown_source` | A panel member's `source` does not match any entry in `/sources`. |
|
|
236
|
+
|
|
237
|
+
The "ref exists on source" check is intentionally lenient: when any binding on the
|
|
238
|
+
source lacks an explicit `display_name`, the structural layer skips matching that
|
|
239
|
+
source's refs entirely. The webapp materializes defaults from reg_meta before emitting
|
|
240
|
+
artifacts, and a pre-authoring SPA-state spec shouldn't be flagged for refs that will
|
|
241
|
+
resolve later.
|
|
242
|
+
|
|
243
|
+
### Effective-key presence is not structural
|
|
244
|
+
|
|
245
|
+
The v0.x `missing_effective_entity_key` / `missing_effective_time_key` codes do **not**
|
|
246
|
+
exist in this layer. Under Model A an omitted `entity_key` / `time_key` inherits from
|
|
247
|
+
the member's variant's `panel_template`, which needs reg_meta state — so the "no
|
|
248
|
+
effective key" case can only be checked once inheritance is materialized at kit-build
|
|
249
|
+
time, a path deferred to the from-scratch MONA rebuild (#707). A member with no panel
|
|
250
|
+
default and no override is simply not flagged at this layer.
|
|
251
|
+
|
|
252
|
+
The composite/literal panel rules that **are** structural live in the issue-code table
|
|
253
|
+
above (`composite_key_inconsistent` ordering, `composite_time_key_mixed_kinds`
|
|
254
|
+
homogeneity, `literal_time_key_duplicate` uniqueness, member-vs-panel
|
|
255
|
+
`time_key_member_kind_mismatch`). The structural layer deliberately keeps the SPA's
|
|
256
|
+
pre-authoring spec valid even while inheritance is still unresolved — it never
|
|
257
|
+
materializes defaults, only checks shapes.
|
|
258
|
+
|
|
259
|
+
The `panel_inheritance_unresolvable` check and key materialization are deferred to the
|
|
260
|
+
MONA rebuild (see `REFACTOR_SPEC.md` §8/9/10a; tracked in #707, archived #699).
|
|
261
|
+
|
|
262
|
+
### Semantic codes — defined, not emitted by `reg_schema`
|
|
263
|
+
|
|
264
|
+
These `code` values are part of the §6.8.0 contract but are raised by the
|
|
265
|
+
**reg_meta-backed §6.8.3 layer** (`reg_webapp`), never by `validate_structural`. Listed
|
|
266
|
+
here so the stable-code registry is complete and the SPA can map them:
|
|
267
|
+
|
|
268
|
+
| Code | Level | Rule (§6.8.3) |
|
|
269
|
+
| ------------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
270
|
+
| `period_outside_state_validity` | error | No `variable_state` covers the binding's `(variant, period)`. |
|
|
271
|
+
| `binding_state_drifts_within_period` | info | A range `period` crosses a state transition (incl. a delivery-column rename), or a chosen `representation` covers only part of the range; the resolver returns per-state subsets. |
|
|
272
|
+
| `range_period_partially_covered` | info | An explicit range `period` is only PARTIALLY covered by the concept's states — the union of every covering state (across all delivery columns) leaves a gap NO column delivers (e.g. SSYK first delivered 2014 under a `from:2010,to:2020` binding → 2010–2013 has no data). The covered sub-range still extracts; the gap is silently dropped, so this surfaces it. Distinct from `binding_state_drifts_within_period`: that is the CHOSEN representation under-covering vs a SIBLING column that DOES deliver the gap (whole-concept covered); this is the whole concept itself under-covering. Zero coverage is `period_outside_state_validity`, not this. Only fires for an explicit `PeriodRange` (a point/token period is a single instant; `_default` has no author-requested window). |
|
|
273
|
+
| `binding_value_set_version_ambiguous` | error | A binding's `(variant, period)` resolves to several **CO-EXISTING delivery columns** (distinct columns valid at the SAME instant — overlapping windows) — parallel REPRESENTATIONS of the one concept (SSYK 3/4/5-digit, age brackets) — and the binding sets no `representation`. The author must pick one (the SPA offers a chooser); this is where the retired `@version` pin's job now lives, keyed on the delivery column. Distinct columns in NON-overlapping windows (a sequential rename) are drift, not ambiguity. (Also re-used as a backstop for the rarer case of distinct value sets co-delivered on ONE column — a reg_meta build co-delivery the `validate` invariant should make unreachable.) |
|
|
274
|
+
| `binding_representation_unknown` | error (→ warning on the steward path) | A binding's `representation` is not a delivery column of the concept at the source's `(variant, period)`. Downgraded for steward-catalog load (reg_meta dropped/renamed the pinned column = drift), like `period_outside_state_validity`. |
|
|
275
|
+
| `deprecated_traversal` | info | The binding resolves to a variable marked `deprecated` in catalog metadata; the FQID still resolves, but the author should prefer a current successor when one is available. |
|
|
276
|
+
| `variable_replaced` | info | The binding has a `variable_replaced_by` edge effective at or before the source's `period`; hint points at the successor. |
|
|
277
|
+
|
|
278
|
+
(The `panel_inheritance_unresolvable` code is **not** in this live set — its kit-build
|
|
279
|
+
check is deferred to the from-scratch MONA rebuild, see above and `REFACTOR_SPEC.md`
|
|
280
|
+
§8/9/10a; tracked in #707, archived #699.)
|
|
281
|
+
|
|
282
|
+
## Why no FQID parser dependency
|
|
283
|
+
|
|
284
|
+
§6.8.1 phrases FQID structural checks as syntactic ("3-segment binding FQID
|
|
285
|
+
`<provider>/<register>/<slug>` / 2-segment `class/<slug>` / 3-part `register_variant`
|
|
286
|
+
coordinate"). The binding leaf is a bare slug — there is no `@version` pin to split off
|
|
287
|
+
(that grammar is retired; co-delivery selection moved to the binding `representation`
|
|
288
|
+
field, §6.8.3), so the per-segment `_FQID_TOKEN` rejects a stray `@`. `reg_schema`
|
|
289
|
+
implements these checks locally rather than importing `reg_meta`'s `Fqid` parser. Two
|
|
290
|
+
reasons:
|
|
291
|
+
|
|
292
|
+
- Keeps the dependency direction one-way (`reg_meta` → `reg_meta_build` is the only
|
|
293
|
+
cross-dep today; adding `reg_schema` → `reg_meta` would pull reg_meta into every
|
|
294
|
+
consumer of reg_schema).
|
|
295
|
+
- Structural well-formedness is a small, stable surface (segment count + segment slug
|
|
296
|
+
characters). Duplicating it is cheaper than the coupling.
|
|
297
|
+
|
|
298
|
+
The same rationale covers the **period-token grammar** (`Source.period` and `TimeKey`
|
|
299
|
+
range endpoints): `structural._PERIOD_TOKEN` is a deliberate mirror of the canonical
|
|
300
|
+
grammar in `reg_meta.fqid._PERIOD_PATTERNS` (year 1900-2099, month 01-12, day 01-31,
|
|
301
|
+
plus `HT/VT`, quarter, half-year forms). Both copies also calendar-validate the
|
|
302
|
+
author-supplied day of a `YYYY-MM-DD` token (`_is_period_endpoint` here mirrors
|
|
303
|
+
`is_period` on the reg_meta side): the regex bounds the day 01-31 syntactically, but an
|
|
304
|
+
impossible day (`2019-02-29` in a non-leap year, `2018-02-30`) is rejected by an extra
|
|
305
|
+
`date.fromisoformat` check. The grammar is kept **bound-for-bound identical** so a spec
|
|
306
|
+
that passes this structural gate doesn't later fail reg_meta's period resolution — a
|
|
307
|
+
looser copy would silently split the period contract across the two packages. #307
|
|
308
|
+
widened the mirror by one function: `structural._endpoint_bounds` duplicates
|
|
309
|
+
`reg_meta.fqid.period_token_to_bounds` (token → inclusive ISO interval, including the
|
|
310
|
+
deliberate synthesized Feb-29 upper bound) because the period-list sorted/non-overlap
|
|
311
|
+
rule needs real interval comparisons across mixed grammars (`HT2018` ⊂ `2018`). The
|
|
312
|
+
cross-grammar parity test (`reg_webapp/backend/tests/test_period_grammar_parity.py`) is
|
|
313
|
+
the CI gate that enforces this invariant — token verdicts AND bounds expansions: any
|
|
314
|
+
future change to one side that breaks parity with the other will fail CI.
|
|
315
|
+
|
|
316
|
+
Semantic FQID resolution stays in `reg_meta` and is invoked by the §6.8.3 layer in
|
|
317
|
+
`reg_webapp`.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: reg-schema
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: project_data.json schema and structural validator
|
|
5
|
+
Project-URL: Homepage, https://github.com/adamaltmejd/registry-research-toolkit
|
|
6
|
+
Project-URL: Repository, https://github.com/adamaltmejd/registry-research-toolkit
|
|
7
|
+
Project-URL: Issues, https://github.com/adamaltmejd/registry-research-toolkit/issues
|
|
8
|
+
Author-email: Adam Altmejd <adam@altmejd.se>
|
|
9
|
+
License: MIT
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Requires-Python: >=3.14
|
|
16
|
+
Requires-Dist: pydantic>=2.13.4
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# reg_schema
|
|
20
|
+
|
|
21
|
+
`project_data.json` schema and structural validator. Importable by the webapp and the
|
|
22
|
+
SPA (via TS codegen).
|
|
23
|
+
|
|
24
|
+
Python ≥3.14. One runtime dependency: Pydantic v2 (the deliberate exception to the
|
|
25
|
+
workspace no-Pydantic rule); the structural validator itself operates on raw dicts and
|
|
26
|
+
needs no third-party deps. See [DESIGN.md](DESIGN.md) for scope and dependency
|
|
27
|
+
direction.
|
|
28
|
+
|
|
29
|
+
## Status
|
|
30
|
+
|
|
31
|
+
v2.0.0 — Model A grammar. The surface: the §6.8.0 cross-runtime contract
|
|
32
|
+
(`ValidationIssue`, `ValidationResult`), the Pydantic v2 models (`ProjectData`,
|
|
33
|
+
`Source`, `Binding`, `Panel`, `PanelMember`, `Period`, `PeriodRange`, `LiteralPeriod`,
|
|
34
|
+
plus the `EntityKey` / `TimeKey` / `TimePoint` aliases), and the unified
|
|
35
|
+
`validate_structural()` entrypoint implementing §6.8.1. See [DESIGN.md](DESIGN.md) for
|
|
36
|
+
the issue-code table.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import json
|
|
40
|
+
|
|
41
|
+
from reg_schema import validate_structural
|
|
42
|
+
|
|
43
|
+
with open("project_data.json") as f:
|
|
44
|
+
spec = json.load(f)
|
|
45
|
+
|
|
46
|
+
result = validate_structural(spec)
|
|
47
|
+
if not result.ok:
|
|
48
|
+
for issue in result.issues:
|
|
49
|
+
if issue.level == "error":
|
|
50
|
+
print(f"{issue.path}: {issue.code} — {issue.message}")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The validator operates on a parsed dict, not on the Pydantic models, because rules like
|
|
54
|
+
"type is one of the enum values" must fire on raw JSON values before any `Literal` cast.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# reg_schema
|
|
2
|
+
|
|
3
|
+
`project_data.json` schema and structural validator. Importable by the webapp and the
|
|
4
|
+
SPA (via TS codegen).
|
|
5
|
+
|
|
6
|
+
Python ≥3.14. One runtime dependency: Pydantic v2 (the deliberate exception to the
|
|
7
|
+
workspace no-Pydantic rule); the structural validator itself operates on raw dicts and
|
|
8
|
+
needs no third-party deps. See [DESIGN.md](DESIGN.md) for scope and dependency
|
|
9
|
+
direction.
|
|
10
|
+
|
|
11
|
+
## Status
|
|
12
|
+
|
|
13
|
+
v2.0.0 — Model A grammar. The surface: the §6.8.0 cross-runtime contract
|
|
14
|
+
(`ValidationIssue`, `ValidationResult`), the Pydantic v2 models (`ProjectData`,
|
|
15
|
+
`Source`, `Binding`, `Panel`, `PanelMember`, `Period`, `PeriodRange`, `LiteralPeriod`,
|
|
16
|
+
plus the `EntityKey` / `TimeKey` / `TimePoint` aliases), and the unified
|
|
17
|
+
`validate_structural()` entrypoint implementing §6.8.1. See [DESIGN.md](DESIGN.md) for
|
|
18
|
+
the issue-code table.
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import json
|
|
22
|
+
|
|
23
|
+
from reg_schema import validate_structural
|
|
24
|
+
|
|
25
|
+
with open("project_data.json") as f:
|
|
26
|
+
spec = json.load(f)
|
|
27
|
+
|
|
28
|
+
result = validate_structural(spec)
|
|
29
|
+
if not result.ok:
|
|
30
|
+
for issue in result.issues:
|
|
31
|
+
if issue.level == "error":
|
|
32
|
+
print(f"{issue.path}: {issue.code} — {issue.message}")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The validator operates on a parsed dict, not on the Pydantic models, because rules like
|
|
36
|
+
"type is one of the enum values" must fire on raw JSON values before any `Literal` cast.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "reg-schema"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "project_data.json schema and structural validator"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
|
+
authors = [{ name = "Adam Altmejd", email = "adam@altmejd.se" }]
|
|
12
|
+
license = { text = "MIT" }
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Scientific/Engineering",
|
|
19
|
+
]
|
|
20
|
+
# reg_schema is the deliberate exception to the workspace no-Pydantic rule
|
|
21
|
+
# (CLAUDE.md stack §): it is the canonical structural validator + the
|
|
22
|
+
# FastAPI response-model source, and `model_json_schema()` feeds the SPA's
|
|
23
|
+
# TS codegen. See reg_schema/DESIGN.md → Two layers: models vs. validator for the
|
|
24
|
+
# Pydantic boundary. (The MONA bundle that consumed these models was archived to
|
|
25
|
+
# `archive/mona-subsystem` pending a from-scratch rebuild.)
|
|
26
|
+
dependencies = [
|
|
27
|
+
"pydantic>=2.13.4",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/adamaltmejd/registry-research-toolkit"
|
|
32
|
+
Repository = "https://github.com/adamaltmejd/registry-research-toolkit"
|
|
33
|
+
Issues = "https://github.com/adamaltmejd/registry-research-toolkit/issues"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/reg_schema"]
|