Skip to content

Project Layout

This page covers how a Sema project is organized once it grows past one file: the src/ layout, splitting code into modules, imports and visibility, the sema.toml manifest, and adding dependencies.

A project is a directory. Its Sema source lives in src/, and the entry point is src/main.sema, whose main() function runs when you sema run the project. A larger project just has more files under src/:

finops/
├── sema.toml # the manifest (optional; see below)
└── src/
├── main.sema # entry point: defines main()
├── domain.sema # data types
└── policies.sema # governance

Each .sema file is one module. A package is the tree rooted at a sema.toml manifest, and the manifest’s [package] name is the import root for every module in that tree. So if sema.toml declares name = "finops", the file src/domain.sema is the module finops.domain.

Import from another module with from … import … or import … [as …]. Both resolve at compile time against the package graph — there is no runtime path search:

from finops.domain import LedgerEntry, Money
import finops.policies as policies

Two rules that differ from Python and matter in practice:

  • Wildcard imports do not exist. You name every symbol you import. This keeps code reviewable and lets the toolchain resolve names statically.
  • Re-export is explicit. To surface a name your module imported as part of your own public API, re-export it: pub from finops.domain import Money.

Cyclic imports and unresolvable imports are compile errors, and two packages that export the same root name require manifest aliasing rather than silently shadowing each other.

Declarations are module-private by default. Mark the public surface with pub (a soft keyword). Anything without pub is invisible outside its own module:

pub struct ReconciliationDecision:
matched: list[LedgerEntry]
pub def reconcile(lines: list[BankLine]) -> list[ReconciliationDecision] !{model.embed}:
...
def candidate_score(bank: BankLine, entry: LedgerEntry) -> f32 !{model.embed}:
... # module-private: only reconcile can call this

Visibility is per declaration, not per file, because the public signature — a pub declaration’s full signature including its contract clauses and effect row — is what the verification engine keys its caches on. A private-across-module access is a compile error that names the missing pub.

sema.toml at the project root is optional: with no manifest you get all defaults. When present, it declares the package and overrides defaults without touching code. A minimal manifest names the package and sets an edition:

[package]
name = "finops"
edition = "2026"
version = "0.1.0"
[assurance]
default = "silver" # module grade if a module doesn't declare its own

Beyond the package identity, the manifest is the single place to configure the runtime. The most useful sections:

[engine]
seed = 12345 # deterministic seed for reproducible runs
temperature = 0.7
[models] # which model backs each capability — swap in your own
embed = "my-custom-embedder"
generate = "models/qwen3-0.6b.Q4_K_M.gguf"
[semantic]
cluster_threshold = 0.9 # default threshold for semantic.cluster / semantic.dedup
[journal] # audit-trail preset (see governance)
level = "standard" # off | minimal | standard | audit

Everything the manifest sets is also readable from a program via config.get, config.model, and config.temperature, so configuration is diffable, tool-readable, and overridable without recompiling. A missing file means all defaults; present keys override; unspecified capabilities keep their smart default.

sema add handles both ecosystems from one command, installing into a project-local, gitignored .sema/ directory.

PyPI packages install into .sema/venv (via uv, with a pip fallback), recorded in .sema/packages.txt, and become immediately usable from Sema through the Python bridge — classes and methods included, no per-package glue:

Terminal window
sema add numpy
sema remove numpy
sema list

Native Sema packages — a directory with a sema-pkg.toml ([package] name = …) and its own src/*.sema — install from a local path or a git+<url> source into .sema/packages/<name>/:

Terminal window
sema add ./greetings
sema add git+https://example.com/greetings.git

Installed package modules import exactly like your own (from greetings.greet import hello). A project module of the same name overrides a package module, which is how you shadow or patch a dependency locally.

The standard library needs no installation

Section titled “The standard library needs no installation”

The std package ships embedded in the compiler — its source lives in Sema itself — so it is available to every project with no sema add:

from std.belief import Belief
from std.cache import memoize
from std.document import Report, render
from std.agent_loop import loop_until

Because the standard library is written in Sema (not hardcoded in Rust), its parameters and logic are inspectable and changeable in the language, and a user module of the same stem shadows the stdlib one. See the Standard Library overview for the full module list.

  • Toolchain — check, run, verify, document, and inspect a project.
  • Modules — the deeper reference on imports, visibility, and the package graph.