<!-- Sema documentation — Architecture & Best Practices
     Sema is a new AI-native, neurosymbolic programming language most models
     have not been trained on. Full corpus: https://sema.49.12.246.95.sslip.io/llms-full.txt
     Install language support: https://sema.49.12.246.95.sslip.io/install-skill/ -->

# Architecture & Best Practices

> How to structure a real Sema codebase — the canonical project skeleton, module decomposition, naming, effect discipline, contract placement, governance layout, agents and circuits, prompts, configuration, and the anti-patterns to avoid.

Sema's constructs are opinions about *where things go*: effects belong on
signatures, policies belong in reviewable artifacts, prompts belong in typed
suites, tests belong next to the unit they defend. A codebase that fights those
opinions fights the compiler. This page is the layout that works *with* them,
derived from the shipped examples — chiefly
[finops-ledger](/reference/examples-api/finops-ledger/),
[graphrag](/reference/examples-api/graphrag/), and
[crisis-logistics](/reference/examples-api/crisis-logistics/) — with the
mechanics on [Project Layout](/start/project-layout/).

## The canonical skeleton

```text
finops/
├── sema.toml            # identity + the dials: assurance floor, engine determinism, model lockfile
├── src/                 # one module = one .sema file = one concern
│   ├── main.sema        # thin entry point: wire, iterate, log — no business logic
│   ├── domain.sema      # structs/enums, sem descriptors, invariants, pure domain helpers
│   ├── models.sema      # pinned model bindings (rev, quant, role, calibration)
│   ├── policies.sema    # named policies + the pure helpers they govern + their tests
│   ├── config.sema      # args/config suites, provide factories, container — all wiring
│   ├── storage.sema     # the effectful edge: fs/db/net adapters, typed SQL
│   ├── reconcile.sema   # a pure-core feature module (the actual algorithm)
│   ├── supervision.sema # supervise blocks, restart/fallback/heal envelopes
│   └── assurance.sema   # cross-module tests and properties
├── tests/               # discovered by check/assure ONLY — never by a plain run
├── docs/api/            # generated by `sema doc` — never hand-edited
└── .sema/               # runtime state: venv, packages, journals, runs — gitignored
```

This is the shape of `examples/finops-ledger/` almost file for file, and the
smaller examples are subsets of it (`examples/graphrag/src/` is
`types / embed / similarity / store / api / main`). Two mechanics make it work
(LANGUAGE.md §5.18, D80):

- **Module identity is the file stem, globally unique.** `src/` is walked
  recursively, so you may group files into subfolders — but grouping is
  *physical only*. `src/storage.sema` and `src/adapters/storage.sema` are the
  same module id, and having both is a loud error: `duplicate module 'storage':
  … module ids are file stems, globally unique across src/ subfolders and
  tests/`. Imports resolve by last segment, so moving a file into a folder
  breaks no import.
- **`tests/` is invisible to `run`.** A sibling `tests/` directory is
  discovered for `check` and `assure` only. Verification code can never leak
  into production control flow, and nothing in `run` will execute it.

`.sema/` holds journals, run state, and installed packages; the repo's
`.gitignore` excludes it (`sema/.gitignore` line 12). Committing it ships
machine-local state and hash-chained journals as if they were source.

## Module decomposition

The corpus pattern, module by module:

| Module | Owns | Corpus reference |
|---|---|---|
| `domain.sema` | structs, enums, `sem` descriptors, `invariant`s, pure domain algebra (operators, predicates) | `examples/finops-ledger/src/domain.sema` |
| `models.sema` | every `model … = model(…)` binding — pinned rev, quant, role, calibration | `examples/finops-ledger/src/models.sema` |
| `policies.sema` | named `policy` suites, the pure helpers they justify (redaction, sanitization), and tests for those helpers | `examples/finops-ledger/src/policies.sema` |
| pure feature modules | the algorithm, mostly `!{}` / `!{model.embed}` | `examples/finops-ledger/src/reconcile.sema`, `examples/graphrag/src/similarity.sema` |
| `storage.sema` / adapters | the effectful edge: `db.*`, `fs.*`, typed SQL | `examples/finops-ledger/src/storage.sema` |
| `supervision.sema` | `supervise` envelopes: restart, fallback, heal | `examples/finops-ledger/src/supervision.sema` |
| `assurance.sema` | cross-module `test` blocks and end-to-end properties | `examples/finops-ledger/src/assurance.sema` |
| `main.sema` | a thin `def main()`: inject, iterate, log | `examples/finops-ledger/src/main.sema` (13 lines) |

`domain.sema` is where meaning is declared once — types carry their own
documentation and their own checks:

```sema
enum MatchState:
    unmatched | candidate | reconciled

struct Money:
    sem "A signed monetary amount in minor units"
    currency: str sem "Settlement currency code"
    minor_units: i64
    invariant len(currency) == 3

def same_currency(a: Money, b: Money) -> bool !{}:
    return a.currency == b.currency

operator +(left: Money, right: Money) -> Money !{}:
    require same_currency(left, right)
    return Money(currency=left.currency, minor_units=left.minor_units + right.minor_units)
```

`main.sema` stays thin — the finops entry point is a config lookup, a loop, and
a log line. Everything with a decision in it lives in a module that `assure`
can reach without going through `main`:

```sema
from finops.domain import Money
from finops.reconcile import amount_delta_abs
from finops.storage import load_statement_lines

def main() -> None !{fs.read, observe.record}:
    lines = load_statement_lines("statement.txt")
    booked = Money(currency="EUR", minor_units=4200)
    posted = Money(currency="EUR", minor_units=4200)
    log.info("batch loaded", lines=len(lines), delta=amount_delta_abs(booked, posted))
```

**When to split:** a module earns its own file when it acquires its own effect
row, its own policy attachment, or its own assure grade — those three attach at
module granularity (§5.18), so a file that wants two of anything is two files.
Don't split by layer reflexively: `policies.sema` keeps its helper functions
*and their tests* in the same file, because the redaction helper is meaningless
apart from the policy that requires it.

## Naming conventions

Derived from the shipped examples, not aspiration:

| Kind | Convention | Corpus evidence |
|---|---|---|
| package / project dir | kebab or snake; hyphens normalize to `_` in the import root | `crisis-logistics` → `from crisis_logistics.domain import …` |
| modules | `snake_case` single-concern nouns | `domain`, `reconcile`, `supervision` |
| structs / enums / traits / events | `PascalCase` | `LedgerEntry`, `MatchState`, `IncidentQuarantined` |
| policies, config/args suites, containers, components | `PascalCase` | `LedgerOps`, `LedgerConfig`, `LedgerCli`, `LedgerApp`, `LedgerRuntime` |
| functions, fields, **agents, circuits**, models, monitors, subscribers, templates | `snake_case` | `choose_assignment`, `agent researcher`, `circuit synthesize`, `anomaly_writer`, `monitor public_briefing_drift` |
| enum variants | lowercase | `usd \| eur`, `unmatched \| candidate \| reconciled` |
| constants (top-level `NAME = expr`) | `UPPER_SNAKE`; leading `_` for module-private caches | `BASE_EPOCH`, `TOPIC`; `_CACHE` in `examples/graphrag/src/main.sema` |
| test names | full sentences stating the property | `test "exact matching requires both currency and amount":` |

The split is principled: **nominal artifacts you attach and review**
(types, policies, configs, containers) are `PascalCase`; **things you call**
are `snake_case` — and agents and circuits are callables, so
`agent researcher(…)` and `circuit synthesize(…)`, never `Researcher`.

Test names are sentences because `assure` prints them as the verdict line —
`"delta is symmetric and zero on identical amounts"` reads as a property held
or falsified, where `test_delta_1` reads as noise.

## Effect discipline: the row is the architecture diagram

Sort every module onto one side of the `!{}` boundary and keep it there. The
effectful edge is thin adapters; the core is pure and therefore fuzzable,
memoizable, and replay-exempt:

```sema
import io

def load_statement_lines(path: str) -> list[str] !{fs.read}:
    mut lines = []
    for line in io.lines(path):
        if len(line.strip()) > 0:
            lines.append(line)
    return lines
```

Read the finops modules by their rows and you have drawn the system:

| Module | Effect rows | Reading |
|---|---|---|
| `domain.sema` | `!{}` everywhere | pure algebra |
| `reconcile.sema` | `!{}` for exact matching; `!{model.embed}` for similarity; `!{fs.read, model.embed, observe.record}` at the top | core with one calibrated edge |
| `ingest.sema` | `!{fs.read, ffi.call, model.invoke, observe.record}` | the adversarial-input boundary |
| `storage.sema` | `!{db.read}` | one capability, one module |
| `reporting.sema` | `!{}` sanitizers; `!{fs.write, net.connect, model.invoke}` exports under `@RegulatedExport` | pure prep, governed egress |
| `main.sema` | the union row | wiring only |

Rules that keep it that way:

- **Declare rows explicitly on everything public.** Inference is fail-closed,
  but at `assure silver` and above an explicit row is *required* — so a later
  `code.exec` sneaking into a dependency shows up as a signature diff in
  review, not a silent change.
- **Never ship `!{*}` in application code.** It is the loud escape hatch —
  flagged at check time, refused under any restricting policy. The one
  legitimate home is generic higher-order code that truly cannot know its
  callees' rows (`stdlib/sema/circuits.sema` uses it for combinators like
  `pipeline`); your modules are not that.
- **Review heuristic:** diff the effect rows before the bodies. A PR that turns
  a `!{}` module into a `!{net.connect}` module is an architecture change
  whatever the diff size says.

## Contracts and verification: what goes where

Each verification construct has one home:

| Construct | Placement |
|---|---|
| `invariant` | on the domain struct, in `domain.sema` — holds at every construction site forever |
| `require` / `ensure` | on `pub` seams — they are part of the public signature the verification cache keys on |
| soft `check semantics(…)` | at model and generated-value boundaries only (see the typed-SQL example below) |
| `test "…":` | next to the unit it defends, in the same module |
| cross-module tests, properties | `assurance.sema` or `tests/` — the fuzz-facing surface |

A feature module carries its contracts and its tests together:

```sema
from finops.domain import Money, same_currency

def amount_delta_abs(a: Money, b: Money) -> i64 !{}:
    require same_currency(a, b)
    ensure result >= 0
    if a.minor_units >= b.minor_units:
        return a.minor_units - b.minor_units
    return b.minor_units - a.minor_units

test "delta is symmetric and zero on identical amounts":
    a = Money(currency="EUR", minor_units=1250)
    b = Money(currency="EUR", minor_units=-300)
    ensure amount_delta_abs(a, b) == 1550
    ensure amount_delta_abs(b, a) == 1550
    ensure amount_delta_abs(a, a) == 0
```

The authored test is not redundant with the fuzzer. `assure` fuzzes every
`ensure` from the parameter types, but a strong `require` (here:
same-currency pairs) can starve random generation — the verdict is then
**amber**, "incomplete — generated 0/64 inputs satisfying the function
preconditions", not a fake green. Your named tests are the evidence that
survives when generation can't reach the precondition; write them for exactly
those functions.

**Grade policy:** set `[assurance] default = "silver"` in the manifest and
treat it as the floor. Bronze is for a spike you haven't shaped yet; gold is
for release-critical modules — finops declares `assure gold` per module, the
crisis example holds `silver`. Precedence is manifest < module `assure` <
per-function `@assure`, so tightening one hot module never requires touching
the rest.

## Governance layout: policies are code review artifacts

All policies live in `policies.sema` as **named, reviewed declarations** — the
finops ledger ships exactly three (`LedgerOps`, `RegulatedExport`,
`AnalystWorkbench`), each attached by name (`@LedgerOps`) where it governs:

```sema
from ledgergov.domain import BankLine

policy StatementIngest:
    allow:
        fs.read("inbound/**"), fs.write("state/**")
        model.invoke, model.embed
    forbid cap:
        net.connect except "bank-gateway.internal:443"
        code.exec, proc.spawn, policy.change
    examples:
        allow:
            fetch("https://bank-gateway.internal:443/statements")
        deny:
            code.exec(BankLine.raw_description)
            proc.spawn("python", ["parse.py", BankLine.raw_description])
    justification "Bank files are untrusted input; ingest must stay deterministic and auditable."
```

- **`examples:` blocks are executable documentation** — validated at compile
  time, so the policy's advertised behavior can't drift from its rules. Write
  the deny examples first, naming the concrete field you're afraid of
  (`BankLine.raw_description`); that is the threat model, in code, in review.
- **Posture vs. program policy.** The runtime posture (the sealed root your
  deployment grants) is the ceiling; program policies compose with it by
  lattice meet, so nested attachment **only shrinks authority** — a program
  policy can forbid more than the posture, never allow more. Design
  accordingly: put the broad envelope in one root policy (`[policy] root` in
  the manifest, as `crisis-logistics/sema.toml` does), narrow per-boundary
  policies (`RegulatedExport`) inside it.
- Note `policy.change` in the forbid list: the ledger policy forbids changing
  itself. Do this in every root policy.

## Agents, circuits, and durability

The escalation ladder — take the first rung that suffices:

1. **`def`** — deterministic logic. Most code stays here.
2. **`simulate def`** — one model-authored function with a typed result,
   contracts, and a `budget`. No identity, no tools, no loop.
3. **`agent`** — the model call gains identity, `use tools`, and its own
   budget envelope. Still a callable.
4. **`circuit`** — orchestration of several agents becomes a *durable*
   aggregate. The run is a memoized **WorkTree** under `.sema/runs/<run-id>/`
   — journaled, with content-keyed completed-leaf memos — so
   `sema circuit resume` reuses every finished leaf instead of re-spending
   the tokens.

```sema
from ledgergov.models import triage_writer

agent researcher(question: str) -> str by triage_writer:
    sem "Collect one attributable finding and separate fact from inference"
    budget model_calls=2, tokens=512
    ensure len(result) >= 1

agent writer(evidence: list[str]) -> str by triage_writer:
    sem "Synthesize the evidence with provenance and explicit uncertainty"
    budget model_calls=1, tokens=512
    ensure len(result) >= 1

circuit synthesize(questions: list[str]) -> str !{model.invoke}:
    budget agents=8, spawn_depth=0, model_calls=16, tokens=8000
    evidence = parallel [researcher(question) for question in questions]
    return writer(evidence)
```

Every model path in the corpus carries a budget — every `agent` and every
`circuit` in `examples/agent-research/src/main.sema` and
`examples/agent-software/src/main.sema` declares `budget …` with model calls,
tokens, and (on circuits) `agents` and `spawn_depth`. Treat a budget-less model
path as a review defect: a child cannot mint a fresh budget, so the envelope
you write at the circuit is the real spend ceiling.

**Placement:** model pins in `models.sema` (rev, quant, and — critically —
`role=generator` vs `role=verifier`, so a convenient generator can't quietly
become its own judge); agents and circuits in the feature module that owns the
workflow (`dispatch.sema` in crisis-logistics), not in a generic `agents.sema`
grab-bag, unless — like the harness above — agents *are* the feature.

## Prompts and templates

Prompt text is program surface. Keep it out of expression position:

```sema
template briefing_system(domain: str) -> Prompt[str]:
    sem "Stable system context for statement triage"
    role system:
        text f"You are a precise assistant for {domain}."
        text "Cite evidence and refuse unsupported claims."
```

- **`template`/`context` suites over inline strings** — the semantic-library
  example keeps a `templates.sema` whose suites (`library_editor_system`,
  `integrate_book_task`) are typed `Prompt[Book]` values, versioned and
  diffable like any declaration (`examples/semantic-library/src/templates.sema`).
- **Triple-quoted strings are raw** — `"""…"""` takes no escapes, so prompt
  blocks with backslashes, quotes, and braces survive verbatim. Use `rf"…"`
  when a prompt scaffold needs live `{holes}` *and* raw backslashes.
- **`sql"…"` templates over string SQL** — a typed SQL value interpolates as
  parameters, never as text, and its `validate` block is where the soft
  semantic check earns its keep:

