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

@symbo.ls/frank-audit

v3.14.772

Published

Source-level audit + verify-or-rollback fixer for Symbols projects. Detects sibling-imports, module-scope state, factory closures, legacy nested-syntax, and more — applies safe fixes that are verified against frank.toJSON and rolled back on regression.

Readme

@symbo.ls/frank-audit

Source-level audit and verify-or-rollback fixer for Symbols projects. Detects 59 classes of patterns across 10 rule families that either break frank's serialization, force frank's bundle-time fixer to guess, or violate the modern smbls runtime contract (design tokens, DOM bans, polyglot, helmet, router). Reports findings by default; --fix applies safe rewrites and verifies every change against frank.toJSON({scanAndFix:true}) — regressions roll back automatically. Findings the mechanical fixer refuses become structured prescriptions an LLM can answer with strict edit ops.

Schema: frank-audit/1.0. See RULES.md for the full per-rule reference.

Quick start

frank-audit                  # report findings (default — no writes)
frank-audit fix              # apply safe fixes with verify-or-rollback
frank-audit verify           # round-trip frank.toJSON; report bundleability

frank-audit looks for a symbols/ subdir of cwd; pass an explicit path as the first positional arg if not running from a project root.

Safety model

frank.toJSON already ships a runtime fixer (scanForSerializationIssuesclassifyFreeVarsrewriteProject) that resolves many free-variable patterns at bundle time. frank-audit's job is to clean up those patterns at the source so what you commit matches what frank ships — never to make a project less publishable.

To do that, every fix run:

  1. Measures a baseline with frank.toJSON({scanAndFix:true}) — records bundle success/failure and remaining scan-issue count.
  2. Snapshots every file before touching it.
  3. Applies fixes with risk-aware verify cadence:
    • safe rules (FA101, FA102, FA007, …) — pure mechanical rewrites, no per-fix verify.
    • medium rules (FA103, FA104, FA105, FA106, FA205, …) — verify after the rule completes; rollback the rule's edits if the bundle regressed.
    • risky rules (FA001, FA006, FA008, FA201–204, FA206, …) — verify after EACH fix; rollback that single fix on regression.
  4. Final verify. If the net result regresses against baseline, rollback ALL fixes.
  5. Refuses to fix on a broken baseline. If frank.toJSON already fails before we touch anything, the audit reports the findings and refuses to make it worse. Pass --force to override.

Every applied edit is also written to <symbolsDir>/.symbols_local/frank-audit/log as NDJSON keyed by opId for post-mortem inspection.

CLI reference

frank-audit <subcommand> [dir] [flags]

Subcommands

| Subcommand | What it does | | -- | -- | | audit | List findings. Read-only. | | fix | Apply auto-fixes (default: high-confidence only) with verify-or-rollback. | | verify | Run frank.toJSON; report isFrankable + residualIssues. | | prescriptions | Emit LLM-ready prescriptions for refused fixes (JSON). | | apply-edits | Apply a JSON file of edit ops produced by an LLM. | | explain <id> | Print docs for a rule (frank-audit explain FA205). | | serve | Start the HTTP server (default 127.0.0.1:7117). | | help | Print usage. |

Flags

| Flag | Effect | | -- | -- | | --json | Machine-readable JSON output. Implied when FRANK_AUDIT_JSON=1. | | --strict | Exit non-zero on critical+high findings (CI mode). | | --rule FA101,FA205 | Whitelist a subset of rules. | | --aggressive | Apply medium-confidence fixes too (with fix). | | --dry-run | Compute mutations, don't write. | | --no-verify | Skip the frank.toJSON round-trip — faster, less safe. | | --force | Apply fixes even on a broken baseline. DANGEROUS. | | --ops <path> | JSON edit-ops file (with apply-edits). | | --port <n>, --host <ip> | Bind address for serve. | | --no-color, -v / --verbose, -h / --help | Standard. |

Exit codes: 0 clean / no findings, 1 findings detected (under --strict) or fix rolled back or op error, 2 CLI usage error.

Programmatic API

Two import surfaces — pick the one that matches your runtime:

// Local Node — fs + verify-or-rollback fixer + HTTP server
import { audit, fix, verify } from '@symbo.ls/frank-audit'           // = './fs'
import { applyEditOps } from '@symbo.ls/frank-audit/fs'

const a  = await audit('/path/to/symbols')
const fr = await fix('/path/to/symbols', a.findings, { aggressive: false })
const v  = await verify('/path/to/symbols')
// Worker / browser / pure-content — no fs, no subprocess
import {
  auditContent, auditFiles,
  validateEditOps, applyContentEditOps,
  ALL_RULES, SCHEMA_VERSION
} from '@symbo.ls/frank-audit/core'

const r = auditContent(jsSourceString, { file: 'Card.js', slot: 'components' })

