Self-contained, deterministic validation engine for OpenSpec v1.1 specifications (Python port of the SpecForge validator).
Project description
A self-contained, deterministic validation engine for OpenSpec v1.1 specifications.
pip install crucible-forge • import crucible
crucible takes a software specification (with its epics and tickets), scores
it against a fixed, configurable rubric, and reports a readiness gate — with
no LLM, fully deterministic. Same input, same score, every time.
It is a faithful Python port of the SpecForge @specforge/validator engine,
extracted as a standalone library, and is the planning gate of
SpecSmither. The output is
byte-equivalent to the reference TypeScript engine, verified by differential
tests across every phase and output layer.
Why
- 🎯 Deterministic — pure scoring, no model calls. Reproducible in CI.
- 📦 Self-contained — pure Python; only
pydanticandpyyamlat runtime. - 🔬 Faithful — output matches the reference TS validator (differential-tested).
- 🎚️ Configurable — every threshold, tier weight, and check lives in config.
- 🏷️ Typed — ships
py.typed; passesmypy --strict.
Install
pip install crucible-forge # → import crucible
# or
uv add crucible-forge
Requires Python ≥ 3.11.
Quick start
from crucible import validate, load_defaults
result = validate(spec, {
"phase": "planning_spec",
"config": load_defaults(), # optional — loaded by default
"returns": ["structural", "scoring", "guidance"],
})
if result.scoring and not result.scoring.skipped:
print(result.scoring.gate_result) # 'pass' | 'fail'
print(result.scoring.local_score) # e.g. 86.11
print(result.passed) # overall gate for the phase
# Canonical camelCase JSON (matches the reference engine):
result.to_json_dict()
spec may be a crucible.Specification model or a plain dict (camelCase,
the OpenSpec v1.1 shape). The context accepts the original keys
(phase, activeEntityId, config, returns) or their snake_case forms.
The model
A spec is a hierarchy:
Specification → Epic → Ticket (+ acceptance criteria, impl steps, tests, files, dependencies)
↘ ticket dependency DAG ↙
validate() walks this tree and produces up to four output layers:
| layer | what it reports |
|---|---|
structural |
schema issues — missing fields, format violations, duplicate orders, entity counts |
scoring |
the readiness score + gate (rubric tiers, weighted blocks, topology penalties, cascade floors) |
crossValidation |
cross-entity consistency — cycles, orphan/island tickets, file conflicts, wave coordination, blueprint coverage |
guidance |
human-readable, prioritized fix suggestions composed from the above |
Request the layers you want via returns; a single phase returns whatever it
computes, and phase: "all" defaults to ["scoring"].
Phases
validate() is driven by context["phase"]:
| phase | scope | needs activeEntityId |
|---|---|---|
planning_spec |
spec-level fields | |
epic_decomposition |
spec + the epic list | |
epic_expansion |
one epic's fields | ✓ |
ticket_decomposition |
tickets under one epic | |
ticket_expansion |
one ticket's fields | ✓ |
cross_validation |
full-tree consistency checks | |
all |
every phase, keyed under by_phase |
# Summative gate over the whole tree:
allr = validate(spec, {"phase": "all", "returns": ["scoring", "crossValidation"]})
planning_passed = (
allr.by_phase["planning_spec"].scoring
and allr.by_phase["planning_spec"].scoring.gate_result == "pass"
)
Structural-only
When you just need the schema check (no scoring/guidance):
from crucible import validate_structural, load_defaults
res = validate_structural(spec, load_defaults(), "planning_spec")
if not res.missing_fields and not res.invalid_fields:
... # schema-clean
Configuration
Every gate is config-driven. load_defaults() returns the packaged
ValidatorConfig (thresholds, tier weights, the 53-entry rubric thresholds,
topology penalties, cross-validation rules). Layer overrides with merge_config:
from crucible import load_defaults, merge_config
config = merge_config(load_defaults(), {"thresholds": {"global": 85}})
The scoring rubric (53 entries: 16 spec · 17 epic · 20 ticket) ships as a data
asset at crucible/guidance/rubric/data/rubric.json, generated verbatim from the
reference source.
Public API
from crucible import (
validate, validate_structural, # entry points
load_defaults, load_from_file, # config loading
load_partial_from_file, merge_config,
CONFIG_DEFAULTS, ValidatorConfigSchema,
PlanningConfigResolver, # project/spec config resolution
ValidatorInputError, # raised on bad context
Specification, Epic, Ticket, Blueprint, # OpenSpec models
models, types, # full model + result namespaces
)
Development
uv sync --all-extras --dev
uv run ruff check src tests
uv run mypy
uv run pytest # 125 tests, incl. differential vs the TS engine
Fidelity is verified against committed golden output captured from the reference engine, so CI needs no extra tooling.
Status
0.1.0 — a complete port, verified byte-for-byte against the current TS
validator across every phase and all four output layers (structural · scoring
· crossValidation · guidance).
License
Apache 2.0 © Gabriel Augusto Gonçalves / blacksmithers
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file crucible_forge-0.1.0.tar.gz.
File metadata
- Download URL: crucible_forge-0.1.0.tar.gz
- Upload date:
- Size: 68.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a789780d04ef20040fb1471f5c564b1610c83bcff30f1f70144dbc258f63d37a
|
|
| MD5 |
11334f2be514d2664403fde0bdac9ca1
|
|
| BLAKE2b-256 |
7423ce5d3c2861251b8af8e24e45a32aabeebb18c2f6edd49609f770f990b4f4
|
Provenance
The following attestation bundles were made for crucible_forge-0.1.0.tar.gz:
Publisher:
publish.yml on blacksmithers/crucible
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
crucible_forge-0.1.0.tar.gz -
Subject digest:
a789780d04ef20040fb1471f5c564b1610c83bcff30f1f70144dbc258f63d37a - Sigstore transparency entry: 1957802151
- Sigstore integration time:
-
Permalink:
blacksmithers/crucible@b97c55b732e5c5ef6f859cfb18c524db4743f140 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/blacksmithers
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b97c55b732e5c5ef6f859cfb18c524db4743f140 -
Trigger Event:
push
-
Statement type:
File details
Details for the file crucible_forge-0.1.0-py3-none-any.whl.
File metadata
- Download URL: crucible_forge-0.1.0-py3-none-any.whl
- Upload date:
- Size: 108.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd4bf63ac125144f6740ca84e2e62279baa9c68185901bd8d578adfe642b026d
|
|
| MD5 |
4dc7f305412eca9fcdaa556ef101b954
|
|
| BLAKE2b-256 |
de81e4e702de550087bf70afa325814778f1aa094bc5b5f271dbcc01482bc675
|
Provenance
The following attestation bundles were made for crucible_forge-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on blacksmithers/crucible
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
crucible_forge-0.1.0-py3-none-any.whl -
Subject digest:
bd4bf63ac125144f6740ca84e2e62279baa9c68185901bd8d578adfe642b026d - Sigstore transparency entry: 1957802205
- Sigstore integration time:
-
Permalink:
blacksmithers/crucible@b97c55b732e5c5ef6f859cfb18c524db4743f140 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/blacksmithers
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b97c55b732e5c5ef6f859cfb18c524db4743f140 -
Trigger Event:
push
-
Statement type: