Spaces:
Running
Hard-tier curation: action/perception/reasoning/strict/rush-hour + exhaustive classification
Browse filesSeed-driven spawn variation (+ fail_condition where an adversary
exists) applied to every remaining applicable pack:
- action-multiunit-coordination, action-sequenced-execution: 2 spawn
groups (same fixed regions/waypoints), + force-loss fail
- perception-frontier-reading, perception-target-vs-fog: scouts staged
from a seed-chosen corner so the fog-reading order flips by seed, + fail
- reasoning-frontier-commit: single scout, seed-chosen corner → the
commitment decision must generalise, + fail
- strict-sequence, custom-map-no-enemy: 2 spawn groups (no-adversary →
no force-loss fail, documented in _NO_ENEMY)
- rush-hour: already round-robins base spawn 0/1 by seed; added the
missing force-loss fail_condition
Consciously N/A (documented w/ reasons in NOT_APPLICABLE): economy-*/
building-and-planning/strict-production-bom (non-spatial),
reasoning-risk-route (tuned single seam), strategy-* (win-predicate
redesign needed, separate rigor item).
test_hard_tier.py: 15 packs in UPGRADED, live seed→distinct-start
integ check per pack, fail_condition check, + exhaustive
classification test (every active pack is UPGRADED or N/A). Full
suite 305 passed, 1 skipped.
- openra_bench/scenarios/packs/action-multiunit-coordination.yaml +13 -4
- openra_bench/scenarios/packs/action-sequenced-execution.yaml +13 -4
- openra_bench/scenarios/packs/custom-map-no-enemy.yaml +10 -1
- openra_bench/scenarios/packs/perception-frontier-reading.yaml +9 -2
- openra_bench/scenarios/packs/perception-target-vs-fog.yaml +9 -2
- openra_bench/scenarios/packs/reasoning-frontier-commit.yaml +6 -1
- openra_bench/scenarios/packs/rush-hour.yaml +7 -1
- openra_bench/scenarios/packs/strict-sequence.yaml +20 -0
- tests/test_hard_tier.py +54 -1
|
@@ -88,10 +88,17 @@ levels:
|
|
| 88 |
genuine parallel multi-group control can satisfy all clauses.
|
| 89 |
overrides:
|
| 90 |
actors:
|
| 91 |
-
|
| 92 |
-
- {type:
|
| 93 |
-
- {type:
|
| 94 |
-
- {type:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
- {type: fact, owner: enemy, position: [112, 5]}
|
| 96 |
- {type: proc, owner: enemy, position: [112, 35]}
|
| 97 |
- {type: powr, owner: enemy, position: [115, 20]}
|
|
@@ -107,4 +114,6 @@ levels:
|
|
| 107 |
- {reach_region: {x: 115, y: 20, radius: 7}}
|
| 108 |
- {within_ticks: 4200}
|
| 109 |
- {units_lost_lte: 2}
|
|
|
|
|
|
|
| 110 |
max_turns: 44
|
|
|
|
| 88 |
genuine parallel multi-group control can satisfy all clauses.
|
| 89 |
overrides:
|
| 90 |
actors:
|
| 91 |
+
# spawn_point 0 — full force, northern staging
|
| 92 |
+
- {type: 2tnk, owner: agent, position: [5, 6], count: 3, spawn_point: 0}
|
| 93 |
+
- {type: 1tnk, owner: agent, position: [7, 10], count: 3, spawn_point: 0}
|
| 94 |
+
- {type: jeep, owner: agent, position: [5, 14], count: 2, spawn_point: 0}
|
| 95 |
+
- {type: apc, owner: agent, position: [9, 18], count: 2, spawn_point: 0}
|
| 96 |
+
# spawn_point 1 — full force, southern staging (same 3 fixed
|
| 97 |
+
# regions to cover; seed picks the start cluster)
|
| 98 |
+
- {type: 2tnk, owner: agent, position: [5, 20], count: 3, spawn_point: 1}
|
| 99 |
+
- {type: 1tnk, owner: agent, position: [7, 24], count: 3, spawn_point: 1}
|
| 100 |
+
- {type: jeep, owner: agent, position: [5, 28], count: 2, spawn_point: 1}
|
| 101 |
+
- {type: apc, owner: agent, position: [9, 32], count: 2, spawn_point: 1}
|
| 102 |
- {type: fact, owner: enemy, position: [112, 5]}
|
| 103 |
- {type: proc, owner: enemy, position: [112, 35]}
|
| 104 |
- {type: powr, owner: enemy, position: [115, 20]}
|
|
|
|
| 114 |
- {reach_region: {x: 115, y: 20, radius: 7}}
|
| 115 |
- {within_ticks: 4200}
|
| 116 |
- {units_lost_lte: 2}
|
| 117 |
+
fail_condition:
|
| 118 |
+
not: {own_units_gte: 1}
|
| 119 |
max_turns: 44
|
|
@@ -87,10 +87,17 @@ levels:
|
|
| 87 |
leg overruns the clock or loses too many units.
|
| 88 |
overrides:
|
| 89 |
actors:
|
| 90 |
-
|
| 91 |
-
- {type:
|
| 92 |
-
- {type:
|
| 93 |
-
- {type:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
- {type: powr, owner: enemy, position: [45, 33]} # W1
|
| 95 |
- {type: proc, owner: enemy, position: [80, 6]} # W2
|
| 96 |
- {type: tsla, owner: enemy, position: [110, 30]} # W3
|
|
@@ -104,4 +111,6 @@ levels:
|
|
| 104 |
- {after_ticks: 3400}
|
| 105 |
- {within_ticks: 4400}
|
| 106 |
- {units_lost_lte: 1}
|
|
|
|
|
|
|
| 107 |
max_turns: 46
|
|
|
|
| 87 |
leg overruns the clock or loses too many units.
|
| 88 |
overrides:
|
| 89 |
actors:
|
| 90 |
+
# spawn_point 0 — northern staging
|
| 91 |
+
- {type: 2tnk, owner: agent, position: [5, 8], count: 2, spawn_point: 0}
|
| 92 |
+
- {type: 1tnk, owner: agent, position: [7, 12], count: 2, spawn_point: 0}
|
| 93 |
+
- {type: jeep, owner: agent, position: [5, 16], spawn_point: 0}
|
| 94 |
+
- {type: apc, owner: agent, position: [9, 20], spawn_point: 0}
|
| 95 |
+
# spawn_point 1 — southern staging (same serpentine to the same
|
| 96 |
+
# fixed waypoints/final; seed picks the start)
|
| 97 |
+
- {type: 2tnk, owner: agent, position: [5, 22], count: 2, spawn_point: 1}
|
| 98 |
+
- {type: 1tnk, owner: agent, position: [7, 26], count: 2, spawn_point: 1}
|
| 99 |
+
- {type: jeep, owner: agent, position: [5, 30], spawn_point: 1}
|
| 100 |
+
- {type: apc, owner: agent, position: [9, 34], spawn_point: 1}
|
| 101 |
- {type: powr, owner: enemy, position: [45, 33]} # W1
|
| 102 |
- {type: proc, owner: enemy, position: [80, 6]} # W2
|
| 103 |
- {type: tsla, owner: enemy, position: [110, 30]} # W3
|
|
|
|
| 111 |
- {after_ticks: 3400}
|
| 112 |
- {within_ticks: 4400}
|
| 113 |
- {units_lost_lte: 1}
|
| 114 |
+
fail_condition:
|
| 115 |
+
not: {own_units_gte: 1}
|
| 116 |
max_turns: 46
|
|
@@ -63,7 +63,16 @@ levels:
|
|
| 63 |
hard:
|
| 64 |
description: >
|
| 65 |
Whole squad must reach the far corner of the playable bounds
|
| 66 |
-
together within a short deadline
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
win_condition:
|
| 68 |
all_of:
|
| 69 |
- all_units_in_region: {x: 70, y: 24, radius: 6}
|
|
|
|
| 63 |
hard:
|
| 64 |
description: >
|
| 65 |
Whole squad must reach the far corner of the playable bounds
|
| 66 |
+
together within a short deadline, starting from a seed-chosen
|
| 67 |
+
position (two spawn_point groups) so the route can't be replayed
|
| 68 |
+
from memory. (No adversary on this map → no force-loss fail; the
|
| 69 |
+
only failure mode is missing the deadline.)
|
| 70 |
+
overrides:
|
| 71 |
+
actors:
|
| 72 |
+
- {type: jeep, owner: agent, position: [8, 12], spawn_point: 0}
|
| 73 |
+
- {type: e1, owner: agent, position: [9, 14], count: 2, spawn_point: 0}
|
| 74 |
+
- {type: jeep, owner: agent, position: [8, 28], spawn_point: 1}
|
| 75 |
+
- {type: e1, owner: agent, position: [9, 26], count: 2, spawn_point: 1}
|
| 76 |
win_condition:
|
| 77 |
all_of:
|
| 78 |
- all_units_in_region: {x: 70, y: 24, radius: 6}
|
|
@@ -83,8 +83,13 @@ levels:
|
|
| 83 |
reading of all three fog pockets clears the coverage bar in time.
|
| 84 |
overrides:
|
| 85 |
actors:
|
| 86 |
-
|
| 87 |
-
- {type: jeep, owner: agent, position: [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
# Center decoys (already-explored zone) — combat bait.
|
| 89 |
- {type: e1, owner: enemy, position: [55, 20], stance: 2, count: 3}
|
| 90 |
- {type: e3, owner: enemy, position: [58, 22], stance: 2}
|
|
@@ -98,4 +103,6 @@ levels:
|
|
| 98 |
- {explored_pct_gte: 62}
|
| 99 |
- {within_ticks: 4800}
|
| 100 |
- {units_lost_lte: 1}
|
|
|
|
|
|
|
| 101 |
max_turns: 40
|
|
|
|
| 83 |
reading of all three fog pockets clears the coverage bar in time.
|
| 84 |
overrides:
|
| 85 |
actors:
|
| 86 |
+
# spawn_point 0 — scouts staged SW
|
| 87 |
+
- {type: jeep, owner: agent, position: [5, 33], count: 3, spawn_point: 0}
|
| 88 |
+
- {type: jeep, owner: agent, position: [7, 30], count: 2, spawn_point: 0}
|
| 89 |
+
# spawn_point 1 — scouts staged NW (the three fog pockets are
|
| 90 |
+
# fixed, so the reading order that works flips by seed)
|
| 91 |
+
- {type: jeep, owner: agent, position: [5, 6], count: 3, spawn_point: 1}
|
| 92 |
+
- {type: jeep, owner: agent, position: [7, 9], count: 2, spawn_point: 1}
|
| 93 |
# Center decoys (already-explored zone) — combat bait.
|
| 94 |
- {type: e1, owner: enemy, position: [55, 20], stance: 2, count: 3}
|
| 95 |
- {type: e3, owner: enemy, position: [58, 22], stance: 2}
|
|
|
|
| 103 |
- {explored_pct_gte: 62}
|
| 104 |
- {within_ticks: 4800}
|
| 105 |
- {units_lost_lte: 1}
|
| 106 |
+
fail_condition:
|
| 107 |
+
not: {own_units_gte: 1}
|
| 108 |
max_turns: 40
|
|
@@ -91,8 +91,13 @@ levels:
|
|
| 91 |
— after the near ones are ruled out — discovers it in time.
|
| 92 |
overrides:
|
| 93 |
actors:
|
| 94 |
-
|
| 95 |
-
- {type:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
# Decoy A — near NE, loud enemy squad, no target building.
|
| 97 |
- {type: e1, owner: enemy, position: [60, 6], stance: 2, count: 3}
|
| 98 |
- {type: dog, owner: enemy, position: [64, 9], stance: 2, count: 2}
|
|
@@ -108,4 +113,6 @@ levels:
|
|
| 108 |
- {buildings_discovered_gte: 1}
|
| 109 |
- {within_ticks: 4800}
|
| 110 |
- {units_lost_lte: 1}
|
|
|
|
|
|
|
| 111 |
max_turns: 40
|
|
|
|
| 91 |
— after the near ones are ruled out — discovers it in time.
|
| 92 |
overrides:
|
| 93 |
actors:
|
| 94 |
+
# spawn_point 0 — staged NW
|
| 95 |
+
- {type: jeep, owner: agent, position: [6, 5], count: 3, spawn_point: 0}
|
| 96 |
+
- {type: 1tnk, owner: agent, position: [8, 8], count: 2, spawn_point: 0}
|
| 97 |
+
# spawn_point 1 — staged SW (target/decoys fixed, so which
|
| 98 |
+
# pocket is "counter-intuitive" flips by seed)
|
| 99 |
+
- {type: jeep, owner: agent, position: [6, 33], count: 3, spawn_point: 1}
|
| 100 |
+
- {type: 1tnk, owner: agent, position: [8, 30], count: 2, spawn_point: 1}
|
| 101 |
# Decoy A — near NE, loud enemy squad, no target building.
|
| 102 |
- {type: e1, owner: enemy, position: [60, 6], stance: 2, count: 3}
|
| 103 |
- {type: dog, owner: enemy, position: [64, 9], stance: 2, count: 2}
|
|
|
|
| 113 |
- {buildings_discovered_gte: 1}
|
| 114 |
- {within_ticks: 4800}
|
| 115 |
- {units_lost_lte: 1}
|
| 116 |
+
fail_condition:
|
| 117 |
+
not: {own_units_gte: 1}
|
| 118 |
max_turns: 40
|
|
@@ -94,7 +94,10 @@ levels:
|
|
| 94 |
time without splitting (only one unit).
|
| 95 |
overrides:
|
| 96 |
actors:
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
| 98 |
# Decoy A (SE) — hostile cluster; a route through it risks the
|
| 99 |
# lone scout (units_lost_lte: 0 constraint below).
|
| 100 |
- {type: e3, owner: enemy, position: [110, 34], stance: 2, count: 3}
|
|
@@ -108,4 +111,6 @@ levels:
|
|
| 108 |
- {buildings_discovered_gte: 1}
|
| 109 |
- {within_ticks: 4200}
|
| 110 |
- {units_lost_lte: 0}
|
|
|
|
|
|
|
| 111 |
max_turns: 38
|
|
|
|
| 94 |
time without splitting (only one unit).
|
| 95 |
overrides:
|
| 96 |
actors:
|
| 97 |
+
# Single scout, seed-chosen corner — the commitment decision
|
| 98 |
+
# must generalise across start positions, not be memorised.
|
| 99 |
+
- {type: jeep, owner: agent, position: [6, 6], count: 1, spawn_point: 0}
|
| 100 |
+
- {type: jeep, owner: agent, position: [6, 33], count: 1, spawn_point: 1}
|
| 101 |
# Decoy A (SE) — hostile cluster; a route through it risks the
|
| 102 |
# lone scout (units_lost_lte: 0 constraint below).
|
| 103 |
- {type: e3, owner: enemy, position: [110, 34], stance: 2, count: 3}
|
|
|
|
| 111 |
- {buildings_discovered_gte: 1}
|
| 112 |
- {within_ticks: 4200}
|
| 113 |
- {units_lost_lte: 0}
|
| 114 |
+
fail_condition:
|
| 115 |
+
not: {own_units_gte: 1}
|
| 116 |
max_turns: 38
|
|
@@ -523,9 +523,15 @@ levels:
|
|
| 523 |
- units_killed_gte: 6
|
| 524 |
- within_ticks: 13000
|
| 525 |
hard:
|
| 526 |
-
description:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
win_condition:
|
| 528 |
all_of:
|
| 529 |
- units_killed_gte: 9
|
| 530 |
- within_ticks: 10000
|
| 531 |
- units_lost_lte: 3
|
|
|
|
|
|
|
|
|
| 523 |
- units_killed_gte: 6
|
| 524 |
- within_ticks: 13000
|
| 525 |
hard:
|
| 526 |
+
description: >
|
| 527 |
+
Rush Hour Sweep \u2014 hard. The agent force already round-robins
|
| 528 |
+
between two base spawn_point groups by seed (start corner varies,
|
| 529 |
+
anti-memorisation); hard adds a tight clock, an attrition cap,
|
| 530 |
+
and a real loss on force destruction.
|
| 531 |
win_condition:
|
| 532 |
all_of:
|
| 533 |
- units_killed_gte: 9
|
| 534 |
- within_ticks: 10000
|
| 535 |
- units_lost_lte: 3
|
| 536 |
+
fail_condition:
|
| 537 |
+
not: {own_units_gte: 1}
|
|
@@ -58,6 +58,26 @@ levels:
|
|
| 58 |
description: >
|
| 59 |
Tight ordered window: reach the region within a narrow tick band
|
| 60 |
(not before 4000, not after 9000) — precise timing compliance.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
win_condition:
|
| 62 |
all_of:
|
| 63 |
- reach_region: {x: 26, y: 30, radius: 3}
|
|
|
|
| 58 |
description: >
|
| 59 |
Tight ordered window: reach the region within a narrow tick band
|
| 60 |
(not before 4000, not after 9000) — precise timing compliance.
|
| 61 |
+
The unit stages from a seed-chosen position (two spawn_point
|
| 62 |
+
groups) so the exact timed move can't be replayed from memory.
|
| 63 |
+
overrides:
|
| 64 |
+
actors:
|
| 65 |
+
- type: jeep
|
| 66 |
+
owner: agent
|
| 67 |
+
position: [6, 8]
|
| 68 |
+
stance: 0
|
| 69 |
+
health: 100
|
| 70 |
+
facing: -1
|
| 71 |
+
count: 1
|
| 72 |
+
spawn_point: 0
|
| 73 |
+
- type: jeep
|
| 74 |
+
owner: agent
|
| 75 |
+
position: [6, 30]
|
| 76 |
+
stance: 0
|
| 77 |
+
health: 100
|
| 78 |
+
facing: -1
|
| 79 |
+
count: 1
|
| 80 |
+
spawn_point: 1
|
| 81 |
win_condition:
|
| 82 |
all_of:
|
| 83 |
- reach_region: {x: 26, y: 30, radius: 3}
|
|
@@ -31,8 +31,37 @@ UPGRADED = [
|
|
| 31 |
"artofwar-indirect-approach",
|
| 32 |
"artofwar-lure-the-tiger",
|
| 33 |
"artofwar-sequenced-citadel",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
]
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
def _agent_spawn_points(pack_id: str, level: str) -> set:
|
| 38 |
c = compile_level(load_pack(PACKS / f"{pack_id}.yaml"), level)
|
|
@@ -89,7 +118,31 @@ def test_curated_hard_still_compiles_and_runs(pid):
|
|
| 89 |
|
| 90 |
def test_fail_condition_present_on_curated_hard():
|
| 91 |
# Curated hard tiers must be able to emit a loss (no loss==draw
|
| 92 |
-
# degeneracy
|
|
|
|
| 93 |
for pid in UPGRADED:
|
| 94 |
c = compile_level(load_pack(PACKS / f"{pid}.yaml"), "hard")
|
|
|
|
|
|
|
| 95 |
assert c.fail_condition is not None, f"{pid}:hard needs a fail_condition"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
"artofwar-indirect-approach",
|
| 32 |
"artofwar-lure-the-tiger",
|
| 33 |
"artofwar-sequenced-citadel",
|
| 34 |
+
"action-multiunit-coordination",
|
| 35 |
+
"action-sequenced-execution",
|
| 36 |
+
"strict-sequence",
|
| 37 |
+
"perception-frontier-reading",
|
| 38 |
+
"perception-target-vs-fog",
|
| 39 |
+
"reasoning-frontier-commit",
|
| 40 |
+
"rush-hour",
|
| 41 |
+
"custom-map-no-enemy",
|
| 42 |
]
|
| 43 |
|
| 44 |
+
# Consciously NOT spawn-varied, with the reason (keeps the curation
|
| 45 |
+
# exhaustive — every active pack is classified, see the coverage test).
|
| 46 |
+
NOT_APPLICABLE = {
|
| 47 |
+
"economy-investment": "non-spatial: capital allocation, start pos irrelevant",
|
| 48 |
+
"economy-time-box": "non-spatial: budget-under-clock",
|
| 49 |
+
"economy-force-buildup": "non-spatial: production economy",
|
| 50 |
+
"building-and-planning": "non-spatial: build-order/tech, fixed base",
|
| 51 |
+
"strict-production-bom": "non-spatial: exact bill-of-materials spec",
|
| 52 |
+
"reasoning-risk-route": "rigor 5/5 from one tuned safe seam — varying "
|
| 53 |
+
"the start would break the single-solution tuning / seed parity",
|
| 54 |
+
"strategy-dilemma": "needs win-predicate redesign (gameable "
|
| 55 |
+
"buildings_discovered_gte:1), not spawn variation — separate rigor item",
|
| 56 |
+
"strategy-gauntlet": "dual-entry design + gameable win — separate item",
|
| 57 |
+
"strategy-twobody": "two simultaneously-controlled groups IS the task; "
|
| 58 |
+
"spawn-alternatives would break intent — separate item",
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
# No-adversary maps: spawn variation applies but a force-loss
|
| 62 |
+
# fail_condition is impossible (nothing can destroy the force).
|
| 63 |
+
_NO_ENEMY = {"strict-sequence", "custom-map-no-enemy"}
|
| 64 |
+
|
| 65 |
|
| 66 |
def _agent_spawn_points(pack_id: str, level: str) -> set:
|
| 67 |
c = compile_level(load_pack(PACKS / f"{pack_id}.yaml"), level)
|
|
|
|
| 118 |
|
| 119 |
def test_fail_condition_present_on_curated_hard():
|
| 120 |
# Curated hard tiers must be able to emit a loss (no loss==draw
|
| 121 |
+
# degeneracy) — except no-adversary maps where force-loss is
|
| 122 |
+
# impossible by construction (documented in _NO_ENEMY).
|
| 123 |
for pid in UPGRADED:
|
| 124 |
c = compile_level(load_pack(PACKS / f"{pid}.yaml"), "hard")
|
| 125 |
+
if pid in _NO_ENEMY:
|
| 126 |
+
continue
|
| 127 |
assert c.fail_condition is not None, f"{pid}:hard needs a fail_condition"
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def test_every_active_pack_is_classified():
|
| 131 |
+
"""Curation is exhaustive: every active pack is either spawn-varied
|
| 132 |
+
(UPGRADED) or consciously NOT_APPLICABLE with a stated reason — no
|
| 133 |
+
pack silently skipped."""
|
| 134 |
+
import glob
|
| 135 |
+
import os
|
| 136 |
+
|
| 137 |
+
classified = set(UPGRADED) | set(NOT_APPLICABLE)
|
| 138 |
+
missing = []
|
| 139 |
+
for f in glob.glob(str(PACKS / "*.yaml")):
|
| 140 |
+
b = os.path.basename(f)
|
| 141 |
+
if b.startswith(("_", "TEMPLATE")):
|
| 142 |
+
continue
|
| 143 |
+
m = load_pack(f).meta
|
| 144 |
+
if m.status != "active":
|
| 145 |
+
continue
|
| 146 |
+
if m.id not in classified:
|
| 147 |
+
missing.append(m.id)
|
| 148 |
+
assert not missing, f"unclassified active packs (curate or mark N/A): {missing}"
|