```sema
from ledgergov.domain import BankLine

def load_lines(db: Db, account_id: str) -> list[BankLine] !{db.read}:
    sem "Tenant-scoped read through a typed SQL interpolation"
    query = validate sql"""
        select id, raw_description
        from bank_lines
        where account_id = {account_id}
        order by posted_epoch_s desc
    """:
        ensure sql.read_only(value)
        ensure sql.has_parameter(value, "account_id")
        check semantics("query cannot read outside the requested account", value, alpha=0.01)
    return db.query(query)
```

The hard `ensure`s prove what is provable (read-only, parameter present); the
`check semantics` adds calibrated evidence for what isn't. That ordering —
deterministic checks first, statistical evidence on top — is the house style
everywhere a model touches data (`examples/finops-ledger/src/storage.sema`).

## Configuration and DI: one wiring module

All wiring lives in `config.sema` — the finops file holds the *entire* chain
`args → config → provide → component → container`, and nothing else in the
codebase constructs a dependency by hand:

```sema
from ledgergov.models import triage_writer

config LedgerConfig:
    source yaml "config/ledger.yaml" optional
    source env prefix "FINOPS_"
    tenant: str = "default"
    paths:
        inbound_dir: str = "inbound/statements"
    models:
        triage_writer:
            temperature: f32 = 0.1 where 0.0 <= value <= 2.0
            max_tokens: int = 2048 where value > 0

provide configured_writer(cfg: LedgerConfig) -> ModelClient lifetime singleton:
    sem "Attach validated sampling config to the pinned triage model"
    return triage_writer.with(cfg.models.triage_writer)

component IngestRuntime:
    sem "Scoped dependency object for one ingest invocation"
    lifetime scoped(run)
    inject:
        cfg: LedgerConfig
        writer: ModelClient named "triage_writer"

container IngestApp:
    config LedgerConfig
    bind ModelClient named "triage_writer" = configured_writer(LedgerConfig)
    bind IngestRuntime lifetime scoped(run)
```

- **Source overlays, not env reads.** `source yaml … optional` + `source env
  prefix "FINOPS_"` + `source cli` merge with defaults-< file < env < cli
  precedence and `where` validation at the boundary — so no function body ever
  needs `env.read`, and the effect rows stay honest.
- **`where` clauses on model sampling knobs** (`temperature`, `max_tokens`)
  turn a bad deploy-time override into a loud config error instead of a quiet
  quality regression.
- **Consumers say `inject T`, never `container.get`.** The container is
  mentioned once, on `main` (`@LedgerApp` in
  `examples/finops-ledger/src/main.sema`); everything else declares needs.

## Anti-patterns

| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Everything in `main.sema` | one module = one effect row, one policy, one grade — a monolith forces the union row and the weakest story on all of it | split until each file has one row and one reason to change |
| `!{*}` in application code | flagged at check, refused under any restricting policy, and it erases the architecture information the row exists to carry | write the real row; silver+ demands it on the public surface anyway |
| Inline `with policy(…)` scattered ad hoc | authority decisions become undiscoverable; nothing is reviewed as an artifact | named policies in `policies.sema`, attached with `@PolicyName`; deny-examples for every feared input |
| Prompts as inline f-strings at call sites | prompt drift is invisible in review; no types, no reuse | `template`/`context` suites in a prompts module; `sql"…"` for queries |
| Committing `.sema/` | ships machine-local venvs, journals, and run state as source | it's in `.gitignore` for a reason; keep it there |
| Expecting `test` blocks to run under `sema run` | tests are check/assure-only by design — `tests/` isn't even *discovered* by `run` (D80) | verification runs in `sema assure`; wire smoke behavior into `main` if you need a runtime probe |
| Deep folder taxonomies as namespaces | folders are physical grouping only; module ids are file stems, and a duplicate stem across folders is a hard error | flat, well-named stems; folders only to shelve related files |

## Next

- [Cheat Sheet](/quick/cheat-sheet/) — every keyword and operator on one page.
- [Declarations](/quick/declarations/) — each top-level form and its clauses.
- [Project Layout](/start/project-layout/) — manifest keys, imports, visibility,
  and dependency mechanics.
- [Policy](/governance/policy/) and [Effects](/governance/effects/) — the
  governance machinery this page places.
