Thin orchestration layer for ML experiment pipelines
- Oban-style Repo injection: Host applications now provide their own Repo via
config :crucible_framework, repo: MyApp.Repo - No auto-start by default: Repo is NOT started automatically; set
start_repo: truefor legacy behavior - New
repo/0andrepo!/0functions: Access the configured Repo module programmatically - Dependency updates:
crucible_tracebumped to~> 0.3.1,telemetryto~> 1.3
- Optional dependencies:
crucible_benchandcrucible_traceare optional; missing bench errors fast, missing trace disables tracing with a warning - Examples refresh: New runnable examples plus
examples/run_all.shrunner - Dependency update:
crucible_benchbumped to~> 0.4.0 - Postgres driver:
postgrexminimum version raised to>= 0.21.1 - Persistence tests: Integration tests are opt-in via
CRUCIBLE_DB_ENABLED=true
- BREAKING:
describe/1callback is now required (removed from@optional_callbacks) - Schema Module: New
Crucible.Stage.Schemafor canonical schema definition and validation - Schema Normalizer: New
Crucible.Stage.Schema.Normalizerfor legacy schema conversion - Options Validator: New
Crucible.Stage.Validatorfor runtime options validation - Registry Enhancements:
list_stages_with_schemas/0andstage_schema/1for schema access - Mix Task: New
mix crucible.stagescommand for stage discovery - Pipeline Validation: Opt-in
validate_options: :warn | :errormode in runner - Conformance Tests: Comprehensive tests for all framework stages
See CHANGELOG.md for the complete migration guide.
- Stage Contract: Enforced
describe/1policy for all stage implementations - Enhanced Documentation: Comprehensive
Crucible.Stagebehaviour docs with schema specification - Runner Documentation: Clarified that
Crucible.Pipeline.Runneris the authoritative runner - Schema Types: Defined type specifications for stage option schemas
- Built-in Stages: Updated all built-in stages with proper
describe/1schemas
- BREAKING: Simplified to pure orchestration layer (~2,000 LOC from ~5,300 LOC)
- Removed: Backend infrastructure (moved to crucible_train)
- Removed: Data loading stages (domain-specific)
- Removed: Analysis adapters (domain-specific)
- Removed: Fairness stages (moved to ExFairness)
- Removed: BackendCall stage (moved to crucible_train)
- Simplified: Context struct with Phoenix-style
assignsfor domain data - Updated: crucible_ir ~> 0.2.0 with new Training/Deployment/Feedback IR
CrucibleFramework provides:
- Pipeline Execution - Sequential stage execution with Context threading
- Stage Behaviour - Clean interface for composable pipeline stages
- Optional Persistence - Ecto-backed experiment run tracking
- Telemetry Integration - Event emission for observability
This library focuses purely on orchestration. Domain-specific functionality belongs in specialized packages:
| Domain | Package |
|---|---|
| Training | crucible_train (future) |
| CNS Dialectics | cns_crucible |
| Fairness | ExFairness |
| XAI | crucible_xai |
def deps do
[
{:crucible_framework, "~> 0.5.1"}
]
endexperiment = %CrucibleIR.Experiment{
id: "my-experiment",
backend: %CrucibleIR.BackendRef{id: :my_backend},
pipeline: [
%CrucibleIR.StageDef{name: :validate},
%CrucibleIR.StageDef{name: :data_checks},
%CrucibleIR.StageDef{name: :bench},
%CrucibleIR.StageDef{name: :report}
]
}
{:ok, ctx} = CrucibleFramework.run(experiment)Runnable scripts live under examples/. Start with:
mix run examples/01_core_pipeline.exsRun the full set with:
./examples/run_all.shSee examples/README.md for descriptions and optional dependency notes.
Runtime context threaded through pipeline stages. Uses Phoenix-style assigns for domain-specific data:
ctx = %Crucible.Context{
experiment_id: "exp-1",
run_id: "run-1",
experiment: experiment
}
# Add metrics
ctx = Crucible.Context.put_metric(ctx, :accuracy, 0.95)
# Store domain data in assigns (training stages, CNS stages, etc.)
ctx = Crucible.Context.assign(ctx, :dataset, my_data)
ctx = Crucible.Context.assign(ctx, :backend_session, session)
ctx = Crucible.Context.assign(ctx, :snos, extracted_snos)
# Track stage completion
ctx = Crucible.Context.mark_stage_complete(ctx, :data_load)| Category | Functions |
|---|---|
| Metrics | put_metric/3, get_metric/3, update_metric/3, merge_metrics/2, has_metric?/2 |
| Outputs | add_output/2, add_outputs/2 |
| Artifacts | put_artifact/3, get_artifact/3, has_artifact?/2 |
| Assigns | assign/2, assign/3 |
| Stages | mark_stage_complete/2, stage_completed?/2, completed_stages/1 |
Behaviour for pipeline stages. All stages must implement both run/2 and describe/1:
defmodule MyApp.Stage.CustomStage do
@behaviour Crucible.Stage
@impl true
def run(%Crucible.Context{} = ctx, opts) do
# Do work, update ctx
{:ok, updated_ctx}
end
@impl true
def describe(_opts) do
%{
name: :custom,
description: "My custom stage",
required: [:input_path],
optional: [:format, :verbose],
types: %{
input_path: :string,
format: {:enum, [:json, :csv]},
verbose: :boolean
},
defaults: %{
format: :json,
verbose: false
}
}
end
endAll stages must implement describe/1 returning a canonical schema:
| Field | Required | Type | Description |
|---|---|---|---|
name |
Yes | atom | Stage identifier |
description |
Yes | string | Human-readable description |
required |
Yes | list of atoms | Required option keys |
optional |
Yes | list of atoms | Optional option keys |
types |
Yes | map | Type specifications for options |
defaults |
No | map | Default values for optional fields |
version |
No | string | Stage version |
__extensions__ |
No | map | Domain-specific metadata |
Use mix crucible.stages to list available stages and their schemas:
$ mix crucible.stages
$ mix crucible.stages --name bench| Stage | Purpose |
|---|---|
Crucible.Stage.Validate |
Pre-flight pipeline validation |
Crucible.Stage.DataChecks |
Lightweight data validation (reads from assigns[:examples]) |
Crucible.Stage.Guardrails |
Safety checks via adapters |
Crucible.Stage.Bench |
Statistical analysis (requires crucible_bench) |
Crucible.Stage.Report |
Output generation |
Stage module resolution from config:
# In config.exs
config :crucible_framework,
stage_registry: %{
validate: Crucible.Stage.Validate,
bench: Crucible.Stage.Bench,
my_stage: MyApp.Stage.Custom
}CrucibleFramework.run(experiment)
|
v
Crucible.Pipeline.Runner
|
+-> Stage 1: Validate
+-> Stage 2: CustomDataLoader (domain-specific)
+-> Stage 3: CustomBackendCall (domain-specific)
+-> Stage 4: Bench
+-> Stage 5: Report
|
v
{:ok, final_context}
Training, CNS, and other domain-specific stages should be implemented in their respective packages and registered via config:
# crucible_train would provide:
config :crucible_framework,
stage_registry: %{
data_load: CrucibleTrain.Stage.DataLoad,
backend_call: CrucibleTrain.Stage.BackendCall,
# ...
}
# cns_crucible would provide:
config :crucible_framework,
stage_registry: %{
cns_extract: CnsCrucible.Stage.SNOExtraction,
cns_topology: CnsCrucible.Stage.TopologyAnalysis,
# ...
}CrucibleFramework uses dynamic repo injection - your host application provides the Repo:
# config/config.exs
config :crucible_framework,
repo: MyApp.Repo, # Required: host app's Repo module
stage_registry: %{
validate: Crucible.Stage.Validate,
data_checks: Crucible.Stage.DataChecks,
guardrails: Crucible.Stage.Guardrails,
bench: Crucible.Stage.Bench,
report: Crucible.Stage.Report
},
guardrail_adapter: Crucible.Stage.Guardrails.Noop
# Your host app's Repo configuration
config :my_app, MyApp.Repo,
database: "my_app_dev",
username: "postgres",
password: "postgres",
hostname: "localhost"Then start your Repo in your application's supervision tree:
# lib/my_app/application.ex
children = [
MyApp.Repo,
# ... other children
]Copy migrations from deps/crucible_framework/priv/repo/migrations/ or run:
mix crucible_framework.installFor backwards compatibility, set start_repo: true to auto-start CrucibleFramework.Repo:
config :crucible_framework,
start_repo: true,
ecto_repos: [CrucibleFramework.Repo]
config :crucible_framework, CrucibleFramework.Repo,
database: "crucible_dev",
username: "crucible_dev",
password: "crucible_dev_pw",
hostname: "localhost"- crucible_ir - Shared experiment IR structs (v0.2.0+)
- crucible_bench - Statistical testing (optional; required for
Crucible.Stage.Bench) - crucible_trace - Causal reasoning traces (optional)
CrucibleFramework runs without the optional packages below; they enable specific features.
- Enables
Crucible.Stage.Benchand statistical testing helpers - If missing and
:benchis used, the stage returns{:error, {:missing_dependency, :crucible_bench}}
- Enables trace lifecycle helpers and
enable_trace: truein the runner - If missing, tracing is disabled and a warning is logged; export/load helpers return
nilor{:error, {:missing_dependency, :crucible_trace}}
def deps do
[
{:crucible_framework, "~> 0.5.1"},
{:crucible_bench, "~> 0.4.0"},
{:crucible_trace, "~> 0.3.0"}
]
endIf you do not need bench or tracing, omit those deps and remove :bench from your pipeline (or from stage_registry) to keep the core slim. See examples/02_bench_optional.exs and examples/03_trace_optional.exs for optional-dep usage.
# Setup
mix deps.get && mix compile
# Tests
mix test
# Integration tests (persistence; requires CRUCIBLE_DB_ENABLED=true)
CRUCIBLE_DB_ENABLED=true MIX_ENV=test mix test --include integration
# Quality checks
mix format
mix credo --strict
mix dialyzer| Repository | Purpose |
|---|---|
| crucible_ir | Shared IR structs |
| crucible_bench | Statistical testing |
| crucible_trace | Causal transparency |
| cns | CNS dialectical reasoning |
| cns_crucible | CNS + Crucible integration |
MIT. See LICENSE.