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 unknown rather 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.ts takes an address and returns the jurisdiction chain (e.g., US → CA → San Francisco), a precisionTier (state | zip | city | rooftop), a geoConfidence, and ambiguityFlags (e.g., zip-spans-multiple-cities). When a number can't be pinned to a rooftop yet, the response carries rooftopPending: true rather than guessing.
  • "Which rule governs?"rule-resolver.ts walks 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 a validationStatus (pending / validated / conflicted / ...) and a sourceConfidenceScore.
  • "Does the pay clear it?"evaluate.ts computes the worker's effective hourly wage (hourly, or annual salary ÷ a 2,080-hour assumption), compares it to the required minimum, and returns a complianceStatus: pass, fail, warning (wage missing), or unknown (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 outPOST /evaluate/offer checks a candidate's proposed wage against the resolved local floor; on a fail it returns a recommendedHourlyWage (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 rosterPOST /evaluate/bulk evaluates up to 10,000 workers in one call, returning per-worker results plus passCount / failCount / warningCount / unknownCount.
  • Check overtime exemption + thresholds — supply weeklyHours + isExemptClassified and 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 transparencyPOST /evaluate/posting reports 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-classification emits 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 payrollPOST /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:

  • Consumessegmentation-studio for canonical-field normalization at workforce-upload time and segment.region.* canonical segments as the geo skeleton for jurisdiction matching; data-anonymizer's min-n-check gates aggregated compliance rollups and redact cleans free-text review-queue comments; calculus stats-enrich for confidence intervals on dollar-exposure.
  • Feedsanycomp: the complianceFloor binding lets comp recommendations respect the legal minimum automatically (PAT-81). It also files five compliance.pay-floor.* metrics into metrics-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.
  • Siblingwage-benchmark answers "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.

  1. Input (OfferEvaluationRequest): { candidateId, location: { city: "San Francisco", stateCode: "CA" }, proposedHourlyWage: 15.50, workerClassification: "general", effectiveDate: "2026-06-01" }.
  2. Resolve — the resolver returns a jurisdiction chain (US → CA → San Francisco) with a city precision tier and a geoConfidence. hasLocationSignal is true (city + state provided).
  3. Rule — the binding minimum-wage rule version effective 2026-06-01 for the general classification supplies requiredHourlyWage = 16.50.
  4. 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.
  5. 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.