VB Project Planning

<iron-triangle>

Project-shape constraint surface — captures Time × Cost × Scope and computes a single capacityPoints budget that downstream planning components can spend on quality decisions.

Overview

The <iron-triangle> component is the planning surface's constraint view, and it is the UI. Three vertex hit-targets — Scope (top), Time (bottom-left), Cost (bottom-right) — open native <dialog> editors on click. The center "Quality" target shows the live capacityPoints integer and routes to the quality decision when activated. Hover any target for a native tooltip preview of its current value.

The author writes a single empty element. The component generates the SVG and the per-vertex dialogs from a fixed schema; inputs inside the dialogs use VB primitives (<form-field>, <fieldset>, data-layout). The component is form-associated; capacityPoints plus the full T/C/S snapshot serialize as JSON for a containing <form>.

The triangle deforms with project shape

Each vertex sits on an equilateral baseline ray and stretches outward by a factor in [0.55, 1.45] derived from that constraint's magnitude relative to the other two (time = total weeks, cost = FTE × hours/week, scope = weighted feature counts). A balanced project stays close to equilateral; a long-deadline project pushes Scope up; a tiny team pulls Cost toward the centroid. The capacity number anchors at the centroid, so the integer never moves when the shape changes.

Per-vertex editors

Click a vertex to open its dialog. Each editor is built from VB <form-field> rows — labels above controls, native validation, no inline width hacks. The dialog's method="dialog" means submitting closes it automatically.

VertexInputsDrives capacity?
Scopemust-have count, should-have count, notesno — informational
Timesprint length, number of sprints, hours/week, deadlineyes (sprintWeeks × sprintCount)
Costteam FTE, budget tier, contractor budgetyes (teamFTE drives the formula)

The cost dialog mixes inputs that drive the formula (team FTE) with inputs that record the shape but don't (budget tier, contractor budget). The latter are metadata — they answer "why does the team look this way?" without polluting the math.

The capacity formula

Capacity is one integer the team picked together. The default formula is intentionally tiny:

The focus factor represents the share of FTE actually available for engineering quality work — the remaining 40% goes to feature delivery, meetings, ops, and the unknowns. 0.6 is the middle of the 50–70% range most teams settle on; tune per project with data-focus-factor. Flip to manual mode and the integer becomes editable directly (for a board commitment or a carried-over OKR); the T/C/S inputs stay visible and saved but no longer drive the readout, and capacitySource records which path produced the number.

The center "Quality" target

The capacity number is the headline; clicking it opens the quality decision. The component dispatches a cancelable iron-triangle:open-quality event with { qualitySummary, capacityPoints }. If the event isn't preventDefault()'d and data-quality-href is set, the page navigates to that URL — SPAs intercept the event and route in-app, static pages just set the href and let the fallback navigate. Set data-quality-summary to surface the saved picks on hover. Every change also recomputes a stable FNV-1a hash of the T/C/S object (exposed via .hash), which downstream consumers stamp into their saved vector so a CI guard can detect drift against a since-changed project shape.

Drift detection

Every change recomputes a stable FNV-1a hash of the T/C/S object — exposed via .hash and emitted in iron-triangle:change. Downstream consumers (notably <quality-target>) stamp this hash into their saved vector so a CI guard can detect when a quality vector was decided against a since-changed project shape.

If you'd rather not wire it manually, point the compass at the triangle by id and the binding happens for you:

Revisions with reasons

Edits made via the revise() method append to revisionLog and emit iron-triangle:revise. The reason is required (≥ 10 characters) so the constraint surface accumulates institutional memory rather than vanishing into Slack threads.

Attributes

AttributeValuesDefaultDescription
namestringtriangleForm-association field name
data-focus-factornumber0.6Multiplier in the default capacity formula
data-min-capacitynumber1Floor for capacityPoints
data-quality-hrefstringNavigation fallback when iron-triangle:open-quality isn't preventDefault-ed
data-quality-summarystringShort summary text shown in the center's native tooltip + aria-label (e.g. '3 critical: perf, sec, a11y')
disabledbooleanAll inputs disabled
lockedbooleanRead-only mode for shipped vectors

