@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 +32 -0
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/package.json +54 -0
- package/src/cli.js +143 -0
- package/src/lib/adapters/next.js +67 -0
- package/src/lib/adapters/react-native.js +61 -0
- package/src/lib/args.js +73 -0
- package/src/lib/brand.js +22 -0
- package/src/lib/config.js +51 -0
- package/src/lib/doctor.js +49 -0
- package/src/lib/engine.js +229 -0
- package/src/lib/init.js +20 -0
- package/src/lib/mcp.js +195 -0
- package/src/lib/output.js +49 -0
- package/src/lib/packs.js +169 -0
- package/src/lib/reporters/html.js +156 -0
- package/src/lib/reporters/human.js +72 -0
- package/src/lib/reporters/json.js +5 -0
- package/src/lib/walk.js +109 -0
- package/src/packs/uk-aadc.json +109 -0
- package/src/packs/uk-osa.json +99 -0
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
|
+
}
|
package/src/lib/args.js
ADDED
|
@@ -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
|
+
}
|
package/src/lib/brand.js
ADDED
|
@@ -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
|
+
};
|