Skip to content

Patch notes + Owned weapon team configs - #41

Merged
woopxwoop merged 20 commits into
mainfrom
develop
Aug 12, 2026
Merged

Patch notes + Owned weapon team configs#41
woopxwoop merged 20 commits into
mainfrom
develop

Conversation

@woopxwoop

@woopxwoop woopxwoop commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added Patch Notes pages, detailed note views, footer navigation, and an automatic popup for unseen updates.
    • Added Planner tools for tracking character and weapon upgrade goals, calculating materials, and syncing saved plans.
    • Added dedicated Abyss, Stygian, enemy, and Pull Suggestions tool pages.
    • Added reusable investment build cards and owned-weapon alternatives to team rankings.
    • Added guide-defined upgrade priority tiers and improved weapon tie-breaking.
  • Improvements
    • Improved tooltip overflow handling, loading indicators, cloud-sync errors, and responsive layouts.
    • Beta characters and data are now clearly identified.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dc8db221-e043-49f0-8bc0-5683f5a708d6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b59edfe4-f6a0-4467-a725-8e00108484f2

📥 Commits

Reviewing files that changed from the base of the PR and between 0be1bf1 and 86ba152.

📒 Files selected for processing (24)
  • src/lib/app/character-summary.ts
  • src/lib/calculator-goals-snapshot.ts
  • src/lib/calculator-goals.test.ts
  • src/lib/character-builds.test.ts
  • src/lib/character-filter.test.ts
  • src/lib/is-beta-character.test.ts
  • src/lib/planner-targets.test.ts
  • src/lib/planner-targets.ts
  • src/lib/server/request-validation.test.ts
  • src/lib/ui/components/CostPopover.svelte
  • src/lib/ui/components/NumberSliderField.svelte
  • src/lib/ui/components/PickModal.svelte
  • src/lib/ui/focus-trap.ts
  • src/lib/upgrade-costs-merge.test.ts
  • src/lib/upgrade-costs-merge.ts
  • src/lib/upgrade-costs.test.ts
  • src/lib/upgrade-costs.ts
  • src/lib/utils.ts
  • src/routes/+page.svelte
  • src/routes/api/calculator-goals/+server.ts
  • src/routes/api/upgrade-costs/+server.ts
  • src/routes/tools/planner/+page.svelte
  • tests/smoke.spec.ts
  • tests/stygian.spec.ts

📝 Walkthrough

Walkthrough

This PR adds calculator and tool routes, patch-note publishing, investment metadata, beta-data handling, owned simulation variants, reusable UI components, and route-aware navigation. Legacy routes redirect to /tools paths.

Changes

Application updates

