v3.0: content-addressed mint tooling, succession runtime, W1 hardening

Implements phase v3.0 of filament_id_plan_v3.md - tooling and client
prerequisites. No filament_id values change in this commit.

Tooling (scripts/assign_filament_ids.py; 106 unit tests):
- The mint key becomes "filament_product/<filament_vendor>/<filament_type>/
  <family_name>", resolved loader-faithfully from the declarer's flattened
  config - bundle-independent and content-addressed; identity fixes re-id
  by design and are made safe by the succession ledger. --mint now takes
  the triple.
- The snapshot gains "triples" and "triple_exceptions" sections, folded
  into the equality gate: any vendor/type/name change surfaces as a
  reviewable snapshot diff. Check 3 rewritten to triple-mint conformance
  with (id, triple) grandfathering; check 5 generalized from OFL generics
  to every OFL-riding vendor preset; new check 8 (triple integrity) and
  check 9 (succession integrity).
- The retired ledger moves to resources/profiles/retired_filament_ids.json
  so it ships with the app; schema {claims, successor} plus cross-island
  "hints". The 14 v1 entries are migrated with mode-rule successors.
  Vanished ids in a foreign island's space (GF*/QD_*) are RELEASED with a
  hint instead of retired: the island catalog owns them and may
  legitimately (re)ship them, which check 4 must never block.
- New maintenance modes: --remint VENDOR, --drop-redundant-ids VENDOR,
  --add-hint "OLD=NEW", and --update-snapshot --forget-never-shipped FILE
  (never-shipped ids drop from lineage; chains splice through them).

Runtime (C++):
- Succession helpers in libslic3r/Preset (pure chain-follow + lazy ledger
  load from resources), consulted only on resolution miss in
  get_filament_by_filament_id, both AMS sync predicates,
  add_ams_filaments, setting_id_to_type, and the calibration-history
  lookup; behavior is byte-identical while the ledger has no matching
  entry. Catch2 coverage in tests/libslic3r.
- W1 hardening: check_ams_filament_valid no longer remote-wipes trays or
  rewrites temps for P-shaped ids that a system preset carries (ten such
  system ids ship today); the size==8 && [0]=='P' assert is relaxed; the
  unguarded filament_list find deref in the temp-equation check returns
  non-destructively on a miss.
