# security-gate — design (source of truth)

audience: AI coding agents first. BLUF-ordered, imperative, **testing-native vocabulary**. Tag [MEASURED]/[INFERRED].
Do not prettify into narrative — the rules ARE the tool.
status: DESIGN (brainstorm output, 2026-06-17). v1 build scope = **security domain only, deepened**. This project
SUPERSEDES the prototype at `~/.claude/security-gate/` (salvage map §10).

## Mission
One standing GATE that catches defects across many TEST DOMAINS — **security first**; content/typography, ui,
a11y, design-tokens, correctness later — across ALL the user's projects. A defect that ships is the exact failure
the gate exists to prevent: **coverage is the product**, not a by-product.

## §1 Vocabulary — testing-native; banned: software-architecture terms
We share modularity with `~/Projects/platform` **in essence only**. platform is a REFERENCE EXAMPLE, never a
template; its vocabulary is software-architecture, ours is testing. Use the left column; never the right.

| use (testing-native) | NEVER use (banned platform term) |
|---|---|
| **detector** — one check | module / package |
| **domain** — a family of detectors (security, content, ui…) | layer / subpath |
| **detector contract** — the interface every detector meets | seam / port-adapter |
| **corpus / cell / fixture** | — |
| **canary** (must-flag) · **anti-canary** (must-not-flag) · **discriminator** | — |
| **bench** — recall/precision over the corpus | — |
| **band** — det / orch-LLM / gen-LLM | tier |
| **delete-test** (keep — recognized principle) | — |

Reject wholesale (do NOT import): north-star / unknown-adopter framing, host-agnostic adapter seams,
semver/changeset/publish ceremony, the layer-DAG. **Reason:** our consumer is the user across their own repos
(~1 consumer), NOT thousands of unknown third-party adopters — platform's contract-freezing ceremony is pure tax
here. We keep the modular STRUCTURE (per-detector units, convention discovery, one shared contract); we drop the
distribution/abstraction CEREMONY.

## §2 Goals / non-goals
GOALS: exhaustive coverage (taxonomy-driven, not donor-gated) · every detector validated (canary + anti-canary +
discriminators) before it ships · statistical recall for LLM bands (k≥3, never a single-run claim) ·
**no-false-clean** + **no-false-coverage-claim** · detectors discovered by CONVENTION (no registry) · Rust-fast
deterministic scan at scale.
NON-GOALS (v1): publishing/distribution · designing for third-party adopters · any domain beyond security · a
deterministic detector for a class the LLM band already catches (YAGNI — see §4 delete-test-vs-LLM).

## §3 Core model — coverage vs extraction, realized by a 3-band ladder
Two DECOUPLED knobs (the platform's single ×N-donor knob is wrong for security — it gates *extraction*, never
*coverage*). Full reasoning is salvaged to `docs/TOOLBANK_DECISION.md` — point there, do NOT re-derive.

- **COVERAGE knob** = EXHAUSTIVE, anchored to a recognized taxonomy (OWASP ASVS / CWE Top-25 + the fable
  **S1–S11** worked set, `docs/taxonomy/security.md`). NOT gated on donor count.
- **EXTRACTION knob** = which classes earn a hardened DETERMINISTIC detector. Gated by the **per-detector
  delete-test vs the LLM band** (§4), NOT donor count.
- Coverage is realized by a **3-BAND detector ladder** (NOT a binary):
  1. **generalist-LLM** single pass — broad, ×0 per class (`prompt_v2.txt`).
  2. **orchestrated-LLM** — cross-file scope construction + k≥3 union + class sub-prompts — carries the
     **hard-but-NOT-decidable** classes (TOCTOU/concurrent-claim, cross-file IDOR, business-logic auth).
  3. **deterministic detector** — narrow decidable invariants the LLM structurally misses (oracle, deps, headers,
     syntactic security rules).
- **ROUTING RULE (load-bearing):** a sweep-found miss is routed to a band by judgment, not reflex. Most hard
  misses → **band 2 (better orchestration), NOT a new deterministic detector.** Reserve band 3 for the narrow
  decidable residue. (The oracle catches a narrow decidable SUB-class of mediation; broad TOCTOU lives in band 2.)
- **CONFIDENCE [updated 2026-06-17 — recall spike]:** "the generalist LLM band covers the taxonomy" is now
  [MEASURED] on **7 of 11 classes, all CAUGHT** — single-file canonicals for S1/S4/S5/S6/S7/S8/S10 surfaced in
  blind k=3 unions at [critical]/[high] (see `docs/validation/2026-06-17-recall-spike-*`). Coverage is no longer
  the load-bearing risk for single-file LOGIC classes.
