Skip to content

WorkWingman — Family Cost Comparison (Technical)

An optional household profile plus a city-to-city comparison of grocery cost and school/tuition cost, projected onto a prospective job's city vs. the user's current city. Built to the same hard rule as job-signals: every figure is either an official/free public source or an explicitly-labeled, cited approximation — nothing is scraped, and nothing is fabricated when a source has no clean answer.

Privacy

FamilyProfile (household size, kids' ages, school type, current spends, current city) is stored locally only, in its own document under LocalJsonStore (family-profile.json in %USERPROFILE%\Wingman\data, or the configured data directory — see LocalJsonStore.cs). It never leaves the machine except via the same Drive-mirror path every other profile section already uses. Every field is optional and defaults to something reasonable or blank — skipping the "Family" section entirely does not block any other part of the app.

Data model

FamilyProfile:

public class FamilyProfile
{
    public int Adults { get; set; } = 2;
    public List<ChildAge> Kids { get; set; } = [];
    public SchoolType CurrentSchoolType { get; set; } = SchoolType.NotApplicable;
    public decimal? CurrentMonthlyGrocerySpend { get; set; }
    public decimal? CurrentPrivateTuitionAnnual { get; set; }
    public string CurrentCity { get; set; } = string.Empty;
    public DateTimeOffset UpdatedAt { get; set; }
}
public class ChildAge { public int AgeYears { get; set; } }
public enum SchoolType { NotApplicable, Public, Private }

Kids' sex is deliberately not collected — see "Grocery estimate method" below for why age alone is enough for this cost model. CurrentCity is free text (e.g. "Austin, TX"), the same shape already used by JobPosting.Location — no shared geocoding dependency on any other in-flight branch.

Source research and verdicts

Groceries — USDA Food Plans + BLS regional index

USE: USDA Official Food Plans ("Cost of Food"), published by the Food and Nutrition Service (rebranded Food and Nutrition Administration, mid-2026) — https://www.fna.usda.gov/research/cnpp/usda-food-plans/cost-food-monthly-reports. Free, official, monthly. Four cost levels (Thrifty, Low-Cost, Moderate-Cost, Liberal) by 15 age/sex bands. Caveat: PDF/Excel-table publication only — no REST API, no clean CSV. The exact per-band Moderate-Cost figures used in UsdaFoodPlanCalculator are transcribed from the most recently published report as of this feature's build and must be refreshed by hand against the current monthly PDF — see "As-of / refresh" below.

USDA's own household-size adjustment (reproduced exactly in UsdaFoodPlanCalculator): 1-person households cost +20% per person, 2-person +10%, 3-person +5%, 4-person no adjustment (baseline), 5-6 person -5%, 7+ person -10% — larger households achieve economies of scale on food purchasing/prep that a flat per-person sum would miss.

USE: BLS CPI regional "food at home" index for the scaling factor — https://www.bls.gov/developers/ (Public Data API v2, free with registration for the full 500-query/day allowance; series family CUUR<area>SEFA, e.g. Northeast urban = CUUR0100SEFA, per https://www.bls.gov/cpi/factsheets/cpi-series-ids.htm). Caveat: metro-level "food at home" coverage is sparse outside the largest metros — BLS publishes it reliably for ~20+ large metro areas and at the Census-region level everywhere else. Given that, BlsRegionalFoodPriceIndexSource ships as a seeded snapshot table (city-name fragment → index, ~18 major metros) rather than a live API call today — a real HTTP adapter against the BLS API (same ApiClientBase/Flurl/Polly shape as SecEdgarSource) can replace its body without touching IFamilyCostService or any caller. Cities outside the snapshot fall back to 1.00 (national average, no adjustment) and are flagged via IsFallbackNationalAverage, never guessed.

Method: UsdaFoodPlanCalculator.NationalBaselineMonthly(adults, kidAges) sums the Moderate-Cost per-person figure for each household member (adults modeled as an even male/female split, rounding the odd extra adult to male — a family-size cost estimate, not a per-person nutrition plan) and applies the household-size adjustment; FamilyCostService.GroceryEstimateAsync multiplies that baseline by the target city's regional index. Kids' sex isn't collected, so a child whose age falls in a sex-specific USDA band (9+) is costed at the average of the male and female figures for that band — a neutral blend, never defaulting to the higher male rate.

Delta baseline: DeltaVsCurrentCity prefers the user's own stated CurrentMonthlyGrocerySpend when present. When it's blank (the model's "use the estimate" case), the delta falls back to the same USDA-scaled estimate computed for the user's CurrentCity (using that city's own regional index), so a user who skips the optional spend field still sees a target-vs-current delta. It's null only when neither a stated spend nor a usable, distinct current city exists.

School district quality — SEDA (NCES EDGE geocoding noted, GreatSchools ruled out)

AVOID: GreatSchools API — confirmed partner/paid-only. Current public pricing: "School Essentials" ~$52.50/mo (15,000 calls), "School Quality" ~$97.50/mo, 14-day free trial, no remaining free tier (https://www.greatschools.org/api). Not used.

USE: Stanford Educational Opportunity Project / SEDA — https://edopportunity.org/get-the-data/. Free, official-research-grade, publishes nationally-comparable district average-achievement AND growth percentiles (not raw state proficiency rates, which aren't comparable across states) as downloadable CSV/Stata files. Current technical documentation is versioned v5.0 (March 2024). Caveat: verify the current license text at edopportunity.org before any commercial redistribution — Stanford's published terms grant a "revocable, non-exclusive, non-transferable right to access and use" the data; this is treated as research/attribution-friendly, not a blanket commercial license, so WorkWingman surfaces the metric with attribution and does not redistribute the underlying dataset.

NOTED, not used directly: NCES EDGE (https://nces.ed.gov/programs/edge/Geographic/DistrictBoundaries) — free district-boundary shapefiles + ArcGIS REST services for address/city → district geocoding. This is the correct long-term tool for mapping an arbitrary "City, ST" string to one SEDA district ID; it carries no quality metric itself. NCES CCD (Common Core of Data, https://nces.ed.gov/ccd/) is free district enrollment/demographic/finance data, also with no quality score — noted for completeness, not wired in.

What ships today: SedaSchoolDistrictSource is a seeded snapshot (city-name fragment → district name + achievement/growth percentile, ~8 major metros), the same documented-placeholder pattern as the grocery index source. A live implementation would chain NCES EDGE geocoding → a SEDA district-ID lookup → the downloaded SEDA CSV, none of which changes ISchoolDistrictSource's contract. Cities outside the snapshot resolve to null — "no clean mapping," never a fabricated percentile.

Private school cost — a documented dead end + a caveated fallback

NCES Private School Universe Survey (PSS) — https://nces.ed.gov/surveys/pss/ — does NOT collect tuition data. This is the obvious "official" candidate and it was checked directly against its published table list and the Digest of Education Statistics: PSS covers religious orientation, enrollment, staffing, and program type, never cost. No federal survey publishes an authoritative private-tuition-by-state figure. This is recorded here so a future contributor doesn't re-research the same dead end.

AVOID (do not scrape): metro-level tuition aggregators (e.g. Private School Review and similar lead-generation sites). Their robots.txt disallows broad site sections and their business model (paid lead generation) makes ToS-ambiguous bulk extraction legally risky — the same LinkedIn-style pattern this codebase avoids everywhere else (see job-signals.md).

What ships today: StateAveragePrivateTuitionSource uses a small seeded snapshot of state-level average annual tuition figures sourced from published state/regional independent-school-association aggregate reports (NAIS-style member surveys) — explicitly labeled in the Source field as an approximate, non-federal figure, not an NCES number. States outside the snapshot resolve to a PrivateTuitionEstimate with a resolved State but a null StateAverageAnnualTuition — "no seeded average," never a guess.

Current-side tuition, by precedence: (1) the family's actual CurrentPrivateTuitionAnnual if entered (labeled Source = "User-provided current tuition (from Family profile)") — honored even for a Public profile, since a stated figure is explicit intent; (2) otherwise the state average, but only when CurrentSchoolType == Private — a Public/NotApplicable family pays $0 today, so filling in the state average there would understate the real cost of moving a kid into private school in the target city, so the current side is left null instead; (3) otherwise null. The target side always keeps the state-average estimate (no user figure to prefer).

Reliable vs. caveated fields

Field Reliability Why
GroceryEstimate.NationalBaselineMonthly Reliable, given an accurate family composition Directly reproduces USDA's own published methodology and adjustment table.
GroceryEstimate.RegionalIndex / MonthlyGroceryCost Caveated Seeded snapshot of ~18 metros today, not a live BLS call; falls back to 1.00 (flagged) elsewhere.
SchoolDistrictSignal.AchievementPercentile / GrowthPercentile Caveated Seeded snapshot of ~8 metros; SEDA itself is a solid nationally-comparable source once a real geocoding step is wired in.
PrivateTuitionEstimate.StateAverageAnnualTuition Caveated, explicitly approximate No federal source exists at all; this is an association-aggregate figure, not an NCES number — labeled as such in Source.

Aggregation

FamilyCostService follows the same shape as CompanyLayoffService: swappable adapters (IRegionalFoodPriceIndexSource, ISchoolDistrictSource, IPrivateTuitionSource), graceful degradation (OperationCanceledException propagates, everything else degrades to a safe default rather than throwing), and no fabricated numbers — a null field always means "no data," never a best-effort guess.

public interface IFamilyCostService
{
    Task<GroceryEstimate> GroceryEstimateAsync(FamilyProfile family, string city, CancellationToken ct = default);
    Task<SchoolComparison> SchoolComparisonAsync(FamilyProfile family, string targetCity, CancellationToken ct = default);
}

Wiring

Registered in Program.cs as plain singletons, same as every other service:

builder.Services.AddSingleton<IFamilyProfileService, FamilyProfileService>();
builder.Services.AddSingleton<IRegionalFoodPriceIndexSource, BlsRegionalFoodPriceIndexSource>();
builder.Services.AddSingleton<ISchoolDistrictSource, SedaSchoolDistrictSource>();
builder.Services.AddSingleton<IPrivateTuitionSource, StateAveragePrivateTuitionSource>();
builder.Services.AddSingleton<IFamilyCostService, FamilyCostService>();

FamilyController exposes GET/PUT /api/family/profile, GET /api/family/groceries?city=, and GET /api/family/schools?targetCity= (optional &currentCity= overrides the saved profile's current city; the current side otherwise comes from the saved FamilyProfile) — a dedicated route separate from ProfileController's /api/profile/*, so this feature never touches IProfileService, IntakeProfile, or StoryProfile.

Frontend: Family is a standalone routed page (/family) for the optional profile form (adults, kids + ages via add/remove rows, school type, optional current spends). FamilyCostCard is a separate, input-only display component (grocery/schools signal inputs, no service injection) meant to compose next to the other cost-comparison cards (financial-profile, lifestyle-costs, col-tax) on a future job-detail view without any file overlap at integration time.

As-of / refresh

  • USDA figures snapshot date: see the per-band comment in UsdaFoodPlanCalculator — refresh by pulling the latest "Cost of Food at Home — Moderate-Cost Plan" monthly PDF from fna.usda.gov/research/cnpp/usda-food-plans and updating MonthlyCostByAgeSex.
  • BlsRegionalFoodPriceIndexSource.SnapshotAsOf, SedaSchoolDistrictSource.SnapshotAsOf, and StateAveragePrivateTuitionSource.SnapshotAsOf each carry their own snapshot date, surfaced on every returned estimate so the UI/docs can show "as of" rather than implying live data.

Tests