-
2026-08-01 —
HelpMixinmisclassified every INHERITED descriptor: properties reported as"attribute", static/classmethods as plain"method"._get_member_typelooked the raw descriptor up inklass.__dict__alone, so for anything reached through the MRO the lookup simply missed and the member fell through to thecallable()test — a property object isn't callable, hence"attribute". This repo composes public classes from private capability mixins (PackageManager(_PkgVersionCheck, _PkgVersionUtils, _PackageManagerHelperMixin, …)), so most descriptors are inherited:PackageManager.installed_ver/.latest_ver/.new_version_availableall reported as attributes, and.update_version/.update_requirementsas methods rather than staticmethods. It now resolves the descriptor from whichever class ininspect.getmro(klass)defines it. Surfaced by the API-registry drift gate once the static walker learned to resolve the same mixins (see m3trik's CHANGELOG);help()output across the ecosystem was wrong the same way.test_help_mixin.py+3 (inherited kinds, own kinds unregressed, kinds through a subclass) → 71. -
2026-08-01 —
StrUtils.collapse_delimiter_runs— collapse the separator residue left when tokens are removed from a delimited name.'a__tok__tokB'withtokstripped yields'a____B'; this collapses it to'a_B'(leading delimiter preserved as a legality prefix, trailing runs stripped by default,strip_trailing=Falseto keep them). Backs the mayatk/blendertkNaming.renameresidue fix (production scene had shape names with 83 consecutive underscores left by stripping accumulated scratch tokens).test_str.py+4. -
2026-08-01 — Optimize now reports file size, and
assessbecame a real dry run. The optimizer's result line read(4096x4096, 24bit (8x3))— no file size, which is the whole point of optimizing.MapOptimizer.format_resultrenders the new one-liner:4096x4096 -> 2048x2048, 24bit (8x3), 12.00 MB -> 3.00 MB (-75%). The source size is now read before the archive/overwrite step (that move used to destroy the "from" half), and an unchanged dimension pair collapses instead of printing a no-op arrow.MapOptimizer.assess(..., predict_size=True, output_type=…, output_profile=…)returns apredictedblock —{width, height, mode, bit_depth, ext, path, size_bytes}.pathcomes from the sameresolve_texture_filenamecalloptimize_mapmakes, so a dry run names the file the real run would write instead of re-deriving the map-type suffix by hand.size_bytesis measured by encoding throughImgUtils.save_image(the same writer, with the profile's bit depth and compression) into a scratch file that's immediately discarded: compression ratios can't be derived from dimensions and mode, so a real encode is the only honest answer to "how much smaller?". Opt-in — bulk report callers assessing every texture in a scene don't pay for it, and the caller's pre-loaded image is passed through uncopied (applyrebinds rather than mutating, the invariantoptimize_mapalready relies on).MapOptimizer.projectreplays a plan's op params to the resulting size/mode with no pixel work;depalettizeandforce_potops now carry their result inparamsso this is a replay, not a second copy ofplan's decision logic.FileUtils.format_bytes/format_bytes_delta— the byte-rendering that was private toMatReport._fmt_size_auto, promoted to the general layer now that the optimizer's reporting needs it too._fmt_size_autois a thin alias.