Skip to content

Relations and Graph Rules

Relations in 52 seconds: a coding agent's shortcut connects the public feed to private drafts, and a path rule stops it, however indirect. Narrated, with captions.

A relation declares one structural link between components and resources: a call, a callback, a provided resource, a multi-party coordination, or a link of a custom kind. Relations are top-level declarations, never members of a component. They are also the only structural links in the Shape model: owns, grants, and effect entries add no edges, and the checker infers none from source. Each relation is a hyperedge over two or more endpoints, and graph rules query the resulting hypergraph.

This model passes shp check:

module editor
resource Revision
component Editor {
}
component RevisionLog {
}
relation EditorCallsLog {
kind calls
connects Editor -> RevisionLog
}
relation LogCallsEditor {
kind callbacks
connects RevisionLog -> Editor
}
relation RevisionLineage {
kind generated_from
connects Revision -> RevisionLog
}
relation RevisionWritePath {
kind coordinated_call
connects Editor -> RevisionLog -> Revision
roles { Editor as caller, RevisionLog as writer }
summary "Editor writes revisions only through RevisionLog."
}
MemberPresenceMeaning
nameRequired, unique (duplicate relation otherwise)Identifies the relation in diagnostics and shp graph output. A stable name keeps both reviewable.
kindRequired, onceA prelude kind (calls, callbacks, provides, coordinated_call) or any other identifier. See Kinds and traversal.
connectsRequired, onceTwo or more distinct endpoints, ordered as A -> B -> C or unordered as { A, B, C }.
rolesOptional, onceLabels such as { Editor as caller }. Each must name a connects endpoint, at most once. shp graph prints them; no rule reads them.
summaryOptional, onceReview text, printed by shp graph after //.
expectsOptional, repeatableexpects Endpoint fingerprint provider("value") pins a resource endpoint’s fingerprint. The endpoint must be a connects endpoint and a resource, otherwise invalid relation. A missing or different fingerprint gives stale fingerprint expectation, and a removed endpoint resource gives unknown relation_endpoint. See Generate Drafts from Source.

Each endpoint names a component or a resource. A bare name resolves in its own module first, then through the module’s imports, and module::Name names another module’s declaration directly. Resolution fails in three ways:

ConditionDiagnostic
No component or resource has the name.unknown relation_endpoint
A component and a resource share the name.invalid relation, with the reason endpoint Name resolves to both a component and a resource
More than one imported module declares the name.ambiguous relation_endpoint; qualify the reference

forbid path and forbid hypercycle walk traversal steps, and each kind contributes steps differently:

KindEndpointsTraversal stepsTypical use
callsExactly two, ordered A -> BOne step, A → BA component calls another
callbacksExactly two, ordered A -> BOne step, A → BA callback edge, often paired with calls
providesExactly two, ordered: a component, then a resourceOne step, A → BA component provides a resource or interface
coordinated_callTwo or more, orderedOne step per consecutive pairMulti-party coordination, such as a write path or saga
Any other identifierTwo or more, ordered or unorderedNoneLineage or documentation links, such as generated_from

Only provides constrains endpoint kinds; the other kinds accept components and resources in any position. The typical-use column is guidance, not a check. In the model above, RevisionWritePath contributes the steps Editor → RevisionLog and RevisionLog → Revision, while RevisionLineage contributes none. Custom kinds appear in shp graph output but never in path or cycle search.

The calls relation EditorCallsLog and the callbacks relation LogCallsEditor each contribute one traversal step, the coordinated_call RevisionWritePath contributes one step per consecutive pair of endpoints, the custom generated_from relation RevisionLineage contributes none, and for the rule no_runtime_cycle over calls or callbacks the two single-step relations form the one reported witness, Editor -> RevisionLog -> Editor.

A relation that breaks a structural rule gives invalid relation, printed as relation module::Name is invalid: reason. The rules are:

  • kind and connects are present, and no member other than expects repeats.
  • No endpoint repeats in connects.
  • The endpoint count and ordering suit the kind, as listed in Kinds and traversal.
  • A provides relation connects a component to a resource. Otherwise the reason is provides provider X must be a component or provides target X must be a resource.
  • Each role names a connects endpoint, and no endpoint has two roles.

A relation with a missing kind or connects, a repeated endpoint, or the wrong count or ordering for its kind is dropped from the hypergraph, so shp graph and graph rules do not see it until it is fixed.

A rule is a named top-level declaration whose body holds forbid clauses. Each graph-rule diagnostic names the rule. A rule can also derive a final forbid from a trait condition; see Effect Model.

forbid provides R except C rejects every provides relation whose target is the resource R and whose provider is not the component C. except is optional and names exactly one component, so no rule form allows two providers; without except, every provider of R is rejected. R must be a declared resource and C a declared component (unknown resource and unknown component otherwise).

module publishing
resource StoryEndpoint
component Editor {
}
component Publisher {
}
relation EditorProvidesStories {
kind provides
connects Editor -> StoryEndpoint
}
relation PublisherProvidesStories {
kind provides
connects Publisher -> StoryEndpoint
}
rule publisher_only_story_endpoint {
forbid provides StoryEndpoint except Publisher
}

shp check exits 1:

