diff --git a/doc/developer-reference/filament_id.md b/doc/developer-reference/filament_id.md deleted file mode 100644 index c17e4e44e4..0000000000 --- a/doc/developer-reference/filament_id.md +++ /dev/null @@ -1,211 +0,0 @@ -# Filament IDs (`filament_id`) - -`filament_id` identifies a **material family**: one commercial product line = one id, shared by -all of that material's per-printer / per-nozzle variants. Devices use it to match a physical -spool or tray to a filament preset. It is never per-color, per-printer, per-nozzle, or -per-preset (per-preset identity is `setting_id`). - -This page is the rule for authoring `filament_id` in system profiles -(`resources/profiles/**`). CI enforces everything below; the short version is: - -> [!IMPORTANT] -> **Never write a `filament_id` value by hand.** New families get their id from -> `python scripts/assign_filament_ids.py`; existing families already have one — inherit it. - -## Who consumes the id - -Every device integration funnels a tray material id (`tray_info_idx`) through the same -matching pipeline (`PresetBundle::sync_ams_list` and friends): - -| Ecosystem | Where the id comes from | -| --- | --- | -| Bambu AMS | device side (RFID / user tray setting) — the `GF*` catalog | -| Qidi box | built from device enums (`QD_*`); needs an exactly matching visible preset | -| Creality CFS | runtime brand/type scoring returns the current preset's id | -| Klipper (AFC / Happy Hare) | runtime lookup by filament type | -| Snapmaker | runtime color/vendor/type match | - -Tray-to-preset matching is printer-scoped, but **several consumers match globally by id alone, -first hit wins**: tray display names, `filament_is_support`, vitrification warnings, and -multi-nozzle filament grouping in the slicing pipeline. Two *different* materials sharing one -id feed wrong data to those consumers even when the presets live in different vendors — so -cross-material id sharing is never safe. Within one printer, duplicate ids silently break AMS -matching (first match wins, the tray-edit dialog hides the second preset); the profile -validator's `-f` check rejects this. - -## Do I need a new id? The one-question test - -> **Would a user consider this a different spool product than anything already in the tree?** - -Different polymer, different sub-brand (Basic / Matte / Silk / HF), fiber-filled sibling, or a -second selectable diameter → **new family, new id**. The same spool tuned for another printer -or nozzle → **join the existing family** (inherit its `@base`, write no id key). Tuning a -generic material → **join the OrcaFilamentLibrary family** (inherit `Generic X @System`, keep -the `Generic X` base name, write no id key). - -| Situation | id | -| --- | --- | -| Per-printer / per-nozzle variant of an existing material | same id (inherit, never write the key) | -| Sub-brand or product line (PLA vs PLA Matte vs PLA Silk vs PLA HF) | new id each | -| Color | never a new id | -| Second diameter selectable on the same printer (1.75 + 2.85) | sibling family, new id | -| "High-speed" tuned for a *different printer model* | same id (it is a printer variant) | -| "High-speed" selectable *alongside* the normal preset on one printer | new id (it is a product line) | - -## Structure rules - -1. **Only family roots carry the key.** Root presets (any preset *not* marked - `"instantiation": "true"`, typically ` @base` with `"instantiation": "false"`) - declare `filament_id`; instantiated variants inherit a root and never - write the key. A family may have several roots (e.g. per-series bases) — all of them must - declare the *identical* id. -2. **The family name is the base name**: the preset name with everything from the first - (optionally space-preceded) `@` stripped. `MyBrand PLA @Orca 3D Fuse1` and `MyBrand PLA@HS` - both belong to family `MyBrand PLA`. -3. **Within a family, variants' `compatible_printers` are pairwise disjoint** — per printer - preset, at most one compatible instantiated preset per id. The C++ validator (`-f`) - enforces this. -4. **Generics belong to OrcaFilamentLibrary.** A vendor tuning a generic material inherits - `Generic X @System`, keeps the `Generic X` base name (that alias is what hides the library - preset on your printers), sets a non-empty `compatible_printers`, and writes no id key. - A vendor-*branded* filament never rides a generic family id. -5. **Ids follow the product identity.** The id is a pure function of the product triple - `(filament_vendor, filament_type, family name)` — correcting any of them re-mints the id - **by design**, and `--update-snapshot` records the old id in the shipped succession ledger - with its successor so device trays, calibration records, and user presets keep resolving - (`renamed_from` still gates preset-*name* compatibility as before). A shipped id is never - recycled for a different material: retired ids are blocked forever. - -## Minting — nobody invents ids - -New ids are deterministic, computed exactly like the `setting_id` precedent -(`scripts/assign_vendor_setting_ids.py`): - -```text -FILAMENT_ID_NAMESPACE = uuid5(setting-id NAMESPACE, "filament_id") - = c4d3ff49-4c32-5534-a3e3-00894157ab97 -filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE, - "filament_product///") ) -``` - -`base62_6` is the low 6 base62 digits (alphabet `0-9A-Za-z`) of the UUID taken as a big-endian -integer, most-significant digit first — 8 chars total, within the AMS length limit. The triple -comes from the family root's *flattened* config: `` is the filament -**manufacturer** (`"Polymaker"`, or `"Generic"` for generics — never the printer brand), -`` the material type, `` the root's base name; the two config -values are inheritable list options and the first element counts. The key contains no bundle -name, so the same product mints the same id in every bundle — hoisting a family into -OrcaFilamentLibrary never changes its id. On the rare collision with any existing or retired -id, the minter salts the input (`…/1`, `…/2`, …) until free and the result is frozen in the -file. Example: `Polymaker/PLA/PolyLite PLA` mints `OF5CgdDq`. - -Workflow for a new family: - -```bash -# 1. Author the family with NO filament_id key anywhere. -python scripts/assign_filament_ids.py # 2. mint + insert ids into the family root(s) -python scripts/assign_filament_ids.py --update-snapshot # 3. record the new claims in the ledger -python scripts/assign_filament_ids.py --check # 4. verify — the same checks CI runs -# 5. Commit the profile edits together with scripts/filament_id_snapshot.json. -``` - -`--mint "filament_vendor/filament_type/family_name"` prints the id a triple would mint without -touching anything. The default run is idempotent and never rewrites a valid existing id. -Maintenance modes (normally only used by id migrations): `--remint VENDOR` re-derives a -vendor's declared ids from their triples (a declaration already equal to a salt iteration -of its own triple is conformant and left alone — deliberate salt splits keeping two -presets of one product apart for per-printer AMS matching survive), -`--drop-redundant-ids VENDOR` deletes declarations -that merely re-declare an inherited OFL id, `--add-hint "OLD=NEW"` records a cross-island -succession hint, and `--retire "OLD=NEW"` records succession for a shipped non-island id -that vanished while another declarer kept it alive (lineage the automatic claim vote can no -longer see). - -If you skip the tooling, CI fails and prints the remedy: the expected id for your family, and -the instruction to run `python scripts/assign_filament_ids.py --update-snapshot` and commit -the resulting diff. - -## Reserved namespaces — never mint or hand-write into - -| Space | Owner | Rule | -| --- | --- | --- | -| `GF*` | Bambu AMS/RFID catalog | BBL vendor only; byte-copies elsewhere only where the snapshot already sanctions them | -| `QD_*` | Qidi device protocol | frozen device contract; Qidi vendor only | -| `P` + 7 hex chars (case-insensitive), `"null"` | user-created custom filaments (`CreatePresetsDialog.cpp`) | never appears in system profiles | -| every already-shipped id | grandfather snapshot | frozen as-is; new claims need maintainer sign-off | -| every retired id | `resources/profiles/retired_filament_ids.json` | never used again, for anything | - -## The succession ledger - -`resources/profiles/retired_filament_ids.json` ships with the app. Each retired id maps to -`{"claims": [...], "successor": }` — successor chains are followed to the live end — -and a `hints` map carries the same forwarding for ids Orca cannot retire because another -island owns them (e.g. a `GF*` id whose material also exists as an OFL family). The client -consults the ledger **only on resolution miss** (AMS tray sync, tray-id type lookup, -calibration history, filament-id preset lookup): a live preset always wins first, so BBL -installs resolve `GF*` natively and behavior is unchanged wherever the raw id still exists. -This is what makes identity-driven re-mints (structure rule 5) safe: the old id keeps -resolving to the family's current preset instead of degrading to a `Generic ` fallback. -The file is append-only and maintained exclusively by `--update-snapshot` / `--add-hint` / -`--retire`. - -## How CI enforces this - -Profile CI (`check_profiles.yml` → `scripts/orca_extra_profile_check.py`) runs -`check_filament_ids()` tree-wide. Its ground truth is -**`scripts/filament_id_snapshot.json` — the sanctioned state**: the id state derived from the -tree must equal the snapshot exactly, in both directions. Any change to the id landscape -therefore surfaces as a diff to that file, and **that snapshot diff is what maintainers review -and gate in a PR**. Never edit the snapshot by hand — `--update-snapshot` regenerates it -deterministically (running it twice changes nothing). - -The checks, in brief: - -- **Format** — every id is either in the snapshot, `OF` + 6 base62 chars, BBL's, or Qidi `QD_*`. -- **Snapshot equality** — tree claims == snapshot claims **and** tree triples == snapshot - triples, both directions: any `filament_vendor`/`filament_type`/family-name change surfaces - as a snapshot diff. -- **Mint conformance** — a non-grandfathered `OF*` id must equal the mint (or a salt - iteration) of its declarer's product triple; the error prints the expected id. -- **Retired reuse** — any tree id present in `resources/profiles/retired_filament_ids.json` is an error. - Ids that fully vanish from the tree are appended there by `--update-snapshot`; the file is - **append-only**. -- **Alias hygiene** — any vendor preset riding an OFL family id must keep the library - preset's base name, a non-empty `compatible_printers`, and no own id key (structure rule 4); - the error names the rename as the cause. -- **Triple integrity** — every declarer outside the BBL/`QD_*` islands must resolve a - non-empty `filament_vendor` and `filament_type` (generics use `"Generic"`), and all - declarers of one family within a bundle must agree on the triple. -- **Succession integrity** — retired successor chains terminate at a live id (or null) with - no cycles; `hints` keys are live, island-owned ids. -- **Reserved namespaces** — `GF*` outside BBL, `QD_*` outside Qidi, `P<7-hex>` or `null` - anywhere, unless that exact claim is grandfathered in the snapshot. -- **Structure** — no `filament_id` key on instantiated presets; no declared-vs-inherited id - drift; every instantiated system filament must resolve an effective id through its - `inherits` chain (an id-less one is a hard load error in C++ that discards the whole vendor - bundle). - -Sharing a **reserved-catalog** id with a new family or vendor (e.g. shipping a Bambu-cataloged -product under another vendor with its authentic `GF*` id) is refused by `--update-snapshot` -unless you pass `--allow-shared-catalog` — and it still lands in the snapshot diff for -maintainer review. Any other new sharing of an existing id is caught by the mint-conformance -check instead. - -## FAQ - -- **A new color of an existing product?** Never a new id — colors are not families. -- **A second diameter (1.75 mm and 2.85 mm) of the same product?** A sibling family with its - own id: two diameters are separately selectable spool products. -- **A high-speed tune of an existing material for another printer model?** Same family: - inherit the family's root, write no id key. -- **A tuned generic ("our profile for Generic PLA")?** Inherit `Generic PLA @System`, keep the - `Generic PLA` base name, set `compatible_printers`, write no id key. -- **I need to rename a family (or fix its `filament_vendor`/`filament_type`).** Add - `renamed_from` for the name, run `--remint ` then `--update-snapshot`: the id - re-derives from the corrected identity and the old id lands in the succession ledger - pointing at the new one. Commit the profile, snapshot, and ledger diffs together. -- **CI says my family needs an id.** Run `python scripts/assign_filament_ids.py`, then - `--update-snapshot`, and commit both diffs. Do not type an id by hand. - -For general profile authoring, see the profile development guide on the -[OrcaSlicer wiki](https://www.orcaslicer.com/wiki). diff --git a/docs/HLSD/filament_id.md b/docs/HLSD/filament_id.md new file mode 100644 index 0000000000..0dfc5471b0 --- /dev/null +++ b/docs/HLSD/filament_id.md @@ -0,0 +1,383 @@ +# Filament IDs (`filament_id`) + +`filament_id` identifies a **material family**: one commercial product line = one id, shared by +all of that material's per-printer / per-nozzle variants, in every profile bundle that ships it. +Devices use it to match a physical spool or tray to a filament preset. It is never per-color, +per-printer, per-nozzle, or per-preset (per-preset identity is `setting_id`), and it is never +per-bundle either — PolyLite PLA carries the same id whether the preset lives in the +OrcaFilamentLibrary (OFL), Qidi, or Snapmaker bundle. + +**How it is generated:** an id is computed, never invented. `scripts/assign_filament_ids.py` +mints it as a deterministic hash of the product's identity — the triple +`(filament_vendor, filament_type, family name)`, where the family name is the preset name +with its `@...` variant suffix stripped — producing an 8-character `OF*` code that is the +same for that product in every bundle, in every PR, on every machine. For example, Polymaker's +PolyLite PLA presets (`PolyLite PLA @base`, `PolyLite PLA@Q2-Series`, …) resolve +`filament_vendor` `Polymaker`, `filament_type` `PLA`, and family name `PolyLite PLA`; hashing +`filament_product/Polymaker/PLA/PolyLite PLA` yields `OF5CgdDq`, and that is the id the +OrcaFilamentLibrary, OrcaArena, Qidi, and Snapmaker bundles all arrive at independently +(derivation details in the Minting section). + +**How it is used:** at runtime the id is the join key between hardware and profiles. +When a printer reports what a tray holds (Bambu AMS, Qidi box, Creality CFS, +Klipper, Snapmaker), OrcaSlicer matches the reported id against the filament presets +compatible with that printer to select the right profile; other features — tray display +names, support-material detection, vitrification warnings, multi-nozzle filament grouping — +look up material properties by id alone. When an id has been retired, a shipped succession +ledger forwards it to its successor so old trays and records keep resolving. + +This page is the rule for authoring `filament_id` in system profiles +(`resources/profiles/**`). CI enforces everything below; the short version is: + +> [!IMPORTANT] +> **Never write a `filament_id` value by hand.** New families get their id from +> `python scripts/assign_filament_ids.py`; existing families already have one — inherit it. + +## The design, in three pieces + +Because several consumers match **globally by id alone, first hit wins** (see the next +section), any two materials sharing one id feed wrong data somewhere — a wrong tray name, a +wrong support-material flag, a wrong nozzle grouping — and inside one printer a duplicated id +makes AMS spool matching a coin toss. Hand-written ids produce such collisions constantly, so +the system is built to make them impossible and to make identity corrections safe: + +1. **Deterministic minting.** An id is a pure hash of the product's identity — no registry to + maintain, no next-free-number ceremony, no way for two concurrent PRs to race for the same + number, and no way to get it wrong by hand, because you never write it by hand. +2. **A sanctioned snapshot.** The complete id landscape derived from the tree must equal + `scripts/filament_id_snapshot.json` exactly, so every change to ids, claims (which bundles + ship which id, and for which family), or product identity surfaces as a reviewable diff to + one file — the maintainer gate. +3. **A shipped succession ledger.** Ids live outside the tree too — on device trays, in + calibration records, in user presets, in project files. When an id is retired, the ledger + forwards it to its successor so none of those references degrade to a generic fallback. + +## Who consumes the id + +The canonical consumer is tray-to-preset matching: a device reports a tray material id +(`tray_info_idx`), and the shared matching pipeline (`PresetBundle::sync_ams_list` and +friends) resolves it to a preset. The matcher is printer-scoped and first-match-wins: +scanning only compatible family roots — system roots plus user-made custom filaments, which +are user roots carrying their own `P*` ids; a preset derived from another resolves through +its root and never matches directly — it picks the first one whose `filament_id` equals the +tray's and retries once through the succession ledger on a miss. Only then does it fall back +by filament type: a system `Generic ` preset (matched by name, then by type +similarity), else the slot's previous selection, else any compatible system generic or, +failing that, any compatible system preset, else the slot is skipped — every fallback +selection surfaces a user-visible notice. + +Today only the Bambu AMS integration follows this pattern end to end — the device itself +reports the id, and the pipeline does all the matching. The other device integrations still +synthesize a preset id client-side in their agents (by type, brand, or color lookups against +the loaded presets) before the pipeline runs; they are intended to converge on the same +pattern, with the device-reported tray material id flowing through the shared matcher. + +| Ecosystem | Where the tray id comes from today | +| --- | --- | +| Bambu AMS | the device itself (RFID / user tray setting) — the `GF*` catalog | +| Qidi box | composed at runtime as `QD___` — vendor and type indices from the device's per-slot saved variables, the series digit inferred client-side from the printer model/name — then translated through the succession ledger to the family's minted id (the `QD_*` values themselves no longer exist as preset ids — see the reserved-namespaces section) | +| Creality CFS | runtime brand/type scoring returns the winning preset's id | +| Klipper (AFC / Happy Hare) | runtime lookup by filament type | +| Snapmaker | runtime color/vendor/type match | + +Tray-to-preset matching is printer-scoped, but **several consumers match globally by id alone, +first hit wins**: tray display names, `filament_is_support`, vitrification warnings, and +multi-nozzle filament grouping in the slicing pipeline (`FilamentGroup::try_merge_filaments` +merges plate slots sharing one `(filament_id, color)` pair, with matching +extruder-printability, onto one nozzle group; the engine is implemented but no grouping path +calls it yet). +Two *different* materials sharing one id +feed wrong data to those consumers even when the presets live in different vendors — so +cross-material id sharing is never safe. Within one printer, duplicate ids break AMS matching: +the matcher picks whichever preset loads first (it now logs an "Ambiguous AMS filament match" +warning, but the pick is still arbitrary) and the tray-edit dialog, which lists one entry per +id, hides the second preset entirely. The profile validator's `-f` check +(`check_filament_subtypes` → `PresetBundle::check_duplicate_filament_subtypes`) rejects this +per printer, and CI runs it tree-wide. + +Two more consumer-side facts worth knowing: + +- The machine-facing dialogs (AMS tray edit, AMS dry control, calibration history, extrusion + calibration) offer the filaments a connected printer can use by the same compatibility rule + the plater uses (an empty `compatible_printers` means *every* printer). Alias shadowing + still applies: a vendor's same-name profile supersedes the library generic. That is what + puts Orca Filament Library materials in those lists — deduplicated to one entry per + `filament_id` in the AMS and calibration-history dialogs, while extrusion calibration + deliberately lists every matching preset by full name. +- The id is load-bearing at startup: an instantiated system filament (one marked + `"instantiation": "true"` — see the structure rules) that resolves **no** + `filament_id` anywhere in its `inherits` chain is a hard load error in the C++ loader + (`Can not find filament_id for `) that discards the entire vendor bundle (for the + OrcaFilamentLibrary itself the failure is messier: library presets loaded before the + failing one survive, and every vendor bundle whose filaments inherit from the library is + then discarded for want of a base). CI's structure check catches this before it ships. + +## Do I need a new id? The one-question test + +> **Would a user consider this a different spool product than anything already in the tree?** + +Different polymer, different sub-brand (Basic / Matte / Silk / HF), fiber-filled sibling, or a +second selectable diameter → **new family, new id**. The same spool tuned for another printer +or nozzle → **join the existing family** (inherit its `@base`, write no id key). Tuning a +generic material → **join the OrcaFilamentLibrary family** (inherit `Generic X @System`, keep +the `Generic X` base name, write no id key). + +| Situation | id | +| --- | --- | +| Per-printer / per-nozzle variant of an existing material | same id (inherit, never write the key) | +| Sub-brand or product line (PLA vs PLA Matte vs PLA Silk vs PLA HF) | new id each | +| Color | never a new id | +| Second diameter of the same product (1.75 + 2.85) | sibling family, new id | +| "High-speed" tuned for a *different printer model* | same id (it is a printer variant) | +| "High-speed" selectable *alongside* the normal preset on one printer | new id (it is a product line) | + +## Structure rules + +1. **Only family roots carry the key.** Root presets (any preset *not* marked + `"instantiation": "true"`, typically ` @base` with `"instantiation": "false"`) + declare `filament_id`; instantiated variants inherit a root and never write the key. + A family may have several roots — Qidi's PolyLite PLA has four per-series roots + (`PolyLite PLA@Q2-Series`, `@Q2C-Series`, `@X-Max 4-Series`, `@X-Plus 5-Series`) — and all + of them must declare the *identical* id. This is the authoring rule for new work; a large + grandfathered tail of older presets breaks it in two ways — keys written on instantiated + presets (frozen in the snapshot's `instantiated_with_id` list) and keys that override the + id the preset would inherit from its family (frozen in `id_overrides`) — and CI ratchets + so no new preset joins either tail. +2. **The family name is the base name**: the preset name with everything from the first + (optionally space-preceded) `@` stripped. `MyBrand PLA @Orca 3D Fuse1` and `MyBrand PLA@HS` + both belong to family `MyBrand PLA`. +3. **Within a family, variants' `compatible_printers` are pairwise disjoint** — per printer, + at most one compatible instantiated preset per id, or AMS matching turns ambiguous. The + C++ validator's `-f` check enforces this. +4. **Generics belong to OrcaFilamentLibrary.** A vendor tuning a generic material inherits + `Generic X @System`, keeps the `Generic X` base name (that alias is what hides the library + preset on your printers), sets a non-empty `compatible_printers`, and writes no id key — + e.g. `Generic PLA @Sovol SV08 MAX` inherits `Generic PLA @System` and lists three Sovol + nozzles. A vendor-*branded* filament never rides a generic family id. +5. **Ids follow the product identity.** The id is a pure function of the product triple + `(filament_vendor, filament_type, family name)` — correcting any of them re-mints the id + **by design** (via `--remint `; the exact sequence is in the FAQ), and + `--update-snapshot` records the old id in the shipped succession ledger with its successor + so device trays, calibration records, and user presets keep resolving (`renamed_from` + still gates preset-*name* compatibility as before). A shipped id is never recycled for a + different material: retired ids are blocked forever. + +## Minting — nobody invents ids + +New ids are deterministic, computed exactly like the `setting_id` precedent +(`scripts/assign_vendor_setting_ids.py`): + +```text +FILAMENT_ID_NAMESPACE = uuid5(setting-id NAMESPACE, "filament_id") + = c4d3ff49-4c32-5534-a3e3-00894157ab97 +filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE, + "filament_product///") ) +``` + +`base62_6` is the low 6 base62 digits (alphabet `0-9A-Za-z`) of the UUID taken as a big-endian +integer, most-significant digit first; with the `OF` prefix the full id is 8 chars, within the +AMS length limit. The triple comes from the family root's *flattened* config: +`` is the filament +**manufacturer** (`"Polymaker"`, or `"Generic"` for generics — never the printer brand), +`` the material type, `` the root's base name; the two config +values are inheritable list options and the first element counts. + +Content-addressing on that triple is what makes the whole system converge. The key contains no +bundle name, so the same product mints the same id in every bundle — hoisting a family into +OrcaFilamentLibrary never changes its id, and two vendors independently shipping the same +product arrive at the same id without coordinating. `Polymaker/PLA/PolyLite PLA` mints +`OF5CgdDq`, and that one id is declared by the OrcaFilamentLibrary, OrcaArena, Qidi, and +Snapmaker bundles alike; the OFL generic `Generic/PLA/Generic PLA` mints `OFDSrzZ8`, claimed +by ten bundles — most by independent declarations converging on the same mint, the rest +purely through inheritance from the OFL family. + +On the rare collision with any existing or retired id, the minter salts the input (`…/1`, +`…/2`, …) until free, and the result is frozen in the profile file. Salting is also used +deliberately: a *salt split* keeps two presets of one product on distinct ids where a single +id would be AMS-ambiguous on the same printer — the "selectable alongside" situation from the +table above, resolved without inventing a second family name. The tooling recognizes salt +iterations of a triple as conformant and preserves such splits across re-mints. + +Workflow for a new family: + +```bash +# 1. Author the family with NO filament_id key anywhere. +python scripts/assign_filament_ids.py # 2. mint + insert ids into the family root(s) +python scripts/assign_filament_ids.py --update-snapshot # 3. record the new claims in the snapshot +python scripts/assign_filament_ids.py --check # 4. verify — the same checks CI runs +# 5. Commit the profile edits together with scripts/filament_id_snapshot.json. +``` + +The default run is idempotent and never rewrites a valid existing id; it edits profile files +byte-preservingly (indentation, BOM, and line endings intact) and re-parses them to fail +loudly. `--mint "filament_vendor/filament_type/family_name"` prints the id a **new** mint of +that triple would get, without touching anything — note that for a triple whose id already +exists it prints the next *free* salt iteration, not the live id (asking for +`Polymaker/PLA/PolyLite PLA` today prints the salt-1 id, because `OF5CgdDq` is taken). + +Maintenance modes (`--remint` is also the step for identity fixes — see the FAQ; the rest are +normally only used by id migrations): + +- `--remint VENDOR` re-derives a vendor's declared ids from their triples. A declaration + already equal to a salt iteration of its own triple is conformant and left alone, so + deliberate salt splits survive; convergence onto an id another bundle already uses for the + *same* triple is legal by design — that is the point. +- `--drop-redundant-ids VENDOR` deletes declarations that merely re-declare an inherited + OrcaFilamentLibrary id. +- `--add-hint "OLD=NEW"` records a succession hint for an id a foreign catalog owns + (`GF*` only — see the ledger section). +- `--retire "OLD=NEW"` records succession for a shipped id that vanished while another + declarer kept it alive — lineage the automatic claim vote (see the succession-ledger + section) can no longer see. +- `--forget-never-shipped FILE` (with `--update-snapshot`) — given a JSON list of ids that + never shipped in any release, drops them from the lineage entirely (no retirement entry) + and splices succession chains that pointed through them. +- `--profiles DIR` points the tooling at a different profile tree (default + `resources/profiles`). + +If you skip the tooling, CI fails and prints the remedy: the expected id for your family and +the instruction to run `python scripts/assign_filament_ids.py`; once the id is minted, the +snapshot checks likewise point at `--update-snapshot` and tell you to commit the resulting +diff. + +## Reserved namespaces — never mint or hand-write into + +An **island** is a frozen id namespace exempt from the mint rule because an external catalog +or device contract owns it. + +| Space | Status | Rule | +| --- | --- | --- | +| `GF*` | Bambu AMS/RFID catalog — the one remaining *island* | BBL vendor only; a claim by anyone else is refused unless the snapshot sanctions that exact claim (via `--allow-shared-catalog` — see the CI section) | +| `QD_*` | Qidi device protocol — *dissolved island* | declarable by **nobody**, Qidi included; every shipped `QD_*` id is retired in the ledger with a live successor | +| `P` + 7 hex chars (case-insensitive), `"null"` | user-created custom filaments (`CreatePresetsDialog.cpp`) | never appears in system profiles | +| every already-shipped id | frozen in the snapshot (grandfathered) | frozen as-is; new claims need maintainer sign-off | +| every retired id | `resources/profiles/retired_filament_ids.json` | never used again, for anything | + +The two device namespaces, in detail: + +- **BBL (`GF*`).** Bambu's device/RFID/cloud catalog is external and opaque, so BBL + declarations are frozen as-is and never re-minted. BBL also carries several dozen + grandfathered legacy codes that are neither `GF*` nor `OF*` (the `BETA` family's `B*` ids, + plus `Generic SBS`'s legacy `BFLSBS99`) — frozen the same way, via the snapshot. +- **Qidi (`QD_*`) — dissolved.** `QD_*` is a device-*protocol* namespace, not a preset id + space: the Qidi box path composes `QD___` ids at runtime (slot + vendor and type indices reported by the device, the series digit inferred client-side + from the printer model/name), and the Qidi agent translates a composed id through the + succession ledger to the family's minted id. Qidi presets themselves carry ordinary minted + `OF*` ids (generics share the OFL ids), and all 204 `QD_*` ids that ever shipped sit in + the ledger as retired entries with live successors — which is also what makes CI refuse + any future `QD_*` occurrence permanently. The alternative — treating per-series protocol ids + as preset ids — would put one product under five ids (`QIDI PLA Rapido` would be `QD_0_1_1` + through `QD_4_1_1`), exactly the fragmentation the mint rule removes. + +## The succession ledger + +`resources/profiles/retired_filament_ids.json` ships with the app and has two maps: + +- **`retired`** — ids Orca owned and withdrew. Each maps to + `{"claims": [...], "successor": }`; successor chains are followed to the live end. + When `--update-snapshot` retires a vanished id, it picks the successor by an automatic + *claim vote*: every old claim whose family still exists votes for that family's current id, + most votes wins. Alongside the deliberate re-mints, this map carries the whole pre-rule + legacy — old ad-hoc vendor codes, Creality's and Snapmaker's numeric ids, and the 204 + `QD_*` protocol ids. +- **`hints`** — the same forwarding for ids Orca *cannot* retire because a foreign island owns + them and may legitimately keep shipping them. That means `GF*` only: e.g. + `GFL99 → OFDSrzZ8` forwards Bambu's Generic PLA catalog id to the OFL generic family on + installs where no live preset carries `GFL99`. + +The client consults the ledger **only on resolution miss** — AMS tray sync and the sync-AMS +dialog's filament listing, the sidebar AMS dropdown, tray-id type lookup, calibration +history, the global filament-id preset lookup, and the Qidi box path. A live preset always +wins first, so BBL installs resolve `GF*` natively and behavior is unchanged wherever the +raw id still exists. This on-miss rule is what makes +identity-driven re-mints (structure rule 5) safe: the old id keeps resolving to the family's +current preset instead of degrading to a `Generic ` fallback. The file is append-only in +its keys — entries are never removed and a retired id never returns — and maintained +exclusively by `--update-snapshot` / `--add-hint` / `--retire` (`--forget-never-shipped`, an +`--update-snapshot` mode, may rewrite an existing entry's successor when splicing chains). + +## How CI enforces this + +Profile CI (`check_profiles.yml`) runs `check_filament_ids()` tree-wide via +`scripts/orca_extra_profile_check.py`. Its ground truth is +**`scripts/filament_id_snapshot.json` — the sanctioned state**: the id state derived from the +tree must equal the snapshot exactly, in both directions. Any change to the id landscape +therefore surfaces as a diff to that file, and **that snapshot diff is what maintainers review +and gate in a PR**. Never edit the snapshot by hand — `--update-snapshot` regenerates it +deterministically (running it twice changes nothing). Besides the live `ids` and `triples` +maps, the snapshot carries grandfather lists (`instantiated_with_id`, `id_overrides`, +`alias_exceptions`, `triple_exceptions`) that freeze pre-existing structure debt while the +checks ratchet all new profiles to the clean rules. + +The checks, in brief: + +- **Format** — every id is either in the snapshot, `OF` + 6 base62 chars, or BBL's. +- **Snapshot equality** — tree claims == snapshot claims **and** tree triples == snapshot + triples, both directions: any `filament_vendor`/`filament_type`/family-name change surfaces + as a snapshot diff. +- **Mint conformance** — a non-grandfathered `OF*` id must equal the mint (or a low salt + iteration) of its declarer's product triple; the error prints the expected id to paste into + the family root. +- **Retired reuse** — any tree id present in the `retired` map of + `resources/profiles/retired_filament_ids.json` is an error, with no grandfathering (keys in + the same file's `hints` map may legitimately stay live — BBL still ships them). Ids that + fully vanish from the tree are appended to the `retired` map by `--update-snapshot`; ids + are only ever added there, never removed or reused. +- **Alias hygiene** — a vendor preset riding an OFL family id must keep the library preset's + base name (a rename re-exposes the library preset, since alias shadowing is name-based), + a non-empty `compatible_printers` (an empty one shadows nothing), and no own id key + (structure rule 4). +- **Triple integrity** — every declarer outside the BBL island must resolve a non-empty + `filament_vendor` and `filament_type` (generics use `"Generic"`), and all declarers of one + family within a bundle must agree on the triple. +- **Succession integrity** — retired successor chains terminate at a live id (or null) with + no cycles; `hints` keys stay in the island namespace (`GF*`), are never also retired, and + their chains end at a live tree id. +- **Reserved namespaces** — `GF*` outside BBL, `P<7-hex>` or `"null"` anywhere, unless that + exact claim is grandfathered in the snapshot; `QD_*` anywhere, with no exception (all are + retired). +- **Structure** — no `filament_id` key on newly instantiated presets; no new + declared-vs-inherited id drift; every instantiated system filament must resolve an + effective id through its `inherits` chain (recall: an id-less one is a hard load error in + C++ that discards the whole vendor bundle). + +Sharing a **reserved-catalog** id with a new family or vendor (e.g. shipping a Bambu-cataloged +product under another vendor with its authentic `GF*` id) is refused by `--update-snapshot` +unless you pass `--allow-shared-catalog` — and it still lands in the snapshot diff for +maintainer review. Any other new sharing via a *declared* id is caught by the mint-conformance +check; sharing through inheritance carries no declaration to check and surfaces only as a new +claim in the snapshot diff — which is exactly why that diff is the gate. + +Complementing the Python checks, CI also runs the C++ profile validator with `-f` +(`check_filament_subtypes`): it loads the bundle exactly as the app does and flags any printer +for which two or more compatible filament presets share one `filament_id` — the runtime-shaped +ambiguity check behind structure rule 3. + +## FAQ + +- **A new color of an existing product?** Never a new id — colors are not families. +- **A second diameter (1.75 mm and 2.85 mm) of the same product?** A sibling family with its + own id: two diameters are separately selectable spool products. +- **A high-speed tune of an existing material for another printer model?** Same family: + inherit the family's root, write no id key. +- **A tuned generic ("our profile for Generic PLA")?** Inherit `Generic PLA @System`, keep the + `Generic PLA` base name, set `compatible_printers`, write no id key. +- **I need to fix a family's `filament_vendor` or `filament_type`.** Fix the config, run + `--remint ` then `--update-snapshot`: the id re-derives from the corrected identity + and the old id lands in the succession ledger pointing at the new one. Commit the profile, + snapshot, and ledger diffs together. +- **I need to rename a family.** Renames need one extra step, because the succession vote + follows family names (`renamed_from` gates preset-name compatibility but is not read by the + id tooling): rename the presets (adding `renamed_from`), run `--update-snapshot` once to + sanction the renamed claims on the old id, then `--remint `, then `--update-snapshot` + again — the old id now lands in the ledger pointing at the new one. Re-minting first would + retire the old id *heirless* (successor `null`), which cannot be repaired afterwards. +- **Can I reuse a `QD_*` id for a Qidi profile?** No — nobody can. The namespace is a + dissolved island: the device still composes those ids at runtime, and the succession ledger + translates them to the families' minted ids. Author Qidi filaments like any other vendor's. +- **CI says my family needs an id.** Run `python scripts/assign_filament_ids.py`, then + `--update-snapshot`, and commit both diffs. Do not type an id by hand. + +For general profile authoring, see the profile development guide on the +[OrcaSlicer wiki](https://www.orcaslicer.com/wiki). diff --git a/filament_id_plan.html b/filament_id_plan.html deleted file mode 100644 index 08f4a06b74..0000000000 --- a/filament_id_plan.html +++ /dev/null @@ -1,1029 +0,0 @@ - - - - - -filament_id — design & migration plan · OrcaSlicer - - - - - - -
- - -
-
DESIGN REVIEW resources/profiles · filament_id
-

