Release 0.4.0#571
Merged
Merged
Conversation
Runs the docs feature-video generation locally the same way CI does, but on a real GPU: silent leading teardown (crash leftovers), e2e stack up, docs/videos deps installed if missing, generate all 8 videos into docs/static/videos/ (gitignored — inspection only), then teardown; exit code reflects generation. Verified end-to-end: 8/8 videos produced, clean teardown, exit 0.
chore: add videos:generate script for one-command local video checks
…h changelog/roadmap
… video-authoring skill - trim-videos: only frozen tails are trimmed; the manifest cap is a QA gate in analyze-videos (with actionable failure hint), not a trim point — cap-trimming shipped texture-sets cut mid-demo at exactly 20s - manifest caps raised to realistic ceilings with CI headroom - helpers: synthetic cursor overlay (dot + click ripple) injected via navigateTo — Playwright screencast renders no pointer, so all mouse motion was invisible - texture-sets spec: prewarm the EXR preview off-camera, replace the slow-motion canvas drag with a geometry-switch payoff (software-GL render loop delays each synthetic input step by ~2s) - new video-authoring skill (pipeline, choreography rules, cap semantics, selector dependencies), registered in CLAUDE.md
Checks docs claims against sources of truth: format lists vs the FileType extension registry, user-interface.md tab coverage vs the TabType union (with reasoned exemptions for disabled features), quick-start URLs vs FRONTEND_PORT, and video embeds vs the video manifest. README format check starts as a warning until the README refresh PR merges. Registered in CLAUDE.md verification + ripples.
The deployed Storybook lives under the /Modelibr/storybook/ sub-path on GitHub Pages, but initialize() registered the service worker at the origin root (/mockServiceWorker.js), which 404s there and fails registration with "[MSW] Failed to register the Service Worker". Pass a relative worker URL so MSW resolves it against the iframe location, finding the worker under whatever base Storybook is served from. Works unchanged in dev (served at the root).
fix(videos): never trim content to the duration cap; cursor overlay; video-authoring skill
…egory selection highlights
PrimeReact Tree in selectionMode="single" expects selectionKeys to be the
selected key string; the { key: true } map form is only for multiple/checkbox
modes, so the active category never received its p-highlight tint. Regression
test renders the real Tree and asserts the highlight.
…p's compact density - tokens.css: document real-app roles per scale step; drop the unused --mod-text-3xl (1.5rem is the deliberate heading ceiling) - ListHeader: replace website-band padding (32/48px) with app density (12/16px) - categories standard: the quiet scripts-sidebar look (transparent, tight rows, primary-tint selection) becomes the CategoryTreePanel default; scripts-page overrides and its sidebar/body card frame removed, page padding tightened - ListToolbarSearchInput chrome moves from FilterPanel.css into ListToolbar.css (a primitive's classes must be styled in its own stylesheet)
- preview: toolbar theme global swaps the Lara stylesheet like the app's useTheme hook (managed link + color-scheme); canvas tracks theme surfaces; html/body overflow unlocked so document-style stories scroll - Design System / Gallery story: all tokens + shared primitives on one page, with pinned Dark and Light variants for the visual suite - npm run design (root + frontend) opens Storybook straight on the gallery
Canonical rules for visual consistency: compact application density (no website styling), token vocabulary with real-app roles, shared primitive catalog with import paths, categories standard, PrimeReact soft-fail traps, and the live gallery/screenshot verification loop for agents. Registered in CLAUDE.md; frontend-patterns styling section now points here.
npm run design:snap -- --label <name> screenshots every main page in the demo build (both themes) into test-report/design-review/<label>/; design:compare builds a side-by-side + flip-in-place HTML for two labels. Pages open via the same localStorage navigation seeding the docs-videos helper uses.
…el sidebar Slice 1 of the page-unification effort (prompt 13/18 prep): the sounds page adopts the app-wide categories standard — the quiet CategoryTreePanel sidebar — replacing its per-feature .category-tab chips. SoundCategoryTabs deleted; existing category dialogs and drag-to-categorize behavior kept. The primitive grew an optional renderNodeActions slot (hover-revealed rename/delete on tree rows, collapsed via max-width so hidden actions don't reserve width) so sounds' per-category actions fit without forcing. Bonus correctness: the sidebar now shows the real category hierarchy the tab strip used to flatten (demo's UI ⊂ Sound Effects). Ripples updated: sounds e2e page object + steps selectors (scenario names unchanged; 116/116 @sounds scenarios green locally), sounds docs video script, feature docs.
…he design-system skill
… maps list The dashed primary border + tint rendered on any mouse hover with no drag in progress — drop feedback must only appear during an actual drag (.drag-over), matching the ModelGrid rule.
…nel sidebar Slice 2 of the page-unification effort — mirror of the sounds slice: SpriteCategoryTabs deleted, quiet sidebar adopted, page scroll moved to the grid container so the sidebar stays pinned, page padding tightened to the density standard. The sidebar again surfaces real category hierarchy the tab strip flattened (demo's HUD ⊂ UI). Ripples: sprites e2e steps switched to shared sidebar selectors via one constant (scenario names unchanged; 117/117 @sprites scenarios green locally incl. the @serial category flows), sprites video script, feature docs.
… skill User-set process: search the catalog before writing; grow a primitive's API instead of forking it; if nothing fits, STOP and propose the standard (API sketch, existing adopters, which queued prompts reuse or strain it, non-goals) and get sign-off before implementing. Also: bug in a pattern = grep for the pattern's clones.
…Sets tab The page list was derived from the TabType union, which still carries the legacy 'textureSets' type for old saved layouts — users can only open Global Materials and Multi-Model Textures since the split. The list now mirrors the New Tab page's tiles and says so.
…ted-state migration The textureSets page was split into Global Materials and Multi-Model Textures, but the legacy TabType stayed in the union with a render case kept for old saved layouts — a silent alias that showed an unfiltered list no page in the app offers anymore (and tricked the design-review harness into snapshotting a nonexistent page). - navigationStore persist bumps to version 1 with a migration rewriting legacy tabs (type, singleton id, label, active-tab pointers) to modelTextures across open windows, recently-closed tabs, and archived windows; verified live against the demo build with a seeded v0 layout - legacy URL ids in the compact tab format normalize to modelTextures - fixes a pre-existing hole the migration exposed: globalMaterials and modelTextures were never in the URL-parsing whitelists, so post-split URLs threw and fell back to the default layout - 'textureSets' deleted from the TabType union and every switch (TabContent, DraggableTab, getTabLabel, openTab dedupe, NewTabPage icons) - tests updated to the successor type + new regression tests for the migration and the legacy-URL mapping (432 total)
…ack states The last two pages without the toolbar identity: h2 header + always-open FilterPanel replaced by the standard toolbar row (search/filters/options/ create + count chip); loading/empty hand-rolled markup replaced by shared LoadingState/EmptyState. Page density aligned (0.75rem padding). E2E page-ready waits retargeted to the toolbar/shared-empty selectors.
EmptyState/LoadingState from @/shared/components/feedback replace the hand-rolled empty/loading markup on models (no-results), sounds, sprites, scripts, texture sets (both variants), environment maps (incl. loading), recycled files, and history. Legacy class names stay on the elements as inert e2e selector aliases (prompt 46 replaces them with testids); their CSS blocks are deleted so the shared look is the only styling. Models' dead local EmptyState/ErrorState components and stories removed. Also: unified card identity — all six card types (model, texture set, sound, script, sprite, environment map) share --mod-radius-lg, shadow sm→md, hover translateY(--asset-card-hover-lift) and --mod-transition-fast, replacing three divergent hover behaviors. Full AssetTile DOM migration deliberately deferred to prompt 18 (see prompt 13 slice log).
…ts view
The e2e suite injected the legacy 'textureSets' tab via localStorage as its
navigation surface ('kept for the kind-filter e2e tests') — with the type
removed from the app and migrated on load, that surface is gone for real:
- TextureSetsPage.goto and all step-level navigation now open the real
Multi-Model Textures tab (New Tab tile — no state injection needed)
- 'kind tab' steps redefined: switching kinds = switching between the
Global Materials / Multi-Model app tabs; active-kind assertion uses the
dock tab state
- dock-system tab locators follow the pi-images icon of modelTextures
- two scenarios @Skip'd with owners: 'Default tab is Global Materials'
(no combined view → no default kind) and 'Drag and drop between kind
tabs' (switcher-only interaction; conversion stays covered by the
context-menu scenarios in 18-texture-set-kind-conversion.feature, drag
replacement belongs to prompt 49's DnD matrix)
- navigation-helper drops textureSets from state-injectable types
Second sweep after the full-suite run: texture-types (three blocks),
thumbnail-previews, merge-channel-mapping, recycled-files-common and
recycled-files-textures all drove the removed .kind-filter-select switcher
('switch to Multi-Model if not active') after navigating — a no-op now
that navigation lands directly on the kind-locked Multi-Model Textures
tab. TextureSetsPage.selectKindTab (used by the demo suite) now opens the
matching app tab instead of clicking switcher buttons.
Validated: the previously-failing scopes (00-texture-sets channel-mapping
+ thumbnails, 04-recycled-files) pass 108/108 locally.
- 'Kind persistence' asserted the old switcher semantics (kind resets to Global Materials on reload); kinds as separate app tabs persist the active tab across reload — assert Multi-Model stays active instead - recycled-files FilesTab step lost its accidental render wait when the obsolete switch-to-Multi-Model block was removed; under full-suite load the toolbar-search helper raced page render — replaced with a deterministic .texture-set-list wait (other removal sites are already gated by TextureSetsPage.goto's built-in wait) Validated: 216 passed / 0 failed across both scenarios' dependency chains.
Every pull-request E2E run since July 4 timed out (4 min) waiting for real asset-processor renders on GitHub's GPU-less runners, while identical code passed push runs and the local lane. Setup scenarios now only create models and shared state; the render assertions move to the local GPU lane: - 00-texture-sets/01-setup.feature: thumbnail waits removed (dependents only need the models to exist) - new 01-model-viewer/03-model-card-thumbnail.feature (@serial): carries the moved card-render assertion via a shared-state-parameterized step - 00-texture-sets/03-default-behavior.feature: the regen-wait scenario joins @serial (same render-blocking class); audit found no other render waits left on the PR lane - dock-system.steps.ts: commented headroom for the software-GL model-viewer mount (tab-dedup stays on PRs — it guards dock logic, not rendering) - e2e-authoring skill: the rule is now documented so it can't regress Validated live: setup 8/8 (27.7s, was minutes), full chromium project 102/0, moved scenarios green in the serial project.
…tegory-sidebar # Conflicts: # CLAUDE.md
Semantic conflict between #559's video rework and the textureSets tab split: the spec navigated to the removed combined tab id, which the legacy-URL mapping now sends to Multi-Model Textures — the wrong page for the global texture set the video creates. The tab-label map gains both split pages.
The #559 choreography plus split-page navigation measures a stable ~44s locally (two runs, zero freeze/black frames) against a 40s cap that would fail the main-push video QA gate. Deliberate raise per the gate's own guidance, with headroom for CI's slower software-GL rendering.
Mirrors the pack slice: ProjectRepository stops self-committing, and every Project command handler calls IUnitOfWork.SaveChangesAsync once at the end. AddModelToProjectCommandHandler (association + batch-upload repair loop) and AddTextureToProjectWithFileCommandHandler (texture-set creation + project association + batch-upload record) previously issued 2-3 independent commits each; the latter still needs an intermediate commit to resolve a freshly created TextureSet's real id before it's used as a raw FK int, noted inline.
Removes the 6 manual IDomainEventDispatcher.PublishAsync + ClearDomainEvents call sites (AddModelCommandHandler x2, CreateModelVersionCommandHandler, CreateModelFromBlendCommandHandler, SetActiveVersionCommandHandler, FinishThumbnailJobCommandHandler) — DomainEventsInterceptor (added in the prior commit) now dispatches every event a tracked aggregate raises, so a handler can no longer forget the boilerplate and silently drop an event. Two handlers (CreateModelVersionCommandHandler, CreateModelFromBlendCommandHandler) raised their event AFTER the last repository call that would trigger a save; reordered so the event is on the aggregate before that save, since dispatch now only fires post-commit. The other three already raised before their last save and needed no reordering. Tests updated to assert the new contract directly: a raised event lands on the aggregate handed to the repository (no dispatch mock needed in the handler tests), and a dedicated DomainEventsInterceptorTests suite proves the save-pipeline mechanism end-to-end — event raised + saved -> handled with zero dispatch code in the caller, a failed SaveChanges dispatches nothing, and a handler that stages further writes during dispatch (mirroring ModelUploadedEventHandler enqueuing a ThumbnailJob) gets them flushed.
…ller Real-Postgres integration test (ConcurrencyTests.Parallel_Model_To_Pack_ Association_No_Duplicates) caught a regression from the packs slice: the duplicate-PackModels-association catch was only wired into IUnitOfWork's explicit interface member, but AddModelToPackCommandHandler's first actual commit now happens inside BatchUploadRepository's still-self-committing UpdateAsync (PackRepository.UpdateAsync only stages the association until that area finishes migrating) — bypassing the catch and surfacing a raw 500 on the known-benign concurrent-add race. Moving the catch into the SaveChangesAsync override itself (rather than only the IUnitOfWork member) protects every caller uniformly, including repositories that still self-commit during the in-progress migration. This is exactly the kind of gap Category=Integration tests exist to catch — Application.Tests' mocked repositories can't see it.
…tgres Adds UnitOfWorkRollbackTests: stages an add on ISettingRepository, then an add on IPackRepository, then a second Setting with a duplicate Key (Setting.Key is uniquely indexed) — all before a single IUnitOfWork.SaveChangesAsync call. Asserts the whole thing throws and NEITHER the Setting nor the unrelated Pack persisted. Runs against real PostgreSQL (Category=Integration) because EF Core's InMemory provider doesn't reliably enforce unique constraints, so it can't distinguish a rolled-back SaveChanges from a row that simply never got added. Adding a second test class on ModelibrWebFactory exposed a latent race: each factory instance drops and recreates the same shared "Modelibr_IntegrationTests" database in its constructor, and xUnit runs different test classes in parallel by default, so two classes' factories could race on that drop/create. Introduces a PostgresIntegrationCollection xUnit collection (both ConcurrencyTests and UnitOfWorkRollbackTests now carry [Collection(...)]) so classes using this factory always run sequentially; each still gets its own factory/database lifecycle.
Source-scan xUnit fact: fails if any file under src/Infrastructure/Repositories calls SaveChangesAsync, with a failure message pointing at the backend-patterns skill's "Transactions — unit of work" section. Verified it actually catches a regression (temporarily reintroduced a self-commit in PackRepository during development; the test failed with the expected message, removed before committing). The full migration lands one bounded area at a time (settings, packs, and projects so far), so this activates the rule for those with an honest, commented allowlist for the 22 repositories not yet migrated — every entry names its area so the list shrinks as each one lands, per skill-authoring's "dormant gate" convention. A companion test guards the allowlist itself against stale entries once a file is migrated.
…ract Replaces the "dispatch is manual and easy to forget" / "there is NO unit of work" claims with the new contract: IUnitOfWork.SaveChangesAsync (one call per handler) and DomainEventsInterceptor (dispatch lives in the save pipeline, never in a handler). Documents the two traps found while implementing this: temporary EF keys on freshly-Added entities needing an early SaveChanges in a few handlers, and the migration-in-progress allowlist in RepositoriesDontSelfCommitTests being the live source of truth rather than a claim duplicated here.
feat(webdav): allow duplicate asset names with id-suffix disambiguation
fix(worker): delete dead legacy pipeline and cancel job timeouts
The e2e stack recreates its DB fresh every run, so first boot always has every migration pending — and its webapi has no backup configuration (no POSTGRES_* env, no BACKUP_STORAGE_PATH), so the automatic pre-migration backup fails at the psql version probe and the new abort-on-failure policy correctly refuses to start. That policy is right for real installs and wrong for this stack: skip explicitly. The pre-migration path stays covered by backup-restore-e2e spec 06 (real pg_dump on first boot) and the DatabaseExtensions integration tests.
ModelRepository, ModelTagRepository, and BatchUploadRepository stop self-committing entirely; ModelVersionRepository's aggregate CRUD does too, while its ModelVersionTextureSet mapping methods intentionally keep their immediate commits (the variant-aware AddTextureMappingAsync is an idempotent-insert primitive that must save at once to catch its unique violation and recover) — the file stays allowlisted with that reason. Handlers migrated: every Models/ command handler, both RecycledFiles handlers (restore + permanent delete, one save per entity-type case), and — because BatchUploadRepository is consumed by upload handlers of every asset type — UploadFileCommandHandler and the sound/sprite/texture-set/env-map *WithFile handlers get their UoW flush now, ahead of their own areas' slices, so staged batch-upload records can't be silently dropped. Two id-semantics constraints surfaced (both documented inline): - Model creation must commit BEFORE Model.CreateVersion for the first version: it sets Model.ActiveVersion, and two still-Added entities trip EF's circular Model.ActiveVersionId <-> ModelVersion.ModelId FK check in a single save. Caught by the Category=Integration ConcurrencyTests against real Postgres (the whole upload path 500'd), invisible to mocked unit tests. - Version inserts commit before their id is copied into raw scalar FKs (File.SetModelVersion), the ModelUploadedEvent, or response DTOs.
ThumbnailRepository and ThumbnailJobEventRepository stop self-committing (and lose their now-unused SaveChangesAsync interface members, along with IThumbnailJobRepository's). ThumbnailJobRepository's Add/Update stage only; its GetNextPendingJobAsync keeps the explicit claim transaction permanently, now with the prompt-27 coordination note (must be wrapped in an execution strategy if EnableRetryOnFailure lands). ThumbnailQueue commits its own writes via IUnitOfWork — deliberately, unlike command handlers: enqueue/complete/fail/retry are durable-queue primitives that must be persisted before workers are notified over SignalR, and its callers include the domain-event pipeline where no command handler exists to commit afterwards. This keeps every caller's semantics unchanged with zero edits at queue call sites. Handlers migrated: Finish*JobCommand (model/sound/texture-set), LogThumbnailJobEventCommand, Upload(Png)ThumbnailCommand, Regenerate(All)ThumbnailsCommand, SetDefaultTextureSetCommand, SetMainVariantCommand, AssociateTextureSetWithModelVersionCommand. Every new-Thumbnail-then-SetThumbnail site commits between the add and the set — ModelVersion.SetThumbnail copies thumbnail.Id into a raw scalar FK, so it needs the real id (same temporary-key trap as the models slice).
TextureSetRepository and TextureProxyRepository stop self-committing. All 16 texture-set command handlers (CRUD, tags, kind, channel, tiling, proxy, associate/disassociate, thumbnail upload/regenerate) commit once via IUnitOfWork. CreateTextureSetWithFileCommand commits right after the set's insert-update pair — its real id feeds a BatchUpload raw scalar, the thumbnail-job enqueue, and the response DTO; UpdateTextureSetKindCommand and RegenerateTextureSetThumbnailCommand commit before enqueuing so the worker sees the durable set state.
SoundRepository stops self-committing. Create/Update/Delete/SoftDelete sound handlers commit once via IUnitOfWork; CreateSoundWithFileCommand commits right after the sound insert because its real id feeds the waveform-job enqueue and the BatchUpload raw scalar. FinishSoundWaveformJobCommand's staged sound update is flushed by the save it already gained in the thumbnails slice.
SpriteRepository stops self-committing. Create/Update/Delete/SoftDelete sprite handlers commit once via IUnitOfWork; CreateSpriteWithFileCommand commits right after the sprite insert because its real id feeds the BatchUpload raw scalar and the response.
feat(backup): automatic pre-migration backup + gated restore drill
…tOfWork ScriptRepository and ScriptTemplateRepository stop self-committing. Every Scripts and ScriptTemplates command handler commits once via IUnitOfWork at the end of Handle. ScriptCategoryRepository stays self-committing — category repos migrate together in the categories slice.
EnvironmentMapRepository stops self-committing. Every EnvironmentMaps command handler, plus the two thumbnail handlers left behind by the thumbnails slice (FinishEnvironmentMapThumbnailJobCommand, UploadEnvironmentMapVariantThumbnailCommand) that write through this repo, commit once via IUnitOfWork. Create*/AddVariant* handlers need an early commit right after the variant/environment-map insert: SetPreviewVariant and the variant.Id>0 preview-assignment check both depend on the database-assigned id, which previously came for free from the repo's internal self-commit. Without moving that commit up, AddEnvironmentMapVariant(WithFile) would have silently stopped auto-assigning the preview variant for an environment map's first variant. EnvironmentMapCategoryRepository stays self-committing — category repos migrate together in the categories slice.
All six per-type category repositories (model, texture-set, sound, sprite, script, environment-map) stop self-committing. The shared CategoryCommandHandlers.CreateAsync/UpdateAsync/DeleteAsync helper — used by every category type except Model's and TextureSet's bespoke Create/Update — now takes IUnitOfWork and commits once per operation; concrete handlers thread it through. Delete's children-first branch delete now lands in a single commit instead of one self-commit per row, so a failure partway through leaves the whole branch untouched (atomicity gain, not a behavior change to preserve). Create commits immediately after the insert: category.Id is database-assigned and feeds the response DTO.
…gh IUnitOfWork FileRepository, FilePersistence, and StageRepository stop self-committing — the last three repositories on the fitness gate's allowlist besides the two permanent exceptions (ModelVersionRepository's texture-mapping upsert, ThumbnailJobRepository's claim transaction). UploadFileCommandHandler and CreateStageCommandHandler need an early commit right after their insert: the database-assigned id feeds a BatchUpload FK / the response DTO, same shape as every other Create*WithFile handler in the prior slices. SoftDeleteFileCommand and UpdateStageCommandHandler get a single commit at the end. FileCreationService's two orphan-cleanup HardDeleteAsync calls (missing physical file, stale recycled record) are untouched and now join whichever caller's handler eventually commits — if that handler fails validation afterward, the cleanup no longer happens independently the way the old self-commit guaranteed. This is the same atomicity trade the categories slice's branch-delete made, and is explicitly in scope per prompt 25; redesigning FileCreationService's disk<->DB compensation itself stays out of scope. Fitness gate allowlist now holds only the two permanent, individually- justified exceptions.
The last four slices (scripts, env maps, categories, files+stages) closed out prompt 25 — the fitness gate's allowlist is down to the two permanent, individually-justified exceptions. Update the skill's stale "in-progress migration" / "settings, packs, projects done" language accordingly, and note the ThumbnailQueue self-commit exception and the hierarchical-delete atomicity gain so the next reader doesn't have to rediscover them.
…ties CreateTextureSetWithFileCommand (and any Add->mutate->Update->SaveChanges handler shape) 500'd with "has a temporary value while attempting to change the entity's state to 'Modified'": since repositories stopped self-committing, AddAsync leaves an aggregate tracked as Added with a temporary key, and calling UpdateAsync on that same reference before the first SaveChanges forced EF to try to move it to Modified. Add DbContextTrackingExtensions.UpdateIfDetached and route every repository's UpdateAsync through it — it only attaches + marks Modified for a genuinely Detached entity; an already-tracked one is persisted in its current state by the next SaveChangesAsync with no extra call. Category/Pack/Project repositories already had this guard hand-rolled; they now share the same helper for consistency. Covers the fix with an integration test reproducing the exact failing upload flow plus a second Add->Update flow against ModelRepository (a different aggregate) and a detached-entity persistence case, plus a fast InMemory-backed unit test on TextureSetRepository.
…missed
Two silent-persistence regression classes surfaced by the PR e2e lane:
- Handlers with no commit at all: AddSpriteToPackCommand ('Add sprite to
pack' + downstream 'Remove sprite from pack' failures), the four model
variant/material/metadata commands, HardDeleteTextureSetCommand, and
GetSettingsQueryHandler's get-or-create-defaults write (a query handler
that persists — pre-existing design, persistence restored).
- Commits only on a conditional path: AddTextureToSetCommand committed
only when a batchId was provided, so the texture-merge flow ('Merge ORM
packed texture using Split Channels') staged textures and dropped them
at scope disposal. The commit is now unconditional at the end of the
mutating path.
Audited the other statically-flagged handlers (ModelCategoryCommands,
UpdateSettingsCommandHandler, TextureSetCategoryCommands,
SetDefaultTextureSet, AddFileToModel, AddModel, RecycledFiles restore/
delete): every mutating path in those already reaches a commit.
Three regression sub-classes from the unit-of-work migration (tracked- entity Update, missing commit, conditional-only commit) proved that per-handler commit discipline cannot be guaranteed by review or grep. Every ICommandHandler<> / ICommandHandler<,> now resolves wrapped in CommandHandlerUnitOfWorkDecorator: after the inner handler returns a success Result, IUnitOfWork.SaveChangesAsync flushes anything still staged; failure Results and exceptions commit nothing. Explicit commits inside handlers remain preferred (mid-handler id materialization) and become harmless no-op flushes. Query and domain-event handlers are not wrapped. No new dependencies — hand-rolled factory registration; the concrete handler is registered under itself so the decorator factory can resolve it without recursing. Architecture test asserts the decorator actually wraps resolved command handlers so the registration cannot be silently dropped; an integration regression test proves a commit-less handler still persists through the decorator, and the no-batchId add-texture flow persists (the exact CI failure).
Adds a 54-model 'Base Meshes (CC0)' pack to demo mode, streamed from the Papyszoo/base-meshes fork pinned to a commit SHA — no binaries in this repo. - baseMeshesSeed.ts generates models/versions/pack with real file sizes and credits The Base Mesh (thebasemesh.com) + M3-org/base-meshes (CC0 1.0) - seedAssetUrl passes absolute URLs through so serveFile streams GLBs from GitHub raw (CORS *) - seedRemoteThumbnails serves pre-rendered animated WebP turntables (made with the asset-processor pipeline) instead of generating in-browser; checked after IndexedDB so user-customized thumbnails still win - contract test pins the cross-module id scheme (drift would silently 404 in the hosted demo)
# Conflicts: # src/Application/EnvironmentMaps/CreateEnvironmentMapCommand.cs # src/Application/EnvironmentMaps/UpdateEnvironmentMapCommand.cs # src/Application/Models/AddFileToVersionCommandHandler.cs # src/Application/Models/SetDefaultTextureSetCommand.cs # src/Application/Models/SetMainVariantCommand.cs # src/Application/Scripts/UpdateScriptCommand.cs # src/Application/Sounds/CreateSoundCommand.cs # src/Application/Sounds/UpdateSoundCommand.cs # src/Application/Sprites/CreateSpriteCommand.cs # src/Application/Sprites/UpdateSpriteCommand.cs # src/Application/TextureSets/AssociateTextureSetWithModelVersionCommand.cs # src/Application/TextureSets/UpdateTextureSetCommand.cs # src/Application/TextureSets/UpdateTextureSetKindCommand.cs # tests/Application.Tests/Models/SetDefaultTextureSetCommandHandlerTests.cs
feat(demo): seed Base Meshes (CC0) pack from remote fork URLs
refactor(backend): unit of work + save-pipeline domain event dispatch
chore(release): bump internal package versions to 0.4.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 0.4.0 — merges
version/0.4intomain.Highlights
Unified category sidebar across all asset types (#562, #563, #564)
CategoryTreePanelsidebar with inline category management (create/rename/delete via context menu), drag-to-assign, and true count badges.Backend: unit of work + save-pipeline domain events (#568)
IUnitOfWork— multi-repo handlers are now atomic (proven against real Postgres).WebDAV: duplicate names + Blender-save safety (#565)
generated-{name}.blendgated on a real cache and generated in background (fix(worker): delete dead legacy pipeline and cancel job timeouts #567 follow-ups included).Backup trust hardening (#566)
Design system & frontend
textureSetstab type removed via persisted-state migration.Demo
Worker
Docs & CI
docs:auditCI gate fails on docs/code drift; video pipeline fixes — no mid-demo trims, synthetic cursor overlay (fix(videos): never trim content to the duration cap; cursor overlay; video-authoring skill #559); one-command local video generation (chore: add videos:generate script for one-command local video checks #556).Verification
version/0.4.