Compare commits

...

37 Commits

Author SHA1 Message Date
GitHub Action
7f0fd79a76 [automated] Apply ESLint and Oxfmt fixes 2026-04-15 04:51:24 +00:00
Alexander Brown
bdced9ce86 fix: add architecture-adventure workspace to knip config
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:03:09 -07:00
Alexander Brown
5d83ee5f3c feat(adventure): scaffold Vite project for Codebase Caverns v2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 16:58:54 -07:00
Alexander Brown
cd7c28ed92 Add .superpowers to gitignore 2026-03-26 16:55:16 -07:00
Alexander Brown
0b425dfd80 docs: clarify ECS instance scoping, subgraph recursion, layout decoupling, and phase gates
Amp-Thread-ID: https://ampcode.com/threads/T-019d2b99-d068-77aa-be88-5846f1e74e54
Co-authored-by: Amp <amp@ampcode.com>
2026-03-26 16:55:15 -07:00
Alexander Brown
0a880faad8 docs: add Command Forge room and extension migration to adventure game
Convert the Side Panel lore room into "The Command Forge" with a new
challenge teaching the Command Pattern / World API layering from the
ecs-world-command-api.md addendum. Enrich the Services room with
extension migration patterns from the updated migration plan.

- 8 challenges -> 9 (new: The Mutation Gateway)
- 13 artifacts -> 16 (CommandExecutor, Command Interface, Extension Migration Guide)
- Generate pixel art icons for new artifacts and choice buttons
- Update room image to command forge theme
- Recalculate speedrun route and spaghetti singularity path
- Update walkthrough with new challenge solutions and stats

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:55:08 -07:00
Alexander Brown
b8f26177c9 fix: address remaining ECS ADR review docs feedback
Amp-Thread-ID: https://ampcode.com/threads/T-019d275e-a5aa-7057-8699-f403397f0df2
Co-authored-by: Amp <amp@ampcode.com>
2026-03-26 16:55:08 -07:00
Alexander Brown
244e549116 docs: add World command API addendum and cross-reference from ADR 0008
- Add ecs-world-command-api.md showing how imperative World calls translate
  to serializable commands executed by systems
- Add 'Relationship to ADR 0003' section to ADR 0008 clarifying the
  complementary layering: Commands (intent) → Systems (handlers) → World (store)

Amp-Thread-ID: https://ampcode.com/threads/T-019d270c-1975-7590-aaae-551eb71b26ff
Co-authored-by: Amp <amp@ampcode.com>
2026-03-26 16:55:07 -07:00
Alexander Brown
d7f425b51b feat: add 5 missing adventure game icons
Generate subgraph challenge choice icons (a/b/c) and artifact icons
(subgraph-structure, typed-contracts) to complete the icon set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:55:06 -07:00
Alexander Brown
1a9091605a More friendly styles, use the whole screen. 2026-03-26 16:55:05 -07:00
DrJKL
805ff2db31 feat: add Subgraph Depths room, improve accessibility and mobile layout
- Add "The Subgraph Depths" room with Widget Promotion Decision
  challenge reflecting the new subgraph boundaries and graph
  unification design from ADR0008
- Update ECS room description to reference 6 entity kinds, flat
  World with graphScope tags, and subgraph-as-component model
- Update Component Gallery challenge to reference graph unification
- Fix WCAG contrast: --muted #8b949e -> #9ea7b0 (AAA on surface),
  .dec-slot.empty text from invisible --border to --muted
- Fix mobile HUD: compact layout with single-letter stat labels,
  reduced padding, smaller buttons
- Fix mobile sidebar: full-width via align-self:stretch, was
  constrained by align-self:flex-start from desktop sticky mode
- Fix map card structure: challenge badge in flex row header for
  consistent alignment across cards with/without challenges
- Update walkthrough: 10 rooms, 8 challenges, 13 artifacts,
  new optimal route, updated Spaghetti path and playthrough count

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:55:04 -07:00
GitHub Action
ac5c291990 [automated] Apply ESLint and Oxfmt fixes 2026-03-26 16:54:58 -07:00
DrJKL
e2f0add917 docs: add subgraph boundaries doc, unify graph model across ECS architecture
- New companion doc: subgraph-boundaries-and-promotion.md
  - Graph model unification: all graphs are isomorphic, 7→6 entity kinds
  - Typed boundary contracts replace virtual nodes and magic IDs
  - Widget promotion as open decision (connections-only vs simplified)
  - Serialization boundary with indefinite backward-compatible loading
- Update ADR 0008: remove SubgraphEntityId, add GraphId scope, flat World
- Update ecs-target-architecture: World diagram, Entity IDs, problem map
- Update ecs-migration-plan: Phase 1a/1c types and World interface
- Update ecs-lifecycle-scenarios: pack/unpack use graphScope re-parenting
- Update proto-ecs-stores and entity-interactions: annotate subgraph rows

Amp-Thread-ID: https://ampcode.com/threads/T-019d2311-3707-746a-ae9c-65e6f0d67f3e
Co-authored-by: Amp <amp@ampcode.com>
2026-03-26 16:54:57 -07:00
DrJKL
dadc5a076f fix: strip leading whitespace from multi-line mermaid labels in architecture docs
Amp-Thread-ID: https://ampcode.com/threads/T-019d2303-d3a2-7659-9beb-24fc8b6813ee
Co-authored-by: Amp <amp@ampcode.com>
2026-03-26 16:54:50 -07:00
DrJKL
ddb6562ff6 fix: tighten ECS architecture docs and migration gates
Amp-Thread-ID: https://ampcode.com/threads/T-019d22e0-5d0a-767a-92c5-84ad5f2a4d00
Co-authored-by: Amp <amp@ampcode.com>
2026-03-26 16:54:39 -07:00
Alexander Brown
7294072f9e fix: replace blanket CSS reset with modern reset, fix text jiggle
- Modern CSS reset (Comeau/Bell style) replaces * { margin: 0; padding: 0 }
- Restore dialog margin: auto for proper centering
- Remove translateY from room transitions to prevent text jiggle
- Opacity-only fade for clean room changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:20 -07:00
Alexander Brown
3ee2cd37c8 fix: correct Spaghetti Singularity walkthrough path
The previous guide suggested picking all bad-rated answers, but
Litegraph A (Rewrite, Debt -20) and Services B (Big bang, Debt -10)
actually lower debt, making it impossible to reach Debt >= 70.

Replaced with a proven path using debt-adding choices (C/C/C/C/C/B/B)
that reaches Debt 95. Added warning about the counterintuitive trap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:19 -07:00
Alexander Brown
cf4bf78aa0 fix: align map arrows and normalize challenge boxes in walkthrough
Map: all pipe bodies now align with arrow heads at same column.
Challenge boxes: all lines normalized to 72 chars wide.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:18 -07:00
Alexander Brown
08eee56a68 fix: normalize ending box widths to 72 chars in walkthrough
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:17 -07:00
Alexander Brown
c3617e0af8 fix: align ASCII art in walkthrough (map, ending boxes, building)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:16 -07:00
Alexander Brown
7c976e128e docs: add GameFAQs-style walkthrough with ASCII art and pro tips
Complete spoiler guide: all 7 challenge solutions, optimal speedrun
route, all 4 endings, achievement unlock strategies, artifact
checklist, and architecture reading order recommendations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:16 -07:00
Alexander Brown
886bc1ef7c refactor: convert map and ending overlays to native dialog elements
- Map uses <dialog> with showModal()/close(), click-outside-to-close
- Ending uses <dialog> with Escape blocked for real endings, allowed for previews
- CSS uses @starting-style for smooth open/close transitions
- Removes manual backdrop div and z-index management
- Native focus trapping and accessibility from <dialog>

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:15 -07:00
Alexander Brown
1cfaaac511 fix: previewing an ending mid-game closes without resetting state
Button shows "Close" for previews, "Play Again" for real endings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:14 -07:00
Alexander Brown
a984872ae3 feat: click unlocked ending badges to review their ending screen
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:13 -07:00
Alexander Brown
b78db34e42 feat: localStorage persistence, restart button, and ending achievements
- Game state persists across page refreshes via localStorage
- Restart button in HUD resets game without losing achievements
- Endings sidebar tracks which of the 4 endings have been unlocked
- Achievement icons generated for each ending (locked/unlocked states)
- Achievements persist across runs in separate storage key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:12 -07:00
Alexander Brown
6c89fa9385 fix: add missing room variable in renderRoom, improve room transitions
Split render into render() (animation) and renderRoom() (content swap).
Fade-out slides up, fade-in slides down for a smoother feel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:11 -07:00
Alexander Brown
1471684821 feat: add Open Graph and Twitter Card meta tags for rich previews
Uses entry room pixel art as preview image via raw.githubusercontent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:11 -07:00
Alexander Brown
94791b4e65 feat: make Log section collapsible with smooth animation
Uses native <details>/<summary> with interpolate-size for smooth
height transition. Chevron indicator rotates on open/close.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:10 -07:00
Alexander Brown
fc8775bf38 feat: preload images for adjacent rooms to prevent flash on navigate
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:09 -07:00
Alexander Brown
6e81d71f1b feat: add inline emoji favicon for adventure game
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:08 -07:00
Alexander Brown
f92205ca46 feat: challenge choice icons and decisions tracker in sidebar
- 20 choice icons generated (one per A/B/C option across 7 challenges)
- Challenge buttons now card-style columns with icon, key badge overlay, and text
- Decisions sidebar section shows icon grid of choices made so far
- Slots colored by rating (green/yellow/red), hover label shows details
- Unified icon generation script handles both artifact and choice prompts
- Choice icon prompt reference JSON

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:07 -07:00
Alexander Brown
96c737bc0e feat: graphical inventory with generated pixel art icons
- 11 artifact icons generated via Z-Image Turbo (128x128)
- Inventory displays as icon grid with hover label strip
- Artifacts in rooms show icon thumbnails alongside name/type
- Icon generation script with skip-existing logic
- Prompt reference JSON for all artifact icons
- Fix inventory not resetting on Play Again
- Add user-select: none to presentational elements

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:07 -07:00
Alexander Brown
301cdda9f0 feat: add educational callouts, scorecard, GitHub links, and fix layout
- Link code references to actual files on GitHub main branch
- Show recommended answer + architecture doc link after each challenge
- End-game scorecard recaps all 7 decisions vs. recommended approach
- Challenge progress counter (0/7) in HUD
- Map is now a centered overlay with backdrop (Escape to close)
- Page scrolls naturally; sidebar sticks; no more clipped content
- Subtle fade transition between rooms

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:06 -07:00
Alexander Brown
91fd59f246 feat: add decision system, stats, generated images to architecture adventure
- 7 architectural challenge encounters with branching choices and stat effects
- 4 tracked stats: Technical Debt, Code Quality, Team Morale, ECS Migration
- 4 endings based on accumulated decisions (Spaghetti Singularity through ECS Enlightenment)
- 9 pixel art room images generated via Z-Image Turbo (1152x640, no LoRA)
- Image generation script (generate-images.py) and prompt reference JSON
- Keyboard navigation for challenges (A/B/C) and rooms (1/2/3)
- Map badges showing pending/completed challenge status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:05 -07:00
Claude
0b924e3ae1 feat: add architecture adventure game skeleton
Interactive HTML game for exploring the ComfyUI frontend architecture.
Includes 9 rooms covering entry point, components, stores, services,
litegraph engine, ECS, renderer, composables, and side panel layers.
Keyboard navigation, map view, inventory, and activity log.

