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

espalier

v0.7.1

Published

An architecture compiler for agent-driven development

Readme

espalier

Espalier (n.): the horticultural practice of training a tree to grow into a deliberate, predetermined form against a supporting structure. The tree grows freely, but into the shape the gardener intended.

espalier is an architecture compiler for agent-driven software development.

It takes a single source-of-truth description of how a project should be shaped — where code goes, what it looks like, how it's written — and provides the two parts of a development loop:

  • documentation placed where agent tools already look, describing what belongs where and how it should be written
  • deterministic validation that catches drift when it happens

Both derive from the same rule files. The guidance states the rules before a change; the linter runs their checks afterwards.

The problem

Coding agents produce technically correct code. They struggle to produce architecturally consistent code over long execution paths.

Even with detailed prompts and carefully written instructions, context dilutes over time. A button component that was supposed to live in components/buttons/ ends up in lib/ui/. A new endpoint follows a pattern that doesn't match any of the existing ones. An import sneaks across a module boundary that's supposed to be closed. None of this is wrong in the sense that the linter or the type checker would catch — but it gradually erodes the shape of the codebase.

The usual response is to write better prompts, longer rule files, more detailed architectural docs. These help, but they share a structural problem: they are guidance only. There is no mechanism that closes the loop between "the agent was told what to do" and "the agent actually did it." When the guidance fails, nothing notices.

espalier closes that loop.

How it works

A project defines its intended architecture as a structured tree of rule files:

espalier
├─ components
│  ├─ buttons
│  │  └─ [button].tsx.mjs
│  ├─ dropdowns
│  │  └─ [dropdown].tsx.mjs
│  └─ layouts
│     └─ [layout].tsx.mjs
├─ models
│  └─ [domain].ts.mjs
├─ endpoints
│  └─ [endpoint].ts.mjs
├─ clients
│  └─ [apiProviderDirectory]
│     └─ [apiProviderImplementation].ts.mjs
└─ main.ts.mjs

Each structural rule file in this tree declares:

  • a content description — what kind of file this represents
  • a rule — the natural-language conventions that file must follow
  • a lint function — the deterministic check that verifies adherence
  • optionally, a template — a typed, documented way to create that kind of file

From this single source, espalier produces two standing surfaces and an optional construction entry point:

Documentation, written into the repository by espalier build:

Espalier places generated guidance at stable points in the repository tree. Each AGENTS.MD maps the structure below it, stopping where another generated document takes over, then describes the rules placed there. A reader starts at the root and follows the nearest generated document as it moves into a subtree.

Linters, run deterministically against the working tree by espalier lint:

After the agent writes or edits a file, espalier lint verifies the result. Findings include the applicable rule text by default. Output is human-readable unless --format jsonl is selected for machine consumption. The agent revises until validation succeeds. See espalier lint.

Creation, invoked for any structural rule by espalier create:

Given a concrete absent path, Espalier creates either its structurally owned file or its complete declared directory. File templates supply source and CLI flags; a directory composes the templates of all its required files. Creation never overwrites an existing path.

Guidance steers and the linter corrects. Both derive from the same rule file, so drift between the two is reviewable. Colocation does not prove the prose and the check are the same obligation.

espalier create <path> can create any structurally declared absent file or directory. A file selects its owning template; a directory creates every required descendant and namespaces each file's flags with its relative path. A rule without a template creates an empty file. Every renderer receives its concrete path and the owning rule's placeholder captures. espalier create <path> help describes either operation without writing. See espalier create.

The convergence loop

espalier does not try to make agents correct on the first attempt. It makes correctness reachable through iteration.

  1. The agent reads the project map at the repository root — always present, small, architectural.
  2. Working in a directory, it reads the nearest applicable generated guidance: what belongs there, what is required, and the conventions for each.
  3. After writing, espalier lint validates the result.
  4. Drift is reported as findings with the applicable rule text; --format jsonl provides machine-readable output.
  5. The agent revises until the project's constraints are satisfied.

