@nugehs/bouncer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@nugehs/bouncer` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-06-07
9
+
10
+ Initial release.
11
+
12
+ ### Added
13
+
14
+ - Static compliance-controls engine: deterministic rule packs evaluated against a
15
+ target repo, with `pass` / `fail` / `unknown` verdicts. `unknown` (surface not
16
+ located) is never reported as a pass.
17
+ - Assertion probes: `find`, `allOf` / `anyOf` / `not`, and `allInFile` with an
18
+ optional `within` line-window for co-occurrence precision.
19
+ - Built-in rule packs:
20
+ - `uk-osa` — UK Online Safety Act 2023 (age assurance, report/block on UGC,
21
+ content moderation, illegal-content risk assessment, CSEA route, terms).
22
+ - `uk-aadc` — ICO Children's Code (age-appropriate application, high-privacy
23
+ defaults, geolocation off, parental consent under 13, DPIA, no nudge patterns).
24
+ - Stack adapters mapping regulation surfaces to file globs: `next`, `react-native`.
25
+ - CLI: `check`, `report`, `list`, `explain`, `packs`, `init`, `doctor`, `mcp`.
26
+ - Self-contained HTML audit report (compliance ring, per-pack control tables,
27
+ file-level evidence).
28
+ - MCP server (stdio) exposing `compliance_check`, `list_rules`, `explain_rule`,
29
+ `list_packs`.
30
+ - Zero runtime dependencies; Node >= 18.
31
+
32
+ [0.1.0]: https://github.com/nugehs/bouncer/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nugehs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # bouncer
2
+
3
+ **Static compliance-controls checker.** bouncer verifies that the controls a
4
+ regulation *requires* actually exist in your code — UK Online Safety Act, ICO
5
+ Children's Code (AADC) — expressed as deterministic **rule packs**. It runs in CI,
6
+ exits non-zero when a required control is missing, and needs **no LLM**.
7
+
8
+ It checks IDs at the door so non-compliant code doesn't get in.
9
+
10
+ > bouncer is an engineering aid, **not legal advice**. A green report means the
11
+ > coded controls a rule looks for were found — it is not a substitute for a
12
+ > compliance / DPO review.
13
+
14
+ ---
15
+
16
+ ## Why
17
+
18
+ Regulators now expect *demonstrable* controls: age assurance, high-privacy
19
+ defaults for children, report/block affordances on user-generated content, a DPIA,
20
+ a risk assessment. Those are concrete things that either exist in a codebase or
21
+ don't. bouncer turns a regulation into a set of static checks over your repo, the
22
+ same way [tieline](https://github.com/nugehs/tieline) turns an API contract into
23
+ drift checks — the engine knows nothing about the law; the **rule packs** do.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npx @nugehs/bouncer init
29
+ npx @nugehs/bouncer check
30
+ ```
31
+
32
+ Or clone and run with plain Node (zero runtime dependencies, Node ≥ 18).
33
+
34
+ ## Usage
35
+
36
+ ```bash
37
+ bouncer init [path] # write a starter bouncer.config.json
38
+ bouncer check # run packs, print report, exit 1 on a missing control
39
+ bouncer check --pack uk-aadc # restrict to one pack
40
+ bouncer check --status fail # show only the failures
41
+ bouncer report --out report.html # self-contained HTML audit report
42
+ bouncer list # every rule the configured packs apply
43
+ bouncer explain <ruleId> # what a rule requires + how it is checked
44
+ bouncer packs # rule packs shipped with bouncer
45
+ bouncer doctor # sanity-check config, adapter, packs
46
+ bouncer mcp # start the MCP server (stdio)
47
+ ```
48
+
49
+ ### Verdicts
50
+
51
+ | Verdict | Meaning |
52
+ | ----------- | ---------------------------------------------------------------------------- |
53
+ | **pass** | the required control was found (evidence: `file:line`) |
54
+ | **fail** | the surface exists, but no evidence of the control was found |
55
+ | **unknown** | the surface could not be located in this repo — *can't determine, not a pass* |
56
+
57
+ `unknown` is deliberate: bouncer never reports a green pass for a surface it could
58
+ not find. Missing surface → honest "can't determine".
59
+
60
+ ## Configuration
61
+
62
+ `bouncer.config.json`:
63
+
64
+ ```json
65
+ {
66
+ "target": {
67
+ "adapter": "next",
68
+ "repo": "../bashbop-event-web",
69
+ "roots": ["app", "src", "components", "redux"]
70
+ },
71
+ "packs": ["uk-osa", "uk-aadc"],
72
+ "packDirs": [],
73
+ "ignore": [],
74
+ "failOn": ["fail"]
75
+ }
76
+ ```
77
+
78
+ - `adapter` — how regulation *surfaces* (sign-up, profile, chat, livestream…) map
79
+ onto files for your stack. Ships with `next` (App Router).
80
+ - `packs` — which rule packs to run. Built-ins: `uk-osa`, `uk-aadc`.
81
+ - `packDirs` — extra directories of your own `*.json` packs.
82
+ - `ignore` — rule ids to skip.
83
+ - `failOn` — which buckets make `check` exit non-zero (default `["fail"]`).
84
+
85
+ ## Rule packs
86
+
87
+ A pack is JSON. Each rule maps a *legal standard* to a static assertion over a
88
+ *surface*:
89
+
90
+ ```json
91
+ {
92
+ "id": "aadc.geolocation-default-off",
93
+ "standard": "Standard 10 — Geolocation",
94
+ "severity": "high",
95
+ "surface": "profile",
96
+ "intent": "Geolocation must default to off for children.",
97
+ "fix": "Default any location-sharing setting to off.",
98
+ "assert": {
99
+ "find": "(geo|location)[^\\n;,]{0,30}(default|initial)[^\\n;,]{0,15}(false|off)",
100
+ "in": ["profile", "any"],
101
+ "expect": "present"
102
+ }
103
+ }
104
+ ```
105
+
106
+ Assertion nodes:
107
+
108
+ - `{ "find": "<regex>", "in": "<surface|glob>", "expect": "present|absent" }`
109
+ - `{ "allOf": [ … ] }` · `{ "anyOf": [ … ] }` · `{ "not": … }`
110
+
111
+ `in` accepts a surface alias (resolved by the adapter), an array of aliases/globs,
112
+ or a raw glob. `expect: "absent"` flips the meaning — a match is a *violation*
113
+ (used for nudge patterns, self-declared age checkboxes, etc.).
114
+
115
+ ### Surfaces (next adapter)
116
+
117
+ `any`, `signup`, `auth`, `profile`, `chat`, `livestream`, `ugc`, `governance`.
118
+
119
+ ## MCP
120
+
121
+ bouncer is also an MCP server (stdio), so an agent can pull the same deterministic
122
+ results:
123
+
124
+ | Tool | Purpose |
125
+ | ------------------ | ------------------------------------------------------------ |
126
+ | `compliance_check` | run packs, return per-control verdicts + evidence |
127
+ | `list_rules` | list rules the configured packs apply |
128
+ | `explain_rule` | a rule's standard, intent, fix, and how it is checked |
129
+ | `list_packs` | available rule packs |
130
+
131
+ ```jsonc
132
+ // .mcp.json
133
+ { "mcpServers": { "bouncer": { "command": "npx", "args": ["-y", "@nugehs/bouncer", "mcp"] } } }
134
+ ```
135
+
136
+ ## CI
137
+
138
+ ```yaml
139
+ - run: npx @nugehs/bouncer check
140
+ ```
141
+
142
+ Fails the build when a required control goes missing — e.g. someone removes an
143
+ age-gate or a report button from a UGC surface.
144
+
145
+ ## License
146
+
147
+ MIT
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@nugehs/bouncer",
3
+ "version": "0.1.0",
4
+ "mcpName": "io.github.nugehs/bouncer",
5
+ "description": "bouncer — static compliance-controls checker. Verifies the controls a regulation requires actually exist in your code (UK Online Safety Act, ICO Children's Code), as deterministic rule packs. No LLM required.",
6
+ "type": "module",
7
+ "bin": {
8
+ "bouncer": "src/cli.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md",
13
+ "LICENSE",
14
+ "CHANGELOG.md"
15
+ ],
16
+ "scripts": {
17
+ "start": "node src/cli.js",
18
+ "check": "node src/cli.js check",
19
+ "mcp": "node src/cli.js mcp",
20
+ "doctor": "node src/cli.js doctor",
21
+ "test": "node --test tests/*.test.js",
22
+ "prepublishOnly": "npm test"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/nugehs/bouncer.git"
33
+ },
34
+ "homepage": "https://github.com/nugehs/bouncer#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/nugehs/bouncer/issues"
37
+ },
38
+ "keywords": [
39
+ "cli",
40
+ "mcp",
41
+ "mcp-server",
42
+ "compliance",
43
+ "online-safety-act",
44
+ "age-appropriate-design-code",
45
+ "childrens-code",
46
+ "aadc",
47
+ "age-assurance",
48
+ "static-analysis",
49
+ "policy-as-code",
50
+ "ci",
51
+ "developer-tools"
52
+ ],
53
+ "license": "MIT"
54
+ }
package/src/cli.js ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgv } from "./lib/args.js";
3
+ import { loadConfig } from "./lib/config.js";
4
+ import { runCheck, listRules, explainRule, availablePacks } from "./lib/packs.js";
5
+ import { reportHuman } from "./lib/reporters/human.js";
6
+ import { reportJson } from "./lib/reporters/json.js";
7
+ import { reportHtml } from "./lib/reporters/html.js";
8
+ import { getDoctorReport, formatDoctorReport } from "./lib/doctor.js";
9
+ import { initProject, formatInitSummary } from "./lib/init.js";
10
+ import { startMcpServer } from "./lib/mcp.js";
11
+ import { printText, printJson, printHelp, writeArtifact } from "./lib/output.js";
12
+
13
+ const commandHandlers = {
14
+ check: handleCheck,
15
+ report: handleReport,
16
+ list: handleList,
17
+ explain: handleExplain,
18
+ packs: handlePacks,
19
+ init: handleInit,
20
+ doctor: handleDoctor,
21
+ mcp: handleMcp,
22
+ help: handleHelp,
23
+ };
24
+
25
+ async function main(argv = process.argv.slice(2)) {
26
+ const parsed = parseArgv(argv);
27
+ const command = parsed.command ?? "check";
28
+ const handler = commandHandlers[command];
29
+
30
+ if (!handler || parsed.flags.help) {
31
+ handleHelp();
32
+ process.exitCode = handler ? 0 : 1;
33
+ return;
34
+ }
35
+
36
+ try {
37
+ await handler(parsed);
38
+ } catch (error) {
39
+ printText(`bouncer: ${error instanceof Error ? error.message : String(error)}`);
40
+ process.exitCode = 2;
41
+ }
42
+ }
43
+
44
+ function cfgFrom(parsed) {
45
+ const cfg = loadConfig(parsed.flags.config);
46
+ if (parsed.flags.pack) cfg.packs = [].concat(parsed.flags.pack);
47
+ return cfg;
48
+ }
49
+
50
+ function handleCheck(parsed) {
51
+ const cfg = cfgFrom(parsed);
52
+ const result = runCheck(cfg);
53
+
54
+ if (parsed.flags.json) reportJson(result);
55
+ else reportHuman(result, { statusFilter: parsed.flags.status || "all" });
56
+
57
+ if (!parsed.flags.no_fail) {
58
+ const failing = (cfg.failOn || ["fail"]).some((k) => (result.totals[k] || 0) > 0);
59
+ if (failing) process.exitCode = 1;
60
+ }
61
+ }
62
+
63
+ function handleReport(parsed) {
64
+ const cfg = cfgFrom(parsed);
65
+ const result = runCheck(cfg);
66
+ const out = parsed.flags.out || "bouncer-report.html";
67
+ const html = reportHtml(result, {
68
+ generatedAt: new Date().toISOString().replace("T", " ").slice(0, 16) + " UTC",
69
+ });
70
+ const { path: written } = writeArtifact(out, html);
71
+ printText(`\n 📄 HTML report written to ${written}\n`);
72
+ }
73
+
74
+ function handleList(parsed) {
75
+ const rules = listRules(cfgFrom(parsed));
76
+ if (parsed.flags.json) return printJson({ rules });
77
+ printText("");
78
+ for (const r of rules) {
79
+ printText(` ${pad(r.severity, 6)} ${pad(r.ruleId, 38)} ${r.standard || ""}`);
80
+ }
81
+ printText(`\n ${rules.length} rules\n`);
82
+ }
83
+
84
+ function handleExplain(parsed) {
85
+ const ruleId = parsed.positionals[0];
86
+ if (!ruleId) throw new Error("usage: bouncer explain <ruleId>");
87
+ const r = explainRule(cfgFrom(parsed), ruleId);
88
+ if (parsed.flags.json) return printJson(r);
89
+ printText("");
90
+ printText(` ${r.id}`);
91
+ printText(` pack: ${r.packTitle} (${r.packId})`);
92
+ printText(` authority:${r.authority || ""}`);
93
+ printText(` standard: ${r.standard || ""}`);
94
+ printText(` severity: ${r.severity || "medium"}`);
95
+ printText("");
96
+ printText(` intent: ${r.intent || ""}`);
97
+ printText(` fix: ${r.fix || ""}`);
98
+ printText("");
99
+ printText(` how it is checked:`);
100
+ for (const line of r.checks) printText(` ${line}`);
101
+ if (r.url) printText(`\n reference: ${r.url}`);
102
+ printText("");
103
+ }
104
+
105
+ function handlePacks(parsed) {
106
+ const dirs = parsed.flags.config ? loadConfig(parsed.flags.config).packDirs : [];
107
+ const packs = availablePacks(dirs);
108
+ if (parsed.flags.json) return printJson({ packs });
109
+ printText("");
110
+ for (const p of packs) {
111
+ printText(` ${pad(p.id, 12)} ${pad(String(p.rules) + " rules", 10)} ${p.title}`);
112
+ printText(` ${" ".repeat(12)} ${" ".repeat(10)} ${p.authority || ""}`);
113
+ }
114
+ printText("");
115
+ }
116
+
117
+ function handleInit(parsed) {
118
+ const result = initProject(parsed.positionals[0] || ".", { force: !!parsed.flags.force });
119
+ if (parsed.flags.json) return printJson(result);
120
+ printText(formatInitSummary(result));
121
+ }
122
+
123
+ function handleDoctor(parsed) {
124
+ const cfg = cfgFrom(parsed);
125
+ const report = getDoctorReport(cfg);
126
+ if (parsed.flags.json) printJson(report);
127
+ else printText(formatDoctorReport(report));
128
+ if (!report.ok) process.exitCode = 1;
129
+ }
130
+
131
+ async function handleMcp() {
132
+ await startMcpServer();
133
+ }
134
+
135
+ function handleHelp() {
136
+ printHelp();
137
+ }
138
+
139
+ function pad(s, n) {
140
+ return String(s || "").padEnd(n);
141
+ }
142
+
143
+ main();
@@ -0,0 +1,67 @@
1
+ // Next.js (App Router) adapter.
2
+ //
3
+ // A "surface" is a regulation-level concept (a sign-up flow, a child profile, a
4
+ // chat box). Rule packs reference surfaces by alias; this adapter knows how those
5
+ // aliases map onto files in a Next App Router codebase. Packs stay portable: only
6
+ // the adapter is stack-specific, exactly like tieline's client/server adapters.
7
+
8
+ export const id = "next";
9
+
10
+ // Files this adapter considers source. Other files are ignored when scanning.
11
+ export const SOURCE_EXT = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
12
+
13
+ // Surface alias -> globs (relative to the repo root). Globs support **, *, ?, {a,b}.
14
+ // These are deliberately broad: a missed surface yields "unknown" (honest), never a
15
+ // false "pass". Tune per project via pack rules that pass explicit `in` globs.
16
+ export const SURFACES = {
17
+ any: ["**/*.{ts,tsx,js,jsx}"],
18
+
19
+ signup: [
20
+ "**/{sign-up,signup,register,registration,onboarding,create-account}/**/*.{ts,tsx}",
21
+ "**/*{SignUp,Signup,Register,Onboard}*.{ts,tsx}",
22
+ ],
23
+
24
+ auth: [
25
+ "**/{auth,login,sign-in,signin}/**/*.{ts,tsx}",
26
+ "**/*{Login,SignIn,Auth}*.{ts,tsx}",
27
+ ],
28
+
29
+ profile: [
30
+ "**/{profile,account,settings,me}/**/*.{ts,tsx}",
31
+ "**/*{Profile,Account,Settings}*.{ts,tsx}",
32
+ ],
33
+
34
+ chat: [
35
+ "**/{chat,messages,messaging,dm,inbox}/**/*.{ts,tsx}",
36
+ "**/*{Chat,Message,Messaging,Conversation}*.{ts,tsx}",
37
+ ],
38
+
39
+ livestream: [
40
+ "**/{livestream,live,stream,meet,broadcast,call}/**/*.{ts,tsx}",
41
+ "**/*{Livestream,LiveStream,Stream,Broadcast,Meeting,Viewer}*.{ts,tsx}",
42
+ ],
43
+
44
+ // Any user-generated-content surface (the union of the live/social surfaces).
45
+ ugc: [
46
+ "**/{chat,messages,messaging,dm,inbox,livestream,live,stream,meet,broadcast,comments,reviews}/**/*.{ts,tsx}",
47
+ "**/*{Chat,Message,Comment,Review,Stream,Broadcast,Profile,Event}*.{ts,tsx}",
48
+ ],
49
+
50
+ // Documentation / governance artifacts (DPIA, risk assessments, policies).
51
+ governance: [
52
+ "**/*{dpia,DPIA,data-protection,risk-assessment,risk_assessment}*",
53
+ "{docs,compliance,legal,governance}/**/*.{md,mdx,pdf}",
54
+ ],
55
+ };
56
+
57
+ /** Resolve a rule's `in` (alias string, alias array, or raw glob array) into a flat glob list. */
58
+ export function resolveSurface(spec) {
59
+ const list = Array.isArray(spec) ? spec : [spec];
60
+ const globs = [];
61
+ for (const item of list) {
62
+ if (typeof item !== "string") continue;
63
+ if (SURFACES[item]) globs.push(...SURFACES[item]);
64
+ else globs.push(item); // treat as a raw glob
65
+ }
66
+ return globs.length ? globs : SURFACES.any;
67
+ }
@@ -0,0 +1,61 @@
1
+ // React Native (Expo / bare) adapter.
2
+ //
3
+ // Same surface model as the Next adapter, but globs tuned for a typical RN/Expo
4
+ // app layout (screens/, src/screens, app/ for expo-router, components/). Packs are
5
+ // unchanged — only this file is stack-specific.
6
+
7
+ export const id = "react-native";
8
+
9
+ export const SOURCE_EXT = [".ts", ".tsx", ".js", ".jsx"];
10
+
11
+ export const SURFACES = {
12
+ any: ["**/*.{ts,tsx,js,jsx}"],
13
+
14
+ signup: [
15
+ "**/{sign-up,signup,register,registration,onboarding,create-account}/**/*.{ts,tsx,js,jsx}",
16
+ "**/*{SignUp,Signup,Register,Onboard}*.{ts,tsx,js,jsx}",
17
+ ],
18
+
19
+ auth: [
20
+ "**/{auth,login,sign-in,signin}/**/*.{ts,tsx,js,jsx}",
21
+ "**/*{Login,SignIn,Auth}*Screen*.{ts,tsx,js,jsx}",
22
+ "**/*{Login,SignIn,Auth}*.{ts,tsx,js,jsx}",
23
+ ],
24
+
25
+ profile: [
26
+ "**/{profile,account,settings,me}/**/*.{ts,tsx,js,jsx}",
27
+ "**/*{Profile,Account,Settings}*Screen*.{ts,tsx,js,jsx}",
28
+ "**/*{Profile,Account,Settings}*.{ts,tsx,js,jsx}",
29
+ ],
30
+
31
+ chat: [
32
+ "**/{chat,messages,messaging,dm,inbox}/**/*.{ts,tsx,js,jsx}",
33
+ "**/*{Chat,Message,Messaging,Conversation}*.{ts,tsx,js,jsx}",
34
+ ],
35
+
36
+ livestream: [
37
+ "**/{livestream,live,stream,meet,broadcast,call}/**/*.{ts,tsx,js,jsx}",
38
+ "**/*{Livestream,LiveStream,Stream,Broadcast,Meeting,Viewer}*.{ts,tsx,js,jsx}",
39
+ ],
40
+
41
+ ugc: [
42
+ "**/{chat,messages,messaging,dm,inbox,livestream,live,stream,meet,broadcast,comments,reviews}/**/*.{ts,tsx,js,jsx}",
43
+ "**/*{Chat,Message,Comment,Review,Stream,Broadcast,Profile,Event}*.{ts,tsx,js,jsx}",
44
+ ],
45
+
46
+ governance: [
47
+ "**/*{dpia,DPIA,data-protection,risk-assessment,risk_assessment}*",
48
+ "{docs,compliance,legal,governance}/**/*.{md,mdx,pdf}",
49
+ ],
50
+ };
51
+
52
+ export function resolveSurface(spec) {
53
+ const list = Array.isArray(spec) ? spec : [spec];
54
+ const globs = [];
55
+ for (const item of list) {
56
+ if (typeof item !== "string") continue;
57
+ if (SURFACES[item]) globs.push(...SURFACES[item]);
58
+ else globs.push(item);
59
+ }
60
+ return globs.length ? globs : SURFACES.any;
61
+ }
@@ -0,0 +1,73 @@
1
+ export function parseArgv(argv) {
2
+ const result = {
3
+ command: undefined,
4
+ positionals: [],
5
+ flags: {},
6
+ };
7
+
8
+ const args = [...argv];
9
+ result.command = args.shift();
10
+
11
+ for (let index = 0; index < args.length; index += 1) {
12
+ const arg = args[index];
13
+ if (arg === "--") {
14
+ result.positionals.push(...args.slice(index + 1));
15
+ break;
16
+ }
17
+
18
+ if (!arg.startsWith("-")) {
19
+ result.positionals.push(arg);
20
+ continue;
21
+ }
22
+
23
+ if (arg.startsWith("--")) {
24
+ const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
25
+ const key = normalizeFlagName(rawKey);
26
+ const next = args[index + 1];
27
+ const value = inlineValue ?? (next && !next.startsWith("-") ? args[++index] : true);
28
+ assignFlag(result.flags, key, value);
29
+ continue;
30
+ }
31
+
32
+ const shortFlags = arg.slice(1);
33
+ if (shortFlags.length === 1 && shortFlagTakesValue(shortFlags)) {
34
+ const next = args[index + 1];
35
+ assignFlag(result.flags, expandShortFlag(shortFlags), next && !next.startsWith("-") ? args[++index] : true);
36
+ continue;
37
+ }
38
+
39
+ for (const short of shortFlags) {
40
+ assignFlag(result.flags, expandShortFlag(short), true);
41
+ }
42
+ }
43
+
44
+ return result;
45
+ }
46
+
47
+ function assignFlag(flags, key, value) {
48
+ if (["pack", "ignore"].includes(key)) {
49
+ flags[key] = Array.isArray(flags[key]) ? [...flags[key], value] : [value];
50
+ return;
51
+ }
52
+ flags[key] = value;
53
+ }
54
+
55
+ function normalizeFlagName(flag) {
56
+ const aliases = {
57
+ o: "out",
58
+ h: "help",
59
+ j: "json",
60
+ p: "pack",
61
+ c: "config",
62
+ s: "status",
63
+ };
64
+ return aliases[flag] ?? flag.replaceAll("-", "_");
65
+ }
66
+
67
+ function expandShortFlag(short) {
68
+ return normalizeFlagName(short);
69
+ }
70
+
71
+ function shortFlagTakesValue(short) {
72
+ return ["o", "p", "c", "s"].includes(short);
73
+ }
@@ -0,0 +1,22 @@
1
+ export const designPrint = [
2
+ "bouncer",
3
+ "+------------------------------------------------------+",
4
+ "| Checks the controls a regulation requires actually |",
5
+ "| exist in your code. No LLM required. |",
6
+ "+--------------------------.---------------------------+",
7
+ " |",
8
+ " .------------+------------.",
9
+ " / regulation -> rules \\",
10
+ " /_____________________________\\",
11
+ " o----o----o----o",
12
+ " pack rule surface verdict",
13
+ " | | | |",
14
+ " OSA AADC code pass/fail",
15
+ ].join("\n");
16
+
17
+ // Verdict glyphs reused across reporters.
18
+ export const GLYPH = {
19
+ pass: "✓", // ✓
20
+ fail: "✗", // ✗
21
+ unknown: "?",
22
+ };
@@ -0,0 +1,51 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const DEFAULTS = {
5
+ target: { adapter: "next", repo: ".", roots: ["app", "src", "components"] },
6
+ packs: ["uk-osa", "uk-aadc"],
7
+ packDirs: [],
8
+ ignore: [],
9
+ failOn: ["fail"],
10
+ };
11
+
12
+ /** Load and resolve bouncer.config.json. The target repo path is resolved relative to the config file. */
13
+ export function loadConfig(explicitPath) {
14
+ const cfgPath = path.resolve(explicitPath || findConfig());
15
+ const dir = path.dirname(cfgPath);
16
+ const raw = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
17
+
18
+ const cfg = {
19
+ ...DEFAULTS,
20
+ ...raw,
21
+ target: { ...DEFAULTS.target, ...(raw.target || {}) },
22
+ };
23
+ cfg.target.repoRoot = path.resolve(dir, cfg.target.repo);
24
+ cfg.packDirs = (cfg.packDirs || []).map((d) => path.resolve(dir, d));
25
+ cfg._path = cfgPath;
26
+ return cfg;
27
+ }
28
+
29
+ function findConfig() {
30
+ let dir = process.cwd();
31
+ for (;;) {
32
+ const p = path.join(dir, "bouncer.config.json");
33
+ if (fs.existsSync(p)) return p;
34
+ const parent = path.dirname(dir);
35
+ if (parent === dir) break;
36
+ dir = parent;
37
+ }
38
+ throw new Error("No bouncer.config.json found (searched up from cwd). Run `bouncer init` or pass --config <path>.");
39
+ }
40
+
41
+ export const CONFIG_TEMPLATE = {
42
+ target: {
43
+ adapter: "next",
44
+ repo: ".",
45
+ roots: ["app", "src", "components", "redux"],
46
+ },
47
+ packs: ["uk-osa", "uk-aadc"],
48
+ packDirs: [],
49
+ ignore: [],
50
+ failOn: ["fail"],
51
+ };