-
2026-07-12 — Review-pass fixes on the extraction change set (pre-commit adversarial review). (1)
.gitignore's bareMANIFESTpattern silently ignored the entire newcore_utils/engines/shots/manifest/package (case-insensitive repo + unanchored pattern): committing the migration would have shipped aDEFAULT_INCLUDEand tests referencing a package git never saw —ModuleNotFoundErroron every fresh clone/CI. Root-anchored to/MANIFEST(the setuptools artifact it was for); the package now surfaces as untracked. (2)TaskFactoryrevert-on-exception:_manage_contextskipped_revert_stateswhen a task raised mid-loop or the with-body threw at the yield — a failed export leftset_*scene state permanently applied. The loop+yield now sit in atry/finally. (3)TaskFactory._is_success([])raisedIndexErroron an empty tuple/list check result; an empty sequence is now a plain failed check. Both TDD'd (red first);test_task_factory.pyalso gained the TASK_ORDER / zero-param-flag / list+dict-splat / LIFO-revert pins its docstring promised (5 → 12). (4) Consumer leakage removed: the base pipeline no longer sets_optimize_keys_enabled(a mayatk task name hard-coded in the host-free primitive); blendertk'sTaskManager— its only reader — sets it in its own_execute_tasks_and_checksoverride. (5) Root surface:plan_reorderjoined its three sibling planners in__all__(star-import parity with the registration), the root-registered manifest surface (parse_csv,ShotManifest,resolve_ranges, the dataclasses) is now in__all__too, and the too-genericapplyroot export was dropped (consumers importshot_apply.applyby module path; the neighboring comment already stated that policy). (6) Seam coverage:compute_duration(resolve_source_fn=…)(resolution args, exception-swallow fallback) andShotManifest._audio_grow_duration(override respected by the re-sync grow pass; pure default = max(template, measured)) are now pinned intest_shots_manifest_core.py(+4). (7) Docs de-staled:docs/README.mdpackage table (engines tenant listed; drape/MapFactoryrows corrected toRailSurface/core_utils/engines/textures),CLAUDE.md'sMapFactoryentry,test_polyline.py's pointer to the moved drape tests;process_streamdocstrings now state the subscribe-replay reentrancy constraint andProcessReader's binary-pipe contract. -
2026-07-12 — Curtain generator decomposed out of pythontk: general
geo_utils/rail_surface.py::RailSurfaceextracted; the curtain-specificCurtainDrapemoved to its DCC consumers (vendored twins, drift-guarded). The curtain drape (geo_utils/drape.py) was a single tool's domain math — too tool-specific forgeo_utils' generality promise, yet (one tool, no model/planner breadth) not a domain core either, so every pythontk home misfit. Decomposed per placement rule #1 instead of relocated: NEWRailSurface(geo_utils, on the lazy root) owns the invariant rail→frames→grid machinery — frames a rail (Polyline) into a(u_segs+1)×(v_segs+1)grid and applies a caller-supplieddisplace(u, v, pos, tan, normal) -> Vecfield; any curve-driven surface generator (banner/ribbon/terrain strip) is a new displacement over it, not a new framework.CurtainDrape(the pleat/fold/catenary/crease displacement + seeded feature precompute + resolution policy) left the package: it now lives with its two consumers as code-identical vendored twins (mayatk/blendertkedit_utils/_curtain_drape.py— they can't import each other, and extapps'test_vendor_sync.pygained aTestCurtainEngineDccSyncdrift guard), shedding its unusedLoggingMixinbase and dead_lerp/_unitaliases in transit.ptk.CurtainDrapeis no longer exported (mayatk'sCurtainMeshsubclasses the local twin; blendertk'screate_curtainbuilds from it; the class contract —__init__/prepare/drape/grid_points— is unchanged). The engines tenant rule is sharpened inCLAUDE.md: a single tool's math is not an engine — extract the general primitive, vendor the remainder. Tests: newtest_rail_surface.py(8) pins the primitive (degenerate rail, seg floors, frame count, row-major grid, normalized u/v, verbatim displace output, vertical-drop field); the 12 drape tests moved to mayatk'stest_curtain_drape.py(pure Python — runs under pytest and the in-Maya runner). Verified: full suite green (1849 — the 12 moved tests exactly account for 1861→1849); extapps 441 (the guard's +2); the moved drape tests 12/12 under pytest; fresh headless Blender 25/25 (identical build through the vendored twin, drift 0.00e+00); fresh mayapy standalone 8/8 (CurtainMeshsubclasses the local twin and builds a matching mesh;ptk.CurtainDrapeconfirmed gone,ptk.RailSurfaceresolves). -
2026-07-12 — Shots engine: two injection seams added while migrating mayatk onto it (the engine's first production adoption beyond blendertk). Auditing the engine against the working Maya code it replaces surfaced two behaviors the extraction had no seam for; both are pure-default hooks so blendertk is unaffected: (1)
behaviors.compute_durationgainsresolve_source_fn(entry_name, entry_kind)— Maya'saudio_clipstracks carry paths independently of the manifest CSV, so an audio entry with nosource_pathcan still resolve one via its normalized track id; without the seam that fallback (and the clip-length sizing it feeds) was silently lost. (2)ShotManifest._audio_grow_duration(audio_objs)— the audio-grow pass in_compute_planis now an overridable hook (pure default: template duration maxed with_measure_audio, exactly the previous inline code); mayatk overrides it to route through its owncompute_durationbinding, which both preserves the original sum-based semantics and keeps the established...behaviors.compute_durationmock seam working against the inherited planner. Also fixed three stale "YAML" docstrings inbehaviors._behaviors(the store is JSON). Engine suites green:test_shots_core+test_shots_manifest_core110/110; full run green. -
2026-07-12 — Cross-DCC extractions:
process_streamprimitives,TaskFactory, blendshapeWeights— while the app-specific Marmoset/Substance engines stay with their consumers.core_utils/process_stream.py—OutputStream(thread-safe multi-consumer(source, line)pub/sub with bounded replayable history +wait_for),ProcessReader(subprocess-pipe reader thread), andLogTailer(rotation-aware log-file tail thread): the generic stream mechanism extracted out of the Substance bridges'connection.py, whose mayatk/blendertk copies now compose it. Exposed on the lazy root (pythontk.OutputStream/ProcessReader/LogTailer) beside the other app-agnostic mechanism (app_handoff,app_launcher,net_utils.rpc,script_template). Newtest/test_process_stream.py(20) pins the pub/sub, replay,wait_for, pipe-reader, and tail/truncate/rotation contracts DCC-free.core_utils/task_factory.py—TaskFactory, the generic reflection-based task/check pipeline (getattr/inspect.signaturedispatch,TASK_ORDERordering, LIFO set/revert, check-result processing) that mayatk's and blendertk's scene-exporterTaskManagerwere vendoring byte-identical (~230 LOC). A general orchestration base (no domain model or planner), so it lives incore_utilsbesideapp_handoff— notengines/— per the "not every shared framework is an engine" rule. Exposed aspythontk.TaskFactory; newtest/test_task_factory.py(5) pins the dispatch/revert/check contract DCC-free (its only prior coverage was DCC-coupled).math_utils/weights.py—Weights(blendShape / shape-key morph weight math: frame→weight, evenly-spaced generation, 3-dp rounding), byte-identical in bothblendshape_animators; blendertk's own docstring said "promote to pythontk when a second consumer shows up" — mayatk was the second consumer. Exposed aspythontk.Weights.- Placement rule applied, both directions: the Marmoset Toolbag and Substance Painter engines were evaluated for a pythontk home and deliberately kept vendored downstream (mayatk / blendertk / extapps' Marmoset panel) — host-agnostic but not app-agnostic (a named commercial product's exe discovery, protocol dialect, in-app plugin, templates), they fail the bottom-of-stack charter;
core_utils/engines/stays domain cores only (shots / instancing / textures), andapp_handoff's own docstring already places app deliverers ("Unity copy-to-Assets, Painter/Toolbag RPC") "with their app glue". The vendored copies are drift-guarded by extapps'test_vendor_sync.py(extended: mayatk↔blendertk code-parity for both engines, on top of the original extapps↔mayatk byte-parity).
Verified: full suite green (1853 passed); mayatk + blendertk each subclass the sharedTaskFactoryand use the sharedWeightsin fresh Maya and fresh Blender (is-identity checks); both DCCconnection.pycopies compose the shared stream primitives (fresh-instanceis-identity probes in both DCCs); vendor-sync guard 5/5; extapps suite 439.
-
2026-07-11 — Two more domain engines relocated into
core_utils/engines/:instancing/,textures/(public API unchanged). Following the namespace's charter (model + algorithm/planner + DI seams, domain-specific, needed by ≥2 downstream packages that can't import each other), two frameworks that were sitting in generality-promising*_utilsbuckets graduated toengines/(the curtain generator was weighed for the same move and decomposed instead — see the 2026-07-12 entry above):engines/instancing/←geo_utils/assembly_sorter.py—AssemblySorter, the separated-part clustering core behind auto-instancing (feature-dict-in → index-plan-out). Only the sorter moved: the general PCA/NN/cluster math stays asgeo_utils.PointCloud(the domain was already decomposed per placement rule #1).engines/textures/← the whole PBR clusterimg_utils/{map_registry, map_factory/, map_optimizer, output_template, mat_report, map_compositor}— model (map_registrytaxonomy/presets) + planner/strategies (map_factory:ConversionRegistry/TextureProcessor/handlers/prepare_maps) + plan/apply optimizer + export-config + report view. Shared by mayatk, blendertk, and the standalone extapps texture tools. Moved as one domain (incl.map_compositor, which rides onmap_registry.WF) rather than split.map_registryis the clean model (imports onlycore_utils), so no import cycle despite the engine composingImgUtilsoutward.
Relocation, not a rename — every public symbol resolves through the lazy root exactly as before (
from pythontk import MapFactory, AssemblySorter, …); onlyDEFAULT_INCLUDEkeys and internal module paths changed. Clean break, no back-compat shims (matching the geo_utilsPolyline/PointCloudprecedent): the 27 files that imported the old internal paths (pythontk.img_utils.map_factory, …) were repointed — mayatk (shader_templates,texture_path_editor), extapps (converter/packer/compositor), the blendertk PIL-injectionsys.moduleskeys, pythontk's own_img_utilslazy imports, and ~10 test files across the four repos. Consumers on the rootptk.Xsurface were untouched. img_utils keepsImgUtils+ the photogrammetry-ingest primitives (MaskGenerator/ExposureEqualizer/ImageCurator).app_handoff.pywas evaluated and deliberately NOT moved — it clears "shared by both DCCs" but is a general Template-Method/Strategy orchestration base with no domain-model or algorithm layer (its own docstring calls it "Generic"), so it stays incore_utilsbesideapp_launcher/preset_store. The "not every shared framework is an engine" rule now lives inCLAUDE.md. Verification: full suite green (1821 passed / 1838, 17 skipped); the relocated tests (textures 328, instancing 77) pass at the new paths; every mayatk/extapps/blendertk edited source ispy_compile-clean and its new pythontk import path resolves headlessly. -
2026-07-11 —
core_utils/engines/domain-engine namespace; the shots system's pure core consolidated here. New charter'd home for domain engines — a complete, pure app core (model + planning + algorithms + DI seams) that is domain-specific by design and needed by ≥2 downstream packages that can't import each other (mayatk + blendertk), so it can't live in a generality-promising*_utilslayer nor be duplicated downstream (rule + rationale in pythontkCLAUDE.md). First tenant:engines/shots/—shot_model(ShotStore/ShotBlock+ overridable scene hooks,SHOT_PALETTE),shot_plan(plan_respace/plan_ripple_downstream/plan_ripple_upstream/plan_reorder→MovePlan; collision-safeapplywith an injected key-mover),shot_detection(purecluster_segments_by_gap/boundaries_from_key_entries),shot_apply, and the Shot Manifest coreengines/shots/manifest/—manifest_model(parse_csv/ColumnMap/BuilderStep/BuilderObject/PlannedShot/StepStatus/detect_behaviors),range_resolver(resolve_ranges/prune_to_top_boundaries),mapping/(CSV column-map templates overTemplateSet),behaviors/(keying-recipe templates; JSON not YAML so Blender's PyYAML-less bundled Python can load them), andmanifest_engine(ShotManifest— a hooked base whosesync/update/assess/_compute_planare pure, with DCC scene seams — fps/audio-measure/scene-discovery/key-emission — overridden by the mayatk/blendertk adapters). Root exports the public surface (ShotStore,ShotManifest,parse_csv,resolve_ranges,plan_reorder,SHOT_PALETTE, store-event dataclasses, …). Backs both mayatk's and blendertk's shot toolsets from one implementation. Fixed the existing-shot audio-grow path inmanifest_engine(it sized viacompute_durationwithout the_measure_audiohook, so an existing audio shot never re-grew when its clip lengthened — a behavioral divergence from the measured mayatk path; now routes through the hook, pure-default no-op preserved). Tests:test_shots_manifest_core.py42; full suite green.