Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ jobs:
python -m mypy --strict lib/python/base_cli
python scripts/validate_docs.py
python scripts/validate_changelog.py
python scripts/validate_schemas.py
python scripts/benchmark_runtime.py --check
python -m compileall -q examples
- name: Run tests with coverage threshold
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ and versions are tracked in the repo-root `VERSION` file.

### Added

- Publish versioned JSON Schema artifacts for output, error, inspection, log,
NDJSON, and decoded command-protocol contracts in the package and docs site.
- Add a framework choice guide, five-minute evaluation path, and clearer
production-lifecycle positioning for Click and Typer adopters.
- Add deterministic SPDX SBOMs, artifact checksums, and OIDC-backed GitHub
Expand Down
33 changes: 33 additions & 0 deletions docs/schemas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# JSON Schema artifacts

Versioned JSON Schema artifacts make the machine contracts usable from any
language. The same files are included in the `base-cli` wheel under
`base_cli/schemas/v1/` and are published here for direct browser or HTTP
consumption:

| Contract | Schema |
| --- | --- |
| Success envelope | [`output.schema.json`](schemas/v1/output.schema.json) |
| Error envelope | [`error.schema.json`](schemas/v1/error.schema.json) |
| Inspection envelope | [`inspection.schema.json`](schemas/v1/inspection.schema.json) |
| JSON log record | [`log.schema.json`](schemas/v1/log.schema.json) |
| NDJSON record | [`ndjson.schema.json`](schemas/v1/ndjson.schema.json) |
| Decoded command protocol | [`command-protocol.schema.json`](schemas/v1/command-protocol.schema.json) |

All artifacts use JSON Schema draft 2020-12 and carry a stable `$id` under the
`/schemas/v1/` URL prefix. A schema version is a compatibility boundary: an
additive change can remain in v1, while a change to required fields or field
meaning requires a new version and migration note.

The command-protocol artifact describes the decoded representation of a
`COMMAND_PROTOCOL_V1` frame. The wire framing itself remains line-oriented and
is validated by the protocol codec documented in
[`json-contracts.md`](json-contracts.md).

Consumers can load a packaged schema without importing base-cli:

```python
from importlib.resources import files

schema_text = files("base_cli").joinpath("schemas/v1/output.schema.json").read_text()
```
15 changes: 15 additions & 0 deletions docs/schemas/v1/command-protocol.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://basefoundry.github.io/base-cli/schemas/v1/command-protocol.schema.json",
"title": "base-cli decoded command-protocol payload",
"description": "Schema for the decoded representation of COMMAND_PROTOCOL_V1 frames.",
"type": "object",
"additionalProperties": false,
"required": ["protocol_header", "record_type", "record_count", "records"],
"properties": {
"protocol_header": {"const": "COMMAND_PROTOCOL_V1"},
"record_type": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]*$"},
"record_count": {"type": "integer", "minimum": 0, "maximum": 1000000},
"records": {"type": "array", "maxItems": 1000000, "items": {"type": "object"}}
}
}
17 changes: 17 additions & 0 deletions docs/schemas/v1/error.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://basefoundry.github.io/base-cli/schemas/v1/error.schema.json",
"title": "base-cli error envelope",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "schema", "code", "type", "message", "details", "run_id"],
"properties": {
"schema_version": {"const": 1},
"schema": {"const": "base-cli.error"},
"code": {"type": "string", "minLength": 1},
"type": {"const": "error"},
"message": {"type": "string"},
"details": {"type": "object"},
"run_id": {"type": ["string", "null"]}
}
}
15 changes: 15 additions & 0 deletions docs/schemas/v1/inspection.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://basefoundry.github.io/base-cli/schemas/v1/inspection.schema.json",
"title": "base-cli inspection envelope",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "command", "status", "data", "error"],
"properties": {
"schema_version": {"const": 1},
"command": {"type": "string", "minLength": 1},
"status": {"enum": ["ok", "warn", "error"]},
"data": {"type": "object"},
"error": {"type": ["object", "null"]}
}
}
18 changes: 18 additions & 0 deletions docs/schemas/v1/log.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://basefoundry.github.io/base-cli/schemas/v1/log.schema.json",
"title": "base-cli JSON log record",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "schema", "timestamp", "level", "logger", "message", "run_id"],
"properties": {
"schema_version": {"const": 1},
"schema": {"const": "base-cli.log"},
"timestamp": {"type": "string", "format": "date-time"},
"level": {"type": "string", "minLength": 1},
"logger": {"type": "string", "minLength": 1},
"message": {"type": "string", "maxLength": 8193},
"run_id": {"type": ["string", "null"]},
"details": {"type": "object"}
}
}
13 changes: 13 additions & 0 deletions docs/schemas/v1/ndjson.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://basefoundry.github.io/base-cli/schemas/v1/ndjson.schema.json",
"title": "base-cli NDJSON record",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "schema", "record"],
"properties": {
"schema_version": {"const": 1},
"schema": {"const": "base-cli.record"},
"record": {"type": "object"}
}
}
17 changes: 17 additions & 0 deletions docs/schemas/v1/output.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://basefoundry.github.io/base-cli/schemas/v1/output.schema.json",
"title": "base-cli success envelope",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "schema", "code", "type", "message", "details", "run_id"],
"properties": {
"schema_version": {"const": 1},
"schema": {"const": "base-cli.output"},
"code": {"type": "string", "minLength": 1},
"type": {"const": "success"},
"message": {"type": "string"},
"details": {"type": "object"},
"run_id": {"type": ["string", "null"]}
}
}
15 changes: 15 additions & 0 deletions lib/python/base_cli/schemas/v1/command-protocol.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://basefoundry.github.io/base-cli/schemas/v1/command-protocol.schema.json",
"title": "base-cli decoded command-protocol payload",
"description": "Schema for the decoded representation of COMMAND_PROTOCOL_V1 frames.",
"type": "object",
"additionalProperties": false,
"required": ["protocol_header", "record_type", "record_count", "records"],
"properties": {
"protocol_header": {"const": "COMMAND_PROTOCOL_V1"},
"record_type": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]*$"},
"record_count": {"type": "integer", "minimum": 0, "maximum": 1000000},
"records": {"type": "array", "maxItems": 1000000, "items": {"type": "object"}}
}
}
17 changes: 17 additions & 0 deletions lib/python/base_cli/schemas/v1/error.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://basefoundry.github.io/base-cli/schemas/v1/error.schema.json",
"title": "base-cli error envelope",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "schema", "code", "type", "message", "details", "run_id"],
"properties": {
"schema_version": {"const": 1},
"schema": {"const": "base-cli.error"},
"code": {"type": "string", "minLength": 1},
"type": {"const": "error"},
"message": {"type": "string"},
"details": {"type": "object"},
"run_id": {"type": ["string", "null"]}
}
}
15 changes: 15 additions & 0 deletions lib/python/base_cli/schemas/v1/inspection.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://basefoundry.github.io/base-cli/schemas/v1/inspection.schema.json",
"title": "base-cli inspection envelope",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "command", "status", "data", "error"],
"properties": {
"schema_version": {"const": 1},
"command": {"type": "string", "minLength": 1},
"status": {"enum": ["ok", "warn", "error"]},
"data": {"type": "object"},
"error": {"type": ["object", "null"]}
}
}
18 changes: 18 additions & 0 deletions lib/python/base_cli/schemas/v1/log.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://basefoundry.github.io/base-cli/schemas/v1/log.schema.json",
"title": "base-cli JSON log record",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "schema", "timestamp", "level", "logger", "message", "run_id"],
"properties": {
"schema_version": {"const": 1},
"schema": {"const": "base-cli.log"},
"timestamp": {"type": "string", "format": "date-time"},
"level": {"type": "string", "minLength": 1},
"logger": {"type": "string", "minLength": 1},
"message": {"type": "string", "maxLength": 8193},
"run_id": {"type": ["string", "null"]},
"details": {"type": "object"}
}
}
13 changes: 13 additions & 0 deletions lib/python/base_cli/schemas/v1/ndjson.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://basefoundry.github.io/base-cli/schemas/v1/ndjson.schema.json",
"title": "base-cli NDJSON record",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "schema", "record"],
"properties": {
"schema_version": {"const": 1},
"schema": {"const": "base-cli.record"},
"record": {"type": "object"}
}
}
17 changes: 17 additions & 0 deletions lib/python/base_cli/schemas/v1/output.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://basefoundry.github.io/base-cli/schemas/v1/output.schema.json",
"title": "base-cli success envelope",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "schema", "code", "type", "message", "details", "run_id"],
"properties": {
"schema_version": {"const": 1},
"schema": {"const": "base-cli.output"},
"code": {"type": "string", "minLength": 1},
"type": {"const": "success"},
"message": {"type": "string"},
"details": {"type": "object"},
"run_id": {"type": ["string", "null"]}
}
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ nav:
- Migration guide: migrations.md
- Output contracts: output-contracts.md
- JSON contracts: json-contracts.md
- JSON Schema artifacts: schemas.md
- Typed user configuration: user-config-typing.md
- Entry-point extensions: extensions.md
- Integrations:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ where = ["lib/python"]
version = {file = "VERSION"}

