Specs that refuse
to drift.

onspec checks every code change against the spec that governs it. Verdicts come from tests and evidence, not vibes.

$ npm install -g onspec
View on npm
onspec verify · pull request #142
$ onspec verify --test-results results.xml

SPEC-0002  Prototype pollution resistance
  ✓ met       C1  Merging untrusted payloads cannot pollute Object.prototype
              test passed: should not override Object prototype
  ✓ met       C2  Inherited enumerable properties are never copied
              test passed: should ignore inherited enumerable properties

SPEC-0004  Plain-object detection
  ✗ unmet     C3  ESM namespace objects merge like ordinary objects
              test failed: works with asterisk-import
  ? uncertain C4  isPlainObject accepts [object Module] values
              no evidence pointer on criterion

Summary: 2 met · 1 unmet · 1 uncertain (4 criteria)

Every criterion gets a verdict. Every verdict cites its evidence.

Specs live in your repo as reviewable files. On every pull request, onspec finds the specs that govern the changed code and checks each acceptance criterion.

Deterministic evidence first

A criterion that names a test resolves through your CI's JUnit report. The test result is the verdict. Assertion evidence resolves by file content. No model involved, no opinion, same answer every run.

An LLM only for the gap

Criteria without hard evidence get a Claude assessment of the diff, and it must cite file:line or the verdict downgrades to uncertain. Falsifiable claims, never bare confidence.

Drift is refused, not documented

Changed code that no approved spec governs gets flagged: update the spec or revert the code. Documentation drifts silently. A source of truth refuses to.

Built for the 2 a.m. hotfix.

Someone patches production directly. Next CI run, the spec notices.

The patch ships tonight. But it never becomes invisible: the repo stays in a state where spec and code visibly disagree until a human reconciles them.

$ onspec drift --base origin/main

⚠ [unspecced-change] src/export/csv.ts changed
  but no spec covers it. Write the spec or
  revert the change.

1 drift finding

A spec is a file in your repo.

Markdown with YAML frontmatter. Humans review it in a normal pull request. Agents read it as a work order. onspec checks it forever after.

# specs/csv-export.spec.md
id: SPEC-0042
title: CSV export includes archived records
status: approved
covers:
  - src/export/**
criteria:
  - id: C1
    text: Archived records appear when include_archived=true
    verify: test
    evidence: tests/export.test.ts::includes archived records
  - id: C2
    text: Export format constant stays RFC 4180
    verify: assertion
    evidence: src/export/csv.ts#FORMAT = "RFC4180"
non_goals:
  - Bulk archive operations

covers maps specs to code

Globs decide which spec governs a change. That mapping powers both conformance checks and drift detection.

evidence makes criteria checkable

Each criterion names its own proof: a test, a code assertion, or an honest manual check that the report will keep surfacing.

status is the human gate

Only approved specs govern code. Approval happens by editing the file in a reviewed pull request, nowhere else.

Four commands. Zero infrastructure.

A CLI that runs locally and in any CI. Everything derives from the repo: no server, no account, no state anywhere else. No API key, no network calls; with a key, only the diff leaves, under your own account.

onspec verify

Criterion-by-criterion conformance verdicts for the current diff, posted as one PR comment.

onspec drift

Flags changed code that no approved spec governs. Advisory by default, blocking when you have earned it.

onspec lint

Grades each spec A to F on verifiability: ambiguous wording, dead globs, missing evidence.

In your CI in one commit.

Write one spec

Or point onspec reverse at your existing code and review its drafts.

Add the workflow

The Action posts a self-updating conformance comment on every pull request.

Stay advisory until trusted

Default exit code is 0. Flip strict when the team wants the gate.

# .github/workflows/onspec.yml
on: pull_request
jobs:
  onspec:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run test:junit
      - uses: bensubercaseaux/onspec@main
        with:
          test-results: test-results.xml