mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 02:44:30 +00:00
fix(career): gold review fixes — family-space style matching, intake guards, rail counter
The review's showstopper: virtuoso mints goldImprov under raw
STYLE_PALETTES ids ('punk', 'djent', 'disco'), which are mostly NOT
family keys — the tier check now matches in family space (artifact style
and passport genre bucket through the same _genre_family keyword match),
so a 'punk' gold reaches a 'punk rock' passport. Also: non-dict
goldImprov 400s loudly instead of silently dropping; evidence-free
artifacts (no verifier) never mint; goldImprov gets the same pre-merge
size bound byNode has (junk under the cap could otherwise persist
forever and wedge every later relay at the post-merge check); the
instrument-rail badge counter counts gold (earning gold no longer made a
badge vanish from the rail); first-artifact-wins is now asserted against
the persisted snapshot instead of vacuously.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
51c2e15e15
commit
d8807c139e
@@ -426,11 +426,19 @@ def _passports_view():
|
|||||||
badge = "shown_not_judged"
|
badge = "shown_not_judged"
|
||||||
elif qualifying >= req["songs"] and len(cleared) == len(required):
|
elif qualifying >= req["songs"] and len(cleared) == len(required):
|
||||||
# Bronze is earned; GOLD upgrades it when a verified improv
|
# Bronze is earned; GOLD upgrades it when a verified improv
|
||||||
# artifact exists for this genre's jam style (exact genre key
|
# artifact exists for this genre's jam style. Virtuoso mints
|
||||||
# or its family — jam styleIds are the family keys). Bronze
|
# under raw STYLE_PALETTES ids ('punk', 'djent', 'disco', ...),
|
||||||
# remains a standalone win; gold never becomes an obligation.
|
# which are mostly NOT family keys — so match in family space:
|
||||||
style = gkey if gkey in gold_improv else _genre_family(gkey)
|
# the same keyword bucketing genres get ('punk' and 'punk
|
||||||
badge = "gold" if style and style in gold_improv else "earned"
|
# rock' both bucket to 'rock'), with the exact key as a direct
|
||||||
|
# hit. Bronze remains a standalone win; gold never becomes an
|
||||||
|
# obligation.
|
||||||
|
fam = _genre_family(gkey)
|
||||||
|
gold = any(
|
||||||
|
s == gkey or (fam is not None and _genre_family(s) == fam)
|
||||||
|
for s in gold_improv
|
||||||
|
)
|
||||||
|
badge = "gold" if gold else "earned"
|
||||||
else:
|
else:
|
||||||
badge = "in_progress"
|
badge = "in_progress"
|
||||||
# Practice invitation: the non-qualifying songs closest to the
|
# Practice invitation: the non-qualifying songs closest to the
|
||||||
@@ -703,8 +711,18 @@ def setup(app, context):
|
|||||||
# drops junk entries, which must not become a size-guard bypass.
|
# drops junk entries, which must not become a size-guard bypass.
|
||||||
if len(json.dumps(body["byNode"])) > DRILL_SNAPSHOT_MAX_BYTES:
|
if len(json.dumps(body["byNode"])) > DRILL_SNAPSHOT_MAX_BYTES:
|
||||||
raise HTTPException(413, "Snapshot too large.")
|
raise HTTPException(413, "Snapshot too large.")
|
||||||
gold_in = body.get("goldImprov")
|
gold_in = body.get("goldImprov", {})
|
||||||
gold_in = gold_in if isinstance(gold_in, dict) else {}
|
if not isinstance(gold_in, dict):
|
||||||
|
# A relay bug must be LOUD, not a silent 200 that drops gold.
|
||||||
|
raise HTTPException(400, "goldImprov must be an object keyed by style id.")
|
||||||
|
# Keep only plausible artifacts: a dict that names its verifier —
|
||||||
|
# an empty {} must not mint an evidence-free gold.
|
||||||
|
gold_in = {k: v for k, v in gold_in.items()
|
||||||
|
if isinstance(v, dict) and v.get("verifier")}
|
||||||
|
# Same pre-merge bound byNode gets: the gained-only merge dropping
|
||||||
|
# junk must not become a size-guard bypass (nor lock-held CPU burn).
|
||||||
|
if len(json.dumps(gold_in)) > DRILL_SNAPSHOT_MAX_BYTES:
|
||||||
|
raise HTTPException(413, "Snapshot too large.")
|
||||||
with _lock:
|
with _lock:
|
||||||
_, existing, existing_gold = _drill_by_node()
|
_, existing, existing_gold = _drill_by_node()
|
||||||
snapshot = {"mode": body.get("mode"), "xp": body.get("xp"),
|
snapshot = {"mode": body.get("mode"), "xp": body.get("xp"),
|
||||||
|
|||||||
@@ -611,7 +611,7 @@
|
|||||||
const data = (_pp.instruments || {})[inst] || { passports: [] };
|
const data = (_pp.instruments || {})[inst] || { passports: [] };
|
||||||
host.innerHTML = ((_pp.config || {}).instruments || []).map((i) => {
|
host.innerHTML = ((_pp.config || {}).instruments || []).map((i) => {
|
||||||
const d = (_pp.instruments || {})[i] || {};
|
const d = (_pp.instruments || {})[i] || {};
|
||||||
const earned = (d.passports || []).filter((p) => p.badge === 'earned').length;
|
const earned = (d.passports || []).filter((p) => p.badge === 'earned' || p.badge === 'gold').length;
|
||||||
const committed = !!d.committed_at;
|
const committed = !!d.committed_at;
|
||||||
return `<button class="pp-inst${i === inst ? ' active' : ''}${committed ? '' : ' uncommitted'}" data-pp-inst="${esc(i)}">
|
return `<button class="pp-inst${i === inst ? ' active' : ''}${committed ? '' : ' uncommitted'}" data-pp-inst="${esc(i)}">
|
||||||
${esc(ppLabel(i))}${earned ? ` <span class="pp-inst-badges">⚡${earned}</span>` : ''}${committed ? '' : ' <span class="pp-inst-plus">+</span>'}
|
${esc(ppLabel(i))}${earned ? ` <span class="pp-inst-badges">⚡${earned}</span>` : ''}${committed ? '' : ' <span class="pp-inst-plus">+</span>'}
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ def test_gold_without_bronze_stays_in_progress(client, meta_db):
|
|||||||
meta_db.add("one.feedpak", 0, 0.9, genre="Soul", arrangements=LEAD)
|
meta_db.add("one.feedpak", 0, 0.9, genre="Soul", arrangements=LEAD)
|
||||||
_open(client, "guitar", "Soul")
|
_open(client, "guitar", "Soul")
|
||||||
client.post("/api/plugins/career/drill-state", json={
|
client.post("/api/plugins/career/drill-state", json={
|
||||||
"byNode": {}, "goldImprov": {"soul": {"at": 1}}})
|
"byNode": {}, "goldImprov": {"soul": {"at": 1, "verifier": "comb"}}})
|
||||||
assert _passport(client, "guitar", "soul")["badge"] == "in_progress"
|
assert _passport(client, "guitar", "soul")["badge"] == "in_progress"
|
||||||
|
|
||||||
|
|
||||||
@@ -407,8 +407,41 @@ def test_gold_merge_is_gained_only(client, meta_db):
|
|||||||
# A stale relay without the artifact never un-mints.
|
# A stale relay without the artifact never un-mints.
|
||||||
client.post("/api/plugins/career/drill-state", json={"byNode": {}})
|
client.post("/api/plugins/career/drill-state", json={"byNode": {}})
|
||||||
assert _passport(client, "guitar", "soul")["badge"] == "gold"
|
assert _passport(client, "guitar", "soul")["badge"] == "gold"
|
||||||
# And a different artifact for the same style never overwrites the first.
|
# And a different artifact for the same style never overwrites the first —
|
||||||
|
# asserted against the PERSISTED snapshot (the view doesn't expose
|
||||||
|
# artifact contents), so a last-write-wins regression can't stay green.
|
||||||
client.post("/api/plugins/career/drill-state", json={
|
client.post("/api/plugins/career/drill-state", json={
|
||||||
"byNode": {}, "goldImprov": {"soul": {"at": 999, "verifier": "yin"}}})
|
"byNode": {}, "goldImprov": {"soul": {"at": 999, "verifier": "yin"}}})
|
||||||
view = client.get("/api/plugins/career/passports").json()
|
_, _, gold = career_routes._drill_by_node()
|
||||||
# (first-artifact-wins is asserted through the intake merge)
|
assert gold["soul"] == {"at": 1, "verifier": "comb"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_gold_matches_raw_style_id_through_family(client, meta_db):
|
||||||
|
# Virtuoso mints under raw STYLE_PALETTES ids ('punk', not 'rock'): a
|
||||||
|
# 'punk rock' passport (family rock) must go gold from a 'punk' artifact.
|
||||||
|
for i in range(5):
|
||||||
|
meta_db.add(f"pk{i}.feedpak", 0, 0.9, genre="Punk Rock", arrangements=LEAD)
|
||||||
|
career_routes._state["passports_content"]["genres"]["rock"] = {} # no drill gate
|
||||||
|
_open(client, "guitar", "Punk Rock")
|
||||||
|
client.post("/api/plugins/career/drill-state", json={
|
||||||
|
"byNode": {}, "goldImprov": {"punk": {"at": 1, "verifier": "comb"}}})
|
||||||
|
assert _passport(client, "guitar", "punk rock")["badge"] == "gold"
|
||||||
|
|
||||||
|
|
||||||
|
def test_gold_intake_rejects_junk(client, meta_db):
|
||||||
|
# A non-dict goldImprov is a relay bug: loud 400, never a silent drop.
|
||||||
|
res = client.post("/api/plugins/career/drill-state",
|
||||||
|
json={"byNode": {}, "goldImprov": ["metal"]})
|
||||||
|
assert res.status_code == 400
|
||||||
|
# Evidence-free artifacts (no verifier) never mint.
|
||||||
|
for i in range(5):
|
||||||
|
meta_db.add(f"j{i}.feedpak", 0, 0.9, genre="Soul", arrangements=LEAD)
|
||||||
|
_open(client, "guitar", "Soul")
|
||||||
|
client.post("/api/plugins/career/drill-state", json={
|
||||||
|
"byNode": {}, "goldImprov": {"soul": {}}})
|
||||||
|
assert _passport(client, "guitar", "soul")["badge"] == "earned"
|
||||||
|
# An oversized goldImprov is bounded BEFORE the merge, like byNode.
|
||||||
|
blob = {f"s{i}": {"verifier": "comb", "pad": "x" * 4096} for i in range(200)}
|
||||||
|
res = client.post("/api/plugins/career/drill-state",
|
||||||
|
json={"byNode": {}, "goldImprov": blob})
|
||||||
|
assert res.status_code == 413
|
||||||
|
|||||||
Reference in New Issue
Block a user