{
  "markdown": "# bouncer\n\n**Static compliance-controls checker — the controls a regulation requires, verified in your code.**\n\n[![npm](https://img.shields.io/npm/v/@nugehs/bouncer?style=flat-square)](https://www.npmjs.com/package/@nugehs/bouncer) [![CI](https://img.shields.io/github/actions/workflow/status/nugehs/bouncer/ci.yml?style=flat-square&label=CI)](https://github.com/nugehs/bouncer/actions/workflows/ci.yml) [![license: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE) [![node](https://img.shields.io/badge/node-%3E%3D18-blue?style=flat-square)](#) [![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square)](#)\n\n**[nugehs.github.io/bouncer-web](https://nugehs.github.io/bouncer-web/)** (site)\n\n![bouncer demo](bouncer-demo.gif)\n\nbouncer verifies that the controls a\nregulation *requires* actually exist in your code — UK Online Safety Act, ICO\nChildren's Code (AADC), and Nigeria (NDPC, FCCPC, FIRS) — expressed as\ndeterministic **rule packs**. It runs in CI,\nexits non-zero when a required control is missing, and needs **no LLM**.\n\nIt checks IDs at the door so non-compliant code doesn't get in.\n\n> bouncer is an engineering aid, **not legal advice**. A green report means the\n> coded controls a rule looks for were found — it is not a substitute for a\n> compliance / DPO review.\n\n---\n\n## Why\n\nRegulators now expect *demonstrable* controls: age assurance, high-privacy\ndefaults for children, report/block affordances on user-generated content, a DPIA,\na risk assessment. Those are concrete things that either exist in a codebase or\ndon't. bouncer turns a regulation into a set of static checks over your repo, the\nsame way [tieline](https://www.npmjs.com/package/@nugehs/tieline) turns an API\ncontract into drift checks — the engine knows nothing about the law; the\n**rule packs** do.\n\n### bouncer vs semgrep / policy-as-code\n\nScanners like semgrep, CodeQL, or Snyk answer *\"is there bad code here?\"* — they\nhunt for vulnerabilities and dangerous patterns that **shouldn't exist**. bouncer\nanswers the opposite question: *\"does the code the regulation requires actually\nexist?\"* — age assurance on sign-up, report/block on UGC surfaces, high-privacy\ndefaults for children. A repo can be vulnerability-free and still fail every one\nof those obligations. Policy-as-code tools (OPA/Rego, Conftest) gate *configs and\ninfrastructure* against policy; bouncer gates *application source* against\nregulatory rule packs, with `file:line` evidence for every control and an honest\n`unknown` when a surface can't be located. In short: **semgrep finds\nvulnerabilities; bouncer proves required controls exist.** They complement each\nother — run both.\n\n## Install\n\n```bash\nnpx @nugehs/bouncer init\nnpx @nugehs/bouncer check\n```\n\nOr clone and run with plain Node (zero runtime dependencies, Node ≥ 18).\n\n## Usage\n\n```bash\nbouncer init [path]                 # write a starter bouncer.config.json\nbouncer check                       # run packs, print report, exit 1 on a missing control\nbouncer check --pack uk-aadc        # restrict to one pack\nbouncer check --status fail         # show only the failures\nbouncer report --out report.html    # self-contained HTML audit report\nbouncer list                        # every rule the configured packs apply\nbouncer explain <ruleId>            # what a rule requires + how it is checked\nbouncer packs                       # rule packs shipped with bouncer\nbouncer doctor                      # sanity-check config, adapter, packs\nbouncer mcp                         # start the MCP server (stdio)\n```\n\n### Verdicts\n\n| Verdict     | Meaning                                                                       |\n| ----------- | ---------------------------------------------------------------------------- |\n| **pass**    | the required control was found (evidence: `file:line`)                        |\n| **fail**    | the surface exists, but no evidence of the control was found                  |\n| **unknown** | the surface could not be located in this repo — *can't determine, not a pass* |\n\n`unknown` is deliberate: bouncer never reports a green pass for a surface it could\nnot find. Missing surface → honest \"can't determine\".\n\n## Configuration\n\n`bouncer.config.json`:\n\n```json\n{\n  \"target\": {\n    \"adapter\": \"next\",\n    \"repo\": \"./my-app\",\n    \"roots\": [\"app\", \"src\", \"components\"]\n  },\n  \"packs\": [\"uk-osa\", \"uk-aadc\"],\n  \"packDirs\": [],\n  \"ignore\": [],\n  \"failOn\": [\"fail\"]\n}\n```\n\n- `adapter` — how regulation *surfaces* (sign-up, profile, chat, livestream…) map\n  onto files for your stack.\n\n> **Adapters shipped today: `next` (App Router) and `react-native`.** That's it —\n> if your stack isn't covered, an adapter is a single small file mapping surface\n> aliases to file globs (see `src/lib/adapters/next.js`). **Adapter PRs are very\n> welcome** — `nuxt`, `sveltekit`, `remix`, `flutter`, `django` are all natural\n> candidates.\n- `packs` — which rule packs to run. Built-ins: `uk-osa`, `uk-aadc`, `ng-ndpc`, `ng-fccpc`, `ng-firs`.\n- `packDirs` — extra directories of your own `*.json` packs.\n- `ignore` — rule ids to skip.\n- `failOn` — which buckets make `check` exit non-zero (default `[\"fail\"]`).\n\n## Rule packs\n\nA pack is JSON. Each rule maps a *legal standard* to a static assertion over a\n*surface*:\n\n```json\n{\n  \"id\": \"aadc.geolocation-default-off\",\n  \"standard\": \"Standard 10 — Geolocation\",\n  \"severity\": \"high\",\n  \"surface\": \"profile\",\n  \"intent\": \"Geolocation must default to off for children.\",\n  \"fix\": \"Default any location-sharing setting to off.\",\n  \"assert\": {\n    \"find\": \"(geo|location)[^\\\\n;,]{0,30}(default|initial)[^\\\\n;,]{0,15}(false|off)\",\n    \"in\": [\"profile\", \"any\"],\n    \"expect\": \"present\"\n  }\n}\n```\n\nAssertion nodes:\n\n- `{ \"find\": \"<regex>\", \"in\": \"<surface|glob>\", \"expect\": \"present|absent\" }`\n- `{ \"allOf\": [ … ] }` · `{ \"anyOf\": [ … ] }` · `{ \"not\": … }`\n\n`in` accepts a surface alias (resolved by the adapter), an array of aliases/globs,\nor a raw glob. `expect: \"absent\"` flips the meaning — a match is a *violation*\n(used for nudge patterns, self-declared age checkboxes, etc.).\n\n### Surfaces (next adapter)\n\n`any`, `signup`, `auth`, `profile`, `chat`, `livestream`, `ugc`, `governance`.\n\n### Nigeria packs\n\nFor a platform operating in Nigeria, three packs ship built-in:\n\n- **`ng-ndpc`** — Nigeria Data Protection Act 2023 (NDPC): privacy notice, opt-in\n  & granular consent, data-subject rights, encryption, retention/erasure, NDPA\n  localization, DPO designation, 72-hour breach plan, cross-border safeguards.\n- **`ng-fccpc`** — consumer protection (FCCPC): blanket \"no refund / all sales\n  final\" clauses flagged as **void** — and high-precision, so a *tiered* or\n  conditional refund policy doesn't trip it — plus refund-policy present, no drip\n  pricing, explicit terms acceptance, terms of service present.\n- **`ng-firs`** — tax (FIRS): 7.5% VAT rate, VAT on commission, WHT on payouts,\n  VAT tax invoice.\n\nSome obligations are process, not code — NDPC registration, signed cross-border\nDPAs, the WHT remittance itself. Those rules surface as **gaps to track** (add\nthem to `ignore` with a note), not things a static scan can prove.\n\n## MCP\n\nbouncer is also an MCP server (stdio), so an agent can pull the same deterministic\nresults:\n\n| Tool               | Purpose                                                       |\n| ------------------ | ------------------------------------------------------------ |\n| `compliance_check` | run packs, return per-control verdicts + evidence            |\n| `list_rules`       | list rules the configured packs apply                        |\n| `explain_rule`     | a rule's standard, intent, fix, and how it is checked        |\n| `list_packs`       | available rule packs                                          |\n\n```jsonc\n// .mcp.json\n{ \"mcpServers\": { \"bouncer\": { \"command\": \"npx\", \"args\": [\"-y\", \"@nugehs/bouncer\", \"mcp\"] } } }\n```\n\n## CI\n\n```yaml\n- run: npx @nugehs/bouncer check\n```\n\nFails the build when a required control goes missing — e.g. someone removes an\nage-gate or a report button from a UGC surface.\n\n## Tests\n\n```bash\nnpm test    # node --test — zero dependencies, nothing to install\n```\n\nThe suite runs on Node's built-in test runner against throwaway fixture repos:\nglob/brace expansion, every assertion probe (`find`, `allOf`/`anyOf`/`not`,\n`allInFile` + `within` windows, `expect: \"absent\"`), the pass/fail/`unknown`\nverdict semantics, and pack loading. CI runs it on Node 18, 20, and 22.\n\n## License\n\nMIT\n\n---\n\n## Part of the toolchain\n\n**bouncer** is one of four tools that form a deterministic trust layer for AI-assisted development. Each answers a question people keep handing to an LLM — with static analysis instead.\n\n- [repoctx](https://www.npmjs.com/package/@nugehs/repoctx) — context: what does this change actually touch?\n- [tieline](https://www.npmjs.com/package/@nugehs/tieline) — contracts: did the front end and back end quietly stop agreeing?\n- **bouncer** (this tool) — compliance: could you defend this to Ofcom?\n- [aiglare](https://www.npmjs.com/package/@nugehs/aiglare) — governance: where can the model do something you can't undo?\n\nMore at [segunolumbe.com](https://segunolumbe.com). *static analysis, never the model.*\n",
  "bytes": 9250,
  "sha": "9d2a8659ff10735a9034806123a59ef5271d785d19441d8680343287f496740d",
  "repo_slug": "nugehs/bouncer",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_nugehs_bouncer_79d15ff7/readme"
}