create-spec-driven-app v0.7.0

Domain packs#

Reusable, versioned domain knowledge. A pack is to a spec tree what a dependency is to code: you install it, you pin it, and you upgrade it deliberately.

The three repositories The tool is an npm package. A domain pack repository holds stack-agnostic templates written by domain experts and is version-tagged like a library. An implementation project renders those templates for one stack and records which pack version it came from, so a later pack release can be pulled in as a diff.
<defs>
  <marker id="csda-tr-a" viewBox="0 0 10 8" refX="9" refY="4"
          markerWidth="9" markerHeight="7" orient="auto-start-reverse">
    <path d="M0 0 L10 4 L0 8 Z" fill="var(--fg-soft)"/>
  </marker>
</defs>

<g class="dia__box dia__box--tool">
  <rect x="270" y="12" width="220" height="76" rx="10"/>
</g>
<text x="380" y="40" text-anchor="middle" class="dia__h">create-spec-driven-app</text>
<text x="380" y="60" text-anchor="middle" class="dia__s">the tool · one npm package</text>
<text x="380" y="78" text-anchor="middle" class="dia__s dia__mono">npx create-spec-driven-app</text>

<g class="dia__box">
  <rect x="20" y="164" width="330" height="180" rx="10"/>
  <rect x="410" y="164" width="330" height="180" rx="10"/>
</g>

<text x="40" y="192" class="dia__h">Domain pack</text>
<text x="40" y="210" class="dia__s">by domain experts · stack-agnostic</text>
<text x="40" y="238" class="dia__s dia__mono">pack.yaml</text>
<text x="40" y="258" class="dia__s dia__mono">templates/spec.md.tpl</text>
<text x="40" y="278" class="dia__s dia__mono">templates/features/**.feature.tpl</text>
<text x="40" y="306" class="dia__s">pinned by tag — <tspan class="dia__mono">v0.2.0</tspan>, like a library</text>
<text x="40" y="330" class="dia__s dia__faint">one pack, many stacks</text>

<text x="440" y="192" class="dia__h">Your project</text>
<text x="440" y="210" class="dia__s">by humans and agents · one stack</text>
<text x="440" y="238" class="dia__s dia__mono">spec.md · AI_RULES.md</text>
<text x="440" y="258" class="dia__s dia__mono">features/**.feature</text>
<text x="440" y="278" class="dia__s dia__mono">docs/specs/traceability.md</text>
<text x="440" y="306" class="dia__s"><tspan class="dia__mono">.specops.lock</tspan> — which pack version, exactly</text>
<text x="440" y="330" class="dia__s dia__faint">Spring · Quarkus · Node · FastAPI · …</text>

<g class="dia__msg">
  <path d="M300 100 H185 V158" marker-end="url(#csda-tr-a)"/>
  <text x="196" y="122" class="dia__s">reads and lints it</text>
  <path d="M460 100 H575 V158" marker-end="url(#csda-tr-a)"/>
  <text x="564" y="122" text-anchor="end" class="dia__s">renders it here</text>

  <path d="M350 254 H404" marker-end="url(#csda-tr-a)"/>
  <text x="377" y="242" text-anchor="middle" class="dia__s dia__mono">specops add</text>
  <path d="M404 288 H350" marker-end="url(#csda-tr-a)" stroke-dasharray="5 4"/>
  <text x="377" y="306" text-anchor="middle" class="dia__s dia__mono">specops sync</text>
</g>
The pack is a dependency, not a copy-paste. A new pack version arrives as a diff you review — which is what keeps eleven services from drifting apart.

Author a domain pack from scratch#

Goal: package a reusable bundle of requirements, use cases, aggregates, events, and Gherkin templates.

# 1. Scaffold a pack
npx create-spec-driven-app@latest pack init \
  --out ./domain-packs \
  --name "Billing Backend" \
  --type backend

# 2. Edit ./domain-packs/billing/backend/pack.yaml
#    Required fields (see schemas/pack.schema.json for the full schema):
#      schema_version, metadata.{name,version,language,project_type}
#      variables.required
#    Optional: requirements, use_cases, commands, aggregates, events,
#              scenarios, outputs.files, rules

# 3. Lint
npx create-spec-driven-app@latest pack lint \
  --pack-root ./domain-packs \
  --pack billing/backend

Minimum viable pack.yaml:

# yaml-language-server: $schema=../../../schemas/pack.schema.json
schema_version: "1.2.0"
metadata:
  name: "Billing Backend"
  version: "0.1.0"
  language: "en"
  project_type: "backend"
variables:
  required: [PROJECT_NAME, PROJECT_SLUG, DOMAIN]
requirements:
  - id: REQ-001
    title: "Charge customer at checkout"
    priority: Must
    status: Draft
outputs:
  files:
    - target: "features/billing/charge.feature"
      template: |
        Feature: Charge customer at checkout
          Scenario: Successful payment for {{PROJECT_NAME}}
            Given a valid card on file
            When the user confirms the order
            Then a CHARGE event is emitted

pack lint blocks publication on schema violations, missing requirements, unresolved placeholders, and duplicate IDs.



Apply a domain pack to an existing project#

Goal: layer a pack onto a project generated in §1, supplying its template variables.

The recommended ergonomic path is specops add (npm-install-style):

# From inside your project (auto-detected project dir)
csda specops add \
  --pack-repo https://github.com/acme/billing-specops.git \
  --pack-version v0.1.0 \
  --pack backend \
  --var PROJECT_NAME="Acme Energy Hub" \
  --var PROJECT_SLUG=acme-energy-hub \
  --var DOMAIN="community energy"

add writes/updates .specops.lock so subsequent specops sync / specops diff calls remember the source, version, and vars.

Lower-level alternative (expand) — same behaviour, more flags:

csda expand \
  --pack-root ./domain-packs \
  --pack billing/backend \
  --project-dir /tmp/acme-energy-hub \
  --var PROJECT_NAME="Acme Energy Hub" \
  --var PROJECT_SLUG=acme-energy-hub \
  --var DOMAIN="community energy"

To take a pack OFF the project:

csda specops remove parking-management/backend

remove drops the entry from .specops.lock but does not delete generated files — you might have hand-edited tests pointing at them. Use git status afterwards and clean up by hand.

Side effects:

Flags:

FlagUse
--dry-runPrint actions without writing.
--no-examplesSkip files marked seed: true (good for production projects).
--cache-dir <path>Override the cache directory for remote packs.


Build a contracts pack for API-first work#

Goal: ship a pack that defines REST/AsyncAPI contracts and consumer-driven Pact tests.

Set project_type: contracts in pack.yaml. The schema then accepts two extra top-level fields:

Example excerpt:

schema_version: "1.2.0"
metadata:
  name: "API Contracts Pack"
  version: "0.1.0"
  project_type: "contracts"
variables:
  required: [PROJECT_NAME, PROVIDER_SERVICE, CONSUMER_SERVICE]

api_contracts:
  - id: AC-001
    title: "Provider REST API — v1"
    type: REST
    provider: "{{PROVIDER_SERVICE}}"
    consumers: ["{{CONSUMER_SERVICE}}"]
    schema_ref: "contracts/openapi/provider-v1.yaml"
    requirement: REQ-001

consumer_driven_tests:
  - id: CDT-001
    consumer: "{{CONSUMER_SERVICE}}"
    provider: "{{PROVIDER_SERVICE}}"
    pact_file: "contracts/pacts/{{CONSUMER_SERVICE}}-{{PROVIDER_SERVICE}}.json"
    requirement: REQ-002

When applied with expand, a contracts pack generates docs/specs/test-strategy.md describing the TDD gates (failing pact = failing build, breaking-change rules, contract versioning policy). Pair it with validate --strict-tdd so every AC-NNN must trace to a CDT-NNN and a .feature.

Browse packs/sample-contracts/contracts/pack.yaml for a complete working example.



Compose multiple packs with specops.config.yaml#

Goal: declare every pack a project depends on in one place, so a fresh clone can rebuild specs with specops sync.

Create specops.config.yaml at the project root:

specops_version: 1
packs:
  - repo: https://github.com/acme/parking-specops.git
    version: v0.1.0
    pack_id: backend
    vars:
      PROJECT_NAME: Smart Parking
      PROJECT_SLUG: smart-parking
      DOMAIN: parking operations
  - repo: https://github.com/acme/billing-specops.git
    version: v0.2.0
    pack_id: contracts
    vars:
      PROJECT_NAME: Smart Parking
      PROVIDER_SERVICE: billing-svc
      CONSUMER_SERVICE: parking-svc

Then on a fresh clone:

npx create-spec-driven-app@latest specops sync --project-dir .

When .specops.lock is absent, sync reads specops.config.yaml, expands every listed pack, and writes the lockfile. When the lockfile exists, the lockfile wins — specops.config.yaml is the intent, the lockfile is the resolved state (think package.json vs package-lock.json).



Bump a pack version safely (specops diff + sync)#

Goal: upgrade parking-management/backend from v0.1.0 to v0.2.0 without surprises.

# 1. Preview the change (no writes)
npx create-spec-driven-app@latest specops diff \
  --project-dir ./smart-parking \
  --pack parking-management/backend \
  --pack-version v0.2.0

# Output:
# ── parking-management/backend @ v0.2.0 (current: v0.1.0) ──
#   + features/pricing/dynamic_pricing.feature
#   ~ docs/specs/use-cases.md
#   ~ docs/specs/traceability.md
#   1 added · 2 modified · 9 unchanged

# 2. Apply once you're satisfied
npx create-spec-driven-app@latest specops sync \
  --project-dir ./smart-parking \
  --pack parking-management/backend \
  --pack-version v0.2.0

# 3. Re-validate
npx create-spec-driven-app@latest validate ./smart-parking --strict-tdd

# 4. Commit the updated .specops.lock and the regenerated spec files
git add .specops.lock docs/specs features
git commit -m "chore(specs): bump parking-management/backend to v0.2.0"

Plain sync (no --pack / --pack-version) re-expands every pack in the lockfile using the vars persisted there — no need to retype --var flags.



Next#