softschema 0.2.0__tar.gz → 0.2.1__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.
- {softschema-0.2.0 → softschema-0.2.1}/AGENTS.md +1 -1
- {softschema-0.2.0 → softschema-0.2.1}/PKG-INFO +12 -13
- {softschema-0.2.0 → softschema-0.2.1}/README.md +11 -12
- {softschema-0.2.0 → softschema-0.2.1}/docs/development.md +10 -12
- {softschema-0.2.0 → softschema-0.2.1}/docs/e2e-testing.runbook.md +31 -13
- {softschema-0.2.0 → softschema-0.2.1}/docs/installation.md +8 -8
- {softschema-0.2.0 → softschema-0.2.1}/docs/publishing.md +25 -10
- {softschema-0.2.0 → softschema-0.2.1}/docs/softschema-guide.md +11 -12
- {softschema-0.2.0 → softschema-0.2.1}/docs/softschema-python-design.md +4 -5
- {softschema-0.2.0 → softschema-0.2.1}/docs/softschema-spec.md +10 -10
- {softschema-0.2.0 → softschema-0.2.1}/docs/softschema-typescript-design.md +18 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/src/softschema/errors.py +1 -1
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/tests/test_cli.py +0 -13
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/tests/test_movie_page_example.py +6 -3
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/tests/test_soft_field.py +4 -9
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/tests/test_warning_codes.py +0 -5
- {softschema-0.2.0 → softschema-0.2.1}/.gitignore +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/LICENSE +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/docs/publishing-npm.md +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/examples/__init__.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/examples/movie_page/README.md +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/examples/movie_page/__init__.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/examples/movie_page/host_integration.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/examples/movie_page/model.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/examples/movie_page/movie-page.schema.yaml +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/examples/movie_page/spirited-away.md +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/examples/parity/__init__.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/examples/parity/model.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/examples/parity/parity.schema.yaml +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/README.md +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/src/softschema/__init__.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/src/softschema/canonicalize.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/src/softschema/cli.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/src/softschema/compile.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/src/softschema/generate.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/src/softschema/models.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/src/softschema/py.typed +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/src/softschema/registry.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/src/softschema/schema_view.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/src/softschema/soft_field.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/src/softschema/validate.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/tests/test_canonicalize.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/tests/test_core.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/tests/test_doc_topics_resolve.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/tests/test_enforced_extras.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/tests/test_errors.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/tests/test_generate.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/tests/test_metadata_rules.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/tests/test_parity.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/tests/test_schema_view.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/python/tests/test_skill_mirror_drift.py +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/typescript/LICENSE +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/packages/typescript/README.md +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/pyproject.toml +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/skills/softschema/SKILL.md +0 -0
- {softschema-0.2.0 → softschema-0.2.1}/tests/golden/README.md +0 -0
|
@@ -7,7 +7,7 @@ Start here:
|
|
|
7
7
|
- [softschema Guide](docs/softschema-guide.md): standalone concept and adoption guide
|
|
8
8
|
for humans and agents.
|
|
9
9
|
- [softschema Spec](docs/softschema-spec.md): exact language-neutral artifact format.
|
|
10
|
-
- [Movie
|
|
10
|
+
- [Movie Page Example](examples/movie_page/README.md): complete Python-backed example.
|
|
11
11
|
|
|
12
12
|
softschema ships two interchangeable implementations with the same CLI and library
|
|
13
13
|
surface: Python/Pydantic (`softschema`, `softschema-py`) and TypeScript/Zod
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: softschema
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Soft schema conventions and validation tools for Markdown/YAML artifacts
|
|
5
5
|
Project-URL: Homepage, https://github.com/jlevy/softschema
|
|
6
6
|
Project-URL: Repository, https://github.com/jlevy/softschema
|
|
@@ -29,12 +29,12 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
# softschema
|
|
30
30
|
|
|
31
31
|
Soft schemas: gradual, practical validation for Markdown/YAML artifacts that mix prose
|
|
32
|
-
and structured data
|
|
32
|
+
and structured data—built for humans and coding agents.
|
|
33
33
|
|
|
34
34
|
## Quick Start
|
|
35
35
|
|
|
36
36
|
Try it anywhere, with nothing installed but [uv](https://docs.astral.sh/uv/) or Node.
|
|
37
|
-
Print the bundled example artifact and its compiled schema, then validate
|
|
37
|
+
Print the bundled example artifact and its compiled schema, then validate—the artifact
|
|
38
38
|
is fully self-describing, so no flags are needed:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
@@ -63,7 +63,7 @@ flexible document context and machine-readable values.
|
|
|
63
63
|
The idea is quite simple, but I’ve found it non-obvious enough that coding agents do not
|
|
64
64
|
come up with this approach themselves.
|
|
65
65
|
|
|
66
|
-
However if given the information and tools in this repo, soft schemas are unreasonably
|
|
66
|
+
However, if given the information and tools in this repo, soft schemas are unreasonably
|
|
67
67
|
effective. Agents become far better at designing and building complex workflows that mix
|
|
68
68
|
structured and unstructured data, such as document processing, data extraction,
|
|
69
69
|
scientific or financial analyses, and many similar applications.
|
|
@@ -203,8 +203,8 @@ flags.
|
|
|
203
203
|
|
|
204
204
|
Every key after `contract` is optional; a minimal artifact carries `contract` alone and
|
|
205
205
|
binds its schema some other way (a `--schema` flag, or a host registry in library use).
|
|
206
|
-
Contract IDs follow an enforced shape, `[namespace:]Name[/version]
|
|
207
|
-
`example.movies:MoviePage/v1` or `com.acme.docs:IncidentReview/1.0
|
|
206
|
+
Contract IDs follow an enforced shape, `[namespace:]Name[/version]`—for example
|
|
207
|
+
`example.movies:MoviePage/v1` or `com.acme.docs:IncidentReview/1.0`—naming a payload
|
|
208
208
|
contract, not a class or import path.
|
|
209
209
|
|
|
210
210
|
## Validate
|
|
@@ -220,7 +220,7 @@ softschema validate doc.md --envelope incident # designate the payload
|
|
|
220
220
|
|
|
221
221
|
`validate` reports structural (JSON Schema) and semantic (Pydantic/Zod model) results
|
|
222
222
|
separately as deterministic JSON. Semantic validation loads a model with
|
|
223
|
-
`--model module:Class` (Python) or `--model path:export` (Zod)
|
|
223
|
+
`--model module:Class` (Python) or `--model path:export` (Zod)—note that `--model`
|
|
224
224
|
imports and executes local code, so use it only with trusted models; a compiled schema
|
|
225
225
|
via `--schema` executes nothing and is the safe path for untrusted input.
|
|
226
226
|
|
|
@@ -232,8 +232,8 @@ Two supported ways to consume softschema; pick by use:
|
|
|
232
232
|
fast, offline, and the only way to `import` it):
|
|
233
233
|
|
|
234
234
|
```bash
|
|
235
|
-
uv add --dev softschema==0.2.
|
|
236
|
-
npm install -D softschema@0.2.
|
|
235
|
+
uv add --dev softschema==0.2.1 # Python
|
|
236
|
+
npm install -D softschema@0.2.1 # Node (or: bun add -d)
|
|
237
237
|
```
|
|
238
238
|
|
|
239
239
|
- **Zero-install** for one-off checks and agent bootstrap:
|
|
@@ -243,10 +243,9 @@ Two supported ways to consume softschema; pick by use:
|
|
|
243
243
|
npx softschema@latest --help
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
-
The rule of thumb: if softschema runs more than once, or in CI, or you import it
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
See [Installation](docs/installation.md) for details, including the supply-chain
|
|
246
|
+
The rule of thumb: if softschema runs more than once, or in CI, or you import it—pin it.
|
|
247
|
+
For a quick check or an agent bootstrapping with nothing installed, use a zero-install
|
|
248
|
+
runner. See [Installation](docs/installation.md) for details, including the supply-chain
|
|
250
249
|
cool-off that makes `@latest` safe to recommend.
|
|
251
250
|
|
|
252
251
|
## Use as a Library
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# softschema
|
|
2
2
|
|
|
3
3
|
Soft schemas: gradual, practical validation for Markdown/YAML artifacts that mix prose
|
|
4
|
-
and structured data
|
|
4
|
+
and structured data—built for humans and coding agents.
|
|
5
5
|
|
|
6
6
|
## Quick Start
|
|
7
7
|
|
|
8
8
|
Try it anywhere, with nothing installed but [uv](https://docs.astral.sh/uv/) or Node.
|
|
9
|
-
Print the bundled example artifact and its compiled schema, then validate
|
|
9
|
+
Print the bundled example artifact and its compiled schema, then validate—the artifact
|
|
10
10
|
is fully self-describing, so no flags are needed:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
@@ -35,7 +35,7 @@ flexible document context and machine-readable values.
|
|
|
35
35
|
The idea is quite simple, but I’ve found it non-obvious enough that coding agents do not
|
|
36
36
|
come up with this approach themselves.
|
|
37
37
|
|
|
38
|
-
However if given the information and tools in this repo, soft schemas are unreasonably
|
|
38
|
+
However, if given the information and tools in this repo, soft schemas are unreasonably
|
|
39
39
|
effective. Agents become far better at designing and building complex workflows that mix
|
|
40
40
|
structured and unstructured data, such as document processing, data extraction,
|
|
41
41
|
scientific or financial analyses, and many similar applications.
|
|
@@ -175,8 +175,8 @@ flags.
|
|
|
175
175
|
|
|
176
176
|
Every key after `contract` is optional; a minimal artifact carries `contract` alone and
|
|
177
177
|
binds its schema some other way (a `--schema` flag, or a host registry in library use).
|
|
178
|
-
Contract IDs follow an enforced shape, `[namespace:]Name[/version]
|
|
179
|
-
`example.movies:MoviePage/v1` or `com.acme.docs:IncidentReview/1.0
|
|
178
|
+
Contract IDs follow an enforced shape, `[namespace:]Name[/version]`—for example
|
|
179
|
+
`example.movies:MoviePage/v1` or `com.acme.docs:IncidentReview/1.0`—naming a payload
|
|
180
180
|
contract, not a class or import path.
|
|
181
181
|
|
|
182
182
|
## Validate
|
|
@@ -192,7 +192,7 @@ softschema validate doc.md --envelope incident # designate the payload
|
|
|
192
192
|
|
|
193
193
|
`validate` reports structural (JSON Schema) and semantic (Pydantic/Zod model) results
|
|
194
194
|
separately as deterministic JSON. Semantic validation loads a model with
|
|
195
|
-
`--model module:Class` (Python) or `--model path:export` (Zod)
|
|
195
|
+
`--model module:Class` (Python) or `--model path:export` (Zod)—note that `--model`
|
|
196
196
|
imports and executes local code, so use it only with trusted models; a compiled schema
|
|
197
197
|
via `--schema` executes nothing and is the safe path for untrusted input.
|
|
198
198
|
|
|
@@ -204,8 +204,8 @@ Two supported ways to consume softschema; pick by use:
|
|
|
204
204
|
fast, offline, and the only way to `import` it):
|
|
205
205
|
|
|
206
206
|
```bash
|
|
207
|
-
uv add --dev softschema==0.2.
|
|
208
|
-
npm install -D softschema@0.2.
|
|
207
|
+
uv add --dev softschema==0.2.1 # Python
|
|
208
|
+
npm install -D softschema@0.2.1 # Node (or: bun add -d)
|
|
209
209
|
```
|
|
210
210
|
|
|
211
211
|
- **Zero-install** for one-off checks and agent bootstrap:
|
|
@@ -215,10 +215,9 @@ Two supported ways to consume softschema; pick by use:
|
|
|
215
215
|
npx softschema@latest --help
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
-
The rule of thumb: if softschema runs more than once, or in CI, or you import it
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
See [Installation](docs/installation.md) for details, including the supply-chain
|
|
218
|
+
The rule of thumb: if softschema runs more than once, or in CI, or you import it—pin it.
|
|
219
|
+
For a quick check or an agent bootstrapping with nothing installed, use a zero-install
|
|
220
|
+
runner. See [Installation](docs/installation.md) for details, including the supply-chain
|
|
222
221
|
cool-off that makes `@latest` safe to recommend.
|
|
223
222
|
|
|
224
223
|
## Use as a Library
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# Development
|
|
2
2
|
|
|
3
3
|
First-time setup of `uv` and Python is covered in [Installation](installation.md).
|
|
4
|
-
Release workflow and PyPI steps are covered in
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
clean-environment checks CI cannot run — is codified in the
|
|
4
|
+
Release workflow and PyPI steps are covered in [Publishing](publishing.md).
|
|
5
|
+
The full validation pass—the automated sweep run locally plus the manual
|
|
6
|
+
clean-environment checks CI cannot run—is codified in the
|
|
8
7
|
[end-to-end testing runbook](e2e-testing.runbook.md).
|
|
9
8
|
|
|
10
9
|
Set up the repo (Python deps, Node tooling for hooks, and the git hooks themselves):
|
|
@@ -39,7 +38,7 @@ uv build
|
|
|
39
38
|
|
|
40
39
|
The Python package is built from `packages/python/src/softschema`.
|
|
41
40
|
|
|
42
|
-
### Git
|
|
41
|
+
### Git Hooks (This Repo)
|
|
43
42
|
|
|
44
43
|
Hooks are managed by [lefthook](https://lefthook.dev) (`lefthook.yml`), installed with
|
|
45
44
|
`make hooks-install`. The `pre-commit` hook formats staged changes so commits stay
|
|
@@ -69,8 +68,7 @@ bun run publint # lint the publishable package layout (run after build)
|
|
|
69
68
|
|
|
70
69
|
It publishes to npm as `softschema` (the same name as the PyPI package) and exposes the
|
|
71
70
|
CLI as both `softschema` and `softschema-ts`. The two packages **release together under
|
|
72
|
-
one version number**; see
|
|
73
|
-
[Publishing](https://github.com/jlevy/softschema/blob/main/docs/publishing.md).
|
|
71
|
+
one version number**; see [Publishing](publishing.md).
|
|
74
72
|
|
|
75
73
|
Documentation changes should follow `common-doc-guidelines.md`
|
|
76
74
|
(github.com/jlevy/practical-prose).
|
|
@@ -81,7 +79,7 @@ exact format rules in `docs/softschema-spec.md`.
|
|
|
81
79
|
|
|
82
80
|
Two softschema checks belong in CI for any project that depends on the package.
|
|
83
81
|
|
|
84
|
-
### Compiled
|
|
82
|
+
### Compiled Schema Drift
|
|
85
83
|
|
|
86
84
|
A committed `.schema.yaml` file is *generated, but committed*. Run
|
|
87
85
|
`softschema compile ... --check` to fail the build when the committed compiled schema
|
|
@@ -96,10 +94,10 @@ uv run softschema compile examples.movie_page.model:MoviePage \
|
|
|
96
94
|
Fix on drift: re-run the same command without `--check` and commit the regenerated
|
|
97
95
|
compiled schema.
|
|
98
96
|
|
|
99
|
-
### Generated-
|
|
97
|
+
### Generated-Section Drift
|
|
100
98
|
|
|
101
99
|
If any Markdown file contains `softschema:generated` markers (see the guide’s “Keep
|
|
102
|
-
Schema Tables
|
|
100
|
+
Schema Tables in Sync with Generated Sections” playbook), run the re-renderer in
|
|
103
101
|
`--check` mode so CI fails when the committed section lags behind the schema:
|
|
104
102
|
|
|
105
103
|
```bash
|
|
@@ -108,7 +106,7 @@ uv run softschema generate examples/movie_page/README.md --check
|
|
|
108
106
|
|
|
109
107
|
Fix on drift: re-run without `--check` and commit the regenerated section.
|
|
110
108
|
|
|
111
|
-
### Artifact
|
|
109
|
+
### Artifact Validation
|
|
112
110
|
|
|
113
111
|
Run `softschema validate` against every artifact under version control whose contract is
|
|
114
112
|
fully defined:
|
|
@@ -154,7 +152,7 @@ jobs:
|
|
|
154
152
|
--schema examples/movie_page/movie-page.schema.yaml
|
|
155
153
|
```
|
|
156
154
|
|
|
157
|
-
### Pre-
|
|
155
|
+
### Pre-Commit Hook
|
|
158
156
|
|
|
159
157
|
For local runs before push, a `pre-commit` config that calls the same drift check:
|
|
160
158
|
|
|
@@ -38,7 +38,7 @@ The automated half of the story runs on every push and PR
|
|
|
38
38
|
| Python-vs-TypeScript byte parity | `cross-impl` job |
|
|
39
39
|
| Tag ↔ `package.json` version match | `publish.yml` guard |
|
|
40
40
|
|
|
41
|
-
**Not** covered by CI
|
|
41
|
+
**Not** covered by CI—the reason this runbook exists:
|
|
42
42
|
|
|
43
43
|
- Installing the built wheel and npm tarball into clean environments and running them
|
|
44
44
|
(entry points, bin shebang, bundled docs/skill resources).
|
|
@@ -86,7 +86,7 @@ This phase proves the **packaged artifacts** from a real install with no repo on
|
|
|
86
86
|
the console entry points resolve, the bin shebang runs under plain Node, and the bundled
|
|
87
87
|
docs/skill resources load from inside the installed package.
|
|
88
88
|
|
|
89
|
-
### Python
|
|
89
|
+
### Python Wheel in a Fresh Venv
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
uv build
|
|
@@ -103,10 +103,10 @@ venv/bin/softschema validate spirited-away.md # exit 0, structural ok, zero fl
|
|
|
103
103
|
cd - && rm -rf "$tmp"
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
### npm
|
|
106
|
+
### npm Tarball under Plain Node
|
|
107
107
|
|
|
108
|
-
`npm pack` runs `prepublishOnly` (build
|
|
109
|
-
from source
|
|
108
|
+
`npm pack` runs `prepublishOnly` (build and publint), so the tarball is freshly rebuilt
|
|
109
|
+
from source—the same artifact `npm publish` would upload.
|
|
110
110
|
Run it under `node` (>= 22.12), the runtime npm users get, not under bun:
|
|
111
111
|
|
|
112
112
|
```bash
|
|
@@ -153,7 +153,7 @@ diff movie-page.schema.yaml ts-schema.yaml # byte-identical
|
|
|
153
153
|
One trap to know about: the artifact names its own schema
|
|
154
154
|
(`schema: movie-page.schema.yaml`), so the schema **must** be saved under that exact
|
|
155
155
|
filename next to the artifact.
|
|
156
|
-
Redirecting it anywhere else makes `validate` fail with `schema_missing
|
|
156
|
+
Redirecting it anywhere else makes `validate` fail with `schema_missing`—correct
|
|
157
157
|
resolution behavior, broken quickstart.
|
|
158
158
|
If the README quickstart ever changes, re-verify the new text the same way before
|
|
159
159
|
merging it.
|
|
@@ -177,21 +177,39 @@ present on disk.
|
|
|
177
177
|
|
|
178
178
|
The tagging and release mechanics live in [publishing.md](publishing.md); this phase is
|
|
179
179
|
what to run **after** `publish.yml` reports success for both registries.
|
|
180
|
-
|
|
181
|
-
version
|
|
180
|
+
|
|
181
|
+
A just-published version sits inside the 14-day supply-chain cool-off, so the Python
|
|
182
|
+
smoke test must override the cutoff.
|
|
183
|
+
Three details make the difference between a real check and a false “no version” failure:
|
|
184
|
+
|
|
185
|
+
- **Override the cutoff to *now*, not midnight.** `--exclude-newer-package` excludes
|
|
186
|
+
releases newer than the given instant.
|
|
187
|
+
A date-only value (`$(date +%F)`) is midnight UTC, which is *before* a version
|
|
188
|
+
published later the same day—so it excludes the very release you are verifying.
|
|
189
|
+
Use a full timestamp at the current moment (`$(date -u +%Y-%m-%dT%H:%M:%SZ)`); “now”
|
|
190
|
+
is always after the publish.
|
|
191
|
+
- **Run from outside the repo.** From the project tree, uv reads its `[tool.uv]`
|
|
192
|
+
settings and applies the project’s own cool-off; a neutral directory (`cd /tmp`)
|
|
193
|
+
avoids that.
|
|
194
|
+
- **`--refresh`** bypasses uv’s cached package index, which may not yet list a release
|
|
195
|
+
published seconds ago.
|
|
182
196
|
|
|
183
197
|
```bash
|
|
184
|
-
|
|
185
|
-
|
|
198
|
+
cd /tmp
|
|
199
|
+
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
200
|
+
uvx --refresh --exclude-newer-package "softschema=$ts" softschema@X.Y.Z --version
|
|
201
|
+
npx -y softschema@X.Y.Z --version
|
|
186
202
|
```
|
|
187
203
|
|
|
188
204
|
Then run the quickstart literally against the published artifacts:
|
|
189
205
|
|
|
190
206
|
```bash
|
|
191
207
|
cd "$(mktemp -d)"
|
|
192
|
-
|
|
193
|
-
uvx --exclude-newer-package "softschema=$
|
|
194
|
-
uvx
|
|
208
|
+
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
209
|
+
uvx() { command uvx --refresh --exclude-newer-package "softschema=$ts" "$@"; }
|
|
210
|
+
uvx softschema@X.Y.Z docs example-artifact > spirited-away.md
|
|
211
|
+
uvx softschema@X.Y.Z docs example-schema > movie-page.schema.yaml
|
|
212
|
+
uvx softschema@X.Y.Z validate spirited-away.md
|
|
195
213
|
```
|
|
196
214
|
|
|
197
215
|
Once the release ages past the cool-off, the unpinned `@latest` forms in the README
|
|
@@ -9,11 +9,11 @@ Pick the runtime you already have; both validate against the same canonical sche
|
|
|
9
9
|
| | Pin as a dependency | Zero-install (`uvx` / `npx`) |
|
|
10
10
|
| --- | --- | --- |
|
|
11
11
|
| **For** | Projects, CI gates, library use | One-off checks, agent bootstrap |
|
|
12
|
-
| **Reproducible** | Yes
|
|
13
|
-
| **Fast / offline** | Yes
|
|
14
|
-
| **Library import** | Yes
|
|
12
|
+
| **Reproducible** | Yes—the version is locked in `uv.lock` / `package-lock.json` | Only if you pin the runner (`uvx softschema@0.2.1`) |
|
|
13
|
+
| **Fast / offline** | Yes—the binary is already on disk | Cold-start fetch; needs the network |
|
|
14
|
+
| **Library import** | Yes—the only way | No |
|
|
15
15
|
|
|
16
|
-
The rule of thumb: **if softschema runs more than once, or in CI, or you import it
|
|
16
|
+
The rule of thumb: **if softschema runs more than once, or in CI, or you import it—pin
|
|
17
17
|
it as a dependency.
|
|
18
18
|
For a quick one-off or an agent bootstrapping with nothing installed,
|
|
19
19
|
use a zero-install runner**, pinned where the result must be repeatable.
|
|
@@ -23,14 +23,14 @@ use a zero-install runner**, pinned where the result must be repeatable.
|
|
|
23
23
|
Python (a dev dependency, or a persistent user tool):
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
uv add --dev softschema==0.2.
|
|
26
|
+
uv add --dev softschema==0.2.1 # project dev dependency; run via `uv run softschema`
|
|
27
27
|
uv tool install softschema # persistent CLI on your PATH
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Node (>= 22.12):
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
npm install -D softschema@0.2.
|
|
33
|
+
npm install -D softschema@0.2.1 # or: pnpm add -D / bun add -d
|
|
34
34
|
npx softschema --help # resolves the local pinned copy
|
|
35
35
|
```
|
|
36
36
|
|
|
@@ -41,7 +41,7 @@ uvx softschema@latest --help # Python implementation, ephemeral
|
|
|
41
41
|
npx softschema@latest --help # Node implementation, ephemeral
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
Use `uvx softschema@0.2.
|
|
44
|
+
Use `uvx softschema@0.2.1` / `npx -y softschema@0.2.1` when a repeated ad-hoc run must
|
|
45
45
|
resolve the same version every time.
|
|
46
46
|
|
|
47
47
|
## Quick Start for Agents
|
|
@@ -79,7 +79,7 @@ A gate such as npm’s `--before` / `NPM_CONFIG_BEFORE`, pnpm’s `minimumReleas
|
|
|
79
79
|
uv’s `--exclude-newer` resolves `@latest` to the newest release old enough to pass, so
|
|
80
80
|
you get the freshest vetted version without pinning.
|
|
81
81
|
A just-published version installs only once it ages past the cutoff.
|
|
82
|
-
Consumer projects should still pin their own dependency
|
|
82
|
+
Consumer projects should still pin their own dependency—a project’s reproducibility is
|
|
83
83
|
the project’s responsibility, not the publisher’s cool-off.
|
|
84
84
|
See [supply-chain-hardening](https://github.com/jlevy/supply-chain-hardening) for the
|
|
85
85
|
rationale.
|
|
@@ -102,16 +102,25 @@ For each release of version `X.Y.Z`:
|
|
|
102
102
|
git push origin vX.Y.Z
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
If the environment blocks pushing tags over git (some hosted or proxied checkouts
|
|
106
|
+
allow branch pushes but reject tag refs with a `403`), skip the `git push` and let
|
|
107
|
+
the next step create the tag: passing `--target` with the merge-commit SHA to
|
|
108
|
+
`gh release create` creates the tag at that commit as part of publishing the release.
|
|
109
|
+
|
|
105
110
|
6. **Create the GitHub release** (this triggers the publish workflow, which publishes to
|
|
106
111
|
both PyPI and npm over OIDC in one run):
|
|
107
112
|
|
|
108
113
|
```bash
|
|
109
|
-
gh release create vX.Y.Z --title "softschema X.Y.Z" --notes
|
|
114
|
+
gh release create vX.Y.Z --title "softschema X.Y.Z" --notes-file notes.md
|
|
115
|
+
# when the tag was not pushed in step 5, create it here at the merge commit:
|
|
116
|
+
gh release create vX.Y.Z --target MERGE_SHA --title "softschema X.Y.Z" --notes-file notes.md
|
|
110
117
|
```
|
|
111
118
|
|
|
112
|
-
Write the notes from the release’s behavior
|
|
113
|
-
the shape: a one-line summary, then
|
|
114
|
-
|
|
119
|
+
Write the notes from the release’s behavior and breaking changes (see prior releases
|
|
120
|
+
for the shape: a one-line summary, then sections for breaking changes, new features,
|
|
121
|
+
fixes and hardening, and testing and release safety).
|
|
122
|
+
Use a notes file for multi-section notes; the inline `--notes` flag is fine only for
|
|
123
|
+
a one-liner.
|
|
115
124
|
|
|
116
125
|
7. **Watch the workflow** until both `Publish to PyPI` and `Publish to npm` report
|
|
117
126
|
success:
|
|
@@ -124,14 +133,20 @@ For each release of version `X.Y.Z`:
|
|
|
124
133
|
artifacts (end-to-end runbook Phase 5):
|
|
125
134
|
|
|
126
135
|
```bash
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
cd /tmp # outside the repo: do not inherit its [tool.uv] cool-off
|
|
137
|
+
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" # cutoff = now, so a same-day publish is included
|
|
138
|
+
uvx --refresh --exclude-newer-package "softschema=$ts" softschema@X.Y.Z --version
|
|
139
|
+
npx -y softschema@X.Y.Z --version
|
|
129
140
|
```
|
|
130
141
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
142
|
+
The `--exclude-newer-package` override is needed because a freshly published version
|
|
143
|
+
sits inside the 14-day supply-chain cool-off; anyone consuming it under that policy
|
|
144
|
+
sees the same friction for ~14 days, which is intentional.
|
|
145
|
+
Three details matter for the smoke test: use a full timestamp at the current moment
|
|
146
|
+
(a date-only `$(date +%F)` is midnight UTC and excludes a version published later the
|
|
147
|
+
same day), run from outside the repo so uv does not apply the project’s own cool-off,
|
|
148
|
+
and pass `--refresh` so uv does not serve a cached index that predates the publish
|
|
149
|
+
(see runbook Phase 5).
|
|
135
150
|
|
|
136
151
|
<!-- This document follows common-doc-guidelines.md.
|
|
137
152
|
See github.com/jlevy/practical-prose and review guidelines before editing.
|
|
@@ -50,7 +50,7 @@ against a named contract.
|
|
|
50
50
|
The practice is language neutral; another project could implement it with any of JSON
|
|
51
51
|
Schema, database records, or hand-written validators.
|
|
52
52
|
|
|
53
|
-
## When
|
|
53
|
+
## When to Use It
|
|
54
54
|
|
|
55
55
|
Reach for softschema when all three of these hold:
|
|
56
56
|
|
|
@@ -178,7 +178,7 @@ Picking a version:
|
|
|
178
178
|
- Additive, optional fields usually don’t need a version bump.
|
|
179
179
|
- Keep versions short (`v1`, `v2`, `1.0`).
|
|
180
180
|
|
|
181
|
-
## Playbook: Adopt softschema
|
|
181
|
+
## Playbook: Adopt softschema for an Existing Markdown Artifact
|
|
182
182
|
|
|
183
183
|
Start with one document type, not a whole repository:
|
|
184
184
|
|
|
@@ -234,7 +234,7 @@ The body stays unchanged.
|
|
|
234
234
|
A consumer that aggregates incidents now reads `incident.affected_service` from YAML
|
|
235
235
|
instead of trying to grep the body.
|
|
236
236
|
|
|
237
|
-
## Playbook: Choose Which Values Belong
|
|
237
|
+
## Playbook: Choose Which Values Belong in YAML
|
|
238
238
|
|
|
239
239
|
The hardest call in adoption is “what goes in YAML, what stays prose?”
|
|
240
240
|
Use the promotion path step by step:
|
|
@@ -270,7 +270,7 @@ file. The contract ID stays; only the shell changes.
|
|
|
270
270
|
A field is ready to promote when: a consumer extracts it, the value type is stable, and
|
|
271
271
|
emitting it consistently is easier than parsing it from prose.
|
|
272
272
|
|
|
273
|
-
## Playbook: Inline Frontmatter
|
|
273
|
+
## Playbook: Inline Frontmatter vs. Companion Data
|
|
274
274
|
|
|
275
275
|
The rule of thumb is **inline-small, companion-large**:
|
|
276
276
|
|
|
@@ -397,7 +397,7 @@ The `result` object reports `structural` (JSON Schema) and `semantic` (Pydantic)
|
|
|
397
397
|
separately, so callers can distinguish “shape was wrong” from “cross-field invariant
|
|
398
398
|
failed” without parsing error strings.
|
|
399
399
|
|
|
400
|
-
## Playbook: Annotate Fields
|
|
400
|
+
## Playbook: Annotate Fields with SoftField
|
|
401
401
|
|
|
402
402
|
`SoftField` is an optional wrapper over Pydantic’s `Field` that records per-field
|
|
403
403
|
authoring metadata (`group`, `owner`, `tier`, `instruction`, `examples`, `aliases`,
|
|
@@ -435,7 +435,7 @@ controlled-vocabulary case.
|
|
|
435
435
|
The recognized keys and the full call shape are documented in
|
|
436
436
|
[Python Package Design](softschema-python-design.md).
|
|
437
437
|
|
|
438
|
-
## Playbook: Keep Schema Tables
|
|
438
|
+
## Playbook: Keep Schema Tables in Sync with Generated Sections
|
|
439
439
|
|
|
440
440
|
When a controlled vocabulary or field list appears in two places (a schema and a runbook
|
|
441
441
|
table), it will drift.
|
|
@@ -478,7 +478,7 @@ A worked example lives in
|
|
|
478
478
|
[examples/movie_page/README.md](../examples/movie_page/README.md); the “Schema Enums”
|
|
479
479
|
section is regenerated from the movie schema.
|
|
480
480
|
|
|
481
|
-
## Playbook: Validate
|
|
481
|
+
## Playbook: Validate in CI
|
|
482
482
|
|
|
483
483
|
Pin softschema as a dev dependency so CI uses a known version:
|
|
484
484
|
|
|
@@ -521,7 +521,7 @@ Two checks belong in CI:
|
|
|
521
521
|
For a full GitHub Actions snippet and a `pre-commit` hook example, see the “Continuous
|
|
522
522
|
integration” section of [docs/development.md](development.md).
|
|
523
523
|
|
|
524
|
-
## Playbook: Migrate
|
|
524
|
+
## Playbook: Migrate an Existing Artifact
|
|
525
525
|
|
|
526
526
|
Take an artifact that doesn’t fit the canonical shape and bring it in line.
|
|
527
527
|
|
|
@@ -631,7 +631,7 @@ The consumer reads YAML now.
|
|
|
631
631
|
For each migration, set `status: soft` or `permissive` initially.
|
|
632
632
|
Tighten only after existing instances validate cleanly.
|
|
633
633
|
|
|
634
|
-
## Playbook: Use softschema
|
|
634
|
+
## Playbook: Use softschema with Agents
|
|
635
635
|
|
|
636
636
|
softschema is built for documents that humans and coding agents both write.
|
|
637
637
|
A few patterns help agents do the right thing:
|
|
@@ -686,7 +686,7 @@ A few patterns help agents do the right thing:
|
|
|
686
686
|
- **Promoting prose that no consumer reads.** Leave background, analysis, and caveats as
|
|
687
687
|
prose. Promote a value only when a code path, QA check, or aggregation reads it.
|
|
688
688
|
|
|
689
|
-
## Relationship
|
|
689
|
+
## Relationship to the Packages
|
|
690
690
|
|
|
691
691
|
Two interchangeable packages implement the language-neutral pattern at exact behavioral
|
|
692
692
|
parity, Python/Pydantic and TypeScript/Zod.
|
|
@@ -759,8 +759,7 @@ For Python-specific module layout, public API decisions, and dependency boundary
|
|
|
759
759
|
- [Movie Page Example](../examples/movie_page/README.md): the complete public example
|
|
760
760
|
backing the snippets above.
|
|
761
761
|
- [Installation](installation.md), [Development](development.md), and
|
|
762
|
-
[Publishing](
|
|
763
|
-
workflow docs.
|
|
762
|
+
[Publishing](publishing.md): workflow docs.
|
|
764
763
|
|
|
765
764
|
<!-- This document follows common-doc-guidelines.md.
|
|
766
765
|
See github.com/jlevy/practical-prose and review guidelines before editing.
|
|
@@ -48,8 +48,7 @@ It should orient a new visitor, show the main example, and point to the guide an
|
|
|
48
48
|
rather than repeating their content.
|
|
49
49
|
|
|
50
50
|
Template workflow docs keep template names: [development.md](development.md),
|
|
51
|
-
[installation.md](installation.md), and
|
|
52
|
-
[publishing.md](https://github.com/jlevy/softschema/blob/main/docs/publishing.md).
|
|
51
|
+
[installation.md](installation.md), and [publishing.md](publishing.md).
|
|
53
52
|
Package design details live in this document unless they grow large enough to justify a
|
|
54
53
|
separate reference.
|
|
55
54
|
|
|
@@ -183,7 +182,7 @@ A relative `schema_path` is resolved against only the document directory and the
|
|
|
183
182
|
working directory, so resolution is predictable and never binds to an unrelated compiled
|
|
184
183
|
schema in a parent directory.
|
|
185
184
|
|
|
186
|
-
### Engine-
|
|
185
|
+
### Engine-Neutral Structural Errors
|
|
187
186
|
|
|
188
187
|
Structural validation runs through `jsonschema`, but the error records it returns are
|
|
189
188
|
synthesized by softschema, not passed through from the library.
|
|
@@ -204,7 +203,7 @@ to stderr; the package version comes from `importlib.metadata` via
|
|
|
204
203
|
`softschema-py` (the latter pairs with a future `softschema-ts` for the shared golden
|
|
205
204
|
corpus).
|
|
206
205
|
|
|
207
|
-
### Skill
|
|
206
|
+
### Skill Resource and Mirrors
|
|
208
207
|
|
|
209
208
|
`skills/softschema/SKILL.md` is the source skill.
|
|
210
209
|
`softschema skill --install` writes full generated copies to
|
|
@@ -242,7 +241,7 @@ if any(w.code.startswith("document-") for w in result.warnings):
|
|
|
242
241
|
A regression test (`tests/test_warning_codes.py`) holds the table to the enum: any new
|
|
243
242
|
emitted code that isn’t a `WarningCode` member fails CI.
|
|
244
243
|
|
|
245
|
-
### Structural
|
|
244
|
+
### Structural Error Kinds
|
|
246
245
|
|
|
247
246
|
`StructuralResult.errors[*].kind` uses a separate `snake_case` namespace because errors
|
|
248
247
|
are blocking, not advisory.
|
|
@@ -41,7 +41,7 @@ These terms are used throughout; each is defined here before it appears in a rul
|
|
|
41
41
|
| **metadata block** | The `softschema:` mapping in the frontmatter (or at the document root for pure YAML). It holds softschema’s own keys, not payload data. |
|
|
42
42
|
| **payload** | The structured values a consumer reads, validated against a contract. |
|
|
43
43
|
| **envelope** | The single top-level key whose value is the payload; the **envelope key** is its name (for example `movie:`). |
|
|
44
|
-
| **contract** | The named payload contract
|
|
44
|
+
| **contract** | The named payload contract—*what* the payload is. |
|
|
45
45
|
| **contract ID** | The string that names the contract (for example `example.movies:MoviePage/v1`). |
|
|
46
46
|
| **model** | A Pydantic class or Zod schema: a language-specific *source* for a contract. |
|
|
47
47
|
| **compiled schema** | The JSON Schema (written as YAML or JSON) that a model compiles to; the language-neutral form of the contract. |
|
|
@@ -212,7 +212,7 @@ An implementation must:
|
|
|
212
212
|
|
|
213
213
|
For example, the movie artifact above carries both `title:` and `movie:`. With two
|
|
214
214
|
non-`softschema` keys, inference does not apply, so the artifact declares
|
|
215
|
-
`envelope: movie` in its metadata block
|
|
215
|
+
`envelope: movie` in its metadata block—`title:` stays an uninterpreted host key, and
|
|
216
216
|
the artifact validates with no flags.
|
|
217
217
|
A caller can still override with `--envelope` on a given run.
|
|
218
218
|
|
|
@@ -242,7 +242,7 @@ product-tag `namespace`, and short versions (`v1`, `1.0`). Examples:
|
|
|
242
242
|
- `com.acme.docs:IncidentReview/1.0`
|
|
243
243
|
|
|
244
244
|
A contract ID may correspond to a Pydantic class, a Zod export, a precompiled JSON
|
|
245
|
-
Schema, a database record, or a hand-authored validator
|
|
245
|
+
Schema, a database record, or a hand-authored validator—all equally valid.
|
|
246
246
|
It is not required to be an import path or a class name.
|
|
247
247
|
|
|
248
248
|
## Status Values
|
|
@@ -282,7 +282,7 @@ Markdown body prose and tables are reader-facing and never authoritative.
|
|
|
282
282
|
|
|
283
283
|
A compiled schema is a generated validation contract, usually JSON Schema written as
|
|
284
284
|
YAML. It is the language-neutral form of a contract: a Pydantic class or Zod schema
|
|
285
|
-
compiles to it (provably identically
|
|
285
|
+
compiles to it (provably identically—the conformance machinery guarantees an equal
|
|
286
286
|
`schema_sha256`), and any language can validate against it.
|
|
287
287
|
|
|
288
288
|
An artifact may bind to its compiled schema with the optional `softschema.schema` key.
|
|
@@ -292,7 +292,7 @@ The compiled schema a validator uses is resolved in this precedence (highest fir
|
|
|
292
292
|
library call.
|
|
293
293
|
2. A host registry binding (a registered contract’s schema path; library path only).
|
|
294
294
|
3. The `softschema.schema` document metadata.
|
|
295
|
-
4. None
|
|
295
|
+
4. None—a metadata-only check (contract/status/envelope rules, no schema).
|
|
296
296
|
|
|
297
297
|
Host-controlled configuration outranks document metadata on purpose: a document must not
|
|
298
298
|
silently redirect a host’s validation to a schema the host did not choose.
|
|
@@ -302,8 +302,8 @@ none, which is what lets a self-describing artifact validate with no flags.
|
|
|
302
302
|
Resolution of a `softschema.schema` value is, by convention, relative to the document
|
|
303
303
|
that carries it; this spec requires only that the value be a non-empty string and leaves
|
|
304
304
|
the exact resolution to the host, because file layout is situational.
|
|
305
|
-
(The reference CLIs accept only relative values in metadata
|
|
306
|
-
`--schema
|
|
305
|
+
(The reference CLIs accept only relative values in metadata—an absolute path must use
|
|
306
|
+
`--schema`—resolve them from the document’s directory, and reject a path whose
|
|
307
307
|
normalized result escapes both the document directory and the working directory.)
|
|
308
308
|
|
|
309
309
|
A compiled schema is not a per-document companion data file.
|
|
@@ -366,8 +366,8 @@ Recognized attributes:
|
|
|
366
366
|
file path. This is a 0.2.0 change; a marker that still uses `contract="...path..."` is
|
|
367
367
|
rejected with a message pointing at the rename.)
|
|
368
368
|
|
|
369
|
-
The output is normative
|
|
370
|
-
|
|
369
|
+
The output is normative—equal inputs produce byte-equal output, and an implementation is
|
|
370
|
+
checked against this spec, not the other way around:
|
|
371
371
|
|
|
372
372
|
- **`enum_table`**: a GFM table with header row `| Field | Allowed values |`; one row
|
|
373
373
|
per string-enum property of the schema, in the schema’s property order; the field name
|
|
@@ -377,7 +377,7 @@ is checked against this spec, not the other way around:
|
|
|
377
377
|
branch is rendered); any other enum shape is skipped.
|
|
378
378
|
With no enum-valued properties the single row is `| _(no enum fields)_ | _(none)_ |`.
|
|
379
379
|
- **`field_list`**: one bullet per top-level property, in schema order:
|
|
380
|
-
`- `name` (type, required): description
|
|
380
|
+
`- `name` (type, required): description`—the JSON type label, `required` or
|
|
381
381
|
`optional`, then `: description` only when the property has one.
|
|
382
382
|
Nested properties are not listed (they appear through their parent’s type).
|
|
383
383
|
With no properties the single bullet is `- _(no fields)_`.
|
|
@@ -103,6 +103,24 @@ Dependencies: `zod`, `yaml`, `commander`, `ajv` (`ajv/dist/2020`) and `ajv-forma
|
|
|
103
103
|
`atomically` (the CLI emits only JSON, so no color dependency).
|
|
104
104
|
The shared `tests/golden/` corpus runs against this CLI via `SOFTSCHEMA_IMPL=ts`.
|
|
105
105
|
|
|
106
|
+
## Packaging
|
|
107
|
+
|
|
108
|
+
`bunup` builds two entrypoints from `src/index.ts` (the library barrel) and `src/cli.ts`
|
|
109
|
+
(the executable). Two packaging decisions keep the library importable; both are guarded
|
|
110
|
+
by `test/library-entrypoint.test.ts`:
|
|
111
|
+
|
|
112
|
+
- **No `"sideEffects": false`.** On a pure re-export barrel, that hint makes Bun’s
|
|
113
|
+
bundler tree-shake the re-exported implementations out of `dist/index.js` while
|
|
114
|
+
leaving the `export { ... }` names, so `import { validateArtifact } from "softschema"`
|
|
115
|
+
throws `SyntaxError: Export 'X' is not defined`. Without the hint, `index.js` and
|
|
116
|
+
`cli.js` share one chunk and every public symbol resolves.
|
|
117
|
+
Do not re-add the flag.
|
|
118
|
+
- **ESM-safe entrypoint check.** `cli.ts` runs the CLI only when it is the process
|
|
119
|
+
entrypoint, detected by comparing `pathToFileURL(realpathSync(process.argv[1])).href`
|
|
120
|
+
with `import.meta.url`. `import.meta.main` is a Bun-only global that the bundler
|
|
121
|
+
lowers to an always-true CommonJS check, which would run the CLI on a plain `import`.
|
|
122
|
+
`realpathSync` resolves the symlink that npm/npx install for the bin.
|
|
123
|
+
|
|
106
124
|
<!-- This document follows common-doc-guidelines.md.
|
|
107
125
|
See github.com/jlevy/practical-prose and review guidelines before editing.
|
|
108
126
|
-->
|
|
@@ -510,16 +510,3 @@ def test_inspect_malformed_softschema_block_exits_two(
|
|
|
510
510
|
err = capsys.readouterr().err
|
|
511
511
|
assert "softschema inspect:" in err
|
|
512
512
|
assert "Traceback" not in err
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
def test_version_flag(capsys: pytest.CaptureFixture[str]) -> None:
|
|
516
|
-
"""--version prints 'softschema <version>' to stdout and exits 0."""
|
|
517
|
-
with pytest.raises(SystemExit) as exc_info:
|
|
518
|
-
softschema_main(["--version"])
|
|
519
|
-
|
|
520
|
-
assert exc_info.value.code == 0
|
|
521
|
-
out = capsys.readouterr().out
|
|
522
|
-
assert out.startswith("softschema ")
|
|
523
|
-
# Must contain a version string, not be empty after the program name.
|
|
524
|
-
version_part = out.strip().removeprefix("softschema ")
|
|
525
|
-
assert len(version_part) > 0
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
import pytest
|
|
6
7
|
from frontmatter_format import fmf_read
|
|
7
8
|
|
|
8
9
|
from examples.movie_page.host_integration import validate_movie_page
|
|
@@ -65,7 +66,9 @@ def test_movie_page_demo_matches_structured_values() -> None:
|
|
|
65
66
|
assert "| MPAA rating | PG |" in body
|
|
66
67
|
|
|
67
68
|
|
|
68
|
-
def test_validate_cli_reads_contract_status_from_demo_yaml(
|
|
69
|
+
def test_validate_cli_reads_contract_status_from_demo_yaml(
|
|
70
|
+
capsys: pytest.CaptureFixture[str],
|
|
71
|
+
) -> None:
|
|
69
72
|
"""`softschema validate` reads contract and status from the document.
|
|
70
73
|
|
|
71
74
|
The demo artifact carries a top-level `title:` alongside `movie:`, so the
|
|
@@ -104,7 +107,7 @@ def test_validate_cli_reads_contract_status_from_demo_yaml(capsys) -> None:
|
|
|
104
107
|
assert output["values"]["title"] == "Spirited Away"
|
|
105
108
|
|
|
106
109
|
|
|
107
|
-
def test_validate_cli_ignores_unrelated_top_level_keys(capsys) -> None:
|
|
110
|
+
def test_validate_cli_ignores_unrelated_top_level_keys(capsys: pytest.CaptureFixture[str]) -> None:
|
|
108
111
|
"""Extra top-level frontmatter keys do not cause validation to fail.
|
|
109
112
|
|
|
110
113
|
softschema must neither forbid nor interpret keys outside the
|
|
@@ -145,7 +148,7 @@ def test_movie_page_validates_with_no_flags() -> None:
|
|
|
145
148
|
assert exit_code == 0
|
|
146
149
|
|
|
147
150
|
|
|
148
|
-
def test_validate_cli_errors_when_envelope_is_ambiguous(capsys) -> None:
|
|
151
|
+
def test_validate_cli_errors_when_envelope_is_ambiguous(capsys: pytest.CaptureFixture[str]) -> None:
|
|
149
152
|
"""Without any designation the CLI must refuse to guess between keys.
|
|
150
153
|
|
|
151
154
|
The spec says the envelope must be designated (flag, registry, or
|
|
@@ -4,17 +4,12 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
+
from frontmatter_format import read_yaml_file
|
|
7
8
|
from pydantic import BaseModel, ConfigDict
|
|
8
9
|
|
|
9
10
|
from softschema import SoftField, compile_model
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def _read_yaml(path: Path) -> dict:
|
|
13
|
-
from frontmatter_format import read_yaml_file
|
|
14
|
-
|
|
15
|
-
return read_yaml_file(path)
|
|
16
|
-
|
|
17
|
-
|
|
18
13
|
class _AnnotatedModel(BaseModel):
|
|
19
14
|
model_config = ConfigDict(extra="forbid")
|
|
20
15
|
|
|
@@ -45,7 +40,7 @@ def test_soft_field_propagates_x_softschema_into_compiled_schema(tmp_path: Path)
|
|
|
45
40
|
out = tmp_path / "annotated.schema.yaml"
|
|
46
41
|
compile_model(_AnnotatedModel, out, contract_id="example:Annotated/v1")
|
|
47
42
|
|
|
48
|
-
schema =
|
|
43
|
+
schema = read_yaml_file(out)
|
|
49
44
|
props = schema["properties"]
|
|
50
45
|
|
|
51
46
|
assert props["title"]["x-softschema"] == {
|
|
@@ -73,7 +68,7 @@ def test_soft_field_omits_empty_optional_metadata(tmp_path: Path) -> None:
|
|
|
73
68
|
"""Defaults (empty examples/aliases, repair=none) stay out of the compiled schema."""
|
|
74
69
|
out = tmp_path / "minimal.schema.yaml"
|
|
75
70
|
compile_model(_AnnotatedModel, out, contract_id="example:Annotated/v1")
|
|
76
|
-
schema =
|
|
71
|
+
schema = read_yaml_file(out)
|
|
77
72
|
|
|
78
73
|
for prop in schema["properties"].values():
|
|
79
74
|
meta = prop.get("x-softschema", {})
|
|
@@ -86,7 +81,7 @@ def test_soft_field_movie_example_genres_block_present() -> None:
|
|
|
86
81
|
"""The movie example annotates `genres`; the committed compiled schema must show it."""
|
|
87
82
|
repo_root = Path(__file__).resolve().parents[3]
|
|
88
83
|
schema_path = repo_root / "examples/movie_page/movie-page.schema.yaml"
|
|
89
|
-
schema =
|
|
84
|
+
schema = read_yaml_file(schema_path)
|
|
90
85
|
genres_meta = schema["properties"]["genres"].get("x-softschema")
|
|
91
86
|
|
|
92
87
|
assert genres_meta is not None
|
|
@@ -9,7 +9,6 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
-
import pytest
|
|
13
12
|
from pydantic import BaseModel
|
|
14
13
|
|
|
15
14
|
from softschema import (
|
|
@@ -129,7 +128,3 @@ def test_no_undocumented_codes_emitted_in_smoke_run(tmp_path: Path) -> None:
|
|
|
129
128
|
f"Validation emitted codes {sorted(unexpected)} not in WarningCode. "
|
|
130
129
|
"Add the new code to WarningCode and the Warning Codes table, or rename it."
|
|
131
130
|
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if __name__ == "__main__": # pragma: no cover
|
|
135
|
-
pytest.main([__file__])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|