One id, one material — on every printer, exactly once.

-

A filament_id names a material family: one commercial product line, shared by all of its - per-printer variants. Devices match spools by filament_id + printer compatibility. - That only works if, for any one printer, at most one compatible preset carries a given id. Bambu keeps this - invariant; the other 64 vendor bundles broke it 1,256 times.

- -
- id anatomy — Bambu's grammar - - - - GF - A - 00 - - - - - - - - - - - - prefix — Bambu's namespace - frozen: AMS RFID tags carry these bytes - family letter — A: Bambu PLA - B ABS/ASA · C PC · G PETG · L generic/3rd-party PLA - N PA · P PP/PE · S support · T PET/PPS · U TPU - tier number - 00–49 branded · 50–59 fiber-filled - 60–70 partners · 95–99 generic, desc. - - - - shared by every variant: - - - Bambu PLA Basic @BBL X1C - - Bambu PLA Basic @BBL P1P - - Bambu PLA Basic @BBL X1C 0.2 nozzle - - - -
- -
-
1,256
printer-level ambiguity errors (validator -f, tree-wide)
-
356
logical collision groups after de-duplication
-
30 / 65
vendor bundles affected / total
-
0
violations in BBL — cleaned in PR #14459, CI-gated
-
-

Every number in this document is reproducible: an independent loader-faithful audit re-derives the - validator's output exactly (1256 = 1256), and every code claim carries a file:line reference.