Layer / File(s) Summary
Investment and upgrade flows
src/lib/types/investment.ts, src/lib/character-builds.ts, src/lib/planner-targets.ts, src/lib/upgrade-costs.ts, src/lib/calculator-goals.ts
Investment tiers, reactions, ascension priorities, owned simulations, calculator goals, upgrade calculations, and planner target derivation are added.
CDN and persistence services
src/lib/asset-urls.ts, src/lib/server/character-kit.ts, src/lib/calculator-goals-snapshot.ts, src/routes/api/*
Live and beta data loading, caching, catalog merging, calculator-goal persistence, validation, and API handlers are added.
Patch-note publishing
src/lib/patch-notes.ts, src/lib/patch-notes-catalog.ts, src/routes/patch-notes/*, src/lib/ui/components/PatchNotesPopup.svelte
Patch notes are parsed, cataloged, served through list and detail routes, and shown through an acknowledgement popup.
Tool routes and migration
src/routes/tools/*, src/routes/abyss/*, src/routes/stygian/*, src/routes/pulls/*, src/routes/calculator/*
Abyss, Stygian, Pull Suggestions, and Planner pages are added under /tools. Legacy routes return HTTP 308 redirects.
Shared UI and build rendering
src/lib/ui/*, src/lib/ui/components/*, src/routes/teams/*, src/routes/characters/*
Navigation, pickers, sliders, tooltips, build cards, owned variants, beta indicators, hydration loading, and character priority rendering are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LayoutServer
  participant PatchNoteCatalog
  participant Layout
  participant PatchNotesPopup
  LayoutServer->>PatchNoteCatalog: Load sorted patch notes
  PatchNoteCatalog-->>LayoutServer: Return latest note metadata
  LayoutServer-->>Layout: Provide latestPatchNote
  Layout->>PatchNotesPopup: Pass latest note
  PatchNotesPopup->>PatchNotesPopup: Compare acknowledged slug
  PatchNotesPopup-->>Layout: Display or suppress popup
Loading

Poem

A rabbit checks the planner’s goals,
Patch notes hop through catalog roles.
Owned builds stay apart from pull advice,
Tool routes guide each page precisely.
New cards and pickers brighten the night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately identifies patch notes and owned weapon team configurations, which are real changes in the pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@woopxwoop

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/character-builds.test.ts`:
- Around line 639-648: Extend the “reaction helpers” test in the
formatReactionName/formatReactionFingerprint suite with an assertion for an
unknown reaction key, verifying formatReactionName uses its title-case fallback
when the key is absent from REACTION_LABELS.

In `@src/lib/character-builds.ts`:
- Around line 130-143: The weapon comparator in the sorting function should not
coerce missing strength values to 0. Compare strength only when both weapons
provide it; otherwise continue to the teams comparison, preserving the existing
ordering for stamped rows and the current non-negative strength behavior.

In `@src/lib/ui/components/HoverTooltip.svelte`:
- Around line 211-212: Update the hover/focus handlers around onEnter and
onLeave in HoverTooltip so pointer activity and focus activity are tracked
independently. Handle pointerenter/pointerleave and focusin/focusout separately,
and call hideTip only when both states are inactive, preserving aria-describedby
and the open tooltip while either interaction remains active.

In `@src/lib/upgrade-priority.test.ts`:
- Around line 67-90: Add an assertion in the “prefers stamped CDN tiers over the
ladder” test for a stamped tier with no impact_tiers/scale labels, such as
resolveUpgradeImpact("solid", 30, ladder). Verify it returns the stamped tier
with its corresponding MERGED_IMPACT_LABELS default label, covering the second
label-resolution path while preserving the existing assertions.

In `@src/lib/upgrade-priority.ts`:
- Around line 21-26: Replace the duplicated `UpgradeTier` union in
`src/lib/upgrade-priority.ts` with a type alias to the imported
`ImportanceImpactTier`, preserving the existing exported `UpgradeTier` name and
updating no other behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b851be28-9945-423d-b5f5-780c74add72a

📥 Commits

Reviewing files that changed from the base of the PR and between 069f229 and ddf014e.

⛔ Files ignored due to path filters (5)
  • .dockerignore is excluded by none and included by none
  • .github/workflows/patch-notes.yml is excluded by none and included by none
  • package.json is excluded by none and included by none
  • patch-notes/2026-08-10-roster-hotfix-and-patch-notes.md is excluded by none and included by none
  • patch-notes/README.md is excluded by none and included by none
📒 Files selected for processing (35)
  • src/lib/character-builds.test.ts
  • src/lib/character-builds.ts
  • src/lib/investment-build-card.ts
  • src/lib/investment-teams.test.ts
  • src/lib/investment-teams.ts
  • src/lib/patch-notes-catalog.ts
  • src/lib/patch-notes-seen.test.ts
  • src/lib/patch-notes-seen.ts
  • src/lib/patch-notes.test.ts
  • src/lib/patch-notes.ts
  • src/lib/site.ts
  • src/lib/types/investment.ts
  • src/lib/ui/NavBar.svelte
  • src/lib/ui/components/CostPopover.svelte
  • src/lib/ui/components/HoverTooltip.svelte
  • src/lib/ui/components/InvestmentBuildCard.svelte
  • src/lib/ui/components/PatchNotesPopup.svelte
  • src/lib/upgrade-priority.test.ts
  • src/lib/upgrade-priority.ts
  • src/routes/+layout.server.ts
  • src/routes/+layout.svelte
  • src/routes/+layout.ts
  • src/routes/abyss/+page.svelte
  • src/routes/characters/+page.svelte
  • src/routes/characters/[slug]/+page.svelte
  • src/routes/dev/ui/+page.svelte
  • src/routes/patch-notes/+page.server.ts
  • src/routes/patch-notes/+page.svelte
  • src/routes/patch-notes/[slug]/+page.server.ts
  • src/routes/patch-notes/[slug]/+page.svelte
  • src/routes/settings/panels/RosterPanel.svelte
  • src/routes/stygian/+page.svelte
  • src/routes/teams/[slug]/+page.svelte
  • src/routes/teams/configs/[slug]/+page.server.ts
  • src/routes/teams/configs/[slug]/+page.svelte

Comment thread src/lib/character-builds.test.ts Outdated
Comment thread src/lib/character-builds.ts Outdated
Comment on lines +130 to +143
return [...weapons].sort((a, b) => {
const ra = getStars(a.key);
const rb = getStars(b.key);
if (ra !== rb) return rb - ra;
const sa = a.strength ?? 0;
const sb = b.strength ?? 0;
if (sa !== sb) return sb - sa;
if (a.teams !== b.teams) return b.teams - a.teams;
const pa = preferred.has(a.key) ? 0 : 1;
const pb = preferred.has(b.key) ? 0 : 1;
if (pa !== pb) return pa - pb;
return a.key.localeCompare(b.key);
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Locate the producer of CharacterWeaponRank.strength and check its sign/coverage.
set -uo pipefail

# Find any pipeline/merge code that writes a Bradley-Terry strength field.
rg -n -C 6 --iglob '!**/node_modules/**' -e 'bradley' -e '\bstrength\b' -g '*.py' -g '*.ts' -g '*.json'

# Find committed fixtures or sample payloads containing weapon ranks.
fd -t f -e json | xargs -r rg -l '"weapons"' | while IFS= read -r f; do
  echo "== $f"
  rg -n -C 3 '"strength"' "$f" | head -40
done

Repository: woopxwoop/lightkeepers

Length of output: 377


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '== relevant files =='
git ls-files | rg '(^|/)(character-builds\.ts|investment\.ts)$|strength|weapon' | head -200
printf '%s\n' '== ranking implementation =='
sed -n '100,155p' src/lib/character-builds.ts
printf '%s\n' '== CharacterWeaponRank type documentation =='
sed -n '295,335p' src/lib/types/investment.ts
printf '%s\n' '== strength references and producers =='
rg -n -C 8 --glob '!**/node_modules/**' --glob '!**/dist/**' -e 'bradley' -e '\bstrength\b' .

Repository: woopxwoop/lightkeepers

Length of output: 14764


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '== tracked strength references =='
git grep -n -i -E 'strength|CharacterWeaponRank|weapon.*rank|rank.*weapon' -- ':!src/lib/data/weapons.json' | head -300
printf '%s\n' '== investment payload-loading code =='
sed -n '1,240p' src/lib/app/investment.ts
printf '%s\n' '== related tests =='
sed -n '1,180p' src/lib/character-builds.test.ts
printf '%s\n' '== tracked JSON files =='
git ls-files '*.json' | head -200
printf '%s\n' '== JSON strength coverage probe =='
python3 - <<'PY'
import json
import subprocess
from pathlib import Path

files = subprocess.check_output(["git", "ls-files", "*.json"], text=True).splitlines()
found = 0
for name in files:
    path = Path(name)
    try:
        data = json.loads(path.read_text())
    except Exception:
        continue
    def walk(value, loc="$"):
        global found
        if isinstance(value, dict):
            if "strength" in value:
                found += 1
                print(f"{name}:{loc}: strength={value['strength']!r}, keys={sorted(value)}")
            for k, v in value.items():
                walk(v, f"{loc}.{k}")
        elif isinstance(value, list):
            for i, v in enumerate(value):
                walk(v, f"{loc}[{i}]")
    walk(data)
print(f"objects_with_strength={found}")
PY

Repository: woopxwoop/lightkeepers

Length of output: 10403


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '== investment endpoint and data-source files =='
git ls-files | rg -i '(^|/)(api|scripts?|data|static|fixtures?|tests?)(/|$)|investment|character'
printf '%s\n' '== investment endpoint references =='
rg -n -C 10 --glob '!**/node_modules/**' -E '"/api/investment"|/api/investment|InvestmentFile|loadInvestment|fetchInvestment' .
printf '%s\n' '== repository metadata =='
git remote -v
git branch --show-current
git status --short
printf '%s\n' '== package scripts =='
cat package.json

Repository: woopxwoop/lightkeepers

Length of output: 4851


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '== investment endpoint =='
sed -n '1,240p' 'src/routes/api/investment/+server.ts'
printf '%s\n' '== HTTP client =='
sed -n '1,260p' src/lib/api/http.ts
printf '%s\n' '== investment references =='
rg -n -C 10 --glob '!**/node_modules/**' '/api/investment|InvestmentFile|loadInvestment|fetchInvestment' .
printf '%s\n' '== server environment and URL configuration =='
rg -n -C 8 --glob '!**/node_modules/**' 'investment|INVESTMENT|SUPABASE|DATABASE|API_URL|fetch\(' src svelte.config.* vite.config.* .env* 2>/dev/null

Repository: woopxwoop/lightkeepers

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '== character page data source =='
sed -n '1,220p' 'src/routes/characters/[slug]/+page.server.ts'
printf '%s\n' '== static API endpoint =='
sed -n '1,260p' 'src/routes/api/static/+server.ts'
printf '%s\n' '== CharacterIndex and build types =='
sed -n '1,180p' src/lib/types/investment.ts
printf '%s\n' '== build/static URL helpers =='
rg -n -C 8 --glob '!**/node_modules/**' 'builds|CharacterIndex|api/static|static.*json|cdn.*character|character.*cdn' src/lib src/routes | head -300

Repository: woopxwoop/lightkeepers

Length of output: 33926


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '== character summary helper =='
sed -n '1,280p' src/lib/server/character-summary.ts
printf '%s\n' '== URL and CDN configuration =='
rg -n -C 10 --glob '!**/node_modules/**' 'CHARACT|character.*json|summary|cdn|lightkeepers\.moe' src/lib/server src/lib/asset-urls.ts src/lib/utils.ts | head -400
printf '%s\n' '== CDN URL literals =='
git grep -n -E 'https?://[^"]*(character|summary|build|data)[^"]*' -- '*.ts' '*.svelte' '*.json' | head -200

Repository: woopxwoop/lightkeepers

Length of output: 23058


🏁 Script executed:

#!/bin/bash
set -u
python3 - <<'PY'
import gzip
import json
import urllib.request

url = "https://images.lightkeepers.moe/sim/characters.json.gz"
req = urllib.request.Request(url, headers={"User-Agent": "read-only-review-probe"})
try:
    with urllib.request.urlopen(req, timeout=20) as response:
        raw = response.read()
        status = response.status
except Exception as exc:
    print(f"fetch_error={exc!r}")
    raise SystemExit(0)

if raw[:2] == b"\x1f\x8b":
    raw = gzip.decompress(raw)
try:
    data = json.loads(raw)
except Exception as exc:
    print(f"json_error={exc!r}, bytes={len(raw)}")
    raise SystemExit(0)

print(f"http_status={status}")
print(f"top_level_type={type(data).__name__}")
if isinstance(data, dict):
    print(f"top_level_keys={sorted(data)[:20]}")

characters = data.get("characters", data) if isinstance(data, dict) else {}
if not isinstance(characters, dict):
    print(f"character_collection_type={type(characters).__name__}")
    raise SystemExit(0)

payloads = rows = present = missing = negative = 0
mixed_payloads = none_strength_payloads = all_strength_payloads = 0
min_strength = None
max_strength = None
missing_examples = []
negative_examples = []

for character_key, payload in characters.items():
    weapons = payload.get("weapons") if isinstance(payload, dict) else None
    if not isinstance(weapons, list) or not weapons:
        continue
    payloads += 1
    local_present = 0
    local_missing = 0
    for index, weapon in enumerate(weapons):
        if not isinstance(weapon, dict):
            continue
        rows += 1
        if "strength" in weapon and weapon["strength"] is not None:
            local_present += 1
            present += 1
            value = weapon["strength"]
            if isinstance(value, (int, float)) and not isinstance(value, bool):
                min_strength = value if min_strength is None else min(min_strength, value)
                max_strength = value if max_strength is None else max(max_strength, value)
                if value < 0:
                    negative += 1
                    if len(negative_examples) < 8:
                        negative_examples.append((character_key, index, weapon.get("key"), value))
        else:
            local_missing += 1
            missing += 1
            if len(missing_examples) < 8:
                missing_examples.append((character_key, index, weapon.get("key")))
    if local_present and local_missing:
        mixed_payloads += 1
    elif local_present:
        all_strength_payloads += 1
    elif local_missing:
        none_strength_payloads += 1

print(f"characters={len(characters)}")
print(f"payloads_with_weapons={payloads}")
print(f"weapon_rows={rows}")
print(f"strength_present={present}")
print(f"strength_missing_or_null={missing}")
print(f"negative_strength={negative}")
print(f"strength_min={min_strength}")
print(f"strength_max={max_strength}")
print(f"payloads_all_strength={all_strength_payloads}")
print(f"payloads_mixed_strength={mixed_payloads}")
print(f"payloads_no_strength={none_strength_payloads}")
print(f"missing_examples={missing_examples}")
print(f"negative_examples={negative_examples}")
PY

Repository: woopxwoop/lightkeepers

Length of output: 332


🏁 Script executed:

#!/bin/bash
set -u
python3 - <<'PY'
import gzip
import json
import ssl
import urllib.request

url = "https://images.lightkeepers.moe/sim/characters.json.gz"
context = ssl._create_unverified_context()
req = urllib.request.Request(url, headers={"User-Agent": "read-only-review-probe"})
try:
    with urllib.request.urlopen(req, context=context, timeout=30) as response:
        raw = response.read()
        status = response.status
except Exception as exc:
    print(f"fetch_error={exc!r}")
    raise SystemExit(0)

if raw[:2] == b"\x1f\x8b":
    raw = gzip.decompress(raw)
data = json.loads(raw)
characters = data.get("characters", data) if isinstance(data, dict) else {}

payloads = rows = present = missing = negative = 0
mixed_payloads = none_strength_payloads = all_strength_payloads = 0
min_strength = max_strength = None
missing_examples = []
negative_examples = []

for character_key, payload in characters.items():
    weapons = payload.get("weapons") if isinstance(payload, dict) else None
    if not isinstance(weapons, list) or not weapons:
        continue
    payloads += 1
    local_present = local_missing = 0
    for index, weapon in enumerate(weapons):
        if not isinstance(weapon, dict):
            continue
        rows += 1
        value = weapon.get("strength")
        if value is not None:
            local_present += 1
            present += 1
            if isinstance(value, (int, float)) and not isinstance(value, bool):
                min_strength = value if min_strength is None else min(min_strength, value)
                max_strength = value if max_strength is None else max(max_strength, value)
                if value < 0:
                    negative += 1
                    if len(negative_examples) < 8:
                        negative_examples.append((character_key, index, weapon.get("key"), value))
        else:
            local_missing += 1
            missing += 1
            if len(missing_examples) < 8:
                missing_examples.append((character_key, index, weapon.get("key")))
    if local_present and local_missing:
        mixed_payloads += 1
    elif local_present:
        all_strength_payloads += 1
    elif local_missing:
        none_strength_payloads += 1

print(f"http_status={status}")
print(f"characters={len(characters)}")
print(f"payloads_with_weapons={payloads}")
print(f"weapon_rows={rows}")
print(f"strength_present={present}")
print(f"strength_missing_or_null={missing}")
print(f"negative_strength={negative}")
print(f"strength_min={min_strength}")
print(f"strength_max={max_strength}")
print(f"payloads_all_strength={all_strength_payloads}")
print(f"payloads_mixed_strength={mixed_payloads}")
print(f"payloads_no_strength={none_strength_payloads}")
print(f"missing_examples={missing_examples}")
print(f"negative_examples={negative_examples}")
PY

Repository: woopxwoop/lightkeepers

Length of output: 725


🏁 Script executed:

#!/bin/bash
set -u
python3 - <<'PY'
import functools
import gzip
import json
import ssl
import urllib.request
from pathlib import Path

url = "https://images.lightkeepers.moe/sim/characters.json.gz"
req = urllib.request.Request(url, headers={"User-Agent": "read-only-review-probe"})
with urllib.request.urlopen(req, context=ssl._create_unverified_context(), timeout=30) as response:
    raw = response.read()
if raw[:2] == b"\x1f\x8b":
    raw = gzip.decompress(raw)
data = json.loads(raw)
characters = data.get("characters", data)

weapon_data = json.loads(Path("src/lib/data/weapons.json").read_text())
if isinstance(weapon_data, dict):
    weapon_data = weapon_data.get("weapons", [])
stars = {}
for item in weapon_data:
    if isinstance(item, dict):
        key = item.get("key") or item.get("nameId") or item.get("name_id")
        if key is not None:
            stars[key] = item.get("stars", 0)

def compare_current(a, b):
    ra, rb = stars.get(a.get("key"), 0), stars.get(b.get("key"), 0)
    if ra != rb: return rb - ra
    sa, sb = a.get("strength") if a.get("strength") is not None else 0, b.get("strength") if b.get("strength") is not None else 0
    if sa != sb: return -1 if sb > sa else 1
    if a["teams"] != b["teams"]: return b["teams"] - a["teams"]
    return (a["key"] > b["key"]) - (a["key"] < b["key"])

def compare_missing_fallback(a, b):
    ra, rb = stars.get(a.get("key"), 0), stars.get(b.get("key"), 0)
    if ra != rb: return rb - ra
    if a.get("strength") is not None and b.get("strength") is not None:
        if a["strength"] != b["strength"]:
            return -1 if b["strength"] > a["strength"] else 1
    if a["teams"] != b["teams"]: return b["teams"] - a["teams"]
    return (a["key"] > b["key"]) - (a["key"] < b["key"])

for character_key, payload in characters.items():
    rows = payload.get("weapons", []) if isinstance(payload, dict) else []
    if not rows or not any(r.get("strength") is None for r in rows):
        continue
    print(f"mixed_character={character_key}")
    for row in rows:
        print(json.dumps({
            "key": row.get("key"),
            "stars": stars.get(row.get("key"), 0),
            "teams": row.get("teams"),
            "strength": row.get("strength"),
        }, sort_keys=True))
    print("current_order=" + ",".join(r["key"] for r in sorted(rows, key=functools.cmp_to_key(compare_current))))
    print("missing_fallback_order=" + ",".join(r["key"] for r in sorted(rows, key=functools.cmp_to_key(compare_missing_fallback))))
PY

Repository: woopxwoop/lightkeepers

Length of output: 5335


Do not default a missing strength to 0.

CharacterIndex payloads can mix stamped and unstamped weapons. For example, TravelerPyro includes SacrificialSword with strength: 1 and FavoniusSword without strength, with equal team counts. Compare strength only when both rows provide it; otherwise compare team count. Current payload strengths are non-negative, so the negative-value case does not apply.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/character-builds.ts` around lines 130 - 143, The weapon comparator in
the sorting function should not coerce missing strength values to 0. Compare
strength only when both weapons provide it; otherwise continue to the teams
comparison, preserving the existing ordering for stamped rows and the current
non-negative strength behavior.

Comment thread src/lib/ui/components/HoverTooltip.svelte Outdated
Comment thread src/lib/upgrade-priority.test.ts
Comment thread src/lib/upgrade-priority.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 23

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/routes/+page.svelte (1)

26-61: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use siteAssetUrl for feature banners.

Import siteAssetUrl from $lib/utils and replace the six /site/*.webp literals with siteAssetUrl("team"), siteAssetUrl("abyss_banner"), siteAssetUrl("stygian_banner"), siteAssetUrl("heizou"), siteAssetUrl("kazuha"), and siteAssetUrl("xiao"). The current CDN objects are available.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/`+page.svelte around lines 26 - 61, Import siteAssetUrl from
$lib/utils in the page module and update each banner field in the features data,
including rosterCard, to use siteAssetUrl with the corresponding asset name:
team, abyss_banner, stygian_banner, heizou, kazuha, and xiao, removing the
hard-coded CDN URLs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/app/character-summary.ts`:
- Around line 29-45: Update the fetch call in the character-summary request flow
to use the existing timeout pattern from upgrade-costs.ts, including
AbortSignal.timeout with the established CDN_FETCH_TIMEOUT_MS constant. Keep the
current response handling and pending cleanup unchanged so stalled requests
settle and later calls can retry.

In `@src/lib/calculator-goals-snapshot.ts`:
- Around line 73-89: Update fetchGoalsCloud to use an AbortController and abort
the GET request after the shared POST_TIMEOUT_MS duration, passing the
controller’s signal to fetch and cleaning up the timeout when the request
settles. Move POST_TIMEOUT_MS above fetchGoalsCloud so it is available there,
while preserving the existing null-return behavior for unauthorized, failed,
aborted, and other errors.

In `@src/lib/is-beta-character.test.ts`:
- Around line 11-14: Add test assertions in the existing “skips travelers and
released rows” case for isBetaCharacter covering the PlayerGirl prefix, and add
an assertion for an empty nameId to exercise the !nameId guard. Keep the
expected result false, matching the existing PlayerBoy and released-character
assertions.

In `@src/lib/planner-targets.ts`:
- Around line 75-85: Update resolveTalentTier to handle a missing
importance[slot] row by returning the corresponding
UPGRADE_DEFAULTS.characterTarget.talents fallback instead of dereferencing it;
preserve existing tier classification for present rows. Add a regression test
covering a malformed loadCharacterSummary response with teams > 0 and an omitted
talent slot.

In `@src/lib/server/request-validation.test.ts`:
- Around line 191-210: Extend the validation tests around requireCalculatorGoals
to cover requireTalentLevels: reject a talent value outside 1..MAX_TALENT,
reject a character missing the talents key, and reject a numeric-string talent
value. Use the existing character fixture and isBadRequest assertion pattern,
keeping the level and ascension cases unchanged.

In `@src/lib/server/request-validation.ts`:
- Around line 252-258: Update the weapon_id validation in the goals payload
handling to require that the positive integer also exists in the weapon catalog
before persistence. Reuse the existing catalog lookup or validation symbol
available in the request-validation flow, and throw the same 400
GOALS_PAYLOAD_ERROR for unknown IDs while preserving acceptance of valid catalog
weapons.

In `@src/lib/ui/components/CostPopover.svelte`:
- Around line 17-20: Update the explanatory text in CostPopover so the subject
and verb agree in the clause about constellation selectors receiving C1 for
free, using the plural form “constellation selectors get” while preserving the
rest of the meaning.

In `@src/lib/ui/components/NumberSliderField.svelte`:
- Around line 68-77: Update the range input in NumberSliderField to remove the
aria-valuemin and aria-valuemax overrides so its announced bounds come from the
native min and max attributes; preserve the existing label and value behavior.
- Around line 38-59: Update the number-input flow around emit and commitDraft so
non-empty typing is retained in draft without clamping or emitting on each
keystroke, then apply snap-to-step and effective bounds only when commitDraft
processes the completed draft. Keep empty-input handling intact, and update the
range input to call clampAndEmit so range changes continue clamping immediately.

In `@src/lib/ui/components/PickModal.svelte`:
- Around line 56-74: Update the open-state effect and its onKey handler in
PickModal so Tab and Shift+Tab cycle focus only among focusable elements inside
.pick-panel, wrapping from the last element to the first and vice versa.
Preserve the existing Escape behavior, focus restoration, and listener cleanup.

In `@src/lib/upgrade-costs-merge.test.ts`:
- Around line 128-140: Add assertions in the mergeUpgradeCostCatalogs test to
verify the exact ordering of merged.characters by name_id and merged.weapons by
id, while preserving the existing membership and precedence checks.

In `@src/lib/upgrade-costs-merge.ts`:
- Around line 14-18: Update sortedCharacters to sort name_id using a
locale-independent raw string comparison or an explicitly fixed locale, ensuring
identical ordering across build, server, and test environments while preserving
the existing copied-array behavior.

In `@src/lib/upgrade-costs.test.ts`:
- Line 61: Replace the boolean-wrapped assertion for result.exp with
assert.equal, preserving the expected value of 8_362_650 so failures report the
actual EXP total.
- Around line 23-36: Ensure the catalog consumed by loadCatalog is available
before upgrade-costs.test.ts runs: either commit the generated curves.json,
characters.json, and weapons.json under the expected output location, or add a
CI workflow step that generates them before pnpm test:unit.

In `@src/lib/upgrade-costs.ts`:
- Around line 62-71: Update minAscensionForLevel to return ascension 0 when
level is 1 or promotes is empty, before sorting or iterating the promote steps.
Preserve the existing sorted lookup and MAX_ASCENSION fallback for non-empty
tables and higher levels.

In `@src/lib/utils.ts`:
- Around line 513-514: Update the SIM_BASE and SIM_CONFIGS_BASE constants to
derive their URLs from the existing CDN_BASE constant instead of repeating the
API host literal, preserving their current /sim and /sim-configs paths.

In `@src/routes/api/calculator-goals/`+server.ts:
- Around line 22-51: Add the SQL migration that creates the
user_calculator_goals table with user_id, goals, and updated_at matching the
generated types, including required constraints and indexes as appropriate. Then
remove the interim GoalsRow and GoalsTable definitions and update goalsTable to
use the typed serverDb client directly, eliminating the unknown cast while
preserving its existing table access.

In `@src/routes/api/upgrade-costs/`+server.ts:
- Around line 48-60: Update tryFetchBetaJson to wrap the entire fetchWithTimeout
request, HTTP handling, and response JSON parsing in a try/catch; return null
for any thrown network, timeout, or parsing error while preserving the existing
status-based soft-fail behavior and warning for non-success HTTP responses.
- Around line 82-84: Update GET and the fetchCatalog path so beta CDN network,
timeout, and JSON parsing errors are caught and converted to null, allowing
Promise.all to succeed when live data is available. Ensure LRUCache.getOrSet
receives a resolving request rather than a rejected one, while preserving
live-data failures and existing successful caching behavior.

In `@src/routes/tools/planner/`+page.svelte:
- Around line 354-377: Update addCharacterWith and addWeaponWith to detect when
appendGoal reaches the existing MAX_CALCULATOR_GOALS cap, report the failure
through the goals panel’s addError, and return before calling beginConfigure;
preserve normal goal creation and dialog behavior when the append succeeds, and
import the limit constant from $lib/calculator-goals.
- Around line 413-424: Update the focus-management $effect around configCloseEl
and closeConfigure to handle Tab key presses by cycling focus among focusable
elements inside .config-panel, wrapping forward and backward at the boundaries
while preserving Escape behavior. Add the same focus trap to PickModal.svelte
only after implementing the shared behavior there.

In `@tests/smoke.spec.ts`:
- Around line 20-22: Add smoke-test coverage for the legacy routes in the test
list alongside the existing `/tools/*` entries, asserting each redirects with
HTTP 308 to its corresponding destination: `/abyss`, `/stygian`,
`/stygian/enemies`, `/stygian/enemies/[id]`, `/calculator`, and `/pulls`.
Preserve the current destination-page coverage.

In `@tests/stygian.spec.ts`:
- Line 9: Extend the test around page.goto("/tools/stygian") to cover the legacy
/stygian route, asserting that it redirects to /tools/stygian. Add one assertion
for the legacy route while preserving the existing destination-page coverage.

---

Outside diff comments:
In `@src/routes/`+page.svelte:
- Around line 26-61: Import siteAssetUrl from $lib/utils in the page module and
update each banner field in the features data, including rosterCard, to use
siteAssetUrl with the corresponding asset name: team, abyss_banner,
stygian_banner, heizou, kazuha, and xiao, removing the hard-coded CDN URLs.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 120c5323-cdc7-40c3-85e3-7100e0611690

📥 Commits

Reviewing files that changed from the base of the PR and between ddf014e and 0be1bf1.

⛔ Files ignored due to path filters (3)
  • package.json is excluded by none and included by none
  • patch-notes/2026-08-11-roster-hotfix-and-patch-notes.md is excluded by none and included by none
  • patch-notes/README.md is excluded by none and included by none
📒 Files selected for processing (90)
  • src/app.css
  • src/lib/app/bootstrapClient.ts
  • src/lib/app/character-summary.ts
  • src/lib/app/upgrade-costs.ts
  • src/lib/asset-urls.ts
  • src/lib/calculator-goals-snapshot.test.ts
  • src/lib/calculator-goals-snapshot.ts
  • src/lib/calculator-goals.test.ts
  • src/lib/calculator-goals.ts
  • src/lib/character-builds.test.ts
  • src/lib/character-builds.ts
  • src/lib/character-filter.test.ts
  • src/lib/character-filter.ts
  • src/lib/definitions.ts
  • src/lib/is-beta-character.test.ts
  • src/lib/is-beta-character.ts
  • src/lib/patch-notes-seen.test.ts
  • src/lib/patch-notes.test.ts
  • src/lib/patch-notes.ts
  • src/lib/planner-targets.test.ts
  • src/lib/planner-targets.ts
  • src/lib/server/character-kit.ts
  • src/lib/server/request-validation.test.ts
  • src/lib/server/request-validation.ts
  • src/lib/server/stygian-enemies.ts
  • src/lib/solver.ts
  • src/lib/stores.ts
  • src/lib/types/calculator-goals.ts
  • src/lib/types/database.types.ts
  • src/lib/types/investment.ts
  • src/lib/types/upgrade-costs.ts
  • src/lib/ui/NavBar.svelte
  • src/lib/ui/components/BrowseFlipCard.svelte
  • src/lib/ui/components/CharacterIcon.svelte
  • src/lib/ui/components/CharacterSearchSelect.svelte
  • src/lib/ui/components/CostPopover.svelte
  • src/lib/ui/components/HoverTooltip.svelte
  • src/lib/ui/components/NumberSliderField.svelte
  • src/lib/ui/components/PatchNotesPopup.svelte
  • src/lib/ui/components/PickModal.svelte
  • src/lib/upgrade-costs-merge.test.ts
  • src/lib/upgrade-costs-merge.ts
  • src/lib/upgrade-costs.test.ts
  • src/lib/upgrade-costs.ts
  • src/lib/upgrade-priority.test.ts
  • src/lib/upgrade-priority.ts
  • src/lib/utils.ts
  • src/routes/+layout.svelte
  • src/routes/+page.svelte
  • src/routes/abyss/+page.server.ts
  • src/routes/abyss/+page.svelte
  • src/routes/api/calculator-goals/+server.ts
  • src/routes/api/character-summary/[key]/+server.ts
  • src/routes/api/upgrade-costs/+server.ts
  • src/routes/calculator/+page.server.ts
  • src/routes/calculator/+page.svelte
  • src/routes/characters/[slug]/+page.server.ts
  • src/routes/characters/[slug]/+page.svelte
  • src/routes/dev/ui/+page.svelte
  • src/routes/patch-notes/+page.svelte
  • src/routes/pulls/+page.server.ts
  • src/routes/pulls/+page.svelte
  • src/routes/settings/panels/DisplayPanel.svelte
  • src/routes/stygian/+page.server.ts
  • src/routes/stygian/+page.svelte
  • src/routes/stygian/enemies/+page.server.ts
  • src/routes/stygian/enemies/+page.svelte
  • src/routes/stygian/enemies/[id]/+page.server.ts
  • src/routes/stygian/enemies/[id]/+page.svelte
  • src/routes/teams/configs/[slug]/+page.svelte
  • src/routes/tools/abyss/+page.server.ts
  • src/routes/tools/abyss/+page.svelte
  • src/routes/tools/calculator/+page.server.ts
  • src/routes/tools/calculator/+page.svelte
  • src/routes/tools/planner/+page.svelte
  • src/routes/tools/planner/+page.ts
  • src/routes/tools/pulls/+page.svelte
  • src/routes/tools/pulls/+page.ts
  • src/routes/tools/stygian/+page.server.ts
  • src/routes/tools/stygian/+page.svelte
  • src/routes/tools/stygian/enemies/+page.server.ts
  • src/routes/tools/stygian/enemies/+page.svelte
  • src/routes/tools/stygian/enemies/[id]/+page.server.ts
  • src/routes/tools/stygian/enemies/[id]/+page.svelte
  • tests/abyss.spec.ts
  • tests/global-setup.ts
  • tests/lazy-api.spec.ts
  • tests/pulls.spec.ts
  • tests/smoke.spec.ts
  • tests/stygian.spec.ts
💤 Files with no reviewable changes (2)
  • src/routes/dev/ui/+page.svelte
  • src/routes/characters/[slug]/+page.svelte

Comment thread src/lib/app/character-summary.ts
Comment thread src/lib/calculator-goals-snapshot.ts
Comment thread src/lib/is-beta-character.test.ts
Comment thread src/lib/planner-targets.ts
Comment thread src/lib/server/request-validation.test.ts
Comment thread src/routes/api/upgrade-costs/+server.ts
Comment thread src/routes/tools/planner/+page.svelte
Comment thread src/routes/tools/planner/+page.svelte
Comment thread tests/smoke.spec.ts
Comment thread tests/stygian.spec.ts Outdated
@woopxwoop

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@woopxwoop

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@woopxwoop
woopxwoop merged commit 9d8ce22 into main Aug 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant