Skip to main content

Self-contained, deterministic work-session completion gate (Python port of the SpecForge assayforge engine).

Project description

assay

A self-contained, deterministic work-session completion gate.

CI PyPI Python versions License: Apache 2.0 mypy strict

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 pydantic and pyyaml at runtime.
  • 🔬 Faithful — output matches the reference TS engine (differential-tested).
  • 🎚️ Configurable — every check and severity knob lives in config.
  • 🏷️ Typed — ships py.typed; passes mypy --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 — computes next_step (the next thing the agent should do) with per-finding verbosity dispatch. Requires options.action.
  • check — computes a summary (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:

  1. Bump the version in pyproject.toml and src/assay/__init__.py, and add a CHANGELOG.md entry.
  2. 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


Download files

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

Source Distribution

assay_forge-0.1.0.tar.gz (40.8 kB view details)

Uploaded Source

Built Distribution

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

assay_forge-0.1.0-py3-none-any.whl (68.4 kB view details)

Uploaded Python 3

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

Hashes for assay_forge-0.1.0.tar.gz
Algorithm Hash digest
SHA256 02b63fb20ace8d032e02e3e6b8e286b958b2e81b9dbbabc000388e908a88bdea
MD5 994497c6a97b959a1e5795e19c7ce4d4
BLAKE2b-256 3586466f087deff8f1506b3f6e9dcdc8bc6a9bd8a52f109fe7352eda6cf61699

See more details on using hashes here.

Provenance

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

Publisher: publish.yml on blacksmithers/assay

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

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

Hashes for assay_forge-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0657b40822c61c269a4148a55017c4b24d6e1ca936bea9348c6edcb30b254dd6
MD5 604781163567bf5a4d70a8faa30fafd4
BLAKE2b-256 0c70ef225540b3ff58d34f16f445fa97c8d7e188bfd721e56d5e76fe81edb36f

See more details on using hashes here.

Provenance

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

Publisher: publish.yml on blacksmithers/assay

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