diff --git a/scripts/sold-staging-up.py b/scripts/sold-staging-up.py
index 09ad314..d5d3f71 100755
--- a/scripts/sold-staging-up.py
+++ b/scripts/sold-staging-up.py
@@ -127,6 +127,9 @@ BLAZE_ASSETS = ("redir_cert.pem", "redir_key.pem")
CONTENT_SRC = "/home/alex/openfut-post-p1/staging/emit/content"
CARDS_NAME = "fifa17-production-cards.json"
CATALOG_NAME = "fifa17-production-catalog.json"
+# The FIFA17 content_kind mapping Core needs to correct a club imported before
+# the taxonomy existed. Emitted alongside the catalog by openfut-import-fifa17.
+RECLASSIFY_NAME = "fifa17-reclassify.json"
# The operator's REAL club (the 1986-item CAGE import), as staged by
# scripts/club-snapshot.py. That snapshot lives OUTSIDE the production state
@@ -1100,6 +1103,49 @@ def migrate_core(lay: Layout) -> None:
proc.wait(timeout=20)
+def reclassify_core(lay: Layout) -> None:
+ """Correct `content_kind` on the club restored from the snapshot.
+
+ The snapshot was imported before the content taxonomy existed, so its
+ coaches, kits and consumables are all recorded as players — and a profile
+ import is once-only, so re-importing cannot fix it. Core takes the FIFA17
+ mapping and applies it in one idempotent transaction. Without this, every
+ fresh bring-up would silently restore the wrong ownership truth.
+ """
+ request = os.path.join(CONTENT_SRC, RECLASSIFY_NAME)
+ if not os.path.exists(request):
+ raise Fatal(
+ f"missing {request}; regenerate it with "
+ "`openfut-import-fifa17 --emit-content
`"
+ )
+ # A binary predating the subcommand ignores it and boots the SERVER instead,
+ # which would block the bring-up forever rather than fail.
+ try:
+ out = subprocess.run(
+ [lay.core_bin, "reclassify", request],
+ env=core_env(lay), cwd=lay.root, capture_output=True, text=True,
+ timeout=120,
+ )
+ except subprocess.TimeoutExpired:
+ raise Fatal(
+ f"{lay.core_bin} did not answer `reclassify` — a binary predating the "
+ "subcommand ignores it and starts the server instead. Rebuild Core."
+ ) from None
+ if out.returncode != 0:
+ raise Fatal(f"core reclassify failed: {out.stderr.strip()[-400:]}")
+ result = json.loads(out.stdout)
+ if result["unmatched_definitions"]:
+ raise Fatal(
+ "reclassify named definitions this club owns no copy of: "
+ f"{result['unmatched_definitions'][:5]}"
+ )
+ ok(
+ f"reclassified owned content: {result['updated']} row(s) corrected, "
+ f"{result['unchanged']} already right (Core is the ownership authority, "
+ "so a coach must not be recorded as a player)"
+ )
+
+
def start_core(lay: Layout) -> Launched:
proc = spawn([lay.core_bin], core_env(lay), lay.root, lay.core_log, append=True)
wait_http(CORE_PORT, "/health", proc, lay.core_log, "staging Core")
@@ -1256,6 +1302,25 @@ def verify(lay: Layout, variant: str, real_club_expected: bool) -> None:
"squad changed without re-stamping its extension fingerprint"
)
+ # Core is the ownership authority. If a snapshot restore silently reverted
+ # every non-player row to `player`, the wire would still look plausible
+ # (the adapter classifies from its catalog) while Core's truth was wrong.
+ if real_club_expected:
+ kinds = {}
+ for kind in ("player", "manager", "staff", "consumable", "kit"):
+ status, body = http_get(
+ CORE_PORT, f"/collection?content_kind={kind}&limit=1"
+ )
+ kinds[kind] = json.loads(body)["total"] if status == 200 else -1
+ ok(f"CORE ownership truth by content_kind: {kinds}")
+ missing = [k for k, v in kinds.items() if v <= 0]
+ if missing:
+ raise Fatal(
+ f"Core owns nothing of kind {missing}; the real club holds "
+ "players, a manager, coaches, consumables and kits. A zero here "
+ "means the reclassify step did not run or did not stick"
+ )
+
# Nothing this process ever opened may live under the production state dir.
leaked = []
for fd in os.listdir(f"/proc/{os.getpid()}/fd"):
@@ -1432,6 +1497,8 @@ def main() -> int:
raise Fatal(f"{lay.core_db} should have been installed from the snapshot")
migrate_core(lay)
seed_core_db(lay, real_club)
+ if real_club is not None:
+ reclassify_core(lay)
started.append(start_core(lay))
banner("STAGING UTAS-HOST")