Guidance steers; the linter corrects. Generated-document freshness prevents stale copies. Executable checks give the same answer for the same recorded inputs. Tests and review establish whether those checks implement the intended architecture. Incomplete, contradictory, or unsatisfiable policy remains the author's.

Quick start

Install the CLI on a machine running Node.js 24 or later:

npm install --global espalier

The global installation is only provisioning. espalier init records its exact version in the repository. Commands that load its configuration require that version; espalier migrate updates the configuration for the CLI that is running. espalier --version prints the installed version. It, help, and examples work independently of repository configuration. See espalier migrate for what migration changes. Projects may instead provide the same pinned executable through a local dependency, package runner, container, or system toolchain.

Start with an empty demonstration directory. It has no .gitignore, so this invocation explicitly declines reading one:

mkdir espalier-demo
cd espalier-demo
espalier init --lang javascript --no-ignore-file

init writes espalier.config.yaml, .espalierignore, and espalier/ holding a .gitkeep and the compact authoring contract at AGENTS.MD. Create README.MD with this content:

# Espalier demo

A minimal repository governed by Espalier.

Then create this structural rule file:

espalier
└─ README.MD.mjs
// espalier/README.MD.mjs

export const description = "the repository root README file";

export const rule = `
The root README must not be empty.
`;

export async function lint({ read, emit }) {
  const text = await read();
  if (text.trim().length === 0) {
    emit({ code: "empty_readme", message: "README is empty" });
  }
}

Generate the documentation your agent reads:

espalier build

Validate the repository:

espalier lint

The generated documentation is a build artifact. The espalier itself is the source of truth.

For an existing project with a .gitignore, use espalier init --lang javascript. Declaring only README.MD does not cover existing source files: each visible, governed file needs a structural owner. Add rules for those files, or use espalier adopt to infer a directory's structure and then fill in its requirements and checks. See espalier init for initialization options.

Projects that want only repository-specific guidance can omit Espalier's canned operational sections:

build:
  espalierGuidance: false

Rule types

There are three kinds of rule: structural rules, per-file constraints, and aggregates.

Structural rules describe what code exists, and where. They are positioned in the rule tree under root by their target file path:

components/buttons/[button].tsx.mjs

This rule can own files such as components/buttons/Submit.tsx, but not Submit.test.tsx: the filename capture cannot contain dots. The captured name is exposed to lint as captures.button. See Matching for literal dots, back-references, and explicit suffix alternatives.

For both files and directories, a placeholder pattern matching a strict subset of a sibling's names takes precedence; the broader sibling owns the rest. Overlapping patterns with no provably narrower winner are rejected. See Ownership for the full specificity rules.

Structural rules define the project's architectural shape. They appear in the project map, they carry the conventions written into each directory's documentation, and they are what makes an undeclared file a violation.

Per-file constraints check each selected file independently. Their paths contain a recursive placeholder. The filename can select a file extension:

[...path]/no-as-any.ts.mjs
backend/[...path]/no-fetch.ts.mjs

The first checks every owned, governed file with complete extension .ts in the project; the second checks those within backend/. foo.d.ts is not selected. To select both, write no-as-any.{ts,d.ts}.mjs.

Aggregates check selected files together and can enforce several conditions on the group, such as equal translation key sets or a fixture corpus's mix and averages. They use the same selection rules as per-file constraints and export aggregate = true:

src/[...path]/message-registry.json.mjs

Per-file constraints and aggregates do not own files. Every governed file must have one structural owner; constraints apply additional checks to the files they select. Which rule wins, and which files are required to exist, is specified in docs/MATCHING.MD (espalier help matching).

A constraint with no filename extension must instead export targets, a non-empty list of positive globs, such as ["*/queries.sql"]. A filename extension and targets cannot appear together. Both are selectors for structurally owned, governed files; neither changes ownership. The globs are relative to the prefix before [...name] and apply equally to per-file and aggregate constraints. targets = ["**/*"] selects every file type in the constraint's directory scope. In rule.up.sql.mjs, the entire .up.sql suffix is the extension selector.

Unlike a filename selector, a **/*.ts target glob also matches names such as foo.d.ts. Target globs use minimatch syntax, so **/*.{ts,tsx} selects both suffixes. See Glob syntax.

An aggregate runs once over its complete selected set, including an empty set. See docs/TYPES.MD for selection and context details.

Path placeholders

Square brackets define named path placeholders:

clients
└─ [provider]
   └─ [implementation].ts.mjs

[name] matches within one path segment and captures the matched text as a string. [...name] matches across zero or more complete path segments, captures them as an array of strings, and marks the rule as a constraint.

Two different placeholders never constrain each other. In the example above, both stripe/stripe.ts and stripe/client.ts match, because the second name captures whatever is there. When two parts of a path must agree, {name} says so:

clients
└─ [provider]
   └─ {provider}.ts.mjs

clients/stripe/stripe.ts matches. clients/stripe/client.ts does not. A back-reference captures nothing and introduces no name — it matches the text a [name] earlier in the path already captured, and it makes that file required for every provider directory that exists.

The alternative is a rule body asking politely and a lint function checking afterwards, for a fact the espalier can simply state. Stating it also puts the requirement in the generated documentation, where an agent reads it before writing the file rather than after.

The matcher normalizes [name] to *, [...name] to **, and {name} to *. Structural rules and per-file constraints receive a normalized context.pattern and matched values in context.captures. Aggregates receive context.patterns for their admission globs and context.matches for the selected files, each with its own path and captures.

Full grammar, matching precedence, and requiredness: docs/MATCHING.MD (espalier help matching).

Writing rules

Every rule file exports rule and lint. Structural rules also require a description. This structural example shows the exports; its check is an outline, not an implementation:

export const description =
  "a concise description of what this file represents";

export const rule = `Props must be a named exported interface \`<Name>Props\`.`;

import { createLinter } from "espalier";

export const lint = createLinter(async (context) => {
  // deterministic validation
});

That rule renders as: Props must be a named exported interface <Name>Props.

An ordinary template literal is the default. Reach for String.raw when the text contains backslashes and no backticks:

export const rule = String.raw`File names must match ^[A-Z][a-zA-Z0-9]*$.`;

createLinter provides contextual editor and TypeScript types and returns the same function unchanged. It is optional: ordinary export function lint(...) rules remain valid. Aggregate constraints have the corresponding createAggregateLinter helper.

For a structural rule, description briefly describes what the file is. For a constraint, it optionally summarizes the requirement. Use the project's vocabulary: "a payment provider client" or "checkout message IDs agree across locales." Put the full requirements in rule.

The rule describes the requirements for the file or selected group. The same requirements apply whether files are being created or edited, and the same text appears in generated guidance and reported findings.

The context for structural rules and per-file constraints describes one file. An aggregate receives the selected files together, with no singular path, pattern, or captures; its read calls must name a path. See docs/TYPES.MD (espalier help types) for the full shapes. Choosing a rule's kind, keeping policy in the rule file, and testing it is docs/AUTHORING.MD (espalier help authoring). espalier init writes the compact contract to espalier/AGENTS.MD, where an agent editing rules can read it.

To import helpers such as createLinter or the testing API from "espalier", install Espalier as a local development dependency at the exact version in espalier.config.yaml's pin. A global CLI installation alone does not make the package available to project imports.

Find or copy the examples packed with this CLI version:

espalier examples --help
espalier examples authoring-corpus
espalier examples authoring-corpus --copy ./scratch/corpus

The named command prints the installed path; --copy creates a writable copy in a new directory. See espalier examples.

// structural rule (e.g. components/buttons/[button].tsx.mjs)
async function lint({
  path,
  pattern,
  captures,
  read,
  emit,
}) {
  const code = await read();
}

// per-file constraint (e.g. [...path]/no-as-any.ts.mjs)
async function lint({ path, pattern, captures, read, emit }) {
  const code = await read();
}

// aggregate (e.g. fixtures/[...path]/corpus-mix.mjs with targets)
async function lint({ matches, patterns, read, emit }) {
  const contents = await Promise.all(matches.map(({ path }) => read(path)));
}

