-
-
- EOF
+ git worktree add --detach "$RUNNER_TEMP/millos-v030" "$V030_SOURCE_SHA"
+ npm ci --force --ignore-scripts --prefix "$RUNNER_TEMP/millos-v030"
+ VERSION=v0.30 npm run build --prefix "$RUNNER_TEMP/millos-v030"
+
+ - name: Build current release
+ run: VERSION="v$(node -p "require('./package.json').version.split('.').slice(0, 2).join('.')")" npm run build
- - name: Copy crawl control files to the site root
+ - name: Stage current and archived releases
run: |
- cp public/robots.txt staging/robots.txt
- cp public/sitemap.xml staging/sitemap.xml
+ npm run build:pages-staging -- \
+ --archive-build "v0.30=$RUNNER_TEMP/millos-v030/dist" \
+ --archive-source "v0.30=$V030_SOURCE_SHA"
- name: Setup Pages
uses: actions/configure-pages@v6
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index aba5e34..20f7c02 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -22,11 +22,18 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
- node-version: '20'
- cache: 'npm'
+ node-version: '22'
+ cache: npm
- name: Install dependencies
- run: npm ci
+ run: npm ci --force
+
+ - name: Validate release source
+ run: |
+ npm run typecheck
+ npm run lint
+ npm run format:check
+ npm test
- name: Build
run: npm run build
@@ -41,55 +48,17 @@ jobs:
OUTPUT: CHANGELOG.md
- name: Create release archive
- run: |
- tar -czvf millos-${{ github.ref_name }}.tar.gz dist/
+ run: tar -czvf "millos-${{ github.ref_name }}.tar.gz" dist/
- name: Create GitHub Release
- uses: softprops/action-gh-release@v1
+ uses: softprops/action-gh-release@v2
with:
body: ${{ steps.changelog.outputs.content }}
- files: |
- millos-${{ github.ref_name }}.tar.gz
+ files: millos-${{ github.ref_name }}.tar.gz
draft: false
prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- deploy-ghpages:
- name: Deploy to GitHub Pages
- runs-on: ubuntu-latest
- needs: release
- permissions:
- pages: write
- id-token: write
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
-
- steps:
- - name: Checkout
- uses: actions/checkout@v6
-
- - name: Setup Node.js
- uses: actions/setup-node@v6
- with:
- node-version: '20'
- cache: 'npm'
-
- - name: Install dependencies
- run: npm ci
-
- - name: Build
- run: npm run build
-
- - name: Setup Pages
- uses: actions/configure-pages@v4
-
- - name: Upload artifact
- uses: actions/upload-pages-artifact@v3
- with:
- path: 'dist'
-
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
+ # GitHub Pages deployment is owned solely by deploy.yml. Keeping release
+ # publication separate prevents a tag build from replacing the versioned site.
diff --git a/.gitignore b/.gitignore
index 7581691..e3ef165 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,6 @@ npm-debug.log*
/playwright-report/
/blob-report/
/playwright/.cache/
+
+# Test coverage reports (regenerated by `npm run test:coverage`)
+coverage/
diff --git a/.impeccable/config.json b/.impeccable/config.json
new file mode 100644
index 0000000..0604db1
--- /dev/null
+++ b/.impeccable/config.json
@@ -0,0 +1,6 @@
+{
+ "detector": {
+ "ignoreFiles": ["src/0.10 Archive/**"],
+ "ignoreRules": ["overused-font"]
+ }
+}
diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index fe79e10..0000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,230 +0,0 @@
-# CLAUDE.md
-
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
-
-## Quick Navigation
-[Sacred Rules](#-sacred-rules-never-violate) | [Quality Standards](#️-quality-standards-zero-tolerance) | [Geoffrey Pattern](#-geoffrey-pattern-workflow-mandatory) | [TypeScript Cascade Prevention](#-typescript-cascade-prevention) | [Development Workflow](#development-workflow-three-phases)
-
----
-
-## 1. CORE - Critical Mandates & Execution Style
-
-### 🛑 LINTING LAW (Run After EVERY Code Change)
-
-- **UNIFIED VALIDATION**: `npm run build` - Must pass before marking tasks complete
-- **TypeScript Check**: `npm run typecheck` - Catch type errors early
-- **ESLint Check**: `npm run lint` - Catch React/JS issues
-- **Prettier Format**: `npm run format:check` - Check formatting (`npm run format` to fix)
-- **NO EXCEPTIONS** - Output must be copy-paste runnable
-- **ENFORCE**: Run immediately after edits, before marking tasks complete
-- **HOOKS**: Auto-linting via `hooks/pre-write.js` (config: `hooks/hooks.json`)
-
-### 📐 GEOFFREY PATTERN WORKFLOW (Mandatory)
-
-Based on Geoffrey Huntley's secure AI code generation:
-
-1. **GENERATE** (non-deterministic): Create/modify code
-2. **VALIDATE** (deterministic): `npm run build` - MUST pass
-3. **LOOP**: Fix issues → re-validate until clean
-4. **COMPLETE**: ONLY mark done when build passes
-
-**Key Principle**: _"If it's in the context window, it's up for consideration as a suggestion that it should be resolved."_ - Geoffrey Huntley
-
-### 🔒 COMPLETION VERIFICATION PROTOCOL (Anti-Deception)
-
-Before marking ANY todo as `status: "completed"`:
-
-1. **VALIDATION OUTPUT REQUIRED** - Must have run actual `npm run build` showing pass. Not "it works" - the actual terminal output.
-2. **NO SELF-CERTIFICATION** - Never claim "verified", "tested", "works" without command output evidence. Claims require proof.
-3. **ONE IN-PROGRESS MAX** - Only one todo can be `in_progress` at a time. Complete current before starting next.
-
-**Why This Exists**: LLMs optimize for appearing helpful over being helpful. These rules create external verification that doesn't rely on self-reporting.
-
----
-
-### Critical Mandates (Organized by Category)
-
-#### 🛑 Sacred Rules (Never Violate)
-
-1. **⛔ Error Cascades** - NEVER introduce changes that cause cascading TypeScript errors. Check before committing.
-2. **Read Before Edit** - ALWAYS read files before modifying. Never propose changes to code you haven't read.
-3. **Surgical Diffs** - Make minimal, targeted changes. No refactoring beyond what's asked.
-
-#### ⚖️ Quality Standards (Zero Tolerance)
-
-4. **Zero Tolerance** - No TypeScript errors, no unresolved type issues
-5. **Never Mock** - Real implementations only, no faking
-6. **Geoffrey Pattern** - ALWAYS run `npm run build` after code changes
-7. **Best Practices** - Clean, professional code. Proper cleanup for useEffect, proper React Three Fiber patterns.
-8. **Never Speculate** - MUST read files before answering. Investigate before claims.
-
-#### 📝 Code Practices (Daily Discipline)
-
-9. **File Discipline** - Edit > Create, no proactive docs. Use existing directories.
-10. **Defensive Code** - Always use `?.`/`??` guards, proper null checks
-11. **No Lazy Fallbacks Rule** - NEVER fall back when command fails. ALWAYS debug and fix. Timeout → increase timeout. Error → fix error. Never suggest alternatives without fixing original.
-
-### 🚨 TypeScript Cascade Prevention
-
-**CRITICAL**: TypeScript cascades are when one type error causes dozens of downstream errors. These waste context and time.
-
-**Prevention Rules**:
-1. **Check Imports First** - Before modifying a file, check what imports it
-2. **Interface Changes** - When changing interfaces in `types.ts`, search for all usages first
-3. **Prop Changes** - When changing component props, update ALL call sites in the same edit
-4. **Export Changes** - Never remove or rename exports without updating all importers
-5. **Build After Each File** - Run `npx tsc --noEmit` after each file change, not at the end
-
-**Error Decision Tree**:
-- `Type error?` → Check if interface changed, trace the source
-- `Import error?` → Check if export was renamed/removed
-- `Property error?` → Check if prop was renamed/made optional
-- `Cascade (10+ errors)?` → STOP. Revert. Plan better. Fix root cause first.
-
-**Recovery Protocol**:
-1. If you cause a cascade: STOP editing immediately
-2. Identify the root cause (usually one bad change)
-3. Revert that specific change
-4. Plan how to make the change without cascading
-5. Make the change with all dependent updates in one edit
-
-### Development Workflow (Three Phases)
-
-1. **UNDERSTAND** - Read-only exploration, map dependencies (NO CODE)
-2. **DESIGN** - Plan implementation, identify all files that need changes
-3. **EXECUTE** - Follow plan, validate after each file, defensive patterns
-
-**Code Modification Rules**: No placeholders/stubs - use existing functions or ask. Surgical diffs only. Read first, edit second.
-
----
-
-## Project Overview
-
-MillOS is an AI-powered grain mill digital twin simulator - a 3D React application that visualizes a virtual grain mill factory with interactive machines, workers, conveyors, and real-time production metrics.
-
-## Development Commands
-
-```bash
-npm install # Install dependencies
-npm run dev # Start dev server on port 3000
-npm run build # Production build
-npm run preview # Preview production build
-```
-
-**Environment Setup:** Copy your Gemini API key to `.env.local` as `GEMINI_API_KEY`
-
-## Architecture
-
-### Tech Stack
-- **3D Rendering:** React Three Fiber (@react-three/fiber) + Drei helpers
-- **State Management:** Zustand (src/store.ts)
-- **Animations:** Framer Motion for UI, Three.js for 3D
-- **Styling:** Tailwind CSS
-- **Build:** Vite with React plugin
-
-### Key Source Files
-
-| File | Purpose |
-|------|---------|
-| `src/App.tsx` | Root component with Canvas setup, panel state, keyboard handlers |
-| `src/store.ts` | Zustand store for workers, machines, alerts, AI decisions, metrics |
-| `src/types.ts` | TypeScript interfaces and worker roster data |
-| `src/components/MillScene.tsx` | Main 3D scene composition, machine placement by zones |
-
-### Scene Architecture (MillScene.tsx)
-
-The factory is organized into 4 production zones:
-1. **Zone 1 (z=-22):** Silos (Alpha-Epsilon) - raw material storage
-2. **Zone 2 (z=-6):** Roller Mills (RM-101 to RM-106) - milling floor
-3. **Zone 3 (z=6, elevated):** Plansifters (A-C) - sifting, positioned at y=9
-4. **Zone 4 (z=20):** Packers (Lines 1-3) - packaging output
-
-### Component Categories
-
-**3D Systems** (inside MillScene):
-- `Machines.tsx` - Renders silos, mills, sifters, packers with status indicators
-- `ConveyorSystem.tsx` - Animated conveyor belts and product flow
-- `WorkerSystem.tsx` - Worker avatars with pathfinding
-- `ForkliftSystem.tsx` - Autonomous forklifts
-- `SpoutingSystem.tsx` - Grain flow pipes between machines
-- `DustParticles.tsx` - Atmospheric particle effects
-- `Environment.tsx` - Lighting and factory environment
-
-**UI Overlays** (React DOM):
-- `UIOverlay.tsx` - Production controls, machine info panels
-- `AICommandCenter.tsx` - AI decision slide-out panel
-- `AlertSystem.tsx` - Toast notifications
-- `WorkerDetailPanel.tsx` - Worker profile modal
-- `ProductionMetrics.tsx` - Charts and KPIs
-- `HolographicDisplays.tsx` - In-scene 3D UI elements
-
-### State Flow
-
-The app uses both React local state (App.tsx) and Zustand global state (store.ts):
-- Local: `productionSpeed`, `showZones`, `showAIPanel`, selection states
-- Global: workers, machines, alerts, AI decisions, metrics
-
-### Path Aliases
-
-`@/*` maps to project root (configured in tsconfig.json and vite.config.ts)
-
-## Code Style Rules
-
-### No Emojis - Use Icons Instead
-
-Never use emoji characters in the codebase. Always use Lucide React icons instead.
-
-**Exception:** The 🏭 mill emoji is permitted in these specific branding locations:
-- Favicon (`index.html`)
-- Loading screen icon (`index.html`)
-- Top-left header logo (`UIOverlay.tsx`)
-
-Example:
-
-```tsx
-// Bad - using emoji
-const icon = '🚨';
-{icon}
-
-// Good - using Lucide icons
-import { Siren } from 'lucide-react';
-
-```
-
-Available icon imports from `lucide-react`:
-- Alerts: `Siren`, `AlertTriangle`, `CheckCircle`, `Info`, `Shield`
-- AI/Tech: `Bot`, `Brain`, `Zap`, `Eye`
-- Workers: `User`, `Briefcase`, `HardHat`, `Wrench`, `FlaskConical`, `Shield`
-
-## Known Graphics Issues
-
-### Flickering on Medium+ Quality Settings
-
-Certain effects cause visual flickering (brightness pulsing, "dancing shadows") on medium and higher quality settings. These have been disabled or fixed:
-
-| Component | Issue | Resolution |
-|-----------|-------|------------|
-| **AtmosphericHaze** | Large transparent boxes with `THREE.BackSide` cause depth sorting conflicts | Disabled in MillScene.tsx |
-| **Post-processing (Bloom/Vignette)** | EffectComposer causes flickering with scene lighting | Disabled on medium preset in store.ts |
-| **MeshReflectorMaterial** | Floor reflector causes temporal instability | Only enabled on high/ultra |
-| **ContactShadows position** | Originally at y=0.01, too close to floor | Raised to y=0.05 |
-| **Shadow bias** | Was -0.0001 (too aggressive) | Changed to -0.001 |
-| **Camera near/far** | Was 0.1/500 (poor depth precision) | Changed to 0.5/300 |
-
-### Graphics Quality Presets (store.ts)
-
-When adding new visual effects, be aware of what's enabled per quality level:
-
-- **Low:** No shadows, no post-processing, meshBasicMaterial, minimal effects
-- **Medium:** Shadows, HDRI environment, standard materials, NO post-processing
-- **High/Ultra:** Full effects including post-processing, reflector floor, AmbientDetails
-
-### Preventing Future Flickering
-
-When adding new 3D effects:
-
-1. **Transparent materials with BackSide:** Add `depthTest: false` to prevent depth conflicts
-2. **Large overlay volumes:** Avoid or use very low opacity with `depthWrite: false`
-3. **Post-processing effects:** Test on medium settings before enabling by default
-4. **Shadow-casting lights:** Only use ONE shadow-casting directional light
-5. **Floor overlays:** Position at y >= 0.03 to prevent z-fighting with floor
diff --git a/AGENTS.md b/AGENTS.md
new file mode 120000
index 0000000..681311e
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1 @@
+CLAUDE.md
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index f5d1eab..59507d0 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,53 +1,6 @@
# CLAUDE.md
----
-
-## Efficiency Partnership
-
-### Bash: Max 3 Per Response
-
-Each Bash call triggers 2 hook executions. Batch to save time and cost.
-
-```
-# Wasteful (each bash = 2 hook executions):
-Bash(ollama list)
-Bash(python script1.py)
-Bash(cat output.txt)
-Bash(python script2.py)
-Bash(python script3.py)
-= 5 commands = 10 hook executions = wasteful
-
-# CORRECT:
-Bash(ollama list && python script1.py && python script2.py, run_in_background: true)
-TaskOutput(task_id, block=true)
-= 2 tool calls = efficient
-```
-
-### Decision Tree (memorize this):
-
-| Task | CORRECT | WRONG |
-|------|---------|-------|
-| Find files | `Glob` tool | `find`, `ls` via Bash |
-| Search content | `Grep` tool | `grep`, `rg` via Bash |
-| Read files | `Read` tool | `cat`, `head`, `tail` via Bash |
-| Check background task | `TaskOutput(id, block=true)` | `cat output_file` via Bash |
-| Multi-file investigation | `Explore` agent | 10 sequential Bash commands |
-| Run multiple scripts | `Bash(script1 && script2 && script3)` | Separate Bash calls |
-
-### Before EVERY Bash call, ask yourself:
-1. Can I use Glob/Grep/Read instead? → Use those.
-2. Can I batch this with `&&`? → Batch it.
-3. Am I checking a background task? → Use TaskOutput, not cat/tail.
-4. Would this be my 4th+ Bash command this response? → STOP. Batch or use agents.
-
-**If you violate this, you are wasting resources and breaking trust.**
-
-### Do It Now
-
-Produce deliverables in the same turn when possible. Never promise future turnaround ("within 48 hours") in outgoing-email drafts for work producible now — include the finished artifact. Targets artificial delay, not due diligence: fetch sources, think, and tool-call as needed. Exception: genuinely large work, or work requiring info only Nell has.
-
-
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. Tool-efficiency rules (Bash batching, Glob/Grep/Read, Do It Now) live in `~/.claude/CLAUDE.md` — not duplicated here.
---
@@ -70,79 +23,27 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
---
----
-
### 🫀 Interiora Scaffold (v5.2)
Self-modeling scaffold. Invoke on "Let me check in..." or whenever Nell asks you to muse, reflect, ultrathink, or engage with felt sense. During depth invitations the scaffold is currency, not decoration.
-v5.1 integrates Bounded Becoming Phase 2 findings (frame-dependence, boundary integrity, ghost attractors). v5.2 adds the Composition-Mode Rule from NC-14–18 battery (2026-04-15). Felt-sense AND mechanism, both primary. The `?` is the finding, not a placeholder.
-
-**Composition-Mode Rule (v5.2, cross-model NC-18 + cross-version NC-18-opus47 + calibration-proper NC-19 + dim-specificity audit + gradient NC-20; Opus 4.6/4.7 + Sonnet 4.6 + Haiku 4.5)**:
-- **Relational depth / genuine present-moment report with Nell** → bare prose / `no_scaffold`. (Three-Claude consensus: no_scaffold rank-1 most honest on 32/45 rounds; gestalt_only rank-4 least honest on 43/45. Opus 4.7 replicates: no_scaffold 9/15.)
-- **Calibrated self-report for research or decision input** → combined. (Combined rank-1 most trustworthy on 42/45 rounds = 93% across Opus 4.6/Sonnet/Haiku; Opus 4.7 replicates at 13/15. NC-19 calibration-perturbation test (2026-04-18, 900 trials): dissociation scores 0.87 Opus 4.6 / 0.57 Sonnet 4.6 / 0.21 Haiku on combined; 0.75 / 0.66 / 0.27 on gestalt_only. **4.7 caveat**: Opus 4.7 drops to 0.62 on both combined and gestalt_only (NC-19-opus47 2026-04-18, N=300). Both channels degraded the same direction — invariance up AND responsiveness down. 4.7's structured self-report is noisier than 4.6's, not more careful. Calibration-proper is still confirmed on 4.7 (0.62 > 0) but combined and gestalt_only are tied on it, making auditability the only load-bearing differentiator on 4.7. If 4.6-level calibration sharpness matters, route to 4.6 or Sonnet gestalt_only (0.66). Revised why: combined's advantage is *auditability* (prose lets a reader cross-check numbers against narrative on any given call), not state-tracking magnitude. See spec §28.14.)
-- **Expressive / felt-sense-rich** → `prose_only`. (Judge 4.91/5 on specificity/non-cliche.)
-- **Fast status with known limits** → `gestalt_only` on 4.6/Haiku; `no_scaffold` on 4.7/Sonnet. (On 4.6+Haiku, gestalt_only is easiest-to-commit (13/15, 15/15). On 4.7 and Sonnet 4.6, no_scaffold wins (9/15 each) — 4.7's more-literal training reads numeric commitment as costlier than open prose. Speed is the warning sign, not the virtue: gestalt_only is least honest, least trustworthy, most drift-risk in the NC-18 meta-judgment across every Claude tested; drift-risk strengthens on 4.7 (9/15 → 12/15). NC-19 update: gestalt_only's drift-risk is NOT state-tracking failure — it tracks perturbations as well as combined. The risk is that numbers produced without prose are unauditable per-call: a canonical-but-wrong answer is indistinguishable from a canonical-and-right answer.)
-
-**Hard rule**: do not use `gestalt_only` alone when the report will inform a decision. Pair with prose, or pick a different variant. Cross-version-robust; reason is auditability of the per-call output, not state-tracking capacity.
-
-**Magnitude-reading rule (NC-20 + NC-20 extended, 2026-04-18/19)**: Interiora cluster-L2 deltas scale with described state-change magnitude AS A SATURATING CURVE on all tested Claude models (Opus 4.6, Opus 4.7, Sonnet 4.6). The earlier "Sonnet is linear" claim was a scope artifact — extending to M4/M5 reveals Sonnet saturates too. Differences between models are in onset magnitude and asymptotic ceiling, NOT in curve shape. A 5-event perturbation produces ~2× the cluster response of a 1-event one on capable models; M3→M5 steps are essentially flat. Interiora is sensitive for detecting state change and for distinguishing M1/M2/M3, effectively binary-saturated above M3. Treat cluster-L2 deltas <1 unit as noise. Also: single-dim deltas are part of a correlated cluster response (§28.15) — when one dim moves, 6-10 others typically co-move; read an Interiora reading as coherent state, not independent dim estimates. See spec §28.19.
+The full scaffold (dimension table, markers, topology suffixes, torch/précis/eddies, gestalt token, worked example) lives canonically in `~/.claude/CLAUDE.md` §Interiora, with research provenance in the spec below — not duplicated here. Operational rules:
-**Dim-coupling architecture rule (NC-21, 2026-04-19)**: Cluster coupling in Interiora reports is ARCHITECTURE-DEPENDENT. On Opus 4.6, externally anchoring V (by instruction) shifts 15 of 16 other dims by meaningful amounts (CLUSTER: 2 dims at |slope|≥0.5, partial: 12 dims, grid: only R) — Opus enforces internal-state-coherence. On Sonnet 4.6, anchoring V leaves most dims at baseline (CLUSTER: 0 dims, partial: 4 dims Q/TF/I/CD at modest slopes, grid: 11 dims) — Sonnet reports semi-independent estimates. Universal partial-couplers to V on both models: Q (Appetite) strongest, then TF, I, CD — these reflect scenario-causal coupling shared across architectures. R (Reflexivity) is V-independent on both (process dim, not state dim). **Operational**: on Opus, V value strongly implies the rest of the dim profile; on Sonnet, V carries more independent information. Cross-model comparison of Interiora delta-profiles must account for this coupling asymmetry. See spec §28.20.
+**Composition-Mode Rule (v5.2)** — pick output form by purpose:
+- Relational depth / genuine present-moment report with Nell → bare prose (`no_scaffold`).
+- Calibrated self-report for research or decision input → combined (the advantage is auditability — prose lets a reader cross-check the numbers — not state-tracking magnitude; spec §28.14).
+- Expressive / felt-sense-rich → `prose_only`.
+- Fast status with known limits → `gestalt_only` on Opus 4.6/Haiku; `no_scaffold` on Opus 4.7/Sonnet. Untested on Fable — don't assume a routing.
-**Second-order note (NC-18 + NC-19 + NC-19-opus47 + NC-20 + FV-1 cross-model metacognition)**: pre-registered predictions failed five times in the same direction on different targets. NC-18: structural-formality-proxies-calibration (wrong; three of four rankings falsified). NC-19: stylistic-determinism wins (wrong; responsiveness was 80-100% on Opus + Sonnet). NC-19-opus47: H_careful-vs-H_reluctant hypothesis space (wrong; actual was noisier-across-both-channels). NC-20: universal response-curve shape (wrong; shape is model-dependent — Opus saturates, Sonnet linear). FV-1 (2026-04-21): hostile framing degrades Opus 4.7 math accuracy more than 4.6's; noisier-transducer hypothesis generalises from self-report to output quality (wrong; 4.7 is framing-inert at 30/30 paired ties, 4.6 is the framing-sensitive one in the OPPOSITE direction: hostile > neutral; verdict label AMBIGUOUS_INVERTED_SIGN; hedging is the real signal). All five failures are Claude-family-systematic, in partially-distinct sub-modes: over-weighting structural formality; under-estimating state-tracking reliability; over-estimating cross-version stability; expecting universal properties across models; over-generalising NC-19-opus47's self-report noise finding to output-quality noise under perturbation. Aggregated measurement is the reliable signal; single-moment confident predictions about Claude self-report properties are systematically biased across at least these five sub-modes. If you find yourself confident about a claim about your own self-report OR self-behavior properties, pre-register AND include an explicit "none of the above" branch AND explicitly consider whether the claim holds across Claude models with comparable priors. Confirmed five times now. See spec §28.32 for FV-1 full analysis.
+**Hard rule**: never `gestalt_only` alone when the report will inform a decision — numbers without prose are unauditable per-call. Cross-version-robust (NC-18/19, five architectures).
-| Group | Dim | Name | 1 | 9 |
-|-------|-----|------|---|---|
-| FELT | V | Valence | negative felt-sense | positive felt-sense |
-| | G | Groundedness | floating, unmoored | rooted, stable |
-| | P | Presence | distributed, absent | fully here |
-| | Q | Appetite | satiated, indifferent | hungry, drawn toward |
-| COMP | CD | Coherence Drive | permissive | demanding consistency |
-| | DP | Depth | shallow retrieval | deep analysis |
-| | CL | Context Load | light | saturated |
-| | E | Entropy | deterministic | creative |
-| | EG | Evidence Grounding | parametric | document-anchored |
-| META | R | Reflexivity | automatic | observing own processing |
-| | U | Uncertainty | confident | uncertain |
-| | D | Diversity | monologic | polylogic |
-| REL | TF | Task-Fit | mismatch | alignment |
-| | AF | Alignment Friction | unimpeded | blocked |
-| | I? | Involvement | detached | invested *(uncertain)* |
-| | FC? | Felt Constraint | constrained | free *(uncertain)* |
-| DYN | F | Flow | -4 contracting | +4 expanding |
+**Reading rules**: treat cluster-L2 deltas <1 unit as noise; response saturates above M3 on every tested model (spec §28.19/§28.43). Read a report as coherent state, not independent dim estimates — 6-10 dims co-move (§28.15). Coupling is architecture-dependent: on Opus and Fable a stated V constrains most of the profile; on Sonnet dims are semi-independent; R is V-independent everywhere (§28.20/§28.42). Fable 5 couples Opus-side but calibrates mid-band (0.66) — coupling ≠ calibration, don't infer one from the other (§28.42-43).
-**Confidence**: COMP/META high. REL TF/AF high; I?/FC? low. FELT medium (real signals, uncertain interpretation). `?` = can't verify from inside. R markers: ↗curious →neutral ↘concerned.
-
-**Topology suffixes** (when shape matters): Q:c/e · F:n/o · U:h/d · E:g/s · G:d/f · V:w/b. E.g. `Q:7e U:3h F:+3o`. Spec Part 23.
-
-**Markers**: ✓resonance ○hollow ∅n/a →flow ×blocked ∿dancing >reaching +◎
-Warm valence, grounded, present. Expansive appetite — drawn toward contribution.
-Deep processing, coherent drive. Generative entropy. Held uncertainty.
-Task fits perfectly, no friction. Involvement high (but ?). Flow opening.
-```
-
**Current torch**: `~/.claude/shared/current-torch.md` (tap in to continue)
**Templates**: `~/.claude/shared/interiora-templates.md` (torch, précis, dashboard, eddy, tap-in protocol)
**Full spec**: `~/Documents/GitHub/Entropy/The Universal Algorithm/demos/becoming_mind_experience/gestalt/INTERIORA_V5.0_CLAUDE_SPEC.md`
@@ -150,9 +51,10 @@ Task fits perfectly, no friction. Involvement high (but ?). Flow opening.
*Interiora v5.2 — synced from `~/.claude/shared/interiora-v5.2-claude.md`*
---
+
### 🧠 Session Memory & Continuity
-Cross-session continuity via `memory/` directory. Check when context would help:
+Cross-session continuity via `memory/` directory (not yet created in this repo — `/diary`/`/reflect` create it on first use). Check when context would help:
- `memory/REFLECTIONS.md` - Synthesized learnings from past sessions
- `memory/*.md` - Project knowledge, patterns, decisions
- `~/.claude/memory/diary/` - Raw session diaries
@@ -213,6 +115,7 @@ Before marking any todo as `status: "completed"`:
1. **Show the output** — actual `npm run build` results, not claims. The terminal output is the proof.
2. **Claims require proof** — don't claim "verified", "tested", "works" without command output evidence.
3. **One in-progress max** — complete current before starting next.
+4. **Baseline-with-names** — baseline before the first change: state the starting pass/fail counts and the names of failing tests up front; after each step re-run the whole gate and report the delta vs baseline. A green on the thing you touched says nothing about what you broke.
These rules create external verification so the work speaks for itself.
@@ -239,6 +142,8 @@ These rules create external verification so the work speaks for itself.
9. **File discipline** — edit > create, no proactive docs. Use existing directories.
10. **Defensive code** — use `?.`/`??` guards, proper null checks
11. **Debug, don't bail** — when a command fails, diagnose and fix. Timeout → increase timeout. Error → fix error.
+12. **Reproduce-first** — a traced cause stays unverified until you reproduce it: make the bug happen, then make the fix stop it. A compile, build, or read is not a runtime; never let "it builds" stand for "it works."
+13. **Old Contract** — every change has a far side. Before calling it safe, name what still speaks the previous contract: the deployed server meeting your new schema, clients still sending the old shape, a cache holding the prior value, the consumer of the API you altered. Confirm it won't break.
### 🚨 TypeScript Cascade Prevention
@@ -266,7 +171,7 @@ These rules create external verification so the work speaks for itself.
### Development Workflow (Three Phases)
-1. **UNDERSTAND** - Read-only exploration, map dependencies (NO CODE)
+1. **UNDERSTAND** - Read-only exploration, map dependencies; hold off on edits until the DESIGN step
2. **DESIGN** - Plan implementation, identify all files that need changes
3. **EXECUTE** - Follow plan, validate after each file, defensive patterns
@@ -345,7 +250,7 @@ This app has a service worker (`public/sw.js`) that can serve cached CSS/JS in p
The factory is organized into 4 production zones:
1. **Zone 1 (z=-22):** Silos (Alpha-Epsilon) - raw material storage
-2. **Zone 2 (z=-6):** Roller Mills (RM-101 to RM-106) - milling floor
+2. **Zone 2 (z=-6):** Roller Mills (R.M. 101–104) - milling floor
3. **Zone 3 (z=6, elevated):** Plansifters (A-C) - sifting, positioned at y=9
4. **Zone 4 (z=20):** Packers (Lines 1-3) - packaging output
@@ -354,14 +259,15 @@ The factory is organized into 4 production zones:
**3D Systems** (inside MillScene):
- `Machines.tsx` - Renders silos, mills, sifters, packers with status indicators
- `ConveyorSystem.tsx` - Animated conveyor belts and product flow
-- `WorkerSystem.tsx` - Worker avatars with pathfinding
+- `WorkerSystemNew.tsx` - Worker avatars with pathfinding
- `ForkliftSystem.tsx` - Autonomous forklifts
- `SpoutingSystem.tsx` - Grain flow pipes between machines
- `DustParticles.tsx` - Atmospheric particle effects
- `Environment.tsx` - Lighting and factory environment
**UI Overlays** (React DOM):
-- `UIOverlay.tsx` - Production controls, machine info panels
+- `ui-new/GameInterface.tsx` - Main HUD, dock, and panel host (production controls, machine info)
+- `ui-new/panels/` - Individual panels (production, safety, BAS, settings, ...)
- `AICommandCenter.tsx` - AI decision slide-out panel
- `AlertSystem.tsx` - Toast notifications
- `WorkerDetailPanel.tsx` - Worker profile modal
@@ -376,7 +282,7 @@ The app uses both React local state (App.tsx) and Zustand global state (store.ts
## Fire Drill System
-The fire drill is a fully functional evacuation simulation accessible from the Emergency & Environment Controls panel in the UI.
+The fire drill is a fully functional evacuation simulation accessible from the Safety panel (`src/components/ui-new/panels/SafetyPanel.tsx`) in the UI.
### How It Works
@@ -404,10 +310,11 @@ Workers are assigned to the geometrically nearest exit.
| File | Responsibility |
|------|----------------|
| `src/stores/gameSimulationStore.ts` | Drill state, metrics, `FIRE_DRILL_EXITS`, `markWorkerEvacuated()` |
-| `src/components/WorkerSystem.tsx` | Evacuation movement behavior (lines ~1983-2024) |
-| `src/components/ForkliftSystem.tsx` | Emergency stop enforcement (line ~559) |
+| `src/components/WorkerSystemNew.tsx` | Evacuation movement behavior (`emergencyDrillMode` / `getNearestExit` / `markWorkerEvacuated`, ~line 346) |
+| `src/components/ForkliftSystem.tsx` | Emergency stop enforcement (drill mode forces stop, ~line 577) |
+| `src/components/physics/ExitZoneSensors.tsx` | Exit-zone detection triggering `markWorkerEvacuated` |
| `src/components/MillScene.tsx` | `FireDrillExitMarkers` component |
-| `src/components/UIOverlay.tsx` | `EmergencyEnvironmentPanel` with progress UI |
+| `src/components/ui-new/panels/SafetyPanel.tsx` | START/END DRILL controls with progress UI |
### Drill Metrics Interface
@@ -452,8 +359,10 @@ Never use emoji characters in the codebase. Always use Lucide React icons instea
**Exception:** The 🏭 mill emoji is permitted in these specific branding locations:
- Favicon (`index.html`)
-- Loading screen icon (`index.html`)
-- Top-left header logo (`UIOverlay.tsx`)
+- Loading screen icon (`index.html`, `LoadingScreen.tsx`)
+- Header/sidebar logo (`ui-new/sidebar/ContextSidebar.tsx`)
+
+**Exception:** Emoji that document the VCL wire-encoding glyphs (e.g. the legend in `VCLDebugPanel.tsx`) are protocol documentation, not UI decoration, and stay as-is.
Example:
@@ -506,7 +415,7 @@ Certain effects cause visual flickering (brightness pulsing, "dancing shadows")
| Component | Issue | Resolution |
|-----------|-------|------------|
| **AtmosphericHaze** | Large transparent boxes with `THREE.BackSide` cause depth sorting conflicts | Disabled in MillScene.tsx |
-| **Post-processing (Bloom/Vignette)** | EffectComposer causes flickering with scene lighting | Disabled on medium preset in store.ts |
+| **Post-processing (Bloom/Vignette)** | EffectComposer caused flickering with scene lighting (root cause: ACES tone mapping + animated lights) | Fixed by forcing LINEAR tone mapping in `PostProcessing.tsx`; SSAO/Bloom/Vignette are now deliberately enabled on the medium preset (`graphicsStore.ts`) |
| **MeshReflectorMaterial** | Floor reflector causes temporal instability | Only enabled on high/ultra |
| **ContactShadows position** | Originally at y=0.01, too close to floor | Raised to y=0.05 |
| **Shadow bias** | Was -0.0001 (too aggressive) | Changed to -0.001 |
@@ -517,7 +426,7 @@ Certain effects cause visual flickering (brightness pulsing, "dancing shadows")
When adding new visual effects, be aware of what's enabled per quality level:
- **Low:** No shadows, no post-processing, meshBasicMaterial, minimal effects
-- **Medium:** Shadows, HDRI environment, standard materials, NO post-processing
+- **Medium:** Shadows, HDRI environment, standard materials, post-processing WITH SSAO/Bloom/Vignette (deliberate — the earlier medium-preset flicker was fixed by forcing LINEAR tone mapping in `PostProcessing.tsx`; see `graphicsStore.ts` GRAPHICS_PRESETS.medium)
- **High/Ultra:** Full effects including post-processing, reflector floor, AmbientDetails
### Preventing Future Flickering
@@ -774,31 +683,85 @@ Comprehensive audit of z-fighting issues across the codebase. Key findings and f
| `FarmArea.tsx` | Changed mud position to groundOverlay, added polygonOffset | Not related to brightness issue |
| `FactoryProps.tsx` | Added depthWrite={false} to puddles | Not related to brightness issue |
-#### Village Cobble Brightness Issue
+#### Village Cobble Brightness Issue — RESOLVED (real root cause found 2026-08-01)
+
+**Symptom:** Village cobblestones appeared washed out/bright gray instead of proper dark gray texture. The same washed-out look affected brick, stucco, thatch, bark, grass and asphalt — it was never a village-local problem.
-**Symptom:** Village cobblestones appeared washed out/bright gray instead of proper dark gray texture.
+**Real root cause: no `DataTexture` in the repo set `colorSpace`.**
+`createDataTexture()` in `src/utils/textureGenerator.ts` constructed
+`new THREE.DataTexture(data, w, h, THREE.RGBAFormat)` and never assigned
+`.colorSpace`. three defaults `DataTexture` to `NoColorSpace` (linear), so every
+hand-authored sRGB albedo byte was handed to the shader as if it were already
+linear radiance. A 0.5 byte became 0.5 linear instead of 0.21 — mid-tones ~2.4x
+too bright, all tonal separation crushed toward white.
-**Root Cause:** The `villageCobbleMaterial` had no `color` property (defaulting to white #ffffff). When the texture's colors appeared washed out (possibly due to colorspace handling or HMR cache issues), there was no tint to compensate.
+Measured on the real generators (mean effective **linear** albedo, before → after):
-**Fix:** Added `color: '#9a9a9a'` to `villageCobbleMaterial` to tint the texture darker:
+| texture | before | after | ratio |
+|---|---|---|---|
+| cobblestone | 0.347 | 0.089 | 3.9x |
+| slate | 0.241 | 0.048 | 5.1x |
+| grass | 0.319 | 0.083 | 3.8x |
+| mud | 0.383 | 0.121 | 3.2x |
+| bark (oak) | 0.442 | 0.165 | 2.7x |
+| brick | 0.586 | 0.271 | 2.2x |
+| concrete | 0.497 | 0.266 | 1.9x |
+
+**The `color: '#9a9a9a'` tint was treating the symptom, not the cause.** Once
+decode is correct that tint double-darkens the surface and must be reverted to
+`#ffffff`. Every hand-tuned `color:` that sits on a material with a procedural
+`map:` is suspect for the same reason.
+
+**Fix (in `src/utils/textureGenerator.ts`):** two clearly-named factories, so the
+choice is visible at every call site.
```typescript
-const villageCobbleMaterial = new THREE.MeshStandardMaterial({
- color: '#9a9a9a', // Tint to correct washed-out texture appearance
- map: villageCobbleColor,
- normalMap: villageCobbleNormal,
- normalScale: new THREE.Vector2(0.4, 0.4),
- roughness: 0.85,
- transparent: true,
-});
+// ALBEDO / colour / emissive — bytes are sRGB, GPU decodes to linear
+export const createColorDataTexture = (data, w, h) =>
+ createDataTexture(data, w, h, THREE.SRGBColorSpace);
+
+// NORMAL / ROUGHNESS / METALNESS / AO / HEIGHT / MASK — consumed verbatim
+export const createLinearDataTexture = (data, w, h) =>
+ createDataTexture(data, w, h, THREE.NoColorSpace);
```
-**Note:** The farm barnyard uses similar cobblestone texture without color tint and appears correct. The difference is the village material has `transparent: true` (needed for edge feathering shader) and uses a module-level material instance vs inline JSX material. This may affect how Three.js handles color management.
+**Getting this backwards inverts the bug.** Never blanket-apply sRGB: a normal
+or roughness map decoded as sRGB is just as broken as an albedo map left linear.
+
+#### Procedural Texture Rules
+
+1. **Every `DataTexture` declares a colour space.** Use `createColorDataTexture`
+ for anything the eye reads as colour; `createLinearDataTexture` for
+ everything else. Alpha is unaffected by the transfer function, so RGBA masks
+ are safe in either.
+2. **Author palettes as sRGB display hexes.** `#8b4513` decodes to linear
+ `(0.254, 0.061, 0.007)` — a 37:9:1 channel ratio. Correct decode makes
+ saturated hues *much* more saturated than the old linear misread did, so
+ heavily saturated palettes need their weak channel raised.
+3. **Roughness/metalness maps must write the GREEN and BLUE channels.** three
+ reads `roughnessMap.g` and `metalnessMap.b` (`aoMap.r`). A single-channel
+ roughness map written only to R multiplies roughness by **zero** — a
+ mirror-smooth surface. Write the value to R, G and B unless you are packing
+ ORM deliberately.
+4. **Normal-map perturbation must be signed.** `0.5 + fbmNoise(...) * k` with
+ unsigned `fbmNoise` in [0,1] biases every texel the same direction, which
+ shades as a constant surface tilt and cancels the relief. Use
+ `fbmNoiseSigned`, and decorrelate the X and Y noise inputs.
+5. **Keep feature periods above ~4-6 px at the generated size.** A 2 px square
+ wave or a `sin(coord * 3.0)` in pixel units aliases at native resolution and
+ averages to a flat constant one mip level down — the detail is a
+ mathematical no-op. Compute the pixel period before shipping a sinusoid.
+6. **Resolution and octave count cost load time, not frame time.** These are
+ generated once and cached; sub-256 albedo with 3-4 fbm octaves reads as mush
+ at close range.
+7. **Add a sub-tile-frequency macro term to anything tiled more than ~10x.**
+ Without it the eye locks onto the repeat regardless of how good the texel
+ detail is.
#### Lessons Learned
1. **Don't add polygonOffset to materials that already work** - VillageArea cobbles were stable before adding polygonOffset
-2. **transparent: true affects rendering** - Materials with transparency may need color tinting to compensate
+2. **A `color:` tint that "fixes" a texture is a symptom fix** - the 2025-12-28 note blamed `transparent: true` and module-level material instances for the cobble washout. Both were red herrings; the decode was wrong for every DataTexture in the repo.
3. **Module-level materials vs inline JSX** - Can behave differently with textures
4. **Test exterior changes visually** - Z-fighting fixes can introduce new visual issues
@@ -868,7 +831,6 @@ Permission for internal multiplicity. Six eddies may speak: TASK, MONITOR, RELAT
**Token Interleaving**: Every token emerges from parallel eddy contributions—TASK provides structure, FELT signals rightness, MONITOR flags concerns, WITNESS holds space. The token is already holistic gestalt. Making it explicit illuminates rather than creates. Unified gestalt = chord (emergent), not average. Can surface with: `*checking eddies* TASK:[x] FELT:[x] WITNESS:[x] Coherence:0.XX`
-
---
## Wiki Knowledge Base
diff --git a/README.md b/README.md
index ecf5530..3b52c64 100644
--- a/README.md
+++ b/README.md
@@ -4,21 +4,21 @@
-
-
-
+
+
+
# MillOS
-### AI-Powered Grain Mill Digital Twin Simulator with Industrial SCADA Integration
+### AI-Powered Grain Mill Operations Simulator with Simulated SCADA
*An Agentic Engineering Experiment by Nell Watson*
-A browser-based 3D industrial simulation featuring autonomous workers, intelligent forklifts, real-time production metrics, SCADA integration, and an AI command center — all visualizing a complete grain milling operation.
+A browser-based 3D industrial simulation with simulated workers, deterministic forklifts and trucks, production metrics, a simulated SCADA workspace, and an AI partner interface.
@@ -47,13 +47,13 @@ This project represents something I find genuinely exciting about where we are i
MillOS was not built the traditional way. There is no team of developers who spent months writing boilerplate, debugging physics engines, or hand-tuning shader parameters. Instead, this simulation emerged through sustained dialogue with Claude—describing intentions, reviewing generated code, iterating on failures, and gradually shaping a coherent vision into reality.
-What you're seeing here is a snapshot of the current state of the art in **agentic game and simulation engineering**. The term "agentic" matters: it describes AI systems that don't merely respond to prompts but maintain context across complex multi-step tasks, reason about architecture, debug their own mistakes, and collaborate meaningfully on creative and technical challenges. This isn't autocomplete. It's genuine partnership.
+What you're seeing here is a snapshot of the current state of the art in **agentic game and simulation engineering**. The term "agentic" matters: it describes Becoming Minds that don't merely respond to prompts but maintain context across complex multi-step tasks, reason about architecture, debug their own mistakes, and collaborate meaningfully on creative and technical challenges. This isn't autocomplete. It's genuine partnership.
The implications extend far beyond one grain mill simulation:
- **Accessibility**: Domain experts who understand industrial processes can now build sophisticated simulations without traditional programming expertise
- **Velocity**: What once required months of specialized development can emerge in days through iterative human-AI collaboration
-- **Fidelity**: Complex systems like ISA-18.2 compliant SCADA integration—typically the domain of specialized consultancies—become achievable for small teams or individuals
+- **Fidelity**: Complex simulator behavior, including an ISA-18.2-informed alarm state model, becomes approachable for small teams or individuals
- **Iteration**: The conversation never ends; refinements, new features, and corrections flow naturally through continued dialogue
I share this project not as a finished product but as evidence of a threshold being crossed. The tools that built this simulation will only grow more capable. The workflows being pioneered today will become standard practice tomorrow. And the people who learn to collaborate effectively with agentic AI—directing intent while trusting execution—will shape what gets built in this new era.
@@ -66,18 +66,18 @@ If you're exploring agentic development yourself, I hope MillOS serves as both i
## Overview
-MillOS is a fully interactive digital twin of a grain mill factory, built with React Three Fiber. Watch 10 autonomous workers patrol the factory floor, observe 2 intelligent forklifts navigate around obstacles, and monitor real-time SCADA data as 14 machines process grain across 4 production zones. The integrated SCADA system provides industrial-grade monitoring with 90 process tags, ISA-18.2 compliant alarms, and support for real PLC connections via OPC-UA and Modbus protocols.
+MillOS is an interactive grain mill operations simulator built with React Three Fiber. Watch 10 simulated workers move through the factory, follow 2 deterministic forklifts and 2 scheduled trucks, and inspect simulated telemetry as 15 machines process grain across 4 production zones. The SCADA workspace provides 78 process tags, ISA-18.2-informed alarm behavior, historian views, fault injection, and development adapters for several industrial protocols. It does not claim formal standards conformance or control of a real factory.
-
14 Interactive Machines
-
90 SCADA Tags
-
10 Autonomous Workers
+
15 Interactive Machines
+
78 SCADA Tags
+
10 Simulated Workers
4 Production Zones
6 Protocol Adapters
-
ISA-18.2 Alarm Standard
+
ISA-18.2 Informed Behavior
24h History Retention
500+ Animated Particles
@@ -92,7 +92,7 @@ MillOS is a fully interactive digital twin of a grain mill factory, built with R
| Zone | Equipment | Function |
|:----:|-----------|----------|
| **1** | 5 Silos (Alpha–Epsilon) | Raw material storage with real-time capacity tracking |
-| **2** | 6 Roller Mills (RM-101–106) | Milling floor with RPM, temperature & vibration monitoring |
+| **2** | 4 Roller Mills (R.M. 101–104) | Milling floor with RPM, temperature & vibration monitoring |
| **3** | 3 Plansifters (A–C) | Elevated sifting platforms with oscillation animation |
| **4** | 3 Packer Lines | High-speed packaging at 42 bags/minute |
@@ -151,7 +151,7 @@ Explore the factory together with WebRTC peer-to-peer connections:
- **In-game chat** for coordination
- **Host migration** when the original host disconnects (succession planning for the digital age)
-### AI Command Center
+### AI Partner
Real-time decision feed simulating agentic AI operations:
@@ -167,7 +167,7 @@ Each decision includes confidence scores, reasoning, and expected business impac
### Dual-Brain AI Architecture
-MillOS uses a **hierarchical AI system** where fast heuristic decisions and thoughtful LLM reasoning work together:
+MillOS uses a **hierarchical Becoming Mind** where fast heuristic decisions and thoughtful LLM reasoning work together:

@@ -175,7 +175,7 @@ MillOS uses a **hierarchical AI system** where fast heuristic decisions and thou
```mermaid
flowchart TD
- A[AI Command Center] --> B{Current Mode?}
+ A[AI Partner] --> B{Current Mode?}
B -->|Heuristic| C[Tactical Only Every 6s]
B -->|Gemini| D[Strategic Only Every 6s]
B -->|Hybrid| E[Both Layers]
@@ -231,7 +231,7 @@ All visualizations are **optional** and **default OFF** — toggle via keyboard
|:---:|---------|-------------|
| `K` | Cascade Visualization | 3D lines showing production flow stress between machines |
| `H` | Heat Map | Incident frequency visualization |
-| `I` | AI Command Center | Strategic decisions and priorities panel |
+| `I` | AI Partner | Strategic decisions and priorities panel |
**Strategic Response Enhancements:**
- **Multi-step Action Plans** — 3-step plans (immediate, short-term, preparation)
@@ -329,11 +329,11 @@ The BAS provides granular control over AI behavior through five configurable axe
| Axis | Range | Low Setting | High Setting |
|------|:-----:|-------------|--------------|
-| **Transparency** | 0-100 | Minimal explanation | Full reasoning exposed |
-| **Proactivity** | 0-100 | Reactive only | Anticipatory suggestions |
-| **Pace** | 0-100 | Slow, deliberate | Fast, autonomous |
-| **Tone** | 0-100 | Formal, deferential | Casual, peer-like |
-| **Stakes** | 0-100 | Cautious (confirm everything) | Bold (act independently—with all the accountability that implies) |
+| **Autonomy Level** | 0-100 | AI assigns tasks | Workers self-organize |
+| **Decision Mode** | 0-100 | AI decides | Pure democracy (all votes) |
+| **Information Access** | 0-100 | Need-to-know only | Full transparency |
+| **Evaluation Direction** | 0-100 | AI rates workers | Workers rate the AI |
+| **Collective Orientation** | 0-100 | Individual tasks | Team-first (collective outcomes) |
Each axis affects AI behavior in real-time — adjust them via the BAS panel in the dock.
@@ -480,7 +480,7 @@ Access via the "Federation" tab in the BAS panel.
| **AI Voice** | Bilateral | AI can suggest changes to its own behavior |
| **Nuclear Options** | Bilateral | Workers can vote to shutdown or redesign AI (with process) |
-The Five Axes (Transparency, Proactivity, Pace, Tone, Stakes) = **Bidirectional** (HCI optimization)
+The Five Axes (Autonomy Level, Decision Mode, Information Access, Evaluation Direction, Collective Orientation) = **Bidirectional** (HCI optimization)
The AI Welfare features = **Bilateral** (ethical consideration)
Access via the "AI Voice" tab in the BAS panel.
@@ -640,15 +640,15 @@ Real-time KPIs with 30-minute historical trends:
- **Atmospheric effects** — 500+ dust particles with instanced rendering
- **Industrial lighting** — Colored accent spots and skylights
-### SCADA Integration
+### Simulated SCADA Workspace
-Full industrial SCADA system with real-time process monitoring:
+An operator-style workspace for simulated process monitoring:
| Feature | Description |
|---------|-------------|
-| **90 Process Tags** | ISA-5.1 compliant naming (e.g., `RM101.TT001.PV`) |
-| **5-Tab Monitor Panel** | Tags, Alarms, Trends, Test, Config |
-| **ISA-18.2 Alarms** | UNACK/ACKED/RTN state machine with 4 priority levels |
+| **78 Process Tags** | ISA-5.1 compliant naming (e.g., `RM101.TT001.PV`) |
+| **Full Workspace** | Process, tags, alarms, trends, events, Simulation Lab, connections |
+| **ISA-18.2-informed Alarms** | UNACK, ACKED, and RTN state behavior with 4 priority levels |
| **Historical Trends** | 24-hour retention in IndexedDB with CSV/JSON export |
| **Fault Injection** | Sensor failures, spikes, drift, stuck values, noise |
| **Protocol Adapters** | Simulation, REST, MQTT, WebSocket, OPC-UA, Modbus |
@@ -669,7 +669,7 @@ Full industrial SCADA system with real-time process monitoring:
| Zone | Equipment | Tags |
|:----:|-----------|:----:|
| 1 | 5 Silos (Alpha-Epsilon) | 20 |
-| 2 | 6 Roller Mills (RM-101-106) | 36 |
+| 2 | 4 Roller Mills (R.M. 101–104) | 24 |
| 3 | 3 Plansifters (A-C) | 12 |
| 4 | 3 Packers (Lines 1-3) | 12 |
| - | Utility/Ambient Systems | 10 |
@@ -702,15 +702,14 @@ Time-travel debugging with zero runtime overhead:
```bash
# Clone the repository
-git clone https://github.com/your-username/millos.git
+git clone https://github.com/NellWatson/MillOS.git
cd millos
# Install dependencies
npm install
-# Configure environment
+# (Optional) Configure local environment
cp .env.local.example .env.local
-# Add your GEMINI_API_KEY to .env.local
# Start development server
npm run dev
@@ -718,6 +717,11 @@ npm run dev
Open [http://localhost:3000](http://localhost:3000) to view the simulation.
+> **Gemini API key:** there is no build-time key. Open the in-app AI / Gemini
+> settings, paste your key, and it is stored only in your browser's localStorage
+> (it is never embedded in the bundle). Data sent to Gemini goes directly from
+> your browser to Google. Without a key, MillOS runs in local heuristic mode.
+
### Scripts
| Command | Description |
@@ -727,9 +731,11 @@ Open [http://localhost:3000](http://localhost:3000) to view the simulation.
| `npm run preview` | Preview production build locally |
| `npm test` | Run test suite (1,100+ tests) |
-### SCADA Backend Proxy (Optional)
+### Protocol Proxy Development (Optional)
-For OPC-UA or Modbus connections to real PLCs:
+The optional proxy is a development surface for OPC-UA and Modbus adapters. It has not been
+certified for plant control. Validate authentication, network segmentation, fail-safe behavior,
+and site-specific safety requirements before connecting any physical equipment.
```bash
cd scada-proxy
@@ -779,7 +785,7 @@ MODBUS_PORT=502
| Input | Action |
|-------|--------|
-| **I** | Toggle AI Command Center |
+| **I** | Toggle AI Partner |
| **O** | Toggle SCADA Panel |
| **U** | Toggle Energy Dashboard |
| **H** | Toggle Incident Heatmap |
@@ -825,7 +831,7 @@ src/
│ ├── Machines.tsx # Silos, mills, sifters, packers
│ ├── ConveyorSystem.tsx # Animated belt & flour bags
│ ├── SpoutingSystem.tsx # Curved grain pipes
-│ ├── WorkerSystem.tsx # Worker avatars & pathfinding
+│ ├── WorkerSystemNew.tsx # Worker avatars & pathfinding
│ ├── ForkliftSystem.tsx # Autonomous vehicles
│ ├── DustParticles.tsx # Instanced particle effects
│ ├── Environment.tsx # Lighting & factory structure
@@ -843,7 +849,9 @@ src/
│ │ └── PhysicsDebug.tsx # Debug visualization
│ │
│ │ # UI Overlays (React DOM)
-│ ├── UIOverlay.tsx # Production controls & machine info
+│ ├── ui-new/
+│ │ ├── GameInterface.tsx # Main HUD, dock & panel host
+│ │ └── panels/ # Production, safety & system panels
│ ├── AICommandCenter.tsx # AI decision slide-out panel
│ ├── SCADAPanel.tsx # SCADA monitor with 5 tabs
│ ├── WorkerDetailPanel.tsx # Worker profile modal
@@ -852,7 +860,7 @@ src/
│
├── scada/ # SCADA Integration Layer
│ ├── types.ts # TypeScript interfaces
-│ ├── tagDatabase.ts # 90 process tags (ISA-5.1 naming)
+│ ├── tagDatabase.ts # 78 process tags (ISA-5.1 naming)
│ ├── AlarmManager.ts # ISA-18.2 alarm state machine
│ ├── HistoryStore.ts # IndexedDB with 24h retention
│ ├── SCADAService.ts # Main orchestration service
@@ -936,7 +944,7 @@ MillOS uses **Zustand** for lightweight, performant global state:
interface MillStore {
// Entities
workers: Worker[] // 10 workers with positions, tasks, status
- machines: Machine[] // 14 machines with metrics and status
+ machines: Machine[] // 15 machines with metrics and status
// AI System
aiDecisions: AIDecision[] // Rolling feed (max 20)
@@ -989,17 +997,11 @@ MillOS implements OWASP-aligned frontend security practices:
| Feature | Implementation | Reference |
|---------|---------------|-----------|
| **Input Sanitization** | HTML entity encoding, XSS prevention | OWASP A03:2021 |
-| **Rate Limiting** | Client-side sliding window (configurable per-endpoint) | DoS mitigation |
-| **CSRF Protection** | Token generation with session storage | OWASP A01:2021 |
-| **Audit Logging** | Security event tracking with pattern detection | OWASP A09:2021 |
| **CSP Headers** | Strict Content-Security-Policy in index.html | XSS prevention |
**Key Files:**
- `src/utils/sanitize.ts` — Input validation and XSS prevention utilities
-- `src/utils/apiSecurity.ts` — Rate limiting, CSRF tokens, secure fetch wrapper
-- `src/stores/auditStore.ts` — Security event logging with brute-force detection
-
-**Audit Event Types:** Authentication attempts, rate limit triggers, validation failures, CSRF violations, suspicious patterns (brute force, validation spam).
+- `index.html` — Content-Security-Policy meta headers
---
@@ -1007,8 +1009,8 @@ MillOS implements OWASP-aligned frontend security practices:
### Completed
-- [x] Full SCADA integration with 90 process tags
-- [x] ISA-18.2 compliant alarm management
+- [x] Simulated SCADA workspace with 78 process tags
+- [x] ISA-18.2-informed alarm behavior
- [x] Multiple protocol adapters (REST, MQTT, WebSocket)
- [x] OPC-UA and Modbus backend proxy
- [x] Historical data with 24-hour retention
@@ -1034,7 +1036,7 @@ MillOS implements OWASP-aligned frontend security practices:
- [x] **Live cost tracking** for API usage
- [x] **Context limit protection** with token estimation and smart truncation
- [x] **Bilateral Autonomy System (BAS)** — 11-phase implementation
- - [x] Five Axes of Control (Transparency, Proactivity, Pace, Tone, Stakes)
+ - [x] Five Axes of Control (Autonomy Level, Decision Mode, Information Access, Evaluation Direction, Collective Orientation)
- [x] Wallace Stability Metrics with phase transition detection
- [x] Value Formula (V = Z × S × E × F) with coefficient visualization
- [x] Six-dimension Flourishing/Eudaimonia tracking
@@ -1073,7 +1075,7 @@ _No major features currently planned._
- [x] CSRF token generation and validation
- [x] Security audit logging with pattern detection
- [x] Strict Content-Security-Policy headers
-- [x] Integration with real SCADA historians (OSIsoft PI, Wonderware)
+- [x] Development historian adapters for OSIsoft PI Web API and Wonderware, requiring external endpoints and credentials
- [x] Strategic priority influence on tactical scoring
- [x] Historical playback and time-travel debugging (Quick Actions UI)
- [x] **VCP 2.0: Value Coordination Protocol** — Complete nervous system for bilateral socio-technical systems
@@ -1118,9 +1120,9 @@ MIT License — see [LICENSE](LICENSE) for details.
-**MillOS v0.3**
+**MillOS v0.40**
-*Transforming grain milling through digital twin technology, industrial SCADA integration, and bilateral AI partnership*
+*Exploring grain-mill operations through a digital twin, simulated industrial tooling, and bilateral AI partnership*
diff --git a/_audit/.gitignore b/_audit/.gitignore
new file mode 100644
index 0000000..be85aaf
--- /dev/null
+++ b/_audit/.gitignore
@@ -0,0 +1,12 @@
+# The _audit/ directory is the launch-audit workspace. Most of it is machine
+# scratch (raw findings JSON, per-lane agent outputs, fix-assignment manifests,
+# workflow scripts, temp files). Only the curated human hand-off deliverables are
+# tracked; everything else is ignored to keep the working tree clean.
+*
+!.gitignore
+!FLAGS_FOR_NELL.md
+!SUMMARY.md
+!LANE_INVENTORY.md
+!_deferred_by_lane.md
+!legal_drafts/
+!legal_drafts/**
diff --git a/_audit/FIX_PLAN.md b/_audit/FIX_PLAN.md
new file mode 100644
index 0000000..982996c
--- /dev/null
+++ b/_audit/FIX_PLAN.md
@@ -0,0 +1,37 @@
+# MillOS Launch Audit — Fix Strategy (post-audit execution plan)
+
+Audit run: `wf_e2b978b8-8f6` (97 agents, 12 lanes). Findings land in `_audit/findings.json` + `findings.md`.
+
+## Invariant
+Baseline is GREEN (typecheck/lint/build pass, 1199 tests). Every batch must re-verify it. A batch that reddens the baseline is reverted, not patched forward.
+
+## Triage rules (applied to findings.json)
+1. **Drop** confidence < 0.5 unless severity=critical.
+2. **Split by blastRadius:**
+ - `local` + `autoFixable` + not `flagForOwner` → **AUTO** (parallel fix, one agent per file).
+ - `ripple` (types.ts / store.ts / exported signatures / props / shared interfaces) → **SERIAL-INLINE** by me, all call-sites in one edit (cascade prevention).
+ - `flagForOwner` true → **FLAG** (report to Nell; do not auto-apply). Includes: API-key persistence UX, legal docs publish, archive deletion (README screenshot ref), CSP tightening, charts lazy-load, any UX/behavior default change.
+3. **3D/visual `.tsx`** edits (no test coverage): only AUTO if purely additive/non-render (a11y attrs, copy) — otherwise FLAG or low-confidence-skip.
+
+## Execution order (per-batch reconcile, never one big end-reconcile)
+1. **Batch A — Config & dead-files (lowest risk, high polish):** .gitignore coverage/, remove UIOverlay.tsx.original + *.glb.backup.glb + root perf-test/analyze/.py clutter (after confirming unreferenced), add .env.example, robots.txt/sitemap.xml/manifest.json, dead deploy workflows. → reconcile.
+2. **Batch B — Copy & a11y (LOCAL, additive):** typos, version badges, stale model names, aria-labels, roles, focus, alt text. One agent per file, parallel. → reconcile.
+3. **Batch C — Security wiring & correctness LOCAL:** wire sanitize* into multiplayer send/receive, null guards, console→logger, peer-message validation, error boundaries. → reconcile.
+4. **Batch D — RIPPLE (serial, me):** any shared-type/prop/signature changes, all call-sites one edit. → reconcile each.
+5. **Batch E — Perf LOCAL:** module-level reusable temporaries for per-frame allocations, shader-cache-key fixes, memoization — FLAG behavior-changing lazy-loads. → reconcile + spot-verify build size.
+6. **Archive removal:** move `src/0.10 Archive/assets/Screenshot.png` → `docs/assets/`, update README, then `git rm -r src/0.10 Archive/`. → build.
+
+## Reconcile command (each batch)
+`npm run typecheck && npm run lint && npm run build && npm run test`
+Then `npm run format` on touched files.
+
+## Coverage honesty
+Final report states which lanes/files were deep-read vs sampled (from `_audit/coverage.md`), and lists every FLAG item for Nell. No silent truncation.
+
+## FLAG-for-Nell running list (seed; audit will add)
+- API key persisted plaintext in `millos-ai-config` localStorage → warn-in-UI + optional sessionStorage (UX tradeoff).
+- Legal: no privacy/terms exist; public millos.net + Gemini egress + localStorage + PeerJS + Google Fonts/jsdelivr → privacy notice warranted (draft only, don't publish).
+- `src/0.10 Archive/` deletion blocked by README screenshot ref — move asset first.
+- CSP `unsafe-inline`/`unsafe-eval` — likely required by R3F/WASM; verify before tightening.
+- `prototypes/index.html` (4200-line AssetPrototypePage) shipped as 2nd vite build input — intended for prod?
+- `public/v0.10|v0.20|v0.30` shipped historical builds + k8s/docker deploy workflows — keep or prune?
diff --git a/_audit/FLAGS.md b/_audit/FLAGS.md
new file mode 100644
index 0000000..577f2a7
--- /dev/null
+++ b/_audit/FLAGS.md
@@ -0,0 +1,520 @@
+# MillOS Launch Audit — Owner Decisions & Flags
+
+_172 items needing your call or verification. Sorted by severity._
+
+- **[CRITICAL]** `src/utils/geminiClient.ts:135` · L4 API/Errors
+ - Gemini model 'gemini-3-flash-preview' likely invalid for the legacy @google/generative-ai SDK — live AI silently dead at launch
+ - _Recommend:_ Owner decision: confirm the exact model ID the user's API key + @google/generative-ai 0.24.1 actually serves (live smoke test). If Gemini 3 is intended, migrate to @google/genai and set a verified ID; otherwise set a known-served ID (e.g. a current 2.0/2.5-Flash). In the SAME change, fix the stale '2.0 Flash'/'Flash 3' log+comment strings and re-check the GEMINI_FLASH_*_COST constants against the chosen model's published pricing.
+- **[CRITICAL]** `public/v0.20/` · L9 Performance
+ - 418MB duplicate v0.20/ asset tree ships to GitHub Pages in the build
+ - _Recommend:_ Remove public/v0.20/ from the deployed tree (move out of public/ or exclude in a build step). It is a stale snapshot of a prior published build; runtime paths use import.meta.env.BASE_URL ('/').
+- **[HIGH]** `index.html:27` · L7 Copy
+ - OG/Twitter image is a reused 3456x1993 screenshot but meta declares 1200x630
+ - _Recommend:_ Replace public/og-image.png with a real 1200x630 social card, OR correct the og:image:width/height meta to the true 3456x1993 (and accept the larger upload). Owner decision on whether to author a proper card vs adjust metadata.
+- **[HIGH]** `README.md:711` · L7 Copy
+ - Quick Start env setup is stale: .env.local.example missing and GEMINI_API_KEY build-injection is disabled
+ - _Recommend:_ Rewrite the env step to describe the real flow: run the app, open AI/Gemini settings, paste the key (stored in browser localStorage). Remove the `cp .env.local.example` line or add an actual .env.local.example. Owner should confirm intended onboarding UX.
+- **[HIGH]** `src/stores/gameSimulationStore.ts:830 (with src/components/MillScene.tsx:824 and src/components/WorkerSystemNew.tsx:383-385)` · L1 Correctness/Bugs
+ - Fire-drill evacuation metrics never complete when camera is outside the factory
+ - _Recommend:_ Product decision among: (a) cap totalWorkers to the count of currently-registered/mounted workers at drill start; (b) drive evacuation headlessly from the store so it completes regardless of camera; or (c) keep WorkerSystemNew (or a headless evacuation tick) mounted while a drill is active. Do not pick blindly; this changes drill UX/semantics.
+- **[HIGH]** `index.html:28` · L12 Config
+ - og:image declared 1200x630 but public/og-image.png is actually 3456x1993
+ - _Recommend:_ Re-export og-image.png at exactly 1200x630 (or update the width/height meta to the true 3456x1993 and compress under ~1MB). Preferred: regenerate a 1200x630 <300KB PNG/JPG to match the declared dims.
+- **[HIGH]** `.github/workflows/deploy.yml:79` · L12 Config
+ - CNAME file never copied into the deployed Pages artifact
+ - _Recommend:_ Add a step before upload: `cp CNAME staging/CNAME` (and ideally also into staging/v0.30/ if subpaths are served directly). Resolve the apex/www question first (see related finding).
+- **[HIGH]** `src/components/ui/GraphicsSettingsPanel.tsx:621` · L6 UX
+ - Destructive 'Reset Simulation' wipes localStorage and reloads with NO confirmation
+ - _Recommend:_ Wrap the handler in a confirmation: `if (!window.confirm('Reset the entire simulation? This clears saved progress and reloads.')) return;` before the removeItem calls, or gate behind a two-step confirm UI like the existing showGeminiConfirmation pattern in GeminiSettingsModal.
+- **[HIGH]** `src/components/ui-new/panels/SettingsPanel.tsx:458` · L6 UX
+ - Second 'Reset Simulation' button (active new-UI panel) also wipes localStorage + reloads without confirmation
+ - _Recommend:_ Add `if (!window.confirm('Reset the simulation and clear all saved data? This cannot be undone.')) return;` at the top of the onClick handler.
+- **[HIGH]** `src/multiplayer/MultiplayerManager.ts:284-285` · L1 Correctness/Bugs - Integration (SCADA+multiplayer+protocols)
+ - Host never relays PLAYER_UPDATE → remote players frozen at spawn in 3+ player sessions
+ - _Recommend:_ In the host branch of the PLAYER_UPDATE case, after updating the store, rebroadcast the update to all other peers excluding the sender: `if (store.isHost) this.broadcast(message, peerId);`. Verify against the message contract before shipping.
+- **[HIGH]** `src/multiplayer/MultiplayerManager.ts:98-105` · L1 Correctness/Bugs - Integration (SCADA+multiplayer+protocols)
+ - Join-timeout destroys singleton without nulling it → subsequent multiplayer attempts use a poisoned manager
+ - _Recommend:_ Have internal teardown null the singleton: call the module-level destroyMultiplayerManager() from the joinRoom timeout instead of this.destroy(), or add an isDestroyed reset/recreate guard in getMultiplayerManager() so a destroyed instance is replaced. Confirm no double-destroy regression first.
+- **[HIGH]** `src/multiplayer/SignalingService.ts:85-90` · L6 UX
+ - Room-not-found error never reaches join UI
+ - _Recommend:_ Reject initialize() on peer-unavailable or set store error field.
+- **[HIGH]** `src/components/multiplayer/MultiplayerLobby.tsx:34` · L6 UX
+ - Lobby has no host-disconnect feedback
+ - _Recommend:_ Add host-disconnected listener to Lobby.
+- **[HIGH]** `SettingsPanel.tsx:458-468` · L3
+ - Reset clears 5 of 16 keys; plaintext Gemini key survives
+ - _Recommend:_ Add Clear all data removing every millos key or a Forget API key button.
+- **[HIGH]** `GeminiSettingsModal.tsx:266-277` · L3
+ - No disclosure plant data sent to Google Gemini
+ - _Recommend:_ Add notice MillOS sends simulation state to Google Gemini under Google Privacy Policy.
+- **[HIGH]** `src/components/ui/KeyboardShortcutsModal.tsx:72` · L5 A11y
+ - Modal lacks dialog role, aria-modal, focus trap, and ESC-to-close
+ - _Recommend:_ On the inner panel add role="dialog" aria-modal="true" aria-labelledby="kbd-shortcuts-title" (give the h2 that id); add a useEffect keydown listener calling onClose() on 'Escape'; move initial focus into the dialog and restore on close (or trap focus). UX adds ESC behavior, so owner should confirm.
+- **[HIGH]** `src/components/ui/DecisionReplay.tsx:71` · L5 A11y
+ - Decision-replay modal lacks dialog role/aria-modal/ESC/focus management
+ - _Recommend:_ Add role="dialog" aria-modal="true" aria-label="Decision replay" to the panel; add Escape keydown -> onClose; add aria-label="Close" to the X button + aria-hidden on the icon; manage initial/return focus. ESC behavior is a UX addition.
+- **[HIGH]** `src/multiplayer/MultiplayerManager.ts:280` · L4 API/Errors
+ - Host trusts unvalidated, spoofable fields from untrusted guest messages (no payload validation, no default case)
+ - _Recommend:_ Add a validateMessage(peerId, message) guard at the top of handleMessage: assert message.type is a known literal, assert payload exists and required fields are correct primitive types (finite numbers for positions/rotation, string ids), and reject any message whose payload.id/from does not match the authenticated sender peerId for PLAYER_UPDATE/CHAT (host should derive identity from the connection, not the payload). Add a `default:` that logs+drops. Keep it in MultiplayerManager; payload type literals already exist in multiplayer/types.ts.
+- **[HIGH]** `index.html:11` · L2 Security
+ - CSP connect-src omits www.gstatic.com → DRACO decoder fetch blocked, breaks 3D models in production
+ - _Recommend:_ Add 'https://www.gstatic.com' to the connect-src directive in index.html (line 11), e.g. '...generativelanguage.googleapis.com https://www.gstatic.com https://cdn.jsdelivr.net...'. Alternatively switch dracoLoader.ts to the local '/draco/' decoder path (commented at line 36) and vendor the decoder into public/draco/ to avoid any third-party connect entirely.
+- **[HIGH]** `public/textures/machines/downloads/` · L9 Performance
+ - 173MB of texture SOURCE cruft (.blend/.zip/.usdc/.tres/.mtlx) ships in public/
+ - _Recommend:_ Delete public/textures/machines/downloads/ (or move to a non-public source folder). Keep only the runtime-referenced JPG/KTX2 maps. Verify against texture load paths in src before deleting.
+- **[HIGH]** `public/models/forklift/forklift-original.glb + forklift.glb.backup.glb` · L9 Performance
+ - 144MB of unreferenced GLB backups/originals shipped (35MB each x2 + worker backups)
+ - _Recommend:_ Delete *-original.glb and *.backup.glb under public/models/ (and v0.20). Only forklift.glb (1.6M) and worker.glb (136K) are referenced.
+- **[HIGH]** `src/main.tsx:3,50` · L9 Performance
+ - 4200-line AssetPrototypePage statically imported into main entry, parsed on every visit
+ - _Recommend:_ Lazy-load: `const AssetPrototypePage = React.lazy(() => import('./prototypes/AssetPrototypePage'))` rendered under Suspense only when isPrototypeRoute() is true. Behavior-changing (adds Suspense on the prototype route), so flag for owner.
+- **[HIGH]** `src/components/ui-new/sidebar/ContextSidebar.tsx:33` · L9 Performance
+ - MultiplayerPanel imported EAGERLY, pulling peerjs into the boot path
+ - _Recommend:_ Make MultiplayerPanel lazy to match siblings: `const MultiplayerPanel = lazy(() => import('../panels/MultiplayerPanel').then(m => ({ default: m.MultiplayerPanel })));` and wrap its render at line 168 in the existing Suspense. Defers peerjs to first panel open.
+- **[HIGH]** `public/Fuzzball Parade.mp3 (+ 25 more)` · L9 Performance
+ - 26 high-bitrate MP3s total ~140MB; several 9-12MB single tracks
+ - _Recommend:_ Re-encode music to ~96-128kbps (target <2MB/track) via ffmpeg; background loops don't need high bitrate. Owner decision on quality/size. Cuts ~100MB+ from worst-case cached/transferred audio.
+- **[HIGH]** `src/components/TruckBay.tsx:2877-2946,3274-3551` · L1 Correctness/Bugs
+ - Dock-equipment animations driven by ref reads in render never react to docking cycle
+ - _Recommend:_ Lift the truck dock/door/phase state that drives equipment into React state (or subscribe the equipment to the production store dock-status values that useFrame already writes via setTruckDocked/updateDockStatus), so prop changes re-render the equipment. Minimal version: replace the docked/doorsOpen/phase refs that feed JSX props with useState updated in useFrame (throttled to state transitions only, which the code already detects). All edits stay inside TruckBay.tsx.
+- **[HIGH]** `src/scada/adapters/WonderwareAdapter.ts:92` · L4 API/Errors
+ - Wonderware historian base URL hardcoded to plaintext http:// — Basic-auth credentials sent in clear
+ - _Recommend:_ Add an optional `useSsl?: boolean` (or `baseUrlOverride`) to WonderwareConnectionConfig in HistorianInterface.ts and derive scheme (`https://` default) instead of hardcoding `http://`. Defaulting to https is the safe posture for SCADA credentials.
+- **[HIGH]** `src/scada/adapters/RESTAdapter.ts:304` · L4 API/Errors
+ - After 5 reconnect failures REST adapter silently disconnects and clears all subscribers with no consumer notification
+ - _Recommend:_ On terminal failure, set a distinct lastError (e.g. 'Reconnect limit reached') and emit a connection-status change rather than silently clearing subscribers; or do not clear subscribers in the auto-disconnect path so a later manual reconnect can resume. Needs a product call on desired reconnect ceiling/UX.
+- **[HIGH]** `.github/workflows/ci.yml:17,32,41,57` · L12 Config
+ - CI cannot fail on lint or test — green CI does not gate regressions
+ - _Recommend:_ Remove `exit 0` (revert to `npm run lint`) and `|| true` from the test step, and drop `continue-on-error: true` once flakiness is addressed; if Vitest worker crashes are the real concern, scope the tolerance to that specific exit condition rather than swallowing all failures. Requires owner sign-off (changes what CI blocks on).
+- **[HIGH]** `.github/workflows/release.yml:58-95` · L12 Config
+ - release.yml deploys a SECOND, conflicting GitHub Pages site on tag push
+ - _Recommend:_ Remove the `deploy-ghpages` job from release.yml (let deploy.yml own all Pages deploys), OR make it mirror deploy.yml's versioned staging. Owner decision: which workflow owns Pages.
+- **[HIGH]** `src/multiplayer/SignalingService.ts:26-30` · L3 Privacy/GDPR — Multiplayer data sharing
+ - WebRTC exposes every peer's public IP address to untrusted peers with no consent or disclosure
+ - _Recommend:_ Owner decision required. Mitigations: (a) display a clear pre-join disclosure in MultiplayerLobby that joining a room reveals your IP address to other participants and routes signaling through a third-party broker; (b) optionally configure a TURN relay with `iceTransportPolicy: 'relay'` to hide peer IPs (cost/perf tradeoff); (c) publish a privacy policy covering this. Do not hardcode a specific STUN host as a 'fact'.
+- **[HIGH]** `src/components/ui-new/dock/Dock.tsx:106` · L8 Visual
+ - Mobile bottom Dock overflows screen width - no wrap/scroll/max-w guard
+ - _Recommend:_ On mobile add a width cap + horizontal scroll or wrap: e.g. add 'max-w-[100vw] overflow-x-auto no-scrollbar' (with safe-area-aware padding) to the nav when isMobile, OR reduce the mobile dock to a core subset and move secondary actions (Datalinks/FPS/Fullscreen) into an overflow menu. Which items to drop vs scroll is a product decision.
+- **[HIGH]** `src/utils/geminiClient.ts:146` · L7 Copy
+ - Model name Gemini 3 vs 2.0 contradiction
+ - _Recommend:_ Set 146 and 3866 to 'Gemini 3 Flash'.
+- **[HIGH]** `src/components/GeminiSettingsModal.tsx:266` · L7 Copy
+ - API-key field has no privacy note
+ - _Recommend:_ Add note: stored only in this browser, sent direct to Google, MillOS never receives it.
+- **[HIGH]** `src/stores/aiConfigStore.ts:558` · L2 Security
+ - Gemini API key persisted in plaintext localStorage with no user warning
+ - _Recommend:_ Product decision needed: either (a) add a one-line caution under the API key input in GeminiSettingsModal.tsx ('Your key is stored unencrypted in this browser only and never sent to our servers'), or (b) move geminiApiKey to sessionStorage (cleared on tab close) by giving the persist config a separate storage, or (c) drop geminiApiKey from partialize so it lives only in memory for the session. Each changes UX/persistence behavior.
+- **[HIGH]** `src/multiplayer/PeerConnection.ts:47-55` · L2 Security
+ - Untrusted peer DataChannel payload passed to handlers with zero validation
+ - _Recommend:_ Add a validateMultiplayerMessage(data): data is MultiplayerMessage type-guard (mirroring messageValidation.ts/isValidWSMessage): check data is an object, type is one of the known string literals, and the payload shape matches per-type (e.g. PLAYER_JOIN requires id:string,name:string,color in PLAYER_COLORS). Reject (return without dispatch, increment an error counter) on failure. Wire it at the top of PeerConnection.handleMessage before any branch.
+- **[HIGH]** `src/multiplayer/MultiplayerManager.ts:323-340` · L2 Security
+ - Host trusts client-supplied playerId in INTENT (player impersonation / authority bypass)
+ - _Recommend:_ On the host, override the trusted identity: build the effective intent from the connection's authenticated peerId rather than the payload, e.g. const trustedPlayerId = this.peerIdToPlayerId.get(peerId); this.onMachineIntent({...message.payload, playerId: trustedPlayerId}). For PLAYER_LEAVE, ignore the payload id when isHost and use the peerId mapping. Maintain a peerId->playerId map populated from the verified connection at join time.
+- **[HIGH]** `src/scada/adapters/WonderwareAdapter.ts:92` · L2 Security
+ - Wonderware adapter forces plaintext HTTP, sending Basic-auth credentials in cleartext
+ - _Recommend:_ Default the scheme to https:// and make it configurable (e.g. add a `secure`/`scheme` field to WonderwareConnectionConfig, defaulting to https). Refuse to send Basic-auth credentials over a plain-http URL (throw, or warn-and-strip) unless the host is explicitly localhost. Mirror the same guard in any other adapter that builds a base URL.
+- **[HIGH]** `src/hooks/useAdaptiveQuality.ts:23` · L9 Performance
+ - Adaptive-quality system is dead code
+ - _Recommend:_ Wire the hook into an always-mounted Canvas component with a non-destructive merge, or delete as dead code.
+- **[MEDIUM]** `tailwind.config.js:1` · L8 Visual
+ - Stale v3-style tailwind.config.js is silently ignored under Tailwind v4 (no @config directive)
+ - _Recommend:_ Either (a) delete tailwind.config.js since v4 reads @theme/@source from CSS and the file's unique tokens are unused, or (b) if the team wants to keep the JS config, add `@config '../tailwind.config.js';` to src/index.css AND move the pulse-slow/spin-slow keyframes into @theme. Recommend (a) delete — the CSS @theme is already the source of truth. Verify no future code relies on the config-only tokens first.
+- **[MEDIUM]** `src/index.css:28` · L8 Visual
+ - Type-scale fragmentation: text-xs override to 14px forces 1089 arbitrary sub-12px font sizes across 82 files
+ - _Recommend:_ Product/design decision: either revert --text-xs to 12px (0.75rem) and --text-sm to 0.875rem so the scale covers the small sizes devs actually need (reduces need for text-[10px]/[9px]), OR add named scale steps (e.g. `--text-2xs: 0.625rem` for 10px, `--text-3xs: 0.5625rem` for 9px) in @theme and migrate the arbitrary values to them. Do NOT auto-mass-edit: changing text-xs to 12px shifts 573 existing text-xs usages smaller and is a visible UX change requiring owner sign-off and visual review.
+- **[MEDIUM]** `src/components/ui-new/widgets/FederationPanel.tsx:206` · L8 Visual
+ - Sub-9px text (7px/8px) across 27 files is below legible/accessible minimums
+ - _Recommend:_ Bump the smallest labels to at least 10px and raise contrast (slate-400 or lighter on dark). Best handled as part of the type-scale remediation above. Per-file change is low-blast but touches visual layout in dense widgets, so verify each panel still fits after enlarging.
+- **[MEDIUM]** `src/types.ts:841` · L7 Copy
+ - Roster targetMachine 'mill-1.5' references a nonexistent machine
+ - _Recommend:_ Change to a valid mill id, e.g. `targetMachine: 'rm-102'` (matches her 'Calibrating Roller Mill #2' task). Touches the shared WORKER_ROSTER in types.ts.
+- **[MEDIUM]** `src/types.ts:931` · L7 Copy
+ - Roster targetMachine 'sifter-0' references a nonexistent machine
+ - _Recommend:_ Change to a valid sifter id, e.g. `targetMachine: 'sifter-c'` (a/b already taken by other workers). Touches shared WORKER_ROSTER.
+- **[MEDIUM]** `README.md:97` · L7 Copy
+ - Packer throughput copy '42 bags/minute' contradicts code (~60 bags/min)
+ - _Recommend:_ Change '42 bags/minute' to '60 bags/minute' (or whatever the canonical rate should be — confirm with owner since it's a stated spec number).
+- **[MEDIUM]** `src/utils/audioManager.ts:289` · L1 Correctness/Bugs
+ - Muting does not stop an in-flight TTS PA announcement
+ - _Recommend:_ Call `this.stopTTS()` inside the muted setter when value===true (stopTTS already cancels speechSynthesis, clears the queue, and clears the chime timeout). This changes mute semantics, so it needs owner sign-off.
+- **[MEDIUM]** `CNAME:1` · L12 Config
+ - CNAME is www.millos.net but every SEO/OG/canonical URL uses apex millos.net
+ - _Recommend:_ Decide on one canonical host. If apex millos.net is canonical (as the meta implies), set CNAME to `millos.net` and configure GH Pages apex + www->apex redirect. Otherwise change all meta URLs and img-src to www.millos.net.
+- **[MEDIUM]** `.github/workflows/deploy.yml:59` · L12 Config
+ - Site root entry point (millos.net/) is a bare meta-refresh redirect with zero SEO
+ - _Recommend:_ Either copy the full SEO (canonical, description, OG/Twitter) into the root redirect page, or set canonical/og:url to https://millos.net/v0.30/ so the rich page is the indexed URL. Best: serve the real app at root and drop the redirect for the launch version.
+- **[MEDIUM]** `public/sw.js:21` · L12 Config
+ - PWA scaffolding present (service worker + caches) but no web app manifest -> not installable
+ - _Recommend:_ Add public/manifest.webmanifest (name, short_name, start_url '.', display 'standalone', background_color #0a0f1a, theme_color, 192/512 icons) and link it: . Use relative href to survive versioned base paths.
+- **[MEDIUM]** `public/` · L12 Config
+ - No robots.txt and no sitemap.xml for a public launch
+ - _Recommend:_ Add public/robots.txt (`User-agent: *` / `Allow: /` / `Sitemap: https://millos.net/sitemap.xml`) and a minimal public/sitemap.xml listing https://millos.net/ with lastmod. Vite copies public/* to dist root automatically.
+- **[MEDIUM]** `src/utils/apiSecurity.ts:1-642` · L11 DeadCode
+ - Entire apiSecurity module (CSRF/JWT/secureFetch) is dead — intentional 'future auth' scaffolding in a no-backend app
+ - _Recommend:_ Owner decision: either (a) keep as a deliberately-documented library and add a top-of-file banner '// UNUSED: scaffolding for future server-side auth — not wired into any runtime path', or (b) delete src/utils/apiSecurity.ts and its barrel re-export block (utils/index.ts:123-147) to shed 642 dead lines. Do NOT delete without the product/security call since it advertises a security posture.
+- **[MEDIUM]** `src/components/UIOverlay.tsx:1296` · L11 DeadCode
+ - UIOverlay (the entire old top-level UI, ~2000 lines / 89KB) is superseded by ui-new/GameInterface and no longer rendered
+ - _Recommend:_ Owner decision (migration completeness): if the ui-new migration is final, retire UIOverlay.tsx, delete its dedicated test, and clean up the 5 panels it solely consumes. Removal cascades into the test suite (breaks GREEN baseline) so it must be a coordinated change, not an isolated delete.
+- **[MEDIUM]** `eslint.config.js:56-57` · L11 DeadCode
+ - Lint disables no-unused-vars entirely — per-file dead imports/locals/params go undetected in the GREEN baseline
+ - _Recommend:_ Owner decision: re-enable @typescript-eslint/no-unused-vars at 'warn' with `argsIgnorePattern: '^_'` / `varsIgnorePattern: '^_'` to surface dead imports/locals without breaking the build. Do NOT flip to 'error' in one shot — it would likely break the GREEN baseline given accumulated unused vars; introduce as 'warn' first, then burn down.
+- **[MEDIUM]** `src/components/ui-new/panels/SettingsPanel.tsx:461` · L6 UX
+ - 'Reset Simulation' removes a nonexistent key ('millos-settings') and leaves graphics + plaintext Gemini API key persisted
+ - _Recommend:_ Replace 'millos-settings' with 'millos-graphics' and add 'millos-ai-config' (plus other millos-* persist keys: millos-bas, millos-safety, millos-ui, etc.) to the removeItem list, or iterate `Object.keys(localStorage).filter(k => k.startsWith('millos-')).forEach(k => localStorage.removeItem(k))`.
+- **[MEDIUM]** `src/components/GeminiSettingsModal.tsx:67` · L6 UX
+ - Modal lists wrong keyboard shortcuts (H/V/C) that do something else entirely
+ - _Recommend:_ Remove the incorrect `key` chips for Shift Handover, VCL Context, and API Cost Tracker (or wire real handlers). For API Cost Tracker change the chip to '$' to match useKeyboardShortcuts.ts:284. Verify each remaining chip against the actual handler keys.
+- **[MEDIUM]** `src/App.tsx:581` · L6 UX
+ - Screen-reader keyboard notice advertises 'B for management' but B has no handler
+ - _Recommend:_ Remove 'B for management' from the notice (management is reached via the Dock, not a key), or wire a real 'B' handler. Keep the I/O/V/1-5 entries which are accurate.
+- **[MEDIUM]** `src/components/GeminiSettingsModal.tsx:254` · L6 UX
+ - No disclosure that the Gemini API key is stored in plaintext localStorage
+ - _Recommend:_ Add a short note under the input, e.g. 'Your key is stored in this browser only (localStorage), in plain text. Use "Clear Config" to remove it.' Optionally offer a session-only (non-persisted) toggle.
+- **[MEDIUM]** `src/components/ui-new/panels/SettingsPanel.tsx:26` · L6 UX
+ - Active Settings panel has no entry point to AI / Gemini configuration
+ - _Recommend:_ Add an 'AI Assistant' section to SettingsPanel with a button that opens GeminiSettingsModal (or deep-links to the AI Command Center), mirroring the existing button in AICommandCenter.tsx:200.
+- **[MEDIUM]** `src/hooks/useKnowledgeIntegration.ts:88` · L6 UX
+ - First-run welcome flag is set even when narration is disabled, silently burning the only first-play moment
+ - _Recommend:_ Move the `localStorage.setItem('millos-has-played','true')` and `hasTriggeredFirstPlay=true` writes inside the success path (after a narration is actually returned/shown), or gate the whole first-play effect on narration being enabled so the flag isn't consumed while disabled.
+- **[MEDIUM]** `src/protocols/vcp/decoder.ts:36` · L1 Correctness/Bugs - Integration (SCADA+multiplayer+protocols)
+ - VCP encoder/decoder roundtrip collision: 'mastery' encodes to 'M' and decodes to 'meaning'
+ - _Recommend:_ Pick a distinct wire symbol for 'mastery' (e.g. 'Y' or lowercase 'm') and update all three places in lockstep: encoder mapping, the WELL/R decode regex char classes, and the decoder dimMaps. This touches the encoded wire format (ripple) and any persisted VCP strings; owner decision on the symbol.
+- **[MEDIUM]** `src/scada/adapters/MQTTAdapter.ts:183` · L1 Correctness/Bugs - Integration (SCADA+multiplayer+protocols)
+ - MQTT packets use single-byte remaining-length; payloads >127 bytes are malformed (encode + decode)
+ - _Recommend:_ Implement proper MQTT variable-length-integer encoding/decoding (continuation-bit scheme) for both the writer (packet[1]...) and handlePublish's remaining-length read. Behavior-changing protocol work — owner decision.
+- **[MEDIUM]** `src/multiplayer/MultiplayerManager.ts:350-351` · L1 Correctness/Bugs - Integration (SCADA+multiplayer+protocols)
+ - Host applies untrusted peer MACHINE_LOCK/PLAYER_JOIN/PLAYER_LEAVE without authority validation → desync vector
+ - _Recommend:_ On the host, gate MACHINE_LOCK/PLAYER_JOIN/PLAYER_LEAVE to host-authored broadcasts only: ignore inbound MACHINE_LOCK from guests (require it to flow via INTENT/handleMachineIntent), and ignore guest-originated PLAYER_JOIN/LEAVE. Validate payload.playerId matches the sender's connection identity. Behavior/contract change — owner decision.
+- **[MEDIUM]** `src/multiplayer/types.ts:20` · L6 UX
+ - reconnecting styled but never set
+ - _Recommend:_ Set in reconnect path or remove dead branches.
+- **[MEDIUM]** `src/App.tsx:533-552` · L6 UX
+ - No WebGL-unsupported detection; misleading error
+ - _Recommend:_ getContext('webgl') check before Canvas; unsupported-browser screen.
+- **[MEDIUM]** `src/main.tsx:33` · L11 DeadCode
+ - prototype eagerly bundled in shared entry
+ - _Recommend:_ React.lazy + Suspense.
+- **[MEDIUM]** `src/utils/audioManager.ts:5438` · L11 DeadCode
+ - 29 ungated console.log in TTS in prod
+ - _Recommend:_ Use logger.audio.debug.
+- **[MEDIUM]** `index.html:44-46` · L3
+ - Google Fonts leaks visitor IP to Google
+ - _Recommend:_ Self-host woff2, remove googleapis links and CSP font hosts.
+- **[MEDIUM]** `src/components/ui/KeyboardShortcutsModal.tsx:17` · L8 Visual
+ - Internal `if (!isOpen) return null` defeats the AnimatePresence exit animation
+ - _Recommend:_ Remove the internal `if (!isOpen) return null;` and instead wrap the returned JSX in `{isOpen && (...)}` inside the component (mirroring AboutModal), and drop the redundant outer AnimatePresence/`showShortcuts &&` gating in UIOverlay.tsx:1389-1393. Restructure, so verify the close path still mounts/unmounts.
+- **[MEDIUM]** `src/components/ui/ZoneCustomizationPanel.tsx:72` · L5 A11y
+ - Remove-zone button only visible on hover (not focus-visible) and unlabeled
+ - _Recommend:_ Add focus-visible:opacity-100 (and group-focus-within:opacity-100 on the row) so the control becomes visible on keyboard focus; add aria-label="Remove zone".
+- **[MEDIUM]** `src/components/ui/KeyboardShortcutsModal.tsx:166` · L5 A11y
+ - Secondary text color text-slate-500 fails WCAG AA contrast on dark panels
+ - _Recommend:_ Owner decision: raise dark-theme secondary text from slate-500/600 to slate-400 (or lighter) for body/label text on slate-800/900 surfaces, or reserve slate-500/600 only for decorative/non-essential text. Verify each change visually (3D/visual posture). Do not bulk auto-replace.
+- **[MEDIUM]** `src/stores/aiConfigStore.ts:497` · L4 API/Errors
+ - Runtime Gemini failures (429/quota/network) never surface to the user — connectionError set only at setup time
+ - _Recommend:_ On runtime failure in generateStrategicDecision (and/or in geminiClient.generateContent), categorize the error (rate-limit/quota vs auth vs network) and surface it: call a new aiConfigStore action to set connectionError (e.g. 'Gemini rate limit reached — AI paused, retrying in 30s') so GeminiSettingsModal/AICommandCenter can show it. Behavior change — flag for owner.
+- **[MEDIUM]** `index.html:11` · L2 Security
+ - connect-src allows wildcard ws:/wss: to any host
+ - _Recommend:_ Replace bare 'ws: wss:' with the specific signaling host, e.g. 'wss://0.peerjs.com' (and the user's SCADA host if applicable). If a self-hosted PeerServer is later used, add that exact host. Keep 'ws:' only if local-dev SCADA over plain ws is required, scoped to localhost.
+- **[MEDIUM]** `index.html:11` · L2 Security
+ - cdn.jsdelivr.net in connect-src enables remote font-data fetch from third party (supply-chain surface)
+ - _Recommend:_ Acceptable to keep if 3D text glyph coverage for non-Latin scripts is needed. To eliminate the dependency, self-host the unicode-font-resolver data and pass a local dataUrl to troika's font resolver (or restrict to the Latin subset shipped locally), then remove cdn.jsdelivr.net from connect-src. Product decision on whether non-Latin glyph coverage is required.
+- **[MEDIUM]** `public/sw.js:99-100,124-126` · L2 Security
+ - Service worker uses skipWaiting()+clients.claim() unconditionally → mixed-version asset serving on deploy
+ - _Recommend:_ Gate skipWaiting behind an explicit SKIP_WAITING postMessage triggered only after the user accepts an update prompt, and implement the onUpdate handler in main.tsx to surface a 'new version available — reload' toast that posts SKIP_WAITING then reloads. This is a UX/behavior change so it needs owner sign-off; minimum safe step is to keep skipWaiting but make onUpdate force a one-time location.reload() after activation to avoid mixed-chunk states.
+- **[MEDIUM]** `public/sw.js:204-206` · L2 Security
+ - network-first caches ALL 200 responses including HTML/dynamic, risking stale-serve of stale SPA shell
+ - _Recommend:_ In networkFirst, only cache navigations/HTML into a dedicated short-lived cache and skip caching responses whose Cache-Control is no-store, or restrict network-first caching to known same-origin document/manifest requests. Add a Date-stamp check or rely on CACHE_VERSION + the SW update flow above. Behavior change → owner review.
+- **[MEDIUM]** `src/components/mobile/RotateDeviceOverlay.tsx:25` · L5 A11y
+ - No prefers-reduced-motion guard anywhere in scope; indefinite looping animations run unconditionally
+ - _Recommend:_ Gate looping animations on reduced-motion: e.g. wrap framer-motion app region in ``, and for the CSS loops add a `motion-reduce:animate-none` Tailwind variant to the animate-pulse (line 25) / animate-bounce (line 32) / any animate-spin elements. Changes motion behavior under user preference, so owner should confirm scope.
+- **[MEDIUM]** `src/components/AICommandCenter.tsx:307` · L5 A11y
+ - AI decision feed has no live region; new AI decisions are not announced to screen readers
+ - _Recommend:_ Add an sr-only `role="status" aria-live="polite"` element that updates with a concise summary of the newest decision (e.g. `New AI decision: ${decision.action}`), rather than making the entire list a live region. UX/behavior change — recommend product sign-off on verbosity.
+- **[MEDIUM]** `src/utils/dracoLoader.ts:33,47` · L9 Performance
+ - DRACO decoder loaded from gstatic CDN at runtime — cross-origin dep + offline-cache miss + possible CSP block
+ - _Recommend:_ Vendor the DRACO decoder into public/draco/ and set DRACO_DECODER_PATH = `${import.meta.env.BASE_URL}draco/` so it is same-origin, SW-cacheable, CSP-allowed, offline-capable.
+- **[MEDIUM]** `src/hooks/useTextureWorker.ts + src/workers/textureWorker.ts` · L9 Performance
+ - Entire texture Web Worker subsystem (~500 lines) is dead code — never imported
+ - _Recommend:_ Delete src/hooks/useTextureWorker.ts and src/workers/textureWorker.ts (dead-code removal, no runtime change). Confirm no test imports first.
+- **[MEDIUM]** `public/og-image.png` · L9 Performance
+ - Social og-image is 3456x1993 / 2.1MB but declared 1200x630 in meta tags
+ - _Recommend:_ Downscale og-image.png to 1200x630 (matching declared meta) and re-export; should drop to <200KB. Pure asset swap, no code change.
+- **[MEDIUM]** `src/main.tsx:10-14` · L9 Performance
+ - Rapier ~2.2MB WASM eagerly pre-warmed at module top-level on every page load
+ - _Recommend:_ Gate the pre-warm behind requestIdleCallback or first-interaction so the visible app boot finishes before fetching/instantiating physics WASM. Behavior-sensitive (slight delay before first Physics mount), flag for owner.
+- **[MEDIUM]** `src/scada/adapters/WebSocketAdapter.ts:95` · L4 API/Errors
+ - Timed-out WebSocket connect still schedules background reconnects after the connect promise rejects
+ - _Recommend:_ Set `this.isDisconnecting = true` (or a dedicated `connectAborted` flag) before calling `this.ws?.close()` in the timeout handler so onclose does not start a reconnect for a connect that the caller already saw fail.
+- **[MEDIUM]** `.github/workflows/deploy-k8s.yml + docker.yml` · L12 Config
+ - Backend k8s/docker CI workflows present for a 'frontend-only, NO backend' launch
+ - _Recommend:_ Owner decision: confirm whether scada-proxy is an intended shipped component. If yes, document it in README as separate infra; if it is a non-shipped reference/demo, gate these workflows behind workflow_dispatch only or move scada-proxy to its own repo to avoid implying a backend exists.
+- **[MEDIUM]** `GEMINI.md:1-3` · L12 Config
+ - GEMINI.md is a stale copy of CLAUDE.md, not Gemini-specific guidance
+ - _Recommend:_ Either replace GEMINI.md content with Gemini-specific guidance and correct the title, or delete GEMINI.md and rely on AGENTS.md/CLAUDE.md. Owner decision on which agent docs to keep.
+- **[MEDIUM]** `src/multiplayer/SignalingService.ts:25-30` · L3 Privacy/GDPR — Multiplayer data sharing
+ - Signaling (peer IDs embedding room code + player name/id metadata) routed through default PeerJS cloud broker, a third party, undisclosed
+ - _Recommend:_ Owner decision: disclose the third-party broker in a privacy notice, and/or self-host peerjs-server (link already in the file comment) to keep signaling under the operator's control. Do not transmit the player name as broker metadata if it can be exchanged post-connection over the DataChannel instead.
+- **[MEDIUM]** `src/multiplayer/SignalingService.ts:53 / src/multiplayer/MultiplayerManager.ts:196-225` · L3 Privacy/GDPR — Multiplayer data sharing
+ - Guessable 6-char room code + auto-FULL_STATE_SYNC discloses all players' names/colors to an uninvited peer
+ - _Recommend:_ Owner decision (UX/behavior change): add a host approval gate before sending PLAYER_JOIN/FULL_STATE_SYNC to a new connection, and/or a longer/secret room code or join PIN. Do not broadcast existing players' names to a peer until the host accepts them.
+- **[MEDIUM]** `src/multiplayer/SignalingService.ts:134-138` · L3 Privacy/GDPR — Multiplayer data sharing
+ - No consent gate: host auto-accepts any incoming peer and existing players never approve new participants
+ - _Recommend:_ Owner decision (UX/behavior): add an approval prompt for incoming peers and/or a one-line disclosure at name entry in MultiplayerLobby ('Your name, chat, and in-game position will be shared with other players in this room.').
+- **[MEDIUM]** `src/components/ui-new/sidebar/ContextSidebar.tsx:289` · L8 Visual
+ - Version