Skip to main content

Self-contained, deterministic validation engine for OpenSpec v1.1 specifications (Python port of the SpecForge validator).

Project description

crucible

A self-contained, deterministic validation engine for OpenSpec v1.1 specifications.

CI PyPI Python versions License: Apache 2.0 mypy strict

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 pydantic and pyyaml at runtime.
  • 🔬 Faithful — output matches the reference TS validator (differential-tested).
  • 🎚️ Configurable — every threshold, tier weight, and check lives in config.
  • 🏷️ Typed — ships py.typed; passes mypy --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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

crucible_forge-0.1.0.tar.gz (68.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

crucible_forge-0.1.0-py3-none-any.whl (108.2 kB view details)

Uploaded Python 3

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

Hashes for crucible_forge-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a789780d04ef20040fb1471f5c564b1610c83bcff30f1f70144dbc258f63d37a
MD5 11334f2be514d2664403fde0bdac9ca1
BLAKE2b-256 7423ce5d3c2861251b8af8e24e45a32aabeebb18c2f6edd49609f770f990b4f4

See more details on using hashes here.

Provenance

The following attestation bundles were made for crucible_forge-0.1.0.tar.gz:

Publisher: publish.yml on blacksmithers/crucible

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for crucible_forge-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bd4bf63ac125144f6740ca84e2e62279baa9c68185901bd8d578adfe642b026d
MD5 4dc7f305412eca9fcdaa556ef101b954
BLAKE2b-256 de81e4e702de550087bf70afa325814778f1aa094bc5b5f271dbcc01482bc675

See more details on using hashes here.

Provenance

The following attestation bundles were made for crucible_forge-0.1.0-py3-none-any.whl:

Publisher: publish.yml on blacksmithers/crucible

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page