Domain Packs
A domain pack is a versioned directory of ordinary .shape modules committed under the project’s shape/ directory. Shape has no registry, downloader, manifest, or lockfile for packs: the committed files are the pinned dependency, and the checker treats them like every other file in the Shape model.
Layout
Section titled “Layout”Put each pack under shape/vendor/, so that default discovery (shape/**/*.shape) loads it with the project’s own files:
shape/├── project.shape└── vendor/ └── durable-history/ └── v1/ └── durable-history.shapeThe pack declares a stable, versioned module name:
module domain.history.v1
trait DurableHistory<T: Resource> { forbid final HardDelete<T>}The project imports that module and applies the pack’s trait beside a local one:
module publishing
import domain.history.v1
trait PublishingRetention<T: Resource> { forbid final Truncate<T>}
resource PublishingHistory : DurableHistory, PublishingRetention
component Publisher { owns PublishingHistory grants Append<PublishingHistory> fn recordPublish source ts("src/publishing/history.ts#recordPublish") effects complete { Append<PublishingHistory> evidence ts("src/publishing/history.ts#recordPublish") }}shp check passes, and shp explain PublishingHistory shows each trait under its module name, with the final forbids the two traits derive:
PublishingHistory kind: resource traits: domain.history.v1::DurableHistory publishing::PublishingRetention
final forbidden effects: HardDelete<PublishingHistory> Truncate<PublishingHistory>Installing and importing
Section titled “Installing and importing”Vendoring installs a pack, and importing only references it:
- Default discovery loads every file under
shape/, includingshape/vendor/, whether or not any module imports it. A pack’s resources, components, and rules join the Shape model as soon as its files are undershape/, and its rules run without an import. import domain.history.v1lets a module refer to that module’s declarations by bare name. A qualified reference such asdomain.history.v1::DurableHistoryworks without the import. Importing a module that no file declares is not itself an error.- When two imported modules declare the same name, a bare reference to it is reported as ambiguous (for example
ambiguous trait) and must be qualified. - A bare prelude trait name such as
AppendOnlyresolves to the prelude even when an imported pack declares a trait with that name. Only a declaration in the same module, or in a file with nomoduleline, shadows a prelude trait.
Traits suit policy that each project opts into: the project applies them to its own resources, as in the example above. A pack-level rule instead governs every model that installs the pack. For example, a vendored file containing this rule rejects any cycle of calls relations in the project, and the caused by: block of the resulting forbidden hypercycle cites the vendored file:
module domain.no_cycles.v1
rule no_call_cycles { forbid hypercycle over calls}Explicit file arguments replace discovery. shp check shape/project.shape loads only that file, so the pack’s rules do not run and references to pack declarations fail. For the publishing project, it exits 1:
error: unknown trait
trait DurableHistory is referenced but not declared.
caused by: - shape/project.shape: resource PublishingHistory : DurableHistoryDiscovery is relative to the working directory and does not search parent directories. From a directory with no shape/ subdirectory, shp check loads nothing and passes, so run commands from the project root. See the CLI Reference.
Changing a pack
Section titled “Changing a pack”Shape has no override keyword. A local declaration with the same name as an imported one silently wins: a module that imports domain.history.v1 and declares its own trait DurableHistory gets the local trait, with none of the pack’s forbids and no diagnostic. Local policy therefore needs its own names, applied alongside the pack’s traits as PublishingRetention is above.
To change a pack’s contract, vendor a new reviewed revision under a new module version, such as domain.history.v2, and update the imports. A pack’s final forbids stay final after import; see Effect Model.
Pinning and updates
Section titled “Pinning and updates”Vendor an exact upstream release or commit, and record the upstream source and revision in the vendoring commit or an adjacent README. CI then reads the same bytes as local development. Do not point CI at a floating branch or download a pack during the check: a moving source would make the Shape model differ between runs.
To update a pack:
- Replace the vendored directory with one exact, reviewed revision.
- Review the
.shapediff for weakened traits, widened effects, and removed final forbids. - Update the imports when the pack changes its module version.
- Run
shp fmt --check,shp check, and the changed-file gate from Run Shape in CI. Defaultshp fmtdiscovery includes vendored files, and the formatter rebuilds each file and drops comments. A pack that is not in canonical form therefore failsshp fmt --check, andshp fmtrewrites it, changing the pinned bytes.