https://claude.ai/code/session_01PivAjqNcLVzZVF3C5irFdW
2026-03-26 16:54:04 -07:00
Alexander Brown
b468ea83ea docs: draft ECS component interfaces and World type
Example interfaces for all 7 entity kinds (Node, Link, Slot, Widget,
Reroute, Group, Subgraph), branded entity ID types with cast helpers,
and a Map-backed World implementation. Reuses existing litegraph types
(Point, Size, INodeFlags, ISlotType, etc.) for migration compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:04 -07:00
Alexander Brown
5892720f69 docs: draft ECS component interfaces and World type
Example interfaces for all 7 entity kinds (Node, Link, Slot, Widget,
Reroute, Group, Subgraph), branded entity ID types with cast helpers,
and a Map-backed World implementation. Reuses existing litegraph types
(Point, Size, INodeFlags, ISlotType, etc.) for migration compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:54:03 -07:00
82 changed files with 5474 additions and 1 deletions

1
.gitignore vendored
View File

@@ -66,6 +66,7 @@ dist.zip
/temp/
/tmp/
.superpowers/
# Generated JSON Schemas
/schemas/

View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Codebase Caverns — ComfyUI Architecture Adventure</title>
<meta
name="description"
content="A prestige-driven architecture adventure game. Discover problems, learn patterns, make decisions, and watch the consequences unfold."
/>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,60 @@
{
"name": "@comfyorg/architecture-adventure",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "catalog:",
"vite": "catalog:"
},
"nx": {
"tags": [
"scope:docs",
"type:app"
],
"targets": {
"dev": {
"executor": "nx:run-commands",
"continuous": true,
"options": {
"cwd": "apps/architecture-adventure",
"command": "vite"
}
},
"build": {
"executor": "nx:run-commands",
"cache": true,
"options": {
"command": "vite build --config apps/architecture-adventure/vite.config.ts"
},
"outputs": [
"{projectRoot}/dist"
]
},
"preview": {
"executor": "nx:run-commands",
"continuous": true,
"dependsOn": [
"build"
],
"options": {
"cwd": "apps/architecture-adventure",
"command": "vite preview"
}
},
"typecheck": {
"executor": "nx:run-commands",
"cache": true,
"options": {
"cwd": "apps/architecture-adventure",
"command": "tsc --noEmit"
}
}
}
}
}

View File

@@ -0,0 +1,7 @@
function main(): void {
const app = document.getElementById('app')
if (!app) throw new Error('Missing #app element')
app.textContent = 'Codebase Caverns v2 — Loading...'
}
main()

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2023",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"paths": {
"@/*": ["./src/*"]
},
"baseUrl": "."
},
"include": ["src/**/*.ts", "vite.config.ts"]
}

View File

@@ -0,0 +1,26 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'
const projectRoot = fileURLToPath(new URL('.', import.meta.url))
export default defineConfig({
root: projectRoot,
base: './',
build: {
target: 'es2022',
outDir: 'dist',
assetsInlineLimit: 1_000_000,
cssCodeSplit: false,
rolldownOptions: {
output: {
inlineDynamicImports: true
}
}
},
resolve: {
alias: {
'@': path.resolve(projectRoot, 'src')
}
}
})

View File

