# Security-gate as a "modular platform"? — decision (2026-06-17)

audience: AI coding agents first. BLUF-ordered, imperative, tagged [MEASURED]/[INFERRED]. Do not prettify.
Scope: answers the directive "could we build a security-gate platform like the modular platform; is a
tool-bank / deterministic-tool templates helpful; can tests be made modular/reusable in platform style."
Decisive lens: judge every candidate by the **platform's OWN gates** (`/home/user/Projects/platform/CLAUDE.md`
§3 + `module-plan-template` Gates 0–3) — same delete-test, convergence>count, YAGNI-on-surface, adapter-minimalism.

## BLUF — verdict
**Adopt the platform PATTERN, reject the platform PACKAGING.** The reusable-test discipline maps cleanly and is
worth building; a literal `@security-gate/*` published monorepo does NOT — it fails the platform's own delete-test
today (one consumer, polyglot tools). Build, in order:
1. **Corpus-as-standing-bench** — the single highest-value transfer (regression-locks BOTH legs, kills overfit
   risk). NOT a tool-bank — a test bench.
2. **A minimal check CONTRACT + conformance suite** — as a directory CONVENTION, not published packages.
3. **Only deterministic checks that pass the per-check delete-test vs the LLM leg** — oracle (have), deps-CVE,
   headers. NOT redundant ones (SQLi etc. — LLM already catches 3/3 [MEASURED]).

Do NOT: clone the turbo monorepo, semver/changeset/GHP-publish checks, build a registry, or write a deterministic
check for a class the LLM leg already covers. Each is YAGNI-on-surface failure by the platform's own §3.

---

## 0. Coverage vs extraction — the two-knob split (resolves the ×N-donor tension)
**The platform's ×2–3-donor rule is the WRONG gate for security COVERAGE — do not import it for that axis.** It
was never a coverage rule. In a boilerplate library, coverage is FREE: a ×1 capability is not *uncovered*, it just
stays inline in the host instead of being promoted to a shared seam. The ×N rule governs ONE thing — *does a
behavior earn EXTRACTION to a frozen seam* — because the only cost it weighs is wasted abstraction.

Security inverts the asymmetry: **coverage IS the product.** A missed attack surface = a shipped blind spot =
the exact failure the gate exists to prevent. So security needs the ONE platform knob split into **TWO decoupled
knobs:**

| knob | security analogue | gate | donor count? |
|---|---|---|---|
| **A. COVERAGE** — what surfaces the gate looks for | must be EXHAUSTIVE | a **taxonomy** (OWASP ASVS / CWE Top-25 + fable S1–S11 as worked instance) — the coverage CONTRACT | **NO — ×0 is fine** |
| **B. EXTRACTION** — which classes get a hardened DETERMINISTIC check | SELECTIVE | the **per-check delete-test vs the LLM leg** (§2) — *not* donor count | replaced by "deterministic strictly beats LLM here" |

**Coverage (knob A) is realized by a 3-BAND detector ladder — not a binary of "LLM vs deterministic."** The
binary is a trap: it mis-routes every hard miss to "build a deterministic check or accept the loss." There is a
load-bearing MIDDLE band — already built, previously unnamed:

| band | mechanism | covers | gate / trigger |
|---|---|---|---|
| **1. Generalist LLM single-pass** | one adversarial prompt, many classes | easy / decidable-by-reading-one-file (SQLi, reflected XSS, obvious IDOR) | default; ×0 per-class |
| **2. ORCHESTRATION** (already built) | cross-file scope construction + k≥3 ensemble union + class-specialized sub-prompts | **hard-but-NOT-decidable**: TOCTOU/concurrent-claim, cross-file IDOR, business-logic auth, multi-hop mediation | a finding/sweep shows the class needs expanded scope or fails single-roll — escalate, don't deterministic-ize |
| **3. Deterministic check** | AST / lockfile / config rule (oracle, deps, headers) | **narrow decidable invariants the LLM structurally misses** | delete-test vs LLM (§2) |