error: forbidden provides
Editor provides StoryEndpoint via relation EditorProvidesStories.
rule publisher_only_story_endpoint forbids provides StoryEndpoint except Publisher.
caused by:
- shape/publishing.shape: relation EditorProvidesStories
- shape/publishing.shape: rule publisher_only_story_endpoint forbids provides StoryEndpoint

PublisherProvidesStories is allowed, and removing or redirecting EditorProvidesStories makes the model pass. The rule emits one diagnostic per offending relation. Written without except, as forbid provides StoryEndpoint in a rule named no_story_endpoint, it reports both relations, and each diagnostic’s second line reads rule no_story_endpoint forbids provides StoryEndpoint.

forbid path A -> B over K1 or K2 rejects a directed path from A to B in which every step comes from a relation of a listed kind. The over list is required; without it the file does not parse. Direction matters, and kinds outside the list cannot complete a path.

module feed
resource PrivateDraft
component Feed {
}
component Recommender {
}
relation FeedCallsRecommender {
kind calls
connects Feed -> Recommender
}
relation RecommenderProvidesDrafts {
kind provides
connects Recommender -> PrivateDraft
}
rule no_feed_to_drafts {
forbid path Feed -> PrivateDraft over calls or provides
}

shp check exits 1:

error: forbidden path
rule no_feed_to_drafts rejects this dependency path:
calls FeedCallsRecommender: Feed -> Recommender
provides RecommenderProvidesDrafts: Recommender -> PrivateDraft
witness: Feed -> Recommender -> PrivateDraft
caused by:
- shape/feed.shape: rule no_feed_to_drafts forbids path Feed -> PrivateDraft over calls or provides
- shape/feed.shape: relation FeedCallsRecommender
- shape/feed.shape: relation RecommenderProvidesDrafts

A forbid path rule must meet these requirements, and a rule that fails one reports the listed diagnostic instead of searching:

  • A and B are distinct. forbid path Feed -> Feed is an invalid rule with the reason forbidden path endpoints must be distinct; use forbid hypercycle for cycles.
  • Each endpoint names a declared component or resource (unknown relation_endpoint otherwise).
  • Every listed kind has traversal steps. A custom kind makes the rule invalid: forbid path Revision -> RevisionLog over generated_from gives invalid rule with the reason relation kind generated_from has no directed traversal semantics.

A relation with an unresolved or ambiguous endpoint, or a provides relation whose provider is not a component or whose target is not a resource, contributes no steps to a path.

forbid hypercycle over K1 or K2 rejects a directed cycle built from steps of the listed kinds. Without over, it searches the steps of every kind that has traversal. Adding this rule to the first model on this page:

rule no_runtime_cycle {
forbid hypercycle over calls or callbacks
}

makes shp check exit 1:

error: forbidden hypercycle
rule no_runtime_cycle rejects this hypercycle:
calls EditorCallsLog
callbacks LogCallsEditor
witness: Editor -> RevisionLog -> Editor
caused by:
- shape/editor.shape: rule no_runtime_cycle forbids hypercycle over calls or callbacks
- shape/editor.shape: relation EditorCallsLog
- shape/editor.shape: relation LogCallsEditor

The over list is not validated. A custom or misspelled kind contributes no steps, so a rule whose over list names only such kinds never matches; it does not fall back to searching every kind. In the first model, RevisionWritePath steps from RevisionLog to Revision and RevisionLineage links Revision back to RevisionLog, yet the model still passes (Shape check passed.) with this rule added:

rule no_lineage_cycle {
forbid hypercycle over coordinated_call or generated_from
}

Each forbid path and each forbid hypercycle reports at most one witness, even when the model contains several violating paths or disjoint cycles:

  • The witness is the shortest, with the fewest steps.
  • Ties break on the vertex sequence in codepoint order, then on relation kind, then on relation name, so the witness does not depend on declaration order.
  • A hypercycle witness starts at its codepoint-smallest vertex and lists each relation once, in walk order. A coordinated_call that contributes several steps appears once.
  • A path witness prints one line per step, as kind Name: From -> To, so a relation that contributes two steps appears twice.

Fixing the reported witness can expose another, so a model can need several rounds of fixes before the rule passes. The usual fix breaks or redirects a cited relation; weakening the rule changes the claim instead. Rule Evaluation describes the search algorithms.

shp graph prints the hypergraph without running checks:

  • shp graph stats [--kind KIND] prints counts of vertices, relations per kind, incidences, arity, and isolated vertices. It sizes up a model before a large relation edit.
  • shp graph show SYMBOL [--kind KIND] prints the relations incident to a component or resource, or a single relation by name.
  • shp graph all [--kind KIND] prints every relation, grouped by kind.

For the first model on this page, shp graph show Editor prints:

Editor (component)
callbacks LogCallsEditor: RevisionLog (component) -> Editor (component)
calls EditorCallsLog: Editor (component) -> RevisionLog (component)
coordinated_call RevisionWritePath: Editor (component) as caller -> RevisionLog (component) as writer -> Revision (resource) // Editor writes revisions only through RevisionLog.

Each line reads kind Name: endpoints, with the endpoint’s declaration kind in parentheses, its role after as, and the summary after //. Unordered endpoints print as { A, B, C }. Flags and exit codes are in the CLI Reference.