Add filament_id plan v4: dissolve the Qidi QD_* island

This commit is contained in:
SoftFever
2026-08-21 18:13:18 +08:00
parent f3fa3a34bd
commit 71563cc6c6

224
filament_id_plan_v4.md Normal file
View File

@@ -0,0 +1,224 @@
# filament_id plan v4: dissolve the Qidi `QD_*` island
Supersedes §0.2 of `filament_id_plan_v3.md` (the "`QD_*` island — untouched, forever"
decision) and amends every v3 section that carved out `QD_*`. Everything else in v3 —
the mint rule, the succession ledger, the BBL island, checks 19 — stands as
implemented (v3.0v3.2, all gates green at commit `f7c1b290fd`).
Decision driver: the island contradicts the catalog architecture v3 built. Qidi presets
carry 204 `QD_<series>_<vendor>_<typeidx>` ids over 264 declarations (49 families,
measured 2026-08-21) — one commercial product carries up to *five* ids (one per printer
series: `QIDI PLA Rapido` = `QD_0_1_1``QD_4_1_1`), which is exactly the
fragmentation v3 exists to remove. The island was frozen because Qidi's filament box
composes these ids from device enums and requires exactly-matching presets
(`QidiPrinterAgent.cpp:146-152`). But v3.0 shipped the machinery that makes freezing
unnecessary: the succession ledger already translates retired ids on resolution miss.
`QD_*` stops being a *preset id space* and becomes a *device protocol namespace*,
translated once at the agent edge.
## 0. The architecture change
Two tiers, **one** frozen island:
1. **BBL island — untouched, unchanged.** All v3 reasoning holds (device/RFID/cloud
contract is external and opaque).
2. **Everything else converges on OFL product ids — now including Qidi.** All 204
`QD_*` ids re-mint from their family triples and gain **retired** ledger entries
(not cross-island hints: after dissolution there is no island to own them, and
check 4's "never again" is exactly the guard we want against upstream re-adding
them). The box keeps working because `QidiPrinterAgent` resolves its composed
`QD_*` id through `resolve_filament_id_succession()` on miss — the same mechanism
every other retired id already uses.
Why retire rather than hint (the one real design choice here): hints exist for ids a
*foreign catalog* owns and may legitimately re-ship (`GF*`). Post-dissolution, nothing
may ever re-ship a `QD_*` id in the profile tree — the device composes them at
runtime, the tree translates them. Retired entries make CI enforce that permanently
(`--update-snapshot` refuses resurrections, check 4 refuses occurrences); hints would
permit re-shipping, which is now always a regression. The QD→family mapping is
strictly 1:1 (verified: no `QD_*` id is claimed by more than one family), so the
mode-rule successor is unambiguous for every entry.
## 1. What the dissolution consists of
Three independent work packages, ordered for bisectability:
- **(A) Re-converge the drifted tree** — prerequisite, not Qidi-specific. The
2026-07 main merge (`f3fa3a34bd`) brought upstream vendor updates in pre-v3 style:
`--check` currently exits with **243 errors** (123 unsanctioned new ids, 46 new
instantiated own-key presets, 31 override drifts, 18 reserved-space claims, 5
hint-keys re-declared — Qidi re-added `GFB99`/`GFG99`/`GFL99`, Snapmaker re-added
`GFG96`/`GFU99` and even four *retired* ids incl. `OGFL99`, plus Snapmaker U1
triple errors and a GreenGate3D rename). This is the standing WF-B maintenance
pass; the dissolution's gates cannot go green on a red base.
- **(B) C++ succession hook in the Qidi agent** — safe to land before any data
changes (pure miss-path: while `QD_*` presets still exist, the hook never fires).
- **(C) The dissolution proper** — tooling flip + Qidi data fixes + re-mint +
snapshot/ledger regeneration, one vendor worksheet in the v3.2 mold.
## 2. Runtime translation (work package B)
`QidiPrinterAgent.cpp:183-192` currently: compose `setting_id` → keep it if a visible
base preset declares it → else degrade to `filament_id_by_type(tray_type)` (i.e. every
QIDI-brand box slot silently becomes Generic once the ids re-mint). Insert the
succession walk between those two steps:
```cpp
} else if (!setting_id.empty() && has_visible_base_preset(bundle->filaments, setting_id)) {
tray.tray_info_idx = setting_id;
} else {
// Retired QD_* protocol ids forward to their minted successors via the shipped ledger.
const std::string successor = setting_id.empty() ? std::string()
: resolve_filament_id_succession(setting_id);
if (!successor.empty() && has_visible_base_preset(bundle->filaments, successor))
tray.tray_info_idx = successor;
else
tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type);
}
```
`resolve_filament_id_succession` is `Preset.hpp:119` (loads once, cycle-guarded,
empty-safe) — no new includes needed beyond what the file already reaches through
`PresetBundle`. This one hook covers both composition paths: the numeric-series
`build_setting_id` lambda *and* the non-numeric fallback
`map_filament_type_to_setting_id` (`:325-342`), whose four hardcoded returns
(`QD_1_0_1`/`_11`/`_41`/`_50` = Generic PLA/ABS/PETG/TPU 95A) become ledger keys in
package C. Keep that function as-is but extend its comment: the returned ids are
retired ledger keys by design, and `scripts/tests/test_filament_id.py` parses the
initializer (see §4 tests) — the Moonraker treatment (`MoonrakerPrinterAgent.cpp:
619-627`) of replacing the table with name lookups was considered and not taken: the
table already routes through the same ledger as the composed ids, and two translation
mechanisms in one agent is worse than one.
Tests (same commit):
- `tests/libslic3r/test_filament_id_succession.cpp`: add a section asserting a
`QD_`-shaped key forwards like any other (`{"QD_2_1_11", "OFnew001"}` resolves to
`"OFnew001"`) — pins that the walk is prefix-agnostic.
- `scripts/tests/test_filament_id.py`: new test parsing the four `QD_` literals out of
`QidiPrinterAgent.cpp::map_filament_type_to_setting_id` (mirror the parser in
`scripts/test_moonraker_lane_data.py`) and asserting each is a ledger key whose
chain terminates at a live tree id. **Add it marked expected-fail/skipped until
package C lands, then flip it on** — it is the permanent code↔ledger lockstep guard.
## 3. Tooling and validation flip (work package C, first commit)
All in `scripts/assign_filament_ids.py`; every touched line measured 2026-08-21:
- `is_island_declaration` (`:327-329`) → `return vendor == "BBL"`. This single change
pulls every `QD_*` declarer into the triple bookkeeping (`:385`) and thus into
checks 3 and 8, into `--remint`'s domain (`:1464`), and out of the hint-key
tolerances (`:877`, `:1100`).
- `reserved_space_owner` (`:596-604`): `QD_*` returns `(True, None)` — reserved,
ownerless, exactly like the P-hex/user-custom space. Consequences, all wanted:
check 6 refuses any future vendor claim; the `--update-snapshot` sanction gate
(`:938-963`) refuses new `QD_*` ids outright; the vanish path (`:994`) routes
`QD_*` to **retired** (owner `None` ≠ island), not released-with-hint. Update the
two message sites that render `owner is None` as "reserved for user-custom presets"
(`:628` docstring, `:958`, and check 6's copy) to name the space generically or
special-case `QD_*` ("Qidi device protocol; dissolved island — retired, never
declarable").
- Check 1 (`:667-668`): delete the `vendor == "Qidi" and fid.startswith("QD_")`
exemption.
- Module docstring (`:27-45`): rewrite the `QD_*` bullet — reserved space stays
listed, but as "device protocol namespace, translated via the succession ledger;
dissolved as a catalog island in v4, may never be declared".
- `scripts/tests/test_filament_id.py`: update the three island assertions —
`reserved_space_owner("QD_X4_PLA")``(True, None)` (`:403`),
`is_island_declaration("Qidi", "QD_X4_PLA")``False` (`:413`), and the
reserved-space message case (`:570`).
No changes to the ledger schema, the C++ checks, `--retire` (post-flip it accepts
`QD_*` olds automatically — they are non-island now), or the validator: check 3's
"non-BBL, non-`QD_*`" phrasing in v3 §3 was always implemented as "non-island", so the
flip *is* the spec change.
## 4. Migration phases
**v4.0 — re-converge the drifted tree (package A).** Per-vendor WF-B worksheets over
the 243 errors: Snapmaker U1 (triple divergence `Generic|Snapmaker|snapmaker`, empty
vendors, four resurrected retired ids — these force re-mints since retirement is
permanent), BBL/addnorth `GF_AN*` overrides (BBL island: grandfather via snapshot,
they are BBL-internal), Qidi/Snapmaker re-declared hint keys (re-mint those declarers,
the check's own prescription), GreenGate3D rename, then `--update-snapshot` (new
upstream `QD_*` ids sanction cleanly — the island is still intact in this phase) and
the full v3 §5 gate battery. Bump `version` in every touched
`resources/profiles/<Vendor>.json`. **Do not start v4.2 until `--check` exits 0.**
**v4.1 — the agent hook (package B).** §2 as written; `libslic3r_tests` +
`scripts/tests/test_filament_id.py` green; behavior-neutral by construction (no `QD_*`
ledger entries exist yet).
**v4.2 — the dissolution (package C).** One Qidi worksheet, v3.2 machinery:
1. Tooling flip commit (§3). `--check` now reports the Qidi island as
non-conformant — expected, red only between commits of this phase.
2. Data fixes, before any re-mint (the v3 "W3 lands first" lesson — type/vendor are
key components): add `filament_vendor: ["QIDI"]` at each QIDI-brand family's
inheritance apex so all declarers resolve it (39 of 49 families currently resolve
none — check 8a would refuse the mint). Verify the six Generic families
(`Generic PLA/ABS/PETG/PC/TPU 95A/PLA Silk`) resolve triples identical to their
OFL counterparts (vendor `Generic`, OFL's `filament_type`) so they *converge onto
the OFL ids* by triple math — the whole point; any mismatch is a W3-style data fix
here, not a fork. No `inherits`/`compatible_printers` re-pointing anywhere: the
series intermediates (`Generic PLA@Q2-Series` etc., all `instantiation: false`)
simply keep declarations whose values become the OFL ids.
3. `python scripts/assign_filament_ids.py --remint Qidi` — rewrites all 264
declarations in place to their family mints (same triple ⇒ same id across a
family's series intermediates and per-nozzle declarers; convergence with OFL ids
is legal by design, `want_id` already permits same-triple collisions `:1447`).
4. `--update-snapshot` — retires every vanished `QD_*` id with its mode-rule
successor. **Audit the ledger diff: all ~204 new entries must be `QD_*`→non-null.**
A null successor (possible only for a declared-only id with zero instantiated
claims) gets an explicit `--retire "QD_x=OFy"` with the family's minted id.
5. Un-skip the §2 lockstep test. Bump `resources/profiles/Qidi.json` version. Full
gate battery (§5).
Known residue, accepted: `QIDI PC-ABS-FR` (series 12) vs `QIDI PC/ABS-FR`
(series 34) are two preset-name families for one product → two ids. Renames were
ruled out in v3 (`renamed_from` rejected for migrations); if upstream ever unifies the
name, content-addressing re-ids and the ledger absorbs it — self-healing, no action
now.
**v4.3 — OFL consolidation (unchanged).** v3.3 stays optional and id-stable; QIDI-brand
families are vendor-unique and stay in the Qidi bundle.
## 5. Gates (per phase, delta from v3 §5)
Unchanged battery: `orca_extra_profile_check.py` exit 0, `assign_filament_ids.py
--check` exit 0, `scripts/tests/test_filament_id.py` all green, `libslic3r_tests`
green, validator `-l 2` tree-wide + `-f` tree-wide + `-v Qidi` exit 0, custom-preset
fixture archives (v4.2 touches no preset visibility, but they are cheap insurance —
run them for v4.0, which touches instantiation-adjacent upstream drift), flatten
config-equivalence. New for v4.2: equivalence diff may contain **only** `filament_id`
value changes (QD→OF) and the added `filament_vendor` keys on QIDI-brand families;
ledger diff audit per §4.4; the map-literal lockstep test. Manual release-checklist
item: Qidi box smoke test — slot holding a QIDI-brand material must surface the brand
preset (not Generic) on an updated client, via the memory-documented local Klipper
test rig.
## 6. Accepted costs (explicit, new relative to v3)
- **Every Qidi box slot resolves through the ledger miss-path forever** (one hash-map
walk per slot per status poll — negligible, and structurally identical to how
every retired id already resolves). The QD→OF translation table is maintained by
the retirement machinery, not by hand.
- **Older Orca clients** (pre-v4 profiles) paired with re-minted profile trees lose
QIDI-brand slot matching (they look up `QD_*` and fall back generic-by-type — the
degradation the hook removes for updated clients). Same one-time field-transition
shape v3 §6 already accepted for AMS/AFC ids.
- **Two ids for PC-ABS-FR** until upstream unifies the family name (§4 residue).
## 7. Evidence
Measured on this tree 2026-08-21 unless cited to v3: 204 distinct `QD_*` ids / 264
declaring presets / 49 families, QD→family strictly 1:1, per-series id sets
(`QD_0..4_1_1` = `QIDI PLA Rapido` etc.); 39 families resolve no `filament_vendor`;
declarations live on `instantiation:false` series intermediates (Q2/Q2C/X5/X4) and
per-nozzle X-Plus-4 presets; composition + miss-fallback at
`QidiPrinterAgent.cpp:146-152, 183-192`, hardcoded fallback table `:325-342`;
succession helpers `Preset.hpp:109-119`; ledger = 575 retired + 194 hints, zero `QD_*`
entries; snapshot holds 160 of the 204 (the 44 newcomers are post-merge drift);
`--check` = 243 errors, categorized in §1(A); island exemption mechanics at
`assign_filament_ids.py:327-329, 385, 596-604, 667, 877, 994, 1464, 1514`; `--remint`
same-triple convergence guard `:1447-1453`; retirement-permanence gate `:965-976`.