The seams CLI
seams is the command line for a Semantic Seams project. It reads a project file, seams.toml, kept next to an app's model. seams generate runs every generator the file names; seams check fails when the model, its gate queries or the generated outputs are not in order, and writes nothing; seams migrate writes the vocabulary migrations a model needs into its files; seams sequence derives an event's sequence diagrams; seams storm prints a process's derived event-storm timeline; seams cml moves the context map to and from Context Mapper DSL; seams diff classifies the shape and operation-contract changes between two model versions as compatible or breaking; seams plan writes those changes and the elements each layer adds and removes as a Markdown change plan; seams coverage lists the BPMN paths, transitions and policies no scenario or test exercises; seams secrets checks that each environment's configuration keys and secrets exist, without reading a value; seams context prints the hashed model facts a service task's code depends on; seams propose drafts a handler body with an LLM command; seams suggest asks an LLM command for a model change and keeps it pending until a person accepts it; seams new-generator scaffolds a generator package from a reference output; seams draft-generator has an LLM command draft that package's template and extract step and keeps them only when its golden test passes. Its distribution name is semanticseams-cli and its import name is seams_cli (packages/seams-cli/). Like the seams library, it is a uv workspace member and is not published to PyPI. uv sync --all-packages installs the seams console script into .venv/bin:
uv sync --all-packages
source .venv/bin/activate
seams generate --project examples/chat-appThe project file
model = "chat-app.trig"
[[generators]]
name = "fastapi"
out = "generated"
[[generators]]
name = "htmx"
out = "generated/templates"
[environments.production]| Key | Type | Meaning |
|---|---|---|
model | string, optional | The TriG file to generate from, or a directory whose *.trig files load together as one model ("." for the project's own directory); payloads and handlers/ resolve against the directory. |
[[generators]] | array of tables | One entry per generator to run, in any order. |
[[generators]].name | string | A generator registered in the seams.generators entry-point group: a built-in or one from an installed distribution (see Generators). |
[[generators]].out | string | The directory the generator writes into. |
gates | array of strings, optional | SPARQL SELECT files that seams check runs over the model; each must return no rows. |
coverage | array of strings, optional | Coverage reports seams check also fails on. The only one is "paths": each BPMN path no scenario runs (see seams coverage). |
[environments.<name>] | table of tables, optional | The app's environment names. Use the local names of the model's arch:Environments (development, production), so seams status --env development and the generated compose.development.yaml name the same environment. |
[propose] provider | string, optional | The provider command seams propose, seams suggest and seams draft-generator send their prompts to; --provider overrides it. |
[propose] model | string, optional | The LLM model seams propose, seams suggest and seams draft-generator ask the provider for, by name: passed as the environment variable SEAMS_LLM_MODEL and part of each command's cache key; seams propose also records it in the lock. --llm-model overrides it. |
[environments.<name>].endpoints | inline table of strings, optional | Container local name to base URL, read by seams status. Any other key in an environment table is ignored. |
Relative paths (model, every out and every gate) resolve against the directory holding the seams.toml, not the working directory, so the command gives the same result wherever it is run from.
The model
The model is the first of:
--model PATHon the command line;- the project file's
model; - the
SEAMS_MODELenvironment variable, when it is set and non-empty.
An explicit path always beats the environment, as with seams.load. With none of the three, seams generate and seams check fail with exit code 2.
seams generate
seams generate # the seams.toml in the working directory
seams generate --project examples/expenses/chapter-03
seams generate examples/expenses/chapter-03 # the same, by position
seams generate --project path/to/seams.toml --model other.trig
seams generate --lenient # preview a broken model; flags seams.lock| Option | Default | Meaning |
|---|---|---|
--project PATH | . | A seams.toml file, or a directory holding one. |
--model PATH | none | The model (.trig); overrides the project file and SEAMS_MODEL. |
PATH | none | --project or --model by position: a directory, a seams.toml or a path without a suffix is the project; any other file is the model, with the seams.toml beside it as the project. Giving PATH and the flag it stands for is a usage error. |
--lenient | off | Collect precondition and generator faults instead of stopping, write what the generators produce and record the faults in seams.lock (see Lenient mode). |
Every generator runs in memory first. The files are written only when all of them succeed, so a failing generator leaves every output untouched. Each output is written as UTF-8 with \n newlines, byte-for-byte what python -m seams_gen <generator> writes into the same directory. The command then prints each written path on stdout, sorted, relative to the working directory when it lies below it:
$ seams generate --project examples/chat-app
compose.development.yaml
compose.production.yaml
compose.development.yaml.trace.json
...
examples/chat-app/generated/chat_api.py
examples/chat-app/generated/chat_api.py.trace.json
examples/chat-app/generated/templates/chat_page.html
...
examples/chat-app/seams.lockThe last line is always the project's lockfile, written after every output.
A generator with a trace hook (the built-in htmx, fastapi, compose and dockerfile) also writes, beside each output <output>, its generation trace <output>.trace.json (ADR-0019): for every non-blank line of the output, the template lines that emitted it and the full IRIs of the model elements behind it. The sidecar is sorted-key JSON with no timestamps or absolute paths, so it is as deterministic as the output. Commit it with the output. A trace that does not name exactly the files the generator rendered exits 2, naming the generator, and nothing is written.
The lockfile
seams generate keeps a seams.lock next to the seams.toml and commits it with the outputs. It is JSON with sorted keys, two-space indentation and a trailing newline, so the same model and generators give the same bytes and a diff shows which model hash and generator version produced each file. The top level holds:
| Key | Meaning |
|---|---|
lockfile_version | The format version, 1. |
model | The model path, relative to the project directory. |
model_hash | seams.model_hash of the model, sha256:<hex>. |
outputs | One entry per generated file, sorted by path. |
faults | Only after seams generate --lenient found faults: one {focus, generator, message, shape} object per fault, sorted. A lock with this key is flagged. |
drafts | Only once seams propose has accepted a draft: one {context_hash, path, provider, sha256, task, template} object per accepted draft, sorted by task, with model too when the draft was asked for an LLM model. seams generate carries it forward unchanged; seams check reports draft-stale and draft-hand-edited from it. |
Each output entry records the generator, its version, the model hash, the file's sha256 and the full IRIs of the model elements the file came from. For a generator with a trace hook these sources are the sorted union of the terms in the file's trace, and the trace sidecar is an entry of its own, of the same generator and with the same sources; a generator without one gives them through sources (see the seams_gen README) and writes no sidecar. Sidecars are therefore audited like any output below: a hand-edited one is reported, a deleted one rewritten and an orphan deleted with its output. The path is POSIX and relative to the project directory, so it may start with ../. An entry from examples/chat-app/seams.lock:
{
"generator": "compose",
"generator_version": "0.1.0",
"model_hash": "sha256:11d35819c97e7e7599603d0e806ac6f4bdf0a195feb6c2256dbf037bcedc06bd",
"path": "../../compose.development.yaml",
"sha256": "49d3aafff4e4e369bba0d8e4be7a31db151a356239a9d530dcc350caa2f32b64",
"sources": [
"https://example.org/chat/chat_api",
"https://example.org/chat/chat_system",
"https://example.org/chat/chat_ui",
"https://example.org/chat/development",
"https://example.org/chat/llm_gateway",
"https://example.org/chat/message_db"
]
}A project whose generators write nothing (expense chapters 1 and 2) gets a lock with an empty outputs list.
Before writing, seams generate compares the old lock with the files on disk and with the new outputs:
- Orphans. An output the old lock lists but no generator writes any more, for example after a generator is removed from
seams.toml, is deleted and printed on stdout asremoved <path>, but only when it is a regular file (not a symlink) whose sha256 still equals the locked one. Otherwise it is kept and reported. A lock can therefore delete only a file whose exact content it already records. - Hand edits. A locked output whose file differs from both the locked hash and the new text is overwritten and reported.
- Deleted outputs. A locked output missing on disk is written again and reported.
The reports go to stderr and do not change the exit code:
seams generate: overwrote hand-edited examples/chat-app/generated/chat_api.py
seams generate: restored deleted examples/chat-app/generated/templates/chat_page.html
seams generate: kept hand-edited orphan examples/chat-app/generated/old.htmlWithout a lock (a first run) there is nothing to compare, so nothing is reported or deleted. A seams.lock that is not valid JSON or does not match the format exits 2 before any file is written, naming the file and the key. A failing generator likewise leaves the old lock and every file untouched. seams check uses the same comparison and fails when an output was edited by hand.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Every output was written. |
| 1 | A file could not be read or written. |
| 2 | The project file is missing or malformed, seams.lock is malformed, no model was given, a generator found a missing or ambiguous graph fact, the model breaks a generator's precondition shape, or a seams.generators entry point cannot be registered. With --lenient, generator facts and preconditions are faults instead (exit 0). |
Errors print to stderr as seams generate: <message>. A project-file error names the file by its absolute path and the offending key, for example /…/examples/chat-app/seams.toml: `generators[0]` needs a string `out` , and an unknown generator name lists the known ones. A failed precondition names the focus node, the shape and the generator, and nothing is written:
seams generate: https://example.org/expenses/broken_model: https://w3id.org/seams/shapes#ModelShape: generator 'strict-gen' requires the model to conform to this shape: Every seam:Model has exactly one seam:notation, and it is a seam:Notation.A shape result of severity sh:Warning (a deprecated vocabulary term, ADR-0021) is never a failed precondition. It prints to stderr as seams generate: warning <focus> <path> <message> and changes neither the outputs nor the exit code.
Lenient mode
seams generate --lenient is for previewing a model that is still broken (D057, ADR-0016). A precondition the model breaks becomes a fault and the generator still runs. A generator that raises GeneratorError writes nothing and adds a fault, and the files it wrote last time are kept and stay in the lock instead of being deleted as orphans. Each fault prints to stderr, then a summary, and the exit code is 0:
seams generate: fault strict-gen: https://example.org/expenses/broken_model: https://w3id.org/seams/shapes#ModelShape: generator 'strict-gen' requires the model to conform to this shape: Every seam:Model has exactly one seam:notation, and it is a seam:Notation.
seams generate: 1 fault(s); output is flaggedThe line is fault <generator>: <focus>: <shape>: <message>; for a raised GeneratorError, <shape> is the error's predicate. The faults go into the lock's faults key, so seams check reports a flagged finding and CI rejects the output until a strict run on a fixed model writes a lock without them. Project-file, lockfile and registry errors still exit 2.
seams check
seams check # the seams.toml in the working directory
seams check --project examples/expenses/chapter-08
seams check examples/expenses/chapter-08/expenses.trig # the same project, by position
seams check --project path/to/seams.toml --model other.trigThe options are those of seams generate. The command runs the checks below, reports every finding and writes nothing, so it is safe in CI and in a pre-commit hook (D044):
- Shapes. The model, unioned with
ontology/*.ttl, is validated with pyshacl (inference off) againstontology/shapes/seam-shapes.ttl, the same check as the viewer's health panel. Each validation result is ashaclfinding: focus node, path and message. - Gates. Each file the project lists under
gatesis a SPARQL SELECT query that runs over the same union graph. Every row it returns is agatefinding: the gate path, relative to the project, then the row's values. This repository's gates are indocs/queries/gates/.ungoverned-writes.rqlists each UI event that can write a data shape with no policy governing it: a write open to any authenticated identity.untranslated-cross-context-calls.rq(ADR-0032) lists each element that reads or writes another bounded context's shape with no translation the context map allows: the element, the shape, the element's context and the shape's. The chat app and expense chapters 7 to 11 run the first, chapter 11 both, with a path that climbs out of the project intodocs/; a project outside this repository keeps its gates next to itsseams.toml, for examplegates = ["gates/ungoverned-writes.rq"]. - Outputs. Every generator runs in memory, leniently, and the result is compared with the disk and with
seams.lock. A lock that records faults is aflaggedfinding; a generator fault the lock does not record makes the locklock-out-of-date. - Decisions. Faults the runtime would hit in a decision, found without running it (backlog K2). A DMN input whose
typeRefdisagrees with thesh:datatypeof itsdec:fromPath(numbertakes the numeric XSD types,string,booleananddatetheir own), and a gatewayconditionExpressionthat reads a variable no upstream business-rule task or user task puts in the task data, aredecisionfindings: the subject IRI (the decision, or the sequence flow as<bpmn model>#<flow id>), the fact (the property, or the variable name) and a message. - Timers. The checks the runtime makes on load for every lifted timer (ADR-0037 section 2): the graph and the BPMN XML agree, the expression is one quoted ISO 8601 literal of an accepted form (no variable, cron string, date-time without an offset, zero duration or cycle without a start), a cycle sits only on a timer start or a non-interrupting boundary, no timer start holds a duration, and each timer-started process has one
seam:schedulescontainer. The first refusal of each process is atimerfinding: the event (or process) IRI, the fact (bpmn:timeCycleand the like) and the message, so a model the runtime would refuse fails here first. - Review. Facts the model states twice or not at all (backlog R1,
seams.review_findings): ash:inlist that copies therdfs:labels of a lifecycle's states, ash:classwhose object is ash:NodeShape, ash:NodeShapewith nosh:targetClassthat is not itself a class, and ash:pathIRI nothing types as a property. Each is areviewfinding: the shape or path IRI, the fact (thesh:path,sh:targetClassorsh:pathitself) and a message. Review findings are warnings: they print, but they never fail the check. - Ontology. A class of a
seam:Ontologymodel whoserdfs:subClassOftarget is declared neither in the ontology nor in a resolved import (a mistypedgist:Transation) is anontology-superclassfinding: the class IRI,rdfs:subClassOf, the target IRI and a message. It is a warning, since rdflib does not reason (ADR-0022 section 5). Anowl:importsthat does not resolve to a file inontology/vendor/(an unknown IRI, or another gist version than the vendored one) is aResolutionError(<ontology>, owl:imports, …)on load, so the model cannot be loaded and the check exits 2. - Profile, only when the model attaches a domain profile with
seam:conformsTo(ADR-0033 section 1). The profile is resolved from the model directory as the viewer resolves it (seams.resolve_profile), and the same union graph is validated against the profile's shapes alone, inference off (the seam shapes are the shapes check's). A first stdout line,profile <IRI> <n> result(s), names the profile and the number of results reported below it; it is not a finding. Eachsh:Violationresult is aprofilefinding (focus node, path and message), eachsh:Warningorsh:Inforesult aprofile-warning. Results of pill shapes (a labelledsh:Infoshape) are not reported. A profile that cannot be resolved is oneprofilefinding: theResolutionError's subject, predicate (seam:conformsToorseam:profileShapes) and detail. A model with noseam:conformsToreads no profile file and prints nothing for this check.seams generateignores profiles. - Coverage, only with
coverage = ["paths"]in the project file. Each start-to-end BPMN path noverify:Scenarioruns is acoveragefinding: the process IRI, then the path's flow node IRIs (see seams coverage). The other two coverage reports are gate queries: adddocs/queries/coverage/unfired-transitions.rqanddocs/queries/coverage/policies-without-deny.rqtogates.
| Kind | Meaning |
|---|---|
shacl | The model breaks a shape. |
gate | A gate query returned a row. |
missing | An output generate would write is absent on disk. |
stale | An output on disk differs from what generate would write now, for example after a model change. |
hand-edited | A stale output whose sha256 is not the locked one either: someone edited it. |
orphan | seams.lock lists an output no generator writes any more, and it is still on disk. |
lock-out-of-date | There is no seams.lock, or the lock generate would write differs from it. |
flagged | seams.lock records faults from seams generate --lenient; the detail is the lock path and N fault(s). |
decision | A DMN typeRef disagrees with its input's sh:datatype, or a gateway condition reads a variable nothing upstream sets. |
timer | The runtime would refuse a timer: its expression, its placement or its scheduling container. |
review | A warning, not a problem: lifecycle labels copied into sh:in, sh:class pointing at a shape, a shape without sh:targetClass, or an undeclared sh:path. |
ontology-superclass | A warning, not a problem: an ontology class's superclass is declared neither in the ontology nor in a resolved import, a likely typo. |
profile | The model breaks a sh:Violation shape of its attached profile, or the profile cannot be resolved. |
profile-warning | A warning, not a problem: a sh:Warning or sh:Info result of the attached profile's shapes. |
coverage | With coverage = ["paths"]: a BPMN path no scenario runs. |
draft-stale | A seams propose draft recorded in seams.lock whose task's context hash has changed (or whose task no longer resolves): review it again. The detail is the task IRI and the module path. |
draft-hand-edited | A recorded draft whose file's sha256 differs from the record, or whose file is missing (detail ends missing). |
Findings print to stdout as <kind> <detail>, sorted, with paths relative to the working directory when they lie below it. A closing count of the problems, every kind but the advisory review, ontology-superclass, shacl-warning, profile-warning and vocabulary-migrated, goes to stderr:
$ seams check --project examples/expenses/chapter-05
gate examples/expenses/chapter-05/gates/events.rq https://example.org/expenses/submit_expense
hand-edited examples/expenses/chapter-05/generated/expense_api.py
lock-out-of-date examples/expenses/chapter-05/seams.lock
review https://example.org/expenses/ApprovalShape http://www.w3.org/ns/shacl#targetClass ApprovalShape has no sh:targetClass, so it says what a form holds but not what class of thing it describes
…
shacl https://example.org/expenses/expense_data https://w3id.org/seams/seam#notation Every seam:Model has exactly one seam:notation, and it is a seam:Notation.
seams check: 4 problem(s)A shacl-warning finding is a shape result of severity sh:Warning: the model uses a deprecated vocabulary term, and the message says what to write instead (ADR-0021 section 6). A vocabulary-migrated finding names a vocabulary, the version the model targets and the version the tool ships: the model was migrated in memory on load (section 5), and its files are unchanged. When the only findings are these advisory kinds, ontology-superclass and review, stderr says seams check: N warning(s) instead and the exit code is 0.
seams generate fixes every output finding except flagged, which needs the model fixed and a strict seams generate. A shacl, gate, decision, timer, review, ontology-superclass, profile, profile-warning or coverage finding is fixed in the model (or, for an unresolvable profile, in its profiles/ package).
Exit codes
| Code | Meaning |
|---|---|
| 0 | No findings, or only review, ontology-superclass, shacl-warning, profile-warning and vocabulary-migrated warnings. |
| 1 | At least one finding of another kind, or a file could not be read. |
| 2 | The project file is missing or malformed (including a coverage entry other than "paths"), a gate is missing, unparsable or not a SELECT query, seams.lock is malformed, no model was given, the model cannot be loaded, or a seams.generators entry point cannot be registered. A generator fault is never exit 2 here: the outputs check runs leniently. |
Errors print to stderr as seams check: <message>.
In CI
The composite action .github/actions/seams-check runs seams check --project for each line of its projects input (default .), each in its own log group, and fails after the last project if any exited non-zero (warnings print in the log but do not fail it). The CLI is not published, so seams must be on PATH before the action runs. This repository's acceptance job does:
- run: uv sync --locked --all-packages
- run: echo "$PWD/.venv/bin" >> "$GITHUB_PATH"
- name: Generated outputs are current
uses: ./.github/actions/seams-check
with:
projects: |
examples/chat-app
examples/expenses/chapter-01Another repository references it as MetaCoding-io/SemanticSeams/.github/actions/seams-check@<ref>.
As a pre-commit hook
The repository root's .pre-commit-hooks.yaml defines the seams-check hook. It uses language: system, so seams must be installed and on PATH; it runs on commits that touch a seams.toml, seams.lock, model, query, BPMN or DMN file or anything under generated/. Without args it checks the seams.toml in the repository root:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/MetaCoding-io/SemanticSeams
rev: <commit or tag>
hooks:
- id: seams-check
args: [--project, examples/chat-app]seams migrate
seams migrate --project examples/expenses/chapter-01
seams migrate --model path/to/model.trigA model declares the vocabulary versions it was written for in its manifest graph (ADR-0021). seams.load, and so every other command, migrates a model that targets an older vocabulary in memory and seams check reports a vocabulary-migrated warning per step. seams migrate makes the migration permanent: it applies the same SPARQL Update steps to a copy of the model as written and saves the difference, graph by graph, as one patch through the model's one write path (ADR-0017). Only the changed triples' text changes; comments and layout stay. The manifest graph's owl:imports declaration is rewritten to the version the tool ships, in place in the block that declares it (every example and tutorial checkpoint has one). For a model written for seam 1.0, run by a tool that ships seam 2.0:
migrated seam 1.0 2.0
examples/expenses/chapter-01/expenses.trigEach step prints as migrated <vocabulary> <from> <to>, then each written file. A model already on the shipped versions prints seams migrate: nothing to migrate to stderr, writes nothing and exits 0, so running it twice is safe. A model newer than the tool, a missing migration step, or a patch the writer cannot place (a directory model with no file holding a <urn:seams:graph:manifest> { } block for the new declaration) prints seams migrate: <message>, exits 2 and writes nothing.
Generators
A generator is a seams_gen.protocol.Generator registered in the seams.generators entry-point group (ADR-0016). seams generate and python -m seams_gen run every registered one, and seams.lock records each output's own generator version. A generator may name precondition shapes, which seams generate checks before running anything (strict mode). The built-ins are below, and the seams_gen README gives each one's rules; Write your own generator registers another.
| Name | Writes into out |
|---|---|
fastapi | <container>.py, the route module of the serving container |
htmx | the Jinja2/htmx page templates and their partials |
compose | one compose.<environment>.yaml per arch:Environment (for example compose.development.yaml and compose.production.yaml), applying each environment's deploy:overrides (ADR-0015); bind-mount sources and build contexts are relative to out, and there is no shared compose.yaml |
tests | test_scenarios.py, test_decisions.py, test_shape_examples.py and test_policies.py, pytest files for the model's verify: scenarios and examples and allow/deny tests for each ODRL permission and party (ADR-0014) |
smoke | test_smoke.py, one pytest per hydra:Operation that calls it on its comm:path against SEAMS_SMOKE_BASE_URL as SEAMS_SMOKE_USER/SEAMS_SMOKE_GROUPS and checks the answer against seam:returnsShape; skipped without a base URL, or when a governing policy refuses the identity |
acceptance | test_<module>.py, one pytest file per module of the handlers package that checks the hand-written handlers against the model: their signatures against the stub's types, their result per verify:validExample against seam:returnsShape or hydra:possibleStatus, the lifecycle guard of the transition the task fires and the decision routing into the task, all in process (backlog C2) |
dockerfile | <route module stem>.Dockerfile (for example chat_api.Dockerfile), the serving container's runtime image build file |
ifmledit | ifmledit.json, the IFML layer as IFMLEdit.org JSON, a conformance check of the ifml: vocabulary (backlog 12); the ifml: terms with no IFMLEdit counterpart are named on stderr and listed as GAP-29 to GAP-35 in the gap register |
jsonld | <shape>.jsonld, a JSON-LD 1.1 context per sh:NodeShape with a sh:targetClass: each field's sh:name mapped to its sh:path, typed by its sh:datatype, category fields mapping their labels to member IRIs; nothing for a model without target classes (ADR-0024) |
mentalmodel | a SemPKM Mental Model: manifest.yaml and ontology/, shapes/ and views/<modelId>.jsonld, from the model's one ontology model (its IRI, owl:versionInfo and rdfs:label fill the manifest), its target-classed shapes and one table view per IFML list or details view; SemPKM installs the directory with POST /api/models/install (ADR-0026) |
python -m seams_gen <generator> <model> --out <dir> still runs a single generator without a project file.
Worked example: the chat app
examples/chat-app/seams.toml names all three generators. The compose file goes to the repository root (out = "../..") because docker compose up runs there:
model = "chat-app.trig"
[[generators]]
name = "fastapi"
out = "generated"
[[generators]]
name = "htmx"
out = "generated/templates"
# One compose.<environment>.yaml per arch:Environment at the repository root
# (ADR-0015); their bind mounts and build contexts are relative to the root.
[[generators]]
name = "compose"
out = "../.."
[environments.production]
[environments.development]
endpoints = { chat_api = "http://localhost:8080" }seams generate --project examples/chat-app rewrites examples/chat-app/generated/, the root compose.development.yaml and compose.production.yaml, and examples/chat-app/seams.lock. Each chapter of the expense tutorial has its own project file in examples/expenses/chapter-NN/seams.toml. CI runs seams check for the chat app and every chapter through the seams-check action, and fails on any finding.
seams status
seams status # the seams.toml in the working directory
seams status --project examples/chat-app --env development
seams status --project examples/expenses/chapter-08 --timeout 2| Option | Default | Meaning |
|---|---|---|
--project PATH | . | A seams.toml file, or a directory holding one. |
--model PATH | none | The model (.trig); overrides the project file and SEAMS_MODEL. |
PATH | none | --project or --model by position: a directory, a seams.toml or a path without a suffix is the project; any other file is the model, with the seams.toml beside it as the project. Giving PATH and the flag it stands for is a usage error. |
--env NAME | the only environment with endpoints | The [environments.<name>] whose endpoints to read. |
--timeout SECONDS | 5 | How long to wait for each /healthz. |
seams status asks each running runtime container which model it is on and compares the answer with the model in the working tree (D045). It writes nothing.
Endpoints
The containers come from the model: they are the runtime containers, the ones the compose generator gives SEAMS_MODEL_HASH. Their base URLs come from the project file, because the generated compose file only exposes a runtime's port inside the stack and the model holds no URLs yet:
[environments.development]
endpoints = { chat_api = "http://localhost:8080" }Each key is a runtime container's local name and each value a base URL; seams status GETs <url>/healthz. In the chat app and expense chapter 8 the UI container (Caddy) on port 8080 proxies /healthz to the runtime, so http://localhost:8080 reaches it from the host.
With no --env, the command uses the only environment that has endpoints. No such environment, more than one, an unknown --env, or an endpoint naming a container that is not a runtime container of the model is an error (exit 2). A runtime container with no endpoint in the chosen environment is reported as unreachable.
Verdicts
Each container gets one verdict by comparing the model_hash its /healthz reports with the hash of the current model (seams.model_hash):
| Verdict | Meaning |
|---|---|
current | The same hash. |
behind | A different hash that some version of the project's seams.lock in HEAD's git history records. The detail counts the model changes behind, for example 2 model changes behind. |
ahead | A different hash that no committed seams.lock records: the container runs a newer or diverged model. Outside a git repository, or with no committed lock, every differing hash is ahead. |
unreachable | No endpoint, a connection error, a timeout, a non-2xx response, invalid JSON, or a body with no string model_hash. The detail names the URL and the error. |
A hash has no order of its own. The lock already records the model hash of every generation, so its history (git log HEAD -- seams.lock, then git show of each version) orders the past hashes: the newest distinct hash before the current one is 1 change behind, the next 2, and so on.
Output
One stdout line per runtime container, sorted by name, as <container> <verdict> <reported hash or -> <detail>, then the current model's hash. Against the stack the generated development file starts from the repository root:
$ docker compose -f compose.development.yaml up -d --wait
$ seams status --project examples/chat-app --env development
chat_api current sha256:11d35819c97e7e7599603d0e806ac6f4bdf0a195feb6c2256dbf037bcedc06bd
current model sha256:11d35819c97e7e7599603d0e806ac6f4bdf0a195feb6c2256dbf037bcedc06bdWith the stack down, the container is unreachable and the count goes to stderr:
$ seams status --project examples/chat-app --env development
chat_api unreachable - http://localhost:8080/healthz: [Errno 111] Connection refused
current model sha256:11d35819c97e7e7599603d0e806ac6f4bdf0a195feb6c2256dbf037bcedc06bd
seams status: 1 of 1 not currentAfter a model edit and seams generate, a container still running the old build reads chat_api behind sha256:… 1 model change behind until it is rebuilt or restarted.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Every runtime container is current. |
| 1 | At least one is behind, ahead or unreachable. |
| 2 | The project file is missing or malformed, the environment is missing, unknown or ambiguous, an endpoint names no runtime container, no model was given, or the model cannot be loaded. |
Errors print to stderr as seams status: <message>.
seams secrets
seams secrets # the seams.toml in the working directory
seams secrets --project examples/chat-app --env development
seams secrets --project examples/expenses/chapter-08 --env production --json
seams secrets --project examples/chat-app --write-example| Option | Default | Meaning |
|---|---|---|
--project PATH | . | A seams.toml file, or a directory holding one. |
--model PATH | none | The model (.trig); overrides the project file and SEAMS_MODEL. |
PATH | none | --project or --model by position: a directory, a seams.toml or a path without a suffix is the project; any other file is the model, with the seams.toml beside it as the project. Giving PATH and the flag it stands for is a usage error. |
--env NAME | every environment | The arch:Environment to check, by local name. |
--write-example | off | Also write <project root>/.env.example. |
--json | off | Print the rows and verdict counts as JSON. |
seams secrets lists every configuration key and secret a deployment needs, per environment, and checks that each one exists before the deployment runs (backlog A8). It never reads a value. It writes nothing unless --write-example is given.
Rows
The rows come from the model, not from a compose file. For each arch:Environment (by IRI), each container (by IRI) and each binding of Container.for_environment, the merge of the shared deploy:bindings with the environment's deploy:overrides (ADR-0015), one row, sorted by key:
| Kind | Binding | Source |
|---|---|---|
value | deploy:value | none (-); the literal is in the model |
secret-ref | deploy:secretRef | the deploy:secretName, else the secret's IRI |
secret-file | deploy:secretFile | the path, resolved against the model's directory and written relative to the project root |
service | deploy:serviceOf | the container it names |
A binding with no kind or more than one is a model fault (exit 2).
Verdicts
| Verdict | Meaning |
|---|---|
present | The key or secret exists. Value and service rows are present by definition. |
missing | A checker looked and found nothing. |
unchecked | No checker could tell. |
Every secret row asks the checkers in this order, and the first answer that is not unchecked wins:
- Files. A
secret-filerow is present when the path exists and missing when it does not. OnlyPath.existsis called; the file is never opened. .env. A key named in<project root>/.envis present, for value andsecret-refrows. Only the text before each line's first=is kept (anexportprefix and comments are skipped). Because the value is never read, a key with a blank value counts as present too: a.envcopied from.env.examplenames every key. A key absent from.envproves nothing, so the row goes on to the next checker.- Docker secrets. A
secret-refrow is present when its secret name is in the namesdocker secret lsprints and missing when it is not. The command runs once, with a 10 second timeout. With nodockeron the path, a failing command (the host is not a swarm manager, for example) or a timeout, the row stays unchecked and the reason goes to stderr asseams secrets: unchecked: <reason>.
No checker reads a value, so no value can reach the output.
Output
One stdout line per row, as <env> <container> <key> <kind> <verdict> <source or ->:
$ seams secrets --project examples/chat-app --env development
development chat_api DB_URL secret-file present ../../docker/local-secrets/db_url
development chat_api LLM_GATEWAY_URL service present llm_gateway
development chat_api SEAMS_DEV_USER value present -
development chat_api SEAMS_SEED value present -
development llm_gateway LLM_API_KEY secret-file present ../../.secrets/llm_api_key
development llm_gateway LLM_GATEWAY_STUB value present -
development llm_gateway LLM_MODEL_OVERRIDE value present -
development llm_gateway UPSTREAM_BASE_URL value present -
development message_db POSTGRES_DB value present -
development message_db POSTGRES_PASSWORD value present -Production reads its connection string and API key from swarm secrets, so on a swarm manager that has not created them the count goes to stderr:
$ seams secrets --project examples/chat-app --env production
production chat_api DB_URL secret-ref missing chat-prod-db-url
production chat_api LLM_GATEWAY_URL service present llm_gateway
production llm_gateway LLM_API_KEY secret-ref missing chat-prod-llm-key
...
seams secrets: 3 missing--json prints {"rows": [...], "counts": {"present": N, "missing": N, "unchecked": N}} instead, each row with environment, container, key, kind, source and verdict.
.env.example
--write-example writes <project root>/.env.example for --env, else for development, else for the first environment, and prints its path. Keys are sorted. A plain value keeps the model's literal (with a # differs per container comment when containers disagree), each secret is blank under # secret: <kind> <source>, and each service is blank under # service: <container>:
# secret: secret-file ../../docker/local-secrets/db_url
DB_URL=
...
POSTGRES_DB=chatThe file is byte-deterministic. The chat app's examples/chat-app/.env.example and expense chapter 8's are committed, and a test keeps each equal to a fresh render. Copy it to .env and fill it in. .gitignore ignores .env and .env.* but not .env.example.
Exit codes
| Code | Meaning |
|---|---|
| 0 | No row is missing. Unchecked rows do not fail. |
| 1 | At least one row is missing. A file could not be read or written. |
| 2 | The project file is missing or malformed, --env names no arch:Environment, no model was given, the model cannot be loaded, or a binding has no kind or more than one. |
Errors print to stderr as seams secrets: <message>.
seams sequence
seams sequence :send_message --project examples/chat-app
seams sequence :submit_expense --project examples/expenses/chapter-08 --out build/sequences
seams sequence :send_message --project examples/chat-app --trace tests/fixtures/traces/chat_send_message.json| Option | Default | Meaning |
|---|---|---|
EVENT | required | The IFML event: a CURIE (:send_message), a local name or a full IRI. |
--project PATH | . | A seams.toml file, or a directory holding one. |
--model PATH | none | The model (.trig); overrides the project file and SEAMS_MODEL. |
--out DIR | <project root>/sequences | The directory the .mmd files are written to. |
--trace RUN | none | Draw one recorded run as <event>.executed.mmd: a trace JSON file, or an instance id in the --db store. |
--db PATH | SEAMS_DB, else <project root>/seams.sqlite | The runtime's SQLite store to read an instance id from. Only valid with --trace. |
seams sequence derives the as-designed Mermaid sequence diagrams of an IFML event (backlog U1, D047). The arrows come only from graph facts, and their order from the sequence flows of the BPMN process the event triggers. There is one diagram per path through the exclusive gateways, never an alt block: the chat app's :send_message gives send_message.mmd, and expense chapter 8's :submit_expense gives submit_expense--auto.mmd, submit_expense--manager--approved.mmd, submit_expense--manager--rejected.mmd, submit_expense--finance--approved.mmd and submit_expense--finance--rejected.mmd. The committed goldens are under docs/generation/sequences/. The fact-to-arrow rules are listed in seams_gen/README.md; python -m seams_gen.sequence <trig> <event> --out DIR runs the same deriver without a project file.
The diagrams are views, so they are not generator outputs: seams generate does not write them and seams.lock does not record them.
As executed: --trace
--trace RUN draws one recorded run instead of the as-designed paths (backlog U4). The rows come from the trace the runtime's worker records, one per BPMN element it ran. RUN is either a JSON file, holding an instance result ({"instance": ..., "trace": [...]}) or a bare list of rows, or an instance id. An instance id is read from the workflow_state checkpoint in the runtime's SQLite store: --db PATH, else SEAMS_DB, else <project root>/seams.sqlite. The diagram is written as <out>/<event>.executed.mmd on the same actor, UI and API lifelines as the as-designed diagram, and its header names the run (%% run: <id>):
$ seams sequence :send_message --project examples/chat-app --trace 3f2a... --out build
build/send_message.executed.mmdA row the model cannot place (an IRI that is not an element of the triggered process, or an unknown container or channel) is a gap, reported as below. An instance id missing from the store, or a trace file that is not JSON, exits 2; --db without --trace is a usage error. The committed golden is docs/generation/sequences/chat-app/send_message.executed.mmd, drawn from tests/fixtures/traces/chat_send_message.json.
Gaps
An arrow the deriver cannot place, because a fact is missing from the model, is a gap. It is written into the diagram's header as a %% gap: comment and printed to stderr. With MessageShape seam:storedIn :message_db removed from the chat app:
$ seams sequence :send_message --project examples/chat-app --model edited.trig --out build
seams sequence: gap send_message: MessageShape written by persist_user_msg has no seam:storedIn
seams sequence: gap send_message: MessageShape written by persist_response has no seam:storedIn
build/send_message.mmdGaps are an audit of the model, not a failure, so the exit code stays 0.
Exit codes
| Code | Meaning |
|---|---|
| 0 | The diagrams were written, with or without gaps. |
| 1 | A file could not be read or written. |
| 2 | The project file is missing or malformed, no model was given, the model cannot be loaded, the event is unknown, the --trace instance is not in the store, the trace file is not JSON, or --db was given without --trace. |
Errors print to stderr as seams sequence: <message>.
seams storm
seams storm approval_process --project examples/expenses/chapter-10
seams storm :submit_expense --project examples/expenses/chapter-10 --out build/storm.mdseams storm prints the derived event-storm timeline of one process as Markdown (backlog B4, seams_gen.storm). NAME is the process, or an IFML event that seam:triggers one, as a CURIE, local name or full IRI; naming submit_expense gives the same timeline as naming approval_process. The model is chosen as for seams generate. The timeline lists, per BPMN step in flow order, the actors, commands, aggregates, domain events, DMN decisions, published channels and started processes, then the read models and the gaps. What each entry is derived from is in Diagrams are views.
With --out FILE the timeline is written to FILE instead of stdout, and the path is printed. The file is not a generator output, so seams.lock does not record it.
Gaps
A fact the view expects and cannot find is a gap. It is listed under ## Gaps in the timeline and printed to stderr. On chapter 10:
$ seams storm approval_process --project examples/expenses/chapter-10 > /dev/null
seams storm: gap approval_process: ApprovalShape has no read model: no ifml page or component seam:reads it
seams storm: gap approval_process: NotificationShape has no read model: no ifml page or component seam:reads itGaps are an audit of the model, not a failure, so the exit code stays 0.
Exit codes
| Code | Meaning |
|---|---|
| 0 | The timeline was printed or written, with or without gaps. |
| 1 | A file could not be read or written. |
| 2 | The project file is missing or malformed, no model was given, the model cannot be loaded, or NAME is no process and no event that triggers exactly one. |
Errors print to stderr as seams storm: <message>, naming the argument.
seams cml
seams cml export examples/expenses/chapter-11
seams cml export --project examples/expenses/chapter-11 --out build/context-map.cml
seams cml import build/context-map.cml examples/expenses/chapter-11seams cml moves the context map between the model and Context Mapper DSL (CML), as ADR-0034 maps it (backlog B5). PATH is a project or a model, or --project and --model choose it as for seams generate.
Export
seams cml export [PATH] [--out FILE] prints one ContextMap that contains every arch:BoundedContext, one line per arch:ContextRelationship (ADR-0032), then a bodiless BoundedContext per context. Each name is the IRI's local name. Each type has one bracket form, upstream on the left:
arch:relationshipType | CML |
|---|---|
arch:CustomerSupplier | <up> [U,S]->[D,C] <down> : <name> |
arch:Conformist | <up> [U]->[D,CF] <down> : <name> |
arch:AntiCorruptionLayer | <up> [U]->[D,ACL] <down> : <name> |
arch:PublishedLanguage | <up> [U,PL]->[D] <down> : <name> |
arch:SharedKernel | <up> [SK]<->[SK] <down> : <name> |
Contexts and relationships are sorted by IRI, so the same model gives the same bytes. Chapter 11 exports as:
ContextMap {
contains expense_context, finance_context
expense_context [U]->[D,CF] finance_context : expense_to_finance
}
BoundedContext expense_context
BoundedContext finance_contextWith --out FILE the CML is written to FILE instead of stdout, and the path is printed. The file is not a generator output, so seams.lock does not record it. A relationship that is not fully stated, or whose type has no CML form, exits 2 and writes nothing.
Import
seams cml import FILE [PATH] reads FILE's ContextMap relationships and adds each one the model can hold as an arch:ContextRelationship with its upstream, downstream and type, through the model's one write path (ADR-0017). The downstream role decides the type first: ACL, then CF, then S/C, then PL (with or without OHS), then SK. Each context name must be the local name of an arch:BoundedContext in the model. The new IRI is the CML name, or <upstream>_to_<downstream> when the line has none, in the upstream context's namespace. For each relationship written it prints added <relationship>, then each written file:
added expense_to_finance
examples/expenses/chapter-11/expenses.trigImport only adds facts and never removes one. A relationship the model already holds, with the same direction and type, is skipped silently, so importing chapter 11's own export writes nothing and running it twice is safe.
Flags
A construct with no mapping is flagged on stderr and skipped. It is never refused, and the exit code stays 0:
seams cml import: flag expense_context [U,OHS]->[D] finance_context: an open host service alone has no arch:RelationshipType (ADR-0032 folds it into PL)
seams cml import: flag expense_context [P]<->[P] billing: a partnership has no arch:RelationshipType (ADR-0032)
seams cml import: flag BoundedContext billing: billing matches no arch:BoundedContext in the model
seams cml import: flag BoundedContext billing attributes: BoundedContext attributes are not read (ADR-0034 section 1)Flagged are an open host service alone or a relationship with no roles, a partnership, a role the type does not use, a context name the model does not have or that names more than one context, a pair the model already relates in another direction or with another type, and every other CML construct (aggregates, entities, services, teams, use cases). The graph has its own facts for those, read from BPMN, DMN and SHACL.
Exit codes
| Code | Meaning |
|---|---|
| 0 | The CML was printed or written, or the import finished, with or without flags. |
| 1 | FILE cannot be parsed (the message names the line), or a file could not be read or written. Nothing is written. |
| 2 | The project file is missing or malformed, no model was given, the model cannot be loaded, or a relationship cannot be exported. |
A parse error prints as seams cml import: <FILE>: line <n>: <message>:
seams cml import: map.cml: line 3: expected the context on the right of the relationship, found '}'seams diff
seams diff examples/expenses/chapter-08/expenses.trig examples/expenses/chapter-09/expenses.trig
seams diff old/ new/ --json| Option | Default | Meaning |
|---|---|---|
OLD | required | The old model: a .trig file, or a directory whose TriG files load as one model. |
NEW | required | The new model, in the same form. |
--json | off | Print the changes, their scopes and the counts as JSON instead of text. |
seams diff says whether a model change is safe for stored data and for operation contracts (backlog U8, D052). It reads no seams.toml. It compares the SHACL shapes and the operation contracts (seam:acceptsShape, seam:returnsShape) of two model versions and classifies each change:
| Change | Verdict |
|---|---|
| A node shape added | compatible |
| A node shape removed | breaking |
A node shape renamed (new owl:sameAs old, stated in NEW) | compatible |
A property added with no sh:minCount, or sh:minCount 0 | compatible |
A property added with sh:minCount above 0 | breaking |
| A property removed | breaking |
An sh:datatype or sh:class changed or added | breaking |
An sh:datatype or sh:class dropped | compatible |
sh:minCount raised, or sh:maxCount lowered or added | breaking |
sh:minCount lowered, or sh:maxCount raised or dropped | compatible |
| An operation added | compatible |
| An operation removed | breaking |
| An operation's accepted or returned shape changed | breaking |
Node shapes and operations are matched by IRI (ADR-0002); property shapes by their node shape and sh:path, since they are often blank nodes. Every change is in the data scope when it is a shape change, and also in the contract scope when an operation in either model accepts or returns that shape; operation changes are in contract only.
Output
Each change prints as <verdict> <kind> <subject> <detail>, sorted by subject, with CURIEs wherever the new or old model binds a prefix. The detail is - for an added or removed shape or operation, the path and the new sh:minCount for an added property, and the path with before -> after for a changed one. Diffing expenses chapter 9 back to chapter 8:
$ seams diff examples/expenses/chapter-09 examples/expenses/chapter-08
breaking shape-removed :NotificationShape -
breaking shape-removed :NotifyProblemShape -
breaking operation-removed :notify_failed -
breaking operation-removed :notify_manager_op -
seams diff: 4 breaking, 0 compatibleDownstream contexts
When the models declare bounded contexts and the relationships between them (ADR-0030, ADR-0032), each change also names the downstream contexts it reaches. A change to a subject (a shape or an operation) belongs to the model it is seam:inModel, or, with no such fact, to the seam:Model declared in the graph that types it; that model's owner is the arch:BoundedContext that seam:owns it. The change reaches every context that is arch:downstream of an arch:ContextRelationship whose arch:upstream is the owner, in either model version. A change to a downstream context's own shape reaches no context, and a model with no relationship reports none.
After the changes, the text output adds one section per downstream context that a breaking change reaches, sorted: a downstream <context> line, then those breaking changes indented two spaces. Changing :amount on expenses chapter 11's :ExpenseShape from xsd:decimal to xsd:integer, which the finance context reads as a conformist downstream:
$ seams diff examples/expenses/chapter-11 changed/
breaking datatype-changed :ExpenseShape :amount xsd:decimal -> xsd:integer
downstream :finance_context
breaking datatype-changed :ExpenseShape :amount xsd:decimal -> xsd:integer
seams diff: 1 breaking, 0 compatibleJSON
--json prints {"changes": [...], "counts": {"breaking": N, "compatible": M}}; each change has verdict, kind, subject and detail with full IRIs, a sorted scopes list and a sorted contexts list (the downstream context IRIs, empty when there are none). The output is deterministic, so it can be committed or compared.
Exit codes
| Code | Meaning |
|---|---|
| 0 | No change is breaking (there may be compatible ones, or none at all). |
| 1 | At least one change is breaking; seams diff: N breaking, M compatible goes to stderr. |
| 2 | A model is missing or cannot be loaded. |
Errors print to stderr as seams diff: <message>.
seams plan
seams plan examples/expenses/chapter-08 examples/expenses/chapter-09
seams plan v1.2:examples/expenses/expenses.trig examples/expenses/expenses.trig --output CHANGES.md| Option | Default | Meaning |
|---|---|---|
OLD | required | The old model: a .trig file, a directory, or a git REF:PATH. |
NEW | required | The new model, in the same forms. |
--output FILE | stdout | Write the plan to FILE and print its path. |
seams plan writes a change plan, or release notes, for a model change (backlog A7, D053). It reads no seams.toml. It runs the seams diff comparison and then compares the elements of each layer by IRI (ADR-0002): architecture (systems, containers, deployment nodes, environments), operations, channels, shapes, lifecycles (with their states and transitions), policies, interfaces (interaction models, pages, components, events, flows), processes (with their BPMN elements) and decisions.
An argument whose text before the first : is not an existing path is a git REF:PATH: seams plan runs git archive REF -- PATH at the repository root and loads the extracted copy, so seams plan main:model model compares the model on main with the working tree. PATH is relative to the working directory, like a plain path.
Output
The plan is Markdown: # Change plan, then ## Breaking changes and ## Compatible changes with one bullet per diff change (<kind> <subject>, plus the detail when there is one), or None; then ## Elements by layer with a ### <Layer> section only for a layer that changed, each listing Added: and Removed: CURIEs. Chapter 8 to chapter 9 of the expenses tutorial:
# Change plan
## Breaking changes
None
## Compatible changes
- shape-added :NotificationShape
- shape-added :NotifyProblemShape
- operation-added :notify_failed
- operation-added :notify_manager_op
## Elements by layer
### Operations
Added:
- :notify_manager_op
### Shapes
Added:
- :NotificationShape
- :NotifyProblemShape
### Processes
Added:
- :approval_bpmn#notify_failed_boundary
- :approval_bpmn#notify_managerThe output is deterministic, so it can be committed as release notes.
Exit codes
| Code | Meaning |
|---|---|
| 0 | No change is breaking. |
| 1 | At least one change is breaking; seams plan: N breaking, M compatible goes to stderr. The plan is still written. |
| 2 | A model is missing or cannot be loaded, or git cannot extract a REF:PATH (an unknown ref, a path absent at the ref, or no repository). |
Errors print to stderr as seams plan: <message>; a git error names the ref.
In CI
The composite action .github/actions/seams-plan takes a base git ref and a projects list like the seams-check action's. For each project whose files differ from the base (git diff --quiet BASE -- PROJECT), it runs seams plan BASE:PROJECT PROJECT and appends the plan to the job summary ($GITHUB_STEP_SUMMARY) under a heading naming the project; a project absent at the base is reported as new. A breaking change adds a warning annotation but does not fail the step: the plan is for the reviewer, and seams check is the gate. An unknown base ref, a failing git diff or an exit 2 from seams plan fails the step after every project has run.
This repository's change-plan job runs it on pull requests, with the same projects as the acceptance job. The checkout needs the base branch's history:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: uv sync --locked --all-packages
- run: echo "$PWD/.venv/bin" >> "$GITHUB_PATH"
- uses: ./.github/actions/seams-plan
with:
base: origin/${{ github.base_ref }}
projects: |
examples/chat-app
examples/expenses/chapter-01seams coverage
seams coverage # the model of the seams.toml in the working directory
seams coverage --project examples/chat-app
seams coverage examples/expenses/chapter-09 --json| Option | Default | Meaning |
|---|---|---|
MODEL | the project's model | A .trig file, or a directory whose TriG files load as one model. With it, no seams.toml is read. |
--project | . | seams.toml or a directory holding one, used when MODEL is not given. |
--json | off | Print the three lists and their counts as JSON instead of text. |
seams coverage lists what the model declares that nothing exercises (backlog U5, D054). It answers three questions with seams.coverage:
| Kind | Printed as | Meaning |
|---|---|---|
path | path <process> <node>... | A start-to-end BPMN path no verify:Scenario runs. Paths are walked over the BPMN sequence flows (ADR-0007), so this is a Python join, not SPARQL. A path through an error boundary is run only by a scenario whose verify:failure fails the operation the boundary catches. A path of a channel-started process is run by a scenario whose run starts it and that expects a state or end event on it. |
transition | transition <lifecycle> <transition> | A state:Transition nothing seam:fires (coverage/unfired-transitions.rq). |
policy | policy <policy> <target> <assignee> | An odrl:permission the tests generator writes no party deny test for, because no party in the model is outside its assignee (coverage/policies-without-deny.rq). - stands for an absent target or assignee. |
Every element is a full IRI, so a line can be pasted into the viewer or a query. Lines come paths first, then transitions, then policies, each in seams.coverage's fixed order. A closing count goes to stderr. The chat app has a gap of each kind:
$ seams coverage --project examples/chat-app
path https://example.org/chat/send_message_process https://example.org/chat/chat_bpmn#start https://example.org/chat/chat_bpmn#persist_user_msg ... https://example.org/chat/chat_bpmn#end
transition https://example.org/chat/message_lifecycle https://example.org/chat/t_fail_send
transition https://example.org/chat/message_lifecycle https://example.org/chat/t_fail_stream
policy https://example.org/chat/user_send_policy https://example.org/chat/send_message_op https://example.org/chat/authenticated_user
seams coverage: 1 unexercised path(s), 2 unfired transition(s), 1 permission(s) without a deny test--json prints {"paths": [...], "transitions": [...], "policies": [...], "counts": {...}}: a path has process, nodes and end, a transition lifecycle and transition, a policy policy, target and assignee (null when absent).
As gates in seams check
Each report can fail seams check: coverage = ["paths"] in seams.toml turns the paths into coverage findings, and the two queries under docs/queries/coverage/ are ordinary gates:
gates = [
"gates/ungoverned-writes.rq",
"coverage/unfired-transitions.rq",
"coverage/policies-without-deny.rq",
]
coverage = ["paths"]Expenses chapter 9 turns on coverage = ["paths"]. Its nine scenarios run every path: both outcomes of each route, the two escalations through the notify-failed boundary event (each with a verify:failure), and both payout paths, which the runs that pay an expense start through :payouts. Every transition is fired and every permission has a deny test too, so seams coverage examples/expenses/chapter-09 prints nothing and exits 0.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Nothing is uncovered. |
| 1 | At least one gap; the count of each kind goes to stderr. A file could not be read. |
| 2 | The project file is missing or malformed, no model was given, or the model cannot be loaded. |
Errors print to stderr as seams coverage: <message>.
seams context
seams context send_now --project examples/expenses/chapter-09
seams context send_now --project examples/expenses/chapter-09 --jsonIt prints the context manifest of one service task (backlog C3): the model facts and native file fragments that code written for the task depends on, each hashed, with a context hash over them. It reads the model only, writes nothing, and calls no LLM, so the manifest serves as a review report: keep it beside hand-written code and a changed item hash shows which fact moved. TASK is a BPMN id, CURIE or full IRI; the model is chosen as for seams generate. The rules that pick and narrow the items are in the seams library reference.
Output
The text output is task <iri>, then one <kind> <ref> <hash> line per item in (kind, ref) order, then context <hash>. A kind is bpmn, container, decision, dmn, process, shape, state, task or transition; a ref is an IRI or a model-relative path#id; a hash is sha256:<hex>. --json prints an object with task, items (each with kind, ref and hash) and context_hash, with sorted keys. Neither form holds an absolute path or a binding value, and the same model gives the same bytes.
Exit codes
| Code | Meaning |
|---|---|
| 0 | The manifest was printed. |
| 1 | The task is unknown, is not a proc:ServiceTask, or a fact it needs is missing. |
| 2 | The project file is missing or malformed, no model was given, or the model cannot be loaded. |
Errors print to stderr as seams context: <message>.
seams propose
seams propose send_now --project examples/expenses/chapter-09 --provider "my-llm --model some-model"
seams propose send_now --project examples/expenses/chapter-09 --force
seams propose send_now --project examples/expenses/chapter-09 --llm-model some-modelIt asks an LLM for the body of one service task's handler and keeps the draft only if the task's acceptance tests pass (backlog C4, ADR-0020). The provider is a command: [propose] provider in seams.toml, or --provider, split like a shell line but run without a shell. The prompt goes to its stdin; the draft is read from its stdout, with one fenced code block stripped. The prompt is built from the task's context manifest, its stub and its acceptance tests with the versioned template draft-v1; model text is quoted as data and bindings are named, never valued.
[propose]
provider = "my-llm --model some-model"The provider command is the pinned provider. To say which model answers, name it with [propose] model or --llm-model (the flag wins). The provider gets the name in the environment variable SEAMS_LLM_MODEL; without a name the variable is removed from its environment. The name is a label: seams checks nothing about it, and the provider decides what to do with it.
[propose]
provider = "my-llm"
model = "some-model"A successful response is cached in .seams/drafts/ (gitignored), keyed by the context hash, the template, the provider and the model name when one is given, so the same context runs again without calling the provider and another model misses the cache. Keys made without a model name are unchanged. The draft is tested in a scratch copy of the project; only on pass is it written to the handlers module, never outside the handlers package, and an existing module only with --force. An accepted draft is recorded in the drafts list of seams.lock (task, path, context hash, template, provider, sha256 and the model name when given), replacing any earlier record of the same task; a rejected one leaves the lock alone. seams propose is never run in CI.
Output
task <iri>, context <hash>, template <name>, provider <command>, llm-model <name> (only when a model is named), cache hit or cache miss, draft sha256:<hex>; then tests passed and wrote <handlers module>, or one failed <test> line per failing test with the pytest output on stderr.
Exit codes
| Code | Meaning |
|---|---|
| 0 | The tests passed and the handlers module was written. |
| 1 | The tests failed (nothing written), or the provider failed: non-zero exit, no output or a timeout. |
| 2 | No provider, an empty --llm-model, an unknown task or one with no handler, an existing module without --force, or a project error (such as a [propose] model that is not a non-empty string). |
Errors print to stderr as seams propose: <message>.
seams suggest
seams suggest --project examples/expenses/chapter-10 --provider "my-llm" --llm-model some-model
seams suggest --project examples/expenses/chapter-10 --list
seams suggest --project examples/expenses/chapter-10 --accept 3f2a9cIt asks an LLM for an improvement to the model itself, checks the change, and keeps it pending until a person accepts it (backlog R2, ADR-0028). The provider is the same command as seams propose's: [propose] provider in seams.toml, or --provider, and the LLM model is named the same way, with [propose] model or --llm-model (the flag wins; ADR-0020's amendment). The first kind of suggestion is condition-to-dmn: a gateway whose outgoing conditions read a name no upstream decision outputs (a form field, as in amount <= 2000) gets a DMN decision, a business-rule task before the gateway, and conditions over the decision's output variable.
The prompt is the versioned template suggest-v1, filled from the model: each such gateway with its flows and the fields upstream of it, and the model's existing decisions as their DMN elements, all as quoted data. The answer must be one JSON object:
{
"kind": "condition-to-dmn",
"gateway": "decision_gateway",
"decision_id": "approval_outcome",
"decision_name": "Approval Outcome",
"inputs": ["amount"],
"output": "outcome",
"rules": [{"when": {"amount": "<= 2000"}, "then": "\"approved\""}, {"then": "\"rejected\""}],
"conditions": {"flow_approved": "outcome == \"approved\"", "flow_rejected": "outcome == \"rejected\""}
}Deterministic code builds the change from it: dec:Decision facts shaped like the model's existing decision, the task's seam links, and the new BPMN and DMN payload text with the modeler's layout kept. The change is written into a scratch copy of the project through seams_edit, which must then pass the seam shapes, the decision checks and every generator. A passing suggestion is stored in .seams/suggestions/<id>/ (gitignored): suggestion.json and one unified diff per changed file, <file>.diff, to read before accepting. The id is the sha256 of the model hash, the template, the provider and the answer. The answer of a passing suggestion is cached in .seams/suggest-cache/, keyed by the model hash, the template, the provider and the LLM model when one is named, so asking again on the same model calls no provider; a rejected answer is neither stored nor cached. Nothing in the model is written until --accept.
--accept ID takes an id or a unique prefix of one. It refuses a suggestion made for another model hash (the model changed since; ask again), rebuilds the change from the stored answer, checks it again, writes it through seams_edit (the TriG files and the payloads, all or none) and removes the pending directory. Review the result and commit it like any other model edit. --list shows the pending suggestions. A model with no such gateway calls no provider. seams suggest is never run in CI.
Output
template <name>, provider <command>, llm-model <name> (only when an LLM model is named), model <hash> (the TriG model's hash), cache hit or cache miss, suggestion <id>, gateway <iri>, one changed <file> line per file, validated pass|fail, generated pass|fail (<n> outputs), then stored .seams/suggestions/<id>. A rejected suggestion prints one rejected <kind> <subject> <predicate> <detail> line per finding instead, where the kind is suggestion (the answer names something the model does not have), patch, shacl, decision or generate.
--list prints <id> <kind> <gateway> current|stale per pending suggestion. --accept prints the verdicts of the re-check, accepted <id> and one wrote <file> line per written file.
Exit codes
| Code | Meaning |
|---|---|
| 0 | A suggestion was stored, listed or accepted, or the model has nothing to suggest. |
| 1 | The answer was rejected (nothing stored), the provider failed, or --accept was refused: a stale suggestion or a failed re-check (nothing written). |
| 2 | No provider, an empty --llm-model, an unknown suggestion id, or a project error. |
Errors print to stderr as seams suggest: <message>.
seams new-generator
seams new-generator checklist --reference checklist.yaml # writes ./seams-checklistIt turns a reference output, a file written by hand exactly as the generator should emit it, into a generator package whose golden test passes as written (backlog K5). NAME is lowercase letters, digits and underscores; the reference is a .py, .yaml, .yml or .html file; --out (default ./seams-<name>) must not exist or be empty. The package holds pyproject.toml with a seams.generators entry point <name> = "seams_<name>:generator", seams_<name>/__init__.py (the Generator), the reference verbatim as seams_<name>/templates/<reference>, tests/golden/<reference> with every non-blank line marked [fixed], and tests/test_<name>.py, which runs the test kit on the model SEAMS_MODEL names, else the chat-app example. Each written path is printed. The same arguments give the same bytes.
A reference that is not UTF-8, is empty, does not parse as its file type, or has a line that already carries a marker, holds a template delimiter ([[, [%, [#) or ends in whitespace is refused. See packages/seams-cli/README.md.
Exit codes
| Code | Meaning |
|---|---|
| 0 | The package was written. |
| 1 | A file could not be written. |
| 2 | A refused name, reference or --out; nothing is written. |
Errors print to stderr as seams new-generator: <message>.
seams draft-generator
seams draft-generator processes --reference processes.yaml --provider "my-llm" --llm-model some-modelIt does the reference-first loop with an LLM: from a reference output, the command drafts the template and the extract step of the package seams new-generator would write, and writes the package only when its golden test passes (backlog R3, ADR-0029). NAME, the reference and --out follow seams new-generator; --force lets the package's files replace files of the same path in a non-empty --out. The model is the project's (--project, --model). The provider is the same command as seams propose's: [propose] provider in seams.toml, or --provider; the LLM model is [propose] model or --llm-model, as for seams propose.
The prompt is the versioned template draft-generator-v1, filled with the reference verbatim, the model's names (one IRI<TAB>label line per labelled subject, sorted) and the scaffold's package module, all as quoted data. The same reference and model give the same prompt bytes. The answer must be one JSON object with exactly three string keys: extract (the Python source of def extract(trig: Path, out: Path) -> dict and the imports it needs), template and golden (every non-blank line marked [fixed] or [graph: ...]).
The draft is accepted only when the template holds no logic (only [[ name ]], [[ name.attr ]], [% for x in xs %] and [% endfor %]), restates no model label that occurs in the reference, and the scaffold's four tests pass in a scratch copy with the answer in place, run on the project's model. An accepted answer is cached in .seams/generator-drafts/ (gitignored), keyed by the reference's hash, the model hash, the template, the provider and the LLM model when one is named, so the same request again calls no provider and another model does. Nothing is written or cached on failure. The written package runs without an LLM and is the author's from then on. seams draft-generator is never run in CI.
Output
template <name>, provider <command>, llm-model <name> (only when one is named), prompt sha256:<hex>, cache hit or cache miss, then each written path, as seams new-generator prints them.
Exit codes
| Code | Meaning |
|---|---|
| 0 | The package was written. |
| 1 | Nothing was written: a refused name, reference or --out, a project or model that cannot be read, no provider, an empty --llm-model, a failed provider, a malformed answer, a template with logic or a restated label, or failed scratch tests (each named). |
Errors print to stderr as seams draft-generator: <message>; failed tests are named one per line after tests failed:.