Wage Compliance — plain-language explainer
Wage Compliance is the operational layer for jurisdictionally-variable pay law: it resolves where a worker actually sits, finds the rule that governs that place on that date, and checks an offer or a whole roster against it — with the source citation attached.
A People Analytics Toolbox component. Built to the portfolio Explainer Standard v1.0. Every claim below is grounded in the spoke's own code and contracts (src/spokes/wage-compliance/, contract 0.17.0, schema wage_compliance, status live in src/lib/contracts/registry.ts); anything not yet built is marked (TBD).
1. What is it?
Wage Compliance answers a deceptively hard question: for this worker, in this place, on this date, what does the law require — and does the pay clear it?
It is a tenant-aware engine built from four pieces: a hierarchical jurisdiction registry (country → state → county → city → district → ZIP → rooftop), a temporal rule engine that applies inheritance and precedence, citation-backed rule versions, and stateless single + bulk evaluation against a roster. Lookup of "what's the minimum wage in San Francisco" is a free byproduct; the work is the orchestration around it — resolving the right cell, picking the version effective on the evaluation date, and reporting the result with its provenance.
Visual — Tier B (step flow). The core pipeline, from core/evaluate.ts:
address → resolve jurisdiction chain → resolve applied rule version (precedence + temporal) → compare worker wage vs. required minimum → ComplianceEvaluationResult (status + discrepancy + jurisdiction trace)
2. What problem does it solve — and why is it different?
The pain it removes: wage law is not one number. It varies by place (federal vs. state vs. ~150 local ordinances), by worker classification (general / tipped / minor / healthcare / agricultural / domestic / prevailing-wage), by date (rules change on scheduled effective dates), and by rule family (minimum wage, overtime threshold, paid leave, pay transparency, local payroll tax). Doing it by hand means a spreadsheet that is stale the moment a city raises its floor.
The difference, stated as a shift:
- FROM a single hard-coded minimum-wage table that ignores locality, classification, and effective dates — and silently goes stale.
- TO a resolved jurisdiction chain, the rule version effective on the evaluation date, a pass/fail/warning/unknown verdict, the dollar discrepancy, and the citation that backs the floor.
How it differs from the obvious substitutes:
- vs. a static minimum-wage spreadsheet — the engine resolves precedence (a city ordinance overrides the state floor where it is higher), honors effective windows, and emits an honest
unknownrather than a wrong number when it cannot resolve the cell. - vs. a generic wage-data vendor — most vendors sell data; this spoke is the operational layer. It runs bulk roster evaluation (up to 10,000 workers per call), an ATS offer gate, a payroll-export pack, and a review queue — the workflow around the number, not just the number.
Visual — Tier B (FROM→TO typographic block). The shift above is the visual; a rendered comparison block is a follow-up.
3. How does it work?
The practitioner's questions, answered concretely:
- "Where is this worker, legally?" —
jurisdiction-resolver.tstakes an address and returns the jurisdiction chain (e.g., US → CA → San Francisco), aprecisionTier(state|zip|city|rooftop), ageoConfidence, andambiguityFlags(e.g.,zip-spans-multiple-cities). When a number can't be pinned to a rooftop yet, the response carriesrooftopPending: truerather than guessing. - "Which rule governs?" —
rule-resolver.tswalks the chain applying precedence + temporal validity: among the rule versions whose effective window contains the evaluation date, it picks the binding one (the higher local floor wins over the state floor). Each rule version carries avalidationStatus(pending/validated/conflicted/ ...) and asourceConfidenceScore. - "Does the pay clear it?" —
evaluate.tscomputes the worker's effective hourly wage (hourly, or annual salary ÷ a 2,080-hour assumption), compares it to the required minimum, and returns acomplianceStatus:pass,fail,warning(wage missing), orunknown(no jurisdiction or no rule).
Data sources are public and citation-backed, never tenant data fed to a model. Seeded sources include US DOL FLSA, state labor agencies, the UC Berkeley Labor Center / NCSL aggregators, City-of-jurisdiction wage-tax pages (Philadelphia, Cleveland, Cincinnati, Louisville Metro, NYC MCTMT), and international government sources (Gov.uk NMW, ESDC Canada, Fair Work Australia, Eurostat, OECD). An AI ordinance extractor (src/lib/connectors/ai-ordinance-extractor/, Claude with a strict citation-required prompt) reads public ordinance URLs and returns per-field citations + confidence — tenant rosters never reach the model.
Differentiation beat: the real question is not "what's the floor" — it's "can I defend this verdict in an audit?" Every applied rule carries its source citation, retrieval timestamp, and confidence; the unknown verdict is first-class, so the engine never launders a missing rule as a passing one.
Visual — Tier B (rule-family fan-out). One engine, one stable schema, many families:
rule_families → { minimum-wage · overtime-threshold · paid-leave · pay-transparency · local-payroll-tax · statutory-pay-min } — each resolved through the same jurisdiction chain + temporal precedence path.
4. What does it enable?
Concrete uses a practitioner would recognize:
- Gate an offer before it goes out —
POST /evaluate/offerchecks a candidate's proposed wage against the resolved local floor; on a fail it returns arecommendedHourlyWage(required + 1¢ buffer so payroll rounding can't re-introduce the gap). The Greenhouse webhook runs this automatically on stage-change to "Offer." - Sweep an entire roster —
POST /evaluate/bulkevaluates up to 10,000 workers in one call, returning per-worker results pluspassCount/failCount/warningCount/unknownCount. - Check overtime exemption + thresholds — supply
weeklyHours+isExemptClassifiedand the evaluator runs the OT rule lookup (FLSA weekly, CA daily-OT, exempt salary floors including NY's regional tiers) alongside the minimum-wage path. - Validate a job posting for pay transparency —
POST /evaluate/postingreports which disclosure fields (salary range, benefits, pay scale) are missing relative to the jurisdiction's rule (CA SB 1162, CO EPEWA, NY/NYC, WA, IL, etc.). - Classify a worker (advisory) —
POST /worker-classificationemits cite-backed federal / state / overlay layers (healthcare, tech, retail-hospitality, gig/1099 ABC tests, construction prevailing-wage) for deck-grounding — explicitly not legal advice. - Hand findings to payroll —
POST /exports/[vendor]projects evaluation rows into ADP / UKG / Paychex CSV conventions, uploads to Blob with a signed URL, and writes an audit row.
Visual — (TBD — a rendered roster-sweep result chart: pass/fail/warning/unknown counts for one tenant).
5. How it fits in the toolbox
Data flow:
- Consumes —
segmentation-studiofor canonical-field normalization at workforce-upload time andsegment.region.*canonical segments as the geo skeleton for jurisdiction matching;data-anonymizer'smin-n-checkgates aggregated compliance rollups andredactcleans free-text review-queue comments;calculusstats-enrichfor confidence intervals on dollar-exposure. - Feeds —
anycomp: thecomplianceFloorbinding lets comp recommendations respect the legal minimum automatically (PAT-81). It also files fivecompliance.pay-floor.*metrics intometrics-catalog. - Emits — the canonical contract at
src/spokes/wage-compliance/contracts/types.ts(ComplianceEvaluationResult,OfferEvaluationResponse,ResolveJurisdictionResponse, and the rule-family payloads). Consumers (Performix, vela, future apps) vendor a copy; they never import at runtime. POST routes are service-key gated (PAT-11); MCP transport mirrors the HTTP surface 1:1. - Sibling —
wage-benchmarkanswers "what is the market paying here"; Wage Compliance answers "what does the law require here." Same jurisdiction/place spine, opposite question: market price vs. the legal floor.
Visual — Tier B (typographic data-flow). HRIS roster + offer → [segmentation-studio normalize] → Wage Compliance → { pass/fail verdict + discrepancy · anycomp complianceFloor · payroll export · review queue }, with public DOL/state/local + international sources upstream.
6. Commercialization / packaging
Wage Compliance is the operational layer of a compliance offering, not a standalone data feed. The positioning is explicit in the spoke README: most vendors sell the data; here the lookup is free and the workflow — bulk evaluation, the ATS gate, payroll exports, the review queue, the temporal alert ladder — is the product.
- Data-license posture: the seeded rules derive from public government sources (DOL, state labor agencies, city wage-tax pages, and international government statistics), which is what lets the floors be shown and cited openly. The AI ordinance extractor reads only public URLs; tenant rosters never reach a model.
- Honesty discipline: it is the toolbox's AI-in-HR case study #1 (
docs/POSITIONING/AI-IN-HR-CASE-STUDIES.md) precisely because the model touches only public ordinance text, every extracted field carries a citation, and human review gates persistence. - Anything about pricing tiers or named packages is (TBD) — not earned yet, so not stated.
Visual — (TBD — product-tier placement diagram showing the data-layer vs. operational-layer split).
7. The vision
Comprehensive, citation-backed coverage of every wage rule that governs a worker — minimum wage, overtime, leave, transparency, and local tax — kept current by an AI workforce that watches the law change and tells you before it bites.
The trajectory is comprehensive coverage plus self-maintenance. The rule-family abstraction (rule_families table) already lets the same engine carry six families without schema rework. The temporal diff agent (PAT-87) watches rule_versions and emits a tiered alert ladder — informational (≥90 days out) / warning (30–89) / critical (7–29) / immediate (<7 or active) — so an upcoming increase surfaces as an alert, not a surprise audit finding. Rooftop precision (PAT-89, PostGIS point-in-polygon) is wired and discloses rooftopPending honestly until the polygons for named sub-municipal jurisdictions (NYC payroll districts, Louisville Metro, Cleveland/Cuyahoga, Philadelphia wage-tax bands) are seeded. The longer arc is a multi-agent AI acquisition workforce (jurisdiction discovery, source validation, conflict detection, confidence scoring) that scales the local-ordinance roster and keeps it honest.
Visual — (TBD — the rule-family × jurisdiction coverage map with the temporal-alert ladder.)
8. Current status
Grounded in the real code state (contract 0.17.0, src/spokes/wage-compliance/, status live):
- Shipped: the jurisdiction resolver + temporal rule engine; single + bulk evaluation (
/evaluate/single,/evaluate/bulk); six rule families (minimum-wage, overtime-threshold, paid-leave, pay-transparency, local-payroll-tax, statutory-pay-min); the ATS offer gate (/evaluate/offer) + Greenhouse webhook; posting/transparency evaluation (/evaluate/posting); paid-leave evaluation (/evaluate/paid-leave); advisory worker classification (/worker-classification); the review queue + AI review queue with a tier-1/2/3 escalation ladder; payroll export packs (ADP/UKG/Paychex); the temporal diff agent + tiered alerts (PAT-87); the conflicts surface; multi-currency context (18-currency static table); an international minimum-wage seed across 34 country jurisdictions; the AI ordinance extractor + jurisdiction-discovery agent. MCP transport mirrors the HTTP surface 1:1. - In flight / planned: rooftop polygon seeding (PAT-89-FU-A/B — Census TIGER + sub-municipal) and the geocoding connector (PAT-89-B); live FX-rate fetching (PAT-96-FU-B, currently a snapshotted table); the full seven-agent AI acquisition workforce (PAT-102..108); the operator surface UX (PAT-84 follow-ups).
Visual — Tier A (live capture). GET /api/spokes/wage-compliance/health returns the real spoke status:
GET /api/spokes/wage-compliance/health
→ {
"spoke": "wage-compliance",
"status": "ok",
"contractVersion": "0.17.0",
"schemaReachable": true,
"latencyMs": <measured at request time>,
"checkedAt": "<ISO timestamp>"
}
(Shape per the toolbox-standard /health contract — src/lib/health/check.ts; contractVersion is the live CONTRACT_VERSION import. latencyMs and checkedAt are measured at request time.)
Worked example — an offer gate, end to end
A clearly-labeled illustrative scenario grounded in the real offer-evaluation contract and the buildOfferResponse comparator (core/offer-evaluate-pure.ts). The wage floor used is the California $16.50/hr general minimum documented in the spoke's PAT-97 seed (CHANGELOG 0.15.0). The arithmetic is exactly what the code computes; the input is illustrative.
A recruiter is about to extend a $15.50/hr offer to a candidate in San Francisco, CA.
- Input (
OfferEvaluationRequest):{ candidateId, location: { city: "San Francisco", stateCode: "CA" }, proposedHourlyWage: 15.50, workerClassification: "general", effectiveDate: "2026-06-01" }. - Resolve — the resolver returns a jurisdiction chain (US → CA → San Francisco) with a
cityprecision tier and ageoConfidence.hasLocationSignalis true (city + state provided). - Rule — the binding minimum-wage rule version effective 2026-06-01 for the
generalclassification suppliesrequiredHourlyWage = 16.50. - Compare (
buildOfferResponse):proposedHourlyWage (15.50) < requiredHourlyWage (16.50)→outcome: "fail".discrepancyAmount = 16.50 − 15.50 = 1.00.recommendedHourlyWage = round((16.50 + 0.01) × 100) / 100 = 16.51. - Result (
OfferEvaluationResponse):{ outcome: "fail", requiredHourlyWage: 16.50, discrepancyAmount: 1.00, recommendedHourlyWage: 16.51, jurisdictionChain: [...], rooftopPending: false, ... }.
What the practitioner does: the offer is held before it goes out; the recruiter raises it to the recommended $16.51/hr (a cent above the floor so payroll rounding never re-introduces the gap) and re-runs the gate to a pass. Run via the Greenhouse webhook, this happens automatically on stage-change to "Offer," and a candidate note can be posted back for the fail.
Floors cited ($16.50 CA general minimum) are from the spoke's own PAT-97 seed (CHANGELOG 0.15.0); the discrepancy and recommended-wage math is the literal output of buildOfferResponse / outcomeFromComparison. The candidate input is an illustrative scenario, clearly labeled — not a real evaluation.