Self-contained, deterministic work-session completion gate (Python port of the SpecForge assayforge engine).
Project description
A self-contained, deterministic work-session completion gate.
pip install assay-forge • import assay
assay takes a work session (a ticket plus the agent's recorded acceptance
checks, implementation-step completions, file changes, and test results), runs
four independent coverage checks, and reports a completion gate — with no
LLM, fully deterministic. Same input, same result, every time.
It is a faithful Python port of the SpecForge @specforge/assayforge engine,
extracted as a standalone library, and is the work-session gate of
SpecSmither (sitting beside the
planning gate, crucible). The output
is byte-equivalent to the reference TypeScript engine, verified by differential
tests across single-check and full-lifecycle flow snapshots.
Why
- 🎯 Deterministic — pure logic, no model calls. Reproducible in CI.
- 📦 Self-contained — pure Python; only
pydanticandpyyamlat runtime. - 🔬 Faithful — output matches the reference TS engine (differential-tested).
- 🎚️ Configurable — every check and severity knob lives in config.
- 🏷️ Typed — ships
py.typed; passesmypy --strict.
Install
pip install assay-forge # → import assay
# or
uv add assay-forge
Requires Python ≥ 3.11.
Quick start
from assay import assay, AssayContext, AssayOptions, deterministic_id_generator
context = AssayContext.model_validate(work_session_dict)
result = assay(context, AssayOptions(mode="action", action="check-imp-step"))
print(result.passed) # overall gate: no finding has severity 'error'
print(result.next_step) # in action mode: what to do next (+ guidance)
# Canonical camelCase JSON (matches the reference engine):
result.to_json_dict()
What it computes
assay(context, options) → AssayResult: four independent checks over the
normalized work-session event tables, then a single gate
passed = no finding has severity 'error'.
| Check | Reads |
|---|---|
acceptance-coverage |
acceptanceChecks[] vs the ticket's acceptance criteria |
implementation-coverage |
implStepCompletions[] vs the ticket's implementation steps |
file-action-coverage |
fileChanges[] vs the ticket's planned files (+ justification states) |
test-result-presence |
testResults[] vs the ticket's test specification (+ skip/failure justification) |
Two modes:
action— computesnext_step(the next thing the agent should do) with per-finding verbosity dispatch. Requiresoptions.action.check— computes asummary(pending_by_action,total_pending,blocking_issues).
Determinism is anchored by an injectable id generator (uuid_id_generator
default, deterministic_id_generator for tests) and a meta block
(engine_version, generated_at, config_hash).
Configuration
from assay import load_defaults, merge_config, config_hash
config = merge_config(load_defaults(), {"checks": {"test-result-presence": {"enabled": False}}})
context = AssayContext.model_validate({**work_session_dict, "config": config})
Severity of every check (and the per-justification severity knobs) is config-driven.
load_defaults() returns the packaged AssayforgeConfig (data asset at
assay/config/data/defaults.yml).
Public API
from assay import (
assay, ENGINE_VERSION, # entry point
AssayContext, AssayOptions, AssayResult, # core models
uuid_id_generator, deterministic_id_generator, # id generators
load_defaults, load_config, merge_config, config_hash, # config helpers
AssayforgeConfigSchema, validate_config, HARDCODED_DEFAULTS,
CHECK_REGISTRY, get_file_action_severity, # checks
resolve_next_step, applicable_actions, action_applies, # next-step
compose_finding_guidance, compose_next_step_guidance, # guidance
compose_check_summary, interpolate,
OPERATIONS, OPERATION_NAMES, is_operation_name, # operations vocab
types, # full type namespace
)
Development
uv sync --all-extras --dev
uv run ruff check src tests
uv run mypy
uv run pytest # incl. differential vs the TS engine
Fidelity is verified against committed golden output captured from the reference
engine (fixtures/golden/), so CI needs no Node tooling.
Releasing
assay-forge publishes to PyPI
automatically via GitHub Actions
(publish.yml) using Trusted Publishing
(OIDC — no API tokens stored in the repo).
One-time PyPI setup (by a maintainer): add a pending publisher for the
assay-forge project — owner blacksmithers, repository assay, workflow
publish.yml, environment pypi.
Cut a release:
- Bump the version in
pyproject.tomlandsrc/assay/__init__.py, and add aCHANGELOG.mdentry. - Tag and push — the tag triggers the workflow (build →
twine check→ publish):git tag v0.1.0 git push origin main --tags
The workflow can also be run manually from the Actions tab
(workflow_dispatch). Build locally any time with uv build (artifacts in
dist/).
Status
0.1.0 — a complete port, verified byte-for-byte against the reference TS
assayforge engine across the single-check and full-lifecycle flow surfaces,
both action and check modes.
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 assay_forge-0.1.0.tar.gz.
File metadata
- Download URL: assay_forge-0.1.0.tar.gz
- Upload date:
- Size: 40.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
02b63fb20ace8d032e02e3e6b8e286b958b2e81b9dbbabc000388e908a88bdea
|
|
| MD5 |
994497c6a97b959a1e5795e19c7ce4d4
|
|
| BLAKE2b-256 |
3586466f087deff8f1506b3f6e9dcdc8bc6a9bd8a52f109fe7352eda6cf61699
|
Provenance
The following attestation bundles were made for assay_forge-0.1.0.tar.gz:
Publisher:
publish.yml on blacksmithers/assay
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
assay_forge-0.1.0.tar.gz -
Subject digest:
02b63fb20ace8d032e02e3e6b8e286b958b2e81b9dbbabc000388e908a88bdea - Sigstore transparency entry: 1970507440
- Sigstore integration time:
-
Permalink:
blacksmithers/assay@8bc2316a8437a81215634b94b347db6fc794980c -
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@8bc2316a8437a81215634b94b347db6fc794980c -
Trigger Event:
push
-
Statement type:
File details
Details for the file assay_forge-0.1.0-py3-none-any.whl.
File metadata
- Download URL: assay_forge-0.1.0-py3-none-any.whl
- Upload date:
- Size: 68.4 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 |
0657b40822c61c269a4148a55017c4b24d6e1ca936bea9348c6edcb30b254dd6
|
|
| MD5 |
604781163567bf5a4d70a8faa30fafd4
|
|
| BLAKE2b-256 |
0c70ef225540b3ff58d34f16f445fa97c8d7e188bfd721e56d5e76fe81edb36f
|
Provenance
The following attestation bundles were made for assay_forge-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on blacksmithers/assay
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
assay_forge-0.1.0-py3-none-any.whl -
Subject digest:
0657b40822c61c269a4148a55017c4b24d6e1ca936bea9348c6edcb30b254dd6 - Sigstore transparency entry: 1970507522
- Sigstore integration time:
-
Permalink:
blacksmithers/assay@8bc2316a8437a81215634b94b347db6fc794980c -
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@8bc2316a8437a81215634b94b347db6fc794980c -
Trigger Event:
push
-
Statement type: