Write a feature spec

You will write one small JSON file that says what a feature is for, what it deliberately will not do, the sentences your team agreed to, and where the durable decisions behind them are recorded. It is the artifact a reviewer — or an AI assistant — reads before touching your code, and the one thing no generator can produce for you.

Putnami calls this spec-driven development (SDD), and it keeps the spec deliberately small. Everything a tool can already see — owners, routes, schemas, migrations, current maturity — stays where it is declared. The spec adds only the intent nothing else holds.

Declare the feature first

A spec details a feature; it never creates one. If the feature does not exist yet, declare it in putnami.features.json at your workspace or project root:

{
  "$schema": "https://putnami.dev/schemas/putnami-features.json",
  "protocolVersion": 1,
  "namespace": "billing",
  "features": [
    {
      "id": "billing/invoice-export",
      "type": "feature",
      "name": "Invoice export",
      "outcome": "Customers can export issued invoices",
      "owner": "billing",
      "target": "coded",
      "requirements": [
        { "id": "implementation", "stage": "coded", "evidenceKinds": ["capability"] }
      ]
    }
  ]
}

No feature declaration, no spec. That is the intended pressure: product intent has exactly one home.

Write the spec

Specs are direct JSON children of specs/ at your workspace root or at an exact project root. The filename is yours to choose — identity is the feature field, so renaming the file changes nothing.

{
  "$schema": "https://putnami.dev/schemas/putnami-spec.json",
  "protocolVersion": 1,
  "feature": "billing/invoice-export",
  "outcomes": [
    "A customer exports an issued invoice without leaving the billing workspace",
    "An exported invoice stays readable after the issuing revision is superseded"
  ],
  "nonGoals": [
    "Exporting a draft invoice",
    "Exporting invoices owned by another tenant"
  ],
  "requirements": [
    {
      "id": "export-format",
      "text": "An export states its format in the response content type and never guesses it from the file name."
    }
  ],
  "decisions": [
    "billing/doc/adr/0001-invoice-export-format.md"
  ]
}

That is the whole contract. Six fields:

Field What it holds
feature the one authored feature ID this spec details
outcomes what the feature intends to deliver, in your order
nonGoals what it deliberately excludes, in your order
requirements stable sentences, each with a spec-local id
decisions workspace-relative links to doc/adr/*.md records
protocolVersion always the integer 1

There is no status, no owner, no approvedAt, no maturity, and no place to paste a route table. Those are either derived facts Putnami already knows, or process state your issue tracker already holds.

Record the durable decisions

When a spec depends on a choice that outlives the pull request, write an architecture decision record next to the code that lives with it, at <project>/doc/adr/NNNN-kebab-title.md. One decision per file, never rewritten in place — a reversal gets a new record that supersedes the old one.

Keep it to four sections:

  • Context — what forced the decision;
  • Decision — what you do, in the present tense;
  • Rejected alternatives — the options considered and why each lost;
  • Consequences — what this costs and what a future author must now do differently.

Then link it from the spec by workspace-relative path. The spec never embeds the decision, and there is no JSON decision format to learn: the markdown record is the durable decision.

What is checked for you

Specs are a strict, versioned wire contract, so mistakes fail loudly instead of rotting quietly:

  • an unknown field — including anything that would let a spec invent a feature, a maturity stage, or evidence — is rejected, not ignored;
  • protocolVersion must be the exact integer 1;
  • a null collection, a duplicate requirement ID, or a duplicate decision link fails;
  • a decision link that escapes the workspace, or that is not a doc/adr/*.md record, fails;
  • a spec that references a feature nobody declared fails, and so does a second spec for a feature that already has one;
  • the file is reformatted to canonical bytes — two-space indentation, one trailing newline — so specs never produce whitespace diffs.

Your editor validates the file as you type if you keep the $schema line: both https://putnami.dev/schemas/putnami-spec.json and https://putnami.dev/schemas/putnami-features.json are published.

Why the spec stays this small

A spec that repeated derived facts would be a second inventory with no producer, stale the day after it was written. Putnami keeps one home per fact: the feature declaration owns identity and owner, build-time producers own routes and schemas and migrations, evidence owns what has actually been proven — and the spec owns only the prose none of them can emit. That is what makes it worth reading months later, and what makes it safe to hand to an assistant as context.

You now have a durable, machine-checked statement of what your feature is for, what it will not do, and which decisions it rests on — small enough that people keep it current, and strict enough that it cannot quietly disagree with your code.