[tool.setuptools.package-data]
base_cli = ["py.typed"]
base_cli = ["py.typed", "schemas/v1/*.json"]

[tool.pytest.ini_options]
addopts = "-q"
Expand Down
1 change: 1 addition & 0 deletions scripts/validate_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"releasing.md",
"security-review.md",
"security-threat-model.md",
"schemas.md",
"typer-adapter.md",
"user-config-typing.md",
}
Expand Down
61 changes: 61 additions & 0 deletions scripts/validate_schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Validate and compare the packaged and documentation JSON Schema artifacts."""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import NoReturn

SCHEMA_ROOT = Path("schemas/v1")
REQUIRED_CONTRACT_KEYS = {"$schema", "$id", "title", "type", "required", "properties"}


def fail(message: str) -> NoReturn:
print(f"schema validation failed: {message}", file=sys.stderr)
raise SystemExit(1)


def _load(path: Path) -> object:
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
fail(f"cannot parse {path}: {exc}")


def validate(root: Path) -> None:
package_root = root / "lib/python/base_cli" / SCHEMA_ROOT
docs_root = root / "docs" / SCHEMA_ROOT
package_files = sorted(package_root.glob("*.json"))
docs_files = sorted(docs_root.glob("*.json"))
if not package_files:
fail(f"no schemas found under {package_root}")
if [path.name for path in package_files] != [path.name for path in docs_files]:
fail("packaged and documentation schema filenames differ")

for package_path, docs_path in zip(package_files, docs_files, strict=True):
package_payload = _load(package_path)
docs_payload = _load(docs_path)
if package_payload != docs_payload:
fail(f"packaged and documentation schemas differ for {package_path.name}")
if not isinstance(package_payload, dict) or not REQUIRED_CONTRACT_KEYS <= package_payload.keys():
fail(f"{package_path.name} is missing required schema metadata")
if package_payload.get("$schema") != "https://json-schema.org/draft/2020-12/schema":
fail(f"{package_path.name} must use JSON Schema draft 2020-12")
if package_payload.get("type") != "object":
fail(f"{package_path.name} must describe an object")
if not isinstance(package_payload.get("required"), list):
fail(f"{package_path.name} must declare required fields")
print(f"Validated {package_path.name}")


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("root", type=Path, nargs="?", default=Path(__file__).resolve().parents[1])
validate(parser.parse_args().root)


if __name__ == "__main__":
main()
39 changes: 39 additions & 0 deletions tests/test_validate_schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from __future__ import annotations

import json
import sys
import tempfile
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from scripts import validate_schemas


class SchemaValidationTests(unittest.TestCase):
def test_repository_schemas_are_valid_and_in_sync(self) -> None:
validate_schemas.validate(Path(__file__).resolve().parents[1])

def test_schema_drift_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
package = root / "lib/python/base_cli/schemas/v1"
docs = root / "docs/schemas/v1"
package.mkdir(parents=True)
docs.mkdir(parents=True)
payload = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.test/schema",
"title": "test",
"type": "object",
"required": [],
"properties": {},
}
(package / "test.json").write_text(json.dumps(payload), encoding="utf-8")
(docs / "test.json").write_text(json.dumps({**payload, "title": "drift"}), encoding="utf-8")
with self.assertRaises(SystemExit):
validate_schemas.validate(root)


if __name__ == "__main__":
unittest.main()
Loading