strictspec 0.1.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.
- strictspec-0.1.0/.github/workflows/ci.yml +33 -0
- strictspec-0.1.0/.github/workflows/publish.yml +187 -0
- strictspec-0.1.0/.gitignore +23 -0
- strictspec-0.1.0/.rlsbl/config.json +13 -0
- strictspec-0.1.0/.rlsbl/lint/python.toml +25 -0
- strictspec-0.1.0/.rlsbl/managed-files.json +9 -0
- strictspec-0.1.0/DESIGN.md +81 -0
- strictspec-0.1.0/LICENSE +32 -0
- strictspec-0.1.0/PKG-INFO +32 -0
- strictspec-0.1.0/README.md +14 -0
- strictspec-0.1.0/pyproject.toml +37 -0
- strictspec-0.1.0/scripts/conformance_adapter.py +117 -0
- strictspec-0.1.0/scripts/gencodes.py +372 -0
- strictspec-0.1.0/src/strictspec/__init__.py +437 -0
- strictspec-0.1.0/src/strictspec/_codes.py +1320 -0
- strictspec-0.1.0/src/strictspec/_diag.py +375 -0
- strictspec-0.1.0/src/strictspec/_doc.py +215 -0
- strictspec-0.1.0/src/strictspec/_ir.py +1565 -0
- strictspec-0.1.0/src/strictspec/_jsondoc.py +523 -0
- strictspec-0.1.0/src/strictspec/_launcher.py +212 -0
- strictspec-0.1.0/src/strictspec/_render.py +239 -0
- strictspec-0.1.0/src/strictspec/_schema.py +790 -0
- strictspec-0.1.0/src/strictspec/_strdecode.py +145 -0
- strictspec-0.1.0/src/strictspec/_tomldoc.py +256 -0
- strictspec-0.1.0/src/strictspec/py.typed +0 -0
- strictspec-0.1.0/tests/test_codes_freshness.py +48 -0
- strictspec-0.1.0/tests/test_cross_check.py +147 -0
- strictspec-0.1.0/tests/test_diag.py +63 -0
- strictspec-0.1.0/tests/test_doc.py +72 -0
- strictspec-0.1.0/tests/test_ir.py +208 -0
- strictspec-0.1.0/tests/test_jsondoc.py +334 -0
- strictspec-0.1.0/tests/test_launcher.py +112 -0
- strictspec-0.1.0/tests/test_public.py +105 -0
- strictspec-0.1.0/tests/test_render.py +258 -0
- strictspec-0.1.0/tests/test_schema_sweep.py +55 -0
- strictspec-0.1.0/tests/test_smoke.py +11 -0
- strictspec-0.1.0/tests/test_tomldoc.py +172 -0
- strictspec-0.1.0/uv.lock +92 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
# Per-SHA group: re-runs of the same commit dedupe, but a new commit never
|
|
11
|
+
# cancels an earlier commit's in-flight run (release CI conclusions stay intact).
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ${{ github.workflow_ref }}-${{ github.sha }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
strategy:
|
|
20
|
+
matrix:
|
|
21
|
+
# requires-python: >= 3.11
|
|
22
|
+
python-version: ["3.12", "3.13", "3.14"]
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v6
|
|
25
|
+
- uses: astral-sh/setup-uv@v7
|
|
26
|
+
- run: uv python install ${{ matrix.python-version }}
|
|
27
|
+
- run: uv sync --locked
|
|
28
|
+
- run: uv run python -c "import strictspec"
|
|
29
|
+
- name: Install gitleaks
|
|
30
|
+
run: |
|
|
31
|
+
GITLEAKS_VERSION=8.24.3
|
|
32
|
+
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar xz -C /usr/local/bin gitleaks
|
|
33
|
+
- run: uv run pytest --rootdir .
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
inputs:
|
|
7
|
+
tag:
|
|
8
|
+
description: Release tag to publish (e.g. v1.2.3). Overrides the ref for
|
|
9
|
+
retry dispatch.
|
|
10
|
+
required: false
|
|
11
|
+
type: string
|
|
12
|
+
|
|
13
|
+
# One publish run per tag: a workflow_dispatch retry at the same tag
|
|
14
|
+
# queues behind the in-flight run instead of racing it. A publish is never
|
|
15
|
+
# cancelled mid-flight.
|
|
16
|
+
concurrency:
|
|
17
|
+
group: publish-${{ inputs.tag || github.ref_name }}
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
id-token: write
|
|
22
|
+
jobs:
|
|
23
|
+
gate:
|
|
24
|
+
name: Gate on CI
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
permissions:
|
|
27
|
+
checks: read
|
|
28
|
+
env:
|
|
29
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
30
|
+
GATE_TIMEOUT_MINUTES: '20'
|
|
31
|
+
GATE_GRACE_MINUTES: '5'
|
|
32
|
+
GATE_POLL_SECONDS: '15'
|
|
33
|
+
GATE_MARKER_ATTEMPTS: '5'
|
|
34
|
+
GATE_MARKER_RETRY_SECONDS: '5'
|
|
35
|
+
CI_CHECK_REGEX: ^(test)( \(.*\))?$
|
|
36
|
+
steps:
|
|
37
|
+
- name: Wait for CI to succeed on the release commit
|
|
38
|
+
run: |
|
|
39
|
+
set -euo pipefail
|
|
40
|
+
|
|
41
|
+
# Commit resolution -- explicit marker-first order (never reads the release
|
|
42
|
+
# event payload; dispatch retries have none, so this path is uniform for
|
|
43
|
+
# release events and dispatch retries alike):
|
|
44
|
+
# (1) Marker: rlsbl writes the exact commit CI ran on into the GitHub
|
|
45
|
+
# Release body as a machine-parseable line of the form
|
|
46
|
+
# <!-- rlsbl-ci-sha: <40-hex> -->
|
|
47
|
+
# Prefer it when present -- it pins the precise commit and is immune to
|
|
48
|
+
# ref races. A just-created release can lag on a GitHub API read replica
|
|
49
|
+
# (the marker is written at release creation but may not be visible on
|
|
50
|
+
# the first read), so RETRY the read GATE_MARKER_ATTEMPTS times,
|
|
51
|
+
# GATE_MARKER_RETRY_SECONDS apart, before concluding it is absent. The
|
|
52
|
+
# tag is inputs.tag (TAG_INPUT) when dispatched with an explicit
|
|
53
|
+
# override, else GITHUB_REF_NAME (matches the router resolver).
|
|
54
|
+
# (2) Fallback: older releases predate the marker, so fall back to
|
|
55
|
+
# $GITHUB_SHA, the tag's commit for both release and dispatch-at-tag.
|
|
56
|
+
extract_ci_sha() {
|
|
57
|
+
# Emit the first rlsbl-ci-sha marker SHA found on stdin, if any.
|
|
58
|
+
sed -n 's/.*<!-- rlsbl-ci-sha: \([0-9a-f]\{40\}\) -->.*/\1/p' | head -n1
|
|
59
|
+
}
|
|
60
|
+
tag="${TAG_INPUT:-$GITHUB_REF_NAME}"
|
|
61
|
+
marker_attempts="${GATE_MARKER_ATTEMPTS:-5}"
|
|
62
|
+
marker_retry_seconds="${GATE_MARKER_RETRY_SECONDS:-5}"
|
|
63
|
+
sha=""
|
|
64
|
+
attempt=1
|
|
65
|
+
while [ "$attempt" -le "$marker_attempts" ]; do
|
|
66
|
+
if body="$(gh release view "$tag" --json body --jq .body 2>/dev/null)"; then
|
|
67
|
+
sha="$(printf '%s\n' "$body" | extract_ci_sha)"
|
|
68
|
+
fi
|
|
69
|
+
if [ -n "$sha" ]; then
|
|
70
|
+
break
|
|
71
|
+
fi
|
|
72
|
+
if [ "$attempt" -lt "$marker_attempts" ]; then
|
|
73
|
+
echo "Publish gate: rlsbl-ci-sha marker not yet visible in the '$tag' release body (attempt $attempt/$marker_attempts); retrying in ${marker_retry_seconds}s..."
|
|
74
|
+
sleep "$marker_retry_seconds"
|
|
75
|
+
fi
|
|
76
|
+
attempt=$(( attempt + 1 ))
|
|
77
|
+
done
|
|
78
|
+
if [ -n "$sha" ]; then
|
|
79
|
+
echo "Publish gate: resolved release commit from rlsbl-ci-sha marker in the '$tag' release body."
|
|
80
|
+
else
|
|
81
|
+
sha="$GITHUB_SHA"
|
|
82
|
+
echo "Publish gate: no rlsbl-ci-sha marker after $marker_attempts attempt(s) on the '$tag' release body; falling back to \$GITHUB_SHA."
|
|
83
|
+
fi
|
|
84
|
+
echo "Publish gate: waiting for CI on $GITHUB_REF_NAME (commit $sha)"
|
|
85
|
+
echo "Check-run name filter: $CI_CHECK_REGEX"
|
|
86
|
+
# Timeout, grace window, and poll interval come from the job env above;
|
|
87
|
+
# edit them there if this repository's CI needs different limits.
|
|
88
|
+
|
|
89
|
+
now() { date +%s; }
|
|
90
|
+
start="$(now)"
|
|
91
|
+
deadline=$(( start + GATE_TIMEOUT_MINUTES * 60 ))
|
|
92
|
+
grace_deadline=$(( start + GATE_GRACE_MINUTES * 60 ))
|
|
93
|
+
|
|
94
|
+
while :; do
|
|
95
|
+
if ! resp="$(gh api --paginate "repos/$GITHUB_REPOSITORY/commits/$sha/check-runs?per_page=100")"; then
|
|
96
|
+
echo "Checks API request failed; retrying in ${GATE_POLL_SECONDS}s..."
|
|
97
|
+
sleep "$GATE_POLL_SECONDS"
|
|
98
|
+
continue
|
|
99
|
+
fi
|
|
100
|
+
# Match this project's CI check runs by name; exclude check runs that
|
|
101
|
+
# belong to THIS workflow run (the gate itself and the queued publish
|
|
102
|
+
# jobs would otherwise deadlock the poll loop).
|
|
103
|
+
# A retried CI run creates a BRAND-NEW check-run with the same name as the
|
|
104
|
+
# old one, so a stale failure would block the gate forever. Project id and
|
|
105
|
+
# started_at, then reduce to the latest check-run per name (max started_at,
|
|
106
|
+
# numeric id as tiebreak for retries within the same second) BEFORE the
|
|
107
|
+
# pending / not-success logic runs. This way the newest run decides: still
|
|
108
|
+
# running -> wait; genuinely red -> hard fail.
|
|
109
|
+
runs="$(jq -s --arg re "$CI_CHECK_REGEX" --arg run_id "$GITHUB_RUN_ID" '
|
|
110
|
+
[ .[].check_runs[]
|
|
111
|
+
| select(.name | test($re))
|
|
112
|
+
| select((.details_url // "") | contains("/actions/runs/" + $run_id + "/") | not)
|
|
113
|
+
| {name, status, conclusion, id, started_at} ]
|
|
114
|
+
| group_by(.name)
|
|
115
|
+
| map(sort_by(.started_at, .id) | last)' <<< "$resp")"
|
|
116
|
+
total="$(jq 'length' <<< "$runs")"
|
|
117
|
+
|
|
118
|
+
if [ "$total" -eq 0 ]; then
|
|
119
|
+
if [ "$(now)" -ge "$grace_deadline" ]; then
|
|
120
|
+
echo "::error::Publish gate: no CI check runs matching $CI_CHECK_REGEX appeared on $sha within $GATE_GRACE_MINUTES minutes."
|
|
121
|
+
echo "A scaffolded repository always has a CI workflow, so the release commit must produce CI check runs."
|
|
122
|
+
echo "If CI jobs were renamed, update CI_CHECK_REGEX in this workflow's gate job to match the new names."
|
|
123
|
+
exit 1
|
|
124
|
+
fi
|
|
125
|
+
echo "No matching CI check runs yet; retrying in ${GATE_POLL_SECONDS}s..."
|
|
126
|
+
sleep "$GATE_POLL_SECONDS"
|
|
127
|
+
continue
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
pending="$(jq '[ .[] | select(.status != "completed") ] | length' <<< "$runs")"
|
|
131
|
+
if [ "$pending" -gt 0 ]; then
|
|
132
|
+
if [ "$(now)" -ge "$deadline" ]; then
|
|
133
|
+
echo "::error::Publish gate: timed out after $GATE_TIMEOUT_MINUTES minutes waiting for CI to complete on $sha."
|
|
134
|
+
jq -r '.[] | " \(.name): status=\(.status) conclusion=\(.conclusion // "none")"' <<< "$runs"
|
|
135
|
+
exit 1
|
|
136
|
+
fi
|
|
137
|
+
echo "$pending of $total matching CI check runs still running; retrying in ${GATE_POLL_SECONDS}s..."
|
|
138
|
+
sleep "$GATE_POLL_SECONDS"
|
|
139
|
+
continue
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
not_success="$(jq '[ .[] | select(.conclusion != "success") ]' <<< "$runs")"
|
|
143
|
+
if [ "$(jq 'length' <<< "$not_success")" -gt 0 ]; then
|
|
144
|
+
echo "::error::Publish gate: CI did not pass on $sha -- refusing to publish."
|
|
145
|
+
jq -r '.[] | " \(.name): \(.conclusion)"' <<< "$not_success"
|
|
146
|
+
while IFS= read -r conclusion; do
|
|
147
|
+
case "$conclusion" in
|
|
148
|
+
failure|timed_out)
|
|
149
|
+
echo "CI concluded '$conclusion' on the release commit. Fix the failure, re-run the CI workflow to green on this exact commit (gh run rerun <run-id>), then re-dispatch this publish workflow at the tag ref: gh workflow run <publish workflow> --ref $GITHUB_REF_NAME"
|
|
150
|
+
;;
|
|
151
|
+
cancelled)
|
|
152
|
+
echo "A CI check run was CANCELLED. A cancelled run proves nothing about the commit, so the gate treats it as a hard failure instead of waiting for a conclusion that will never come. Re-run the cancelled CI workflow (gh run rerun <run-id>), then re-dispatch this publish workflow at the tag ref."
|
|
153
|
+
;;
|
|
154
|
+
skipped)
|
|
155
|
+
echo "A CI check run matching the filter was SKIPPED. The gate cannot treat a skipped check as passing: this project's own CI must actually run on the release commit. Check paths filters and job conditions, re-run CI on this commit, then re-dispatch this publish workflow at the tag ref."
|
|
156
|
+
;;
|
|
157
|
+
*)
|
|
158
|
+
echo "CI check concluded '$conclusion' (not success). The gate only proceeds when every matching check concluded success."
|
|
159
|
+
;;
|
|
160
|
+
esac
|
|
161
|
+
done <<< "$(jq -r '.[].conclusion' <<< "$not_success" | sort -u)"
|
|
162
|
+
exit 1
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
echo "Publish gate: all $total matching CI check runs succeeded."
|
|
166
|
+
jq -r '.[] | " \(.name): \(.conclusion)"' <<< "$runs"
|
|
167
|
+
exit 0
|
|
168
|
+
done
|
|
169
|
+
pypi:
|
|
170
|
+
needs: gate
|
|
171
|
+
runs-on: ubuntu-latest
|
|
172
|
+
steps:
|
|
173
|
+
- uses: actions/checkout@v6
|
|
174
|
+
with:
|
|
175
|
+
ref: ${{ inputs.tag || github.event.release.tag_name }}
|
|
176
|
+
- uses: astral-sh/setup-uv@v7
|
|
177
|
+
- name: Install gitleaks
|
|
178
|
+
run: |
|
|
179
|
+
GITLEAKS_VERSION=8.24.3
|
|
180
|
+
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar xz -C /usr/local/bin gitleaks
|
|
181
|
+
- run: uv build --out-dir dist
|
|
182
|
+
- name: Scan artifacts for secrets
|
|
183
|
+
run: |
|
|
184
|
+
gitleaks dir dist/
|
|
185
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
186
|
+
with:
|
|
187
|
+
skip-existing: true
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
dist/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
.mypy_cache/
|
|
8
|
+
.venv/
|
|
9
|
+
|
|
10
|
+
node_modules/
|
|
11
|
+
*.log
|
|
12
|
+
.DS_Store
|
|
13
|
+
coverage/
|
|
14
|
+
build/
|
|
15
|
+
target/
|
|
16
|
+
.rlsbl-notes-*.tmp
|
|
17
|
+
.rlsbl/lock
|
|
18
|
+
.rlsbl-monorepo/lock
|
|
19
|
+
.credentials.json
|
|
20
|
+
.*-cache.json
|
|
21
|
+
.env
|
|
22
|
+
.env.local
|
|
23
|
+
*.local-only
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[forbidden-imports]
|
|
2
|
+
modules = [
|
|
3
|
+
"argparse",
|
|
4
|
+
"click",
|
|
5
|
+
"typer",
|
|
6
|
+
"flask",
|
|
7
|
+
"fastapi",
|
|
8
|
+
"django",
|
|
9
|
+
"uvicorn",
|
|
10
|
+
"granian",
|
|
11
|
+
"starlette",
|
|
12
|
+
"tornado",
|
|
13
|
+
"bottle",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[stdout]
|
|
17
|
+
enabled = true
|
|
18
|
+
ignore = []
|
|
19
|
+
|
|
20
|
+
[entry-point]
|
|
21
|
+
enabled = true
|
|
22
|
+
ignore = []
|
|
23
|
+
|
|
24
|
+
[files]
|
|
25
|
+
exclude = []
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"files": {
|
|
4
|
+
".github/workflows/ci.yml": "71084265487b6ac5bbe6ce9fc2dabd5598db35afae5eaba0cb64c36b1131f0d1",
|
|
5
|
+
".github/workflows/publish.yml": "9bc0b409f8c5c2a3c5795a3e7c66095b9479c1a03fb41250ff0461e1221b54f2",
|
|
6
|
+
".gitignore": "89e4405f5bf2af8e5c1165bf3cc1e5592f8b12e9c21b3aea9e4f05077a23b1f2",
|
|
7
|
+
".rlsbl/lint/python.toml": "9cfbcef2e010d5fad243437bf2fd8df54a1fae70ee0762f9d11da70fe207501c"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# python/ — Python Runtime Library and PyPI Releasable
|
|
2
|
+
|
|
3
|
+
NOT the toolchain (that's Go). Runtime support for generated Python code, published to PyPI as
|
|
4
|
+
`strictspec`. There is NO separate binary package: the former `-bin` wrapper is eliminated
|
|
5
|
+
(decision 31). The PyPI `strictspec` wheel (stays `py3-none-any`) exposes a `strictspec`
|
|
6
|
+
console script that lazy-downloads the exact-version Go binary from the GitHub Release — with
|
|
7
|
+
SHA-256 checksum verification — into a platform cache on FIRST CLI INVOCATION. Library-only
|
|
8
|
+
installs (importing the runtime without ever invoking the CLI) do zero network access. The
|
|
9
|
+
download shim is built on rlsbl's first-party "launcher" artifact mechanism (checksum-verifying
|
|
10
|
+
templates). Runtime package version = strictspec release version, so the lazy download and the
|
|
11
|
+
exact version-pairing rule agree by construction.
|
|
12
|
+
|
|
13
|
+
## Generated-code style
|
|
14
|
+
|
|
15
|
+
Frozen dataclasses + explicit generated checks + generated `with_*` copy helpers. pydantic and
|
|
16
|
+
msgspec are out: two-phase domain checks require partial-subtree binding, and deterministic
|
|
17
|
+
generated output requires full control of every emitted line. Zero third-party runtime deps in
|
|
18
|
+
generated code; one emitter architecture across all three languages. Generated checks always
|
|
19
|
+
hard-error on unknown keys — a language invariant, not a declared policy and never a library
|
|
20
|
+
default. Freezing is shallow-plus-generated-immutability (nested edits go through `with_*`
|
|
21
|
+
helpers). Generated files land chmod 444 in consumer repos, carry a `# ruff: noqa` header plus
|
|
22
|
+
the generated-by header, and are formatted by the generator's own canonical emitter — no
|
|
23
|
+
external formatter is ever involved, and consumers never hand-silence linters on generated
|
|
24
|
+
paths.
|
|
25
|
+
|
|
26
|
+
## Entry points (per the Generated API Contract)
|
|
27
|
+
|
|
28
|
+
1. Raw text/bytes: this runtime's reader parses losslessly (lexeme classification per the
|
|
29
|
+
primitives appendix; duplicate keys surfaced via object_pairs_hook and hard-errored;
|
|
30
|
+
int64/float64 overflow and number-scalar unrepresentable lexemes hard-errored) into tagged
|
|
31
|
+
document-model values, then validates.
|
|
32
|
+
2. Tagged document-model values: from the reader or from generated typed constructors (where
|
|
33
|
+
integer/float/number/datetime is explicit in the type). Raw untagged dicts are NOT accepted
|
|
34
|
+
as validation input — ambiguity never enters the model. This serves in-memory
|
|
35
|
+
mutate-then-validate consumers (PixelWeaver's server validates dicts built by MCP mutations
|
|
36
|
+
today; post-migration it builds tagged values via constructors and `with_*` helpers).
|
|
37
|
+
|
|
38
|
+
Result type: (typed value | None) x ordered diagnostics; every diagnostic is an error (no
|
|
39
|
+
severity field, no warnings); the assert-style wrapper raises on any diagnostic. Version gate
|
|
40
|
+
first, with the structured remediation payload.
|
|
41
|
+
|
|
42
|
+
## Contents
|
|
43
|
+
|
|
44
|
+
- Diagnostics: the spec/ error model (code, path, message, expected/got, suggestion, optional
|
|
45
|
+
position — NO severity), terminal + JSON renderers emitting from the spec-pinned message
|
|
46
|
+
templates, did-you-mean per appendix item 7 (cross-target normative — codes, paths, AND
|
|
47
|
+
rendered message text are the conformance surface), the constructor for consumer-prefixed
|
|
48
|
+
codes (used by consumer-native checks downstream of validation).
|
|
49
|
+
- Tagged document model: lexeme-retaining values; JSON reader (ordered, duplicate-rejecting),
|
|
50
|
+
TOML round-trip via tomlkit (within-backend fixpoint; never byte-identical to the Go
|
|
51
|
+
substrate), JSONL streamed line-by-line (memory bounded by the largest line, not the file;
|
|
52
|
+
all-errors-in-one-pass applies per line; positions are byte offsets; LF-only; per-line
|
|
53
|
+
positional errors), O_APPEND single-writer appends, temp+rename rewrites, immutability
|
|
54
|
+
helpers. Datetime scalars per appendix item 11 (TOML natives; RFC 3339 strings in JSON;
|
|
55
|
+
binds datetime/date/time with kind guards). Write side per the canonical-serialization
|
|
56
|
+
appendix: untouched values keep their lexemes; constructed floats render with float lexemes;
|
|
57
|
+
the write path REFUSES non-current format_version serialization (producer-current-only).
|
|
58
|
+
Validating a TOML-syntax document against a schema in which a nullable union is reachable =
|
|
59
|
+
canonical hard error.
|
|
60
|
+
- The inline version-gate helper (three-message pattern + structured remediation payload).
|
|
61
|
+
- Scalar guards per spec/ (number scalar with unrepresentable-lexeme rejection; datetime
|
|
62
|
+
kinds; integral-float, bool-not-int, non-finite).
|
|
63
|
+
- The CONSTRAINT ENGINE: the ported cross-document vocabulary evaluator plus the Python
|
|
64
|
+
implementations of the evidence resolvers (filesystem, sibling documents, git where
|
|
65
|
+
available). There is no consumer registration surface — vocabulary checks are
|
|
66
|
+
schema-declared and portable; the bespoke tail is consumer-native code over typed values,
|
|
67
|
+
run by the consumer after validation. A resolver this environment cannot satisfy is a hard
|
|
68
|
+
error naming the resolver.
|
|
69
|
+
- Boundary-checkpoint support: generated ingest write-doors and egress wrappers invoke the
|
|
70
|
+
migration engine via the packaged CLI (the engine itself never lives in this runtime); the
|
|
71
|
+
wrappers are generated only for manifest-declared stores/channels.
|
|
72
|
+
|
|
73
|
+
## Invariants
|
|
74
|
+
|
|
75
|
+
- No toolchain logic: no schema parsing, no generation, no migration engine (checkpoints
|
|
76
|
+
delegate to the packaged CLI).
|
|
77
|
+
- No lenient modes; loading and validation inseparable; discovery collects per-file errors and
|
|
78
|
+
fails loudly. No warnings anywhere.
|
|
79
|
+
- Version pairing with generated code: exact match per release; dev builds pair only with
|
|
80
|
+
themselves. The pairing hard error is the intended surfacing of skew under always-latest
|
|
81
|
+
dependencies; remediation is regeneration, never pinning.
|
strictspec-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 S. M. Hosseini
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Generated output.
|
|
26
|
+
|
|
27
|
+
Code that the strictspec generator emits into a consumer's repository is
|
|
28
|
+
unencumbered: no license obligation attaches to generated output. You may use,
|
|
29
|
+
modify, and distribute strictspec-generated code without restriction and
|
|
30
|
+
without any requirement to reproduce this license or the copyright notice
|
|
31
|
+
above. This MIT license governs the strictspec toolchain and runtimes
|
|
32
|
+
themselves, not the code they generate for you.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: strictspec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Strict multi-language schema toolchain for declarative spec files (Python runtime + CLI stub)
|
|
5
|
+
Project-URL: Homepage, https://github.com/smm-h/strictspec
|
|
6
|
+
Project-URL: Repository, https://github.com/smm-h/strictspec
|
|
7
|
+
Author-email: "S. M. Hosseini" <m.hosseini@veliu.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: codegen,rlsbl,schema,strictspec,toml,validation
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: tomlkit
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# strictspec (Python)
|
|
20
|
+
|
|
21
|
+
Python runtime library and CLI stub for [strictspec](https://github.com/smm-h/strictspec),
|
|
22
|
+
a strict multi-language schema toolchain for declarative spec files.
|
|
23
|
+
|
|
24
|
+
This is a placeholder skeleton. The runtime (document I/O, diagnostics, tagged
|
|
25
|
+
values, constraint engine) and the CLI launcher stub land in a later phase.
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv sync
|
|
31
|
+
uv run pytest
|
|
32
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# strictspec (Python)
|
|
2
|
+
|
|
3
|
+
Python runtime library and CLI stub for [strictspec](https://github.com/smm-h/strictspec),
|
|
4
|
+
a strict multi-language schema toolchain for declarative spec files.
|
|
5
|
+
|
|
6
|
+
This is a placeholder skeleton. The runtime (document I/O, diagnostics, tagged
|
|
7
|
+
values, constraint engine) and the CLI launcher stub land in a later phase.
|
|
8
|
+
|
|
9
|
+
## Development
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv sync
|
|
13
|
+
uv run pytest
|
|
14
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "strictspec"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Strict multi-language schema toolchain for declarative spec files (Python runtime + CLI stub)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "S. M. Hosseini", email = "m.hosseini@veliu.com" }]
|
|
14
|
+
keywords = ["strictspec", "schema", "codegen", "validation", "toml", "rlsbl"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
dependencies = ["tomlkit"]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
strictspec = "strictspec._launcher:main"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = ["pytest"]
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/strictspec"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/smm-h/strictspec"
|
|
37
|
+
Repository = "https://github.com/smm-h/strictspec"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Conformance-adapter for the strictspec Python runtime.
|
|
2
|
+
|
|
3
|
+
The Python-side invocation contract for the cross-target conformance harness's
|
|
4
|
+
``python`` target. It reads a JSON request on stdin describing one fixture
|
|
5
|
+
(schema path + input document + optional cross-document evidence), drives the
|
|
6
|
+
PUBLIC Python runtime (``compile_embedded`` + ``Program.validate`` / the
|
|
7
|
+
meta-schema reader for meta fixtures), and writes the observed outcome as JSON on
|
|
8
|
+
stdout in the runner's expected shape::
|
|
9
|
+
|
|
10
|
+
{"valid": bool, "diagnostics": [{"code","path","message"}, ...]}
|
|
11
|
+
|
|
12
|
+
This is the exact request/response contract the Go ``conformance-adapter`` speaks
|
|
13
|
+
(go/cmd/conformance-adapter/main.go); byte-identical outcomes across targets are
|
|
14
|
+
what the parity checker asserts. The harness prepares the Python environment once
|
|
15
|
+
(uv) and invokes this script per fixture -- the strictcli conformance pattern:
|
|
16
|
+
compile the schema once, feed the input document.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
import strictspec
|
|
26
|
+
from strictspec import _doc, _render, _schema
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _build_fileset(schema_path: str) -> tuple[dict[str, str], str]:
|
|
30
|
+
"""Read every ``.toml`` in the schema's directory into an in-memory FileSet,
|
|
31
|
+
keyed by basename. This mirrors the Go adapter: imports reference sibling
|
|
32
|
+
files by bare filename, and the scalar manifest is a sibling ``.toml`` that
|
|
33
|
+
``load_manifest_scalars_from`` merges across every file in the set.
|
|
34
|
+
"""
|
|
35
|
+
directory = os.path.dirname(schema_path)
|
|
36
|
+
files: dict[str, str] = {}
|
|
37
|
+
for name in os.listdir(directory):
|
|
38
|
+
full = os.path.join(directory, name)
|
|
39
|
+
if os.path.isdir(full) or not name.endswith(".toml"):
|
|
40
|
+
continue
|
|
41
|
+
with open(full, encoding="utf-8") as f:
|
|
42
|
+
files[name] = f.read()
|
|
43
|
+
return files, os.path.basename(schema_path)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _read_input(req: dict) -> bytes:
|
|
47
|
+
inline = req.get("input_inline")
|
|
48
|
+
if inline:
|
|
49
|
+
return inline.encode("utf-8")
|
|
50
|
+
return open(req["input_path"], "rb").read()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _render_diags(diags: list) -> list[dict[str, str]]:
|
|
54
|
+
return [
|
|
55
|
+
{"code": d.code, "path": d.path.render(), "message": _render.render(d)}
|
|
56
|
+
for d in diags
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _meta_validate(req: dict) -> list[dict[str, str]]:
|
|
61
|
+
"""Read the input file AS a schema/type-definition file and return the
|
|
62
|
+
meta-schema reader's authoring diagnostics (mirrors Go's metaValidate)."""
|
|
63
|
+
src = _read_input(req)
|
|
64
|
+
try:
|
|
65
|
+
d = _tomldoc_parse(src)
|
|
66
|
+
except _doc.ParseError as pe:
|
|
67
|
+
return _render_diags([strictspec._parse_diag(pe)])
|
|
68
|
+
_, diags = _schema.read_schema(d.root, "")
|
|
69
|
+
return _render_diags(diags)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _tomldoc_parse(src: bytes):
|
|
73
|
+
from strictspec import _tomldoc
|
|
74
|
+
|
|
75
|
+
return _tomldoc.parse(src)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def run(req: dict) -> dict:
|
|
79
|
+
schema_path = req["schema"]
|
|
80
|
+
files, main = _build_fileset(schema_path)
|
|
81
|
+
|
|
82
|
+
# Meta-schema mode: the fixture's "document" is itself a schema file, read AS
|
|
83
|
+
# a document of the built-in meta-schema -- the reader's authoring diagnostics
|
|
84
|
+
# ARE the outcome.
|
|
85
|
+
s, _ = _schema.parse_from(files, main)
|
|
86
|
+
if s.name == "strictspec-meta-schema":
|
|
87
|
+
diagnostics = _meta_validate(req)
|
|
88
|
+
return {"valid": len(diagnostics) == 0, "diagnostics": diagnostics}
|
|
89
|
+
|
|
90
|
+
program = strictspec.compile_embedded(files, main)
|
|
91
|
+
src = _read_input(req)
|
|
92
|
+
evidence = req.get("evidence") or None
|
|
93
|
+
result = program.validate_with_evidence(src, req["input_syntax"], evidence)
|
|
94
|
+
diagnostics = [
|
|
95
|
+
{"code": d.code, "path": d.path, "message": d.message}
|
|
96
|
+
for d in result.diagnostics
|
|
97
|
+
]
|
|
98
|
+
return {"valid": result.valid, "diagnostics": diagnostics}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main() -> int:
|
|
102
|
+
try:
|
|
103
|
+
req = json.load(sys.stdin)
|
|
104
|
+
except json.JSONDecodeError as exc:
|
|
105
|
+
json.dump({"error": f"bad request: {exc}"}, sys.stderr)
|
|
106
|
+
return 2
|
|
107
|
+
try:
|
|
108
|
+
resp = run(req)
|
|
109
|
+
except Exception as exc: # noqa: BLE001 -- surface any failure to the harness
|
|
110
|
+
json.dump({"error": str(exc)}, sys.stderr)
|
|
111
|
+
return 2
|
|
112
|
+
json.dump(resp, sys.stdout)
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
sys.exit(main())
|