Language Syntax
This page mirrors the current Langium grammar at packages/shp-checker/src/language/shape.langium.
Module
Section titled “Module”module audit
import shared.resources
resource AuditEvent : AppendOnlymodule is optional, but named modules make imports and diagnostics clearer. Declarations are scoped by module, so two modules may both declare Store without colliding. References resolve local declarations first and then explicit imports. Use other.module::Name when an authored claim should point at a specific module unambiguously.
Module path segments and other bare identifiers cannot be reserved keywords. Keywords such as evidence, effect, resource, component, relation, change, memory, and fn are not valid module segments (module experiment.evidence is a parse error; use module experiment.with_evidence or similar).
Top-level declarations
Section titled “Top-level declarations”Shape modules can contain:
resource AuditEvent : AppendOnlytrait AppendOnly<T: Resource> { ... }component AuditStore { ... }relation AuditWritePath { ... }effect candidate AppendEventCandidate { ... }implementation AuditStoreImpl { ... }binding CheckerDocs { ... }change RefactorDecision { ... }attest no_shape_change { ... }rule NoCallsCycle { ... }rationale InlineDecision : InlineRationale<fn Gateway.derivePolicyDecision> { ... }memory DecisionRefactorConstraint : RefactorConstraint<fn Gateway.derivePolicyDecision> { ... }reevaluation DecisionShapeRechecked { ... }role GatewayTeampolicy RequireApprover { require approver }Resources
Section titled “Resources”module audit
resource AuditEvent : AppendOnly { storage postgres.table("audit_events")}Storage and fingerprint declarations use provider names and string values. Fingerprints are checkable resource metadata:
module generated.audit
resource PurgeOldEventsAstAnchor { storage ast.anchor("src/audit/store.rs#purge_old_events") fingerprint ast.semantic_subtree_v1("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}Traits
Section titled “Traits”module audit
trait AppendOnly<T: Resource> { allow Append<T> allow Read<T> forbid final HardDelete<T> require Append<T>}Trait members are allow, forbid, require effect patterns, and optional require_context ContextType<T> obligations (satisfied_by rationale, memory, or either).
A trait can define its own context obligation with require_context ContextType<T>, where T is the trait’s type parameter and its bound sets the target kind: Fn (or unbound) maps to a function target, Component to a component, and Resource to a resource. A parameter that is not declared, or an unrecognised bound, is reported as invalid require_context. An optional satisfied_by clause restricts which context kind satisfies the obligation; the default accepts either rationale or memory. A trait declared with the same name as a built-in shape trait replaces (shadows) the built-in obligation through name resolution. require_context and satisfied_by are reserved keywords.
Components
Section titled “Components”Components carry ownership, grants, and function summaries only. Structural dependencies between components and resources are declared as top-level relation blocks; they are not part of a component body.
module audit
resource AuditEvent : AppendOnly
component AuditStore { owns AuditEvent grants Append<AuditEvent> fn appendEvent source ts("src/audit/store.ts#appendEvent") effects complete { Append<AuditEvent> evidence ts("src/audit/store.ts#appendEvent") }}Function summaries support shape traits, source, optional description, optional unsafe, effects complete, effects unknown, function-level requires (capability term, used with unsafe), reason, and expires.
Generated AST drafts may include candidate effect evidence. These declarations are not reviewed effect claims; they are machine-readable hints that agents can compare with authored effects complete summaries:
module shape.generated.ast.audit
resource AuditEvent
resource AuditStoreAppendEventAstAnchor { fingerprint ast.semantic_subtree_v1("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}
component AuditStore { fn appendEvent source ts("src/audit/store.ts#AuditStore.appendEvent") effects unknown}
effect candidate AppendEventCandidate { fn AuditStore.appendEvent effect Append<AuditEvent> source ts("src/audit/store.ts#AuditStore.appendEvent") confidence low pin AuditStoreAppendEventAstAnchor fingerprint ast.semantic_subtree_v1("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}Relations
Section titled “Relations”Relations are the only structural primitive. A relation is a hyperedge connecting two or more components or resources. Binary relations are simply 2-vertex hyperedges.
module audit
resource AuditEvent
resource PurgeOldEventsAstAnchor { fingerprint ast.semantic_subtree_v1("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}
component Gateway {}
component AuditStore {}
relation GatewayCallsAudit { kind calls connects Gateway -> AuditStore}
relation AuditWritePath { kind coordinated_call connects Gateway -> AuditStore -> AuditEvent summary "Audit writes flow Gateway -> AuditStore -> AuditEvent."}
relation ReviewedFromAst { kind generated_from connects AuditStore -> PurgeOldEventsAstAnchor expects PurgeOldEventsAstAnchor fingerprint ast.semantic_subtree_v1("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}Relation members:
kind— a relation kind name (e.g.calls,callbacks,provides,coordinated_call).connects— eitherA -> B -> ...(ordered) or{ A, B, ... }(unordered). At least two endpoints are required.roles— optional{ Gateway as caller, AuditStore as callee }tagging.expects— optional endpoint fingerprint pin, written asexpects Endpoint fingerprint provider("value").summary— optional review text.
Directional prelude kinds must use ordered A -> B syntax. Binary directional kinds (calls, callbacks, provides) must have exactly two endpoints, and provides must connect a component provider to a resource target. coordinated_call must use ordered A -> B -> ... syntax.
Fingerprint expectations must name one of the relation endpoints. The endpoint must be a resource with a matching fingerprint provider and value, otherwise shp check reports a stale fingerprint expectation.
See Relations and Hypergraphs for the kind registry and traversal semantics.
Implementations
Section titled “Implementations”module audit
resource AuditEvent : AppendOnly
component AuditStore { owns AuditEvent}
implementation AuditStoreImpl { paths { "src/audit/**/*.ts" } conforms_to AuditStore on_change require shape_update}Implementation members are paths, conforms_to, and optional on_change require shape_update.
Bindings
Section titled “Bindings”Bindings couple one set of changed paths to another. They are useful when a code or model change affects a public review surface such as docs.
module repo
binding CheckerDocs { when_changed paths { "packages/shp-checker/src/checker.ts" "shape/checker.shape" } require_changed paths { "docs-site/src/content/docs/inside-shape/rule-evaluation.md" "docs-site/src/content/docs/reference/diagnostics.md" } allow attest docs_not_needed}When shp check --changed-files runs, a matching when_changed path requires at least one require_changed path in the same changed-file list. A narrow attestation can satisfy the binding only when the attestation’s .shape file is also in that changed-file list:
module repo
attest docs_not_needed { source ts("packages/shp-checker/src/checker.ts") reason "Internal extraction only; no documented behavior changed."}Bindings enforce review coupling. They do not prove that the paired docs are complete.
Global model edits and change blocks
Section titled “Global model edits and change blocks”The repository workflow updates the global model directly. Add, modify, or remove normal declarations in the owning module:
module audit
component AuditStore { fn purgeOldEvents source ts("src/audit/purge.ts#purgeOldEvents") effects complete { HardDelete<AuditEvent> evidence ts("src/audit/purge.ts#purgeOldEvents") }}Guarded refactors can also use a top-level change block to declare intended adds, modifies, and removes without rewriting history as free prose:
module gateway
change RefactorDecision { modify fn Gateway.derivePolicyDecision effects complete { Read<PolicySnapshot> }}Change entries include add/modify/remove for functions. Declaration entries support add/modify for resource, trait, component, relation, implementation, binding, attest, and rule, plus remove for those kinds except attest. A modify fn may declare transform Label1, Label2 for intent-sensitive guards.
module rules
rule NoCallsCycle { forbid hypercycle over calls or callbacks}
rule NoSecretRoute { forbid path Gateway -> SecretStore over calls or provides}
rule GatewayBoundary { forbid provides JsonRpcEndpoint except Gateway}Rules currently support when subject has TraitName, forbid effect patterns (including forbid final), forbid provides TARGET except COMPONENT, forbid hypercycle [over KIND or KIND ...], and forbid path SOURCE -> TARGET over KIND or KIND .... A path’s kind filter is mandatory, its endpoints must be distinct declared components or resources, and each listed kind must have directed traversal semantics. The checker rejects any matching path and reports the canonical fewest-hop witness.
Rule headers do not take type parameters; when T has TraitName binds the subject name used by final effect forbids. Repeated when clauses for the same subject are conjunctive. A final-effect condition trait is either a zero-parameter marker or declares exactly one explicit Resource parameter; other generic shapes are rejected. Concrete forbid targets, path endpoints, trait references, and exception references may be module-qualified with module.name::Declaration.
Rationale, memory, and reevaluation
Section titled “Rationale, memory, and reevaluation”Rationale and memory declarations attach typed design context to an existing target:
module gateway
resource PolicySnapshot
component Gateway { owns PolicySnapshot grants Read<PolicySnapshot> fn derivePolicyDecision : RefactorSensitive effects complete { Read<PolicySnapshot> }}
memory DecisionRefactorConstraint : RefactorConstraint<fn Gateway.derivePolicyDecision> { applies_to fn Gateway.derivePolicyDecision status Unexplained confidence High summary "Previous refactors changed error normalisation behaviour." who { owner GatewayTeam } guards { on_change require ReEvaluation<Self> }}rationale members can include applies_to, why, summary, evidence, and the grouped guard blocks protects, guards, who, and when.
memory members can include applies_to, status, confidence, observed, summary, sensitive, evidence, and the grouped guard blocks protects, guards, who, and when.
A protects block lists the protected properties, comma-separated: each entry is either a property with a value, such as shape PreserveInline, or the local description written as description. Guards whose protected properties are all detectable (a named shape trait, or the description) fire only when that property is removed.
A guards block lists guard actions: each is either on_change require ReEvaluation<Self> or forbid transform Label. A modify fn change can declare its intent with transform Label1, Label2; a forbid transform guard fires only when a matching transform intent is declared.
A memory may carry a sensitive flag. A top-level role Name declaration registers a valid reviewer/approver identity, and a top-level policy Name { require approver } declaration requires an approver on reevaluations that satisfy a sensitive memory. role, policy, and sensitive are reserved keywords.
Guard members are authored as grouped blocks — protects { ... }, guards { ... }, who { owner ... }, and when { review_by ... }. This is the single canonical syntax; shp fmt always emits these blocks. who and when are single-valued (one owner, one review_by). who is a reserved keyword.
reevaluation records review for a guarded change:
module gateway
reevaluation DecisionShapeRechecked { satisfies memory gateway::DecisionRefactorConstraint outcome Confirmed summary "Refactor preserves error-normalisation behaviour." reviewer GatewayTeam approver Security decided_on "2026-06-02" evidence test("gateway/error-normalisation.test.ts")}reevaluation members can include satisfies, outcome, summary, evidence, reviewer, approver, and decided_on. satisfies can use a module-qualified rationale or memory name.