Speccle

/oracle

speccle-oracle

[tool]

The deterministic half of Speccle: one bin, two commands. Everything here is a Speccle tool — deterministic, independently runnable, emits typed JSON, and never calls an LLM. Trust comes from the tools; judgement comes from the skills.

speccle-oracle
$ speccle-oracle strength targets/checkoutfeatures/basket/SPEC.md  BASKET-1    ████████████████████  100.0%    16/16  Adding an item increments its quantity by exactly 1  BASKET-2    ████████████████████  100.0%    29/29  Removing the last item leaves the basket empty  line coverage 100.0%features/checkout/SPEC.md  CHECKOUT-1  ████████████████████  100.0%    14/14  Tax rounds half-up to 2dp per line item  CHECKOUT-2  ████████████████████  100.0%      7/7  An empty basket totals zero  CHECKOUT-3  ██████████████████░░   88.2%    15/17  Checkout rejects a basket of more than 100 line items      features/checkout/checkout.ts:13:11  StringLiteral → ``      features/checkout/checkout.ts:14:17  StringLiteral → ""  line coverage 100.0%oracle strength 95.7% (44/46)   line coverage 100.0%2 surviving mutants — each one a change no test noticed

This is the exact output for the repo's targets/checkout proving ground — the same report rendered live in the demo.

Commands

speccle-oracle lint [path]Lint every SPEC.md under path against the nine fixed rules. Exit 0 clean, 1 violations, 2 usage error.
speccle-oracle strength [path]Join specs + Stryker mutation report + coverage into killed ÷ covered per criterion. Exits 0 whenever it produced a report — judging against a threshold is a separate concern.

Both take --json to emit the typed report — LintReport or StrengthReport — the contract other tooling consumes. A bare invocation is a usage error: both commands are explicit subcommands.

lint — enforce the convention

Nine fixed rules: six structural, three quality heuristics that judge the heading statement only — the body beneath a criterion is never linted. One severity, no configuration: a spec either lints clean or it doesn't.

missing-keyFrontmatter key absent or malformed
key-collisionTwo specs declare the same key
key-mismatchA criterion id's key differs from the spec's declared key
malformed-idH2 without a well-formed [KEY-n] token
duplicate-idThe same id appears twice
empty-statementCriterion heading has a token but no statement
weasel-wordingStatement hedges (should, appropriately, as expected, …)
compound-criterionStatement contains more than one testable clause
unmeasurableStatement asserts nothing observable

unmeasurableflags vacuous shapes ("is handled", "works"), never unlisted verbs — a domain verb the rule has never seen passes. It under-flags by design.

strength — score the tests

Joins three inputs into one number per criterion: the SPEC.md files under the path, a StrykerJS mutation report, and an Istanbul coverage summary. Oracle strength is killed ÷ covered— of the mutants a criterion's tests execute, the fraction the suite kills. A kill counts for every criterion covering that mutant, so a criterion below 100% always has at least one surviving mutant listed beneath it: the exact code change no test noticed.

The command reads reports; it never runs Stryker. It requires coverageAnalysis: "perTest" — without it the report carries no per-test coverage, and strength refuses rather than guessing. A criterion no test claims is reported as unclaimed, not as zero strength; mutants that run at module load are reported apart as static, since no criterion can claim them.

The convention

The written contract both commands enforce. A criterion is an H2 heading carrying a [KEY-n] id and one testable statement; the body beneath is free — rationale, edge cases, examples. Ids are names, not order: a new criterion takes the next never-used number, and an id is never renumbered or reused.

SPEC.md

---
key: CHECKOUT
---

# Checkout

## [CHECKOUT-1] Tax rounds half-up per line item

Tax is computed per line item and rounded half-up to 2dp before summing.

- three items of £1.99 at 20% → £1.20 tax; taxing the £5.97 total would give £1.19

## [CHECKOUT-2] Empty basket totals zero

A test defends a criterion when its token appears anywhere in the test's full concatenated name — enclosing describe titles included, so one describe('[CHECKOUT-1] …') block claims every test inside it. Mutation and coverage reports already carry full names, so the join is mechanical.

Try it

The repo bundles targets/checkout with its mutation and coverage reports already captured, so you can run both commands without generating anything. Node ≥ 24 runs the TypeScript sources directly — no build needed:

shell

git clone https://github.com/matthewalton/speccle.git
cd speccle && pnpm install

node packages/oracle/src/cli.ts lint targets/checkout
# → 2 spec files, clean

node packages/oracle/src/cli.ts strength targets/checkout
# → the heatmap above