@@ -0,0 +1,683 @@
===============================================================================
____ _ _ ____
/ ___|___ __| | ___| |__ __ _ ___ ___ / ___|__ ___ _____ _ __ _ __ ___
| | / _ \ / _` |/ _ \ '_ \ / _` / __|/ _ \ | | / _` \ \ / / _ \ '__| '_ \/ __|
| |__| (_) | (_| | __/ |_) | (_| \__ \ __/ | |__| (_| |\ V / __/ | | | | \__ \
\____\___/ \__,_|\___|_.__/ \__,_|___/\___| \____\__,_| \_/ \___|_| |_| |_|___/
ComfyUI Frontend Architecture Adventure - Complete Walkthrough
===============================================================================
Platform: Web Browser (any modern browser)
Version: 1.0
Author: An Architect Who Has Seen Things
Last Updated: 2026-03-24
Spoilers: YES. This guide contains ALL solutions and ALL endings.
===============================================================================
TABLE OF CONTENTS
===============================================================================
I. Introduction & Controls
II. Game Mechanics
III. Room Guide & Map
IV. Challenge Solutions (SPOILERS)
V. Optimal Route - "The ECS Enlightenment" Speedrun
VI. All Four Endings
VII. Achievements
VIII. Artifacts Checklist
IX. Pro Tips & Secrets
===============================================================================
I. INTRODUCTION & CONTROLS
===============================================================================
Codebase Caverns is an interactive choose-your-own-adventure game that
teaches you the architecture of the ComfyUI frontend codebase. You explore
10 rooms representing different architectural layers, face 9 real engineering
challenges, collect artifacts, and reach one of 4 endings based on your
decisions.
Every challenge in this game is based on REAL architectural problems
documented in the ComfyUI frontend repo. The "correct" answers match the
actual migration strategy being used in production.
CONTROLS:
=========
1, 2, 3 Navigate between rooms (press the number key)
A, B, C Choose a challenge option (press the letter key)
M Toggle the map overlay
Escape Close the map / close ending preview
BUTTONS:
========
Map [M] Opens the room map overlay
Restart Resets the current run (keeps achievements)
Play Again After an ending, starts a new run
Your progress auto-saves to localStorage. Close the tab and come back
later - you'll pick up right where you left off.
===============================================================================
II. GAME MECHANICS
===============================================================================
STATS
=====
You have four stats tracked in the HUD at the top:
Debt [||||||||..] 50 Technical debt. LOWER is better.
Quality [|||.......] 30 Code quality. HIGHER is better.
Morale [||||||....] 60 Team morale. HIGHER is better.
ECS [.........] 0/5 Migration progress. 5 is max.
Each challenge choice modifies these stats. Your final stats determine
which of the 4 endings you get.
CHALLENGES
==========
9 of the 10 rooms contain a one-time challenge - an architectural dilemma
with 2-3 options. Each option has a rating:
[GOOD] Best practice. Matches the real migration strategy.
Usually: Debt down, Quality up, +1 ECS progress.
[OK] Pragmatic but imperfect. Gets the job done.
Mixed stat effects.
[BAD] Tempting but harmful. Short-term gain, long-term pain.
Usually: Debt up or Morale down.
After choosing, you see your result, the recommended answer, and a link
to the real architecture documentation that explains why.
ARTIFACTS
=========
Rooms contain collectible artifacts - key files and concepts from the
codebase. These are auto-collected when you enter a room. They appear
as icons in your Inventory sidebar.
ENDINGS
=======
After resolving all 8 challenges, you get one of 4 endings based on
your accumulated stats. See Section VI for details.
ACHIEVEMENTS
============
Each ending you reach is permanently saved as an achievement badge.
Achievements persist across runs - even after restarting. Click an
unlocked badge to review that ending's screen.
===============================================================================
III. ROOM GUIDE & MAP
===============================================================================
+-------------------+
| ENTRY POINT |
| (src/main.ts) |
+-+--------+------+-+
| | |
+----------+ | +-----------+
| | |
+---v----------+ +-----v--------+ +------v---------+
| COMPONENT | | STORE | | SERVICE |
| GALLERY | | VAULTS | | CORRIDORS |
| [Challenge] | | [Challenge] | | [Challenge] |
+--+------+----+ +--+------+----+ +--------+-------+
| | | | |
| | +----v---+ | +------v-------+
| | | ECS | | | COMPOSABLES |
| | | CHAMB. | | | WORKSHOP |
| | | [Chal] | | | [Challenge] |
| | +---+----+ | +--------------+
| | | +----v------+
| | +----v--+--+ |
| | |SUBGRAPH| RENDERER |
| | | DEPTHS | OVERLOOK |
| | | [Chal] | [Chal] |
| | +--------+----------+
| |
+--v------v----+
| LITEGRAPH |
| ENGINE |
| [Challenge] |
+------+-------+
|
+------v-------+
| COMMAND |
| FORGE |
| [Challenge] |
+--------------+
ROOM DETAILS:
=============
1. THE ENTRY POINT [src/main.ts]
No challenge. No artifacts. Starting room.
Exits: Components (1), Stores (2), Services (3)
2. THE COMPONENT GALLERY [Presentation]
Challenge: The Circular Dependency
Artifacts: GraphView.vue
Exits: Litegraph (1), Command Forge (2), Entry (3)
3. THE STORE VAULTS [State]
Challenge: The Scattered Mutations
Artifacts: widgetValueStore.ts, layoutStore.ts
Exits: ECS (1), Renderer (2), Entry (3)
4. THE SERVICE CORRIDORS [Services]
Challenge: The Migration Question
Artifacts: litegraphService.ts, Extension Migration Guide
Exits: Composables (1), Entry (2)
5. THE LITEGRAPH ENGINE ROOM [Graph Engine]
Challenge: The God Object Dilemma
Artifacts: LGraphCanvas.ts, LGraphNode.ts
Exits: ECS (1), Components (2), Entry (3)
6. THE ECS ARCHITECT'S CHAMBER [ECS]
Challenge: The ID Crossroads
Artifacts: World Registry, Branded Entity IDs
Exits: Subgraph Depths (1), Renderer (2), Entry (3)
7. THE SUBGRAPH DEPTHS [Graph Boundaries]
Challenge: The Widget Promotion Decision
Artifacts: SubgraphStructure, Typed Interface Contracts
Exits: ECS (1), Litegraph (2), Entry (3)
8. THE RENDERER OVERLOOK [Renderer]
Challenge: The Render-Time Mutation
Artifacts: QuadTree Spatial Index, Y.js CRDT Layout
Exits: ECS (1), Entry (2)
9. THE COMPOSABLES WORKSHOP [Composables]
Challenge: The Collaboration Protocol
Artifacts: useCoreCommands.ts
Exits: Stores (1), Entry (2)
10. THE COMMAND FORGE [Commands & Intent]
Challenge: The Mutation Gateway
Artifacts: CommandExecutor, Command Interface
Exits: Components (1), Stores (2), Entry (3)
===============================================================================
IV. CHALLENGE SOLUTIONS (SPOILERS)
===============================================================================
*** WARNING: FULL SOLUTIONS BELOW ***
*** SCROLL PAST SECTION VI IF YOU WANT TO PLAY BLIND ***
.--------------------------------------------------------------------.
| CHALLENGE 1: The Circular Dependency | Room: Components |
|------------------------------------------------------------------ |
| Subgraph extends LGraph, but LGraph creates Subgraph instances. |
| Circular import forces order-dependent barrel exports. |
|------------------------------------------------------------------ |
| |
| >>> A. Composition over inheritance [GOOD] <<< |
| Debt -10, Quality +15, Morale +5, ECS +1 |
| A subgraph IS a graph - just a node with SubgraphStructure. |
| Under graph unification, no class inheritance at all. |
| |
| B. Barrel file reordering [BAD] |
| Debt +10, Quality -5, Morale -5 |
| Band-aid. The coupling remains and will break again. |
| |
| C. Factory injection [OK] |
| Debt -5, Quality +10 |
| Pragmatic fix but classes stay coupled at runtime. |
'--------------------------------------------------------------------'
.--------------------------------------------------------------------.
| CHALLENGE 2: The Scattered Mutations | Room: Stores |
|------------------------------------------------------------------ |
| graph._version++ appears in 19 locations across 7 files. |
| One missed site = silent data loss. |
|------------------------------------------------------------------ |
| |
| >>> A. Centralize into graph.incrementVersion() [GOOD] <<< |
| Debt -15, Quality +15, ECS +1 |
| This is Phase 0a of the real migration plan. |
| 19 sites -> 1 method. Auditable change tracking. |
| |
| B. Add a JavaScript Proxy [OK] |
| Debt +5, Quality +5, Morale -5 |
| Catches mutations but adds opaque runtime overhead. |
| |
| C. Leave it as-is [BAD] |
| Debt +10, Morale +5 |
| "It works, don't touch it" - until it doesn't. |
'--------------------------------------------------------------------'
.--------------------------------------------------------------------.
| CHALLENGE 3: The Migration Question | Room: Services |
|------------------------------------------------------------------ |
| Legacy litegraph works. How to migrate to ECS without breaking |
| production for thousands of users? |
|------------------------------------------------------------------ |
| |
| >>> A. 5-phase incremental plan [GOOD] <<< |
| Quality +15, Morale +10, ECS +1 |
| Foundation -> Types -> Bridge -> Systems -> Legacy Removal. |
| Each phase independently shippable. This is the real plan. |
| |
| B. Big bang rewrite [BAD] |
| Debt -10, Quality +5, Morale -20 |
| Feature freeze + scope creep + burnout = disaster. |
| |
| C. Strangler fig pattern [OK] |
| Quality +10, Morale +5 |
| Solid pattern but lacks clear milestones without a plan. |
'--------------------------------------------------------------------'
.--------------------------------------------------------------------.
| CHALLENGE 4: The God Object Dilemma | Room: Litegraph |
|------------------------------------------------------------------ |
| LGraphCanvas: ~9,100 lines. LGraphNode: ~4,300 lines. |
| God objects mixing rendering, serialization, connectivity, etc. |
|------------------------------------------------------------------ |
| |
| A. Rewrite from scratch [BAD] |
| Debt -20, Quality +5, Morale -25 |
| Heroic rewrite stalls at month three. Team burns out. |
| |
| >>> B. Extract incrementally [GOOD] <<< |
| Debt -10, Quality +15, Morale +5, ECS +1 |
| Position -> Connectivity -> Rendering. Small testable PRs. |
| This matches the actual migration strategy. |
| |
| C. Add a facade layer [OK] |
| Debt +5, Quality +5, Morale +10 |
| Nicer API but complexity lives behind the facade. |
| |
| NOTE: This is the only challenge where A is NOT the best answer! |
'--------------------------------------------------------------------'
.--------------------------------------------------------------------.
| CHALLENGE 5: The ID Crossroads | Room: ECS |
|------------------------------------------------------------------ |
| NodeId is number | string. Nothing prevents passing a LinkId |
| where a NodeId is expected. Six entity kinds share one ID space. |
|------------------------------------------------------------------ |
| |
| >>> A. Branded types with cast helpers [GOOD] <<< |
| Debt -15, Quality +20, ECS +1 |
| type NodeEntityId = number & { __brand: 'NodeEntityId' } |
| Compile-time safety, zero runtime cost. Phase 1a. |
| |
| B. String prefixes at runtime [OK] |
| Debt +5, Quality +5, Morale -5 |
| "node:42" - parsing overhead everywhere. |
| |
| C. Keep plain numbers [BAD] |
| Debt +15, Quality -5 |
| "Just be careful" - someone WILL pass the wrong ID. |
'--------------------------------------------------------------------'
.--------------------------------------------------------------------.
| CHALLENGE 6: The Widget Promotion Decision | Room: Subgraph |
|------------------------------------------------------------------ |
| A user promotes a widget from inside a subgraph to the parent. |
| Today this needs PromotionStore + ViewManager + PromotedWidgetView |
| — a parallel state system. Two ECS candidates. |
|------------------------------------------------------------------ |
| |
| >>> A. Connections-only: promotion = typed input [GOOD] <<< |
| Debt -15, Quality +15, Morale +5, ECS +1 |
| Promotion = adding an interface input. Type->widget mapping |
| creates the widget automatically. Eliminates PromotionStore, |
| ViewManager, and PromotedWidgetView entirely. |
| |
| B. Simplified component promotion [OK] |
| Debt -5, Quality +10, Morale +5 |
| WidgetPromotion component on widget entities. Removes |
| ViewManager but keeps promotion as a distinct concept. |
| Shared subgraph instance ambiguity remains. |
| |
| C. Keep the current three-layer system [BAD] |
| Debt +10, Quality -5, Morale -5 |
| The parallel state system persists indefinitely. |
'--------------------------------------------------------------------'
.--------------------------------------------------------------------.
| CHALLENGE 7: The Render-Time Mutation | Room: Renderer |
|------------------------------------------------------------------ |
| drawNode() calls _setConcreteSlots() and arrange() during the |
| render pass. Draw order affects layout. Classic mutation-in- |
| render bug. |
|------------------------------------------------------------------ |
| |
| >>> A. Separate update and render phases [GOOD] <<< |
| Debt -15, Quality +15, ECS +1 |
| Input -> Update (layout) -> Render (read-only). |
| Matches the ECS system pipeline design. |
| |
| B. Dirty flags and deferred render [OK] |
| Debt -5, Quality +5, Morale +5 |
| Reduces symptoms but render pass can still mutate. |
| |
| NOTE: Only 2 options here. Both are reasonable; A is optimal. |
'--------------------------------------------------------------------'
.--------------------------------------------------------------------.
| CHALLENGE 8: The Collaboration Protocol | Room: Composables |
|------------------------------------------------------------------ |
| Multiple users want to edit the same workflow simultaneously. |
| layoutStore already extracts position data. How to sync? |
|------------------------------------------------------------------ |
| |
| >>> A. Y.js CRDTs [GOOD] <<< |
| Debt -10, Quality +15, Morale +10 |
| Conflict-free replicated data types. Already proven. |
| This is what the real layoutStore uses. |
| |
| B. Polling-based sync [BAD] |
| Debt +10, Quality -5, Morale -5 |
| Flickering, lag, silent data loss. Support nightmare. |
| |
| C. Skip collaboration for now [OK] |
| Morale +5 |
| Pragmatic delay but cloud team won't be happy. |
'--------------------------------------------------------------------'
.--------------------------------------------------------------------.
| CHALLENGE 9: The Mutation Gateway | Room: Command Forge |
|------------------------------------------------------------------ |
| The World's imperative API (world.setComponent()) vs. the command |
| pattern requirement from ADR 0003. How should external callers |
| mutate the World? |
|------------------------------------------------------------------ |
| |
| >>> A. Commands as intent; systems as handlers; World as store <<< |
| Debt -10, Quality +15, Morale +5, ECS +1 [GOOD] |
| Caller -> Command -> System -> World -> Y.js. Commands are |
| serializable. ADR 0003 and ADR 0008 are complementary. |
| |
| B. Make World.setComponent() itself serializable [OK] |
| Debt +5, Quality +5, Morale -5 |
| Conflates store with command layer. Batch ops become noisy. |
| |
| C. Skip commands - let callers mutate directly [BAD] |
| Debt +15, Quality -10 |
| No undo/redo, no replay, no CRDT sync, no audit trail. |
'--------------------------------------------------------------------'
===============================================================================
V. OPTIMAL ROUTE - "THE ECS ENLIGHTENMENT" SPEEDRUN
===============================================================================
This route hits all 8 challenges picking the GOOD answer, collecting
all 13 artifacts, visiting all 10 rooms. Order matters for efficiency
(fewest key presses).
Starting stats: Debt 50, Quality 30, Morale 60, ECS 0/5
ENTRY POINT
Press 1 -> Component Gallery
COMPONENT GALLERY
Challenge: The Circular Dependency -> Press A (Composition)
[Debt 40, Quality 45, Morale 65, ECS 1/5]
Press 2 -> Command Forge
THE COMMAND FORGE
Challenge: The Mutation Gateway -> Press A (Commands as intent)
[Debt 30, Quality 60, Morale 70, ECS 2/5]
Press 2 -> Store Vaults
STORE VAULTS
Challenge: The Scattered Mutations -> Press A (Centralize)
[Debt 15, Quality 75, Morale 70, ECS 3/5]
Press 1 -> ECS Chamber
ECS ARCHITECT'S CHAMBER
Challenge: The ID Crossroads -> Press A (Branded types)
[Debt 0, Quality 95, Morale 70, ECS 4/5]
Press 1 -> Subgraph Depths
SUBGRAPH DEPTHS
Challenge: The Widget Promotion Decision -> Press A (Connections-only)
[Debt 0, Quality 100, Morale 75, ECS 5/5]
Press 1 -> ECS Chamber
Press 2 -> Renderer
RENDERER OVERLOOK
Challenge: The Render-Time Mutation -> Press A (Separate phases)
[Debt 0, Quality 100, Morale 75, ECS 5/5]
Press 2 -> Entry Point
ENTRY POINT
Press 3 -> Services
SERVICE CORRIDORS
Challenge: The Migration Question -> Press A (5-phase plan)
[Debt 0, Quality 100, Morale 85, ECS 5/5]
Press 1 -> Composables
COMPOSABLES WORKSHOP
Challenge: The Collaboration Protocol -> Press A (Y.js CRDTs)
[Debt 0, Quality 100, Morale 95, ECS 5/5]
Press 2 -> Entry Point
ENTRY POINT
Press 1 -> Components
Press 1 -> Litegraph
LITEGRAPH ENGINE ROOM
Challenge: The God Object Dilemma -> Press B (Extract incrementally)
[Debt 0, Quality 100, Morale 100, ECS 5/5]
FINAL STATS: Debt 0 | Quality 100 | Morale 100 | ECS 5/5
*** ENDING: THE ECS ENLIGHTENMENT ***
Total key presses: 28 (including challenge answers)
Rooms visited: 10/10
Artifacts: 16/16
Challenges: 9/9 correct
===============================================================================
VI. ALL FOUR ENDINGS
===============================================================================
Endings are checked in order. First match wins.
.--------------------------------------------------------------------.
| |
| ENDING 1: THE ECS ENLIGHTENMENT [BEST] |
| |
| Requirements: Debt < 25 AND Quality >= 75 AND Morale >= 60 |
| |
| "The World registry hums with clean data. Node removal: |
| 30 lines instead of 107. Serialization: one system instead |
| of six scattered methods. Branded IDs catch bugs at compile |
| time. Y.js CRDTs enable real-time collaboration. The team |
| ships features faster than ever." |
| |
| HOW TO GET IT: Pick ALL good answers. Hard to miss if you |
| read the hints carefully. |
'--------------------------------------------------------------------'
.--------------------------------------------------------------------.
| |
| ENDING 2: THE CLEAN ARCHITECTURE [GOOD] |
| |
| Requirements: Debt < 40 AND Quality >= 50 |
| |
| "The migration completes on schedule. Systems hum along, |
| the ECS World holds most entity state, and the worst god |
| objects have been tamed." |
| |
| HOW TO GET IT: Pick mostly good answers, 1-2 OK answers. |
'--------------------------------------------------------------------'
.--------------------------------------------------------------------.
| |
| ENDING 3: THE ETERNAL REFACTOR [MEH] |
| |
| Requirements: Debt < 70 |
| |
| "The migration... continues. Every sprint has a 'cleanup' |
| ticket that never quite closes." |
| |
| HOW TO GET IT: Mix of OK and BAD answers. The "safe" middle. |
'--------------------------------------------------------------------'
.--------------------------------------------------------------------.
| |
| ENDING 4: THE SPAGHETTI SINGULARITY [WORST] |
| |
| Requirements: Debt >= 70 (catch-all) |
| |
| "The god objects grew sentient. LGraphCanvas hit 12,000 lines |
| and developed a circular dependency with itself." |
| |
| HOW TO GET IT: Pick all BAD answers. You have to try. |
| Starting debt is 50, so you need +20 from bad choices. |
'--------------------------------------------------------------------'
===============================================================================
VII. ACHIEVEMENTS
===============================================================================
Achievements are permanently saved across runs. You need 4 playthroughs
(minimum) to unlock all endings, since each run can only reach one.
[x] The ECS Enlightenment - All good answers
[x] The Clean Architecture - Mostly good, few OK
[x] The Eternal Refactor - Mix of OK and bad
[x] The Spaghetti Singularity - Maximize debt (see pro tip below)
Click any unlocked achievement badge in the Endings sidebar panel
to review that ending's screen without resetting your current game.
PRO TIP: "The Spaghetti Singularity" requires Debt >= 70. This is
TRICKY because some "bad" answers actually LOWER debt! Rewrites
(Litegraph A: Debt -20) and big bang rewrites (Services B: Debt -10)
reduce debt short-term even though they tank morale.
To hit Debt >= 70 you must pick options that ADD debt or leave it
alone. Here's the proven path (starting at Debt 50):
Components: B (Barrel file reordering) Debt +10 -> 60
Command Forge: C (Skip commands) Debt +15 -> 75
Stores: C (Leave it as-is) Debt +10 -> 85
Services: C (Strangler fig) Debt +0 -> 85
Litegraph: C (Add a facade) Debt +5 -> 90
ECS: C (Keep plain numbers) Debt +15 -> 100
Subgraph: C (Keep three-layer system) Debt +10 -> 100
Renderer: B (Dirty flags) Debt -5 -> 95
Composables: B (Polling-based sync) Debt +10 -> 100
Final: Debt 100 / Quality 10 / Morale 50 -> SPAGHETTI SINGULARITY
WARNING: Picking "all bad-rated answers" does NOT work! The bad
answers for Litegraph (A: Rewrite, Debt -20) and Services (B: Big
bang, Debt -10) have negative debt effects that pull you back
under 70.
===============================================================================
VIII. ARTIFACTS CHECKLIST
===============================================================================
Room | Artifact | Type
==================|============================|==================
Component Gallery | GraphView.vue | Component
Store Vaults | widgetValueStore.ts | Proto-ECS Store
Store Vaults | layoutStore.ts | Proto-ECS Store
Service Corridors | litegraphService.ts | Service
Service Corridors | Extension Migration Guide | Design Pattern
Litegraph Engine | LGraphCanvas.ts | God Object
Litegraph Engine | LGraphNode.ts | God Object
ECS Chamber | World Registry | ECS Core
ECS Chamber | Branded Entity IDs | Type Safety
Subgraph Depths | SubgraphStructure | ECS Component
Subgraph Depths | Typed Interface Contracts | Design Pattern
Renderer Overlook | QuadTree Spatial Index | Data Structure
Renderer Overlook | Y.js CRDT Layout | Collaboration
Composables | useCoreCommands.ts | Composable
Command Forge | CommandExecutor | ECS Core
Command Forge | Command Interface | Design Pattern
Total: 16 artifacts across 9 rooms.
Entry Point has no artifacts.
===============================================================================
IX. PRO TIPS & SECRETS
===============================================================================
* Your game auto-saves after every room change and challenge. Close
the tab and come back anytime - you won't lose progress.
* The Restart button in the HUD resets your run but KEEPS your
achievement badges. Use it to go for a different ending.
* Every code reference in the room descriptions is a clickable link
to the actual file on GitHub. Open them in new tabs to read the
real code while you play.
* After each challenge, the "Read more" link takes you to the
architecture documentation that explains the real engineering
rationale behind the recommended answer.
* The map overlay (press M) shows challenge badges:
[?] = challenge available but not yet attempted
[v] = challenge completed
* Room navigation preloads images for adjacent rooms, so transitions
should be instant after the first visit.
* The Command Forge (formerly the Side Panel) teaches the Command
Pattern - how commands relate to systems and the World. Its challenge
covers the architectural layering from ADR 0003 and ADR 0008.
* The ECS Migration Progress stat maxes at 5, matching the 5 phases
of the real migration plan. But 9 challenges can give +1 each
(8 of the 9 GOOD answers grant +1 ECS). The Services challenge
("5-phase plan") gives +1 ECS but no debt reduction - it's pure
planning, not implementation.
* There are between 2-3 choices per challenge, giving
3*3*3*3*3*3*2*3*3 = 13,122 possible playthroughs. But only 4
distinct endings. Most paths lead to "The Clean Architecture"
or "The Eternal Refactor."
* If you want to learn the ComfyUI frontend architecture for real,
the recommended reading order matches the optimal speedrun route:
1. src/main.ts (entry point)
2. src/views/GraphView.vue (main canvas)
3. src/stores/ (state management)
4. src/ecs/ (the future)
5. docs/architecture/ecs-world-command-api.md (command layer)
6. src/renderer/core/ (canvas pipeline)
7. docs/architecture/ecs-migration-plan.md (the plan)
8. src/composables/ (Vue logic hooks)
9. src/lib/litegraph/src/ (the legacy engine)
* The pixel art images were generated using the Z-Image Turbo
pipeline on the same ComfyUI that this frontend controls.
Meta, isn't it?
===============================================================================
This document Copyright (c) 2026 A Concerned Architect
ComfyUI is maintained by Comfy-Org: https://github.com/Comfy-Org
"In a world of god objects, be an entity-component-system."
___
| |
___| |___
| |
| COMFY UI |
| FRONTEND |
|___________|
| | | | | | |
| | | | | | |
_| | | | | | |_
|_______________|
GG. GIT GUD.
===============================================================================

View File

@@ -0,0 +1,26 @@
{
"meta": {
"style": "Pixel art badge/medal icon, 128x128, dark background, achievement unlock style",
"usage": "Each key matches an ending ID. Shown in achievements panel when that ending has been reached.",
"model": "Z-Image Turbo (no LoRA)",
"resolution": "128x128"
},
"achievements": {
"great": {
"title": "The ECS Enlightenment",
"prompt": "Pixel art achievement badge of a radiant crystal temple with clean geometric architecture, bright green and gold triumphant glow, laurel wreath border, dark background"
},
"good": {
"title": "The Clean Architecture",
"prompt": "Pixel art achievement badge of a solid fortress with neat organized blocks, blue and silver steady glow, star emblem, dark background"
},
"mediocre": {
"title": "The Eternal Refactor",
"prompt": "Pixel art achievement badge of an hourglass with sand still flowing endlessly, amber and grey weary glow, circular border, dark background"
},
"disaster": {
"title": "The Spaghetti Singularity",
"prompt": "Pixel art achievement badge of a tangled mass of spaghetti code wires collapsing into a black hole, red and purple chaotic glow, cracked border, dark background"
}
}
}

View File

@@ -0,0 +1,114 @@
{
"meta": {
"style": "Pixel art icon, 128x128, dark background, game UI button icon style, clean readable silhouette",
"usage": "Each key is {room}-{choiceKey lowercase}. Used in challenge choice buttons in adventure.html.",
"model": "Z-Image Turbo (no LoRA)",
"resolution": "128x128"
},
"choices": {
"components-a": {
"label": "Composition over inheritance",
"prompt": "Pixel art icon of puzzle pieces snapping together cleanly, green glow, dark background, game UI icon"
},
"components-b": {
"label": "Barrel file reordering",
"prompt": "Pixel art icon of a stack of files being shuffled with arrows, amber warning glow, dark background, game UI icon"
},
"components-c": {
"label": "Factory injection",
"prompt": "Pixel art icon of a factory building with a syringe injecting into it, blue mechanical glow, dark background, game UI icon"
},
"stores-a": {
"label": "Centralize into graph.incrementVersion()",
"prompt": "Pixel art icon of scattered dots converging into a single glowing funnel point, green glow, dark background, game UI icon"
},
"stores-b": {
"label": "Add a JavaScript Proxy",
"prompt": "Pixel art icon of a shield proxy intercepting arrows mid-flight, amber translucent glow, dark background, game UI icon"
},
"stores-c": {
"label": "Leave it as-is",
"prompt": "Pixel art icon of a shrug gesture with cobwebs on old machinery, grey muted glow, dark background, game UI icon"
},
"services-a": {
"label": "5-phase incremental plan",
"prompt": "Pixel art icon of five stepping stones ascending in a staircase with checkmarks, green glow, dark background, game UI icon"
},
"services-b": {
"label": "Big bang rewrite",
"prompt": "Pixel art icon of a dynamite stick with lit fuse and explosion sparks, red danger glow, dark background, game UI icon"
},
"services-c": {
"label": "Strangler fig pattern",
"prompt": "Pixel art icon of vines growing around and enveloping an old tree trunk, green and brown organic glow, dark background, game UI icon"
},
"litegraph-a": {
"label": "Rewrite from scratch",
"prompt": "Pixel art icon of a wrecking ball demolishing a building into rubble, red destructive glow, dark background, game UI icon"
},
"litegraph-b": {
"label": "Extract incrementally",
"prompt": "Pixel art icon of surgical tweezers carefully extracting a glowing module from a larger block, green precise glow, dark background, game UI icon"
},
"litegraph-c": {
"label": "Add a facade layer",
"prompt": "Pixel art icon of a decorative mask covering a cracked wall, yellow cosmetic glow, dark background, game UI icon"
},
"ecs-a": {
"label": "Branded types with cast helpers",
"prompt": "Pixel art icon of ID badges with distinct colored stamps and a compiler checkmark, green type-safe glow, dark background, game UI icon"
},
"ecs-b": {
"label": "String prefixes at runtime",
"prompt": "Pixel art icon of text labels being parsed with a magnifying glass at runtime, amber slow glow, dark background, game UI icon"
},
"ecs-c": {
"label": "Keep plain numbers",
"prompt": "Pixel art icon of bare numbers floating unprotected with a question mark, red risky glow, dark background, game UI icon"
},
"renderer-a": {
"label": "Separate update and render phases",
"prompt": "Pixel art icon of two clean pipeline stages labeled U and R with an arrow between them, green orderly glow, dark background, game UI icon"
},
"renderer-b": {
"label": "Dirty flags and deferred render",
"prompt": "Pixel art icon of a flag with a smudge mark and a clock showing delay, amber patch glow, dark background, game UI icon"
},
"composables-a": {
"label": "Y.js CRDTs",
"prompt": "Pixel art icon of two documents merging seamlessly with sync arrows and no conflicts, green collaboration glow, dark background, game UI icon"
},
"composables-b": {
"label": "Polling-based sync",
"prompt": "Pixel art icon of a clock with circular refresh arrows and flickering signal, red laggy glow, dark background, game UI icon"
},
"composables-c": {
"label": "Skip collaboration for now",
"prompt": "Pixel art icon of a single person at a desk with a pause symbol, grey neutral glow, dark background, game UI icon"
},
"subgraph-a": {
"label": "Connections-only: promotion = adding a typed input",
"prompt": "Pixel art icon of a function signature with typed input slots and a green checkmark, clean minimal glow, dark background, game UI icon"
},
"subgraph-b": {
"label": "Simplified component promotion",
"prompt": "Pixel art icon of a widget being lifted up with a promotion arrow and a component badge, amber glow, dark background, game UI icon"
},
"subgraph-c": {
"label": "Keep the current three-layer system",
"prompt": "Pixel art icon of three stacked translucent layers with proxy shadows underneath, red complex glow, dark background, game UI icon"
},
"sidepanel-a": {
"label": "Commands as intent; systems as handlers; World as store",
"prompt": "Pixel art icon of a layered architectural diagram with arrows flowing top-to-bottom through five labeled tiers, green glow, dark background, game UI icon"
},
"sidepanel-b": {
"label": "Make World.setComponent() itself serializable",
"prompt": "Pixel art icon of a database with every cell being logged into a scroll, amber overflow glow, dark background, game UI icon"
},
"sidepanel-c": {
"label": "Skip commands — let callers mutate directly",
"prompt": "Pixel art icon of multiple hands reaching into a glowing orb simultaneously causing cracks, red chaos glow, dark background, game UI icon"
}
}
}

View File

@@ -0,0 +1,90 @@
{
"meta": {
"style": "Pixel art icon on transparent black background, 128x128, clean edges, glowing accent color, game inventory item style",
"usage": "Each key is an artifact ID used in adventure.html. Generate one icon per artifact.",
"model": "Z-Image Turbo (no LoRA)",
"resolution": "128x128"
},
"artifacts": {
"graphview": {
"name": "GraphView.vue",
"type": "Component",
"prompt": "Pixel art icon of a glowing canvas frame with connected nodes and wires inside, blue accent glow, dark background, game inventory item"
},
"widgetvaluestore": {
"name": "widgetValueStore.ts",
"type": "Proto-ECS Store",
"prompt": "Pixel art icon of a vault door with a glowing slider widget embossed on it, purple and gold accents, dark background, game inventory item"
},
"layoutstore": {
"name": "layoutStore.ts",
"type": "Proto-ECS Store",
"prompt": "Pixel art icon of a grid blueprint with glowing position markers, purple accent lines, dark background, game inventory item"
},
"litegraphservice": {
"name": "litegraphService.ts",
"type": "Service",
"prompt": "Pixel art icon of a gear with a graph node symbol in the center, copper and blue metallic glow, dark background, game inventory item"
},
"lgraphcanvas": {
"name": "LGraphCanvas.ts",
"type": "God Object",
"prompt": "Pixel art icon of a massive cracked monolith radiating red warning light, labeled 9100, ominous dark background, game inventory item"
},
"lgraphnode": {
"name": "LGraphNode.ts",
"type": "God Object",
"prompt": "Pixel art icon of an oversized cube with tangled wires bursting from every face, red and amber glow, dark background, game inventory item"
},
"world-registry": {
"name": "World Registry",
"type": "ECS Core",
"prompt": "Pixel art icon of a glowing crystalline orb containing tiny entity symbols, bright blue and white aura, dark background, game inventory item"
},
"branded-ids": {
"name": "Branded Entity IDs",
"type": "Type Safety",
"prompt": "Pixel art icon of a set of ID cards with distinct colored borders and brand stamps, green checkmark glow, dark background, game inventory item"
},
"quadtree": {
"name": "QuadTree Spatial Index",
"type": "Data Structure",
"prompt": "Pixel art icon of a square recursively divided into four quadrants with glowing dots at intersections, teal accent, dark background, game inventory item"
},
"yjs-crdt": {
"name": "Y.js CRDT Layout",
"type": "Collaboration",
"prompt": "Pixel art icon of two overlapping document layers merging with sync arrows, purple and green glow, dark background, game inventory item"
},
"usecorecommands": {
"name": "useCoreCommands.ts",
"type": "Composable",
"prompt": "Pixel art icon of a hook tool with keyboard key symbols orbiting it, yellow and blue glow, dark background, game inventory item"
},
"subgraph-structure": {
"name": "SubgraphStructure",
"type": "ECS Component",
"prompt": "Pixel art icon of nested rectangular frames inside each other like Russian dolls with glowing typed connections at each boundary, purple and teal accent, dark background, game inventory item"
},
"typed-contracts": {
"name": "Typed Interface Contracts",
"type": "Architecture",
"prompt": "Pixel art icon of a sealed scroll with a glowing typed signature stamp and interface brackets, gold and blue accent, dark background, game inventory item"
},
"command-executor": {
"name": "CommandExecutor",
"type": "ECS Core",
"prompt": "Pixel art icon of a glowing anvil with a gear and execute arrow symbol, blue-purple forge glow, dark background, game inventory item"
},
"command-interface": {
"name": "Command Interface",
"type": "Design Pattern",
"prompt": "Pixel art icon of a sealed scroll with a type discriminator tag and execute method seal, blue glow, dark background, game inventory item"
},
"extension-migration": {
"name": "Extension Migration Guide",
"type": "Design Pattern",
"prompt": "Pixel art icon of a scroll with legacy code on left transforming via arrow to ECS code on right, green transition glow, dark background, game inventory item"
}
}
}

View File

@@ -0,0 +1,61 @@
{
"meta": {
"style": "Pixel art, 16:9 aspect ratio, dark moody palette with glowing accent lighting",
"usage": "Each key corresponds to a room ID in adventure.html. Generate images with generate-images.py.",
"model": "Z-Image Turbo (no LoRA)",
"resolution": "1152x640",
"generated": "2026-03-24"
},
"rooms": {
"entry": {
"title": "The Entry Point",
"prompt": "Pixel art of a glowing terminal in a vast dark server room, Vue.js and TypeScript logos floating as holographic projections, three corridors branching ahead lit by blue, green, and purple lights",
"path": "images/entry.png"
},
"components": {
"title": "The Component Gallery",
"prompt": "Pixel art gallery hall with framed Vue component cards hung on stone walls, a massive canvas painting labeled 'GraphView' in the center, smaller panels flanking either side, warm torchlight",
"path": "images/components.png"
},
"stores": {
"title": "The Store Vaults",
"prompt": "Pixel art underground vault with 60 glowing vault doors lining the walls, three doors in front glow brightest (labeled widget, layout, promotion), a Pinia pineapple emblem etched in stone above",
"path": "images/stores.png"
},
"services": {
"title": "The Service Corridors",
"prompt": "Pixel art clean corridors with labeled pipes connecting rooms overhead, data flowing as glowing particles through transparent tubes, service names etched on brass plaques",
"path": "images/services.png"
},
"litegraph": {
"title": "The Litegraph Engine Room",
"prompt": "Pixel art dark engine room with three massive monolith machines labeled 9100, 4300, and 3100 lines of code, warning lights flashing amber, tangled wires and cables everywhere",
"path": "images/litegraph.png"
},
"ecs": {
"title": "The ECS Architect's Chamber",
"prompt": "Pixel art architect's drafting room with blueprints pinned to walls showing entity-component diagrams, a glowing World orb floating in the center, branded ID cards scattered across the desk",
"path": "images/ecs.png"
},
"subgraph": {
"title": "The Subgraph Depths",
"prompt": "Pixel art recursive fractal chamber where identical rooms nest inside each other like Russian dolls, typed contract scrolls float at each boundary doorway, a DAG tree diagram glows on the ceiling",
"path": "images/subgraph.png"
},
"renderer": {
"title": "The Renderer Overlook",
"prompt": "Pixel art observation deck overlooking a vast canvas being painted by precise robotic arms, Y.js CRDT symbols floating in the air, a QuadTree grid visible on the floor below",
"path": "images/renderer.png"
},
"composables": {
"title": "The Composables Workshop",
"prompt": "Pixel art workshop with hooks hanging from a pegboard wall, each labeled (useCoreCommands, useCanvasDrop, etc.), workbenches for auth, canvas, and queue domains, cozy lantern light",
"path": "images/composables.png"
},
"sidepanel": {
"title": "The Command Forge",
"prompt": "Pixel art anvil forge where glowing command scrolls are being hammered into structured objects, a layered diagram on the wall showing five architectural tiers connected by arrows, blue and purple forge light, dark background",
"path": "images/sidepanel.png"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,196 @@
"""
Generate pixel art inventory icons for the Architecture Adventure game.
Uses Z-Image Turbo pipeline via local ComfyUI server (no LoRA).
Skips icons that already exist on disk.
Usage: python docs/architecture/generate-icons.py
"""
import json
import os
import time
import urllib.request
import urllib.error
COMFY_URL = "http://localhost:8188"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ARTIFACT_PROMPTS = os.path.join(SCRIPT_DIR, "adventure-icon-prompts.json")
CHOICE_PROMPTS = os.path.join(SCRIPT_DIR, "adventure-choice-icon-prompts.json")
ACHIEVEMENT_PROMPTS = os.path.join(SCRIPT_DIR, "adventure-achievement-icon-prompts.json")
OUTPUT_DIR = os.path.join(SCRIPT_DIR, "icons")
BASE_SEED = 7777
WIDTH = 128
HEIGHT = 128
def build_workflow(prompt_text, seed, prefix):
return {
"1": {
"class_type": "UNETLoader",
"inputs": {
"unet_name": "ZIT\\z_image_turbo_bf16.safetensors",
"weight_dtype": "default",
},
},
"2": {
"class_type": "CLIPLoader",
"inputs": {
"clip_name": "qwen_3_4b.safetensors",
"type": "lumina2",
"device": "default",
},
},
"3": {
"class_type": "VAELoader",
"inputs": {"vae_name": "ae.safetensors"},
},
"4": {
"class_type": "ModelSamplingAuraFlow",
"inputs": {"shift": 3, "model": ["1", 0]},
},
"6": {
"class_type": "CLIPTextEncode",
"inputs": {"text": prompt_text, "clip": ["2", 0]},
},
"7": {
"class_type": "ConditioningZeroOut",
"inputs": {"conditioning": ["6", 0]},
},
"8": {
"class_type": "EmptySD3LatentImage",
"inputs": {"width": WIDTH, "height": HEIGHT, "batch_size": 1},
},
"9": {
"class_type": "KSampler",
"inputs": {
"seed": seed,
"control_after_generate": "fixed",
"steps": 8,
"cfg": 1,
"sampler_name": "res_multistep",
"scheduler": "simple",
"denoise": 1,
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["8", 0],
},
},
"10": {
"class_type": "VAEDecode",
"inputs": {"samples": ["9", 0], "vae": ["3", 0]},
},
"11": {
"class_type": "SaveImage",
"inputs": {"filename_prefix": prefix, "images": ["10", 0]},
},
}
def submit_prompt(workflow):
payload = json.dumps({"prompt": workflow}).encode("utf-8")
req = urllib.request.Request(
f"{COMFY_URL}/prompt",
data=payload,
headers={"Content-Type": "application/json"},
)
try:
resp = urllib.request.urlopen(req)
return json.loads(resp.read())
except urllib.error.HTTPError as e:
body = e.read().decode()
raise RuntimeError(f"HTTP {e.code}: {body}")
def poll_history(prompt_id, timeout=120):
start = time.time()
while time.time() - start < timeout:
try:
resp = urllib.request.urlopen(f"{COMFY_URL}/history/{prompt_id}")
data = json.loads(resp.read())
if prompt_id in data:
return data[prompt_id]
except Exception:
pass
time.sleep(2)
return None
def download_image(filename, subfolder, dest_path):
url = f"{COMFY_URL}/view?filename={urllib.request.quote(filename)}&subfolder={urllib.request.quote(subfolder)}&type=output"
urllib.request.urlretrieve(url, dest_path)
def main():
os.makedirs(OUTPUT_DIR, exist_ok=True)
# Collect all icons from both prompt files
all_icons = {}
with open(ARTIFACT_PROMPTS) as f:
data = json.load(f)
for icon_id, entry in data["artifacts"].items():
all_icons[icon_id] = entry["prompt"]
if os.path.exists(CHOICE_PROMPTS):
with open(CHOICE_PROMPTS) as f:
data = json.load(f)
for icon_id, entry in data["choices"].items():
all_icons[icon_id] = entry["prompt"]
if os.path.exists(ACHIEVEMENT_PROMPTS):
with open(ACHIEVEMENT_PROMPTS) as f:
data = json.load(f)
for icon_id, entry in data["achievements"].items():
all_icons[f"ending-{icon_id}"] = entry["prompt"]
# Filter out already-generated icons
to_generate = {}
for icon_id, prompt in all_icons.items():
dest = os.path.join(OUTPUT_DIR, f"{icon_id}.png")
if os.path.exists(dest):
print(f" Skipping {icon_id}.png (already exists)")
else:
to_generate[icon_id] = prompt
if not to_generate:
print("All icons already generated. Nothing to do.")
return
# Submit jobs
jobs = []
for i, (icon_id, prompt) in enumerate(to_generate.items()):
prefix = f"adventure-icons/{icon_id}"
wf = build_workflow(prompt, BASE_SEED + i, prefix)
result = submit_prompt(wf)
prompt_id = result["prompt_id"]
jobs.append((icon_id, prompt_id))
print(f" Submitted: {icon_id} -> {prompt_id}")
print(f"\n{len(jobs)} jobs queued. Polling for completion...\n")
# Poll for completion
completed = set()
while len(completed) < len(jobs):
for icon_id, prompt_id in jobs:
if prompt_id in completed:
continue
history = poll_history(prompt_id, timeout=5)
if history:
completed.add(prompt_id)
outputs = history.get("outputs", {})
for node_out in outputs.values():
for img in node_out.get("images", []):
src_filename = img["filename"]
subfolder = img.get("subfolder", "")
dest = os.path.join(OUTPUT_DIR, f"{icon_id}.png")
download_image(src_filename, subfolder, dest)
print(f" [{len(completed)}/{len(jobs)}] {icon_id}.png downloaded")
if len(completed) < len(jobs):
time.sleep(2)
print(f"\nDone! {len(completed)} icons saved to {OUTPUT_DIR}/")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,165 @@
"""
Generate pixel art room images for the Architecture Adventure game.
Uses Z-Image Turbo pipeline via local ComfyUI server (no LoRA).
Usage: python docs/architecture/generate-images.py
"""
import json
import os
import time
import urllib.request
COMFY_URL = "http://localhost:8188"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROMPTS_FILE = os.path.join(SCRIPT_DIR, "adventure-image-prompts.json")
OUTPUT_DIR = os.path.join(SCRIPT_DIR, "images")
BASE_SEED = 2024
WIDTH = 1152
HEIGHT = 640
def build_workflow(prompt_text, seed, prefix):
return {
"1": {
"class_type": "UNETLoader",
"inputs": {
"unet_name": "ZIT\\z_image_turbo_bf16.safetensors",
"weight_dtype": "default",
},
},
"2": {
"class_type": "CLIPLoader",
"inputs": {
"clip_name": "qwen_3_4b.safetensors",
"type": "lumina2",
"device": "default",
},
},
"3": {
"class_type": "VAELoader",
"inputs": {"vae_name": "ae.safetensors"},
},
"4": {
"class_type": "ModelSamplingAuraFlow",
"inputs": {"shift": 3, "model": ["1", 0]},
},
"6": {
"class_type": "CLIPTextEncode",
"inputs": {"text": prompt_text, "clip": ["2", 0]},
},
"7": {
"class_type": "ConditioningZeroOut",
"inputs": {"conditioning": ["6", 0]},
},
"8": {
"class_type": "EmptySD3LatentImage",
"inputs": {"width": WIDTH, "height": HEIGHT, "batch_size": 1},
},
"9": {
"class_type": "KSampler",
"inputs": {
"seed": seed,
"control_after_generate": "fixed",
"steps": 8,
"cfg": 1,
"sampler_name": "res_multistep",
"scheduler": "simple",
"denoise": 1,
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["8", 0],
},
},
"10": {
"class_type": "VAEDecode",
"inputs": {"samples": ["9", 0], "vae": ["3", 0]},
},
"11": {
"class_type": "SaveImage",
"inputs": {"filename_prefix": prefix, "images": ["10", 0]},
},
}
def submit_prompt(workflow):
payload = json.dumps({"prompt": workflow}).encode("utf-8")
req = urllib.request.Request(
f"{COMFY_URL}/prompt",
data=payload,
headers={"Content-Type": "application/json"},
)
try:
resp = urllib.request.urlopen(req)
return json.loads(resp.read())
except urllib.error.HTTPError as e:
body = e.read().decode()
raise RuntimeError(f"HTTP {e.code}: {body}")
def poll_history(prompt_id, timeout=120):
start = time.time()
while time.time() - start < timeout:
try:
resp = urllib.request.urlopen(f"{COMFY_URL}/history/{prompt_id}")
data = json.loads(resp.read())
if prompt_id in data:
return data[prompt_id]
except Exception:
pass
time.sleep(2)
return None
def download_image(filename, subfolder, dest_path):
url = f"{COMFY_URL}/view?filename={urllib.request.quote(filename)}&subfolder={urllib.request.quote(subfolder)}&type=output"
urllib.request.urlretrieve(url, dest_path)
def main():
with open(PROMPTS_FILE) as f:
data = json.load(f)
rooms = data["rooms"]
os.makedirs(OUTPUT_DIR, exist_ok=True)
# Submit all jobs
jobs = []
for i, (room_id, room) in enumerate(rooms.items()):
prefix = f"adventure/{room_id}"
wf = build_workflow(room["prompt"], BASE_SEED + i, prefix)
result = submit_prompt(wf)
prompt_id = result["prompt_id"]
jobs.append((room_id, prompt_id, prefix))
print(f" Submitted: {room_id} -> {prompt_id}")
print(f"\n{len(jobs)} jobs queued. Polling for completion...\n")
# Poll for completion
completed = set()
while len(completed) < len(jobs):
for room_id, prompt_id, prefix in jobs:
if prompt_id in completed:
continue
history = poll_history(prompt_id, timeout=5)
if history:
completed.add(prompt_id)
# Find output filename
outputs = history.get("outputs", {})
for node_id, node_out in outputs.items():
images = node_out.get("images", [])
for img in images:
src_filename = img["filename"]
subfolder = img.get("subfolder", "")
dest = os.path.join(OUTPUT_DIR, f"{room_id}.png")
download_image(src_filename, subfolder, dest)
print(f" [{len(completed)}/{len(jobs)}] {room_id}.png downloaded")
if len(completed) < len(jobs):
time.sleep(2)
print(f"\nDone! {len(completed)} images saved to {OUTPUT_DIR}/")
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -28,6 +28,10 @@ const config: KnipConfig = {
'packages/ingest-types': {
project: ['src/**/*.{js,ts}']
},
'apps/architecture-adventure': {
project: ['src/**/*.ts'],
vite: { config: ['vite.config.ts'] }
},
'apps/website': {
entry: [
'src/pages/**/*.astro',
@@ -61,7 +65,9 @@ const config: KnipConfig = {
// Pending integration in stacked PR
'src/components/sidebar/tabs/nodeLibrary/CustomNodesPanel.vue',
// Agent review check config, not part of the build
'.agents/checks/eslint.strict.config.js'
'.agents/checks/eslint.strict.config.js',
// ECS draft interfaces (ADR 0008) — not yet consumed by production code
'src/ecs/**/*.ts'
],
vite: {
config: ['vite?(.*).config.mts']

9
pnpm-lock.yaml generated
View File

@@ -829,6 +829,15 @@ importers:
specifier: 'catalog:'
version: 3.25.1(zod@3.25.76)
apps/architecture-adventure:
devDependencies:
typescript:
specifier: 'catalog:'
version: 5.9.3
vite:
specifier: ^8.0.0
version: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
apps/desktop-ui:
dependencies:
'@comfyorg/comfyui-electron-types':

View File

@@ -0,0 +1,32 @@
/**
* Group components.
*
* Groups are visual containers that hold nodes and reroutes.
* Currently no state has been extracted from LGraphGroup — these
* components represent the full extraction target.
*/
import type { NodeEntityId, RerouteEntityId } from '../entityId'
/** Metadata for a group. */
export interface GroupMeta {
title: string
font?: string
fontSize: number
}
/** Visual properties for group rendering. */
export interface GroupVisual {
color?: string
}
/**
* Entities contained within a group.
*
* Replaces LGraphGroup._children (Set<Positionable>) and
* LGraphGroup._nodes (LGraphNode[]).
*/
export interface GroupChildren {
nodeIds: ReadonlySet<NodeEntityId>
rerouteIds: ReadonlySet<RerouteEntityId>
}

View File

@@ -0,0 +1,51 @@
/**
* Link components.
*
* Decomposes LLink into endpoint topology, visual state, and
* transient interaction state.
*/
import type {
CanvasColour,
ISlotType,
Point
} from '@/lib/litegraph/src/interfaces'
import type { NodeEntityId, RerouteEntityId } from '../entityId'
/**
* The topological endpoints of a link.
*
* Replaces origin_id/origin_slot/target_id/target_slot/type on LLink.
* Slot indices will migrate to SlotEntityId references once slots
* have independent IDs.
*/
export interface LinkEndpoints {
originNodeId: NodeEntityId
originSlotIndex: number
targetNodeId: NodeEntityId
targetSlotIndex: number
/** Data type flowing through this link (e.g., 'IMAGE', 'MODEL'). */
type: ISlotType
/** Reroute that owns this link segment, if any. */
parentRerouteId?: RerouteEntityId
}
/** Visual properties for link rendering. */
export interface LinkVisual {
color?: CanvasColour
/** Cached rendered path (invalidated on position change). */
path?: Path2D
/** Cached center point of the link curve. */
centerPos?: Point
/** Cached angle at the center point. */
centerAngle?: number
}
/** Transient interaction state for a link. */
export interface LinkState {
/** True while the user is dragging this link. */
dragging: boolean
/** Arbitrary data payload flowing through the link. */
data?: unknown
}

View File

@@ -0,0 +1,87 @@
/**
* Node-specific components.
*
* These decompose the ~4,300-line LGraphNode class into focused data
* objects. Each component captures one concern; systems provide behavior.
*
* Reuses existing types from litegraph where possible to ease migration.
*/
import type { Dictionary, INodeFlags } from '@/lib/litegraph/src/interfaces'
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
import type {
LGraphEventMode,
RenderShape
} from '@/lib/litegraph/src/types/globalEnums'
import type { SlotEntityId, WidgetEntityId } from '../entityId'
/** Static identity and classification of a node. */
export interface NodeType {
/** Registered node type string (e.g., 'KSampler', 'CLIPTextEncode'). */
type: string
/** Display title. */
title: string
/** Category path for the node menu (e.g., 'sampling'). */
category?: string
/** Backend node definition data, if resolved. */
nodeData?: unknown
/** Optional description shown in tooltips/docs. */
description?: string
}
/** Visual / rendering properties of a node. */
export interface NodeVisual {
color?: string
bgcolor?: string
boxcolor?: string
shape?: RenderShape
}
/**
* Connectivity — references to this node's slot entities.
*
* Replaces the `inputs[]` and `outputs[]` arrays on LGraphNode.
* Actual slot data lives on SlotIdentity / SlotConnection components
* keyed by SlotEntityId.
*/
export interface Connectivity {
inputSlotIds: readonly SlotEntityId[]
outputSlotIds: readonly SlotEntityId[]
}
/** Execution scheduling state. */
export interface Execution {
/** Computed execution order (topological sort index). */
order: number
/** How this node participates in execution. */
mode: LGraphEventMode
/** Behavioral flags (pinned, collapsed, ghost, etc.). */
flags: INodeFlags
}
/** User-defined key-value properties on a node. */
export interface Properties {
properties: Dictionary<NodeProperty | undefined>
propertiesInfo: readonly PropertyInfo[]
}
export interface PropertyInfo {
name?: string
type?: string
default_value?: NodeProperty
widget?: string
label?: string
values?: unknown[]
}
/**
* Container for widget entities owned by this node.
*
* Replaces the `widgets[]` array on LGraphNode.
* Actual widget data lives on WidgetIdentity / WidgetValue components
* keyed by WidgetEntityId.
*/
export interface WidgetContainer {
widgetIds: readonly WidgetEntityId[]
}

View File

@@ -0,0 +1,24 @@
/**
* Position component — shared by Node, Reroute, and Group entities.
*
* Plain data object. No methods, no back-references.
* Corresponds to the spatial data currently on LGraphNode.pos/size,
* Reroute.pos, and LGraphGroup._bounding.
*
* During the bridge phase, this mirrors data from the LayoutStore
* (Y.js CRDTs). See migration plan Phase 2a.
*/
import type { Point, Size } from '@/lib/litegraph/src/interfaces'
export interface Position {
/** Position in graph coordinates (top-left for nodes/groups, center for reroutes). */
pos: Point
/** Width and height. Undefined for point-like entities (reroutes). */
size?: Size
/**
* Bounding rectangle as [x, y, width, height].
* May extend beyond pos/size (e.g., nodes with title overhang).
*/
bounding: readonly [x: number, y: number, w: number, h: number]
}

View File

@@ -0,0 +1,35 @@
/**
* Reroute components.
*
* Reroutes are waypoints on link paths. Position is shared via the
* Position component. These components capture the link topology
* and visual state specific to reroutes.
*/
import type { CanvasColour } from '@/lib/litegraph/src/interfaces'
import type { LinkEntityId, RerouteEntityId } from '../entityId'
/**
* Link topology for a reroute.
*
* A reroute can be chained (parentId) and carries a set of links
* that pass through it.
*/
export interface RerouteLinks {
/** Parent reroute in the chain, if any. */
parentId?: RerouteEntityId
/** Links that pass through this reroute. */
linkIds: ReadonlySet<LinkEntityId>
/** Floating (in-progress) links passing through this reroute. */
floatingLinkIds: ReadonlySet<LinkEntityId>
}
/** Visual state specific to reroute rendering. */
export interface RerouteVisual {
color?: CanvasColour
/** Cached path for the link segment. */
path?: Path2D
/** Angle at the reroute center (for directional rendering). */
centerAngle?: number
}

View File

@@ -0,0 +1,76 @@
/**
* Slot components.
*
* Slots currently lack independent IDs — they're identified by their
* index on a parent node's inputs/outputs array. The ECS assigns each
* slot a synthetic SlotEntityId, making them first-class entities.
*
* Decomposes SlotBase / INodeInputSlot / INodeOutputSlot into identity,
* connection topology, and visual state.
*/
import type {
CanvasColour,
ISlotType,
Point
} from '@/lib/litegraph/src/interfaces'
import type {
LinkDirection,
RenderShape
} from '@/lib/litegraph/src/types/globalEnums'
import type { LinkEntityId, NodeEntityId } from '../entityId'
/** Immutable identity of a slot. */
export interface SlotIdentity {
/** Display name (e.g., 'model', 'positive'). */
name: string
/** Localized display name, if available. */
localizedName?: string
/** Optional label override. */
label?: string
/** Data type accepted/produced by this slot. */
type: ISlotType
/** Whether this is an input or output slot. */
direction: 'input' | 'output'
/** The node that owns this slot. */
parentNodeId: NodeEntityId
/** Position index on the parent node (0-based). */
index: number
}
/**
* Connection state of a slot.
*
* Input slots have at most one link. Output slots can have many.
*/
export interface SlotConnection {
/**
* For input slots: the single connected link, or null.
* For output slots: all connected links.
*/
linkIds: readonly LinkEntityId[]
/** Widget locator, if this slot backs a promoted widget. */
widgetLocator?: SlotWidgetLocator
}
export interface SlotWidgetLocator {
name: string
nodeId: NodeEntityId
}
/** Visual / rendering properties of a slot. */
export interface SlotVisual {
/** Computed position relative to the node. */
pos?: Point
/** Bounding rectangle for hit testing. */
boundingRect: readonly [x: number, y: number, w: number, h: number]
/** Color when connected. */
colorOn?: CanvasColour
/** Color when disconnected. */
colorOff?: CanvasColour
/** Render shape (circle, arrow, grid, etc.). */
shape?: RenderShape
/** Flow direction for link rendering. */
dir?: LinkDirection
}

View File

@@ -0,0 +1,34 @@
/**
* Subgraph components.
*
* A subgraph is a graph that lives inside a SubgraphNode. It inherits
* all node components (via its SubgraphNode entity) and adds structural
* and metadata components for its interior.
*
* This is the most complex entity kind — it depends on Node and Link
* extraction being complete first. See migration plan Phase 2.
*/
import type { LinkEntityId, NodeEntityId, RerouteEntityId } from '../entityId'
/**
* The interior structure of a subgraph.
*
* Replaces the recursive LGraph container that Subgraph inherits.
* Entity IDs reference entities that live in the World — not in a
* nested graph instance.
*/
export interface SubgraphStructure {
/** Nodes contained within the subgraph. */
nodeIds: readonly NodeEntityId[]
/** Internal links (both endpoints inside the subgraph). */
linkIds: readonly LinkEntityId[]
/** Internal reroutes. */
rerouteIds: readonly RerouteEntityId[]
}
/** Descriptive metadata for a subgraph definition. */
export interface SubgraphMeta {
name: string
description?: string
}

View File

@@ -0,0 +1,75 @@
/**
* Widget components.
*
* Widget value extraction is already complete (WidgetValueStore).
* These interfaces formalize the target shape and add the layout
* component that remains on the BaseWidget class.
*
* The 23+ widget subclasses (NumberWidget, ComboWidget, etc.) become
* configuration data here. Widget-type-specific rendering behavior
* will live in the RenderSystem.
*/
import type {
IWidgetOptions,
TWidgetValue
} from '@/lib/litegraph/src/types/widgets'
import type { NodeEntityId } from '../entityId'
/** Immutable identity of a widget within its parent node. */
export interface WidgetIdentity {
/** Widget name (unique within a node). */
name: string
/**
* Widget type string (e.g., 'number', 'combo', 'toggle', 'text').
* Determines which system handles rendering and interaction.
*/
widgetType: string
/** The node that owns this widget. */
parentNodeId: NodeEntityId
}
/**
* Widget value and configuration.
*
* Structurally equivalent to the existing WidgetState in
* WidgetValueStore — the bridge layer can share the same objects.
*/
export interface WidgetValue {
/** Current value (type depends on widgetType). */
value: TWidgetValue
/** Configuration options (min, max, step, values, etc.). */
options: IWidgetOptions
/** Display label override. */
label?: string
/** Whether the widget is disabled. */
disabled?: boolean
/** Whether to include this widget's value in serialized workflow JSON. */
serialize?: boolean
}
/**
* Layout metrics computed during the arrange phase.
*
* Currently lives as mutable properties on BaseWidget (y,
* computedHeight, width). The LayoutSystem will own these writes;
* the RenderSystem reads them.
*/
export interface WidgetLayout {
/** Vertical position relative to the node body. */
y: number
/** Computed height after layout distribution. */
computedHeight: number
/** Width override (undefined = use node width). */
width?: number
/** Layout size constraints from computeLayoutSize(). */
constraints?: WidgetLayoutConstraints
}
export interface WidgetLayoutConstraints {
minHeight: number
maxHeight?: number
minWidth?: number
maxWidth?: number
}

64
src/ecs/entityId.ts Normal file
View File

@@ -0,0 +1,64 @@
/**
* Branded entity ID types for compile-time cross-kind safety.
*
* Each entity kind gets a nominal type wrapping its underlying primitive.
* The brand prevents accidentally passing a LinkEntityId where a
* NodeEntityId is expected — a class of bugs that plain `number` allows.
*
* At runtime these are just numbers (or strings for subgraphs). The brand
* is erased by TypeScript and has zero runtime cost.
*
* @see {@link ../../../docs/adr/0008-entity-component-system.md}
*/
// -- Branded ID types -------------------------------------------------------
type Brand<T, B extends string> = T & { readonly __brand: B }
export type NodeEntityId = Brand<number, 'NodeEntityId'>
export type LinkEntityId = Brand<number, 'LinkEntityId'>
export type SubgraphEntityId = Brand<string, 'SubgraphEntityId'>
export type WidgetEntityId = Brand<number, 'WidgetEntityId'>
export type SlotEntityId = Brand<number, 'SlotEntityId'>
export type RerouteEntityId = Brand<number, 'RerouteEntityId'>
export type GroupEntityId = Brand<number, 'GroupEntityId'>
/** Union of all entity ID types. */
export type EntityId =
| NodeEntityId
| LinkEntityId
| SubgraphEntityId
| WidgetEntityId
| SlotEntityId
| RerouteEntityId
| GroupEntityId
// -- Cast helpers (for use at system boundaries) ----------------------------
export function asNodeEntityId(id: number): NodeEntityId {
return id as NodeEntityId
}
export function asLinkEntityId(id: number): LinkEntityId {
return id as LinkEntityId
}
export function asSubgraphEntityId(id: string): SubgraphEntityId {
return id as SubgraphEntityId
}
export function asWidgetEntityId(id: number): WidgetEntityId {
return id as WidgetEntityId
}
export function asSlotEntityId(id: number): SlotEntityId {
return id as SlotEntityId
}
export function asRerouteEntityId(id: number): RerouteEntityId {
return id as RerouteEntityId
}
export function asGroupEntityId(id: number): GroupEntityId {
return id as GroupEntityId
}

242
src/ecs/world.ts Normal file
View File

@@ -0,0 +1,242 @@
/**
* World — the central registry for all entity state.
*
* The World is a typed container mapping branded entity IDs to their
* component sets. Systems query the World to read/write components;
* entities never reference each other directly.
*
* This is the initial type definition (Phase 1c of the migration plan).
* The implementation starts as plain Maps. CRDT backing, transactions,
* and reactivity are future concerns.
*/
import type {
Connectivity,
Execution,
NodeType,
NodeVisual,
Properties,
WidgetContainer
} from './components/node'
import type { GroupChildren, GroupMeta, GroupVisual } from './components/group'
import type { LinkEndpoints, LinkState, LinkVisual } from './components/link'
import type { Position } from './components/position'
import type { RerouteLinks, RerouteVisual } from './components/reroute'
import type {
SlotConnection,
SlotIdentity,
SlotVisual
} from './components/slot'
import type { SubgraphMeta, SubgraphStructure } from './components/subgraph'
import type {
WidgetIdentity,
WidgetLayout,
WidgetValue
} from './components/widget'
import type {
GroupEntityId,
LinkEntityId,
NodeEntityId,
RerouteEntityId,
SlotEntityId,
SubgraphEntityId,
WidgetEntityId
} from './entityId'
// -- Component bundles per entity kind --------------------------------------
export interface NodeComponents {
position: Position
nodeType: NodeType
visual: NodeVisual
connectivity: Connectivity
execution: Execution
properties: Properties
widgetContainer: WidgetContainer
}
export interface LinkComponents {
endpoints: LinkEndpoints
visual: LinkVisual
state: LinkState
}
export interface SlotComponents {
identity: SlotIdentity
connection: SlotConnection
visual: SlotVisual
}
export interface WidgetComponents {
identity: WidgetIdentity
value: WidgetValue
layout: WidgetLayout
}
export interface RerouteComponents {
position: Position
links: RerouteLinks
visual: RerouteVisual
}
export interface GroupComponents {
position: Position
meta: GroupMeta
visual: GroupVisual
children: GroupChildren
}
export interface SubgraphComponents {
structure: SubgraphStructure
meta: SubgraphMeta
}
// -- Entity kind registry ---------------------------------------------------
export interface EntityKindMap {
node: { id: NodeEntityId; components: NodeComponents }
link: { id: LinkEntityId; components: LinkComponents }
slot: { id: SlotEntityId; components: SlotComponents }
widget: { id: WidgetEntityId; components: WidgetComponents }
reroute: { id: RerouteEntityId; components: RerouteComponents }
group: { id: GroupEntityId; components: GroupComponents }
subgraph: { id: SubgraphEntityId; components: SubgraphComponents }
}
export type EntityKind = keyof EntityKindMap
// -- World interface --------------------------------------------------------
export interface World {
/** Per-kind entity stores. */
nodes: Map<NodeEntityId, NodeComponents>
links: Map<LinkEntityId, LinkComponents>
slots: Map<SlotEntityId, SlotComponents>
widgets: Map<WidgetEntityId, WidgetComponents>
reroutes: Map<RerouteEntityId, RerouteComponents>
groups: Map<GroupEntityId, GroupComponents>
subgraphs: Map<SubgraphEntityId, SubgraphComponents>
/**
* Create a new entity of the given kind, returning its branded ID.
* The entity starts with no components — call setComponent() to populate.
*/
createEntity<K extends EntityKind>(kind: K): EntityKindMap[K]['id']
/**
* Remove an entity and all its components.
* Returns true if the entity existed, false otherwise.
*/
deleteEntity<K extends EntityKind>(
kind: K,
id: EntityKindMap[K]['id']
): boolean
/**
* Get a single component from an entity.
* Returns undefined if the entity or component doesn't exist.
*/
getComponent<
K extends EntityKind,
C extends keyof EntityKindMap[K]['components']
>(
kind: K,
id: EntityKindMap[K]['id'],
component: C
): EntityKindMap[K]['components'][C] | undefined
/**
* Set a single component on an entity.
* Creates the component if it doesn't exist, overwrites if it does.
*/
setComponent<
K extends EntityKind,
C extends keyof EntityKindMap[K]['components']
>(
kind: K,
id: EntityKindMap[K]['id'],
component: C,
data: EntityKindMap[K]['components'][C]
): void
}
// -- Factory ----------------------------------------------------------------
export function createWorld(): World {
const counters = {
node: 0,
link: 0,
slot: 0,
widget: 0,
reroute: 0,
group: 0,
subgraph: 0
}
const stores = {
nodes: new Map<NodeEntityId, NodeComponents>(),
links: new Map<LinkEntityId, LinkComponents>(),
slots: new Map<SlotEntityId, SlotComponents>(),
widgets: new Map<WidgetEntityId, WidgetComponents>(),
reroutes: new Map<RerouteEntityId, RerouteComponents>(),
groups: new Map<GroupEntityId, GroupComponents>(),
subgraphs: new Map<SubgraphEntityId, SubgraphComponents>()
}
const storeForKind: Record<EntityKind, Map<unknown, unknown>> = {
node: stores.nodes,
link: stores.links,
slot: stores.slots,
widget: stores.widgets,
reroute: stores.reroutes,
group: stores.groups,
subgraph: stores.subgraphs
}
return {
...stores,
createEntity<K extends EntityKind>(kind: K): EntityKindMap[K]['id'] {
const id = ++counters[kind]
const store = storeForKind[kind]
store.set(id, {} as never)
return id as EntityKindMap[K]['id']
},
deleteEntity<K extends EntityKind>(
kind: K,
id: EntityKindMap[K]['id']
): boolean {
return storeForKind[kind].delete(id)
},
getComponent<
K extends EntityKind,
C extends keyof EntityKindMap[K]['components']
>(
kind: K,
id: EntityKindMap[K]['id'],
component: C
): EntityKindMap[K]['components'][C] | undefined {
const entity = storeForKind[kind].get(id) as
| EntityKindMap[K]['components']
| undefined
return entity?.[component]
},
setComponent<
K extends EntityKind,
C extends keyof EntityKindMap[K]['components']
>(
kind: K,
id: EntityKindMap[K]['id'],
component: C,
data: EntityKindMap[K]['components'][C]
): void {
const entity = storeForKind[kind].get(id)
if (entity) {
Object.assign(entity, { [component]: data })
}
}
}
}