# Spec C — Build context + dep Add + auto-disambiguation rules > **Lineage:** sits on top of Spec A (multi-branch picker) and Spec B+F (collection expansion / live drain). Adds context-aware default selection. Does **not** modify the picker contract — Spec A §8 ownership still holds. ## §1 Overview Three loosely-related improvements that share the same core: the system has more context than it has been using. The user already tells us their PZ build via the `pzBuild` localStorage toggle. The user already gives us a list of mod_ids (via the wsids in their input). The system should consult both before deciding which branches of a multi-branch wsid land in `MODS_LINE` by default. **Goal:** smarter pre-ticked boxes in the multi-branch picker. **Non-goal:** any form of "magic" sort that emits a branch the user didn't see. ## §2 Build context (`pzBuild`) The frontend stores `sortof.pzBuild` in localStorage with values `"B41" | "B42"`, default `"B41"`. It already drives `MODS_LINE` rendering (B42 prefixes mod_ids with `\`). This spec extends `pzBuild` to: - Travel with `/api/sort` POST body as `pz_build: "B41" | "B42"`. The backend defaults to `"B42"` when missing or invalid. - Inform Rule A (§4.3) of auto-disambiguation. `pz_build` is **not** sent on `/api/resort` — the resort flow uses an explicit mod_id list and never re-evaluates rules. ## §3 Dep Add (already shipped) Documenting the existing behavior so it's part of the locked design. When `mlos_sort` reports a missing requirement (mod A requires mod B, B is not in the user's enabled set), `build_warnings` enriches each `tag: "missing"` warning with one of: - `actions: [{type: "add-wsid", wsid, modId, label}]` — when `mod_parsed` has a row with `mod_id == B` (using `DISTINCT ON (mod_id) ORDER BY parsed_at_time_updated DESC`) - `actions: [{type: "search-workshop", modId, url, label}]` — when no cache hit; URL is `https://steamcommunity.com/workshop/browse/?appid=108600&searchtext=` The frontend renders `[add modId]` as a filled blue chip; clicking appends the wsid to the input textarea AND auto-resorts (no separate sort click needed). The search variant is `[↗ find modId]` and opens Steam's search in a new tab. ## §4 Auto-disambiguation rules ### §4.1 Design principle (locked) These rules **adjust which boxes are pre-ticked in the picker**. They never bypass the picker and never silently emit a branch the user didn't see. Spec A §8 ownership holds — the picker is the source of truth. The rules are applied at `/api/sort` time, before `MODS_LINE` is composed. The response's `MOD_DB` always contains every cached branch (so the picker can offer them); `SORTED_ORDER` and `MODS_LINE` reflect only the auto-selected set. The frontend reads the picker default from `SORTED_ORDER` membership. ### §4.2 Order of evaluation per wsid ``` A → C → B ``` The first rule that single-ticks a branch wins; subsequent rules emit warnings only. Exception: A and C are orthogonal (build × addon) and both may tick the same wsid simultaneously — see §4.7. If a wsid is **coordinated** (any branch references a sibling via `requirements` / `loadAfter` / `loadBefore`) or **radio** (any branch lists a sibling in `incompatibleMods`), it is **exempt from rules A/B/C**. Coordinated → all branches stay; radio → first only. ### §4.3 Rule A — build-aware default *(highest ROI)* A branch is **B41-flavored** if `mod_id`: - ends with `B41` (case-insensitive), or - contains `_legacy_` followed by version digits (e.g., `_legacy_42_12`, `_legacy_41_*`) A branch is **B42-flavored** if `mod_id`: - ends with `B42` or `_b42` (case-insensitive), or - contains `_b42_` (e.g., `vac_mod_b42_utils`) A branch is **un-flavored** otherwise. Apply: if exactly one branch is flavored to match the active `pz_build` AND no other branch shares that flavor, pre-tick that one. Un-flavored branches are treated as "the build the author considers default" — currently always B42. If no branch matches the active build (e.g., user is on B41 but every branch is B42-flavored), fall through to Rule C / B and emit a `build-mismatch` warning: `"no variant for (); using author default"`. Rule A also fires when the wsid has **exactly two branches** and one is `B41`-flavored, one is unflavored — the unflavored is treated as B42 default. So a B42 user gets the unflavored, a B41 user gets the B41-flavored. ### §4.4 Rule B — prefix-base tiebreaker When the auto-pick path needs to pick *one* branch (Rule A didn't single-tick, Rule C didn't fire), use the **shortest mod_id that is a strict prefix of every other branch's mod_id** instead of alphabetical-first. **Strict prefix definition:** `A` is a strict prefix of `B` iff: 1. `A` ≠ `B`, AND 2. `B.startswith(A)`, AND 3. The character at position `len(A)` in `B` is a non-lowercase-letter — separator (`_` `-` ` `), digit (`0`-`9`), or uppercase letter (`A`-`Z`). Boundary regex: `^([_\- ]|[A-Z]|[0-9]|$)`. **Examples that qualify:** - `ArmoredVests` ⊂ `ArmoredVestsPatch` (boundary: `P`) - `ToadTraits` ⊂ `ToadTraitsDisablePrepared` (boundary: `D`) - `LitSortOGSN` ⊂ `LitSortOGSN_chocolate` (boundary: `_`) - `WaterDispenser` ⊂ `WaterDispenser2` (boundary: `2`) **Examples that do NOT qualify:** - `Foo` vs `Foobar` (boundary: `b`, lowercase continuation) - `Lit` vs `LitSort` (boundary: `S`, capital — actually qualifies; this case becomes prefix-base, deliberate) If multiple branches are mutual prefixes (impossible by definition) or if no branch is a prefix-of-all-others, fall back to alphabetical-first by `mod_id`. Rule B still emits the `auto-picked-branch` warning (Spec A §4) and renders the click-to-expand picker buttons — the only behavior change is the choice of which branch wins the auto-pick. ### §4.5 Rule C — input cross-reference *(solves Jeeve's Patches)* For each ambiguous branch whose mod_id matches the pattern `_` (i.e., contains an underscore, with a base prefix shared with at least one sibling branch): 1. Extract `` (the part after the last `_`). 2. Look up `` against the resolved mod_id set from the user's input — case-insensitive substring match against any cached `mod_parsed.mod_id` whose wsid is in the user's input. 3. Match requires `` length ≥ 3 (avoid `_a`, `_x` false positives). **Hit:** pre-tick that branch alongside the base branch. **No hits on any suffix-tokened branch:** tick the base branch only and emit `unmatched-addons` warning listing the unticked branches by name. The "base branch" inside a Rule-C wsid is determined by Rule B (prefix-base tiebreaker), with alphabetical fallback. **Worked example: Jeeve's Patches** (wsid 3684025083, branches `JeevesPatches`, `JeevesPatches_AZ`, `JeevesPatches_DAMN`, `JeevesPatches_GGS`, `JeevesPatches_ISA`, `JeevesPatches_PlayerStatus`, `JeevesPatches_Spongie`, `JeevesPatches_Tanker`, `JeevesPatches_Towing`, `JeevesPatches_Vanilla`, `JeevesPatches_ZRE`): - Base = `JeevesPatches` (Rule B: prefix-of-others). - Tokens: `AZ`, `DAMN`, `GGS`, `ISA`, `PlayerStatus`, `Spongie`, `Tanker`, `Towing`, `Vanilla`, `ZRE`. - User submits `AuthenticZ`-related wsids → some cached mod_id contains `AZ` (case-insensitive substring) → tick `JeevesPatches_AZ`. - User submits `JeevesIntegration` → no token hits → tick `JeevesPatches` only, emit `unmatched-addons` warning. ### §4.6 Rules D + G — hint text only When the picker renders an ambiguous wsid's branches, attach a per-branch `hint` field surfaced in the picker row. **No state mutation.** Hints are: | Suffix pattern | Hint text | |---|---| | `_Lite`, `_Light` | "lighter alternate variant — pick one" | | `_HD`, `_DetailsHD` | "high-resolution variant" | | `_NoCE`, `_NoVanilla`, `_FarmDisable`, `_Disable*` | "opt-out variant" | | `_USDM`, `_Imports`, `_Exotics`, `_RealNames` | "alternate variant — pick one" | | `_v\d+(_\d+)*`, `_legacy_*` | "legacy build — usually not what you want" | | `_AZ`, `_DAMN`, `_GGS`, etc. (Rule C suffixes that didn't match input) | "addon for `` — only if you have it" | Pattern matching is case-insensitive with anchored end-of-string for short tags. Multiple hints on one branch concatenate with " · ". ### §4.7 Edge cases **A and C both fire and disagree** (build hint says B42, input cross-ref says addon `_AZ`): both tick. Build × addon are orthogonal axes; they don't compete for the same slot. **A says no match (build-mismatch), C also fires:** C still fires; the build-mismatch warning surfaces alongside C's selections. Rule A's "fall back to author default" doesn't override C. **Coordinated wsid where one branch happens to be B42-flavored and another B41-flavored:** the wsid is exempt from A/B/C (coordinated detection runs first). All branches stay regardless of build mismatch. This is the right answer because coordinated branches by definition need each other. **Stored selections** in localStorage from previous sessions: the existing `runResort(branchSelections)` flow fires *after* the initial sort response is rendered. User's stored selections override the rules. Rules only determine the initial render's pre-ticked state for never-touched wsids. **`pz_build` missing or invalid in request:** backend treats as `"B42"`. Forwards-compatible if frontend on a stale build doesn't send it. ## §5 Implementation notes **Backend** (`adapters.py` + `app.py`): - `SortRequest` gains `pz_build: Optional[str]`. - `_autopick_ambiguous(mods)` is renamed `_apply_branch_rules(mods, *, pz_build, input_modids)` and replaces the alphabetical-first picker with the rule pipeline above. - Returns `(drop_ids, warnings, hints)` — `hints` is `Dict[mod_id -> str]` consumed by `build_response` and attached to MOD_DB entries as `hint?: string`. - `sort_endpoint` computes `input_modids = set(by_id.keys())` (the cached mod_ids) and passes alongside `pz_build`. **Frontend** (`sortof-app.jsx`): - `onSort` POST body adds `pz_build: pzBuild`. - `defaultSelectionForBranches(branches, activeSet)` accepts an `activeSet: Set` (the union of `D.SORTED_ORDER` mod_ids). Returns `branches.filter(b => activeSet.has(b.modId)).map(b => b.modId)`, falling back to `[branches[0].modId]` if none match. - All call sites of `defaultSelectionForBranches` pass `new Set(D.SORTED_ORDER || [])`. - `BranchPicker` renders `branch.hint` when present, as a small italic line under the mod_id. The frontend doesn't reimplement the rules — it simply reflects the backend's chosen `SORTED_ORDER`. Same data path keeps the picker as source of truth and makes the rules introspectable from a curl response. ## §6 Acceptance criteria - [ ] `POST /api/sort` accepts `pz_build` and defaults to `"B42"` when omitted. - [ ] B42 user submits a wsid with branches `Foo` (un-flavored) and `FooB41`: `MODS_LINE` includes `Foo` only. B41 user gets `FooB41`. - [ ] User submits Jeeve's Patches wsid alone: `MODS_LINE` is `JeevesPatches`. WARNINGS includes `unmatched-addons` listing the 10 untouched branches. - [ ] User submits Jeeve's Patches + a wsid whose mod_id is `AuthenticZ_Current`: `MODS_LINE` includes `JeevesPatches` and `JeevesPatches_AZ`. - [ ] Wsid `1962761540` (`ArmoredVests`, `ArmoredVestsPatch`): auto-pick selects `ArmoredVests` (prefix-base), not alphabetical-first (which is the same here — pick a wsid where they differ to verify). - [ ] Coordinated wsid `2791656602` (fhqMotoriusZone): all 5 branches stay in `MODS_LINE` regardless of `pz_build`. - [ ] Picker UI shows hint text per branch for D/G-class suffixes. - [ ] `WORKSHOP_ITEMS_LINE` matches `wsids[]` regardless of which branches got ticked (Spec A §8 unchanged). ## §7 Test recipes 1. **Build A — B42 default.** `pz_build=B42` + wsid with branches `[Foo, FooB41]`. Expect `MODS_LINE = "Foo"`, no `build-mismatch` warning. 2. **Build A — B41 + only-B42-variants → mismatch warning.** `pz_build=B41` + wsid where every branch is B42-flavored. Expect `build-mismatch` warning, fall through to Rule B. 3. **Rule C — Jeeve's alone.** Submit only Jeeve's Patches wsid. Expect MODS_LINE = `JeevesPatches`, `unmatched-addons` warning lists the 10 others. 4. **Rule C — Jeeve's + AuthenticZ.** Submit Jeeve's wsid + AuthenticZ wsid. Expect MODS_LINE includes `JeevesPatches` and `JeevesPatches_AZ`. 5. **Rule B — prefix-base picks non-alphabetical.** Find a wsid where alphabetical-first ≠ prefix-base; verify prefix-base wins. 6. **Hint text — `_Lite` / `_legacy_*`.** Picker row shows the appropriate hint string. 7. **Coordinated exemption.** fhqMotoriusZone: all 5 branches in MODS_LINE for both B41 and B42 users. 8. **Stored-selection override.** User has `branchSelections[wsid] = [explicit]` from previous session. Rules don't override on next sort — runResort runs with the explicit selection after initial render.