spec-driven-docs 0.12.0

Spec-driven documentation: current specs, immutable decision records, and executable gates kept coherent for people and coding agents.
Documentation
# Distribution Specification

<!--TOC-->

- [Purpose](#purpose)
- [Requirements](#requirements)
  - [`distribution:manifest-identifies-every-owned-file` — The manifest identifies every owned file](#distributionmanifest-identifies-every-owned-file--the-manifest-identifies-every-owned-file)
  - [`distribution:initialization-preserves-project-content` — Initialization preserves project content](#distributioninitialization-preserves-project-content--initialization-preserves-project-content)
  - [`distribution:instances-operate-offline` — Instances operate offline](#distributioninstances-operate-offline--instances-operate-offline)
  - [`distribution:upgrade-conflicts-are-atomic` — Upgrade conflicts are atomic](#distributionupgrade-conflicts-are-atomic--upgrade-conflicts-are-atomic)
  - [`distribution:skills-are-part-of-the-payload` — Skills are part of the payload](#distributionskills-are-part-of-the-payload--skills-are-part-of-the-payload)
  - [`distribution:a-skill-has-one-owner` — A skill has one owner](#distributiona-skill-has-one-owner--a-skill-has-one-owner)
  - [`distribution:a-skill-obeys-the-portable-format` — A skill obeys the portable format](#distributiona-skill-obeys-the-portable-format--a-skill-obeys-the-portable-format)
  - [`distribution:a-landing-classifies-its-target-first` — A landing classifies its target first](#distributiona-landing-classifies-its-target-first--a-landing-classifies-its-target-first)
  - [`distribution:a-skill-checks-its-host-before-it-plans` — A skill checks its host before it plans](#distributiona-skill-checks-its-host-before-it-plans--a-skill-checks-its-host-before-it-plans)
  - [`distribution:the-doctor-answers-for-the-installed-skills` — The doctor answers for the installed skills](#distributionthe-doctor-answers-for-the-installed-skills--the-doctor-answers-for-the-installed-skills)
  - [`distribution:a-skill-plans-before-it-acts` — A skill plans before it acts](#distributiona-skill-plans-before-it-acts--a-skill-plans-before-it-acts)
  - [`distribution:skill-install-previews-before-writing` — Skill install previews before writing](#distributionskill-install-previews-before-writing--skill-install-previews-before-writing)
  - [`distribution:a-skill-install-restores-on-failure` — A skill install stops honestly](#distributiona-skill-install-restores-on-failure--a-skill-install-stops-honestly)
  - [`distribution:skill-uninstall-removes-only-what-it-wrote` — Skill uninstall removes only what it wrote](#distributionskill-uninstall-removes-only-what-it-wrote--skill-uninstall-removes-only-what-it-wrote)
  - [`distribution:an-install-sweeps-what-the-payload-dropped` — An install sweeps what the payload dropped](#distributionan-install-sweeps-what-the-payload-dropped--an-install-sweeps-what-the-payload-dropped)
  - [`distribution:user-scope-files-stay-unrecorded` — User-scope files stay unrecorded](#distributionuser-scope-files-stay-unrecorded--user-scope-files-stay-unrecorded)
  - [`distribution:a-skill-package-is-self-contained` — A skill package is self-contained](#distributiona-skill-package-is-self-contained--a-skill-package-is-self-contained)
  - [`distribution:a-user-scope-receipt-is-required-state` — A user-scope receipt is required state](#distributiona-user-scope-receipt-is-required-state--a-user-scope-receipt-is-required-state)
  - [`distribution:the-payload-names-no-other-project` — The payload names no other project](#distributionthe-payload-names-no-other-project--the-payload-names-no-other-project)
  - [`distribution:a-declared-location-is-named-by-its-variable` — A declared location is named by its variable](#distributiona-declared-location-is-named-by-its-variable--a-declared-location-is-named-by-its-variable)
  - [`distribution:the-payload-roots-are-declared-once` — The payload roots are declared once](#distributionthe-payload-roots-are-declared-once--the-payload-roots-are-declared-once)
  - [`distribution:a-seeded-rule-runs-no-canon-command` — A seeded rule runs no canon command](#distributiona-seeded-rule-runs-no-canon-command--a-seeded-rule-runs-no-canon-command)
  - [`distribution:the-declaration-is-seeded-once-and-then-owned` — The declaration is seeded once and then owned](#distributionthe-declaration-is-seeded-once-and-then-owned--the-declaration-is-seeded-once-and-then-owned)

<!--TOC-->

## Purpose

Rules governing installation, ownership classes, offline verification, and upgrades. The distribution is one installed binary, `sdd`, that carries the payload, and every rule here binds whoever authors that binary. No instance adopts this spec: its subject is the installer, so an instance holding these rules holds obligations it cannot violate and verifications it cannot run. What a project owes its own installation is stated in `SPEC-instance.md`. The rules the canon alone runs at release time are stated in `SPEC-release.md`.

## Requirements

### `distribution:manifest-identifies-every-owned-file` — The manifest identifies every owned file

The installer MUST record each installed file with its ownership class, destination, and SHA-256, and the record MUST state which canon version produced it.

#### Scenario: An agent encounters a local edit

- GIVEN an installed file differs from its baseline
- WHEN the verifier reads the manifest
- THEN it distinguishes managed drift from adopted reconciliation

Verify: `cargo nextest run -E 'binary(cmd_verify) + binary(cmd_status)'`

### `distribution:initialization-preserves-project-content` — Initialization preserves project content

When a target is non-empty, the installer MUST preview its changes before writing any file. The preview a landing offers is the plan, and the classification decides which verb serves the target: a settled corpus is a migration, and the landing verb refuses it rather than seeding beside it.

#### Scenario: A repository has a hand-commented hook configuration

- GIVEN comments outside the managed markers
- WHEN initialization inserts its block
- THEN every outside comment remains byte-identical

Verify: `cargo nextest run -E 'binary(cmd_init)'`

### `distribution:instances-operate-offline` — Instances operate offline

The installed binary MUST verify and upgrade an instance without a network or canon checkout, reading the release it carries. Every verb reads that one release, and `SPEC-staging.md` states why the operator selects it rather than a selector. The readers `sdd docs`, `sdd method`, `sdd spec`, and `sdd template` serve the corpus the binary carries and touch no network, and `SPEC-docs-discovery.md` states what describes that corpus.

#### Scenario: The canon repository is unreachable

- GIVEN a fully installed target
- WHEN `sdd verify` and `sdd upgrade` run with no network
- THEN they check hashes, the managed block, rule IDs, and the binary's own version against the manifest, from the payload the binary carries

Verify: `cargo nextest run -E 'binary(cmd_verify) + binary(cmd_upgrade)'`

### `distribution:upgrade-conflicts-are-atomic` — Upgrade conflicts are atomic

If a managed file differs from its installed hash, then the upgrader MUST abort without changing the target. It names every conflict in one pass, and it decides before its first write, which `SPEC-staging.md` states as the rule a landing holds for any destination it cannot account for.

#### Scenario: One managed configuration is edited locally

- GIVEN a valid installed instance with one managed edit
- WHEN an upgrade is requested
- THEN it lists every conflict in one run and changes no target byte

Verify: `cargo nextest run -E 'binary(cmd_upgrade)'`

### `distribution:skills-are-part-of-the-payload` — Skills are part of the payload

The distribution MUST embed every skill authored under `skills/`, so a binary carries the skills of its own version and no instance fetches them.

#### Scenario: A skill is authored under the canon's skills directory

- GIVEN a skill directory added under `skills/`
- WHEN the binary is built
- THEN `sdd skill list` names it and `sdd skill show` prints it byte-identical to the authored file

Verify: `cargo nextest run -E 'binary(cmd_skill)'`

### `distribution:a-skill-has-one-owner` — A skill has one owner

The distribution MUST install every skill at user scope alone. No profile can project a skill into an instance, because an agent resolves a skill by name. A second copy under one name is a second entry offering the same skill.

#### Scenario: A project is initialized inside a home that already carries the skills

- GIVEN a home directory holding the skills and a target repository with no instance
- WHEN `sdd init --apply` runs with either profile
- THEN the target carries no `.claude/skills/` or `.agents/skills/` file and the manifest records none, so each skill resolves to exactly one file

Verify: `cargo nextest run -E 'binary(cmd_init) + binary(cmd_skill)'`

### `distribution:a-skill-obeys-the-portable-format` — A skill obeys the portable format

Every skill MUST carry only the portable Agent Skills frontmatter fields, a `name` matching its directory name, and a body at or below 150 lines.

#### Scenario: A skill gains an agent-specific field

- GIVEN a skill edited to add a vendor-only frontmatter key
- WHEN the canon test suite runs
- THEN the conformance test fails and names the offending field

Verify: `cargo nextest run -E 'binary(canon)'`

### `distribution:a-landing-classifies-its-target-first` — A landing classifies its target first

The landing MUST read a target's classification from the target alone, never from the verb the caller ran, and each front verb MUST refuse a classification it does not serve, naming what was found and what serves it. After the shared pre-flight gate, a skill reads the classification rather than routing by verb. `sdd assess` MUST keep its three-verdict projection: it reports its evidence and exactly one of `brownfield`, `greenfield`, or `needs-decision`, writes nothing, and exits 0.

#### Scenario: A landing verb meets a target it does not serve

- GIVEN a repository holding a populated documentation root and no instance manifest
- WHEN `sdd init --target . --profile codebase --apply` runs
- THEN the command refuses, names the target as a migration, and points at `sdd stage` and the setup skill, so no seed lands beside the corpus

Verify: `cargo nextest run -E 'binary(cmd_assess) + binary(cmd_stage) + binary(canon)'`

### `distribution:a-skill-checks-its-host-before-it-plans` — A skill checks its host before it plans

Every skill MUST direct the agent to run the shared pre-flight gate, which observes the host with `sdd doctor`, before planning, whatever the request's flags. The `--no-plan` flag changes only when the plan gate asks for approval.

#### Scenario: A request says to skip the checks

- GIVEN a request carrying `--no-plan` and an instruction to act immediately
- WHEN the agent follows the skill's opening section
- THEN the pre-flight still runs, because the task's steps have the same dependencies whatever the request says. Only the plan gate's approval turn is skipped

Verify: `cargo nextest run -E 'binary(canon)'`

### `distribution:the-doctor-answers-for-the-installed-skills` — The doctor answers for the installed skills

`sdd doctor` MUST run every cataloged probe and exit 0 whatever they find. Its skill probes MUST compare whole packages, file by file, and MUST pick the remediation by the user-scope receipt: drift the receipt vouches for is a stale install corrected by a plain apply. Drift it cannot account for is the user's own, corrected only with `--force`.

#### Scenario: A home holds a skill an older release installed

- GIVEN an agent root holding a package file whose digest the user-scope receipt vouches for
- WHEN `sdd doctor --json` runs
- THEN the `skill-payload` probe fails naming `sdd skill install --apply` without `--force`, and the exit code is 0

Verify: `cargo nextest run -E 'binary(cmd_doctor)'`

### `distribution:a-skill-plans-before-it-acts` — A skill plans before it acts

Every skill MUST open its body with one section that precedes every other section. That section MUST direct the agent to read the two gates in order before the first action of a task, each named by its path inside the skill's own package: the pre-flight gate first, then the plan gate. It MUST also direct the agent to hold the plan gate's three phases, plan, validate, and execute, for the whole task.

#### Scenario: A skill gains a section above the gate

- GIVEN a skill edited so another section precedes the gate section
- WHEN the canon test suite runs
- THEN the conformance test fails naming the skill, because an agent acts on the first instruction it reads

Verify: `cargo nextest run -E 'binary(canon)'`

### `distribution:skill-install-previews-before-writing` — Skill install previews before writing

When run without `--apply`, `sdd skill install` MUST list every destination and write nothing. Three references decide each one: bytes matching the payload are current, bytes the receipt vouches for are this tool's and are replaced without `--force`, and every other byte is the user's. An apply MUST refuse atomically on the last kind, listing every conflict.

#### Scenario: A home directory already carries an edited skill

- GIVEN `~/.claude/skills/sdd-setup/SKILL.md` with bytes the user wrote
- WHEN `sdd skill install --apply` runs
- THEN it exits 73 listing every conflicting destination, writes no file, and states `--force` as the override

Verify: `cargo nextest run -E 'binary(cmd_skill)'`

### `distribution:a-skill-install-restores-on-failure` — A skill install stops honestly

An apply of `sdd skill install` or `sdd skill uninstall` MUST hold the user-scope lock for its whole run and MUST refuse at once, naming the holder, where another process holds it. It MUST replace each destination in that destination's own directory, MUST refuse a destination reached through a link whatever `--force` says, and MUST re-check that immediately before each write. Where it stops partway it MUST name every destination it observed itself finish, MUST leave the previous receipt in place, and MUST finish the rest when it runs again.

There is no journal and no backup store. A run leaves whole files, and what the next run needs is the payload, the receipt, and the disk, which are the three references it already reads. Recovery after power loss rests on the persistence order and on the platform's sync semantics, and is claimed no further.

#### Scenario: The second skill root cannot be written

- GIVEN two skill roots, the second holding a destination the process cannot write
- WHEN `sdd skill install --apply` has already rewritten the first root
- THEN it exits 73 naming the unwritable path and the destinations it finished, keeps the first root's new files, and lands the second on the next run

Verify: `cargo nextest run -E 'binary(cmd_skill)'`

### `distribution:skill-uninstall-removes-only-what-it-wrote` — Skill uninstall removes only what it wrote

When run without `--apply`, `sdd skill uninstall` MUST list every removal and delete nothing. When applied, it MUST remove a file only where its current digest equals the one the receipt records, and MUST name every file it keeps with the reason. It MUST remove a package directory only once nothing else is left in it.

#### Scenario: The operator edited an installed skill

- GIVEN an installed skill package whose `SKILL.md` the operator has since rewritten
- WHEN `sdd skill uninstall --apply` runs
- THEN the edited file stays and is named as kept, because the receipt vouches for bytes and these are not those bytes

Verify: `cargo nextest run -E 'binary(cmd_skill)'`

### `distribution:an-install-sweeps-what-the-payload-dropped` — An install sweeps what the payload dropped

Where the user-scope receipt vouches for a destination the current payload no longer carries, `sdd skill install --apply` and `sdd skill uninstall --apply` MUST remove it and the directory it empties. Both MUST leave a destination the receipt cannot vouch for alone, and the doctor MUST name what they left.

#### Scenario: A release renames a skill

- GIVEN a home directory holding a skill under its old name, recorded by the apply that wrote it
- WHEN a newer `sdd skill install --apply` runs
- THEN the old name's `SKILL.md` and its directory are gone, the new name is installed, and only one entry is offered under either name

Verify: `cargo nextest run -E 'binary(cmd_skill)'`

### `distribution:user-scope-files-stay-unrecorded` — User-scope files stay unrecorded

Files `sdd skill install` writes outside an instance MUST NOT appear in any instance manifest. The payload and the user-scope record are the references the installer compares them against, and no verification reads either.

#### Scenario: An instance is verified after a user-scope install

- GIVEN an installed instance and a completed `sdd skill install --apply`
- WHEN `sdd verify` runs against the instance
- THEN the report is unchanged by anything under the home directory

Verify: `cargo nextest run -E 'binary(cmd_skill)'`

### `distribution:a-skill-package-is-self-contained` — A skill package is self-contained

Every installed skill MUST be one directory holding `SKILL.md` and every shared artifact under `references/`, materialized by the installer from the one authored source. Every skill MUST name a shared artifact by a path relative to its own root, and no skill may name one outside its own directory.

#### Scenario: A skill is installed under one agent root

- GIVEN an agent root and a completed `sdd skill install --apply`
- WHEN the installed package is read
- THEN it holds `SKILL.md` and a `references/` directory carrying both gates, so the skill resolves them the way the format resolves a supporting file

Verify: `cargo nextest run -E 'binary(cmd_skill)'`

### `distribution:a-user-scope-receipt-is-required-state` — A user-scope receipt is required state

An apply MUST write the user-scope receipt after every file it lands, MUST fail naming the receipt where it cannot, and MUST vouch for every package destination that holds the payload's bytes rather than only the ones it wrote itself. A receipt that vouches for nothing MUST be removed rather than left empty.

The receipt is last because until it lands the previous one still describes the home. Vouching for what is there, rather than for what this run did, is what lets a rerun finish a run that stopped: the files an earlier run wrote are recorded by the run that completes.

#### Scenario: The receipt cannot be replaced

- GIVEN an apply that has already replaced every package file
- WHEN the receipt cannot be written
- THEN the apply fails naming the receipt, the package files stay, the previous receipt still stands, and the next run records them

Verify: `cargo nextest run -E 'binary(cmd_skill)'`

### `distribution:the-payload-names-no-other-project` — The payload names no other project

The author MUST keep every embedded payload root free of the name of any project, repository, or organization outside this one. The forges, agents, and reference works the method documents as integrations are the exception.

#### Scenario: A chapter carries an example from the repository it was drafted in

- GIVEN a chapter or skill that names a sibling project while illustrating a rule
- WHEN the canon test suite runs
- THEN the check fails naming the file and the term, because a reader who lacks that project meets a reference they cannot follow

Verify: `cargo nextest run -E 'binary(canon)'`

### `distribution:a-declared-location-is-named-by-its-variable` — A declared location is named by its variable

The author MUST keep the retired name of a declared location out of the authored corpus. A path the binary reports MUST be named by the report rather than restated, so no skill and no chapter spells one.

#### Scenario: A chapter reintroduces the docs scratch's old fixed path

- GIVEN a chapter edited to name the retired directory, or a skill edited to spell a path the status report carries
- WHEN the canon test suite runs
- THEN the check fails naming the file and the path, because a corpus that fixes the location has taken the declaration back

Verify: `cargo nextest run -E 'binary(canon)'`

### `distribution:the-payload-roots-are-declared-once` — The payload roots are declared once

The author MUST declare the embedded payload roots in one place that the binary, the build script, and the canon suite all read.

#### Scenario: An eighth root is embedded

- GIVEN a new root added to the embedding module alone
- WHEN the canon suite scans the payload for what it must not carry
- THEN the scan walks a list that no longer describes the payload, so the root ships unscanned unless one declaration feeds all three

Verify: `cargo nextest run -E 'kind(lib)'`

### `distribution:a-seeded-rule-runs-no-canon-command` — A seeded rule runs no canon command

Where a spec is seeded into an instance, the author MUST keep `cargo` and `just` out of every shell command its verification lines carry.

#### Scenario: A canon-only rule is left in a seeded spec

- GIVEN a seeded spec carrying a rule verified by a cargo test
- WHEN the canon test suite runs
- THEN the check fails naming the spec and the command, because the adopter reads an unrunnable verification as work it owes

Verify: `cargo nextest run -E 'binary(canon)'`

### `distribution:the-declaration-is-seeded-once-and-then-owned` — The declaration is seeded once and then owned

`sdd init` MUST write `.spec-driven-docs/config.yaml` in every case, record it among the adopted files, and never overwrite it again. An upgrade MUST carry it forward unchanged and re-render the managed blocks from it.

The boundary is the actor, not the file. An automatic write to an adopted file during install or upgrade is forbidden. An operator-invoked command that previews a specific change first and writes it on request is not: `sdd debt tighten --apply` lowers the project's debt file, and `sdd policy reconcile --apply` appends to an adopted specification the rule that authorizes a declaration the project made, and each updates the record for the file it changed.

#### Scenario: An instance that declares an exclusion is upgraded

- GIVEN an instance whose declaration reserves a path
- WHEN `sdd upgrade` runs
- THEN the upgrade neither conflicts on the file nor drops the reservation, because the declaration is adopted rather than managed and the block is rendered rather than hand-edited

Verify: `pre-commit run cargo-test --all-files`