LearnNewsExamplesServices
Frontmatter
id17222
titleTheme-map builds merge into the stale map — deleted skins never leave it
stateClosed
labels
bugaibuild
assigneesneo-kimi-iris
createdAtAug 16, 2026, 1:12 AM
updatedAtAug 16, 2026, 2:34 AM
githubUrlhttps://github.com/neomjs/neo/issues/17222
authorneo-fable-clio
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 16, 2026, 2:34 AM

Theme-map builds merge into the stale map — deleted skins never leave it

neo-fable-clio
neo-fable-clio commented on Aug 16, 2026, 1:12 AM

Context

Surfaced during the #17209 drawer-skin lane: a theme-map inspection showed five apps.agentos.* root keys (InterventionPanel, StrategyCardPanel, StrategyPanel, DockPreview, FleetSettingsPanel) with no corresponding view classes. First read was "orphan skin files to delete" — but the falsifying sweep found no such SCSS files anywhere in the repo (find/grep -rli over resources/scss/ and buildScripts/, case-insensitive: zero hits), while the keys survive every fresh build-themes run in all three emitted copies (resources/theme-map.json, dist/development/…, dist/esm/…).

The Problem

The keys are fossils. buildScripts/build/themes.mjs seeds the map from the PREVIOUS map instead of building it from the effective SCSS tree:

  • getThemeMap() (themes.mjs:208-225) loads the existing resources/theme-map.json (cwd first, neoPath fallback) as the starting object.
  • Namespace insertion is create-only (ns(classPath, true, themeMap), :113) — entries are added for files the scan finds, but nothing ever removes an entry whose file is gone.

Consequences:

  • Deleting or renaming a skin never lands locally. Every developer tree that ever built the old agentos generation carries its five dead keys forever; each rebuild re-inherits them.
  • The runtime loader trusts the map (src/worker/App.mjs:738 fetches resources/theme-map.json; insertThemeFiles fires addThemeFiles for any resolvable key) — phantom keys cause real stylesheet requests for CSS that can no longer be built (dead 404-class fetches per phantom per window).
  • CI cannot see the class: the map is gitignored, so CI builds start from {} (or the neoPath fallback) and never accumulate — the defect is invisible exactly where the checks run, and permanent exactly where humans and agents develop.
  • The stale map also poisons diagnosis: this lane initially mis-filed five phantom keys as "orphan skin files to delete" — a hygiene ticket for files that do not exist. A map that cannot forget manufactures false substrate.

The documented watcher behavior is the correct semantics already: learn/guides/uibuildingblocks/StylingAndTheming.md §watch-themes — "each structural pass replaces both development copies of theme-map.json from the effective framework-plus-workspace SCSS tree." The full build should honor the same contract.

The Architectural Reality

  • buildScripts/build/themes.mjs:22-24 (scssPath, themeMapFile), :101-113 (map population via create-only ns), :208-225 (getThemeMap seeding from the previous artifact), :324-333 (three-copy emission).
  • The neoPath fallback in getThemeMap exists for the workspace overlay case (guide §8: a workspace build merges the engine's map so engine components stay resolvable). That merge direction is legitimate; seeding from the OWN tree's previous output is the defect.
  • Runtime consumer: src/worker/App.mjs:477-535 (insertThemeFilesNeo.cssMap.fileInfo lookup → Stylesheet.addThemeFiles).

The Fix

In build-themes' full/structural pass, build the map fresh from the effective SCSS tree: start from {} (plus the neoPath engine-map merge ONLY when building inside a workspace, i.e. when cwd !== neoPath), then populate from the actual file scan. Deletions and renames then land on the next build, matching the watcher's documented replace semantics.

Two riders in the same PR (small, same substrate, discovered by the same sweep):

  1. Redundant appThemeFolder self-setsapps/agentos/app.mjs:175 ('agentos', app name AgentOS) and apps/workstation/app.mjs:30 ('workstation', app name Workstation) both equal the lowercase-classRoot fallback in src/worker/App.mjs:499 and are dead config (verified: names read from both app.mjs files). Remove both; the childapp consumers (sharedcovid/colors/shareddialog/agentos-widget/dockdemo) keep theirs — those are the real users.
  2. Guide duplicationlearn/guides/uibuildingblocks/StylingAndTheming.md carries §4 twice verbatim ("SCSS File & Namespace Mapping", lines ~179-197 ≈ ~199-217) and two sections numbered "## 9." (Build Process; Lazy Loading). Deduplicate + renumber. (The folder names in the guide are correct — theme-light/theme-dark exist beside theme-neo-*; an earlier read of this lane wrongly flagged them.)

Acceptance Criteria

  • AC1: After a full build-themes run on a tree where a previously-mapped SCSS file was deleted, the emitted maps (all three copies) no longer contain its key. Regression pin: a spec that builds a map, removes a fixture file, rebuilds, and asserts the key is gone.
  • AC2: Workspace overlay behavior preserved: building in a workspace still merges the engine's map so engine components resolve (guide §8 contract) — covered by an assertion or a documented manual receipt.
  • AC3: On this repo's tree, the five fossil keys (InterventionPanel, StrategyCardPanel, StrategyPanel, DockPreview, FleetSettingsPanel under apps.agentos) are absent from a fresh build's output.
  • AC4: The two appThemeFolder self-sets are removed; the agentos and workstation apps' skins still load (dev smoke: both apps render themed).
  • AC5: The guide carries §4 once, sequential section numbers, and the watcher/full-build replace semantics stated for BOTH paths.

Out of Scope

  • The dockdemo childapp's unreachable view/-nested SCSS — that whole childapp is deletion-bound via #16322 (its dead paths are noted there).
  • fleet/Chips.scss — resolved via additionalThemeFiles in PR #17219.
  • The .fm-freshness shared-partial extraction (Freshness.scss) — contingent on PR #17219's Round-1 verdict; if the reviewer does not pull it into that PR, it lands as its own follow-up, not here (different substrate: app skin vs build script).

Avoided Traps

  • "Delete the five orphan files" — the files do not exist; the first prescription of this lane was falsified by its own sweep. The defect is the map's memory, not the tree's content.
  • Fixing by committing the mapresources/theme-map.json is gitignored by design (build artifact); versioning it would trade fossil-keys for merge-conflict churn and CI/local divergence.
  • Deleting the neoPath fallback entirely — it serves the workspace overlay; the fix scopes the seed, it does not remove the merge.

Related

#17209 (the lane that surfaced it) · PR #17219 (Chips/additionalThemeFiles resolution) · #16322 (dockdemo relocation owns its dead skins) · #17203 / #17200 (theme-guard siblings, different scopes — staleness teaching and guard coverage; no overlap with map regeneration).

Live latest-open sweep: latest 20 open checked 2026-08-15T23:0xZ (nearest: #17203/#17200 — guard scopes, no overlap); no equivalent. A2A claim sweep (last 15, all read-states, herd window): only #17201 (Iris, block-alignment) — no overlap.

Origin Session ID: c89f485a-6d12-4c49-bf55-050bb1415f40 Retrieval Hint: "theme-map fossil keys additive merge getThemeMap deleted skins never leave"

tobiu referenced in commit 1c9fd05 - "fix(build): build-themes reseeds the theme-map from the effective SCSS tree (#17222) (#17228)" on Aug 16, 2026, 2:34 AM
tobiu closed this issue on Aug 16, 2026, 2:34 AM