Files
OrcaSlicer/filament_id_plan_v4.md

14 KiB
Raw Blame History

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_1QD_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:

} 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.