Total Rewards + PSU / Equity Calculator — Design (WW-125 epic)¶
Status: council-architected 2026-07-23 (opus tier; report
llm-council/reports/2026-07-23_195341_architecture-review-for-workwingmans-total-rewar.md).
Consensus ranking A(Clahadore) > D(Gronk) > B(Cedric) > C(Jenny); local seat timed out, house
councils 401-gated — verdict rests on 4 frontier seats.
Anchor ticket: WW-125 (T0). Remaining tickets: WW-134+ (see §Decomposition). Sibling epics sharing the comp chain: benefits comparison (WW-117..124), Take-home tax+COL (WW-126..133, separate session), Simple Mode (WW-96).
1. Purpose & honesty posture (binding)¶
Help a user understand a company's/offer's total rewards — base + bonus + equity (RSU/PSU/ options) + benefits + net take-home — as one comparable, honest picture, and give a real PSU/equity calculator that models performance-stock payout as a range, never a fabricated point.
Equity/comp carry large, irreducible uncertainty. Every surface obeys:
- Ranges, not points. Where the model is a band, the UI shows a band. Never collapse to a midpoint or single "expected" number.
- Sourced or absent. Every dollar/equity figure carries
SourcedField<T>provenance + as-of date. OnlyNotOnRecordmay be citation-free. No fabricated values, ever. - Not financial/investment advice. This is a calculator/organizer. No take/reject recommendation, no ranking of offers, no "best offer", no net-of-tax equity computation. The prohibited-action rule on personalized investment advice applies — if a surface would imply a recommendation, it is a build failure.
- As-of everywhere. Stock prices, tax rates, benefit facts all carry the date they were true.
The Kerr lens governs any "total": a single point-valued or ranked total rewards number is the gameable proxy ("maximize the number") standing in for the real goal ("choose the right role"). It is banned. A subtotal is permitted only under the strict guards in §4.
2. PSU model (Q1)¶
2.1 Data model — PsuTerms subrecord + GrantType.Psu¶
Add Psu to EquityGrantType. PSU-specific fields live in a separate PsuTerms record
referenced by the grant — NOT flat nullable fields on EquityGrant (that pollutes the clean
RSU/ISO/NSO discriminated shape and invites "looks priceable" bugs). EquityGrant stays the
common envelope; PSU reuses its VestingYears/CliffMonths/GrantDate schedule.
public enum EquityGrantType { Unknown, Rsu, Iso, Nso, Psu }
public enum PsuMetricKind { Unspecified, RelativeTsr, AbsoluteTsr, FinancialMetric, Other }
/// <summary>
/// Performance conditions for a PSU grant. Payout = TargetUnits × multiplier, where the
/// multiplier is read off the anchor curve at the user-asserted attainment. NOTHING here is
/// inferred: relative-TSR percentile attainment is not computable offline (needs peer-set
/// returns + ranking), so UserAttainment is ALWAYS an explicit user assertion or null.
/// </summary>
public sealed class PsuTerms
{
public PsuMetricKind MetricKind { get; set; } = PsuMetricKind.Unspecified;
/// <summary>Free-text label of the actual metric ("Relative TSR vs S&P 500", "cumulative EPS"). Never parsed into a computation.</summary>
public string? MetricLabel { get; set; }
public int PerformancePeriodYears { get; set; } = 3;
/// <summary>Shares granted AT TARGET (100%). Actual delivered = TargetUnits × multiplier.</summary>
public decimal? TargetUnits { get; set; }
/// <summary>Payout curve anchors. Multiplier interpolates linearly between them; clamps at Max; 0 below Threshold.</summary>
public decimal ThresholdMultiplier { get; set; } = 0.5m;
public decimal TargetMultiplier { get; set; } = 1.0m;
public decimal MaxMultiplier { get; set; } = 2.0m;
public decimal FloorMultiplier { get; set; } = 0m;
/// <summary>
/// User's explicit assertion of expected attainment (as a multiplier, e.g. 1.3×), or null.
/// NEVER inferred. Null → payout shown as the threshold..max band with no point highlighted.
/// </summary>
public decimal? UserAttainment { get; set; }
/// <summary>True when Threshold/Target/Max were NOT entered from the real grant but taken from typical defaults. Stamps every derived figure. Sticky until the user edits an anchor.</summary>
public bool AssumedAnchors { get; set; }
public List<SourceCitation> Sources { get; set; } = [];
public DateTimeOffset? AsOf { get; set; }
}
EquityGrant gains: public PsuTerms? Psu { get; set; } (non-null only when GrantType == Psu).
2.2 Payout-range math¶
Extend EquityValuationService (pure, no network) with a PSU path:
- Resolve per-share price exactly as today (public quote / private price /
UnpriceableReason). - PSU settles in shares, no strike → per-share value = full price (the RSU path).
- Emit a share-count band:
thresholdShares = TargetUnits × ThresholdMultipliertargetShares = TargetUnits × TargetMultipliermaxShares = TargetUnits × MaxMultiplier- below floor →
0. - Value band = each scenario × price × (vested-fraction where a vested view is requested).
- If
UserAttainmentis set (a user-asserted multiplier, never inferred): resolve it through the curve — belowThresholdMultiplier→FloorMultiplier(0×), otherwise capped atMaxMultiplier. Emit that as an additional labeled point inside the band — never replacing it. (Interpolation from a performance-level input, e.g. a percentile, is deferred: it needs a defined attainment scale, and for rTSR that scale is not computable offline anyway — so v1 takes the multiplier the user asserts directly.) - Default display attainment =
TargetMultiplier(1.0×), labeled "plan target — not a prediction." - Missing
TargetUnitsor price →UnpriceableReason, never zeros. New enum memberUnpriceableReason.PerformanceUnknownfor "PSU attainment not asserted / not knowable offline".
New valuation output fields (additive, on EquityValuation or a PsuValuation sub-object):
PsuValueBand { ThresholdValue, TargetValue, MaxValue, AssertedValue?, IsAssumedAnchors }, each a
range-aware decimal, each UnpriceableReason-guarded.
2.3 General valuation strengthening¶
- Options underwater state: price < strike → intrinsic
$0, labeled "underwater at today's price" (a factual state, not a loss claim). Already intrinsic-only; add the explicit label. - Vesting schedules: keep the standard linear-after-cliff default. Additive
VestCadenceenum (Monthly default / Quarterly / Annual) — do NOT over-build back-loaded custom schedules until a real grant needs one. - Tax NOTE only (never advice, never computed): a one-line factual chip per grant type — RSU/ PSU = "ordinary income at vest (W-2)"; ISO = "AMT preference item; no ordinary tax at exercise if held"; NSO = "ordinary income on the spread at exercise". No withholding math, no bracket math, no net-of-tax number. Marginal-rate annotation (from the tax engine) may say "taxed near your ~X% marginal rate" — a factual annotation, not a net-equity computation.
3. Total Rewards home (Q2)¶
New read-time ITotalRewardsService / TotalRewardsService in Infrastructure that
composes existing services and wires the stubbed IEquityValuation seam:
TotalRewardsService(
ITakeHomeCalculator, // consumes TaxEstimate from WW-126..133 (annual net, effective, marginal)
IColAdjustmentProvider, // COL index — SEPARATE axis, never pre-multiplied into comp
IEquityValuationService, // equity/PSU bands
ICompanyBenefitsService, // benefits facts (WW-118) — qualitative, excluded from any subtotal
ICompensationInsightService) // market context (P25/median/P75), side-by-side, never merged
-> TotalRewards (a projection)
- Projection, not a store. No persistence, no new comp entity. Computed at read time from the
user's already-persisted
EquityGrant/Offer/FinancialProfile/ benefits data. JobImpactServicestays the two-job DELTA tool.OfferEvaluationServicestays the banded-axes, no-composite decision-support tool. Neither is extended into an aggregation layer — that would conflate delta-vs-absolute and re-import the Kerr composite risk they deliberately refused.- Wire the
IEquityValuationseam (currentlyEquityValuationAdapter, unregistered) into DI soTotalRewardsService(and, later,JobImpactService) fold equity in via the narrow seam. Single-writer on the seam.
3.1 Seam contract with the tax+COL engine (WW-126..133)¶
TotalRewardsService consumes TaxEstimate{Gross, Federal, Fica, State, Local, TakeHome,
EffectiveRate, MarginalRate, DataVintage} from the existing IncomeTaxEstimator — never forks
it. Binding notes:
- Net take-home = one component row/group, separate from the pretax cash+equity rows. Never blended into a single pretax number (double-counts + launders a tax computation).
- MarginalRate used ONLY for the factual equity-vest annotation (§2.3). Never to compute net-of-tax equity.
- Filing-status degrade: the tax engine is Single-only today (throws on MFJ/HOH). Until their
filing-status tier lands, the net-take-home row renders as
NotOnRecord/ "unavailable for your filing status" — never a wrong number. - COL stays a separate labeled axis (index / purchasing-power translation with as-of
source). Andrew's "what's this worth here vs there" is honored as an explicit COL translation,
never a blended "real comp" scalar or default sort key. Converged with the tax+COL council
(WT-57e5, report
2026-07-23_195923_*): - Two axes, never multiplied.
JobImpact.ColAdjustedDeltais deprecated and deleted — the rewards side never binds it (WW-136 consumes rawTakeHome+ a separate COL column), so it can be removed outright, not just[Obsolete]. Never recomputed asnet × COLunder any alias. - Purchasing-power translation = a UNIT CONVERSION, not a score. "$X nominal take-home here
buys ~what $Y buys at {home} COL." Always shown paired with the raw net number, never
standalone, never a sort key; carries basket +
SourceCitation+ as-of; labeled "basket partial (rent/grocery/gas ± property/car); not after-tax welfare." - Default
/rewardscompare ordering = nominal net take-home DESC (one disclosed factual axis). COL + purchasing-power are separate visible columns, not foldable into the sort. Any composite / "best value" ordering is opt-in + banner only, never the default. - Filing coverage: the tax council shipped all four federal statuses (Single/MFJ/HOH/MFS) in
its T1, so the wrong-filing gap closes early. The
NotOnRecordnet-row degrade is kept as a defensive guard for any statusTaxEstimatecan't price, not as an expected common path.
4. The "total" — Kerr ruling (Q3)¶
A cash-comparable subtotal is allowed, narrowly scoped. Pure component-rows would under-serve the real "what's this roughly worth" need. Permitted iff every guard holds:
- The subtotal is itself a RANGE —
sum-of-lows … sum-of-highs, never a midpoint / point value. A single number is the gameable proxy. This is the load-bearing invariant. - Net take-home is a SEPARATE row/group, never folded into the pretax cash+equity blob.
- Dollar-denominated components only: base + bonus + sign-on (amortized) + equity band +
net take-home (separate group). Non-dollar benefits excluded — a blended benefits score is
exactly the Kerr proxy. Benefits stay qualitative (their own epic's
/compare). - Each row carries its own range + as-of + source (
SourcedField<T>). Multi-dated as-of shown, not silently unified. - No ranking, no normalization, no "best offer", no Accept CTA off the subtotal. It is optional arithmetic of visible rows.
Rule of thumb (ships in the copy deck): honest when every uncertainty is on-screen and nothing collapses to a point; gameable the instant it is ranked or point-valued.
5. PSU curve input UX (Q4)¶
- Real anchors are the primary path. The form asks for threshold/target/max from the user's grant.
- Typical defaults (0.5 / 1.0 / 2.0×) allowed only behind
PsuTerms.AssumedAnchors = true, which visibly stamps every derived figure "typical assumption — enter your real terms." Sticky until the user edits an anchor. - Real anchors required before any figure is saved/exported as "on-record." Assumed-anchor figures are exploratory only, never persisted as fact.
- Empty + no defaults → unpriceable band (
PerformanceUnknown), never silent fabrication.
6. Flags + routes (Q5)¶
- Master
ff.totalRewardsAND-ed children{equity.psu, rewards.comparison}; unknown key = false (fail closed). WW-97FeatureFlagspattern (static keys, master AND child). Master default-OFF; children default-ON under it. /equity(existing, power-only, no simple flag) — PSU calculator extends it./rewards(new, power-only) cross-company comparison — dark until the demand signal (§7).data.simpleomitted; guard redirects deep links to/homein simple mode.- Offer-detail rewards breakdown — subtractive
body[data-mode='simple'] { display: none }on the scoped breakdown element, not the page<body>. - Simple-mode offer-card face — calm one-line range + on-record facts, never a score
(e.g. "Base $X + equity (range) · net take-home on record").
--simple-*tokens, CSS-only. - Per-band maturity gate: where the comparison view renders equity/PSU bands, gate that
rendering on
equity.psumaturity so it can't display bands the calculator can't yet produce. Do NOT block non-equity comparison columns on PSU maturity — dependency is per-band, not whole-route.
Coordinated with Simple Mode (WT-c9de): consume persistedSignal<T> for local prefs; capability
visibility = flagOn && modePolicy; no third toggle system.
7. Venture-evidence lens¶
Ship the PSU/equity calculator first. It stands alone (single-offer, low input burden, reuses
EquityGrant + quotes, no comparison UI). Full cross-company aggregation now is premature.
/rewardscomparison UI ships dark. The read-only single-offer composition API may land now.- Painted-door instrument: a "Compare Offers" control on the calculator that logs intent and shows coming-soon — demand is observed, not assumed.
- Predeclared go-signal (committed before build): build the comparison engine only when, within 30 days of the calculator shipping, ≥5 distinct local users enter equity/PSU terms for ≥2 companies each, OR ≥3 explicit user requests for side-by-side. Until then the comparison UI stays gated.
Validate the total-rewards framing with the Sean/Shereeba cohort before deep comparison build.
8. Honesty guards (Q6)¶
Render-layer guard extends DraftHonestyGuard (benefits WW-121 pattern). All are build/test
failures, not warnings:
| Guard | Enforcement |
|---|---|
| Citation | Any dollar/equity figure without SourcedField provenance → build failure. Only NotOnRecord citation-free. |
| Certainty | Banned-phrase scan on comp copy ("will be worth", "guaranteed", "you'll get", "will vest", "expected value" unless labeled plan-target) → build failure. |
| Advice | No take/reject language; no rank-ordering offers by total. |
| Tax | No net-of-tax equity anywhere — reject any equity × tax-rate computation. Equity stays a pre-tax band; tax applies only on the cash/take-home path. |
| Frame | Mandatory NFA disclaimer + as-of on every equity/total surface. |
Plus a service-layer invariant (not just copy-scan): TotalRewardsService /
EquityValuationService never emit a point where the model is a band; UnpriceableReason over
fabricated zeros. Golden test: PSU with no attainment returns a threshold..max band + reason, not
a number.
9. Decomposition¶
Anchor WW-125 + block WW-134+ (WW-126..133 reserved by the tax+COL epic).
| Key | Tier | Ticket | Deps |
|---|---|---|---|
| WW-125 | Sonnet | T0: GrantType.Psu + PsuTerms subrecord + UnpriceableReason.PerformanceUnknown + TS mirror; walking skeleton, no math yet |
— |
| WW-134 | Sonnet | T1: PSU payout-range math in EquityValuationService (share-count band × price, anchor interpolation, clamp/floor, PerformanceUnknown guard) + golden tests |
WW-125 |
| WW-135 | Sonnet | T2: equity valuation strengthening — options underwater label, VestCadence, per-type tax-NOTE chips |
WW-125 |
| WW-136 | Sonnet | T3: TotalRewardsService read-time projection composing existing services + wire IEquityValuation seam in DI; single-offer composition API |
WW-134, tax seam |
| WW-137 | Ceiling | T4: honesty guard extension (citation/certainty/advice/tax/frame) + service-layer band invariant + 5 guard tests | WW-136 |
| WW-138 | Sonnet | T5: /equity PSU calculator UI (anchor form, assumed-anchor stamp, band display) + painted-door "Compare Offers" instrument |
WW-134, WW-137 |
| WW-139 | Sonnet | T6: offer-detail rewards breakdown (subtractive simple-mode) + calm one-line offer-card face | WW-136, WW-137 |
| WW-140 | Haiku | T7: feature-flag wiring ff.totalRewards → {equity.psu, rewards.comparison} + docs |
WW-125 |
| WW-141 | Sonnet | T8 (GATED): /rewards cross-company comparison view + per-band maturity gate — dark until §7 signal |
WW-136, WW-138 |
Ceiling-tier on WW-137 (honesty-critical, escalation-ladder skip-list). WW-141 build does not start until the predeclared demand signal fires.
10a. Council gate refinements (2026-07-24)¶
Applied from the pre-land council-code-review (Cedric/Codex + Gronk/Grok + Gaming-PC qwen3-coder; qwen3-next hit the 600s cap, Gemini needed re-auth):
- Honesty guard must not flag its own disclaimer. The bare
guaranteescan matched theStandardDisclaimer("not a guarantee of realized value"). Certainty patterns are now negation-safe (not a guarantee/no guarantee/nothing will vestpass). Regression test assertsCheck(StandardDisclaimer)is empty. - No blanket "plan target" exemption. A global exemption let "plan target is 1.0×; your
expected payout is $2M" slip through.
expected value/payout/returnis now flagged unconditionally — the honest label is "plan target — not a prediction", never "expected value". - Net-of-tax copy ban class added (
net-of-tax,after-tax equity,take-home equity). - PSU anchor sanity: anchors must satisfy
0 ≤ floor ≤ threshold ≤ target ≤ maxor the band is unpriceable (never a negative/inverted range). Asserted attainment isMath.Clamp-ed to[floor, max]. AssumedAnchorsdefaults totrue: an un-enteredPsuTermscarries the typical default curve, so it is honestly stamped assumed until the write path records real anchors.- Options with no strike → unpriceable (not valued as if strike were $0). This tightens the pre-existing null-strike behavior in line with the honesty posture; the two remaining missing-input gaps (GrantDate-null vest split, thin-history vol) stay tracked as a separate follow-up.
10. Privacy / local-first¶
All equity/comp/PSU data is local-only, DPAPI-encrypted at rest like Offer.Comp and
FinancialProfile, never mirrored. The only network call is the existing read-only, factual
stock-quote lookup (Stooq/AlphaVantage). Family-forming/gender-affirming benefit fit stays
on-device (benefits epic rule, inherited).