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

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