The core tier is what the HTTPS MCP bundles for Cloudflare Worker mode. Anything that needs to touch disk, spawn frank, or run verify-or-rollback lives in fs. The default entry re-exports both.

Result envelopes

Every JSON-emitting surface returns the same envelope shape:

{
  schema: 'frank-audit/1.0',
  opId:   'audit-…',          // per-run UUID, sortable
  startedAt, completedAt,     // ISO timestamps
  …payload
}

fix results add applied[], skipped[], edits[], errors[], baseline, finalState, rolledBack, passes. apply-edits results add mutatedFiles[], createdFiles[], deletedFiles[]. Errors come back as { ok:false, error: { code, message, category, retryable, details } } — see Error categories.

HTTP server mode

frank-audit serve --port 7117

Wraps the fs-tier API in REST endpoints so other tools (MCP servers, CI runners, in-app debug panels) can call frank-audit without a subprocess hop.

| Endpoint | Body | | -- | -- | | GET /health | — | | POST /audit | { dir, ruleIds? } | | POST /fix | { dir, ruleIds?, aggressive?, dryRun?, verify?, force? } | | POST /verify | { dir } | | POST /prescriptions | { dir } | | POST /apply-edits | { dir, ops, verify?, dryRun? } | | POST /audit-content | { code, file?, slot?, ruleIds? } (no fs needed — pure) |

Every response carries the schema field and the same envelope shape as the CLI's --json output.

MCP integration

Both Symbols MCP servers delegate to frank-audit. They expose the same three tools to the LLM:

| MCP Tool | What it does | | -- | -- | | audit_and_fix_frankability(dir, mode) | mode='report' runs audit; mode='safe-fix' applies mechanical fixes via verify-or-rollback. | | prescribe_frankability_fixes(dir) | Returns LLM-ready prescriptions for findings the mechanical fixer refused. | | apply_frankability_edit_ops(dir, ops) | Applies LLM-emitted edit ops (validated, verified, rolled back on regression). |

Python stdio (symbols-mcp): _run_frank_audit shells out to the frank-audit CLI. Override the binary via FRANK_AUDIT_BIN=/path/to/frank-audit, or point at the HTTP server with FRANK_AUDIT_URL=http://127.0.0.1:7117. Single source of truth — no parallel rule implementation.

Node HTTPS (server/packages/symbols-mcp): same delegation via frank-audit-bridge.js, packaged as both Express server and Cloudflare Worker. The Worker target imports the core tier directly so the bundle stays Worker-safe; the Express server proxies to a local frank-audit HTTP server when one is reachable, otherwise spawns the CLI.

The LLM workflow is documented in symbols://skills/frank-fix-workflow (bundled into get_project_rules()).

Edit-op contract

Every fix the audit produces — and every fix an LLM can propose — is one of these 10 strict op kinds. Validated by validateEditOp before any IO, so malformed ops fail fast.

{ "kind": "removeImport",       "file": "...", "specifier": "...", "source": "..." }
{ "kind": "moveFile",           "from": "...", "to": "..." }
{ "kind": "addToIndexFile",     "dir": "...",  "filename": "..." }
{ "kind": "addToGlobalScope",   "name": "...", "value": <json>, "valueIsCode": false }
{ "kind": "removeTopLevelDecl", "file": "...", "name": "..." }
{ "kind": "addElementScope",    "file": "...", "componentName": "...", "key": "...", "value": <json>, "valueIsCode": false }
{ "kind": "replaceTokenValue",  "file": "...", "line": <n>, "oldValue": "...", "newValue": "..." }
{ "kind": "skip",               "reason": "..." }

One-line examples:

{ "kind": "removeImport",       "file": "components/Card.js", "specifier": "fmtMoney", "source": "../functions/utils.js" }
{ "kind": "moveFile",           "from": "utils/helpers.js", "to": "functions/helpers.js" }
{ "kind": "addToIndexFile",     "dir": "components", "filename": "Card.js" }
{ "kind": "addToGlobalScope",   "name": "TAX_RATE", "value": 0.07 }
{ "kind": "removeTopLevelDecl", "file": "components/Counter.js", "name": "count" }
{ "kind": "addElementScope",    "file": "components/Counter.js", "componentName": "Counter", "key": "count", "value": 0 }
{ "kind": "replaceTokenValue",  "file": "components/Card.js", "line": 12, "oldValue": "16px", "newValue": "A" }
{ "kind": "skip",               "reason": "intent unclear — needs human review" }

value is a JSON literal by default. Set valueIsCode: true to opt into a JS expression string (the validator parses it to verify syntax). The core exports validateEditOps and applyContentEditOps for callers that want to validate or simulate without touching disk.

Architecture

