Effect Model
The effect model is the part of the Shape model that shp check judges function by function. Resources name the data the architecture protects, and traits attach final forbids to them. Components own resources, grant effects, and contain function summaries. Each summary declares the effects its function emits and can cite the code behind each claim.
The examples on this page build on this model, which passes shp check:
module history
resource Revision : AppendOnly
component RevisionLog { owns Revision grants Append<Revision> grants Read<Revision> fn appendRevision source ts("src/history/log.ts#appendRevision") effects complete { Append<Revision> evidence ts("src/history/log.ts#appendRevision") } fn listRevisions source ts("src/history/log.ts#listRevisions") effects complete { Read<Revision> evidence ts("src/history/log.ts#listRevisions") }}Resources
Section titled “Resources”A resource is an architectural target that the model protects: a table, stream, bucket, ledger, queue, secret, endpoint, or domain object. It need not be a single runtime type. Traits follow the name after :, separated by commas, and an optional body holds metadata:
module history
resource Revision : AppendOnly { storage postgres.table("revisions")}storage provider("value")records where the resource lives.shp checkignores it;shp analyze --shape-filesuses the value as an alias when it matches source operations to resources. See Analyzer Hints.fingerprint provider("value")records a content hash, usually of a generated AST anchor, that relationexpectspins andeffect candidatepins compare against. A resource declares each provider at most once. See Generate Drafts from Source.
Traits and final forbids
Section titled “Traits and final forbids”A trait declares members that apply to whatever carries it. Only two kinds of member affect shp check:
| Member | Effect on shp check |
|---|---|
forbid final Effect<T> | Enforced. A declared effect that matches the pattern on a resource carrying the trait fails with forbidden effect, whether or not the component grants it. |
require_context Context<T> | Enforced. Creates a design-context obligation; see Design Memory. |
allow Effect<T> | Parsed and formatted, not enforced. |
require Effect<T> | Parsed and formatted, not enforced. Nothing checks that any function emits the effect. |
forbid Effect<T> | Parsed and formatted, not enforced. Only forbid final blocks an effect. |
Declaring a trait
Section titled “Declaring a trait”module history
trait Immutable<T: Resource> { forbid final Update<T>}
resource Revision : AppendOnly, ImmutableA trait application names the trait without type arguments. resource Revision : AppendOnly, Immutable binds each trait’s type parameter, whatever its name, to Revision. A pattern written without a target, such as forbid final Update, also binds to the resource that carries the trait, so it forbids Update<Revision>. shp explain Revision lists the final forbids that result, the first three from the prelude AppendOnly:
Revision kind: resource traits: AppendOnly history::Immutable
final forbidden effects: HardDelete<Revision> Truncate<Revision> DropStorage<Revision> Update<Revision>Prelude traits
Section titled “Prelude traits”The standard prelude supplies these traits without a declaration:
AppendOnly<T: Resource>has three final forbids:HardDelete<T>,Truncate<T>, andDropStorage<T>.Persistent,Ephemeral,PII,Secret,Public,External, andInternalare markers. They forbid nothing, but a rule can test for them withwhen T has Trait.- Shape traits such as
PreserveInline,RequiresDescription, andRefactorSensitivederive no effect policy. They require design context instead; see Design Memory.
A trait declared in a module under a prelude name replaces the prelude trait for that module, with no diagnostic. Other modules still resolve the bare name to the prelude trait, with one exception: a prelude-named trait declared in a file with no module line replaces the prelude trait for every module in the Shape model. Either way, a replacement trait AppendOnly whose only member is forbid final HardDelete<T> leaves Truncate and DropStorage unforbidden. A prelude name should not be redeclared.
Rule-derived final forbids
Section titled “Rule-derived final forbids”A rule can derive a final forbid from a condition instead of a trait member. This model marks Revision with an empty trait, and the rule forbids hard deletes on every resource that carries it:
module history
trait Protected<T: Resource> {}
resource Revision : Protected
component RevisionLog { owns Revision grants Read<Revision> fn listRevisions effects complete { Read<Revision> }}
rule protected_revisions_are_not_deleted { forbid final HardDelete<T> when T has Protected}when T has Protectedbinds the rule-local subjectTto each resource that carriesProtected, andTinHardDelete<T>refers to that subject.- A rule with
forbid finalbinds exactly one subject. With nowhenclause it is reported asinvalid rulewith the reasonfinal effect forbids require exactly one when subject; with two subjects, the reason isfinal effect forbids may bind only one subject, but found T, U. - Several
whenclauses on the same subject must all hold. - The condition trait declares no type parameters, or exactly one
Resource-bound parameter. Any other shape makes the rule aninvalid rule. - A plain
forbidin a rule, like one in a trait, is not enforced.
A function in this model that emits HardDelete<Revision> fails with forbidden effect. The message names the condition trait (Revision has trait Protected. Protected forbids final HardDelete<Revision>.), and the last caused by: line names the rule: rule protected_revisions_are_not_deleted forbids final HardDelete<T>.
Nothing waives a final forbid
Section titled “Nothing waives a final forbid”A final forbid, whether it comes from a trait or a rule, stays in force whatever else the model declares. A matching grants line, a rationale or memory, a reevaluation, an attestation, and an import all leave it in force. Permitting the effect takes a change to a claim: removing the trait from the resource, removing the forbid from the trait or rule, or shadowing the trait with a same-named declaration (see Prelude traits and Domain Packs). An effect entry rewritten so that it no longer matches, by renaming the effect or dropping its <Resource> target, also passes; see Check order. Each of these changes appears in the .shape diff.
Components, ownership, and grants
Section titled “Components, ownership, and grants”A component is the authority boundary. It owns resources, grants effects, and contains function summaries, and its body holds only owns, grants, and fn members, in any order. Traits after the component name are shape traits (see Design Memory). A structural link such as a call between components is a top-level relation, never a component member; see Relations and Graph Rules.
owns Revisionclaims ownership, and the checker only checks thatRevisionis a declared resource (unknown resourceotherwise). Ownership does not gate grants or effects, several components may own the same resource, andownsadds no edge to the relation graph. It says nothing about runtime allocation.grants Append<Revision>permits every function in the component to emit that effect. A grant matches on the exact effect name and the resolved resource, and it covers only its own component.
Function summaries
Section titled “Function summaries”A fn member summarizes one function. Its members appear in this fixed order, and any other order is a parse error:
| Position | Member | Notes |
|---|---|---|
| 1 | : Trait, ... | Optional shape traits; see Design Memory. |
| 2 | source tag("path#symbol") | Optional, at most one; see Source and evidence. |
| 3 | description "..." or description required "..." | Optional. With required, an empty description gives missing required description. |
| 4 | unsafe | Optional; see Unsafe functions. |
| 5 | effects unknown or effects complete { ... } | Required. |
| 6 | requires Capability, reason "...", expires "..." | Optional, in any order. |
Unknown and complete effects
Section titled “Unknown and complete effects”| Summary | Claim | Result |
|---|---|---|
effects unknown | The function’s effects are not yet known. | In an authored module, error: unknown effects (exit 1). |
effects complete { } | The function emits no effects. | Passes, because there is nothing to check. |
effects complete { Append<Revision> ... } | The function emits exactly these effects. | Each entry goes through the check order. |
An empty effects complete { } is a claim, not a placeholder: it asserts that the function has no effects. While the effects are not known, effects unknown is the accurate summary, and shp check keeps the gap visible by rejecting it. Two exceptions apply:
shp check --allow-unknown-effectsdowngrades only unknown effects to warnings. It is for local drafting, never for CI; see CLI Reference.- A module named
shape.generated.astorshape.generated.ast.*whose file is undershape/generated/ast/may keepeffects unknownwith no diagnostic. Both conditions are required; see Generate Drafts from Source.
A constraint that the team knows about but cannot fully explain yet belongs in design memory, not in effects unknown; see Design Memory.
Unsafe functions
Section titled “Unsafe functions”module history
resource Revision : AppendOnly
component RevisionLog { owns Revision grants Update<Revision> fn backfillRevisions source ts("src/history/backfill.ts#backfillRevisions") unsafe effects complete { Update<Revision> } expires "2026-12-31" reason "One-off backfill for the revision schema migration." requires MigrationWindow}A function marked unsafe must also declare a non-empty reason, a non-empty expires, and at least one requires term. When any is missing, shp check reports unsafe effects missing policy metadata and lists what is missing (Missing: reason, expires, required capability.). unsafe changes nothing else: the forbid and grant checks still apply, expires is not read as a date, and requires terms are recorded but not resolved.
Source and evidence
Section titled “Source and evidence”A function’s source names the code that implements the function, and each effect entry’s evidence names the code that performs that effect. Both are optional and written tag("path#symbol"):
- The tag is any identifier, such as
ts,rust,test, ormd, and is never validated. shp checknever opens the path and does not validate the anchor.- A function has at most one
source, and each effect entry at most oneevidence. - The
forbidden effectdiagnostic prints the offending entry’sevidence:line.
A #symbol anchor survives unrelated line movement, so it is the preferred form. A file-only reference suits code with no stable named symbol. Line numbers go stale and should be avoided.
Coverage reads these references. It drops the #anchor and any :line or :line-line suffix, compares the path with the changed-file list, and counts a reference only when its own .shape file is also in that list; see Keep the Model Current. Coverage verifies that the model changed alongside the code, not that the reference describes it, so evidence pointed at an unrelated file satisfies coverage while misleading reviewers.
shp analyze can compare obvious destructive operations in source with the declared effects. Its warnings are advisory; see Analyzer Hints.
Check order
Section titled “Check order”For each function, shp check applies these checks in order:
effects unknownin an authored module giveserror: unknown effects.- A targeted effect that matches a final forbid on its target resource gives
error: forbidden effect. The grant check is skipped for that entry, so nomissing grantis reported. - Any other targeted effect that the component does not grant gives
error: missing grant.
An effect without a <Resource> target skips both the forbid check and the grant check, so HardDelete written without a target passes even when every resource is AppendOnly. This includes a trait pattern written without a target: forbid final Update forbids Update<Revision>, not a bare Update. A named target must be a declared resource; otherwise unknown resource is reported, alongside missing grant when the effect is not granted. Effect names are free identifiers matched literally against grants and forbids. The prelude names (Read, Append, Update, Redact, LogicalDelete, HardDelete, Truncate, DropStorage, Export, Import) are editor completions, not a closed set.
This model exercises each step:
module history
resource Revision : AppendOnly
component RevisionLog { owns Revision grants Append<Revision> fn importLegacyRevisions source ts("src/history/import.ts#importLegacyRevisions") effects unknown fn purgeOldRevisions source ts("src/history/purge.ts#purgeOldRevisions") effects complete { HardDelete<Revision> evidence ts("src/history/purge.ts#purgeOldRevisions") } fn redactRevision source ts("src/history/redact.ts#redactRevision") effects complete { Notify Redact<Revision> }}shp check exits 1:
error: forbidden effect
RevisionLog.purgeOldRevisions emits HardDelete<Revision>.Revision has trait AppendOnly.AppendOnly forbids final HardDelete<Revision>.evidence: ts("src/history/purge.ts#purgeOldRevisions")
caused by: - shape/history.shape: effect RevisionLog.purgeOldRevisions emits HardDelete<Revision> - shape/history.shape: resource Revision : AppendOnly - standard prelude: trait AppendOnly forbids final HardDelete<T>
error: missing grant
RevisionLog.redactRevision emits Redact<Revision>.RevisionLog does not grant Redact<Revision>.
caused by: - shape/history.shape: effect RevisionLog.redactRevision emits Redact<Revision> - shape/history.shape: component RevisionLog
error: unknown effects
RevisionLog.importLegacyRevisions declares effects unknown.
caused by: - shape/history.shape: fn RevisionLog.importLegacyRevisionsHardDelete<Revision> is not granted, yet only forbidden effect appears for it. Adding grants HardDelete<Revision> to RevisionLog leaves this output unchanged. Notify has no target, so neither check applies to it. The diagnostics are sorted by kind and then by text, not by check order; see Diagnostics.