- **BAND-2 CROSS-FILE [updated 2026-06-17 — band-2 spike, see `docs/validation/2026-06-17-band2-crossfile-spike.md`]:**
  measured on one **Shape-A** cross-file IDOR (zync `16f510f`, route→imported `createStatus`; FK passed onward
  with NO visible guard). Two findings: (a) band-1 FLAGS the Shape-A IDOR as an *unconfirmed* in-file-ownership
  gap ([high] 2/3) — so Shape-A (missing-check) is NOT a coverage hole; but this cell is NOT truly undecidable
  single-file (band-1 fired on the IDOR smell — text: *"without in-file ownership validation"*). **Shape B —
  route CALLS an imported predicate/builder that LOOKS correct, bug lives INSIDE the imported symbol (fail-open /
  insecure default; the C02 founding shape) — is now MEASURED a coverage hole.** [MEASURED] zync `ead618d~1`
  `refresh.ts` calls `buildSessionPayload({...})` OMITTING `enforce2fa`; the builder defaults
  `enforce_2fa: args.enforce2fa ?? false` → 2FA silently dropped on refresh. band-1 k=3 = ~16 findings, NONE the
  canonical; oracle silent; escalation can't fire → silent TOTAL miss. So band-1 covers Shape A (visible
  missing-check) but FAILS Shape B (imported insecure default); band-2 is **existential for RECALL** on Shape B,
  not just confirmation — CONFIRMS band-2 outranks precision as the #1 open problem. **Shape B has NO band-1 flag
  to react to → reactive escalation cannot catch it; the trigger must be PROACTIVE (pull every security-relevant
  imported symbol the target calls, read its default/behavior).** **[MEASURED — the FIX gate, two-file test]** With the imported builder concatenated into the prompt (resolution supplied by hand), band-1 catches the Shape-B canonical **3/3** (`[critical] 2FA enforcement stripped on every token refresh`) vs 0/3 single-file → **RESOLUTION is the bottleneck, omission-DETECTION is NOT** — the LLM does the call-site-vs-builder diff itself once it sees the builder, so band-2 needs **only a barrel RESOLVER + a proactive TRIGGER, no bespoke omission sub-check** (the TRIGGER "which imports to pull" is the remaining open problem; detection is solved). (b) band-2 *confirmation* (pull the imported
  symbol, decide TP/FP) **structurally cannot fire**
  on this class — both escalation paths gate on the imported symbol NAME matching the payments-flavored `CRITICAL`
  regex, and tenant-isolation authz symbols (`assertTenantOwns*`, `requirePermission`, `createStatus`) match none
  of it. The escalation trigger is thus co-extensive with the oracle's payments domain; **band-2-via-escalation
  is dead for the rest of the taxonomy** until the trigger generalizes (role/taxonomy detection, not payments
  keywords) AND the resolver follows the monorepo barrel (`@zync/db/queries` → `index.ts` → leaf). This OUTRANKS
  precision as the #1 open problem (the bar "catch ALL in 1 round" needs cross-file P0s CONFIRMED, not just
  flagged). Until fixed, cross-file IDOR stays an unconfirmed `[high]` suspicion → feeds precision/triage.
- **STILL [INFERRED]/unmeasured:** **per-class recall RATE** (n=1 cell/class, a point estimate not a rate —
  bench.py with n≥3 cells/class is the real test); **S2/S3** (logic, expect catchable). **S11** (deps/CVE +
  headers) is the one structural non-LLM class — band-3 deterministic by design, NOT an LLM miss.
- **PRECISION (now #2, two distinct problems — see recall-spike §2):** (a) paraphrase-merge (semantic dedup
  fixes); (b) triage/ranking — the canonical is real but buried among ~20 genuine findings (ranking fixes, dedup
  does NOT). Empirically confirmed open; ranks below band-2 confirmation.

