This guide covers work on the Shape repository itself: the checker, the shp
CLI, the docs site, the agent skills, and the release tooling. To use Shape in
another repository, install the released shp binary
(currently 0.10.0 / v0.10.0) as the
Quickstart describes;
that needs none of this workspace.
- Bun. The root
package.jsonrecords the expected version inpackageManager, andbun.lockis the lockfile. Use that exact Bun version locally;oven-sh/setup-bunreads the same field in CI. - Node 24 or later on
PATH.bun run langium:generateandbun run docs:checkinvoke tooling through Node, and CI pins Node 24. The repository ships an.nvmrc, sonvm useselects the right version. - Git. The changed-file and generated-AST scripts call it.
- For release builds (
bun run build:release):zstd, without which the builder exits, andsha256sum, which writeschecksums.txt.
The semantic-kernel prototype also needs Rust through rustup, which reads the
pinned rust-toolchain.toml; see
experiments/semantic-kernel/README.md.
bun install --frozen-lockfile
bun run langium:generatelangium:generate regenerates the parser artifacts under
packages/shp-checker/src/language/generated from the grammar. The artifacts are
committed, and CI's Codegen job fails when regenerating them produces a diff.
Find your change in What to update per change type
and read the shape/*.shape claims whose source, evidence, or
implementation paths cover the files you will touch. Reuse existing abstractions. The TypeScript
config is strict; do not cast to any to silence an error.
Checks catch some breaches of these rules but cannot judge intent, so keep to them even when every check passes:
- Treat
.shapefiles as code, not prose: every claim changes what the checker accepts. The files undershape/are the guide to syntax and modelling style. - Never write an empty
effects completefor effects you have not established.effects unknownrecords the gap while drafting (shp check --allow-unknown-effects), but the gate is strict, so establish the real effects before the pull request. - A
forbid finalis final: never add a way for rationale, memory, reevaluations, or grants to waive it. - Diagnostics are a product surface: each says what failed, why, and which declaration or evidence caused it. Keep diagnostics, formatting, graph output, and witnesses deterministic so review and CI can compare them.
- Never loosen a rule, trait, memory guard, or governed path to make a check
pass. Change the code instead, or stop and ask the maintainer. Write a
## Rule looseningreason only for a loosening the maintainer asked for, and link to where they asked; see Rule loosening. - Before editing the
sourcefile of a guarded function (one amemoryprotects), runbun shp obligations,bun shp memory, andbun shp explain Component.fn. Keep the behaviour the memory protects, or check your change against the memory's summary and then add areevaluationwritten for this change; see The Shape gate. - Model structural dependencies as top-level
relationdeclarations, preferring prelude kinds such ascalls,callbacks,provides, andcoordinated_call. Before relation-heavy edits, runbun shp graph stats;bun shp graph show SYMBOL --kind KINDshows one symbol's relations.
| Path | Contents |
|---|---|
packages/shp-checker |
Parser, formatter, fact lowering, semantic checker, graph/explain/memory helpers, authoring helpers, editor helpers, and analyzer hints. The checker lives under src/checker/ (rules/, lowering/, diagnostics.ts); src/checker.ts re-exports it. Behavioural tests live in src/behavioural/. |
packages/shp-checker/src/language/shape.langium |
The Langium grammar. |
packages/shp-checker/src/language/generated |
Generated Langium artifacts. Regenerate them with bun run langium:generate; never hand-edit them. |
packages/shp-cli |
The shp command-line wrapper around @shape/shp-checker. Each command lives in src/commands/<command>/ (command.ts for flags, impl.ts for behaviour) and is registered in src/app.ts. The language server is in src/lsp/. |
shape/ |
Shape's own model of the language, checker, tooling, runtime, and delivery pipeline: checker.shape, delivery.shape, incremental-checker.shape, language.shape, runtime.shape, and tooling.shape. |
shape/generated/ast/ |
Committed generated AST context and its manifest.json (see Generated AST context). |
fixtures/pass, fixtures/fail |
Focused semantic examples used by tests, one directory per case. |
fixtures/changed, fixtures/diffs, fixtures/projects, fixtures/source, fixtures/skills |
Changed-file lists, authoring diffs, multi-file projects, analyzer and AST source samples, and the skill release cases (fixtures/skills/cases.json). |
fixtures/loosening |
Before/after model pairs for shp check --check-loosening: base/ and head/ hold the Shape files at repository paths, with an optional changed-files.txt (not changed.txt, which .gitignore excludes). |
docs-site/ |
The Astro/Starlight documentation site: pages in src/content/docs/, diagrams in src/assets/diagrams/, Shape syntax highlighting in src/syntax/, and the Shape-fence verifier in scripts/verify-shape-blocks.ts. |
docs/releases/ |
Release notes, one vX.Y.Z.md per release. |
plugins/shapelang/skills |
Agent skills for using Shape in other repositories. Keep each new skill in plugins/shapelang/skills/<skill-name>/SKILL.md so both plugin manifests expose it. |
plugins/shapelang/.codex-plugin/plugin.json, plugins/shapelang/.claude-plugin/plugin.json |
Plugin manifests that publish the bundled skills to Codex and Claude Code. |
.agents/plugins/marketplace.json, .claude-plugin/marketplace.json |
Marketplace indexes that expose the local Shape plugin. |
experiments/semantic-kernel |
An isolated Rust/WebAssembly prototype. The checker, CLI, and release archives do not depend on it. |
scripts/ |
Repository scripts: release building, smoke tests, and canaries (build-release-assets.sh, smoke-release-binary.sh, run-release-canaries.ts); release gates (check-release-metadata.ts, check-release-approval.ts); check-skills.ts; generate-ast-shapes.ts; and write-changed-files.sh, which writes changed.txt and changed-base.txt. |
.github/ |
Workflows (shape.yml, docs-pages.yml, release-candidate.yml, release.yml), the claude-skill-review composite action, the Claude job runner in scripts/, its prompts in prompts/, and result schemas in shape-contract/schemas/. |
action.yml, install.sh, install.ps1 |
The GitHub setup action and the installers for released shp binaries. |
AGENTS.md |
A short statement of the project's purpose that coding agents load automatically, linking to this file and RELEASING.md. Working rules belong in this file, not there. CLAUDE.md is a symlink to it. |
DESIGN.md, RELEASING.md |
Diagram and visual rules; the release procedure. |
.research/ |
Ignored local research material. Nothing tracked may depend on it; when research drives a decision, record the conclusion in tracked docs, code comments, Shape memory or reevaluations, or the pull request text. |
Run these from the repository root before opening a pull request:
bun run changed-files
bun run format:check
bun run lint
bun run skills:check
bun test
bun run typecheck
bun run shape:ci
bun run shape:loosening
bun run docs:check| Command | What it checks | CI job |
|---|---|---|
bun run changed-files |
Writes changed.txt, the changed-file list that shape:ci reads, and changed-base.txt, the commit it was diffed against. |
Shape |
bun run format:check |
oxfmt --check ., then shp fmt --check on every .shape file in the repository, fixtures included, except under ./node_modules/ and ./.research/. |
Format |
bun run lint |
oxlint --deny-warnings . |
Lint |
bun run skills:check |
Lints the shipped skill corpus (scripts/check-skills.ts). |
Lint |
bun test |
Every workspace test. Behavioural tests follow packages/shp-checker/TESTING.md. | Test |
bun run typecheck |
tsgo -p tsconfig.json --noEmit |
Typecheck |
bun run shape:ci |
The Shape gate, described below. | Shape |
bun run shape:loosening |
shp check --check-loosening against changed-base.txt: fails when an edit to a rule, trait, memory, or governed path is what lets the change pass. See Rule loosening. |
Rule Loosening |
bun run docs:check |
astro check, the Shape-fence verifier, and the static site build. |
Docs |
These commands cover the CI jobs that you reproduce locally. The other jobs are listed in CI in this repository and need no local run, with two exceptions:
-
After a grammar change, run
bun run langium:generateand commit the output, because Codegen fails on any diff underpackages/shp-checker/src/language/generated. -
After a release or install change, run the Build job's check on a Linux x64 host, with
X.Y.Zset to the version inpackages/shp-cli/package.json:bun run build:release scripts/smoke-release-binary.sh --expected-version X.Y.Z dist/release/shp-linux-x64.tar.gz
bun run shape:ci is the Shape gate that CI runs. Run bun run changed-files
first: it writes changed.txt and changed-base.txt, which the gate reads. The gate stops at the first
failing step:
bun run ast:checkchecks that the generated AST context is fresh.bun run format:shape:checkruns the sameshp fmt --checkpass over every.shapefile thatformat:checkruns.bun shp check --changed-files changed.txt --base-ref "$(cat changed-base.txt)" --check-cited-pathsruns the semantic checks, coverage, and docs bindings, counts only attestations written for this change, and fails if the model cites a file that no longer exists. Docs pages are mapped toDocsSitewithout a coverage obligation, so a docs-only edit needs no attestation; the cited-path check catches a cited page being renamed or deleted.bun shp obligationsandbun shp memoryprint open obligations and design memory.
changed.txt lists the files changed against a base, plus unstaged, staged, and
untracked files. In CI the base is the pull request's base branch or, on a
push, the commit the branch pointed at before the push. Locally it is the merge base with origin/$BASE_REF
when BASE_REF is set and that ref exists, otherwise with origin/HEAD,
origin/main, or origin/master. On a branch stacked on another branch, run
BASE_REF=<parent branch> bun run changed-files, with the parent pushed and
fetched, and check that the printed base is the parent.
The gate fails in two common cases:
- Coverage, reported as
error: governed source changed without current Shape update. A path is governed when animplementationinshape/*.shapematches it and declareson_change require shape_update. Update the Shape claims in the same change: a function'ssource, or anevidenceon one of its effect entries, must name the changed path exactly (an#anchoror:linesuffix is ignored), in a.shapefile that is itself inchanged.txt. If the architecture contract did not change, add a narrowattest no_shape_changewhosesourcenames the exact path and whosereasonis non-empty. - Docs bindings, reported as
error: bound docs change missing. Abinding(for exampleCliDocsinshape/delivery.shape) couples source and model files to docs. When awhen_changedpath changes, change at least one of the binding'srequire_changedpaths. Otherwise each triggering path needs a current attestation of a kind the binding'sallow attestlists; every binding in this repository allowsdocs_not_needed.
The checker treats every source or evidence ref in a .shape file listed in
changed.txt as current, including old ones, so when you touch a file that
holds many refs, check which of them still apply. Attestations are held to the
change itself: shape:ci compares them with the base in changed-base.txt, and
one carried over from an earlier change does not count and is reported as a
stale attestation. Delete those with
bun shp attest prune --base-ref "$(cat changed-base.txt)" in every pull
request that touches shape/. Functions in generated AST modules never count
as a Shape update.
The Keep the Model Current guide explains coverage, bindings, and attestations in full.
Editing the source file of a guarded function also fails the gate, with
error: guarded source changed, until a reevaluation of that guard written
for this change is added. One carried over from an earlier change does not
count.
bun run shape:loosening checks the change against the rules at its base: it
puts back, one at a time, each trait, rule, memory, rationale, implementation,
binding, role, policy, or trait list the change removed or edited, and fails
with error: rule loosening when that brings back an error. Fix the code
instead of the rule where you can. When the loosening is intended, the pull
request body must give every loosening a reason under a ## Rule loosening
heading, one line each that names the loosened declaration, says why, and links
to where the maintainer asked:
## Rule loosening
- `Revision`: loses `AppendOnly` because #140 asks for revisions older than seven years to be purged.The line names the declaration that the report names last (either name of a
renamed resource) and gives at least three words of reason. Text inside an HTML
comment or a collapsed <details> block does not count, so the reason is always
in front of the reviewer.
Shape Contract Guard reads the same section and treats a line that names the loosened symbol and gives a specific reason as support for that loosening. It reads the body as it was when the pull request was opened or last pushed to, so it picks up an edited section on the next push.
A semantic change is complete only when the grammar, generated artifacts,
checker rules, fixtures, docs, and Shape model agree. Docs paths below are
relative to docs-site/src/content/docs/.
| Change | Code and tests | Docs and Shape model |
|---|---|---|
| Grammar or parser | packages/shp-checker/src/language/shape.langium; the regenerated files under language/generated; parser.ts; formatter.ts, which rebuilds files from the AST; the highlighter in docs-site/src/syntax/shape-language.mjs; parser, formatter, and lowering tests. |
reference/language-syntax.md and inside-shape/langium-grammar.md (the GrammarDocs binding); shape/language.shape. |
| Checker rule | packages/shp-checker/src/checker/rules/*.ts, ordered in checker/rules.ts; lowering under checker/lowering/; rendered output in checker/diagnostics.ts; cases under fixtures/pass and fixtures/fail; src/checker*.test.ts and src/behavioural/. |
The concept or reference page that teaches the behaviour; reference/diagnostics.md for a new or changed diagnostic; inside-shape/rule-evaluation.md; shape/checker.shape. |
| CLI | packages/shp-cli/src/commands/<command>/, and src/app.ts for a new command; src/index.test.ts and src/cli-contract.test.ts. |
reference/cli.md and guides/ci.md (a change to either satisfies the CliDocs binding), and README.md where it describes the changed behaviour. shape/tooling.shape. |
| Docs | The page; for a new page, the sidebar in docs-site/astro.config.mjs. |
Nothing in the model: DocsContent in shape/delivery.shape maps pages to DocsSite without on_change. When a renamed or deleted page is cited as source or evidence, update the citation, or --check-cited-paths fails. |
| Release or install | install.sh, install.ps1, action.yml, scripts/build-release-assets.sh, scripts/smoke-release-binary.sh, .github/workflows/release.yml, and .github/workflows/release-candidate.yml. |
The README Quick Start snippets and learn/quickstart.md; shape/delivery.shape; RELEASING.md. |
A new platform target starts in
packages/shp-checker/src/tree-sitter-native-targets.ts, which the release
builder, runtime parser selection, and shp update read. install.sh,
install.ps1, and action.yml map platforms to assets separately, and
release.yml and release-candidate.yml list the archives by name, so add the
target in each of them too.
bun run docs:dev # dev server
bun run docs:build # static build into docs-site/dist
bun run docs:preview # serve the static buildPages live under docs-site/src/content/docs/. Each has frontmatter with a
title and a one-sentence description, and the sidebar in
docs-site/astro.config.mjs lists every page explicitly.
Shape fences. bun run docs:check parses every ```shape fence in the
.md, .mdx, and .mdoc files under docs-site/src/content/docs with the repo
parser, and fails on any fence that does not parse. It skips a fence only when
its info string contains no-verify, so mark an intentional fragment:
```shape no-verify
fn fragmentOnly
effects unknown
```The fence still renders for readers. The verifier matches only fences that open
at the start of a line, so an indented fence, such as one inside a list item, is
not parsed. It checks syntax only and never runs shp check: run each example
you present as passing or failing, and copy its real output.
Links. The site is served under the base /shapelang/ with trailing
slashes. Link between pages with root-absolute URLs that include the base and
the trailing slash, for example [CLI Reference](/shapelang/reference/cli/).
Relative links break on the built site. Repository-root Markdown files link to
the site with full https://timbrinded.github.io/shapelang/... URLs.
Diagrams. Diagrams are hand-authored SVG files in
docs-site/src/assets/diagrams/ that follow DESIGN.md. Embed one
with Markdown image syntax and a relative asset path (from a page one directory
deep, ../../../assets/diagrams/NAME.svg). The alt text states the diagram's
assertion in one sentence.
Release pins. Some pages carry literal version snippets that
bun run release:metadata and bun test check. The list is
releaseVersionPins in scripts/check-release-metadata.ts. Keep those snippets
verbatim; if one has to move, update the list in the same change.
Deployment. .github/workflows/docs-pages.yml runs bun run docs:check and
deploys docs-site/dist to https://timbrinded.github.io/shapelang/ on every
push to master, or when run manually. The repository's Pages source must be
set to GitHub Actions.
This repository commits generated AST context under shape/generated/ast/,
with a manifest.json that records each generated module and its source file.
bun run ast:generate # regenerate the committed files
bun run ast:check # exit 1 if any manifest-owned file is stale, missing, or extraBoth scripts run shp ast source --out-dir shape/generated/ast over one source
set: tracked and untracked, non-ignored files
(git ls-files --cached --others --exclude-standard) with the extensions .ts,
.tsx, .mts, .cts, .js, .jsx, .mjs, .cjs, .rs, .go, .py, or
.swift. The set excludes any node_modules/ or dist/ directory,
docs-site/.astro/,
packages/shp-checker/src/language/generated/, and shape/generated/, and it
skips files that Git lists but the working tree no longer has.
Two source paths can normalize to the same module name, for example Python
__init__.py and init.py. Generation processes sources in codepoint order of
their repository-relative paths: the first keeps the module name, and each later
one gets a _g<8 hex> suffix hashed from its path, so the result is
deterministic. Two source paths that map to the same output file, such as
foo.ts and foo.tsx, fail with
error: generated AST output path collision and exit code 2 before any file is
written.
CI's Codegen and Shape jobs, bun run shape:ci, and both release workflows run
ast:check. A source change that stales the context therefore fails until you
run bun run ast:generate and commit the result.
.github/workflows/shape.yml (workflow name CI) runs on every pull request and
on every push to master:
| Job | Runs |
|---|---|
| Codegen | langium:generate with a diff check, then ast:check |
| Format | format:check |
| Lint | lint, then skills:check |
| Typecheck | typecheck |
| Test | bun test |
| Semantic Kernel Prototype | kernel:check, kernel:build, kernel:e2e; uploads the browser WASM build |
| Shape | changed-files, then shape:ci |
| Docs | docs:check |
| Build | build:release, then smoke-release-binary.sh on the Linux x64 archive |
| Links | lychee over the repository, including #fragment anchors; docs-site, published-site, and master links resolve against the checkout |
| Typos | crate-ci/typos |
On pull requests, three Claude-powered jobs also run. One script drives them all:
.github/scripts/run-claude-skill.mjs, invoked through the
.github/actions/claude-skill-review composite action. Each job first looks for
an ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN secret and skips cleanly when
neither is set. The script then runs twice:
- A
--prefilterpass either finishes the job deterministically or emits the prompt andclaude_argsforanthropics/claude-code-action, which runs the model (Sonnet 5.5 by default) with structured output. Review, contract guard, index coverage, and release evaluation all inherit this shared default. - A gate pass validates the result against the job's JSON schema under
.github/shape-contract/schemas/, writes the job summary, and applies the job's pass/fail policy. When a proxy gateway drops the structured output, the gate recovers the result from the action's execution log.
| Job | Checks | Fails when |
|---|---|---|
Shape Claude Review (shape-claude-review) |
Source-to-model drift, following .github/prompts/shape-contract-review.md. |
The status is not pass, or a pass carries findings. |
Shape Contract Guard (shape-guard) |
The authored .shape diff against the pull request base, following plugins/shapelang/skills/shape-contract-guard/SKILL.md and .github/prompts/shape-guard.md: removed final forbids, weakened traits, widened grants or effects, weakened relations or coverage, and weak attestations. The prompt also quotes the visible ## Rule loosening section of the pull request body as decision evidence. The prefilter passes without calling Claude when no authored .shape file (outside shape/generated/) changed. |
The review errors, a pass carries findings, or a finding is high-impact and suspicious. Other findings, including high-impact ones marked supported, are advisory. |
Shape Index Coverage (shape-index) |
Follows plugins/shapelang/skills/shape-index/SKILL.md and .github/prompts/shape-index.md. The prefilter lists changed source files that no source or evidence ref or implementation glob in shape/*.shape covers, passes without Claude when there are none, and otherwise asks Claude to judge that remainder for architecture-significant subsystems without coverage. |
The review errors, a pass carries gaps, or it finds gaps while the Actions variable SHAPE_INDEX_STRICT is true. Otherwise gaps appear in the job summary but do not block. |
.github/workflows/shape-loosening.yml (workflow name Rule loosening) runs a
Rule Loosening job on pull requests, including when the pull request body is
edited. It runs shape:loosening, then
.github/scripts/check-loosening-rationale.mjs, which fails when a reported
loosening has no reason in the body's visible ## Rule loosening section and
reports each one as an error annotation. Other errors are the Shape job's to
report.
After these jobs, Shape PR Summary Comment (shape-pr-comment) upserts one
comment on the pull request. It reports the results of Shape, Shape Claude
Review, Shape Contract Guard, and Shape Index Coverage for the latest commit, and
links to the workflow run. It runs only for pull requests from branches of this
repository.
Releases add the Release Candidate: Skills workflow, a manual approval, and the
tag-triggered Release workflow; RELEASING.md describes them.