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 ID INV Severity Description
missing_paper_trail INV-011 :error Sensitive resource missing AshPaperTrail.Resource extension
missing_archival INV-012 :error Sensitive resource missing AshArchival.Resource extension
missing_runbook INV-005 :error Reactor with >3 steps missing @runbook declaration
missing_idempotency INV-004 :error Reactor with side-effect steps missing idempotency key
missing_description INV-006 :error Ash resource attribute missing description: value
ash_version_outdated:error Resolved ash version in mix.lock is below 3.x
elixir_version_unsupported:error Elixir version below minimum required for Ash 3.x
adapter_version_not_active:warning Provider 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.

Class When Approver Auto-apply
:structural New resource, attribute, relationship, description, test skeleton Any developer Configurable (auto_apply_structural)
:behavioral New Rule, Transfer step, Reactor, Oban job, state machine transition Domain lead Never
:sensitive Changes to resources in manifest.sensitive_resources Sensitive lead + one other (dual approval) Never
:compliance Changes to compliance declarations, policy modules, compliance-gated flags Compliance officer Never — 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

Module Role
Foundry.FileSystem Validated read boundary — all file access routes through here; enforces permitted root paths
Foundry.SparkMeta Spark 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.Registry Explicit rule registration; module_rules/0 and manifest_validators/0
Foundry.Lint.Runner High-level orchestrator: discovers modules, runs registry rules, emits LintReport
Foundry.Context.GraphBuilder Assembles the full node+edge graph from all project modules
Foundry.Context.NodeBuilder Constructs a NodeEntry from SparkMeta output
Foundry.Context.NodeEntry Core typed output struct for per-module context
Foundry.Context.EdgeEntry Typed edge between two nodes
Foundry.Context.LockFile Writes and validates .foundry/context.lock
Foundry.Context.ModuleDiscovery Discovers all compiled project modules
Foundry.Context.SpecKitIndexBuilder Walks docs/adrs/, docs/findings/, docs/runbooks/, docs/regulations/; populates spec_kit field
Foundry.Context.SessionState Captures system map state for graph delta computation
Foundry.Manifest Ash resource for manifest schema + validation (no database — Simple data layer)
Foundry.Manifest.Parser Reads and parses .foundry/manifest.exs
Foundry.Status Composes the runtime health picture from all Phase 1 sources
Foundry.Status.StackVersions Extracts 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.