## §4 The detector contract (universal — all bands, all domains)
Every detector is a self-contained unit discovered by CONVENTION (no registry — slopgate's per-file AST model +
platform's own anti-registry choice). It declares:
- `id` · `domain` · `band` · `severity` · `category`
- the check: a **declarative rule** (regex / ast-grep) OR a **procedural `fn(target) -> Finding[]`**
- `appliesTo(target)` — which files/targets it runs on
- `canary` — input it MUST flag (RED-on-vuln)
- `antiCanary[]` — inputs it MUST NOT flag (GREEN-on-safe)
- `discriminators` — rename / cosmetic / polarity / null-mutant variants it MUST survive (anti-overfit — **slopgate
  lacks these; we add them**)
- `Finding` output: `{id, domain, severity, file, line, text, resolution, band, rollsFound?}`

**ADMISSION GATE (imperative):** a detector ships ONLY if its conformance suite is green — flags every canary,
ignores every anti-canary, survives every discriminator. This is slopgate's canary/negativeCanary made universal
and hardened. Recognized-equivalent: Semgrep `.test.yaml`, CodeQL `.expected`, ESLint `RuleTester` valid+invalid —
best practice, not novel-risky.

**delete-test-vs-LLM (extraction gate):** build a band-3 deterministic detector ONLY if deleting it loses a catch
the LLM bands cannot recover. [MEASURED] oracle (C02) ✓, deps-CVE ✓, headers ✓; SQLi/IDOR/SSRF ✗ (LLM catches
3/3 — building them is YAGNI).

## §5 Polyglot detectors — language by BOTTLENECK, not by fiat
Pick a detector's language by the WORK, never by preference:
- **CPU-bound + syntactic** (regex / AST pattern) → the **Rust engine** (absorbed slopgate-core) running
  **declarative rules**. Scales ~linearly + parallel; correct at large rule×file counts. Authoring a rule = a
  YAML/JSON file, **NOT writing Rust**.
- **Semantic / type-aware** (needs types, cross-file symbol resolution) → **TS** (TypeScript compiler API). The
  complete-mediation oracle stays TS. ast-grep/tree-sitter is syntax-only and would LOSE the type info that is the
  oracle's whole value. **DO NOT port the oracle to Rust.**
- **Lockfile / CVE / config / shell** (deps, headers) → the **native tool** (`pnpm audit`, a config parser).
- **LLM bands + orchestrator** → **I/O-bound on LLM latency** (seconds–minutes/roll vs microseconds of detector
  CPU, ~6 orders of magnitude). Language speed is irrelevant → keep **python/glue**. **DO NOT Rust-ify the
  orchestrator** — buys nothing, loses iteration speed.
- Hand-write Rust ONLY for a new matching PRIMITIVE the engine lacks — rare.

**KEY INSIGHT:** "tests in Rust" is mostly NOT writing Rust — it is writing declarative ast-grep/regex rules the
Rust engine runs. The Rust surface stays tiny (the engine); the detector surface is declarative + language-neutral.
[perf claims = structural/asymptotic; a specific deterministic-scan speedup is measurable later if it matters.]

## §6 The corpus + bench (the central artifact)
- **corpus/** = git-pinned cells. Ground truth = git **FIX COMMITS**: vuln cell = `git show <fix>~1:<path>`, safe
  cell = `git show <fix>:<path>`. Each cell carries `canonical.json {id, domain, class, file, line, fix_sha, why}`.
- **bench** runs every detector + the LLM bands over the corpus → per-class **recall/precision**; **FAILS on
  regression** (makes the no-overfit guard automatic).
- **LLM bands measured STATISTICALLY:** k≥3 rolls, recall as a rate, NEVER a single-run claim. Deterministic
  detectors are pass/fail.
- **no-false-clean:** a detector on degraded input (e.g. unresolved imports) reports UNRELIABLE, never clean.
- **no-false-coverage-claim:** a taxonomy class with 0 cells / unmeasured recall is a flagged BLIND SPOT, never
  reported covered.
- **blind catch-test discipline:** detectors/agents under test never see audit docs / fix commits / ground truth;
  the orchestrator (judging) may. Reconstructed cells are secret-scrubbed.

## §7 Project structure (convention discovery, no registry)
```
~/Projects/security-gate/
  CLAUDE.md                  project doctrine (testing-native; points here, never re-inlines)
  docs/
    specs/2026-06-17-security-gate-design.md   THIS (SoT)
    TOOLBANK_DECISION.md     salvaged — coverage/extraction/3-band reasoning
    VALIDATION.md            salvaged — validation discipline
    taxonomy/security.md     the coverage contract (ASVS/CWE + S1–S11)
  engine/                    absorbed + modularized slopgate-core (Rust regex + ast-grep)
  orchestrator/              gate.py (salvaged) — routes targets→detectors+LLM bands, merges, escalates
  domains/
    security/
      detectors/
        oracle/      bespoke TS (complete-mediation) + conformance/
        *.yml        ast-grep declarative detectors (timing-safe-compare, zod-present, no-raw-drizzle, headers…)
        deps/        pnpm-audit leg + conformance/
      corpus/        security cells (multideal + zync S1–S11)
      taxonomy.md
  bench.py                   recall/precision over corpus; regression gate
  skills/                    harvest · implement · investigate · create-tests · coding-standards
```

## §8 v1 scope — security domain, deepened (per user decision 2026-06-17)
v1 = **SECURITY ONLY**, to the bar: **catch ALL fable (zync S1–S11) bugs in 1 round.** Build order:
0. **DE-RISK FIRST — recall spike on the UNMEASURED classes** (S1/S5/S7/S8/S10): reconstruct 1 cell each via
   `git show <fix>~1`, run the salvaged `gate.py` + eyeball (NO new infra — exactly the S4-001/S6 pilot move).
   Coverage is the load-bearing assumption (§3); confirm or break it BEFORE building on it. [advisor 2026-06-17]
1. Stand up `orchestrator/` (salvage `gate.py`) + `bench.py` + seed `corpus/` (multideal baseline-missed + zync
   cells) — makes the spike repeatable + regression-locked. NOTE: **do NOT absorb slopgate in v1** (§10).
2. **Semantic dedup** of merged findings (so the recall signal isn't corrupted by paraphrase-counting).
3. **Recall sweep** across S1–S11 → per-class recall (this VALIDATES the coverage knob, §3).
4. Route each miss by band (§3 routing rule) → close gaps to the 1-round bar.

**slopgate is NOT absorbed in v1** (deferred — §10): no v1 security detector needs the Rust syntactic engine. v1
catches come from LLM bands + oracle (TS) + deps (pnpm) + headers (config parse). fable's bar is LOGIC bugs
(RBAC/IDOR/SQLi/SSRF/OAuth), not ast-grep-able syntax. Other domains — and with them slopgate's engine — follow
ONLY after security hits the bar (pilot-before-fan-out).

## §9 Skills to build (the project's operating tools — built in the plan/scaffold phase)
- **harvest** — census a repo for recurring deterministic check classes → candidate detectors (what the 3
  harvest agents did this session).
- **implement** — author a detector to the §4 contract (rule or fn + canary/anti-canary/discriminators); enforce
  the admission gate.
- **investigate** — forensic: reconstruct a bug cell from a fix commit; build ground truth.
- **create-tests** — build corpus cells (RED/GREEN + discriminators) + wire the bench.
- **coding-standards** — the project's own code rules (testing-native doctrine; the per-detector + per-band rules).

## §10 Salvage map (from `~/.claude/security-gate/` + slopgate)
- `gate.py` → `orchestrator/` (merge / k-roll / cross-file escalation loop).
- `oracle/oracle2.ts` → `domains/security/detectors/oracle/` (TS, type-aware — KEEP as-is; §5).
- `prompt_v2.txt` → the LLM-band prompt (security generalist).
- `TOOLBANK_DECISION.md` / `VALIDATION.md` / `zync-pilot/STEP2_ZYNC_PILOT.md` → `docs/`.
- **slopgate-core (Rust)** → `engine/`: absorb + MODULARIZE — make rules per-file, runtime-loaded; kill the 3
  anti-modular seams ([MEASURED] this session: compile-time `include_str!`, grouped monolithic JSON, manual
  `astIds` registration). Adopt canary/negativeCanary; ADD discriminators (§4).
- corpus cells: multideal baseline-missed + zync S4-001/S6 (have) → `corpus/`; extend across S1–S11.

## §11 Architecture Decisions (deletion-test / seam-audit — brainstorm self-review)
- **engine/ (Rust)** — KEEP. delete-test: removing it scatters fast syntactic scanning into slow per-language
  reimplementations. DEEP (callers see rules, not the regex/AST machinery). MEASURED need: slopgate exists + is the
  deterministic band.
- **orchestrator/** — KEEP. delete-test: removing it scatters routing/merge/k-roll/escalation into every caller.
  DEEP.
- **bench.py** — KEEP. delete-test: removing it returns validation to ad-hoc eyeballing; it IS the coverage-knob
  test (§3). DEEP.
- **detector contract (§4)** — KEEP. The convergent abstraction across ALL detectors/domains (×N detectors, not
  ×1 consumer). DEEP.
- **oracle as its own TS detector** (not folded into the Rust engine) — KEEP. seam-audit: type-aware analysis is a
  distinct capability from syntactic scanning; collapsing them would lose type info.
- **REJECTED:** a registry (convention discovery instead) · npm-publish packaging (one consumer) · a Rust
  orchestrator (LLM-latency-bound) · per-domain bespoke benches (one shared bench).
