Skip to main content

KeelSpec

Product-as-code as a validated graph — for any project.

Your product intent — the glossary, the constraints, the business rules, the features, the decisions — usually lives as hopeful markdown that quietly rots. KeelSpec turns it into code: you declare your typed artifacts in one config file, and it loads them, enforces the schema and the cross-references, compiles a JSON bundle your app or docs can render, and fails CI when the committed bundle drifts.

It started as the product-as-code engine inside a payments platform and was extracted so any team can use it.

pip install keelspec

Quickstart

  1. Describe your artifact types in product.config.yaml:

    product_dir: product
    output: build/product.generated.json
    types:
      - name: terms                 # a glossary
        dir: terms
        id_prefix: "TERM-"          # ids must be TERM-<slug> and match the filename
        required: [id, term, definition]
        fields: [term, definition, see_also]
        references:
          - field: see_also         # every see_also must resolve to another term
            to: terms
      - name: rules                 # the durable invariants
        dir: rules
        id_prefix: "BR-"
        required: [id, title, statement]
        fields: [title, statement]
    
  2. Author artifacts as markdown-with-frontmatter under product/:

    ---
    id: TERM-payment
    term: Payment
    definition: An instruction to move money between two parties.
    see_also: [TERM-ledger]
    ---
    The canonical unit of work in the system.
    
  3. Build the bundle and gate it in CI:

    keelspec build       # -> build/product.generated.json  (commit it)
    keelspec validate    # schema + graph + staleness; non-zero exit on errors
    

While you author

The gate is deterministic, which means everything it knows is available before you push. Four surfaces expose the same graph, and none of them forms its own opinion — every verdict below is validate's, unchanged. An editor that said green while CI said PAC-020 would be a second judge, and a project whose whole claim is a deterministic gate cannot afford two.

keelspec validate --watch          # the gate, re-run on every save
keelspec lsp                       # language server (pip install 'keelspec[lsp]')
keelspec mcp                       # the graph as tools (pip install 'keelspec[mcp]')

--watch

Re-validates whenever a product/ file, the config, or the committed bundle changes. No extra dependency; it polls. Deliberately not a gate — it never exits non-zero, so a red tree you are mid-fix does not kill the terminal you are fixing it in. CI runs plain validate.

The language server

Diagnostics, completion, hover, go-to-definition, find-references and workspace symbols over your product/ tree, in any LSP client — VS Code, JetBrains, Neovim, Zed. A VS Code client lives in editors/vscode/; it launches this and owns no logic of its own.

Completion is the load-bearing one: in a reference field it offers the ids of the type that field must resolve to, and in an enum field its members — both read from the same config the validator reads, so an accepted completion cannot be a PAC-020 or a PAC-030. Hover shows what an id means and how many artifacts point at it; find-references answers "what breaks if I retire this rule?".

Diagnostics come from the file on disk, so they refresh on save rather than on every keystroke. That is the trade for having one judge. Completion, hover and navigation do read the unsaved buffer, because they answer "what is here?" rather than "is this allowed?".

The MCP server

The same graph, spoken to an agent rather than an editor — because in a repo run this way the author of product/features/refunds.md is increasingly a model, and asked for a constraints list it will happily invent CON-refund-window.

Six read-only tools: product_schema, product_list, product_get, product_allowed_values, product_references_to and product_validate. Read-only on purpose — an agent proposes with its own tools, and a server that could also write would let the thing being judged edit the evidence.

{ "mcpServers": { "keelspec": { "command": "keelspec", "args": ["mcp"] } } }

The agent-built lifecycle

keelspec orchestrate runs a station line over one artifact: Understand → Disambiguate (human gate) → Design → Develop → Test → adversarial Verify, with a bounded repair loop.

keelspec orchestrate FEAT-refunds                  # needs ANTHROPIC_API_KEY
keelspec orchestrate FEAT-refunds --no-clean       # skip the simplification pass
keelspec orchestrate FEAT-refunds --dry-run        # offline; exercises the pipeline
pip install 'keelspec[agents]'                     # the reasoning stations

Four critics attack the result through independent lenses — correctness, coverage, security, reproduce — each defaulting to refuted unless convinced. A blocking verdict feeds back to a repair round; after --max-repair rounds the run reports honestly rather than proposing.

The human gate

Open questions stop the line before design — building the wrong thing correctly is the expensive failure. A blocking human gate cannot live inside one autonomous run, so it is two-phase: run 1 writes the questions to .keelspec/disambiguations/<id>.json and stops, a person fills in each answer, run 2 reads them and proceeds.

{
  "status": "pending",
  "questions": [
    { "id": "q1", "dimension": "refund window start",
      "question": "From authorization, settlement or delivery?", "answer": "" }
  ]
}

--resolve q1="from settlement" does the same thing inline for one-offs.

Writing code

Develop is the only station that needs tools, so it runs a bounded loop with three of them — list, read, write — confined to the project root. Absolute paths, .. and symlink escapes are refused, not sanitised; there is no shell, no network, and no delete.

keelspec orchestrate FEAT-refunds            # proposes a diff, writes nothing
keelspec orchestrate FEAT-refunds --write    # lets it edit your working tree

--write is opt-in on purpose. Run it on a clean branch.

A critic that does not share the builder's blind spots

OPENROUTER_API_KEY=... keelspec orchestrate FEAT-refunds --cross-provider

