npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

specsy

v0.3.0

Published

A linter for specifications. Catches vague, untestable, and untraceable requirements before an AI agent turns them into code.

Downloads

76

Readme

A linter for specifications. It catches vague, untestable and untraceable requirements before an AI agent turns them into code.

openspec/changes/add-auth/specs/auth/spec.md
  16:24  warn   "reasonable" is not measurable.            no-weasel-words
                → Replace with the observable outcome. What would you check
                  to know "reasonable" was achieved?
  16:67  error  Performance claim "fast" has no target.    quantify-performance
                → State a budget and a percentile, e.g. "p95 under 200ms".

openspec/changes/add-auth/tasks.md
  6:1    error  Task references "AUTH-9", which is not defined in this change.
                → Either the id is a typo or the requirement was never written down.

✖ 10 problems (4 errors, 6 warnings) in 3 documents.

Why this exists

Spec-driven development tools — OpenSpec, GitHub Spec Kit, Kiro, BMAD — all give you a template. None of them check whether what you wrote into it is any good.

So teams fill a beautifully structured spec.md with "should be fast", "handle errors gracefully", "user-friendly" — and the agent, which cannot ask a follow-up question at 2am, invents an answer. The spec was never the bottleneck. The quality of the spec was.

npx specsy is the compiler pass that was missing. It reads the specs you already have, in the format you already use, and refuses to let an unanswerable requirement reach an agent.

It is not another spec format. It sits on top of the one you have.

How it works

npx specsy sits between the spec and the code. Findings come back with a file:line and a concrete fix, and the command exits 1 — so a spec that cannot be built correctly never reaches the implementation step.

With the MCP server the agent operates that gate itself: it writes a spec, lints it, reads the fix, rewrites, and only implements once the spec is clean. Nobody copies output between windows.

Walkthrough

You have an idea and nothing else

mkdir tally && cd tally && git init
npx @fission-ai/openspec@latest init      # the spec format — not specsy's job
claude mcp add specsy -- npx -y specsy mcp

Before writing anything, learn the bar. Ask your agent to call list_rules, or run:

npx specsy rules
npx specsy explain quantify-performance

Then propose a change (/opsx:propose add receipt upload) and have the agent lint its own work. Left alone it writes what everyone writes — "upload must be fast and handle errors gracefully" — and specsy sends it back:

proposal.md:1:1     warn   [require-non-goals] Proposal has no non-goals section.
specs/…/spec.md:12  warn   [quantify-performance] Performance claim "fast" has no target.
specs/…/spec.md:19  error  [require-acceptance-criteria] Requirement "Amount extraction" has no acceptance criteria.

The agent can fix most of that alone. What it can't fix — "what accuracy target, and what happens on a blurry photo?" — it now has to ask you. That question is the point. Without it the agent picks silently and you find out in review.

Once clean, check what the change will cost to keep re-reading, then build:

npx specsy footprint    # 9.4k tokens; ~75k over 8 agent turns, ~$0.38

Finally, make it permanent — a pre-commit hook and a CI job:

npx specsy || exit 1
- run: npx specsy --reporter github --max-warnings 0

You already have specs

Look before adopting. Nothing is installed, nothing is written:

cd my-project
npx specsy

If it is noisy, walk away — that is the honest way to judge a linter. If it is useful, adopt it as a ratchet rather than a rewrite. Fix the errors, defer the warnings:

npx specsy --quiet          # errors only; warnings still counted for the exit code

Then hold new specs to the full bar while the backlog drains, by putting only errors in CI:

- run: npx specsy --quiet

Turn rules back on in .specsyrc.json as you go. Finally, commit a .mcp.json so every agent on the team self-checks before implementing, and stop being the person pasting terminal output into chat.

Install

npx specsy lints the directory you run it from, so run it inside a project:

cd my-project
npx specsy

Or add it as a dev dependency:

npm install -D specsy     # or: pnpm add -D specsy
npx specsy

A local install is not on your PATH — typing a bare specsy will fail with "not recognized as an internal or external command". Use npx specsy, or add "lint:spec": "specsy" to your package.json scripts, where node_modules/.bin is already on the path. Install globally (npm i -g specsy) only if you want the bare command everywhere.

Usage

npx specsy                      # auto-detect the format, lint everything
npx specsy openspec/            # point at a directory
npx specsy --format openspec    # force an adapter
npx specsy --reporter json      # machine-readable output
npx specsy --reporter github    # inline annotations on a PR diff
npx specsy --quiet              # errors only
npx specsy --max-warnings 0     # treat warnings as failures too
npx specsy rules                # list every rule
npx specsy explain <rule>       # one rule, with a failing and a passing example
npx specsy footprint            # token cost of each change
npx specsy mcp                  # run as an MCP server for agents

