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

boundry

v0.8.1

Published

Compile a C4 architecture diagram into a deterministic dependency linter. Deterministic architectural guardrails for AI agents and humans.

Readme

Boundry

Compile a C4 architecture diagram into a deterministic dependency linter.

You draw the allowed architecture once, as a LikeC4 diagram. Boundry turns it into a dependency-cruiser ruleset and checks your code against it — locally and in CI. No model calls, no heuristics, no judgement: the architecture you drew is the linter.

It's built for a world where AI agents write most of the code. Review doesn't scale and LLM-judge supervisors are non-deterministic; Boundry gives agents a hard boundary they can't cross instead of a suggestion they might.

diagram (LikeC4)  ──►  boundary model  ──►  dependency-cruiser rules  ──►  ✓ / ✗
     you draw            source-agnostic         generated                 the gate

How it works

  1. You annotate each element in your diagram with the source it owns — a folder, metadata { folder 'src/domain' }, or a single file, metadata { file 'src/ports/contract.ts' }.
  2. Every relationship you draw (a -> b) is an allowed dependency. Anything you don't draw is forbidden.
  3. Boundry lifts the diagram into a source-agnostic boundary model, compiles a dependency-cruiser ruleset from it, and runs the linter over your code.

Elements without a folder (actors, external systems, notes) are ignored, so a rich communication diagram and an enforcement diagram can be the same file.

Governing a whole root (opt-in)

By default a folder no element maps to is ignored — any module may import it. That is what keeps a communication diagram usable as an enforcement diagram, but it means brand-new, unmodelled code is free to import. Declare a root as fully governed and the whole tree becomes the universe instead:

system app 'App' {
  metadata { governRoot 'src' }
}

Now importing anything under src/ that no module claims is a violation, and check warns about code under the root that no module covers. That is the mirror of the zero-files warning: the model failing to cover the code is as much a gap as the code failing to back the model. Nothing changes unless you declare it.

A mapped folder claims its entire subtree, so the abstraction level stays yours: one box on src/domain covers everything beneath it. You add finer boxes where you want finer rules, not to satisfy the coverage check.

Mapping a single file (deep nested diagrams)

A rich C4 model nests: applicationmetricsportsstory-points-readstub, with a drill-down view at each level. Sometimes a file is the guarded thing — a contract, a port, a single repository — sitting beside its sibling sub-folders. Map it to a file instead of a folder:

component ports 'ports' {
  metadata { folder 'src/ports' }

  component store 'in-memory-store' {
    metadata { file 'src/ports/in-memory-store.ts' }   // a leaf, not a folder
  }
  component read 'story-points-read' {
    metadata { folder 'src/ports/story-points-read' }
    component stub 'stub' {
      metadata { folder 'src/ports/story-points-read/stub' }
    }
  }
}

stub -> store   // a legal cross-subtree edge

This is what lets a deep tree be both the documentation and the enforcement model. Collapse that file into its parent folder and the edge becomes stub -> ports — a descendant importing its ancestor, which LikeC4 rejects with Invalid parent-child relationship. As a file leaf, store is a sibling, so the edge is legal and Boundry governs the file exactly: only stub may import it, and the surrounding ports folder no longer owns it.

A file module owns exactly its file; a folder module owns its subtree minus any mapped descendants — nested folders and file leaves alike.

Exempting test files and ambient declarations

Every file under a mapped folder is governed as a rule source by default — including tests. An integration test that wires a real database adapter across a layer boundary is doing its job, not breaking the architecture. Exempt them:

system app 'App' {
  metadata {
    exemptImporters '/__tests__/|\\.d\\.ts$'
  }
}

Matched files are still analysed, but they are dropped from every rule's from side, so they may import anything. This is from-side only — they stay governed as import targets, so production reaching into a test helper is as forbidden as it ever was. Patterns are regexes, union across every element that declares one, and change nothing unless you declare one.

Double your backslashes. LikeC4 processes string escapes, so '\.d\.ts$' arrives as .d.ts$, where . is a wildcard that matches far more than you meant. Write '\\.d\\.ts$'. Boundry warns when an exemption matches zero files, or when one matches every file, since both mean the pattern is wrong.

An exemption is a grant — it lifts whole files out of every rule — so verify reports one added since the approved base, exactly like an undrawn edge.

The composition root — #anything

Every repo has one place that legitimately imports everything: the entry point that constructs the object graph. Give it a module and wire it to a wildcard:

  // Owns 'src' minus every mapped descendant — the loose files at the root.
  module entry 'Composition root' {
    metadata { folder 'src' }
  }
  element anything 'Anything' {
    #anything
  }

  entry -> anything

A box tagged #anything maps to no folder and stands for "the rest of the code". A module with an edge into it is exempt from every rule — and only that module; the exemption doesn't leak.

The alternative was to leave src/index.ts unmapped, which grants it the same freedom by omission: nothing drawn, nothing to review. The wildcard makes the exemption a visible box someone approved on purpose.

See it

The diagram you draw is the whole spec. Below is the example architecture that Boundry's own end-to-end suite enforces — a hexagonal model with a pure DDD core, CQRS, and a public-API boundary.

Explore every view interactively →

Top-level layers

Top-level layers: entry point, domain, application, infrastructure

Inside the domain — the rules are what's not drawn

Aggregates compose Entities and hold Value Objects; Entities may reach Value Objects but never Aggregates; Value Objects import nothing. Every missing arrow is a forbidden dependency Boundry will reject.

Domain internals: aggregates, entities, value objects

Install

npm install --save-dev boundry

Requires Node 20+. likec4 and dependency-cruiser come along as dependencies.

Quickstart

Draw your architecture — arch/architecture.likec4:

specification {
  element module { style { shape rectangle } }
}

model {
  module domain 'Domain' {
    metadata { folder 'src/domain' }
  }
  module infra 'Infrastructure' {
    metadata { folder 'src/infra' }
  }

  // Allowed dependency. Everything not drawn is forbidden.
  infra -> domain
}

views {
  view index { include * }
}

Check your code against it:

npx boundry check --arch arch src
# ✓ no boundary violations                          (exit 0)
# ✗ src/domain/user.ts → src/infra/db.ts [boundary-domain]   (exit 1)

A domain → infra import is now a build failure; infra → domain is fine.

Changing the architecture — propose, approve, commit

If agents can edit the diagram, they can grant themselves any dependency they like, and the guardrail is theatre. So a change to the architecture goes through a lifecycle:

proposeapprovecommit

An agent blocked by a boundary adds the edge with a marker:

  domain -> shared #proposed

A #proposed edge is intent, not permission. It's excluded from the allow-list, so check stays red and the agent stays blocked. It has asked, not taken.

A human approves by stripping the marker — that's what boundry approve does, deterministically, by splicing the LikeC4 CST. No model call, no reformatting: source-preserving, idempotent, byte-exact.

boundry verify  --arch arch   # any change vs the lock that skipped a marker?
boundry approve --arch arch   # HUMAN ONLY: strip markers = approve, update the lock

verify is the parity gate: it compares the working diagram against the accepted boundry.lock and rejects anything that changed without a marker — an addition without #proposed (a self-grant) or a removal without #proposal-delete (a silent deletion). check runs this gate before it enforces, so the diagram can never silently disagree with the lock. approve runs it too, so it won't accept un-annotated drift; then it enacts the markers and records the new accepted state to the lock.

The baseline is the lock, not a git ref: it's the state Boundry owns, so "accepted" never collapses into merely "committed". (One consequence, by design: the gate leans on approve being a human act — the lock moves only when someone approves. That's why the skill forbids agents from running it.)

To retire an edge or a box, propose its removal with #proposal-delete instead of deleting it. The marker colours it red; a pending deletion changes nothing (the edge stays allowed, the box stays enforced) so it never breaks the build. Then approve removes the marked edge or box outright:

  module legacy 'Legacy' {
    #proposal-delete
    metadata { folder 'src/legacy' }
  }
  api -> legacy #proposal-delete

So #proposed and #proposal-delete are the two halves of a change — an amber addition that approve makes permanent, and a red removal that approve takes away — each a visible mark on the diagram until a human acts.

Point your agents at .claude/skills/define-architecture-boundaries and they'll follow this protocol.

Catching drift — annotate (prototype)

verify catches drift and fails; annotate is the other half — it rewrites the same drift into a reviewable proposal. Both read the same baseline, the accepted boundry.lock that approve records beside the diagram, so they can never disagree about what "accepted" means.

boundry approve  --arch arch          # enact proposals AND write boundry.lock
boundry annotate --arch arch          # stage drift as #proposed / #proposal-delete

annotate is symmetric. An addition that drifted past the lock without a marker — a bare new edge or box — is rewritten in place as #proposed: it leaves the allow-list, so the silent grant becomes a red-again check awaiting approval. A removal — an element or edge that is in the lock but was deleted from the diagram — is re-materialised back into the source tagged #proposal-delete, reconstructing a deleted nested subtree in place and re-declaring its edges. So a restructure that removes a component is reviewable exactly like one that adds: the deletion comes back as a red proposal you can see in diff and enact with approve, rather than as silent drift a failing check only hints at.

It also paints the markers — every #proposed edge/box gets an intrinsic style { color amber }, every #proposal-delete a style { color red }, written onto the element itself. Intrinsic style is the one styling LikeC4 renders on every surface — base views and the "relationships of X" panel — so a proposal shows up highlighted wherever a reviewer looks, not only in the generated diff views (whose view-scoped rules stop at the view boundary). approve strips this styling back out with the marker: a #proposed edge returns to bare, a #proposed box stays but loses its colour, a #proposal-delete is removed outright. Requires LikeC4 ≥ 1.58 to render.

Reviewing a proposal — the diff view (prototype)

diff generates a single proposed-changes view — every pending change on one landing — into a derived boundry.diff.likec4:

boundry diff --arch arch              # (re)write boundry.diff.likec4
likec4 serve arch                     # open 'Boundry diff — proposed changes'

The highlighting is generated, not hand-styleddiff emits the LikeC4 rules into the derived file, so every #proposed box fills amber and edge goes amber + solid, every #proposal-delete red, deterministically. That closes the last manual seam: annotate marks, diff colours, no agent-dependent styling step. Unchanged elements keep their defaults, and the rules live only in the generated view, so your own views are untouched.

The view uses include * -> * where tag is #proposed (no bare include *), so it pulls in exactly the proposed edges and the leaf modules they touch — a deeply-nested proposal renders as its own coloured node instead of collapsing into a grey ancestor. A proposed module with no edge yet is included too, so a proposed-but-unwired box still shows. Each change nests under its layer/system for context.

For a small change you can ask for the old per-layer shape instead — one focused view … of <scope> for every layer that draws a change:

boundry diff --arch arch --per-layer

The file is a derived artifact: overwritten every run, removed when nothing is proposed, so it always matches the current diagram. approve deletes it too, as part of enacting — the moment the last proposal is approved the view is stale, so the post-approve workspace validates clean. It reads the diagram's own markers (not the lock), so it frames whatever annotate or a human has marked. Being derived, it's a .gitignore candidate (boundry.diff.likec4).

Rendering the coloured diff views needs LikeC4 ≥ 1.58 (the style-rule syntax Boundry emits). Boundry itself depends on that floor; the tool you review with (likec4 serve, the CLI, or the IDE extension) has to meet it too.

Governing an agent's change scope — #touch

The import rules govern the project's standing structure. A change scope governs a different axis: which components a single task may modify at all — its blast radius, pre-authorized before the agent writes any code. An agent's most common failure isn't a bad edge; it's touching far more than the task needed. Boundry can't infer intent, so it's declared and sealed up front, then enforced deterministically while the agent works.

declareapproveworkreviewseal

A plan-agent tags every component the task may modify with #touch:

  module orders 'Orders' {
    #touch
    metadata { folder 'src/orders' }
  }

…then runs boundry scope, which paints the tagged components green and draws a reviewable boundry.scope.likec4 — so a human can see the declared scope before sealing it. A human then approves it — sealing the set into an ephemeral boundry.changeset (cut from the current commit) and stripping the tags, so the committed diagram stays clean. The coding agent then works inside that envelope, and boundry check maps every file it changed to a module and fails on any component outside the scope — the leash it hits in its own loop, not an audit read afterward.

boundry scope   --arch arch          # plan-agent: paint the declared #touch scope green + draw boundry.scope.likec4
boundry approve --arch arch          # HUMAN: fold the diagram into the lock AND seal the #touch scope
boundry check   --arch arch src      # imports + change scope; exit 1 if a component was modified out of scope
boundry review  --arch arch          # green = authorized & modified · grey = authorized, untouched · red = out of scope
boundry seal    --arch arch          # HUMAN/CI: strip boundry.changeset before merge — only the lock reaches main

review also draws a navigable boundry.review.likec4 colouring each module by its state, so a reviewer can traverse the sealed scope against what actually changed. Like the touch set itself, the changeset is per-task and ephemeralseal removes it before merge, a natural CI gate ("no changeset on main"), so the trunk only ever carries the permanent lock.

Under a governRoot, the gate also catches additive work: a changed file that no module claims is a scope violation — the same rule that blocks unmapped imports — so a new folder tree can't slip past by being un-modelled. Model it (a new module + #touch, folded and sealed in one approve) or exempt it.

Crucially, approve seals a plan — it runs before the work, so the parity role lives in check, which gates every edit against the sealed scope. A blocked agent must not widen its own #touch set (only a human's approve seals one); it reverts, or asks for a wider scope. The skill spells out both roles. A repo with no boundry.changeset has no scope gate — the import rules apply alone.

CLI

boundry check    [--arch <dir>] [--cwd <dir>] [--base <ref>] [--strict-plan] [sources...]
boundry generate [--arch <dir>] [--cwd <dir>] [--out <file>]
boundry verify   [--arch <dir>] [--cwd <dir>]
boundry approve  [--arch <dir>] [--cwd <dir>] [--base <ref>]
boundry annotate [--arch <dir>]
boundry diff     [--arch <dir>] [--per-layer]
boundry scope    [--arch <dir>]
boundry review   [--arch <dir>] [--cwd <dir>] [--base <ref>]
boundry seal     [--arch <dir>]

| Flag | Meaning | | --- | --- | | --arch <dir> | LikeC4 workspace directory (all .likec4 files in it are merged). Default .. | | --cwd <dir> | Repo root to check. folder paths are relative to it. Lets you run from anywhere. | | --out <file> | generate only: where to write the dependency-cruiser config. Default .dependency-cruiser.cjs. | | --per-layer | diff only: emit one focused view per layer instead of the single proposed-changes view. | | --base <ref> | Change-scope commands: the git ref changes are measured from. Defaults to the sealed base (review/check) or HEAD (approve). | | --strict-plan | check only: warn on components authorized (#touch) but never modified. | | sources... | check only: paths to lint. Default src. |

  • check enforces the accepted boundry.lock (not the working diagram) against your code, and runs the linter. When a diagram is present it first runs the parity gate — rejecting any un-annotated drift between the diagram and the lock — then enforces. With no diagram, the lock alone enforces (engine-independent). When a boundry.changeset is present it also runs the change-scope gate, failing on any component modified outside the sealed #touch scope. Needs a lock; exits non-zero on any violation.
  • generate emits the dependency-cruiser config from the lock, so it matches exactly what check enforces.
  • verify is the parity gate on its own: it rejects any change between the diagram and the lock that skipped a marker — an addition without #proposed or a removal without #proposal-delete.
  • approve folds the diagram into the lock — the only op that changes what is enforced. It refuses un-annotated drift first, then enacts markers (strips #proposed, removes #proposal-delete) and writes boundry.lock. It also seals any #touch scope into boundry.changeset and strips the tags. For humans, not agents.
  • annotate stages drift as proposals, symmetrically: an undeclared addition becomes #proposed; an element/edge deleted from the diagram is re-materialised as #proposal-delete. Diffs against boundry.lock.
  • diff generates a single colour-coded "proposed changes" review view into a derived boundry.diff.likec4 (or, with --per-layer, one focused view per layer that draws a change).
  • scope makes a declared scope reviewable before approve seals it: it paints the #touch-tagged components green and draws a derived boundry.scope.likec4, and warns on any #touch that maps no files. The one change-scope command an agent may run — it changes no enforced state.
  • review colour-backs a task's sealed change scope against what actually changed — green (authorized & modified), grey (authorized, untouched), red (out of scope) — printing it and drawing a derived boundry.review.likec4.
  • seal strips the ephemeral boundry.changeset before merge, so only the permanent lock reaches main. Pairs with a CI "no changeset on main" check. For humans/CI, not agents.

Commit boundry.lock — it is the enforced contract, and CI checks against it.

Boundry warns (but does not fail) when a mapped folder matches zero files, and fails outright when a check analysed no files at all — so a passing check can never silently enforce nothing. A guardrail that fails open is worse than none.

CI

# .github/workflows/architecture.yml
name: architecture
on: [push, pull_request]
jobs:
  boundry:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      # Enforces the committed boundry.lock against the code, rejects any
      # un-annotated drift (the parity gate), and — when a boundry.changeset is
      # present — fails on any component modified outside the sealed #touch scope.
      - run: npx boundry check --arch arch src

If you use change scopes, add a gate so an ephemeral boundry.changeset never lands on main (it should be sealed just before merge):

      # On the default branch only: the changeset is per-task and must not persist.
      - run: test ! -f arch/boundry.changeset || (echo "run 'boundry seal' before merge" && exit 1)

Programmatic use (SDK)

The CLI is a thin wrapper over the SDK. Everything is pluggable — the diagram source and the target linter are both adapters behind ports.

import { readFileSync } from 'node:fs';
import { Pipeline, LikeC4Visualizer, DepCruiserEnforcer } from 'boundry';

const pipeline = new Pipeline(
  new LikeC4Visualizer('arch'),
  new DepCruiserEnforcer(),
);

// Enforce the accepted lock — the committed contract — against your code.
const lock = readFileSync('arch/boundry.lock', 'utf8');
const result = await pipeline.check(lock, ['src']);
if (!result.ok) {
  for (const v of result.violations) console.error(`${v.from} → ${v.to}`);
  process.exit(1);
}

Status & scope

Early but real — Boundry enforces its own architecture on itself, and ships an end-to-end test suite covering a hexagonal + CQRS + DDD model (pure domain core, read/write separation, a public-API boundary).

Today: TypeScript via dependency-cruiser, LikeC4 as the diagram source. Both are adapters, so more languages/linters and diagram formats can plug in without touching the core. Boundry now governs two axes off the one diagram: the import rules (standing structure) and a per-task change scope (#touch, an agent's pre-authorized blast radius).

Current limitations:

  • One element maps to exactly one path — a folder or a single file.
  • Nesting is supported: you can map a parent folder and its children. A parent's edges govern only the parent's own files — a child never inherits them and must be permitted explicitly.
  • folder paths are relative to the repo root (--cwd), not the diagram file.
  • With a governRoot, unmapped code is blocked as an import target but is not yet constrained as an importer — rules are generated per mapped module, so unmapped code has no rules of its own.

See the changelog for what's in each release. 0.1.0 is deprecated — it silently enforces nothing (see the changelog).

License

MIT © Maksymilian Piechota