Adds a fifth verdict from a non-Claude model, joining the same refute/repair logic. Diversity is a correctness lever: a critic from the same family can share the builder's failure modes. Without the key the lens is skipped and the run is identical to native-only; if the call fails it abstains rather than blocking.

What is structural

  • Nothing merges. The orchestrator's terminal state is a report. validate is the judge and a human decides; every station past the gate is deterministic or human by construction, and a test asserts it.
  • Minor findings do not block. A gate that fires on nitpicks trains people to bypass it, and a bypassed gate is worse than none.
  • An unreachable critic abstains. One flaky provider must not become an outage.

Control flow is ordinary code; reasoning lives inside a station. Supply your own Runner to change what any station does.

Evidence

keelspec evidence emits a content-addressed record of what was checked, when, on which commit, and with what result — for the audit conversation that starts "prove your documented rules matched your implementation":

keelspec evidence -o build/evidence.json      # make a record
keelspec evidence --verify build/evidence.json # check one against this tree

Every artifact, the config and the bundle carry a SHA-256, so the record is verified by recomputing digests rather than by trusting whoever produced it. Historical evidence needs no special support: check out the commit and run it again — bundles are byte-stable, so the digests match. Exit code follows validate, so it gates CI and records it in one step.

What validate enforces

Every finding carries a stable PAC-NNN code (an API — CI and dashboards match on the code, never the prose):

code meaning
PAC-001 an artifact is missing a required field
PAC-010 an id doesn't start with its type's id_prefix
PAC-011 an id doesn't match its filename
PAC-012 a duplicate id within a type
PAC-020 a reference field doesn't resolve to a known artifact
PAC-030 a field value isn't in the type's allowed set (enums)
PAC-060 the committed generated bundle is missing or stale

Config reference

Each entry under types:

  • name — the collection name (and the key in the bundle).
  • dir — the subdirectory under product_dir.
  • id_prefix (optional) — required id prefix; with id_matches_filename (default true), the id must equal the filename stem.
  • required — frontmatter fields that must be present and non-empty (id always is).
  • fields — the fields captured into the bundle.
  • enums — field -> [allowed values].
  • references — [{ field, to }]: values of field must be the id of a to artifact.

Extending it — plugins

Real projects need more than schema: cross-file checks ("does this links.code path exist?"), derived bundle fields (reverse edges, rollups), extra generated outputs (a runtime catalogue), and their own diagnostic codes. A plugin registers those without forking the engine — which is how a large project migrates onto KeelSpec with no loss of functionality:

# product_hooks.py
from keelspec import register

def register(reg):
    register("PAC-900", "links.code path does not exist on disk")

    @reg.validator
    def code_paths_exist(bundle, config, root, report):
        for f in bundle["features"]:
            for path in f.get("code") or []:
                if not (root / path).exists():
                    report.add("PAC-900", f"missing {path}", f["_file"])

    @reg.bundle_transformer
    def reverse_edges(bundle, config, root):
        ...  # enrich the bundle in place

    reg.extra_output("build/catalogue.json", render_catalogue)  # staleness-gated too
keelspec -p product_hooks.py validate

Start a new repo in one command

Scaffold a ready-to-go product-as-code repo (config, example artifacts, and the CI gate) with the copier template:

pipx run copier copy --trust gh:tarvitave/keelspec my-product-repo

--trust lets the template run keelspec build once so the new repo is valid on its first commit.

CI

Use the reusable Action — it installs KeelSpec and runs the gate:

# .github/workflows/product.yml
name: product-as-code
on: [pull_request, push]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: tarvitave/keelspec@v0.7.0
        with:
          config: product.config.yaml     # optional (default)
          plugin: product_hooks.py         # optional project checks
Prefer plain pip?
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install keelspec
      - run: keelspec validate

Used in production

KeelSpec is the product-as-code engine of the pharthing / KibiPay payments platform (50 features, a concept catalogue, 70 decisions, and a ~490 KB render bundle). pharthing's CI runs keelspec validate as its sole gate via a plugin that adds domain checks — a byte-identical parity test proves nothing was lost in the extraction. That's the plugin system above, doing real work.

Releasing

Publishing to PyPI is automated via GitHub Releases + Trusted Publishing — see RELEASING.md. Changes per version are in CHANGELOG.md.

License

Apache-2.0. Permissive: use it, embed it, ship it, no obligations back beyond the notice. See LICENSE and NOTICE.

Versions up to 0.6.3 were published as fastpdlc under LGPL-3.0-or-later. That package has been removed from PyPI; keelspec is the only name.

Metadata

Release files for keelspec 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for keelspec 0.7.0
File Size Uploaded
keelspec-0.7.0.tar.gz 89.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for keelspec 0.7.0
File Interpreter ABI Platform
keelspec-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 152.7 kB

Release files / keelspec-0.7.0.tar.gz

Download URL keelspec-0.7.0.tar.gz
Size 89.9 kB
Tags Source
SHA-256 checksum
How to use checksums
66b66598005f50707edad00811383d34390cacdf3b47df83b69da8c03c985520
BLAKE2b-256 checksum
How to use checksums
2a9a6d9f1649db91552a990a350b51b6212c3f63fc073e8b66273104812c2a1c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release files / keelspec-0.7.0-py3-none-any.whl

Download URL keelspec-0.7.0-py3-none-any.whl
Size 62.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8543f38da5b492a3fea73304b323f1188043ecf9644031f9b9691c07029957bb
BLAKE2b-256 checksum
How to use checksums
75183399e9aa539d9b6ac9aef088840f765f69d013b154c31af2e3c23896550d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.7.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page