filament_id v4.2a: dissolve the QD_* island in the tooling (plan v4 SS3)

- is_island_declaration: only BBL remains an island; Qidi QD_* declarations
  now enter the triple bookkeeping (checks 3 and 8, --remint's domain).
- reserved_space_owner: QD_* stays reserved but ownerless -- no vendor may
  declare it, --update-snapshot refuses new QD_* ids outright, and vanished
  QD_* ids take the retired-with-successor path instead of the island
  release-with-hint path. New reserved_space_desc() names the space in
  check 6 / sanction-gate messages.
- Check 1 drops the vendor-Qidi QD_* format exemption; --add-hint now
  refuses QD_* keys (island space is GF* only); docstrings updated.
- Snapshot regenerated: the 204 QD_* declarer triples are now recorded
  (declared triples 1113 -> 1317); ids/claims unchanged.
- Tests updated for the flip; new (skipped until v4.2b) lockstep test
  asserting the QidiPrinterAgent fallback QD_* literals stay retired
  ledger keys chaining to live ids.

Tree state is untouched: presets still declare their QD_* ids, all
grandfathered by the snapshot. --check exit 0; 117 script tests OK.
This commit is contained in:
SoftFever
2026-08-21 18:34:20 +08:00
parent 1c405f8a2c
commit 8d83414d09
3 changed files with 1488 additions and 21 deletions

View File

@@ -26,7 +26,11 @@ Policy (companion to assign_vendor_setting_ids.py; see filament_id_plan_v3.md):
resolving. resolving.
* Reserved id spaces that are never minted into or altered: * Reserved id spaces that are never minted into or altered:
- GF* Bambu AMS/RFID catalog (vendor BBL untouchable) - GF* Bambu AMS/RFID catalog (vendor BBL untouchable)
- QD_* Qidi device protocol - QD_* Qidi device protocol (dissolved island, plan v4:
every shipped QD_* id is retired in the ledger and
the QidiPrinterAgent translates device-composed
QD_* ids through the succession walk; no preset
may ever declare one again)
- P + 7 hex chars (case-insensitive) and the literal "null" - P + 7 hex chars (case-insensitive) and the literal "null"
user-custom presets (CreatePresetsDialog.cpp) user-custom presets (CreatePresetsDialog.cpp)
- every already-shipped id, grandfathered via scripts/filament_id_snapshot.json - every already-shipped id, grandfathered via scripts/filament_id_snapshot.json
@@ -40,7 +44,7 @@ Policy (companion to assign_vendor_setting_ids.py; see filament_id_plan_v3.md):
rule over the vanished id's old claims — and the runtime follows those rule over the vanished id's old claims — and the runtime follows those
chains (plus cross-island "hints" for ids Orca cannot retire, e.g. GF*) chains (plus cross-island "hints" for ids Orca cannot retire, e.g. GF*)
on resolution miss. A retired id may never be used again for anything. on resolution miss. A retired id may never be used again for anything.
Vanished ids in a FOREIGN island's space (GF*/QD_*) are released with a Vanished ids in a FOREIGN island's space (GF*) are released with a
hint instead of retired: the island's catalog owns them and may hint instead of retired: the island's catalog owns them and may
legitimately (re)ship them. legitimately (re)ship them.
@@ -325,8 +329,9 @@ def resolve_triple(name, filaments, ofl_filaments):
def is_island_declaration(vendor, fid): def is_island_declaration(vendor, fid):
"""Declarations frozen outside the OF mint domain: all of BBL, QD_* in Qidi.""" """Declarations frozen outside the OF mint domain: all of BBL (the QD_*
return vendor == "BBL" or (vendor == "Qidi" and fid.startswith("QD_")) island was dissolved by plan v4 — Qidi declarations mint like any other)."""
return vendor == "BBL"
def analyze_tree(profiles_dir): def analyze_tree(profiles_dir):
@@ -598,12 +603,21 @@ def reserved_space_owner(fid):
if fid.startswith("GF"): if fid.startswith("GF"):
return True, "BBL" return True, "BBL"
if fid.startswith("QD_"): if fid.startswith("QD_"):
return True, "Qidi" return True, None # dissolved Qidi device-protocol space: NO vendor may declare it
if USER_CUSTOM_ID_RE.match(fid) or fid == "null": if USER_CUSTOM_ID_RE.match(fid) or fid == "null":
return True, None # user-custom space: no system vendor may own it return True, None # user-custom space: no system vendor may own it
return False, None return False, None
def reserved_space_desc(fid, owner):
"""Human description of a reserved space for error messages."""
if owner:
return f"owned by {owner}"
if fid.startswith("QD_"):
return "Qidi device protocol; dissolved island — ids are retired, never declarable"
return "reserved for user-custom presets"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Checks (imported and called tree-wide by orca_extra_profile_check.py) # Checks (imported and called tree-wide by orca_extra_profile_check.py)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -613,8 +627,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
"""Validate filament_id state across every vendor. Returns the error count. """Validate filament_id state across every vendor. Returns the error count.
1. Format: every id occurring in the tree (declared or effective) must be in 1. Format: every id occurring in the tree (declared or effective) must be in
the snapshot, or match ^OF[0-9A-Za-z]{6}$, or belong to vendor BBL, or be the snapshot, or match ^OF[0-9A-Za-z]{6}$, or belong to vendor BBL.
a QD_* id within vendor Qidi.
2. Snapshot equality, both directions: the tree-derived id->families multimap 2. Snapshot equality, both directions: the tree-derived id->families multimap
AND the id->triples map of the declarers must equal the snapshot exactly AND the id->triples map of the declarers must equal the snapshot exactly
(the snapshot diff is the maintainer gate). (the snapshot diff is the maintainer gate).
@@ -625,8 +638,8 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
5. Alias hygiene: a vendor preset riding an OFL family must keep the OFL 5. Alias hygiene: a vendor preset riding an OFL family must keep the OFL
base name, claim printers via non-empty compatible_printers, and declare base name, claim printers via non-empty compatible_printers, and declare
no filament_id key of its own. no filament_id key of its own.
6. Reserved namespaces (GF*/QD_*/P-hex/"null") only for their owner vendors, 6. Reserved namespaces (GF* for BBL; QD_*/P-hex/"null" for nobody) must not
except claims grandfathered in the snapshot. be claimed by other vendors, except claims grandfathered in the snapshot.
7. Structure ratchet: (a) no NEW instantiated preset carries its own 7. Structure ratchet: (a) no NEW instantiated preset carries its own
filament_id key; (b) no NEW declared-vs-inherited id drift; (c) every filament_id key; (b) no NEW declared-vs-inherited id drift; (c) every
instantiated filament resolves an effective id (a hard load error in C++). instantiated filament resolves an effective id (a hard load error in C++).
@@ -664,8 +677,6 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
continue continue
if vendor == "BBL": if vendor == "BBL":
continue continue
if vendor == "Qidi" and fid.startswith("QD_"):
continue
print_error( print_error(
f'filament_id "{fid}" ({vendor}) is neither grandfathered in the ' f'filament_id "{fid}" ({vendor}) is neither grandfathered in the '
f'snapshot nor a minted "OF" id; new family ids must come from ' f'snapshot nor a minted "OF" id; new family ids must come from '
@@ -765,7 +776,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
continue continue
if claim in snap_ids.get(fid, []): if claim in snap_ids.get(fid, []):
continue # grandfathered continue # grandfathered
space = f"owned by {owner}" if owner else "reserved for user-custom presets" space = reserved_space_desc(fid, owner)
print_error( print_error(
f'filament_id "{fid}" of "{claim}" is in a reserved id space ' f'filament_id "{fid}" of "{claim}" is in a reserved id space '
f"({space}) and must not be claimed by system presets of other vendors") f"({space}) and must not be claimed by system presets of other vendors")
@@ -861,7 +872,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
if island is None: if island is None:
print_error( print_error(
f'hint key "{key}" ({RETIRED_REL}) is not in an island id space ' f'hint key "{key}" ({RETIRED_REL}) is not in an island id space '
f"(GF*/QD_*); non-island ids are retired with a successor instead") f"(GF*); non-island ids are retired with a successor instead")
errors += 1 errors += 1
continue continue
if key in retired: if key in retired:
@@ -902,7 +913,7 @@ def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
Vanished ids gain a succession-ledger entry whose successor is picked by Vanished ids gain a succession-ledger entry whose successor is picked by
the mode rule over their old claims (None when the family died heirless); the mode rule over their old claims (None when the family died heirless);
vanished ids in a foreign island's space (GF*/QD_*) are released with a vanished ids in a foreign island's space (GF*) are released with a
hint instead — the island catalog owns them, so they must stay mintable hint instead — the island catalog owns them, so they must stay mintable
there and must never be blocked by check 4. there and must never be blocked by check 4.
--forget-never-shipped lists ids to drop from lineage instead of retiring --forget-never-shipped lists ids to drop from lineage instead of retiring
@@ -955,7 +966,7 @@ def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
refusals.append((fid, f"{vendor}/(declared only)", owner)) refusals.append((fid, f"{vendor}/(declared only)", owner))
if refusals and not allow_shared_catalog: if refusals and not allow_shared_catalog:
for fid, claim, owner in refusals: for fid, claim, owner in refusals:
space = f"owned by {owner}" if owner else "reserved for user-custom presets" space = reserved_space_desc(fid, owner)
print_error( print_error(
f'refusing to sanction new claim "{claim}" on reserved-namespace id ' f'refusing to sanction new claim "{claim}" on reserved-namespace id '
f'"{fid}" ({space}); pass --allow-shared-catalog only for ' f'"{fid}" ({space}); pass --allow-shared-catalog only for '
@@ -977,7 +988,7 @@ def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
# Retire ids that fully vanished from the tree (append-only ledger), with a # Retire ids that fully vanished from the tree (append-only ledger), with a
# mode-rule successor so devices and user presets keep resolving. Vanished # mode-rule successor so devices and user presets keep resolving. Vanished
# ids in a FOREIGN island's reserved space (GF*/QD_*) are never retired — # ids in a FOREIGN island's reserved space (GF*) are never retired —
# the island's catalog still owns them and may legitimately (re)ship them — # the island's catalog still owns them and may legitimately (re)ship them —
# they are RELEASED, with a succession hint so they keep resolving on miss. # they are RELEASED, with a succession hint so they keep resolving on miss.
claim_ids = claim_effective_ids(analysis) claim_ids = claim_effective_ids(analysis)
@@ -1073,7 +1084,7 @@ def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
def add_hints(pairs, profiles_dir=PROFILES_DIR, retired_path=RETIRED_PATH): def add_hints(pairs, profiles_dir=PROFILES_DIR, retired_path=RETIRED_PATH):
"""--add-hint "OLD=NEW": forward an island-space id to a live id. """--add-hint "OLD=NEW": forward an island-space id to a live id.
OLD must sit in an island id space (GF*/QD_*), must not be retired, and — OLD must sit in an island id space (GF*), must not be retired, and —
when live in the tree — may be declared only by island vendors (Orca cannot when live in the tree — may be declared only by island vendors (Orca cannot
retire it). An absent OLD is fine: island catalogs own ids Orca never retire it). An absent OLD is fine: island catalogs own ids Orca never
shipped or has released. NEW must be live in the tree. All pairs validate shipped or has released. NEW must be live in the tree. All pairs validate
@@ -1103,7 +1114,7 @@ def add_hints(pairs, profiles_dir=PROFILES_DIR, retired_path=RETIRED_PATH):
errors += 1 errors += 1
elif island is None: elif island is None:
print_error( print_error(
f'--add-hint: "{old}" is not in an island id space (GF*/QD_*); ' f'--add-hint: "{old}" is not in an island id space (GF*); '
f"non-island ids are retired with a successor by --update-snapshot") f"non-island ids are retired with a successor by --update-snapshot")
errors += 1 errors += 1
elif outsiders: elif outsiders:

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ import contextlib
import io import io
import json import json
import os import os
import re
import shutil import shutil
import sys import sys
import tempfile import tempfile
@@ -400,7 +401,8 @@ class TestTripleResolution(unittest.TestCase):
class TestReservedSpaces(unittest.TestCase): class TestReservedSpaces(unittest.TestCase):
def test_owners(self): def test_owners(self):
self.assertEqual(afi.reserved_space_owner("GFL99"), (True, "BBL")) self.assertEqual(afi.reserved_space_owner("GFL99"), (True, "BBL"))
self.assertEqual(afi.reserved_space_owner("QD_X4_PLA"), (True, "Qidi")) # dissolved island (plan v4): reserved, but no vendor may declare it
self.assertEqual(afi.reserved_space_owner("QD_X4_PLA"), (True, None))
self.assertEqual(afi.reserved_space_owner("P1234abc"), (True, None)) self.assertEqual(afi.reserved_space_owner("P1234abc"), (True, None))
self.assertEqual(afi.reserved_space_owner("pAbCdEf1"), (True, None)) # case-insensitive self.assertEqual(afi.reserved_space_owner("pAbCdEf1"), (True, None)) # case-insensitive
self.assertEqual(afi.reserved_space_owner("null"), (True, None)) self.assertEqual(afi.reserved_space_owner("null"), (True, None))
@@ -410,7 +412,8 @@ class TestReservedSpaces(unittest.TestCase):
def test_island_declarations(self): def test_island_declarations(self):
self.assertTrue(afi.is_island_declaration("BBL", "GFL99")) self.assertTrue(afi.is_island_declaration("BBL", "GFL99"))
self.assertTrue(afi.is_island_declaration("BBL", "OF5CgdDq")) self.assertTrue(afi.is_island_declaration("BBL", "OF5CgdDq"))
self.assertTrue(afi.is_island_declaration("Qidi", "QD_X4_PLA")) # dissolved island (plan v4): Qidi declarations mint like any other
self.assertFalse(afi.is_island_declaration("Qidi", "QD_X4_PLA"))
self.assertFalse(afi.is_island_declaration("Qidi", "OF5CgdDq")) self.assertFalse(afi.is_island_declaration("Qidi", "OF5CgdDq"))
self.assertFalse(afi.is_island_declaration("VendorA", "GFL99")) self.assertFalse(afi.is_island_declaration("VendorA", "GFL99"))
@@ -567,7 +570,7 @@ class TestChecks(SyntheticTreeCase):
def test_check6_reserved_namespace_claims(self): def test_check6_reserved_namespace_claims(self):
for fid, marker in [("GFX99", "owned by BBL"), for fid, marker in [("GFX99", "owned by BBL"),
("QD_X_PLA", "owned by Qidi"), ("QD_X_PLA", "dissolved island"),
("P1a2b3c4", "user-custom"), ("P1a2b3c4", "user-custom"),
("null", "user-custom")]: ("null", "user-custom")]:
with self.subTest(fid=fid): with self.subTest(fid=fid):
@@ -1478,6 +1481,31 @@ class TestRealTree(unittest.TestCase):
self.assertEqual(analysis["missing_effective"], []) self.assertEqual(analysis["missing_effective"], [])
self.assertEqual(analysis["read_errors"], []) self.assertEqual(analysis["read_errors"], [])
# Code<->ledger lockstep for the Qidi box (plan v4 SS2): the QD_* literals
# QidiPrinterAgent::map_filament_type_to_setting_id returns must stay
# retired ledger keys whose chains end at a live preset id, because the
# agent resolves them through resolve_filament_id_succession() at runtime.
@unittest.skip("enable with plan v4.2: QD_* ids are not retired yet")
def test_qidi_agent_fallback_ids_resolve_through_ledger(self):
cpp = os.path.join(REPO_ROOT, "src", "slic3r", "Utils", "QidiPrinterAgent.cpp")
with open(cpp, encoding="utf-8") as f:
src = f.read()
m = re.search(
r"map_filament_type_to_setting_id\(const std::string& filament_type\)"
r"\s*\{(.*?)\n\}", src, re.DOTALL)
self.assertIsNotNone(m, "map_filament_type_to_setting_id not found in QidiPrinterAgent.cpp")
qd_ids = re.findall(r'return "(QD_[0-9_]+)";', m.group(1))
self.assertTrue(qd_ids, "no QD_* literals parsed from map_filament_type_to_setting_id")
ledger = afi.load_ledger(afi.RETIRED_PATH)
live = set(afi.analyze_tree(REAL_PROFILES)["ids"])
for fid in qd_ids:
self.assertIn(fid, ledger["retired"], f'"{fid}" must be a retired ledger key')
status, node = afi.follow_succession(
fid, set(), ledger["retired"], ledger["hints"], live)
self.assertEqual(
status, "live",
f'"{fid}" succession must end at a live id (got {status} at "{node}")')
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# review-fix regressions # review-fix regressions