Check the specs you are working on

By default, Specsy checks all in-flight changes and the living spec. Narrow the run explicitly when you only want findings for current work:

npx specsy --scope changes      # every in-flight change
npx specsy --scope living       # the living spec only
npx specsy --change add-auth    # one in-flight change by directory name
npx specsy --scope all          # everything (the default)

--scope and --change cannot be combined. Either flag overrides the configured scope. Unknown changes, empty selections, and non-default scopes on the generic adapter are refused with exit code 2. Archives are excluded at every scope. Narrowed reports name the scope and the number of documents examined.

For a project that normally checks in-flight work, add this to .specsyrc.json:

{
  "format": "auto",
  "scope": "changes",
  "rules": {}
}

CLI and MCP read and validate the configuration nearest to the target directory. MCP lint_specs accepts the same scope values and its existing change argument. Explicit arguments override the configuration. The JSON reporter includes a scope object, such as {"kind":"changes"} or {"kind":"change","id":"add-auth"}.

--max-warnings accepts non-negative safe integers, including 0. footprint --turns accepts positive safe integers. Invalid values, including decimals and trailing text, are refused rather than partially parsed.

npx specsy --help lists every command with when to reach for it. A mistyped command is caught rather than read as a directory name:

$ npx specsy footprnt
Unknown command "footprnt".

Did you mean:  npx specsy footprint
Commands:      rules  explain  footprint  mcp

Run 'npx specsy --help' to see what each one is for.

Exit codes:

| Code | Meaning | |---|---| | 0 | No errors. Warnings alone do not fail — add --max-warnings 0 if they should | | 1 | One or more errors, or warnings above --max-warnings | | 2 | specsy could not run: bad config, unknown format or reporter, or nothing was examined |

--quiet prints errors only. It is a display filter — warnings are still found, still counted, and still fail the run when they exceed --max-warnings.

A run that examines zero documents exits 2, never 0. A config whose ignore matches every file is a CI gate that passes because it checked nothing, which is worse than one that fails.

Supported formats

| Adapter | Reads | |---|---| | openspec | openspec/changes/<id>/{proposal,design,tasks}.md plus delta specs and the living specs/ tree | | generic | any directory of markdown — opt-in, via --format generic. Never auto-detected: "a folder containing markdown" describes a home directory as readily as a spec folder |

Spec Kit and Kiro adapters are the next ones planned. An adapter only has to describe a directory layout; all parsing and every rule is shared. See src/adapters/.

Rules

16 rules, all deterministic — no model calls, no API key, no network. Run them on every commit for free.

| Rule | Default | Applies to | What it catches | |---|---|---|---| | no-weasel-words | warn | spec, proposal | Subjective words that cannot be turned into a test. | | quantify-performance | warn | spec, proposal | Performance claims with no number and unit. | | use-normative-keywords | warn | spec | Soft modals ("will", "needs to") instead of MUST / SHOULD / MAY. | | one-requirement-per-statement | warn | spec | Sentences bundling several obligations into one. Line wrapping does not affect it. | | no-ambiguous-pronoun | warn | spec | Requirements opening with a pronoun that has no antecedent. | | require-acceptance-criteria | error | spec | Requirements with no scenario to verify them. | | no-placeholders | error | all | TODO/TBD markers and unfilled template slots. | | no-empty-sections | warn | all | Headings with nothing beneath them. | | require-sections | warn | proposal, design | Documents missing the sections their kind expects. | | no-implementation-in-requirements | warn | spec | Requirements naming a library instead of a behaviour. | | unique-requirement-ids | error | all | Two requirements sharing an id, anywhere in the change. | | tasks-reference-requirements | warn | tasks | Tasks not linked to any requirement. †| | no-dangling-references | error | tasks | Tasks citing a requirement id that does not exist. | | requirements-have-tasks | warn | spec | Requirements no task implements. †| | require-non-goals | warn | proposal | Proposals that never say what they are not doing. | | require-requirement-ids | warn | spec | Requirements with no stable id to cite. †|

† These three rules switch themselves on. Requirement ids and task citations are disciplines a team opts into — no spec format mandates them, and OpenSpec's tasks.md never cites requirements. Demanding them unconditionally would flag every line of a perfectly good spec, so each rule stays silent until the change already uses the convention somewhere. Adopt ids on one requirement and the rest of the spec is held to it.

Code fences are never linted, so example snippets inside a spec stay untouched.

Requirement id format. An id is PREFIX-N — two to five uppercase alphanumerics, a hyphen, then digits: REQ-001, ACCT-14, TXN-7. It must open the requirement's heading (### Requirement: ACCT-001 Account creation), and ids must be unique across the whole change, not merely within one file — a task citing ACCT-001 cannot tell two spec.md files apart. Ids are optional; see the † note above.

