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

@petriflow/rules

v0.1.0

Published

Declarative rules DSL for PetriFlow tool gating.

Readme

@petriflow/rules

Declarative rules DSL for PetriFlow tool gating.

Declarative DSL

Write rules in a .rules file or as inline strings. The compiler turns each rule into a verified SkillNet.

# safety.rules
require backup before delete
require human-approval before deploy
block rm
limit push to 3 per session
limit push to 1 per test    # refill budget after each test
import { loadRules, createGateManager } from "@petriflow/rules";

const { nets, verification } = await loadRules("./safety.rules");

// Every net is verified at compile time
console.log(verification);
// [
//   { name: "require-backup-before-delete", reachableStates: 3 },
//   { name: "approve-before-deploy",        reachableStates: 2 },
//   { name: "block-rm",                     reachableStates: 2 },
//   { name: "limit-push-3",                 reachableStates: 5 },
//   { name: "limit-push-1-per-test",        reachableStates: 3 },
// ]

const manager = createGateManager(nets, { mode: "enforce" });

compile() also accepts inline strings if you prefer:

import { compile } from "@petriflow/rules";

const { nets } = compile(`
  require backup before delete
  block rm
`);

Rule types

require A before B — A must succeed before B is allowed. Resets after B fires.

require human-approval before B — B requires manual UI confirmation every time.

block A — A is permanently blocked.

limit A to N per session — A can fire N times total.

limit A to N per action — A can fire N times, budget refills when action fires.

Dot notation for action-dispatch tools

Many tools (Discord, Slack, WhatsApp) use a single tool name with an action field in the input. Use dot notation to gate specific actions:

const { nets } = compile(`
  require discord.readMessages before discord.sendMessage
  require human-approval before discord.sendMessage
  block discord.timeout
  limit discord.sendMessage to 5 per session
`);

discord.sendMessage means: tool name is discord, input has action: "sendMessage". The compiler generates a toolMapper automatically. Actions not mentioned in any rule pass through freely — discord.react, discord.readMessages, etc. are ungated.

Tool mapping with map

For tools where discrimination requires pattern matching (like bash commands), use map to define virtual tool names:

map bash.command rm as delete
map bash.command cp as backup
map bash.command deploy as deploy-cmd

require backup before delete
require human-approval before deploy-cmd

Syntax: map <tool>.<field> <keyword> as <name>

  • bash.command means: match against input.command of the bash tool
  • Bare words use word-boundary matching — rm matches rm -rf build/ but not format or mkdir
  • Unmatched bash commands pass through freely (nets abstain)
  • Works with any tool and field, not just bash: map slack.action sendMessage as slack-send
  • For complex patterns, use regex with / delimiters: map bash.command /cp\s+-r/ as backup

Syntax

  • One rule per line
  • # starts a comment (to end of line)
  • Blank lines are ignored
  • Tool names support dot notation (tool.action) for action-dispatch tools
  • map statements define virtual tool names via regex pattern matching
  • Accepts a multiline string or an array of strings

Verification

compile() automatically verifies every net by enumerating all reachable states. This catches unbounded nets, structural errors, and confirms each rule compiles to a finite, well-formed state machine. Verification runs at compile time — before your agent starts.

How rules compose

Each rule compiles to its own independent Petri net. At runtime, every net is checked on every tool call — a tool can only fire if all nets allow it.

require lint before test
require test before deploy

This produces two separate nets, not one. But the effect is transitive: deploy requires test (net 2), and test requires lint (net 1), so deploy implicitly requires lint → test → deploy.

This works because safety properties compose by intersection. If net A says "no deploy without lint" and net B says "no deploy without test," enforcing both gives you "no deploy without lint AND test." No coordination between nets is needed — they don't know about each other.

The practical consequence: each net is small enough to verify exhaustively (a few reachable states), but their combined enforcement covers complex multi-step policies. You get compositional guarantees without a combinatorial explosion.

Custom SkillNets

For complete control, build custom SkillNets with defineSkillNet:

import { defineSkillNet, createGateManager } from "@petriflow/rules";

const myNet = defineSkillNet({
  name: "my-custom-net",
  places: ["idle", "ready"],
  initialMarking: { idle: 1, ready: 0 },
  transitions: [
    { name: "start", type: "auto", inputs: ["idle"], outputs: ["ready"] },
    { name: "go", type: "auto", inputs: ["ready"], outputs: ["ready"], tools: ["my-tool"] },
  ],
  freeTools: ["read", "ls"],
  terminalPlaces: [],
});

const manager = createGateManager([myNet], { mode: "enforce" });

Composing rules and custom nets

DSL rules and custom SkillNets compose naturally:

import { compile, defineSkillNet, createGateManager } from "@petriflow/rules";

const { nets: dslNets } = compile("block rm");
const custom = defineSkillNet({ /* ... */ });

const manager = createGateManager([...dslNets, custom], { mode: "enforce" });