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

@weatherboard/gyde-design

v0.6.0

Published

Scaffolds a design system into a product repository, then keeps auditing it.

Readme

Gyde — design system scaffolding and gate

Gyde measures a repository's design system, scaffolds what is missing, and then keeps auditing what you build against it. It emits code you own, and it gates against a ledger that may only ever get shorter.

gyde-design scan <path>    measure a repository as it is
gyde-design plan <path>    say what would be emitted; write nothing
gyde-design init <path>    write it; never overwrites
gyde-design gate <path>    fail on anything new since the committed ledger
gyde-design tokens         print the stylesheet for the seed dictionary

What Gyde is

An always-on creative director. A product does not maintain its own review standards in-tree and hope they keep firing; it asks Gyde, and Gyde answers with a verdict.

The boundary in one line: Gyde decides whether it is valid; the product decides what it is.

Gyde owns the rules, the normalised representation they are written against, the ledger format, the templates, and the upgrade path that reaches an edited file.

Gyde never owns your components, your token values, your content, layouts, brand or product decisions, or your repository. Everything Gyde emits is yours from the moment it is written, and Gyde will not overwrite it. Gyde enforces the token contract; the values belong to you.

If design-system logic ends up living in your repo, that is a bug in the installation, not a feature of the product.

Authority

The only sanctioned escape valve is the allowance ledger: per file, per rule, per count, and only ever ratcheting down. There is no env var, no skip flag, and no supported way to make a failing gate pass by editing a baseline upward.

gate with no ledger records one and exits non-zero. Recording debt is the sanctioned way it enters — amnesty never — and a run that recorded rather than judged is not a pass.

Degraded mode fails loudly, never silently green. If Gyde cannot run, the gate fails or reports "Gyde did not run" as an explicit state that CI treats as failure. There is no configuration in which "could not run" produces a pass. This is load-bearing, not a preference: the most expensive failures this tool was built against were checks that reported success without executing — a paths: filter skipping a job, a package filter matching nothing and exiting 0.

Installing it

Repository CI takes Gyde as a GitHub Action. No dependency, no token, no .npmrc:

# .github/workflows/gyde.yml
name: Gyde
on:
  pull_request:
  push:
    branches: [main]
permissions:
  contents: read
jobs:
  design-system:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: Weatherboard-Studio/gyde@v1
        with:
          fail-on: new

Your dependency graph is untouched and no credential enters your CI. There is no install step inside the action either: the engine imports nothing but node: builtins, so checking the action out is the installation. A test pins that property, and a registry outage cannot fail your gate.

Laptops, sandboxes and agents take the package, because neither runs GitHub Actions:

npm i -D @weatherboard/gyde-design

The package name is under review as part of going public — see the scope question in docs/decisions/g-64-public.md.

fail-on: none still runs, still reports, still writes evidence, and still fails if the gate could not run. It only declines to block on the verdict, and it does so visibly. It is what an adopting repository uses for a fortnight while it decides whether the ledger is right. It is not what it uses forever. continue-on-error is not supported and never will be — it makes every failure look like a pass, which is the one outcome a gate exists to prevent.

The one idea

Every design-system audit we had was written against one repository's spelling, and each was correct locally and useless one repository over. One matched Tailwind utilities; another matched CSS declarations; neither would have found anything in the other's repo.

So the rules do not match source text. Five spellings of one decision —

rounded-lg              Tailwind utility
border-radius: 8px      CSS declaration
borderRadius: 8         JS / StyleX object
borderRadius: radius.card
var(--radius-card)      custom property

— all normalise to the same radius decision, and one rule asks the question it was always trying to ask: is this tokenised? rules.test.mjs proves it against all five.

The modules

