Skip to content

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:

sh
uv sync --all-packages
source .venv/bin/activate
seams generate --project examples/chat-app

The project file ​

toml
model = "chat-app.trig"

[[generators]]
name = "fastapi"
out = "generated"

[[generators]]
name = "htmx"
out = "generated/templates"

[environments.production]
KeyTypeMeaning
modelstring, optionalThe 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 tablesOne entry per generator to run, in any order.
[[generators]].namestringA generator registered in the seams.generators entry-point group: a built-in or one from an installed distribution (see Generators).
[[generators]].outstringThe directory the generator writes into.
gatesarray of strings, optionalSPARQL SELECT files that seams check runs over the model; each must return no rows.
coveragearray of strings, optionalCoverage 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, optionalThe 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] providerstring, optionalThe provider command seams propose, seams suggest and seams draft-generator send their prompts to; --provider overrides it.
[propose] modelstring, optionalThe 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>].endpointsinline table of strings, optionalContainer 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:

  1. --model PATH on the command line;
  2. the project file's model;
  3. the SEAMS_MODEL environment 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 ​

sh
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
OptionDefaultMeaning
--project PATH.A seams.toml file, or a directory holding one.
--model PATHnoneThe model (.trig); overrides the project file and SEAMS_MODEL.
PATHnone--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.
--lenientoffCollect 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.lock

The 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:

KeyMeaning
lockfile_versionThe format version, 1.
modelThe model path, relative to the project directory.
model_hashseams.model_hash of the model, sha256:<hex>.
outputsOne entry per generated file, sorted by path.
faultsOnly after seams generate --lenient found faults: one {focus, generator, message, shape} object per fault, sorted. A lock with this key is flagged.
draftsOnly 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:

