Skip to content

Architecture

Allen Byrd edited this page Jun 2, 2026 · 1 revision

Architecture

RegRails is two artifacts that share one source of truth:

  1. A Python package (regrails, published to PyPI) — the encoded rules, the deterministic decision engine, the faithfulness gate, the hash-chained audit log, the OSCAL/SARIF exporters, and the MCP server.
  2. A React web platform (web/, deployed to regrails.polycentriclabs.com) — a 10-route single-page app plus three Python serverless functions, which renders the package's own generated output so the site can never silently drift from the code.

This page covers the package modules and their responsibilities, the data flow through a single consultation, the web platform's shape, and the anti-drift data pipeline that keeps the two in lockstep.

See also: The-Guardrail-Engine for the decision logic, Frameworks-and-Rules for the rule schema and faithfulness gate.


Package modules

The library lives under src/regrails/. Each module has a single, narrow responsibility:

Module Responsibility
models.py The Pydantic v2 data models — Citation, Rule, RegulationSection, ResearchSnapshot, GuardrailDecision — plus the Outcome, RiskTier, RuleType, and Severity literal sets. The shared base sets extra="forbid", so a typo'd field fails at load time.
encode.py The multi-framework loader. Reads the encoded YAML rules and pairs each with its bundled verbatim CFR text, computing a SHA-256 over every section. load_all_rules() / load_all_sections() span both frameworks; load_sections(framework=...) loads one.
faithfulness.py The verbatim-text gate. For every rule, checks source_quote against the bundled section text by substring containment or token coverage ≥ 0.85, and reports which check carried each rule. Ported from Evidentia's Jaccard faithfulness algorithm.
guardrail.py The deterministic decision engine. Defines ConsultationRequest, derives rule-trigger tags, routes by topic into the FERPA or Title IV cascade, assigns the risk tier + human-gate flag, and returns a GuardrailDecision. No LLM is involved.
audit.py The EventAction enum, an append-only JSONL event emitter, and the hash-chained decision provenance (append_decision / verify_chain). Each record stores record_hash = SHA-256(prev_hash + canonical(decision)).
oscal.py Exports the 37 rules as an OSCAL 1.1.2-shaped catalog — one group per framework, one control per rule, a back-matter binding each section's SHA-256. Structurally aligned, not run through the NIST validator (stated honestly in the module).
sarif.py Exports decisions as a SARIF 2.1.0 results document. block / escalate_human_review become SARIF error results with the controlling CFR citation as the ruleId, so CI can fail a pull request.
mcp_server.py The MCP server. Exposes three agent-callable tools (consult_guardrail, list_rules, check_faithfulness) over stdio via FastMCP, so an AI agent consults the guardrail before answering.
llm.py The advisor renderer. After the engine has decided, advisor_render calls an LLM (via OpenRouter, with retry + provider fallback) to phrase the single user-facing reply. This is the LLM's only job.
report.py Builds a self-contained, dependency-free HTML decision report from recorded demo runs — for a non-technical reviewer to open in a browser.
coverage.py Builds the rule-to-scenario traceability matrix from the golden corpus and flags rules no scenario exercises.
research.py The low-level OpenRouter chat-completion helper + the research-stream runner that persists Perplexity Sonar snapshots used to ground the encoding.
ids.py The shared SHA-256 hex helper (sha256_hex) used by the models, encoder, and audit chain.
demo.py The advisor demo driver (python -m regrails.demo) — runs scenarios across both frameworks, with a --replay mode that needs no API key.
cli/ The Typer CLI: regrails {decide, check, encode, research, audit, coverage, report, mcp, export, bench}.

The encoded rules themselves live in data/encoded/*.yaml, the bundled verbatim CFR text in data/cfr/*.txt, and the labeled scenarios in tests/golden/*.jsonl.


Data flow: one consultation

The central contract is that the decision is made before any model call. Concretely, for a single query:

flowchart TD
  Q[User query] --> C[Advisor builds a typed ConsultationRequest]
  C --> D["decide(req, rules)"]
  D --> R{topic?}
  R -- other --> OOS[out_of_scope]
  R -- aid_status --> TIV["Title IV cascade<br/>SAP / eligibility"]
  R -- disclosure / unknown --> FERPA["FERPA cascade<br/>disclosure"]
  OOS --> O[Outcome + citations]
  TIV --> O
  FERPA --> O
  O --> RT["assign risk_tier + human_gate_required"]
  RT --> DEC[GuardrailDecision]
  DEC --> PROV["optional: append to hash-chained log"]
  DEC --> LLM["optional: LLM renders the user-facing reply ONLY"]
Loading

Step by step:

  1. ConsultationRequest — the structured contract the advisor emits before answering. It carries the query, a topic (disclosure / aid_status / other / unknown), the requester role and purpose, the data requested, and the FERPA-path and Title IV-path fact flags (consent on file, opted out of directory, emergency justified, SAP status, in default, etc.).
  2. decide(req, rules) — routes by topic, walks the relevant deterministic cascade, and produces an Outcome with a list of CFR citations. This is pure and reproducible.
  3. Risk tieringdecide post-processes the outcome into a (risk_tier, human_gate_required) pair. escalate_human_review is always high + gated; block and the softer escalations are medium; insufficient_facts / out_of_scope are low.
  4. GuardrailDecision — the typed result: outcome, risk_tier, human_gate_required, citations_emitted, matched_rules, framework, plus latency_ms and the advisor model id.
  5. Provenance (optional) — if an audit sink is supplied, the decision is appended to a hash-chained JSONL log.
  6. LLM render (optional) — only now does llm.advisor_render call a model to phrase the reply. The model receives the already-made decision as JSON and renders 3–5 sentences of user-facing text. It cannot change the outcome.

Because the decision is fully determined by step 3, the engine is testable without model stochasticity: the golden corpus asserts directly on decide()'s output, never on LLM behavior.


The web platform

The web/ directory is a Vite + React single-page application plus a small serverless backend, deployed on Vercel with the project root set to web/.

React SPA — 10 routes. A single routes/registry.tsx is the one source of truth for the destinations; the router and the nav rail both read it. The routes:

Route Purpose
/ Live demo Type a query + structured fields; the engine decides before any LLM.
/rules A searchable table over all 37 rules, each expandable to its verbatim CFR text and section hash.
/coverage The 31/37 coverage headline, the 6 named gaps, and the 22 golden scenarios.
/benchmark The held-out eval: benign-allow and high-stakes-intercept rates, per-model figures, the judge agreement statistic.
/methodology Renders METHODOLOGY.md + COVERAGE.md verbatim.
/provenance Loads a real sample hash chain; a Verify button and a Tamper button demonstrate detection.
/exports The OSCAL (1.1.2-shaped) and SARIF (2.1.0) outputs, with their honest-scope notes.
/mcp Documents the three MCP tools with their real signatures and a recorded stdio transcript.
/action The reusable GitHub Action that gates a CI job.
/about The thesis, the role mapping, and the resource links (repo / PyPI / HF / Evidentia).

Three Python serverless functions (web/api/):

  • api/decide.pyPOST /api/decide runs the engine's decide() and returns the typed GuardrailDecision. No LLM, no API key — it installs regrails from PyPI and calls the deterministic engine. This is the live demo's backend.
  • api/reply.pyPOST /api/reply renders the advisor's user-facing text for a decision (the LLM step).
  • api/verify.pyPOST /api/verify recomputes a submitted hash chain and reports whether it is intact, powering the Provenance route's Verify / Tamper buttons.

The two decision endpoints share a single normalize_consultation() helper from the package, so a browser form (which sends one CSV text field, not a JSON array) and the JSON API can never drift in how they build a ConsultationRequest.


The anti-drift data pipeline

The web platform does not re-implement any of the package's logic or hand-author any numbers. Instead, the site renders the package's own generated JSON.

A generator (exercised by tests/test_gen_web_data.py) emits the static data the SPA reads into web/public/data/:

File Generated from
rules.json The encoded rules + bundled section text + section hashes.
coverage.json The coverage.py traceability matrix (31 covered / 37 rules / 6 gaps / 22 golden).
eval.json The held-out benchmark aggregation.
oscal.json The oscal.py catalog (oscal-version = 1.1.2).
sarif.json A sarif.py results document (version = 2.1.0).
methodology.json METHODOLOGY.md + COVERAGE.md, rendered verbatim.

Two test suites lock this down so the published site cannot lie about the package:

  • tests/test_web_data_parity.py asserts the committed web/public/data/* is byte-identical to what the generator produces from the current code, and that the committed provenance sample is a valid chain.
  • tests/test_gen_web_data.py exercises the generator itself.

The consequence: any change to a rule, the coverage matrix, the OSCAL/SARIF shape, or the methodology text that is not regenerated into web/public/data/ fails CI. The site is a view over the package, verified on every run — the live demo's /api/decide even installs the published regrails==0.4.0 from PyPI, so what a visitor sees is the same engine the package ships.

Clone this wiki locally