Vanilla Breeze

nfr-compass

11-ility prioritization decision surface that spends a numeric capacity budget supplied by . Outputs a quality vector with required rationales for every Critical pick — and a captured overrunRationale when the team consciously chooses to spend over budget.

Overview

The <nfr-compass> component is the Planning Pack's quality-decision surface. It presents the eleven non-functional requirement (NFR) categories — performance, accessibility, security, reliability, maintainability, observability, compatibility, scalability, portability, internationalization, privacy — as one form, asks the team to mark each as Critical, Important, Acceptable, or Not relevant, and enforces scarcity by spending a numeric capacity budget against every Critical pick. Reach for it whenever a requirements document tries to flag everything as "must"; the compass replaces that anti-pattern with a forced-trade interface backed by real numbers from <iron-triangle>.

The output is a structured quality vector with required rationales — each Critical pick must include a one-sentence "why," and going over budget requires a written overrunRationale. Downstream tools (ADRs, validators, dashboards, CI guards) read the vector; the rationale is the protective shell that stops the choice from being relitigated every Monday.

Live demo — paired with iron-triangle

Two siblings: an <iron-triangle id="shape"> supplies the capacity budget, and the compass binds to it via data-bind-to="shape". Mark a row as Critical to spend points; cross the budget and an over-budget rationale field appears below the rows.

The cost-weight + capacity model

Two layers, both with shipped defaults, both override-friendly.

Capacity comes from <iron-triangle>

Capacity is resolved in this order on upgrade and on every iron-triangle:change:

  1. A sibling <iron-triangle> matched by data-bind-to="id". The compass listens for iron-triangle:change and updates live.
  2. The literal data-capacity-points attribute (compass-only usage with a fixed budget).
  3. The capacityPoints property (programmatic).

If none resolves, capacity is Infinity and the compass becomes informational — you can pick Criticals freely with no budget gate, and a console warning suggests pairing with <iron-triangle>.

Cost weights per ility

Critical picks spend points; Important / Acceptable / Not-relevant cost 0. Default Critical cost weights (opinionated, easy to override):

IlityDefault Critical cost
accessibility 3
performance 5
security 5
reliability 4
observability 3
internationalization 4
compatibility 2
portability 3
privacy 4
scalability 5
maintainability 2

Sum at all-Critical = 40. A realistic mid-size project budget is 10–15, so the team is forced to choose. Override per project via attribute (the merged weights are frozen into the saved vector as costWeights so a future re-validation produces the same arithmetic):

Over-budget rationale flow

The compass allows going over budget, but captures the override so it can't quietly happen again. When criticalSum > capacityPoints:

  1. The pick lands. The header readout flips into the over-budget tone, and an over-budget chip appears next to the heaviest Critical(s).
  2. An overrunRationale textarea reveals below the row list with the prompt "You're over budget by N points — explain why."
  3. The form refuses to submit until overrunRationale.length >= data-min-overrun-rationale (default 10).
  4. The saved vector includes overrunRationale and the override is now durable.

The companion uucd nfr check CLI rejects vectors where criticalSum > capacityPoints and overrunRationale is missing (only possible via hand-edit; the UI prevents it).

Default 11 ilities

If you omit the <fieldset>s entirely, the compass injects the default eleven on upgrade. To opt out and ship a project-specific list (e.g. a docs site that only cares about six), author the <fieldset name="…"> rows yourself in the default slot and the component will use yours instead.

Add matching entries to data-cost-weights if your custom rows are absent from the default weight table.

Compass-only (no triangle)

If there's no project-shape surface to bind to, supply a literal capacity number. The over-budget gate still works; you just lose drift detection.

Attributes