json
{
  "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 as removed <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.html

Without 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 ​

CodeMeaning
0Every output was written.
1A file could not be read or written.
2The 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:

text
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:

text
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 flagged

The 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 ​

sh
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.trig

The 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):

  1. Shapes. The model, unioned with ontology/*.ttl, is validated with pyshacl (inference off) against ontology/shapes/seam-shapes.ttl, the same check as the viewer's health panel. Each validation result is a shacl finding: focus node, path and message.
  2. Gates. Each file the project lists under gates is a SPARQL SELECT query that runs over the same union graph. Every row it returns is a gate finding: the gate path, relative to the project, then the row's values. This repository's gates are in docs/queries/gates/. ungoverned-writes.rq lists 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 into docs/; a project outside this repository keeps its gates next to its seams.toml, for example gates = ["gates/ungoverned-writes.rq"].
  3. 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 a flagged finding; a generator fault the lock does not record makes the lock lock-out-of-date.
  4. Decisions. Faults the runtime would hit in a decision, found without running it (backlog K2). A DMN input whose typeRef disagrees with the sh:datatype of its dec:fromPath (number takes the numeric XSD types, string, boolean and date their own), and a gateway conditionExpression that reads a variable no upstream business-rule task or user task puts in the task data, are decision findings: 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.
  5. 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:schedules container. The first refusal of each process is a timer finding: the event (or process) IRI, the fact (bpmn:timeCycle and the like) and the message, so a model the runtime would refuse fails here first.
  6. Review. Facts the model states twice or not at all (backlog R1, seams.review_findings): a sh:in list that copies the rdfs:labels of a lifecycle's states, a sh:class whose object is a sh:NodeShape, a sh:NodeShape with no sh:targetClass that is not itself a class, and a sh:path IRI nothing types as a property. Each is a review finding: the shape or path IRI, the fact (the sh:path, sh:targetClass or sh:path itself) and a message. Review findings are warnings: they print, but they never fail the check.
  7. Ontology. A class of a seam:Ontology model whose rdfs:subClassOf target is declared neither in the ontology nor in a resolved import (a mistyped gist:Transation) is an ontology-superclass finding: the class IRI, rdfs:subClassOf, the target IRI and a message. It is a warning, since rdflib does not reason (ADR-0022 section 5). An owl:imports that does not resolve to a file in ontology/vendor/ (an unknown IRI, or another gist version than the vendored one) is a ResolutionError(<ontology>, owl:imports, …) on load, so the model cannot be loaded and the check exits 2.
  8. 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. Each sh:Violation result is a profile finding (focus node, path and message), each sh:Warning or sh:Info result a profile-warning. Results of pill shapes (a labelled sh:Info shape) are not reported. A profile that cannot be resolved is one profile finding: the ResolutionError's subject, predicate (seam:conformsTo or seam:profileShapes) and detail. A model with no seam:conformsTo reads no profile file and prints nothing for this check. seams generate ignores profiles.
  9. Coverage, only with coverage = ["paths"] in the project file. Each start-to-end BPMN path no verify:Scenario runs is a coverage finding: the process IRI, then the path's flow node IRIs (see seams coverage). The other two coverage reports are gate queries: add docs/queries/coverage/unfired-transitions.rq and docs/queries/coverage/policies-without-deny.rq to gates.
KindMeaning
shaclThe model breaks a shape.
gateA gate query returned a row.
missingAn output generate would write is absent on disk.
staleAn output on disk differs from what generate would write now, for example after a model change.
hand-editedA stale output whose sha256 is not the locked one either: someone edited it.
orphanseams.lock lists an output no generator writes any more, and it is still on disk.
lock-out-of-dateThere is no seams.lock, or the lock generate would write differs from it.
flaggedseams.lock records faults from seams generate --lenient; the detail is the lock path and N fault(s).
decisionA DMN typeRef disagrees with its input's sh:datatype, or a gateway condition reads a variable nothing upstream sets.
timerThe runtime would refuse a timer: its expression, its placement or its scheduling container.
reviewA 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-superclassA warning, not a problem: an ontology class's superclass is declared neither in the ontology nor in a resolved import, a likely typo.
profileThe model breaks a sh:Violation shape of its attached profile, or the profile cannot be resolved.
profile-warningA warning, not a problem: a sh:Warning or sh:Info result of the attached profile's shapes.
coverageWith coverage = ["paths"]: a BPMN path no scenario runs.
draft-staleA 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-editedA 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 ​

CodeMeaning
0No findings, or only review, ontology-superclass, shacl-warning, profile-warning and vocabulary-migrated warnings.
1At least one finding of another kind, or a file could not be read.
2The 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:

yaml
- 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-01

Another 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:

yaml
# .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 ​

sh
seams migrate --project examples/expenses/chapter-01
seams migrate --model path/to/model.trig

A 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:

text
migrated seam 1.0 2.0
examples/expenses/chapter-01/expenses.trig

Each 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.

NameWrites into out
fastapi<container>.py, the route module of the serving container
htmxthe Jinja2/htmx page templates and their partials
composeone 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
teststest_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)
smoketest_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
acceptancetest_<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
ifmleditifmledit.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)
mentalmodela 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:

toml
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 ​

sh
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
OptionDefaultMeaning
--project PATH.A seams.toml file, or a directory holding one.
--model PATHnoneThe model (.trig); overrides the project file and SEAMS_MODEL.
PATHnone--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 NAMEthe only environment with endpointsThe [environments.<name>] whose endpoints to read.
--timeout SECONDS5How 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:

toml
[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):

VerdictMeaning
currentThe same hash.
behindA 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.
aheadA 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.
unreachableNo 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:11d35819c97e7e7599603d0e806ac6f4bdf0a195feb6c2256dbf037bcedc06bd

With 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 current

After 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 ​

CodeMeaning
0Every runtime container is current.
1At least one is behind, ahead or unreachable.
2The 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 ​

sh
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
OptionDefaultMeaning
--project PATH.A seams.toml file, or a directory holding one.
--model PATHnoneThe model (.trig); overrides the project file and SEAMS_MODEL.
PATHnone--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 NAMEevery environmentThe arch:Environment to check, by local name.
--write-exampleoffAlso write <project root>/.env.example.
--jsonoffPrint 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:

KindBindingSource
valuedeploy:valuenone (-); the literal is in the model
secret-refdeploy:secretRefthe deploy:secretName, else the secret's IRI
secret-filedeploy:secretFilethe path, resolved against the model's directory and written relative to the project root
servicedeploy:serviceOfthe container it names

A binding with no kind or more than one is a model fault (exit 2).

Verdicts ​

VerdictMeaning
presentThe key or secret exists. Value and service rows are present by definition.
missingA checker looked and found nothing.
uncheckedNo checker could tell.

Every secret row asks the checkers in this order, and the first answer that is not unchecked wins:

  1. Files. A secret-file row is present when the path exists and missing when it does not. Only Path.exists is called; the file is never opened.
  2. .env. A key named in <project root>/.env is present, for value and secret-ref rows. Only the text before each line's first = is kept (an export prefix and comments are skipped). Because the value is never read, a key with a blank value counts as present too: a .env copied from .env.example names every key. A key absent from .env proves nothing, so the row goes on to the next checker.
  3. Docker secrets. A secret-ref row is present when its secret name is in the names docker secret ls prints and missing when it is not. The command runs once, with a 10 second timeout. With no docker on 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 as seams 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=chat

The 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 ​

CodeMeaning
0No row is missing. Unchecked rows do not fail.
1At least one row is missing. A file could not be read or written.
2The 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 ​

sh
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
OptionDefaultMeaning
EVENTrequiredThe 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 PATHnoneThe model (.trig); overrides the project file and SEAMS_MODEL.
--out DIR<project root>/sequencesThe directory the .mmd files are written to.
--trace RUNnoneDraw one recorded run as <event>.executed.mmd: a trace JSON file, or an instance id in the --db store.
--db PATHSEAMS_DB, else <project root>/seams.sqliteThe 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.mmd

A 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.mmd

Gaps are an audit of the model, not a failure, so the exit code stays 0.

Exit codes ​

CodeMeaning
0The diagrams were written, with or without gaps.
1A file could not be read or written.
2The 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 ​

sh
seams storm approval_process --project examples/expenses/chapter-10
seams storm :submit_expense --project examples/expenses/chapter-10 --out build/storm.md

seams 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 it

Gaps are an audit of the model, not a failure, so the exit code stays 0.

Exit codes ​

CodeMeaning
0The timeline was printed or written, with or without gaps.
1A file could not be read or written.
2The 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 ​

sh
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-11

seams 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:relationshipTypeCML
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:

text
ContextMap {
  contains expense_context, finance_context
  expense_context [U]->[D,CF] finance_context : expense_to_finance
}

BoundedContext expense_context

BoundedContext finance_context

With --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:

text
added expense_to_finance
examples/expenses/chapter-11/expenses.trig

Import 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 ​

CodeMeaning
0The CML was printed or written, or the import finished, with or without flags.
1FILE cannot be parsed (the message names the line), or a file could not be read or written. Nothing is written.
2The 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 ​

sh
seams diff examples/expenses/chapter-08/expenses.trig examples/expenses/chapter-09/expenses.trig
seams diff old/ new/ --json
OptionDefaultMeaning
OLDrequiredThe old model: a .trig file, or a directory whose TriG files load as one model.
NEWrequiredThe new model, in the same form.
--jsonoffPrint 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:

ChangeVerdict
A node shape addedcompatible
A node shape removedbreaking
A node shape renamed (new owl:sameAs old, stated in NEW)compatible
A property added with no sh:minCount, or sh:minCount 0compatible
A property added with sh:minCount above 0breaking
A property removedbreaking
An sh:datatype or sh:class changed or addedbreaking
An sh:datatype or sh:class droppedcompatible
sh:minCount raised, or sh:maxCount lowered or addedbreaking
sh:minCount lowered, or sh:maxCount raised or droppedcompatible
An operation addedcompatible
An operation removedbreaking
An operation's accepted or returned shape changedbreaking

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 compatible

Downstream 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 compatible

JSON ​

--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 ​

CodeMeaning
0No change is breaking (there may be compatible ones, or none at all).
1At least one change is breaking; seams diff: N breaking, M compatible goes to stderr.
2A model is missing or cannot be loaded.

Errors print to stderr as seams diff: <message>.

seams plan ​

sh
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
OptionDefaultMeaning
OLDrequiredThe old model: a .trig file, a directory, or a git REF:PATH.
NEWrequiredThe new model, in the same forms.
--output FILEstdoutWrite 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:

markdown
# 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_manager

The output is deterministic, so it can be committed as release notes.

Exit codes ​

CodeMeaning
0No change is breaking.
1At least one change is breaking; seams plan: N breaking, M compatible goes to stderr. The plan is still written.
2A 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:

yaml
- 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-01

seams coverage ​

sh
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
OptionDefaultMeaning
MODELthe project's modelA .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.
--jsonoffPrint 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:

KindPrinted asMeaning
pathpath <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.
transitiontransition <lifecycle> <transition>A state:Transition nothing seam:fires (coverage/unfired-transitions.rq).
policypolicy <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:

toml
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 ​

CodeMeaning
0Nothing is uncovered.
1At least one gap; the count of each kind goes to stderr. A file could not be read.
2The 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 ​

sh
seams context send_now --project examples/expenses/chapter-09
seams context send_now --project examples/expenses/chapter-09 --json

It 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 ​

CodeMeaning
0The manifest was printed.
1The task is unknown, is not a proc:ServiceTask, or a fact it needs is missing.
2The 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 ​

sh
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-model

It 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.

toml
[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.

toml
[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 ​

CodeMeaning
0The tests passed and the handlers module was written.
1The tests failed (nothing written), or the provider failed: non-zero exit, no output or a timeout.
2No 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 ​

sh
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 3f2a9c

It 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:

json
{
  "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 ​

CodeMeaning
0A suggestion was stored, listed or accepted, or the model has nothing to suggest.
1The answer was rejected (nothing stored), the provider failed, or --accept was refused: a stale suggestion or a failed re-check (nothing written).
2No provider, an empty --llm-model, an unknown suggestion id, or a project error.

Errors print to stderr as seams suggest: <message>.

seams new-generator ​

sh
seams new-generator checklist --reference checklist.yaml   # writes ./seams-checklist

It 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 ​

CodeMeaning
0The package was written.
1A file could not be written.
2A refused name, reference or --out; nothing is written.

Errors print to stderr as seams new-generator: <message>.

seams draft-generator ​

sh
seams draft-generator processes --reference processes.yaml --provider "my-llm" --llm-model some-model

It 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 ​

CodeMeaning
0The package was written.
1Nothing 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:.

Released under the Apache-2.0 License.