Make a filament's id depend only on the filament itself

A filament_id is now exactly what the preset's own filament_vendor,
filament_type and filament name mint, wherever it inherits from. Inheriting
settings no longer limits what a preset may claim, so the checks that policed
inheritance are gone, and so are the four grandfather lists that held thousands
of presets as permanent exceptions. The snapshot records sanctioned state rather
than excuses: one entry per id, carrying the product it names beside the presets
claiming it.

Profiles that disagreed are corrected instead of excused. The Elegoo TPU and
PAHT roots were named for a different product than all of their variants and
become TPU 95A and PAHT-CF; Elegoo PET-CF gains the filament_type its variants
already set; the Snapmaker breakaway support presets get an id of their own
rather than riding the PVA chain; and a BBL preset name carrying a doubled space
is fixed behind renamed_from. Their ids re-mint from the corrected identities.
The tooling and the design note also drop the word "family", which invited
reading a brand's Lite and Pro spools as one id.

No change to slicing output — only filament_id values, three preset names, the
inherits lines following those renames and the vendor indexes move.
This commit is contained in:
SoftFever
2026-09-05 23:07:49 +08:00
parent 4aa0e1d60b
commit bf20b041e2
33 changed files with 8465 additions and 13835 deletions

View File

@@ -1,19 +1,21 @@
# 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.
`filament_id` identifies one **filament product**: one named spool product = one id, shared by
all of that product'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.
OrcaFilamentLibrary (OFL), Qidi, or Snapmaker bundle. The granularity is the name on the spool,
not the brand behind it: `AAA PLA Lite` and `AAA PLA Pro` are two filaments with two ids, not
variants of one.
**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
`(filament_vendor, filament_type, filament name)`, where the filament 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_vendor` `Polymaker`, `filament_type` `PLA`, and filament 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).
@@ -31,8 +33,8 @@ 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.
> **Never write a `filament_id` value by hand.** A new filament gets its id from
> `python scripts/assign_filament_ids.py`; one already in the tree has one — inherit it.
## The design, in two pieces
@@ -47,7 +49,7 @@ the system is built to make them impossible:
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
ship which id, and for which filament), or product identity surfaces as a reviewable diff to
one file — the maintainer gate.
## Who consumes the id
@@ -55,7 +57,7 @@ the system is built to make them impossible:
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
scanning only compatible root presets — 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. On a miss it falls back by filament type: a system `Generic <type>` preset
@@ -73,7 +75,7 @@ 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), in Bambu's own `GF*` catalog; `BBLPrinterAgent` rewrites it into our id before the matcher sees it (see [The Bambu catalog map](#the-bambu-catalog-map)) |
| Qidi box | composed at runtime as `QD_<series>_<vendor>_<typeidx>` — vendor and type indices from the device's per-slot saved variables, the series digit inferred client-side from the printer model/name. No preset carries a `QD_*` value, so the slot currently resolves by filament type; mapping the composed id onto the family's minted id belongs in the agent |
| Qidi box | composed at runtime as `QD_<series>_<vendor>_<typeidx>` — vendor and type indices from the device's per-slot saved variables, the series digit inferred client-side from the printer model/name. No preset carries a `QD_*` value, so the slot currently resolves by filament type; mapping the composed id onto the filament's minted id belongs in the agent |
| 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 |
@@ -115,45 +117,47 @@ Two more consumer-side facts worth knowing:
> **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).
second selectable diameter → **new filament, new id**. The same spool tuned for another printer
or nozzle → **join the existing filament** (keep its base name and inherit it; no id
key needed). Tuning a generic material → **join the OrcaFilamentLibrary filament** (inherit
`Generic X @System` and keep the `Generic X` base name; no id key needed).
| Situation | id |
| --- | --- |
| Per-printer / per-nozzle variant of an existing material | same id (inherit, never write the key) |
| Per-printer / per-nozzle variant of an existing material | same id (inherit it) |
| 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 |
| Second diameter of the same product (1.75 + 2.85) | sibling filament, 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 `<Family> @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
1. **Every preset carries the id of its own product, wherever it gets it from.** The id is a
function of the preset's own triple (rule 5), and `inherits` carries settings, never
identity. So a preset may declare the key itself or inherit it from any ancestor — a
`<Filament> @base` root, a real (instantiated) preset of the same filament, an
OrcaFilamentLibrary preset — and CI checks one thing: the id it ends up with equals the
mint of *its* triple. The usual shape is one `@base` root (`"instantiation": "false"`)
declaring the key and the per-printer variants inheriting it; a filament 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`) — which then all declare the identical
id. A branded filament that borrows a generic's settings (`Flashforge ABS Basic @FF C5`
inherits `Generic ABS @System`) declares its own id, because its triple is its own.
2. **The filament 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,
are both the filament `MyBrand PLA`.
3. **Within one filament, 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.
preset on your printers, and it is what makes its triple — and so its id — the library's)
and sets a non-empty `compatible_printers`e.g. `Generic PLA @Sovol SV08 MAX` inherits
`Generic PLA @System` and lists three Sovol nozzles. Renaming such a preset makes it a
different product by rule 5, so it then needs its own id.
5. **Ids follow the product identity.** The id is a pure function of the product triple
`(filament_vendor, filament_type, family name)`, so correcting any of them re-mints the id
`(filament_vendor, filament_type, filament name)`, so correcting any of them re-mints the id
**by design** (via `--remint <Vendor>`; the exact sequence is in the FAQ). Nothing forwards
the old value, so anything outside the tree that stored it — a device tray, a calibration
record, a saved project — falls back to matching by filament type until the user re-selects
@@ -169,48 +173,48 @@ New ids are deterministic, computed exactly like the `setting_id` precedent
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/<filament_vendor>/<filament_type>/<family_name>") )
"filament_product/<filament_vendor>/<filament_type>/<filament_name>") )
```
`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:
AMS length limit. The triple comes from the root preset's *flattened* config:
`<filament_vendor>` is the filament
**manufacturer** (`"Polymaker"`, or `"Generic"` for generics — never the printer brand),
`<filament_type>` the material type, `<family_name>` the root's base name; the two config
`<filament_type>` the material type, `<filament_name>` 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
bundle name, so the same product mints the same id in every bundle — moving a filament 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.
by 35 bundles — most by independent declarations converging on the same mint, the rest
purely through inheritance from the OFL preset.
On the rare collision with an existing 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
table above, resolved without inventing a second filament name. The tooling recognizes salt
iterations of a triple as conformant and preserves such splits across re-mints.
Workflow for a new family:
Workflow for a new filament:
```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)
# 1. Author the filament with NO filament_id key anywhere.
python scripts/assign_filament_ids.py # 2. mint + insert ids into the root preset(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 mints ids for id-less families and replaces any declaration that is not in
The default run mints ids for id-less filaments and replaces any declaration that is not in
`OF` format; it never rewrites a valid `OF` id, so it is idempotent and a no-op once every
family has one. It edits profile files byte-preservingly (indentation, BOM, and line endings
filament has one. 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
`--mint "filament_vendor/filament_type/filament_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).
@@ -228,7 +232,7 @@ normally only used by id migrations):
- `--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
If you skip the tooling, CI fails and prints the remedy: the expected id for your filament 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.
@@ -244,23 +248,21 @@ bundle — not even the one whose printers use the catalog — that may write on
| `GF*` | Bambu AMS/RFID catalog | declarable by **nobody**, BBL included: Bambu's own ids live in the generated catalog map, never in a profile |
| `QD_*` | Qidi device protocol | declarable by **nobody**, Qidi included: the box composes these ids at runtime and they are not preset ids |
| `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 |
The two device namespaces, in detail:
- **Bambu (`GF*`).** Bambu's device/RFID/cloud catalog is external and opaque, which is a
reason to keep it out of the profiles rather than to let one bundle own it. Every BBL family
reason to keep it out of the profiles rather than to let one bundle own it. Every BBL filament
mints an `OF` id from its triple like every other vendor's, and the correspondence to Bambu's
catalog ids lives in one generated file the app applies at the printer boundary — the next
section. Nothing under `resources/profiles/**` carries a `GF*` id today and the snapshot
grandfathers none, so a `GF*` id appearing anywhere in the tree is a mistake, whoever wrote
it.
section. Nothing under `resources/profiles/**` carries a `GF*` id today and nothing can be
exempted, so a `GF*` id appearing anywhere in the tree is a mistake, whoever wrote it.
- **Qidi (`QD_*`).** `QD_*` is a device-*protocol* namespace, not a preset id space: the
Qidi box path composes `QD_<series>_<vendor>_<typeidx>` ids at runtime (slot vendor and
type indices reported by the device, the series digit inferred client-side from the printer
model/name). Qidi presets carry ordinary minted `OF*` ids (generics share the OFL ids), so
a composed id matches no preset and the slot falls back to filament type; translating it to
the family's id belongs in `QidiPrinterAgent`. The alternative — treating per-series
the filament's id belongs in `QidiPrinterAgent`. 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.
@@ -294,7 +296,7 @@ it from **BambuStudio's own shipped BBL bundle** — a sparse shallow clone of u
or `--bambustudio-dir <a BambuStudio resources/profiles checkout>`. Our BBL bundle is a fork of
Bambu's, tuned and extended independently, so it is not the source of truth for Bambu's ids.
A row's key is whatever id our tree already mints for that same
`(filament_vendor, filament_type, family)` triple; a product we do not ship gets a freshly
`(filament_vendor, filament_type, filament name)` triple; a product we do not ship gets a freshly
generated key and the row sits inert until some bundle claims that triple — `OFdyfQvU` /
`GFG03`, "Bambu PETG Matte", is such a row today.
@@ -303,18 +305,18 @@ prints. Two lines, both informational, neither blocking the write:
```text
upstream ships 'Bambu PETG Matte' (Bambu Lab/PETG), we ship nothing with that identity
Orca BBL families with no row: 135 Orca-only product(s)
Orca BBL filaments with no row: 135 Orca-only product(s)
```
The first names each upstream product our BBL bundle has no same-identity family for —
The first names each upstream product our BBL bundle has no same-identity filament for —
sometimes a genuinely missing product, sometimes a name drift a follow-up rename would
converge. The second counts our own BBL families that matched no row: 135 of 234 today, of
converge. The second counts our own BBL filaments that matched no row: 135 of 234 today, of
which 109 send an `OF` id on the wire and 26 already rode `OF` ids inherited from the
OrcaFilamentLibrary. **135 is the number to expect at every regeneration** — 109 was the
one-off size of the transition and stopped being computable from the tree once the BBL bundle
was re-minted, so do not "fix" the report to print it.
**Check 8** lives in `check_filament_ids`, so profile CI runs it alongside the other seven. It
**Check 6** lives in `check_filament_ids`, so profile CI runs it alongside the other five. It
holds the file to its contract: it parses, carries `source` / `bambustudio_commit` /
`generated`, keys only `OF`-format ids, maps each Bambu id at most once, and — for every row
whose key the tree actually claims — agrees with the tree on that id's `(vendor, type, name)`
@@ -408,49 +410,41 @@ Profile CI (`check_profiles.yml`) runs `check_filament_ids()` tree-wide via
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.
deterministically (running it twice changes nothing). The snapshot holds one map, `ids`: each
entry is the product the id is minted from (`filament_vendor`, `filament_type`, `name`) and the
`filaments` claiming it (`Vendor/Filament`), and it sanctions *state*, never exceptions: no check
consults it to excuse a preset from a rule, and there is no grandfather list of any kind.
The checks, in brief:
- **Format** — every id occurring in the tree is `OF` + 6 base62 chars. No exceptions: not a
grandfathered snapshot entry, not BBL.
- **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.
- **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 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.
snapshot entry, not BBL.
- **Snapshot equality** — tree claims == snapshot claims **and** each id's declared triple ==
its snapshot entry, both directions: any `filament_vendor`/`filament_type`/name change
surfaces as a snapshot diff.
- **Identity** — the id is a function of the triple alone. A declared `OF*` id must equal the
mint (or a low salt iteration) of its declarer's own triple; the id an instantiated preset
*inherits* must equal the mint of *its* own triple, however it inherits it (a root, a real
filament, a library preset — structure rule 1); and every instantiated system filament must
resolve an effective id at all (recall: an id-less one is a hard load error in C++ that
discards the whole vendor bundle). The errors print the expected id.
- **Reserved namespaces** — `GF*`, `QD_*`, `P<7-hex>` or `"null"` claimed by any vendor,
BBL and Qidi included, unless that exact claim is grandfathered in the snapshot (none is
today).
- **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).
BBL and Qidi included.
- **Triple integrity** — every declarer must resolve a non-empty `filament_vendor` and
`filament_type` (generics use `"Generic"`), and all declarers of one filament within a
bundle must agree on the triple.
- **Bambu catalog map** — `resources/printers/bambu_filament_ids.json` parses, carries its
`source` / `bambustudio_commit` / `generated` header, keys only `OF`-format ids, maps each
Bambu id at most once, and agrees with the tree on the triple of every row whose key the
tree claims. See [The Bambu catalog map](#the-bambu-catalog-map); the remedy is always to
regenerate, never to hand-edit.
Any new claim on a **reserved namespace** — a profile that declares a `GF*`, `QD_*` or
`P<7-hex>` id, whatever its vendor — is refused by `--update-snapshot` unless you pass
`--allow-shared-catalog`, and even then it lands in the snapshot diff for maintainer review.
There is no such claim in the tree today and adding one should be a last resort: for a
Bambu-cataloged product, the catalog map is where the correspondence belongs. 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.
A profile that declares a **reserved-namespace** id — `GF*`, `QD_*` or `P<7-hex>`, whatever
its vendor — cannot pass the format check, so `--update-snapshot` refuses to sanction it
rather than hide the mistake until CI. For a Bambu-cataloged product, the catalog map is where
the correspondence belongs. Any other new sharing via a *declared* id is caught by the identity
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.
`orca_extra_profile_check.py` separately holds every declared id to the AMS 8-character limit,
tree-wide and for every vendor alike, scoped to the presets a vendor's index actually
@@ -463,24 +457,27 @@ 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
- **A new color of an existing product?** Never a new id — colors are not filaments.
- **A second diameter (1.75 mm and 2.85 mm) of the same product?** A sibling filament 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 high-speed tune of an existing material for another printer model?** Same filament:
keep the base name and inherit its root; no id key needed.
- **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
`Generic PLA` base name, set `compatible_printers`; no id key needed.
- **A branded filament that borrows a generic's settings?** Fine — inherit `Generic X @System`
(or any real filament) for the settings and declare the id of your own filament; run
`python scripts/assign_filament_ids.py` to mint it. Inheritance never changes the id.
- **I need to fix a filament's `filament_vendor` or `filament_type`.** Fix the config, run
`--remint <Vendor>` then `--update-snapshot`, and commit the profile and snapshot diffs
together. The id re-derives from the corrected identity, and nothing forwards the old
value, so a tray or record still holding it falls back to matching by filament type.
- **I need to rename a family.** Rename the presets (adding `renamed_from`, which keeps the
- **I need to rename a filament.** Rename the presets (adding `renamed_from`, which keeps the
preset *name* resolving), then `--remint <Vendor>`, then `--update-snapshot`. The id follows
the new family name; as with any identity fix, the old id is not forwarded.
the new filament name; as with any identity fix, the old id is not forwarded.
- **Can I reuse a `QD_*` id for a Qidi profile?** No — nobody can. It is the device protocol's
own id space: the box composes those values at runtime and no preset carries one. Author
Qidi filaments like any other vendor's.
- **CI says my family needs an id.** Run `python scripts/assign_filament_ids.py`, then
- **CI says my filament 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

View File

@@ -9510,7 +9510,7 @@
"sub_path": "filament/COEX/COEX NYLEX PA6-CF @BBL X1C.json"
},
{
"name": "COEX NYLEX UNFILLED @BBL X1C",
"name": "COEX NYLEX UNFILLED @BBL X1C",
"sub_path": "filament/COEX/COEX NYLEX UNFILLED @BBL X1C.json"
},
{

View File

@@ -1,6 +1,7 @@
{
"type": "filament",
"name": "COEX NYLEX UNFILLED @BBL X1C",
"name": "COEX NYLEX UNFILLED @BBL X1C",
"renamed_from": "COEX NYLEX UNFILLED @BBL X1C",
"inherits": "COEX NYLEX UNFILLED @base",
"from": "system",
"setting_id": "CXPAUB21_01",

View File

@@ -1241,8 +1241,8 @@
"sub_path": "filament/BASE/Generic ASA @base.json"
},
{
"name": "Elegoo TPU @base",
"sub_path": "filament/BASE/Elegoo TPU @base.json"
"name": "Elegoo TPU 95A @base",
"sub_path": "filament/BASE/Elegoo TPU 95A @base.json"
},
{
"name": "Elegoo PETG @base",
@@ -1261,8 +1261,8 @@
"sub_path": "filament/BASE/Elegoo ABS @base.json"
},
{
"name": "Elegoo PAHT @base",
"sub_path": "filament/BASE/Elegoo PAHT @base.json"
"name": "Elegoo PAHT-CF @base",
"sub_path": "filament/BASE/Elegoo PAHT-CF @base.json"
},
{
"name": "Elegoo PC @base",

View File

@@ -1,9 +1,9 @@
{
"type": "filament",
"name": "Elegoo PAHT @base",
"name": "Elegoo PAHT-CF @base",
"inherits": "fdm_filament_pa",
"from": "system",
"filament_id": "OF0h7ERL",
"filament_id": "OF20kXr5",
"instantiation": "false",
"filament_density": [
"1.25"

View File

@@ -3,6 +3,9 @@
"name": "Elegoo PET-CF @base",
"inherits": "Elegoo PETG @base",
"from": "system",
"filament_id": "OFV5tNw6",
"instantiation": "false"
"filament_id": "OFe98A1y",
"instantiation": "false",
"filament_type": [
"PET-CF"
]
}

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"name": "Elegoo Rapid TPU 95A @base",
"inherits": "Elegoo TPU @base",
"inherits": "Elegoo TPU 95A @base",
"from": "system",
"filament_id": "OF30Ftju",
"instantiation": "false"

View File

@@ -1,9 +1,9 @@
{
"type": "filament",
"name": "Elegoo TPU @base",
"name": "Elegoo TPU 95A @base",
"inherits": "fdm_filament_tpu",
"from": "system",
"filament_id": "OFmhJs5V",
"filament_id": "OFqHQNoZ",
"instantiation": "false",
"filament_max_volumetric_speed": [
"3.6"

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"name": "Elegoo PAHT-CF @EC",
"inherits": "Elegoo PAHT @base",
"inherits": "Elegoo PAHT-CF @base",
"from": "system",
"setting_id": "ZRd4WPQrh8nC8n7G",
"instantiation": "true",

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"name": "Elegoo TPU 95A @EC",
"inherits": "Elegoo TPU @base",
"inherits": "Elegoo TPU 95A @base",
"from": "system",
"setting_id": "hqmWADQDYWBuniTB",
"instantiation": "true",

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"name": "Elegoo PAHT-CF @EC2",
"inherits": "Elegoo PAHT @base",
"inherits": "Elegoo PAHT-CF @base",
"from": "system",
"setting_id": "miv95d8rXD4CAgjC",
"instantiation": "true",

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"name": "Elegoo TPU 95A @EC2",
"inherits": "Elegoo TPU @base",
"inherits": "Elegoo TPU 95A @base",
"from": "system",
"setting_id": "6LUQGjzmx0y5A6Qn",
"instantiation": "true",

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"name": "Elegoo PAHT-CF @ECC",
"inherits": "Elegoo PAHT @base",
"inherits": "Elegoo PAHT-CF @base",
"from": "system",
"setting_id": "iDRlRpUzPNVYv0lY",
"instantiation": "true",

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"name": "Elegoo TPU 95A @ECC",
"inherits": "Elegoo TPU @base",
"inherits": "Elegoo TPU 95A @base",
"from": "system",
"setting_id": "cOI05FCm8wrkqtDD",
"instantiation": "true",

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"name": "Elegoo PAHT-CF @ECC2",
"inherits": "Elegoo PAHT @base",
"inherits": "Elegoo PAHT-CF @base",
"from": "system",
"setting_id": "hmTwVvyCAIZI7rSY",
"instantiation": "true",

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"name": "Elegoo TPU 95A @ECC2",
"inherits": "Elegoo TPU @base",
"inherits": "Elegoo TPU 95A @base",
"from": "system",
"setting_id": "v0Qj3xMBkUGnxZPi",
"instantiation": "true",

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"name": "Elegoo TPU 95A @EN3 Series",
"inherits": "Elegoo TPU @base",
"inherits": "Elegoo TPU 95A @base",
"from": "system",
"setting_id": "4W6ZheJxNPUPxe1s",
"instantiation": "true",

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"name": "Elegoo TPU 95A @EN4 Series",
"inherits": "Elegoo TPU @base",
"inherits": "Elegoo TPU 95A @base",
"from": "system",
"setting_id": "6pp381J3ahRYYtFE",
"instantiation": "true",

View File

@@ -525,8 +525,8 @@
"sub_path": "filament/DREMC/DREMC PA6-CF @base.json"
},
{
"name": "Elegoo PAHT @base",
"sub_path": "filament/Elegoo/Elegoo PAHT @base.json"
"name": "Elegoo PAHT-CF @base",
"sub_path": "filament/Elegoo/Elegoo PAHT-CF @base.json"
},
{
"name": "FILL3D PA @base",
@@ -1169,8 +1169,8 @@
"sub_path": "filament/DREMC/DREMC TPU 95A @base.json"
},
{
"name": "Elegoo TPU @base",
"sub_path": "filament/Elegoo/Elegoo TPU @base.json"
"name": "Elegoo TPU 95A @base",
"sub_path": "filament/Elegoo/Elegoo TPU 95A @base.json"
},
{
"name": "Eolas Prints TPU D60 UV Resistant @System",

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"name": "Elegoo PAHT-CF @System",
"inherits": "Elegoo PAHT @base",
"inherits": "Elegoo PAHT-CF @base",
"from": "system",
"setting_id": "S073oXCeOlTEqh6L",
"instantiation": "true",

View File

@@ -1,9 +1,9 @@
{
"type": "filament",
"name": "Elegoo PAHT @base",
"name": "Elegoo PAHT-CF @base",
"inherits": "fdm_filament_pa",
"from": "system",
"filament_id": "OF0h7ERL",
"filament_id": "OF20kXr5",
"instantiation": "false",
"activate_air_filtration": [
"1"

View File

@@ -3,6 +3,9 @@
"name": "Elegoo PET-CF @base",
"inherits": "Elegoo PETG @base",
"from": "system",
"filament_id": "OFV5tNw6",
"instantiation": "false"
"filament_id": "OFe98A1y",
"instantiation": "false",
"filament_type": [
"PET-CF"
]
}

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"name": "Elegoo Rapid TPU 95A @base",
"inherits": "Elegoo TPU @base",
"inherits": "Elegoo TPU 95A @base",
"from": "system",
"filament_id": "OF30Ftju",
"instantiation": "false"

View File

@@ -1,7 +1,7 @@
{
"type": "filament",
"name": "Elegoo TPU 95A @System",
"inherits": "Elegoo TPU @base",
"inherits": "Elegoo TPU 95A @base",
"from": "system",
"setting_id": "SLcn59fIZPDl2brJ",
"instantiation": "true",

View File

@@ -1,9 +1,9 @@
{
"type": "filament",
"name": "Elegoo TPU @base",
"name": "Elegoo TPU 95A @base",
"inherits": "fdm_filament_tpu",
"from": "system",
"filament_id": "OFmhJs5V",
"filament_id": "OFqHQNoZ",
"instantiation": "false",
"filament_density": [
"1.21"

View File

@@ -4,6 +4,7 @@
"inherits": "Snapmaker Breakaway Support @base",
"from": "system",
"setting_id": "mJoaud6wulT4p8SA",
"filament_id": "OFnmp2pO",
"instantiation": "true",
"compatible_printers": [
"Snapmaker U1 (0.2 nozzle)"

View File

@@ -4,6 +4,7 @@
"inherits": "Snapmaker Breakaway Support @base",
"from": "system",
"setting_id": "rKUUREJNhstIU0I2",
"filament_id": "OFnmp2pO",
"instantiation": "true",
"compatible_printers": [
"Snapmaker U1 (0.6 nozzle)"

View File

@@ -4,6 +4,7 @@
"inherits": "Snapmaker Breakaway Support @base",
"from": "system",
"setting_id": "hQ8jQ5GKQKGKuPew",
"filament_id": "OFnmp2pO",
"instantiation": "true",
"compatible_printers": [
"Snapmaker U1 (0.8 nozzle)"

View File

@@ -4,6 +4,7 @@
"inherits": "Snapmaker Breakaway Support @base",
"from": "system",
"setting_id": "5DqT3CYUgp1oJD1V",
"filament_id": "OFnmp2pO",
"instantiation": "true",
"compatible_printers": [
"Snapmaker U1 (0.4 nozzle)"

View File

@@ -4,24 +4,25 @@ Mint deterministic filament_id values for OrcaSlicer system filament products an
validate the tree against the sanctioned-state snapshot.
Policy (companion to assign_vendor_setting_ids.py; see docs/HLSD/filament_id.md):
* filament_id is a PRODUCT id: one commercial product line = one id, shared by
all of that material's per-printer/per-nozzle variants in every bundle.
OrcaFilamentLibrary (OFL) is the product catalog: a family's id is declared
once, on its family root (instantiation != "true"); vendor bundles carry
only specializations of OFL families — same base name, non-empty
compatible_printers, no filament_id key — which resolve the OFL id through
the loader's inherits/base-bundle walk. Products OFL does not carry mint
their id in the vendor bundle with the same rule; the key is
bundle-independent, so hoisting a family into OFL never changes its id.
* New ids are content-addressed by the product triple, resolved from the
declaring preset's flattened config (filament_vendor and filament_type are
inheritable list options — first element; family name = preset base name):
* filament_id is a PRODUCT id: one named spool product = one id, shared by all
of that product's per-printer/per-nozzle variants in every bundle. The
granularity is the name on the spool, not the brand: "AAA PLA Lite" and
"AAA PLA Pro" are two products with two ids, not variants of one. The
id is a pure function of the product triple (below), so WHERE a preset gets
it from is irrelevant: it may declare the key itself or inherit it from any
ancestor — a root preset, a real (instantiated) filament, an
OrcaFilamentLibrary (OFL) preset — as long as the id it ends up with is the
mint of its OWN triple. Inheritance carries settings, never identity; the
key is bundle-independent, so moving a filament into OFL never changes it.
* Ids are content-addressed by the product triple, resolved from the preset's
flattened config (filament_vendor and filament_type are inheritable list
options — first element; filament name = preset base name):
filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE,
"filament_product/<filament_vendor>/<filament_type>/<family_name>") )
"filament_product/<filament_vendor>/<filament_type>/<filament_name>") )
8 chars total, which satisfies the AMS length limit. Nobody invents ids by
hand; on the astronomically rare collision with an existing id the input
is salted ("/1", "/2", ...) until free and the result is frozen in file.
Identity changes (a family rename, a filament_vendor/filament_type fix)
Identity changes (a filament rename, a filament_vendor/filament_type fix)
change the id BY DESIGN.
* Reserved id spaces that are never minted into or altered:
- GF* Bambu AMS/RFID catalog: frozen, no preset of any
@@ -33,11 +34,12 @@ Policy (companion to assign_vendor_setting_ids.py; see docs/HLSD/filament_id.md)
preset may declare one
- P + 7 hex chars (case-insensitive) and the literal "null"
user-custom presets (CreatePresetsDialog.cpp)
- every already-shipped id, grandfathered via scripts/filament_id_snapshot.json
* scripts/filament_id_snapshot.json is the sanctioned-state snapshot (ids,
claims and declared triples): it must exactly equal the tree-derived state
at all times, so any id/claim/triple change shows up as a reviewable diff to
that file (the maintainer gate).
* scripts/filament_id_snapshot.json is the sanctioned-state snapshot: one
entry per id, carrying the product triple it is minted from and the
"Vendor/Filament" presets claiming it. It must exactly equal the tree-derived
state at all times, so any id/claim/triple change shows up as a reviewable
diff to that file (the maintainer gate). It sanctions state, never
exceptions: no check consults it to excuse a preset from the rules above.
The effective-id resolution below is loader-faithful (PresetBundle.cpp
load_vendor_configs_from_json): own filament_id key, else walk `inherits` within
@@ -46,10 +48,10 @@ OFL it stays in OFL; a vendor chain that dead-ends id-less retries its direct
parent in the OFL map. filament_vendor / filament_type resolve the same way.
Run from anywhere: python3 scripts/assign_filament_ids.py
(default) mint + insert ids for id-less families, mint + replace
(default) mint + insert ids for id-less filaments, mint + replace
non-OF-format declarations; idempotent, never rewrites a
valid OF-format id; a no-op once every family has one
--mint "Vendor/Type/Family"
valid OF-format id; a no-op once every filament has one
--mint "Vendor/Type/Filament"
print the id that triple would mint; touches nothing
--update-snapshot regenerate the snapshot from the tree
--remint VENDOR re-derive VENDOR's declared ids from their triples and
@@ -90,10 +92,10 @@ OF_ID_RE = re.compile(r"^OF[0-9A-Za-z]{6}$")
# User-custom id space minted by CreatePresetsDialog.cpp ("P" + md5(name)[0:7]);
# reserved case-insensitively, together with its "null" sentinel.
USER_CUSTOM_ID_RE = re.compile(r"^P[0-9A-Fa-f]{7}$", re.IGNORECASE)
# Family name = preset base name: strip the first "@..." suffix. The space before
# Filament name = preset base name: strip the first "@..." suffix. The space before
# "@" is optional because names like "Afinia PLA@HS" exist.
BASE_NAME_RE = re.compile(r"\s?@.*$")
# Salt iterations accepted by the mint-conformance check (check 3).
# Salt iterations accepted by the identity check (check 3).
MAX_CHECK_SALT = 8
UPDATE_HINT = 'run "python scripts/assign_filament_ids.py --update-snapshot" and commit the diff for maintainer review'
@@ -130,19 +132,19 @@ def _utf8_console():
# ---------------------------------------------------------------------------
def base_name(name):
"""Family name of a preset: name with the first "@..." suffix stripped."""
"""Filament name of a preset: name with the first "@..." suffix stripped."""
return BASE_NAME_RE.sub("", name, count=1)
def generate_filament_id(filament_vendor, filament_type, family, salt=0):
def generate_filament_id(filament_vendor, filament_type, filament_name, salt=0):
"""Deterministic "OF" + 6-char base62 filament_id for a filament product.
input = "filament_product/<filament_vendor>/<filament_type>/<family_name>"
input = "filament_product/<filament_vendor>/<filament_type>/<filament_name>"
(+ "/<salt>" when salted); u = uuid5(FILAMENT_ID_NAMESPACE, input); the id
tail is the low FILAMENT_ID_LENGTH base62 digits of int(u.bytes, "big"),
most-significant first — the same derivation as generate_preset_setting_id.
"""
key = f"filament_product/{filament_vendor}/{filament_type}/{family}"
key = f"filament_product/{filament_vendor}/{filament_type}/{filament_name}"
if salt:
key = f"{key}/{salt}"
u = uuid.uuid5(FILAMENT_ID_NAMESPACE, key)
@@ -154,14 +156,19 @@ def generate_filament_id(filament_vendor, filament_type, family, salt=0):
return "OF" + "".join(reversed(digits))
def mint_filament_id(filament_vendor, filament_type, family, taken):
def mint_filament_id(filament_vendor, filament_type, filament_name, taken):
"""Mint the product's id, salting past any id in `taken`."""
for salt in range(10000):
candidate = generate_filament_id(filament_vendor, filament_type, family, salt)
candidate = generate_filament_id(filament_vendor, filament_type, filament_name, salt)
if candidate not in taken:
return candidate
raise RuntimeError(
f"could not mint a free filament_id for {filament_vendor}/{filament_type}/{family}")
f"could not mint a free filament_id for {filament_vendor}/{filament_type}/{filament_name}")
def mint_iterations(triple):
"""The ids that count as the mint of `triple`: salt 0..MAX_CHECK_SALT."""
return {generate_filament_id(*triple, salt=s) for s in range(MAX_CHECK_SALT + 1)}
# ---------------------------------------------------------------------------
@@ -234,7 +241,8 @@ def resolve_filament_id(name, filaments, ofl_filaments, seen=None, in_ofl=False,
vendor is re-tried against the OFL map keyed by its direct parent name.
skip_own ignores the first preset's own filament_id key (used to compute the
id its inherits chain would resolve WITHOUT the declaration — check 6b drift).
id its inherits chain would resolve WITHOUT the declaration — the
--drop-redundant-ids redundancy test).
Returns (filament_id or None, source, ofl_entry) where source is one of
"own"/"inherited"/"missing"/"dangling"/"cycle" and ofl_entry is the name of
@@ -307,7 +315,7 @@ def resolve_filament_field(name, field, filaments, ofl_filaments, seen=None, in_
def resolve_triple(name, filaments, ofl_filaments):
"""The preset's mint-key triple (filament_vendor, filament_type, family)."""
"""The preset's mint-key triple (filament_vendor, filament_type, filament name)."""
return (resolve_filament_field(name, "filament_vendor", filaments, ofl_filaments),
resolve_filament_field(name, "filament_type", filaments, ofl_filaments),
base_name(name))
@@ -317,7 +325,7 @@ def analyze_tree(profiles_dir):
"""Load every vendor bundle and derive the full filament_id state.
Returns a dict with the tree-derived snapshot sections plus the working data
the checks and the assign pass need. All claims are "Vendor/Family" strings
the checks and the assign pass need. All claims are "Vendor/Filament" strings
over INSTANTIATED system filaments, tree-wide including OFL and BBL.
"""
profiles_dir = str(profiles_dir)
@@ -325,7 +333,6 @@ def analyze_tree(profiles_dir):
ofl_filaments, ofl_errors = (
load_vendor_filaments(profiles_dir, OFL) if OFL in vendor_names else ({}, [])
)
ofl_declared = {r["filament_id"] for r in ofl_filaments.values() if r.get("filament_id")}
vendors = {}
read_errors = list(ofl_errors)
@@ -336,51 +343,39 @@ def analyze_tree(profiles_dir):
filaments, errs = load_vendor_filaments(profiles_dir, vendor)
read_errors.extend(errs)
for rec in filaments.values():
eff, src, ofl_entry = resolve_filament_id(rec["name"], filaments, ofl_filaments)
eff, src, _entry = resolve_filament_id(rec["name"], filaments, ofl_filaments)
rec["eff_filament_id"] = eff
rec["id_source"] = src
rec["ofl_entry"] = ofl_entry
vendors[vendor] = filaments
# id -> set of "Vendor/Family" claims over instantiated presets. Every id
# id -> set of "Vendor/Filament" claims over instantiated presets. Every id
# occurring in the tree is a key; ids only ever DECLARED (e.g. on a root
# whose children all override them) keep an empty claim list, so that the
# snapshot exactly equals the tree-derived state and the format/retirement
# checks can grandfather them.
# none of whose descendants instantiate) keep an empty claim list, so that
# the snapshot exactly equals the tree-derived state.
ids = {}
vendor_ids = {} # vendor -> set of ids occurring there (declared or effective)
declared_ids = {} # vendor -> set of ids DECLARED in that vendor's own files
instantiated_with_id = [] # "Vendor/PresetName" (own filament_id key on an instantiated preset)
overrides = [] # (vendor, name, declared, inherited, file)
missing_effective = [] # (vendor, name, file) instantiated presets resolving no id
alias_candidates = [] # (vendor, rec, ofl_entry, own_key) presets riding an OFL family
inherited = [] # (vendor, rec, eff, triple) instantiated presets inheriting an OF id
triples = {} # fid -> set of triples of its declarers
declarer_triples = [] # (vendor, rec, fid, triple) per declarer
family_triples = {} # (vendor, family) -> {triple: [declarer names]}
filament_triples = {} # (vendor, filament_name) -> {triple: [declarers]}
for vendor, filaments in vendors.items():
occurring = vendor_ids.setdefault(vendor, set())
for rec in filaments.values():
triple = resolve_triple(rec["name"], filaments, ofl_filaments)
rec["triple"] = triple
if rec.get("filament_id"):
fid = rec["filament_id"]
occurring.add(fid)
declared_ids.setdefault(vendor, set()).add(fid)
ids.setdefault(fid, set())
triple = resolve_triple(rec["name"], filaments, ofl_filaments)
rec["triple"] = triple
declarer_triples.append((vendor, rec, fid, triple))
triples.setdefault(fid, set()).add(triple)
family_triples.setdefault(
filament_triples.setdefault(
(vendor, base_name(rec["name"])), {}).setdefault(
triple, []).append(rec["name"])
if rec.get("inherits"):
inherited, _src, skip_entry = resolve_filament_id(
rec["name"], filaments, ofl_filaments, skip_own=True)
if inherited and inherited != fid:
overrides.append((vendor, rec["name"], fid, inherited, rec["file"]))
if (vendor != OFL and skip_entry and inherited
and inherited in ofl_declared):
alias_candidates.append((vendor, rec, skip_entry, True))
if not rec["instantiation"]:
continue
eff = rec.get("eff_filament_id")
@@ -389,47 +384,28 @@ def analyze_tree(profiles_dir):
continue
occurring.add(eff)
ids.setdefault(eff, set()).add(f"{vendor}/{base_name(rec['name'])}")
if rec.get("filament_id"):
instantiated_with_id.append(f"{vendor}/{rec['name']}")
if vendor != OFL and rec["ofl_entry"] and eff in ofl_declared:
alias_candidates.append((vendor, rec, rec["ofl_entry"], False))
if not rec.get("filament_id") and OF_ID_RE.match(eff):
inherited.append((vendor, rec, eff, triple))
# Alias hygiene (check 4): a vendor preset that rides an OFL family (its id
# resolves through OFL) is matched to the OFL preset by ALIAS (base name);
# renaming it re-exposes the OFL preset and creates a live duplicate, empty
# compatible_printers cannot claim any printer, and declaring its own
# filament_id key forks the family off the catalog id.
alias_violations = [] # (vendor, name, ofl_entry, reason, file)
for vendor, rec, entry, own_key in alias_candidates:
expected = base_name(entry)
own_base = base_name(rec["name"])
if own_key:
alias_violations.append((
vendor, rec["name"], entry,
f'declares its own filament_id "{rec["filament_id"]}" — dropping the key '
f"lets it resolve the OFL family's id through inherits",
rec["file"]))
elif own_base != expected:
alias_violations.append((
vendor, rec["name"], entry,
f'base name "{own_base}" != "{expected}" — the rename re-exposes the '
f"OFL preset on its printers (alias shadowing is name-based)",
rec["file"]))
elif not rec["compatible_printers"]:
alias_violations.append((
vendor, rec["name"], entry,
"empty compatible_printers cannot shadow the OFL preset anywhere",
rec["file"]))
# Identity (check 3b): an inherited id must be the mint of the preset's
# OWN triple. A declared id is held to the same rule as a declarer (3a),
# and an id whose declarer already fails 3a is reported there once, not
# again under every preset inheriting it.
unminted = {fid for _v, _r, fid, triple in declarer_triples
if OF_ID_RE.match(fid) and fid not in mint_iterations(triple)}
id_mismatches = [
(vendor, rec, eff, triple) for vendor, rec, eff, triple in inherited
if eff not in unminted and eff not in mint_iterations(triple)]
# Cross-bundle triple divergence (check 7, warning only): the same family
# Cross-bundle triple divergence (check 5, warning only): the same filament
# name declared in several bundles with different triples cannot converge
# on one id until the divergence is fixed.
name_bundles = {}
for (vendor, family), tmap in family_triples.items():
name_bundles.setdefault(family, {})[vendor] = frozenset(tmap)
for (vendor, filament_name), tmap in filament_triples.items():
name_bundles.setdefault(filament_name, {})[vendor] = frozenset(tmap)
cross_bundle_triples = [
(family, {v: sorted(ts) for v, ts in per_vendor.items()})
for family, per_vendor in sorted(name_bundles.items())
(filament_name, {v: sorted(ts) for v, ts in per_vendor.items()})
for filament_name, per_vendor in sorted(name_bundles.items())
if len(per_vendor) > 1 and len(set(per_vendor.values())) > 1
]
@@ -439,18 +415,12 @@ def analyze_tree(profiles_dir):
"ids": {fid: sorted(claims) for fid, claims in ids.items()},
"vendor_ids": vendor_ids,
"declared_ids": declared_ids,
"instantiated_with_id": sorted(instantiated_with_id),
"overrides": overrides,
"id_overrides": sorted(f"{v}/{n}" for v, n, _d, _i, _f in overrides),
"missing_effective": sorted(missing_effective),
"alias_violations": alias_violations,
"alias_exceptions": sorted(f"{v}/{n}" for v, n, _e, _r, _f in alias_violations),
"id_mismatches": id_mismatches,
"triples": {fid: sorted(list(t) for t in ts) for fid, ts in triples.items()},
"triple_sets": triples,
"declarer_triples": declarer_triples,
"family_triples": family_triples,
"triple_exceptions": sorted(
f"{v}/{f}" for (v, f), tmap in family_triples.items() if len(tmap) > 1),
"filament_triples": filament_triples,
"cross_bundle_triples": cross_bundle_triples,
}
@@ -460,14 +430,19 @@ def analyze_tree(profiles_dir):
# ---------------------------------------------------------------------------
def snapshot_from_analysis(analysis):
return {
"ids": {fid: sorted(claims) for fid, claims in analysis["ids"].items()},
"instantiated_with_id": analysis["instantiated_with_id"],
"id_overrides": analysis["id_overrides"],
"alias_exceptions": analysis["alias_exceptions"],
"triples": analysis["triples"],
"triple_exceptions": analysis["triple_exceptions"],
}
"""One entry per id, in id order: the product triple it is minted from and
the "Vendor/Filament" claims on it. Requires exactly one declared triple per
id (update_snapshot refuses any other state; check 3 rejects it anyway)."""
ids = {}
for fid, claims in sorted(analysis["ids"].items()):
[(vendor, ftype, filament_name)] = analysis["triples"][fid]
ids[fid] = {"filaments": sorted(claims), "name": filament_name,
"filament_type": ftype, "filament_vendor": vendor}
return {"ids": ids}
def snapshot_triple(entry):
return [entry["filament_vendor"], entry["filament_type"], entry["name"]]
def load_snapshot(path):
@@ -475,18 +450,15 @@ def load_snapshot(path):
if not os.path.exists(path):
return None
data = load_json(path)
for key in ("ids", "triples"):
data.setdefault(key, {})
for key in ("instantiated_with_id", "id_overrides", "alias_exceptions",
"triple_exceptions"):
data.setdefault(key, [])
data.setdefault("ids", {})
return data
def write_snapshot(path, obj):
"""Deterministic serialization: sorted keys, indent 1, LF, trailing newline."""
"""Deterministic serialization: snapshot_from_analysis order, indent 1, LF,
trailing newline."""
with open(path, "w", encoding="utf-8", newline="\n") as f:
json.dump(obj, f, indent=1, ensure_ascii=False, sort_keys=True)
json.dump(obj, f, indent=1, ensure_ascii=False)
f.write("\n")
@@ -526,29 +498,27 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
1. Format: every id occurring in the tree (declared or effective) must
match ^OF[0-9A-Za-z]{6}$. No exceptions: not the snapshot, not BBL.
2. Snapshot equality, both directions: the tree-derived id->families multimap
AND the id->triples map of the declarers must equal the snapshot exactly
(the snapshot diff is the maintainer gate).
3. Mint conformance: an OF-format declaration must equal the mint of the
declarer's triple or a salted iteration, unless that exact (id, triple)
pair is grandfathered in the snapshot.
4. Alias hygiene: a vendor preset riding an OFL family must keep the OFL
base name, claim printers via non-empty compatible_printers, and declare
no filament_id key of its own.
5. Reserved namespaces (GF*/QD_*/P-hex/"null", all ownerless) must not be
claimed by any vendor, except claims grandfathered in the snapshot.
6. Structure ratchet: (a) no NEW instantiated preset carries its own
filament_id key; (b) no NEW declared-vs-inherited id drift; (c) every
instantiated filament resolves an effective id (a hard load error in C++).
7. Triple integrity: (a) every declarer resolves non-empty filament_vendor
and filament_type (hard error, no grandfathering); (b) declarers of one
(bundle, family) resolve identical triples, unless grandfathered in
snapshot triple_exceptions; cross-bundle divergence on the same family
name is a warning only.
8. Bambu catalog map: resources/printers/bambu_filament_ids.json must parse,
2. Snapshot equality, both directions: every id in the tree, the filaments
claiming it and the triple its declarers resolve must equal the snapshot
entry exactly (the snapshot diff is the maintainer gate).
3. Identity: the id is a function of the triple alone. (a) A declared id
must equal the mint of the declarer's own triple or a salted iteration;
(b) the id an instantiated preset inherits must equal the mint of ITS
own triple — how it inherits it (a root, a real filament, an OFL
preset) is irrelevant; (c) every instantiated filament resolves an
effective id at all (an id-less one is a hard load error in C++).
4. Reserved namespaces (GF*/QD_*/P-hex/"null", all ownerless) must not be
claimed by any vendor.
5. Triple integrity: (a) every declarer resolves non-empty filament_vendor
and filament_type; (b) declarers of one (bundle, filament) resolve
identical triples; cross-bundle divergence on the same filament name is a
warning only.
6. Bambu catalog map: resources/printers/bambu_filament_ids.json must parse,
carry source/bambustudio_commit/generated, key only OF-format ids, map
each Bambu id at most once, and for every row whose key the tree claims,
the tree's triple for that id must equal the row's (vendor, type, name).
Nothing is grandfathered: the snapshot sanctions state, never exceptions.
"""
_utf8_console()
errors = 0
@@ -571,24 +541,36 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
continue
print_error(
f'filament_id "{fid}" ({vendor}) is not a minted "OF" id; new '
f'family ids must come from "python scripts/assign_filament_ids.py" '
f'filament ids must come from "python scripts/assign_filament_ids.py" '
f'(see --mint)')
errors += 1
# -- 2. snapshot equality (both directions) -----------------------------
tree_triples = analysis["triples"]
for fid in sorted(tree_ids):
if fid not in snap_ids:
entry = snap_ids.get(fid)
if entry is None:
print_error(
f'filament_id "{fid}" is not sanctioned by '
f"scripts/filament_id_snapshot.json; {UPDATE_HINT}")
errors += 1
continue
for claim in tree_ids[fid]:
if claim not in snap_ids[fid]:
if claim not in entry["filaments"]:
print_error(
f'filament_id "{fid}" claim "{claim}" is not sanctioned by '
f"scripts/filament_id_snapshot.json; {UPDATE_HINT}")
errors += 1
# Every tree id has at least one declarer; the snapshot records one
# triple per id, so a divergent declarer is a mismatch in both directions.
sanctioned = snapshot_triple(entry)
for t in tree_triples[fid]:
if t != sanctioned:
print_error(
f'filament_id "{fid}" triple "{"/".join(t)}" is not sanctioned by '
f'scripts/filament_id_snapshot.json, which records '
f'"{"/".join(sanctioned)}"; {UPDATE_HINT}')
errors += 1
for fid in sorted(snap_ids):
if fid not in tree_ids:
print_error(
@@ -596,91 +578,31 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
f"{UPDATE_HINT}")
errors += 1
continue
for claim in snap_ids[fid]:
for claim in snap_ids[fid]["filaments"]:
if claim not in tree_ids[fid]:
print_error(
f'filament_id stability: snapshot claim "{claim}" of id "{fid}" '
f"vanished from the tree; {UPDATE_HINT}")
errors += 1
# ... and the declared triples, both directions.
snap_triples = snapshot["triples"]
tree_triples = analysis["triples"]
for fid in sorted(tree_triples):
for t in tree_triples[fid]:
if t not in snap_triples.get(fid, []):
print_error(
f'filament_id "{fid}" triple "{"/".join(t)}" is not sanctioned by '
f"scripts/filament_id_snapshot.json; {UPDATE_HINT}")
errors += 1
for fid in sorted(snap_triples):
for t in snap_triples[fid]:
if t not in tree_triples.get(fid, []):
print_error(
f'filament_id triple stability: snapshot triple "{"/".join(t)}" of '
f'id "{fid}" vanished from the tree; {UPDATE_HINT}')
errors += 1
# -- 3. mint conformance for OF-format declarations ----------------------
# -- 3. identity: the id is a function of the triple alone ---------------
for vendor, rec, fid, triple in sorted(
analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])):
if not OF_ID_RE.match(fid):
continue
if list(triple) in snap_triples.get(fid, []):
continue # grandfathered (id, triple) pair
minted = [generate_filament_id(*triple, salt=s) for s in range(MAX_CHECK_SALT + 1)]
if fid not in minted:
print_error(
f'filament_id "{fid}" declared by "{rec["name"]}" ({rec["file"]}) does '
f'not match the mint of its triple "{"/".join(triple)}": expected '
f'"{minted[0]}" (or a salted iteration); paste the expected id into the '
f"family root (or, for an intentionally kept id, {UPDATE_HINT})")
errors += 1
# -- 4. alias hygiene for presets riding OFL families --------------------
exceptions = set(snapshot["alias_exceptions"])
for vendor, name, entry, reason, file in analysis["alias_violations"]:
if f"{vendor}/{name}" in exceptions:
if not OF_ID_RE.match(fid) or fid in mint_iterations(triple):
continue
print_error(
f'preset "{name}" ({file}) rides the OFL family "{entry}" but {reason}; '
f"a vendor specialization keeps the OFL base name, sets non-empty "
f"compatible_printers and declares no filament_id key — or the family "
f"gets its own minted id")
f'filament_id "{fid}" declared by "{rec["name"]}" ({rec["file"]}) does '
f'not match the mint of its triple "{"/".join(triple)}": expected '
f'"{generate_filament_id(*triple)}" (or a salted iteration); paste the '
f"expected id, or fix the triple and --remint the vendor")
errors += 1
# -- 5. reserved namespaces ----------------------------------------------
for fid in sorted(tree_ids):
is_reserved, owner = reserved_space_owner(fid)
if not is_reserved:
continue
for claim in tree_ids[fid]:
vendor = claim.split("/", 1)[0]
if vendor == owner:
continue
if claim in snap_ids.get(fid, []):
continue # grandfathered
space = reserved_space_desc(fid, owner)
print_error(
f'filament_id "{fid}" of "{claim}" is in a reserved id space '
f"({space}) and must not be claimed by system presets of other vendors")
errors += 1
# -- 6. structure ratchet -------------------------------------------------
grandfathered = set(snapshot["instantiated_with_id"])
for key in analysis["instantiated_with_id"]:
if key not in grandfathered:
print_error(
f'instantiated preset "{key}" declares its own filament_id key; the key '
f"belongs on the family root preset only (variants inherit it)")
errors += 1
grandfathered = set(snapshot["id_overrides"])
for vendor, name, declared, inherited, file in analysis["overrides"]:
if f"{vendor}/{name}" in grandfathered:
continue
for vendor, rec, eff, triple in sorted(
analysis["id_mismatches"], key=lambda x: (x[0], x[1]["file"])):
print_error(
f'preset "{name}" ({file}) declares filament_id "{declared}" but its '
f'inherits chain resolves "{inherited}"; a preset must not override its '
f"family's id")
f'preset "{rec["name"]}" ({rec["file"]}) inherits filament_id "{eff}" but '
f'its own triple "{"/".join(triple)}" mints "{generate_filament_id(*triple)}"; '
f"a preset carries the id of its own product: inherit a preset of the "
f"same filament, or declare its own key")
errors += 1
ofl_map = analysis["vendors"].get(OFL, {})
for vendor, name, file in analysis["missing_effective"]:
@@ -689,11 +611,26 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
print_error(
f'instantiated filament "{name}" ({file}) resolves no filament_id anywhere '
f"in its inherits chain — this is a hard load error in the C++ loader; "
f'run "python scripts/assign_filament_ids.py" (expected id for family '
f'run "python scripts/assign_filament_ids.py" (expected id for filament '
f'"{vendor}/{base_name(name)}": "{expected}", salted if taken)')
errors += 1
# -- 7. triple integrity ---------------------------------------------------
# -- 4. reserved namespaces ----------------------------------------------
for fid in sorted(tree_ids):
is_reserved, owner = reserved_space_owner(fid)
if not is_reserved:
continue
for claim in tree_ids[fid]:
vendor = claim.split("/", 1)[0]
if vendor == owner:
continue
space = reserved_space_desc(fid, owner)
print_error(
f'filament_id "{fid}" of "{claim}" is in a reserved id space '
f"({space}) and must not be claimed by system presets of other vendors")
errors += 1
# -- 5. triple integrity ---------------------------------------------------
for vendor, rec, fid, triple in sorted(
analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])):
if triple[0] and triple[1]:
@@ -706,27 +643,27 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
f"resolves empty {missing}; the mint key needs both (generic materials "
f'use filament_vendor "Generic")')
errors += 1
triple_exceptions = set(snapshot["triple_exceptions"])
for (vendor, family), tmap in sorted(analysis["family_triples"].items()):
if len(tmap) < 2 or f"{vendor}/{family}" in triple_exceptions:
for (vendor, filament_name), tmap in sorted(analysis["filament_triples"].items()):
if len(tmap) < 2:
continue
detail = "; ".join(
f'"{"/".join(t)}" ({", ".join(sorted(names))})'
for t, names in sorted(tmap.items()))
print_error(
f'family "{vendor}/{family}" declarers resolve divergent triples: {detail}; '
f"declarers of one family must agree on (filament_vendor, filament_type)")
f'filament "{vendor}/{filament_name}" declarers resolve divergent triples: '
f"{detail}; declarers of one filament must agree on "
f"(filament_vendor, filament_type)")
errors += 1
for family, per_vendor in analysis["cross_bundle_triples"]:
for filament_name, per_vendor in analysis["cross_bundle_triples"]:
detail = "; ".join(
f'{v}: {", ".join("/".join(t) for t in ts)}'
for v, ts in sorted(per_vendor.items()))
print_warning(
f'family name "{family}" resolves different triples across bundles '
f'filament name "{filament_name}" resolves different triples across bundles '
f"({detail}); bundles of one product converge on one id only once "
f"their triples agree")
# -- 8. Bambu catalog map --------------------------------------------------
# -- 6. Bambu catalog map --------------------------------------------------
try:
bambu_map = load_json(map_path)
if not isinstance(bambu_map, dict):
@@ -785,61 +722,52 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
# --update-snapshot
# ---------------------------------------------------------------------------
def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
allow_shared_catalog=False):
def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH):
"""Regenerate the snapshot from the tree.
Refuses to sanction NEW reserved-namespace ids (or new claims on them) for
non-owner vendors unless --allow-shared-catalog is passed. Idempotent: a
second run over an unchanged tree changes nothing. Returns 0 on success.
Refuses to sanction a reserved-namespace id (or a claim on one) and an id
declared under more than one triple: neither can ever pass --check, so
writing it into the snapshot would only hide the mistake until CI.
Idempotent: a second run over an unchanged tree changes nothing. Returns 0
on success.
"""
analysis = analyze_tree(profiles_dir)
for msg in analysis["read_errors"]:
print_error(msg)
refusals = 0
for vendor in sorted(analysis["vendor_ids"]):
for fid in sorted(analysis["vendor_ids"][vendor]):
is_reserved, owner = reserved_space_owner(fid)
if is_reserved and vendor != owner:
print_error(
f'refusing to sanction filament_id "{fid}" ({vendor}): reserved id '
f"space, {reserved_space_desc(fid, owner)}")
refusals += 1
for fid, ts in sorted(analysis["triples"].items()):
if len(ts) > 1:
print_error(
f'refusing to sanction filament_id "{fid}": declared under {len(ts)} '
f'triples ({"; ".join("/".join(t) for t in ts)}); one id names one '
f"product (check 3)")
refusals += 1
if refusals:
return 1
new_snap = snapshot_from_analysis(analysis)
old_snap = load_snapshot(snapshot_path)
old_ids = old_snap["ids"] if old_snap else {}
# Gate: new reserved-namespace ids / claims for non-owner vendors.
refusals = []
for fid, claims in sorted(new_snap["ids"].items()):
is_reserved, owner = reserved_space_owner(fid)
if not is_reserved:
continue
for claim in claims:
if claim in old_ids.get(fid, []):
continue
vendor = claim.split("/", 1)[0]
if vendor == owner:
continue
refusals.append((fid, claim, owner))
if not claims and fid not in old_ids:
# Declared-only new id: attribute it to its declaring vendor(s).
for vendor in sorted(analysis["vendor_ids"]):
if fid in analysis["vendor_ids"][vendor] and vendor != owner:
refusals.append((fid, f"{vendor}/(declared only)", owner))
if refusals and not allow_shared_catalog:
for fid, claim, owner in refusals:
space = reserved_space_desc(fid, owner)
print_error(
f'refusing to sanction new claim "{claim}" on reserved-namespace id '
f'"{fid}" ({space}); pass --allow-shared-catalog only for '
f"maintainer-approved shared-catalog families")
return 1
# Diff summary.
added_ids = sorted(set(new_snap["ids"]) - set(old_ids))
removed_ids = sorted(set(old_ids) - set(new_snap["ids"]))
added_claims = sum(
len(set(claims) - set(old_ids.get(fid, [])))
for fid, claims in new_snap["ids"].items())
len(set(entry["filaments"]) - set(old_ids.get(fid, {}).get("filaments", [])))
for fid, entry in new_snap["ids"].items())
removed_claims = sum(
len(set(claims) - set(new_snap["ids"].get(fid, [])))
for fid, claims in old_ids.items())
old_snap = old_snap or {"ids": {}, "instantiated_with_id": [], "id_overrides": [],
"alias_exceptions": [], "triples": {},
"triple_exceptions": []}
changed = new_snap != old_snap
len(set(entry["filaments"]) - set(new_snap["ids"].get(fid, {}).get("filaments", [])))
for fid, entry in old_ids.items())
changed = new_snap != (old_snap or {"ids": {}})
if changed:
write_snapshot(snapshot_path, new_snap)
@@ -847,11 +775,6 @@ def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
print_info(f"snapshot ids : {len(new_snap['ids'])} (+{len(added_ids)} / -{len(removed_ids)})")
print_info(f"claims added : {added_claims}")
print_info(f"claims removed : {removed_claims}")
print_info(f"declared triples : {len(new_snap['triples'])}")
for section in ("instantiated_with_id", "id_overrides", "alias_exceptions",
"triple_exceptions"):
before, after = len(old_snap.get(section, [])), len(new_snap[section])
print_info(f"{section:<18}: {after} ({after - before:+d})")
if changed:
print_success(f"snapshot written to {snapshot_path}")
else:
@@ -952,25 +875,26 @@ def remove_filament_id(path, old_id):
# ---------------------------------------------------------------------------
# Default run: mint + insert ids for id-less families
# Default run: mint + insert ids for id-less filaments
# ---------------------------------------------------------------------------
def assign_missing_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH):
"""Mint + insert ids for id-less families; mint + replace non-OF-format
"""Mint + insert ids for id-less filaments; mint + replace non-OF-format
declarations. Never rewrites a valid (OF-format) existing id.
A family = (vendor, base name) group over instantiated filaments with no
effective id. The minted id is a pure function of the family's triple —
(filament_vendor, filament_type) resolved on the root(s), family name — and
is inserted into the family's root(s): the presets its members inherit that
carry no id, or the member itself when it has no vendor-side parent.
A filament = (vendor, base name) group over instantiated presets with no
effective id. The minted id is a pure function of the filament's triple —
(filament_vendor, filament_type) resolved on the root(s), filament name —
and is inserted into the filament's root(s): the id-less presets of the
SAME filament its members inherit, or the member itself otherwise (a parent
of another filament cannot carry this filament's id — check 3).
A declaration whose value is not OF-format (e.g. a vendor bundle synced
from an upstream source that ships its own catalog ids, such as BBL's GF*)
is treated the same as a missing family: a fresh id is minted for the
is treated the same as a missing filament: a fresh id is minted for the
declarer's triple and the value is replaced in place (declarers that share
one triple across several per-printer roots converge on the same id, same
as the multi-root families above). This is what makes a future BBL sync
as the multi-root filaments above). This is what makes a future BBL sync
self-healing: upstream files arrive with GF ids, this pass replaces them,
and the generated Bambu catalog map (keyed by the ids this mints) already
knows the resulting rows.
@@ -983,8 +907,8 @@ def assign_missing_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH):
print_error(msg)
errors += 1
# Group id-less instantiated presets into families.
families = {} # (vendor, family) -> [rec]
# Group id-less instantiated presets by filament.
filaments = {} # (vendor, filament_name) -> [rec]
for vendor, name, _file in analysis["missing_effective"]:
rec = analysis["vendors"][vendor][name]
if rec["id_source"] in ("cycle", "dangling"):
@@ -992,17 +916,17 @@ def assign_missing_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH):
f'({rec["id_source"]})')
errors += 1
continue
families.setdefault((vendor, base_name(name)), []).append(rec)
filaments.setdefault((vendor, base_name(name)), []).append(rec)
# Declarations whose value is not OF-format: treated as missing too (see
# docstring). Grouped by triple, not by (vendor, family), so declarers
# docstring). Grouped by triple, not by (vendor, filament), so declarers
# that legitimately share one triple across several files converge on one
# freshly minted id instead of each getting their own.
non_of_declarers = [
(vendor, rec, fid, triple) for vendor, rec, fid, triple in analysis["declarer_triples"]
if not OF_ID_RE.match(fid)]
if not families and not non_of_declarers:
if not filaments and not non_of_declarers:
print_success("every instantiated filament already resolves an OF-format "
"filament_id; nothing to do (0 files changed)")
return 0, errors
@@ -1013,46 +937,34 @@ def assign_missing_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH):
for occurring in analysis["vendor_ids"].values():
taken |= occurring
# Family root(s): the direct vendor-side parents of the members (id-less by
# construction), or the member itself when it has none.
roots = {} # (vendor, family) -> {preset name: rec}
root_claims = {} # (vendor, root name) -> set of families wanting to write it
for key, members in sorted(families.items()):
vendor = key[0]
# Root preset(s): the direct vendor-side parents of the members (id-less by
# construction) that belong to the same filament, or the member itself.
roots = {} # (vendor, filament_name) -> {preset name: rec}
for key, members in sorted(filaments.items()):
vendor, filament_name = key
vendor_map = analysis["vendors"][vendor]
family_roots = {}
filament_roots = {}
for rec in members:
parent = rec.get("inherits")
root = vendor_map.get(parent) if parent else None
if root is None or root.get("filament_id"):
if (root is None or root.get("filament_id")
or base_name(root["name"]) != filament_name):
root = rec # root-less member carries the id itself
family_roots[root["name"]] = root
root_claims.setdefault((vendor, root["name"]), set()).add(key)
roots[key] = family_roots
filament_roots[root["name"]] = root
roots[key] = filament_roots
ofl_map = analysis["vendors"].get(OFL, {})
files_changed = 0
families_minted = 0
for key, family_roots in sorted(roots.items()):
vendor, family = key
shared = [n for n in family_roots
if len(root_claims[(vendor, n)]) > 1]
if shared:
others = sorted({f"{v}/{f}" for n in shared
for (v, f) in root_claims[(vendor, n)] if (v, f) != key})
print_error(
f'cannot mint for family "{vendor}/{family}": root(s) '
f"{sorted(shared)} are shared with famil(ies) {others}; split the "
f"roots so each family has its own")
errors += 1
continue
filaments_minted = 0
for key, filament_roots in sorted(roots.items()):
vendor, filament_name = key
vendor_map = analysis["vendors"][vendor]
fields = {(resolve_filament_field(n, "filament_vendor", vendor_map, ofl_map),
resolve_filament_field(n, "filament_type", vendor_map, ofl_map))
for n in family_roots}
for n in filament_roots}
if len(fields) > 1:
print_error(
f'cannot mint for family "{vendor}/{family}": its roots resolve '
f'cannot mint for filament "{vendor}/{filament_name}": its roots resolve '
f"divergent (filament_vendor, filament_type) pairs {sorted(fields)}; "
f"align the fields first")
errors += 1
@@ -1063,26 +975,27 @@ def assign_missing_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH):
k for k, v in (("filament_vendor", fvendor),
("filament_type", ftype)) if not v)
print_error(
f'cannot mint for family "{vendor}/{family}": it resolves empty '
f'cannot mint for filament "{vendor}/{filament_name}": it resolves empty '
f'{missing}; the mint key needs both (generic materials use '
f'filament_vendor "Generic")')
errors += 1
continue
new_id = mint_filament_id(fvendor, ftype, family, taken)
new_id = mint_filament_id(fvendor, ftype, filament_name, taken)
taken.add(new_id)
families_minted += 1
for name in sorted(family_roots):
root = family_roots[name]
filaments_minted += 1
for name in sorted(filament_roots):
root = filament_roots[name]
write_filament_id(root["path"], new_id)
files_changed += 1
print_info(f'family "{vendor}/{family}": filament_id "{new_id}" -> {root["file"]}')
print_info(f'filament "{vendor}/{filament_name}": filament_id "{new_id}" '
f'-> {root["file"]}')
# Non-OF-format declarations: mint once per triple, rewrite every declarer
# that shares it (see docstring).
declarations_reminted = 0
assigned = {} # triple -> id chosen this run
for vendor, rec, fid, triple in sorted(non_of_declarers, key=lambda x: (x[0], x[1]["file"])):
fvendor, ftype, family = triple
fvendor, ftype, filament_name = triple
if not fvendor or not ftype:
missing = " and ".join(
k for k, v in (("filament_vendor", fvendor),
@@ -1094,16 +1007,16 @@ def assign_missing_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH):
errors += 1
continue
if triple not in assigned:
assigned[triple] = mint_filament_id(fvendor, ftype, family, taken)
assigned[triple] = mint_filament_id(fvendor, ftype, filament_name, taken)
taken.add(assigned[triple])
new_id = assigned[triple]
rewrite_filament_id(rec["path"], fid, new_id)
files_changed += 1
declarations_reminted += 1
print_info(f'family "{vendor}/{family}": non-OF filament_id "{fid}" -> "{new_id}" '
f'({rec["file"]})')
print_info(f'filament "{vendor}/{filament_name}": non-OF filament_id "{fid}" '
f'-> "{new_id}" ({rec["file"]})')
print_info(f"families minted : {families_minted}")
print_info(f"filaments minted : {filaments_minted}")
print_info(f"non-OF reminted : {declarations_reminted}")
print_info(f"files changed : {files_changed}")
if files_changed:
@@ -1178,8 +1091,7 @@ def remint_vendors(vendor_list, profiles_dir=PROFILES_DIR):
# mint-conformant (check 3) — leave it. This keeps deliberate salt
# splits (two presets of one product that must stay distinct for
# per-printer AMS matching, validator -f) stable across re-mints.
if fid in {generate_filament_id(*rec["triple"], salt=s)
for s in range(MAX_CHECK_SALT + 1)}:
if fid in mint_iterations(rec["triple"]):
continue
want = want_id(rec["triple"])
if fid == want:
@@ -1197,8 +1109,8 @@ def remint_vendors(vendor_list, profiles_dir=PROFILES_DIR):
def drop_redundant_ids(vendor, profiles_dir=PROFILES_DIR):
"""Delete filament_id declarations in `vendor` that merely re-declare an OFL
family's id path: ignoring its own key, the preset's inherits chain enters
OFL and resolves an OFL-declared id, and the preset keeps the OFL family's
filament's id path: ignoring its own key, the preset's inherits chain enters
OFL and resolves an OFL-declared id, and the preset keeps the OFL filament's
base name. Such a preset is a specialization and rides the OFL id (v3.2(b)).
Never touches the snapshot. Returns (dropped, errors).
"""
@@ -1246,25 +1158,22 @@ def drop_redundant_ids(vendor, profiles_dir=PROFILES_DIR):
def main(argv=None):
_utf8_console()
parser = argparse.ArgumentParser(
description="Mint deterministic filament_id values for id-less filament "
"families and validate the tree against the sanctioned snapshot.")
parser.add_argument("--mint", metavar='"Vendor/Type/Family"',
description="Mint deterministic filament_id values for id-less filaments "
"and validate the tree against the sanctioned snapshot.")
parser.add_argument("--mint", metavar='"Vendor/Type/Filament"',
help="print the id the (filament_vendor, filament_type, "
"family name) triple would mint; touches nothing")
"filament name) triple would mint; touches nothing")
parser.add_argument("--update-snapshot", action="store_true",
help="regenerate scripts/filament_id_snapshot.json from "
"the tree")
parser.add_argument("--check", action="store_true",
help="run the filament_id checks; exit nonzero on errors")
parser.add_argument("--allow-shared-catalog", action="store_true",
help="with --update-snapshot: allow sanctioning new claims "
"on reserved-namespace ids for non-owner vendors")
parser.add_argument("--remint", metavar="VENDOR", action="append",
help="re-derive VENDOR's declared filament_ids from their "
"triples and rewrite mismatches in place; repeatable")
parser.add_argument("--drop-redundant-ids", metavar="VENDOR",
help="delete filament_id declarations in VENDOR that "
"re-declare the OFL family id they already resolve "
"re-declare the OFL filament id they already resolve "
"through inherits")
parser.add_argument("--profiles", default=PROFILES_DIR,
help="profiles directory (default: resources/profiles)")
@@ -1274,8 +1183,8 @@ def main(argv=None):
if args.mint:
parts = args.mint.split("/", 2)
if len(parts) != 3 or not all(parts):
parser.error('--mint expects "filament_vendor/filament_type/family_name" '
"with all three components non-empty (check 7 rejects empty "
parser.error('--mint expects "filament_vendor/filament_type/filament_name" '
"with all three components non-empty (check 5 rejects empty "
"vendor/type in the tree)")
snapshot = load_snapshot(SNAPSHOT_PATH) or {"ids": {}}
taken = set(snapshot["ids"])
@@ -1295,8 +1204,7 @@ def main(argv=None):
return 1 if errors else 0
if args.update_snapshot:
return update_snapshot(args.profiles, SNAPSHOT_PATH,
allow_shared_catalog=args.allow_shared_catalog)
return update_snapshot(args.profiles, SNAPSHOT_PATH)
if args.check:
errors = check_filament_ids(args.profiles, SNAPSHOT_PATH)

File diff suppressed because it is too large Load Diff

View File

@@ -111,11 +111,10 @@ class SyntheticTree:
# -- pipeline wrappers ---------------------------------------------------
def update_snapshot(self, allow_shared_catalog=False):
def update_snapshot(self):
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
rc = afi.update_snapshot(self.profiles, self.snapshot,
allow_shared_catalog=allow_shared_catalog)
rc = afi.update_snapshot(self.profiles, self.snapshot)
return rc, buf.getvalue()
def check(self, map_path=None):
@@ -144,19 +143,18 @@ class SyntheticTree:
return dropped, errors, buf.getvalue()
def make_clean_tree(apla_id="AX01", generic_id="OGFL99"):
"""Baseline tree: OFL base+generic, a vendor family, a clean tuned generic.
"""Baseline tree: OFL base+generic, a vendor filament, a clean tuned generic.
apla_id/generic_id default to arbitrary non-OF placeholders (grandfathered
into the snapshot below) since most tests only need "already assigned,
don't touch". TestAssign passes real OF-format ids instead: assign_missing_ids
now treats a non-OF declaration as missing and remints it, so a non-OF
baseline would no longer be a no-op there.
apla_id/generic_id default to arbitrary non-OF placeholders (sanctioned by
the snapshot below) since most tests only need "already assigned, don't
touch" and never run the checks. TestAssign and the check tests pass real
OF-format ids instead (OfCleanTreeCase).
"""
t = SyntheticTree()
t.add_vendor(OFL, [
preset("fdm_pla", filament_id=generic_id, instantiation=False,
preset("Generic PLA @base", filament_id=generic_id, instantiation=False,
filament_vendor="Generic", filament_type="PLA"),
preset("Generic PLA @System", inherits="fdm_pla",
preset("Generic PLA @System", inherits="Generic PLA @base",
compatible_printers=[]),
])
t.add_vendor("VendorA", [
@@ -180,7 +178,7 @@ class SyntheticTreeCase(unittest.TestCase):
class OfCleanTreeCase(unittest.TestCase):
"""Like SyntheticTreeCase, but the baseline family/generic already carry
"""Like SyntheticTreeCase, but the baseline filament/generic already carry
real OF-format ids (check 1 now rejects "AX01"/"OGFL99" unconditionally,
with no snapshot exemption), so an otherwise-untouched tree still passes
check_filament_ids. Tests that specifically need a non-OF baseline to
@@ -190,7 +188,7 @@ class OfCleanTreeCase(unittest.TestCase):
def setUp(self):
self.t = make_clean_tree(
apla_id=afi.generate_filament_id("AVendor", "PLA", "APLA"),
generic_id=afi.generate_filament_id("Generic", "PLA", "fdm_pla"))
generic_id=afi.generate_filament_id("Generic", "PLA", "Generic PLA"))
self.addCleanup(self.t.cleanup)
@@ -206,7 +204,7 @@ class TestMint(unittest.TestCase):
def test_known_vector(self):
# Hardcoded, independently computed vectors: freeze prefix, input string
# layout ("filament_product/<vendor>/<type>/<family>") and base62 tail.
# layout ("filament_product/<vendor>/<type>/<name>") and base62 tail.
self.assertEqual(afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA"),
"OF5CgdDq")
self.assertEqual(afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA",
@@ -247,7 +245,7 @@ class TestMint(unittest.TestCase):
class TestBaseName(unittest.TestCase):
def test_family_derivation(self):
def test_filament_name_derivation(self):
cases = [
("X @base", "X"),
("Afinia PLA@HS", "Afinia PLA"),
@@ -256,8 +254,8 @@ class TestBaseName(unittest.TestCase):
("A @B @C", "A"), # first @ wins
("Filár PLA 拓竹 @0.4 nozzle", "Filár PLA 拓竹"),
]
for name, family in cases:
self.assertEqual(afi.base_name(name), family, msg=name)
for name, filament_name in cases:
self.assertEqual(afi.base_name(name), filament_name, msg=name)
# ---------------------------------------------------------------------------
@@ -465,7 +463,17 @@ class TestChecks(OfCleanTreeCase):
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn('triple "AVendor/PETG/APLA" is not sanctioned', out)
self.assertIn('triple stability: snapshot triple "AVendor/PLA/APLA"', out)
self.assertIn('which records "AVendor/PLA/APLA"', out)
# Sanctioning the new triple is not enough: the old id is no longer its
# mint (check 3, nothing grandfathered) — the identity fix is a re-mint,
# reported once on the root, not again under the variant inheriting it.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 1, out)
self.assertIn("does not match the mint of its triple", out)
_changed, errors, out = self.t.remint(["VendorA"])
self.assertEqual(errors, 0, out)
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
@@ -492,8 +500,8 @@ class TestChecks(OfCleanTreeCase):
_errors, out = self.t.check() # check 2 still wants a snapshot update
self.assertNotIn("does not match the mint", out)
def test_check3_grandfathered_id_triple_pair(self):
# A non-conforming (id, triple) pair sanctioned by the snapshot passes.
def test_check3_no_grandfathering_of_a_wrong_declaration(self):
# Sanctioning the tree does not excuse a declaration from its mint.
self.t.write_preset("VendorA", preset("CNEW @base", filament_id="OFZZZZZZ",
instantiation=False,
filament_vendor="CV", filament_type="PLA"))
@@ -502,66 +510,72 @@ class TestChecks(OfCleanTreeCase):
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
self.assertEqual(errors, 1, out)
self.assertIn("does not match the mint of its triple", out)
def test_check4_renamed_tuned_generic(self):
def test_check3_inherited_id_must_be_the_mint_of_own_triple(self):
# A preset of another filament inheriting APLA's root takes APLA's id,
# which is not the mint of ITS triple (AVendor/PLA/Tuned PLA).
self.t.write_preset("VendorA", preset("Tuned PLA @P1", inherits="APLA @base",
compatible_printers=["P1"]))
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn('"Tuned PLA @P1" (VendorA/filament/Tuned PLA @P1.json) inherits '
'filament_id "%s"' % afi.generate_filament_id("AVendor", "PLA", "APLA"),
out)
self.assertIn('mints "%s"' % afi.generate_filament_id("AVendor", "PLA", "Tuned PLA"),
out)
# ... and sanctioning the tree does not excuse it either.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 1, out)
def test_check3_renamed_tuned_generic_is_an_identity_error(self):
# Riding the OFL generic under another base name: same rule, same error.
self.t.write_preset("VendorA", preset("Tuned PLA @P1",
inherits="Generic PLA @System",
compatible_printers=["P1"]))
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn("rename re-exposes the OFL preset", out)
self.assertIn("Tuned PLA @P1", out)
self.assertIn("inherits filament_id", out)
def test_check4_empty_compatible_printers(self):
self.t.write_preset("VendorA", preset("Generic PLA @P2",
inherits="Generic PLA @System",
compatible_printers=[]))
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn("cannot shadow the OFL preset", out)
def test_check4_own_id_key_on_ofl_rider(self):
# A vendor root that declares its own id while inheriting an OFL family
# forks the family off the catalog id.
self.t.write_preset("VendorA", preset("GPLA @base", filament_id="AX77",
instantiation=False,
inherits="Generic PLA @System"))
self.t.write_preset("VendorA", preset("GPLA @P1", inherits="GPLA @base",
compatible_printers=["P1"]))
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn('declares its own filament_id "AX77"', out)
def test_check4_applies_to_non_generic_ofl_families(self):
fid = afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA")
self.t.write_preset(OFL, preset("poly_pla_base", filament_id=fid,
instantiation=False,
filament_vendor="Polymaker",
filament_type="PLA"))
self.t.write_preset(OFL, preset("PolyLite PLA @System",
inherits="poly_pla_base",
compatible_printers=[]))
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
self.t.write_preset("VendorA", preset("PolyX PLA @P1",
inherits="PolyLite PLA @System",
compatible_printers=["P1"]))
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn("rename re-exposes the OFL preset", out)
self.assertIn("PolyX PLA @P1", out)
def test_check4_exception_is_grandfathered(self):
self.t.write_preset("VendorA", preset("Tuned PLA @P1",
def test_check3_own_key_on_an_instantiated_preset_is_fine(self):
# Where the id comes from is irrelevant: a variant may carry the key.
apla_id = afi.generate_filament_id("AVendor", "PLA", "APLA")
self.t.write_preset("VendorA", preset("APLA @P1", filament_id=apla_id,
inherits="APLA @base",
compatible_printers=["P1 0.4 nozzle"]),
register=False)
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
def test_check3_inheriting_a_real_filament_of_another_product_is_fine(self):
# A branded product may inherit the OFL generic (an instantiated
# preset) for its settings; it declares its own triple's id.
fid = afi.generate_filament_id("BV", "PLA", "Branded PLA")
self.t.write_preset("VendorA", preset("Branded PLA @P1", filament_id=fid,
inherits="Generic PLA @System",
filament_vendor="BV",
compatible_printers=["P1"]))
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
# With a wrong key it is a plain mint mismatch: the parent plays no
# part in the verdict.
drifted = afi.generate_filament_id("AVendor", "PLA", "APLA")
self.t.write_preset("VendorA", preset("Branded PLA @P1", filament_id=drifted,
inherits="Generic PLA @System",
filament_vendor="BV",
compatible_printers=["P1"]),
register=False)
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn("does not match the mint of its triple", out)
def test_check5_reserved_namespace_claims(self):
def test_check4_reserved_namespace_claims(self):
for fid, marker in [("GFX99", "Bambu AMS/RFID catalog"),
("QD_X_PLA", "composed by the device"),
("P1a2b3c4", "user-custom"),
@@ -579,31 +593,7 @@ class TestChecks(OfCleanTreeCase):
self.assertIn("reserved id space", out)
self.assertIn(marker, out)
def test_check6a_instantiated_preset_with_own_key(self):
# Same value the child would already resolve through inherits: this
# isolates check 6a (own key present) from check 6b (value drift).
apla_id = afi.generate_filament_id("AVendor", "PLA", "APLA")
self.t.write_preset("VendorA", preset("APLA @P1", filament_id=apla_id,
inherits="APLA @base",
compatible_printers=["P1 0.4 nozzle"]),
register=False)
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn("declares its own filament_id key", out)
def test_check6b_declared_vs_inherited_drift(self):
apla_id = afi.generate_filament_id("AVendor", "PLA", "APLA")
drifted = afi.generate_filament_id("AVendor", "PLA", "APLA", salt=1)
self.t.write_preset("VendorA", preset("APLA @P1", filament_id=drifted,
inherits="APLA @base",
compatible_printers=["P1 0.4 nozzle"]),
register=False)
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn(f'declares filament_id "{drifted}" but its inherits chain resolves '
f'"{apla_id}"', out)
def test_check6c_unresolvable_instantiated_filament(self):
def test_check3c_unresolvable_instantiated_filament(self):
self.t.write_preset("VendorA", preset("DNEW @P1", compatible_printers=["P1"]))
errors, out = self.t.check()
self.assertGreater(errors, 0)
@@ -617,8 +607,8 @@ class TestChecks(OfCleanTreeCase):
self.assertIn("snapshot not found", out)
class TestCheck7(OfCleanTreeCase):
def test_7a_empty_vendor_is_hard_error(self):
class TestCheck5(OfCleanTreeCase):
def test_5a_empty_vendor_is_hard_error(self):
fid = afi.generate_filament_id("", "PLA", "NVPLA")
self.t.write_preset("VendorA", preset("NVPLA @base", filament_id=fid,
instantiation=False,
@@ -629,14 +619,14 @@ class TestCheck7(OfCleanTreeCase):
self.assertGreater(errors, 0)
self.assertIn("resolves empty filament_vendor", out)
self.assertIn('filament_vendor "Generic"', out)
# No grandfathering: sanctioning the tree does not silence check 8a.
# No grandfathering: sanctioning the tree does not silence check 5a.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 1, out)
self.assertIn("resolves empty filament_vendor", out)
def test_7b_divergent_family_triples(self):
def test_5b_divergent_filament_triples(self):
id1 = afi.generate_filament_id("MV", "PLA", "MPLA")
id2 = afi.generate_filament_id("MV", "PETG", "MPLA")
self.t.write_preset("VendorA", preset("MPLA @base1", filament_id=id1,
@@ -652,14 +642,14 @@ class TestCheck7(OfCleanTreeCase):
self.assertIn("divergent triples", out)
self.assertIn("MV/PLA/MPLA", out)
self.assertIn("MV/PETG/MPLA", out)
# ... until --update-snapshot grandfathers the family in triple_exceptions.
# No grandfathering: sanctioning the tree does not silence check 5b.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
self.assertIn("VendorA/MPLA", load_json_file(self.t.snapshot)["triple_exceptions"])
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
self.assertEqual(errors, 1, out)
self.assertIn("divergent triples", out)
def test_7_cross_bundle_divergence_is_warning_only(self):
def test_5_cross_bundle_divergence_is_warning_only(self):
fid = afi.generate_filament_id("BV", "PETG", "APLA")
self.t.add_vendor("VendorB", [
preset("APLA @base", filament_id=fid, instantiation=False,
@@ -676,7 +666,7 @@ class TestCheck7(OfCleanTreeCase):
self.assertIn('"APLA"', out)
class TestCheck8(OfCleanTreeCase):
class TestCheck6(OfCleanTreeCase):
def _write_map(self, rows):
path = os.path.join(self.t.dir, "bambu_filament_ids.json")
ubfi.write_map(path, rows, "testcommit", "2026-09-04")
@@ -784,15 +774,36 @@ class TestUpdateSnapshot(SyntheticTreeCase):
self.assertTrue(first.endswith(b"\n"))
self.assertNotIn(b"\r", first)
snap = json.loads(first.decode("utf-8"))
self.assertEqual(list(snap), ["ids"]) # state only, no exception lists
self.assertEqual(list(snap["ids"]), sorted(snap["ids"]))
self.assertEqual(snap["ids"]["AX01"], ["VendorA/APLA"])
self.assertEqual(snap["ids"]["OGFL99"],
["OrcaFilamentLibrary/Generic PLA", "VendorA/Generic PLA"])
self.assertEqual(snap["triples"]["AX01"], [["AVendor", "PLA", "APLA"]])
self.assertEqual(snap["triples"]["OGFL99"], [["Generic", "PLA", "fdm_pla"]])
self.assertEqual(snap["triple_exceptions"], [])
self.assertEqual(snap["ids"]["AX01"], {
"filaments": ["VendorA/APLA"], "name": "APLA",
"filament_type": "PLA", "filament_vendor": "AVendor"})
self.assertEqual(snap["ids"]["OGFL99"], {
"filaments": ["OrcaFilamentLibrary/Generic PLA", "VendorA/Generic PLA"],
"name": "Generic PLA", "filament_type": "PLA", "filament_vendor": "Generic"})
# Key order is part of the on-disk format.
self.assertEqual(list(snap["ids"]["AX01"]),
["filaments", "name", "filament_type", "filament_vendor"])
def test_refuses_new_reserved_namespace_claims(self):
def test_refuses_an_id_declared_under_two_triples(self):
# VendorB re-declares APLA's id for a different product: one id, two
# triples. No single entry can describe it, and check 3 rejects it anyway.
self.t.add_vendor("VendorB", [
preset("BPLA @base", filament_id="AX01", instantiation=False,
filament_vendor="BVendor", filament_type="PLA"),
preset("BPLA @P1", inherits="BPLA @base", compatible_printers=["P1"]),
])
with open(self.t.snapshot, "rb") as f:
before = f.read()
rc, out = self.t.update_snapshot()
self.assertEqual(rc, 1)
self.assertIn('refusing to sanction filament_id "AX01": declared under 2 triples '
'(AVendor/PLA/APLA; BVendor/PLA/BPLA)', out)
with open(self.t.snapshot, "rb") as f:
self.assertEqual(f.read(), before) # nothing written on refusal
def test_refuses_reserved_namespace_ids(self):
self.t.write_preset("VendorA", preset("CNEW @base", filament_id="GFX99",
instantiation=False,
filament_vendor="CV",
@@ -806,10 +817,6 @@ class TestUpdateSnapshot(SyntheticTreeCase):
self.assertIn("refusing to sanction", out)
with open(self.t.snapshot, "rb") as f:
self.assertEqual(f.read(), before) # nothing written on refusal
rc, _out = self.t.update_snapshot(allow_shared_catalog=True)
self.assertEqual(rc, 0)
snap = load_json_file(self.t.snapshot)
self.assertEqual(snap["ids"]["GFX99"], ["VendorA/CNEW"])
# ---------------------------------------------------------------------------
@@ -822,7 +829,7 @@ class TestAssign(OfCleanTreeCase):
self.assertEqual((changed, errors), (0, 0))
self.assertIn("nothing to do (0 files changed)", out)
def test_mints_into_family_root_and_rootless_member(self):
def test_mints_into_filament_root_and_rootless_member(self):
self.t.write_preset("VendorA", preset("FNEW @base", instantiation=False,
filament_vendor="FV",
filament_type="PLA"))
@@ -850,7 +857,7 @@ class TestAssign(OfCleanTreeCase):
self.assertEqual((changed, errors), (0, 0))
self.assertIn("nothing to do", out)
def test_refuses_family_with_incomplete_triple(self):
def test_refuses_filament_with_incomplete_triple(self):
self.t.write_preset("VendorA", preset("ENEW @base", instantiation=False))
self.t.write_preset("VendorA", preset("ENEW @P1", inherits="ENEW @base",
compatible_printers=["P1"]))
@@ -860,7 +867,7 @@ class TestAssign(OfCleanTreeCase):
self.assertIn("resolves empty filament_vendor and filament_type", out)
self.assertIn("mint key needs both", out)
def test_refuses_family_with_divergent_root_fields(self):
def test_refuses_filament_with_divergent_root_fields(self):
self.t.write_preset("VendorA", preset("HNEW @base1", instantiation=False,
filament_vendor="HV",
filament_type="PLA"))
@@ -876,7 +883,10 @@ class TestAssign(OfCleanTreeCase):
self.assertGreater(errors, 0)
self.assertIn("divergent (filament_vendor, filament_type)", out)
def test_shared_root_between_families_is_refused(self):
def test_parent_of_another_filament_never_receives_the_key(self):
# Members whose id-less parent belongs to another filament (here one
# parent shared by two filaments) carry the key themselves: the
# parent's own triple would mint a different id (check 3).
self.t.write_preset("VendorA", preset("shared_base", instantiation=False,
filament_vendor="SV",
filament_type="PLA"))
@@ -885,14 +895,24 @@ class TestAssign(OfCleanTreeCase):
self.t.write_preset("VendorA", preset("INEW @P1", inherits="shared_base",
compatible_printers=["P1"]))
changed, errors, out = self.t.assign()
self.assertEqual(changed, 0)
self.assertGreater(errors, 0)
self.assertIn("shared with famil", out)
self.assertEqual(errors, 0, out)
self.assertEqual(changed, 2)
for filament_name in ("HNEW", "INEW"):
member = load_json_file(self.t.preset_path("VendorA", f"{filament_name} @P1"))
self.assertEqual(member["filament_id"],
afi.generate_filament_id("SV", "PLA", filament_name))
parent = load_json_file(self.t.preset_path("VendorA", "shared_base"))
self.assertNotIn("filament_id", parent)
# ... and the tree they leave behind passes the identity check.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
def test_non_of_declaration_is_treated_as_missing(self):
# A declaration that isn't OF-format (e.g. a vendor bundle synced from
# an upstream catalog, like BBL's GF ids) is reminted like a missing
# family, not left alone.
# filament, not left alone.
self.t.write_preset("VendorA", preset("Synced PLA @base", filament_id="GFZZ00",
instantiation=False,
filament_vendor="ZV", filament_type="PLA"))
@@ -913,7 +933,7 @@ class TestAssign(OfCleanTreeCase):
changed, errors, _out = self.t.assign()
self.assertEqual((changed, errors), (0, 0))
def test_non_of_multi_root_family_converges_on_one_id(self):
def test_non_of_multi_root_filament_converges_on_one_id(self):
# Two per-printer roots of one product (same triple), both carrying
# the SAME non-OF id — the shape a synced vendor bundle ships (e.g.
# BambuStudio's own per-printer @base files). They must converge on
@@ -1068,7 +1088,7 @@ class TestRemint(SyntheticTreeCase):
root = load_json_file(self.t.preset_path("VendorA", "APLA @base"))
self.assertEqual(root["filament_id"], want)
# OFL was not part of the run
ofl_root = load_json_file(self.t.preset_path(OFL, "fdm_pla"))
ofl_root = load_json_file(self.t.preset_path(OFL, "Generic PLA @base"))
self.assertEqual(ofl_root["filament_id"], "OGFL99")
# idempotent: a second run over the same vendor rewrites nothing
changed, errors, _out = self.t.remint(["VendorA"])
@@ -1170,7 +1190,7 @@ class TestDropRedundantIds(SyntheticTreeCase):
self.assertNotIn("filament_id", gone)
kept = load_json_file(self.t.preset_path("VendorA", "Tuned PLA @P3"))
self.assertEqual(kept["filament_id"], "OGFL99")
# vendor-rooted families are untouched
# vendor-rooted filaments are untouched
root = load_json_file(self.t.preset_path("VendorA", "APLA @base"))
self.assertEqual(root["filament_id"], "AX01")
@@ -1196,7 +1216,7 @@ class TestMintCli(unittest.TestCase):
def test_requires_exactly_three_parts(self):
with self.assertRaises(SystemExit), \
contextlib.redirect_stderr(io.StringIO()):
afi.main(["--mint", "Vendor/Family"])
afi.main(["--mint", "Vendor/Filament"])
# ---------------------------------------------------------------------------
@@ -1222,7 +1242,7 @@ class TestRealTree(unittest.TestCase):
class TestReviewFixes(OfCleanTreeCase):
def test_check3_skips_of_id_inherited_from_other_vendor(self):
# An OFL family carries its own minted OF id and a vendor tunes it
# An OFL filament carries its own minted OF id and a vendor tunes it
# correctly (same base name, non-empty printers). The new claim must
# trip only the snapshot gate, never mint conformance.
fid = afi.generate_filament_id("Generic", "PLA", "Generic PLA Matte")
@@ -1248,7 +1268,7 @@ class TestReviewFixes(OfCleanTreeCase):
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
def test_check6c_prints_expected_mint(self):
def test_check3c_prints_expected_mint(self):
self.t.write_preset("VendorA", preset("Orphan PLA @P1",
compatible_printers=["P1 0.4 nozzle"],
filament_vendor="OV",
@@ -1263,24 +1283,24 @@ class TestReviewFixes(OfCleanTreeCase):
# ---------------------------------------------------------------------------
class TestBambuMap(unittest.TestCase):
def _bs_tree(self, families):
# families: list of (family, bambu_id, vendor, type); builds a minimal BBL bundle
# with an @base per family carrying the id and one instantiated child.
def _bs_tree(self, filaments):
# filaments: list of (name, bambu_id, vendor, type); builds a minimal BBL
# bundle with an @base per filament carrying the id and one instantiated child.
t = SyntheticTree()
self.addCleanup(t.cleanup)
presets = []
for family, bambu_id, vendor, ftype in families:
presets.append(preset(f"{family} @base", filament_id=bambu_id,
for filament_name, bambu_id, vendor, ftype in filaments:
presets.append(preset(f"{filament_name} @base", filament_id=bambu_id,
instantiation=False, filament_vendor=vendor,
filament_type=ftype))
presets.append(preset(f"{family} @P1", inherits=f"{family} @base",
presets.append(preset(f"{filament_name} @P1", inherits=f"{filament_name} @base",
compatible_printers=["P1 0.4 nozzle"]))
t.add_vendor("BBL", presets)
filaments, errors = afi.load_vendor_filaments(t.profiles, "BBL")
self.assertEqual(errors, [])
return filaments
def test_one_row_per_family(self):
def test_one_row_per_filament(self):
rows = ubfi.derive_rows(self._bs_tree([("Bambu ABS", "GFB00", "Bambu Lab", "ABS")]), {})
self.assertEqual(rows, {afi.generate_filament_id("Bambu Lab", "ABS", "Bambu ABS"):
{"bambu_id": "GFB00", "vendor": "Bambu Lab", "type": "ABS", "name": "Bambu ABS"}})
@@ -1308,13 +1328,13 @@ class TestOrcaTriplesFromAnalysis(unittest.TestCase):
t.add_vendor("VendorA", [
# Same triple declared under two different ids on purpose: mirrors
# a real, sanctioned case (Cubicon's xCeler line keeps its own id
# per printer instead of inheriting the family's @base id).
# per printer instead of inheriting the filament's @base id).
# Ambiguous, but must stay harmless unless something needs it.
preset("Ambig PLA @base", filament_id="OF111111", instantiation=False,
filament_vendor="V", filament_type="PLA"),
preset("Ambig PLA @P1", filament_id="OF222222", inherits="Ambig PLA @base",
compatible_printers=["P1"]),
# An unambiguous family elsewhere in the same tree.
# An unambiguous filament elsewhere in the same tree.
preset("Needed PLA @base", filament_id="OF333333", instantiation=False,
filament_vendor="V2", filament_type="PLA"),
preset("Needed PLA @P1", inherits="Needed PLA @base",
@@ -1375,9 +1395,9 @@ class TestDriftReport(unittest.TestCase):
compatible_printers=["P1"]),
])
rows = {
# matches the BBL bundle's "Match PLA" family: no drift either way
# matches the BBL bundle's "Match PLA" filament: no drift either way
"OFmatch01": {"bambu_id": "GFX01", "vendor": "V", "type": "PLA", "name": "Match PLA"},
# no family of this identity in the BBL bundle above
# no filament of this identity in the BBL bundle above
"OFghost01": {"bambu_id": "GFX02", "vendor": "V", "type": "PLA", "name": "Upstream Only PLA"},
}
orca_analysis = afi.analyze_tree(t.profiles)
@@ -1386,8 +1406,8 @@ class TestDriftReport(unittest.TestCase):
report = "\n".join(lines)
self.assertIn("Upstream Only PLA", report)
self.assertIn("Orca BBL families with no row: 1", report)
self.assertNotIn("Match PLA", report) # the matched family is not drift
self.assertIn("Orca BBL filaments with no row: 1", report)
self.assertNotIn("Match PLA", report) # the matched filament is not drift
if __name__ == "__main__":

View File

@@ -12,11 +12,11 @@ src/slic3r/Utils/BBLPrinterAgent.cpp loads it at runtime and translates an id
only where it crosses to or from a Bambu printer, so the correspondence never
has to be hand-maintained. See docs/HLSD/filament_id.md.
One row per BambuStudio filament PRODUCT: one commercial line = one
One row per BambuStudio filament PRODUCT: one named spool product = one
"@base"-declared filament_id, shared by every per-printer/per-nozzle
instantiation of it (BambuStudio follows the same one-product-one-id shape
Orca's own filament_id policy does). A row's key reuses whatever OF id Orca
already ships for that same (filament_vendor, filament_type, family) triple;
already ships for that same (filament_vendor, filament_type, filament) triple;
a triple Orca does not ship anywhere yet gets a freshly generated one.
Map format:
@@ -38,9 +38,9 @@ Run from anywhere: python3 scripts/update_bambu_filament_ids.py
--ref REF clone this BambuStudio ref instead of master
--output PATH write here instead of resources/printers/bambu_filament_ids.json
After writing, an informational drift report is printed: BambuStudio families
After writing, an informational drift report is printed: BambuStudio filaments
Orca ships nothing with the same identity for, and a count of Orca's own BBL
families that matched no BambuStudio row. Neither blocks the write; both are
filaments that matched no BambuStudio row. Neither blocks the write; both are
for a human to read.
"""
@@ -78,32 +78,34 @@ BAMBUSTUDIO_REPO = "https://github.com/bambulab/BambuStudio"
# ---------------------------------------------------------------------------
def derive_rows(bs_filaments, orca_triples):
"""orca_triples: {(vendor, type, family): orca_id} built from analyze_tree()["triples"]."""
by_family = {} # family -> (bambu_id, triple)
"""orca_triples: {(vendor, type, name): orca_id} from analyze_tree()["triples"]."""
by_filament = {} # filament_name -> (bambu_id, triple)
for rec in bs_filaments.values():
if not rec["instantiation"]:
continue
bambu_id, _src, _entry = resolve_filament_id(rec["name"], bs_filaments, {})
if not bambu_id:
continue
family = base_name(rec["name"])
filament_name = base_name(rec["name"])
triple = resolve_triple(rec["name"], bs_filaments, {})
prev = by_family.setdefault(family, (bambu_id, triple))
prev = by_filament.setdefault(filament_name, (bambu_id, triple))
if prev != (bambu_id, triple):
raise SystemExit(f"BambuStudio family {family!r} is not one product: {prev} vs {(bambu_id, triple)}")
raise SystemExit(f"BambuStudio filament {filament_name!r} is not one "
f"product: {prev} vs {(bambu_id, triple)}")
rows, seen = {}, {}
for family, (bambu_id, triple) in sorted(by_family.items()):
for filament_name, (bambu_id, triple) in sorted(by_filament.items()):
if bambu_id in seen:
raise SystemExit(f"Bambu id {bambu_id} is shared by {seen[bambu_id]!r} and {family!r}")
seen[bambu_id] = family
raise SystemExit(f"Bambu id {bambu_id} is shared by "
f"{seen[bambu_id]!r} and {filament_name!r}")
seen[bambu_id] = filament_name
orca_id = orca_triples.get(triple) or generate_filament_id(*triple) # reuse a salted id if we ship one
rows[orca_id] = {"bambu_id": bambu_id, "vendor": triple[0], "type": triple[1], "name": triple[2]}
return rows
def orca_triples_from_analysis(orca_analysis, needed_triples):
"""{(vendor, type, family): orca_id}, inverted from analyze_tree()["triples"]
(id -> [[vendor, type, family], ...]) and restricted to `needed_triples`
"""{(vendor, type, name): orca_id}, inverted from analyze_tree()["triples"]
(id -> [[vendor, type, name], ...]) and restricted to `needed_triples`
(the triples BambuStudio's own bundle ships, i.e. the only ones derive_rows
will ever look up).
@@ -118,7 +120,7 @@ def orca_triples_from_analysis(orca_analysis, needed_triples):
Restricting the scan to needed_triples matters for that check: elsewhere
in the tree one triple legitimately resolves two different ids on purpose
(e.g. Cubicon's xCeler line declares its own id per printer instead of
inheriting its family's, sanctioned in scripts/filament_id_snapshot.json)
inheriting its root's, sanctioned in scripts/filament_id_snapshot.json)
and that pre-existing, Bambu-unrelated divergence must not block a map
that never reads it.
"""
@@ -150,9 +152,9 @@ def drift_report(rows, orca_analysis):
independently, so only the (vendor, type, name) identity is comparable).
Returns print-ready lines: one per BambuStudio row triple with no
same-triple family in Orca's BBL bundle (upstream ships it, we ship
same-triple filament in Orca's BBL bundle (upstream ships it, we ship
nothing with that identity there — sometimes a genuinely missing product,
sometimes a renamed one), then a count of Orca's own BBL families that
sometimes a renamed one), then a count of Orca's own BBL filaments that
matched no BambuStudio row (Orca-only products, e.g. a name-drifted
duplicate of one already counted in the first list).
"""
@@ -160,19 +162,20 @@ def drift_report(rows, orca_analysis):
bbl_filaments = orca_analysis["vendors"].get("BBL", {})
ofl_filaments = orca_analysis["vendors"].get(OFL, {})
orca_families = {}
orca_filaments = {}
for rec in bbl_filaments.values():
if not rec["instantiation"]:
continue
triple = resolve_triple(rec["name"], bbl_filaments, ofl_filaments)
orca_families.setdefault(base_name(rec["name"]), triple)
orca_filaments.setdefault(base_name(rec["name"]), triple)
lines = []
for triple in sorted(row_triples - set(orca_families.values())):
for triple in sorted(row_triples - set(orca_filaments.values())):
lines.append(f'upstream ships {triple[2]!r} ({triple[0]}/{triple[1]}), '
"we ship nothing with that identity")
orca_only = [family for family, triple in orca_families.items() if triple not in row_triples]
lines.append(f"Orca BBL families with no row: {len(orca_only)} Orca-only product(s)")
orca_only = [name for name, triple in orca_filaments.items()
if triple not in row_triples]
lines.append(f"Orca BBL filaments with no row: {len(orca_only)} Orca-only product(s)")
return lines