- MoonrakerPrinterAgent: the 23 hardcoded OFL generic ids are replaced by
  a runtime lookup of "Generic <family> @System" (id-equivalent on
  today's shipped profiles, follows future re-mints automatically);
  scripts/test_moonraker_lane_data.py derives its expectations from the
  shipped profiles and gains --check-ofl-map.

Profile data (W3 - type is now a key component, so type bugs are fixed
before any re-mint):
- 17 same-name-different-type groups corrected across 50 files (OFL
  Generic PETG-CF/PE-CF/PP-CF; Flashforge ASA Basic/ASA-CF/ABS-CF/HIPS/
  PAHT-CF/PLA Silk; FusRock PAHT; Creality Generic PA6-CF; InfiMech PETG;
  Anycubic TPU 95A / TPU for ACE; Prusa Generic PA-CF/PLA-CF) - every
  value validated against MaterialType::all(); 3 Snapmaker U1 roots gain
  their missing filament_vendor. Flattened-config equivalence vs the
  previous commit: exactly the 54 intended diffs, zero BBL.
- doc/developer-reference/filament_id.md rewritten for the v3 rule.

Ambiguous type divergences (Prusa Generic TPU/TPU HF FLEX-vs-TPU, FusRock
S-Multi/S-PAHT) and all same-type vendor-tag divergences are deferred to
the v3.2 vendor worksheets, catalogued with analysis.
This commit is contained in:
SoftFever
2026-07-04 16:28:35 +08:00
parent 67406b24ba
commit 2c0867619c
67 changed files with 13311 additions and 330 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +0,0 @@
{
"retired": {
"GFABS": [
"Anycubic/Anycubic ABS"
],
"GFL92": [
"Anycubic/Anycubic PLA"
],
"GFL93": [
"Anycubic/Anycubic PLA+"
],
"GFPLA": [
"Anycubic/Anycubic PLA"
],
"GFPLA High Speed": [
"Anycubic/Anycubic PLA High Speed"
],
"GFPLA Matte": [
"Anycubic/Anycubic PLA Matte"
],
"GFPLA Silk": [
"Anycubic/Anycubic PLA Silk"
],
"GFPLA+": [
"Anycubic/Anycubic PLA+"
],
"P11851ba": [
"Artillery/Artillery PET"
],
"P284941e": [
"Artillery/Artillery PETG",
"Artillery/Artillery PETG Basic"
],
"P2fbf0c0": [
"Artillery/Artillery TPU"
],
"P39db358": [
"Artillery/Artillery ABS"
],
"P8163162": [
"Artillery/Artillery PVA"
],
"P87436f6": [
"Artillery/Artillery ASA"
]
}
}

View File

@@ -5,10 +5,15 @@ Inserts/deletes/modifies random lane data in Moonraker database,
then reads back and displays with colored output.
"""
import requests
try:
import requests
except ImportError:
requests = None # only needed for live-printer operations, not --check-ofl-map
import random
import argparse
import json
import os
import re
import time
import sys
@@ -16,6 +21,11 @@ import sys
DEFAULT_HOST = "192.168.88.9"
DEFAULT_PORT = 7125
NAMESPACE = "lane_data"
# Repo-relative paths for the offline generic-map check
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MOONRAKER_AGENT_CPP = os.path.join(REPO_ROOT, "src", "slic3r", "Utils", "MoonrakerPrinterAgent.cpp")
OFL_FILAMENT_DIR = os.path.join(REPO_ROOT, "resources", "profiles", "OrcaFilamentLibrary", "filament")
LANE_KEYS = [f"lane{i}" for i in range(1, 9)] # lane1-lane8
MATERIALS = ["PLA", "ABS", "PETG", "ASA", "ASA Sparkle", "TPU", ""]
@@ -30,6 +40,95 @@ MATERIAL_TEMPS = {
"": {"nozzle": None, "bed": None},
}
def parse_cpp_type_map():
"""Extract the normalized-type -> OFL generic family table from MoonrakerPrinterAgent.cpp.
Reads MoonrakerPrinterAgent::map_filament_type_to_generic_id's type_to_ofl_family
initializer so the check tracks the C++ normalization without a duplicated list.
"""
with open(MOONRAKER_AGENT_CPP, encoding="utf-8") as f:
src = f.read()
m = re.search(r"type_to_ofl_family\s*=\s*\{(.*?)\n\s*\};", src, re.DOTALL)
if not m:
raise RuntimeError(f"type_to_ofl_family table not found in {MOONRAKER_AGENT_CPP}")
pairs = re.findall(r'\{\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\}', m.group(1))
if not pairs:
raise RuntimeError("type_to_ofl_family table parsed empty")
return dict(pairs)
def load_ofl_presets():
"""Map preset name -> parsed JSON for every OrcaFilamentLibrary filament profile."""
presets = {}
for root, _dirs, files in os.walk(OFL_FILAMENT_DIR):
for fn in files:
if not fn.endswith(".json"):
continue
try:
with open(os.path.join(root, fn), encoding="utf-8") as f:
data = json.load(f)
except (OSError, ValueError):
continue
name = data.get("name")
if isinstance(name, str) and name:
presets[name] = data
return presets
def resolve_ofl_filament_id(presets, name):
"""Follow the inherits chain (within OFL) until a filament_id is declared."""
seen = set()
while name and name not in seen:
seen.add(name)
preset = presets.get(name)
if preset is None:
return None
fid = preset.get("filament_id")
if fid:
return fid
name = preset.get("inherits")
return None
def check_ofl_generic_map():
"""Assert every material type the C++ normalization handles resolves to a shipped
OrcaFilamentLibrary generic preset carrying a filament_id.
Expectations are derived from the shipped profiles, not pinned id literals, so the
check stays valid across filament_id re-mints.
"""
print("Checking C++ generic-type map against shipped OrcaFilamentLibrary presets...")
try:
type_map = parse_cpp_type_map()
except (OSError, RuntimeError) as e:
print(f" FAIL: {e}")
return False
presets = load_ofl_presets()
if not presets:
print(f" FAIL: no OFL filament profiles found under {OFL_FILAMENT_DIR}")
return False
errors = []
for family in sorted(set(type_map.values())):
preset_name = f"Generic {family} @System"
if preset_name not in presets:
errors.append(f"{preset_name}: no such OFL preset")
continue
if str(presets[preset_name].get("instantiation", "")).lower() != "true":
errors.append(f"{preset_name}: not instantiated — the C++ runtime lookup "
f"only sees presets loaded into the PresetBundle")
continue
fid = resolve_ofl_filament_id(presets, preset_name)
if not fid:
errors.append(f"{preset_name}: no filament_id resolvable through inherits")
continue
aliases = ", ".join(sorted(t for t, fam in type_map.items() if fam == family))
print(f" {fid:10s} {preset_name:32s} <- {aliases}")
if errors:
for e in errors:
print(f" FAIL: {e}")
print(f"OFL generic map check FAILED ({len(errors)} error(s))")
return False
print(f"OFL generic map check passed: {len(type_map)} type aliases, "
f"{len(set(type_map.values()))} OFL generic presets\n")
return True
def test_connection(host, port, api_key=None, verbose=False):
"""Test basic connectivity to Moonraker."""
url = f"http://{host}:{port}/server/info"
@@ -404,11 +503,25 @@ def main():
help="Only read and display current lane data")
parser.add_argument("--load", metavar="FILE",
help="Load lane data from JSON file and overwrite printer lanes")
parser.add_argument("--check-ofl-map", action="store_true",
help="Only run the offline check that the C++ generic-type map "
"resolves against shipped OrcaFilamentLibrary presets")
parser.add_argument("--verbose", "-v", action="store_true",
help="Verbose output for debugging")
args = parser.parse_args()
# Offline check first: the C++ type normalization must resolve against shipped
# OFL presets (no printer needed).
if not check_ofl_generic_map():
return 1
if args.check_ofl_map:
return 0
if requests is None:
print("The 'requests' module is required for live printer operations (pip install requests).")
return 1
print(f"\nConnecting to Moonraker at {args.host}:{args.port}...")
# First test basic connectivity

File diff suppressed because it is too large Load Diff