GCA · CORE CONCEPTS

    Three-Tier Analysis Model

    How Gadriel Code analyzes source through three layers, each with a distinct job.

    The Three-Tier Analysis Model

    Gadriel Code analyzes source through three layers, each with a distinct job, a distinct trust level, and a distinct default. The guiding principle:

    Math finds. Graph understands. Claude explains.

    Only the deterministic engine (Tier 0) decides what gates CI. The other two tiers add corroboration, external-tool aggregation, and semantic reach — but they are advisory, and neither can weaken a deterministic verdict.

    Related: AI-coding integration · Privacy boundary


    Overview

    TierWhat it isRuns whereDefaultRole in CI
    Tier 0In-house deterministic engine (pattern / AST / taint)Local, offlineAlways onGate of record
    Tier 1Your own locally-installed analyzers (clang-tidy, gosec, bandit, eslint-security, …)Local subprocessOff (--corroborate)Advisory only
    Tier 2The host AI assistant's already-running LLM, borrowed via MCPHost agent's own LLM connectionOff (opt-in)Advisory only — never gates
                        ┌───────────────────────────────────────┐
       source on disk ─▶│  TIER 0  deterministic engine          │──▶ findings.json
                        │          pattern · AST · taint          │    (THE GATE)
                        └───────────────────────────────────────┘
                                         │  ▲
                    (opt-in)  --corroborate │  │ union / dedup (additive)
                                         ▼  │
                        ┌───────────────────────────────────────┐
       your scanners ──▶│  TIER 1  gosec / bandit / eslint / …   │──▶ corroborated_by
                        └───────────────────────────────────────┘    (advisory)
                                         │  ▲
            (opt-in, default OFF)        │  │ submit_ai_findings (advisory)
                                         ▼  │
                        ┌───────────────────────────────────────┐
       host LLM (MCP) ─▶│  TIER 2  authz · IDOR · business-logic │──▶ ai_advisory
                        └───────────────────────────────────────┘    (never gates)
    

    Tier 0 — the deterministic engine (the gate of record)

    Gadriel's own engine: tree-sitter-based parsing, AST analysis, and taint/flow tracking, running entirely on the developer's machine or their CI runner. It is always on, needs no network, and its output is the single source of truth for --fail-on, compliance KPIs, "Open Criticals", and OCSF emission.

    Everything downstream is measured against this floor. Tiers 1 and 2 exist to complement it (catch bugs it structurally cannot reach, or triage its findings), never to replace or override it.


    Tier 1 — orchestrate the developer's own analyzers

    Gadriel does not need to out-analyze a language's best OSS scanner to benefit from running it — it needs to aggregate. Tier 1 makes Gadriel the intelligence layer (cross-stream correlation, effective-risk re-scoring, domain gating, waivers, compliance-mapped reports) over scanners you already have installed.

    gadriel code scan --corroborate
    • Opt-in, off by default. --corroborate is a single global opt-in; a test (corroborate_flag_is_opt_in_and_off_by_default) locks that in. It can also be set via [scan] corroborate in .gadriel.toml (default false).
    • Advisory only. In doctor, Tier-1 checks are Warn, never Fail — a missing or too-old toolchain never hard-gates; CI gets one advisory line and proceeds on Tier 0.
    • Local subprocesses on source already on disk — the same shape as the C clang-tidy driver, covered by the privacy boundary. (The one guardrail: a semgrep adapter must pin --config to a vendored local ruleset and never pass auto/registry references, which would resolve rules over the network.)

    Adapters:

    AdapterLanguageInvocationOutput
    gosecGogosec -fmt=sarif ./...SARIF
    banditPythonbandit -r <path> -f jsonJSON
    eslint-securityJS/TSeslint --format jsonESLint JSON
    SpotBugs + FindSecBugsJavaspotbugs -sarif -pluginList findsecbugs.jarSARIF
    clippy / cargo-auditRustcargo clippy / cargo audit --jsonJSON (candidate; may not ship)

    Merge policy is additive, not suppressive. On overlap (normalized_path, line ± window, cwe_family) Gadriel keeps the higher confidence tier; ties keep the in-house finding (richer taint metadata) and attach the external (adapter_id, rule_id) as corroborated_by. On no overlap, both survive — most of the measured value comes from disjoint findings. A Tier-1 external finding is still FindingOrigin::Deterministic (it is not an AI finding), tagged ToolProvenance::Tier1External, and lands at ConfidenceTier::Medium until benchmarked — never High by assumption.

    Measure before defaulting on. No adapter is enabled by default without a measured union-lift of ≥ 0.05 at zero FP-on-fix regression on a proper corpus. As of the latest measurement pass, all four adapters remain opt-in; no default was flipped.


    Tier 2 — borrow the host assistant's LLM

    Pattern/AST/taint analysis is structurally blind to a whole class of vulnerabilities — the semantic long-tail:

    • missing authorization checks (the absence of a call is invisible to taint)
    • IDOR / broken object-level auth (data flows correctly; no missing sanitizer)
    • business-logic flaws, insecure design, missing defense-in-depth
    • intent admitted in comments/TODOs

    These require reasoning about what the code is supposed to do. Tier 2 reaches them by borrowing the LLM the host AI assistant is already running — via the MCP submit_ai_findings callback — rather than opening a new API relationship. That is what makes it zero-marginal-cost and privacy-preserving (see privacy.md).

    Opt-in, default OFF

    Tier 2 is off by default. It engages only when both of these resolve true in .gadriel.toml (or the equivalent flags/env):

    [tier2]
    enabled = false # default OFF — the on/off switch
    source_consent = false # default OFF — privacy gate for sending source

    Mechanically, [tier2] enabled is the switch: the MCP server only creates a Tier2SessionRegistry when it resolves true. When off, validate_file / validate_buffer emit tier2_request: null and never invite the host agent; submit_ai_findings is still registered but is inert (it is backed by a private registry that never receives a session). Resolution order for the enable flag is --tier2 flag ▸ GADRIEL_TIER2 env ▸ [tier2].enabledfalse.

    The default gadriel code scan is, and remains, 100% deterministic: every finding carries finding_origin: deterministic with no tier2/ai_* fields.

    How it flows

    validate_file (Tier-2 ON) ─▶ opens a session, returns tier2_request{session_id, chunks}
            │
            ▼
    host agent's LLM answers the semantic questions
            │
            ▼
    submit_ai_findings{session_id, ai_findings[]} ─▶ fuse() ─▶ FusionReport
    

    Sessions are ephemeral: in-process only, one-shot consume, 120-second TTL.


    The invariant: AI is advisory and can never weaken the gate

    This is enforced as a type-level invariant in Rust, not a render flag (the Finding::domain() invariant):

    1. Never-gate. --fail-on, compliance KPIs, "Open Criticals", and OCSF emission count only deterministic and deterministic + ai_corroborated findings. An AI-only finding is FindingDomain::AiAdvisory (non-gating) regardless of scan type or pillar — a rule cannot promote an AI finding into the gate. Every gate consumer excludes AI-only findings with no change to scan.rs.
    2. Hard confidence floor. An ai_advisory finding is floored to ConfidenceTier::Low at emission, so a "Critical" AI claim renders as Effective-Risk Low / unverified=true everywhere.
    3. AI can never suppress a deterministic finding. A deterministic finding always survives and keeps gating; an AI refutation does not remove it.

    The one bounded exception: SAST-Medium refutation

    This narrows invariant 3 to a single, auditable case. A Tier-2 refutation may demote one business field — confidence_tier — and only for a finding that is scan_type == Sast and confidence_tier == Medium. It touches nothing else:

    • it does not change severity;
    • it does not set FindingOrigin::AiAdvisory (so the type-level never-gate short-circuit never engages);
    • it does not remove the finding — the refuted finding stays in findings.json/the report with a full ai_refutation audit block.

    The actual CodeVuln → CiConfig reclassification is produced by the pre-existing gate reading the demoted value — no new gate is added. If that gate is ever removed, refutation silently becomes a no-op (fails closed), never a bypass. Low-confidence SAST is already non-gating, and SAST never reaches High, so Medium is the only tier where a refutation can change the gating outcome — and even there it is guarded by a rationale signal-word check, a server-side eligibility re-check, and a hard protected-true-positive zero-loss canary gate.

    Why the split of labor

    ClassStructural reachabilityBest tier
    Memory safety (UAF, buffer overflow)deterministic taint / clangTier 0 / Tier 1
    Injection with a known sinkpattern / AST / taintTier 0
    Missing authz, IDOR, business-logicinvisible to pattern/AST/taintTier 2 (advisory)
    FP triage of existing findingsTier 2 (corroborate/refute)

    Independent literature (mid-2026) puts LLM code-analysis at a >85% spurious rate on real projects, which is exactly why the ai_advisory band is labeled, floored to Low, never gated, and structurally filtered. The value is delivery + grounding + correlation against the deterministic floor — not the LLM in isolation.