Keep the Model Current
When application code changes, the Shape model should change with it. shp checks this against a changed-file list. Coverage asks whether each changed file that the model governs has a current Shape update or attestation. Bindings ask whether a paired review surface, such as a docs page, changed alongside a trigger path.
Three checks
Section titled “Three checks”| Check | Question | Run by |
|---|---|---|
| Conformance (semantic checks) | Is the Shape model coherent? | shp check, shp coverage, and shp check --changed-files |
| Coverage | Did each changed governed file get a current Shape update or attestation? | shp coverage --changed-files F and shp check --changed-files F |
| Bindings | When a trigger path changed, did a required path change too, or is there a current allowed attestation? | shp check --changed-files F only |
shp check --changed-files changed.txt runs all three checks. Use it locally and as the single gate in CI.
- Without a changed-file list, or with an empty one, coverage, bindings, and guards on edited function sources check nothing (see Guarded targets).
shp coverage --changed-files Fruns conformance and coverage but not bindings. It has no--allow-unknown-effectsor freshness flags, so anyeffects unknownin an authored module fails it.
Flags and exit codes for both commands are in the CLI Reference.
The changed-file list
Section titled “The changed-file list”The changed-file list is a text file, conventionally changed.txt, with one path per line. Paths are relative to the repository root, and shp takes the working directory as the repository root. Blank lines and surrounding whitespace are ignored, a leading ./ is dropped, and absolute paths are made relative to the working directory.
The list must include the .shape files that the change edits. That is how coverage tells which .shape files this change touched: a reference counts only when its file is in the list, and so does an attestation when the check has no base model.
Build the list the same way locally and in CI: from a diff against the base branch. A diff of commits misses uncommitted and untracked files, so a local run adds them:
{ git diff --name-only origin/main...HEAD git diff --name-only HEAD git ls-files --others --exclude-standard} | sort -u > changed.txtReplace main with your base branch, and add changed.txt to .gitignore so that the list does not name itself. Run Shape in CI has the CI version.
Governed paths
Section titled “Governed paths”An implementation block declares which source paths the model governs:
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") }}
implementation RevisionLogImpl { paths { "src/history/**/*.ts" } conforms_to RevisionLog on_change require shape_update}| Field | Meaning |
|---|---|
paths { ... } | Globs matched against each whole path in the changed-file list. * matches within one path segment, ** matches across segments, and ? matches one character. There is no brace or bracket expansion, so src/**/*.{ts,tsx} matches neither .ts nor .tsx files; write one glob per extension. |
conforms_to | The component that the paths implement. The checker only verifies that the name resolves (otherwise error: unknown component); coverage never reads it. |
on_change require shape_update | Turns on coverage for the paths. shape_update is the only supported value; any other, such as the typo shape_updates, fails the check with error: invalid implementation instead of silently disabling coverage. |
A changed file is governed when it matches a paths glob of an implementation that declares on_change require shape_update. Changed .shape files are never governed, even when a glob matches them.
Coverage cannot see a path that no such implementation governs: that path can change freely without a Shape update. Govern every production path that CI should hold to the model. Keep the globs tight enough that tests, fixtures, and generated code are not forced into architecture review.
What counts as covered and current
Section titled “What counts as covered and current”A .shape file is current when it is in the changed-file list. A changed governed file is covered when either of these exists:
- A Shape update. In a current
.shapefile, a function’ssource, or theevidenceon an entry in itseffects completesummary, names the changed file. - A current attestation. An
attest no_shape_changehas asourcethat names the changed file and areasonthat is not empty or whitespace. With a base model (--base-refor--base-model), it is current only when its kind, path, and reason are new relative to that base. Without one, it is current when its.shapefile is current.
Matching compares exact paths after dropping any #anchor and any :line or :line-line suffix. So ts("src/history/export.ts#exportRevisions") matches src/history/export.ts, but ts("src/history") matches nothing below that directory. The tag (ts, md, and so on) is any identifier and plays no part in the match. Other details:
- A reference from any component counts, because coverage never consults
conforms_to. - A function with
effects unknownstill contributes itssource. - Functions in generated-AST modules do not count. See Generate Drafts from Source.
- Other references do not count:
evidenceandobservedlines in design memory (rationale,memory,reevaluation), and thesourceof aneffect candidate.
For Shape updates, currency is per file: the checker does not know which references were written for this change, so every reference in a .shape file that is in the list counts, including old ones. Two consequences follow:
- A reference in a file that the change does not touch never covers the change, however well it matches.
- An old reference in a file that the change does touch covers its path again. If a change adds an attestation to
shape/history.shapeforsrc/history/reporting.ts, the existingsource ts("src/history/log.ts#appendRevision")also covers any change tosrc/history/log.tsin the same change set.
Reviewers therefore need to check which references in a touched .shape file still describe the change.
Attestations can be held to the change itself. Give the check the commit the changed-file list was diffed against:
shp check --changed-files changed.txt --base-ref origin/main--base-ref reads the Shape model at the merge base of origin/main and HEAD from git, and an attestation counts only when no attestation with the same kind, path, and reason exists there. One carried over unchanged from an earlier change does not count, even if its file was edited for another reason, and the check reports it as a stale attestation warning. Without a base, attestations follow the per-file rule too, so an unrelated edit to their file revives every attestation in it; pass a base in CI.
Coverage verifies co-change, not fidelity. It confirms that a current .shape file names each changed governed file. It does not check that the claims describe what the code does; that judgement belongs to the reviewer, optionally helped by the Claude review job.
Update the model when code changes
Section titled “Update the model when code changes”- Make the edit that fits the case below.
- Write
changed.txtas described above. Do this after the edit, so that the list names the.shapefile beside the code. - Run
shp fmt --checkandshp check --changed-files changed.txt. The formatter drops comments, so a.shapefile that contains comments never passesshp fmt --check; see the CLI Reference.
If shp explain TARGET shows a guard on a claim you are changing, read Guarded targets first. If the check reports bound docs change missing, see Bindings.
The architecture changed
Section titled “The architecture changed”Find the claims that describe the changed behaviour, such as a component and its functions, a resource, an implementation, a relation, a rule, or design memory, and edit them in place so that they match the code. Cite the changed file in a function’s source or in an effect entry’s evidence; only those references make the update count for coverage.
In this example, a change adds src/history/export.ts, which reads revisions. Add the function to RevisionLog in shape/history.shape:
fn exportRevisions source ts("src/history/export.ts#exportRevisions") effects complete { Read<Revision> evidence ts("src/history/export.ts#exportRevisions") }changed.txt then contains both files:
shape/history.shapesrc/history/export.ts$ shp check --changed-files changed.txtShape check passed.A covered change still has to pass conformance. If the new claim were HardDelete<Revision>, coverage would accept it, but the forbid final on AppendOnly would reject it; the Quickstart walks through that failure.
Deleting a governed file needs one more step. The deleted path still appears in the diff, and coverage has no notion of removal. When the change also removes the function whose source named the file, nothing in the model names the path any more, so coverage fails for it. The only way to cover it today is an attest no_shape_change that names the deleted path and gives a reason recording the removal; the next case shows the form.
The architecture is unchanged
Section titled “The architecture is unchanged”When a governed file changes without changing any claim, such as a formatting-only edit, add an attestation to a .shape file:
attest no_shape_change { source ts("src/history/reporting.ts") reason "Formatting-only change; no resource access or effect changed."}With shape/history.shape and src/history/reporting.ts in changed.txt, shp check --changed-files changed.txt prints Shape check passed.
An attestation declares exactly one source and one reason, so use one attestation per changed file. Write it for this change: with a base model, an older attestation for the same path does not count, so give the new one its own reason and delete the stale one. Use no_shape_change only when the contract is truly unchanged: coverage accepts the attestation whether or not its reason is true, so it can hide real model drift from everyone but the reviewer. An attestation never waives a forbid final; nothing does. See the Effect Model.
The effects are not known yet
Section titled “The effects are not known yet”When a function’s effects are not yet known, write effects unknown rather than guessing or writing an empty effects complete {}:
fn importLegacyRevisions source ts("src/history/import.ts#importLegacyRevisions") effects unknownA draft that is not ready for strict checks stays outside shape/, where default discovery does not load it. Check it by naming the file, here draft.shape, a copy of the history module with this function added. Naming files replaces discovery, so the draft must declare everything it references:
$ shp check --allow-unknown-effects draft.shapewarning: unknown effects
RevisionLog.importLegacyRevisions declares effects unknown.
caused by: - draft.shape: fn RevisionLog.importLegacyRevisions
Shape check passed with warnings.The exit code is 0. --allow-unknown-effects turns only unknown effects into warnings, and every other check still blocks; the flag is described in the CLI Reference.
Once the function moves into shape/history.shape and that file is in the list, its source covers src/history/import.ts, but strict shp check rejects the unknown summary with exit code 1:
$ shp check --changed-files changed.txterror: unknown effects
RevisionLog.importLegacyRevisions declares effects unknown.
caused by: - shape/history.shape: fn RevisionLog.importLegacyRevisionsA strict CI gate therefore rejects the change until every unknown in an authored module is resolved. The Effect Model explains unknown and complete summaries.
When coverage fails
Section titled “When coverage fails”If the change adds src/history/export.ts but no current .shape file names it, coverage fails. With the model from Governed paths unchanged and only src/history/export.ts in changed.txt, the check writes this to stderr and exits 1:
$ shp check --changed-files changed.txterror: governed source changed without current Shape update
Changed file: src/history/export.tsGoverned by: history::RevisionLogImplMatched path: src/history/**/*.tsRequired: update a current .shape file with matching source/evidence, or add a no_shape_change attestation.
caused by: - shape/history.shape: implementation RevisionLogImpl - shape/history.shape: implementation RevisionLogImpl path src/history/**/*.tsThe same diagnostic appears when shape/history.shape has the new function but is missing from changed.txt. Fix it with one of the cases above. shp coverage --changed-files changed.txt reports the same diagnostic.
Bindings
Section titled “Bindings”A binding pairs trigger paths with required paths that must change alongside them, typically documentation:
binding HistoryDocs { when_changed paths { "src/history/**/*.ts" } require_changed paths { "docs/history.md" } allow attest docs_not_needed}A changed path that matches a when_changed glob is a trigger path, and it triggers the binding. One changed path that matches any require_changed glob then satisfies the whole binding. Otherwise, each trigger path needs a current attestation of a kind that the binding lists in allow attest, with the same exact-path, non-empty-reason, and currency rules as coverage. A binding with no allow attest line has no attestation escape. Unlike coverage, bindings do not skip .shape files, so a when_changed glob can name model files.
With this binding in shape/history.shape, the exportRevisions update above now fails with exit code 1, because docs/history.md did not change:
$ shp check --changed-files changed.txterror: bound docs change missing
binding history::HistoryDocs was triggered by src/history/export.ts.Required: change one of docs/history.md, or add attest docs_not_needed.
caused by: - shape/history.shape: binding HistoryDocs - shape/history.shape: binding HistoryDocs when_changed src/history/**/*.tsChanging docs/history.md fixes it. When the docs do not need to change, add the permitted attestation to a .shape file in the list:
attest docs_not_needed { source ts("src/history/export.ts") reason "Export is internal tooling; the history docs describe no export behaviour."}The attestation kind is any identifier, and docs_not_needed is a convention: a binding accepts exactly the kinds its allow attest lines name, and coverage accepts only no_shape_change. Only shp check --changed-files enforces bindings; shp coverage does not.
With a base model, a .shape file that changed only in its attestations does not trigger a binding: its text with attestations removed matches the base version, so no model claim changed. Adding or deleting attestations therefore never demands a docs change.
Prune stale attestations
Section titled “Prune stale attestations”Once a change merges, its attestations have done their job. Later checks with a base model report them as stale attestation warnings, and shp attest prune deletes them:
shp attest prune --base-ref origin/mainPrune removes each attestation whose kind, path, and reason already exist in the base model, including one added inside a change block, together with the whitespace that separates it from its neighbour. It keeps every other byte of the file and the attestations you wrote for the current change. Git history keeps every removed decision. Running it in any change that touches the model keeps attestations from piling up. Flags are in the CLI Reference.
Guarded targets
Section titled “Guarded targets”Some claims carry design-memory guards. A guard fires from a change declaration that modifies or removes the guarded target: modify fn, remove fn, or the component, resource, and relation forms. Editing a guarded declaration in place in the model produces no change event, so its guard does not fire. Editing the guarded code does: with --changed-files, a guard on a function fires when the file in its source is in the list, and it needs a reevaluation written for this change, since one carried over from an earlier change does not count. A fired guard is satisfied by a valid reevaluation, never by an attestation. Before changing a guarded target, run shp obligations for open obligations and shp explain TARGET to see which contexts guard it; shp memory lists recorded memory and rationale but not their guards. Design Memory explains guards, change declarations, and reevaluation.