Files
OrcaSlicer/filament_id_plan.html
SoftFever 051cdd4560 init
2026-07-02 21:57:56 +08:00

1030 lines
72 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>filament_id — design &amp; migration plan · OrcaSlicer</title>
<style>
/* ================= tokens ================= */
:root{
--paper:#F7FAF7;
--panel:#FFFFFF;
--grid:#DEE8DF;
--line:#C9D6CB;
--ink:#17251D;
--muted:#5B6B60;
--faint:#8A988E;
--teal:#008A5E;
--teal-ink:#00593D;
--teal-bg:#E7F5EE;
--amber:#B45309;
--amber-bg:#FBF1DF;
--red:#BE3A25;
--red-bg:#FBEDE9;
--blue:#2E5E8E;
--blue-bg:#EAF1F8;
--slate:#141F1B;
--slate-2:#1C2B25;
--slate-line:#2E4238;
--slate-text:#D9E5DE;
--mono:"Cascadia Code","Consolas","SF Mono",ui-monospace,monospace;
--sans:"Segoe UI Variable Text","Segoe UI",system-ui,-apple-system,sans-serif;
--disp:"Segoe UI Variable Display","Segoe UI",system-ui,sans-serif;
--rail:212px;
--maxw:920px;
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
body{
background:var(--paper);
color:var(--ink);
font-family:var(--sans);
font-size:15.5px;
line-height:1.65;
}
::selection{background:#BFE8D6}
/* ================= nav rail ================= */
nav.rail{
position:fixed;top:0;left:0;bottom:0;width:var(--rail);
border-right:1px solid var(--line);
background:var(--paper);
padding:26px 18px;
overflow-y:auto;
z-index:10;
}
nav.rail .brand{font-family:var(--mono);font-size:12px;color:var(--teal-ink);letter-spacing:.02em}
nav.rail .brand b{display:block;font-size:14px;color:var(--ink)}
nav.rail ol{list-style:none;margin-top:22px}
nav.rail a{
display:flex;gap:9px;align-items:baseline;
padding:6px 8px;border-radius:6px;
color:var(--muted);text-decoration:none;font-size:13px;
}
nav.rail a .n{font-family:var(--mono);font-size:11px;color:var(--faint);min-width:20px}
nav.rail a:hover{background:#EEF4EF;color:var(--ink)}
nav.rail a.on{background:var(--teal-bg);color:var(--teal-ink)}
nav.rail a.on .n{color:var(--teal)}
nav.rail .foot{margin-top:26px;font-size:11px;color:var(--faint);line-height:1.5}
main{margin-left:var(--rail);padding:0 40px 120px}
@media (max-width:1080px){
nav.rail{position:static;width:auto;border-right:0;border-bottom:1px solid var(--line);padding:18px 22px}
nav.rail ol{display:flex;flex-wrap:wrap;gap:2px;margin-top:12px}
main{margin-left:0;padding:0 20px 90px}
}
/* ================= structure ================= */
section{max-width:var(--maxw);margin:0 auto;padding-top:74px}
.eyebrow{
font-family:var(--mono);font-size:11px;letter-spacing:.16em;
text-transform:uppercase;color:var(--teal);
display:flex;align-items:center;gap:12px;
}
.eyebrow::after{content:"";height:1px;flex:1;background:
repeating-linear-gradient(90deg,var(--line) 0 14px,transparent 14px 20px)}
.eyebrow .sn{background:var(--ink);color:var(--paper);padding:2px 8px;border-radius:4px;letter-spacing:.06em}
h2{font-family:var(--disp);font-size:clamp(25px,3.4vw,32px);font-weight:750;letter-spacing:-.015em;margin:12px 0 8px}
.lede{color:var(--muted);max-width:64ch;margin-bottom:26px}
h3{font-family:var(--disp);font-size:18px;font-weight:700;margin:34px 0 10px}
p{max-width:70ch;margin-bottom:12px}
p.tight{margin-bottom:6px}
a{color:var(--blue)}
code,.id{
font-family:var(--mono);font-size:.88em;
background:#EDF3EE;border:1px solid var(--line);
border-radius:5px;padding:1px 6px;white-space:nowrap;
}
.id.ok{background:var(--teal-bg);border-color:#9ED4BC;color:var(--teal-ink)}
.id.legacy{background:var(--amber-bg);border-color:#E8C692;color:var(--amber)}
.id.bad{background:var(--red-bg);border-color:#EBB2A5;color:var(--red)}
.ref{font-family:var(--mono);font-size:11.5px;color:var(--faint);white-space:nowrap}
.cap{font-size:12.5px;color:var(--muted);margin:10px 2px 0;max-width:78ch}
.cap b{color:var(--ink)}
.note{
border-left:3px solid var(--teal);background:var(--teal-bg);
padding:12px 16px;border-radius:0 8px 8px 0;margin:16px 0;max-width:74ch;font-size:14px;
}
.note.warn{border-color:var(--amber);background:var(--amber-bg)}
.note.danger{border-color:var(--red);background:var(--red-bg)}
/* panels & diagrams */
.panel{
background:var(--panel);
border:1px solid var(--line);
border-radius:12px;
padding:22px;
margin:18px 0;
background-image:
linear-gradient(var(--grid) 1px,transparent 1px),
linear-gradient(90deg,var(--grid) 1px,transparent 1px);
background-size:26px 26px;
background-position:center;
position:relative;
overflow-x:auto;
}
.panel .tag{
position:absolute;top:10px;right:14px;
font-family:var(--mono);font-size:10px;letter-spacing:.1em;
color:var(--faint);text-transform:uppercase;
}
.panel.dark{
background-color:var(--slate);
border-color:var(--slate-line);
background-image:
linear-gradient(rgba(120,180,150,.10) 1px,transparent 1px),
linear-gradient(90deg,rgba(120,180,150,.10) 1px,transparent 1px);
color:var(--slate-text);
}
.panel.dark .tag{color:#5E7A6C}
.panel.plain{background-image:none}
svg text{font-family:var(--mono)}
.legend{display:flex;flex-wrap:wrap;gap:16px;font-size:12px;color:var(--muted);margin-top:10px}
.legend span{display:inline-flex;align-items:center;gap:6px}
.dotk{width:10px;height:10px;border-radius:3px;display:inline-block}
/* stat strip */
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:26px 0 8px}
.stat{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px}
.stat .v{font-family:var(--mono);font-size:26px;font-weight:600;letter-spacing:-.02em}
.stat .k{font-size:12px;color:var(--muted);margin-top:2px;line-height:1.45}
.stat.red .v{color:var(--red)} .stat.teal .v{color:var(--teal-ink)} .stat.amber .v{color:var(--amber)}
/* chips / preset boxes */
.chip{
display:inline-flex;align-items:center;gap:7px;
border:1px solid var(--line);background:var(--panel);
border-radius:7px;padding:4px 9px;font-size:12.5px;white-space:nowrap;
}
.chip .cid{font-family:var(--mono);font-size:11px;padding:0 5px;border-radius:4px}
.cid.g{background:var(--teal-bg);color:var(--teal-ink)}
.cid.a{background:var(--amber-bg);color:var(--amber)}
.cid.r{background:var(--red-bg);color:var(--red)}
/* tables */
table{border-collapse:collapse;width:100%;font-size:13.5px;margin:14px 0;background:var(--panel)}
th{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);text-align:left}
th,td{border:1px solid var(--line);padding:8px 11px;vertical-align:top}
td .id{font-size:12px}
tr.hl td{background:var(--teal-bg)}
/* taxonomy cards */
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(268px,1fr));gap:14px;margin:18px 0}
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px;display:flex;flex-direction:column;gap:7px}
.card .hd{display:flex;align-items:center;gap:8px}
.card .pn{font-family:var(--mono);font-size:11px;background:var(--ink);color:var(--paper);border-radius:4px;padding:1px 6px}
.card .nm{font-family:var(--mono);font-size:12.5px;font-weight:600}
.card .ct{margin-left:auto;font-family:var(--mono);font-size:12px;color:var(--red)}
.card .ct.dim{color:var(--faint)}
.card p{font-size:12.8px;color:var(--muted);margin:0;line-height:1.5}
.card .fx{font-size:12.3px;border-top:1px dashed var(--line);padding-top:7px}
.card .fx b{color:var(--teal-ink);font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
/* migration phases */
.phases{display:flex;flex-direction:column;gap:0;margin:18px 0}
.phase{display:grid;grid-template-columns:54px 1fr;gap:0}
.phase .ml{display:flex;flex-direction:column;align-items:center}
.phase .dot{width:30px;height:30px;border-radius:50%;background:var(--panel);border:2px solid var(--teal);color:var(--teal-ink);
font-family:var(--mono);font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:center;flex:none}
.phase .bar{width:2px;flex:1;background:repeating-linear-gradient(var(--teal) 0 6px,transparent 6px 11px);min-height:18px}
.phase:last-child .bar{display:none}
.phase .bd{padding:2px 0 26px 14px}
.phase h4{font-family:var(--disp);font-size:15.5px;font-weight:700}
.phase p{font-size:13.5px;color:var(--muted);margin:4px 0 0;max-width:66ch}
/* decision cards */
.dec{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px;margin:18px 0}
.dec .card .opt{font-size:12.8px;display:flex;gap:8px;align-items:baseline}
.dec .card .opt.rec::before{content:"→";color:var(--teal);font-weight:700}
.dec .card .opt.alt::before{content:"·";color:var(--faint)}
.dec .card .opt.rec b{color:var(--teal-ink)}
details{border:1px solid var(--line);border-radius:9px;background:var(--panel);margin:12px 0;max-width:var(--maxw)}
summary{cursor:pointer;padding:11px 15px;font-weight:600;font-size:13.5px;color:var(--teal-ink)}
details > div{padding:2px 16px 14px;font-size:13.5px}
kbd.k{font-family:var(--mono);font-size:11px;border:1px solid var(--line);border-bottom-width:2px;border-radius:4px;padding:0 5px;background:#fff}
/* hero */
header.hero{max-width:var(--maxw);margin:0 auto;padding-top:56px}
.hero .eyebrow{color:var(--muted)}
.hero h1{font-family:var(--disp);font-size:clamp(30px,4.6vw,44px);font-weight:800;letter-spacing:-.02em;line-height:1.12;margin:16px 0 10px;max-width:20ch}
.hero .sub{color:var(--muted);max-width:66ch;font-size:16px}
.hero .meta{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px;font-size:12px;color:var(--faint);font-family:var(--mono)}
@media (prefers-reduced-motion:no-preference){
.anat .dim{stroke-dasharray:600;stroke-dashoffset:600;animation:draw 1.1s ease .25s forwards}
.anat .lbl{opacity:0;animation:fade .5s ease 1.05s forwards}
@keyframes draw{to{stroke-dashoffset:0}}
@keyframes fade{to{opacity:1}}
}
:focus-visible{outline:2px solid var(--teal);outline-offset:2px;border-radius:4px}
@media print{
nav.rail{display:none} main{margin:0;padding:0 8mm}
.panel{break-inside:avoid} section{padding-top:34px}
}
</style>
</head>
<body>
<nav class="rail" aria-label="Sections">
<div class="brand">OrcaSlicer<b>filament_id plan</b></div>
<ol>
<li><a href="#overview"><span class="n">§0</span>Overview</a></li>
<li><a href="#matching"><span class="n">§1</span>How matching works</a></li>
<li><a href="#landscape"><span class="n">§2</span>The landscape today</a></li>
<li><a href="#rule"><span class="n">§3</span>The rule</a></li>
<li><a href="#taxonomy"><span class="n">§4</span>Error taxonomy</a></li>
<li><a href="#migration"><span class="n">§5</span>Migration plan</a></li>
<li><a href="#enforcement"><span class="n">§6</span>Enforcement</a></li>
<li><a href="#evidence"><span class="n">§7</span>Evidence</a></li>
<li><a href="#decisions"><span class="n">§8</span>Open decisions</a></li>
</ol>
<div class="foot">Companion to <span style="font-family:var(--mono)">filament_id_plan.md</span>.<br>Follow-up to PR&nbsp;#14459.<br>2026-07-02</div>
</nav>
<main>
<!-- ============================ HERO ============================ -->
<header class="hero" id="top">
<div class="eyebrow"><span class="sn">DESIGN&nbsp;REVIEW</span> resources/profiles · filament_id</div>
<h1>One id, one material — on every printer, exactly once.</h1>
<p class="sub">A filament_id names a <b>material family</b>: one commercial product line, shared by all of its
per-printer variants. Devices match spools by <span class="id">filament_id + printer compatibility</span>.
That only works if, for any one printer, at most one compatible preset carries a given id. Bambu keeps this
invariant; the other 64 vendor bundles broke it 1,256 times.</p>
<div class="panel plain" style="margin-top:28px">
<span class="tag">id anatomy — Bambu's grammar</span>
<svg class="anat" viewBox="0 0 880 250" role="img" aria-label="Anatomy of the Bambu id GFA00: GF prefix, family letter A, tier number 00. The same id is shared by all printer variants of the material.">
<!-- big id -->
<g font-size="64" font-weight="700" fill="var(--ink)">
<text x="330" y="78">G</text><text x="378" y="78">F</text>
<text x="424" y="78" fill="var(--teal-ink)">A</text>
<text x="472" y="78" fill="var(--blue)">0</text><text x="512" y="78" fill="var(--blue)">0</text>
</g>
<!-- dimension lines + leaders -->
<g class="dim" stroke="var(--faint)" stroke-width="1.2" fill="none">
<path d="M332 92 v14 h84 v-14"/>
<path d="M426 92 v26 h24 v-26"/>
<path d="M474 92 v14 h72 v-14"/>
<path d="M374 106 v10 L290 132" stroke-dasharray="3 4"/>
<path d="M438 118 v34" stroke-dasharray="3 4"/>
<path d="M510 106 v10 L596 132" stroke-dasharray="3 4"/>
</g>
<g class="lbl" font-size="12.5" fill="var(--muted)">
<text x="286" y="146" text-anchor="end" font-weight="700" fill="var(--amber)">prefix — Bambu's namespace</text>
<text x="286" y="162" text-anchor="end">frozen: AMS RFID tags carry these bytes</text>
<text x="438" y="172" text-anchor="middle" font-weight="700" fill="var(--teal-ink)">family letter — A: Bambu PLA</text>
<text x="438" y="189" text-anchor="middle">B ABS/ASA · C PC · G PETG · L generic/3rd-party PLA</text>
<text x="438" y="204" text-anchor="middle">N PA · P PP/PE · S support · T PET/PPS · U TPU</text>
<text x="600" y="146" font-weight="700" fill="var(--blue)">tier number</text>
<text x="600" y="162">0049 branded · 5059 fiber-filled</text>
<text x="600" y="177">6070 partners · 9599 generic, desc.</text>
</g>
<!-- shared-by-variants strip -->
<g class="lbl" font-size="11" >
<text x="40" y="240" fill="var(--faint)" font-size="12">shared by every variant:</text>
<g fill="var(--ink)">
<rect x="210" y="226" width="190" height="20" rx="5" fill="var(--teal-bg)" stroke="#9ED4BC"/>
<text x="219" y="240">Bambu PLA Basic @BBL X1C</text>
<rect x="410" y="226" width="190" height="20" rx="5" fill="var(--teal-bg)" stroke="#9ED4BC"/>
<text x="419" y="240">Bambu PLA Basic @BBL P1P</text>
<rect x="610" y="226" width="252" height="20" rx="5" fill="var(--teal-bg)" stroke="#9ED4BC"/>
<text x="619" y="240">Bambu PLA Basic @BBL X1C 0.2 nozzle</text>
</g>
</g>
</svg>
</div>
<div class="stats">
<div class="stat red"><div class="v">1,256</div><div class="k">printer-level ambiguity errors (validator <code>-f</code>, tree-wide)</div></div>
<div class="stat red"><div class="v">356</div><div class="k">logical collision groups after de-duplication</div></div>
<div class="stat amber"><div class="v">30 / 65</div><div class="k">vendor bundles affected / total</div></div>
<div class="stat teal"><div class="v">0</div><div class="k">violations in BBL — cleaned in PR&nbsp;#14459, CI-gated</div></div>
</div>
<p class="cap">Every number in this document is reproducible: an independent loader-faithful audit re-derives the
validator's output <b>exactly</b> (1256 = 1256), and every code claim carries a <span class="ref">file:line</span> reference.</p>
</header>
<!-- ============================ §0 ============================ -->
<section id="overview">
<div class="eyebrow"><span class="sn">§0</span> overview</div>
<h2>The plan in one screen</h2>
<p class="lede">Two deliverables: a <b>generation rule</b> so ids can never become ambiguous again (goal&nbsp;1), and a
<b>migration</b> that fixes the 356 existing collisions without breaking users (goal&nbsp;2).</p>
<div class="panel plain">
<span class="tag">the whole plan</span>
<svg viewBox="0 0 880 190" role="img" aria-label="Flow: today's broken state, through rule plus tooling, migration per vendor, to tree-wide enforced invariant.">
<defs>
<marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="var(--faint)"/>
</marker>
</defs>
<g font-size="12.5">
<rect x="10" y="30" width="190" height="112" rx="10" fill="var(--red-bg)" stroke="#EBB2A5"/>
<text x="24" y="54" font-weight="700" fill="var(--red)">TODAY</text>
<text x="24" y="76" fill="var(--ink)">7+ ad-hoc id schemes</text>
<text x="24" y="94" fill="var(--ink)">356 collision groups</text>
<text x="24" y="112" fill="var(--ink)">zero written rules</text>
<text x="24" y="130" fill="var(--muted)">CI covers BBL only</text>
<rect x="250" y="30" width="190" height="112" rx="10" fill="var(--blue-bg)" stroke="#B5CCE2"/>
<text x="264" y="54" font-weight="700" fill="var(--blue)">RULE + TOOLING §3 §6</text>
<text x="264" y="76" fill="var(--ink)">deterministic OF* mint</text>
<text x="264" y="94" fill="var(--ink)">id lives on @base only</text>
<text x="264" y="112" fill="var(--ink)">CI ratchets + snapshot</text>
<text x="264" y="130" fill="var(--muted)">no profile changes yet</text>
<rect x="490" y="30" width="190" height="112" rx="10" fill="var(--amber-bg)" stroke="#E8C692"/>
<text x="504" y="54" font-weight="700" fill="var(--amber)">MIGRATION §5</text>
<text x="504" y="76" fill="var(--ink)">fresh ids only, ever</text>
<text x="504" y="94" fill="var(--ink)">OFL first, then per vendor</text>
<text x="504" y="112" fill="var(--ink)">names never change</text>
<text x="504" y="130" fill="var(--muted)">one reviewable PR each</text>
<rect x="730" y="30" width="140" height="112" rx="10" fill="var(--teal-bg)" stroke="#9ED4BC"/>
<text x="744" y="54" font-weight="700" fill="var(--teal-ink)">DONE</text>
<text x="744" y="76" fill="var(--ink)">invariant holds</text>
<text x="744" y="94" fill="var(--ink)">tree-wide,</text>
<text x="744" y="112" fill="var(--ink)">by construction</text>
</g>
<g stroke="var(--faint)" stroke-width="1.5" marker-end="url(#ar)">
<line x1="202" y1="86" x2="246" y2="86"/>
<line x1="442" y1="86" x2="486" y2="86"/>
<line x1="682" y1="86" x2="726" y2="86"/>
</g>
<text x="440" y="172" font-size="11.5" fill="var(--faint)" text-anchor="middle">BBL profiles are never touched · Qidi QD_* device ids are never touched · no preset is renamed or deleted</text>
</svg>
</div>
<div class="note">
<b>Why migration is safe</b> — verified in code: user presets <i>re-derive</i> filament_id from their parent on
every load <span class="ref">Preset.cpp:1658-1682</span>; 3mf projects resolve presets by <i>name + config</i>, not id
<span class="ref">Preset.cpp:2490-2576</span>; Klipper, Creality and Snapmaker derive tray ids at runtime from the
installed bundle. Fresh, never-reused ids on non-BBL system presets break nothing.
</div>
</section>
<!-- ============================ §1 ============================ -->
<section id="matching">
<div class="eyebrow"><span class="sn">§1</span> how matching works</div>
<h2>Every ecosystem funnels through this one id</h2>
<p class="lede">Not just Bambu. Five device ecosystems put a filament_id into the same pipeline; the slicer then
picks the single preset that matches the id <i>and</i> is compatible with the active printer.</p>
<div class="panel dark">
<span class="tag">runtime — device → preset</span>
<svg viewBox="0 0 900 330" role="img" aria-label="Pipeline: five device ecosystems produce tray_info_idx, which flows through DevAmsTray and filament_ams_list into a find_if match against compatible presets.">
<defs>
<marker id="ard" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#5E7A6C"/>
</marker>
</defs>
<!-- sources -->
<g font-size="11.5">
<rect x="12" y="14" width="163" height="44" rx="7" fill="#26352E" stroke="#B45309"/>
<text x="22" y="32" fill="#F0B463" font-weight="700">BBL AMS</text>
<text x="22" y="47" fill="#AFC4B8" font-size="10">from device RFID — frozen</text>
<rect x="187" y="14" width="163" height="44" rx="7" fill="#26352E" stroke="#B45309"/>
<text x="197" y="32" fill="#F0B463" font-weight="700">Qidi box</text>
<text x="197" y="47" fill="#AFC4B8" font-size="10">QD_&lt;series&gt;_&lt;v&gt;_&lt;t&gt; — frozen</text>
<rect x="362" y="14" width="163" height="44" rx="7" fill="#26352E" stroke="#00A873"/>
<text x="372" y="32" fill="#6FDCB4" font-weight="700">Creality CFS</text>
<text x="372" y="47" fill="#AFC4B8" font-size="10">scored at runtime — safe</text>
<rect x="537" y="14" width="163" height="44" rx="7" fill="#26352E" stroke="#00A873"/>
<text x="547" y="32" fill="#6FDCB4" font-weight="700">Klipper AFC/HH</text>
<text x="547" y="47" fill="#AFC4B8" font-size="10">by filament type — safe</text>
<rect x="712" y="14" width="163" height="44" rx="7" fill="#26352E" stroke="#00A873"/>
<text x="722" y="32" fill="#6FDCB4" font-weight="700">Snapmaker</text>
<text x="722" y="47" fill="#AFC4B8" font-size="10">color/type match — safe</text>
</g>
<g stroke="#5E7A6C" stroke-width="1.3" marker-end="url(#ard)">
<line x1="93" y1="58" x2="360" y2="102"/>
<line x1="268" y1="58" x2="395" y2="102"/>
<line x1="443" y1="58" x2="443" y2="102"/>
<line x1="618" y1="58" x2="495" y2="102"/>
<line x1="793" y1="58" x2="530" y2="102"/>
</g>
<!-- pipeline -->
<g font-size="12">
<rect x="330" y="104" width="230" height="30" rx="6" fill="#1C2B25" stroke="#3E584B"/>
<text x="445" y="124" text-anchor="middle" fill="#D9E5DE">tray_info_idx (MQTT / agent)</text>
<rect x="330" y="152" width="230" height="30" rx="6" fill="#1C2B25" stroke="#3E584B"/>
<text x="445" y="172" text-anchor="middle" fill="#D9E5DE">filament_ams_list["filament_id"]</text>
<rect x="180" y="204" width="530" height="34" rx="6" fill="#10281D" stroke="#00A873"/>
<text x="445" y="226" text-anchor="middle" fill="#6FDCB4">find_if( f.is_compatible &amp;&amp; base(f)==f &amp;&amp; f.filament_id == id )</text>
</g>
<g stroke="#5E7A6C" stroke-width="1.3" marker-end="url(#ard)">
<line x1="445" y1="134" x2="445" y2="148"/>
<line x1="445" y1="182" x2="445" y2="200"/>
<line x1="445" y1="238" x2="445" y2="258"/>
</g>
<g font-size="11">
<text x="575" y="124" fill="#5E7A6C">Plater.cpp:3440</text>
<text x="720" y="226" fill="#5E7A6C">PresetBundle.cpp:3132/3233</text>
</g>
<rect x="330" y="262" width="230" height="30" rx="6" fill="#1C2B25" stroke="#6FDCB4"/>
<text x="445" y="282" font-size="12" text-anchor="middle" fill="#D9E5DE">→ the ONE matching preset</text>
<text x="445" y="316" font-size="11.5" text-anchor="middle" fill="#F08A73">two matches? find_if silently returns whichever sorts first — no log, no warning; the AMS edit dialog even hides the second preset</text>
</svg>
</div>
<h3>The invariant, drawn</h3>
<div class="panel plain">
<span class="tag">per printer × per id</span>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(330px,1fr));gap:22px">
<div>
<p class="tight" style="font-size:13px"><b style="color:var(--teal-ink)">✓ Correct — Bambu PLA Basic, id <span class="id ok">GFA00</span></b></p>
<table style="font-size:12.5px">
<tr><th>printer preset</th><th>compatible presets with GFA00</th></tr>
<tr><td>X1C</td><td><span class="chip">…@BBL X1C <span class="cid g">1</span></span></td></tr>
<tr><td>X1C 0.2 nozzle</td><td><span class="chip">…@BBL X1C 0.2 nozzle <span class="cid g">1</span></span></td></tr>
<tr><td>P1P</td><td><span class="chip">…@BBL P1P <span class="cid g">1</span></span></td></tr>
</table>
<p class="cap">Variants share the id; their <code>compatible_printers</code> are disjoint. Each printer sees the id exactly once.</p>
</div>
<div>
<p class="tight" style="font-size:13px"><b style="color:var(--red)">✗ Broken — Qidi, id <span class="id bad">GFB99</span> (real case)</b></p>
<table style="font-size:12.5px">
<tr><th>printer preset</th><th>compatible presets with GFB99</th></tr>
<tr><td>X-Max&nbsp;3 0.4 nozzle</td><td>Bambu&nbsp;ABS · HATCHBOX&nbsp;ABS · Overture&nbsp;ABS · PolyLite&nbsp;ABS · QIDI&nbsp;ABS&nbsp;Rapido · QIDI&nbsp;ABS-GF · QIDI&nbsp;ASA · ASA-Aero · PC-ABS-FR · Generic&nbsp;ABS … <span class="cid r">16</span></td></tr>
</table>
<p class="cap">Sixteen different materials answer to one id on one printer. A spool tagged “generic ABS” matches
<b>whichever preset sorts first</b> — and <code>GFB99</code> was stamped into <b>317 Qidi files</b>.</p>
</div>
</div>
</div>
<h3>Two consumers you can't see from the profiles</h3>
<p>Most matching is printer-scoped, but several code paths match <b>globally, by id alone</b> — tray display name,
<code>temperature_vitrification</code> warnings, calibration history, and the multi-nozzle grouping key
<span class="ref">FilamentGroup.cpp:513</span>. So two <i>different materials</i> must never share an id even across
vendors: the first preset in the whole installed collection supplies the name, type and temperature data.</p>
<div class="note warn"><b>Two identity systems.</b> The library (OFL) hides its global presets per printer by
<b>alias</b> (name before <span class="id">&nbsp;@</span>) — <span class="ref">Preset.cpp:3684</span> — while devices match by
<b>filament_id</b>. When a vendor tunes <span class="id">Generic PLA</span> but renames it
<span class="id bad">Flashforge PLA Basic</span>, the alias no longer matches, shadowing fails, and both presets are
visible with the same id. The rule in §3 aligns the two systems: <b>one family = one alias = one id</b>.</div>
</section>
<!-- ============================ §2 ============================ -->
<section id="landscape">
<div class="eyebrow"><span class="sn">§2</span> the landscape today</div>
<h2>Seven schemes, zero rules, one epidemic</h2>
<p class="lede">The only guidance that ever existed was “≤ 8 characters”, checked for BBL only. Every other
convention was one contributor's invention, merged without comment, then imitated.</p>
<div class="panel plain">
<span class="tag">who uses which id shape — 5,866 instantiated presets</span>
<svg viewBox="0 0 880 235" role="img" aria-label="Bar chart of id shapes across all instantiated presets.">
<g font-size="12">
<text x="0" y="30" fill="var(--ink)">GF&lt;letter&gt;&lt;NN&gt; (Bambu classic, mass-copied)</text>
<rect x="8" y="38" width="560" height="15" rx="3" fill="#D97706" opacity=".85"/><text x="576" y="50" fill="var(--muted)">2,921</text>
<text x="0" y="76" fill="var(--ink)">free-form ("GFPLA Silk", "LHF_pla", 36-char names…)</text>
<rect x="8" y="84" width="430" height="15" rx="3" fill="#BE3A25" opacity=".8"/><text x="446" y="96" fill="var(--muted)">2,241</text>
<text x="0" y="122" fill="var(--ink)">GF&lt;BRAND&gt;&lt;NNN&gt; (Bambu partner ids)</text>
<rect x="8" y="130" width="76" height="15" rx="3" fill="#D97706" opacity=".55"/><text x="92" y="142" fill="var(--muted)">395</text>
<text x="0" y="168" fill="var(--ink)">O-prefixed (OGF* — OrcaFilamentLibrary mirrors)</text>
<rect x="8" y="176" width="41" height="15" rx="3" fill="#008A5E" opacity=".7"/><text x="57" y="188" fill="var(--muted)">211</text>
<text x="0" y="214" fill="var(--ink)">GF&lt;x&gt;##_## (Afinia/Tiertime suffix) + other GF-shaped</text>
<rect x="8" y="222" width="19" height="15" rx="3" fill="#BE3A25" opacity=".55"/><text x="35" y="234" fill="var(--muted)">43 + 55</text>
</g>
</svg>
<div class="legend">
<span><i class="dotk" style="background:#D97706"></i>Bambu's space, used outside Bambu</span>
<span><i class="dotk" style="background:#BE3A25"></i>invented, unowned</span>
<span><i class="dotk" style="background:#008A5E"></i>deliberate Orca convention</span>
</div>
</div>
<h3>How it got here</h3>
<div class="panel plain">
<span class="tag">convention timeline</span>
<svg viewBox="0 0 880 195" role="img" aria-label="Timeline of filament id conventions from 2025 to 2026.">
<line x1="30" y1="98" x2="855" y2="98" stroke="var(--line)" stroke-width="2"/>
<g stroke="var(--line)" stroke-width="1.2">
<line x1="205" y1="90" x2="205" y2="76"/>
<line x1="475" y1="90" x2="475" y2="76"/>
<line x1="745" y1="90" x2="745" y2="76"/>
</g>
<g font-size="11">
<circle cx="70" cy="98" r="5" fill="var(--teal)"/>
<text x="70" y="124" text-anchor="middle" font-weight="700" fill="var(--ink)">2025-01</text>
<text x="70" y="141" text-anchor="middle" fill="var(--muted)">OrcaFilamentLibrary</text>
<text x="70" y="155" text-anchor="middle" fill="var(--faint)">created</text>
<circle cx="205" cy="98" r="5" fill="var(--teal)"/>
<text x="205" y="40" text-anchor="middle" font-weight="700" fill="var(--ink)">2025-03</text>
<text x="205" y="57" text-anchor="middle" fill="var(--muted)">OGF* prefix born</text>
<text x="205" y="71" text-anchor="middle" fill="var(--faint)">direct commit, no rationale</text>
<circle cx="340" cy="98" r="5" fill="var(--amber)"/>
<text x="340" y="124" text-anchor="middle" font-weight="700" fill="var(--ink)">2025-05</text>
<text x="340" y="141" text-anchor="middle" fill="var(--muted)">“≤ 8 chars” check</text>
<text x="340" y="155" text-anchor="middle" fill="var(--faint)">BBL-only · PR #9574</text>
<circle cx="475" cy="98" r="5" fill="var(--red)"/>
<text x="475" y="40" text-anchor="middle" font-weight="700" fill="var(--ink)">2025-06</text>
<text x="475" y="57" text-anchor="middle" fill="var(--muted)">_## suffix invented</text>
<text x="475" y="71" text-anchor="middle" fill="var(--faint)">PR #9739, imitated since</text>
<circle cx="610" cy="98" r="5" fill="var(--red)"/>
<text x="610" y="124" text-anchor="middle" font-weight="700" fill="var(--ink)">2025-11</text>
<text x="610" y="141" text-anchor="middle" fill="var(--muted)">profile wiki removed</text>
<text x="610" y="155" text-anchor="middle" fill="var(--faint)">its example taught copy-paste</text>
<circle cx="745" cy="98" r="5" fill="var(--teal)"/>
<text x="745" y="40" text-anchor="middle" font-weight="700" fill="var(--ink)">2026-06</text>
<text x="745" y="57" text-anchor="middle" fill="var(--muted)">validator -f · BBL → 0</text>
<text x="745" y="71" text-anchor="middle" fill="var(--faint)">PR #14459</text>
<circle cx="848" cy="98" r="6" fill="none" stroke="var(--teal)" stroke-width="2.5"/>
<text x="846" y="124" text-anchor="middle" font-weight="700" fill="var(--teal-ink)">now</text>
<text x="846" y="141" text-anchor="middle" fill="var(--teal-ink)">this plan</text>
</g>
</svg>
</div>
<h3>Beyond the validator's reach</h3>
<p>The <code>-f</code> check compares a printer only against its own vendor's filaments. Three further ledgers exist:</p>
<div class="stats">
<div class="stat amber"><div class="v">16</div><div class="k">OFL-internal ids covering several materials — visible on <b>every</b> printer (e.g. <span class="id bad">OEPLAB00</span> = 14 Elegoo PLA products)</div></div>
<div class="stat amber"><div class="v">42</div><div class="k">OFL×vendor same-id pairs — 32 already neutralized by alias shadowing, <b>10 live</b></div></div>
<div class="stat amber"><div class="v">67</div><div class="k">ids meaning <b>different materials</b> in different vendors (<span class="id bad">GFU99</span> also covers a PEBA; Anycubic's <span class="id bad">GFL95</span> “Matte” ≠ Bambu's GFL95 “High Speed”)</div></div>
</div>
</section>
<!-- ============================ §3 ============================ -->
<section id="rule">
<div class="eyebrow"><span class="sn">§3</span> the rule</div>
<h2>Nobody invents ids. Structure carries the rest.</h2>
<p class="lede">Chosen by a 3-design → 2-judge → 3-attacker adversarial process. Deterministic minting won on
ambiguity-prevention, contributor simplicity and enforceability; every “breaks” finding from the attack pass is
folded in below.</p>
<h3>The one-question test</h3>
<div class="panel plain">
<span class="tag">decision — same id or new id?</span>
<svg viewBox="0 0 880 300" role="img" aria-label="Decision tree: is this a different spool product? If yes, new family and minted id. If it is the same material tuned for another printer, join the existing family. If it is a tuned generic, join the OFL family.">
<defs>
<marker id="ar2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="var(--faint)"/>
</marker>
</defs>
<rect x="245" y="10" width="390" height="52" rx="9" fill="var(--panel)" stroke="var(--ink)" stroke-width="1.4"/>
<text x="440" y="32" font-size="13.5" font-weight="700" text-anchor="middle" fill="var(--ink)">Would a user call this a different spool product</text>
<text x="440" y="50" font-size="13.5" font-weight="700" text-anchor="middle" fill="var(--ink)">than anything already in the tree?</text>
<g stroke="var(--faint)" stroke-width="1.4" marker-end="url(#ar2)">
<path d="M330 62 L160 108" fill="none"/>
<path d="M440 62 L440 108" fill="none"/>
<path d="M550 62 L724 108" fill="none"/>
</g>
<g font-size="11.5" fill="var(--muted)">
<text x="170" y="92">YES — polymer, sub-brand,</text>
<text x="170" y="105">fiber-filled, 2nd diameter</text>
<text x="455" y="92">NO — same spool, tuned for</text>
<text x="455" y="105">another printer / nozzle</text>
<text x="660" y="92">it's a GENERIC</text>
<text x="660" y="105">material</text>
</g>
<rect x="20" y="112" width="280" height="118" rx="9" fill="var(--teal-bg)" stroke="#9ED4BC"/>
<text x="36" y="136" font-size="12.5" font-weight="700" fill="var(--teal-ink)">NEW FAMILY</text>
<text x="36" y="157" font-size="12">create «Family @base», not instantiated</text>
<text x="36" y="175" font-size="12">write NO filament_id anywhere</text>
<text x="36" y="193" font-size="12">run the mint script — or paste the id</text>
<text x="36" y="211" font-size="12">that CI prints for you</text>
<rect x="320" y="112" width="240" height="118" rx="9" fill="var(--panel)" stroke="var(--line)"/>
<text x="336" y="136" font-size="12.5" font-weight="700" fill="var(--ink)">JOIN THE FAMILY</text>
<text x="336" y="157" font-size="12">inherit the existing @base</text>
<text x="336" y="175" font-size="12">never write the filament_id key</text>
<text x="336" y="193" font-size="12">keep compatible_printers disjoint</text>
<text x="336" y="211" font-size="12">from the other variants</text>
<rect x="580" y="112" width="288" height="118" rx="9" fill="var(--blue-bg)" stroke="#B5CCE2"/>
<text x="596" y="136" font-size="12.5" font-weight="700" fill="var(--blue)">JOIN THE OFL FAMILY</text>
<text x="596" y="157" font-size="12">inherit «Generic X @System»</text>
<text x="596" y="175" font-size="12">KEEP the «Generic X» name/alias</text>
<text x="596" y="193" font-size="12">(alias shadowing then hides the OFL</text>
<text x="596" y="211" font-size="12">preset on your printers) · no id key</text>
<text x="440" y="252" font-size="12" text-anchor="middle" fill="var(--muted)">color → never a new id · “high-speed” for a different printer → same family</text>
<text x="440" y="270" font-size="12" text-anchor="middle" fill="var(--muted)">“high-speed” selectable alongside the normal preset on one printer → new family</text>
<text x="440" y="288" font-size="12" text-anchor="middle" fill="var(--muted)">second selectable diameter on the same printer → sibling family with its own id</text>
</svg>
</div>
<h3>Minted ids — the setting_id precedent, applied to families</h3>
<div class="panel plain">
<span class="tag">id anatomy — the new namespace</span>
<svg class="anat2" viewBox="0 0 880 235" role="img" aria-label="Anatomy of a minted id: OF prefix plus six base62 characters derived from uuid5 of vendor and family name.">
<g font-size="56" font-weight="700">
<text x="240" y="66" fill="var(--teal-ink)">OF</text>
<text x="330" y="66" fill="var(--ink)">q3xT9k</text>
</g>
<g stroke="var(--faint)" stroke-width="1.2" fill="none">
<path d="M242 80 v10 h64 v-10"/>
<path d="M332 80 v10 h188 v-10"/>
<path d="M274 90 v8 L160 116" stroke-dasharray="3 4"/>
<path d="M426 90 v10 L440 122" stroke-dasharray="3 4"/>
</g>
<g font-size="12.5">
<text x="40" y="134" font-weight="700" fill="var(--teal-ink)">OF — Orca Family namespace</text>
<text x="40" y="151" fill="var(--muted)">disjoint from GF · QD_ · P· at two chars</text>
<text x="330" y="140" font-weight="700" fill="var(--ink)">base62_6( uuid5( NS, "filament_family/&lt;Vendor&gt;/&lt;Family&gt;" ) )</text>
<text x="330" y="157" fill="var(--muted)">e.g. filament_family/Elegoo/Elegoo PLA Matte → OFq3xT9k</text>
<text x="330" y="173" fill="var(--muted)">same derivation as setting_id · 8 chars total (AMS limit)</text>
</g>
<g font-size="12">
<text x="20" y="205" fill="var(--faint)">same input → same id:</text>
<text x="185" y="205" fill="var(--ink)">no “next number” races · a fork mints the id upstream expects · CI recomputes and verifies</text>
<text x="20" y="224" fill="var(--faint)">renamed family?</text>
<text x="185" y="224" fill="var(--ink)">the id does not change — immutable once shipped; renamed_from covers the name</text>
</g>
</svg>
</div>
<p>Author workflow is two lines: commit the family <b>with no id anywhere</b>; run
<code>python scripts/assign_filament_ids.py</code> (or read the id CI prints in its failure message and paste it).
A <code>--mint "&lt;Vendor&gt;/&lt;Family&gt;"</code> one-shot prints an id without touching the tree.</p>
<h3>Structure: the id lives in exactly one place</h3>
<div class="panel plain">
<span class="tag">family shape</span>
<svg viewBox="0 0 880 282" role="img" aria-label="Family structure: a base preset owns the id; variants inherit it and never redefine it; their compatible printers are disjoint.">
<rect x="290" y="12" width="300" height="58" rx="9" fill="var(--teal-bg)" stroke="var(--teal)" stroke-width="1.5"/>
<text x="440" y="35" font-size="13" font-weight="700" text-anchor="middle" fill="var(--teal-ink)">Elegoo PLA Matte @base</text>
<text x="440" y="55" font-size="12" text-anchor="middle" fill="var(--ink)">instantiation:false · filament_id: "OFq3xT9k" ← only here</text>
<g stroke="var(--faint)" stroke-width="1.3">
<path d="M440 70 v18 M200 118 h480 M200 118 v14 M440 88 v44 M680 118 v14" fill="none"/>
</g>
<g font-size="12">
<rect x="90" y="134" width="220" height="72" rx="8" fill="var(--panel)" stroke="var(--line)"/>
<text x="200" y="156" text-anchor="middle" font-weight="600" fill="var(--ink)">…@Centauri Carbon</text>
<text x="200" y="174" text-anchor="middle" fill="var(--muted)">inherits @base · no id key</text>
<text x="200" y="192" text-anchor="middle" fill="var(--blue)">compat: {CC 0.4}</text>
<rect x="330" y="134" width="220" height="72" rx="8" fill="var(--panel)" stroke="var(--line)"/>
<text x="440" y="156" text-anchor="middle" font-weight="600" fill="var(--ink)">…@CC 0.2 nozzle</text>
<text x="440" y="174" text-anchor="middle" fill="var(--muted)">inherits @base · no id key</text>
<text x="440" y="192" text-anchor="middle" fill="var(--blue)">compat: {CC 0.2}</text>
<rect x="570" y="134" width="220" height="72" rx="8" fill="var(--panel)" stroke="var(--line)"/>
<text x="680" y="156" text-anchor="middle" font-weight="600" fill="var(--ink)">…@Neptune 4</text>
<text x="680" y="174" text-anchor="middle" fill="var(--muted)">inherits @base · no id key</text>
<text x="680" y="192" text-anchor="middle" fill="var(--blue)">compat: {N4, N4 Pro}</text>
</g>
<text x="440" y="238" font-size="12" text-anchor="middle" fill="var(--muted)">disjoint compatible_printers = the PR #14459 invariant, by construction</text>
<text x="440" y="256" font-size="12" text-anchor="middle" fill="var(--muted)">family identity is DECLARED — an optional "filament_family" key on the root overrides name derivation</text>
<text x="440" y="274" font-size="12" text-anchor="middle" fill="var(--faint)">multi-root families are legal (Qidi's per-series bases) — every root must declare the identical id</text>
</svg>
</div>
<h3>Reserved namespaces — never mint or hand-write into</h3>
<div class="panel plain">
<span class="tag">the id space, partitioned</span>
<svg viewBox="0 0 880 150" role="img" aria-label="Partition of the id namespace into frozen, grandfathered and open regions.">
<defs>
<pattern id="hatch" width="7" height="7" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">
<rect width="7" height="7" fill="var(--amber-bg)"/><line x1="0" y1="0" x2="0" y2="7" stroke="#DCA95E" stroke-width="1.6"/>
</pattern>
</defs>
<g font-size="12">
<rect x="10" y="26" width="200" height="52" rx="7" fill="url(#hatch)" stroke="var(--amber)"/>
<text x="110" y="48" text-anchor="middle" font-weight="700" fill="var(--amber)">GF*</text>
<text x="110" y="66" text-anchor="middle" fill="var(--ink)">Bambu AMS / RFID — frozen</text>
<rect x="220" y="26" width="130" height="52" rx="7" fill="url(#hatch)" stroke="var(--amber)"/>
<text x="285" y="48" text-anchor="middle" font-weight="700" fill="var(--amber)">QD_*</text>
<text x="285" y="66" text-anchor="middle" fill="var(--ink)">Qidi device — frozen</text>
<rect x="360" y="26" width="160" height="52" rx="7" fill="#EFEFEF" stroke="var(--faint)"/>
<text x="440" y="48" text-anchor="middle" font-weight="700" font-size="10.5" fill="var(--muted)">P[0-9A-Fa-f]{7} · "null"</text>
<text x="440" y="66" text-anchor="middle" fill="var(--ink)">user-custom presets</text>
<rect x="530" y="26" width="160" height="52" rx="7" fill="var(--panel)" stroke="var(--line)" stroke-dasharray="5 4"/>
<text x="610" y="48" text-anchor="middle" font-weight="700" fill="var(--muted)">OGF* + legacy shapes</text>
<text x="610" y="66" text-anchor="middle" fill="var(--ink)">grandfathered, closed</text>
<rect x="700" y="26" width="170" height="52" rx="7" fill="var(--teal-bg)" stroke="var(--teal)" stroke-width="1.6"/>
<text x="785" y="48" text-anchor="middle" font-weight="700" fill="var(--teal-ink)">OF[0-9A-Za-z]{6}</text>
<text x="785" y="66" text-anchor="middle" fill="var(--ink)">open — minted only</text>
</g>
<text x="10" y="112" font-size="11.5" fill="var(--muted)">Byte-copies of authentic Bambu ids stay legal where the checked-in <tspan font-weight="700">shared_catalog</tspan> sanctions the family (BBL, OFL, Fiberon).</text>
<text x="10" y="132" font-size="11.5" fill="var(--muted)">Everything already shipped and unambiguous is snapshot-frozen exactly as-is — the migration touches only colliding families.</text>
</svg>
</div>
<div class="note"><b>Accepted trade-off:</b> minted ids are opaque — <span class="id ok">OFq3xT9k</span> doesn't say “PLA”.
The family name sits in the same file; in exchange there is no registry, no “next free number” ceremony, no PR races,
and forks mint the same id upstream expects. The mnemonic registry-grammar runner-up is preserved in §8 if you prefer it —
everything else in this plan works under either format.</div>
</section>
<!-- ============================ §4 ============================ -->
<section id="taxonomy">
<div class="eyebrow"><span class="sn">§4</span> error taxonomy — goal 2</div>
<h2>Eleven patterns explain all 356 groups</h2>
<p class="lede">Classified group-by-group against the actual profile files, then independently spot-checked
(1 substantive disagreement in 14 samples, corrected). The task's two known patterns cover about half; the rest
are new findings.</p>
<div class="panel plain">
<span class="tag">what's actually wrong — classified groups</span>
<svg viewBox="0 0 880 214" role="img" aria-label="Bar chart of error patterns by group count.">
<g font-size="12">
<text x="0" y="26" fill="var(--ink)">P1 copy_paste_id — different materials share an id verbatim</text>
<rect x="8" y="33" width="640" height="14" rx="3" fill="var(--red)" opacity=".8"/><text x="656" y="45" fill="var(--muted)">167</text>
<text x="0" y="66" fill="var(--ink)">P2 wrong_inherits / id-less product line</text>
<rect x="8" y="73" width="333" height="14" rx="3" fill="var(--red)" opacity=".6"/><text x="349" y="85" fill="var(--muted)">~87</text>
<text x="0" y="106" fill="var(--ink)">P3 generic_family_overlap — branded preset rides a generic id</text>
<rect x="8" y="113" width="261" height="14" rx="3" fill="var(--amber)" opacity=".8"/><text x="277" y="125" fill="var(--muted)">68</text>
<text x="0" y="146" fill="var(--ink)">P4 overclaim_compat — broader variant claims a dedicated printer</text>
<rect x="8" y="153" width="92" height="14" rx="3" fill="var(--amber)" opacity=".55"/><text x="108" y="165" fill="var(--muted)">24</text>
<text x="0" y="186" fill="var(--ink)">other — deliberate coexistence, hygiene, one true duplicate</text>
<rect x="8" y="193" width="80" height="14" rx="3" fill="var(--faint)" opacity=".6"/><text x="96" y="205" fill="var(--muted)">21</text>
</g>
</svg>
<p class="cap">Fixes across all groups: <b>319 × mint a fresh id</b> · 21 × trim <code>compatible_printers</code> ·
21 × split compat-or-id · 5 × re-point <code>inherits</code> · 1 × merge. Which preset keeps a contested id follows a
mechanical precedence: <b>Bambu-catalog material → OFL generic family → historical first owner</b>.</p>
</div>
<div class="cards">
<div class="card"><div class="hd"><span class="pn">P1</span><span class="nm">copy_paste_id</span><span class="ct">~167</span></div>
<p>The id came along when a profile was copied. Qidi stamped <span class="id bad">GFB99</span> into 317 files;
Peopoly put PLA-Silk's id on ABS.</p>
<div class="fx"><b>fix</b> — impostor families get minted ids on their roots; the owner keeps the id.</div></div>
<div class="card"><div class="hd"><span class="pn">P2</span><span class="nm">wrong_inherits / no root</span><span class="ct">~87</span></div>
<p>A product line never got its own <code>@base</code> — every variant inherits another family's id.
Flashforge's umbrella collapses ~140 presets into one id.</p>
<div class="fx"><b>fix</b> — create per-family roots with minted ids; re-point <code>inherits</code>. Never delete the old
shadow files — convert them (they carry real config).</div></div>
<div class="card"><div class="hd"><span class="pn">P3</span><span class="nm">generic_family_overlap</span><span class="ct">~68</span></div>
<p>A vendor-<i>branded</i> filament rides a generic family id via inheritance, colliding with the true generic
on the same printer.</p>
<div class="fx"><b>fix</b> — branded → minted id. True generic tunings instead join the OFL family with matching alias.
Sovol is the elegant case: just delete its wrong id lines and inherit OFL's.</div></div>
<div class="card"><div class="hd"><span class="pn">P4</span><span class="nm">overclaim_compat</span><span class="ct">~24</span></div>
<p>Same material; the broad variant claims a printer that a dedicated variant covers — the BBL H2DP pattern
from PR #14459.</p>
<div class="fx"><b>fix</b> — trim the broader preset's <code>compatible_printers</code>. Ids untouched.</div></div>
<div class="card"><div class="hd"><span class="pn">P5</span><span class="nm">template-carried id</span><span class="ct dim">in P1/P2</span></div>
<p>The id sits on a shared settings template (<span class="id">fdm_filament_*</span>), so every family that inherits
the template collapses onto it (Prusa, Ginger Additive).</p>
<div class="fx"><b>fix</b> — move ids off templates onto family roots.</div></div>
<div class="card"><div class="hd"><span class="pn">P6</span><span class="nm">OFL-internal collision</span><span class="ct">16 ids</span></div>
<p>One OFL id spans several materials — and OFL presets are visible on <b>every</b> printer.
<span class="id bad">OGFL06</span> = eSUN PLA-Marble <i>and</i> Fiberon PETG-ESD.</p>
<div class="fx"><b>fix</b> — fix OFL first: it's the base bundle every vendor resolves against.</div></div>
<div class="card"><div class="hd"><span class="pn">P7</span><span class="nm">alias-mismatch re-exposure</span><span class="ct">10 live</span></div>
<p>A vendor tunes a generic but renames it; alias shadowing fails and the OFL preset resurfaces beside it
(Snapmaker <span class="id">PolyTerra J1 PLA</span>).</p>
<div class="fx"><b>fix</b> — rename-to-alias where it's genuinely the same family, else mint.</div></div>
<div class="card"><div class="hd"><span class="pn">P8</span><span class="nm">per-variant ids</span><span class="ct dim">~210 presets</span></div>
<p>The opposite failure: every nozzle variant has its <i>own</i> id (Prusa, SeeMeCNC, Afinia <span class="id legacy">_##</span>).
No ambiguity — but devices can't recognize the material across nozzles.</p>
<div class="fx"><b>fix</b> — grandfather (they're unambiguous); converge opportunistically; document as anti-pattern.</div></div>
<div class="card"><div class="hd"><span class="pn">P9</span><span class="nm">format violations</span><span class="ct dim">hygiene</span></div>
<p>Ids with spaces (<span class="id bad">"GFPLA Silk"</span>), 11-char ids, invented GF-shaped ids colliding with
Bambu's real allocations.</p>
<div class="fx"><b>fix</b> — disappears as a byproduct of re-minting; the unambiguous rest is snapshot-frozen.</div></div>
<div class="card"><div class="hd"><span class="pn">P10</span><span class="nm">cross-vendor semantic</span><span class="ct">67 ids</span></div>
<p>Same id, different material, different vendors — feeds wrong name/type/temperature data to the globally-matching
consumers (§1).</p>
<div class="fx"><b>fix</b> — mostly eliminated by P1P3 re-minting; benign same-material generic sharing is legalized
by the snapshot.</div></div>
<div class="card"><div class="hd"><span class="pn">P11</span><span class="nm">deliberate coexistence</span><span class="ct dim">4</span></div>
<p>Snapmaker “Benchy” demo presets coexist on purpose (gated by <code>compatible_prints</code>, which the id check
can't see); one preset lists the same printer three times.</p>
<div class="fx"><b>fix</b> — mint ids for demo presets; de-duplicate list entries; add a lint.</div></div>
</div>
</section>
<!-- ============================ §5 ============================ -->
<section id="migration">
<div class="eyebrow"><span class="sn">§5</span> migration plan</div>
<h2>Fresh ids only, family-atomic, names never change</h2>
<p class="lede">The persistence analysis makes one strategy provably safe — and everything else forbidden.</p>
<h3>What survives an id change</h3>
<table>
<tr><th>surface</th><th>behavior across releases</th><th>verdict</th></tr>
<tr><td>User presets that inherit a system preset</td><td>filament_id re-derived from the parent on <b>every load</b> <span class="ref">Preset.cpp:1658-1682</span></td><td><b style="color:var(--teal-ink)">safe</b></td></tr>
<tr><td>Saved 3mf projects</td><td>presets resolve by <b>name + config equality</b>, never by id <span class="ref">Preset.cpp:2490-2576</span></td><td><b style="color:var(--teal-ink)">safe</b></td></tr>
<tr><td>Klipper / Creality / Snapmaker sync</td><td>tray ids derived at runtime from the installed bundle</td><td><b style="color:var(--teal-ink)">safe</b></td></tr>
<tr><td>User <i>root</i> custom presets</td><td>copied a system id at creation and persist it forever — AMS auto-match falls back to generic-by-type</td><td><b style="color:var(--amber)">low residue</b></td></tr>
<tr><td>Qidi <span class="id legacy">QD_*</span> ids</td><td>encode the device protocol <span class="ref">QidiPrinterAgent.cpp:146</span> — an exact-match contract</td><td><b style="color:var(--red)">frozen</b></td></tr>
<tr><td>BBL <span class="id legacy">GF*</span> ids</td><td>live on RFID tags and printer-side records; hardcoded k-values in C++</td><td><b style="color:var(--red)">frozen</b></td></tr>
<tr><td>Recycling an old id for a different material</td><td>stale ids in user roots / old 3mfs would silently match the <b>wrong material</b></td><td><b style="color:var(--red)">forbidden, forever</b></td></tr>
<tr><td>Renaming / deleting preset names</td><td>user presets whose <code>inherits</code> no longer resolves are <b>dropped at load</b> <span class="ref">Preset.cpp:1687-1691</span></td><td><b style="color:var(--red)">forbidden</b> (use <code>renamed_from</code> if unavoidable)</td></tr>
</table>
<h3>Phases</h3>
<div class="phases">
<div class="phase"><div class="ml"><div class="dot">0</div><div class="bar"></div></div><div class="bd">
<h4>Tooling + snapshot — no profile changes</h4>
<p>Land <code>scripts/assign_filament_ids.py</code>, the extended CI checks in ratchet mode, the validator OFL
cross-check and the runtime warning. Generate <code>filament_id_snapshot.json</code> (id→families multimap over main)
and an empty <code>retired_ids.json</code>; both are checked in and append-only.</p>
</div></div>
<div class="phase"><div class="ml"><div class="dot">1</div><div class="bar"></div></div><div class="bd">
<h4>OrcaFilamentLibrary first</h4>
<p>OFL is the base bundle every vendor resolves against. Fix its 16 internal collisions and give
<span class="id">Generic PETG HF/PETG-CF/PP-CF/… @System</span> their own family roots instead of collapsing into their
parent generic. Unambiguous ids stay byte-identical.</p>
</div></div>
<div class="phase"><div class="ml"><div class="dot">2</div><div class="bar"></div></div><div class="bd">
<h4>Per-vendor PRs, worst first — each widens the CI ratchet</h4>
<p>Qidi → Flashforge → Elegoo → Prusa → Cubicon → Anycubic → InfiMech → Snapmaker → long tail (22 vendors,
mostly 18 one-line fixes). Each merged PR appends its vendor to the workflow's <code>-f -v</code> scope
(today: <code>-v BBL -f</code>); when all are in, drop <code>-v</code> and run tree-wide.</p>
</div></div>
<div class="phase"><div class="ml"><div class="dot">3</div><div class="bar"></div></div><div class="bd">
<h4>Delete the allowlists</h4>
<p>Once tree-wide zero holds, the ratchet allowlists disappear and every check becomes a hard rule for anything
born after the snapshot.</p>
</div></div>
</div>
<div class="panel plain">
<span class="tag">where the work is — collision groups per vendor</span>
<svg viewBox="0 0 880 330" role="img" aria-label="Horizontal bar chart of collision groups per vendor. Qidi 97, Flashforge 69, Elegoo 37, Prusa 31, Cubicon 17, Anycubic 14, InfiMech 14, Snapmaker 11, Artillery 8, Creality 8, FlyingBear 8, Sovol 6, plus 18 more vendors with 40 groups total.">
<g font-size="12" fill="var(--ink)">
<text x="88" y="26" text-anchor="end">Qidi</text><rect x="96" y="15" width="640" height="14" rx="3" fill="var(--red)" opacity=".85"/><text x="744" y="27" fill="var(--muted)">97</text>
<text x="88" y="50" text-anchor="end">Flashforge</text><rect x="96" y="39" width="455" height="14" rx="3" fill="var(--red)" opacity=".75"/><text x="559" y="51" fill="var(--muted)">69</text>
<text x="88" y="74" text-anchor="end">Elegoo</text><rect x="96" y="63" width="244" height="14" rx="3" fill="var(--amber)" opacity=".9"/><text x="348" y="75" fill="var(--muted)">37</text>
<text x="88" y="98" text-anchor="end">Prusa</text><rect x="96" y="87" width="205" height="14" rx="3" fill="var(--amber)" opacity=".85"/><text x="309" y="99" fill="var(--muted)">31</text>
<text x="88" y="122" text-anchor="end">Cubicon</text><rect x="96" y="111" width="112" height="14" rx="3" fill="var(--amber)" opacity=".7"/><text x="216" y="123" fill="var(--muted)">17</text>
<text x="88" y="146" text-anchor="end">Anycubic</text><rect x="96" y="135" width="92" height="14" rx="3" fill="var(--amber)" opacity=".65"/><text x="196" y="147" fill="var(--muted)">14</text>
<text x="88" y="170" text-anchor="end">InfiMech</text><rect x="96" y="159" width="92" height="14" rx="3" fill="var(--amber)" opacity=".65"/><text x="196" y="171" fill="var(--muted)">14</text>
<text x="88" y="194" text-anchor="end">Snapmaker</text><rect x="96" y="183" width="73" height="14" rx="3" fill="var(--amber)" opacity=".6"/><text x="177" y="195" fill="var(--muted)">11</text>
<text x="88" y="218" text-anchor="end">Artillery</text><rect x="96" y="207" width="53" height="14" rx="3" fill="var(--faint)" opacity=".8"/><text x="157" y="219" fill="var(--muted)">8</text>
<text x="88" y="242" text-anchor="end">Creality</text><rect x="96" y="231" width="53" height="14" rx="3" fill="var(--faint)" opacity=".8"/><text x="157" y="243" fill="var(--muted)">8</text>
<text x="88" y="266" text-anchor="end">FlyingBear</text><rect x="96" y="255" width="53" height="14" rx="3" fill="var(--faint)" opacity=".8"/><text x="157" y="267" fill="var(--muted)">8</text>
<text x="88" y="290" text-anchor="end">Sovol</text><rect x="96" y="279" width="40" height="14" rx="3" fill="var(--faint)" opacity=".8"/><text x="144" y="291" fill="var(--muted)">6</text>
<text x="88" y="314" text-anchor="end" fill="var(--muted)">18 more</text><rect x="96" y="303" width="264" height="14" rx="3" fill="var(--line)"/><text x="368" y="315" fill="var(--muted)">40 total (Volumic 6 · Ratrig 5 · Chuanying 4 · Dremel 3 · 4×2 · 10×1)</text>
</g>
</svg>
<p class="cap">Vendor observations show these are <b>systematic mistakes, not hundreds of independent bugs</b>: Qidi is one
blanket-stamp in three profile generations; Elegoo is a single inherit-the-class-base habit; Sovol is fixed by deleting
six wrong lines.</p>
</div>
<h3>The migration script's contract <span class="ref" style="font-weight:400">(hardened by the adversarial pass)</span></h3>
<table>
<tr><th>rule</th><th>why</th></tr>
<tr><td><b>Re-mint everything</b> — id literals in analysis notes are ignored; only group membership, fix category and keep-id precedence are consumed. Assert no emitted id matches <span class="id">^(GF|QD_|P[0-9A-Fa-f]{7}$)</span>.</td><td>~40% of classification notes suggested ids in the old invented-GF culture; CI would reject them.</td></tr>
<tr><td><b>Family-atomic</b> — re-idding any preset re-ids every same-family sibling in the same commit, even outside the collision group.</td><td>Otherwise a family splits across two ids (FlyingBear <span class="id legacy">GFB99</span> @S1 vs @Ghost7) — worse than the bug being fixed.</td></tr>
<tr><td><b>Bounded diff</b> — a vendor PR may only touch collision-group files + same-family siblings; every id that is unambiguous today stays byte-identical.</td><td>Makes each PR mechanically auditable.</td></tr>
<tr><td><b>Config-equivalence gate</b> — flattened effective config of every preset must be identical before/after, except the prescribed id/inherits/compat edits.</td><td>Makes structural conversions provably behavior-neutral (Flashforge's id-less shadow files become named family roots carrying their config byte-for-byte).</td></tr>
<tr><td><b>No deletions, no renames</b> — redundant twins are re-minted, not dropped; consolidation via <code>renamed_from</code> is a separate human-reviewed cleanup.</td><td>Deleting a name silently drops users' derived presets at load.</td></tr>
</table>
</section>
<!-- ============================ §6 ============================ -->
<section id="enforcement">
<div class="eyebrow"><span class="sn">§6</span> enforcement</div>
<h2>Ratchets, not absolutes</h2>
<p class="lede">The grandfathered landscape keeps today's benign sharing legal; the checks forbid anything <i>new</i>.
All checks import the same mint function — the setting_id precedent.</p>
<table>
<tr><th style="width:34px">#</th><th>check</th><th>where</th></tr>
<tr><td>1</td><td><b>Format</b> — valid iff <span class="id ok">OF[0-9A-Za-z]{6}</span> <i>or</i> in the snapshot <i>or</i> vendor==BBL <i>or</i> <span class="id legacy">QD_*</span> in Qidi. The legacy set is closed.</td><td rowspan="6"><code>orca_extra_profile_check.py</code><br><span class="ref">drop BBL-only gate :292, OFL skip :601</span></td></tr>
<tr><td>2</td><td><b>Uniqueness ratchet</b> — tree-wide id→families multimap; no id may gain a family claim not recorded in the snapshot (<code>shared_catalog</code> is the sanctioned exception).</td></tr>
<tr><td>3</td><td><b>Structure ratchet</b> — id key only on roots; every instantiated preset resolves an effective id (simulated loader walk); all family members resolve identically; <b>no preset may override its inherited id</b> (the <span class="id legacy">Generic SBS</span> drift class).</td></tr>
<tr><td>4</td><td><b>Mint conformance</b> — a new id must equal the mint (or a salt iteration); the failure message prints the expected value so hand-editors can paste it.</td></tr>
<tr><td>5</td><td><b>Stability</b> — an (id, family) pair on main may not change or vanish, <i>following <code>renamed_from</code> chains</i>, unless listed in a maintainer-gated migrations file. Retired ids go to an append-only ledger and are never redefined.</td></tr>
<tr><td>6</td><td><b>Alias hygiene</b> — a preset inheriting an OFL <span class="id">Generic&nbsp;* @System</span> (no own id in its chain) must keep the OFL base name and non-empty <code>compatible_printers</code>; the error names the rename as the cause.</td></tr>
<tr class="hl"><td>7</td><td><b>OFL cross-check</b> — extend <code>check_duplicate_filament_subtypes</code> to include OFL presets in every vendor's per-printer check, minus alias-excluded ones. <code>m_excluded_from</code> is already populated in validator context, so the 32 shadowed pairs won't false-positive and the 10 live ones are caught. Widen CI's <code>-f</code> per vendor.</td><td>C++ validator<br><span class="ref">PresetBundle.cpp:5654 · :2302</span></td></tr>
<tr class="hl"><td>8</td><td><b>Runtime backstop</b> — log a warning when the AMS-sync <code>find_if</code> sees 2+ compatible presets for one id. The only layer that can see side-loaded and forked bundles. One line.</td><td>runtime<br><span class="ref">PresetBundle.cpp:3132/3233</span></td></tr>
</table>
<p>Plus: the rule document ships in-repo (<span class="id">doc/developer-reference/filament_id.md</span>) so CI messages
have a stable link, and the profile-PR template gets one checkbox: <i>“new materials: no filament_id key anywhere —
CI prints the minted id.”</i></p>
</section>
<!-- ============================ §7 ============================ -->
<section id="evidence">
<div class="eyebrow"><span class="sn">§7</span> evidence</div>
<h2>How this was verified</h2>
<p class="lede">The analysis was run as a 52-agent pipeline over the real tree and real git history, with an
adversarial design phase — findings were attacked before being adopted.</p>
<div class="panel plain">
<span class="tag">methodology</span>
<svg viewBox="0 0 880 250" role="img" aria-label="Methodology flow: five code analysts and 38 classifiers feed three designs, judged by two judges, attacked by three adversaries, folded into the plan.">
<defs>
<marker id="ar3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="var(--faint)"/>
</marker>
</defs>
<g font-size="11.5">
<rect x="10" y="16" width="196" height="62" rx="8" fill="var(--blue-bg)" stroke="#B5CCE2"/>
<text x="22" y="36" font-weight="700" fill="var(--blue)">5 code analysts</text>
<text x="22" y="53" fill="var(--ink)">runtime · persistence · loader</text>
<text x="22" y="68" fill="var(--ink)">Bambu grammar · git history</text>
<rect x="10" y="94" width="196" height="62" rx="8" fill="var(--blue-bg)" stroke="#B5CCE2"/>
<text x="22" y="114" font-weight="700" fill="var(--blue)">38 classifiers</text>
<text x="22" y="131" fill="var(--ink)">every collision group, read</text>
<text x="22" y="146" fill="var(--ink)">from the actual profile files</text>
<rect x="10" y="172" width="196" height="62" rx="8" fill="var(--teal-bg)" stroke="#9ED4BC"/>
<text x="22" y="192" font-weight="700" fill="var(--teal-ink)">ground truth</text>
<text x="22" y="209" fill="var(--ink)">audit script = validator</text>
<text x="22" y="224" font-family="var(--mono)" fill="var(--teal-ink)">1256 = 1256 · exact</text>
<rect x="286" y="58" width="170" height="96" rx="8" fill="var(--panel)" stroke="var(--line)"/>
<text x="298" y="80" font-weight="700" fill="var(--ink)">3 designs</text>
<text x="298" y="99" fill="var(--muted)">deterministic mint</text>
<text x="298" y="115" fill="var(--muted)">registry grammar</text>
<text x="298" y="131" fill="var(--muted)">structure-only</text>
<rect x="516" y="58" width="150" height="96" rx="8" fill="var(--panel)" stroke="var(--line)"/>
<text x="528" y="80" font-weight="700" fill="var(--ink)">2 judges</text>
<text x="528" y="100" fill="var(--muted)">both chose the mint</text>
<text x="528" y="117" font-family="var(--mono)" fill="var(--muted)">58 / 50 / 56</text>
<text x="528" y="133" font-family="var(--mono)" fill="var(--muted)">60 / 54 / 56</text>
<rect x="726" y="30" width="144" height="152" rx="8" fill="var(--red-bg)" stroke="#EBB2A5"/>
<text x="738" y="52" font-weight="700" fill="var(--red)">3 attackers</text>
<text x="738" y="72" fill="var(--ink)">35 scenarios</text>
<text x="738" y="90" fill="var(--ink)">authors · runtime ·</text>
<text x="738" y="106" fill="var(--ink)">migration mechanics</text>
<text x="738" y="130" fill="var(--muted)">every “breaks” →</text>
<text x="738" y="146" fill="var(--muted)">an amendment §3§5</text>
<text x="738" y="170" fill="var(--muted)">spot-check: 1/14</text>
</g>
<g stroke="var(--faint)" stroke-width="1.4" marker-end="url(#ar3)">
<line x1="208" y1="60" x2="282" y2="88"/>
<line x1="208" y1="125" x2="282" y2="112"/>
<line x1="208" y1="196" x2="282" y2="136"/>
<line x1="458" y1="106" x2="512" y2="106"/>
<line x1="668" y1="106" x2="722" y2="106"/>
</g>
</svg>
<p class="cap">Amendments that came out of the attack pass: family identity <b>declared</b> on roots (not name-derived —
<span class="id legacy">Afinia PLA@HS</span> has no space before <code>@</code>); multi-root families legalized; checks 23 as
<b>ratchets</b>, not absolutes; the no-deletion rule; shadow-file <b>conversion</b> instead of deletion; classification id
literals quarantined; diameter siblings; case-insensitive reservation of the user id space.</p>
</div>
<details>
<summary>Key discoveries that reshaped the design (with sources)</summary>
<div>
<p>· The “26 Flashforge presets with no id” were a false alarm — the loader resolves them through the OFL base-bundle
fallback to <span class="id legacy">OGFL99/OGFG99</span> <span class="ref">PresetBundle.cpp:4904-4909</span>. The real bug is
~10 different Flashforge PLA products sharing one id.</p>
<p>· A missing id can never ship: it's a hard load error that discards the whole vendor bundle
<span class="ref">PresetBundle.cpp:5072, :5141</span>.</p>
<p>· Qidi's <span class="id legacy">QD_*</span> ids are a device-protocol contract, discovered in
<span class="ref">QidiPrinterAgent.cpp:146-152</span> — a second frozen namespace nobody had documented.</p>
<p>· User-custom ids occupy <span class="id">P + 7 hex</span> <span class="ref">CreatePresetsDialog.cpp:533</span>, and
<span class="id">"null"</span> is a sentinel — both reserved.</p>
<p>· OFL shadowing is keyed by <b>alias</b>, not id <span class="ref">Preset.cpp:3684-3714</span>, and is already active in
validator context <span class="ref">PresetBundle.cpp:2302</span> — which is what makes check #7 precise.</p>
</div>
</details>
</section>
<!-- ============================ §8 ============================ -->
<section id="decisions">
<div class="eyebrow"><span class="sn">§8</span> open decisions</div>
<h2>Five calls that are yours to make</h2>
<p class="lede">Everything else in the plan holds under any of these answers. <b></b> marks the recommendation.</p>
<div class="dec">
<div class="card"><div class="hd"><span class="pn">D1</span><span class="nm">id format</span></div>
<div class="opt rec"><span><b>Opaque deterministic mint</b> <span class="id ok">OFq3xT9k</span> — no registry, no races, fork-friendly; judges 2/2.</span></div>
<div class="opt alt"><span>Mnemonic registry grammar <span class="id">&lt;NS&gt;&lt;FAM&gt;&lt;SEQ&gt;</span> — readable ids, but someone allocates numbers forever.</span></div></div>
<div class="card"><div class="hd"><span class="pn">D2</span><span class="nm">multi-vendor brands</span></div>
<div class="opt rec"><span><b>shared_catalog list</b> — sanction authentic Bambu ids for byte-matching families (Snapmaker's Fiberon), low churn.</span></div>
<div class="opt alt"><span>Hoist those families into OFL — cleaner long-term, more restructuring now.</span></div></div>
<div class="card"><div class="hd"><span class="pn">D3</span><span class="nm">demo presets</span></div>
<div class="opt rec"><span><b>Mint ids for Benchy-style presets</b> — simple, keeps the validator strict.</span></div>
<div class="opt alt"><span>Teach the validator <code>compatible_prints</code> gating — more machinery for 3 presets.</span></div></div>
<div class="card"><div class="hd"><span class="pn">D4</span><span class="nm">the 10 live OFL duplicates</span></div>
<div class="opt rec"><span><b>Rename-to-alias where it's truly the same family</b> (with <code>renamed_from</code>) — restores shadowing.</span></div>
<div class="opt alt"><span>Mint vendor ids everywhere — safer mechanically, more ids in the world.</span></div></div>
<div class="card"><div class="hd"><span class="pn">D5</span><span class="nm">where the rule doc lives</span></div>
<div class="opt rec"><span><b>In-repo</b> <span class="id">doc/developer-reference/filament_id.md</span> — CI messages need a stable link.</span></div>
<div class="opt alt"><span>Wiki-only — discoverable, but drifts from the checks that enforce it.</span></div></div>
</div>
<div class="note" style="margin-top:26px">
<b>Suggested first step:</b> land phase 0 (tooling + snapshot, zero profile changes). It is entirely additive,
makes every later PR mechanically checkable, and turns the 356-group backlog into a shrinking allowlist that CI
reports on every profile PR.
</div>
</section>
</main>
<script>
(function(){
var links = document.querySelectorAll('nav.rail a[href^="#"]');
var map = {};
links.forEach(function(a){ map[a.getAttribute('href').slice(1)] = a; });
if (!('IntersectionObserver' in window)) return;
var current = null;
var obs = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting){
if (current) current.classList.remove('on');
current = map[e.target.id];
if (current) current.classList.add('on');
}
});
}, {rootMargin:'-20% 0px -70% 0px'});
document.querySelectorAll('main section, header.hero').forEach(function(s){
if (s.id && map[s.id]) obs.observe(s);
});
})();
</script>
</body>
</html>