Rules execute independently, in unspecified order. Read governed repository data through read and files, and pass contents to helpers and addons. Shared analysis may memoize identical inputs when answers remain independent of rule order and previous calls.

Every applicable rule runs by default. espalier lint --cache enables optional persistent findings caching; see docs/CACHE.MD (espalier help cache) for when it helps and its dependency requirements.

The project layout

Generated guidance includes a map of the project's declared structure. For example:

    ./
    ├─ clients/
    ├─ components/
    │  ├─ buttons/
    │  ├─ dropdowns/
    │  └─ layouts/
    ├─ endpoints/
    ├─ models/
    └─ main.ts      the application entry point (required)

Espalier handles document placement and presentation. Authors declare the structure and write the guidance; readers can discover what belongs where and which files are required. Set fold: true in the root ESPALIER.MD when your tooling needs all guidance in one root document, or in a subtree to fold its guidance upward. See the build reference for output details.

CI/CD integration

This catches the case where the espalier changed but the committed documentation did not, preventing agents from being guided by stale guidance.

espalier build --check

Who changes the espalier

Architecture changes are design decisions to discuss. Before changing rules, architectural guidance, or configuration policy, the agent explains the gap, presents reasonable options and tradeoffs, and recommends an approach. The proposal identifies changes to placement, requiredness, checks, and exclusions, including guarantees it would remove. A path that passes lint can still be the wrong architectural home.

Once the design is agreed, the agent implements it without repeatedly asking for approval. Rules and governed code can change together to realize that design. A materially different design needs renewed discussion; ordinary code changes that follow the agreed architecture can proceed.

Prefer static nodes for known roles and dynamic patterns for instances sharing a defined responsibility and contract. Review inferred structure from adopt before treating it as the intended architecture.

Edit the corresponding ESPALIER.MD, rule files, or configuration, then regenerate guidance. Review the generated documents, test affected rules, run espalier lint, and check freshness with espalier build --check. Generated documents are outputs, not edit targets.

Espalier checks the current policy; it cannot establish that a policy change was agreed. Projects needing enforced review can require approval for rule and configuration changes through repository permissions and code review.

Design philosophy

espalier is strict by default and provides exactly one escape hatch per concern.

  • The structural model is strict. Governed files not matching the espalier are violations. ignoreFiles defines which paths Espalier can see; .espalierignore documents repository paths that remain visible but sit outside the project layout.
  • Severity is decided by the rule, not by configuration. Reusable checks ship as library functions that expose severity as an argument; the project's rule file passes the value it wants. Policy lives where the rule lives.
  • Issue codes are free-form strings. espalier does not validate uniqueness or namespace them automatically. Projects adopt their own conventions.

Every additional knob is a place where deterministic behavior leaks. Repeatable feedback is preserved by keeping escape hatches few and coarse.

Testing rules

Rules are code, and a lint function that quietly stops catching what it was written to catch is worse than no rule at all: the repository still lints clean, and nothing distinguishes that from conformance.

So espalier is a library as well as a CLI, and returns issues instead of printing them.

import { runRule } from "espalier";
import * as rule from "../espalier/components/[name].tsx.mjs";

const issues = await runRule(rule, {
  path: "components/Button.tsx",
  captures: { name: "Button" },
  tree: { "components/Button.tsx": "export default function Other() {}" },
});

Use runAggregate for a supplied set of matches and check for repository matching and selection. There is no espalier test command; these functions work with the project's existing test framework. See docs/API.MD (espalier help api).

What espalier is not

  • Not a replacement for general-purpose linters. ESLint, ruff, golangci-lint, and their peers do excellent work on language-specific code quality. espalier is about architectural enforcement: where code lives, what shape it takes, how modules relate. The two are complementary.
  • Not an agent. espalier does not invent code, generate suggestions, or decide architecture. It enforces decisions you have already made; create can write initial source from templates you supply.
  • Not an attempt to constrain agents. The goal is not to prevent agents from doing things — it is to give them a clear, machine-verifiable target so that correction is reachable through iteration. A well-shaped espalier makes the agent's job easier, not harder.