Skip to content

Build lifecycle

What this answers: what states does a Build move through, what triggers each transition, and how does compat evaluation flow when the Build changes?

State machine

stateDiagram-v2
    [*] --> Empty
    Empty --> Partial : add component
    Partial --> Empty : clear all
    Partial --> Complete : fill last slot
    Complete --> Partial : remove component
    Empty --> SavedAnon : save (cookie)
    Partial --> SavedAnon : save (cookie)
    Complete --> SavedAnon : save (cookie)
    SavedAnon --> SavedAccount : sign in & claim
    SavedAccount --> Shared : generate share slug
    SavedAnon --> Shared : generate share slug
    Shared --> [*] : delete
    SavedAccount --> [*] : delete

Saved → Shared is bidirectional in practice — sharing doesn't change the save state, it adds a public slug. The state diagram simplifies for clarity.

States explained

State Meaning Where the data lives
Empty Build exists conceptually (someone opened /build) but no slots filled. Nowhere persisted; URL state is empty.
Partial At least one slot filled, not yet complete. URL params (?cpu=...&gpu=...) for the in-progress browser session.
Complete All 8 slots filled. Same as Partial — URL params.
SavedAnon Cookied to the visitor's browser via signed build_session cookie, 1y expiry. Cookie holds the slot map (or a server-side session ID if too large). See ADR-0003.
SavedAccount Persisted to the Build table, owned by a User. Cross-device. Postgres.
Shared Has a public unlisted slug. Can be SavedAnon + Shared or SavedAccount + Shared. Postgres Build.shareSlug.

Transition triggers

  • add component / remove component — user picks or clears a slot in /build. URL updates immediately.
  • save (cookie) — explicit user action via the "Save" button. Cookie is signed server-side.
  • sign in & claim — Auth.js completes; the same response that finalises sign-in claims the cookied build into the user's account, then clears the cookie. See ADR-0003.
  • generate share slug — explicit "Share" button. Creates a random unlisted slug, persists alongside the Build.

Compatibility evaluation

Whenever the Build moves between any of the in-progress states (Empty / Partial / Complete), every compat rule re-evaluates:

flowchart TD
    Start([Build state changes]) --> Iter[For each compat rule]
    Iter --> Eval{Evaluate rule}
    Eval -->|pass| Next[Next rule]
    Eval -->|warn| W[Annotate slot: warn]
    Eval -->|block| Bk[Annotate slot: block]
    Eval -->|info| I[Annotate slot: info]
    W --> Next
    Bk --> Next
    I --> Next
    Next --> More{More rules?}
    More -->|yes| Iter
    More -->|no| Done([Surface annotations + overall status])

Rules in scope today (M1):

  • CPU socket vs Motherboard socket
  • PSU wattage vs CPU TDP + GPU TDP (basic sum)
  • GPU length vs Case clearance

Rules planned for #22:

  • RAM type (DDR4/DDR5) vs Motherboard
  • Cooler height vs Case max cooler height

Verdicts: each rule produces pass, warn, block, or info. The Build's overall status is the max severity across all rules. block does not prevent saving — UC-9 explicitly allows incomplete or incompatible builds (per Use cases).

Implementation: pure functions in src/rules/, aggregated in src/lib/build.ts:computeCompatibility.

Notes

  • The cookie save state is decided by ADR-0003 — every saved build is shareable, regardless of ownership.
  • "Compatibility" is a derived attribute, not a state — it's recomputed on every change rather than stored. This avoids the need to invalidate cached compat results when rules change.
  • Soft delete vs hard delete on saved builds is undecided. Hard delete is simpler; soft delete preserves the share URL longer.