Guardrail Panel

Pro

Safety policy as an operating surface: the rules, what they caught, and the answer the policy actually produced. 3 variants: rules, feed and redaction preview.

</>sv-guardrail-panel aisafetyguardrailspii
import { SvGuardrailPanelComponent } from '@styloviz-pro/guardrail-panel';

Demo & usage

Basic usage

Pro

Usage example — open the gallery for the full interactive demo.

Pro

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Feed & redaction preview

Pro

A policy tuned to your own traffic

Pro

When to use

  • Tuning a safety policy, where the question is not how many things it caught but what it did to the output.
  • Reviewing PII redaction before it ships, so an over-redacted answer is caught by a human rather than by a customer.
  • Auditing which rules earn their latency — a policy grows rules and never loses them.

Async states

  • loading
  • error
  • retry

Item identity & owned state

Restorable state
  • pendingRuleIds
  • revealedEventIds

Bind these to put a saved view back — the open sections, the hidden columns, the selected rows — and read their Change events to store what the user does.

Inputs

NameTypeDefaultDescription
loadingbooleanfalseTrue while the policy is being fetched; shows a skeleton in place of the panel.
errorSvDataError | string | nullnullLoad failure, shown as an error panel in place of the content. Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`.
variantGuardrailVariant'rules'Presentation: the policy, the feed, or one event's text.
rulesreadonly PolicyRule[][]The policy.
eventsreadonly GuardrailEvent[][]What the policy has seen.
selectedEventIdstring | null (two-way)nullThe event whose text the `redaction` variant shows. Two-way, because selecting a row in the feed should move it. Falls back to the first event that actually had something masked — a redaction preview opening on an untouched answer teaches nothing.
maskstring'[REDACTED]'What a masked run is replaced with.
revealScopeSvRevealScope'on-request'Who may see the original text behind a redaction. THE REDACTION REVIEWER SEES EXACTLY WHAT THE REDACTION EXISTS TO HIDE. Unavoidable for the person judging whether a rule fired correctly — and not a reason to render every event's raw text to everyone who opens the panel, on a surface that gets screenshotted into tickets and shared over calls. `'on-request'` needs the role *and* a click on that event; `'all'` is for a role whose job is reading raw matches all day, because a reviewer who clicks reflexively two hundred times is not consenting to anything.
editablebooleantrueWhether the reader may change the policy. A GUARDRAIL TOGGLE IS AN AUTHORIZATION DECISION, AND THE COMPONENT DOES NOT KNOW THE ROLE. Set it false and the rules stay listed and readable with no controls — a policy a reader cannot see is a policy nobody revisits, so read-only is the right shape rather than an empty panel.
pendingRuleIdsreadonly string[] | ReadonlySet<string>[]Ids of rules with a toggle in flight. Their controls disable, so a second click cannot fire the same privileged change twice.
policyVersionstring | nullnullThe policy's version, as the host's system of record names it.
policyHistoryreadonly SvPolicyChange[][]Recorded changes, newest first. ORDERED BY THE HOST, NOT SORTED HERE. These are display strings from whatever keeps the audit log, and parsing them to sort would invent an ordering out of "2 Sept" and "yesterday".
nownumber | nullnullClock for the toggle payload, injectable so a test is not at the mercy of the wall clock.
lowConfidenceMatchnumberSV_LOW_CONFIDENCE_MATCHConfidence below which a match counts as a guess, `0`–`1`. EVERY THRESHOLD ON THIS COMPONENT IS A POLICY QUESTION WEARING A NUMBER, and every default is about somebody else's traffic. 0.7 is where a classifier's guess stops being trusted — a team matching medical identifiers wants it higher than a team filtering profanity.
overRedactionPctnumberSV_OVER_REDACTION_PCTShare of an answer past which the policy has probably destroyed it, `0`–`100`. 30 assumes an answer of a few hundred characters. On an SMS reply it fires constantly; on a ten-page report it never does.
destructiveRunCharsnumberSV_DESTRUCTIVE_RUN_CHARSA single masked run this long means a claim went, not a phone number. 120 characters is a paragraph in prose and three lines of a table.
dormantMinEventsnumberSV_DORMANT_MIN_EVENTSEvents needed before "this rule never fired" means anything. On forty requests a zero-hit rule has not had the chance. Calling it dormant there teaches the reader to ignore the label, and then the genuinely dead rule goes unread with the rest — which is the failure the dormant badge exists to prevent.
revealedEventIdsReadonlySet<string> (two-way)new Set()Event ids the reader has explicitly asked to see in the clear. A `model()`, NOT A PRIVATE SIGNAL — and `check:collection-identity` caught it as one. Keyed state a host cannot reach is state that cannot be restored after a route change, and on *this* surface it is worse than inconvenient: a host logging reveals through `(revealRequested)` needs to be able to put the same set back, or its own audit trail and what the panel shows disagree after every navigation.

Outputs

EventPayload typeDescription
retryvoidEmitted when the user presses the error panel's "Try again" button (retryable errors only); the host should re-run the failed load.
ruleToggleSvRuleToggleThe reader asked to enable or disable a rule. CARRIES THE STATE THE READER SAW. Disabling a guardrail is a privileged, logged act, and a payload of just the rule says which rule was clicked without saying what the reader believed they were changing. Two operators disabling the same rule during an incident is the ordinary case: the second toggle is a no-op, not a second change, and an audit log that records it as one describes an event that never happened. `svRuleToggleIsStale()` is the comparison. **Breaking:** the payload was `PolicyRule`. Migration is `$event` → `$event.rule`.
revealRequestedGuardrailEventThe reader asked to see one event's original text. Log it.
eventSelectGuardrailEventThe reader activated an event.
selectedEventIdChangestring | nullEmits the new `selectedEventId` whenever it changes — the output half of `[(selectedEventId)]`.
revealedEventIdsChangeReadonlySet<string>Emits the new `revealedEventIds` whenever it changes — the output half of `[(revealedEventIds)]`.

Wire it to your API

This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.

Template

Pro

Bound to the names this component actually exposes.

Host class

Pro

Drive the three async inputs from your own fetch.

  • This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `events`, `loading` and `error`.
  • Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.

Full walkthrough: Connecting to a backend.

Customization & theming

  • `[revealScope]` decides who sees the original text behind a redaction: `on-request` (the default — the role allows it and the reader asks for that one event), `all` (a role whose job is reading raw matches all day, because a reviewer told to check two hundred matches cannot click two hundred times) or `none`. The feed masks **by span**, using the ranges the policy itself found, so it is offset-exact — which is why it does not hit the objection that keeps the pack-wide pattern policy (`X-2`) out of this component. Every reveal fires `(revealRequested)` so the host can log who looked at what; hiding it again does not, because nobody needs an audit trail of somebody closing a panel. The redaction preview's **As generated** view is the same reveal under the same scope: under `none` the control is absent and the preview stays on the delivered text; under `on-request` showing the original adds the event to the shared `[(revealedEventIds)]` and fires the same `(revealRequested)`, so one audit log covers both surfaces; under `all` it is a plain view switch. The preview opens masked in every scope, including for an event already revealed in the feed.
  • `[editable]` false shows every rule and its state with no controls — a guardrail toggle is an authorization decision and the component does not know the role, and a policy nobody can see is a policy nobody revisits. The handler refuses as well as the control disappearing, so a host calling it directly cannot change a policy either.
  • `(ruleToggle)` carries `{ rule, enabled, observedEnabled, at }`. Two operators disabling the same rule during an incident is the ordinary case: the second toggle is a **no-op, not a second change**, and an audit log that records it as one describes an event that never happened. `svRuleToggleIsStale(toggle, current)` is the comparison. `[pendingRuleIds]` disables a rule while its change is in flight, so one click cannot fire the same privileged change twice. **Breaking:** the payload used to be the bare `PolicyRule` — migrate `$event` to `$event.rule`.
  • `[policyVersion]` and `[policyHistory]` (newest first, in the host’s own order — parsing '2 Sept' and 'yesterday' to sort would invent an ordering) render the version and the last change with its author, because 'who turned this off, and when' is the first question asked after an incident.
  • The pack-wide privacy policy (`X-2`) deliberately does NOT apply to this panel. Every span here is a pair of offsets into the event text, so masking that text first would shift every offset after it and the highlighting would colour the wrong characters; and this is the surface where an operator reads what their own redaction policy did, so a second invisible layer would hide the over-redaction evidence the panel exists to show.
  • `rules` and `events` are inputs and both default to empty; import `DEMO_RULES` / `DEMO_EVENTS` for a demo.
  • Three layouts via `variant`: `rules` (the policy in review order), `feed` (what it saw, newest first) and `redaction` (one event before and after).
  • Overlapping spans are merged before anything is masked. Two rules matching the same address produce overlapping ranges, and splicing them one at a time shifts every offset after the first replacement.
  • A merged run keeps the LOWEST confidence and the STRONGEST action of its contributors: a run containing a guess is a guess, and a `block` overlapping a `redact` is a block.
  • `[overRedactionPct]` (30) and `[destructiveRunChars]` (120) each independently flag a redaction that probably destroyed the answer — they catch different failures, one scattered and one contiguous.
  • `[dormantMinEvents]` (200) is the traffic needed before "this rule never fired" means anything. Disabled rules are never called dormant: they cost nothing.
  • All four thresholds are inputs defaulting to the exported `SV_LOW_CONFIDENCE_MATCH`, `SV_OVER_REDACTION_PCT`, `SV_DESTRUCTIVE_RUN_CHARS` and `SV_DORMANT_MIN_EVENTS`, which are still exported for direct callers of `svRuleStats` / `svRedactionImpact` / `svIsUncertainMatch` — each of those now takes the thresholds as an optional last argument. `thresholds()` reports what the component settled on, and `svResolveGuardrailThresholds` is exported so a host can compute the same answer.
  • Emits `ruleToggle` and `eventSelect`; `selectedEventId` is two-way, so a feed row moves the preview.

Best practices

  • Do: Read the redaction preview. "Redaction applied" reads as a success on every dashboard, and it is also what a policy that ate the whole answer reports.
  • Do: Give classifier matches a `confidence`. A regex either matched or it did not; a model guessed, and the two must not be silently redacted alike.
  • Don't: Do not sum blocks and redactions into one "violations" number. Three refusals and a hundred masked phone numbers are not a hundred and three of anything.
  • Do: Set the four thresholds to what they mean in your domain. 0.7 confidence suits a mixed matcher, 30% assumes an answer a few hundred characters long, 120 characters is a paragraph in prose and three lines of a table, and 200 events is an afternoon for one product and a quarter for another.
  • Don't: Don't pass `[lowConfidenceMatch]` as a percentage. 70 is not 0.7: it would make every match a guess, every answer suspect, and a reviewer who stops reading the flag by the end of the day. Out-of-range figures fall back to the documented default rather than being clamped into range, because a 70 clamped to 1 behaves just as wrongly with nothing to show why.
  • Tip: Rules that never fire are listed first, not last. Each one costs latency on every request, and volume ordering puts them below the fold forever.

Accessibility

  • Each rule row states its action, hits and the dormancy caveat in its accessible name, not only in a badge.
  • Blocked and redacted are given visibly different colours as well as different words, because one of them is a user who got nothing.
  • The redaction toggle reports `aria-pressed`; masked runs carry a title naming the rule and its confidence.

Feedback on Guardrail Panel

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.