**Routing rule (encode it — this is the advisor's load-bearing correction):** when the recall sweep finds a class
the gate misses, route the FIX by band, not by reflex. Most hard misses (TOCTOU, cross-file IDOR, logic-auth)
belong to **band 2 — the lever is better orchestration (scope/ensemble/sub-prompt), NOT a new deterministic
check.** Reserve band 3 for the narrow decidable residue. The oracle is the proof of this distinction: it catches
a *narrow decidable SUB-class* of mediation (identity-collision); the broader TOCTOU/mediation class lives in
band 2, not band 3 — no clean deterministic check exists for it. [MEASURED: the webhook concurrent-claim TOCTOU
was uncatchable from `webhook.ts` alone — caught only via band-2 cross-file pull-in + k≥3, never by a deterministic
rule.] Mis-filing a band-2 class as band-3 = chasing a deterministic check that cannot exist.

**Why exhaustive coverage at ×0 is even possible (the structural dis-analogy with the platform):** security HAS a
generalist detector the platform has no equivalent of — the **adversarial LLM leg (bands 1+2) covers many classes
with one prompt + orchestration**, at ×0 donors-per-class. [MEASURED] the v2 prompt caught SQLi, SSRF, RBAC it was
never specialized for. So coverage breadth is the LLM+orchestration job; the deterministic bank (band 3) stays
small. The platform has no "generalist module," which is exactly why it must gate every seam on convergence — we
do not.

**CONFIDENCE CAVEAT — knob A is a HYPOTHESIS until the recall sweep runs [advisor #2].** "Bands 1+2 cover the
taxonomy at ×0" is [MEASURED] on only ~3 classes (SQLi/SSRF/RBAC); S1/S5/S7/S8/S10 are [INFERRED, UNMEASURED]
(`STEP2_ZYNC_PILOT.md` "expect catchable — UNMEASURED"). The recall sweep is therefore NOT merely build-step #4 —
it **IS the empirical test of the entire coverage knob.** Until it runs, this whole two-knob/3-band design is
provisional on it. Treat the sweep as the validation of the model, not a downstream chore.

**This is NOT a departure from the platform — it invokes the platform's own escape hatch.** Platform CLAUDE.md §3:
donor count is *"a floor signal, not the whole test"*; a *"foundational capability a domain-census shows broadly
demanded across the app classes"* earns a build at ×1 / even ×0 — iff it still passes the delete-test + same-seam +
YAGNI-on-surface. Security coverage is precisely that foundational, broadly-demanded forward-need; the **taxonomy
is the forward-need census.** Knob B still honors the delete-test (vs the LLM, §2). So both knobs stay inside the
platform's own rules.

**How exhaustiveness is engineered (honest — you cannot PROVE a closed set; open-world problem):**
1. **Anchor to a recognized taxonomy** (ASVS/CWE + S1–S11). The taxonomy is the auditable coverage contract — what
   the gate *claims* to look for. Best-practice (ASVS is a coverage-checklist standard), not a private list.
2. **Default detector = the generalist LLM leg** (covers classes at ×0 — one prompt, many classes).
3. **The corpus/bench is the coverage METER** — per-class recall shows which taxonomy classes the gate *actually*
   catches vs *claims* to. A taxonomy class with 0 cells = **UNMEASURED = a flagged blind spot, never reported
   clean** (extends no-false-clean → **no-false-coverage-claim**; "we cover the taxonomy + measure it + flag every
   unmeasured class," NOT the false "we cover everything").
4. **Completeness critic** — a standing step: *which taxonomy class has no detector / no cell / unmeasured recall?*
   → that is the next work (mirrors the workflow completeness-critic + fable convergence-rescan).

Consequence for the build: **coverage scales with the LLM leg (cheap, broad); the deterministic bank stays
curated (delete-tested).** That is exactly why the modular structure stays solid AND scalable under "every attack
surface" — the two knobs scale on different curves. [INFERRED, grounded in the two measured pilots + platform §3.]

---

## 1. Judge candidates by the platform's keystone (Gate-1 delete-test)
*"Delete this abstraction — does complexity VANISH, or just RELOCATE into N consumers + a new dep?"* (platform
CLAUDE.md §3). Apply to each thing we'd build:

| candidate | delete-test result | verdict |
|---|---|---|
| **Shared validation corpus** (pinned vuln/safe cells + expected findings) | delete → recall goes back to ad-hoc per-session eyeballing; regression-detection + overfit-guard VANISH | **BUILD — passes cleanly** |
| **Check contract + conformance suite** (each check ships RED-on-vuln + GREEN-on-safe + discriminator cells; admitted iff suite green) | delete → each check's "no-false-clean" becomes per-check reinvention (I already do this ad-hoc for the oracle) | **BUILD — but as a convention, ≥2 checks needed; have oracle, +deps/+headers = 3** |
| **npm-style PUBLISHING ceremony** (tsup/DTS/ESM/changeset/semver/GHP-publish) | delete → complexity RELOCATES into build+version+publish tooling; removes NOTHING; ONE installer | **REJECT today — revisit only on a 2nd independent installer** |
| **Domain HIERARCHY shell** (one shared contract+corpus+bench+coverage-meter, reused across security/ui/ux/money domains) | delete → each domain RE-INVENTS corpus format, bench runner, check contract, conformance, coverage-meter — duplicated ×N domains | **BUILD — passes at ×N domains (see §1b); I delete-tested the wrong object first** |

The split is the whole answer: **the hierarchy + shared-shell pattern passes, the npm-publishing ceremony fails.**

## 1b. Hierarchy across test domains — the abstraction I under-scoped [user + advisor correction]
**Correcting myself: my first delete-test targeted the wrong object.** I asked "delete a *published package* — does
complexity vanish?" → one consumer → fails → I concluded "reject packaging." But the valuable object is NOT an npm
package; it is a **hierarchy of test DOMAINS sharing one shell**. Re-run the delete-test on THAT object and it
PASSES — because the convergence is not ×1-consumer, it is **×N-domains**:

The user already has the donors: **security** (built), **ux** (ux-audit exists), **money/payments** (multideal),
and "many audits." That is ≥3 real domains demanding the SAME meta-structure: a coverage taxonomy + a corpus of
vuln/safe cells + a detector ladder (bands 1–3) + a bench/coverage-meter + a conformance gate. By the platform's
OWN rule (×2 same-seam = designed, ≥3 = strong), a meta-contract recurring across ≥3 domains is a **strong build**.
So the hierarchy is not YAGNI — it is convergence-justified. **The earlier "reject" was about distribution
ceremony, not structure.**

**"Package" is the wrong name — it imports the npm-publish baggage we reject while discarding the hierarchy we
want.** Recommended naming (rename to kill the npm prior): top node = **domain**; the whole thing = an **audit
platform** (matches the user's existing "audits" vocabulary); a domain's detectors stay **legs/bands**. Use
`domain`, never `package`.

```
audit-platform/
  contract/        SHARED shell (the ×N-convergent abstraction): Finding schema · appliesTo() · conformance
                   harness · bench runner · coverage-meter · the 3-band detector-ladder interface
  domains/
    security/      taxonomy.md (ASVS/CWE + S1–S11) · corpus/ · detectors/{llm(band1+2), oracle, deps, headers}
    ui/            taxonomy.md (WCAG/a11y/contrast/focus) · corpus/ · detectors/{llm, axe(band3-deterministic)}
    ux/            taxonomy.md (flow friction/dead-ends/empty-states) · corpus/ · detectors/{llm}   # mostly fuzzy → few band-3
    money/         taxonomy.md (rounding/idempotency/ledger-invariants) · corpus/ · detectors/{llm, ledger-invariant}
```

**Hierarchy discipline (so "hierarchy is king" does not become a taxonomy for its own sake) — delete-test EACH
node:**
- **Top level (domains)** earns boundaries: delete `ui` → a11y coverage scatters; each domain has its OWN
  coverage taxonomy. Real boundary. KEEP.
- **Shared `contract/`** is the convergent shell (§ above) — the ONE thing extracted; everything else is per-domain.
- **Sub-nodes (detectors)** = exactly TWO levels deep (domain → detector), organized by BAND. A band-3
  deterministic detector earns its node ONLY via the per-check delete-test vs the LLM (§2). NEVER nest deeper for
  tidiness — the platform KILLED its own registry + several modules; depth is earned, not decorative.
- **Convention discovery, NOT a registry** (platform's own choice). NOT npm packages — a directory hierarchy +
  one shared contract, polyglot-friendly (§4 divergences still hold: the shell is language-agnostic glue, the
  detectors stay multi-runtime).

**Build order respects pilot-before-fan-out:** finish **security** as the pilot domain; **extract `contract/` when
`ux` becomes the 2nd real wired domain** — NOT by scaffolding empty ui/ux/money shells now. The security `bench.py`
(§7.1) IS the first prototype of the shared bench runner — building it advances BOTH the security recall sweep AND
the hierarchy foundation. [INFERRED, grounded in platform §3 convergence + the user's existing audit donors.]

## 2. The "is it even helpful?" filter — per-check delete-test vs the LLM leg
A deterministic check earns a place ONLY if deleting it loses a catch the LLM leg cannot recover. This is the
critical YAGNI gate — most "security tool-banks" people build fail it.

| check | LLM leg can do it? | delete-test vs LLM | build? |
|---|---|---|---|
| **Oracle — complete-mediation / identity-collision (C02/C09)** | NO — provably ([MEASURED]: no LLM config caught C02) | lose the class entirely | **YES (have it)** |
| **Deps-CVE** (lockfile ↔ advisory DB; `pnpm audit`) | NO — can't read a lockfile/advisory it wasn't given | lose all dep CVEs | **YES — build (mirrors fable S11)** |
| **Response-header / config presence** (CSP/HSTS/etc. in `wrangler.toml`) | flaky-yes, but it's a DECIDABLE presence check | deterministic is strictly better + ~free | **YES — build (cheap, decidable)** |
| **SQLi / IDOR / SSRF sink detectors** | YES — [MEASURED] S4-001 SQLi caught 3/3 #1; S6 SSRF caught | redundant — LLM recovers it | **NO — building it = YAGNI failure** |

Rule to encode: **deterministic legs only for decidable invariants the LLM structurally misses** (oracle-class,
deps, headers). Everything fuzzy/logic stays on the LLM leg. The tool-bank stays SMALL and curated — not a
framework. [INFERRED from the taxonomy + the two measured pilots; the redundant-SQLi row is the load-bearing
"don't build it" evidence.]

## 3. What the platform pattern actually buys us — the corpus-as-bench (lead with this)
The biggest win is NOT a tool-bank. It is turning ad-hoc validation into a **standing regression bench**, exactly
the platform's "one behavioral test per export" applied to the gate's two legs.

- **Today [MEASURED reality]:** validation is manual — run cells, eyeball recall, *worry* whether a prompt edit
  regressed the 6 payments cells (open task #16). Overfit guard is willpower, not a gate.
- **Platform-style:** one runner over a **git-pinned corpus** (multideal baseline-missed cells + zync S1–S11 cells,
  each with its canonical) reports **per-class recall/precision** and **FAILS on regression**. The fixture IS the
  proof (platform: `test-fixture.ts` factory + behavioral contract). This directly serves the standing
  disciplines: no-overfit (held-out cells), regression-guard (#16 becomes automatic), measure-before-recommend,
  and STEP-2's recall sweep (`STEP2_ZYNC_PILOT.md` rec #4). Build this FIRST.

## 4. Where the analogy BREAKS — do not force it (honesty)
The platform is a single-stack TS library repo for thousands of unknown adopters. Three hard divergences mean
**mirror the pattern, never the toolchain:**
1. **The LLM leg is non-deterministic.** Platform contract tests assert deterministic pass/fail; the LLM leg
   CANNOT be a equality-assert. Its "test" is a **statistical recall gate over k rolls** (recall ≥ threshold),
   not `expect(x).toEqual`. Different epistemics — the bench must measure a rate, with k≥3, never a single run.
2. **Polyglot by nature.** Gate = python orchestrator + bun oracle + `claude -p` + `pnpm audit`. Platform = TS /
   vitest / tsup / ESM. A published ESM monorepo would force multi-runtime tools into one wrong shape. The
   convergent contract is the **Finding output schema + an `appliesTo(target)` predicate**, NOT a uniform input
   signature (inputs differ: a `.ts` file vs a lockfile vs a config).
3. **~One consumer.** Platform's whole north-star ("design every contract for the unknown adopter") justifies its
   semver/freeze/publish ceremony. The gate has one consumer (this machine, across my projects). That ceremony is
   pure tax here — **YAGNI-on-surface** (platform §3) says don't build it until a real 2nd installer appears.

## 5. Recommended shape (platform pattern, right-sized)
A structured DIRECTORY under `~/.claude/security-gate/`, discovered by convention (platform killed its registry;
so do we — `gate.py` already discovers by convention):

```
security-gate/
  corpus/                         # the bench (build FIRST)
    <class>/<id>/vuln.<ext>       # pre-fix cell (must FLAG)        git-pinned to fix~1
    <class>/<id>/safe.<ext>       # fixed cell  (must NOT flag)     git-pinned to fix
    <class>/<id>/canonical.json   # {id, class, file, line, fix_sha, why}
  checks/                         # deterministic legs (curated, delete-tested vs LLM)
    oracle/         {check.ts, conformance/{vuln,safe,rename,cosmetic,polarity}/...}
    deps/           {check.sh|ts, conformance/...}     # pnpm audit leg
    headers/        {check.ts, conformance/...}        # config presence leg
  bench.py                        # runs both legs over corpus → per-class recall/precision; FAILS on regression
  gate.py / oracle/ / prompt_v2.txt / skill            # unchanged — the consumer
```

Contract every check satisfies (the reusable abstraction = the test-kit, NOT the detection logic):
- `appliesTo(target) -> bool` + `check(target) -> Finding[]`; `Finding` = one shared schema (id/class/file/line/severity/why).
- **Conformance suite is the admission gate** — a check ships ONLY if: RED on every `vuln` cell, GREEN on every
  `safe` cell, and survives the discriminator cells (rename predicates / cosmetic edit / polarity flip / null
  mutant). This is the oracle's own validation methodology made first-class — and is exactly Semgrep `.test.yaml`
  / CodeQL `.expected` / ESLint `RuleTester` valid+invalid (best-practice, not novel-risky).
- **Templatable = the module shell + the conformance harness; NOT the detection algorithm.** The oracle's AST
  mediation logic cannot be templated into a header check. Scaffold the shell (à la `turbo gen`); hand-write the
  logic. [INFERRED but well-grounded — matches platform "lazy in impl, strict at the seam."]

## 6. Do-NOT list (YAGNI teeth)
- DO NOT gate COVERAGE on donor count — coverage is taxonomy-driven + LLM-default at ×0 (§0). Donor-count gates
  only DETERMINISTIC-CHECK extraction, and even there it is replaced by the delete-test-vs-LLM (§2).
- DO NOT report a taxonomy class as clean when its corpus recall is UNMEASURED — flag it as a blind spot (§0.3).
- DO NOT clone the turbo monorepo / publish `@security-gate/*` / add changesets+semver — one consumer, fails §3.
- DO NOT build a plugin registry — convention discovery (platform's own choice after killing its registry).
- DO NOT write a deterministic check for a class the LLM leg already catches (SQLi/IDOR/SSRF — [MEASURED] covered).
- DO NOT assert deterministic pass/fail on the LLM leg — it is a statistical recall gate (k≥3).
- DO NOT build the corpus speculatively wide — seed it from cells already validated (multideal + the 3 zync
  pilots), grow it as the recall sweep runs. Pilot before fan-out.

## 7. Order of work (ties into STEP-2)
1. **`bench.py` + seed `corpus/`** from the already-validated cells (multideal baseline-missed + zync S4-001/S6).
   Highest leverage; makes #16 regression-guard automatic. → unblocks the STEP-2 recall sweep (rec #4).
2. Fix semantic dedup (STEP-2 GAP 1 / task #17) — the bench's recall numbers are corrupted until dedup is semantic.
3. Add `checks/deps` + `checks/headers` under the contract (STEP-2 GAP 2) — each with its conformance suite.
4. Light prompt generalization (#16) — now regression-checked automatically by `bench.py`. Then graduate to a
   packaged platform ONLY if a second independent installer materializes.
```
