Skip to content

The seams library ​

seams is the Python library that reads Semantic Seams models without executing them. Its import name is seams; its distribution name is semanticseams (packages/seams/pyproject.toml). Its only dependencies are rdflib and lxml, so importing it never pulls in SpiffWorkflow, FastAPI or seams_runtime, and the package is strictly type-checked (uv run pyright).

v1 is a uv workspace member only and is not published to PyPI (D031). Install it with the rest of the workspace:

sh
uv sync --all-packages

The generators in seams_gen, the viewer and seams_runtime all read models through it. To write a generator of your own, follow Write your own generator.

API ​

python
import seams

model = seams.load("examples/chat-app/chat-app.trig")
model.hash        # "sha256:..."
model.payloads()  # [("chat_process.bpmn", Path(...)), ("routing.dmn", Path(...))]
  • seams.load(path=None, *, ontology_dir=None, check_versions=True, check_imports=True) -> Model loads a model.
    • A file is parsed as TriG; payloads resolve against its directory.
    • A directory loads every *.trig directly inside it (not recursive, sorted by file name) into one Dataset; payloads resolve against the directory. A directory with no *.trig raises FileNotFoundError.
    • Without a path, the SEAMS_MODEL environment variable names the model. An explicit path always wins; an unset or empty variable raises ValueError.
    • The model passes the vocabulary version check first (see Vocabulary versions), then the ontology import check (see Ontology imports).
  • Model exposes path, base_dir, files (the TriG files loaded), ds (the merged rdflib Dataset, default_union=True), subject, curie(term), label(iri), payload_path(model_iri) and:
    • compact(term): the inverse of curie. It returns prefix:local for the longest bound namespace the IRI starts with (ties broken by prefix), where local is the last fragment or segment, so a BPMN element <…approval_bpmn#approve_expense> shows as :approve_expense. An IRI no namespace matches comes back as <iri>. compact(term, whole=True) keeps everything after the namespace (:approval_bpmn#approve_expense), which curie expands back to the same IRI.
    • label(iri, strict=True): the single rdfs:label, where the default falls back to the local name. None or several raise ResolutionError(iri, rdfs:label) ("expected exactly one, found none"), the rule the generators use.
    • hash: computed once per instance.
    • migrations: the vocabulary migration steps applied on load, sorted.
    • ontology_dir: the ontology directory the checks used, or None when there is none.
    • imports: the vendored ontologies the model's ontology imports, sorted by version IRI; dataset_imports: the ontology models of the same Dataset they import (DatasetOntology), sorted by version IRI; import_statuses: the ImportStatus of each import, read even with check_imports=False.
    • payloads(): (relative posix path, absolute path) for every seam:renderPayload, sorted by relative path. A missing payload raises ResolutionError.
  • seams.model_hash(path), seams.payload_path(payload_iri, base_dir) and seams.model_dir(path) (the directory payloads and handlers/ resolve against: a directory model itself, else the TriG file's parent) are module-level helpers.
  • Errors: SeamsError (base; http_status, to_dict()) and ResolutionError(subject, predicate, detail=""), also a LookupError, raised when a graph hop is missing.
  • Namespaces: SEAM, COMM, DEC, PROC, STATE, HYDRA, ODRL, DEPLOY, IFML, ARCH, DASH (dash: editors), VERIFY (ADR-0014 scenarios and examples), plus local_name(iri).
  • ModelGraph(path, dataset=None, *, ontology_dir=None, check_versions=True, check_imports=True) (and ModelGraph.load(path)) is a Model, taking the same load options, plus the one-of-each resolvers the runtime and the generators share, such as landmark_page(), task_iri(bpmn_id), triggering_operation(process), is_channel_started(process), decision_binding(task), implementing_container(operation), scheduling_container(process) (the one container that seam:schedules the process, ADR-0037; none or several raise ResolutionError(process, seam:schedules, <sorted container IRIs>)), lifecycle_for(shape), policy(policy) and shape_graph(). published_channel(element) is the one comm:Channel the element seam:publishesTo (None when it declares none, more than one raises), and published_channels(process) lists the distinct channels any element of the process's BPMN model publishes to, sorted by IRI; the runtime streams deltas only on the first and closes all of the second on failure (D046). Each answers from graph facts and raises ResolutionError naming the subject and predicate when a hop is missing. landmark_page() and task_iri() are view-backed: landmark_page() is model.interfaces.landmark(), and task_iri() resolves through model.processes across any number of BPMN models (the graph element with that id, else the one BPMN file declaring it; an id in several models raises). The pages an app serves are model.interfaces.served_pages(). Results are frozen dataclasses (Transition, DecisionBinding, Container, OperationPath, ChannelBinding, ChannelStart, ProcessFacts, Lifecycle, PolicyFacts and the rest), and dmn_decision_ids, dmn_input_types and bpmn_start_event_ids read the payload XML. ModelGraph is a transitional facade over the views: every resolver reads through model.processes, operations, channels, lifecycles, policies, interfaces, architecture, shapes and decisions (only shape_graph() copies triples, for SHACL). task_element(task), shape(iri), page(iri) and component(iri) hand back the interned view element, and has_lifecycle(shape), governing_policies(element) and rendered_components() answer the runtime's yes/no and list questions; Lifecycle.transitions lists the machine's state:hasTransition IRIs. store_plan() resolves where the runtime keeps its rows (ADR-0011) and store_health() reports what is wrong with that; see Store resolution. operation_implementation(op) reads what serves an operation (ADR-0012); see Operation implementation. seams_runtime re-exports it unchanged. Three of its result types share a name with a view class (Container, Lifecycle, Transition): import those from seams.graph (or seams_runtime.model); the top-level names are the view classes.

Vocabulary versions ​

seams.load runs the ADR-0021 load check. Each vocabulary in ontology/<name>.ttl carries owl:versionIRI <https://w3id.org/seams/<name>/MAJOR.MINOR>; a model declares the versions it targets as owl:imports in <urn:seams:graph:manifest>, and a vocabulary it does not declare targets 1.0.

python
model = seams.load("examples/expenses/chapter-01", ontology_dir="tests/fixtures/vocab-v2")
model.migrations  # (MigrationStep(vocabulary='seam', source='1.2', target='2.0'),)
  • DEFAULT_ONTOLOGY_DIR: the repository's ontology/, which load checks against when given no ontology_dir (D060). When it does not exist (an installed copy of seams) the check is skipped; an explicit ontology_dir that does not exist raises FileNotFoundError. check_versions=False loads the model as written.
  • MANIFEST_GRAPH: <urn:seams:graph:manifest>, the graph holding the declarations.
  • shipped_versions(ontology_dir): {name: (major, minor)} from each vocabulary's owl:versionIRI. A vocabulary without one raises ResolutionError(<ontology>, owl:versionIRI).
  • declared_versions(ds, shipped): the versions a Dataset declares, by name. Imports of other ontologies are ignored.
  • check_versions(ds, ontology_dir): the check itself, migrating ds in place and returning the MigrationSteps applied. Per vocabulary: the same major with a minor at most the tool's loads as written; a newer version raises ResolutionError(<urn:seams:graph:manifest>, owl:imports, "targets seam 2.1; this tool supports seam up to 2.0"); an older major runs ontology/migrations/<name>/<k>-to-<k+1>.ru (SPARQL Update) per step, in sorted vocabulary order and ascending step order, then replaces the declaration with the tool's version IRI. Disagreeing declarations, an unpublished version, a missing step and a step calling NOW(), RAND(), UUID(), STRUUID() or BNODE() raise the same ResolutionError before the Dataset changes.
  • MigrationStep(vocabulary, source, target): one applied step, versions as "MAJOR.MINOR" strings.
  • version_iri(name, (major, minor)): https://w3id.org/seams/<name>/MAJOR.MINOR.

Migration happens in memory only: no file is written and model.hash is unchanged.

Ontology imports ​

A seam:Ontology model is also an owl:Ontology (ADR-0022); its owl:imports name the version IRIs of the ontologies it builds on. seams.load resolves them offline, first against the other ontology models of the same Dataset by owl:versionIRI (ADR-0030 section 4), then against the files in <ontology_dir>/vendor/ (VENDOR_DIR), never over the network, and never merges their triples into model.ds, so model.hash, SHACL and the generators see only the model.

python
model = seams.load("model.trig")
model.imports  # (VendoredOntology(ontology=…/gistCore, version=…/gistCore14.0.0, path=…),)
  • vendored_ontologies(ontology_dir): a VendoredOntology(ontology, version, path) per vendor/*.ttl, read from the file's own owl:Ontology and owl:versionIRI, sorted by version IRI. A file without exactly one of each raises ResolutionError.
  • vendored_graph(vendored): the file's triples as a read-only graph.
  • dataset_ontologies(ds): a DatasetOntology(ontology, version) per seam:Ontology model of ds with one owl:versionIRI, sorted by version IRI; dataset_imports(statuses) the ones a set of import statuses resolves to.
  • import_statuses(ds, ontology_dir): an ImportStatus(ontology, imported, vendored, detail, dataset) per import of each seam:Ontology model, sorted by (ontology, import); resolved and error (the ResolutionError, or None) derive from it.
  • check_imports(ds, ontology_dir): the check load runs, returning the vendored ontologies. The version IRI of another ontology model of ds, or of a vendored file, resolves; an ontology never resolves its own. The first fault raises ResolutionError(<ontology>, owl:imports, detail): a mismatch, also for the bare ontology IRI ("imports gistCore14.1.0; the vendored version is gistCore14.0.0", or for a dataset ontology "imports <…/finance_ontology/2.0>; the dataset's finance_ontology is version <…/finance_ontology/1.0>"), a SemanticSeams vocabulary version, which belongs in the manifest graph (ADR-0021), or anything else ("imports <iri>, which no file in ontology/vendor/ declares"). check_imports=False skips it.

model.ontologies lists the ontology models as Ontology (version_iri, imports, classes()). An OntologyClass has label, definition (skos:definition), properties (by rdfs:domain) and superclasses: a sorted tuple of Superclass(iri, label, definition, source), where source is the model that declares the superclass, the version IRI of the resolved import that declares it (labelled from its skos:prefLabel), or None when neither does.

Views ​

The views read every model of a layer at once. Several BPMN, IFML or statechart models in one graph are the normal case, not an error.

python
model = seams.load("examples/expenses/chapter-08")
api = model.architecture.containers().by_iri(":expense_api")
api.executes.iris()                    # (…approval_process, …payout_process)
op = model.operations.by_iri(":submit_expense_op")
op.exposed_by.only() is api            # True: one object per IRI and model
op.governed_by.only().permissions      # (Permission(target=…, action=odrl:use, …),)
model.lifecycles.only()                # ResolutionError: two state machines
model.interfaces.served_pages().iris() # (…expenses_page, …payouts_page)
payout = model.processes.for_bpmn_process("payout_process").only()
payout.channel_starts.only().channel   # Channel(iri=…payouts, …)
model.processes.element("choose_payout").only().decided_by.decision_id  # "payout_method"
  • Many[T] is an immutable tuple, always built in a documented order (by str(iri) unless the view says otherwise). It remembers the subject and predicate it was read from: the model and rdf:type for a layer's list, the element and the edge predicate for an edge.

    • .only() returns the single item. It raises ResolutionError(subject, predicate, "expected exactly one, found [...]") for zero items or more than one.
    • .by_iri(iri_or_curie) returns the item with that IRI, and raises ResolutionError when it is absent.
    • .where(pred) filters the items and keeps the same subject and predicate. .iris() returns the IRIs.
  • Element is the frozen dataclass every view item extends. Its fields are iri, label (rdfs:label, else the local name) and model (the seam:Model it is seam:inModel, or None). It also has these properties:

    • raw: the rdflib node.
    • edges: every outgoing seam: triple as (predicate, object), sorted.
    • graph: the Dataset.

    Elements compare and hash by type and IRI, and assigning to one raises FrozenInstanceError. A per-model registry (model.views) builds each view once and interns each element, so a cross-layer edge and the layer's own list return the same object. An edge to a node of a type no view covers yet returns a plain Element.

  • View collections. model.architecture is an Architecture and model.interfaces an Interfaces: objects with the list methods below. model.processes is a Processes and model.lifecycles a Lifecycles, both a Many with extra lookups. model.operations, model.channels, model.shapes, model.decisions, model.policies, model.scenarios and model.decision_examples are plain Manys.

  • model.architecture has domains(), contexts(), systems(), containers(), deployment_nodes(), environments() and serving_containers() (each container that seam:executes a process or seam:exposes an operation an ifml:Event seam:submitsTo: the ones that serve routes and hold process state).

    • Domain (ADR-0030) has classification (the arch:Core, arch:Supporting or arch:Generic IRI; none, several or a literal raise ResolutionError(domain, arch:classification)), classification_label (its rdfs:label: the model's own when it states one, else the shipped arch vocabulary's, such as "core"), and systems and contexts (the arch:Systems and arch:BoundedContexts it arch:contains).
    • BoundedContext has domain (the one arch:Domain that arch:contains it, else ResolutionError(context, arch:contains)), models (the seam:Models it seam:owns), ontology (the one owned model whose notation is seam:Ontology, an Ontology, else ResolutionError(context, seam:owns)) and containers, derived: each arch:Container that seam:presents an owned model or seam:executes a process in one. A model that declares no context has empty domains() and contexts().
    • System.containers (the arch:Containers it arch:contains).
    • Container has technology, exposes and consumes (a Many[Operation | Channel]), executes, schedules, deployed_to, depends_on (C4 arch:dependsOn) and systems. service_bindings is a read-only mapping from deploy:key to deploy:serviceOf, sorted by key. store_kinds lists its deploy:storeKind IRIs (sorted), stored_shapes the shapes seam:storedIn it, and connecting_bindings its bindings whose connects_to is set.
    • Its deploy: facts are frozen dataclasses: artifact (an Artifact with reference, version and build, a Build with context and dockerfile when it is built from source, or None), ports (Port: container_port, published_port), volumes (Volume: name, mount_path), healthcheck (Healthcheck: probe, interval, or None), replicas, deploy_depends_on (start order) and bindings (Binding: key, kinds, value, secret_ref, secret_name, secret_file, service_of, connects_to, by key), and proxies (the container it deploy:proxies, or None). connects_to is the store container a binding deploy:connectsTo; it sits beside the value source and is not one of kinds. They sit on blank nodes, so a missing or duplicated value raises ResolutionError(container, predicate). These are the shared facts.
    • Environments (ADR-0015): overrides lists its deploy:overrides as Overrides (environment, and artifact, bindings and replicas, each None when the override does not state it), by environment IRI. for_environment(environment) applies the override rule and returns a Deployment (environment, artifact, bindings, replicas): an override's artifact or replicas replace the shared ones whole, its bindings replace the shared binding with the same key or add one, and anything it does not state is shared. An override that is no deploy:Override, states another predicate, names no single arch:Environment, shares its environment with another, binds a key twice or changes a binding's deploy:connectsTo store raises ResolutionError(container, predicate); the shapes report the same faults.
    • DeploymentNode has hosts and environments; Environment has nodes.
  • model.operations and model.channels:

    • Operation has method, path, accepts, returns, media_kind, request_media_type, implemented_by, exposed_by, consumed_by, governed_by, renders (the components it seam:rendersComponent) and submitted_by (the events that seam:submitsTo it). path is a PathTemplate (template, mappings: PathMappings with variable, property and property_iri, sorted by variable, variables). accepts and returns are a Shape or None. errors is a Many[Status]: the error statuses it hydra:possibleStatus, each with a comm:errorCode (ADR-0013). error(code) is the one with that code, else ResolutionError(operation, comm:errorCode).
    • Status (a hydra:Status) has status_code (hydra:statusCode), error_code (comm:errorCode), returns (the error shape it seam:returnsShape, or None) and operations (those it is the hydra:possibleStatus of).
    • Channel has transport, event_name, path, accepts, exposed_by, consumed_by, published_by and subscribed_by.
  • model.shapes: a Shape has these members:

    • fields(): a Many[Field] sorted by sh:order (fields without one last), then by path IRI. A Field has path, name (its sh:name, else the path's local name: the wire name, ADR-0023), label (rdfs:label, else the name), datatype, min_count, max_count, order, required, max_length (sh:maxLength), editor (dash:editor, e.g. DASH.TextAreaEditor) and options: the sh:in literals in list order, or the rdfs:labels of its category members (an sh:in of IRIs in list order, or the members of the gist:Category subclass in category, in lifecycle order when they are states). node is a FlatNode for a flattened sh:node field: its nested shape, node_class, free_path and the fixed (path, sh:hasValue) pairs; the field's datatype, options and max_length then come from the free property. An empty category, a member without exactly one rdfs:label, two members with one label, members from two lifecycles, an sh:in mixing IRIs and literals and an sh:node that does not flatten raise ResolutionError(shape, predicate, detail).
    • target_class: the OntologyClass it names with sh:targetClass, or None; a class no seam:Ontology model declares raises ResolutionError.
    • lifecycle, read_by, written_by and stored_in (the store containers it is seam:storedIn).
    • aggregate: the Aggregate it is a seam:aggregateMember of, or None; a shape two aggregates list raises ResolutionError(shape, seam:aggregateMember, detail) naming both.
    • valid_examples and invalid_examples: the IRIs of the nodes it verify:validExample or verify:invalidExample (ADR-0014), by IRI.
    • example_values(node): (Field, Literal) pairs, one for each value an example node has on one of the shape's fields, in fields() order (a field's values by lexical form). A category member IRI reads as its label and a flattened field reads through the nested node to its free path. The smoke generator fills paths and request bodies from them.
  • model.lifecycles: one Lifecycle per state:StateMachine. model.lifecycles.transitions() lists the transitions of all of them.

    • Lifecycle has initial, states(), finals(), transitions(), shape and shapes (every shape naming it, by IRI).
    • Transition has from_state, to_state (State objects), from_states, trigger, lifecycle, lifecycles (every machine that state:hasTransition it) and fired_by.
  • model.aggregates: one Aggregate per subject of seam:aggregateMember (ADR-0035), by IRI: the owl:Class or sh:NodeShape that names the shapes it commits in one store transaction. An aggregate with no members declares no boundary and is not listed.

    • Aggregate has members (its listed Shapes, by IRI, never derived from sh:targetClass) and lifecycle (the Lifecycle it names, or None).
  • model.scenarios and model.decision_examples: what the model says it should do (ADR-0014), by IRI.

    • Scenario has process (a Process), actor (the party IRI), givens, steps, failures, expect_state (a State or None) and expect_end_event (an IRI or None). A Failure has operation (an Operation) and status (the Status it fails with), sorted by operation IRI. A Given has field (the sh:path IRI) and value (a Literal), sorted by field. A Step has task (a ProcessElement), actor and givens, in verify:step list order.
    • DecisionExample has decision (a Decision), givens and expected: the ExpectedOutputs (variable, value), sorted by variable.
    • A missing or duplicated fact, or a field given twice, raises ResolutionError(subject, predicate); the ADR-0014 shapes flag the same faults.
  • model.policies: a Policy has permissions and governs. permissions holds Permission tuples sorted as ModelGraph.policy sorts them. governs lists the operations and tasks that are seam:governedBy the policy.

  • model.interfaces (IFML) has models(), pages(), components() (every element typed with an IFML component class), events(), flows(), landmark() and served_pages(). Each list covers every IFML model; .where(lambda e: e.in_model(":my_ifml")) keeps one model's members (Element.in_model(model) is true for each seam:inModel it states).

    • landmark() is the one page with ifml:isLandmark true. Zero or several raise ResolutionError(model, ifml:isLandmark, "expected one ifml:ViewContainer with ifml:isLandmark true, found [...]").
    • served_pages() lists the pages the generated app serves at their ifml:path: the landmark first, then by IRI every other page with a path that does not host a task form.
    • InteractionModel has pages() and presented_by (the Container that seam:presents it).
    • Page has path, title (ifml:title), is_landmark, is_xor, is_default (ifml:isXOR/ifml:isDefault true), pages() (the nested pages it contains), parents (the pages that contain it), visible_when (lifecycle States, as on Component), interaction_model, components() (by ifml:order, then IRI; a non-integer order raises ResolutionError(component, ifml:order)) and hosts_task_form (a part it contains is the form a user task is seam:completedOn, ADR-0009).
    • Component has kind (Form, Details, List, ViewComponent), types (its concrete classes among ifml:List, ifml:Details and ifml:Form, in that order), order, page, screens (every page that contains it), reads, writes, parameters() (labels), parameter_bindings, events(), data_flows, rendered_by (the operations that render it), visible_when (the lifecycle States it renders in) and completes (the user tasks completed on the form). parameter_bindings holds one Parameter (iri, label, bound_to: the sh:path it is ifml:boundTo, or None) per parameter, by IRI.
    • Event has trigger, component (exactly one owner, as a Component), owners (every element that ifml:hasEvent it, typed as the graph says), triggers (a Process), triggers_all (every process it triggers), governed_by (a Many[Policy]), submits_to (an Operation), flows, navigates_to (a Page) and updates (the components its navigation flows swap).
    • Flow is an ifml:NavigationFlow or ifml:DataFlow. It has kind, source and target (exactly one each), sources, targets, swap (ifml:Replace, ifml:Append or ifml:Prepend; a missing one raises ResolutionError(flow, ifml:swap, "expected exactly one, found none")) and binds: one ParameterBinding (source, target parameter IRIs) per ifml:binds, sorted. event.flows.where(lambda f: f.kind == IFML.NavigationFlow).only() raises ResolutionError(event, ifml:source) when there is none or several.
  • model.processes: one Process per proc:Process, by IRI.

    • Process has bpmn_model (the model it is seam:detailedBy), bpmn_models (all of them, by IRI), bpmn_path, bpmn_process_ids (read from the BPMN XML), executed_by, scheduled_by (the containers that seam:schedules it), triggered_by, triggering_operations (the operations its triggering events seam:submitsTo), invoked_operations (what it or an element of its BPMN model seam:invokes), correlation_key, correlation_parameter, correlation_parameters, channel_starts (each MessageStart: a message start event and the Channel it seam:subscribesTo), elements(), element_iri(bpmn_id), error_boundaries() and timer_events().
    • A ProcessElement is a task or event whose IRI is <bpmn_model>#<bpmn id>. It has bpmn_id, kind (UserTask, ServiceTask, ErrorBoundaryEvent, TimerStartEvent, TimerCatchEvent, ...), types (its proc: classes), process, invokes, reads, writes, fires, decided_by, completed_on (a Component), publishes_to, subscribes_to and catches (the Status a proc:ErrorBoundaryEventseam:catches, ADR-0013).
    • error_boundaries() joins each lifted proc:ErrorBoundaryEvent to the XML: one ErrorBoundary (iri, node and task, the FlowNodes of the boundary and the task it is attached to, operation that task seam:invokes, status it seam:catches, and code, the errorCode of its bpmn:error) per error boundary on an invoking task, in flow order. Where the graph and the XML disagree it raises ResolutionError naming the boundary: seam:catches for a boundary that is not lifted, catches no status or several, catches a status that is not an error of the invoked operation or whose comm:errorCode differs from the XML's, or is lifted with no matching boundary in the XML; bpmn:errorEventDefinition for a catch-all; bpmn:errorRef / bpmn:errorCode for a ref that names no bpmn:error or one without a code.
    • timer_events() joins each lifted proc:TimerStartEvent and proc:TimerCatchEvent to the XML (ADR-0037): one TimerEvent (iri, node, kind, attached_to, the user task's FlowNode for a boundary, interrupting, its cancelActivity, expression, the local name of its timeDate, timeDuration or timeCycle, and body, that element's text as written; is_start and expression_predicate) per in-scope timer, in flow order. In scope are a process startEvent (not one inside an event sub-process), an intermediateCatchEvent and a boundaryEvent on a proc:UserTask. Where the graph and the XML disagree it raises ResolutionError(<event>, bpmn:timerEventDefinition, ...): an in-scope timer not lifted or lifted with the wrong type, a lifted timer out of scope or with no timer event in the XML, a second event definition, and a definition with no expression or more than one. It never parses the expression; the runtime does (bpmn.TimerDefinition and TimerExpression carry it as written).
    • model.processes.for_bpmn_process(pid) lists the processes whose BPMN file declares bpmn:process pid. model.processes.element(bpmn_id) lists the elements with that id across every BPMN model. Call .only() on either; an id used in two BPMN models raises.
    • elements() lists what the graph states, by IRI. nodes() walks the BPMN XML in flow order instead: every flow node of every bpmn:process in bpmn_path (processes in document order), lifted into the graph or not, so start and end events and gateways are included. tasks() and gateways() filter that walk and lanes() lists the Lanes (id, name, node_ids). Each is built once.
    • A FlowNode has iri (element_iri(bpmn_id)), node (the seams.bpmn.BpmnNode), process, element (the same ProcessElement object as elements(), or None when the graph does not type the IRI), bpmn_id, tag (userTask, exclusiveGateway, startEvent, ...), name, lane, incoming, outgoing and paths (SequenceFlows with id, name, source, target and condition), kind, the element's edges (invokes, decided_by, completed_on, fires; reads, writes, publishes_to, subscribes_to), which are None or empty when unlifted, catches, and starts_from (the Channel of channel_starts whose event it is).
    python
    approval = model.processes.for_bpmn_process("approval_process").only()
    for gw in approval.gateways():
        print(gw.bpmn_id, [(f.name, f.target, f.condition) for f in gw.paths])
    # route_gateway [("auto", "record_approval", 'route == "auto"'), …]

    A BpmnNode also has attached_to (a boundary event's attachedToRef), error_ref (its errorEventDefinition's errorRef) and is_error_boundary; a BpmnProcess has errors (the file's bpmn:errors as BpmnError: id, name, code) and error(id).

    seams.bpmn (parse_bpmn) and seams.dmn (parse_dmn, decision_table) are the lxml-only parsers behind these joins; they read any BPMN or DMN namespace version and raise ResolutionError naming the element and attribute (bpmn:targetRef, dmn:decisionTable, ...) that failed.

  • model.decisions: each dec:Decision a task is seam:decidedBy. It has decision_id, dmn_model, dmn_path, inputs and outputs (InputBinding and OutputBinding, as ModelGraph.decision_binding returns them), reads and decides_for.

    • Decision.table is the DMN decision table, read from dmn_path with lxml by seams.dmn (parsed once per file): a DecisionTable with decision_id, decision_name, table_id, hit_policy (UNIQUE when the file states none), aggregation, inputs (TableInput: id, label, expression, type_ref), outputs (TableOutput: id, label, name, type_ref) and rules (Rule: id, inputs and outputs as the entry texts, description), all in document order.
    • A decision without a decisionTable, or a rule whose entries do not match the table's inputs or outputs, raises ResolutionError naming dmn:decisionTable, dmn:inputEntry or dmn:outputEntry.
    python
    table = model.processes.element("route_expense").only().decided_by.table
    [(r.id, r.inputs, r.outputs) for r in table.rules]  # [("rule_auto", ("<= 100",), ('"auto"',)), …]

Single-valued edges (accepts, lifecycle, shape) return None when the fact is absent. When the graph states the fact twice, they raise ResolutionError naming the element and the predicate.

SPARQL and canned questions ​

python
model = seams.load("examples/chat-app/chat-app.trig")
model.sparql("SELECT ?p WHERE { ?p a ifml:ViewContainer }")  # [{"p": URIRef(…chat_page)}]
model.question("impact-of-node", node=":db01")              # [{"impacted": URIRef(…ConversationShape)}, …]
seams.question_names()  # ["c4-l1-rollup", "impact-of-node", …]
  • model.sparql(query, **bindings) runs a read-only SELECT against model.ds with seams.PREFIXES declared (seam, comm, dec, proc, state, hydra, odrl, deploy, ifml, arch, rdf, rdfs, sh, xsd), so it needs no PREFIX lines. It returns a list of dict rows (variable name to rdflib term; unbound variables are absent), in the query's ORDER BY order when it has one, else sorted by the rows' string values.
    • Each keyword binds the variable of that name through initBindings, never by string interpolation. URIRefs and Literals pass through, a str containing : is expanded with model.curie (":db01", a full IRI), and any other value becomes a Literal.
    • An update (INSERT, DELETE, LOAD, DROP, ...) raises ValueError before anything runs. So does a non-SELECT query; use seams.sparql.sparql(model, query) for the raw rdflib Result of an ASK or CONSTRUCT.
  • model.question(name, **bindings) runs one of the canned questions in docs/queries/, which ship as package data (seams/queries/*.rq, byte-identical copies; tests/test_seams_query.py guards the drift). seams.question_names() lists them and seams.load_question(name) returns the text. An unknown name raises KeyError listing the available ones; a name is never turned into a path unchecked.
  • The coverage queries in docs/queries/coverage/ load by folder-qualified name: model.question("coverage/unfired-transitions") and model.question("coverage/policies-without-deny"), each returning no rows when the model is covered. seams.question_names("coverage") lists them; seams.question_names() with no folder still lists only the top-level questions. Any folder other than coverage raises KeyError.

Coverage ​

seams.coverage answers what the model declares that no scenario or test exercises (D054). It covers three things:

  • seams.unexercised_paths(model, process=None) lists the BPMN paths no verify:Scenario runs, as ProcessPath records. Each record has process, nodes (the FlowNodes in walk order), iris, bpmn_ids, start, end and scenarios. seams.process_paths returns every path along with the scenarios that run it. Sequence flows live only in the BPMN XML (ADR-0007), so this is a Python join, not SPARQL. A path is a simple walk from a bpmn:startEvent to a node with no way out. A boundary event counts as one more way out of its task. A walk that can only loop back is dropped.

  • A scenario's run is a walk: a path of its process, then, when a node on that path seam:publishesTo a channel, a path of a process whose start event seam:subscribesTo it (one hop). A scenario exercises a walk when all of these hold:

    • its verify:expectEndEvent is on the walk;
    • a node on the walk seam:fires a transition into its verify:expectState;
    • its steps are exactly the user tasks on the walk, in order, up to the last of those expectations, leaving out a task the walk leaves through its timer boundary (the timer fires while the task is open, so no step completes it);
    • the walk invokes every operation a verify:failure names, leaves each such task through the error boundary that catches the failure's status, and passes an error boundary only that way;
    • no step's task comes after a timer boundary on the walk. A timer boundary needs no failure: a scenario's run lets the timers its instance waits on fire once its steps are done (ADR-0037).

    The scenario then exercises its own process's path. It also exercises the started process's path when one of its expectations lies on it.

  • seams.unfired_transitions(model, lifecycle=None) runs coverage/unfired-transitions and returns a Many[Transition].

  • seams.policies_without_deny(model, policy=None) runs coverage/policies-without-deny and returns UndeniedPermission records (policy, target, assignee).

  • seams.core_operations_untested(model, context=None) runs coverage/core-operations-untested (ADR-0030 section 6) and returns a Many[Operation], by (domain, context, operation): each hydra:Operation whose seam:inModel model a context in an arch:Core domain seam:owns, that is not seam:implementedBy a comm:PythonHandler and that no scenario reaches through the process it starts or the user task it completes. A dataset with no domain has no rows.

On chapter 9 the nine scenarios run every path: both outcomes of each route, the two notify-failed escalations (:notify_failed_approved and :notify_failed_rejected fail :notify_manager_op) and both payout paths (:paid_in_batch and :paid_now expect the payment's final state). Both SPARQL reports are empty too.

Decision checks ​

seams.decisions_check finds decision faults that the runtime would otherwise hit only while running (backlog K2). Each fault is one DecisionFinding, with subject (an IRI), fact and message. The lists are sorted.

  • seams.type_ref_findings(model) checks the input types of each dec:Decision that has a dec:payloadElement. Each dec:input is matched to the table input whose inputExpression text is its dec:variable, which is how the engine matches them. The table input's typeRef must then accept the sh:datatype of the dec:fromPath property. The property is looked up on the shapes the decision seam:reads, or on every shape if it reads none. The accepted datatypes are:

    • number: xsd:decimal, xsd:integer and its subtypes, xsd:double and xsd:float;
    • string: xsd:string;
    • boolean: xsd:boolean;
    • date: xsd:date.

    A dec:Length input is always a number, so its typeRef must be number. A missing or unlisted typeRef gives no finding, and neither does a property without a datatype. In each finding the subject is the decision and the fact is the dec:fromPath IRI.

  • seams.condition_findings(model) checks BPMN conditions. Spiff evaluates each conditionExpression as Python over the task data. Every free name a condition reads (seams.decisions_check.condition_names) must be set by a node upstream of the flow, in the same process. Two kinds of node set names:

    • a business-rule task sets the dec:processVariables and input dec:variables of the decision it is seam:decidedBy;
    • a user task sets the fields of the shape it writes.

    In each finding the subject is the flow IRI (<bpmn model>#<flow id>). The fact is the variable, or the expression itself if it does not parse.

  • seams.table_findings(model) checks UNIQUE decision tables (backlog X13): one finding per pair of overlapping rules and one for the table's first uncovered input, naming the rules or the input. The subject is the decision and the fact dmn:rule. It reads input entries that are -, number comparisons, ranges, quoted strings or comma lists of these; a table with any other entry is skipped, not guessed at.

  • seams.decision_findings(model) returns the findings of all three checks.

A decision or process whose payload file is missing is skipped. Any other ResolutionError met while reading one becomes a finding. Every example model yields no findings.

Model review checks ​

seams.review_check flags facts the model states twice or not at all (backlog R1). Each finding is one ReviewFinding, with subject (an IRI), fact and message. The lists are sorted. A review finding flags the model; it never refuses it, and the checks do not raise.

  • lifecycle_label_findings(model): a sh:in list on a shape with a seam:lifecycle whose members are all rdfs:labels of that lifecycle's states. The subject is the shape, the fact the property's sh:path, and the message names the state IRIs to use instead.
  • shape_class_findings(model): a sh:class whose value is a sh:NodeShape. The subject is the shape, the fact the sh:path.
  • target_class_findings(model): a sh:NodeShape with no sh:targetClass that is not itself an rdfs:Class or owl:Class. The fact is sh:targetClass.
  • undeclared_path_findings(model): a sh:path IRI that no rdf:Property, owl:ObjectProperty, owl:DatatypeProperty or owl:AnnotationProperty declares. Path expressions such as sh:inversePath are skipped. The subject is the path, the fact sh:path.
  • seams.review_findings(model) returns the findings of all four.

The chat app has eleven: its MessageShape status list, sh:class :MessageShape, both shapes and all seven paths. Backlog O1 and O2 remove them.

Domain profiles ​

seams.profiles resolves the domain profile a model attaches to with seam:conformsTo (ADR-0018, ADR-0033 section 5). resolve_profile(ds, model_dir) finds the profiles/*/profile.ttl in model_dir (the directory of the loaded TriG file, seams.model_dir(path)) that declares the IRI a seam:Profile. It returns None when the model states no seam:conformsTo, and then reads no file. Otherwise it returns a Profile:

  • iri: the profile IRI;
  • classes and labels: the seam:offersClass classes, sorted, and their skos:prefLabel, else rdfs:label;
  • generators: the seam:usesGenerator names, sorted;
  • shapes: one graph parsed from the seam:profileShapes files;
  • graph: the parsed profile.ttl;
  • pills: each pill shape (a sh:NodeShape in shapes with sh:severity sh:Info and a label, ADR-0033 section 4) mapped to its label.

The resolver reads and parses; it does not validate. seams check and the viewer run pyshacl with shapes. A model naming two profiles, a profile no profile.ttl declares or two declare, and a profile or shapes file that is missing or does not parse raise ResolutionError naming seam:conformsTo or seam:profileShapes, with paths relative to model_dir.

Context assembly ​

seams.context gathers the model facts that the code of a service task depends on (backlog C3). context_for(model, task) takes a Model or ModelGraph and a task given as a BPMN id, CURIE or IRI. It returns a Manifest with task (an IRI), items (ContextItems sorted by kind and ref) and context_hash. Each ContextItem has a kind, a ref (an IRI, or a model-relative path#id for an XML fragment), a hash (sha256:<hex>) and the content that was hashed.

Each item keeps only the facts the task uses. A container is narrowed to its technology and the deploy:key of each binding. Binding values are never read. Graph items hash sorted, canonical N-Triples. XML fragments hash their exclusive C14N. context_hash is sha256 over the items, each framed as kind, NUL, ref, NUL, hash, newline. An unknown task, a task that is not a proc:ServiceTask, or a missing hop raises ResolutionError. The package README lists the rules for each kind.

The model hash ​

"sha256:<hex>", where each payload is framed as relative path, NUL, decimal length, bytes, sorted by relative path:

  • File: the TriG bytes, then the framed payloads. This is byte-identical to the hash embedded in generated code (model-hash: headers).
  • Directory: each TriG file framed the same way (its name, sorted by name), then the framed payloads.

Editing or renaming the model or any payload changes the hash.

Store resolution ​

ModelGraph.store_plan() answers which store the runtime keeps its rows in, from three graph facts (ADR-0011): deploy:storeKind on the store container, a deploy:connectsTo binding on the container that runs the model, and seam:storedIn from each shape. It returns a frozen StorePlan:

FieldMeaning
kinddeploy:PostgreSQL or deploy:SQLite (RELATIONAL_STORE_KINDS)
storethe store container, or None for the default
containerthe runtime container whose binding connects to the store, or None
keythat binding's deploy:key, the environment variable holding the connection string, or None
declaredFalse only for the embedded SQLite default
sempkmthe SemPKMPlan of a deploy:SemPKM store (ADR-0025), or None
python
seams.ModelGraph("examples/chat-app/chat-app.trig").store_plan()
# StorePlan(kind=deploy:PostgreSQL, store=:message_db, container=:chat_api, key="DB_URL", declared=True)

Runtime containers are the containers that seam:executes a process or seam:exposes an operation that seam:writes a shape. A model with no connecting binding on one and no seam:storedIn at all gets StorePlan(deploy:SQLite, None, None, None, declared=False). Otherwise the store is the one a runtime container connects to, else the single store the written shapes are seam:storedIn. A deploy:SQLite store is embedded: key may be None and the file path still comes from SEAMS_DB.

Each failure raises ResolutionError(subject, predicate):

Casesubjectpredicate
a store with no deploy:storeKind, or more than onethe storedeploy:storeKind
a kind not in SUPPORTED_STORE_KINDS ("unsupported")the storedeploy:storeKind
a PostgreSQL store no runtime container connects tothe first runtime container, or the store when there is nonedeploy:connectsTo
a written shape with no seam:storedIn, or stored elsewherethe shapeseam:storedIn
one container connecting to two stores, or two containers to different stores (v1 runs one store per model)the containerdeploy:connectsTo

A SemPKM store ​

SUPPORTED_STORE_KINDS is the relational kinds plus deploy:SemPKM (ADR-0025). A SemPKM store holds the entities of the shapes stored in it; process state stays in the relational store, so a runtime container connects to at most one store of each family, and each written shape is stored in one of the two. The SemPKM store resolves as StorePlan.sempkm, a frozen SemPKMPlan, while kind, store, container, key and declared keep describing the relational store (the embedded SQLite default when there is none):

FieldMeaning
storethe deploy:SemPKM store container
containerthe runtime container whose bindings reach it
keythe deploy:key of the binding that deploy:connectsTo it: the base URL
token_keythe deploy:key of the binding that deploy:authenticatesTo it: the service token
shapesthe shapes seam:storedIn it, sorted

The SemPKM rows of the error contract (ADR-0025 section 11):

Casesubjectpredicate
a SemPKM store no runtime container connects tothe first runtime container, or the store when there is nonedeploy:connectsTo
no binding that deploy:authenticatesTo the store, more than one, or one whose value is not a deploy:secretRef or deploy:secretFilethe connecting containerdeploy:authenticatesTo
one container connecting to two SemPKM stores, or to two relational storesthe containerdeploy:connectsTo
a shape stored in the SemPKM store with no sh:targetClassthe shapesh:targetClass
a nested field of such a shape whose sh:node shape has no sh:targetClassthe nested shapesh:targetClass
a gistCore import other than SEMPKM_GIST_VERSION (gistCore 14.0.0)the ontologyowl:imports

The shape view's stored_in and the container view's store_kinds, stored_shapes, connecting_bindings and authenticating_bindings read the same facts; Binding.authenticates_to is the store a token binding names.

ModelGraph.store_health() turns the same checks into findings for a model-health tool (the viewer's health panel uses it). It never raises for a model the loader accepts, and returns a list of frozen StoreFinding(severity, subject, predicate, message), sorted by severity (STORE_FINDING_SEVERITIES: "Violation", "Warning", "Info", the SHACL severity names) and then subject:

SeverityWhensubjectpredicate
Violationstore_plan() raises any of the errors abovethe error's subjectthe error's predicate
Warninga store container (one with a deploy:storeKind, or that a binding deploy:connectsTo or deploy:authenticatesTo) that no shape is seam:storedInthe storeseam:storedIn
Infothe model declares no store, so the runtime keeps embedded SQLite at SEAMS_DBNoneNone
Infothe model declares only a SemPKM store, so process state stays in embedded SQLite at SEAMS_DBNoneNone

The warning is reported whether or not the plan resolves, so a declared PostgreSQL container next to an embedded-SQLite runtime is visible:

python
seams.ModelGraph("examples/chat-app/chat-app.trig").store_health()
# []
seams.ModelGraph("examples/checkout.trig").store_health()
# [StoreFinding(severity="Violation", subject=:order_api, predicate=deploy:connectsTo, message="cannot resolve ...")]

SemPKM webhook channels ​

A channel whose comm:transport is comm:SemPKMWebhook is inbound (ADR-0027): SemPKM POSTs its deliveries to the channel's comm:path on the one container that seam:exposes it, and each changed object of the accepted shape's class starts the processes subscribed to the channel. ModelGraph.webhook_channel_iris() lists those channels, sorted; is_webhook_channel(channel) asks about one; webhook_channel(channel) resolves one to a frozen WebhookChannel, and webhook_channels() resolves them all, sorted by IRI:

FieldMeaning
iri, labelthe channel
event_nameits comm:eventName, a WEBHOOK_EVENT_TYPES member ("object.changed")
pathits comm:path template, which has no variables
shape, target_classthe shape it seam:acceptsShape and that shape's sh:targetClass
storethe deploy:SemPKM store the shape is seam:storedIn
containerthe receiving container, the one that seam:exposes the channel
key, token_keythe deploy:keys of its bindings that deploy:connectsTo and deploy:authenticatesTo the store
processesthe subscribed channel-started processes (subscribed_processes), sorted

Each row of ADR-0027 section 10 raises ResolutionError(subject, predicate):

Casesubjectpredicate
no comm:eventName, more than one, or one not in WEBHOOK_EVENT_TYPESthe channelcomm:eventName
no seam:acceptsShape, or more than onethe channelseam:acceptsShape
the accepted shape is not seam:storedIn exactly one store, a deploy:SemPKM onethe shapeseam:storedIn
the accepted shape has no sh:targetClass, or more than onethe shapesh:targetClass
no comm:path, more than one, or one with a hydra:mappingthe channelcomm:path
two webhook channels of one container with the same path templatethe containercomm:path
no container seam:exposes the channel, or more than onethe channelseam:exposes
no channel-started process subscribes to itthe channelseam:subscribesTo
a subscriber the receiving container does not seam:executesthe containerseam:executes
an element seam:publishesTo itthe channelseam:publishesTo
the container has no binding that deploy:connectsTo the storethe containerdeploy:connectsTo
no token binding for the store, more than one, or one that is not a secretthe containerdeploy:authenticatesTo
an element of a process the container executes seam:writes the accepted shapethe shapeseam:writes
comm:SemPKMWebhook and another comm:transport, or a channel that is not a webhook channelthe channelcomm:transport

seamsh:SemPKMWebhookChannelShape reports the same faults in seams check, so a model that passes the shapes resolves.

ModelGraph.replica_health() reports replicas that would not share state (ADR-0038) in the same StoreFinding form, and the viewer's health panel shows it beside the store findings. Replicas of a serving container share process instances and the live-update stream through a deploy:PostgreSQL store; an embedded or deploy:SQLite store lives in one process. A serving container whose deploy:replicas is above 1 in some environment (under the override rule, or its shared count in a model with no environment) while store_plan() is not PostgreSQL is a Violation with predicate deploy:replicas naming the declared counts and the store kind; the compose generator refuses the same model, and seamsh:ReplicatedStoreShape reports it in seams check. Facts the override rule cannot resolve are a Violation too, and a store plan that cannot be resolved is left to store_health(). The chat app's :chat_api declares 3 on its PostgreSQL store, so it gets no finding.

Operation implementation ​

ModelGraph.operation_implementation(op) reads the operation's seam:implementedBy (ADR-0012) and returns a frozen Implementation(iri, kind, handler), or None when the operation declares none. kind is one of IMPLEMENTATION_KINDS (comm:HTTPEndpoint, comm:LLMCompletion, comm:PythonHandler); handler is the comm:handler dotted path of a Python handler and None for the other kinds. The operation view's implemented_by lists the implementation nodes.

python
seams.ModelGraph("examples/chat-app/chat-app.trig").operation_implementation(":completion_op")
# Implementation(iri=:completion_impl, kind=comm:LLMCompletion, handler=None)

Each model fault raises ResolutionError(subject, predicate):

Casesubjectpredicate
more than one seam:implementedBy, or one that is not an IRIthe operationseam:implementedBy
no rdf:type, several, or one not in IMPLEMENTATION_KINDSthe implementationrdf:type
a comm:PythonHandler with no comm:handler, several, or one with a segment that is not a Python identifierthe implementationcomm:handler

Whether the kind agrees with where the operation is served, and whether a handler imports, are the runtime's checks at load, not the library's.

Errors ​

Every error the library raises for a model problem is a SeamsError.

  • SeamsError is the base class. It carries http_status (500) and to_dict(), a JSON-safe dict with type, message and http_status plus the error's own fields, so a route handler can return it as a response body.

  • ResolutionError(subject, predicate, detail="") is raised when a graph hop is missing, duplicated or points nowhere. It is also a LookupError. Its subject and predicate are the IRI strings it is about, and its message reads cannot resolve <subject>: missing <predicate> (<detail>):

    python
    try:
        model.lifecycles.only()
    except seams.ResolutionError as err:
        err.predicate  # "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
        err.detail     # "expected exactly one, found [...]"

Loading itself raises the standard errors: FileNotFoundError for a missing file or a directory without *.trig, and ValueError when no path is given and SEAMS_MODEL is unset or empty.

Released under the Apache-2.0 License.