/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 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-key | Frontmatter key absent or malformed |
| key-collision | Two specs declare the same key |
| key-mismatch | A criterion id's key differs from the spec's declared key |
| malformed-id | H2 without a well-formed [KEY-n] token |
| duplicate-id | The same id appears twice |
| empty-statement | Criterion heading has a token but no statement |
| weasel-wording | Statement hedges (should, appropriately, as expected, …) |
| compound-criterion | Statement contains more than one testable clause |
| unmeasurable | Statement 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