尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Find the right clothes for winter sports based on conditions
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
SWTTR helps you pick the right layers for outdoor activities based on conditions. It blends biophysics-based insulation targets with a gear-aware wardrobe so recommendations are grounded in real clo values.查看完整 README ↗SWTTR helps you pick the right layers for outdoor activities based on conditions. It blends biophysics-based insulation targets with a gear-aware wardrobe so recommendations are grounded in real clo values.
Biophysics support (signed in): Alpine Skiing, Backcountry Skiing (ski touring), XC Skiing, Running, and Biking. Hiking / Snowshoeing and signed-out users get static recommendations from src/data/layerRecommendations.json, which covers Alpine, XC, and Hiking. The app labels those as general guidance. Where there are no static layers (signed-out Running, Biking, and Backcountry), it keeps the outing on screen with a sign-in prompt instead. Signed-in users get the same fallback, with Add gear or Try again in place of sign-in, when their wardrobe has no usable gear or the request fails. Multi-day Plan Ahead uses the static table for everyone, so it works signed out too. Its packing list matches items to a wardrobe only for signed-in users.
The biophysics-supported activities share a common recommendation pipeline built on the IREQ standard (ISO 11079). The diagrams below describe the architecture for contributors. All calculations live server-side:
src/lib/biophysics/ — the thermal models (IREQ, target ranges, CoWEDA buffer, ensemble prediction, scoring)src/lib/recommendations/ — the shared pipeline: request parsing, thermal-targets.ts, gear-pool loading, and the route factory in handler.tssrc/lib/recommendations/sports/ — one module per sport with its ensemble builder, extremity rules, and response shapesrc/app/api/v1/recommendations/<sport>/route.ts — one-line routes that pair the factory with a sport module (the paid /api/agent/recommendations/* routes reuse them)Golden tests in src/app/api/v1/recommendations/golden.test.ts pin every sport's full response against a snapshot of the real gear catalog; an intended output change shows up as a snapshot diff to review and update with npx vitest run -u.
Running/XC garment and sustained-effort policy contracts, calibration evidence, generic catalog estimates, migration order and validation limits are documented in Running and XC foundations. These foundations precede the remaining selector and presentation changes in #243.
Every biophysics recommendation passes through the same pipeline, from metabolic rate lookup through final comfort classification. Multi-phase sports run the target steps once per phase: alpine computes IREQ at the 60/40 ski-and-lift cycle metabolic rate and relative air speed, retaining chairlift DLE, and ski touring computes uphill, downhill, and transition phases.
graph TD
A[Activity + Exertion Level] --> B[Metabolic Rate Selection<br/>Running/XC: shared MET-to-W/m² conversion<br/>Rate helper uses default mass and body area<br/>Recommendation uses user mass and body area<br/>Other sports: existing rate and size heuristic]
B --> XCP[XC only: outing pattern and exposure context<br/>Effective wind is max of ambient and exposure floor<br/>Keep existing motion correction; distinguish dry snow from wet]
XCP --> C[IREQ Calculation]
C --> D[Activity Target Range<br/>Running/XC: retain heat-balance minimum<br/>Taper only added activity allowance]
D --> E[CoWEDA-inspired heuristic buffer<br/>Running/XC: taper with cold, wind and wet exposure]
E --> F[Regional & Extremity Targets<br/>Whole-body and regional bands converted to basic clo<br/>ISO 11079 wind and motion correction<br/>Inferred movement capped at 0.7 m/s<br/>Hands and head keep resultant IREQ]
F --> P[Load Gear Pool:<br/>wardrobe, or catalog filtered by activity score]
P -->|No usable garments| T[Targets-only response]
P --> G[Ensemble Building]
G --> R[Running/XC only: clothing requirements<br/>from regional and extremity targets<br/>owned match, generic suggestion, missing or not needed<br/>unsuitable wardrobe items are not presented as matches]
R --> XCCARRY[XC: Carry extra warmth for stops and after skiing<br/>Excluded from moving insulation and comfort]
XCCARRY --> I[Canonical server phase assessment:<br/>actual worn regional and extremity basic clo<br/>local shortfalls and excess, moisture and clothing gaps]
I --> H[Ensemble thermal scoring and API/UI reasons<br/>consume the same phase assessment]
subgraph "Shared Biophysics Core — thermal-targets.ts"
B
C
D
E
F
end
subgraph "Shared Pipeline — handler.ts, gear-pool.ts"
P
T
end
subgraph "Sport-Specific — sports/*.ts"
G
end
subgraph "Shared Evaluation — layer-evaluation.ts and response-builder.ts"
H
I
end
Before ensemble building begins, all wardrobe garments are split into four mutually exclusive pools based on their category. Each garment belongs to exactly one pool.
graph TD
W[User Wardrobe] --> DATA[Require known covered-region thermal data<br/>Carry type, standalone usage and partial coverage]
DATA --> CAT[categorizeGarments]
CAT --> BL[Base Layers]
CAT --> ML[Mid Layers]
CAT --> INS[Insulation]
CAT --> SH[Shells]
subgraph "Pool: Base Layers"
BL --> BL1[base_layer]
end
subgraph "Pool: Mid Layers"
ML --> ML1[mid_layer_light]
ML --> ML2[mid_layer_heavy]
end
subgraph "Pool: Insulation"
INS --> INS1[insulation_synthetic]
INS --> INS2[insulation_down]
INS --> INS3[outer_insulated<br/>Alpine treats these as outer layers]
end
subgraph "Pool: Shells"
SH --> SH1[soft_shell]
SH --> SH2[hard_shell]
SH --> SH3[windbreaker]
end
BL1 --> SEL[Ensemble Selection]
ML1 --> SEL
ML2 --> SEL
INS1 --> SEL
INS2 --> SEL
INS3 --> SEL
SH1 --> SEL
SH2 --> SEL
SH3 --> SEL
Running compares complete wearable moving outfits made from reviewed standalone tops, shorts or tights, optional underlayers/mids and protective outers. A thermal base is never mandatory. Shorts and short sleeves intentionally leave skin exposed; regional thermal data already includes that exposure. The running search caches shared-model regional contributions, fixes any garments spanning the upper body and legs (including arms without torso coverage), and prunes inferior regional priorities before pairing the surviving choices. Identical selection capabilities keep the first sorted ID; no candidate cap or thermal bins discard distinct outfits. It preserves actual wind/rain protection, prefers waterproof coverage in precipitation, rejects excessive warmth before filling a clothing gap, and balances whole-body/regional shortfalls and excess before using layer count and breathability as tie-breaks. Selection and final/manual evaluation use the same regional ensemble regression and calibrated targets, with the shared 0.05 clo cold tolerance. A light clothing need cannot be satisfied by overly warm full coverage; absent suitable basics remain missing requirements with generic suggestions. Requirements assess the selected combination, including outer warmth and protection, and reasons name the worn outfit. When the best available combination still misses a target, both requirements and guidance name that clothing and explain the remaining shortfall, excess warmth, or protection gap. Missing wind and rain protection are reported separately. Extra clothing for stops stays separate. Ski bibs and insulated outerwear without explicit running suitability are excluded from moving outfits. The small mismatch allowances are wardrobe heuristics, not measured physiological limits. Cycling and touring retain their existing builders.
Alpine and XC compare complete base + optional mid/puffy + outer combinations for each region, accounting for outer-layer warmth before choosing a mid. They allow at most one item in each slot per covered region, including items that span multiple regions, so insulated outerwear takes the outer slot and no shell goes over it. Alpine puts a puffy only under a hard shell; XC allows one under any shell, but not under insulated outerwear. Regional budgets use the same thermal regression coefficients as ensemble scoring. Bibs cover the torso but sit under a jacket, so they fill only the legs slots. After preserving coverage and rain protection, alpine minimizes each region's combined shortfall below its minimum and excess above its neutral target, so a small overshoot never loses to a large shortfall and a large overshoot never buys a small gain. XC uses the same regional enumeration with a separate assessment: reviewed standalone coverage, actual wind/wet protection, excessive warmth before filling clothing gaps, then regional thermal fit and breathability. It permits one suitable Nordic tight or pant without an extra base or outer. XC requirements assess the selected combination, including all its warmth and protection. XC never leaves a region in underwear alone. Weather-protection scoring counts windbreakers and insulated outerwear as shells. When targets cannot be met, alpine returns the best available wearable combination. Regional warnings require both a shortfall and an available wearable alternative that could reduce it by more than 0.12 clo while preserving coverage and rain protection. The thermal comfort score and overall cold warnings still report cold conditions when regional targets are unreachable.
graph TD
START[Target Clo Range from Pipeline] --> RUNNING{Running?}
RUNNING -->|Yes| RUNOPT[Compare complete top and leg outfit combinations<br/>Standalone coverage without mandatory thermal bases<br/>Short sleeves and shorts allowed<br/>Exclude unreviewed ski bibs and insulated outers]
RUNOPT --> RCACHE[Cache regional contributions and partial totals<br/>Fix torso/arms + leg garments; prune inferior regional choices]
RCACHE --> RUNRANK[Actual wind/rain protection, then excess warmth<br/>Clothing coverage, thermal fit, layers, breathability]
RUNRANK --> RUNREQ[Evaluate with shared regional regression and targets<br/>Match selected combinations or report missing basics<br/>Distinguish best available clothing from absent basics]
RUNNING -->|No| SPORT{Alpine or XC?}
SPORT -->|Yes| CACHE[Cache garment features once<br/>Reuse prefix and base/mid totals]
CACHE --> COMB[Compare optional base, mid/puffy and outer<br/>for torso, then legs<br/>XC can use a standalone garment]
COMB --> CAP[One item per slot per covered region<br/>Insulated outer replaces shell<br/>Bibs fill leg slots under a jacket]
CAP --> RANK{Sport}
RANK -->|Alpine| REG[Preserve coverage and rain protection<br/>Minimize combined shortfall and excess, then layers]
REG --> WARM[Search all compatible outfit combinations for regional maxima<br/>Keep selected coverage and rain protection]
WARM --> WARN[Warn only when a regional shortfall<br/>can be reduced by more than 0.12 clo]
RANK -->|XC| XREG[Actual full-region wind/wet protection<br/>Bound excess, require reviewed standalone coverage<br/>Fit regional bands, then breathable and fewer layers]
XREG --> XREQ[Match selected combinations or report gaps<br/>Carry stop warmth separately]
SPORT -->|No| SORT[Sort Pool by Sport Strategy]
SORT --> BASE[Select Base Layer]
BASE --> MID[Select Mid Layer]
MID --> SHELL[Select Shell]
BASE --> T1[Pick torso garment]
T1 --> L1[Pick legs garment — must differ from torso]
subgraph "Guards Applied at Each Step"
G1{Garment already selected?}
G2{Insulating layers within budget?<br/>Running/biking shells bypass clo cap}
G3{Covers required body region?}
G4{Meets breathability threshold?<br/>Required protective outer layers qualify<br/>by wind/wet capability}
end
G1 -->|Duplicate| SKIP[Skip Garment]
G1 -->|New| G2
G2 -->|Exceeds budget| SKIP
G2 -->|Fits| G3
G3 -->|No| SKIP
G3 -->|Yes| G4
G4 -->|Below threshold| SKIP
G4 -->|Passes| ADD[Add to Ensemble]
Sports differ in candidate generation, ranking, budget model, and layer handling.
graph TD
SPORT{Sport} --> RUN[Running]
SPORT --> BIKE[Biking]
SPORT --> ALP[Alpine Skiing]
SPORT --> XC[XC Skiing]
SPORT --> TOUR[Ski Touring]
subgraph "Running — complete outfit search"
RUN --> R1[Standalone top and shorts or tights<br/>Optional base, mid and outer combinations]
R1 --> R2[Fix shared garments, prune regional choices<br/>Pair survivors using cached shared-model contributions<br/>One item per layer slot and covered region]
R2 --> R3[Same regional ensemble model and targets<br/>as final and manual evaluation]
R3 --> R4[Protection and thermal fit before breathability<br/>Missing suitable basics remain clothing gaps]
R4 --> R5[Reasons describe worn outfit<br/>Stop layers are separate]
end
subgraph "Biking — shared breathable builder"
BIKE --> B1[Breathability-sorted]
B1 --> B2[Shells always processed]
B2 --> B3[Basic clo budget for insulation<br/>Protective shells outside clo cap]
end
subgraph "Alpine Skiing"
ALP --> A1[Compare complete regional outfits]
A1 --> A2[One base + at most one mid/puffy<br/>+ one outer per region]
A2 --> A3[Insulated outerwear occupies outer slot<br/>Puffies pair only with hard shells or no outer]
A3 --> A4[Budget all layers by regional clo<br/>Balance shortfalls against excess warmth]
A4 --> A5[60/40 ski-and-lift cycle IREQ<br/>at time-weighted metabolism and relative wind<br/>Convert cycle target to basic clo<br/>Chairlift DLE retained]
end
subgraph "XC Skiing — regional builder shared with alpine"
XC --> X1[Compare complete regional outfits:<br/>torso and legs independent]
X1 --> X2[Standalone Nordic tights/pants or layered outfits<br/>Wind/wet needs follow exposure and precipitation<br/>Unknown usage and underwear alone cannot meet coverage]
X2 --> X3[Insulated outerwear occupies outer slot<br/>Puffies fit under any shell, not insulated outerwear]
X3 --> X4[Actual protection, bounded excess, coverage and regional fit<br/>Then breathable and fewer layers]
X4 --> X5[Thin functional XC accessories use local bands<br/>Carry extra warmth for stops, outside moving score]
end
subgraph "Ski Touring"
TOUR --> T1a[Triple-phase IREQ:<br/>uphill / downhill / transition]
T1a --> T2a[Uphill ensemble only —<br/>breathability-first]
T2a --> T3a[Insulation pool excluded<br/>from uphill — pack items only]
T3a --> T4a[Pack items scored by<br/>warmth-to-weight ratio]
T4a --> T5a[Transition protocol:<br/>urgent / quick / normal]
end
Shell selection varies significantly by sport. This decision tree shows when and which shells are considered.
graph TD
START{Which sport?} --> RUN[Running]
START --> BIKE[Biking]
START --> ALP[Alpine]
START --> XC[XC Skiing]
START --> TOUR[Ski Touring]
RUN --> RCHECK{Policy wind or wet protection needed?}
RCHECK -->|Yes| RPROTECT[Compare actual protective properties and coverage<br/>Prefer waterproof coverage in precipitation<br/>Protection takes priority over thermal budget]
RCHECK -->|No| ROPT[Outer optional within complete outfit<br/>Keep only when it improves thermal fit]
RPROTECT --> RRANK[Rank complete outfit using shared regional model<br/>No extra shell over insulated outerwear]
ROPT --> RRANK
RRANK --> RGAP[Warn and report clothing gaps<br/>when available protection is insufficient]
BIKE --> BADD[Always consider shells<br/>— breathability-sorted]
ALP --> AOUT[Compare shell or insulated outer<br/>as part of each regional outfit]
AOUT --> AWET{Precipitation?}
AWET -->|Yes| AWP[Prefer waterproof outer coverage]
AWET -->|No| ABUDGET[Fit regional insulation targets]
AWP --> ABUDGET
ABUDGET --> ACAP[One outer per covered region<br/>No shell over insulated outerwear<br/>Bibs sit under the jacket]
XC --> XNEED[XC exposure: max of ambient and exposure floor<br/>Ordinary motion stays in the existing insulation correction<br/>Rain/mixed/near-melting snow is wet; colder snow is dry]
XNEED --> XREQ[Protect full torso/arms and legs using actual properties<br/>Standalone garments can supply protection]
XREQ --> XFIT[Freezing alone never forces an extra garment<br/>Fit regional bands and standalone coverage<br/>then prefer breathable outers, evap ≥ 0.20]
XFIT --> XCAP[One outer per covered region<br/>No shell over insulated outerwear]
TOUR --> THARD{Hard shell available?}
THARD -->|Yes| THSEL[Select hard shell —<br/>soft shells excluded]
THARD -->|No| TOTHER[Fall back to<br/>soft shells / windbreakers]
Running/XC evaluate optional thermal coverage against the phase's local min/max bands. The absence of insulation competes with known suitable gear. When nothing fits, going without is weighed against the nearest item, with a shortfall counting double an excess, so a much warmer item never wins just because it is the only one. Catalog temperature ratings check the heuristic bands in deep cold: items rated for these conditions win, and the lightest rated choice sets the upper limit that scoring and manual checks use. Functional XC gloves have a separate pole-grip purpose. Cycling, alpine and touring explicitly retain the earlier selection rules pending calibration; required alpine/descent helmets remain outside the optional path.
graph TD
EXT[Server sport and phase policy<br/>Local minimum, neutral and upper limits] --> MODE{Sustained running or XC?}
MODE -->|Yes| RATED[Prefer items rated for the conditions:<br/>active minimum at most air temperature + 10°C<br/>Neck pieces must be rated]
RATED --> BARE[Evaluate bare hands and no thermal headwear<br/>alongside known suitable accessories]
BARE --> HAND[Hands: prefer gloves within the local band<br/>XC: choose among dexterous pole-grip gloves when any exist<br/>Thin-glove allowance does not add thermal demand]
BARE --> HEAD[Head plus neck combinations share one budget<br/>Include worn hood insulation<br/>Avoid overlapping neck layers]
HAND --> FIT{Anything within the band?}
HEAD --> FIT
FIT -->|No| NEAREST[Weigh going without against the nearest item<br/>shortfall counts double an excess<br/>Deep cold: rated and unrated gear owned,<br/>so a short part gets the lightest rated item]
NEAREST --> LIMIT[Lightest rated choice above the band<br/>raises that part's upper limit]
FIT -->|Yes| STATE
LIMIT --> STATE[Report selected, not needed,<br/>missing suitable gear, or unknown data<br/>Explain thermal or functional purpose]
STATE --> MATCH[Head requirements prefer selected head warmth<br/>Neck fallback only when no head layer is selected]
MATCH --> EDIT[Manual edits use the same local limits, raised ones included<br/>Catalog data and worn hoods resolved on server<br/>Cold shortfalls and local excess affect comfort]
MODE -->|No: compatibility policy| LEGACY[Temperature and target selection retained]
LEGACY --> LHAND[Biking: active glove thresholds<br/>Alpine and touring: static glove thresholds]
LEGACY --> LHEAD[Head warmth by temperature<br/>Neck layer below freezing]
LHEAD --> HELMET{Alpine or touring descent?}
HELMET -->|Yes| REQUIRED[Required helmet selected separately<br/>Only compatible hoods contribute]
HELMET -->|No| NOHELMET[No helmet from thermal selection]
The response's recommendation.evaluation and manual POST /api/v1/ensembles/evaluate share evaluatePhase. Touring exposes a separate descent_evaluation; packed layers never raise climb insulation. decision.status, summary, reasons and warnings are server-owned. The legacy riskType is retained for saved-outfit compatibility; it is a thermal mismatch direction, not a medical diagnosis. UI judgments must use the server summary rather than infer comfort from a score.
The old fixed +0.3 clo extension no longer makes an outfit comfortable. Whole-body excess uses a 0.01 clo numerical tolerance (the reported 0.353 versus 0.18–0.31 case is warmer); local excess uses the same 0.05 clo tolerance as local deficits. Upper targets are the phase's regional/neutral bands or explicit accessory limits, including XC's functional glove allowance. Small differences produce adjustment guidance without an injury warning. Significant mismatch messages retain conservative engineering allowances relative to the phase band; no numerical sport calibration or physiological safety threshold is introduced here. Field validation remains necessary. Missing regional catalog data or custom arm estimates produce unknown comfort; missing suitable clothing produces an incomplete outfit rather than an ideal outfit scored as worn. Existing evaporative resistance reasoning (#67) stays separate from clo.
Body-part targets are bands, like the whole-body target range. Whole-body and regional bands are converted from resultant IREQ to basic garment clo using each phase’s wind, movement and assumed outer permeability; the conversion never lowers a positive resultant requirement. Recommendations and manual edits compare actual regional garment clo with the same basic-clo phase bands. Standalone scoring retains its regional permeability conversion when no canonical phase assessment is supplied. Running targets assume only wind-resistant fabric when wind protection is required; a wet-only requirement accepts water-resistant fabrics with no known wind rating, so it cannot imply a membrane. These conservative targets stay independent of wardrobe ownership.
Running/XC taper added cold margins while preserving the calculated whole-body heat-balance minimum; mild air alone cannot erase a positive requirement for a smaller body or an easy effort. The torso, arms and legs split the whole-body range by the activity's regional multipliers, rescaled so their area-weighted mean is 1: an outfit with every region at its minimum sits exactly at the whole-body minimum. Alpine uses equal multipliers for torso, arms and legs, with no extra leg bias. Hands and head scale the whole-body IREQ of the phase they follow; alpine uses the skiing phase, since its extremity multipliers already add chairlift and wind exposure. These local targets are heuristics, so running/XC taper their full bands to permit zero thermal accessory requirements in suitable mild conditions. The comfort status, the body-part pills, and the comfort score all judge shortfalls against the bottom of each band, in src/lib/biophysics/comfort.ts. Running/XC also apply the same 0.05 clo tolerance above explicit accessory upper limits during selection and manual evaluation. XC allows existing thin liner gloves up to 0.55 clo (plus that tolerance) for pole grip; this is a catalog-based engineering allowance, not a physiological threshold.
graph TD
BALANCE[Whole-body heat-balance minimum] --> RANGE[Whole-body target range<br/>Running/XC taper added margins only<br/>Minimum never below heat balance]
RANGE --> SPLIT[Split across torso, arms and legs<br/>multipliers rescaled to an area-weighted mean of 1<br/>Alpine: equal torso, arm and leg targets]
SPLIT --> REG[Convert whole-body and regional bands to basic clo<br/>Phase-specific wind, motion and outer permeability<br/>Shared clothing area factor<br/>Never below resultant heat balance]
PIREQ[Whole-body IREQ of the phase the hands and head follow<br/>alpine: skiing phase] --> EXT[Hand and head targets:<br/>activity multiplier × wind factor<br/>× temperature factor from 0.8 to 1<br/>+ CoWEDA-inspired extremity buffer<br/>Running/XC: taper both bands and buffer<br/>Allow zero thermal coverage in mild dry exercise]
RANGE --> DEC{Comfort decision}
REG --> DEC
EXT --> DEC
EDIT[Manual outfit edits:<br/>UUID references or explicit source-free estimates<br/>Ownership and names do not trigger checks] --> LOOKUP[Server resolves catalog regional data<br/>Public rows reused for up to 30 seconds<br/>Custom items always read for signed-in owner]
LOOKUP --> PICKER[Picker and packed garments use the same<br/>server status, regional clo and provenance]
LOOKUP --> KNOWN{All worn items have usable thermal data?}
KNOWN -->|Yes: regional averages already include partial coverage| DEC
KNOWN -->|No| UNKNOWN[Comfort unknown with missing body areas<br/>No total clo, decision or score]
DEC -->|Whole body more than 0.05 clo below the range<br/>or local shortfall beyond 0.05 clo| COLD[Cooler than recommended]
DEC -->|Whole body more than 0.01 clo above the range<br/>or local excess beyond 0.05 clo| HOT[Warmer than recommended]
COLD --> MIX{Both local cold and excess warmth?}
HOT --> MIX
MIX -->|Yes| UNEVEN[Uneven warmth<br/>Extra torso warmth never covers bare required regions]
DEC -->|Within numerical tolerances| OK[Likely comfortable<br/>Estimated fit, never Optimal or a confidence percentage]
DEC --> WARN[Separate significant mismatch warnings<br/>Cold gap exceeds band width, with a 0.2 clo floor<br/>Warm excess exceeds the larger of<br/>band width and the legacy 0.3 clo allowance<br/>Engineering heuristics, not medical risk thresholds]
DEC --> MOISTURE[Existing evaporative-potential heuristic<br/>Report moisture limitation separately<br/>Never change fabric insulation]
OK --> S1[Score 85 to 100 by distance from band midpoint]
COLD --> S2[Score 85 × share of needed insulation<br/>at the worst-covered part, including 0.5 clo surface air]
HOT --> S3[Existing linear excess score to half-score<br/>then a smooth positive tail for severe excess<br/>or the local cold score if lower]
LOOKUP --> ACTUAL[Recompute worn arms and hood from current catalog items<br/>Carried clothing contributes only when worn in that phase]
ACTUAL --> DEC
DEC --> GAP[Missing standalone clothing or functional protection:<br/>explicit incomplete status, separate from unknown data]
DEC --> UI[API, edited outfit status, body-area pills and explanations<br/>use this phase assessment]
UI --> STALE[Pending or failed edit: retain outfit and labeled previous verdict<br/>Hide current score, body statuses and reasons until evaluated]
npm install
Create a .env.local file:
# Clerk authentication
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
# Supabase: persistent wardrobe, preferences, trips, and calibrated gear data.
# The service-role key is server-only (never prefix it with NEXT_PUBLIC_).
NEXT_PUBLIC_SUPABASE_URL=https://<project-ref>.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your_service_role_or_secret_key
# Paid agent API (optional; /api/agent/* returns 503 without it).
# See docs/paid-agent-api-mpp.md.
MPP_SECRET_KEY=...
# Trip place search (optional; server-only). Without it, location search says
# it's unavailable, but stays can still be saved with an address or typed
# coordinates. See docs/trip-locations.md.
GEOAPIFY_API_KEY=...
Without Supabase configured, database-backed API routes return 503. The home page then falls back to static layer recommendations from src/data/layerRecommendations.json, or shows a Try again prompt for activities that have none.
npm run dev
Open http://localhost:3000 in your browser.
npm run build
CI runs these on every pull request (production builds come from Vercel):
npm run lint # ESLint, zero warnings allowed
npm run typecheck # next typegen + tsc
npm run knip # unused files, exports, and dependencies
npm test -- --run # Vitest
This project uses Vitest with React Testing Library.
# Run all tests (watch mode by default in dev)
npm test
# Run tests with UI
npm run test:ui
# Run tests with coverage
npm run test:coverage
Route tests use src/test/fakeSupabase.ts, an in-memory stand-in for the Supabase query builder. The recommendation golden tests run against src/test/fixtures/gear-catalog.json, a snapshot of the real gear catalog.
The alpine outfit search retains parity tests against its previous implementation; XC has standalone/protection behavioral tests. The shared search also has an isolated performance benchmark.
Trip packing reports one coverage record per trip date, with forecast availability separate from clothing guidance. It uses the static table (Alpine, XC and Hiking; Climb is an explicit hiking approximation), rather than the sport-specific biophysics endpoints. Partial daytime forecasts remain visible, past/out-of-window dates stay planned, and saved manual kits are reviewed separately until packing can derive requirements from those kits. Packing fetches forecasts only for stops needed by automatic guidance, with up to three requests in flight, and labels skipped weather requests explicitly. Missing day plans can be restored from day detail without overwriting existing plans. Trip day weather is summarized server-side by the authenticated /api/v1/trips/[id]/days/[date]/weather route.
Trip subpages share an in-memory snapshot through their layout, scoped to the trip and Clerk session. Saves refresh it before returning to the overview; navigation and returning to a visible tab recheck snapshots older than one minute without hiding the current page. Switching trips, accounts, or sessions drops the snapshot. Packing-only visits do not fetch an unused full-trip snapshot. Public Open-Meteo responses use Next's Data Cache with five-minute revalidation, and simultaneous requests for the same coordinates share one provider request. User and trip data are never stored in that server cache.
src/
app/
api/
agent/recommendations/ # Paid (MPP) mirrors of the v1 recommendation routes
v1/recommendations/ # One route per sport, built from lib/recommendations
v1/ensembles/evaluate/ # Evaluates edited layers against the targets
v1/trips/ # Trips, stops, crew, days, kits, group gear, pack list
wardrobe/ # Wardrobe items, catalog, custom items
preferences/ # User preferences
weather/, geocode/ # Open-Meteo forecast and location search
plan-ahead/ # Multi-day forecast plan
packing-list/ # Packing list for a plan
media/ # Brand logos and item images
trips/ # Trips pages
wardrobe/ # My Gear page
faq/ # FAQ page
page.tsx # Home: pick an activity and Gear Up
components/
layers/ # Recommendation view: weather header, gauges, body-part sections, layer picker
trips/ # Trip UI primitives
wardrobe/ # Wardrobe components
ui/ # shadcn/ui components (vendored)
LayerDisplay.tsx # Recommendation view
data/
activities.ts # Activity definitions
layerRecommendations.json # Static layers for signed-out users
hooks/ # Client hooks: useGearUp, usePreferences, useWardrobe, useTrip, ...
lib/
api.ts # Route helpers: requireUser, readJson, jsonError
biophysics/ # Thermal models: IREQ, targets, CoWEDA buffer, ensembles, scoring
recommendations/ # Recommendation pipeline, sport modules, layer evaluation
payments/mpp.ts # Paid agent API (MPP)
supabase.ts # Server-only service-role client
trips.ts # Trip data access and requireTripAccess
test/ # Supabase fake and test fixtures
types/ # API contracts shared by server and client, and domain types
proxy.ts # Clerk middleware (Next 16's name for middleware.ts)
The app uses a biophysics-based garment database with calibrated thermal properties. See supabase/migrations/ for the full schema. Key tables:
garments - Clothing items with brand, model, category, and body coveragegarment_thermal_properties - Rcl (thermal resistance) and Recl (evaporative resistance)garment_protection - Wind and water resistance ratingsgarment_activity_ratings - Activity-specific suitability scoreshandwear / headwear - Extremity items with thermal propertiesuser_wardrobe - Links users to their owned gearuser_custom_items, user_item_mappings, user_preferences - Per-user datatrips, trip_stops, trip_members, trip_days, trip_member_day_kits, trip_group_gear - Trips/crew planningAccess model: the browser never talks to Supabase. API routes use the service-role key (src/lib/supabase.ts, guarded by server-only) and enforce per-user access in code, for example with requireUser() in src/lib/api.ts and requireTripAccess() in src/lib/trips.ts. Migration 014_restrict_to_service_role.sql revokes all table access from the public anon/authenticated roles, so the anon key grants nothing.
Applying migrations: production's migration history doesn't match the numbered files here (some migrations were applied by hand), so don't supabase db push the whole folder. Apply new migrations individually, for example in the SQL editor or with the Supabase MCP apply_migration.
The app is hosted at swttr.vercel.app.
Deploy your own instance with Vercel: