Skip to content

Commit aae201c

Browse files
feat(otdf-sdk-mgr): schema dump CLI + xtest/schema canonical JSON Schemas (DSPX-3302)
Headless runs of scenario-from-ticket kept trying `python3 -c "from otdf_sdk_mgr.schema import Scenario; ..."` to introspect Pydantic model shape while authoring scenarios. That form isn't in the plugin's Bash allowlist (deliberately — it's arbitrary code execution), so the agent fell back to Reading schema.py source. Static, committed JSON Schemas give the same information declaratively without needing a python verb in the allowlist at all. - `otdf-sdk-mgr schema dump [--out-dir]`: writes `xtest/schema/{scenario,instance}.schema.json` from `Model.model_json_schema()`, sorted-keys + trailing newline so output is byte-stable. Add new models to `SCHEMAS` in cli_schema.py and they get picked up automatically. - `xtest/schema/` is committed with the generated files plus brief README/CLAUDE.md (progressive-disclosure, mirroring xtest/features/). - `test_schema_sync.py` parametrizes over `SCHEMAS` and fails if any committed file drifts from the live model — the safety net for "someone edited a Pydantic model without regenerating." - `scenario-from-ticket` SKILL.md Step 5 now points at `xtest/schema/scenario.schema.json` as the canonical field list. - `xtest/README.md` lists the new directory alongside `scenarios/` and `features/`. No allowlist changes needed — `Bash(uv run otdf-sdk-mgr *)` already covers the dump subcommand, and `Read` is unrestricted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ec8513f commit aae201c

9 files changed

Lines changed: 828 additions & 2 deletions

File tree

.claude/skills/scenario-from-ticket/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Likely candidates: `test_tdfs.py` (roundtrip), `test_abac.py` (ABAC), `test_lega
9292

9393
## Step 5 — Write `xtest/scenarios/<id>.yaml`
9494

95-
The schema (`otdf_sdk_mgr.schema.Scenario`) rejects unknown fields. Each pin (`PlatformPin`, `KasPin`) requires **exactly one** of `dist:`, `source:`, or `image:`. `image:` is reserved for forward-compat and rejected today — pick `dist:` or `source:`.
95+
The canonical field list (titles, types, defaults, `anyOf` branches) lives in `xtest/schema/scenario.schema.json``Read` it whenever you need to know what's allowed. Each pin (`PlatformPin`, `KasPin`) requires **exactly one** of `dist:`, `source:`, or `image:`. `image:` is reserved for forward-compat and rejected today — pick `dist:` or `source:`.
9696

9797
Released-version pin (typical Bug scenario):
9898

otdf-sdk-mgr/src/otdf_sdk_mgr/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import typer
1111

1212
from otdf_sdk_mgr.cli_install import install_app
13+
from otdf_sdk_mgr.cli_schema import schema_app
1314
from otdf_sdk_mgr.cli_versions import versions_app
1415
from otdf_sdk_mgr.config import ALL_SDKS, get_sdk_dirs
1516

@@ -20,6 +21,7 @@
2021
)
2122

2223
app.add_typer(install_app, name="install")
24+
app.add_typer(schema_app, name="schema")
2325
app.add_typer(versions_app, name="versions")
2426

2527

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""`otdf-sdk-mgr schema` subcommands.
2+
3+
Emit canonical JSON Schemas for the Pydantic models in `otdf_sdk_mgr.schema`
4+
so agents (and humans) can introspect the on-disk YAML formats without
5+
running `python -c` against the package. The generated files live under
6+
`xtest/schema/` and are kept in sync via `tests/test_schema_sync.py`.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import json
12+
from pathlib import Path
13+
from typing import Annotated
14+
15+
import typer
16+
from otdf_sdk_mgr.schema import Instance, Scenario
17+
18+
schema_app = typer.Typer(help="Emit JSON Schemas for the scenario/instance models.")
19+
20+
# (model_class, output_filename). Add new models here and `schema dump`
21+
# will pick them up automatically.
22+
SCHEMAS: tuple[tuple[type, str], ...] = (
23+
(Scenario, "scenario.schema.json"),
24+
(Instance, "instance.schema.json"),
25+
)
26+
27+
28+
def render(model: type) -> str:
29+
"""Render `model.model_json_schema()` as a deterministic JSON string.
30+
31+
Sorted keys and a trailing newline so byte-equality comparisons in the
32+
sync test are stable.
33+
"""
34+
return json.dumps(model.model_json_schema(), indent=2, sort_keys=True) + "\n"
35+
36+
37+
@schema_app.command("dump")
38+
def dump(
39+
out_dir: Annotated[
40+
Path,
41+
typer.Option(
42+
"--out-dir",
43+
help="Directory to write *.schema.json files into.",
44+
),
45+
] = Path("xtest/schema"),
46+
) -> None:
47+
"""Write JSON Schemas for every canonical scenario/instance model.
48+
49+
Overwrites existing files. Re-run whenever a Pydantic model changes;
50+
the committed schemas in xtest/schema/ are otherwise the source of
51+
truth that the scenario-authoring skills read.
52+
"""
53+
out_dir.mkdir(parents=True, exist_ok=True)
54+
for model, filename in SCHEMAS:
55+
path = out_dir / filename
56+
path.write_text(render(model), encoding="utf-8")
57+
typer.echo(f" wrote {path}")
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Guard that the committed JSON Schemas under xtest/schema/ stay in sync
2+
with the live Pydantic models.
3+
4+
The skills authoring scenarios read those JSON files directly to know what
5+
fields are allowed; if a Pydantic model gains, loses, or renames a field
6+
without a corresponding `uv run otdf-sdk-mgr schema dump`, the skills will
7+
silently rely on a stale schema. This test makes that drift loud.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from pathlib import Path
13+
14+
import pytest
15+
from otdf_sdk_mgr.cli_schema import SCHEMAS, render
16+
17+
18+
def _xtest_schema_dir() -> Path:
19+
"""Locate xtest/schema/ relative to this test file.
20+
21+
The repo layout puts otdf-sdk-mgr/tests/ next to xtest/, so two parents
22+
up from this file is the tests/ root.
23+
"""
24+
return Path(__file__).resolve().parents[2] / "xtest" / "schema"
25+
26+
27+
@pytest.mark.parametrize(("model", "filename"), SCHEMAS, ids=lambda v: getattr(v, "__name__", v))
28+
def test_committed_schema_matches_model(model: type, filename: str) -> None:
29+
path = _xtest_schema_dir() / filename
30+
assert path.is_file(), (
31+
f"Missing {path}. Run `uv run otdf-sdk-mgr schema dump` to regenerate."
32+
)
33+
expected = render(model)
34+
actual = path.read_text(encoding="utf-8")
35+
assert actual == expected, (
36+
f"{path} is out of sync with {model.__name__}. "
37+
f"Run `uv run otdf-sdk-mgr schema dump` to regenerate."
38+
)

xtest/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,5 +127,6 @@ pytest test_tdfs.py
127127

128128
- **`scenarios/`** — Per-ticket scenario YAMLs that pin a platform / KAS / SDK topology to a specific pytest selection. Consumed by `otdf-local scenario run`.
129129
- **`features/`** — Multi-repo feature specs: features that touch more than one OpenTDF repo (platform + SDKs) authored as a single declaration of intent. See `features/README.md`.
130+
- **`schema/`** — Generated JSON Schemas for the canonical scenario / instance models. Regenerate via `uv run otdf-sdk-mgr schema dump` after editing the Pydantic models in `otdf-sdk-mgr/src/otdf_sdk_mgr/schema.py`. See `schema/README.md`.
130131

131-
Both are produced by the Claude Code skills under `tests/.claude/skills/` (`scenario-from-ticket`, `feature-design`, etc.) and can also be hand-authored.
132+
The first two are produced by the Claude Code skills under `tests/.claude/skills/` (`scenario-from-ticket`, `feature-design`, etc.) and can also be hand-authored.

xtest/schema/CLAUDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Agent guidance for xtest/schema
2+
3+
These JSON Schemas are the canonical reference for the on-disk YAML formats. When you need to know what fields a scenario or instance accepts:
4+
5+
- **Read these files**. Don't run `python -c "from otdf_sdk_mgr.schema import ..."` to introspect — those forms aren't in the plugin's allowlist, and the JSON Schemas have the same information in declarative form (titles, types, `anyOf` for ref-vs-version pins, `additionalProperties: false`, default values, etc.).
6+
- The files are byte-stable and sorted; safe to grep, diff, or quote.
7+
8+
If a Pydantic model changes and these files drift, the user (or CI) will regenerate them via `uv run otdf-sdk-mgr schema dump`. Don't try to regenerate them yourself unless you're explicitly fixing the drift in a schema-editing PR.

xtest/schema/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# xtest/schema
2+
3+
JSON Schemas for the canonical scenario / instance YAML formats. One file per Pydantic model in `otdf-sdk-mgr/src/otdf_sdk_mgr/schema.py`:
4+
5+
- `scenario.schema.json` — the shape that `xtest/scenarios/<id>.yaml` validates against.
6+
- `instance.schema.json` — the shape of `tests/instances/<name>/instance.yaml`.
7+
8+
These files are generated artifacts. To refresh them after editing a Pydantic model:
9+
10+
```bash
11+
uv run --project otdf-sdk-mgr otdf-sdk-mgr schema dump
12+
```
13+
14+
A pytest in `otdf-sdk-mgr/tests/test_schema_sync.py` fails CI if the committed files drift from what the live models would produce.
15+
16+
See `CLAUDE.md` for how Claude Code skills consume these files.

0 commit comments

Comments
 (0)