| | | | --- | --- | | workspace.mjs | Discovers packages from the workspace declaration, never from directory names. Classifies each; every exclusion carries a reason. | | normalise.mjs | Reduces Tailwind utilities, CSS declarations, JS/StyleX objects and cva strings to one representation. | | rules.mjs | The violation rules, written once against that representation. Refuses to load a rule without both a failing and a passing fixture. | | scan.mjs | Walks a tree, classifies, scores. Never emits a percentage without its denominator. | | tokens.mjs | The dictionary shape, its guarantees, and generators for CSS, plain constants and DTCG. | | boundaries.mjs | Import and dependency boundaries, the exemption register, and version-drift detection. | | ratchet.mjs | The allowance ledger. A cleared entry is deleted, never zeroed. | | emit.mjs | The seed components and the two structural modules. | | catalogue.mjs | The catalogue, generated from the same list the barrel is. | | wiring.mjs | Is the design system actually connected to the app that uses it? | | usage.mjs | Which component is used where, and what the product keeps reinventing. | | upgrade.mjs | Provenance, and the three-way classification that uses it. | | agentdocs.mjs | What an agent building the product reads before writing UI. | | ruleindex.mjs | Every rule as data — id, name, intent, fix shape. Describes; never decides. | | selfgate.mjs | Emits a scaffold and gates it. Gyde held to its own rules. | | floor.mjs | Opt-in scoped adoption floors and their named structural exceptions. Fails closed on a scope that matches nothing. |

Measured, on the three repositories it was built from

De-identified, because the numbers are the evidence and the names are not.

| | product files | decls | tokenised | findings | | --- | ---: | ---: | ---: | ---: | | System A — mature system, CSS-declaration styling | 176 | 379 | 98% | 9 | | System B — mature system, low application adoption | 1,801 | 7,361 | 15% | 6,078 | | System C — shadcn-style, utility-first page code | 394 | 2,642 | 8% | 2,437 |

System A's 98% is corroboration worth having: that team independently reports 96% adoption from a rule set built on entirely different premises. Two points apart is the best evidence available that the normalised form measures the same thing a hand-written rule set does.

The two low numbers are not the same number twice. System B has a mature system its applications have not adopted. System C's apps import its UI package in 109 files and define zero bespoke components — its 8% is Tailwind utilities in page code, which is what shadcn expects you to write. A verdict that cannot tell those apart is worthless, which is why scan classifies before it scores.

Why plan and init share a call

Both go through one buildEmission(). The dry run cannot describe a file the real run would not write, and that is structural rather than a promise — a scaffolder whose two modes can diverge is one nobody can review before it writes into their repository.

init refuses to overwrite anything. Gyde emits once and the file becomes the product's; a scaffolder that clobbers has taken ownership of something it does not own, silently.

Importing a module directly

The barrel (@weatherboard/gyde-design) is the curated named surface. Every module the package ships is also reachable by its own subpath:

import { themeStartup } from "@weatherboard/gyde-design/theme-entry.mjs";
import pkg from "@weatherboard/gyde-design/package.json" with { type: "json" };

That is a stated guarantee rather than an accident of file layout, and exports.test.mjs fails if a shipped module is not reachable — so verifying your own repository never means reaching into node_modules by filesystem path. The two exceptions are named there with their reasons: the CLI, which is the bin and executes on import, and the vendored parsers under vendor/, which ship so the Action can run with nothing installed and are not ours to promise.

The rules, as data

A rule id reaches you three times — in a finding, in the rules stamp inside gyde-allowance.json, and in a failing gate — and every time it is a bare slug. optional-prop says what matched. It does not say what the rule defends or what the fix looks like.

So the rules are also available as data:

import { rules, rulesJson } from "@weatherboard/gyde-design/ruleindex.mjs";

rules();
// [
//   {
//     id: "optional-prop",
//     name: "Optional prop",
//     intent: "A prop a caller may omit, leaving the component to make the decision silently.",
//     fix: "Make it required. Where absence is itself a real answer, make it required and NULLABLE …",
//   },
//   …
// ]

rules() returns frozen entries sorted by id; rulesJson() is the same thing as a JSON string, and node ruleindex.mjs prints it. Use it to generate a rules page, to give an agent the fix shape alongside the finding, or to diff what a version change means.

It describes and it never decides. Nothing in the scan, the ledger or the gate reads it, and nothing may start to: a description that can change a verdict is a second copy of the rule kept in prose, and it will disagree with the first one quietly. The predicates stay in rules.mjs, props.mjs, compound.mjs and docdrift.mjs.

Completeness is checked in both directions, against the rules stamp that a real gate run wrote rather than a list typed out beside it. A rule implemented and not described fails; a rule described and not implemented fails too — an index naming a rule nobody runs is the shape that reads as coverage.

Gyde is held to its own rules

npm run verify emits a scaffold into a temporary directory, runs gyde-design gate over it through the CLI, and fails unless the recorded baseline is zero.