AttributeTypeDefaultDescription
name string nfr-vector Form-association field name.
data-bind-to string ID of a sibling <iron-triangle> to read capacity from.
data-capacity-points integer Literal capacity fallback when no triangle is bound.
data-cost-weights JSON object string Per-ility cost-weight overrides; merged on top of the defaults.
data-min-rationale integer 10 Min characters for a Critical row's rationale.
data-max-rationale integer 200 Max characters for the rationale textarea.
data-min-overrun-rationale integer 10 Min characters for overrunRationale.
data-max-overrun-rationale integer 400 Max characters for overrunRationale.
disabled boolean absent All inputs disabled.
locked boolean absent Read-only mode for shipped vectors.

Slots

SlotExpected contentRequired
title Heading text shown above the rows no (defaults to "NFR Compass")
(default slot) <fieldset name="…"> rows; omit to get the default 11 ilities no
notes Project-supplied advisory notes no
overrun-rationale Pre-existing <textarea name="overrunRationale">; one is created when missing no
footer Save / Submit area no

Events

EventDetailWhen
nfr-compass:change { vector, rationales, costWeights, capacityPoints, criticalSum, overrunRationale, source } Any pick or rationale change.
nfr-compass:over-budget { delta, criticalSum, capacityPoints } The selection just crossed into over-budget.
nfr-compass:under-budget { slack, criticalSum, capacityPoints } The selection just returned within budget.

Internal state hooks

StateWhen
:state(over-budget) Set when criticalSum > capacityPoints; CSS can highlight the readout.
:state(missing-rationale) Set during validation when any Critical lacks a complete rationale or overrunRationale is absent while over-budget.

JavaScript API

PropertyTypeDescription
.vector Record<ility, 'critical' \| 'important' \| 'acceptable' \| 'not-relevant'> Current picks. Setting it replaces the form state.
.rationales Record<ility, string> Get / set rationale strings. Keys must match Criticals in the vector.
.costWeights Record<ility, integer> Effective merged weights (defaults + overrides).
.capacityPoints integer Current capacity (resolved from sibling, attribute, or property).
.criticalSum integer (readonly) Live sum of weights for current Criticals.
.overBudget boolean (readonly) criticalSum > capacityPoints.
.overrunRationale string Required textarea contents when over budget.
.value object (readonly) Full snapshot for serialization.

CSS Tokens

TokenDefaultPurpose
--nfr-compass-padding var(--size-l) Outer padding
--nfr-compass-row-gap var(--size-s) Vertical gap between rows
--nfr-compass-pill-bg var(--color-surface-raised) Segmented-control track
--nfr-compass-critical-bg var(--color-error-subtle) Critical pip background
--nfr-compass-important-bg var(--color-warning-subtle) Important pip background
--nfr-compass-acceptable-bg var(--color-success-subtle) Acceptable pip background
--nfr-compass-not-relevant-bg var(--color-surface) Not-relevant pip background
--nfr-compass-overbudget-color var(--color-error) Over-budget chip + readout when over

Static fallback

Without JavaScript, the compass is a plain <form> with one <fieldset> per ility, four-radio priority groups, and hidden rationale textareas. The form submits to its action with all picks intact, and a server can validate the budget against a stored capacity. After upgrade the component reveals rationale fields when Critical is selected, computes criticalSum live, surfaces the over-budget chip, and emits the events documented above.

Accessibility

  • Native form semantics throughout. Real <fieldset> / <legend> / radiogroup; no ARIA role overrides.
  • Live header readout uses <output aria-live="polite"> so screen readers hear "Now spending 12 of 10 points; rationale required" as picks change.
  • Over-budget state is conveyed by text + icon, never color alone.
  • Tab order: across rows, then within a row across the four priority buttons. The rationale textarea joins the tab order only when the row is Critical.
  • Logical properties throughout for clean RTL and writing-mode support.

Related

  • <iron-triangle>upstream capacity supplier. Compass reads capacityPoints from a paired triangle and stamps ironTriangleHash into the saved vector for drift detection.
  • <adr-wc> — record an over-budget Critical pick as an ADR so the rationale survives the review cycle.
  • UX Planning Pack — loads iron-triangle, nfr-compass, and the rest of the planning surface together.