Skip to content

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.

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.shape

The 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>

Vendoring installs a pack, and importing only references it:

  • Default discovery loads every file under shape/, including shape/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 under shape/, and its rules run without an import.
  • import domain.history.v1 lets a module refer to that module’s declarations by bare name. A qualified reference such as domain.history.v1::DurableHistory works 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 AppendOnly resolves 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 no module line, 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 : DurableHistory

Discovery 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.

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.

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:

  1. Replace the vendored directory with one exact, reviewed revision.
  2. Review the .shape diff for weakened traits, widened effects, and removed final forbids.
  3. Update the imports when the pack changes its module version.
  4. Run shp fmt --check, shp check, and the changed-file gate from Run Shape in CI. Default shp fmt discovery includes vendored files, and the formatter rebuilds each file and drops comments. A pack that is not in canonical form therefore fails shp fmt --check, and shp fmt rewrites it, changing the pinned bytes.