-
- - -
-
§0 overview
-

The plan in one screen

-

Two deliverables: a generation rule so ids can never become ambiguous again (goal 1), and a - migration that fixes the 356 existing collisions without breaking users (goal 2).

- -
- the whole plan - - - - - - - - - TODAY - 7+ ad-hoc id schemes - 356 collision groups - zero written rules - CI covers BBL only - - - RULE + TOOLING §3 §6 - deterministic OF* mint - id lives on @base only - CI ratchets + snapshot - no profile changes yet - - - MIGRATION §5 - fresh ids only, ever - OFL first, then per vendor - names never change - one reviewable PR each - - - DONE - invariant holds - tree-wide, - by construction - - - - - - - BBL profiles are never touched · Qidi QD_* device ids are never touched · no preset is renamed or deleted - -
- -
- Why migration is safe — verified in code: user presets re-derive filament_id from their parent on - every load Preset.cpp:1658-1682; 3mf projects resolve presets by name + config, not id - Preset.cpp:2490-2576; Klipper, Creality and Snapmaker derive tray ids at runtime from the - installed bundle. Fresh, never-reused ids on non-BBL system presets break nothing. -
-
- - -
-
§1 how matching works
-

Every ecosystem funnels through this one id

-

Not just Bambu. Five device ecosystems put a filament_id into the same pipeline; the slicer then - picks the single preset that matches the id and is compatible with the active printer.

- -
- runtime — device → preset - - - - - - - - - - BBL AMS - from device RFID — frozen - - Qidi box - QD_<series>_<v>_<t> — frozen - - Creality CFS - scored at runtime — safe - - Klipper AFC/HH - by filament type — safe - - Snapmaker - color/type match — safe - - - - - - - - - - - - tray_info_idx (MQTT / agent) - - filament_ams_list["filament_id"] - - find_if( f.is_compatible && base(f)==f && f.filament_id == id ) - - - - - - - - Plater.cpp:3440 - PresetBundle.cpp:3132/3233 - - - → the ONE matching preset - two matches? find_if silently returns whichever sorts first — no log, no warning; the AMS edit dialog even hides the second preset - -
- -

The invariant, drawn

-
- per printer × per id -
-
-

✓ Correct — Bambu PLA Basic, id GFA00

- - - - - -
printer presetcompatible presets with GFA00
X1C…@BBL X1C 1
X1C 0.2 nozzle…@BBL X1C 0.2 nozzle 1
P1P…@BBL P1P 1
-

Variants share the id; their compatible_printers are disjoint. Each printer sees the id exactly once.

-
-
-

✗ Broken — Qidi, id GFB99 (real case)

- - - -
printer presetcompatible presets with GFB99
X-Max 3 0.4 nozzleBambu ABS · HATCHBOX ABS · Overture ABS · PolyLite ABS · QIDI ABS Rapido · QIDI ABS-GF · QIDI ASA · ASA-Aero · PC-ABS-FR · Generic ABS … 16
-

Sixteen different materials answer to one id on one printer. A spool tagged “generic ABS” matches - whichever preset sorts first — and GFB99 was stamped into 317 Qidi files.

-
-
-
- -

Two consumers you can't see from the profiles

-

Most matching is printer-scoped, but several code paths match globally, by id alone — tray display name, - temperature_vitrification warnings, calibration history, and the multi-nozzle grouping key - FilamentGroup.cpp:513. So two different materials must never share an id even across - vendors: the first preset in the whole installed collection supplies the name, type and temperature data.

-
Two identity systems. The library (OFL) hides its global presets per printer by - alias (name before  @) — Preset.cpp:3684 — while devices match by - filament_id. When a vendor tunes Generic PLA but renames it - Flashforge PLA Basic, the alias no longer matches, shadowing fails, and both presets are - visible with the same id. The rule in §3 aligns the two systems: one family = one alias = one id.
-
- - -
-
§2 the landscape today
-

Seven schemes, zero rules, one epidemic

-

The only guidance that ever existed was “≤ 8 characters”, checked for BBL only. Every other - convention was one contributor's invention, merged without comment, then imitated.

- -
- who uses which id shape — 5,866 instantiated presets - - - GF<letter><NN> (Bambu classic, mass-copied) - 2,921 - free-form ("GFPLA Silk", "LHF_pla", 36-char names…) - 2,241 - GF<BRAND><NNN> (Bambu partner ids) - 395 - O-prefixed (OGF* — OrcaFilamentLibrary mirrors) - 211 - GF<x>##_## (Afinia/Tiertime suffix) + other GF-shaped - 43 + 55 - - -
- Bambu's space, used outside Bambu - invented, unowned - deliberate Orca convention -
-
- -

How it got here

-
- convention timeline - - - - - - - - - - 2025-01 - OrcaFilamentLibrary - created - - 2025-03 - OGF* prefix born - direct commit, no rationale - - 2025-05 - “≤ 8 chars” check - BBL-only · PR #9574 - - 2025-06 - _## suffix invented - PR #9739, imitated since - - 2025-11 - profile wiki removed - its example taught copy-paste - - 2026-06 - validator -f · BBL → 0 - PR #14459 - - now - this plan - - -
- -

Beyond the validator's reach

-

The -f check compares a printer only against its own vendor's filaments. Three further ledgers exist:

-
-
16
OFL-internal ids covering several materials — visible on every printer (e.g. OEPLAB00 = 14 Elegoo PLA products)
-
42
OFL×vendor same-id pairs — 32 already neutralized by alias shadowing, 10 live
-
67
ids meaning different materials in different vendors (GFU99 also covers a PEBA; Anycubic's GFL95 “Matte” ≠ Bambu's GFL95 “High Speed”)
-
-
- - -
-
§3 the rule
-

Nobody invents ids. Structure carries the rest.

-

Chosen by a 3-design → 2-judge → 3-attacker adversarial process. Deterministic minting won on - ambiguity-prevention, contributor simplicity and enforceability; every “breaks” finding from the attack pass is - folded in below.

- -

The one-question test

-
- decision — same id or new id? - - - - - - - - Would a user call this a different spool product - than anything already in the tree? - - - - - - - YES — polymer, sub-brand, - fiber-filled, 2nd diameter - NO — same spool, tuned for - another printer / nozzle - it's a GENERIC - material - - - NEW FAMILY - create «Family @base», not instantiated - write NO filament_id anywhere - run the mint script — or paste the id - that CI prints for you - - JOIN THE FAMILY - inherit the existing @base - never write the filament_id key - keep compatible_printers disjoint - from the other variants - - JOIN THE OFL FAMILY - inherit «Generic X @System» - KEEP the «Generic X» name/alias - (alias shadowing then hides the OFL - preset on your printers) · no id key - color → never a new id · “high-speed” for a different printer → same family - “high-speed” selectable alongside the normal preset on one printer → new family - second selectable diameter on the same printer → sibling family with its own id - -
- -

Minted ids — the setting_id precedent, applied to families

-
- id anatomy — the new namespace - - - OF - q3xT9k - - - - - - - - - OF — Orca Family namespace - disjoint from GF · QD_ · P· at two chars - base62_6( uuid5( NS, "filament_family/<Vendor>/<Family>" ) ) - e.g. filament_family/Elegoo/Elegoo PLA Matte → OFq3xT9k - same derivation as setting_id · 8 chars total (AMS limit) - - - same input → same id: - no “next number” races · a fork mints the id upstream expects · CI recomputes and verifies - renamed family? - the id does not change — immutable once shipped; renamed_from covers the name - - -
-

Author workflow is two lines: commit the family with no id anywhere; run - python scripts/assign_filament_ids.py (or read the id CI prints in its failure message and paste it). - A --mint "<Vendor>/<Family>" one-shot prints an id without touching the tree.

- -

Structure: the id lives in exactly one place

-
- family shape - - - Elegoo PLA Matte @base - instantiation:false · filament_id: "OFq3xT9k" ← only here - - - - - - …@Centauri Carbon - inherits @base · no id key - compat: {CC 0.4} - - …@CC 0.2 nozzle - inherits @base · no id key - compat: {CC 0.2} - - …@Neptune 4 - inherits @base · no id key - compat: {N4, N4 Pro} - - disjoint compatible_printers = the PR #14459 invariant, by construction - family identity is DECLARED — an optional "filament_family" key on the root overrides name derivation - multi-root families are legal (Qidi's per-series bases) — every root must declare the identical id - -
- -

Reserved namespaces — never mint or hand-write into

-
- the id space, partitioned - - - - - - - - - GF* - Bambu AMS / RFID — frozen - - QD_* - Qidi device — frozen - - P[0-9A-Fa-f]{7} · "null" - user-custom presets - - OGF* + legacy shapes - grandfathered, closed - - OF[0-9A-Za-z]{6} - open — minted only - - Byte-copies of authentic Bambu ids stay legal where the checked-in shared_catalog sanctions the family (BBL, OFL, Fiberon). - Everything already shipped and unambiguous is snapshot-frozen exactly as-is — the migration touches only colliding families. - -
-
Accepted trade-off: minted ids are opaque — OFq3xT9k doesn't say “PLA”. - The family name sits in the same file; in exchange there is no registry, no “next free number” ceremony, no PR races, - and forks mint the same id upstream expects. The mnemonic registry-grammar runner-up is preserved in §8 if you prefer it — - everything else in this plan works under either format.
-
- - -
-
§4 error taxonomy — goal 2
-

Eleven patterns explain all 356 groups

-

Classified group-by-group against the actual profile files, then independently spot-checked - (1 substantive disagreement in 14 samples, corrected). The task's two known patterns cover about half; the rest - are new findings.

- -
- what's actually wrong — classified groups - - - P1 copy_paste_id — different materials share an id verbatim - 167 - P2 wrong_inherits / id-less product line - ~87 - P3 generic_family_overlap — branded preset rides a generic id - 68 - P4 overclaim_compat — broader variant claims a dedicated printer - 24 - other — deliberate coexistence, hygiene, one true duplicate - 21 - - -

Fixes across all groups: 319 × mint a fresh id · 21 × trim compatible_printers · - 21 × split compat-or-id · 5 × re-point inherits · 1 × merge. Which preset keeps a contested id follows a - mechanical precedence: Bambu-catalog material → OFL generic family → historical first owner.

-
- -
-
P1copy_paste_id~167
-

The id came along when a profile was copied. Qidi stamped GFB99 into 317 files; - Peopoly put PLA-Silk's id on ABS.

-
fix — impostor families get minted ids on their roots; the owner keeps the id.
-
P2wrong_inherits / no root~87
-

A product line never got its own @base — every variant inherits another family's id. - Flashforge's umbrella collapses ~140 presets into one id.

-
fix — create per-family roots with minted ids; re-point inherits. Never delete the old - shadow files — convert them (they carry real config).
-
P3generic_family_overlap~68
-

A vendor-branded filament rides a generic family id via inheritance, colliding with the true generic - on the same printer.

-
fix — branded → minted id. True generic tunings instead join the OFL family with matching alias. - Sovol is the elegant case: just delete its wrong id lines and inherit OFL's.
-
P4overclaim_compat~24
-

Same material; the broad variant claims a printer that a dedicated variant covers — the BBL H2DP pattern - from PR #14459.

-
fix — trim the broader preset's compatible_printers. Ids untouched.
-
P5template-carried idin P1/P2
-

The id sits on a shared settings template (fdm_filament_*), so every family that inherits - the template collapses onto it (Prusa, Ginger Additive).

-
fix — move ids off templates onto family roots.
-
P6OFL-internal collision16 ids
-

One OFL id spans several materials — and OFL presets are visible on every printer. - OGFL06 = eSUN PLA-Marble and Fiberon PETG-ESD.

-
fix — fix OFL first: it's the base bundle every vendor resolves against.
-
P7alias-mismatch re-exposure10 live
-

A vendor tunes a generic but renames it; alias shadowing fails and the OFL preset resurfaces beside it - (Snapmaker PolyTerra J1 PLA).

-
fix — rename-to-alias where it's genuinely the same family, else mint.
-
P8per-variant ids~210 presets
-