plugins/frank-audit/
├── bin/frank-audit.js         single CLI binary, all subcommands
├── index.js                   re-exports core + fs
├── src/
│   ├── core/                  Worker-safe — pure logic, no fs
│   │   ├── auditContent.js    auditContent / auditFiles entry points
│   │   ├── classifier.js      per-finding decision: scope vs globalScope vs leave vs ask
│   │   ├── constants.js       FRANK_DIRS, FLAT_HTML_ATTRS, KNOWN_GLOBALS, …
│   │   ├── contentIndex.js    in-memory index for pure auditing
│   │   ├── editOps.js         10 op kinds + validator + pure applier
│   │   ├── parser.js          babel + recast wrapper, source-preserving
│   │   ├── rules/             one file per rule (59 total), each exports
│   │   │                      { id, severity, confidence, risk, category,
│   │   │                        detect, fix?, explain }
│   │   ├── report.js          text + JSON output, --explain rendering
│   │   ├── scopeMoverHelpers  shared FA20x AST helpers
│   │   └── hardening/         schema versioning, opId, error categories,
│   │                          NDJSON audit-log builders
│   └── fs/                    Local Node — adds project ops + fixer
│       ├── audit.js           public API: audit, fix, verify
│       ├── indexer.js         walks symbols/, builds the project index
│       ├── fixer.js           atomic write + verify-or-rollback engine
│       ├── verifier.js        round-trip frank.toJSON
│       ├── parser.js          parseFile / writeBack
│       ├── prescriptions.js   builds LLM prescriptions from refused fixes
│       ├── applyEditOps.js    fs-tier op applier (snapshot + verify + rollback)
│       └── serve.js           HTTP server (frank-audit serve)
└── RULES.md                   full per-rule reference (generated)

Rule families

| Family | Count | What | | -- | -- | -- | | FA0xx | 5 | Discovery + structure (sibling-imports, orphan files, index completeness, name mismatches) | | FA1xx | 7 | Flat-syntax cleanup (el.props.Xel.X, on:{}onX, attr:{} flattening, handler signature) | | FA2xx | 6 | Scope movers — module-state, multi-file helpers/constants, single-component constants, factory closures | | FA3xx | 4 | Design-token enforcement (hex/rgb/hsl/raw px) | | FA4xx | 8 | Modern-stack bypass (window.location, fetch, axios, XHR, document.title, theme writes, polyglot) | | FA5xx | 12 | DOM bans (querySelector, classList, innerHTML, addEventListener, append/remove/insert, traversal) | | FA6xx | 4 | Icon bans (raw <svg>, inline SVG html) | | FA7xx | 1 | Polyglot hints (hardcoded English text — heuristic) | | FA8xx | 8 | Structural details (page-extends-page, lowercase keys, redundant Flex/Box/Text wrappers, extends variables) | | FA9xx | 4 | Advisory (unresolvable free vars, side-effect imports, component-as-function, circular globalScope) |

16 rules ship with auto-fix; 43 are detect-only and become prescriptions for the LLM.

Error categories

Every failure carries a structured category so consumers can decide what to do:

| Category | Meaning | Retryable | | -- | -- | -- | | user-error | Caller passed bad input (invalid path, malformed op, bad rule id) | no | | transient | Temporary condition (file lock, ENOMEM, network) | yes | | permanent | Structural problem (broken baseline, parse error in source, missing dep) | only after the user fixes the underlying issue | | internal | Bug in frank-audit itself | no — file an issue |

What --fix does NOT promise

  • It does not guarantee zero remaining findings. Many rules are detect-only by design (advisory, design-token catches, DOM bans), and even fixable rules are skipped when applying them would regress the bundle. Skipped findings appear under --verbose and become prescriptions via prescriptions.
  • It does not make every project frankable. Projects with deeper architectural issues (broken handler bodies, circular globalScope dependencies, side-effecting module-init) need either an LLM via the prescription path or human judgment.
  • It does not change frank's runtime behaviour. Frank's bundle-time fixer continues to do its work; frank-audit just reduces how much frank has to guess at.
  • It does not move files across packages or rewrite shared-library code. Anything resolved through sharedLibraries.js is read-only — if a fix would require editing a shared-package file, it is skipped with a refusal reason.

Decisions

  • Recast, not raw @babel/generator — preserves untouched formatting on --fix.
  • Source stays clean (bare references), frank does the __scope.X injection at bundle time. --aggressive opts into rewriting source to el.scope.X directly so devs see the mechanics.
  • Idempotent. Running twice with no source changes produces no edits.
  • Atomic with verify-or-rollback. Risky fixes verify against frank.toJSON before being kept; the whole batch rolls back if the net result regresses against baseline.

Further reading

  • RULES.md — full per-rule reference (every rule with severity, confidence, risk, auto-fix, examples).
  • symbols://skills/frankability — narrative companion to RULES.md (wrong-vs-canonical patterns).
  • symbols://skills/frank-fix-workflow — the LLM's reference card for the prescription → edit-op flow.
  • @symbo.ls/frank — the serializer this audit aligns the source against.