mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-09 02:06:54 +00:00
remove retired_filament_ids.json
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Mint deterministic filament_id values for OrcaSlicer system filament products and
|
||||
validate the tree against the sanctioned-state ledger.
|
||||
validate the tree against the sanctioned-state snapshot.
|
||||
|
||||
Policy (companion to assign_vendor_setting_ids.py; see filament_id_plan_v3.md):
|
||||
Policy (companion to assign_vendor_setting_ids.py; see docs/HLSD/filament_id.md):
|
||||
* filament_id is a PRODUCT id: one commercial product line = one id, shared by
|
||||
all of that material's per-printer/per-nozzle variants in every bundle.
|
||||
OrcaFilamentLibrary (OFL) is the product catalog: a family's id is declared
|
||||
@@ -19,34 +19,22 @@ Policy (companion to assign_vendor_setting_ids.py; see filament_id_plan_v3.md):
|
||||
filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE,
|
||||
"filament_product/<filament_vendor>/<filament_type>/<family_name>") )
|
||||
8 chars total, which satisfies the AMS length limit. Nobody invents ids by
|
||||
hand; on the astronomically rare collision with any existing or retired id
|
||||
the input is salted ("/1", "/2", ...) until free and the result is frozen
|
||||
in file. Identity changes (a family rename, a filament_vendor/filament_type
|
||||
fix) change the id BY DESIGN — the succession ledger keeps old ids
|
||||
resolving.
|
||||
hand; on the astronomically rare collision with an existing id the input
|
||||
is salted ("/1", "/2", ...) until free and the result is frozen in file.
|
||||
Identity changes (a family rename, a filament_vendor/filament_type fix)
|
||||
change the id BY DESIGN.
|
||||
* Reserved id spaces that are never minted into or altered:
|
||||
- GF* Bambu AMS/RFID catalog (vendor BBL untouchable)
|
||||
- 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)
|
||||
- QD_* Qidi device protocol: the box composes these ids
|
||||
at runtime, they are not preset ids, and no
|
||||
preset may declare one
|
||||
- P + 7 hex chars (case-insensitive) and the literal "null"
|
||||
user-custom presets (CreatePresetsDialog.cpp)
|
||||
- every already-shipped id, grandfathered via scripts/filament_id_snapshot.json
|
||||
* scripts/filament_id_snapshot.json is the sanctioned-state ledger (ids,
|
||||
* scripts/filament_id_snapshot.json is the sanctioned-state snapshot (ids,
|
||||
claims and declared triples): it must exactly equal the tree-derived state
|
||||
at all times, so any id/claim/triple change shows up as a reviewable diff to
|
||||
that file (the maintainer gate). Ids that fully vanish from the tree are
|
||||
appended to the SHIPPED succession ledger
|
||||
resources/profiles/retired_filament_ids.json as
|
||||
{"claims": [...], "successor": <id|null>} — successor picked by the mode
|
||||
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*)
|
||||
on resolution miss. A retired id may never be used again for anything.
|
||||
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
|
||||
legitimately (re)ship them.
|
||||
that file (the maintainer gate).
|
||||
|
||||
The effective-id resolution below is loader-faithful (PresetBundle.cpp
|
||||
load_vendor_configs_from_json): own filament_id key, else walk `inherits` within
|
||||
@@ -59,16 +47,7 @@ Run from anywhere: python3 scripts/assign_filament_ids.py
|
||||
rewrites a valid existing id; a no-op on a fully-idded tree
|
||||
--mint "Vendor/Type/Family"
|
||||
print the id that triple would mint; touches nothing
|
||||
--update-snapshot regenerate the snapshot from the tree; retire vanished ids
|
||||
with a mode-rule successor (--forget-never-shipped FILE
|
||||
drops listed never-shipped ids from lineage instead)
|
||||
--add-hint "OLD=NEW"
|
||||
record a cross-island succession hint (standalone)
|
||||
--retire "OLD=NEW"
|
||||
retire a vanished non-island shipped id with an explicit
|
||||
successor (standalone; for lineage the claim vote can no
|
||||
longer see because the claims migrated while another
|
||||
declarer kept the id alive)
|
||||
--update-snapshot regenerate the snapshot from the tree
|
||||
--remint VENDOR re-derive VENDOR's declared ids from their triples and
|
||||
rewrite mismatches in place (repeatable)
|
||||
--drop-redundant-ids VENDOR
|
||||
@@ -96,10 +75,6 @@ FILAMENT_ID_LENGTH = 6 # base62 digits after the "OF" prefix -> 8 chars total
|
||||
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PROFILES_DIR = os.path.normpath(os.path.join(SCRIPTS_DIR, "..", "resources", "profiles"))
|
||||
SNAPSHOT_PATH = os.path.join(SCRIPTS_DIR, "filament_id_snapshot.json")
|
||||
# The succession ledger ships with the app (resources/) so the runtime can
|
||||
# follow retired->successor chains and cross-island hints on resolution miss.
|
||||
RETIRED_PATH = os.path.join(PROFILES_DIR, "retired_filament_ids.json")
|
||||
RETIRED_REL = "resources/profiles/retired_filament_ids.json"
|
||||
|
||||
OFL = "OrcaFilamentLibrary"
|
||||
|
||||
@@ -171,7 +146,7 @@ def generate_filament_id(filament_vendor, filament_type, family, salt=0):
|
||||
|
||||
|
||||
def mint_filament_id(filament_vendor, filament_type, family, taken):
|
||||
"""Mint the product's id, salting past any id in `taken` (existing + retired)."""
|
||||
"""Mint the product's id, salting past any id in `taken`."""
|
||||
for salt in range(10000):
|
||||
candidate = generate_filament_id(filament_vendor, filament_type, family, salt)
|
||||
if candidate not in taken:
|
||||
@@ -193,7 +168,7 @@ def list_vendor_names(profiles_dir):
|
||||
"""Vendor bundles = subdirectories with a matching <name>.json index file.
|
||||
|
||||
(Ignores stray non-bundle entries such as the tracked "user" directory,
|
||||
which has no user.json index, and the retired_filament_ids.json ledger.)
|
||||
which has no user.json index.)
|
||||
"""
|
||||
profiles_dir = str(profiles_dir)
|
||||
return sorted(
|
||||
@@ -250,7 +225,7 @@ def resolve_filament_id(name, filaments, ofl_filaments, seen=None, in_ofl=False,
|
||||
vendor is re-tried against the OFL map keyed by its direct parent name.
|
||||
|
||||
skip_own ignores the first preset's own filament_id key (used to compute the
|
||||
id its inherits chain would resolve WITHOUT the declaration — check 7b drift).
|
||||
id its inherits chain would resolve WITHOUT the declaration — check 6b drift).
|
||||
|
||||
Returns (filament_id or None, source, ofl_entry) where source is one of
|
||||
"own"/"inherited"/"missing"/"dangling"/"cycle" and ofl_entry is the name of
|
||||
@@ -379,7 +354,6 @@ def analyze_tree(profiles_dir):
|
||||
triples = {} # fid -> set of triples of its non-island declarers
|
||||
declarer_triples = [] # (vendor, rec, fid, triple) per non-island declarer
|
||||
family_triples = {} # (vendor, family) -> {triple: [declarer names]}
|
||||
renamed_claims = {} # "Vendor/OldFamily" -> set of ids the rename now carries
|
||||
|
||||
for vendor, filaments in vendors.items():
|
||||
occurring = vendor_ids.setdefault(vendor, set())
|
||||
@@ -413,19 +387,12 @@ def analyze_tree(profiles_dir):
|
||||
continue
|
||||
occurring.add(eff)
|
||||
ids.setdefault(eff, set()).add(f"{vendor}/{base_name(rec['name'])}")
|
||||
# A renamed preset keeps voting under the names it used to have, so
|
||||
# the id its old family carried retires to the id the rename minted.
|
||||
renamed = rec.get("renamed_from")
|
||||
for old in (renamed if isinstance(renamed, str) else "").split(";"):
|
||||
if old.strip():
|
||||
renamed_claims.setdefault(
|
||||
f"{vendor}/{base_name(old.strip())}", set()).add(eff)
|
||||
if rec.get("filament_id"):
|
||||
instantiated_with_id.append(f"{vendor}/{rec['name']}")
|
||||
if vendor != OFL and rec["ofl_entry"] and eff in ofl_declared:
|
||||
alias_candidates.append((vendor, rec, rec["ofl_entry"], False))
|
||||
|
||||
# Alias hygiene (check 5): a vendor preset that rides an OFL family (its id
|
||||
# Alias hygiene (check 4): a vendor preset that rides an OFL family (its id
|
||||
# resolves through OFL) is matched to the OFL preset by ALIAS (base name);
|
||||
# renaming it re-exposes the OFL preset and creates a live duplicate, empty
|
||||
# compatible_printers cannot claim any printer, and declaring its own
|
||||
@@ -452,7 +419,7 @@ def analyze_tree(profiles_dir):
|
||||
"empty compatible_printers cannot shadow the OFL preset anywhere",
|
||||
rec["file"]))
|
||||
|
||||
# Cross-bundle triple divergence (check 8, warning only): the same family
|
||||
# Cross-bundle triple divergence (check 7, warning only): the same family
|
||||
# name declared in several bundles with different triples cannot converge
|
||||
# on one id until the divergence is fixed.
|
||||
name_bundles = {}
|
||||
@@ -468,7 +435,6 @@ def analyze_tree(profiles_dir):
|
||||
"vendors": vendors,
|
||||
"read_errors": read_errors,
|
||||
"ids": {fid: sorted(claims) for fid, claims in ids.items()},
|
||||
"renamed_claims": {claim: sorted(fids) for claim, fids in renamed_claims.items()},
|
||||
"vendor_ids": vendor_ids,
|
||||
"declared_ids": declared_ids,
|
||||
"instantiated_with_id": sorted(instantiated_with_id),
|
||||
@@ -488,7 +454,7 @@ def analyze_tree(profiles_dir):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Snapshot / succession-ledger IO
|
||||
# Snapshot IO
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def snapshot_from_analysis(analysis):
|
||||
@@ -515,102 +481,13 @@ def load_snapshot(path):
|
||||
return data
|
||||
|
||||
|
||||
def load_ledger(path):
|
||||
"""The shipped succession ledger:
|
||||
{"retired": {id: {"claims": [...], "successor": id|None}}, "hints": {id: id}}.
|
||||
Empty maps when the file is absent.
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
return {"retired": {}, "hints": {}}
|
||||
data = load_json(path)
|
||||
return {"retired": data.get("retired", {}), "hints": data.get("hints", {})}
|
||||
|
||||
|
||||
def load_retired(path):
|
||||
"""The ledger's retired map (see load_ledger); {} when the file is absent."""
|
||||
return load_ledger(path)["retired"]
|
||||
|
||||
|
||||
def write_ledger(path, obj):
|
||||
def write_snapshot(path, obj):
|
||||
"""Deterministic serialization: sorted keys, indent 1, LF, trailing newline."""
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||
json.dump(obj, f, indent=1, ensure_ascii=False, sort_keys=True)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def claim_effective_ids(analysis):
|
||||
"""Reverse of analysis["ids"]: claim "Vendor/Family" -> set of effective ids.
|
||||
|
||||
A preset's "renamed_from" names count as claims of that preset too, so an id
|
||||
whose family was renamed still finds a successor in the claim vote.
|
||||
"""
|
||||
claim_ids = {}
|
||||
for fid, claims in analysis["ids"].items():
|
||||
for claim in claims:
|
||||
claim_ids.setdefault(claim, set()).add(fid)
|
||||
for claim, fids in analysis.get("renamed_claims", {}).items():
|
||||
if claim not in claim_ids:
|
||||
claim_ids[claim] = set(fids)
|
||||
return claim_ids
|
||||
|
||||
|
||||
def successor_by_mode(claims, claim_ids):
|
||||
"""Mode rule for a retiring id's successor: every old claim whose family
|
||||
still exists votes for the family's current effective id(s); most votes
|
||||
wins, ties break to the lexicographically smallest id, no votes -> None.
|
||||
Returns (successor_or_None, vote_counts).
|
||||
"""
|
||||
counts = {}
|
||||
for claim in claims:
|
||||
for fid in claim_ids.get(claim, ()):
|
||||
counts[fid] = counts.get(fid, 0) + 1
|
||||
if not counts:
|
||||
return None, counts
|
||||
best = max(counts.values())
|
||||
return min(f for f, n in counts.items() if n == best), counts
|
||||
|
||||
|
||||
def migrate_retired_entries(old_retired, claim_ids):
|
||||
"""One-shot v3 schema migration: list-valued entries {id: [claims]} become
|
||||
{id: {"claims": [...], "successor": <mode-rule id|None>}}; object entries
|
||||
pass through untouched.
|
||||
"""
|
||||
migrated = {}
|
||||
for fid, entry in old_retired.items():
|
||||
if isinstance(entry, dict):
|
||||
migrated[fid] = entry
|
||||
continue
|
||||
successor, _counts = successor_by_mode(entry, claim_ids)
|
||||
migrated[fid] = {"claims": sorted(entry), "successor": successor}
|
||||
return migrated
|
||||
|
||||
|
||||
def follow_succession(fid, seen, retired, hints, live):
|
||||
"""Follow retired->successor / hints->target forwarding starting at `fid`.
|
||||
|
||||
Returns (status, node): "null" (chain ended on an heirless retirement),
|
||||
"live" (reached a live tree id), "cycle", or "dead-end" (an id that is
|
||||
neither live nor forwarded anywhere). `seen` pre-seeds the cycle guard.
|
||||
"""
|
||||
seen = set(seen)
|
||||
cur = fid
|
||||
while True:
|
||||
if cur is None:
|
||||
return "null", None
|
||||
if cur in seen:
|
||||
return "cycle", cur
|
||||
seen.add(cur)
|
||||
if cur in live:
|
||||
return "live", cur
|
||||
entry = retired.get(cur)
|
||||
if entry is not None:
|
||||
cur = entry.get("successor") if isinstance(entry, dict) else None
|
||||
elif cur in hints:
|
||||
cur = hints[cur]
|
||||
else:
|
||||
return "dead-end", cur
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reserved namespaces
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -631,7 +508,7 @@ def reserved_space_desc(fid, owner):
|
||||
if owner:
|
||||
return f"owned by {owner}"
|
||||
if fid.startswith("QD_"):
|
||||
return "Qidi device protocol; dissolved island — ids are retired, never declarable"
|
||||
return "Qidi device protocol; composed by the device, never a preset id"
|
||||
return "reserved for user-custom presets"
|
||||
|
||||
|
||||
@@ -639,8 +516,7 @@ def reserved_space_desc(fid, owner):
|
||||
# Checks (imported and called tree-wide by orca_extra_profile_check.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
retired_path=RETIRED_PATH):
|
||||
def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH):
|
||||
"""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
|
||||
@@ -651,23 +527,19 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
3. Mint conformance: a non-island OF-format declaration must equal the mint
|
||||
of the declarer's triple or a salted iteration, unless that exact
|
||||
(id, triple) pair is grandfathered in the snapshot.
|
||||
4. Retired ids may never occur again.
|
||||
5. Alias hygiene: a vendor preset riding an OFL family must keep the OFL
|
||||
4. Alias hygiene: a vendor preset riding an OFL family must keep the OFL
|
||||
base name, claim printers via non-empty compatible_printers, and declare
|
||||
no filament_id key of its own.
|
||||
6. Reserved namespaces (GF* for BBL; QD_*/P-hex/"null" for nobody) must not
|
||||
5. Reserved namespaces (GF* for BBL; QD_*/P-hex/"null" for nobody) must not
|
||||
be claimed by other vendors, except claims grandfathered in the snapshot.
|
||||
7. Structure ratchet: (a) no NEW instantiated preset carries its own
|
||||
6. Structure ratchet: (a) no NEW instantiated preset carries its own
|
||||
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++).
|
||||
8. Triple integrity: (a) every non-island declarer resolves non-empty
|
||||
7. Triple integrity: (a) every non-island declarer resolves non-empty
|
||||
filament_vendor and filament_type (hard error, no grandfathering);
|
||||
(b) declarers of one (bundle, family) resolve identical triples, unless
|
||||
grandfathered in snapshot triple_exceptions; cross-bundle divergence on
|
||||
the same family name is a warning only.
|
||||
9. Succession integrity: retired successor chains terminate at a live id or
|
||||
null without cycles; hint keys are live island-declared ids and hint
|
||||
chains terminate live.
|
||||
"""
|
||||
_utf8_console()
|
||||
errors = 0
|
||||
@@ -676,10 +548,6 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
if snapshot is None:
|
||||
print_error(f"filament_id snapshot not found at {snapshot_path}; {UPDATE_HINT}")
|
||||
return 1
|
||||
ledger = load_ledger(retired_path)
|
||||
retired = ledger["retired"]
|
||||
hints = ledger["hints"]
|
||||
|
||||
for msg in analysis["read_errors"]:
|
||||
print_error(msg)
|
||||
errors += 1
|
||||
@@ -717,8 +585,8 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
for fid in sorted(snap_ids):
|
||||
if fid not in tree_ids:
|
||||
print_error(
|
||||
f'filament_id stability: snapshot id "{fid}" vanished from the tree '
|
||||
f"(shipped ids retire through the succession ledger); {UPDATE_HINT}")
|
||||
f'filament_id stability: snapshot id "{fid}" vanished from the tree; '
|
||||
f"{UPDATE_HINT}")
|
||||
errors += 1
|
||||
continue
|
||||
for claim in snap_ids[fid]:
|
||||
@@ -761,16 +629,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
f"family root (or, for an intentionally kept id, {UPDATE_HINT})")
|
||||
errors += 1
|
||||
|
||||
# -- 4. retired ids may never come back ---------------------------------
|
||||
for vendor in sorted(analysis["vendor_ids"]):
|
||||
for fid in sorted(analysis["vendor_ids"][vendor]):
|
||||
if fid in retired:
|
||||
print_error(
|
||||
f'filament_id "{fid}" ({vendor}) is retired '
|
||||
f"({RETIRED_REL}) and may never be reused")
|
||||
errors += 1
|
||||
|
||||
# -- 5. alias hygiene for presets riding OFL families --------------------
|
||||
# -- 4. alias hygiene for presets riding OFL families --------------------
|
||||
exceptions = set(snapshot["alias_exceptions"])
|
||||
for vendor, name, entry, reason, file in analysis["alias_violations"]:
|
||||
if f"{vendor}/{name}" in exceptions:
|
||||
@@ -782,7 +641,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
f"gets its own minted id")
|
||||
errors += 1
|
||||
|
||||
# -- 6. reserved namespaces ----------------------------------------------
|
||||
# -- 5. reserved namespaces ----------------------------------------------
|
||||
for fid in sorted(tree_ids):
|
||||
is_reserved, owner = reserved_space_owner(fid)
|
||||
if not is_reserved:
|
||||
@@ -799,7 +658,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
f"({space}) and must not be claimed by system presets of other vendors")
|
||||
errors += 1
|
||||
|
||||
# -- 7. structure ratchet -------------------------------------------------
|
||||
# -- 6. structure ratchet -------------------------------------------------
|
||||
grandfathered = set(snapshot["instantiated_with_id"])
|
||||
for key in analysis["instantiated_with_id"]:
|
||||
if key not in grandfathered:
|
||||
@@ -827,7 +686,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
f'"{vendor}/{base_name(name)}": "{expected}", salted if taken)')
|
||||
errors += 1
|
||||
|
||||
# -- 8. triple integrity ---------------------------------------------------
|
||||
# -- 7. triple integrity ---------------------------------------------------
|
||||
for vendor, rec, fid, triple in sorted(
|
||||
analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])):
|
||||
if triple[0] and triple[1]:
|
||||
@@ -860,62 +719,6 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
f"({detail}); bundles of one product converge on one id only once "
|
||||
f"their triples agree")
|
||||
|
||||
# -- 9. succession integrity (ledger-internal) -----------------------------
|
||||
live = set(tree_ids)
|
||||
for fid in sorted(retired):
|
||||
entry = retired[fid]
|
||||
if (not isinstance(entry, dict)
|
||||
or not isinstance(entry.get("claims", []), list)
|
||||
or not (entry.get("successor") is None
|
||||
or isinstance(entry.get("successor"), str))):
|
||||
print_error(
|
||||
f'retired entry "{fid}" ({RETIRED_REL}) is not of the form '
|
||||
f'{{"claims": [...], "successor": <id|null>}}')
|
||||
errors += 1
|
||||
continue
|
||||
status, node = follow_succession(entry.get("successor"), {fid}, retired, hints, live)
|
||||
if status == "cycle":
|
||||
print_error(
|
||||
f'succession chain of retired id "{fid}" cycles at "{node}" ({RETIRED_REL})')
|
||||
errors += 1
|
||||
elif status == "dead-end":
|
||||
print_error(
|
||||
f'succession chain of retired id "{fid}" dead-ends at "{node}", which is '
|
||||
f"neither a live tree id nor retired/hinted ({RETIRED_REL})")
|
||||
errors += 1
|
||||
for key in sorted(hints):
|
||||
target = hints[key]
|
||||
_is_reserved, island = reserved_space_owner(key)
|
||||
if island is None:
|
||||
print_error(
|
||||
f'hint key "{key}" ({RETIRED_REL}) is not in an island id space '
|
||||
f"(GF*); non-island ids are retired with a successor instead")
|
||||
errors += 1
|
||||
continue
|
||||
if key in retired:
|
||||
print_error(
|
||||
f'hint key "{key}" ({RETIRED_REL}) is also retired; an id is either '
|
||||
f"retired (Orca-owned) or hinted (island-owned), never both")
|
||||
errors += 1
|
||||
continue
|
||||
# A hint key may be absent from the tree (released to its island); when
|
||||
# live, only island vendors may declare it.
|
||||
outsiders = sorted(
|
||||
v for v in analysis["declared_ids"]
|
||||
if key in analysis["declared_ids"][v] and not is_island_declaration(v, key))
|
||||
if outsiders:
|
||||
print_error(
|
||||
f'hint key "{key}" ({RETIRED_REL}) is declared by non-island vendor(s) '
|
||||
f"{outsiders}; hints apply only once no non-island vendor declares the "
|
||||
f"id — re-mint those declarers first")
|
||||
errors += 1
|
||||
status, node = follow_succession(target, {key}, retired, hints, live)
|
||||
if status != "live":
|
||||
print_error(
|
||||
f'hint "{key}" -> "{target}" ({RETIRED_REL}) must chain-terminate at a '
|
||||
f"live tree id (got {status} at \"{node}\")")
|
||||
errors += 1
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@@ -924,18 +727,8 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
retired_path=RETIRED_PATH, allow_shared_catalog=False,
|
||||
forget_path=None):
|
||||
"""Regenerate the snapshot from the tree; retire ids that fully vanished.
|
||||
|
||||
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);
|
||||
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
|
||||
there and must never be blocked by check 4.
|
||||
--forget-never-shipped lists ids to drop from lineage instead of retiring
|
||||
(never shipped => nothing outside the tree references them); succession
|
||||
chains through them are spliced to their mode-rule successor.
|
||||
allow_shared_catalog=False):
|
||||
"""Regenerate the snapshot from the tree.
|
||||
|
||||
Refuses to sanction NEW reserved-namespace ids (or new claims on them) for
|
||||
non-owner vendors unless --allow-shared-catalog is passed. Idempotent: a
|
||||
@@ -948,21 +741,6 @@ def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
old_snap = load_snapshot(snapshot_path)
|
||||
old_ids = old_snap["ids"] if old_snap else {}
|
||||
|
||||
forget = set()
|
||||
if forget_path:
|
||||
data = load_json(forget_path)
|
||||
if not isinstance(data, list) or not all(isinstance(x, str) for x in data):
|
||||
print_error(f"--forget-never-shipped expects a JSON list of ids: {forget_path}")
|
||||
return 1
|
||||
forget = set(data)
|
||||
still_live = sorted(forget & set(new_snap["ids"]))
|
||||
if still_live:
|
||||
for fid in still_live:
|
||||
print_error(
|
||||
f'--forget-never-shipped: id "{fid}" is still in the tree and can '
|
||||
f"neither be forgotten nor retired")
|
||||
return 1
|
||||
|
||||
# Gate: new reserved-namespace ids / claims for non-owner vendors.
|
||||
refusals = []
|
||||
for fid, claims in sorted(new_snap["ids"].items()):
|
||||
@@ -990,72 +768,6 @@ def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
f"maintainer-approved shared-catalog families")
|
||||
return 1
|
||||
|
||||
# Retirement is permanent: refuse to sanction a tree that resurrects a
|
||||
# retired id (check 4 would reject the resulting snapshot forever anyway).
|
||||
ledger = load_ledger(retired_path)
|
||||
retired = ledger["retired"]
|
||||
reused = sorted(set(new_snap["ids"]) & set(retired))
|
||||
if reused:
|
||||
for fid in reused:
|
||||
print_error(
|
||||
f'refusing to sanction retired filament_id "{fid}" '
|
||||
f"({RETIRED_REL} is append-only; retired ids may "
|
||||
f"never be reused — mint a fresh id for the family instead)")
|
||||
return 1
|
||||
|
||||
# Retire ids that fully vanished from the tree (append-only ledger), with a
|
||||
# mode-rule successor so devices and user presets keep resolving. Vanished
|
||||
# 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 —
|
||||
# they are RELEASED, with a succession hint so they keep resolving on miss.
|
||||
claim_ids = claim_effective_ids(analysis)
|
||||
newly_retired = []
|
||||
forgotten = []
|
||||
released = []
|
||||
for fid in sorted(old_ids):
|
||||
if fid in new_snap["ids"]:
|
||||
continue
|
||||
if fid in forget:
|
||||
forgotten.append(fid)
|
||||
print_info(f'forgotten (never shipped): "{fid}" — no retirement entry')
|
||||
continue
|
||||
_is_reserved, island = reserved_space_owner(fid)
|
||||
if island is not None:
|
||||
successor, _counts = successor_by_mode(old_ids[fid], claim_ids)
|
||||
if successor and fid not in ledger["hints"]:
|
||||
ledger["hints"][fid] = successor
|
||||
released.append(fid)
|
||||
print_info(
|
||||
f'released to the {island} island space: "{fid}"'
|
||||
+ (f' — hint -> "{ledger["hints"][fid]}"' if fid in ledger["hints"]
|
||||
else " (no successor computable; --add-hint if one is needed)"))
|
||||
continue
|
||||
entry = retired.get(fid)
|
||||
if not isinstance(entry, dict):
|
||||
successor, counts = successor_by_mode(old_ids[fid], claim_ids)
|
||||
if len(counts) > 1:
|
||||
losers = sorted(set(counts) - {successor})
|
||||
print_warning(
|
||||
f'retiring "{fid}": successor "{successor}" won the claim vote '
|
||||
f"({counts[successor]}); losing candidate(s): {', '.join(losers)}")
|
||||
entry = {"claims": list(entry) if entry else [], "successor": successor}
|
||||
entry["claims"] = sorted(set(entry["claims"]) | set(old_ids[fid]))
|
||||
retired[fid] = entry
|
||||
newly_retired.append(fid)
|
||||
|
||||
# Splice succession chains through forgotten ids so they stay live.
|
||||
spliced = []
|
||||
for fid in sorted(retired):
|
||||
entry = retired[fid]
|
||||
successor = entry.get("successor") if isinstance(entry, dict) else None
|
||||
if successor in forget:
|
||||
new_successor, _counts = successor_by_mode(old_ids.get(successor, []), claim_ids)
|
||||
entry["successor"] = new_successor
|
||||
spliced.append(fid)
|
||||
print_info(
|
||||
f'spliced succession: "{fid}" -> {json.dumps(new_successor)} '
|
||||
f'(through forgotten "{successor}")')
|
||||
|
||||
# Diff summary.
|
||||
added_ids = sorted(set(new_snap["ids"]) - set(old_ids))
|
||||
removed_ids = sorted(set(old_ids) - set(new_snap["ids"]))
|
||||
@@ -1071,22 +783,12 @@ def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
changed = new_snap != old_snap
|
||||
|
||||
if changed:
|
||||
write_ledger(snapshot_path, new_snap)
|
||||
if newly_retired or spliced or released or not os.path.exists(retired_path):
|
||||
write_ledger(retired_path, {"retired": retired, "hints": ledger["hints"]})
|
||||
write_snapshot(snapshot_path, new_snap)
|
||||
|
||||
print_info(f"snapshot ids : {len(new_snap['ids'])} (+{len(added_ids)} / -{len(removed_ids)})")
|
||||
print_info(f"claims added : {added_claims}")
|
||||
print_info(f"claims removed : {removed_claims}")
|
||||
print_info(f"declared triples : {len(new_snap['triples'])}")
|
||||
print_info(f"ids retired now : {len(newly_retired)}" +
|
||||
(f" ({', '.join(newly_retired)})" if newly_retired else ""))
|
||||
if forgotten:
|
||||
print_info(f"ids forgotten : {len(forgotten)} ({', '.join(forgotten)})")
|
||||
if released:
|
||||
print_info(f"ids released : {len(released)} ({', '.join(released)})")
|
||||
if spliced:
|
||||
print_info(f"chains spliced : {len(spliced)} ({', '.join(spliced)})")
|
||||
for section in ("instantiated_with_id", "id_overrides", "alias_exceptions",
|
||||
"triple_exceptions"):
|
||||
before, after = len(old_snap.get(section, [])), len(new_snap[section])
|
||||
@@ -1098,125 +800,6 @@ def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
return 0
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
before anything is written. Returns 0 on success.
|
||||
"""
|
||||
_utf8_console()
|
||||
analysis = analyze_tree(profiles_dir)
|
||||
errors = 0
|
||||
for msg in analysis["read_errors"]:
|
||||
print_error(msg)
|
||||
errors += 1
|
||||
ledger = load_ledger(retired_path)
|
||||
live = set(analysis["ids"])
|
||||
parsed = []
|
||||
for pair in pairs:
|
||||
if "=" not in pair:
|
||||
print_error(f'--add-hint expects "OLD=NEW", got "{pair}"')
|
||||
errors += 1
|
||||
continue
|
||||
old, new = pair.split("=", 1)
|
||||
_is_reserved, island = reserved_space_owner(old)
|
||||
outsiders = sorted(
|
||||
v for v in analysis["declared_ids"]
|
||||
if old in analysis["declared_ids"][v] and not is_island_declaration(v, old))
|
||||
if old in ledger["retired"]:
|
||||
print_error(f'--add-hint: "{old}" is retired; its succession entry already forwards it')
|
||||
errors += 1
|
||||
elif island is None:
|
||||
print_error(
|
||||
f'--add-hint: "{old}" is not in an island id space (GF*); '
|
||||
f"non-island ids are retired with a successor by --update-snapshot")
|
||||
errors += 1
|
||||
elif outsiders:
|
||||
print_error(
|
||||
f'--add-hint: "{old}" is declared by non-island vendor(s) {outsiders}; '
|
||||
f"hints apply only once no non-island vendor declares the id — re-mint "
|
||||
f"those declarers first")
|
||||
errors += 1
|
||||
elif new not in live:
|
||||
print_error(f'--add-hint: target "{new}" is not a live tree id')
|
||||
errors += 1
|
||||
else:
|
||||
parsed.append((old, new))
|
||||
if errors:
|
||||
return 1
|
||||
for old, new in parsed:
|
||||
ledger["hints"][old] = new
|
||||
print_info(f'hint added: "{old}" -> "{new}"')
|
||||
write_ledger(retired_path, {"retired": ledger["retired"], "hints": ledger["hints"]})
|
||||
return 0
|
||||
|
||||
|
||||
def retire_ids(pairs, profiles_dir=PROFILES_DIR, retired_path=RETIRED_PATH):
|
||||
"""--retire "OLD=NEW": retire a vanished non-island id with an explicit successor.
|
||||
|
||||
For shipped ids whose lineage the claim vote can no longer see: the id
|
||||
vanished from the tree in an earlier phase's shadow — another declarer kept
|
||||
it alive while its claims migrated — so no --update-snapshot run ever saw
|
||||
the id vanish while its claims could still vote. OLD must be absent from
|
||||
the tree (declared or effective) and outside every reserved id space
|
||||
(island ids forward via --add-hint; user-custom ids are never system ids),
|
||||
and must not already be retired or hinted. NEW must be a live tree id.
|
||||
Appends {"claims": [], "successor": NEW}. All pairs validate before
|
||||
anything is written. Returns 0 on success.
|
||||
"""
|
||||
_utf8_console()
|
||||
analysis = analyze_tree(profiles_dir)
|
||||
errors = 0
|
||||
for msg in analysis["read_errors"]:
|
||||
print_error(msg)
|
||||
errors += 1
|
||||
ledger = load_ledger(retired_path)
|
||||
live = set(analysis["ids"])
|
||||
declared_anywhere = set()
|
||||
for vids in analysis["declared_ids"].values():
|
||||
declared_anywhere |= set(vids)
|
||||
parsed = []
|
||||
for pair in pairs:
|
||||
if "=" not in pair:
|
||||
print_error(f'--retire expects "OLD=NEW", got "{pair}"')
|
||||
errors += 1
|
||||
continue
|
||||
old, new = pair.split("=", 1)
|
||||
is_reserved, _island = reserved_space_owner(old)
|
||||
if old in ledger["retired"]:
|
||||
print_error(f'--retire: "{old}" is already retired')
|
||||
errors += 1
|
||||
elif old in ledger["hints"]:
|
||||
print_error(f'--retire: "{old}" is a hint key; it already forwards')
|
||||
errors += 1
|
||||
elif is_reserved:
|
||||
print_error(
|
||||
f'--retire: "{old}" sits in a reserved id space; island ids '
|
||||
f"forward via --add-hint, user-custom ids are never retired")
|
||||
errors += 1
|
||||
elif old in live or old in declared_anywhere:
|
||||
print_error(
|
||||
f'--retire: "{old}" still lives in the tree; re-mint or delete '
|
||||
f"its declarers first (an id that vanishes normally gets its "
|
||||
f"successor voted by --update-snapshot)")
|
||||
errors += 1
|
||||
elif new not in live:
|
||||
print_error(f'--retire: successor "{new}" is not a live tree id')
|
||||
errors += 1
|
||||
else:
|
||||
parsed.append((old, new))
|
||||
if errors:
|
||||
return 1
|
||||
for old, new in parsed:
|
||||
ledger["retired"][old] = {"claims": [], "successor": new}
|
||||
print_info(f'retired: "{old}" -> "{new}"')
|
||||
write_ledger(retired_path, {"retired": ledger["retired"], "hints": ledger["hints"]})
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Byte-preserving profile edits
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1313,8 +896,7 @@ def remove_filament_id(path, old_id):
|
||||
# Default run: mint + insert ids for id-less families
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def assign_missing_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
retired_path=RETIRED_PATH):
|
||||
def assign_missing_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH):
|
||||
"""Mint + insert ids for id-less families. Never rewrites a valid existing id.
|
||||
|
||||
A family = (vendor, base name) group over instantiated filaments with no
|
||||
@@ -1346,9 +928,9 @@ def assign_missing_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
"nothing to do (0 files changed)")
|
||||
return 0, errors
|
||||
|
||||
# Ids already spoken for: whole tree (declared or effective) + ledgers.
|
||||
# Ids already spoken for: the whole tree (declared or effective) + snapshot.
|
||||
snapshot = load_snapshot(snapshot_path) or {"ids": {}}
|
||||
taken = set(snapshot["ids"]) | set(load_retired(retired_path))
|
||||
taken = set(snapshot["ids"])
|
||||
for occurring in analysis["vendor_ids"].values():
|
||||
taken |= occurring
|
||||
|
||||
@@ -1427,7 +1009,7 @@ def assign_missing_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
# --remint / --drop-redundant-ids (v3.1/v3.2 migration modes)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def remint_vendors(vendor_list, profiles_dir=PROFILES_DIR, retired_path=RETIRED_PATH):
|
||||
def remint_vendors(vendor_list, profiles_dir=PROFILES_DIR):
|
||||
"""Re-derive every non-island declared id in the given vendors from its
|
||||
triple; rewrite mismatching declarations in place (byte-preserving). Never
|
||||
touches the snapshot — run --update-snapshot afterwards and review the diff.
|
||||
@@ -1435,8 +1017,8 @@ def remint_vendors(vendor_list, profiles_dir=PROFILES_DIR, retired_path=RETIRED_
|
||||
A declaration already equal to ANY salt iteration of its own triple is
|
||||
mint-conformant (check 3) and left alone — deliberate salt splits (distinct
|
||||
presets of one product kept apart for per-printer AMS matching) survive.
|
||||
A candidate id is blocked when it is a retired key, a hints key, or occurs
|
||||
in the tree with any triple set other than exactly {T}. Equal-triple reuse
|
||||
A candidate id is blocked when it occurs in the tree with any triple set
|
||||
other than exactly {T}. Equal-triple reuse
|
||||
is convergence: the same product must end up under the same id everywhere.
|
||||
Returns (rewritten, errors).
|
||||
"""
|
||||
@@ -1455,8 +1037,6 @@ def remint_vendors(vendor_list, profiles_dir=PROFILES_DIR, retired_path=RETIRED_
|
||||
print_error(f"--remint: unknown vendor {v!r}")
|
||||
return 0, errors + len(unknown)
|
||||
|
||||
ledger = load_ledger(retired_path)
|
||||
blocked_keys = set(ledger["retired"]) | set(ledger["hints"])
|
||||
occurring = set()
|
||||
for vids in analysis["vendor_ids"].values():
|
||||
occurring |= vids
|
||||
@@ -1470,8 +1050,6 @@ def remint_vendors(vendor_list, profiles_dir=PROFILES_DIR, retired_path=RETIRED_
|
||||
return assigned[triple]
|
||||
for salt in range(10000):
|
||||
cand = generate_filament_id(*triple, salt=salt)
|
||||
if cand in blocked_keys:
|
||||
continue
|
||||
if cand in occurring and triple_sets.get(cand, set()) != {triple}:
|
||||
continue
|
||||
if run_taken.get(cand, triple) != triple:
|
||||
@@ -1570,27 +1148,13 @@ def main(argv=None):
|
||||
help="print the id the (filament_vendor, filament_type, "
|
||||
"family name) triple would mint; touches nothing")
|
||||
parser.add_argument("--update-snapshot", action="store_true",
|
||||
help="regenerate scripts/filament_id_snapshot.json from the "
|
||||
"tree; ids that fully vanished are retired with a "
|
||||
"mode-rule successor")
|
||||
help="regenerate scripts/filament_id_snapshot.json from "
|
||||
"the tree")
|
||||
parser.add_argument("--check", action="store_true",
|
||||
help="run the filament_id checks; exit nonzero on errors")
|
||||
parser.add_argument("--allow-shared-catalog", action="store_true",
|
||||
help="with --update-snapshot: allow sanctioning new claims "
|
||||
"on reserved-namespace ids for non-owner vendors")
|
||||
parser.add_argument("--forget-never-shipped", metavar="JSON_FILE",
|
||||
help="with --update-snapshot: vanished ids listed in the file "
|
||||
"(JSON list) are dropped without a retirement entry and "
|
||||
"succession chains are spliced through them; refused if "
|
||||
"a listed id is still in the tree")
|
||||
parser.add_argument("--add-hint", metavar='"OLD=NEW"', action="append",
|
||||
help="record a cross-island succession hint (island-owned "
|
||||
"live id -> live id) in the shipped ledger; standalone, "
|
||||
"repeatable")
|
||||
parser.add_argument("--retire", metavar='"OLD=NEW"', action="append",
|
||||
help="retire a vanished non-island shipped id with an "
|
||||
"explicit successor (for lineage the claim vote can "
|
||||
"no longer see); standalone, repeatable")
|
||||
parser.add_argument("--remint", metavar="VENDOR", action="append",
|
||||
help="re-derive VENDOR's declared filament_ids from their "
|
||||
"triples and rewrite mismatches in place; repeatable")
|
||||
@@ -1602,35 +1166,22 @@ def main(argv=None):
|
||||
help="profiles directory (default: resources/profiles)")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.forget_never_shipped and not args.update_snapshot:
|
||||
parser.error("--forget-never-shipped requires --update-snapshot")
|
||||
|
||||
if args.mint:
|
||||
parts = args.mint.split("/", 2)
|
||||
if len(parts) != 3 or not all(parts):
|
||||
parser.error('--mint expects "filament_vendor/filament_type/family_name" '
|
||||
"with all three components non-empty (check 8 rejects empty "
|
||||
"with all three components non-empty (check 7 rejects empty "
|
||||
"vendor/type in the tree)")
|
||||
snapshot = load_snapshot(SNAPSHOT_PATH) or {"ids": {}}
|
||||
taken = set(snapshot["ids"]) | set(load_retired(RETIRED_PATH))
|
||||
taken = set(snapshot["ids"])
|
||||
print(mint_filament_id(parts[0], parts[1], parts[2], taken))
|
||||
return 0
|
||||
|
||||
if args.add_hint:
|
||||
if (args.update_snapshot or args.check or args.remint
|
||||
or args.drop_redundant_ids or args.retire):
|
||||
parser.error("--add-hint runs standalone")
|
||||
return add_hints(args.add_hint, args.profiles, RETIRED_PATH)
|
||||
|
||||
if args.retire:
|
||||
if args.update_snapshot or args.check or args.remint or args.drop_redundant_ids:
|
||||
parser.error("--retire runs standalone")
|
||||
return retire_ids(args.retire, args.profiles, RETIRED_PATH)
|
||||
|
||||
if args.remint:
|
||||
if args.update_snapshot or args.check or args.drop_redundant_ids:
|
||||
parser.error("--remint cannot be combined with other modes")
|
||||
_changed, errors = remint_vendors(args.remint, args.profiles, RETIRED_PATH)
|
||||
_changed, errors = remint_vendors(args.remint, args.profiles)
|
||||
return 1 if errors else 0
|
||||
|
||||
if args.drop_redundant_ids:
|
||||
@@ -1640,19 +1191,18 @@ def main(argv=None):
|
||||
return 1 if errors else 0
|
||||
|
||||
if args.update_snapshot:
|
||||
return update_snapshot(args.profiles, SNAPSHOT_PATH, RETIRED_PATH,
|
||||
allow_shared_catalog=args.allow_shared_catalog,
|
||||
forget_path=args.forget_never_shipped)
|
||||
return update_snapshot(args.profiles, SNAPSHOT_PATH,
|
||||
allow_shared_catalog=args.allow_shared_catalog)
|
||||
|
||||
if args.check:
|
||||
errors = check_filament_ids(args.profiles, SNAPSHOT_PATH, RETIRED_PATH)
|
||||
errors = check_filament_ids(args.profiles, SNAPSHOT_PATH)
|
||||
if errors:
|
||||
print_error(f"filament_id check: {errors} error(s)")
|
||||
return 1
|
||||
print_success("filament_id check: no errors")
|
||||
return 0
|
||||
|
||||
_changed, errors = assign_missing_ids(args.profiles, SNAPSHOT_PATH, RETIRED_PATH)
|
||||
_changed, errors = assign_missing_ids(args.profiles, SNAPSHOT_PATH)
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ $ProfilesDir = (Resolve-Path -LiteralPath $ProfilesDir).Path
|
||||
# validator compares the <Vendor>.json stem case-sensitively, while Windows' case-insensitive
|
||||
# filesystem would let "creality" pass a path test and then match no vendor. A vendor is a
|
||||
# <name>.json with a sibling <name> directory; that pair is also what tells one apart from
|
||||
# blacklist.json and retired_filament_ids.json, which sit in the same folder.
|
||||
# blacklist.json, which sits in the same folder.
|
||||
if ($Vendor) {
|
||||
$found = $false
|
||||
$suggestion = ''
|
||||
|
||||
@@ -138,7 +138,7 @@ PROFILES_DIR="$(cd -- "${PROFILES_DIR}" && pwd)"
|
||||
# validator compares the <Vendor>.json stem case-sensitively, while a case-insensitive filesystem
|
||||
# (macOS, Windows) would let "creality" pass a file test and then match no vendor. A vendor is a
|
||||
# <name>.json with a sibling <name>/ directory; that pair is also what tells one apart from
|
||||
# blacklist.json and retired_filament_ids.json, which sit in the same folder.
|
||||
# blacklist.json, which sits in the same folder.
|
||||
if [ -n "${VENDOR}" ]; then
|
||||
wanted="$(printf '%s' "${VENDOR}" | tr '[:upper:]' '[:lower:]')"
|
||||
found=""
|
||||
|
||||
@@ -54,14 +54,13 @@ def preset(name, filament_id=None, inherits=None, instantiation=True,
|
||||
|
||||
|
||||
class SyntheticTree:
|
||||
"""A throwaway resources/profiles-shaped directory plus ledger paths."""
|
||||
"""A throwaway resources/profiles-shaped directory plus a snapshot path."""
|
||||
|
||||
def __init__(self):
|
||||
self.dir = tempfile.mkdtemp(prefix="filament_id_test_")
|
||||
self.profiles = os.path.join(self.dir, "profiles")
|
||||
os.makedirs(self.profiles)
|
||||
self.snapshot = os.path.join(self.dir, "filament_id_snapshot.json")
|
||||
self.retired = os.path.join(self.dir, "retired_filament_ids.json")
|
||||
|
||||
def cleanup(self):
|
||||
shutil.rmtree(self.dir, ignore_errors=True)
|
||||
@@ -109,37 +108,31 @@ class SyntheticTree:
|
||||
with open(idx_path, "w", encoding="utf-8") as f:
|
||||
json.dump(index, f, indent=4, ensure_ascii=False)
|
||||
|
||||
def write_ledger(self, retired=None, hints=None):
|
||||
afi.write_ledger(self.retired, {"retired": retired or {},
|
||||
"hints": hints or {}})
|
||||
|
||||
# -- pipeline wrappers ---------------------------------------------------
|
||||
|
||||
def update_snapshot(self, allow_shared_catalog=False, forget_path=None):
|
||||
def update_snapshot(self, allow_shared_catalog=False):
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
rc = afi.update_snapshot(self.profiles, self.snapshot, self.retired,
|
||||
allow_shared_catalog=allow_shared_catalog,
|
||||
forget_path=forget_path)
|
||||
rc = afi.update_snapshot(self.profiles, self.snapshot,
|
||||
allow_shared_catalog=allow_shared_catalog)
|
||||
return rc, buf.getvalue()
|
||||
|
||||
def check(self):
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
errors = afi.check_filament_ids(self.profiles, self.snapshot, self.retired)
|
||||
errors = afi.check_filament_ids(self.profiles, self.snapshot)
|
||||
return errors, buf.getvalue()
|
||||
|
||||
def assign(self):
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
changed, errors = afi.assign_missing_ids(self.profiles, self.snapshot,
|
||||
self.retired)
|
||||
changed, errors = afi.assign_missing_ids(self.profiles, self.snapshot)
|
||||
return changed, errors, buf.getvalue()
|
||||
|
||||
def remint(self, vendors):
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
changed, errors = afi.remint_vendors(vendors, self.profiles, self.retired)
|
||||
changed, errors = afi.remint_vendors(vendors, self.profiles)
|
||||
return changed, errors, buf.getvalue()
|
||||
|
||||
def drop_redundant(self, vendor):
|
||||
@@ -148,19 +141,6 @@ class SyntheticTree:
|
||||
dropped, errors = afi.drop_redundant_ids(vendor, self.profiles)
|
||||
return dropped, errors, buf.getvalue()
|
||||
|
||||
def add_hint(self, pairs):
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
rc = afi.add_hints(pairs, self.profiles, self.retired)
|
||||
return rc, buf.getvalue()
|
||||
|
||||
def retire(self, pairs):
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
rc = afi.retire_ids(pairs, self.profiles, self.retired)
|
||||
return rc, buf.getvalue()
|
||||
|
||||
|
||||
def make_clean_tree():
|
||||
"""Baseline tree: OFL base+generic, a vendor family, a clean tuned generic."""
|
||||
t = SyntheticTree()
|
||||
@@ -196,7 +176,7 @@ class SyntheticTreeCase(unittest.TestCase):
|
||||
|
||||
class TestMint(unittest.TestCase):
|
||||
def test_namespace_literal(self):
|
||||
# Frozen: derived from the setting_id namespace; baked into the ledger.
|
||||
# Frozen: derived from the setting_id namespace; baked into the snapshot.
|
||||
self.assertEqual(afi.FILAMENT_ID_NAMESPACE,
|
||||
uuid.UUID("c4d3ff49-4c32-5534-a3e3-00894157ab97"))
|
||||
|
||||
@@ -401,7 +381,7 @@ class TestTripleResolution(unittest.TestCase):
|
||||
class TestReservedSpaces(unittest.TestCase):
|
||||
def test_owners(self):
|
||||
self.assertEqual(afi.reserved_space_owner("GFL99"), (True, "BBL"))
|
||||
# dissolved island (plan v4): reserved, but no vendor may declare it
|
||||
# Qidi device protocol: 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("pAbCdEf1"), (True, None)) # case-insensitive
|
||||
@@ -412,7 +392,7 @@ class TestReservedSpaces(unittest.TestCase):
|
||||
def test_island_declarations(self):
|
||||
self.assertTrue(afi.is_island_declaration("BBL", "GFL99"))
|
||||
self.assertTrue(afi.is_island_declaration("BBL", "OF5CgdDq"))
|
||||
# dissolved island (plan v4): Qidi declarations mint like any other
|
||||
# Qidi presets mint like any other vendor's:
|
||||
self.assertFalse(afi.is_island_declaration("Qidi", "QD_X4_PLA"))
|
||||
self.assertFalse(afi.is_island_declaration("Qidi", "OF5CgdDq"))
|
||||
self.assertFalse(afi.is_island_declaration("VendorA", "GFL99"))
|
||||
@@ -503,15 +483,7 @@ class TestChecks(SyntheticTreeCase):
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 0, out)
|
||||
|
||||
def test_check4_retired_id_reuse(self):
|
||||
self.t.write_ledger(retired={"AX01": {"claims": ["VendorA/APLA"],
|
||||
"successor": None}})
|
||||
errors, out = self.t.check()
|
||||
self.assertGreater(errors, 0)
|
||||
self.assertIn("retired", out)
|
||||
self.assertIn("AX01", out)
|
||||
|
||||
def test_check5_renamed_tuned_generic(self):
|
||||
def test_check4_renamed_tuned_generic(self):
|
||||
self.t.write_preset("VendorA", preset("Tuned PLA @P1",
|
||||
inherits="Generic PLA @System",
|
||||
compatible_printers=["P1"]))
|
||||
@@ -520,7 +492,7 @@ class TestChecks(SyntheticTreeCase):
|
||||
self.assertIn("rename re-exposes the OFL preset", out)
|
||||
self.assertIn("Tuned PLA @P1", out)
|
||||
|
||||
def test_check5_empty_compatible_printers(self):
|
||||
def test_check4_empty_compatible_printers(self):
|
||||
self.t.write_preset("VendorA", preset("Generic PLA @P2",
|
||||
inherits="Generic PLA @System",
|
||||
compatible_printers=[]))
|
||||
@@ -528,7 +500,7 @@ class TestChecks(SyntheticTreeCase):
|
||||
self.assertGreater(errors, 0)
|
||||
self.assertIn("cannot shadow the OFL preset", out)
|
||||
|
||||
def test_check5_own_id_key_on_ofl_rider(self):
|
||||
def test_check4_own_id_key_on_ofl_rider(self):
|
||||
# A vendor root that declares its own id while inheriting an OFL family
|
||||
# forks the family off the catalog id.
|
||||
self.t.write_preset("VendorA", preset("GPLA @base", filament_id="AX77",
|
||||
@@ -540,7 +512,7 @@ class TestChecks(SyntheticTreeCase):
|
||||
self.assertGreater(errors, 0)
|
||||
self.assertIn('declares its own filament_id "AX77"', out)
|
||||
|
||||
def test_check5_applies_to_non_generic_ofl_families(self):
|
||||
def test_check4_applies_to_non_generic_ofl_families(self):
|
||||
fid = afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA")
|
||||
self.t.write_preset(OFL, preset("poly_pla_base", filament_id=fid,
|
||||
instantiation=False,
|
||||
@@ -559,7 +531,7 @@ class TestChecks(SyntheticTreeCase):
|
||||
self.assertIn("rename re-exposes the OFL preset", out)
|
||||
self.assertIn("PolyX PLA @P1", out)
|
||||
|
||||
def test_check5_exception_is_grandfathered(self):
|
||||
def test_check4_exception_is_grandfathered(self):
|
||||
self.t.write_preset("VendorA", preset("Tuned PLA @P1",
|
||||
inherits="Generic PLA @System",
|
||||
compatible_printers=["P1"]))
|
||||
@@ -568,9 +540,9 @@ class TestChecks(SyntheticTreeCase):
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 0, out)
|
||||
|
||||
def test_check6_reserved_namespace_claims(self):
|
||||
def test_check5_reserved_namespace_claims(self):
|
||||
for fid, marker in [("GFX99", "owned by BBL"),
|
||||
("QD_X_PLA", "dissolved island"),
|
||||
("QD_X_PLA", "composed by the device"),
|
||||
("P1a2b3c4", "user-custom"),
|
||||
("null", "user-custom")]:
|
||||
with self.subTest(fid=fid):
|
||||
@@ -586,7 +558,7 @@ class TestChecks(SyntheticTreeCase):
|
||||
self.assertIn("reserved id space", out)
|
||||
self.assertIn(marker, out)
|
||||
|
||||
def test_check7a_instantiated_preset_with_own_key(self):
|
||||
def test_check6a_instantiated_preset_with_own_key(self):
|
||||
self.t.write_preset("VendorA", preset("APLA @P1", filament_id="AX01",
|
||||
inherits="APLA @base",
|
||||
compatible_printers=["P1 0.4 nozzle"]),
|
||||
@@ -595,7 +567,7 @@ class TestChecks(SyntheticTreeCase):
|
||||
self.assertGreater(errors, 0)
|
||||
self.assertIn("declares its own filament_id key", out)
|
||||
|
||||
def test_check7b_declared_vs_inherited_drift(self):
|
||||
def test_check6b_declared_vs_inherited_drift(self):
|
||||
self.t.write_preset("VendorA", preset("APLA @P1", filament_id="AX02",
|
||||
inherits="APLA @base",
|
||||
compatible_printers=["P1 0.4 nozzle"]),
|
||||
@@ -604,7 +576,7 @@ class TestChecks(SyntheticTreeCase):
|
||||
self.assertGreater(errors, 0)
|
||||
self.assertIn('declares filament_id "AX02" but its inherits chain resolves "AX01"', out)
|
||||
|
||||
def test_check7c_unresolvable_instantiated_filament(self):
|
||||
def test_check6c_unresolvable_instantiated_filament(self):
|
||||
self.t.write_preset("VendorA", preset("DNEW @P1", compatible_printers=["P1"]))
|
||||
errors, out = self.t.check()
|
||||
self.assertGreater(errors, 0)
|
||||
@@ -618,8 +590,8 @@ class TestChecks(SyntheticTreeCase):
|
||||
self.assertIn("snapshot not found", out)
|
||||
|
||||
|
||||
class TestCheck8(SyntheticTreeCase):
|
||||
def test_8a_empty_vendor_is_hard_error(self):
|
||||
class TestCheck7(SyntheticTreeCase):
|
||||
def test_7a_empty_vendor_is_hard_error(self):
|
||||
fid = afi.generate_filament_id("", "PLA", "NVPLA")
|
||||
self.t.write_preset("VendorA", preset("NVPLA @base", filament_id=fid,
|
||||
instantiation=False,
|
||||
@@ -637,7 +609,7 @@ class TestCheck8(SyntheticTreeCase):
|
||||
self.assertEqual(errors, 1, out)
|
||||
self.assertIn("resolves empty filament_vendor", out)
|
||||
|
||||
def test_8b_divergent_family_triples(self):
|
||||
def test_7b_divergent_family_triples(self):
|
||||
id1 = afi.generate_filament_id("MV", "PLA", "MPLA")
|
||||
id2 = afi.generate_filament_id("MV", "PETG", "MPLA")
|
||||
self.t.write_preset("VendorA", preset("MPLA @base1", filament_id=id1,
|
||||
@@ -660,7 +632,7 @@ class TestCheck8(SyntheticTreeCase):
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 0, out)
|
||||
|
||||
def test_8_cross_bundle_divergence_is_warning_only(self):
|
||||
def test_7_cross_bundle_divergence_is_warning_only(self):
|
||||
fid = afi.generate_filament_id("BV", "PETG", "APLA")
|
||||
self.t.add_vendor("VendorB", [
|
||||
preset("APLA @base", filament_id=fid, instantiation=False,
|
||||
@@ -677,125 +649,6 @@ class TestCheck8(SyntheticTreeCase):
|
||||
self.assertIn('"APLA"', out)
|
||||
|
||||
|
||||
class TestCheck9(SyntheticTreeCase):
|
||||
def test_ok_chain_null_and_live(self):
|
||||
self.t.write_ledger(retired={
|
||||
"R1": {"claims": ["VendorA/Gone1"], "successor": "AX01"},
|
||||
"R2": {"claims": ["VendorA/Gone2"], "successor": "R1"},
|
||||
"R3": {"claims": ["VendorA/Gone3"], "successor": None},
|
||||
})
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 0, out)
|
||||
|
||||
def test_cycle_is_an_error(self):
|
||||
self.t.write_ledger(retired={
|
||||
"C1": {"claims": [], "successor": "C2"},
|
||||
"C2": {"claims": [], "successor": "C1"},
|
||||
})
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 2, out)
|
||||
self.assertIn("cycles", out)
|
||||
|
||||
def test_dead_end_is_an_error(self):
|
||||
self.t.write_ledger(retired={
|
||||
"D1": {"claims": [], "successor": "OFnope99"},
|
||||
})
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 1, out)
|
||||
self.assertIn("dead-ends", out)
|
||||
self.assertIn("OFnope99", out)
|
||||
|
||||
def test_hint_key_must_be_in_island_space(self):
|
||||
self.t.write_ledger(hints={"AX01": "OGFL99"})
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 1, out)
|
||||
self.assertIn("not in an island id space", out)
|
||||
|
||||
def test_hint_key_may_be_absent_island_id(self):
|
||||
# Released / never-shipped island-catalog ids are valid hint keys.
|
||||
self.t.write_ledger(hints={"GFXX99": "AX01"})
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 0, out)
|
||||
|
||||
def test_hint_key_must_not_be_retired_too(self):
|
||||
self.t.write_ledger(retired={"GFXX99": {"claims": [], "successor": "AX01"}},
|
||||
hints={"GFXX99": "AX01"})
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 1, out)
|
||||
self.assertIn("never both", out)
|
||||
|
||||
def test_live_hint_key_with_non_island_declarer_is_an_error(self):
|
||||
self.t.write_preset("VendorA", preset(
|
||||
"GPLA @base", filament_id="GFZZ01", instantiation=False,
|
||||
filament_vendor="Generic", filament_type="PLA"))
|
||||
self.t.write_preset("VendorA", preset(
|
||||
"GPLA @P1", inherits="GPLA @base",
|
||||
compatible_printers=["P1 0.4 nozzle"]))
|
||||
rc, _out = self.t.update_snapshot(allow_shared_catalog=True)
|
||||
self.assertEqual(rc, 0)
|
||||
self.t.write_ledger(hints={"GFZZ01": "AX01"})
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 1, out)
|
||||
self.assertIn("re-mint those declarers first", out)
|
||||
|
||||
def test_island_hint_ok_and_value_must_chain_live(self):
|
||||
self.t.add_vendor("BBL", [
|
||||
preset("BPLA @base", filament_id="GFB01", instantiation=False),
|
||||
preset("BPLA @X1C", inherits="BPLA @base",
|
||||
compatible_printers=["X1C 0.4 nozzle"]),
|
||||
])
|
||||
rc, _out = self.t.update_snapshot()
|
||||
self.assertEqual(rc, 0)
|
||||
self.t.write_ledger(hints={"GFB01": "AX01"})
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 0, out)
|
||||
# A hint chain ending on a null retirement is dead for the runtime.
|
||||
self.t.write_ledger(retired={"R9": {"claims": [], "successor": None}},
|
||||
hints={"GFB01": "R9"})
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 1, out)
|
||||
self.assertIn("chain-terminate at a live tree id", out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# successor mode rule + ledger schema migration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestSuccessorMode(unittest.TestCase):
|
||||
def test_majority_wins(self):
|
||||
claim_ids = {"V/A": {"ID1"}, "V/B": {"ID1"}, "V/C": {"ID2"}}
|
||||
succ, counts = afi.successor_by_mode(["V/A", "V/B", "V/C"], claim_ids)
|
||||
self.assertEqual(succ, "ID1")
|
||||
self.assertEqual(counts, {"ID1": 2, "ID2": 1})
|
||||
|
||||
def test_tie_breaks_lexicographically(self):
|
||||
claim_ids = {"V/A": {"IDB"}, "V/B": {"IDA"}}
|
||||
succ, counts = afi.successor_by_mode(["V/A", "V/B"], claim_ids)
|
||||
self.assertEqual(succ, "IDA")
|
||||
self.assertEqual(counts, {"IDA": 1, "IDB": 1})
|
||||
|
||||
def test_no_surviving_family_means_null(self):
|
||||
self.assertEqual(afi.successor_by_mode(["V/GONE"], {}), (None, {}))
|
||||
self.assertEqual(afi.successor_by_mode([], {"V/A": {"ID1"}}), (None, {}))
|
||||
|
||||
|
||||
class TestLedgerMigration(SyntheticTreeCase):
|
||||
def test_backfill_successors_via_mode_rule(self):
|
||||
analysis = afi.analyze_tree(self.t.profiles)
|
||||
claim_ids = afi.claim_effective_ids(analysis)
|
||||
old = {
|
||||
"X9": ["VendorA/APLA"], # family lives on
|
||||
"Y9": ["VendorA/GONE"], # family died
|
||||
"Z9": {"claims": ["kept"], "successor": "AX01"}, # already migrated
|
||||
}
|
||||
migrated = afi.migrate_retired_entries(old, claim_ids)
|
||||
self.assertEqual(migrated["X9"],
|
||||
{"claims": ["VendorA/APLA"], "successor": "AX01"})
|
||||
self.assertEqual(migrated["Y9"],
|
||||
{"claims": ["VendorA/GONE"], "successor": None})
|
||||
self.assertEqual(migrated["Z9"], old["Z9"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# --update-snapshot
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -820,108 +673,6 @@ class TestUpdateSnapshot(SyntheticTreeCase):
|
||||
self.assertEqual(snap["triples"]["OGFL99"], [["Generic", "PLA", "fdm_pla"]])
|
||||
self.assertEqual(snap["triple_exceptions"], [])
|
||||
|
||||
def test_vanished_island_id_released_with_hint_not_retired(self):
|
||||
self.t.write_preset("VendorA", preset(
|
||||
"GPLA @base", filament_id="GFZZ01", instantiation=False,
|
||||
filament_vendor="Generic", filament_type="PLA"))
|
||||
self.t.write_preset("VendorA", preset(
|
||||
"GPLA @P1", inherits="GPLA @base",
|
||||
compatible_printers=["P1 0.4 nozzle"]))
|
||||
rc, _out = self.t.update_snapshot(allow_shared_catalog=True)
|
||||
self.assertEqual(rc, 0)
|
||||
# The family re-mints away from the island id: released, not retired.
|
||||
self.t.write_preset("VendorA", preset(
|
||||
"GPLA @base", filament_id="OFaaaaaa", instantiation=False,
|
||||
filament_vendor="Generic", filament_type="PLA"), register=False)
|
||||
rc, out = self.t.update_snapshot()
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("released to the BBL island space", out)
|
||||
ledger = afi.load_ledger(self.t.retired)
|
||||
self.assertNotIn("GFZZ01", ledger["retired"])
|
||||
self.assertEqual(ledger["hints"].get("GFZZ01"), "OFaaaaaa")
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 0, out)
|
||||
# The island may legitimately (re)ship a released id later.
|
||||
self.t.add_vendor("BBL", [
|
||||
preset("BPLA @base", filament_id="GFZZ01", instantiation=False),
|
||||
preset("BPLA @X1C", inherits="BPLA @base",
|
||||
compatible_printers=["X1C 0.4 nozzle"]),
|
||||
])
|
||||
rc, _out = self.t.update_snapshot()
|
||||
self.assertEqual(rc, 0)
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 0, out)
|
||||
|
||||
def test_vanished_id_is_retired_with_null_successor(self):
|
||||
self.t.remove_preset("VendorA", "APLA @base")
|
||||
self.t.remove_preset("VendorA", "APLA @P1")
|
||||
rc, out = self.t.update_snapshot()
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("AX01", out)
|
||||
retired = load_json_file(self.t.retired)["retired"]
|
||||
self.assertEqual(retired,
|
||||
{"AX01": {"claims": ["VendorA/APLA"], "successor": None}})
|
||||
snap = load_json_file(self.t.snapshot)
|
||||
self.assertNotIn("AX01", snap["ids"])
|
||||
self.assertNotIn("AX01", snap["triples"])
|
||||
# append-only + idempotent: a second run keeps the ledger intact
|
||||
rc, out = self.t.update_snapshot()
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("nothing changed", out)
|
||||
self.assertEqual(retired, load_json_file(self.t.retired)["retired"])
|
||||
# ... and a retired id may never be minted again
|
||||
self.assertNotIn(afi.mint_filament_id("AVendor", "PLA", "APLA",
|
||||
set(retired)), retired)
|
||||
|
||||
def test_vanished_id_successor_follows_surviving_family(self):
|
||||
# The family lives on under a new id (a re-mint): the mode rule finds it.
|
||||
self.t.write_preset("VendorA", preset("APLA @base", filament_id="AXNEW",
|
||||
instantiation=False,
|
||||
filament_vendor="AVendor",
|
||||
filament_type="PLA"),
|
||||
register=False)
|
||||
rc, out = self.t.update_snapshot()
|
||||
self.assertEqual(rc, 0)
|
||||
retired = load_json_file(self.t.retired)["retired"]
|
||||
self.assertEqual(retired["AX01"],
|
||||
{"claims": ["VendorA/APLA"], "successor": "AXNEW"})
|
||||
|
||||
def test_multi_candidate_retirement_warns_and_ties_break(self):
|
||||
self.t.write_preset("VendorA", preset("SHRD @base", filament_id="SH01",
|
||||
instantiation=False,
|
||||
filament_vendor="SV",
|
||||
filament_type="PLA"))
|
||||
self.t.write_preset("VendorA", preset("CH1 @P1", inherits="SHRD @base",
|
||||
compatible_printers=["P1"]))
|
||||
self.t.write_preset("VendorA", preset("CH2 @P1", inherits="SHRD @base",
|
||||
compatible_printers=["P1"]))
|
||||
rc, _out = self.t.update_snapshot()
|
||||
self.assertEqual(rc, 0)
|
||||
# Split the two families onto their own roots and drop the shared id.
|
||||
self.t.write_preset("VendorA", preset("CH1 @base", filament_id="SA01",
|
||||
instantiation=False,
|
||||
filament_vendor="SV",
|
||||
filament_type="PLA"))
|
||||
self.t.write_preset("VendorA", preset("CH2 @base", filament_id="SB01",
|
||||
instantiation=False,
|
||||
filament_vendor="SV",
|
||||
filament_type="PLA"))
|
||||
self.t.write_preset("VendorA", preset("CH1 @P1", inherits="CH1 @base",
|
||||
compatible_printers=["P1"]),
|
||||
register=False)
|
||||
self.t.write_preset("VendorA", preset("CH2 @P1", inherits="CH2 @base",
|
||||
compatible_printers=["P1"]),
|
||||
register=False)
|
||||
self.t.remove_preset("VendorA", "SHRD @base")
|
||||
rc, out = self.t.update_snapshot()
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("[WARNING]", out)
|
||||
self.assertIn("SB01", out) # the losing candidate is named
|
||||
retired = load_json_file(self.t.retired)["retired"]
|
||||
self.assertEqual(retired["SH01"]["successor"], "SA01")
|
||||
self.assertEqual(retired["SH01"]["claims"],
|
||||
["VendorA/CH1", "VendorA/CH2"])
|
||||
|
||||
def test_refuses_new_reserved_namespace_claims(self):
|
||||
self.t.write_preset("VendorA", preset("CNEW @base", filament_id="GFX99",
|
||||
instantiation=False,
|
||||
@@ -942,181 +693,6 @@ class TestUpdateSnapshot(SyntheticTreeCase):
|
||||
self.assertEqual(snap["ids"]["GFX99"], ["VendorA/CNEW"])
|
||||
|
||||
|
||||
class TestForgetNeverShipped(SyntheticTreeCase):
|
||||
def forget_file(self, ids):
|
||||
path = os.path.join(self.t.dir, "forget.json")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(ids, f)
|
||||
return path
|
||||
|
||||
def test_refuses_when_listed_id_is_live(self):
|
||||
with open(self.t.snapshot, "rb") as f:
|
||||
before = f.read()
|
||||
rc, out = self.t.update_snapshot(forget_path=self.forget_file(["AX01"]))
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("still in the tree", out)
|
||||
with open(self.t.snapshot, "rb") as f:
|
||||
self.assertEqual(f.read(), before)
|
||||
|
||||
def test_forgotten_id_is_not_retired(self):
|
||||
self.t.write_preset("VendorA", preset("ZN @base", filament_id="Y999",
|
||||
instantiation=False,
|
||||
filament_vendor="ZV",
|
||||
filament_type="PLA"))
|
||||
self.t.write_preset("VendorA", preset("ZN @P1", inherits="ZN @base",
|
||||
compatible_printers=["P1"]))
|
||||
rc, _out = self.t.update_snapshot()
|
||||
self.assertEqual(rc, 0)
|
||||
self.t.remove_preset("VendorA", "ZN @base")
|
||||
self.t.remove_preset("VendorA", "ZN @P1")
|
||||
rc, out = self.t.update_snapshot(forget_path=self.forget_file(["Y999"]))
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("forgotten (never shipped)", out)
|
||||
self.assertNotIn("Y999", load_json_file(self.t.retired)["retired"])
|
||||
self.assertNotIn("Y999", load_json_file(self.t.snapshot)["ids"])
|
||||
|
||||
def test_splices_chains_through_forgotten_id(self):
|
||||
self.t.write_preset("VendorA", preset("ZN @base", filament_id="Y999",
|
||||
instantiation=False,
|
||||
filament_vendor="ZV",
|
||||
filament_type="PLA"))
|
||||
self.t.write_preset("VendorA", preset("ZN @P1", inherits="ZN @base",
|
||||
compatible_printers=["P1"]))
|
||||
rc, _out = self.t.update_snapshot()
|
||||
self.assertEqual(rc, 0)
|
||||
# A shipped id already forwards to Y999, which turns out never shipped.
|
||||
self.t.write_ledger(retired={
|
||||
"OLD1": {"claims": ["VendorA/ZN"], "successor": "Y999"}})
|
||||
# The family re-mints to NEWZ and Y999 vanishes.
|
||||
self.t.write_preset("VendorA", preset("ZN @base", filament_id="NEWZ",
|
||||
instantiation=False,
|
||||
filament_vendor="ZV",
|
||||
filament_type="PLA"),
|
||||
register=False)
|
||||
rc, out = self.t.update_snapshot(forget_path=self.forget_file(["Y999"]))
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("spliced succession", out)
|
||||
retired = load_json_file(self.t.retired)["retired"]
|
||||
self.assertNotIn("Y999", retired)
|
||||
self.assertEqual(retired["OLD1"],
|
||||
{"claims": ["VendorA/ZN"], "successor": "NEWZ"})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# --add-hint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestAddHint(SyntheticTreeCase):
|
||||
def add_bbl(self):
|
||||
self.t.add_vendor("BBL", [
|
||||
preset("BPLA @base", filament_id="GFB01", instantiation=False),
|
||||
preset("BPLA @X1C", inherits="BPLA @base",
|
||||
compatible_printers=["X1C 0.4 nozzle"]),
|
||||
])
|
||||
rc, _out = self.t.update_snapshot()
|
||||
assert rc == 0
|
||||
|
||||
def test_adds_island_hint(self):
|
||||
self.add_bbl()
|
||||
rc, out = self.t.add_hint(["GFB01=AX01"])
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertEqual(load_json_file(self.t.retired)["hints"],
|
||||
{"GFB01": "AX01"})
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 0, out)
|
||||
|
||||
def test_rejects_non_island_old(self):
|
||||
rc, out = self.t.add_hint(["AX01=OGFL99"])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("not in an island id space", out)
|
||||
self.assertEqual(afi.load_ledger(self.t.retired)["hints"], {})
|
||||
|
||||
def test_accepts_absent_island_old(self):
|
||||
# Island catalogs own ids Orca never shipped (or has released).
|
||||
rc, out = self.t.add_hint(["GFXX99=AX01"])
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertEqual(load_json_file(self.t.retired)["hints"],
|
||||
{"GFXX99": "AX01"})
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 0, out)
|
||||
|
||||
def test_rejects_retired_old(self):
|
||||
self.t.write_ledger(retired={"ROLD": {"claims": [], "successor": None}})
|
||||
rc, out = self.t.add_hint(["ROLD=AX01"])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("is retired", out)
|
||||
|
||||
def test_rejects_dead_target(self):
|
||||
self.add_bbl()
|
||||
rc, out = self.t.add_hint(["GFB01=OFnope99"])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("not a live tree id", out)
|
||||
|
||||
def test_rejects_malformed_pair(self):
|
||||
rc, out = self.t.add_hint(["GFB01"])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn('expects "OLD=NEW"', out)
|
||||
|
||||
|
||||
class TestRetire(SyntheticTreeCase):
|
||||
def test_retires_vanished_id_with_successor(self):
|
||||
rc, out = self.t.retire(["OFOLD001=AX01"])
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertEqual(load_json_file(self.t.retired)["retired"]["OFOLD001"],
|
||||
{"claims": [], "successor": "AX01"})
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 0, out)
|
||||
|
||||
def test_rejects_live_old(self):
|
||||
rc, out = self.t.retire(["AX01=OGFL99"])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("still lives in the tree", out)
|
||||
self.assertEqual(afi.load_ledger(self.t.retired)["retired"], {})
|
||||
|
||||
def test_rejects_declaration_only_old(self):
|
||||
# The inert-fork case: an id declared by a claim-less root still lives
|
||||
# in the tree; the declaration must be deleted before --retire.
|
||||
self.t.write_preset("VendorA", preset("ZOLD @base", filament_id="ZDECL01",
|
||||
instantiation=False,
|
||||
filament_vendor="ZV",
|
||||
filament_type="PLA"))
|
||||
rc, out = self.t.retire(["ZDECL01=AX01"])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("still lives in the tree", out)
|
||||
|
||||
def test_rejects_island_space_old(self):
|
||||
rc, out = self.t.retire(["GFXX99=AX01"])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("reserved id space", out)
|
||||
|
||||
def test_rejects_user_space_old(self):
|
||||
rc, out = self.t.retire(["P1234ABC=AX01"])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("reserved id space", out)
|
||||
|
||||
def test_rejects_already_retired_old(self):
|
||||
self.t.write_ledger(retired={"ROLD": {"claims": [], "successor": None}})
|
||||
rc, out = self.t.retire(["ROLD=AX01"])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("already retired", out)
|
||||
|
||||
def test_rejects_hinted_old(self):
|
||||
self.t.write_ledger(hints={"HOLD": "AX01"})
|
||||
rc, out = self.t.retire(["HOLD=AX01"])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("hint key", out)
|
||||
|
||||
def test_rejects_dead_successor(self):
|
||||
rc, out = self.t.retire(["OFOLD001=OFnope99"])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("not a live tree id", out)
|
||||
|
||||
def test_rejects_malformed_pair(self):
|
||||
rc, out = self.t.retire(["OFOLD001"])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn('expects "OLD=NEW"', out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# default run: mint + insert
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1373,24 +949,6 @@ class TestRemint(SyntheticTreeCase):
|
||||
a = load_json_file(self.t.preset_path("VendorA", "APLA @base"))
|
||||
self.assertEqual(a["filament_id"], want)
|
||||
|
||||
def test_blocked_by_retired_id(self):
|
||||
want0 = afi.generate_filament_id(*self.TRIPLE)
|
||||
self.t.write_ledger(retired={want0: {"claims": [], "successor": None}})
|
||||
changed, errors, _out = self.t.remint(["VendorA"])
|
||||
self.assertEqual((changed, errors), (1, 0))
|
||||
root = load_json_file(self.t.preset_path("VendorA", "APLA @base"))
|
||||
self.assertEqual(root["filament_id"],
|
||||
afi.generate_filament_id(*self.TRIPLE, salt=1))
|
||||
|
||||
def test_blocked_by_hint_key(self):
|
||||
want0 = afi.generate_filament_id(*self.TRIPLE)
|
||||
self.t.write_ledger(hints={want0: "AX01"})
|
||||
changed, errors, _out = self.t.remint(["VendorA"])
|
||||
self.assertEqual((changed, errors), (1, 0))
|
||||
root = load_json_file(self.t.preset_path("VendorA", "APLA @base"))
|
||||
self.assertEqual(root["filament_id"],
|
||||
afi.generate_filament_id(*self.TRIPLE, salt=1))
|
||||
|
||||
def test_blocked_by_other_triple_occurrence(self):
|
||||
want0 = afi.generate_filament_id(*self.TRIPLE)
|
||||
self.t.add_vendor("VendorB", [
|
||||
@@ -1481,63 +1039,11 @@ class TestRealTree(unittest.TestCase):
|
||||
self.assertEqual(analysis["missing_effective"], [])
|
||||
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.
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestReviewFixes(SyntheticTreeCase):
|
||||
def test_update_snapshot_refuses_retired_id_reuse(self):
|
||||
fid = afi.generate_filament_id("LV", "PLA", "LONER")
|
||||
self.t.write_preset("VendorA", preset("LONER @base", filament_id=fid,
|
||||
instantiation=False,
|
||||
filament_vendor="LV",
|
||||
filament_type="PLA"))
|
||||
self.t.write_preset("VendorA", preset("LONER @P1", inherits="LONER @base",
|
||||
compatible_printers=["P1 0.4 nozzle"]))
|
||||
rc, _ = self.t.update_snapshot()
|
||||
self.assertEqual(rc, 0)
|
||||
self.t.remove_preset("VendorA", "LONER @base")
|
||||
self.t.remove_preset("VendorA", "LONER @P1")
|
||||
rc, out = self.t.update_snapshot()
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertIn(fid, afi.load_retired(self.t.retired))
|
||||
# Resurrect the same id: --update-snapshot must refuse, not sanction.
|
||||
self.t.write_preset("VendorA", preset("LONER @base", filament_id=fid,
|
||||
instantiation=False,
|
||||
filament_vendor="LV",
|
||||
filament_type="PLA"))
|
||||
self.t.write_preset("VendorA", preset("LONER @P1", inherits="LONER @base",
|
||||
compatible_printers=["P1 0.4 nozzle"]))
|
||||
rc, out = self.t.update_snapshot()
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("retired", out)
|
||||
self.assertNotIn(fid, load_json_file(self.t.snapshot)["ids"])
|
||||
|
||||
def test_check3_skips_of_id_inherited_from_other_vendor(self):
|
||||
# An OFL family carries its own minted OF id and a vendor tunes it
|
||||
# correctly (same base name, non-empty printers). The new claim must
|
||||
@@ -1565,7 +1071,7 @@ class TestReviewFixes(SyntheticTreeCase):
|
||||
errors, out = self.t.check()
|
||||
self.assertEqual(errors, 0, out)
|
||||
|
||||
def test_check7c_prints_expected_mint(self):
|
||||
def test_check6c_prints_expected_mint(self):
|
||||
self.t.write_preset("VendorA", preset("Orphan PLA @P1",
|
||||
compatible_printers=["P1 0.4 nozzle"],
|
||||
filament_vendor="OV",
|
||||
|
||||
Reference in New Issue
Block a user