Foundry

A governed build environment for Elixir/Ash/Phoenix platforms. Foundry extracts a typed system map from your project, enforces invariants via a lint suite, and provides a structured runtime health picture for CI and the Foundry Studio UI.


What Foundry Does


Requirements


Installation

Add to your mix.exs dependencies:

{:foundry, "~> 0.1", only: [:dev, :test]}

For umbrella projects, add to the specific app that owns the build tooling. Foundry does not need to be in every umbrella app's deps.

Run:

mix deps.get

Quickstart

1. Create your project manifest:

mkdir -p .foundry

Create .foundry/manifest.exs — see The Manifest below.

2. Generate the system map:

mix foundry.project.context

Emits a JSON graph of all Ash/Spark modules to stdout and writes .foundry/context.lock.

3. Check project health:

mix foundry.project.status

4. Run the lint suite:

mix foundry.lint.all

The Manifest (.foundry/manifest.exs)

Foundry reads .foundry/manifest.exs from the project root. This file is committed to your project repository. It is a plain Elixir keyword list.

Minimum required manifest:

# .foundry/manifest.exs
[
project_name: "MyApp",
domain_type: :other, # :igaming | :fintech | :healthcare | :legal | :insurance | :other
approvers: [
sensitive_lead: "lead@company.com",
compliance_officer: "compliance@company.com"
],
sensitive_resources: [
MyApp.Finance.LedgerEntry,
MyApp.Finance.Wallet
# ash_authentication User and Token resources are always treated as sensitive —
# do not list them here; Foundry detects them automatically.
]
]

The manifest is validated at startup. Invalid manifests prevent tasks from running. Required fields: project_name, approvers.sensitive_lead, approvers.compliance_officer.

Full schema reference: docs/manifest-schema-draft.md


Mix Tasks

mix foundry.project.context

Generates the project system map.

mix foundry.project.context # full graph — all modules
mix foundry.project.context MyApp.Finance.Wallet # single module NodeEntry
mix foundry.project.context --check # CI staleness check

Bulk output emits JSON to stdout:

{
"generated_at": "2026-03-22T10:00:00Z",
"project": "MyApp",
"project_type": "standard",
"domain_type": "igaming",
"nodes": [ "..." ],
"edges": [ "..." ],
"spec_kit": { "..." },
"graph_delta": null
}

Also writes .foundry/context.lock — a SHA256 hash of all lib/**/*.ex and test/**/*.ex files. Use --check in CI to verify the lock is current.

Exit codes:

Schema: docs/project_context_schema.md


mix foundry.project.status

Emits a runtime health snapshot as JSON.

mix foundry.project.status
mix foundry.project.status --json # compact output (no pretty-print)

Output includes: compilation timestamp, stack versions, lint summary, pending migrations, open proposals, compliance matrix, CI state, and manifest metadata.

Schema: docs/mix_task_summary_schemas.md


mix foundry.lint.all

Runs the full Foundry lint suite. Emits a JSON report and exits non-zero if any :error violations are found.

mix foundry.lint.all # JSON output (default)
mix foundry.lint.all --format=text # human-readable summary

Exit codes:

Output shape:

{
"passed": false,
"error_count": 1,
"warning_count": 2,
"info_count": 0,
"violations": [
{
"rule_id": "missing_paper_trail",
"severity": "error",
"message": "MyApp.Finance.Wallet is sensitive but does not use AshPaperTrail.Resource",
"module": "MyApp.Finance.Wallet"
}
],
"generated_at": "2026-03-22T10:00:00Z"
}

Lint Rules

The following rules run in mix foundry.lint.all. Full catalogue including planned rules: docs/lint-catalogue.md

Rule IDINVSeverityDescription
missing_paper_trailINV-011:errorSensitive resource missing AshPaperTrail.Resource extension
missing_archivalINV-012:errorSensitive resource missing AshArchival.Resource extension
missing_runbookINV-005:errorReactor with >3 steps missing @runbook declaration
missing_idempotencyINV-004:errorReactor with side-effect steps missing idempotency key
missing_descriptionINV-006:errorAsh resource attribute missing description: value
ash_version_outdated:errorResolved ash version in mix.lock is below 3.x
elixir_version_unsupported:errorElixir version below minimum required for Ash 3.x
adapter_version_not_active:warningProvider adapter registered but not active
manifest_missing_required_approver:errorapprovers.sensitive_lead or compliance_officer absent
manifest_invalid_coverage_weights:errorcoverage_weights values do not sum to 1.0
manifest_missing_cldr_backend:error:ash_money declared but no CLDR backend discoverable

Rules are registered in Foundry.LintRules.Registry. New rules must be added there explicitly — accidental registration is worse than deliberate omission.


CI Integration

Add to your CI pipeline (GitHub Actions example):

- name: Check context lock
run: mix foundry.project.context --check
- name: Run lint suite
run: mix foundry.lint.all

Context lock check:mix foundry.project.context --check computes sha256(lib/**/*.ex + test/**/*.ex) and compares it against .foundry/context.lock. Fails if the lock is absent or stale.

Regenerate the lock whenever source files change:

mix foundry.project.context
git add .foundry/context.lock

Optional — write CI status for the Studio health panel:

After your CI run, write .foundry/ci_status.json:

{
"last_run_at": "2026-03-22T10:00:00Z",
"commit": "a3f9d12",
"branch": "main",
"lint_passed": true,
"tests_passed": true
}

mix foundry.project.status merges this file into the ci field of its output. The file must live at exactly .foundry/ci_status.json in the project root.


Change Classification

Every proposed change is classified into one of four classes. Classification determines approval requirements.

ClassWhenApproverAuto-apply
:structuralNew resource, attribute, relationship, description, test skeletonAny developerConfigurable (auto_apply_structural)
:behavioralNew Rule, Transfer step, Reactor, Oban job, state machine transitionDomain leadNever
:sensitiveChanges to resources in manifest.sensitive_resourcesSensitive lead + one other (dual approval)Never
:complianceChanges to compliance declarations, policy modules, compliance-gated flagsCompliance officerNever — ADR link required

When in doubt, classify upward. A :behavioral change misclassified as :structural and auto-applied is a governance failure. The reverse is merely inconvenient.


Internal Architecture (Contributors)

Module Map

ModuleRole
Foundry.FileSystemValidated read boundary — all file access routes through here; enforces permitted root paths
Foundry.SparkMetaSpark DSL walker — extracts typed information from compiled modules
SparkLint.Rule (package)Behaviour for lint rules: check/2 → {:ok, [violation]}
SparkLint.Runner (package)Executes rules across all modules, collects violations
SparkLint.Violation (package)Violation struct: rule_id, module, message, severity, step, attribute
Foundry.LintRules.*Rule implementations — one module per rule
Foundry.LintRules.RegistryExplicit rule registration; module_rules/0 and manifest_validators/0
Foundry.Lint.RunnerHigh-level orchestrator: discovers modules, runs registry rules, emits LintReport
Foundry.Context.GraphBuilderAssembles the full node+edge graph from all project modules
Foundry.Context.NodeBuilderConstructs a NodeEntry from SparkMeta output
Foundry.Context.NodeEntryCore typed output struct for per-module context
Foundry.Context.EdgeEntryTyped edge between two nodes
Foundry.Context.LockFileWrites and validates .foundry/context.lock
Foundry.Context.ModuleDiscoveryDiscovers all compiled project modules
Foundry.Context.SpecKitIndexBuilderWalks docs/adrs/, docs/findings/, docs/runbooks/, docs/regulations/; populates spec_kit field
Foundry.Context.SessionStateCaptures system map state for graph delta computation
Foundry.ManifestAsh resource for manifest schema + validation (no database — Simple data layer)
Foundry.Manifest.ParserReads and parses .foundry/manifest.exs
Foundry.StatusComposes the runtime health picture from all Phase 1 sources
Foundry.Status.StackVersionsExtracts resolved dependency versions from mix.lock

Adding a Lint Rule

1. Create lib/foundry/lint_rules/your_rule.ex:

defmodule Foundry.LintRules.YourRule do
@behaviour SparkLint.Rule
def check(module, ctx) do
# ctx.metadata[:sensitive_modules] — list of sensitive module atoms
# ctx.modules — all discovered project modules
# ctx.metadata[:manifest] — parsed manifest keyword list
violations = []
{:ok, violations}
end
end

2. Register it in Foundry.LintRules.Registry:

@module_rules [
...,
Foundry.LintRules.YourRule
]

3. Add the rule to docs/lint-catalogue.md with status: planned before implementing, status: active once shipped.

4. Write a test in test/foundry/lint_rules/your_rule_test.exs.

Rules must handle crashes gracefully. Use rescue around any Spark introspection calls — Spark extensions may not be loaded for all modules in the project under test.