Standards references are not requirement ids. ISO-4217, ISO-8601, RFC-3339 and SHA-256 match the shape of an id but are excluded, and a requirement takes its id only from the start of its heading (### Requirement: REQ-014 Account creation) — a mention in prose never christens one.

Proposals are linted as prose. A proposal rarely contains formal MUST requirements, so the clarity rules read every line of it — except the motivation sections (Why, Background, Context, Problem, Rationale). "Search feels slow" there is a problem statement, not an unmeasurable requirement, and is left alone.

Use it from an agent (MCP)

Without this, the loop has a human in it: the agent writes a spec, you run specsy, you paste the output back. With it, the agent lints its own spec and fixes it before writing any code.

claude mcp add specsy -- npx -y specsy mcp

Or commit a .mcp.json at your repo root so the whole team gets it:

{
  "mcpServers": {
    "specsy": { "command": "npx", "args": ["-y", "specsy", "mcp"] }
  }
}

Cursor, Windsurf and Zed take the same command/args pair in their own MCP config. The server speaks stdio and needs no API key.

| Tool | What the agent uses it for | |---|---| | lint_specs | Check specs before implementing. Returns each finding with file:line and a concrete fix. | | explain_rule | Understand a finding, with a failing and a passing example. | | list_rules | Learn the bar before writing a spec. | | spec_footprint | See what a change costs in context tokens. | | specsy_usage | See what specsy itself has added to the conversation. |

Context cost

A spec is not read once — an agent re-reads it on every turn, so its size is a recurring cost:

npx specsy footprint
add-core-ledger  11k tokens
    2.8k  design        openspec/changes/add-core-ledger/design.md
    2.1k  spec          openspec/changes/add-core-ledger/specs/ledger/transactions/spec.md
    ...
          re-read over 8 agent turns: ~86k tokens, ~$0.43

Counts are estimated locally (±15%), with no network call and no API key — the same promise as the rules. --exact uses Anthropic's count_tokens endpoint for real, model-specific numbers when a credential is available.

specsy cannot see your agent's total token usage — that belongs to Claude Code or Cursor and its API account. What it measures is the specs themselves, and its own contribution to the conversation. Those are the parts of the bill it is honestly able to see.

Configuration

Drop a .specsyrc.json anywhere at or above the directory you lint:

{
  "format": "auto",
  "rules": {
    "require-requirement-ids": "error",
    "no-implementation-in-requirements": "off"
  },
  "weaselWords": ["performant-ish", "TBC"],
  "ignore": ["**/vendor/**"]
}

Every rule takes "error", "warn" or "off". weaselWords is merged with the built-in list, not replaced. ignore takes globs and drops matching files from the run.

no-weasel-words allows "most recent" and numeric bounds such as "at most 10". It still flags unspecified quantities such as "most users" and "at most several".

The config is validated before anything acts on it — a misspelled rule id or an unknown setting exits 2 rather than being ignored:

Invalid config in .specsyrc.json:
  Unknown rule "no-weasle-words". Did you mean "no-weasel-words"?

In CI

- run: npx specsy --reporter github --max-warnings 0

The github reporter emits workflow annotations, so findings land inline on the pull request diff.

As a pre-commit hook:

npx specsy --quiet || exit 1

Programmatic use

import { detectAdapter, lint, formatJson } from "specsy";

const adapter = await detectAdapter("./openspec");
const project = await adapter.load("./openspec");
const result = lint(project, { rules: { "no-weasel-words": "off" } });

console.log(result.errorCount, result.diagnostics);

The CLI is a thin wrapper over exactly these exports.

Roadmap

v0.1 is the lint layer. The two things that follow are where this gets genuinely hard, and genuinely valuable:

  • spec trace — build the requirement → task → file → test graph and report what is uncovered on either side.
  • spec drift — diff the code against the last archived spec and flag requirements whose implementation changed without a spec delta. Specs rot the moment a hotfix lands, and nothing today detects it.

Adapters for Spec Kit and Kiro, and a rule-authoring guide, are also planned.

Contributing

A new rule is one file in src/rules/ and one entry in the registry. Rules see a neutral model (src/model.ts), never raw markdown, so a rule written once works across every format.

pnpm install
pnpm build
pnpm test

To run this source checkout with npm (on Windows PowerShell, use npm.cmd):

npm run build
npm run lint:spec
node dist/cli.js --help
npm test

Tests build the CLI first. Publishing runs the build and full test suite through prepublishOnly; no global pnpm installation is required for that step.

Please add a fixture case in tests/fixtures/ for anything a new rule should — and should not — flag. False positives are worse than missed findings here: a linter people mute is a linter that does nothing.

License

MIT