-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
RegRails is two artifacts that share one source of truth:
- 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. - 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.
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.
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"]
Step by step:
-
ConsultationRequest— the structured contract the advisor emits before answering. It carries thequery, atopic(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.). -
decide(req, rules)— routes bytopic, walks the relevant deterministic cascade, and produces anOutcomewith a list of CFR citations. This is pure and reproducible. -
Risk tiering —
decidepost-processes the outcome into a(risk_tier, human_gate_required)pair.escalate_human_reviewis alwayshigh+ gated;blockand the softer escalations aremedium;insufficient_facts/out_of_scopearelow. -
GuardrailDecision— the typed result:outcome,risk_tier,human_gate_required,citations_emitted,matched_rules,framework, pluslatency_msand the advisormodelid. - Provenance (optional) — if an audit sink is supplied, the decision is appended to a hash-chained JSONL log.
-
LLM render (optional) — only now does
llm.advisor_rendercall 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/ 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.py—POST /api/decideruns the engine'sdecide()and returns the typedGuardrailDecision. No LLM, no API key — it installsregrailsfrom PyPI and calls the deterministic engine. This is the live demo's backend. -
api/reply.py—POST /api/replyrenders the advisor's user-facing text for a decision (the LLM step). -
api/verify.py—POST /api/verifyrecomputes 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 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.pyasserts the committedweb/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.pyexercises 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.