The check is on the baseline and not on the exit code, and that distinction is the whole of it. A first gate on a tree with no ledger RECORDS what it finds and exits 1 on purpose; the second run then passes whatever the first recorded. So a scaffold shipping ten violations gates green — correctly, for a customer adopting Gyde, and uselessly as a check on ourselves.

That is not hypothetical. The scaffold emitted on 2026-09-15 carried ten optional-prop findings across all five seed components, and the consuming repository found them, not us. The emitted set now declares every prop required, nullable where absence is a real answer, with the ordinary values in defaults.ts — exported from the package barrel, because an escape hatch behind a deep import into src/ is one the boundary rules forbid you to use.

Rules that go quiet

A rule that matched nothing anywhere is reported as suspicious, not clean. We have a recorded case of a rule reporting zero against 178 real hits with nobody able to tell. Silence and success must not render the same.

A number we got wrong

The first version of the table above read System B at 12% and System C at 4,061 findings. Every class inside a className attribute was counted twice, so utility-heavy repos were measured at roughly double. It was found end to end — a scaffolded fixture recorded seven findings for four real defects — and not by any fixture, which is worth remembering when reading the rest of this.

Two silences this makes audible

An app can score 100% adoption while rendering unstyled HTML. If it imports design-system components but never the token stylesheet, every var(--color-text) resolves to nothing. Nothing throws, the build passes, and the audit is delighted — only system imports, no literal values. wiring.mjs checks the link, following @import across workspace packages, and reports "could not tell" as its own outcome rather than as "fine".

A component nobody renders looks identical to one used everywhere. usage.mjs answers the two questions the audit cannot: does the set already compose this? and is anybody using what we shipped? Advisory, never blocking — a cluster of bespoke class names is evidence of a missing capability, and a gate that cannot tell that from a broken rule just makes the workaround harder to find.

A gap the catalogue cannot close

A closed component set cannot force its own interactive states. Select's open state is where theming across a portal boundary is proved, and exposing defaultOpen to demonstrate it would be an escape hatch opened for the catalogue's convenience. The generated catalogue records that as a note on the entry rather than printing "open" beside a control that is closed — a label that lies is worse than an admission.

The upgrade, which is the wedge

Gyde emits a component set, the product edits it — editing it is the point — and later Gyde improves the template. Reaching a file somebody has been working in is shadcn's openly unsolved problem: three multi-year issues, and its own docs call re-syncing "an ongoing responsibility rather than a solved problem".

init records provenance: the template version and a hash of each file's exact emitted bytes. That is the missing third point, and with it every file is one of four cases:

| | | | --- | --- | | neither changed | nothing to do | | Gyde changed only | safe to apply | | product changed only | their edit is the truth; left alone | | both changed | a conflict — reported, never merged |

It never auto-merges, never touches a file it has no provenance for, never deletes a file it stopped emitting, and never restores one the product deleted.

The subtle part: an unapplied change keeps its old baseline. Otherwise the next upgrade reads it as a product edit, and a conflict quietly becomes "yours" and is never offered again.

The one artefact that changes what gets written

.gyde/design-system.md is generated from the same metadata as the barrel and the catalogue, so it cannot name a component that does not exist — the failure it replaces is a hand-written registry pointing at a superseded generation of its own components.

Its most important section is what to do when the set cannot do the thing. Without it the honest agent and the lazy one produce the same bespoke CSS, and only one of them knew it was a compromise.

Gyde does not write into your CLAUDE.md. It emits a fragment for you to include — that file is the last one a tool should edit unasked.

For an existing product-owned design system, run gyde-design guidance . to check whether the generated guide matches its current component exports. Run gyde-design guidance . --write to refresh the guide, then review the diff. This command updates only .gyde/design-system.md and refuses to replace a handwritten file. It lists component names without guessing their props or claiming theme APIs that Gyde has not verified in that product.

A floor over a scope you have finished

The ledger answers has this got worse? That is the right instrument for a repository carrying four thousand findings and the wrong one for the opposite case: a product that has finished the work over a defined slice of its tree and wants the finishing to stay finished. An empty ledger cannot say that — "nothing was recorded here" is also what an unmeasured scope says.

So a product may declare a floor, and nothing more happens without one:

{
  "design": {
    "adoptionFloors": [
      {
        "scope": "apps/console/src",
        "minimum": 100,
        "exceptions": [
          {
            "file": "apps/console/src/frame/MediaFrame.tsx",
            "element": "iframe",
            "because": "A browser-owned element the design system cannot render or restyle from inside."
          }
        ]
      }
    ]
  }
}

| key | | | --- | --- | | scope | A path inside the repository. Not an npm scope — that is design.scope, a different key answering a different question. | | minimum | A whole number, 0 to 100. The fraction of eligible visual elements under the scope that must be the system's. | | exceptions | Elements excluded from the denominator. Each names one element, in one file, with one reason. |

gyde-design gate prints the figure with its numerator, its denominator and the scope, and prints the exceptions underneath it rather than inside it. A working example is in examples/scoped-floor.

100% means zero. The verdict is taken on the counts, never on the rendered percentage: 999 of 1,000 floors to 99% and does not meet a floor of 100. That is the whole reason this exists — a report reading "100%" over a denominator that still holds a bespoke element is a lie that looks like an achievement.

Exceptions cannot grow quietly. One entry excuses one element. Until an element is declared it is an ordinary bespoke element and it fails the floor, so the seventh exception costs a diff a reviewer reads. And an exception that stops matching anything is an error: a stale exemption is a line sitting open for whatever lands under that name next, and it is the only way the list could shrink invisibly. The same discipline the allowance ledger has — it may only ever get shorter, and never by accident.

One file + tag may repeat, but not with different reasons. An exception identifies its element by file and tag name matched in source order, so nothing records which <div> a reason was written about. Many elements sharing one argument is normal and safe — an inline-styled block of markup is genuinely one decision — and reordering them changes nothing a reader could act on. Two entries for one file and tag carrying different arguments is a different thing: a reorder reattaches each reason to an element nobody wrote it about, with the count, the figure and the verdict all unchanged. That is an error, and the fix is one reason true of the whole group, because that is the granularity the exemption actually has. No occurrence index is offered: an explicit ordinal makes the identity visible without making it stable, so the same reorder still reattaches the reasons, now with a number agreeing.

Every path fails closed. A malformed entry, an unknown key, a scope that does not resolve, a scope that matches no markup files, a scope whose files make no visual decision, an unreadable component barrel, an exception naming something that is gone — each is an ERROR that names the cause, and none of them is allowanceable or recordable as a baseline. The zero-file case is the one that mattered most: a floor over an empty denominator would report success forever, and a rule that matches nothing reads exactly like a rule that passes.

What a floor does not protect against

  • Gyde does not judge the exceptions. It checks that each names a real bespoke element and carries a reason somebody wrote. It cannot tell a true reason from a plausible sentence. What it buys is that every one of them is on the artefact, in the diff, with a name and a line number attached.
  • Element identity is still ordinal. The reason-collision check above is a check on the configuration, never on the source. A single entry for <div> in a file that later grows a second bespoke <div> forms no group, so nothing fires — and that entry now matches whichever comes first in source order, which may not be the one it was written about. Within an identical-reason group nothing is guarded at all, by design; if one member is later edited so the shared reason stops being true of it, the group still validates.
  • It measures markup, not rendering. An element is the system's if its tag is one of the design system's exports. A component that imports the system and then restyles it at the call site still counts — that is the styling layer's question, not this one.
  • The scope is a path, not a semantic boundary. Code moved out from under it leaves the denominator silently. The figure stays true and the coverage shrinks; nothing here notices. The scope is printed beside the figure precisely so that a reader can.
  • A small denominator is still a denominator. A scope of two hundred files where three carry markup reports three. Whether three is enough evidence is a judgement Gyde does not make for you — which is why the number is never printed without it.

Not built yet

Build-pipeline wiring for a compiled token layer — moot while the emitted tokens are plain CSS custom properties, and required the moment they are not. Service-side ledger and provenance storage; both are committed files where they want to be unforgeable. And the genericity check exists only as a test over the templates, not as a command anyone can run.

Status and licence

Pre-1.0. The action tag v1 names the action's input/output interface, which has been stable; the package is versioned independently and moves faster. No stability promise is made below 1.0.

The licence is being decided as part of making this public. Until a LICENSE file lands, all rights are reserved.