Slots

SlotPurposeRequired
title Heading text shown above the form no (defaults to "Iron Triangle")
time-controls Override the default Time inputs no
cost-controls Override the default Cost inputs no
scope-controls Override the default Scope inputs no
capacity-readout Override the readout block (advanced) no
footer Save / Submit button area no

Per VB convention, content lives in slots and state lives in attributes. The default-slot fieldsets named time, cost, scope, and capacity are the markup contract — you can hand-author them (as in the static fallback) or let the component inject sensible defaults if absent.

Events

EventDetailWhen
iron-triangle:change { time, cost, scope, capacityPoints, capacitySource, hash, source } Any input or property change. source is 'pointer', 'keyboard', or 'api'.
iron-triangle:revise { field, from, to, reason } A revision is committed via revise().
iron-triangle:mode { from, to } Capacity source flips between 'formula' and 'manual'.

Internal state hooks

Iron-triangle exposes CustomStateSet entries for CSS targeting via :state().

StateWhen
:state(formula) Capacity source is the formula (default).
:state(manual) Capacity source is a manual integer.
:state(over-deadline) The Time-fieldset deadline date is in the past (passive warning).
:state(unbudgeted) Computed capacityPoints < data-min-capacity.

JavaScript API

Property / MethodTypeDescription
.time { sprintWeeks, sprintCount, hoursPerWeek, deadline } Get / set the Time corner.
.cost { teamFTE, budgetTier, contractorBudget } Get / set the Cost corner.
.scope { mustHaveCount, shouldHaveCount, scopeNotes } Get / set the Scope corner.
.capacityPoints integer Current budget. Read-only when capacitySource === 'formula'; writable in manual mode.
.capacitySource 'formula' \| 'manual' Which mode produced the saved number.
.hash string (readonly) FNV-1a of the T/C/S object — for drift detection.
.revisionLog array (readonly) Append-only edit history.
.value object (readonly) Full snapshot for serialization.
.revise(field, newValue, reason) method Programmatic edit; throws if reason.length < 10.
.setManual(integer) method Switch to manual mode and set capacityPoints.
.setFormula(formulaString?) method Switch back to formula mode (default formula or custom).
.recalc() method Force capacity recomputation (rarely needed).

CSS Tokens

TokenDefaultPurpose
--iron-triangle-padding var(--size-l) Outer padding
--iron-triangle-section-gap var(--size-l) Vertical gap between Time / Cost / Scope
--iron-triangle-input-min 8rem Min width per numeric input
--iron-triangle-readout-bg var(--color-surface-raised)Capacity readout background
--iron-triangle-readout-size var(--font-size-3xl) Capacity number font size
--iron-triangle-formula-color var(--color-text-muted) Formula explanation text
--iron-triangle-warning-color var(--color-warning) Over-deadline / unbudgeted warning

Static fallback

Without JavaScript, the form still works — three <fieldset>s of native inputs that submit raw T/C/S values to the form's action. The server can persist the values and even compute capacity. The component just adds the live readout, formula visualization, and revision-tracking machinery on upgrade.

Accessibility

Native form semantics throughout: real <fieldset> / <legend> grouping, and the capacity readout is an <output> with aria-live="polite". The formula / manual switch is a real <button aria-pressed>, not a checkbox-and-label hack. Warnings are never color-only — the deadline-in-past warning includes the literal text "Deadline has passed" alongside any styling. Logical properties throughout keep RTL and writing-mode support clean. Without JavaScript the form still works: three <fieldset>s of native inputs submit raw T/C/S values to the form's action.

Related

  • <quality-target> — primary consumer; spends capacityPoints on quality picks and stamps the triangle's hash for drift detection.
  • <capacity-plan> — reconciles this capacity against quality spend and slotted feature costs in a stacked-bar ledger.
  • <requirement-card> — renders a single ility's priority row downstream of the quality decision.
  • <adr-wc> — record the iron-triangle decision itself as an ADR.