The opposite failure: every nozzle variant has its own id (Prusa, SeeMeCNC, Afinia _##). - No ambiguity — but devices can't recognize the material across nozzles.

-
fix — grandfather (they're unambiguous); converge opportunistically; document as anti-pattern.
-
P9format violationshygiene
-

Ids with spaces ("GFPLA Silk"), 11-char ids, invented GF-shaped ids colliding with - Bambu's real allocations.

-
fix — disappears as a byproduct of re-minting; the unambiguous rest is snapshot-frozen.
-
P10cross-vendor semantic67 ids
-

Same id, different material, different vendors — feeds wrong name/type/temperature data to the globally-matching - consumers (§1).

-
fix — mostly eliminated by P1–P3 re-minting; benign same-material generic sharing is legalized - by the snapshot.
-
P11deliberate coexistence4
-

Snapmaker “Benchy” demo presets coexist on purpose (gated by compatible_prints, which the id check - can't see); one preset lists the same printer three times.

-
fix — mint ids for demo presets; de-duplicate list entries; add a lint.
-
-
- - -
-
§5 migration plan
-

Fresh ids only, family-atomic, names never change

-

The persistence analysis makes one strategy provably safe — and everything else forbidden.

- -

What survives an id change

- - - - - - - - - - -
surfacebehavior across releasesverdict
User presets that inherit a system presetfilament_id re-derived from the parent on every load Preset.cpp:1658-1682safe
Saved 3mf projectspresets resolve by name + config equality, never by id Preset.cpp:2490-2576safe
Klipper / Creality / Snapmaker synctray ids derived at runtime from the installed bundlesafe
User root custom presetscopied a system id at creation and persist it forever — AMS auto-match falls back to generic-by-typelow residue
Qidi QD_* idsencode the device protocol QidiPrinterAgent.cpp:146 — an exact-match contractfrozen
BBL GF* idslive on RFID tags and printer-side records; hardcoded k-values in C++frozen
Recycling an old id for a different materialstale ids in user roots / old 3mfs would silently match the wrong materialforbidden, forever
Renaming / deleting preset namesuser presets whose inherits no longer resolves are dropped at load Preset.cpp:1687-1691forbidden (use renamed_from if unavoidable)
- -

Phases

-
-
0
-

Tooling + snapshot — no profile changes

-

Land scripts/assign_filament_ids.py, the extended CI checks in ratchet mode, the validator OFL - cross-check and the runtime warning. Generate filament_id_snapshot.json (id→families multimap over main) - and an empty retired_ids.json; both are checked in and append-only.

-
-
1
-

OrcaFilamentLibrary first

-

OFL is the base bundle every vendor resolves against. Fix its 16 internal collisions and give - Generic PETG HF/PETG-CF/PP-CF/… @System their own family roots instead of collapsing into their - parent generic. Unambiguous ids stay byte-identical.

-
-
2
-

Per-vendor PRs, worst first — each widens the CI ratchet

-

Qidi → Flashforge → Elegoo → Prusa → Cubicon → Anycubic → InfiMech → Snapmaker → long tail (22 vendors, - mostly 1–8 one-line fixes). Each merged PR appends its vendor to the workflow's -f -v scope - (today: -v BBL -f); when all are in, drop -v and run tree-wide.

-
-
3
-

Delete the allowlists

-

Once tree-wide zero holds, the ratchet allowlists disappear and every check becomes a hard rule for anything - born after the snapshot.

-
-
- -
- where the work is — collision groups per vendor - - - Qidi97 - Flashforge69 - Elegoo37 - Prusa31 - Cubicon17 - Anycubic14 - InfiMech14 - Snapmaker11 - Artillery8 - Creality8 - FlyingBear8 - Sovol6 - 18 more40 total (Volumic 6 · Ratrig 5 · Chuanying 4 · Dremel 3 · 4×2 · 10×1) - - -

Vendor observations show these are systematic mistakes, not hundreds of independent bugs: Qidi is one - blanket-stamp in three profile generations; Elegoo is a single inherit-the-class-base habit; Sovol is fixed by deleting - six wrong lines.

-
- -

The migration script's contract (hardened by the adversarial pass)

- - - - - - - -
rulewhy
Re-mint everything — id literals in analysis notes are ignored; only group membership, fix category and keep-id precedence are consumed. Assert no emitted id matches ^(GF|QD_|P[0-9A-Fa-f]{7}$).~40% of classification notes suggested ids in the old invented-GF culture; CI would reject them.
Family-atomic — re-idding any preset re-ids every same-family sibling in the same commit, even outside the collision group.Otherwise a family splits across two ids (FlyingBear GFB99 @S1 vs @Ghost7) — worse than the bug being fixed.
Bounded diff — a vendor PR may only touch collision-group files + same-family siblings; every id that is unambiguous today stays byte-identical.Makes each PR mechanically auditable.
Config-equivalence gate — flattened effective config of every preset must be identical before/after, except the prescribed id/inherits/compat edits.Makes structural conversions provably behavior-neutral (Flashforge's id-less shadow files become named family roots carrying their config byte-for-byte).
No deletions, no renames — redundant twins are re-minted, not dropped; consolidation via renamed_from is a separate human-reviewed cleanup.Deleting a name silently drops users' derived presets at load.
-
- - -
-
§6 enforcement
-

Ratchets, not absolutes

-

The grandfathered landscape keeps today's benign sharing legal; the checks forbid anything new. - All checks import the same mint function — the setting_id precedent.

- - - - - - - - - - - -
#checkwhere
1Format — valid iff OF[0-9A-Za-z]{6} or in the snapshot or vendor==BBL or QD_* in Qidi. The legacy set is closed.orca_extra_profile_check.py
drop BBL-only gate :292, OFL skip :601
2Uniqueness ratchet — tree-wide id→families multimap; no id may gain a family claim not recorded in the snapshot (shared_catalog is the sanctioned exception).
3Structure ratchet — id key only on roots; every instantiated preset resolves an effective id (simulated loader walk); all family members resolve identically; no preset may override its inherited id (the Generic SBS drift class).
4Mint conformance — a new id must equal the mint (or a salt iteration); the failure message prints the expected value so hand-editors can paste it.
5Stability — an (id, family) pair on main may not change or vanish, following renamed_from chains, unless listed in a maintainer-gated migrations file. Retired ids go to an append-only ledger and are never redefined.
6Alias hygiene — a preset inheriting an OFL Generic * @System (no own id in its chain) must keep the OFL base name and non-empty compatible_printers; the error names the rename as the cause.
7OFL cross-check — extend check_duplicate_filament_subtypes to include OFL presets in every vendor's per-printer check, minus alias-excluded ones. m_excluded_from is already populated in validator context, so the 32 shadowed pairs won't false-positive and the 10 live ones are caught. Widen CI's -f per vendor.C++ validator
PresetBundle.cpp:5654 · :2302
8Runtime backstop — log a warning when the AMS-sync find_if sees 2+ compatible presets for one id. The only layer that can see side-loaded and forked bundles. One line.runtime
PresetBundle.cpp:3132/3233
- -

Plus: the rule document ships in-repo (doc/developer-reference/filament_id.md) so CI messages - have a stable link, and the profile-PR template gets one checkbox: “new materials: no filament_id key anywhere — - CI prints the minted id.”

-
- - -
-
§7 evidence
-

How this was verified

-

The analysis was run as a 52-agent pipeline over the real tree and real git history, with an - adversarial design phase — findings were attacked before being adopted.

- -
- methodology - - - - - - - - - 5 code analysts - runtime · persistence · loader - Bambu grammar · git history - - 38 classifiers - every collision group, read - from the actual profile files - - ground truth - audit script = validator - 1256 = 1256 · exact - - - 3 designs - deterministic mint - registry grammar - structure-only - - - 2 judges - both chose the mint - 58 / 50 / 56 - 60 / 54 / 56 - - - 3 attackers - 35 scenarios - authors · runtime · - migration mechanics - every “breaks” → - an amendment §3–§5 - spot-check: 1/14 - - - - - - - - - -

Amendments that came out of the attack pass: family identity declared on roots (not name-derived — - Afinia PLA@HS has no space before @); multi-root families legalized; checks 2–3 as - ratchets, not absolutes; the no-deletion rule; shadow-file conversion instead of deletion; classification id - literals quarantined; diameter siblings; case-insensitive reservation of the user id space.

-
- -
- Key discoveries that reshaped the design (with sources) -
-

· The “26 Flashforge presets with no id” were a false alarm — the loader resolves them through the OFL base-bundle - fallback to OGFL99/OGFG99 PresetBundle.cpp:4904-4909. The real bug is - ~10 different Flashforge PLA products sharing one id.

-

· A missing id can never ship: it's a hard load error that discards the whole vendor bundle - PresetBundle.cpp:5072, :5141.

-

· Qidi's QD_* ids are a device-protocol contract, discovered in - QidiPrinterAgent.cpp:146-152 — a second frozen namespace nobody had documented.

-

· User-custom ids occupy P + 7 hex CreatePresetsDialog.cpp:533, and - "null" is a sentinel — both reserved.

-

· OFL shadowing is keyed by alias, not id Preset.cpp:3684-3714, and is already active in - validator context PresetBundle.cpp:2302 — which is what makes check #7 precise.

-
-
-
- - -
-
§8 open decisions
-

Five calls that are yours to make

-

Everything else in the plan holds under any of these answers. marks the recommendation.

- -
-
D1id format
-
Opaque deterministic mint OFq3xT9k — no registry, no races, fork-friendly; judges 2/2.
-
Mnemonic registry grammar <NS><FAM><SEQ> — readable ids, but someone allocates numbers forever.
-
D2multi-vendor brands
-
shared_catalog list — sanction authentic Bambu ids for byte-matching families (Snapmaker's Fiberon), low churn.
-
Hoist those families into OFL — cleaner long-term, more restructuring now.
-
D3demo presets
-
Mint ids for Benchy-style presets — simple, keeps the validator strict.
-
Teach the validator compatible_prints gating — more machinery for 3 presets.
-
D4the 10 live OFL duplicates
-
Rename-to-alias where it's truly the same family (with renamed_from) — restores shadowing.
-
Mint vendor ids everywhere — safer mechanically, more ids in the world.
-
D5where the rule doc lives
-
In-repo doc/developer-reference/filament_id.md — CI messages need a stable link.
-
Wiki-only — discoverable, but drifts from the checks that enforce it.
-
- -
- Suggested first step: land phase 0 (tooling + snapshot, zero profile changes). It is entirely additive, - makes every later PR mechanically checkable, and turns the 356-group backlog into a shrinking allowlist that CI - reports on every profile PR. -
-
- -
- - - - diff --git a/filament_id_plan.md b/filament_id_plan.md deleted file mode 100644 index 7dc4b43d04..0000000000 --- a/filament_id_plan.md +++ /dev/null @@ -1,397 +0,0 @@ -# filament_id: generation rule + system-profile fix plan - -Follow-up to PR #14459 (commit `c2e91cb8`, validator `-f` / `check_duplicate_filament_subtypes`). -Goal 1: a filament_id generation rule for all vendors. Goal 2: an error-pattern taxonomy and a -migration plan that removes every ambiguous filament_id, without touching Bambu (BBL) profiles. - -All numbers below are reproducible: a loader-faithful audit script re-derives the validator's -output **exactly** (1256/1256 printer-level errors, 356 logical collision groups, 30 vendors). -Every code claim was verified against source with `file:line` references. - ---- - -## 0. Executive summary - -- `filament_id` is a **material-family id**: one id per commercial product line, shared by all - of that material's per-printer/per-nozzle variants. Matching is always `(filament_id + - printer compatibility)`; the invariant from PR #14459 is *per printer preset, at most one - compatible instantiated filament preset per id*. -- Every modern device ecosystem funnels through this id — not just Bambu AMS: Qidi box, - Creality CFS, Klipper AFC/Happy Hare, Snapmaker all emit/consume `tray_info_idx` - (see §1). Several consumers match **globally, without printer scoping**, so two *different - materials* sharing one id is unsafe even across vendors. -- **Proposed rule (§3):** deterministic, script-minted ids — `OF` + 6 base62 chars from - `uuid5(vendor + family)`, declared **only on family-root (`@base`) presets**; variants inherit. - Nobody ever invents an id by hand; CI prints the expected id when one is missing. Existing - unambiguous ids are grandfathered and frozen; `GF*` (Bambu), `QD_*` (Qidi device protocol), - and `P<7-hex>` (user custom presets) are reserved namespaces that must never be minted into. - This was selected by an adversarial design/judge process over a registry-grammar alternative - and a structure-only alternative, then stress-tested; amendments from that stress test are - folded in below. -- **Migration (§5):** fresh-never-reused ids only, family-atomic, names never changed. This is - provably safe: user presets re-derive `filament_id` from their parent on every load - (`Preset.cpp:1658-1682`), 3mf resolves presets by name+config (`Preset.cpp:2490-2576`), and - Klipper/Creality/Snapmaker derive tray ids at runtime. One PR per vendor; CI's `-f` scope - ratchets per vendor until tree-wide. - ---- - -## 1. How filament_id actually works (verified) - -### Consumers and scoping - -All device integrations converge on one pipeline: device/agent sets `tray_info_idx` → -`DevAmsTray.setting_id` → `Sidebar::build_filament_ams_list` (`Plater.cpp:3423-3493`) → -`PresetBundle::sync_ams_list` / `get_ams_cobox_infos` (`PresetBundle.cpp:3112-3308`) match it -against filament presets. - -| Ecosystem | Where the id comes from | Effect of changing a system id | -|---|---|---| -| BBL AMS | device-side (RFID / user tray setting), `DeviceManager.cpp:3823+` | breaks matching — **frozen by mandate** | -| Qidi box | built from device enums: `"QD_" + series + "_" + vendor + "_" + type_idx`, `QidiPrinterAgent.cpp:146-152`; needs an exactly-matching visible preset | breaks matching — **`QD_*` ids are a frozen device contract** | -| Creality CFS | runtime brand/type scoring returns current preset's id (`CrealityPrintAgent.cpp:46-118`) | invisible | -| Klipper (AFC / Happy Hare) | runtime `filament_id_by_type` (`MoonrakerPrinterAgent.cpp:808,936`) | invisible | -| Snapmaker | runtime color/vendor/type match (`SnapmakerPrinterAgent.cpp:22-64`) | invisible | - -Matching is printer-scoped (`is_compatible`) in the AMS sync paths and all printer agents — this -is what makes the per-printer invariant sufficient there. But several consumers match -**globally by id alone**, first match wins: - -- `get_filament_by_filament_id("")` — tray display name, `filament_is_support`, - `temperature_vitrification` warnings (`PresetBundle.cpp:690-733`; callers - `DevFilaBlackList.cpp:70`, `Plater.cpp:3453`, `SelectMachine.cpp:3560,4623`). The code - comment at `:695` states the assumption outright: an id maps to ONE material globally. -- `MachineObject::setting_id_to_type` (`DeviceManager.cpp:2538`), calibration-history name - lookup (`CaliHistoryDialog.cpp:62`), custom-filament cloud grouping (`Preset.cpp:2839`). -- The slicing pipeline itself: multi-nozzle filament grouping merges project filaments whose - `(filament_id, color)` match (`FilamentGroup.cpp:513-528` via `ToolOrdering.cpp:1164`). - -**Consequence:** within one printer, duplicate ids break AMS matching (silent first-wins, -`find_if` at `PresetBundle.cpp:3132/3233`; the AMS tray-edit dialog even *hides* the second -preset, `AMSMaterialsSetting.cpp:894-897`). Across vendors, the same id on *different -materials* feeds wrong name/type/vitrification data to the global consumers and can merge -different materials into one nozzle group. Same id on the *same* material (e.g. `GFL99` = -Generic PLA in 29 vendors) is comparatively benign — those attributes agree. - -### Identity machinery - -- **Effective id resolution** (`PresetBundle.cpp:4842-5080`): own `filament_id` key → vendor - `filament_id_maps[inherits]` (file order in the vendor index is load-bearing) → - OrcaFilamentLibrary base-bundle map. An instantiated system filament that resolves *no* id is - a hard load error that discards the whole vendor bundle (`:5072`, throw at `:5141-5147`) — - so "missing id" cannot ship; what looked like 26 id-less Flashforge presets actually resolve - to OFL's `OGFL99`/`OGFG99` through the base-bundle fallback. -- **Two family-identity systems exist**: `filament_id` (device matching) and `alias` (name - before `" @"`). OFL shadowing is keyed on **alias**: `update_library_profile_excluded_from` - (`Preset.cpp:3684-3714`) hides an OFL preset (empty `compatible_printers` = compatible with - everything, `Preset.cpp:837`) on printers claimed by a same-alias vendor preset. There is - **no id-based shadowing**. A vendor preset that tunes an OFL generic but renames it - re-exposes the OFL preset and creates a live duplicate. The rule below aligns the two - systems: one family = one alias = one id. -- **User-custom id space**: user-created filaments get `"P" + md5(name)[0:7]` (8 chars, - `CreatePresetsDialog.cpp:533`), or *reuse a system id* when the base name matches an existing - preset (`:510-528`). `"null"` is used as a sentinel. Root user presets persist their id - forever; inheriting user presets re-derive it on every load. - -## 2. The id landscape today - -**Bambu's grammar** (derived from all 1970 BBL instantiated presets; BBL is internally clean — -181 id definitions, 0 duplicates): - -- Classic `GF`: letter = family (A Bambu-PLA, B ABS/ASA, C PC, G PETG/PCTG, - L third-party+generic PLA, N PA/PPA, P PP/PE, R misc, S support, T PET/PPS, U TPU). - Numbers: 00-49 branded ascending, 50-59 fiber-filled, 60-70 partner block, **95-99 generic - tier descending** (99 = the family's plain generic). -- Brand partners `GF`: GFPM Polymaker, GFOT Overture, GFSNL SUNLU, GFNMK - Numakers. One id per product line; never per color, never per printer/nozzle/diameter. -- Structural rule: the id lives on the material's `@base`; every variant inherits it. -- Hardcoded in C++: `GFS00/GFS01` support check (`DeviceManager.cpp:4739`), per-family PA - defaults `GFU01/03/04` (`CalibUtils.cpp:54-75`) — `GF*` is Bambu's space, byte-frozen. - -**Everything else is ad-hoc, invented by individual contributors and imitated** (full history -in §7): OFL's `O`-prefix mirrors (`OGFA00`; introduced 2025-03-31, commit `8c4a65e3e1`), -Tiertime/Afinia `GFx##_##` per-printer-line suffixes, SeeMeCNC per-nozzle ids, LH `LHF_pla`, -LONGER 11-char pseudo-GF ids, Anycubic ids **with spaces** (`"GFPLA Silk"`), Prusa ids that are -entire preset names (36 chars), and mass copy-paste of `GFL99/GFB99/GFG99` onto everything -(Qidi alone stamped `GFB99` into **317 files** across all materials). The only guidance that -ever existed was "≤ 8 chars" — enforced for BBL only (`orca_extra_profile_check.py:292,320`), -and the (now removed) profile wiki's own examples *taught* id copy-pasting. - -**The damage, quantified** (audit reproduces validator 1256/1256): - -| Ledger | Count | -|---|---| -| Within-vendor logical collision groups (validator `-f`) | **356** across 30 vendors (1256 printer-level errors) | -| OFL×vendor same-id groups (validator blind spot) | 42 — of which **10 are live** (alias mismatch defeats shadowing); 32 already neutralized by alias shadowing | -| OFL-internal: one id, several materials, visible on every printer | **16 ids** (e.g. `OEPLAB00` = 14 distinct Elegoo PLA products; `OGFL06` = eSUN PLA-Marble *and* Fiberon PETG-ESD) | -| Cross-vendor semantic collisions (same id, different materials) | **67 ids** (e.g. `GFU99` also covers a PEBA; Anycubic minted `GFL95` "Matte" ≠ Bambu `GFL95` "High Speed") | - -Worst vendors by groups: Qidi 97, Flashforge 69, Elegoo 37, Prusa 31, Cubicon 17, -Anycubic 14, InfiMech 14, Snapmaker 11, Artillery 8, Creality 8, FlyingBear 8. - ---- - -## 3. The rule (proposal) - -Selected by a 3-design / 2-judge adversarial process (deterministic-mint won over -registry-grammar and structure-only on ambiguity-prevention, contributor simplicity, and -enforceability), then hardened by three adversarial review passes. This section is written as -the future authoring doc. - -### 3.1 The one-question test - -> **Would a user consider this a different spool product than anything already in the tree?** -> Different polymer, different sub-brand (Basic / Matte / Silk / HF), fiber-filled sibling, or -> a second selectable diameter → **new family, new id**. The same spool tuned for another -> printer or nozzle → **join the existing family** (inherit its `@base`, no id key). Tuning a -> generic material → **join the OFL family** (inherit the `Generic X @System` preset, keep the -> `Generic X` base name, add no id). - -Same id / new id at a glance: - -| Situation | id | -|---|---| -| Per-printer / per-nozzle variant of an existing material | same id (inherit, never write the key) | -| Sub-brand or product line (PLA vs PLA Matte vs PLA Silk vs PLA HF) | new id each | -| Color | never a new id | -| Second diameter selectable on the same printer (1.75 + 2.85) | sibling family, new id | -| "High-speed" tuned for a *different printer model* | same id (it's a printer variant) | -| "High-speed" selectable *alongside* the normal preset on one printer | new id (it's a product line) | - -### 3.2 Structure - -1. **One family = one root.** Each material family has root preset(s) (`instantiation:false`, - typically ` @base`) and only roots carry the `filament_id` key. Instantiated - variants inherit a root and never write `filament_id`. (A family MAY have several roots — - e.g. Qidi's per-series bases — but they must all declare the *identical* id.) -2. **Family identity is declared, not name-derived.** Default: the family name is the - instantiated presets' base name (name with `/\s?@.*$/` stripped — note *optional* space, - because `Afinia PLA@HS`-style names exist). When vendor naming makes that ambiguous, the - root declares an explicit `"filament_family"` key that overrides derivation; tooling errors - loudly when a root's derived family differs from its children's. -3. **Within a family, variants' `compatible_printers` are pairwise disjoint** — that *is* the - PR #14459 invariant, enforced by the validator. -4. **Generics belong to OFL.** A vendor tuning `Generic PLA` inherits - `Generic PLA @System`, keeps the `Generic PLA` base name/alias (so alias shadowing excludes - the OFL preset on those printers, `Preset.cpp:3684`), sets non-empty `compatible_printers`, - and writes no id. A vendor-*branded* filament never rides a generic family id. -5. **Ids are immutable once shipped.** Renaming a family does not change its id (use - `renamed_from`). No id is ever recycled for a different material — stale ids live on in - user root presets and old 3mfs, and a recycled id would silently match the wrong material. - -### 3.3 Minting — nobody invents ids - -New family ids are computed, exactly like the `setting_id` precedent -(`scripts/assign_vendor_setting_ids.py` / `Slic3r::generate_preset_setting_id`): - -``` -filament_id = "OF" + base62_6( uuid5( NAMESPACE, "filament_family//" ) ) -``` - -8 chars total (satisfies the AMS length limit), same base62 derivation and a dedicated -namespace constant. On the astronomically rare collision with an existing id, the minter salts -the input (`.../1`, `/2`, …) until free; the result is simply frozen in the file. - -- **Script path:** author commits the family with *no id anywhere*; - `python scripts/assign_filament_ids.py` inserts the minted id into the root(s). Idempotent; - never rewrites a valid existing id. A `--mint "/"` one-shot prints the id - without touching the tree. -- **No-script path:** CI fails with the exact line to paste: - `family "MyBrand PLA" (vendor X) needs filament_id "OFq3xT9k" in "MyBrand PLA @base.json"`. - -**Reserved namespaces — never mint or hand-write into:** - -| Space | Owner | Status | -|---|---|---| -| `GF*` | Bambu AMS/RFID catalog | byte-copies of authentic Bambu ids only, and only where a checked-in `shared_catalog` list sanctions the family (BBL bundle; OFL mirrors; byte-matching families in other vendors, e.g. Snapmaker's Fiberon) | -| `QD_*` | Qidi device protocol | frozen; Qidi-only; exempt from family-shape checks | -| `P[0-9A-Fa-f]{7}`, `"null"` | user-created custom filaments (`CreatePresetsDialog.cpp:533`) | never emitted for system presets (reserve case-insensitively) | -| everything already shipped | grandfather snapshot | frozen as-is (§5) | - -Trade-off accepted: minted ids are opaque (`OFq3xT9k` carries no "PLA" mnemonic — the family -name in the same file provides that). The judges preferred this over a Bambu-style extended -grammar because it removes the "who allocates the next number" ceremony, cannot race between -concurrent PRs, and needs no registry maintenance. If mnemonic ids are strongly preferred, the -runner-up design (`` + registry file) is documented in the workflow -records; everything else in this plan is unchanged under either format. - -### 3.4 What CI enforces (all vendors, ratcheted) - -Extend `scripts/orca_extra_profile_check.py` (it imports the same mint function; setting_id -precedent) and the C++ validator: - -1. **Format**: an id is valid iff `OF[0-9A-Za-z]{6}` **or** in the grandfather snapshot **or** - vendor==BBL **or** `QD_*` in Qidi. No whitespace/ASCII/length checks needed outside the - grandfather set — new ids are minted, and the grandfather set is closed. -2. **Uniqueness ratchet**: id→families multimap computed tree-wide; no id may acquire a family - claim not recorded in the snapshot (snapshot legalizes today's benign `GFL99`-style sharing; - new multi-claims are errors; `shared_catalog` entries are the sanctioned exception). -3. **Structure ratchet**: `filament_id` key only on roots; every instantiated filament must - resolve an effective id via the simulated loader walk; all members of one family resolve the - same id; **a preset may not declare an id different from its inherited effective id** (the - `Generic SBS` drift bug class). Pre-existing violations are snapshot-frozen; new ones error. -4. **Mint conformance**: an id new relative to the snapshot must equal the mint (or a salt - iteration); the error message prints the expected value. -5. **Stability**: an `(id, family)` pair on main may not change or vanish, *following - `renamed_from` chains* (so an honest rename passes), unless listed in a maintainer-gated - migrations file. Retired ids go to an append-only `retired_ids` ledger; a retired id may - never be defined again for any family. -6. **Alias hygiene for tuned generics**: a preset inheriting an OFL `Generic * @System` (with - no own id anywhere in its vendor chain) must keep the OFL base name and have non-empty - `compatible_printers` — error message names the rename as the cause. -7. **C++ validator**: extend `check_duplicate_filament_subtypes` (`PresetBundle.cpp:5654`) to - include OFL presets in every vendor's per-printer check, *minus* alias-excluded ones — - `m_excluded_from` is already populated in the validator context (`update_system_maps` at - `PresetBundle.cpp:2302`), so the 32 shadowed pairs won't false-positive and the 10 live ones - will be caught. Run `-f` per vendor in CI, widening as vendors are cleaned (§5). -8. **Runtime backstop** (one-line change): log a warning when the AMS-sync `find_if` - (`PresetBundle.cpp:3132/3233`) finds 2+ compatible presets for one id — the only layer that - can see side-loaded/forked bundles. - ---- - -## 4. Error-pattern taxonomy (goal 2) — with counts and fixes - -356 groups were classified by 37 agents reading the actual profiles, spot-checked -independently (1 substantive disagreement in 14 samples). Counts below fold the spot-check -corrections in. **Fix rule for all patterns: replacement id values are always freshly minted -`OF*`; the "which preset keeps the id" decision uses the precedence _Bambu-catalog material > -OFL generic family > family that historically introduced the id_.** - -| # | Pattern | Groups | Fix | -|---|---|---|---| -| P1 | `copy_paste_id` — different materials share an id verbatim (task's error 1). Qidi's `GFB99`×317-files epidemic; Anycubic's three id "eras"; Peopoly `GFSL99` on ABS | ~167 | impostor families get minted ids on their roots; owner keeps the id | -| P2 | `wrong_inherits` / id-less product lines — a variant inherits another family's root (PR #14459's Panchroma case) or a product line never got its own root (Prusa HF; Flashforge's ~140-preset `FFG01` umbrella) | ~87 | create per-family roots with minted ids; re-point `inherits`; **never** delete the id-less shadow/base files — convert them (they carry real config: Flashforge `fdm_filament_pla` differs materially from OFL's) | -| P3 | `generic_family_overlap` — vendor-*branded* preset rides a generic family id via inheritance (task's error 2, generalized). Includes the 10 live OFL duplicates | ~68 | branded presets get minted family ids; true generic tunings instead adopt the OFL family *with matching alias* (rule 3.2.4); Sovol is the elegant case — just **delete** its wrong own-id lines and let OFL ids flow through inheritance | -| P4 | `overclaim_compat` — same material, broader variant claims a printer that a dedicated variant covers (the BBL H2DP pattern fixed in #14459) | ~24 | trim `compatible_printers` of the broader preset (Dremel, Cubicon `@base`s that are also instantiated, Wanhao France Bowden/Direct) | -| P5 | template-carried id — id declared on a shared settings template (`fdm_filament_*`, `fdm_filament_common`) so every family inheriting it collapses (Prusa, Ginger Additive, Snapmaker TPU base) | inside P1/P2 counts | move ids off templates onto family roots | -| P6 | OFL-internal collisions — 16 ids spanning several materials, visible on every printer (Elegoo blocks, Elas `OGFA00`×3, `OGFL06` polymer mismatch, `Generic PETG HF/PETG-CF @System` missing own ids) | 16 ids | fix inside OFL first (it's the base bundle every vendor resolves against) | -| P7 | alias-mismatch re-exposure — vendor tunes a generic under a different name, OFL preset resurfaces (Snapmaker `PolyTerra J1 PLA` vs OFL `PolyTerra PLA`) | 10 live | rename-to-alias where it's genuinely the same family, else mint | -| P8 | per-variant ids — no ambiguity, but family semantics broken: every variant has its own id (Prusa name-ids, SeeMeCNC nozzle suffixes, Afinia/Tiertime `_##`, iQ) so device matching can't identify the material across nozzles | ~210 presets | grandfather (they're unambiguous); converge opportunistically; document as anti-pattern | -| P9 | format violations — spaces (`"GFPLA Silk"`), >8 chars (LONGER, SeeMeCNC, LH), GF-shaped inventions (Anycubic `GFL93-97`, CoLiDo `GFA99`) | in the above | fixed as a byproduct of re-minting; snapshot freezes the unambiguous rest | -| P10 | cross-vendor semantic collisions — 67 ids meaning different materials in different vendors (dangerous via the global unscoped consumers, §1) | 67 ids | mostly eliminated by P1-P3 re-minting; the remaining same-material generic sharing is legalized by the snapshot | -| P11 | deliberate coexistence & data hygiene — Snapmaker "Benchy" demo presets (gated by `compatible_prints`, which the id check can't see) and a self-collision from duplicate `compatible_printers` entries | 4 | give demo presets own minted ids; dedupe list entries; add a lint for duplicate array entries | - -New patterns beyond the two in the task (goal 2.3): P5-P11. - ---- - -## 5. Migration plan - -### Safety foundation (verified, §1/§7) - -Safe: fresh never-used ids, family-consistent; trims of `compatible_printers`; inherits -re-pointing; OFL id changes (children re-derive). Unsafe: touching `BBL`/`QD_*`; recycling or -swapping ids; splitting a family's id; **deleting or renaming preset names** (user presets -whose `inherits` no longer resolves are dropped at load, `Preset.cpp:1687-1691`) — if a name -must go, `renamed_from` coverage is mandatory. - -### Phases - -0. **Land the rule + tooling first** (no profile changes): `scripts/assign_filament_ids.py` - (mint + insert + `--mint`), the extended `orca_extra_profile_check.py` checks in - snapshot-ratchet mode, the C++ validator OFL cross-check, the runtime warning, the rule doc. - Generate `filament_id_snapshot.json` (id→families multimap over main) and empty - `retired_ids.json` — both checked in. -1. **OFL first** (it's the base bundle every vendor resolves against): fix the 16 internal - collisions (Elas/eSUN/DREMC copy-pastes get mints; `Generic PETG HF/PETG-CF/PP-CF/PP-GF/ - PE-CF/PLA Matte @System` get their own family roots+ids instead of collapsing into their - parent generic), keeping every current effective id that is unambiguous. -2. **Per-vendor PRs, worst-first**: Qidi → Flashforge → Elegoo → Prusa → Cubicon → Anycubic → - InfiMech → Snapmaker → the long tail (22 vendors, mostly 1-8 one-line fixes). Each PR flips - that vendor into CI's `-f` scope (`check_profiles.yml` currently `-v BBL -f`; append - vendors as they reach zero; when all are in, drop `-v` and run tree-wide). -3. **Delete the ratchet allowlists** once tree-wide zero holds; checks become hard rules for - everything born after the snapshot. - -### Migration-script contract (from the adversarial pass — important) - -- Consumes from the classification only: group membership, fix category, keep-id precedence, - inherits-repoint targets. **All replacement id values are recomputed via the mint** — id - literals in analysis notes (e.g. `GFA00_02`, `GFS98`, `GFG96`) are legacy-culture artifacts - and are ignored with a warning; assert no emitted id matches `^(GF|QD_|P[0-9A-Fa-f]{7}$)`. -- **Family-atomic**: re-idding any preset re-ids every same-family sibling in the same commit, - even siblings outside the collision group (FlyingBear `GFB99 @S1` vs `@Ghost7`); Prusa is - family-atomic per material (its HF/CF families span frozen `_N`-suffix ids — freeze what's - unambiguous, mint once per family for the colliding members). -- **Diff bound**: a vendor migration PR may only touch collision-group files + same-family - siblings of re-idded presets; every id that is per-printer-unambiguous today stays - byte-identical. -- **Config-equivalence gate**: dump every instantiated preset's flattened effective config on - main and on the PR head; the diff must be empty except `filament_id`/`inherits`/ - `compatible_printers` edits the plan prescribes (this is what makes the Flashforge - shadow-file conversions safe: each id-less `fdm_filament_*` shadow becomes a named vendor - family root carrying its config byte-for-byte, children re-pointed, then the shadow name - retired). -- **No deletions**: redundant presets (Flashforge's byte-identical `Generic X`/`Flashforge X` - twins) are re-minted, not dropped; consolidation with `renamed_from` is a separate, - human-reviewed cleanup. -- Expected user impact: none for inheriting user presets, 3mfs, Klipper/Creality/Snapmaker - sync. Residue: user *root* presets that copied an old system id keep it forever - (AMS auto-match falls back to generic-by-type — low severity, unavoidable from the repo). - -### Vendor-specific notes (from classification) - -- **Qidi (97)**: three profile generations. `QD_*` generics are correct and frozen; the fix is - the brand families (Bambu/HATCHBOX/Overture/PolyLite/Tinmorry/QIDI-brand) that all carry - `GFB99/GFG99/GFL99`. Multi-root families are the norm (`...@Q2-Series` / `@Q2C-Series` / - `@X-Max 4-Series` bases) — same mint lands in every series root of one family. -- **Flashforge (69)**: two umbrellas (`FFG01` ~140 presets; `GFB99/GFG99/GFL99` G3U-era) + - OFL-riding branded presets + shadow-file conversion (above). -- **Elegoo (37)**: single mistake — every commercial variant inherits the material-class - `@base` (`EB00`); mint one id per product line (Silk/Matte/PRO/Rapid/…), roots exist. -- **Prusa (31)**: HF product lines need their own roots; ids move off `fdm_filament_*` - templates; frozen name-shaped and `_N` ids stay. -- **Sovol (6)**: delete the wrong own-id lines; correct ids flow from OFL by inheritance. -- **Cubicon (17)**: `@base` presets are themselves instantiated + over-claiming; 9 file edits. - ---- - -## 6. Concrete work items (PR-sized) - -1. `scripts/assign_filament_ids.py` + mint function + tests (incl. the 14-group regression set - from the adversarial pass). *(new)* -2. `orca_extra_profile_check.py`: checks §3.4-1..6 in ratchet mode + snapshot/ledger files; - drop the BBL/OFL-only gate at `:292` and the OFL skip at `:601`. *(extend)* -3. C++ validator: OFL-aware `check_duplicate_filament_subtypes`; runtime ambiguity warning at - the two `find_if` sites. *(small)* -4. Rule documentation: `doc/developer-reference/filament_id.md` (recreate the path; wiki - cross-link) + profile-PR template checkbox ("new materials: no filament_id key anywhere; CI - prints the minted id"). *(new)* -5. OFL migration PR (phase 1). -6. Per-vendor migration PRs (phase 2), each widening CI `-f` scope. - -The audit tooling from this analysis (loader-faithful resolver; reproduces the validator -1256/1256) is in this session's scratchpad (`audit_filament_ids.py`) and is the natural seed -for items 1-2. - -## 7. Evidence & methodology - -- Validator ground truth: `OrcaSlicer_profile_validator -f` tree-wide → 1256 errors; audit - script reproduces exactly (356 logical groups after dedup by (vendor, id, preset-set)). -- Code analysis: 5 parallel agents over runtime consumers, persistence/migration surface, - loader semantics, Bambu grammar, and convention history — all claims carry `file:line`. -- Classification: 37 agents (one per vendor chunk) reading actual profile JSONs; 14-sample - independent re-derivation found 1 substantive error (a Flashforge group mislabeled - `missing_id`; corrected — the loader resolves those ids from OFL). -- Design: 3 independent designs → 2 judges (both chose the deterministic mint; scores 58/50/56 - and 60/54/56) → 3 adversarial attackers (35 scenarios; every `breaks` finding is folded into - §3.2-3.4/§5 as an amendment: declared families, multi-root support, ratchet-not-absolute - checks, no-deletion rule, shadow-file conversion, classification-id-literal quarantine, - diameter siblings, case-insensitive P-hex reservation, fork guidance). -- Key history: `OGF*` born 2025-03-31 (`8c4a65e3e1`, no PR); `_##` suffix born PR #9739; - 8-char check born PR #9574; wiki (with the id-copy-paste example) removed 2025-11-24 - (`f0d79b99eb`). - -## 8. Open decisions for maintainers - -1. **Id format**: opaque deterministic `OF*` mint (recommended, judges 2/2) vs mnemonic - registry grammar (runner-up). Everything else in the plan is format-agnostic. -2. **Multi-vendor brands** (Snapmaker ships Fiberon with authentic `GF*` ids): sanction via - `shared_catalog` (recommended, low churn) vs hoisting those families into OFL. -3. **Benchy-style demo presets**: mint ids per demo preset (recommended) vs teaching the - validator `compatible_prints` gating. -4. **Alias alignment**: fix the 10 live alias-mismatch OFL duplicates by rename-to-alias - (better long-term, needs `renamed_from`) vs minting vendor ids (safer, more ids). -5. **Where the rule doc lives**: in-repo `doc/` (recommended — CI messages need a stable link) - vs wiki-only. diff --git a/filament_id_plan_v2.md b/filament_id_plan_v2.md deleted file mode 100644 index b5bf0f1c65..0000000000 --- a/filament_id_plan_v2.md +++ /dev/null @@ -1,314 +0,0 @@ -# filament_id plan v2: the `get_filament_id` (P+md5) proposal, validated - -> **Status note:** the verdict in §0-§4 and §7-§8 stands. The work items in §5 are -> superseded by `filament_id_plan_v3.md` (OFL-as-catalog, content-addressed triple key, -> succession ledger): W1/W3 are carried into v3.0, W2/W5 carry unchanged, W4 is dropped. - -Follow-up to `filament_id_plan.md` (v1, implemented on `feature/filament_id`: deterministic -`OF*` mint + snapshot ledger + 30-vendor migration, tree-wide validator `-f` = 0). - -**Proposal under review:** mint system `filament_id`s with the scheme of -`static std::string get_filament_id(std::string vendor_typr_serial)` -(`src/slic3r/GUI/CreatePresetsDialog.cpp:487-552`) — Bambu's user-custom-filament id -allocator — so system ids are "compatible with Bambu AMS sync". - -Every claim below was verified in source (`file:line`), against upstream BambuStudio, or in -primary online sources (URLs in §8). Verdict first, evidence after. - ---- - -## 0. Executive summary — the verdict - -**Do not re-mint system ids as `P+md5(name)`. Keep the v1 `OF*` mint.** Three facts decide it: - -1. **The AMS/device path never validates id *shape*.** `tray_info_idx` is an opaque string - end-to-end: sent verbatim (`DeviceManager.cpp:1642`), parsed verbatim - (`DevFilaSystem.cpp:512-514`), and firmware persists arbitrary bytes — an A1 stored and - echoed a *corrupted* id `Pde2\xea58c` (bambulab/BambuStudio#5436). A P-shaped system id - is not "more acceptable" to the device than an `OF*` one. -2. **P-shape buys zero recognition.** The printer resolves tray ids only against its built-in - `GF*` catalog plus the *account's* cloud custom-filament cache (official Bambu wiki); ids - it cannot resolve round-trip as `?`. Orca can never upload system presets into that cache - (`Preset.cpp:2071` gates cloud upload on `is_user()`), so an Orca system `P` id shows `?` - exactly like an `OF` id. The public catalog id space is 100% `GF*` (ha-bambulab - `filaments.json`: 86/86 ids). -3. **P-shape is the one shape the codebase punishes.** The *only* id-shape dispatch in the - entire tree is `MachineObject::check_ams_filament_valid` + `update_filament_list` - (`DeviceManager.cpp:5335/5352/5395/5410`, gate `setting_id.size()==8 && [0]=='P'`), a - user-custom lifecycle reconciler that **remotely wipes an AMS tray** - (`command_ams_filament_settings(..., "", "", white, "", 0, 0)` at `:5345`) or rewrites its - temps (`:5361-5367`) when a P-shaped tray id drops out of the user-root preset list - (`:5219`, armed at `:5268`). Name-keyed minting makes system id == user-custom id *by - design* (same name → same id), so "user deletes their now-redundant custom after the - system preset ships" becomes a remote tray wipe. `GF*` (wrong length) and `OF*` (wrong - first char) are structurally immune. - -The proposal's *real* value lives in two places, and v2 captures both without the re-mint: - -- **The adopt step** (the function's first branch, `:509-511/:526-528`): same base name → - same id. In-app, this already gives Orca-local convergence today — a user custom named - like a system family adopts the system id *whatever its shape*. At authoring time, the - same semantics = the v1 `shared_catalog` mechanism; §5/W4 extends it (curated, optional) - to let byte-authentic Bambu-catalog families carry authentic `GF*` ids. -- **The hash step belongs to user space.** `P+md5(name)` is Bambu's allocator for *user* - presets; the community is already asking for it there (OrcaSlicer PR #13315, unique ids - for inherited user presets). §5/W5 endorses that lane. System profiles stay out of the - `P` namespace — and v2 adds client hardening (W1) because ten *already-shipped* system - P-hex ids (Cubicon `P510cf**`×8, Ginger `P510eff9`, Artillery `Pfcf9c4c`) sit inside the - wipe-gate's shape match today. - -Judge panel (3 independent lenses over 4 variants; §7): keep-`OF*` won 23/40 aggregate -(device-ecosystem 6, identity-semantics 8, migration-enforceability 9) vs full Bambu -emulation 11, hash-only name-keyed 12, vendor-scoped P-hash 12. The single dissent -(device-ecosystem, favoring adoption) is honored by W4; its own verdict on the shape was -"adopt the semantics, not the costume". - ---- - -## 1. What the proposed generator actually is (verified) - -`get_filament_id(vendor_typr_serial)` (`CreatePresetsDialog.cpp:487-552`), called with -`" "` (`:1128`, `PLA-AERO`→`"PLA Aero"`), and per-preset with the -display base name when cloning presets for a user-created printer -(`Preset.cpp:2785-2803` → `:2798-2800`): - -1. **Adopt:** scan a temp bundle of *every* vendor bundle on disk - (`PresetBundle::load_system_filaments_json`, `PresetBundle.cpp:2342-2391`) plus the - user's presets, then the live bundle (`Preset.cpp:2831-2842`); if any preset's base name - (`name.substr(0, first('@') - 1)`) equals the input and its id ≠ `"null"`, **return that - id** — which can be `GF*`, `QD_*`, `OF*`, or another user's P-hex. -2. **Mint:** `"P" + md5(input)[0:7]`, lowercase hex over the raw UTF-8 bytes (`:533`, - `:480`). -3. **Collide:** if the id is held by a *different* name, re-hash with a local wall-clock - salt (`md5(input + get_curr_time())`, `:547`) — non-deterministic by construction. - -Upstream parity: the function and its caller input construction are **byte-identical** in -BambuStudio master (`CreatePresetsDialog.cpp` L434-499 / L1061; diff empty, fetched -2026-07-03). So identical dialog inputs in BBS and Orca yield identical ids — the kernel of -the proposal's compatibility claim. - -Reproduction limits (why "exactly the approach" cannot be ported to system minting anyway): - -- The base-name parser truncates names lacking a space before `@` (`"Afinia PLA@HS"` → - `"Afinia PL"`); 48 shipped presets hit this, and the *clone* path parses differently - (`" @"` two-char, `Preset.cpp:2792`), so the app itself is internally inconsistent. -- 597 presets (361 whole families, e.g. all of Anker) contain no `@` and are invisible to - the adopt scan and its collision map (`:505-508`). -- The collision salt is wall-clock time — unreproducible across machines, so id values - cannot be CI-verified from names; a ledger would still be the sole source of truth. -- Shipped P-hex ids don't round-trip from names today: Cubicon `P510cfb0` ≠ - `P+md5("Cubicon ABS")` = `Pc624b68` (verified) — so even Bambu-lineage P ids are not - name-derivable in practice. - -## 2. What Bambu AMS sync actually consumes (verified) - -- Send: `command_ams_filament_settings` puts the preset's `filament_id` verbatim into - `tray_info_idx` (`DeviceManager.cpp:1642`); cloud vs LAN differ only in transport - (`:2502-2536`). No client- or firmware-side rejection path exists; the ack parse stores - whatever the printer echoes (`:3808-3860`). -- Resolve (slicer side): tray id → first compatible root preset with equal `filament_id` — - system *and* user roots, no shape filter (`PresetBundle.cpp:3151`, `:3252-3254`); misses - fall back by `"Generic "` name, then similarity, then keep-previous - (`:3157-3169`, `:3260-3305`). `setting_id_to_type` resolves against `is_system` presets - only (`DeviceManager.cpp:2538-2559`) — a system preset resolves *because it is system*, - never because of its id shape. -- Resolve (device side): built-in `GF*` catalog + the account's cloud custom cache; - custom-filament sync is **cloud-only** (not LAN), and an id the printer has no data for - displays `?` until opened once on the printer screen (official wiki, §8). Third-party - consumers see customs as "unknown" (ha-bambulab #466/#540). -- Since Jan 2025, AMS configuration is gated behind Bambu-signed clients on new firmware - (Developer Mode / older firmware exempt) — any AMS-sync benefit for Orca is conditional - on that regardless of id scheme (Bambu authorization blog, §8). -- Orca already ships non-`GF`/non-`P` shapes to Bambu trays in the field: OFL presets are - compatible with every printer (`PresetBundle.cpp:5681-5684`) and carry `OGF*`/`OFL*` ids - (e.g. `OFLSBS99`, hardcoded in `MoonrakerPrinterAgent.cpp:654`). The premise "the - ecosystem has only ever seen `GF*`/P-hex" is already false, with no observed rejection. - -**Cross-client reality check.** The name-keyed benefit ("a BBS-created custom named like an -Orca system preset lands on the same id, so Orca's sync matches the system preset") is -mechanically true but narrow: it needs byte-exact names (including the `PLA Aero` mapping -and single spacing), the unsalted hash path, cloud mode, and permissive firmware. The -symmetric case already works in Orca *without* any re-mint: an Orca user creating a custom -named like a system family **adopts the system id** via the in-app adopt branch, so local -custom ↔ system ↔ tray matching is shape-independent today. - -## 3. The disqualifying finding, in mechanism form - -`update_filament_list` (`DeviceManager.cpp:5208-5272`) snapshots `{filament_id → temps}` -over **user root presets only** (`preset.is_user() && preset.inherits() == ""`, `:5219`); -ids that vanish or change temps between snapshots are armed into `checked_filament` -(`:5268`). `check_ams_filament_valid` (`:5311-5445`) then, for every AMS/virtual tray whose -`setting_id` matches `size()==8 && [0]=='P'`: - -- id armed and **not** in the current user-root list → remotely **clears the tray** - (`:5335-5349`, `:5395-5408`); -- id armed and in the list with unequal temps → **rewrites tray temps from the user - preset**, ignoring same-id system presets (`:5352-5374`; `PresetBundle.cpp:3719` skips - non-user presets; unguarded `find()->second` at `:3717`). - -Failure scenario under the proposal: system `"PolyLite PLA"` ships with -`P+md5("PolyLite PLA")`. A user's same-named custom (BBS- or Orca-created — same id by -design) is on an AMS tray. The user deletes the now-redundant custom. Next status refresh: -the id is armed, absent from user roots → Orca wipes the tray, even though the system -preset still resolves that id perfectly. Runs continuously (`StatusPanel.cpp:3275-3277`). - -Two corollaries independent of the proposal (→ W1): - -- The ten shipped P-hex **system** ids (Cubicon/Ginger/Artillery) already pass the shape - gate; only the "id must transit a user root" arming condition protects them, and the - in-app adopt branch can create exactly that transit today. -- `assert(it->first.size() == 8 && it->first[0] == 'P')` (`:5252`) is violable today: a - user custom named `"Bambu PLA Basic"` adopts `GFA00` onto a user root and aborts debug - builds. - -## 4. Identity costs the re-mint would add (quantified, tree dry-run) - -- 1148 distinct family base names; **198** appear in 2+ vendors; **159** groups carry ≥2 - distinct ids today and would silently merge under name-keying (Generic PETG alone: 15 - ids today). 112 involve BBL; 82 are `GF↔OGF` mirror pairs. -- **4** cross-vendor groups share a name across materially different `filament_type` - (Generic PLA Silk, Generic PETG-CF, Generic PA6-CF, Generic PE-CF) — one merged id would - feed wrong type/name/vitrification to the global unscoped consumers - (`PresetBundle.cpp:690-733`; `DeviceManager.cpp:2538`; `FilamentGroup.cpp:513-519`) (→ W3). -- 33 deliberately-split families on the branch would re-merge (25 Elegoo-vs-OFL pairs, 7 - FlyingBear-vs-InfiMech "Other *" pairs, the salted Cubicon PC pair) — undoing v1 - decisions that the fixture gate forced. -- Renames become id migrations: name-keyed ids re-key on any marketing rename; the ledger - can freeze them, but then name-derivability — the scheme's selling point — dies family - by family. The `OF*` key (`vendor/family`) survives display-name churn. -- Entropy drops 35.7 → 28 bits (still 0 collisions at today's 1148 names; ~0.75 expected - at 20k). Re-mint surface: 391 unshipped ids across 1327 files, 13 hardcoded tests, the - reserved-namespace policy inversion, and a retired-ledger rebuild. - -## 5. Work items (delta from `feature/filament_id` HEAD) - -**W1 — client hardening (C++, small, ships with the migration PR train).** - a. Skip the tray-reset and temp-rewrite in `check_ams_filament_valid` when **any - `is_system` filament preset carries the tray's id** — no compatibility filter, - mirroring the semantics of the global consumers (`get_filament_by_filament_id`, - `setting_id_to_type`). This protects the ten shipped P-hex system ids and any BBS - custom colliding with them, while ids resolving only to user presets (or to nothing) - keep today's cleanup behavior. - b. Relax the debug assert `DeviceManager.cpp:5252` to a log (user roots legitimately - carry adopted `GF*`/`OF*` ids). - c. Guard `PresetBundle.cpp:3717` (`find()` unchecked before `->second`). - d. Regression-check that AMS sync never clears trays whose id it merely cannot resolve - (OrcaSlicer#4431 class): the keep-previous fallbacks at `PresetBundle.cpp:3163-3169`, - `:3296-3305` must cover the UI paths. - -**W2 — tooling + doc guardrails.** - a. Document `^OF[0-9A-Za-z]{6}$` as the system-mint namespace. **Do not add it to - `reserved_space_owner`** (`scripts/assign_filament_ids.py:394-402`): check 6 exempts - only snapshot-grandfathered claims and the `--update-snapshot` refusal gate - (`:596-621`) would then reject every *future* mint. The space is already enforced — - check 3 (mint conformance, `:488-509`) errors on any OF-shaped id that does not equal - its own family's mint (covered by `test_check3_of_id_must_match_mint`), and the - snapshot diff is the human gate. Add one doc paragraph + a test asserting a foreign - vendor claiming another family's OF id fails check 3. - b. `doc/developer-reference/filament_id.md`: add a "why not P-hex" section citing the - `DeviceManager.cpp:5335` gate and the account-cache resolution model — community - pressure toward `P+md5` exists (PR #13315) and will recur. - c. Document that user roots may legitimately hold adopted system ids (incl. what that - means for cloud upload, `Preset.cpp:1874-1876`), matching BBS behavior. - -**W3 — data hygiene: the 4 same-name-different-type groups.** Audit each (they confuse - name-based fallback matching and any future interop even under vendor-scoped ids): - verify whether the divergent `filament_type` is a data bug (e.g. OFL `Generic PETG-CF` - resolving `PETG`, OFL `Generic PE-CF` resolving `PE`, Flashforge `Generic PLA Silk` - mixing `SILK`+`PLA`, Creality-vs-Elegoo `Generic PA6-CF` as `PA-CF`/`PA6-CF`) or - intentional; fix by type alignment or rename with evidence, gated by - config-equivalence (only `filament_type` diffs as prescribed) + full validator suite. - -**W4 — optional, curated GF adoption (the proposal's adopt step, done safely).** Extend the - v1 `--allow-shared-catalog` mechanism into an explicit per-family adoption worksheet: a - family may carry a byte-authentic Bambu catalog id iff (i) it is verifiably the same - commercial product as the BBL family (vendor evidence + equal `filament_type`), (ii) - validator `-f` stays 0 tree-wide (alias shadowing covers BBL/OFL overlaps), (iii) the - fixture gate passes, (iv) the snapshot diff records the adoption. Candidates: the 13 - actionable BBL-overlapping families minted `OF*` in v1 (e.g. Qidi `Bambu ABS`→`GFB00`, - `PolyLite PLA`→`GFL00`, `Overture PLA`→`GFL04`, `PolyLite ABS`→`GFB60`; `OFLSBS99` is - shipped and frozen; the two type-hazard OFL families are excluded until W3 lands). - **Honest benefit statement:** these presets are compatible only with non-Bambu printers, - so no Bambu device resolves their ids today; the payoff is one-product-one-id coherence - for the global consumers, project portability, and readiness for W6. Recommended seed: - the four Qidi brand families; generics deferred. - -**W5 — user-preset lane (where the proposal's hash belongs).** Support unique ids for - inherited user presets (the PR #13315 pain: children share the parent's id, so AMS always - resolves the generic parent). `P + md5(preset name)` matching - `CreatePresetsDialog::get_filament_id` is correct *in user space*: adopt-first against - existing ids, then hash; ids frozen per preset after mint. Constraints: never emitted - into system profiles (W2a makes that mechanical); W1 must land first so shared-id - lifecycles can't wipe trays. - -**W6 — recorded future option:** SoftFever's "upload OrcaSlicer's filament database - (especially the filament ID) to the printer" (PR #12724 comment). If pursued, Orca ids - become first-class device-visible artifacts and the P-mimicry question is permanently - moot; the `OF*` scheme is the *better* citizen there (collision-free vendor-scoped ids, - no user-custom masquerade). - -Unchanged from v1: mint rule, snapshot/retired ledgers (no OF retirement — nothing is -re-minted), CI wiring, migration commits. `filament_id_plan.md` remains the implemented -baseline; this document records the v2 decision and its follow-on work. - -## 6. Gates (per work item) - -Same battery as v1, all local: `python scripts/orca_extra_profile_check.py` exit 0; -`assign_filament_ids.py --check` exit 0 (46+ unit tests green; new tests for W2a -reservation and, when implemented, W4 adoption sanctioning); validator `-l 2` exit 0, -`-f` tree-wide exit 0, `-r` BBL+Qidi exit 0; custom-preset fixture overlays (the W1/W4 -changes touch exactly the preset-visibility machinery the fixtures exist to protect); -config-equivalence: flattened effective configs differ only in prescribed keys -(`filament_id` for W4, `filament_type` for W3, none for W1/W2). W1 additionally needs a -manual AMS smoke test on a live Bambu printer (tray set/clear round-trip) — a release -checklist item, not CI (the workflows have no hardware runners). - -## 7. Scheme comparison (judge panel record) - -| Variant | Device lens | Identity lens | Migration lens | Σ | -|---|---|---|---|---| -| V1 full Bambu emulation (adopt incl. `GF*` + P+md5 + salt) | 7 | 2 | 2 | 11 | -| V2 hash-only name-keyed `P+md5(base name)` | 4 | 4 | 4 | 12 | -| V3 vendor-scoped P-hash (`P+md5(vendor/family)`) | 2 | 5 | 5 | 12 | -| **V4 keep `OF*` uuid5 vendor-scoped (chosen)** | 6 | 8 | 9 | **23** | - -All three judges answered the pivotal question the same way: the P-hex **shape** confers no -device/cloud benefit (resolution is by value against catalog + account cache; the path is -otherwise shape-agnostic) and is the only shape with a destructive client-side gate. V3 is -strictly dominated (all of the re-mint cost, none of the name-keyed benefit). V1's genuine -half — GF adoption — is captured by W4 at zero re-mint cost. - -## 8. Evidence index - -Code (this branch): `CreatePresetsDialog.cpp:487-552,1128,2798-2800` (generator + callers); -`DeviceManager.cpp:1642` (verbatim send), `:2538-2559` (system-only type resolve), -`:5208-5272` (user-root snapshot, `:5219`, `:5252` assert, `:5268` arming), `:5311-5445` -(P-shape gate + tray wipe `:5335/:5345`, temp rewrite `:5352-5374`, virtual tray -`:5395-5435`); `DevFilaSystem.cpp:512-524`; `PresetBundle.cpp:690-733,3116-3128,3151, -3252-3254,3709-3767,5674-5753`; `Preset.cpp:1874-1876,2071,2785-2842`; -`AMSMaterialsSetting.cpp:886,894-897`; `WebGuideDialog.cpp:67`; -`PresetComboBoxes.cpp:1952-1966`; `MoonrakerPrinterAgent.cpp:654`; -`scripts/assign_filament_ids.py:69,71-72,118-144,394-402,596-621`. - -Online (fetched 2026-07-03): BambuStudio master `CreatePresetsDialog.cpp` L434-499 -(byte-identical generator); wiki.bambulab.com `create-filament` (custom filaments AMS-able -from firmware 1.6.6; only dialog-created presets sync) and `custom-filament-issue` -(cloud-only sync; unknown ids show `?`); blog.bambulab.com authorization-control (Jan-2025 -AMS-config gating); bambulab/BambuStudio#5436 (firmware persists corrupted id); -OrcaSlicer #4431 (tray clobber to `?`), #3874 (cloud-only confirmation), PR #14459 -(the `-f` invariant), PR #14423 + PR #12724 + PR #13315 (maintainer statements / community -P+md5 pressure); greghesp/ha-bambulab `filaments.json` (86/86 `GF*`) + #466/#540. - -Method: 6 parallel evidence agents (generator semantics; AMS/device path; exhaustive -shape-dispatch sweep; upstream/online; tree-wide dry-run over 5892 presets / 1455 families; -branch change inventory) → 3-judge panel (device-ecosystem, identity-semantics, -migration-enforceability lenses). Dry-run artifacts: session scratchpad -`phex_dryrun_{summary,details,followup}.json`. - -## 9. Open decisions for maintainers - -1. **W4 scope**: none / 4 Qidi brand families (recommended) / + generic-tier re-adoption. -2. **W5 timing**: implement in-repo vs review upstream PR #13315 with the W1 hardening as a - prerequisite either way. -3. **W6**: pursue the filament-database-upload design (makes Orca ids device-visible and - ends the mimicry debate for good). diff --git a/filament_id_plan_v3.md b/filament_id_plan_v3.md deleted file mode 100644 index 14b547600f..0000000000 --- a/filament_id_plan_v3.md +++ /dev/null @@ -1,213 +0,0 @@ -# filament_id plan v3: OFL as the catalog, content-addressed product ids - -Supersedes §5 of `filament_id_plan_v2.md` (whose P+md5 verdict stands unchanged) and -revises the mint rule of `filament_id_plan.md` (v1, implemented on `feature/filament_id`). -Decision driver: v1's mint key scoped families to the *profile bundle* (printer brand), so -one commercial product tuned in N bundles carries N ids — e.g. PolyLite PLA ships in five -bundles (`BBL`, `OrcaFilamentLibrary`, `OrcaArena`, `Qidi`, `Snapmaker`), all with -`filament_vendor: "Polymaker"`, under up to five different ids (`GFL00`, `OFceJcLf`, …). -That contradicts what `filament_id` means: one id per commercial product line. - -## 0. The architecture - -Three tiers, two frozen islands: - -1. **BBL island — untouched.** Bambu profiles keep their `GF*` catalog ids byte-for-byte - (device/RFID/cloud contract). No convergence between GF and OFL ids is attempted: the - same product may permanently carry `GFL00` on the BBL side and an `OF*` id everywhere - else. This is a deliberate trade for simplicity; v2's curated-GF-adoption (old W4) is - dropped. -2. **`QD_*` island — untouched.** Qidi's box builds `QD___` from - device enums and requires exactly-matching presets (`QidiPrinterAgent.cpp:146-152`). - Qidi presets carrying `QD_*` are exempt from re-minting, forever. -3. **Everything else converges on OrcaFilamentLibrary.** OFL is the product catalog: a - material family's id is declared once, on its OFL family root. Vendor bundles carry - only per-printer *specializations* of OFL families — same base name, non-empty - `compatible_printers`, **no `filament_id` key** — which (a) alias-shadow the OFL preset - on the printers they claim (`Preset.cpp:3684-3714`: "Generic PLA @Qidi Q2 0.4mm nozzle" - hides "Generic PLA @System" on that machine) and (b) resolve the OFL family's id - through the loader's inherits/base-bundle walk (`PresetBundle.cpp:4842-5080`). Products - OFL does not (yet) carry mint their id in the vendor bundle **with the same rule**; - because the key is bundle-independent (§1), later hoisting the family into OFL never - changes its id. - -Convergence therefore happens by *single declaration point*, not by copying ids across -bundles — no adoption registry, no cross-bundle id claims to curate. - -## 1. The mint rule - -``` -triple = (filament_vendor, filament_type, family_name) # from the family ROOT's flattened config -key = "filament_product///" -id = "OF" + base62_6( uuid5( FILAMENT_ID_NAMESPACE, key ) ) # namespace unchanged: - # c4d3ff49-4c32-5534-a3e3-00894157ab97 -``` - -- **Triple resolution.** All three values come from the family root preset's *flattened - effective config* (`filament_vendor` and `filament_type` are inheritable list options — - take the first element; family_name = the root's base name, `\s?@.*` stripped once). - For own-key-layout families (no root), each declaring preset's flattened config is used - and CI requires all declarers of one family to agree on the triple. A root that resolves - an empty `filament_vendor` or `filament_type` is a CI error (generics use `"Generic"`). - Values enter the key verbatim (UTF-8, no case folding). -- **Bundle-independent by design.** The key contains no bundle name, so the same product - yields the same id whether minted in OFL or in a vendor bundle; migrating a family into - OFL (§0.3) is id-stable. Two bundles independently adding the same triple converge - automatically — and that is correct, because equal (vendor, type, name) *is* the - definition of "same product" here. The known look-alike hazards stay separated by the - type component: the 4 same-name-different-type groups (Flashforge `Generic PLA Silk` as - SILK, OFL `Generic PETG-CF` as PETG, `Generic PA6-CF`, `Generic PE-CF`) hash apart until - their type bugs are fixed (§5, W3) and converge automatically after — self-healing that - pure name-keying (rejected in v2) could not provide. -- **Format and salt unchanged from v1**: 8 chars, `OF` + 6 base62; deterministic `/1`, - `/2`… salt past any taken or retired id (the O+7 widening was evaluated and rejected: - 62⁶ expects 1.9×10⁻⁵ collisions at today's 1,455 families, and `^O.{7}$` would sweep 35 - legacy ids into the conformance gate vs. one today). -- **Content-addressed, deliberately.** If any triple component changes — a family rename, - a `filament_vendor` correction, a `filament_type` fix — the id changes with it. This - *replaces* v1's "ids are immutable once shipped" with "ids are derivable from the - product identity, and identity changes are migrations" — made safe by the succession - ledger (§2). `renamed_from` still gates preset-name compatibility as before. -- CLI: `--mint "Polymaker/PLA/PolyLite PLA"` prints without touching the tree; running - the script plain inserts missing ids; CI errors print the expected id. - -## 2. The succession ledger (the amendment that makes this safe) - -Shipped ids are referenced outside the tree: AFC/Klipper lane data, Bambu AMS trays -holding OFL-only materials, on-device PA-calibration records, user-root preset copies, -3mf `slice_info`. Retiring an id without a forwarding pointer downgrades all of those to -`Generic ` fallbacks. Therefore: - -- **Schema.** `scripts/retired_filament_ids.json` entries become objects: - `{"retired": {"OGFL99": {"claims": [...], "successor": "OFxxxxxx"}}}`. Append-only as - before; a successor may itself be retired later (chains allowed, cycle-checked, and - followed to the live end). A retired id may never be minted again (check 4 unchanged). - Cross-island *hint* entries are permitted for ids Orca cannot retire because another - island owns them (e.g. `GFL99 → `): consulted only when no live - preset matches, so BBL installs still resolve `GFL99` natively first. By the same - principle, an id in a foreign island's space (`GF*`/`QD_*`) that vanishes from the tree - is **released with a hint, never retired** — the island's catalog owns it and may - legitimately (re)ship it later, which check 4 must never block. (Implementation - amendment, v3.0: `--update-snapshot` routes such ids to `hints` automatically; hint - keys may be absent from the tree.) -- **Shipped and consulted at runtime.** The ledger ships in `resources/`; a small helper - (`resolve_filament_id_succession(id)`) follows the chain and is consulted **only on - resolution miss**, before the `Generic ` name fallback, in: - `PresetBundle::get_filament_by_filament_id` (covers `DevFilaBlackList`, `SelectMachine` - warnings, `Plater` tray configs in one place), the AMS sync match predicates - (`PresetBundle.cpp:3151`, `:3252-3254` miss paths at `:3157-3169`/`:3260-3305`), - `PresetComboBoxes::add_ams_filaments`, `MachineObject::setting_id_to_type` - (`DeviceManager.cpp:2545` miss branch), calibration-history name lookup - (`CaliHistoryDialog.cpp:62`), and the #14423 Moonraker lane matching when it lands. - With this in place the OFL re-mint is near-residue-free and every future - content-addressed rename stays safe. -- **Kill the hardcoded generic map.** `MoonrakerPrinterAgent::map_filament_type_to_generic_id` - (`MoonrakerPrinterAgent.cpp:608-658`) hardcodes 23 OFL ids (`OGFL99`… `OFLSBS99`). - Replace it with a runtime lookup of the OFL generic preset by name ("Generic PLA - @System" → its current id), removing the code↔profile lockstep permanently. (This also - releases `OFLSBS99`, v2's one frozen OF-shaped legacy id, for normal re-minting.) - -## 3. Validation rule changes - -- **Check 3 (mint conformance)** becomes a pure function of the root's triple: a non-BBL, - non-`QD_*` id must equal `mint(triple)` ± salt, or be snapshot-grandfathered (the - grandfather set shrinks to ≈ nothing for non-BBL once migration completes). -- **Check 5 (alias hygiene) generalizes and becomes load-bearing.** For *every* - OFL-carried family (not just `Generic * @System`): a vendor specialization must keep the - OFL base name (else the OFL preset un-shadows and creates a live per-printer duplicate — - v1's P7 pattern) and non-empty `compatible_printers`, and must not declare an id. The - C++ validator's OFL-aware `-f` (`PresetBundle.cpp:5674-5753`) already enforces the - runtime consequence; the script check names the rename as the cause. -- **New check 8 (triple integrity):** every id-declaring family resolves a complete, - family-consistent triple; roots missing `filament_vendor`/`filament_type` error. -- **New check 9 (succession integrity):** every retired entry's successor chain ends at a - live tree id (or a documented cross-island hint target); no cycles; retired ids absent - from the tree. -- Snapshot mechanism, reserved spaces (`GF*`→BBL, `QD_*`→Qidi, `P`-hex/`null`→user-custom; - v2's do-not-reserve-`OF*` note stands), and check 1/2/4/7 are unchanged. - -## 4. Migration phases - -**v3.0 — tooling + prerequisites (no profile changes).** New mint + triple resolver -(reuse the flatten machinery), succession schema migration + C++ lookup helper wired into -the §2 miss paths, Moonraker map → runtime lookup, checks 3/5/8/9, tests. Carry v2's W1 -client hardening (skip tray-wipe/temp-rewrite when a system preset holds the id; relax -the `DeviceManager.cpp:5252` assert; guard the `PresetBundle.cpp:3717` deref) — it ships -first regardless. **W3 type fixes land here**, *before* any re-mint: type is now a key -component, so minting before fixing OFL `Generic PETG-CF`/`Generic PE-CF` would re-id -those families twice. - -**v3.1 — OFL re-mint.** Every OFL-declared id re-derives from its triple (mirrors like -`OGFA00`, generics like `OGFL99`, blocks like `OEPLAB00` — all of it); each old shipped id -gains a succession entry pointing at its replacement; the 26 GF-shaped ids OFL declares -(measured at v3.0: only one of them, `GFOT001`, is also declared by BBL) are released to -the BBL island space with hints at the re-minted families — two-island purity. Snapshot -regenerated; full gate battery + fixture overlays (this phase touches preset-visibility -machinery only via ids, but the fixtures are cheap insurance). Implementation notes -(v3.1, executed): OFL generic ids relocate from the fdm_filament_* template bases onto -the product-named "Generic X @System" presets first, so their triples carry the product -name; `fdm_filament_pc` keeps a (transitional) declaration because 7 Prusa presets -inherit it directly — the v3.2 Prusa worksheet re-homes them and deletes it. - -**v3.2 — vendor bundles, worksheet-per-vendor (v1 machinery).** Three sub-cases: - (a) the **391 unshipped v1 `OF*` mints** re-derive under the triple key — no succession - entries (they never shipped; a documented one-time `--forget-never-shipped ` drops - them from the ledger lineage instead of retiring them, since the ledger's rationale — - ids live on in user presets and 3mfs — cannot apply to unreleased ids); - (b) **true generic tunings** riding copied legacy ids (`GFL99`-class) drop their own id - and re-point to the OFL generic family (the v1 Sovol pattern) — their old ids get - cross-island hints where BBL owns them, succession entries otherwise; - (c) remaining **shipped legacy ids** (numeric, name-shaped, pseudo-GF, the 57 - multi-vendor GF residue, the 10 P-hex — everything non-BBL/non-`QD_*`, ~800 ids) - re-mint with succession entries. Retiring the 10 P-hex system ids also removes the last - system ids from `check_ams_filament_valid`'s destructive P-gate. - -**v3.3 — ongoing consolidation (optional, per-vendor, id-stable).** Hoist vendor-unique -products into OFL where a family is genuinely multi-vendor material; thanks to the -bundle-independent key this never changes ids, so it can proceed opportunistically. - -## 5. Gates (every phase) - -`python scripts/orca_extra_profile_check.py` exit 0; `assign_filament_ids.py --check` -exit 0; unit tests green (existing 46 + new triple/succession/adoption tests); validator -`-l 2` exit 0, `-f` tree-wide exit 0, `-r` BBL+Qidi exit 0; custom-preset fixture -archives; config-equivalence — flattened effective configs differ only in `filament_id` -(re-mints), `inherits`/`compatible_printers` (re-points, as prescribed per worksheet), and -the W3 `filament_type` corrections. New for v3: a C++ test that a retired id resolves -through the succession chain in the sync miss path, and a Moonraker test that the generic -map lookup matches the shipped OFL presets. Manual AMS smoke test (tray set → old-id -resolve → clear) stays a release-checklist item. - -## 6. Accepted costs (explicit) - -- **GF ↔ OFL divergence is permanent** for products in both catalogs (PolyLite PLA ≠ - `GFL00` outside BBL). The forward-looking fix is PR #12724-style filament-database - upload, where Orca ids become first-class device artifacts. -- **Renames/type-fixes re-id families** (by design); the succession ledger absorbs the - device/user residue, but each one is still a ledger entry and a snapshot diff to review. -- **User roots** keep whatever id they copied at creation; with succession lookup in - `get_filament_by_filament_id` they now *resolve* instead of dangling — strictly better - than v1's accepted residue. -- **One-time field transition**: devices holding pre-v3 ids (AFC lanes, AMS trays, cali - records) resolve via succession on updated clients; *older* Orca versions and - BambuStudio never resolved OFL-only ids anyway (`?`/generic fallback — unchanged for - them). - -## 7. Open decisions - -1. Whether v3.2(b)'s cross-island hints (`GFL99` → OFL generic) are wanted at launch or - deferred (pure-miss-path feature; zero risk to BBL installs, small review surface). -2. v3.3 pacing: per-vendor PRs opportunistically vs. a dedicated consolidation train. -3. Whether to fold v2's W5 (P+md5 ids for inherited *user* presets, PR #13315) into v3.0's - C++ work or keep it a separate PR (recommended: separate; W1 is its only prerequisite). - -## 8. Evidence - -Carried from v2 §8 (all re-verified this session), plus: `filament_vendor` audit — 49 -distinct strings tree-wide, `"Polymaker"` byte-consistent across all five PolyLite-PLA -bundles; multi-vendor different-family GF residue = 57 ids; `MoonrakerPrinterAgent.cpp: -608-658` = 23 hardcoded OFL ids; mint examples verified live (`Qidi/PolyLite PLA` → -`OFceJcLf` under the v1 key — the fragmentation this plan removes; salt determinism -`OF8afiMO`). Alias shadowing and the OFL id fallback verified at `Preset.cpp:3684-3714` -and `PresetBundle.cpp:4842-5080` during the v1 audit; per-printer `-f` semantics at -`PresetBundle.cpp:5674-5753`. diff --git a/filament_id_plan_v4.md b/filament_id_plan_v4.md deleted file mode 100644 index 046a7cae1c..0000000000 --- a/filament_id_plan_v4.md +++ /dev/null @@ -1,224 +0,0 @@ -# filament_id plan v4: dissolve the Qidi `QD_*` island - -Supersedes §0.2 of `filament_id_plan_v3.md` (the "`QD_*` island — untouched, forever" -decision) and amends every v3 section that carved out `QD_*`. Everything else in v3 — -the mint rule, the succession ledger, the BBL island, checks 1–9 — stands as -implemented (v3.0–v3.2, all gates green at commit `f7c1b290fd`). - -Decision driver: the island contradicts the catalog architecture v3 built. Qidi presets -carry 204 `QD___` ids over 264 declarations (49 families, -measured 2026-08-21) — one commercial product carries up to *five* ids (one per printer -series: `QIDI PLA Rapido` = `QD_0_1_1` … `QD_4_1_1`), which is exactly the -fragmentation v3 exists to remove. The island was frozen because Qidi's filament box -composes these ids from device enums and requires exactly-matching presets -(`QidiPrinterAgent.cpp:146-152`). But v3.0 shipped the machinery that makes freezing -unnecessary: the succession ledger already translates retired ids on resolution miss. -`QD_*` stops being a *preset id space* and becomes a *device protocol namespace*, -translated once at the agent edge. - -## 0. The architecture change - -Two tiers, **one** frozen island: - -1. **BBL island — untouched, unchanged.** All v3 reasoning holds (device/RFID/cloud - contract is external and opaque). -2. **Everything else converges on OFL product ids — now including Qidi.** All 204 - `QD_*` ids re-mint from their family triples and gain **retired** ledger entries - (not cross-island hints: after dissolution there is no island to own them, and - check 4's "never again" is exactly the guard we want against upstream re-adding - them). The box keeps working because `QidiPrinterAgent` resolves its composed - `QD_*` id through `resolve_filament_id_succession()` on miss — the same mechanism - every other retired id already uses. - -Why retire rather than hint (the one real design choice here): hints exist for ids a -*foreign catalog* owns and may legitimately re-ship (`GF*`). Post-dissolution, nothing -may ever re-ship a `QD_*` id in the profile tree — the device composes them at -runtime, the tree translates them. Retired entries make CI enforce that permanently -(`--update-snapshot` refuses resurrections, check 4 refuses occurrences); hints would -permit re-shipping, which is now always a regression. The QD→family mapping is -strictly 1:1 (verified: no `QD_*` id is claimed by more than one family), so the -mode-rule successor is unambiguous for every entry. - -## 1. What the dissolution consists of - -Three independent work packages, ordered for bisectability: - -- **(A) Re-converge the drifted tree** — prerequisite, not Qidi-specific. The - 2026-07 main merge (`f3fa3a34bd`) brought upstream vendor updates in pre-v3 style: - `--check` currently exits with **243 errors** (123 unsanctioned new ids, 46 new - instantiated own-key presets, 31 override drifts, 18 reserved-space claims, 5 - hint-keys re-declared — Qidi re-added `GFB99`/`GFG99`/`GFL99`, Snapmaker re-added - `GFG96`/`GFU99` and even four *retired* ids incl. `OGFL99`, plus Snapmaker U1 - triple errors and a GreenGate3D rename). This is the standing WF-B maintenance - pass; the dissolution's gates cannot go green on a red base. -- **(B) C++ succession hook in the Qidi agent** — safe to land before any data - changes (pure miss-path: while `QD_*` presets still exist, the hook never fires). -- **(C) The dissolution proper** — tooling flip + Qidi data fixes + re-mint + - snapshot/ledger regeneration, one vendor worksheet in the v3.2 mold. - -## 2. Runtime translation (work package B) - -`QidiPrinterAgent.cpp:183-192` currently: compose `setting_id` → keep it if a visible -base preset declares it → else degrade to `filament_id_by_type(tray_type)` (i.e. every -QIDI-brand box slot silently becomes Generic once the ids re-mint). Insert the -succession walk between those two steps: - -```cpp -} else if (!setting_id.empty() && has_visible_base_preset(bundle->filaments, setting_id)) { - tray.tray_info_idx = setting_id; -} else { - // Retired QD_* protocol ids forward to their minted successors via the shipped ledger. - const std::string successor = setting_id.empty() ? std::string() - : resolve_filament_id_succession(setting_id); - if (!successor.empty() && has_visible_base_preset(bundle->filaments, successor)) - tray.tray_info_idx = successor; - else - tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); -} -``` - -`resolve_filament_id_succession` is `Preset.hpp:119` (loads once, cycle-guarded, -empty-safe) — no new includes needed beyond what the file already reaches through -`PresetBundle`. This one hook covers both composition paths: the numeric-series -`build_setting_id` lambda *and* the non-numeric fallback -`map_filament_type_to_setting_id` (`:325-342`), whose four hardcoded returns -(`QD_1_0_1`/`_11`/`_41`/`_50` = Generic PLA/ABS/PETG/TPU 95A) become ledger keys in -package C. Keep that function as-is but extend its comment: the returned ids are -retired ledger keys by design, and `scripts/tests/test_filament_id.py` parses the -initializer (see §4 tests) — the Moonraker treatment (`MoonrakerPrinterAgent.cpp: -619-627`) of replacing the table with name lookups was considered and not taken: the -table already routes through the same ledger as the composed ids, and two translation -mechanisms in one agent is worse than one. - -Tests (same commit): - -- `tests/libslic3r/test_filament_id_succession.cpp`: add a section asserting a - `QD_`-shaped key forwards like any other (`{"QD_2_1_11", "OFnew001"}` resolves to - `"OFnew001"`) — pins that the walk is prefix-agnostic. -- `scripts/tests/test_filament_id.py`: new test parsing the four `QD_` literals out of - `QidiPrinterAgent.cpp::map_filament_type_to_setting_id` (mirror the parser in - `scripts/test_moonraker_lane_data.py`) and asserting each is a ledger key whose - chain terminates at a live tree id. **Add it marked expected-fail/skipped until - package C lands, then flip it on** — it is the permanent code↔ledger lockstep guard. - -## 3. Tooling and validation flip (work package C, first commit) - -All in `scripts/assign_filament_ids.py`; every touched line measured 2026-08-21: - -- `is_island_declaration` (`:327-329`) → `return vendor == "BBL"`. This single change - pulls every `QD_*` declarer into the triple bookkeeping (`:385`) and thus into - checks 3 and 8, into `--remint`'s domain (`:1464`), and out of the hint-key - tolerances (`:877`, `:1100`). -- `reserved_space_owner` (`:596-604`): `QD_*` returns `(True, None)` — reserved, - ownerless, exactly like the P-hex/user-custom space. Consequences, all wanted: - check 6 refuses any future vendor claim; the `--update-snapshot` sanction gate - (`:938-963`) refuses new `QD_*` ids outright; the vanish path (`:994`) routes - `QD_*` to **retired** (owner `None` ≠ island), not released-with-hint. Update the - two message sites that render `owner is None` as "reserved for user-custom presets" - (`:628` docstring, `:958`, and check 6's copy) to name the space generically or - special-case `QD_*` ("Qidi device protocol; dissolved island — retired, never - declarable"). -- Check 1 (`:667-668`): delete the `vendor == "Qidi" and fid.startswith("QD_")` - exemption. -- Module docstring (`:27-45`): rewrite the `QD_*` bullet — reserved space stays - listed, but as "device protocol namespace, translated via the succession ledger; - dissolved as a catalog island in v4, may never be declared". -- `scripts/tests/test_filament_id.py`: update the three island assertions — - `reserved_space_owner("QD_X4_PLA")` → `(True, None)` (`:403`), - `is_island_declaration("Qidi", "QD_X4_PLA")` → `False` (`:413`), and the - reserved-space message case (`:570`). - -No changes to the ledger schema, the C++ checks, `--retire` (post-flip it accepts -`QD_*` olds automatically — they are non-island now), or the validator: check 3's -"non-BBL, non-`QD_*`" phrasing in v3 §3 was always implemented as "non-island", so the -flip *is* the spec change. - -## 4. Migration phases - -**v4.0 — re-converge the drifted tree (package A).** Per-vendor WF-B worksheets over -the 243 errors: Snapmaker U1 (triple divergence `Generic|Snapmaker|snapmaker`, empty -vendors, four resurrected retired ids — these force re-mints since retirement is -permanent), BBL/addnorth `GF_AN*` overrides (BBL island: grandfather via snapshot, -they are BBL-internal), Qidi/Snapmaker re-declared hint keys (re-mint those declarers, -the check's own prescription), GreenGate3D rename, then `--update-snapshot` (new -upstream `QD_*` ids sanction cleanly — the island is still intact in this phase) and -the full v3 §5 gate battery. Bump `version` in every touched -`resources/profiles/.json`. **Do not start v4.2 until `--check` exits 0.** - -**v4.1 — the agent hook (package B).** §2 as written; `libslic3r_tests` + -`scripts/tests/test_filament_id.py` green; behavior-neutral by construction (no `QD_*` -ledger entries exist yet). - -**v4.2 — the dissolution (package C).** One Qidi worksheet, v3.2 machinery: - -1. Tooling flip commit (§3). `--check` now reports the Qidi island as - non-conformant — expected, red only between commits of this phase. -2. Data fixes, before any re-mint (the v3 "W3 lands first" lesson — type/vendor are - key components): add `filament_vendor: ["QIDI"]` at each QIDI-brand family's - inheritance apex so all declarers resolve it (39 of 49 families currently resolve - none — check 8a would refuse the mint). Verify the six Generic families - (`Generic PLA/ABS/PETG/PC/TPU 95A/PLA Silk`) resolve triples identical to their - OFL counterparts (vendor `Generic`, OFL's `filament_type`) so they *converge onto - the OFL ids* by triple math — the whole point; any mismatch is a W3-style data fix - here, not a fork. No `inherits`/`compatible_printers` re-pointing anywhere: the - series intermediates (`Generic PLA@Q2-Series` etc., all `instantiation: false`) - simply keep declarations whose values become the OFL ids. -3. `python scripts/assign_filament_ids.py --remint Qidi` — rewrites all 264 - declarations in place to their family mints (same triple ⇒ same id across a - family's series intermediates and per-nozzle declarers; convergence with OFL ids - is legal by design, `want_id` already permits same-triple collisions `:1447`). -4. `--update-snapshot` — retires every vanished `QD_*` id with its mode-rule - successor. **Audit the ledger diff: all ~204 new entries must be `QD_*`→non-null.** - A null successor (possible only for a declared-only id with zero instantiated - claims) gets an explicit `--retire "QD_x=OFy"` with the family's minted id. -5. Un-skip the §2 lockstep test. Bump `resources/profiles/Qidi.json` version. Full - gate battery (§5). - -Known residue, accepted: `QIDI PC-ABS-FR` (series 1–2) vs `QIDI PC/ABS-FR` -(series 3–4) are two preset-name families for one product → two ids. Renames were -ruled out in v3 (`renamed_from` rejected for migrations); if upstream ever unifies the -name, content-addressing re-ids and the ledger absorbs it — self-healing, no action -now. - -**v4.3 — OFL consolidation (unchanged).** v3.3 stays optional and id-stable; QIDI-brand -families are vendor-unique and stay in the Qidi bundle. - -## 5. Gates (per phase, delta from v3 §5) - -Unchanged battery: `orca_extra_profile_check.py` exit 0, `assign_filament_ids.py ---check` exit 0, `scripts/tests/test_filament_id.py` all green, `libslic3r_tests` -green, validator `-l 2` tree-wide + `-f` tree-wide + `-v Qidi` exit 0, custom-preset -fixture archives (v4.2 touches no preset visibility, but they are cheap insurance — -run them for v4.0, which touches instantiation-adjacent upstream drift), flatten -config-equivalence. New for v4.2: equivalence diff may contain **only** `filament_id` -value changes (QD→OF) and the added `filament_vendor` keys on QIDI-brand families; -ledger diff audit per §4.4; the map-literal lockstep test. Manual release-checklist -item: Qidi box smoke test — slot holding a QIDI-brand material must surface the brand -preset (not Generic) on an updated client, via the memory-documented local Klipper -test rig. - -## 6. Accepted costs (explicit, new relative to v3) - -- **Every Qidi box slot resolves through the ledger miss-path forever** (one hash-map - walk per slot per status poll — negligible, and structurally identical to how - every retired id already resolves). The QD→OF translation table is maintained by - the retirement machinery, not by hand. -- **Older Orca clients** (pre-v4 profiles) paired with re-minted profile trees lose - QIDI-brand slot matching (they look up `QD_*` and fall back generic-by-type — the - degradation the hook removes for updated clients). Same one-time field-transition - shape v3 §6 already accepted for AMS/AFC ids. -- **Two ids for PC-ABS-FR** until upstream unifies the family name (§4 residue). - -## 7. Evidence - -Measured on this tree 2026-08-21 unless cited to v3: 204 distinct `QD_*` ids / 264 -declaring presets / 49 families, QD→family strictly 1:1, per-series id sets -(`QD_0..4_1_1` = `QIDI PLA Rapido` etc.); 39 families resolve no `filament_vendor`; -declarations live on `instantiation:false` series intermediates (Q2/Q2C/X5/X4) and -per-nozzle X-Plus-4 presets; composition + miss-fallback at -`QidiPrinterAgent.cpp:146-152, 183-192`, hardcoded fallback table `:325-342`; -succession helpers `Preset.hpp:109-119`; ledger = 575 retired + 194 hints, zero `QD_*` -entries; snapshot holds 160 of the 204 (the 44 newcomers are post-merge drift); -`--check` = 243 errors, categorized in §1(A); island exemption mechanics at -`assign_filament_ids.py:327-329, 385, 596-604, 667, 877, 994, 1464, 1514`; `--remint` -same-triple convergence guard `:1447-1453`; retirement-permanence gate `:965-976`.