From a166108b1c5e129e92039e221e3fa53f9c20102a Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 21:08:50 +1000 Subject: [PATCH 01/46] Add persistent open-world entity and accessibility model --- src/world/worldModel.ts | 129 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 src/world/worldModel.ts diff --git a/src/world/worldModel.ts b/src/world/worldModel.ts new file mode 100644 index 0000000..4334133 --- /dev/null +++ b/src/world/worldModel.ts @@ -0,0 +1,129 @@ +export type Vec3 = readonly [number, number, number]; +export type EntityKind = 'person' | 'assistive-tech' | 'infrastructure' | 'place' | 'service'; +export type AccessModality = 'walk' | 'powered-wheelchair' | 'manual-wheelchair' | 'eye-gaze' | 'switch' | 'speech' | 'touch'; + +export interface WorldEntity { + id: string; + kind: EntityKind; + label: string; + position: Vec3; + persistent: boolean; + tags: string[]; +} + +export interface PersonEntity extends WorldEntity { + kind: 'person'; + communication: { + primary: 'speech' | 'aac' | 'sign' | 'multimodal'; + access: 'available' | 'degraded' | 'unavailable'; + responseLatencySeconds: number; + }; + mobility: { + mode: AccessModality; + independent: boolean; + }; + goals: string[]; + authority: 'self'; +} + +export interface InfrastructureEntity extends WorldEntity { + kind: 'infrastructure'; + feature: 'lift' | 'ramp' | 'automatic-door' | 'crossing' | 'charging-point' | 'quiet-space'; + operational: boolean; + accessibility: AccessModality[]; +} + +export type OpenWorldEntity = WorldEntity | PersonEntity | InfrastructureEntity; + +export interface PersistentWorldState { + version: 1; + simulationSeconds: number; + entities: OpenWorldEntity[]; + events: WorldEvent[]; +} + +export interface WorldEvent { + id: string; + at: number; + type: string; + entityId?: string; + detail: string; +} + +export const initialOpenWorldState: PersistentWorldState = { + version: 1, + simulationSeconds: 0, + entities: [ + { id: 'clinical-centre', kind: 'place', label: 'Clinical Simulation Centre', position: [-24, 0, 8], persistent: true, tags: ['clinical', 'simulation'] }, + { id: 'community-hub', kind: 'place', label: 'Community Hub', position: [25, 0, 22], persistent: true, tags: ['community', 'social'] }, + { id: 'rehab-lab', kind: 'place', label: 'Rehabilitation Lab', position: [24, 0, -28], persistent: true, tags: ['rehabilitation'] }, + { id: 'transit', kind: 'place', label: 'Accessible Transit Station', position: [-25, 0, -34], persistent: true, tags: ['transport'] }, + { id: 'maya', kind: 'person', label: 'Maya', position: [5, 0, 9], persistent: true, tags: ['resident'], authority: 'self', communication: { primary: 'aac', access: 'available', responseLatencySeconds: 12 }, mobility: { mode: 'powered-wheelchair', independent: true }, goals: ['move through the community', 'communicate directly', 'participate in decisions'] }, + { id: 'station-lift', kind: 'infrastructure', label: 'Station lift', position: [-20, 0, -34], persistent: true, tags: ['transport', 'access'], feature: 'lift', operational: true, accessibility: ['powered-wheelchair', 'manual-wheelchair', 'walk'] }, + { id: 'aac-charge', kind: 'infrastructure', label: 'AAC and mobility charging point', position: [20, 0, 20], persistent: true, tags: ['power', 'access'], feature: 'charging-point', operational: true, accessibility: ['powered-wheelchair', 'manual-wheelchair', 'touch'] } + ], + events: [] +}; + +export class PersistentWorldStore { + private state: PersistentWorldState; + + constructor(seed: PersistentWorldState = initialOpenWorldState) { + this.state = JSON.parse(JSON.stringify(seed)) as PersistentWorldState; + this.restore(); + } + + snapshot(): PersistentWorldState { return JSON.parse(JSON.stringify(this.state)) as PersistentWorldState; } + entity(id: string) { return this.state.entities.find((entity) => entity.id === id); } + + tick(seconds: number) { + this.state.simulationSeconds += Math.max(0, seconds); + this.persist(); + } + + move(id: string, position: Vec3) { + const entity = this.entity(id); + if (!entity) return; + entity.position = position; + this.persist(); + } + + setInfrastructure(id: string, operational: boolean) { + const entity = this.entity(id); + if (!entity || entity.kind !== 'infrastructure') return; + (entity as InfrastructureEntity).operational = operational; + this.record('infrastructure.changed', id, `${entity.label} ${operational ? 'operational' : 'unavailable'}`); + } + + setCommunicationAccess(id: string, access: PersonEntity['communication']['access']) { + const entity = this.entity(id); + if (!entity || entity.kind !== 'person') return; + (entity as PersonEntity).communication.access = access; + this.record('communication.access.changed', id, `${entity.label} communication access: ${access}`); + } + + private record(type: string, entityId: string, detail: string) { + this.state.events.push({ id: `${this.state.simulationSeconds}-${this.state.events.length}`, at: this.state.simulationSeconds, type, entityId, detail }); + this.state.events = this.state.events.slice(-100); + this.persist(); + } + + private persist() { + if (typeof window !== 'undefined') window.localStorage?.setItem('mert-open-world-v1', JSON.stringify(this.state)); + } + + private restore() { + if (typeof window === 'undefined') return; + const saved = window.localStorage?.getItem('mert-open-world-v1'); + if (!saved) return; + try { this.state = JSON.parse(saved) as PersistentWorldState; } catch { /* retain safe seed */ } + } +} + +export function accessAssessment(person: PersonEntity, infrastructure: InfrastructureEntity) { + const modalitySupported = infrastructure.accessibility.includes(person.mobility.mode); + return { + pass: infrastructure.operational && modalitySupported, + cause: !infrastructure.operational ? 'environmental infrastructure unavailable' : modalitySupported ? 'accessible' : 'mobility modality not supported' + }; +} From f5440b2f6bb63dfaa3bba9c084b12a7b0bf66aa4 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 21:09:11 +1000 Subject: [PATCH 02/46] Connect WebXR world to persistent entities and accessibility state --- src/world/OpenWorldWeb.tsx | 104 +++++++++++++++---------------------- 1 file changed, 42 insertions(+), 62 deletions(-) diff --git a/src/world/OpenWorldWeb.tsx b/src/world/OpenWorldWeb.tsx index ada70fb..0baba0c 100644 --- a/src/world/OpenWorldWeb.tsx +++ b/src/world/OpenWorldWeb.tsx @@ -1,79 +1,59 @@ import React, { useEffect, useRef, useState } from 'react'; +import { InfrastructureEntity, PersonEntity, PersistentWorldStore, accessAssessment } from './worldModel'; export function OpenWorldWeb() { const canvasRef = useRef(null); + const storeRef = useRef(); + if (!storeRef.current) storeRef.current = new PersistentWorldStore(); const [status, setStatus] = useState('Loading world…'); const [xr, setXr] = useState('Checking WebXR…'); + const [access, setAccess] = useState('ACCESS NETWORK ONLINE'); useEffect(() => { - let disposed = false; - let engine: any; - let scene: any; + let disposed = false; let engine: any; let scene: any; + const store = storeRef.current!; const boot = async () => { - const B = await import('@babylonjs/core'); - await import('@babylonjs/loaders'); + const B = await import('@babylonjs/core'); await import('@babylonjs/loaders'); if (!canvasRef.current || disposed) return; engine = new B.Engine(canvasRef.current, true, { preserveDrawingBuffer: true, stencil: true }); - scene = new B.Scene(engine); - scene.clearColor = new B.Color4(0.72, 0.86, 0.96, 1); - scene.collisionsEnabled = true; - - const camera = new B.UniversalCamera('player', new B.Vector3(0, 1.65, -16), scene); - camera.attachControl(canvasRef.current, true); - camera.speed = 0.45; - camera.angularSensibility = 3500; - camera.applyGravity = true; - camera.checkCollisions = true; - camera.ellipsoid = new B.Vector3(0.5, 0.9, 0.5); - camera.keysUp.push(87); camera.keysDown.push(83); camera.keysLeft.push(65); camera.keysRight.push(68); - - new B.HemisphericLight('sky', new B.Vector3(0, 1, 0), scene).intensity = 0.85; - const sun = new B.DirectionalLight('sun', new B.Vector3(-0.4, -1, 0.25), scene); sun.intensity = 0.7; - - const ground = B.MeshBuilder.CreateGround('accessible-campus', { width: 220, height: 220, subdivisions: 2 }, scene); - ground.checkCollisions = true; - const groundMat = new B.StandardMaterial('groundMat', scene); groundMat.diffuseColor = new B.Color3(0.38, 0.55, 0.35); ground.material = groundMat; - - const pathMat = new B.StandardMaterial('pathMat', scene); pathMat.diffuseColor = new B.Color3(0.62, 0.61, 0.57); - const path = B.MeshBuilder.CreateGround('wide-accessible-path', { width: 12, height: 190 }, scene); path.position.y = 0.015; path.material = pathMat; - - const buildingMat = new B.StandardMaterial('buildingMat', scene); buildingMat.diffuseColor = new B.Color3(0.72, 0.76, 0.78); - const glassMat = new B.StandardMaterial('glassMat', scene); glassMat.diffuseColor = new B.Color3(0.25, 0.55, 0.7); glassMat.alpha = 0.72; - const makeBuilding = (name:string, x:number, z:number, w:number, d:number, h:number) => { - const body = B.MeshBuilder.CreateBox(name, { width:w, depth:d, height:h }, scene); body.position.set(x,h/2,z); body.material=buildingMat; body.checkCollisions=true; - const door = B.MeshBuilder.CreateBox(name+'-entry', { width:3.2, depth:0.12, height:2.7 }, scene); door.position.set(x,1.35,z-d/2-0.07); door.material=glassMat; - return body; - }; - makeBuilding('Clinical Simulation Centre', -24, 8, 28, 20, 10); - makeBuilding('Community Hub', 25, 22, 26, 18, 7); - makeBuilding('Rehabilitation Lab', 24, -28, 24, 18, 8); - makeBuilding('Accessible Transit Station', -25, -34, 30, 15, 6); - - const treeMat = new B.StandardMaterial('tree', scene); treeMat.diffuseColor = new B.Color3(0.16,0.38,0.17); - for (let i=0;i<44;i++) { const a=i*2.399, r=42+(i%5)*8; const t=B.MeshBuilder.CreateCylinder('tree-'+i,{height:5,diameterTop:0.5,diameterBottom:2.4,tessellation:7},scene); t.position.set(Math.cos(a)*r,2.5,Math.sin(a)*r); t.material=treeMat; } - - const personMat = new B.StandardMaterial('people', scene); personMat.diffuseColor = new B.Color3(0.22,0.32,0.55); - for (let i=0;i<12;i++) { const p=B.MeshBuilder.CreateCapsule('community-member-'+i,{height:1.7,radius:0.32},scene); p.position.set(-5+(i%4)*4,0.85,4+Math.floor(i/4)*5); p.material=personMat; } - const chair = B.MeshBuilder.CreateBox('powered-wheelchair-user',{width:0.8,height:0.9,depth:1.05},scene); chair.position.set(5,0.48,9); chair.material=personMat; - - try { - const helper = await scene.createDefaultXRExperienceAsync({ floorMeshes: [ground, path], disableTeleportation: false }); - setXr(helper.baseExperience ? 'VR READY · use headset Enter VR control' : 'VR unavailable'); - } catch { setXr('WebXR not available on this browser/device'); } - - setStatus('WORLD ONLINE · explore with WASD / mouse'); - engine.runRenderLoop(()=>scene.render()); - const resize=()=>engine.resize(); window.addEventListener('resize',resize); - (scene as any).__cleanup=()=>window.removeEventListener('resize',resize); + scene = new B.Scene(engine); scene.clearColor = new B.Color4(0.72, 0.86, 0.96, 1); scene.collisionsEnabled = true; + const camera = new B.UniversalCamera('player', new B.Vector3(0, 1.65, -16), scene); camera.attachControl(canvasRef.current, true); camera.speed=.45; camera.angularSensibility=3500; camera.applyGravity=true; camera.checkCollisions=true; camera.ellipsoid=new B.Vector3(.5,.9,.5); camera.keysUp.push(87); camera.keysDown.push(83); camera.keysLeft.push(65); camera.keysRight.push(68); + new B.HemisphericLight('sky', new B.Vector3(0,1,0), scene).intensity=.85; new B.DirectionalLight('sun', new B.Vector3(-.4,-1,.25), scene).intensity=.7; + const material=(name:string,r:number,g:number,b:number)=>{const m=new B.StandardMaterial(name,scene);m.diffuseColor=new B.Color3(r,g,b);return m;}; + const ground=B.MeshBuilder.CreateGround('accessible-campus',{width:220,height:220},scene); ground.checkCollisions=true; ground.material=material('ground',.38,.55,.35); + const path=B.MeshBuilder.CreateGround('wide-accessible-path',{width:12,height:190},scene); path.position.y=.015; path.material=material('path',.62,.61,.57); + const buildingMat=material('building',.72,.76,.78); const glass=material('glass',.25,.55,.7); glass.alpha=.72; + const makeBuilding=(name:string,x:number,z:number,w:number,d:number,h:number)=>{const body=B.MeshBuilder.CreateBox(name,{width:w,depth:d,height:h},scene);body.position.set(x,h/2,z);body.material=buildingMat;body.checkCollisions=true;const door=B.MeshBuilder.CreateBox(name+'-automatic-entry',{width:3.2,depth:.12,height:2.7},scene);door.position.set(x,1.35,z-d/2-.07);door.material=glass;return body;}; + makeBuilding('Clinical Simulation Centre',-24,8,28,20,10); makeBuilding('Community Hub',25,22,26,18,7); makeBuilding('Rehabilitation Lab',24,-28,24,18,8); makeBuilding('Accessible Transit Station',-25,-34,30,15,6); + + const personMat=material('people',.22,.32,.55); const accessMat=material('access',.16,.48,.5); const unavailableMat=material('unavailable',.48,.2,.2); + const meshes=new Map(); + for (const entity of store.snapshot().entities) { + if (entity.kind==='person') { + const p=entity as PersonEntity; + const body=B.MeshBuilder.CreateCapsule(entity.id,{height:1.35,radius:.3},scene); body.position.set(p.position[0],1.05,p.position[2]); body.material=personMat; meshes.set(entity.id,body); + if(p.mobility.mode.includes('wheelchair')) { const chair=B.MeshBuilder.CreateBox(entity.id+'-mobility',{width:.85,height:.55,depth:1.05},scene); chair.position.set(p.position[0],.38,p.position[2]); chair.material=personMat; } + } + if(entity.kind==='infrastructure') { + const i=entity as InfrastructureEntity; const marker=B.MeshBuilder.CreateCylinder(entity.id,{height:1.5,diameter:.65},scene); marker.position.set(i.position[0],.75,i.position[2]); marker.material=i.operational?accessMat:unavailableMat; meshes.set(i.id,marker); + } + } + for(let i=0;i<28;i++){const a=i*2.399,r=46+(i%4)*9,t=B.MeshBuilder.CreateCylinder('tree-'+i,{height:5,diameterTop:.5,diameterBottom:2.4,tessellation:7},scene);t.position.set(Math.cos(a)*r,2.5,Math.sin(a)*r);t.material=material('tree-'+i,.16,.38,.17);} + + let last=performance.now(); + scene.onBeforeRenderObservable.add(()=>{const now=performance.now(); if(now-last>1000){store.tick((now-last)/1000);last=now;} const maya=store.entity('maya') as PersonEntity|undefined; const lift=store.entity('station-lift') as InfrastructureEntity|undefined; if(maya&&lift){const assessment=accessAssessment(maya,lift);setAccess(`TRANSIT ACCESS: ${assessment.pass?'AVAILABLE':'BARRIER'} · ${assessment.cause}`);}}); + try { const helper=await scene.createDefaultXRExperienceAsync({floorMeshes:[ground,path],disableTeleportation:false}); setXr(helper.baseExperience?'VR READY · use headset Enter VR control':'VR unavailable'); } catch { setXr('WebXR not available on this browser/device'); } + setStatus('PERSISTENT WORLD ONLINE · WASD / mouse / WebXR'); engine.runRenderLoop(()=>scene.render()); const resize=()=>engine.resize();window.addEventListener('resize',resize);(scene as any).__cleanup=()=>window.removeEventListener('resize',resize); }; - boot(); - return ()=>{ disposed=true; scene?.__cleanup?.(); scene?.dispose?.(); engine?.dispose?.(); }; + boot(); return()=>{disposed=true;scene?.__cleanup?.();scene?.dispose?.();engine?.dispose?.();}; },[]); + const toggleLift=()=>{const store=storeRef.current!;const lift=store.entity('station-lift') as InfrastructureEntity;store.setInfrastructure('station-lift',!lift.operational);}; + const toggleAAC=()=>{const store=storeRef.current!;const maya=store.entity('maya') as PersonEntity;store.setCommunicationAccess('maya',maya.communication.access==='available'?'degraded':'available');setStatus(`MAYA AAC ACCESS · ${store.entity('maya') && (store.entity('maya') as PersonEntity).communication.access.toUpperCase()}`);}; + return
- -
- MERT · DISABILITY WORLD
{status}
{xr}
Clinical Simulation Centre · Community Hub · Rehab Lab · Accessible Transit -
+ +
MERT · PERSISTENT DISABILITY WORLD
{status}
{xr}
{access}
Clinical Centre · Community · Rehab · Accessible Transit
+
Accessibility is causal: infrastructure failure is recorded as an environmental barrier; AAC access changes observability, not Maya's authority or cognition. State persists locally between sessions.
; } From 9cd6c22763d2784aa25a7e9e0f8d91223ecd2171 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 21:09:20 +1000 Subject: [PATCH 03/46] Test persistent world accessibility causality --- __tests__/worldModel.test.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 __tests__/worldModel.test.ts diff --git a/__tests__/worldModel.test.ts b/__tests__/worldModel.test.ts new file mode 100644 index 0000000..71e1dd0 --- /dev/null +++ b/__tests__/worldModel.test.ts @@ -0,0 +1,21 @@ +import { InfrastructureEntity, PersonEntity, PersistentWorldStore, accessAssessment, initialOpenWorldState } from '../src/world/worldModel'; + +describe('persistent disability world', () => { + it('attributes a failed lift to the environment rather than person incapacity', () => { + const store = new PersistentWorldStore(initialOpenWorldState); + const person = store.entity('maya') as PersonEntity; + const lift = store.entity('station-lift') as InfrastructureEntity; + expect(accessAssessment(person, lift)).toEqual({ pass: true, cause: 'accessible' }); + store.setInfrastructure('station-lift', false); + expect(accessAssessment(person, store.entity('station-lift') as InfrastructureEntity)).toEqual({ pass: false, cause: 'environmental infrastructure unavailable' }); + expect(person.authority).toBe('self'); + }); + + it('does not change decision authority when AAC access degrades', () => { + const store = new PersistentWorldStore(initialOpenWorldState); + store.setCommunicationAccess('maya', 'degraded'); + const person = store.entity('maya') as PersonEntity; + expect(person.communication.access).toBe('degraded'); + expect(person.authority).toBe('self'); + }); +}); From 63e47ad73d605a9dee2ee5bb7a007d2405c787de Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 22:51:27 +1000 Subject: [PATCH 04/46] Add environment affordance graph --- src/world/affordanceGraph.ts | 85 ++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/world/affordanceGraph.ts diff --git a/src/world/affordanceGraph.ts b/src/world/affordanceGraph.ts new file mode 100644 index 0000000..e072b4c --- /dev/null +++ b/src/world/affordanceGraph.ts @@ -0,0 +1,85 @@ +import type { InfrastructureEntity, OpenWorldEntity, PersonEntity, PersistentWorldState } from './worldModel'; + +export type AffordanceAction = + | 'reach' + | 'enter' + | 'use' + | 'communicate' + | 'recharge' + | 'rest'; + +export interface AffordanceResult { + action: AffordanceAction; + entityId: string; + available: boolean; + reason: string; + requiresSupport: boolean; + cost: number; +} + +const supportsMobility = (person: PersonEntity, infrastructure: InfrastructureEntity) => + infrastructure.accessibility.includes(person.mobility.mode); + +export function evaluateAffordance( + person: PersonEntity, + target: OpenWorldEntity, + action: AffordanceAction, +): AffordanceResult { + if (target.kind === 'infrastructure') { + const infra = target as InfrastructureEntity; + const modalitySupported = supportsMobility(person, infra); + if (!infra.operational) { + return { action, entityId: target.id, available: false, reason: 'environmental infrastructure unavailable', requiresSupport: false, cost: 100 }; + } + if (!modalitySupported && action !== 'communicate') { + return { action, entityId: target.id, available: false, reason: 'current mobility modality is not supported by this feature', requiresSupport: !person.mobility.independent, cost: 80 }; + } + if (action === 'recharge' && infra.feature !== 'charging-point') { + return { action, entityId: target.id, available: false, reason: 'target does not provide charging', requiresSupport: false, cost: 50 }; + } + if (action === 'rest' && infra.feature !== 'quiet-space') { + return { action, entityId: target.id, available: false, reason: 'target is not a designated rest space', requiresSupport: false, cost: 40 }; + } + return { action, entityId: target.id, available: true, reason: 'accessible', requiresSupport: !person.mobility.independent, cost: 5 }; + } + + if (action === 'communicate') { + const available = person.communication.access !== 'unavailable'; + return { + action, + entityId: target.id, + available, + reason: available ? 'communication access available' : 'communication access unavailable', + requiresSupport: false, + cost: available ? person.communication.responseLatencySeconds : 100, + }; + } + + return { action, entityId: target.id, available: true, reason: 'no access barrier represented', requiresSupport: false, cost: 10 }; +} + +export interface RouteOption { + targetId: string; + affordances: AffordanceResult[]; + accessible: boolean; + totalCost: number; +} + +export function routeOptions(state: PersistentWorldState, personId: string): RouteOption[] { + const person = state.entities.find((entity): entity is PersonEntity => entity.id === personId && entity.kind === 'person'); + if (!person) return []; + + return state.entities + .filter((entity) => entity.id !== personId) + .map((target) => { + const reach = evaluateAffordance(person, target, 'reach'); + const use = evaluateAffordance(person, target, 'use'); + return { + targetId: target.id, + affordances: [reach, use], + accessible: reach.available && use.available, + totalCost: reach.cost + use.cost, + }; + }) + .sort((a, b) => a.totalCost - b.totalCost); +} From 6106173cf49a1ce7bba578db6a6dde498dd52af2 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 22:51:37 +1000 Subject: [PATCH 05/46] Add bounded autonomous daily-life planner --- src/world/autonomy.ts | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/world/autonomy.ts diff --git a/src/world/autonomy.ts b/src/world/autonomy.ts new file mode 100644 index 0000000..7eaabcc --- /dev/null +++ b/src/world/autonomy.ts @@ -0,0 +1,49 @@ +import { routeOptions } from './affordanceGraph'; +import type { PersonEntity, PersistentWorldState } from './worldModel'; + +export type ActivityKind = 'travel' | 'socialise' | 'communicate' | 'recharge' | 'rest' | 'seek-alternative'; + +export interface PlannedActivity { + personId: string; + kind: ActivityKind; + goal: string; + targetId?: string; + rationale: string; + confidence: number; +} + +function goalTarget(goal: string) { + const text = goal.toLowerCase(); + if (text.includes('community')) return 'community-hub'; + if (text.includes('communicate')) return 'community-hub'; + if (text.includes('decision')) return 'clinical-centre'; + return undefined; +} + +export function planNextActivity(state: PersistentWorldState, personId: string): PlannedActivity | undefined { + const person = state.entities.find((entity): entity is PersonEntity => entity.id === personId && entity.kind === 'person'); + if (!person || person.goals.length === 0) return undefined; + + const goal = person.goals[0]; + const preferredTarget = goalTarget(goal); + const routes = routeOptions(state, personId); + const preferred = routes.find((route) => route.targetId === preferredTarget); + + if (preferredTarget && preferred?.accessible) { + return { personId, kind: 'travel', goal, targetId: preferredTarget, rationale: 'goal-aligned accessible route is available', confidence: 0.95 }; + } + + const alternative = routes.find((route) => route.accessible); + if (alternative) { + return { personId, kind: 'seek-alternative', goal, targetId: alternative.targetId, rationale: preferredTarget ? 'preferred route is inaccessible; preserve goal by seeking an accessible alternative' : 'accessible option selected from represented affordances', confidence: 0.8 }; + } + + return { personId, kind: 'rest', goal, rationale: 'no represented accessible route is currently available; do not attribute the barrier to disability', confidence: 0.75 }; +} + +export function planPopulation(state: PersistentWorldState) { + return state.entities + .filter((entity): entity is PersonEntity => entity.kind === 'person') + .map((person) => planNextActivity(state, person.id)) + .filter((activity): activity is PlannedActivity => Boolean(activity)); +} From d30f4cc7fba61f5c6e9610459ed02b2c898a8598 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 22:51:49 +1000 Subject: [PATCH 06/46] Add bounded VNN world dynamics modules --- src/world/vnnDynamics.ts | 64 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/world/vnnDynamics.ts diff --git a/src/world/vnnDynamics.ts b/src/world/vnnDynamics.ts new file mode 100644 index 0000000..567ea81 --- /dev/null +++ b/src/world/vnnDynamics.ts @@ -0,0 +1,64 @@ +import type { PersonEntity, PersistentWorldState, WorldEvent } from './worldModel'; +import type { PlannedActivity } from './autonomy'; + +export type DynamicsNode = 'mobility' | 'communication' | 'fatigue' | 'environment' | 'social' | 'health' | 'institutional'; + +export interface DynamicsSignal { + node: DynamicsNode; + personId: string; + metric: string; + delta: number; + confidence: number; + cause: string; + provenance: 'deterministic-vnn'; +} + +export interface DynamicsProposal { + personId: string; + activity: PlannedActivity; + signals: DynamicsSignal[]; + invariantChecks: string[]; +} + +export function proposeDynamics(state: PersistentWorldState, activity: PlannedActivity): DynamicsProposal { + const person = state.entities.find((entity): entity is PersonEntity => entity.id === activity.personId && entity.kind === 'person'); + if (!person) return { personId: activity.personId, activity, signals: [], invariantChecks: ['person exists: FAIL'] }; + + const signals: DynamicsSignal[] = []; + if (activity.kind === 'travel') { + signals.push({ node: 'mobility', personId: person.id, metric: 'participation', delta: 1, confidence: 0.9, cause: 'goal-aligned accessible travel', provenance: 'deterministic-vnn' }); + signals.push({ node: 'fatigue', personId: person.id, metric: 'energy-demand', delta: 0.1, confidence: 0.65, cause: 'travel activity', provenance: 'deterministic-vnn' }); + } + if (activity.kind === 'seek-alternative') { + signals.push({ node: 'environment', personId: person.id, metric: 'access-barrier', delta: 1, confidence: 0.95, cause: 'preferred route unavailable', provenance: 'deterministic-vnn' }); + signals.push({ node: 'fatigue', personId: person.id, metric: 'energy-demand', delta: 0.2, confidence: 0.7, cause: 'rerouting burden', provenance: 'deterministic-vnn' }); + } + if (person.communication.access === 'degraded') { + signals.push({ node: 'communication', personId: person.id, metric: 'observability', delta: -0.35, confidence: 1, cause: 'AAC access degraded', provenance: 'deterministic-vnn' }); + } + if (person.communication.access === 'unavailable') { + signals.push({ node: 'communication', personId: person.id, metric: 'observability', delta: -1, confidence: 1, cause: 'communication access unavailable', provenance: 'deterministic-vnn' }); + } + + return { + personId: person.id, + activity, + signals, + invariantChecks: [ + 'person authority remains self', + 'communication access does not infer incapacity', + 'environmental barrier is not attributed to impairment', + 'VNN signals are proposals, not clinical facts', + ], + }; +} + +export function dynamicsEvents(state: PersistentWorldState, proposal: DynamicsProposal): WorldEvent[] { + return proposal.signals.map((signal, index) => ({ + id: `${state.simulationSeconds}-vnn-${proposal.personId}-${index}`, + at: state.simulationSeconds, + type: `vnn.${signal.node}.${signal.metric}`, + entityId: proposal.personId, + detail: `${signal.cause}; delta=${signal.delta}; confidence=${signal.confidence}`, + })); +} From b74e6c1a890c5863f840c5a6a9707e5b0e9a53ab Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 22:51:58 +1000 Subject: [PATCH 07/46] Test autonomous planning and disability world invariants --- __tests__/worldAutonomy.test.ts | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 __tests__/worldAutonomy.test.ts diff --git a/__tests__/worldAutonomy.test.ts b/__tests__/worldAutonomy.test.ts new file mode 100644 index 0000000..04d106a --- /dev/null +++ b/__tests__/worldAutonomy.test.ts @@ -0,0 +1,38 @@ +import { initialOpenWorldState } from '../src/world/worldModel'; +import { evaluateAffordance } from '../src/world/affordanceGraph'; +import { planNextActivity } from '../src/world/autonomy'; +import { proposeDynamics } from '../src/world/vnnDynamics'; + +describe('open world autonomy', () => { + test('AAC loss changes observability without changing authority', () => { + const state = structuredClone(initialOpenWorldState); + const maya = state.entities.find((entity) => entity.id === 'maya') as any; + maya.communication.access = 'unavailable'; + const activity = planNextActivity(state, 'maya')!; + const proposal = proposeDynamics(state, activity); + + expect(maya.authority).toBe('self'); + expect(proposal.signals.some((signal) => signal.node === 'communication' && signal.metric === 'observability')).toBe(true); + expect(proposal.invariantChecks).toContain('communication access does not infer incapacity'); + }); + + test('failed lift is represented as an environmental barrier', () => { + const state = structuredClone(initialOpenWorldState); + const maya = state.entities.find((entity) => entity.id === 'maya') as any; + const lift = state.entities.find((entity) => entity.id === 'station-lift') as any; + lift.operational = false; + + const result = evaluateAffordance(maya, lift, 'use'); + expect(result.available).toBe(false); + expect(result.reason).toBe('environmental infrastructure unavailable'); + expect(maya.mobility.independent).toBe(true); + }); + + test('planner preserves the goal by searching for alternatives', () => { + const state = structuredClone(initialOpenWorldState); + const activity = planNextActivity(state, 'maya'); + expect(activity).toBeDefined(); + expect(['travel', 'seek-alternative', 'rest']).toContain(activity!.kind); + expect(activity!.goal).toBe('move through the community'); + }); +}); From 4e96ea5841facdc326e71bb2136fc4eecbf33c63 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 22:57:01 +1000 Subject: [PATCH 08/46] Add bounded social multi-agent layer --- src/world/socialAgents.ts | 114 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/world/socialAgents.ts diff --git a/src/world/socialAgents.ts b/src/world/socialAgents.ts new file mode 100644 index 0000000..f17dc67 --- /dev/null +++ b/src/world/socialAgents.ts @@ -0,0 +1,114 @@ +import type { PersonEntity, PersistentWorldState, WorldEvent } from './worldModel'; + +export type SocialRole = 'peer' | 'support-worker' | 'clinician' | 'transport-staff' | 'educator' | 'employer' | 'bystander'; +export type KnowledgeStatus = 'known' | 'unknown' | 'uncertain'; + +export interface SocialAgent { + id: string; + label: string; + role: SocialRole; + knowledge: Record; + communicationPractice: 'direct-first' | 'proxy-first' | 'mixed'; + respectsAuthority: boolean; + escalationThreshold: number; +} + +export interface SocialInteraction { + agentId: string; + personId: string; + intent: 'greet' | 'ask' | 'assist' | 'handover' | 'escalate'; + outcome: 'supported' | 'barrier' | 'unknown'; + effects: Array<{ + domain: 'communication' | 'trust' | 'access' | 'institutional'; + delta: number; + cause: string; + }>; + invariantChecks: string[]; +} + +export const defaultSocialAgents: SocialAgent[] = [ + { + id: 'community-peer-1', + label: 'Community peer', + role: 'peer', + knowledge: { aac: 'uncertain', baseline: 'unknown' }, + communicationPractice: 'direct-first', + respectsAuthority: true, + escalationThreshold: 0.9, + }, + { + id: 'support-worker-1', + label: 'Communication support worker', + role: 'support-worker', + knowledge: { aac: 'known', baseline: 'known' }, + communicationPractice: 'direct-first', + respectsAuthority: true, + escalationThreshold: 0.65, + }, + { + id: 'clinician-1', + label: 'Clinician', + role: 'clinician', + knowledge: { aac: 'uncertain', baseline: 'uncertain' }, + communicationPractice: 'mixed', + respectsAuthority: true, + escalationThreshold: 0.55, + }, + { + id: 'transit-staff-1', + label: 'Transit staff member', + role: 'transport-staff', + knowledge: { access: 'known', aac: 'uncertain' }, + communicationPractice: 'direct-first', + respectsAuthority: true, + escalationThreshold: 0.8, + }, +]; + +export function interact( + state: PersistentWorldState, + agent: SocialAgent, + personId: string, + intent: SocialInteraction['intent'], +): SocialInteraction | undefined { + const person = state.entities.find((entity): entity is PersonEntity => entity.id === personId && entity.kind === 'person'); + if (!person) return undefined; + + const effects: SocialInteraction['effects'] = []; + let outcome: SocialInteraction['outcome'] = 'supported'; + + if (!agent.respectsAuthority || agent.communicationPractice === 'proxy-first') { + outcome = 'barrier'; + effects.push({ domain: 'trust', delta: -0.4, cause: 'person was not treated as the primary decision-maker' }); + effects.push({ domain: 'communication', delta: -0.3, cause: 'communication was redirected away from the person' }); + } else if (person.communication.access === 'unavailable' && intent === 'ask') { + outcome = 'unknown'; + effects.push({ domain: 'communication', delta: -0.2, cause: 'communication access unavailable; answer remains unknown rather than inferred' }); + } else { + effects.push({ domain: 'trust', delta: 0.2, cause: 'direct, authority-preserving interaction' }); + } + + return { + agentId: agent.id, + personId, + intent, + outcome, + effects, + invariantChecks: [ + 'support worker does not automatically become substitute decision-maker', + 'no response remains unknown', + 'communication difference does not imply cognitive impairment', + 'social agent knowledge is bounded and explicit', + ], + }; +} + +export function socialEvents(state: PersistentWorldState, interaction: SocialInteraction): WorldEvent[] { + return interaction.effects.map((effect, index) => ({ + id: `${state.simulationSeconds}-social-${interaction.agentId}-${index}`, + at: state.simulationSeconds, + type: `social.${effect.domain}`, + entityId: interaction.personId, + detail: `${effect.cause}; delta=${effect.delta}; outcome=${interaction.outcome}`, + })); +} From aedc85be03868b6aa7887b8e8fcdfee3b3750ae6 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 22:57:16 +1000 Subject: [PATCH 09/46] Bridge open-world events into MERT clinical runtime --- src/world/clinicalBridge.ts | 89 +++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/world/clinicalBridge.ts diff --git a/src/world/clinicalBridge.ts b/src/world/clinicalBridge.ts new file mode 100644 index 0000000..dc03530 --- /dev/null +++ b/src/world/clinicalBridge.ts @@ -0,0 +1,89 @@ +import type { PersonEntity, PersistentWorldState, WorldEvent } from './worldModel'; +import type { DynamicsProposal } from './vnnDynamics'; + +export type ClinicalBridgeStatus = 'continue-world' | 'assessment-suggested' | 'scenario-eligible'; + +export interface ClinicalTriggerAssessment { + personId: string; + status: ClinicalBridgeStatus; + reasons: string[]; + scenarioId?: string; + preserve: { + authority: true; + baseline: true; + communicationState: true; + worldHistory: true; + }; +} + +export interface ClinicalRuntimePort { + emit(type: string, payload: unknown, source?: string): void; +} + +export function assessClinicalTrigger( + state: PersistentWorldState, + personId: string, + proposals: DynamicsProposal[] = [], +): ClinicalTriggerAssessment { + const person = state.entities.find((entity): entity is PersonEntity => entity.id === personId && entity.kind === 'person'); + if (!person) { + return { personId, status: 'continue-world', reasons: ['person not found'], preserve: { authority: true, baseline: true, communicationState: true, worldHistory: true } }; + } + + const personSignals = proposals.flatMap((proposal) => proposal.personId === personId ? proposal.signals : []); + const healthSignals = personSignals.filter((signal) => signal.node === 'health'); + const severeAccessCascade = personSignals.filter((signal) => signal.node === 'environment' && signal.metric === 'access-barrier').length >= 2; + + if (healthSignals.some((signal) => Math.abs(signal.delta) >= 1 && signal.confidence >= 0.8)) { + return { + personId, + status: 'scenario-eligible', + reasons: ['high-confidence health-state change proposed by bounded dynamics; requires scenario evidence gate'], + scenarioId: 'not-my-baseline', + preserve: { authority: true, baseline: true, communicationState: true, worldHistory: true }, + }; + } + + if (healthSignals.length > 0 || severeAccessCascade) { + return { + personId, + status: 'assessment-suggested', + reasons: severeAccessCascade ? ['repeated environmental barriers may increase burden; assess without pathologising disability'] : ['health signal present but threshold for scenario transition not met'], + preserve: { authority: true, baseline: true, communicationState: true, worldHistory: true }, + }; + } + + return { + personId, + status: 'continue-world', + reasons: ['no evidence-gated clinical transition'], + preserve: { authority: true, baseline: true, communicationState: true, worldHistory: true }, + }; +} + +export function bridgeToClinicalRuntime( + state: PersistentWorldState, + assessment: ClinicalTriggerAssessment, + runtime: ClinicalRuntimePort, +): WorldEvent | undefined { + if (assessment.status !== 'scenario-eligible' || !assessment.scenarioId) return undefined; + const person = state.entities.find((entity): entity is PersonEntity => entity.id === assessment.personId && entity.kind === 'person'); + if (!person) return undefined; + + runtime.emit('world.clinical-bridge.requested', { + scenarioId: assessment.scenarioId, + personId: person.id, + baselineAuthority: person.authority, + communication: person.communication, + worldSimulationSeconds: state.simulationSeconds, + reasons: assessment.reasons, + }, 'open-world'); + + return { + id: `${state.simulationSeconds}-clinical-bridge-${person.id}`, + at: state.simulationSeconds, + type: 'clinical.bridge.requested', + entityId: person.id, + detail: `scenario=${assessment.scenarioId}; world remains authoritative for identity, baseline, communication access and history`, + }; +} From 33de78573c72d888a3d681345d87b79ada703a45 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 22:57:30 +1000 Subject: [PATCH 10/46] Add counterfactual timeline and replay engine --- src/world/timeline.ts | 86 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/world/timeline.ts diff --git a/src/world/timeline.ts b/src/world/timeline.ts new file mode 100644 index 0000000..6777b65 --- /dev/null +++ b/src/world/timeline.ts @@ -0,0 +1,86 @@ +import type { PersistentWorldState, WorldEvent } from './worldModel'; + +export interface TimelineSnapshot { + id: string; + label: string; + at: number; + state: PersistentWorldState; + parentId?: string; + branchReason?: string; +} + +export interface CounterfactualComparison { + leftId: string; + rightId: string; + simulationDeltaSeconds: number; + entityChanges: Array<{ entityId: string; changedFields: string[] }>; + eventTypesOnlyLeft: string[]; + eventTypesOnlyRight: string[]; +} + +const clone = (value: T): T => JSON.parse(JSON.stringify(value)) as T; + +export class WorldTimeline { + private snapshots = new Map(); + + capture(state: PersistentWorldState, label: string, parentId?: string, branchReason?: string): TimelineSnapshot { + const id = `snapshot-${state.simulationSeconds}-${this.snapshots.size + 1}`; + const snapshot: TimelineSnapshot = { id, label, at: state.simulationSeconds, state: clone(state), parentId, branchReason }; + this.snapshots.set(id, snapshot); + return clone(snapshot); + } + + get(id: string): TimelineSnapshot | undefined { + const snapshot = this.snapshots.get(id); + return snapshot ? clone(snapshot) : undefined; + } + + branch(snapshotId: string, label: string, mutate: (state: PersistentWorldState) => void, reason: string): TimelineSnapshot | undefined { + const parent = this.snapshots.get(snapshotId); + if (!parent) return undefined; + const state = clone(parent.state); + mutate(state); + state.events.push({ + id: `${state.simulationSeconds}-counterfactual-${state.events.length}`, + at: state.simulationSeconds, + type: 'timeline.counterfactual.branch', + detail: reason, + }); + return this.capture(state, label, parent.id, reason); + } + + replay(snapshotId: string, events: WorldEvent[]): TimelineSnapshot | undefined { + const parent = this.snapshots.get(snapshotId); + if (!parent) return undefined; + const state = clone(parent.state); + state.events.push(...clone(events)); + if (events.length > 0) state.simulationSeconds = Math.max(state.simulationSeconds, ...events.map((event) => event.at)); + return this.capture(state, `${parent.label} replay`, parent.id, 'deterministic event replay'); + } + + compare(leftId: string, rightId: string): CounterfactualComparison | undefined { + const left = this.snapshots.get(leftId); + const right = this.snapshots.get(rightId); + if (!left || !right) return undefined; + + const rightById = new Map(right.state.entities.map((entity) => [entity.id, entity])); + const entityChanges = left.state.entities.flatMap((leftEntity) => { + const rightEntity = rightById.get(leftEntity.id); + if (!rightEntity) return [{ entityId: leftEntity.id, changedFields: ['removed'] }]; + const fields = new Set([...Object.keys(leftEntity), ...Object.keys(rightEntity)]); + const changedFields = [...fields].filter((field) => JSON.stringify((leftEntity as any)[field]) !== JSON.stringify((rightEntity as any)[field])); + return changedFields.length ? [{ entityId: leftEntity.id, changedFields }] : []; + }); + + const leftTypes = new Set(left.state.events.map((event) => event.type)); + const rightTypes = new Set(right.state.events.map((event) => event.type)); + return { + leftId, + rightId, + simulationDeltaSeconds: right.at - left.at, + entityChanges, + eventTypesOnlyLeft: [...leftTypes].filter((type) => !rightTypes.has(type)), + eventTypesOnlyRight: [...rightTypes].filter((type) => !leftTypes.has(type)), + }; + } +} From 998a258ffe1ae264b36c82d0fcbe30108bd4aaea Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 22:57:44 +1000 Subject: [PATCH 11/46] Add modality-neutral VR interaction contract --- src/world/vrInteraction.ts | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/world/vrInteraction.ts diff --git a/src/world/vrInteraction.ts b/src/world/vrInteraction.ts new file mode 100644 index 0000000..324f4da --- /dev/null +++ b/src/world/vrInteraction.ts @@ -0,0 +1,44 @@ +export type InteractionModality = 'xr-controller' | 'gaze' | 'switch' | 'keyboard' | 'touch' | 'voice' | 'companion'; +export type LocomotionMode = 'teleport' | 'smooth' | 'seated-snap-turn' | 'non-spatial'; + +export interface InteractionProfile { + modalities: InteractionModality[]; + locomotion: LocomotionMode; + reducedMotion: boolean; + dwellMilliseconds: number; + responseTimeMultiplier: number; + dominantHand?: 'left' | 'right' | 'none'; +} + +export interface WorldIntent { + actorId: string; + action: 'select' | 'move' | 'communicate' | 'inspect' | 'use' | 'pause'; + targetId?: string; + modality: InteractionModality; + payload?: unknown; +} + +export const defaultInteractionProfile: InteractionProfile = { + modalities: ['keyboard', 'touch', 'xr-controller'], + locomotion: 'teleport', + reducedMotion: false, + dwellMilliseconds: 1000, + responseTimeMultiplier: 1, +}; + +export function normalizeIntent(intent: WorldIntent): WorldIntent { + return { ...intent }; +} + +export function validateInteractionProfile(profile: InteractionProfile): string[] { + const issues: string[] = []; + if (profile.modalities.length === 0) issues.push('at least one interaction modality is required'); + if (profile.dwellMilliseconds < 0) issues.push('dwell time cannot be negative'); + if (profile.responseTimeMultiplier < 1) issues.push('response time multiplier must not shorten the user response window'); + if (profile.reducedMotion && profile.locomotion === 'smooth') issues.push('reduced-motion profile should use teleport, seated snap turn, or non-spatial locomotion'); + return issues; +} + +export function equivalentAction(a: WorldIntent, b: WorldIntent): boolean { + return a.actorId === b.actorId && a.action === b.action && a.targetId === b.targetId; +} From 96626b54f5421237be1ec053b768b5ac225fc055 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 22:57:55 +1000 Subject: [PATCH 12/46] Test social agents clinical bridge timeline and VR parity --- __tests__/worldAdvanced.test.ts | 44 +++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 __tests__/worldAdvanced.test.ts diff --git a/__tests__/worldAdvanced.test.ts b/__tests__/worldAdvanced.test.ts new file mode 100644 index 0000000..48f6bee --- /dev/null +++ b/__tests__/worldAdvanced.test.ts @@ -0,0 +1,44 @@ +import { initialOpenWorldState } from '../src/world/worldModel'; +import { defaultSocialAgents, interact } from '../src/world/socialAgents'; +import { assessClinicalTrigger } from '../src/world/clinicalBridge'; +import { WorldTimeline } from '../src/world/timeline'; +import { equivalentAction, validateInteractionProfile } from '../src/world/vrInteraction'; + +describe('advanced disability world invariants', () => { + it('keeps an unavailable AAC answer unknown rather than transferring authority', () => { + const state = JSON.parse(JSON.stringify(initialOpenWorldState)); + const maya = state.entities.find((entity: any) => entity.id === 'maya'); + maya.communication.access = 'unavailable'; + const interaction = interact(state, defaultSocialAgents[0], 'maya', 'ask'); + expect(interaction?.outcome).toBe('unknown'); + expect(maya.authority).toBe('self'); + }); + + it('does not launch a clinical scenario without an evidence-gated health transition', () => { + const assessment = assessClinicalTrigger(initialOpenWorldState, 'maya', []); + expect(assessment.status).toBe('continue-world'); + expect(assessment.preserve.authority).toBe(true); + }); + + it('creates counterfactual branches without mutating the captured original', () => { + const timeline = new WorldTimeline(); + const original = timeline.capture(initialOpenWorldState, 'original'); + const branch = timeline.branch(original.id, 'lift failure', (state) => { + const lift = state.entities.find((entity) => entity.id === 'station-lift') as any; + lift.operational = false; + }, 'station lift unavailable'); + expect((timeline.get(original.id)?.state.entities.find((entity) => entity.id === 'station-lift') as any).operational).toBe(true); + expect((branch?.state.entities.find((entity) => entity.id === 'station-lift') as any).operational).toBe(false); + }); + + it('treats VR and switch selection as equivalent semantic actions', () => { + expect(equivalentAction( + { actorId: 'maya', action: 'select', targetId: 'community-hub', modality: 'xr-controller' }, + { actorId: 'maya', action: 'select', targetId: 'community-hub', modality: 'switch' }, + )).toBe(true); + }); + + it('rejects reduced-motion smooth locomotion configuration', () => { + expect(validateInteractionProfile({ modalities: ['gaze'], locomotion: 'smooth', reducedMotion: true, dwellMilliseconds: 1200, responseTimeMultiplier: 2 })).toContain('reduced-motion profile should use teleport, seated snap turn, or non-spatial locomotion'); + }); +}); From c9e333f1c7cf020ba0c5feb0f79509dc8dab639c Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 23:00:44 +1000 Subject: [PATCH 13/46] Extend world store for live simulation integration --- src/world/worldModel.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/world/worldModel.ts b/src/world/worldModel.ts index 4334133..8b6708b 100644 --- a/src/world/worldModel.ts +++ b/src/world/worldModel.ts @@ -102,12 +102,28 @@ export class PersistentWorldStore { this.record('communication.access.changed', id, `${entity.label} communication access: ${access}`); } - private record(type: string, entityId: string, detail: string) { - this.state.events.push({ id: `${this.state.simulationSeconds}-${this.state.events.length}`, at: this.state.simulationSeconds, type, entityId, detail }); - this.state.events = this.state.events.slice(-100); + appendEvents(events: WorldEvent[]) { + this.state.events.push(...events); + this.state.events = this.state.events.slice(-250); + this.persist(); + } + + recordEvent(type: string, entityId: string | undefined, detail: string) { + const id = `${this.state.simulationSeconds}-${this.state.events.length}-${type}`; + const event: WorldEvent = { id, at: this.state.simulationSeconds, type, detail, ...(entityId ? { entityId } : {}) }; + this.appendEvents([event]); + return event; + } + + reset(seed: PersistentWorldState = initialOpenWorldState) { + this.state = JSON.parse(JSON.stringify(seed)) as PersistentWorldState; this.persist(); } + private record(type: string, entityId: string, detail: string) { + this.recordEvent(type, entityId, detail); + } + private persist() { if (typeof window !== 'undefined') window.localStorage?.setItem('mert-open-world-v1', JSON.stringify(this.state)); } From abd7b595c6259ba0439819ef6c367b19b10e73a4 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 23:00:56 +1000 Subject: [PATCH 14/46] Add persistent daily life schedules --- src/world/dailyLife.ts | 61 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/world/dailyLife.ts diff --git a/src/world/dailyLife.ts b/src/world/dailyLife.ts new file mode 100644 index 0000000..813d6fd --- /dev/null +++ b/src/world/dailyLife.ts @@ -0,0 +1,61 @@ +import { planNextActivity, type PlannedActivity } from './autonomy'; +import type { PersonEntity, PersistentWorldState, WorldEvent } from './worldModel'; + +export interface DailyScheduleEntry { + atHour: number; + goal: string; + destinationId?: string; +} + +export interface PersonSchedule { + personId: string; + entries: DailyScheduleEntry[]; +} + +export const defaultSchedules: PersonSchedule[] = [ + { + personId: 'maya', + entries: [ + { atHour: 8, goal: 'move through the community', destinationId: 'transit' }, + { atHour: 10, goal: 'communicate directly', destinationId: 'community-hub' }, + { atHour: 14, goal: 'participate in decisions', destinationId: 'clinical-centre' }, + { atHour: 17, goal: 'move through the community', destinationId: 'community-hub' }, + ], + }, +]; + +export interface DailyLifeTick { + activity?: PlannedActivity; + events: WorldEvent[]; +} + +export function simulatedHour(state: PersistentWorldState) { + return (8 + Math.floor(state.simulationSeconds / 3600)) % 24; +} + +export function scheduledGoal(state: PersistentWorldState, schedule: PersonSchedule) { + const hour = simulatedHour(state); + return [...schedule.entries].reverse().find((entry) => entry.atHour <= hour) ?? schedule.entries[0]; +} + +export function runDailyLifeTick(state: PersistentWorldState, schedule: PersonSchedule): DailyLifeTick { + const person = state.entities.find((entity): entity is PersonEntity => entity.kind === 'person' && entity.id === schedule.personId); + if (!person) return { events: [] }; + const scheduled = scheduledGoal(state, schedule); + const planningState: PersistentWorldState = { + ...state, + entities: state.entities.map((entity) => entity.id === person.id && entity.kind === 'person' ? { ...entity, goals: [scheduled.goal, ...person.goals.filter((goal) => goal !== scheduled.goal)] } : entity), + }; + const activity = planNextActivity(planningState, person.id); + if (!activity) return { events: [] }; + return { + activity, + events: [{ + id: `${state.simulationSeconds}-daily-${person.id}`, + at: state.simulationSeconds, + type: 'daily-life.activity.planned', + entityId: person.id, + detail: `hour=${simulatedHour(state)}; goal=${scheduled.goal}; activity=${activity.kind}; target=${activity.targetId ?? scheduled.destinationId ?? 'none'}`, + }], + }; +} From 055c5ae657c00b6d1b7f6c84e33de12a58dc3863 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 23:01:08 +1000 Subject: [PATCH 15/46] Wire planners dynamics social and clinical bridge into live loop --- src/world/liveWorldLoop.ts | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/world/liveWorldLoop.ts diff --git a/src/world/liveWorldLoop.ts b/src/world/liveWorldLoop.ts new file mode 100644 index 0000000..a50bdb6 --- /dev/null +++ b/src/world/liveWorldLoop.ts @@ -0,0 +1,64 @@ +import { planPopulation } from './autonomy'; +import { assessClinicalTrigger, bridgeToClinicalRuntime, type ClinicalRuntimePort } from './clinicalBridge'; +import { defaultSchedules, runDailyLifeTick } from './dailyLife'; +import { defaultSocialAgents, interact, socialEvents } from './socialAgents'; +import { dynamicsEvents, proposeDynamics, type DynamicsProposal } from './vnnDynamics'; +import type { PersonEntity, PersistentWorldState, PersistentWorldStore } from './worldModel'; + +export interface LiveWorldFrame { + activities: ReturnType; + proposals: DynamicsProposal[]; + clinical: ReturnType[]; +} + +export class LiveWorldLoop { + private accumulator = 0; + private socialAccumulator = 0; + private lastFrame: LiveWorldFrame = { activities: [], proposals: [], clinical: [] }; + + constructor(private store: PersistentWorldStore, private clinicalRuntime?: ClinicalRuntimePort) {} + + snapshot() { return this.lastFrame; } + + step(realSeconds: number): LiveWorldFrame { + this.store.tick(realSeconds); + this.accumulator += realSeconds; + this.socialAccumulator += realSeconds; + if (this.accumulator < 5) return this.lastFrame; + this.accumulator = 0; + + let state = this.store.snapshot(); + const dailyEvents = defaultSchedules.flatMap((schedule) => runDailyLifeTick(state, schedule).events); + this.store.appendEvents(dailyEvents); + state = this.store.snapshot(); + + const activities = planPopulation(state); + const proposals = activities.map((activity) => proposeDynamics(state, activity)); + this.store.appendEvents(proposals.flatMap((proposal) => dynamicsEvents(state, proposal))); + + if (this.socialAccumulator >= 15) { + this.socialAccumulator = 0; + const person = state.entities.find((entity): entity is PersonEntity => entity.kind === 'person'); + const agent = defaultSocialAgents[Math.floor(state.simulationSeconds / 15) % defaultSocialAgents.length]; + if (person && agent) { + const interaction = interact(state, agent, person.id, person.communication.access === 'available' ? 'greet' : 'ask'); + if (interaction) this.store.appendEvents(socialEvents(state, interaction)); + } + } + + state = this.store.snapshot(); + const clinical = state.entities + .filter((entity): entity is PersonEntity => entity.kind === 'person') + .map((person) => assessClinicalTrigger(state, person.id, proposals)); + + if (this.clinicalRuntime) { + for (const assessment of clinical) { + const event = bridgeToClinicalRuntime(state, assessment, this.clinicalRuntime); + if (event) this.store.appendEvents([event]); + } + } + + this.lastFrame = { activities, proposals, clinical }; + return this.lastFrame; + } +} From 0781c26e9646c5dd7139a0e1c29dbef4b5ab1025 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 23:01:18 +1000 Subject: [PATCH 16/46] Add open world scenario studio model --- src/world/scenarioStudio.ts | 64 +++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/world/scenarioStudio.ts diff --git a/src/world/scenarioStudio.ts b/src/world/scenarioStudio.ts new file mode 100644 index 0000000..a6fcbde --- /dev/null +++ b/src/world/scenarioStudio.ts @@ -0,0 +1,64 @@ +import type { AccessModality, InfrastructureEntity, PersistentWorldState, Vec3 } from './worldModel'; + +export interface WorldStudioDraft { + title: string; + description: string; + seed: PersistentWorldState; +} + +export interface PlaceDraft { + id: string; + label: string; + position: Vec3; + tags?: string[]; +} + +export interface InfrastructureDraft { + id: string; + label: string; + position: Vec3; + feature: InfrastructureEntity['feature']; + accessibility: AccessModality[]; + operational?: boolean; +} + +export function addPlace(draft: WorldStudioDraft, place: PlaceDraft): WorldStudioDraft { + return { + ...draft, + seed: { + ...draft.seed, + entities: [...draft.seed.entities, { ...place, kind: 'place', persistent: true, tags: place.tags ?? [] }], + }, + }; +} + +export function addInfrastructure(draft: WorldStudioDraft, infrastructure: InfrastructureDraft): WorldStudioDraft { + const entity: InfrastructureEntity = { + ...infrastructure, + kind: 'infrastructure', + persistent: true, + tags: ['access'], + operational: infrastructure.operational ?? true, + }; + return { ...draft, seed: { ...draft.seed, entities: [...draft.seed.entities, entity] } }; +} + +export interface StudioValidationIssue { severity: 'error' | 'warning'; path: string; message: string; } + +export function validateWorldDraft(draft: WorldStudioDraft): StudioValidationIssue[] { + const issues: StudioValidationIssue[] = []; + const ids = new Set(); + for (const entity of draft.seed.entities) { + if (ids.has(entity.id)) issues.push({ severity: 'error', path: `entities.${entity.id}`, message: 'Entity IDs must be unique.' }); + ids.add(entity.id); + if (entity.kind === 'person' && entity.authority !== 'self') issues.push({ severity: 'error', path: `entities.${entity.id}.authority`, message: 'Open-world person authority cannot be silently reassigned by a scenario draft.' }); + if (entity.kind === 'infrastructure' && entity.accessibility.length === 0) issues.push({ severity: 'warning', path: `entities.${entity.id}.accessibility`, message: 'Infrastructure has no represented access modalities; verify this is intentional.' }); + } + return issues; +} + +export function exportWorldDraft(draft: WorldStudioDraft) { + const issues = validateWorldDraft(draft); + if (issues.some((issue) => issue.severity === 'error')) throw new Error('World draft contains validation errors.'); + return JSON.stringify(draft, null, 2); +} From 94ba8efb549fc01400cda4401f988465990534d2 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 23:01:37 +1000 Subject: [PATCH 17/46] Wire autonomous world systems into live runtime --- src/world/liveWorldRuntime.ts | 76 +++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/world/liveWorldRuntime.ts diff --git a/src/world/liveWorldRuntime.ts b/src/world/liveWorldRuntime.ts new file mode 100644 index 0000000..f575f19 --- /dev/null +++ b/src/world/liveWorldRuntime.ts @@ -0,0 +1,76 @@ +import { planPopulation, type PlannedActivity } from './autonomy'; +import { assessClinicalTrigger, bridgeToClinicalRuntime, type ClinicalRuntimePort } from './clinicalBridge'; +import { proposeDynamics, dynamicsEvents, type DynamicsProposal } from './vnnDynamics'; +import { defaultSocialAgents, interact, socialEvents } from './socialAgents'; +import { TimelineStore } from './timeline'; +import type { PersonEntity, PersistentWorldState, PersistentWorldStore, Vec3, WorldEvent } from './worldModel'; + +export interface LiveWorldFrame { + state: PersistentWorldState; + activities: PlannedActivity[]; + proposals: DynamicsProposal[]; + clinical: ReturnType[]; +} + +export class LiveWorldRuntime { + private accumulator = 0; + private timeline = new TimelineStore(); + private lastFrame?: LiveWorldFrame; + + constructor(private store: PersistentWorldStore, private clinicalRuntime?: ClinicalRuntimePort) {} + + step(deltaSeconds: number): LiveWorldFrame { + this.store.tick(deltaSeconds); + this.accumulator += deltaSeconds; + if (this.lastFrame && this.accumulator < 2) return { ...this.lastFrame, state: this.store.snapshot() }; + this.accumulator = 0; + + const state = this.store.snapshot(); + const activities = planPopulation(state); + const proposals = activities.map((activity) => proposeDynamics(state, activity)); + const generated: WorldEvent[] = proposals.flatMap((proposal) => dynamicsEvents(state, proposal)); + + for (const activity of activities) this.advancePerson(activity, state); + + for (const person of state.entities.filter((entity): entity is PersonEntity => entity.kind === 'person')) { + const peer = defaultSocialAgents[0]; + const social = interact(state, peer, person.id, 'greet'); + if (social && state.simulationSeconds % 30 < 2) generated.push(...socialEvents(state, social)); + } + + this.store.appendEvents(generated); + const next = this.store.snapshot(); + const clinical = next.entities + .filter((entity): entity is PersonEntity => entity.kind === 'person') + .map((person) => assessClinicalTrigger(next, person.id, proposals)); + + if (this.clinicalRuntime) { + for (const assessment of clinical) { + const event = bridgeToClinicalRuntime(next, assessment, this.clinicalRuntime); + if (event) this.store.appendEvents([event]); + } + } + + this.timeline.capture('live', this.store.snapshot(), `live world at ${Math.round(next.simulationSeconds)}s`); + this.lastFrame = { state: this.store.snapshot(), activities, proposals, clinical }; + return this.lastFrame; + } + + latest() { return this.lastFrame; } + timelineStore() { return this.timeline; } + + private advancePerson(activity: PlannedActivity, state: PersistentWorldState) { + if (!activity.targetId || (activity.kind !== 'travel' && activity.kind !== 'seek-alternative')) return; + const person = state.entities.find((entity): entity is PersonEntity => entity.kind === 'person' && entity.id === activity.personId); + const target = state.entities.find((entity) => entity.id === activity.targetId); + if (!person || !target) return; + const [px, py, pz] = person.position; + const [tx, , tz] = target.position; + const dx = tx - px; const dz = tz - pz; const distance = Math.hypot(dx, dz); + if (distance < 1.5) return; + const speed = person.mobility.mode === 'powered-wheelchair' ? 1.25 : 0.9; + const step = Math.min(speed * 2, distance); + const next: Vec3 = [px + (dx / distance) * step, py, pz + (dz / distance) * step]; + this.store.move(person.id, next); + } +} From 04de7b266a32bc9b3f305bff67d02e87973db84f Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 23:01:41 +1000 Subject: [PATCH 18/46] Wire live world dynamics into Babylon WebXR scene --- src/world/OpenWorldWeb.tsx | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/world/OpenWorldWeb.tsx b/src/world/OpenWorldWeb.tsx index 0baba0c..05ea3b6 100644 --- a/src/world/OpenWorldWeb.tsx +++ b/src/world/OpenWorldWeb.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import { InfrastructureEntity, PersonEntity, PersistentWorldStore, accessAssessment } from './worldModel'; +import { LiveWorldLoop } from './liveWorldLoop'; export function OpenWorldWeb() { const canvasRef = useRef(null); @@ -8,10 +9,13 @@ export function OpenWorldWeb() { const [status, setStatus] = useState('Loading world…'); const [xr, setXr] = useState('Checking WebXR…'); const [access, setAccess] = useState('ACCESS NETWORK ONLINE'); + const [life, setLife] = useState('DAILY LIFE ENGINE STARTING'); + const [clinical, setClinical] = useState('CLINICAL BRIDGE · STANDBY'); useEffect(() => { let disposed = false; let engine: any; let scene: any; const store = storeRef.current!; + const loop = new LiveWorldLoop(store); const boot = async () => { const B = await import('@babylonjs/core'); await import('@babylonjs/loaders'); if (!canvasRef.current || disposed) return; @@ -32,7 +36,7 @@ export function OpenWorldWeb() { if (entity.kind==='person') { const p=entity as PersonEntity; const body=B.MeshBuilder.CreateCapsule(entity.id,{height:1.35,radius:.3},scene); body.position.set(p.position[0],1.05,p.position[2]); body.material=personMat; meshes.set(entity.id,body); - if(p.mobility.mode.includes('wheelchair')) { const chair=B.MeshBuilder.CreateBox(entity.id+'-mobility',{width:.85,height:.55,depth:1.05},scene); chair.position.set(p.position[0],.38,p.position[2]); chair.material=personMat; } + if(p.mobility.mode.includes('wheelchair')) { const chair=B.MeshBuilder.CreateBox(entity.id+'-mobility',{width:.85,height:.55,depth:1.05},scene); chair.position.set(p.position[0],.38,p.position[2]); chair.material=personMat; meshes.set(entity.id+'-mobility',chair); } } if(entity.kind==='infrastructure') { const i=entity as InfrastructureEntity; const marker=B.MeshBuilder.CreateCylinder(entity.id,{height:1.5,diameter:.65},scene); marker.position.set(i.position[0],.75,i.position[2]); marker.material=i.operational?accessMat:unavailableMat; meshes.set(i.id,marker); @@ -40,20 +44,27 @@ export function OpenWorldWeb() { } for(let i=0;i<28;i++){const a=i*2.399,r=46+(i%4)*9,t=B.MeshBuilder.CreateCylinder('tree-'+i,{height:5,diameterTop:.5,diameterBottom:2.4,tessellation:7},scene);t.position.set(Math.cos(a)*r,2.5,Math.sin(a)*r);t.material=material('tree-'+i,.16,.38,.17);} - let last=performance.now(); - scene.onBeforeRenderObservable.add(()=>{const now=performance.now(); if(now-last>1000){store.tick((now-last)/1000);last=now;} const maya=store.entity('maya') as PersonEntity|undefined; const lift=store.entity('station-lift') as InfrastructureEntity|undefined; if(maya&&lift){const assessment=accessAssessment(maya,lift);setAccess(`TRANSIT ACCESS: ${assessment.pass?'AVAILABLE':'BARRIER'} · ${assessment.cause}`);}}); + let last=performance.now(); let uiLast=0; + scene.onBeforeRenderObservable.add(()=>{ + const now=performance.now(); const elapsed=Math.max(0,(now-last)/1000); last=now; + const frame=loop.step(elapsed); + const snapshot=store.snapshot(); + for(const entity of snapshot.entities){const mesh=meshes.get(entity.id);if(mesh)mesh.position.x=entity.position[0],mesh.position.z=entity.position[2];if(entity.kind==='infrastructure'&&mesh)mesh.material=(entity as InfrastructureEntity).operational?accessMat:unavailableMat;} + if(now-uiLast>1000){uiLast=now;const maya=store.entity('maya') as PersonEntity|undefined;const lift=store.entity('station-lift') as InfrastructureEntity|undefined;if(maya&&lift){const assessment=accessAssessment(maya,lift);setAccess(`TRANSIT ACCESS: ${assessment.pass?'AVAILABLE':'BARRIER'} · ${assessment.cause}`);}const activity=frame.activities.find((item)=>item.personId==='maya');setLife(activity?`MAYA · ${activity.kind.toUpperCase()} · ${activity.rationale}`:'MAYA · NO ACTIVE PLAN');const bridge=frame.clinical.find((item)=>item.personId==='maya');setClinical(`CLINICAL BRIDGE · ${(bridge?.status??'continue-world').toUpperCase().replaceAll('-',' ')}`);} + }); try { const helper=await scene.createDefaultXRExperienceAsync({floorMeshes:[ground,path],disableTeleportation:false}); setXr(helper.baseExperience?'VR READY · use headset Enter VR control':'VR unavailable'); } catch { setXr('WebXR not available on this browser/device'); } - setStatus('PERSISTENT WORLD ONLINE · WASD / mouse / WebXR'); engine.runRenderLoop(()=>scene.render()); const resize=()=>engine.resize();window.addEventListener('resize',resize);(scene as any).__cleanup=()=>window.removeEventListener('resize',resize); + setStatus('LIVE WORLD ONLINE · WASD / mouse / WebXR'); engine.runRenderLoop(()=>scene.render()); const resize=()=>engine.resize();window.addEventListener('resize',resize);(scene as any).__cleanup=()=>window.removeEventListener('resize',resize); }; boot(); return()=>{disposed=true;scene?.__cleanup?.();scene?.dispose?.();engine?.dispose?.();}; },[]); const toggleLift=()=>{const store=storeRef.current!;const lift=store.entity('station-lift') as InfrastructureEntity;store.setInfrastructure('station-lift',!lift.operational);}; - const toggleAAC=()=>{const store=storeRef.current!;const maya=store.entity('maya') as PersonEntity;store.setCommunicationAccess('maya',maya.communication.access==='available'?'degraded':'available');setStatus(`MAYA AAC ACCESS · ${store.entity('maya') && (store.entity('maya') as PersonEntity).communication.access.toUpperCase()}`);}; + const toggleAAC=()=>{const store=storeRef.current!;const maya=store.entity('maya') as PersonEntity;store.setCommunicationAccess('maya',maya.communication.access==='available'?'degraded':'available');setStatus(`MAYA AAC ACCESS · ${(store.entity('maya') as PersonEntity).communication.access.toUpperCase()}`);}; + const resetWorld=()=>{storeRef.current!.reset();setStatus('WORLD RESET · PERSON AUTHORITY AND BASELINES RESTORED');}; return
- -
MERT · PERSISTENT DISABILITY WORLD
{status}
{xr}
{access}
Clinical Centre · Community · Rehab · Accessible Transit
-
Accessibility is causal: infrastructure failure is recorded as an environmental barrier; AAC access changes observability, not Maya's authority or cognition. State persists locally between sessions.
+ +
MERT · LIVE DISABILITY WORLD
{status}
{xr}
{access}
{life}
{clinical}
Clinical Centre · Community · Rehab · Accessible Transit
+
The live loop runs bounded daily-life planning, affordance evaluation, VNN proposals, social interactions and the evidence-gated clinical bridge. Accessibility changes world causality; it does not rewrite Maya's cognition, authority or underlying capacity.
; } From 7fdd228f4444646173a17ed71e115489e4ac4416 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 23:01:48 +1000 Subject: [PATCH 19/46] Add persistent daily schedule system --- src/world/schedules.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/world/schedules.ts diff --git a/src/world/schedules.ts b/src/world/schedules.ts new file mode 100644 index 0000000..86b7881 --- /dev/null +++ b/src/world/schedules.ts @@ -0,0 +1,35 @@ +import type { PersistentWorldState } from './worldModel'; + +export interface DailyScheduleEntry { + id: string; + personId: string; + startMinute: number; + endMinute: number; + activity: 'home' | 'travel' | 'community' | 'work' | 'study' | 'appointment' | 'social' | 'rest' | 'charge'; + targetId?: string; + priority: number; + flexible: boolean; +} + +export const defaultDailySchedules: DailyScheduleEntry[] = [ + { id: 'maya-community', personId: 'maya', startMinute: 9 * 60, endMinute: 11 * 60, activity: 'community', targetId: 'community-hub', priority: 0.8, flexible: true }, + { id: 'maya-charge', personId: 'maya', startMinute: 11 * 60, endMinute: 11 * 60 + 30, activity: 'charge', targetId: 'aac-charge', priority: 0.9, flexible: true }, + { id: 'maya-clinical', personId: 'maya', startMinute: 14 * 60, endMinute: 15 * 60, activity: 'appointment', targetId: 'clinical-centre', priority: 0.7, flexible: false }, +]; + +export function worldMinute(state: PersistentWorldState, dayStartMinute = 8 * 60) { + return (dayStartMinute + Math.floor(state.simulationSeconds / 60)) % (24 * 60); +} + +export function currentSchedule(state: PersistentWorldState, personId: string, entries = defaultDailySchedules) { + const minute = worldMinute(state); + return entries + .filter((entry) => entry.personId === personId && minute >= entry.startMinute && minute < entry.endMinute) + .sort((a, b) => b.priority - a.priority)[0]; +} + +export function scheduleSummary(state: PersistentWorldState, personId: string, entries = defaultDailySchedules) { + const active = currentSchedule(state, personId, entries); + if (!active) return 'No fixed activity: personal goals and accessible opportunities remain available.'; + return `${active.activity.toUpperCase()} · ${active.targetId ?? 'self-directed'} · ${active.flexible ? 'flexible timing' : 'time-specific'}`; +} From ccd9a2ad54f3086504d89cedf22b76cda7ac9a20 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 23:01:52 +1000 Subject: [PATCH 20/46] Test live world integration and studio safeguards --- __tests__/liveWorldLoop.test.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 __tests__/liveWorldLoop.test.ts diff --git a/__tests__/liveWorldLoop.test.ts b/__tests__/liveWorldLoop.test.ts new file mode 100644 index 0000000..ea3880d --- /dev/null +++ b/__tests__/liveWorldLoop.test.ts @@ -0,0 +1,27 @@ +import { defaultSchedules, runDailyLifeTick } from '../src/world/dailyLife'; +import { LiveWorldLoop } from '../src/world/liveWorldLoop'; +import { addInfrastructure, exportWorldDraft, validateWorldDraft } from '../src/world/scenarioStudio'; +import { initialOpenWorldState, PersistentWorldStore } from '../src/world/worldModel'; + +describe('live disability world integration', () => { + it('plans daily life without replacing person authority', () => { + const tick = runDailyLifeTick(initialOpenWorldState, defaultSchedules[0]); + expect(tick.activity?.personId).toBe('maya'); + const maya = initialOpenWorldState.entities.find((entity) => entity.id === 'maya'); + expect(maya?.kind === 'person' && maya.authority).toBe('self'); + }); + + it('runs the bounded world loop and records dynamics events', () => { + const store = new PersistentWorldStore(initialOpenWorldState); + const loop = new LiveWorldLoop(store); + loop.step(6); + expect(store.snapshot().events.some((event) => event.type.startsWith('vnn.'))).toBe(true); + }); + + it('scenario studio validates duplicate IDs and authority invariants', () => { + const draft = { title: 'Test', description: '', seed: initialOpenWorldState }; + const withDuplicate = addInfrastructure(draft, { id: 'station-lift', label: 'Duplicate lift', position: [0,0,0], feature: 'lift', accessibility: ['powered-wheelchair'] }); + expect(validateWorldDraft(withDuplicate).some((issue) => issue.severity === 'error')).toBe(true); + expect(() => exportWorldDraft(withDuplicate)).toThrow(); + }); +}); From 3b79344b6100a9f098543ffca5c445024fea8543 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 23:02:17 +1000 Subject: [PATCH 21/46] Fix live runtime timeline integration --- src/world/liveWorldRuntime.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/world/liveWorldRuntime.ts b/src/world/liveWorldRuntime.ts index f575f19..881db45 100644 --- a/src/world/liveWorldRuntime.ts +++ b/src/world/liveWorldRuntime.ts @@ -2,7 +2,7 @@ import { planPopulation, type PlannedActivity } from './autonomy'; import { assessClinicalTrigger, bridgeToClinicalRuntime, type ClinicalRuntimePort } from './clinicalBridge'; import { proposeDynamics, dynamicsEvents, type DynamicsProposal } from './vnnDynamics'; import { defaultSocialAgents, interact, socialEvents } from './socialAgents'; -import { TimelineStore } from './timeline'; +import { WorldTimeline } from './timeline'; import type { PersonEntity, PersistentWorldState, PersistentWorldStore, Vec3, WorldEvent } from './worldModel'; export interface LiveWorldFrame { @@ -14,7 +14,7 @@ export interface LiveWorldFrame { export class LiveWorldRuntime { private accumulator = 0; - private timeline = new TimelineStore(); + private timeline = new WorldTimeline(); private lastFrame?: LiveWorldFrame; constructor(private store: PersistentWorldStore, private clinicalRuntime?: ClinicalRuntimePort) {} @@ -51,7 +51,7 @@ export class LiveWorldRuntime { } } - this.timeline.capture('live', this.store.snapshot(), `live world at ${Math.round(next.simulationSeconds)}s`); + this.timeline.capture(this.store.snapshot(), `live world at ${Math.round(next.simulationSeconds)}s`); this.lastFrame = { state: this.store.snapshot(), activities, proposals, clinical }; return this.lastFrame; } From a49683a83489e49255459601ac65e152b93df607 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 23:02:44 +1000 Subject: [PATCH 22/46] Advance autonomous people through live world --- src/world/liveWorldLoop.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/world/liveWorldLoop.ts b/src/world/liveWorldLoop.ts index a50bdb6..a10e1ee 100644 --- a/src/world/liveWorldLoop.ts +++ b/src/world/liveWorldLoop.ts @@ -3,7 +3,7 @@ import { assessClinicalTrigger, bridgeToClinicalRuntime, type ClinicalRuntimePor import { defaultSchedules, runDailyLifeTick } from './dailyLife'; import { defaultSocialAgents, interact, socialEvents } from './socialAgents'; import { dynamicsEvents, proposeDynamics, type DynamicsProposal } from './vnnDynamics'; -import type { PersonEntity, PersistentWorldState, PersistentWorldStore } from './worldModel'; +import type { PersonEntity, PersistentWorldState, PersistentWorldStore, Vec3 } from './worldModel'; export interface LiveWorldFrame { activities: ReturnType; @@ -24,7 +24,8 @@ export class LiveWorldLoop { this.store.tick(realSeconds); this.accumulator += realSeconds; this.socialAccumulator += realSeconds; - if (this.accumulator < 5) return this.lastFrame; + if (this.accumulator < 2) return this.lastFrame; + const stepSeconds = this.accumulator; this.accumulator = 0; let state = this.store.snapshot(); @@ -35,6 +36,7 @@ export class LiveWorldLoop { const activities = planPopulation(state); const proposals = activities.map((activity) => proposeDynamics(state, activity)); this.store.appendEvents(proposals.flatMap((proposal) => dynamicsEvents(state, proposal))); + for (const activity of activities) this.advanceActivity(state, activity, stepSeconds); if (this.socialAccumulator >= 15) { this.socialAccumulator = 0; @@ -61,4 +63,18 @@ export class LiveWorldLoop { this.lastFrame = { activities, proposals, clinical }; return this.lastFrame; } + + private advanceActivity(state: PersistentWorldState, activity: ReturnType[number], seconds: number) { + if (!activity.targetId || (activity.kind !== 'travel' && activity.kind !== 'seek-alternative')) return; + const person = state.entities.find((entity): entity is PersonEntity => entity.kind === 'person' && entity.id === activity.personId); + const target = state.entities.find((entity) => entity.id === activity.targetId); + if (!person || !target) return; + const [px, py, pz] = person.position; const [tx, , tz] = target.position; + const dx = tx - px; const dz = tz - pz; const distance = Math.hypot(dx, dz); + if (distance < 1.25) return; + const metresPerSecond = person.mobility.mode === 'powered-wheelchair' ? 1.25 : 0.9; + const amount = Math.min(distance, metresPerSecond * seconds); + const next: Vec3 = [px + dx / distance * amount, py, pz + dz / distance * amount]; + this.store.move(person.id, next); + } } From c7729d9e569d3c20476d272315f31fbb2a723b4e Mon Sep 17 00:00:00 2001 From: ausdisau Date: Sun, 16 Aug 2026 23:02:53 +1000 Subject: [PATCH 23/46] Test live open-world integration --- __tests__/liveWorldIntegration.test.ts | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 __tests__/liveWorldIntegration.test.ts diff --git a/__tests__/liveWorldIntegration.test.ts b/__tests__/liveWorldIntegration.test.ts new file mode 100644 index 0000000..aed49a7 --- /dev/null +++ b/__tests__/liveWorldIntegration.test.ts @@ -0,0 +1,35 @@ +import { LiveWorldLoop } from '../src/world/liveWorldLoop'; +import { PersistentWorldStore, initialOpenWorldState, type PersonEntity } from '../src/world/worldModel'; +import { addInfrastructure, validateWorldDraft, type WorldStudioDraft } from '../src/world/scenarioStudio'; + +function memoryStore() { + return new PersistentWorldStore(initialOpenWorldState); +} + +describe('live disability world integration', () => { + test('autonomous travel changes position without changing person authority', () => { + const store = memoryStore(); + const before = store.entity('maya') as PersonEntity; + const start = [...before.position]; + const loop = new LiveWorldLoop(store); + loop.step(3); + const after = store.entity('maya') as PersonEntity; + expect(after.position).not.toEqual(start); + expect(after.authority).toBe('self'); + }); + + test('infrastructure failure remains an environmental event', () => { + const store = memoryStore(); + store.setInfrastructure('station-lift', false); + const event = store.snapshot().events.at(-1); + expect(event?.type).toBe('infrastructure.changed'); + expect(event?.detail).toContain('unavailable'); + expect((store.entity('maya') as PersonEntity).authority).toBe('self'); + }); + + test('Scenario Studio rejects silent authority reassignment', () => { + const draft: WorldStudioDraft = { title: 'Test', description: 'Test world', seed: initialOpenWorldState }; + const extended = addInfrastructure(draft, { id: 'test-ramp', label: 'Test ramp', position: [0,0,0], feature: 'ramp', accessibility: ['powered-wheelchair'] }); + expect(validateWorldDraft(extended).filter(issue => issue.severity === 'error')).toHaveLength(0); + }); +}); From 8c56096f4aa14566498f7d7ee15d13c089de24a1 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:41:26 +1000 Subject: [PATCH 24/46] Add entity upsert for multi-person world runtime --- src/world/worldModel.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/world/worldModel.ts b/src/world/worldModel.ts index 8b6708b..30086c1 100644 --- a/src/world/worldModel.ts +++ b/src/world/worldModel.ts @@ -88,6 +88,13 @@ export class PersistentWorldStore { this.persist(); } + upsertEntity(entity: OpenWorldEntity) { + const index = this.state.entities.findIndex((item) => item.id === entity.id); + if (index >= 0) this.state.entities[index] = JSON.parse(JSON.stringify(entity)) as OpenWorldEntity; + else this.state.entities.push(JSON.parse(JSON.stringify(entity)) as OpenWorldEntity); + this.persist(); + } + setInfrastructure(id: string, operational: boolean) { const entity = this.entity(id); if (!entity || entity.kind !== 'infrastructure') return; @@ -120,9 +127,7 @@ export class PersistentWorldStore { this.persist(); } - private record(type: string, entityId: string, detail: string) { - this.recordEvent(type, entityId, detail); - } + private record(type: string, entityId: string, detail: string) { this.recordEvent(type, entityId, detail); } private persist() { if (typeof window !== 'undefined') window.localStorage?.setItem('mert-open-world-v1', JSON.stringify(this.state)); From 339a6d2daf61a68d86d720706fe70ff4bed5e402 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:41:39 +1000 Subject: [PATCH 25/46] Add multi-person population and relationships --- src/world/population.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/world/population.ts diff --git a/src/world/population.ts b/src/world/population.ts new file mode 100644 index 0000000..a9b2cbe --- /dev/null +++ b/src/world/population.ts @@ -0,0 +1,27 @@ +import type { PersonEntity, PersistentWorldStore } from './worldModel'; +import type { PersonSchedule } from './dailyLife'; + +export type RelationshipKind = 'friend' | 'peer' | 'family' | 'support' | 'clinician' | 'colleague'; +export interface Relationship { from: string; to: string; kind: RelationshipKind; trust: number; } + +export const population: PersonEntity[] = [ + { id:'liam', kind:'person', label:'Liam', position:[-8,0,4], persistent:true, tags:['resident','peer'], authority:'self', communication:{primary:'speech',access:'available',responseLatencySeconds:2}, mobility:{mode:'manual-wheelchair',independent:true}, goals:['meet friends','use community services','travel independently'] }, + { id:'rohan', kind:'person', label:'Rohan', position:[18,0,-8], persistent:true, tags:['resident','student'], authority:'self', communication:{primary:'multimodal',access:'available',responseLatencySeconds:5}, mobility:{mode:'walk',independent:true}, goals:['attend rehabilitation','study','meet friends'] }, + { id:'aisha', kind:'person', label:'Aisha', position:[12,0,26], persistent:true, tags:['resident','worker'], authority:'self', communication:{primary:'sign',access:'available',responseLatencySeconds:4}, mobility:{mode:'walk',independent:true}, goals:['work','socialise','travel through community'] } +]; + +export const relationships: Relationship[] = [ + { from:'maya', to:'liam', kind:'friend', trust:.9 }, { from:'liam', to:'maya', kind:'friend', trust:.9 }, + { from:'maya', to:'aisha', kind:'peer', trust:.75 }, { from:'rohan', to:'liam', kind:'peer', trust:.7 } +]; + +export const populationSchedules: PersonSchedule[] = [ + { personId:'liam', entries:[{atHour:8,goal:'travel independently',destinationId:'transit'},{atHour:11,goal:'meet friends',destinationId:'community-hub'},{atHour:16,goal:'use community services',destinationId:'community-hub'}] }, + { personId:'rohan', entries:[{atHour:8,goal:'study',destinationId:'community-hub'},{atHour:13,goal:'attend rehabilitation',destinationId:'rehab-lab'},{atHour:17,goal:'meet friends',destinationId:'community-hub'}] }, + { personId:'aisha', entries:[{atHour:8,goal:'work',destinationId:'community-hub'},{atHour:12,goal:'travel through community',destinationId:'transit'},{atHour:17,goal:'socialise',destinationId:'community-hub'}] } +]; + +export function ensurePopulation(store: PersistentWorldStore) { + for (const person of population) if (!store.entity(person.id)) store.upsertEntity(person); + store.recordEvent('population.ready', undefined, `persistent population=${population.length + 1}; relationships=${relationships.length}`); +} From a5a2d436b314fca07f150a9aa752c04690efdb72 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:41:49 +1000 Subject: [PATCH 26/46] Add semantic spatial interaction and wayfinding --- src/world/spatialInteraction.ts | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/world/spatialInteraction.ts diff --git a/src/world/spatialInteraction.ts b/src/world/spatialInteraction.ts new file mode 100644 index 0000000..961ff0e --- /dev/null +++ b/src/world/spatialInteraction.ts @@ -0,0 +1,38 @@ +import { evaluateAffordance } from './affordanceGraph'; +import { normalizeIntent, type InteractionModality, type WorldIntent } from './vrInteraction'; +import type { OpenWorldEntity, PersonEntity, PersistentWorldState, Vec3 } from './worldModel'; + +export interface NearbyEntity { entity: OpenWorldEntity; distance: number; } +const distance=(a:Vec3,b:Vec3)=>Math.hypot(a[0]-b[0],a[2]-b[2]); + +export function nearbyEntities(state: PersistentWorldState, actorId: string, radius=4): NearbyEntity[] { + const actor=state.entities.find((e):e is PersonEntity=>e.id===actorId&&e.kind==='person'); + if(!actor)return[]; + return state.entities.filter(e=>e.id!==actorId).map(entity=>({entity,distance:distance(actor.position,entity.position)})).filter(x=>x.distance<=radius).sort((a,b)=>a.distance-b.distance); +} + +export function createSpatialIntent(actorId:string,targetId:string,action:WorldIntent['action'],modality:InteractionModality):WorldIntent { + return normalizeIntent({actorId,targetId,action,modality}); +} + +export function evaluateSpatialIntent(state:PersistentWorldState,intent:WorldIntent){ + const actor=state.entities.find((e):e is PersonEntity=>e.id===intent.actorId&&e.kind==='person'); + const target=state.entities.find(e=>e.id===intent.targetId); + if(!actor||!target)return{allowed:false,reason:'actor or target not represented'}; + if(intent.action==='communicate'){ + if(target.kind!=='person')return{allowed:false,reason:'communication target is not a person'}; + if(actor.communication.access==='unavailable')return{allowed:false,reason:'communication access unavailable; intent is preserved and may be retried'}; + return{allowed:true,reason:'direct communication available'}; + } + if(intent.action==='inspect'||intent.action==='select')return{allowed:true,reason:'semantic target available'}; + const affordance=evaluateAffordance(actor,target,intent.action==='use'?'use':'reach'); + return{allowed:affordance.available,reason:affordance.reason,affordance}; +} + +export function wayfind(state:PersistentWorldState,actorId:string,targetId:string){ + const actor=state.entities.find((e):e is PersonEntity=>e.id===actorId&&e.kind==='person'); + const target=state.entities.find(e=>e.id===targetId); + if(!actor||!target)return undefined; + const direct=evaluateAffordance(actor,target,'reach'); + return{actorId,targetId,destination:target.position,distance:distance(actor.position,target.position),accessible:direct.available,reason:direct.reason}; +} From 7718b2e26b51453aa94c335c596b6dc0fcbc5946 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:42:10 +1000 Subject: [PATCH 27/46] Consolidate authoritative world runtime with population schedules --- src/world/liveWorldLoop.ts | 91 +++++++++----------------------------- 1 file changed, 20 insertions(+), 71 deletions(-) diff --git a/src/world/liveWorldLoop.ts b/src/world/liveWorldLoop.ts index a10e1ee..cb16fda 100644 --- a/src/world/liveWorldLoop.ts +++ b/src/world/liveWorldLoop.ts @@ -1,80 +1,29 @@ import { planPopulation } from './autonomy'; import { assessClinicalTrigger, bridgeToClinicalRuntime, type ClinicalRuntimePort } from './clinicalBridge'; import { defaultSchedules, runDailyLifeTick } from './dailyLife'; +import { ensurePopulation, populationSchedules } from './population'; import { defaultSocialAgents, interact, socialEvents } from './socialAgents'; import { dynamicsEvents, proposeDynamics, type DynamicsProposal } from './vnnDynamics'; import type { PersonEntity, PersistentWorldState, PersistentWorldStore, Vec3 } from './worldModel'; -export interface LiveWorldFrame { - activities: ReturnType; - proposals: DynamicsProposal[]; - clinical: ReturnType[]; -} - -export class LiveWorldLoop { - private accumulator = 0; - private socialAccumulator = 0; - private lastFrame: LiveWorldFrame = { activities: [], proposals: [], clinical: [] }; - - constructor(private store: PersistentWorldStore, private clinicalRuntime?: ClinicalRuntimePort) {} - - snapshot() { return this.lastFrame; } - - step(realSeconds: number): LiveWorldFrame { - this.store.tick(realSeconds); - this.accumulator += realSeconds; - this.socialAccumulator += realSeconds; - if (this.accumulator < 2) return this.lastFrame; - const stepSeconds = this.accumulator; - this.accumulator = 0; - - let state = this.store.snapshot(); - const dailyEvents = defaultSchedules.flatMap((schedule) => runDailyLifeTick(state, schedule).events); - this.store.appendEvents(dailyEvents); - state = this.store.snapshot(); - - const activities = planPopulation(state); - const proposals = activities.map((activity) => proposeDynamics(state, activity)); - this.store.appendEvents(proposals.flatMap((proposal) => dynamicsEvents(state, proposal))); - for (const activity of activities) this.advanceActivity(state, activity, stepSeconds); - - if (this.socialAccumulator >= 15) { - this.socialAccumulator = 0; - const person = state.entities.find((entity): entity is PersonEntity => entity.kind === 'person'); - const agent = defaultSocialAgents[Math.floor(state.simulationSeconds / 15) % defaultSocialAgents.length]; - if (person && agent) { - const interaction = interact(state, agent, person.id, person.communication.access === 'available' ? 'greet' : 'ask'); - if (interaction) this.store.appendEvents(socialEvents(state, interaction)); - } - } - - state = this.store.snapshot(); - const clinical = state.entities - .filter((entity): entity is PersonEntity => entity.kind === 'person') - .map((person) => assessClinicalTrigger(state, person.id, proposals)); - - if (this.clinicalRuntime) { - for (const assessment of clinical) { - const event = bridgeToClinicalRuntime(state, assessment, this.clinicalRuntime); - if (event) this.store.appendEvents([event]); - } - } - - this.lastFrame = { activities, proposals, clinical }; - return this.lastFrame; - } - - private advanceActivity(state: PersistentWorldState, activity: ReturnType[number], seconds: number) { - if (!activity.targetId || (activity.kind !== 'travel' && activity.kind !== 'seek-alternative')) return; - const person = state.entities.find((entity): entity is PersonEntity => entity.kind === 'person' && entity.id === activity.personId); - const target = state.entities.find((entity) => entity.id === activity.targetId); - if (!person || !target) return; - const [px, py, pz] = person.position; const [tx, , tz] = target.position; - const dx = tx - px; const dz = tz - pz; const distance = Math.hypot(dx, dz); - if (distance < 1.25) return; - const metresPerSecond = person.mobility.mode === 'powered-wheelchair' ? 1.25 : 0.9; - const amount = Math.min(distance, metresPerSecond * seconds); - const next: Vec3 = [px + dx / distance * amount, py, pz + dz / distance * amount]; - this.store.move(person.id, next); +export interface LiveWorldFrame { activities: ReturnType; proposals: DynamicsProposal[]; clinical: ReturnType[]; } + +export class WorldRuntime { + private accumulator=0; private socialAccumulator=0; + private lastFrame:LiveWorldFrame={activities:[],proposals:[],clinical:[]}; + constructor(private store:PersistentWorldStore,private clinicalRuntime?:ClinicalRuntimePort){ensurePopulation(store);} + snapshot(){return this.lastFrame;} + step(realSeconds:number):LiveWorldFrame{ + this.store.tick(realSeconds);this.accumulator+=realSeconds;this.socialAccumulator+=realSeconds;if(this.accumulator<2)return this.lastFrame; + const stepSeconds=this.accumulator;this.accumulator=0;let state=this.store.snapshot();const schedules=[...defaultSchedules,...populationSchedules]; + this.store.appendEvents(schedules.flatMap(schedule=>runDailyLifeTick(state,schedule).events));state=this.store.snapshot(); + const activities=planPopulation(state);const proposals=activities.map(activity=>proposeDynamics(state,activity));this.store.appendEvents(proposals.flatMap(p=>dynamicsEvents(state,p)));for(const activity of activities)this.advanceActivity(state,activity,stepSeconds); + if(this.socialAccumulator>=15){this.socialAccumulator=0;const people=state.entities.filter((e):e is PersonEntity=>e.kind==='person');for(let i=0;ie.kind==='person').map(p=>assessClinicalTrigger(state,p.id,proposals));if(this.clinicalRuntime)for(const assessment of clinical){const event=bridgeToClinicalRuntime(state,assessment,this.clinicalRuntime);if(event)this.store.appendEvents([event]);} + this.lastFrame={activities,proposals,clinical};return this.lastFrame; } + private advanceActivity(state:PersistentWorldState,activity:ReturnType[number],seconds:number){if(!activity.targetId||(activity.kind!=='travel'&&activity.kind!=='seek-alternative'))return;const person=state.entities.find((e):e is PersonEntity=>e.kind==='person'&&e.id===activity.personId),target=state.entities.find(e=>e.id===activity.targetId);if(!person||!target)return;const[px,py,pz]=person.position,[tx,,tz]=target.position,dx=tx-px,dz=tz-pz,d=Math.hypot(dx,dz);if(d<1.25)return;const speed=person.mobility.mode==='powered-wheelchair'?1.25:person.mobility.mode==='manual-wheelchair'?1.0:1.15,amount=Math.min(d,speed*seconds);this.store.move(person.id,[px+dx/d*amount,py,pz+dz/d*amount] as Vec3);} } + +/** @deprecated Use WorldRuntime. */ +export class LiveWorldLoop extends WorldRuntime {} From dcc188009c3d9e7bbca0257c871fbc60f41eb681 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:42:29 +1000 Subject: [PATCH 28/46] Wire consolidated runtime and semantic nearby interactions into WebXR world --- src/world/OpenWorldWeb.tsx | 74 +++++--------------------------------- 1 file changed, 9 insertions(+), 65 deletions(-) diff --git a/src/world/OpenWorldWeb.tsx b/src/world/OpenWorldWeb.tsx index 05ea3b6..2a9c7b8 100644 --- a/src/world/OpenWorldWeb.tsx +++ b/src/world/OpenWorldWeb.tsx @@ -1,70 +1,14 @@ import React, { useEffect, useRef, useState } from 'react'; import { InfrastructureEntity, PersonEntity, PersistentWorldStore, accessAssessment } from './worldModel'; -import { LiveWorldLoop } from './liveWorldLoop'; +import { WorldRuntime } from './liveWorldLoop'; +import { nearbyEntities, createSpatialIntent, evaluateSpatialIntent } from './spatialInteraction'; export function OpenWorldWeb() { - const canvasRef = useRef(null); - const storeRef = useRef(); - if (!storeRef.current) storeRef.current = new PersistentWorldStore(); - const [status, setStatus] = useState('Loading world…'); - const [xr, setXr] = useState('Checking WebXR…'); - const [access, setAccess] = useState('ACCESS NETWORK ONLINE'); - const [life, setLife] = useState('DAILY LIFE ENGINE STARTING'); - const [clinical, setClinical] = useState('CLINICAL BRIDGE · STANDBY'); - - useEffect(() => { - let disposed = false; let engine: any; let scene: any; - const store = storeRef.current!; - const loop = new LiveWorldLoop(store); - const boot = async () => { - const B = await import('@babylonjs/core'); await import('@babylonjs/loaders'); - if (!canvasRef.current || disposed) return; - engine = new B.Engine(canvasRef.current, true, { preserveDrawingBuffer: true, stencil: true }); - scene = new B.Scene(engine); scene.clearColor = new B.Color4(0.72, 0.86, 0.96, 1); scene.collisionsEnabled = true; - const camera = new B.UniversalCamera('player', new B.Vector3(0, 1.65, -16), scene); camera.attachControl(canvasRef.current, true); camera.speed=.45; camera.angularSensibility=3500; camera.applyGravity=true; camera.checkCollisions=true; camera.ellipsoid=new B.Vector3(.5,.9,.5); camera.keysUp.push(87); camera.keysDown.push(83); camera.keysLeft.push(65); camera.keysRight.push(68); - new B.HemisphericLight('sky', new B.Vector3(0,1,0), scene).intensity=.85; new B.DirectionalLight('sun', new B.Vector3(-.4,-1,.25), scene).intensity=.7; - const material=(name:string,r:number,g:number,b:number)=>{const m=new B.StandardMaterial(name,scene);m.diffuseColor=new B.Color3(r,g,b);return m;}; - const ground=B.MeshBuilder.CreateGround('accessible-campus',{width:220,height:220},scene); ground.checkCollisions=true; ground.material=material('ground',.38,.55,.35); - const path=B.MeshBuilder.CreateGround('wide-accessible-path',{width:12,height:190},scene); path.position.y=.015; path.material=material('path',.62,.61,.57); - const buildingMat=material('building',.72,.76,.78); const glass=material('glass',.25,.55,.7); glass.alpha=.72; - const makeBuilding=(name:string,x:number,z:number,w:number,d:number,h:number)=>{const body=B.MeshBuilder.CreateBox(name,{width:w,depth:d,height:h},scene);body.position.set(x,h/2,z);body.material=buildingMat;body.checkCollisions=true;const door=B.MeshBuilder.CreateBox(name+'-automatic-entry',{width:3.2,depth:.12,height:2.7},scene);door.position.set(x,1.35,z-d/2-.07);door.material=glass;return body;}; - makeBuilding('Clinical Simulation Centre',-24,8,28,20,10); makeBuilding('Community Hub',25,22,26,18,7); makeBuilding('Rehabilitation Lab',24,-28,24,18,8); makeBuilding('Accessible Transit Station',-25,-34,30,15,6); - - const personMat=material('people',.22,.32,.55); const accessMat=material('access',.16,.48,.5); const unavailableMat=material('unavailable',.48,.2,.2); - const meshes=new Map(); - for (const entity of store.snapshot().entities) { - if (entity.kind==='person') { - const p=entity as PersonEntity; - const body=B.MeshBuilder.CreateCapsule(entity.id,{height:1.35,radius:.3},scene); body.position.set(p.position[0],1.05,p.position[2]); body.material=personMat; meshes.set(entity.id,body); - if(p.mobility.mode.includes('wheelchair')) { const chair=B.MeshBuilder.CreateBox(entity.id+'-mobility',{width:.85,height:.55,depth:1.05},scene); chair.position.set(p.position[0],.38,p.position[2]); chair.material=personMat; meshes.set(entity.id+'-mobility',chair); } - } - if(entity.kind==='infrastructure') { - const i=entity as InfrastructureEntity; const marker=B.MeshBuilder.CreateCylinder(entity.id,{height:1.5,diameter:.65},scene); marker.position.set(i.position[0],.75,i.position[2]); marker.material=i.operational?accessMat:unavailableMat; meshes.set(i.id,marker); - } - } - for(let i=0;i<28;i++){const a=i*2.399,r=46+(i%4)*9,t=B.MeshBuilder.CreateCylinder('tree-'+i,{height:5,diameterTop:.5,diameterBottom:2.4,tessellation:7},scene);t.position.set(Math.cos(a)*r,2.5,Math.sin(a)*r);t.material=material('tree-'+i,.16,.38,.17);} - - let last=performance.now(); let uiLast=0; - scene.onBeforeRenderObservable.add(()=>{ - const now=performance.now(); const elapsed=Math.max(0,(now-last)/1000); last=now; - const frame=loop.step(elapsed); - const snapshot=store.snapshot(); - for(const entity of snapshot.entities){const mesh=meshes.get(entity.id);if(mesh)mesh.position.x=entity.position[0],mesh.position.z=entity.position[2];if(entity.kind==='infrastructure'&&mesh)mesh.material=(entity as InfrastructureEntity).operational?accessMat:unavailableMat;} - if(now-uiLast>1000){uiLast=now;const maya=store.entity('maya') as PersonEntity|undefined;const lift=store.entity('station-lift') as InfrastructureEntity|undefined;if(maya&&lift){const assessment=accessAssessment(maya,lift);setAccess(`TRANSIT ACCESS: ${assessment.pass?'AVAILABLE':'BARRIER'} · ${assessment.cause}`);}const activity=frame.activities.find((item)=>item.personId==='maya');setLife(activity?`MAYA · ${activity.kind.toUpperCase()} · ${activity.rationale}`:'MAYA · NO ACTIVE PLAN');const bridge=frame.clinical.find((item)=>item.personId==='maya');setClinical(`CLINICAL BRIDGE · ${(bridge?.status??'continue-world').toUpperCase().replaceAll('-',' ')}`);} - }); - try { const helper=await scene.createDefaultXRExperienceAsync({floorMeshes:[ground,path],disableTeleportation:false}); setXr(helper.baseExperience?'VR READY · use headset Enter VR control':'VR unavailable'); } catch { setXr('WebXR not available on this browser/device'); } - setStatus('LIVE WORLD ONLINE · WASD / mouse / WebXR'); engine.runRenderLoop(()=>scene.render()); const resize=()=>engine.resize();window.addEventListener('resize',resize);(scene as any).__cleanup=()=>window.removeEventListener('resize',resize); - }; - boot(); return()=>{disposed=true;scene?.__cleanup?.();scene?.dispose?.();engine?.dispose?.();}; - },[]); - - const toggleLift=()=>{const store=storeRef.current!;const lift=store.entity('station-lift') as InfrastructureEntity;store.setInfrastructure('station-lift',!lift.operational);}; - const toggleAAC=()=>{const store=storeRef.current!;const maya=store.entity('maya') as PersonEntity;store.setCommunicationAccess('maya',maya.communication.access==='available'?'degraded':'available');setStatus(`MAYA AAC ACCESS · ${(store.entity('maya') as PersonEntity).communication.access.toUpperCase()}`);}; - const resetWorld=()=>{storeRef.current!.reset();setStatus('WORLD RESET · PERSON AUTHORITY AND BASELINES RESTORED');}; - - return
- -
MERT · LIVE DISABILITY WORLD
{status}
{xr}
{access}
{life}
{clinical}
Clinical Centre · Community · Rehab · Accessible Transit
-
The live loop runs bounded daily-life planning, affordance evaluation, VNN proposals, social interactions and the evidence-gated clinical bridge. Accessibility changes world causality; it does not rewrite Maya's cognition, authority or underlying capacity.
-
; + const canvasRef=useRef(null);const storeRef=useRef();if(!storeRef.current)storeRef.current=new PersistentWorldStore(); + const [status,setStatus]=useState('Loading world…'),[xr,setXr]=useState('Checking WebXR…'),[access,setAccess]=useState('ACCESS NETWORK ONLINE'),[life,setLife]=useState('DAILY LIFE ENGINE STARTING'),[clinical,setClinical]=useState('CLINICAL BRIDGE · STANDBY'),[nearby,setNearby]=useState('NEARBY · scanning'); + useEffect(()=>{let disposed=false,engine:any,scene:any;const store=storeRef.current!,loop=new WorldRuntime(store);const boot=async()=>{const B=await import('@babylonjs/core');await import('@babylonjs/loaders');if(!canvasRef.current||disposed)return;engine=new B.Engine(canvasRef.current,true,{preserveDrawingBuffer:true,stencil:true});scene=new B.Scene(engine);scene.clearColor=new B.Color4(.72,.86,.96,1);scene.collisionsEnabled=true;const camera=new B.UniversalCamera('player',new B.Vector3(0,1.65,-16),scene);camera.attachControl(canvasRef.current,true);camera.speed=.45;camera.angularSensibility=3500;camera.applyGravity=true;camera.checkCollisions=true;camera.ellipsoid=new B.Vector3(.5,.9,.5);camera.keysUp.push(87);camera.keysDown.push(83);camera.keysLeft.push(65);camera.keysRight.push(68);new B.HemisphericLight('sky',new B.Vector3(0,1,0),scene).intensity=.85;new B.DirectionalLight('sun',new B.Vector3(-.4,-1,.25),scene).intensity=.7;const material=(n:string,r:number,g:number,b:number)=>{const m=new B.StandardMaterial(n,scene);m.diffuseColor=new B.Color3(r,g,b);return m;};const ground=B.MeshBuilder.CreateGround('accessible-campus',{width:220,height:220},scene);ground.checkCollisions=true;ground.material=material('ground',.38,.55,.35);const path=B.MeshBuilder.CreateGround('wide-accessible-path',{width:12,height:190},scene);path.position.y=.015;path.material=material('path',.62,.61,.57);const buildingMat=material('building',.72,.76,.78),glass=material('glass',.25,.55,.7);glass.alpha=.72;const makeBuilding=(n:string,x:number,z:number,w:number,d:number,h:number)=>{const body=B.MeshBuilder.CreateBox(n,{width:w,depth:d,height:h},scene);body.position.set(x,h/2,z);body.material=buildingMat;body.checkCollisions=true;const door=B.MeshBuilder.CreateBox(n+'-automatic-entry',{width:3.2,depth:.12,height:2.7},scene);door.position.set(x,1.35,z-d/2-.07);door.material=glass;};makeBuilding('Clinical Simulation Centre',-24,8,28,20,10);makeBuilding('Community Hub',25,22,26,18,7);makeBuilding('Rehabilitation Lab',24,-28,24,18,8);makeBuilding('Accessible Transit Station',-25,-34,30,15,6); + const personMat=material('people',.22,.32,.55),accessMat=material('access',.16,.48,.5),unavailableMat=material('unavailable',.48,.2,.2),meshes=new Map();const createEntityMesh=(entity:any)=>{if(meshes.has(entity.id))return;if(entity.kind==='person'){const p=entity as PersonEntity,body=B.MeshBuilder.CreateCapsule(entity.id,{height:1.35,radius:.3},scene);body.position.set(p.position[0],1.05,p.position[2]);body.material=personMat;meshes.set(entity.id,body);if(p.mobility.mode.includes('wheelchair')){const chair=B.MeshBuilder.CreateBox(entity.id+'-mobility',{width:.85,height:.55,depth:1.05},scene);chair.position.set(p.position[0],.38,p.position[2]);chair.material=personMat;meshes.set(entity.id+'-mobility',chair);}}else if(entity.kind==='infrastructure'){const i=entity as InfrastructureEntity,marker=B.MeshBuilder.CreateCylinder(entity.id,{height:1.5,diameter:.65},scene);marker.position.set(i.position[0],.75,i.position[2]);marker.material=i.operational?accessMat:unavailableMat;meshes.set(i.id,marker);}};for(const entity of store.snapshot().entities)createEntityMesh(entity);for(let i=0;i<28;i++){const a=i*2.399,r=46+(i%4)*9,t=B.MeshBuilder.CreateCylinder('tree-'+i,{height:5,diameterTop:.5,diameterBottom:2.4,tessellation:7},scene);t.position.set(Math.cos(a)*r,2.5,Math.sin(a)*r);t.material=material('tree-'+i,.16,.38,.17);} + let last=performance.now(),uiLast=0;scene.onBeforeRenderObservable.add(()=>{const now=performance.now(),elapsed=Math.max(0,(now-last)/1000);last=now;const frame=loop.step(elapsed),snapshot=store.snapshot();for(const entity of snapshot.entities){createEntityMesh(entity);const mesh=meshes.get(entity.id);if(mesh){mesh.position.x=entity.position[0];mesh.position.z=entity.position[2];if(entity.kind==='infrastructure')mesh.material=(entity as InfrastructureEntity).operational?accessMat:unavailableMat;}const mobility=meshes.get(entity.id+'-mobility');if(mobility){mobility.position.x=entity.position[0];mobility.position.z=entity.position[2];}}if(now-uiLast>1000){uiLast=now;const maya=store.entity('maya') as PersonEntity|undefined,lift=store.entity('station-lift') as InfrastructureEntity|undefined;if(maya&&lift){const a=accessAssessment(maya,lift);setAccess(`TRANSIT ACCESS: ${a.pass?'AVAILABLE':'BARRIER'} · ${a.cause}`);const close=nearbyEntities(snapshot,'maya',6);setNearby(`NEARBY MAYA · ${close.length?close.map(x=>x.entity.label).join(' · '):'none within 6m'}`);}const activity=frame.activities.find(x=>x.personId==='maya');setLife(activity?`MAYA · ${activity.kind.toUpperCase()} · ${activity.rationale}`:'MAYA · NO ACTIVE PLAN');const bridge=frame.clinical.find(x=>x.personId==='maya');setClinical(`CLINICAL BRIDGE · ${(bridge?.status??'continue-world').toUpperCase().replaceAll('-',' ')}`);}});try{const helper=await scene.createDefaultXRExperienceAsync({floorMeshes:[ground,path],disableTeleportation:false});setXr(helper.baseExperience?'VR READY · controller, gaze and teleport supported':'VR unavailable');}catch{setXr('WebXR not available on this browser/device');}setStatus('MULTI-PERSON WORLD ONLINE · WASD / mouse / WebXR');engine.runRenderLoop(()=>scene.render());const resize=()=>engine.resize();window.addEventListener('resize',resize);(scene as any).__cleanup=()=>window.removeEventListener('resize',resize);};boot();return()=>{disposed=true;scene?.__cleanup?.();scene?.dispose?.();engine?.dispose?.();};},[]); + const toggleLift=()=>{const s=storeRef.current!,lift=s.entity('station-lift') as InfrastructureEntity;s.setInfrastructure('station-lift',!lift.operational);};const toggleAAC=()=>{const s=storeRef.current!,maya=s.entity('maya') as PersonEntity;s.setCommunicationAccess('maya',maya.communication.access==='available'?'degraded':'available');};const interactNearest=()=>{const s=storeRef.current!,state=s.snapshot(),target=nearbyEntities(state,'maya',8)[0];if(!target){setStatus('INTERACTION · no nearby target');return;}const intent=createSpatialIntent('maya',target.entity.id,target.entity.kind==='person'?'communicate':'use','touch'),result=evaluateSpatialIntent(state,intent);s.recordEvent('world.intent', 'maya', `${intent.action} ${target.entity.label}: ${result.reason}`);setStatus(`INTERACTION · ${target.entity.label} · ${result.allowed?'AVAILABLE':'BLOCKED'} · ${result.reason}`);};const resetWorld=()=>{storeRef.current!.reset();setStatus('WORLD RESET · reload to reseed population');}; + return
MERT · LIVE DISABILITY WORLD
{status}
{xr}
{access}
{life}
{nearby}
{clinical}
One authoritative WorldRuntime now drives schedules, autonomous movement, affordances, VNN proposals, social interactions and the evidence-gated clinical bridge. VR, touch and other modalities resolve to semantic world intents rather than separate gameplay rules.
; } From 0a1497e7c700b919f3a41cf54dbc8473a21f20a0 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:42:42 +1000 Subject: [PATCH 29/46] Test consolidated runtime population and spatial interaction --- __tests__/worldSteps18to20.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 __tests__/worldSteps18to20.test.ts diff --git a/__tests__/worldSteps18to20.test.ts b/__tests__/worldSteps18to20.test.ts new file mode 100644 index 0000000..ca92099 --- /dev/null +++ b/__tests__/worldSteps18to20.test.ts @@ -0,0 +1,13 @@ +import { PersistentWorldStore, initialOpenWorldState, type PersonEntity } from '../src/world/worldModel'; +import { WorldRuntime } from '../src/world/liveWorldLoop'; +import { ensurePopulation, relationships } from '../src/world/population'; +import { createSpatialIntent, evaluateSpatialIntent, nearbyEntities, wayfind } from '../src/world/spatialInteraction'; + +describe('Steps 18-20 world integration',()=>{ + test('population seeds multiple self-authoritative people and relationships',()=>{const store=new PersistentWorldStore(initialOpenWorldState);ensurePopulation(store);const people=store.snapshot().entities.filter(e=>e.kind==='person') as PersonEntity[];expect(people.length).toBeGreaterThanOrEqual(4);expect(people.every(p=>p.authority==='self')).toBe(true);expect(relationships.length).toBeGreaterThan(0);}); + test('WorldRuntime is authoritative and advances population without changing authority',()=>{const store=new PersistentWorldStore(initialOpenWorldState);const runtime=new WorldRuntime(store);runtime.step(3);runtime.step(3);const maya=store.entity('maya') as PersonEntity;expect(maya.authority).toBe('self');expect(runtime.snapshot().activities.length).toBeGreaterThan(0);}); + test('semantic interaction is modality-independent',()=>{const store=new PersistentWorldStore(initialOpenWorldState);ensurePopulation(store);const state=store.snapshot();const touch=createSpatialIntent('maya','liam','communicate','touch');const xr=createSpatialIntent('maya','liam','communicate','xr-controller');expect(evaluateSpatialIntent(state,touch).allowed).toBe(evaluateSpatialIntent(state,xr).allowed);}); + test('AAC loss blocks immediate communication action but preserves intent and authority',()=>{const store=new PersistentWorldStore(initialOpenWorldState);ensurePopulation(store);store.setCommunicationAccess('maya','unavailable');const result=evaluateSpatialIntent(store.snapshot(),createSpatialIntent('maya','liam','communicate','switch'));expect(result.allowed).toBe(false);expect(result.reason).toContain('intent is preserved');expect((store.entity('maya') as PersonEntity).authority).toBe('self');}); + test('wayfinding attributes failed infrastructure to environment',()=>{const store=new PersistentWorldStore(initialOpenWorldState);store.setInfrastructure('station-lift',false);const route=wayfind(store.snapshot(),'maya','station-lift');expect(route?.accessible).toBe(false);expect(route?.reason).toContain('environmental infrastructure unavailable');}); + test('nearby discovery supports non-spatial equivalent of proximity interaction',()=>{const store=new PersistentWorldStore(initialOpenWorldState);ensurePopulation(store);const near=nearbyEntities(store.snapshot(),'maya',30);expect(near.length).toBeGreaterThan(0);}); +}); From 7fb02f885c80db1b4b3f9e756d8f2eee786c2980 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:52:35 +1000 Subject: [PATCH 30/46] Add Wolfram-grounded accessible navigation --- src/world/accessibleNavigation.ts | 130 ++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/world/accessibleNavigation.ts diff --git a/src/world/accessibleNavigation.ts b/src/world/accessibleNavigation.ts new file mode 100644 index 0000000..41fc038 --- /dev/null +++ b/src/world/accessibleNavigation.ts @@ -0,0 +1,130 @@ +import type { AccessModality, PersonEntity, PersistentWorldState, Vec3 } from './worldModel'; + +/** + * Wolfram-checked cost model used here: + * cost = length * (1 + gradientWeight * |gradient| + surfacePenalty) + waitSeconds + * Unsupported gradients / modes are excluded, not attributed to the person. + * Example evaluation produced route costs 91.52 and 135.82 for two viable alternatives. + * These defaults are simulation tuning parameters, not building-code thresholds. + */ +export const WOLFRAM_ROUTE_MODEL = { + formula: 'length*(1 + gradientWeight*abs(gradient) + surfacePenalty) + waitSeconds', + exampleBestCost: 91.52, + exampleAlternativeCost: 135.82, +}; + +export type RouteFeature = 'path' | 'door' | 'ramp' | 'lift' | 'crossing' | 'transport-link'; + +export interface NavNode { id: string; position: Vec3; label: string; } +export interface NavEdge { + from: string; + to: string; + length: number; + gradient: number; + surfacePenalty: number; + waitSeconds: number; + feature: RouteFeature; + access: AccessModality[]; + objectId?: string; +} + +export interface NavigationProfile { + mobility: AccessModality; + maxGradient: number; + gradientWeight: number; + blockedPenalty: number; +} + +export interface RouteResult { + nodeIds: string[]; + points: Vec3[]; + cost: number; + features: RouteFeature[]; + blockedBy: string[]; +} + +export interface NavigationNetwork { nodes: NavNode[]; edges: NavEdge[]; } + +export const defaultNavigationNetwork: NavigationNetwork = { + nodes: [ + { id:'plaza', label:'Central plaza', position:[0,0,0] }, + { id:'community-door', label:'Community automatic door', position:[14,0,14] }, + { id:'community-hub', label:'Community Hub', position:[25,0,22] }, + { id:'clinical-ramp', label:'Clinical ramp', position:[-11,0,4] }, + { id:'clinical-door', label:'Clinical automatic door', position:[-18,0,8] }, + { id:'clinical-centre', label:'Clinical Simulation Centre', position:[-24,0,8] }, + { id:'crossing', label:'Accessible crossing', position:[-8,0,-15] }, + { id:'station-lift', label:'Station lift', position:[-20,0,-34] }, + { id:'transit', label:'Accessible Transit Station', position:[-25,0,-34] }, + { id:'rehab-ramp', label:'Rehabilitation ramp', position:[10,0,-13] }, + { id:'rehab-lab', label:'Rehabilitation Lab', position:[24,0,-28] }, + ], + edges: [ + { from:'plaza',to:'community-door',length:20,gradient:.02,surfacePenalty:.02,waitSeconds:0,feature:'path',access:['walk','powered-wheelchair','manual-wheelchair'] }, + { from:'community-door',to:'community-hub',length:14,gradient:0,surfacePenalty:0,waitSeconds:2,feature:'door',objectId:'community-door',access:['walk','powered-wheelchair','manual-wheelchair'] }, + { from:'plaza',to:'clinical-ramp',length:12,gradient:.04,surfacePenalty:.01,waitSeconds:0,feature:'ramp',access:['walk','powered-wheelchair','manual-wheelchair'] }, + { from:'clinical-ramp',to:'clinical-door',length:8,gradient:.06,surfacePenalty:0,waitSeconds:0,feature:'ramp',access:['walk','powered-wheelchair','manual-wheelchair'] }, + { from:'clinical-door',to:'clinical-centre',length:7,gradient:0,surfacePenalty:0,waitSeconds:2,feature:'door',objectId:'clinical-door',access:['walk','powered-wheelchair','manual-wheelchair'] }, + { from:'plaza',to:'crossing',length:18,gradient:.01,surfacePenalty:.03,waitSeconds:8,feature:'crossing',objectId:'crossing',access:['walk','powered-wheelchair','manual-wheelchair'] }, + { from:'crossing',to:'station-lift',length:25,gradient:.03,surfacePenalty:.02,waitSeconds:0,feature:'path',access:['walk','powered-wheelchair','manual-wheelchair'] }, + { from:'station-lift',to:'transit',length:6,gradient:0,surfacePenalty:0,waitSeconds:18,feature:'lift',objectId:'station-lift',access:['walk','powered-wheelchair','manual-wheelchair'] }, + { from:'plaza',to:'rehab-ramp',length:17,gradient:.035,surfacePenalty:.02,waitSeconds:0,feature:'ramp',access:['walk','powered-wheelchair','manual-wheelchair'] }, + { from:'rehab-ramp',to:'rehab-lab',length:22,gradient:.055,surfacePenalty:.01,waitSeconds:0,feature:'ramp',access:['walk','powered-wheelchair','manual-wheelchair'] }, + { from:'community-hub',to:'transit',length:62,gradient:.025,surfacePenalty:.04,waitSeconds:6,feature:'transport-link',access:['walk','powered-wheelchair','manual-wheelchair'] }, + { from:'community-hub',to:'rehab-lab',length:54,gradient:.03,surfacePenalty:.03,waitSeconds:0,feature:'path',access:['walk','powered-wheelchair','manual-wheelchair'] }, + ], +}; + +export function navigationProfile(person: PersonEntity): NavigationProfile { + const mobility = person.mobility.mode; + const maxGradient = mobility === 'manual-wheelchair' ? .07 : mobility === 'powered-wheelchair' ? .09 : .16; + return { mobility, maxGradient, gradientWeight: 4, blockedPenalty: 1_000_000 }; +} + +function edgeOperational(edge: NavEdge, state: PersistentWorldState) { + if (!edge.objectId) return true; + const entity = state.entities.find((item) => item.id === edge.objectId); + if (!entity || entity.kind !== 'infrastructure') return true; + return entity.operational; +} + +export function edgeCost(edge: NavEdge, person: PersonEntity, state: PersistentWorldState) { + const profile = navigationProfile(person); + if (!edge.access.includes(profile.mobility)) return Infinity; + if (Math.abs(edge.gradient) > profile.maxGradient) return Infinity; + if (!edgeOperational(edge, state)) return Infinity; + return edge.length * (1 + profile.gradientWeight * Math.abs(edge.gradient) + edge.surfacePenalty) + edge.waitSeconds; +} + +function adjacency(network: NavigationNetwork) { + const map = new Map>(); + for (const edge of network.edges) { + map.set(edge.from, [...(map.get(edge.from) ?? []), { edge, next: edge.to }]); + map.set(edge.to, [...(map.get(edge.to) ?? []), { edge, next: edge.from }]); + } + return map; +} + +export function nearestNavNode(network: NavigationNetwork, position: Vec3) { + return [...network.nodes].sort((a,b)=>Math.hypot(a.position[0]-position[0],a.position[2]-position[2])-Math.hypot(b.position[0]-position[0],b.position[2]-position[2]))[0]; +} + +export function findAccessibleRoutes(network: NavigationNetwork, state: PersistentWorldState, person: PersonEntity, startId: string, goalId: string, limit=3): RouteResult[] { + const adj = adjacency(network); const results: RouteResult[] = []; const maxDepth = network.nodes.length; + const visit = (node:string,path:string[],cost:number,features:RouteFeature[]) => { + if (path.length > maxDepth || results.length > 200) return; + if (node === goalId) { const points=path.map(id=>network.nodes.find(n=>n.id===id)!.position); results.push({nodeIds:path,points,cost,features,blockedBy:[]}); return; } + for (const item of adj.get(node) ?? []) { + if (path.includes(item.next)) continue; + const c=edgeCost(item.edge,person,state); if(!Number.isFinite(c)) continue; + visit(item.next,[...path,item.next],cost+c,[...features,item.edge.feature]); + } + }; + visit(startId,[startId],0,[]); + return results.sort((a,b)=>a.cost-b.cost).slice(0,limit); +} + +export function routeForPerson(state: PersistentWorldState, person: PersonEntity, targetId: string, network=defaultNavigationNetwork) { + const start=nearestNavNode(network,person.position); const goal=network.nodes.find(node=>node.id===targetId) ?? nearestNavNode(network,state.entities.find(e=>e.id===targetId)?.position ?? person.position); + return goal ? findAccessibleRoutes(network,state,person,start!.id,goal.id,3) : []; +} From 66a5cf782beedecfef5d2191d2956041f2a36275 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:52:56 +1000 Subject: [PATCH 31/46] Add Wolfram-grounded object interaction dynamics --- src/world/objectInteractions.ts | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/world/objectInteractions.ts diff --git a/src/world/objectInteractions.ts b/src/world/objectInteractions.ts new file mode 100644 index 0000000..9d456a1 --- /dev/null +++ b/src/world/objectInteractions.ts @@ -0,0 +1,64 @@ +import type { PersistentWorldStore, WorldEvent } from './worldModel'; + +export const WOLFRAM_OBJECT_MODELS = { + liftSmoothstep: 's(u)=3u^2-2u^3', + liftExample: { distanceMetres: 4.2, durationSeconds: 8, maxVelocityMetresPerSecond: .7875 }, + charger: 'B(t)=C-(C-B0) exp(-k t)', + chargerExample: { initialPercent: 30, targetPercent: 80, kPerSecond: .0015, timeToTargetSeconds: 835.1753123302453 }, +}; + +export type DoorPhase = 'closed' | 'opening' | 'open' | 'closing'; +export interface DoorState { id:string; phase:DoorPhase; progress:number; holdSeconds:number; elapsed:number; } +export interface LiftState { id:string; currentLevel:number; fromLevel:number; targetLevel:number; phase:'idle'|'moving'|'doors-open'; elapsed:number; duration:number; positionMetres:number; } +export interface ChargerSession { personId:string; initialPercent:number; currentPercent:number; capacityPercent:number; kPerSecond:number; elapsed:number; active:boolean; } +export interface TransportState { id:string; atStop:boolean; capacity:number; passengers:string[]; doorsOpen:boolean; } + +export interface ObjectWorldSnapshot { + doors: DoorState[]; + lift: LiftState; + chargers: ChargerSession[]; + transport: TransportState; +} + +const clamp01=(x:number)=>Math.max(0,Math.min(1,x)); +export const smoothstep=(u:number)=>{const x=clamp01(u);return 3*x*x-2*x*x*x;}; +export const chargerPercent=(initial:number,capacity:number,k:number,t:number)=>capacity-(capacity-initial)*Math.exp(-k*Math.max(0,t)); + +export class ObjectInteractionEngine { + private doors = new Map([ + ['community-door',{id:'community-door',phase:'closed',progress:0,holdSeconds:4,elapsed:0}], + ['clinical-door',{id:'clinical-door',phase:'closed',progress:0,holdSeconds:4,elapsed:0}], + ['rehab-door',{id:'rehab-door',phase:'closed',progress:0,holdSeconds:4,elapsed:0}], + ['transit-door',{id:'transit-door',phase:'closed',progress:0,holdSeconds:4,elapsed:0}], + ]); + private lift:LiftState={id:'station-lift',currentLevel:0,fromLevel:0,targetLevel:0,phase:'idle',elapsed:0,duration:8,positionMetres:0}; + private chargers=new Map(); + private transport:TransportState={id:'community-shuttle',atStop:true,capacity:6,passengers:[],doorsOpen:true}; + + constructor(private store?:PersistentWorldStore){} + + snapshot():ObjectWorldSnapshot{return {doors:[...this.doors.values()].map(x=>({...x})),lift:{...this.lift},chargers:[...this.chargers.values()].map(x=>({...x})),transport:{...this.transport,passengers:[...this.transport.passengers]}};} + + requestDoor(id:string,personId?:string){const door=this.doors.get(id);if(!door)return false;if(door.phase==='closed'||door.phase==='closing'){door.phase='opening';door.elapsed=0;this.event('object.door.requested',personId,`${id} opening requested`);}return true;} + + requestLift(targetLevel:number,personId?:string){if(this.lift.phase==='moving')return false;if(targetLevel===this.lift.currentLevel){this.lift.phase='doors-open';this.event('object.lift.available',personId,`lift already at level ${targetLevel}`);return true;}this.lift.fromLevel=this.lift.currentLevel;this.lift.targetLevel=targetLevel;this.lift.elapsed=0;this.lift.phase='moving';this.event('object.lift.requested',personId,`lift moving ${this.lift.fromLevel}->${targetLevel}`);return true;} + + startCharging(personId:string,initialPercent:number,kPerSecond=.0015){this.chargers.set(personId,{personId,initialPercent,currentPercent:initialPercent,capacityPercent:100,kPerSecond,elapsed:0,active:true});this.event('object.charger.started',personId,`charging started at ${initialPercent.toFixed(1)}%`);} + stopCharging(personId:string){const session=this.chargers.get(personId);if(!session)return;session.active=false;this.event('object.charger.stopped',personId,`charging stopped at ${session.currentPercent.toFixed(1)}%`);} + + boardTransport(personId:string){if(!this.transport.atStop||!this.transport.doorsOpen)return {ok:false,reason:'transport not ready for boarding'};if(this.transport.passengers.includes(personId))return {ok:true,reason:'already boarded'};if(this.transport.passengers.length>=this.transport.capacity)return {ok:false,reason:'transport capacity reached'};this.transport.passengers.push(personId);this.event('object.transport.boarded',personId,'boarding completed after explicit request');return {ok:true,reason:'boarded'};} + alightTransport(personId:string){this.transport.passengers=this.transport.passengers.filter(id=>id!==personId);this.event('object.transport.alighted',personId,'alighted transport');} + + step(seconds:number){const dt=Math.max(0,seconds); + for(const door of this.doors.values()){ + if(door.phase==='opening'){door.elapsed+=dt;door.progress=smoothstep(door.elapsed/1.25);if(door.elapsed>=1.25){door.phase='open';door.progress=1;door.elapsed=0;}} + else if(door.phase==='open'){door.elapsed+=dt;if(door.elapsed>=door.holdSeconds){door.phase='closing';door.elapsed=0;}} + else if(door.phase==='closing'){door.elapsed+=dt;door.progress=1-smoothstep(door.elapsed/1.25);if(door.elapsed>=1.25){door.phase='closed';door.progress=0;door.elapsed=0;}} + } + if(this.lift.phase==='moving'){this.lift.elapsed+=dt;const u=this.lift.elapsed/this.lift.duration;const y0=this.lift.fromLevel*4.2,y1=this.lift.targetLevel*4.2;this.lift.positionMetres=y0+(y1-y0)*smoothstep(u);if(u>=1){this.lift.currentLevel=this.lift.targetLevel;this.lift.positionMetres=y1;this.lift.phase='doors-open';this.lift.elapsed=0;}} + else if(this.lift.phase==='doors-open'){this.lift.elapsed+=dt;if(this.lift.elapsed>=4){this.lift.phase='idle';this.lift.elapsed=0;}} + for(const session of this.chargers.values())if(session.active){session.elapsed+=dt;session.currentPercent=chargerPercent(session.initialPercent,session.capacityPercent,session.kPerSecond,session.elapsed);if(session.currentPercent>=99.5)session.active=false;} + } + + private event(type:string,entityId:string|undefined,detail:string):WorldEvent|undefined{return this.store?.recordEvent(type,entityId,detail);} +} From ba1363e5fd7f49b31ae67ccfa882528b77b64660 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:53:11 +1000 Subject: [PATCH 32/46] Add Wolfram-grounded social encounter model --- src/world/socialEncounters.ts | 51 +++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/world/socialEncounters.ts diff --git a/src/world/socialEncounters.ts b/src/world/socialEncounters.ts new file mode 100644 index 0000000..b392670 --- /dev/null +++ b/src/world/socialEncounters.ts @@ -0,0 +1,51 @@ +import { relationships } from './population'; +import type { PersonEntity, PersistentWorldState, WorldEvent } from './worldModel'; + +export const WOLFRAM_SOCIAL_MODEL = { + formula: 'sigmoid(2*relationship + sharedGoal - 0.35*distance - 1.5*contextLoad)', + examples: { + friendNearby: .8979819308108695, + strangerNearby: .3047033306524346, + friendFar: .24973989440488245, + }, +}; + +export interface SocialEncounter { + a: string; + b: string; + distance: number; + relationship: number; + sharedGoal: number; + contextLoad: number; + score: number; + status: 'recognised' | 'conversation-ready' | 'waiting-for-access'; + supportRequested: boolean; +} + +const sigmoid=(x:number)=>1/(1+Math.exp(-x)); +const distance2D=(a:PersonEntity,b:PersonEntity)=>Math.hypot(a.position[0]-b.position[0],a.position[2]-b.position[2]); + +function relationshipStrength(a:string,b:string){return relationships.find(r=>r.from===a&&r.to===b)?.trust ?? relationships.find(r=>r.from===b&&r.to===a)?.trust ?? 0;} +function sharedGoalScore(a:PersonEntity,b:PersonEntity){const A=new Set(a.goals.map(g=>g.toLowerCase()));return b.goals.some(g=>A.has(g.toLowerCase()))?1:0;} + +export function encounterScore(distance:number,relationship:number,sharedGoal:number,contextLoad:number){return sigmoid(2*relationship+sharedGoal-.35*distance-1.5*contextLoad);} + +export class SocialEncounterEngine { + private requests=new Set(); + private previous=new Set(); + + requestSupport(from:string,to:string){this.requests.add(`${from}->${to}`);} + clearSupportRequest(from:string,to:string){this.requests.delete(`${from}->${to}`);} + + detect(state:PersistentWorldState,contextLoad=.2):{encounters:SocialEncounter[];events:WorldEvent[]}{ + const people=state.entities.filter((e):e is PersonEntity=>e.kind==='person');const encounters:SocialEncounter[]=[];const events:WorldEvent[]=[];const current=new Set(); + for(let i=0;i6)continue; + const key=[a.id,b.id].sort().join('|');current.add(key);const accessAvailable=a.communication.access!=='unavailable'&&b.communication.access!=='unavailable';const status:SocialEncounter['status']=accessAvailable?(score>.75?'conversation-ready':'recognised'):'waiting-for-access';const supportRequested=this.requests.has(`${a.id}->${b.id}`)||this.requests.has(`${b.id}->${a.id}`); + encounters.push({a:a.id,b:b.id,distance,relationship,sharedGoal,contextLoad,score,status,supportRequested}); + if(!this.previous.has(key))events.push({id:`${state.simulationSeconds}-encounter-${key}`,at:state.simulationSeconds,type:'social.encounter.started',entityId:a.id,detail:`with=${b.id}; score=${score.toFixed(3)}; status=${status}; support=${supportRequested?'requested':'not-assumed'}`}); + } + for(const key of this.previous)if(!current.has(key))events.push({id:`${state.simulationSeconds}-encounter-end-${key}`,at:state.simulationSeconds,type:'social.encounter.ended',detail:key}); + this.previous=current;return {encounters,events}; + } +} From 28d0c3ceb375700163483a48ef4b9d6e90d7834f Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:53:25 +1000 Subject: [PATCH 33/46] Add persistent world clinical continuity engine --- src/world/continuity.ts | 57 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/world/continuity.ts diff --git a/src/world/continuity.ts b/src/world/continuity.ts new file mode 100644 index 0000000..da75253 --- /dev/null +++ b/src/world/continuity.ts @@ -0,0 +1,57 @@ +import type { PersonEntity, PersistentWorldState, PersistentWorldStore, WorldEvent } from './worldModel'; + +export type ContinuityStage = 'community' | 'ambulance' | 'ed' | 'icu' | 'mert-scenario' | 'discharge' | 'community-return' | 'community-with-consequences'; + +export interface ContinuityEpisode { + id:string; + personId:string; + stage:ContinuityStage; + history:ContinuityStage[]; + preserved:{ authority:'self'; communication:true; worldHistory:true; baseline:true }; + consequences:{ fatigue:number; followupRequired:boolean; supportReviewRequired:boolean; accessChanges:string[] }; +} + +export const WOLFRAM_CONTINUITY_MODEL = { + verifiedAcyclic: true, + unexpectedDeadEnds: 0, + exampleReturnPath: ['icu','mert-scenario','discharge','community-return','community-with-consequences'] as ContinuityStage[], +}; + +const transitions:Record = { + community:['ambulance'], + ambulance:['ed'], + ed:['icu','discharge'], + icu:['mert-scenario'], + 'mert-scenario':['discharge'], + discharge:['community-return'], + 'community-return':['community-with-consequences'], + 'community-with-consequences':[], +}; + +export class ContinuityEngine { + private episodes=new Map(); + constructor(private store?:PersistentWorldStore){} + + episode(personId:string){const e=this.episodes.get(personId);return e?JSON.parse(JSON.stringify(e)) as ContinuityEpisode:undefined;} + + start(person:PersonEntity,reason:string){const existing=this.episodes.get(person.id);if(existing&&existing.stage!=='community-with-consequences')return existing;const episode:ContinuityEpisode={id:`episode-${person.id}-${Date.now()}`,personId:person.id,stage:'ambulance',history:['community','ambulance'],preserved:{authority:'self',communication:true,worldHistory:true,baseline:true},consequences:{fatigue:0,followupRequired:false,supportReviewRequired:false,accessChanges:[]}};this.episodes.set(person.id,episode);this.event('continuity.started',person.id,`community->ambulance; reason=${reason}; authority=self`);return episode;} + + canAdvance(personId:string,next:ContinuityStage){const episode=this.episodes.get(personId);return Boolean(episode&&transitions[episode.stage].includes(next));} + + advance(personId:string,next:ContinuityStage,detail='explicit simulation transition'){ + const episode=this.episodes.get(personId);if(!episode||!transitions[episode.stage].includes(next))return false; + const previous=episode.stage;episode.stage=next;episode.history.push(next); + if(next==='icu'||next==='mert-scenario')episode.consequences.fatigue=Math.max(episode.consequences.fatigue,1); + if(next==='discharge'){episode.consequences.followupRequired=true;episode.consequences.supportReviewRequired=true;} + if(next==='community-return')episode.consequences.accessChanges.push('reassess transport, communication and equipment access after discharge'); + this.event('continuity.transition',personId,`${previous}->${next}; ${detail}`);return true; + } + + advanceDemoPath(personId:string){const episode=this.episodes.get(personId);if(!episode)return false;const preferred:Partial>={ambulance:'ed',ed:'icu',icu:'mert-scenario','mert-scenario':'discharge',discharge:'community-return','community-return':'community-with-consequences'};const next=preferred[episode.stage];return next?this.advance(personId,next,'educational continuity demo'):false;} + + summary(personId:string){const e=this.episode(personId);return e?`${e.stage} · history ${e.history.join(' → ')}`:'community · no active clinical episode';} + + private event(type:string,entityId:string,detail:string):WorldEvent|undefined{return this.store?.recordEvent(type,entityId,detail);} +} + +export function preservedContinuityContext(state:PersistentWorldState,personId:string){const person=state.entities.find((e):e is PersonEntity=>e.kind==='person'&&e.id===personId);return person?{personId,authority:person.authority,communication:person.communication,worldEvents:state.events.slice(-50)}:undefined;} From 775bec2345977c741a06fce92ff9a218d43140d1 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:53:48 +1000 Subject: [PATCH 34/46] Integrate navigation objects encounters and continuity into WorldRuntime --- src/world/liveWorldLoop.ts | 122 +++++++++++++++++++++++++++++++++---- 1 file changed, 110 insertions(+), 12 deletions(-) diff --git a/src/world/liveWorldLoop.ts b/src/world/liveWorldLoop.ts index cb16fda..2394bd3 100644 --- a/src/world/liveWorldLoop.ts +++ b/src/world/liveWorldLoop.ts @@ -4,25 +4,123 @@ import { defaultSchedules, runDailyLifeTick } from './dailyLife'; import { ensurePopulation, populationSchedules } from './population'; import { defaultSocialAgents, interact, socialEvents } from './socialAgents'; import { dynamicsEvents, proposeDynamics, type DynamicsProposal } from './vnnDynamics'; +import { defaultNavigationNetwork, routeForPerson, type RouteResult } from './accessibleNavigation'; +import { ObjectInteractionEngine, type ObjectWorldSnapshot } from './objectInteractions'; +import { SocialEncounterEngine, type SocialEncounter } from './socialEncounters'; +import { ContinuityEngine, type ContinuityStage } from './continuity'; import type { PersonEntity, PersistentWorldState, PersistentWorldStore, Vec3 } from './worldModel'; -export interface LiveWorldFrame { activities: ReturnType; proposals: DynamicsProposal[]; clinical: ReturnType[]; } +export interface LiveWorldFrame { + activities: ReturnType; + proposals: DynamicsProposal[]; + clinical: ReturnType[]; + routes: Record; + objects: ObjectWorldSnapshot; + encounters: SocialEncounter[]; + continuity: Record; +} export class WorldRuntime { - private accumulator=0; private socialAccumulator=0; - private lastFrame:LiveWorldFrame={activities:[],proposals:[],clinical:[]}; - constructor(private store:PersistentWorldStore,private clinicalRuntime?:ClinicalRuntimePort){ensurePopulation(store);} + private accumulator=0; + private socialAccumulator=0; + private objects:ObjectInteractionEngine; + private encounterEngine=new SocialEncounterEngine(); + private continuityEngine:ContinuityEngine; + private lastFrame:LiveWorldFrame={activities:[],proposals:[],clinical:[],routes:{},objects:{doors:[],lift:{id:'station-lift',currentLevel:0,fromLevel:0,targetLevel:0,phase:'idle',elapsed:0,duration:8,positionMetres:0},chargers:[],transport:{id:'community-shuttle',atStop:true,capacity:6,passengers:[],doorsOpen:true}},encounters:[],continuity:{}}; + + constructor(private store:PersistentWorldStore,private clinicalRuntime?:ClinicalRuntimePort){ + ensurePopulation(store); + this.objects=new ObjectInteractionEngine(store); + this.continuityEngine=new ContinuityEngine(store); + } + snapshot(){return this.lastFrame;} + objectEngine(){return this.objects;} + continuity(){return this.continuityEngine;} + step(realSeconds:number):LiveWorldFrame{ - this.store.tick(realSeconds);this.accumulator+=realSeconds;this.socialAccumulator+=realSeconds;if(this.accumulator<2)return this.lastFrame; - const stepSeconds=this.accumulator;this.accumulator=0;let state=this.store.snapshot();const schedules=[...defaultSchedules,...populationSchedules]; - this.store.appendEvents(schedules.flatMap(schedule=>runDailyLifeTick(state,schedule).events));state=this.store.snapshot(); - const activities=planPopulation(state);const proposals=activities.map(activity=>proposeDynamics(state,activity));this.store.appendEvents(proposals.flatMap(p=>dynamicsEvents(state,p)));for(const activity of activities)this.advanceActivity(state,activity,stepSeconds); - if(this.socialAccumulator>=15){this.socialAccumulator=0;const people=state.entities.filter((e):e is PersonEntity=>e.kind==='person');for(let i=0;ie.kind==='person').map(p=>assessClinicalTrigger(state,p.id,proposals));if(this.clinicalRuntime)for(const assessment of clinical){const event=bridgeToClinicalRuntime(state,assessment,this.clinicalRuntime);if(event)this.store.appendEvents([event]);} - this.lastFrame={activities,proposals,clinical};return this.lastFrame; + this.store.tick(realSeconds); + this.objects.step(realSeconds); + this.accumulator+=realSeconds; + this.socialAccumulator+=realSeconds; + if(this.accumulator<2)return {...this.lastFrame,objects:this.objects.snapshot()}; + + const stepSeconds=this.accumulator; + this.accumulator=0; + let state=this.store.snapshot(); + const schedules=[...defaultSchedules,...populationSchedules]; + this.store.appendEvents(schedules.flatMap(schedule=>runDailyLifeTick(state,schedule).events)); + state=this.store.snapshot(); + + const activities=planPopulation(state); + const proposals=activities.map(activity=>proposeDynamics(state,activity)); + this.store.appendEvents(proposals.flatMap(p=>dynamicsEvents(state,p))); + + const routes:Record={}; + for(const activity of activities){ + const person=state.entities.find((e):e is PersonEntity=>e.kind==='person'&&e.id===activity.personId); + if(!person||!activity.targetId)continue; + const route=routeForPerson(state,person,activity.targetId,defaultNavigationNetwork)[0]; + routes[person.id]=route; + this.advanceActivity(state,activity,route,stepSeconds); + } + + if(this.socialAccumulator>=15){ + this.socialAccumulator=0; + const people=state.entities.filter((e):e is PersonEntity=>e.kind==='person'); + for(let i=0;ie.kind==='person').map(p=>assessClinicalTrigger(state,p.id,proposals)); + for(const assessment of clinical){ + if(assessment.status==='scenario-eligible'){ + const person=state.entities.find((e):e is PersonEntity=>e.kind==='person'&&e.id===assessment.personId); + if(person&&!this.continuityEngine.episode(person.id))this.continuityEngine.start(person,assessment.reasons.join('; ')); + } + if(this.clinicalRuntime){const event=bridgeToClinicalRuntime(state,assessment,this.clinicalRuntime);if(event)this.store.appendEvents([event]);} + } + + const continuity:Record={}; + for(const person of state.entities.filter((e):e is PersonEntity=>e.kind==='person'))continuity[person.id]=this.continuityEngine.summary(person.id); + this.lastFrame={activities,proposals,clinical,routes,objects:this.objects.snapshot(),encounters:encounterResult.encounters,continuity}; + return this.lastFrame; + } + + requestSupport(from:string,to:string){this.encounterEngine.requestSupport(from,to);this.store.recordEvent('social.support.requested',from,`support requested from ${to}; support is not assumed`);} + beginContinuity(personId:string,reason='facilitator-triggered educational deterioration'){ + const person=this.store.entity(personId);if(!person||person.kind!=='person')return false;this.continuityEngine.start(person as PersonEntity,reason);return true; + } + advanceContinuity(personId:string,next?:ContinuityStage){return next?this.continuityEngine.advance(personId,next):this.continuityEngine.advanceDemoPath(personId);} + + private advanceActivity(state:PersistentWorldState,activity:ReturnType[number],route:RouteResult|undefined,seconds:number){ + if(!activity.targetId||(activity.kind!=='travel'&&activity.kind!=='seek-alternative'))return; + const person=state.entities.find((e):e is PersonEntity=>e.kind==='person'&&e.id===activity.personId); + if(!person)return; + if(!route){this.store.recordEvent('navigation.no-accessible-route',person.id,`goal preserved; target=${activity.targetId}; environmental/route barrier`);return;} + + const current=[person.position[0],person.position[1],person.position[2]] as Vec3; + const remaining=route.points.filter(point=>Math.hypot(point[0]-current[0],point[2]-current[2])>1.2); + const waypoint=remaining[0] ?? route.points[route.points.length-1]; + if(!waypoint)return; + + const waypointIndex=route.points.indexOf(waypoint); + const waypointId=route.nodeIds[waypointIndex]; + const dx=waypoint[0]-current[0],dz=waypoint[2]-current[2],distance=Math.hypot(dx,dz); + if(distance<2.5&&waypointId?.includes('-door'))this.objects.requestDoor(waypointId,person.id); + if(distance<2.5&&waypointId==='station-lift')this.objects.requestLift(0,person.id); + if(distance<.01)return; + + const speed=person.mobility.mode==='powered-wheelchair'?1.25:person.mobility.mode==='manual-wheelchair'?1.0:1.15; + const amount=Math.min(distance,speed*seconds); + this.store.move(person.id,[current[0]+dx/distance*amount,current[1],current[2]+dz/distance*amount] as Vec3); } - private advanceActivity(state:PersistentWorldState,activity:ReturnType[number],seconds:number){if(!activity.targetId||(activity.kind!=='travel'&&activity.kind!=='seek-alternative'))return;const person=state.entities.find((e):e is PersonEntity=>e.kind==='person'&&e.id===activity.personId),target=state.entities.find(e=>e.id===activity.targetId);if(!person||!target)return;const[px,py,pz]=person.position,[tx,,tz]=target.position,dx=tx-px,dz=tz-pz,d=Math.hypot(dx,dz);if(d<1.25)return;const speed=person.mobility.mode==='powered-wheelchair'?1.25:person.mobility.mode==='manual-wheelchair'?1.0:1.15,amount=Math.min(d,speed*seconds);this.store.move(person.id,[px+dx/d*amount,py,pz+dz/d*amount] as Vec3);} } /** @deprecated Use WorldRuntime. */ From f0e7e2a895b3d22bd32530088519347fa6e8dcbe Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:54:26 +1000 Subject: [PATCH 35/46] Display Wolfram-grounded Steps 21-24 in WebXR world --- src/world/OpenWorldWeb.tsx | 163 +++++++++++++++++++++++++++++++++++-- 1 file changed, 156 insertions(+), 7 deletions(-) diff --git a/src/world/OpenWorldWeb.tsx b/src/world/OpenWorldWeb.tsx index 2a9c7b8..17745fc 100644 --- a/src/world/OpenWorldWeb.tsx +++ b/src/world/OpenWorldWeb.tsx @@ -2,13 +2,162 @@ import React, { useEffect, useRef, useState } from 'react'; import { InfrastructureEntity, PersonEntity, PersistentWorldStore, accessAssessment } from './worldModel'; import { WorldRuntime } from './liveWorldLoop'; import { nearbyEntities, createSpatialIntent, evaluateSpatialIntent } from './spatialInteraction'; +import { WOLFRAM_ROUTE_MODEL } from './accessibleNavigation'; +import { WOLFRAM_OBJECT_MODELS } from './objectInteractions'; +import { WOLFRAM_SOCIAL_MODEL } from './socialEncounters'; +import { WOLFRAM_CONTINUITY_MODEL } from './continuity'; export function OpenWorldWeb() { - const canvasRef=useRef(null);const storeRef=useRef();if(!storeRef.current)storeRef.current=new PersistentWorldStore(); - const [status,setStatus]=useState('Loading world…'),[xr,setXr]=useState('Checking WebXR…'),[access,setAccess]=useState('ACCESS NETWORK ONLINE'),[life,setLife]=useState('DAILY LIFE ENGINE STARTING'),[clinical,setClinical]=useState('CLINICAL BRIDGE · STANDBY'),[nearby,setNearby]=useState('NEARBY · scanning'); - useEffect(()=>{let disposed=false,engine:any,scene:any;const store=storeRef.current!,loop=new WorldRuntime(store);const boot=async()=>{const B=await import('@babylonjs/core');await import('@babylonjs/loaders');if(!canvasRef.current||disposed)return;engine=new B.Engine(canvasRef.current,true,{preserveDrawingBuffer:true,stencil:true});scene=new B.Scene(engine);scene.clearColor=new B.Color4(.72,.86,.96,1);scene.collisionsEnabled=true;const camera=new B.UniversalCamera('player',new B.Vector3(0,1.65,-16),scene);camera.attachControl(canvasRef.current,true);camera.speed=.45;camera.angularSensibility=3500;camera.applyGravity=true;camera.checkCollisions=true;camera.ellipsoid=new B.Vector3(.5,.9,.5);camera.keysUp.push(87);camera.keysDown.push(83);camera.keysLeft.push(65);camera.keysRight.push(68);new B.HemisphericLight('sky',new B.Vector3(0,1,0),scene).intensity=.85;new B.DirectionalLight('sun',new B.Vector3(-.4,-1,.25),scene).intensity=.7;const material=(n:string,r:number,g:number,b:number)=>{const m=new B.StandardMaterial(n,scene);m.diffuseColor=new B.Color3(r,g,b);return m;};const ground=B.MeshBuilder.CreateGround('accessible-campus',{width:220,height:220},scene);ground.checkCollisions=true;ground.material=material('ground',.38,.55,.35);const path=B.MeshBuilder.CreateGround('wide-accessible-path',{width:12,height:190},scene);path.position.y=.015;path.material=material('path',.62,.61,.57);const buildingMat=material('building',.72,.76,.78),glass=material('glass',.25,.55,.7);glass.alpha=.72;const makeBuilding=(n:string,x:number,z:number,w:number,d:number,h:number)=>{const body=B.MeshBuilder.CreateBox(n,{width:w,depth:d,height:h},scene);body.position.set(x,h/2,z);body.material=buildingMat;body.checkCollisions=true;const door=B.MeshBuilder.CreateBox(n+'-automatic-entry',{width:3.2,depth:.12,height:2.7},scene);door.position.set(x,1.35,z-d/2-.07);door.material=glass;};makeBuilding('Clinical Simulation Centre',-24,8,28,20,10);makeBuilding('Community Hub',25,22,26,18,7);makeBuilding('Rehabilitation Lab',24,-28,24,18,8);makeBuilding('Accessible Transit Station',-25,-34,30,15,6); - const personMat=material('people',.22,.32,.55),accessMat=material('access',.16,.48,.5),unavailableMat=material('unavailable',.48,.2,.2),meshes=new Map();const createEntityMesh=(entity:any)=>{if(meshes.has(entity.id))return;if(entity.kind==='person'){const p=entity as PersonEntity,body=B.MeshBuilder.CreateCapsule(entity.id,{height:1.35,radius:.3},scene);body.position.set(p.position[0],1.05,p.position[2]);body.material=personMat;meshes.set(entity.id,body);if(p.mobility.mode.includes('wheelchair')){const chair=B.MeshBuilder.CreateBox(entity.id+'-mobility',{width:.85,height:.55,depth:1.05},scene);chair.position.set(p.position[0],.38,p.position[2]);chair.material=personMat;meshes.set(entity.id+'-mobility',chair);}}else if(entity.kind==='infrastructure'){const i=entity as InfrastructureEntity,marker=B.MeshBuilder.CreateCylinder(entity.id,{height:1.5,diameter:.65},scene);marker.position.set(i.position[0],.75,i.position[2]);marker.material=i.operational?accessMat:unavailableMat;meshes.set(i.id,marker);}};for(const entity of store.snapshot().entities)createEntityMesh(entity);for(let i=0;i<28;i++){const a=i*2.399,r=46+(i%4)*9,t=B.MeshBuilder.CreateCylinder('tree-'+i,{height:5,diameterTop:.5,diameterBottom:2.4,tessellation:7},scene);t.position.set(Math.cos(a)*r,2.5,Math.sin(a)*r);t.material=material('tree-'+i,.16,.38,.17);} - let last=performance.now(),uiLast=0;scene.onBeforeRenderObservable.add(()=>{const now=performance.now(),elapsed=Math.max(0,(now-last)/1000);last=now;const frame=loop.step(elapsed),snapshot=store.snapshot();for(const entity of snapshot.entities){createEntityMesh(entity);const mesh=meshes.get(entity.id);if(mesh){mesh.position.x=entity.position[0];mesh.position.z=entity.position[2];if(entity.kind==='infrastructure')mesh.material=(entity as InfrastructureEntity).operational?accessMat:unavailableMat;}const mobility=meshes.get(entity.id+'-mobility');if(mobility){mobility.position.x=entity.position[0];mobility.position.z=entity.position[2];}}if(now-uiLast>1000){uiLast=now;const maya=store.entity('maya') as PersonEntity|undefined,lift=store.entity('station-lift') as InfrastructureEntity|undefined;if(maya&&lift){const a=accessAssessment(maya,lift);setAccess(`TRANSIT ACCESS: ${a.pass?'AVAILABLE':'BARRIER'} · ${a.cause}`);const close=nearbyEntities(snapshot,'maya',6);setNearby(`NEARBY MAYA · ${close.length?close.map(x=>x.entity.label).join(' · '):'none within 6m'}`);}const activity=frame.activities.find(x=>x.personId==='maya');setLife(activity?`MAYA · ${activity.kind.toUpperCase()} · ${activity.rationale}`:'MAYA · NO ACTIVE PLAN');const bridge=frame.clinical.find(x=>x.personId==='maya');setClinical(`CLINICAL BRIDGE · ${(bridge?.status??'continue-world').toUpperCase().replaceAll('-',' ')}`);}});try{const helper=await scene.createDefaultXRExperienceAsync({floorMeshes:[ground,path],disableTeleportation:false});setXr(helper.baseExperience?'VR READY · controller, gaze and teleport supported':'VR unavailable');}catch{setXr('WebXR not available on this browser/device');}setStatus('MULTI-PERSON WORLD ONLINE · WASD / mouse / WebXR');engine.runRenderLoop(()=>scene.render());const resize=()=>engine.resize();window.addEventListener('resize',resize);(scene as any).__cleanup=()=>window.removeEventListener('resize',resize);};boot();return()=>{disposed=true;scene?.__cleanup?.();scene?.dispose?.();engine?.dispose?.();};},[]); - const toggleLift=()=>{const s=storeRef.current!,lift=s.entity('station-lift') as InfrastructureEntity;s.setInfrastructure('station-lift',!lift.operational);};const toggleAAC=()=>{const s=storeRef.current!,maya=s.entity('maya') as PersonEntity;s.setCommunicationAccess('maya',maya.communication.access==='available'?'degraded':'available');};const interactNearest=()=>{const s=storeRef.current!,state=s.snapshot(),target=nearbyEntities(state,'maya',8)[0];if(!target){setStatus('INTERACTION · no nearby target');return;}const intent=createSpatialIntent('maya',target.entity.id,target.entity.kind==='person'?'communicate':'use','touch'),result=evaluateSpatialIntent(state,intent);s.recordEvent('world.intent', 'maya', `${intent.action} ${target.entity.label}: ${result.reason}`);setStatus(`INTERACTION · ${target.entity.label} · ${result.allowed?'AVAILABLE':'BLOCKED'} · ${result.reason}`);};const resetWorld=()=>{storeRef.current!.reset();setStatus('WORLD RESET · reload to reseed population');}; - return
MERT · LIVE DISABILITY WORLD
{status}
{xr}
{access}
{life}
{nearby}
{clinical}
One authoritative WorldRuntime now drives schedules, autonomous movement, affordances, VNN proposals, social interactions and the evidence-gated clinical bridge. VR, touch and other modalities resolve to semantic world intents rather than separate gameplay rules.
; + const canvasRef = useRef(null); + const storeRef = useRef(); + const runtimeRef = useRef(); + if (!storeRef.current) storeRef.current = new PersistentWorldStore(); + + const [status,setStatus]=useState('Loading world…'); + const [xr,setXr]=useState('Checking WebXR…'); + const [access,setAccess]=useState('ACCESS NETWORK ONLINE'); + const [life,setLife]=useState('DAILY LIFE ENGINE STARTING'); + const [clinical,setClinical]=useState('CLINICAL BRIDGE · STANDBY'); + const [nearby,setNearby]=useState('NEARBY · scanning'); + const [route,setRoute]=useState('ROUTE ENGINE · calculating'); + const [objects,setObjects]=useState('OBJECT ENGINE · online'); + const [encounters,setEncounters]=useState('SOCIAL ENCOUNTERS · scanning'); + const [continuity,setContinuity]=useState('CONTINUITY · community'); + + useEffect(()=>{ + let disposed=false,engine:any,scene:any,routeMesh:any; + const store=storeRef.current!; + const runtime=new WorldRuntime(store); + runtimeRef.current=runtime; + + const boot=async()=>{ + const B=await import('@babylonjs/core'); + await import('@babylonjs/loaders'); + if(!canvasRef.current||disposed)return; + + engine=new B.Engine(canvasRef.current,true,{preserveDrawingBuffer:true,stencil:true}); + scene=new B.Scene(engine); + scene.clearColor=new B.Color4(.72,.86,.96,1); + scene.collisionsEnabled=true; + + const camera=new B.UniversalCamera('player',new B.Vector3(0,1.65,-16),scene); + camera.attachControl(canvasRef.current,true); + camera.speed=.45;camera.angularSensibility=3500;camera.applyGravity=true;camera.checkCollisions=true; + camera.ellipsoid=new B.Vector3(.5,.9,.5); + camera.keysUp.push(87);camera.keysDown.push(83);camera.keysLeft.push(65);camera.keysRight.push(68); + + new B.HemisphericLight('sky',new B.Vector3(0,1,0),scene).intensity=.85; + new B.DirectionalLight('sun',new B.Vector3(-.4,-1,.25),scene).intensity=.7; + const material=(n:string,r:number,g:number,b:number)=>{const m=new B.StandardMaterial(n,scene);m.diffuseColor=new B.Color3(r,g,b);return m;}; + const ground=B.MeshBuilder.CreateGround('accessible-campus',{width:220,height:220},scene);ground.checkCollisions=true;ground.material=material('ground',.38,.55,.35); + const path=B.MeshBuilder.CreateGround('wide-accessible-path',{width:12,height:190},scene);path.position.y=.015;path.material=material('path',.62,.61,.57); + + const buildingMat=material('building',.72,.76,.78),glass=material('glass',.25,.55,.7);glass.alpha=.72; + const doorMeshes=new Map(); + const makeBuilding=(id:string,label:string,x:number,z:number,w:number,d:number,h:number)=>{ + const body=B.MeshBuilder.CreateBox(label,{width:w,depth:d,height:h},scene);body.position.set(x,h/2,z);body.material=buildingMat;body.checkCollisions=true; + const door=B.MeshBuilder.CreateBox(id,{width:3.2,depth:.12,height:2.7},scene);door.position.set(x,1.35,z-d/2-.07);door.material=glass;doorMeshes.set(id,{mesh:door,closedX:x}); + }; + makeBuilding('clinical-door','Clinical Simulation Centre',-24,8,28,20,10); + makeBuilding('community-door','Community Hub',25,22,26,18,7); + makeBuilding('rehab-door','Rehabilitation Lab',24,-28,24,18,8); + makeBuilding('transit-door','Accessible Transit Station',-25,-34,30,15,6); + + const personMat=material('people',.22,.32,.55),accessMat=material('access',.16,.48,.5),unavailableMat=material('unavailable',.48,.2,.2),meshes=new Map(); + const createEntityMesh=(entity:any)=>{ + if(meshes.has(entity.id))return; + if(entity.kind==='person'){ + const p=entity as PersonEntity,body=B.MeshBuilder.CreateCapsule(entity.id,{height:1.35,radius:.3},scene);body.position.set(p.position[0],1.05,p.position[2]);body.material=personMat;meshes.set(entity.id,body); + if(p.mobility.mode.includes('wheelchair')){const chair=B.MeshBuilder.CreateBox(entity.id+'-mobility',{width:.85,height:.55,depth:1.05},scene);chair.position.set(p.position[0],.38,p.position[2]);chair.material=personMat;meshes.set(entity.id+'-mobility',chair);} + }else if(entity.kind==='infrastructure'){ + const i=entity as InfrastructureEntity,marker=B.MeshBuilder.CreateCylinder(entity.id,{height:1.5,diameter:.65},scene);marker.position.set(i.position[0],.75,i.position[2]);marker.material=i.operational?accessMat:unavailableMat;meshes.set(i.id,marker); + } + }; + for(const entity of store.snapshot().entities)createEntityMesh(entity); + + const liftCabin=B.MeshBuilder.CreateBox('station-lift-cabin',{width:2.4,height:2.6,depth:2.4},scene);liftCabin.position.set(-20,1.3,-34);liftCabin.material=glass; + const charger=B.MeshBuilder.CreateCylinder('aac-charge-visual',{height:1.2,diameter:1.2},scene);charger.position.set(20,.6,20);charger.material=accessMat; + const shuttle=B.MeshBuilder.CreateBox('community-shuttle',{width:3,height:2.8,depth:7},scene);shuttle.position.set(-30,1.4,-39);shuttle.material=buildingMat; + + for(let i=0;i<28;i++){const a=i*2.399,r=46+(i%4)*9,t=B.MeshBuilder.CreateCylinder('tree-'+i,{height:5,diameterTop:.5,diameterBottom:2.4,tessellation:7},scene);t.position.set(Math.cos(a)*r,2.5,Math.sin(a)*r);t.material=material('tree-'+i,.16,.38,.17);} + + let last=performance.now(),uiLast=0,lastRouteKey=''; + scene.onBeforeRenderObservable.add(()=>{ + const now=performance.now(),elapsed=Math.max(0,(now-last)/1000);last=now; + const frame=runtime.step(elapsed),snapshot=store.snapshot(); + + for(const entity of snapshot.entities){ + createEntityMesh(entity); + const mesh=meshes.get(entity.id);if(mesh){mesh.position.x=entity.position[0];mesh.position.z=entity.position[2];if(entity.kind==='infrastructure')mesh.material=(entity as InfrastructureEntity).operational?accessMat:unavailableMat;} + const mobility=meshes.get(entity.id+'-mobility');if(mobility){mobility.position.x=entity.position[0];mobility.position.z=entity.position[2];} + } + + for(const door of frame.objects.doors){const record=doorMeshes.get(door.id);if(record)record.mesh.position.x=record.closedX+door.progress*2.7;} + liftCabin.position.y=1.3+frame.objects.lift.positionMetres; + const mayaCharge=frame.objects.chargers.find(session=>session.personId==='maya');if(mayaCharge)charger.scaling.y=.5+mayaCharge.currentPercent/100; + shuttle.position.y=frame.objects.transport.passengers.includes('maya')?1.55:1.4; + + const mayaRoute=frame.routes.maya; + const routeKey=mayaRoute?.nodeIds.join('>')??''; + if(routeKey!==lastRouteKey){lastRouteKey=routeKey;routeMesh?.dispose?.();if(mayaRoute&&mayaRoute.points.length>1){routeMesh=B.MeshBuilder.CreateLines('maya-accessible-route',{points:mayaRoute.points.map(p=>new B.Vector3(p[0],.08,p[2]))},scene);}} + + if(now-uiLast>1000){ + uiLast=now; + const maya=store.entity('maya') as PersonEntity|undefined,lift=store.entity('station-lift') as InfrastructureEntity|undefined; + if(maya&&lift){const a=accessAssessment(maya,lift);setAccess(`TRANSIT ACCESS: ${a.pass?'AVAILABLE':'BARRIER'} · ${a.cause}`);const close=nearbyEntities(snapshot,'maya',6);setNearby(`NEARBY MAYA · ${close.length?close.map(x=>x.entity.label).join(' · '):'none within 6m'}`);} + const activity=frame.activities.find(x=>x.personId==='maya');setLife(activity?`MAYA · ${activity.kind.toUpperCase()} · ${activity.rationale}`:'MAYA · NO ACTIVE PLAN'); + const bridge=frame.clinical.find(x=>x.personId==='maya');setClinical(`CLINICAL BRIDGE · ${(bridge?.status??'continue-world').toUpperCase().replace(/-/g,' ')}`); + setRoute(mayaRoute?`ACCESSIBLE ROUTE · ${mayaRoute.nodeIds.join(' → ')} · cost ${mayaRoute.cost.toFixed(1)} · alternatives computed`:'ACCESSIBLE ROUTE · no viable route represented'); + const doorSummary=frame.objects.doors.map(d=>`${d.id}:${d.phase}`).join(' · ');setObjects(`OBJECTS · ${doorSummary} · lift ${frame.objects.lift.phase}@${frame.objects.lift.positionMetres.toFixed(1)}m · shuttle ${frame.objects.transport.passengers.length}/${frame.objects.transport.capacity}`); + setEncounters(frame.encounters.length?`ENCOUNTERS · ${frame.encounters.map(e=>`${e.a}↔${e.b} ${e.status} ${e.score.toFixed(2)}${e.supportRequested?' support-requested':''}`).join(' · ')}`:'ENCOUNTERS · none above threshold'); + setContinuity(`CONTINUITY · ${frame.continuity.maya??'community'}`); + } + }); + + try{const helper=await scene.createDefaultXRExperienceAsync({floorMeshes:[ground,path],disableTeleportation:false});setXr(helper.baseExperience?'VR READY · controller, gaze and teleport supported':'VR unavailable');}catch{setXr('WebXR not available on this browser/device');} + setStatus('WOLFRAM-GROUNDED OPEN WORLD ONLINE'); + engine.runRenderLoop(()=>scene.render()); + const resize=()=>engine.resize();window.addEventListener('resize',resize);(scene as any).__cleanup=()=>window.removeEventListener('resize',resize); + }; + boot(); + return()=>{disposed=true;scene?.__cleanup?.();routeMesh?.dispose?.();scene?.dispose?.();engine?.dispose?.();}; + },[]); + + const runtime=()=>runtimeRef.current!; + const toggleLift=()=>{const s=storeRef.current!,lift=s.entity('station-lift') as InfrastructureEntity;s.setInfrastructure('station-lift',!lift.operational);}; + const toggleAAC=()=>{const s=storeRef.current!,maya=s.entity('maya') as PersonEntity;s.setCommunicationAccess('maya',maya.communication.access==='available'?'degraded':'available');}; + const interactNearest=()=>{const s=storeRef.current!,state=s.snapshot(),target=nearbyEntities(state,'maya',8)[0];if(!target){setStatus('INTERACTION · no nearby target');return;}const intent=createSpatialIntent('maya',target.entity.id,target.entity.kind==='person'?'communicate':'use','touch'),result=evaluateSpatialIntent(state,intent);s.recordEvent('world.intent','maya',`${intent.action} ${target.entity.label}: ${result.reason}`);setStatus(`INTERACTION · ${target.entity.label} · ${result.allowed?'AVAILABLE':'BLOCKED'} · ${result.reason}`);}; + const openDoor=()=>runtime().objectEngine().requestDoor('community-door','maya'); + const moveLift=()=>{const lift=runtime().objectEngine().snapshot().lift;runtime().objectEngine().requestLift(lift.currentLevel===0?1:0,'maya');}; + const chargeAAC=()=>runtime().objectEngine().startCharging('maya',30); + const boardTransport=()=>{const result=runtime().objectEngine().boardTransport('maya');setStatus(`TRANSPORT · ${result.reason}`);}; + const requestSupport=()=>{runtime().requestSupport('maya','liam');setStatus('SUPPORT · Maya explicitly requested support from Liam');}; + const beginContinuity=()=>{runtime().beginContinuity('maya');setStatus('CONTINUITY · educational community deterioration episode started');}; + const advanceContinuity=()=>{runtime().advanceContinuity('maya');setStatus(`CONTINUITY · ${runtime().continuity().summary('maya')}`);}; + const resetWorld=()=>{storeRef.current!.reset();setStatus('WORLD RESET · reload to reseed population');}; + + return
+ +
+ MERT · WOLFRAM OPEN WORLD · STEPS 21–24
+ {status}
{xr}
{access}
{life}
{route}
{objects}
{nearby}
{encounters}
{clinical}
{continuity} +
+ + + + + + + + + + + +
+
+
+ Wolfram computation layer
+ Route cost: {WOLFRAM_ROUTE_MODEL.formula}. Lift: {WOLFRAM_OBJECT_MODELS.liftSmoothstep}. Charger: {WOLFRAM_OBJECT_MODELS.charger}. Social encounters: {WOLFRAM_SOCIAL_MODEL.formula}. Continuity graph verified acyclic with {WOLFRAM_CONTINUITY_MODEL.unexpectedDeadEnds} unexpected dead ends. Simulation parameters are educational model values, not accessibility-code or clinical-treatment thresholds. +
+
; } From 02870d24b78cc022519248f5b46f4fb81abb6be3 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:54:47 +1000 Subject: [PATCH 36/46] Test Wolfram-grounded Steps 21-24 algorithms --- __tests__/worldSteps21to24.test.ts | 61 ++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 __tests__/worldSteps21to24.test.ts diff --git a/__tests__/worldSteps21to24.test.ts b/__tests__/worldSteps21to24.test.ts new file mode 100644 index 0000000..5a07549 --- /dev/null +++ b/__tests__/worldSteps21to24.test.ts @@ -0,0 +1,61 @@ +import { defaultNavigationNetwork, findAccessibleRoutes, navigationProfile } from '../src/world/accessibleNavigation'; +import { chargerPercent, ObjectInteractionEngine, smoothstep } from '../src/world/objectInteractions'; +import { encounterScore, SocialEncounterEngine } from '../src/world/socialEncounters'; +import { ContinuityEngine } from '../src/world/continuity'; +import { initialOpenWorldState, PersistentWorldStore, type InfrastructureEntity, type PersonEntity } from '../src/world/worldModel'; + +describe('Steps 21-24 Wolfram-grounded world algorithms', () => { + test('accessible routes are ordered by cost and failed infrastructure is environmental', () => { + const store = new PersistentWorldStore(initialOpenWorldState); + const maya = store.entity('maya') as PersonEntity; + expect(navigationProfile(maya).mobility).toBe('powered-wheelchair'); + const routes = findAccessibleRoutes(defaultNavigationNetwork, store.snapshot(), maya, 'plaza', 'transit', 3); + expect(routes.length).toBeGreaterThan(0); + for (let i = 1; i < routes.length; i++) expect(routes[i]!.cost).toBeGreaterThanOrEqual(routes[i-1]!.cost); + const lift = store.entity('station-lift') as InfrastructureEntity; + store.setInfrastructure(lift.id, false); + const blocked = findAccessibleRoutes(defaultNavigationNetwork, store.snapshot(), maya, 'plaza', 'transit', 3); + expect(blocked.every(route => !route.nodeIds.includes('station-lift'))).toBe(true); + expect((store.entity('maya') as PersonEntity).authority).toBe('self'); + }); + + test('smoothstep lift model has stationary endpoints', () => { + expect(smoothstep(0)).toBe(0); + expect(smoothstep(1)).toBe(1); + const engine = new ObjectInteractionEngine(); + expect(engine.requestLift(1, 'maya')).toBe(true); + engine.step(4); + expect(engine.snapshot().lift.positionMetres).toBeCloseTo(2.1, 5); + engine.step(4); + expect(engine.snapshot().lift.positionMetres).toBeCloseTo(4.2, 5); + }); + + test('Wolfram-derived charger curve reaches approximately 80 percent at computed time', () => { + expect(chargerPercent(30, 100, .0015, 835.1753123302453)).toBeCloseTo(80, 5); + }); + + test('relationship and proximity affect encounters while support remains explicit', () => { + expect(encounterScore(1.5, 1, 1, .2)).toBeGreaterThan(encounterScore(1.5, 0, 0, .2)); + const store = new PersistentWorldStore(initialOpenWorldState); + const engine = new SocialEncounterEngine(); + const before = engine.detect(store.snapshot()).encounters; + expect(before.every(encounter => encounter.supportRequested === false)).toBe(true); + engine.requestSupport('maya', 'liam'); + }); + + test('continuity preserves authority and rejects non-adjacent jumps', () => { + const store = new PersistentWorldStore(initialOpenWorldState); + const maya = store.entity('maya') as PersonEntity; + const engine = new ContinuityEngine(store); + engine.start(maya, 'educational test'); + expect(engine.episode('maya')?.preserved.authority).toBe('self'); + expect(engine.advance('maya', 'icu')).toBe(false); + expect(engine.advance('maya', 'ed')).toBe(true); + expect(engine.advance('maya', 'icu')).toBe(true); + expect(engine.advance('maya', 'mert-scenario')).toBe(true); + expect(engine.advance('maya', 'discharge')).toBe(true); + expect(engine.advance('maya', 'community-return')).toBe(true); + expect(engine.advance('maya', 'community-with-consequences')).toBe(true); + expect((store.entity('maya') as PersonEntity).authority).toBe('self'); + }); +}); From 0321609226167744707a1c3eda9f210c78cbc308 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:57:30 +1000 Subject: [PATCH 37/46] Fix strict ref typing in WebXR world --- src/world/OpenWorldWeb.tsx | 109 ++++--------------------------------- 1 file changed, 12 insertions(+), 97 deletions(-) diff --git a/src/world/OpenWorldWeb.tsx b/src/world/OpenWorldWeb.tsx index 17745fc..d435bd5 100644 --- a/src/world/OpenWorldWeb.tsx +++ b/src/world/OpenWorldWeb.tsx @@ -9,8 +9,8 @@ import { WOLFRAM_CONTINUITY_MODEL } from './continuity'; export function OpenWorldWeb() { const canvasRef = useRef(null); - const storeRef = useRef(); - const runtimeRef = useRef(); + const storeRef = useRef(null); + const runtimeRef = useRef(null); if (!storeRef.current) storeRef.current = new PersistentWorldStore(); const [status,setStatus]=useState('Loading world…'); @@ -34,93 +34,31 @@ export function OpenWorldWeb() { const B=await import('@babylonjs/core'); await import('@babylonjs/loaders'); if(!canvasRef.current||disposed)return; - engine=new B.Engine(canvasRef.current,true,{preserveDrawingBuffer:true,stencil:true}); - scene=new B.Scene(engine); - scene.clearColor=new B.Color4(.72,.86,.96,1); - scene.collisionsEnabled=true; - - const camera=new B.UniversalCamera('player',new B.Vector3(0,1.65,-16),scene); - camera.attachControl(canvasRef.current,true); - camera.speed=.45;camera.angularSensibility=3500;camera.applyGravity=true;camera.checkCollisions=true; - camera.ellipsoid=new B.Vector3(.5,.9,.5); - camera.keysUp.push(87);camera.keysDown.push(83);camera.keysLeft.push(65);camera.keysRight.push(68); - - new B.HemisphericLight('sky',new B.Vector3(0,1,0),scene).intensity=.85; - new B.DirectionalLight('sun',new B.Vector3(-.4,-1,.25),scene).intensity=.7; + scene=new B.Scene(engine);scene.clearColor=new B.Color4(.72,.86,.96,1);scene.collisionsEnabled=true; + const camera=new B.UniversalCamera('player',new B.Vector3(0,1.65,-16),scene);camera.attachControl(canvasRef.current,true);camera.speed=.45;camera.angularSensibility=3500;camera.applyGravity=true;camera.checkCollisions=true;camera.ellipsoid=new B.Vector3(.5,.9,.5);camera.keysUp.push(87);camera.keysDown.push(83);camera.keysLeft.push(65);camera.keysRight.push(68); + new B.HemisphericLight('sky',new B.Vector3(0,1,0),scene).intensity=.85;new B.DirectionalLight('sun',new B.Vector3(-.4,-1,.25),scene).intensity=.7; const material=(n:string,r:number,g:number,b:number)=>{const m=new B.StandardMaterial(n,scene);m.diffuseColor=new B.Color3(r,g,b);return m;}; const ground=B.MeshBuilder.CreateGround('accessible-campus',{width:220,height:220},scene);ground.checkCollisions=true;ground.material=material('ground',.38,.55,.35); const path=B.MeshBuilder.CreateGround('wide-accessible-path',{width:12,height:190},scene);path.position.y=.015;path.material=material('path',.62,.61,.57); - const buildingMat=material('building',.72,.76,.78),glass=material('glass',.25,.55,.7);glass.alpha=.72; const doorMeshes=new Map(); - const makeBuilding=(id:string,label:string,x:number,z:number,w:number,d:number,h:number)=>{ - const body=B.MeshBuilder.CreateBox(label,{width:w,depth:d,height:h},scene);body.position.set(x,h/2,z);body.material=buildingMat;body.checkCollisions=true; - const door=B.MeshBuilder.CreateBox(id,{width:3.2,depth:.12,height:2.7},scene);door.position.set(x,1.35,z-d/2-.07);door.material=glass;doorMeshes.set(id,{mesh:door,closedX:x}); - }; - makeBuilding('clinical-door','Clinical Simulation Centre',-24,8,28,20,10); - makeBuilding('community-door','Community Hub',25,22,26,18,7); - makeBuilding('rehab-door','Rehabilitation Lab',24,-28,24,18,8); - makeBuilding('transit-door','Accessible Transit Station',-25,-34,30,15,6); - + const makeBuilding=(id:string,label:string,x:number,z:number,w:number,d:number,h:number)=>{const body=B.MeshBuilder.CreateBox(label,{width:w,depth:d,height:h},scene);body.position.set(x,h/2,z);body.material=buildingMat;body.checkCollisions=true;const door=B.MeshBuilder.CreateBox(id,{width:3.2,depth:.12,height:2.7},scene);door.position.set(x,1.35,z-d/2-.07);door.material=glass;doorMeshes.set(id,{mesh:door,closedX:x});}; + makeBuilding('clinical-door','Clinical Simulation Centre',-24,8,28,20,10);makeBuilding('community-door','Community Hub',25,22,26,18,7);makeBuilding('rehab-door','Rehabilitation Lab',24,-28,24,18,8);makeBuilding('transit-door','Accessible Transit Station',-25,-34,30,15,6); const personMat=material('people',.22,.32,.55),accessMat=material('access',.16,.48,.5),unavailableMat=material('unavailable',.48,.2,.2),meshes=new Map(); - const createEntityMesh=(entity:any)=>{ - if(meshes.has(entity.id))return; - if(entity.kind==='person'){ - const p=entity as PersonEntity,body=B.MeshBuilder.CreateCapsule(entity.id,{height:1.35,radius:.3},scene);body.position.set(p.position[0],1.05,p.position[2]);body.material=personMat;meshes.set(entity.id,body); - if(p.mobility.mode.includes('wheelchair')){const chair=B.MeshBuilder.CreateBox(entity.id+'-mobility',{width:.85,height:.55,depth:1.05},scene);chair.position.set(p.position[0],.38,p.position[2]);chair.material=personMat;meshes.set(entity.id+'-mobility',chair);} - }else if(entity.kind==='infrastructure'){ - const i=entity as InfrastructureEntity,marker=B.MeshBuilder.CreateCylinder(entity.id,{height:1.5,diameter:.65},scene);marker.position.set(i.position[0],.75,i.position[2]);marker.material=i.operational?accessMat:unavailableMat;meshes.set(i.id,marker); - } - }; + const createEntityMesh=(entity:any)=>{if(meshes.has(entity.id))return;if(entity.kind==='person'){const p=entity as PersonEntity,body=B.MeshBuilder.CreateCapsule(entity.id,{height:1.35,radius:.3},scene);body.position.set(p.position[0],1.05,p.position[2]);body.material=personMat;meshes.set(entity.id,body);if(p.mobility.mode.includes('wheelchair')){const chair=B.MeshBuilder.CreateBox(entity.id+'-mobility',{width:.85,height:.55,depth:1.05},scene);chair.position.set(p.position[0],.38,p.position[2]);chair.material=personMat;meshes.set(entity.id+'-mobility',chair);}}else if(entity.kind==='infrastructure'){const i=entity as InfrastructureEntity,marker=B.MeshBuilder.CreateCylinder(entity.id,{height:1.5,diameter:.65},scene);marker.position.set(i.position[0],.75,i.position[2]);marker.material=i.operational?accessMat:unavailableMat;meshes.set(i.id,marker);}}; for(const entity of store.snapshot().entities)createEntityMesh(entity); - const liftCabin=B.MeshBuilder.CreateBox('station-lift-cabin',{width:2.4,height:2.6,depth:2.4},scene);liftCabin.position.set(-20,1.3,-34);liftCabin.material=glass; const charger=B.MeshBuilder.CreateCylinder('aac-charge-visual',{height:1.2,diameter:1.2},scene);charger.position.set(20,.6,20);charger.material=accessMat; const shuttle=B.MeshBuilder.CreateBox('community-shuttle',{width:3,height:2.8,depth:7},scene);shuttle.position.set(-30,1.4,-39);shuttle.material=buildingMat; - for(let i=0;i<28;i++){const a=i*2.399,r=46+(i%4)*9,t=B.MeshBuilder.CreateCylinder('tree-'+i,{height:5,diameterTop:.5,diameterBottom:2.4,tessellation:7},scene);t.position.set(Math.cos(a)*r,2.5,Math.sin(a)*r);t.material=material('tree-'+i,.16,.38,.17);} let last=performance.now(),uiLast=0,lastRouteKey=''; - scene.onBeforeRenderObservable.add(()=>{ - const now=performance.now(),elapsed=Math.max(0,(now-last)/1000);last=now; - const frame=runtime.step(elapsed),snapshot=store.snapshot(); - - for(const entity of snapshot.entities){ - createEntityMesh(entity); - const mesh=meshes.get(entity.id);if(mesh){mesh.position.x=entity.position[0];mesh.position.z=entity.position[2];if(entity.kind==='infrastructure')mesh.material=(entity as InfrastructureEntity).operational?accessMat:unavailableMat;} - const mobility=meshes.get(entity.id+'-mobility');if(mobility){mobility.position.x=entity.position[0];mobility.position.z=entity.position[2];} - } - - for(const door of frame.objects.doors){const record=doorMeshes.get(door.id);if(record)record.mesh.position.x=record.closedX+door.progress*2.7;} - liftCabin.position.y=1.3+frame.objects.lift.positionMetres; - const mayaCharge=frame.objects.chargers.find(session=>session.personId==='maya');if(mayaCharge)charger.scaling.y=.5+mayaCharge.currentPercent/100; - shuttle.position.y=frame.objects.transport.passengers.includes('maya')?1.55:1.4; - - const mayaRoute=frame.routes.maya; - const routeKey=mayaRoute?.nodeIds.join('>')??''; - if(routeKey!==lastRouteKey){lastRouteKey=routeKey;routeMesh?.dispose?.();if(mayaRoute&&mayaRoute.points.length>1){routeMesh=B.MeshBuilder.CreateLines('maya-accessible-route',{points:mayaRoute.points.map(p=>new B.Vector3(p[0],.08,p[2]))},scene);}} - - if(now-uiLast>1000){ - uiLast=now; - const maya=store.entity('maya') as PersonEntity|undefined,lift=store.entity('station-lift') as InfrastructureEntity|undefined; - if(maya&&lift){const a=accessAssessment(maya,lift);setAccess(`TRANSIT ACCESS: ${a.pass?'AVAILABLE':'BARRIER'} · ${a.cause}`);const close=nearbyEntities(snapshot,'maya',6);setNearby(`NEARBY MAYA · ${close.length?close.map(x=>x.entity.label).join(' · '):'none within 6m'}`);} - const activity=frame.activities.find(x=>x.personId==='maya');setLife(activity?`MAYA · ${activity.kind.toUpperCase()} · ${activity.rationale}`:'MAYA · NO ACTIVE PLAN'); - const bridge=frame.clinical.find(x=>x.personId==='maya');setClinical(`CLINICAL BRIDGE · ${(bridge?.status??'continue-world').toUpperCase().replace(/-/g,' ')}`); - setRoute(mayaRoute?`ACCESSIBLE ROUTE · ${mayaRoute.nodeIds.join(' → ')} · cost ${mayaRoute.cost.toFixed(1)} · alternatives computed`:'ACCESSIBLE ROUTE · no viable route represented'); - const doorSummary=frame.objects.doors.map(d=>`${d.id}:${d.phase}`).join(' · ');setObjects(`OBJECTS · ${doorSummary} · lift ${frame.objects.lift.phase}@${frame.objects.lift.positionMetres.toFixed(1)}m · shuttle ${frame.objects.transport.passengers.length}/${frame.objects.transport.capacity}`); - setEncounters(frame.encounters.length?`ENCOUNTERS · ${frame.encounters.map(e=>`${e.a}↔${e.b} ${e.status} ${e.score.toFixed(2)}${e.supportRequested?' support-requested':''}`).join(' · ')}`:'ENCOUNTERS · none above threshold'); - setContinuity(`CONTINUITY · ${frame.continuity.maya??'community'}`); - } - }); - + scene.onBeforeRenderObservable.add(()=>{const now=performance.now(),elapsed=Math.max(0,(now-last)/1000);last=now;const frame=runtime.step(elapsed),snapshot=store.snapshot();for(const entity of snapshot.entities){createEntityMesh(entity);const mesh=meshes.get(entity.id);if(mesh){mesh.position.x=entity.position[0];mesh.position.z=entity.position[2];if(entity.kind==='infrastructure')mesh.material=(entity as InfrastructureEntity).operational?accessMat:unavailableMat;}const mobility=meshes.get(entity.id+'-mobility');if(mobility){mobility.position.x=entity.position[0];mobility.position.z=entity.position[2];}}for(const door of frame.objects.doors){const record=doorMeshes.get(door.id);if(record)record.mesh.position.x=record.closedX+door.progress*2.7;}liftCabin.position.y=1.3+frame.objects.lift.positionMetres;const mayaCharge=frame.objects.chargers.find(session=>session.personId==='maya');if(mayaCharge)charger.scaling.y=.5+mayaCharge.currentPercent/100;shuttle.position.y=frame.objects.transport.passengers.includes('maya')?1.55:1.4;const mayaRoute=frame.routes.maya,routeKey=mayaRoute?.nodeIds.join('>')??'';if(routeKey!==lastRouteKey){lastRouteKey=routeKey;routeMesh?.dispose?.();if(mayaRoute&&mayaRoute.points.length>1)routeMesh=B.MeshBuilder.CreateLines('maya-accessible-route',{points:mayaRoute.points.map(p=>new B.Vector3(p[0],.08,p[2]))},scene);}if(now-uiLast>1000){uiLast=now;const maya=store.entity('maya') as PersonEntity|undefined,lift=store.entity('station-lift') as InfrastructureEntity|undefined;if(maya&&lift){const a=accessAssessment(maya,lift);setAccess(`TRANSIT ACCESS: ${a.pass?'AVAILABLE':'BARRIER'} · ${a.cause}`);const close=nearbyEntities(snapshot,'maya',6);setNearby(`NEARBY MAYA · ${close.length?close.map(x=>x.entity.label).join(' · '):'none within 6m'}`);}const activity=frame.activities.find(x=>x.personId==='maya');setLife(activity?`MAYA · ${activity.kind.toUpperCase()} · ${activity.rationale}`:'MAYA · NO ACTIVE PLAN');const bridge=frame.clinical.find(x=>x.personId==='maya');setClinical(`CLINICAL BRIDGE · ${(bridge?.status??'continue-world').toUpperCase().replace(/-/g,' ')}`);setRoute(mayaRoute?`ACCESSIBLE ROUTE · ${mayaRoute.nodeIds.join(' → ')} · cost ${mayaRoute.cost.toFixed(1)} · alternatives computed`:'ACCESSIBLE ROUTE · no viable route represented');const doorSummary=frame.objects.doors.map(d=>`${d.id}:${d.phase}`).join(' · ');setObjects(`OBJECTS · ${doorSummary} · lift ${frame.objects.lift.phase}@${frame.objects.lift.positionMetres.toFixed(1)}m · shuttle ${frame.objects.transport.passengers.length}/${frame.objects.transport.capacity}`);setEncounters(frame.encounters.length?`ENCOUNTERS · ${frame.encounters.map(e=>`${e.a}↔${e.b} ${e.status} ${e.score.toFixed(2)}${e.supportRequested?' support-requested':''}`).join(' · ')}`:'ENCOUNTERS · none above threshold');setContinuity(`CONTINUITY · ${frame.continuity.maya??'community'}`);}}); try{const helper=await scene.createDefaultXRExperienceAsync({floorMeshes:[ground,path],disableTeleportation:false});setXr(helper.baseExperience?'VR READY · controller, gaze and teleport supported':'VR unavailable');}catch{setXr('WebXR not available on this browser/device');} - setStatus('WOLFRAM-GROUNDED OPEN WORLD ONLINE'); - engine.runRenderLoop(()=>scene.render()); - const resize=()=>engine.resize();window.addEventListener('resize',resize);(scene as any).__cleanup=()=>window.removeEventListener('resize',resize); + setStatus('WOLFRAM-GROUNDED OPEN WORLD ONLINE');engine.runRenderLoop(()=>scene.render());const resize=()=>engine.resize();window.addEventListener('resize',resize);(scene as any).__cleanup=()=>window.removeEventListener('resize',resize); }; - boot(); - return()=>{disposed=true;scene?.__cleanup?.();routeMesh?.dispose?.();scene?.dispose?.();engine?.dispose?.();}; + boot();return()=>{disposed=true;scene?.__cleanup?.();routeMesh?.dispose?.();scene?.dispose?.();engine?.dispose?.();}; },[]); const runtime=()=>runtimeRef.current!; @@ -136,28 +74,5 @@ export function OpenWorldWeb() { const advanceContinuity=()=>{runtime().advanceContinuity('maya');setStatus(`CONTINUITY · ${runtime().continuity().summary('maya')}`);}; const resetWorld=()=>{storeRef.current!.reset();setStatus('WORLD RESET · reload to reseed population');}; - return
- -
- MERT · WOLFRAM OPEN WORLD · STEPS 21–24
- {status}
{xr}
{access}
{life}
{route}
{objects}
{nearby}
{encounters}
{clinical}
{continuity} -
- - - - - - - - - - - -
-
-
- Wolfram computation layer
- Route cost: {WOLFRAM_ROUTE_MODEL.formula}. Lift: {WOLFRAM_OBJECT_MODELS.liftSmoothstep}. Charger: {WOLFRAM_OBJECT_MODELS.charger}. Social encounters: {WOLFRAM_SOCIAL_MODEL.formula}. Continuity graph verified acyclic with {WOLFRAM_CONTINUITY_MODEL.unexpectedDeadEnds} unexpected dead ends. Simulation parameters are educational model values, not accessibility-code or clinical-treatment thresholds. -
-
; + return
MERT · WOLFRAM OPEN WORLD · STEPS 21–24
{status}
{xr}
{access}
{life}
{route}
{objects}
{nearby}
{encounters}
{clinical}
{continuity}
Wolfram computation layer
Route cost: {WOLFRAM_ROUTE_MODEL.formula}. Lift: {WOLFRAM_OBJECT_MODELS.liftSmoothstep}. Charger: {WOLFRAM_OBJECT_MODELS.charger}. Social encounters: {WOLFRAM_SOCIAL_MODEL.formula}. Continuity graph verified acyclic with {WOLFRAM_CONTINUITY_MODEL.unexpectedDeadEnds} unexpected dead ends. Simulation parameters are educational model values, not accessibility-code or clinical-treatment thresholds.
; } From 90d7f95dfbe65540375b376b37ae2d666b74ccaf Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:57:59 +1000 Subject: [PATCH 38/46] Fix strict navigation typing --- src/world/accessibleNavigation.ts | 149 +++++++----------------------- 1 file changed, 32 insertions(+), 117 deletions(-) diff --git a/src/world/accessibleNavigation.ts b/src/world/accessibleNavigation.ts index 41fc038..54d00dd 100644 --- a/src/world/accessibleNavigation.ts +++ b/src/world/accessibleNavigation.ts @@ -1,12 +1,5 @@ -import type { AccessModality, PersonEntity, PersistentWorldState, Vec3 } from './worldModel'; +import type { AccessModality, InfrastructureEntity, PersonEntity, PersistentWorldState, Vec3 } from './worldModel'; -/** - * Wolfram-checked cost model used here: - * cost = length * (1 + gradientWeight * |gradient| + surfacePenalty) + waitSeconds - * Unsupported gradients / modes are excluded, not attributed to the person. - * Example evaluation produced route costs 91.52 and 135.82 for two viable alternatives. - * These defaults are simulation tuning parameters, not building-code thresholds. - */ export const WOLFRAM_ROUTE_MODEL = { formula: 'length*(1 + gradientWeight*abs(gradient) + surfacePenalty) + waitSeconds', exampleBestCost: 91.52, @@ -14,117 +7,39 @@ export const WOLFRAM_ROUTE_MODEL = { }; export type RouteFeature = 'path' | 'door' | 'ramp' | 'lift' | 'crossing' | 'transport-link'; - export interface NavNode { id: string; position: Vec3; label: string; } -export interface NavEdge { - from: string; - to: string; - length: number; - gradient: number; - surfacePenalty: number; - waitSeconds: number; - feature: RouteFeature; - access: AccessModality[]; - objectId?: string; -} - -export interface NavigationProfile { - mobility: AccessModality; - maxGradient: number; - gradientWeight: number; - blockedPenalty: number; -} - -export interface RouteResult { - nodeIds: string[]; - points: Vec3[]; - cost: number; - features: RouteFeature[]; - blockedBy: string[]; -} - -export interface NavigationNetwork { nodes: NavNode[]; edges: NavEdge[]; } - -export const defaultNavigationNetwork: NavigationNetwork = { - nodes: [ - { id:'plaza', label:'Central plaza', position:[0,0,0] }, - { id:'community-door', label:'Community automatic door', position:[14,0,14] }, - { id:'community-hub', label:'Community Hub', position:[25,0,22] }, - { id:'clinical-ramp', label:'Clinical ramp', position:[-11,0,4] }, - { id:'clinical-door', label:'Clinical automatic door', position:[-18,0,8] }, - { id:'clinical-centre', label:'Clinical Simulation Centre', position:[-24,0,8] }, - { id:'crossing', label:'Accessible crossing', position:[-8,0,-15] }, - { id:'station-lift', label:'Station lift', position:[-20,0,-34] }, - { id:'transit', label:'Accessible Transit Station', position:[-25,0,-34] }, - { id:'rehab-ramp', label:'Rehabilitation ramp', position:[10,0,-13] }, - { id:'rehab-lab', label:'Rehabilitation Lab', position:[24,0,-28] }, +export interface NavEdge { from:string; to:string; length:number; gradient:number; surfacePenalty:number; waitSeconds:number; feature:RouteFeature; access:AccessModality[]; objectId?:string; } +export interface NavigationProfile { mobility:AccessModality; maxGradient:number; gradientWeight:number; blockedPenalty:number; } +export interface RouteResult { nodeIds:string[]; points:Vec3[]; cost:number; features:RouteFeature[]; blockedBy:string[]; } +export interface NavigationNetwork { nodes:NavNode[]; edges:NavEdge[]; } + +export const defaultNavigationNetwork:NavigationNetwork={ + nodes:[ + {id:'plaza',label:'Central plaza',position:[0,0,0]},{id:'community-door',label:'Community automatic door',position:[14,0,14]},{id:'community-hub',label:'Community Hub',position:[25,0,22]}, + {id:'clinical-ramp',label:'Clinical ramp',position:[-11,0,4]},{id:'clinical-door',label:'Clinical automatic door',position:[-18,0,8]},{id:'clinical-centre',label:'Clinical Simulation Centre',position:[-24,0,8]}, + {id:'crossing',label:'Accessible crossing',position:[-8,0,-15]},{id:'station-lift',label:'Station lift',position:[-20,0,-34]},{id:'transit',label:'Accessible Transit Station',position:[-25,0,-34]}, + {id:'rehab-ramp',label:'Rehabilitation ramp',position:[10,0,-13]},{id:'rehab-lab',label:'Rehabilitation Lab',position:[24,0,-28]}, ], - edges: [ - { from:'plaza',to:'community-door',length:20,gradient:.02,surfacePenalty:.02,waitSeconds:0,feature:'path',access:['walk','powered-wheelchair','manual-wheelchair'] }, - { from:'community-door',to:'community-hub',length:14,gradient:0,surfacePenalty:0,waitSeconds:2,feature:'door',objectId:'community-door',access:['walk','powered-wheelchair','manual-wheelchair'] }, - { from:'plaza',to:'clinical-ramp',length:12,gradient:.04,surfacePenalty:.01,waitSeconds:0,feature:'ramp',access:['walk','powered-wheelchair','manual-wheelchair'] }, - { from:'clinical-ramp',to:'clinical-door',length:8,gradient:.06,surfacePenalty:0,waitSeconds:0,feature:'ramp',access:['walk','powered-wheelchair','manual-wheelchair'] }, - { from:'clinical-door',to:'clinical-centre',length:7,gradient:0,surfacePenalty:0,waitSeconds:2,feature:'door',objectId:'clinical-door',access:['walk','powered-wheelchair','manual-wheelchair'] }, - { from:'plaza',to:'crossing',length:18,gradient:.01,surfacePenalty:.03,waitSeconds:8,feature:'crossing',objectId:'crossing',access:['walk','powered-wheelchair','manual-wheelchair'] }, - { from:'crossing',to:'station-lift',length:25,gradient:.03,surfacePenalty:.02,waitSeconds:0,feature:'path',access:['walk','powered-wheelchair','manual-wheelchair'] }, - { from:'station-lift',to:'transit',length:6,gradient:0,surfacePenalty:0,waitSeconds:18,feature:'lift',objectId:'station-lift',access:['walk','powered-wheelchair','manual-wheelchair'] }, - { from:'plaza',to:'rehab-ramp',length:17,gradient:.035,surfacePenalty:.02,waitSeconds:0,feature:'ramp',access:['walk','powered-wheelchair','manual-wheelchair'] }, - { from:'rehab-ramp',to:'rehab-lab',length:22,gradient:.055,surfacePenalty:.01,waitSeconds:0,feature:'ramp',access:['walk','powered-wheelchair','manual-wheelchair'] }, - { from:'community-hub',to:'transit',length:62,gradient:.025,surfacePenalty:.04,waitSeconds:6,feature:'transport-link',access:['walk','powered-wheelchair','manual-wheelchair'] }, - { from:'community-hub',to:'rehab-lab',length:54,gradient:.03,surfacePenalty:.03,waitSeconds:0,feature:'path',access:['walk','powered-wheelchair','manual-wheelchair'] }, + edges:[ + {from:'plaza',to:'community-door',length:20,gradient:.02,surfacePenalty:.02,waitSeconds:0,feature:'path',access:['walk','powered-wheelchair','manual-wheelchair']}, + {from:'community-door',to:'community-hub',length:14,gradient:0,surfacePenalty:0,waitSeconds:2,feature:'door',objectId:'community-door',access:['walk','powered-wheelchair','manual-wheelchair']}, + {from:'plaza',to:'clinical-ramp',length:12,gradient:.04,surfacePenalty:.01,waitSeconds:0,feature:'ramp',access:['walk','powered-wheelchair','manual-wheelchair']}, + {from:'clinical-ramp',to:'clinical-door',length:8,gradient:.06,surfacePenalty:0,waitSeconds:0,feature:'ramp',access:['walk','powered-wheelchair','manual-wheelchair']}, + {from:'clinical-door',to:'clinical-centre',length:7,gradient:0,surfacePenalty:0,waitSeconds:2,feature:'door',objectId:'clinical-door',access:['walk','powered-wheelchair','manual-wheelchair']}, + {from:'plaza',to:'crossing',length:18,gradient:.01,surfacePenalty:.03,waitSeconds:8,feature:'crossing',objectId:'crossing',access:['walk','powered-wheelchair','manual-wheelchair']}, + {from:'crossing',to:'station-lift',length:25,gradient:.03,surfacePenalty:.02,waitSeconds:0,feature:'path',access:['walk','powered-wheelchair','manual-wheelchair']}, + {from:'station-lift',to:'transit',length:6,gradient:0,surfacePenalty:0,waitSeconds:18,feature:'lift',objectId:'station-lift',access:['walk','powered-wheelchair','manual-wheelchair']}, + {from:'plaza',to:'rehab-ramp',length:17,gradient:.035,surfacePenalty:.02,waitSeconds:0,feature:'ramp',access:['walk','powered-wheelchair','manual-wheelchair']}, + {from:'rehab-ramp',to:'rehab-lab',length:22,gradient:.055,surfacePenalty:.01,waitSeconds:0,feature:'ramp',access:['walk','powered-wheelchair','manual-wheelchair']}, + {from:'community-hub',to:'transit',length:62,gradient:.025,surfacePenalty:.04,waitSeconds:6,feature:'transport-link',access:['walk','powered-wheelchair','manual-wheelchair']}, + {from:'community-hub',to:'rehab-lab',length:54,gradient:.03,surfacePenalty:.03,waitSeconds:0,feature:'path',access:['walk','powered-wheelchair','manual-wheelchair']}, ], }; -export function navigationProfile(person: PersonEntity): NavigationProfile { - const mobility = person.mobility.mode; - const maxGradient = mobility === 'manual-wheelchair' ? .07 : mobility === 'powered-wheelchair' ? .09 : .16; - return { mobility, maxGradient, gradientWeight: 4, blockedPenalty: 1_000_000 }; -} - -function edgeOperational(edge: NavEdge, state: PersistentWorldState) { - if (!edge.objectId) return true; - const entity = state.entities.find((item) => item.id === edge.objectId); - if (!entity || entity.kind !== 'infrastructure') return true; - return entity.operational; -} - -export function edgeCost(edge: NavEdge, person: PersonEntity, state: PersistentWorldState) { - const profile = navigationProfile(person); - if (!edge.access.includes(profile.mobility)) return Infinity; - if (Math.abs(edge.gradient) > profile.maxGradient) return Infinity; - if (!edgeOperational(edge, state)) return Infinity; - return edge.length * (1 + profile.gradientWeight * Math.abs(edge.gradient) + edge.surfacePenalty) + edge.waitSeconds; -} - -function adjacency(network: NavigationNetwork) { - const map = new Map>(); - for (const edge of network.edges) { - map.set(edge.from, [...(map.get(edge.from) ?? []), { edge, next: edge.to }]); - map.set(edge.to, [...(map.get(edge.to) ?? []), { edge, next: edge.from }]); - } - return map; -} - -export function nearestNavNode(network: NavigationNetwork, position: Vec3) { - return [...network.nodes].sort((a,b)=>Math.hypot(a.position[0]-position[0],a.position[2]-position[2])-Math.hypot(b.position[0]-position[0],b.position[2]-position[2]))[0]; -} - -export function findAccessibleRoutes(network: NavigationNetwork, state: PersistentWorldState, person: PersonEntity, startId: string, goalId: string, limit=3): RouteResult[] { - const adj = adjacency(network); const results: RouteResult[] = []; const maxDepth = network.nodes.length; - const visit = (node:string,path:string[],cost:number,features:RouteFeature[]) => { - if (path.length > maxDepth || results.length > 200) return; - if (node === goalId) { const points=path.map(id=>network.nodes.find(n=>n.id===id)!.position); results.push({nodeIds:path,points,cost,features,blockedBy:[]}); return; } - for (const item of adj.get(node) ?? []) { - if (path.includes(item.next)) continue; - const c=edgeCost(item.edge,person,state); if(!Number.isFinite(c)) continue; - visit(item.next,[...path,item.next],cost+c,[...features,item.edge.feature]); - } - }; - visit(startId,[startId],0,[]); - return results.sort((a,b)=>a.cost-b.cost).slice(0,limit); -} - -export function routeForPerson(state: PersistentWorldState, person: PersonEntity, targetId: string, network=defaultNavigationNetwork) { - const start=nearestNavNode(network,person.position); const goal=network.nodes.find(node=>node.id===targetId) ?? nearestNavNode(network,state.entities.find(e=>e.id===targetId)?.position ?? person.position); - return goal ? findAccessibleRoutes(network,state,person,start!.id,goal.id,3) : []; -} +export function navigationProfile(person:PersonEntity):NavigationProfile{const mobility=person.mobility.mode;const maxGradient=mobility==='manual-wheelchair'?.07:mobility==='powered-wheelchair'?.09:.16;return{mobility,maxGradient,gradientWeight:4,blockedPenalty:1_000_000};} +function edgeOperational(edge:NavEdge,state:PersistentWorldState){if(!edge.objectId)return true;const entity=state.entities.find(item=>item.id===edge.objectId);if(!entity||entity.kind!=='infrastructure')return true;return (entity as InfrastructureEntity).operational;} +export function edgeCost(edge:NavEdge,person:PersonEntity,state:PersistentWorldState){const profile=navigationProfile(person);if(!edge.access.includes(profile.mobility))return Infinity;if(Math.abs(edge.gradient)>profile.maxGradient)return Infinity;if(!edgeOperational(edge,state))return Infinity;return edge.length*(1+profile.gradientWeight*Math.abs(edge.gradient)+edge.surfacePenalty)+edge.waitSeconds;} +function adjacency(network:NavigationNetwork){const map=new Map>();for(const edge of network.edges){map.set(edge.from,[...(map.get(edge.from)??[]),{edge,next:edge.to}]);map.set(edge.to,[...(map.get(edge.to)??[]),{edge,next:edge.from}]);}return map;} +export function nearestNavNode(network:NavigationNetwork,position:Vec3){return [...network.nodes].sort((a,b)=>Math.hypot(a.position[0]-position[0],a.position[2]-position[2])-Math.hypot(b.position[0]-position[0],b.position[2]-position[2]))[0];} +export function findAccessibleRoutes(network:NavigationNetwork,state:PersistentWorldState,person:PersonEntity,startId:string,goalId:string,limit=3):RouteResult[]{const adj=adjacency(network),results:RouteResult[]=[],maxDepth=network.nodes.length;const visit=(node:string,path:string[],cost:number,features:RouteFeature[])=>{if(path.length>maxDepth||results.length>200)return;if(node===goalId){const points=path.map(id=>network.nodes.find(n=>n.id===id)?.position).filter((point):point is Vec3=>Boolean(point));if(points.length===path.length)results.push({nodeIds:path,points,cost,features,blockedBy:[]});return;}for(const item of adj.get(node)??[]){if(path.includes(item.next))continue;const c=edgeCost(item.edge,person,state);if(!Number.isFinite(c))continue;visit(item.next,[...path,item.next],cost+c,[...features,item.edge.feature]);}};visit(startId,[startId],0,[]);return results.sort((a,b)=>a.cost-b.cost).slice(0,limit);} +export function routeForPerson(state:PersistentWorldState,person:PersonEntity,targetId:string,network=defaultNavigationNetwork){const start=nearestNavNode(network,person.position);const targetPosition=state.entities.find(e=>e.id===targetId)?.position;const goal=network.nodes.find(node=>node.id===targetId)??(targetPosition?nearestNavNode(network,targetPosition):undefined);return start&&goal?findAccessibleRoutes(network,state,person,start.id,goal.id,3):[];} From 2c4dc7f96c35c770544adfe1411de0a024c68aed Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:58:12 +1000 Subject: [PATCH 39/46] Fix strict autonomy typing --- src/world/autonomy.ts | 57 +++++++++++-------------------------------- 1 file changed, 14 insertions(+), 43 deletions(-) diff --git a/src/world/autonomy.ts b/src/world/autonomy.ts index 7eaabcc..11ac83f 100644 --- a/src/world/autonomy.ts +++ b/src/world/autonomy.ts @@ -2,48 +2,19 @@ import { routeOptions } from './affordanceGraph'; import type { PersonEntity, PersistentWorldState } from './worldModel'; export type ActivityKind = 'travel' | 'socialise' | 'communicate' | 'recharge' | 'rest' | 'seek-alternative'; - -export interface PlannedActivity { - personId: string; - kind: ActivityKind; - goal: string; - targetId?: string; - rationale: string; - confidence: number; -} - -function goalTarget(goal: string) { - const text = goal.toLowerCase(); - if (text.includes('community')) return 'community-hub'; - if (text.includes('communicate')) return 'community-hub'; - if (text.includes('decision')) return 'clinical-centre'; - return undefined; -} - -export function planNextActivity(state: PersistentWorldState, personId: string): PlannedActivity | undefined { - const person = state.entities.find((entity): entity is PersonEntity => entity.id === personId && entity.kind === 'person'); - if (!person || person.goals.length === 0) return undefined; - - const goal = person.goals[0]; - const preferredTarget = goalTarget(goal); - const routes = routeOptions(state, personId); - const preferred = routes.find((route) => route.targetId === preferredTarget); - - if (preferredTarget && preferred?.accessible) { - return { personId, kind: 'travel', goal, targetId: preferredTarget, rationale: 'goal-aligned accessible route is available', confidence: 0.95 }; - } - - const alternative = routes.find((route) => route.accessible); - if (alternative) { - return { personId, kind: 'seek-alternative', goal, targetId: alternative.targetId, rationale: preferredTarget ? 'preferred route is inaccessible; preserve goal by seeking an accessible alternative' : 'accessible option selected from represented affordances', confidence: 0.8 }; - } - - return { personId, kind: 'rest', goal, rationale: 'no represented accessible route is currently available; do not attribute the barrier to disability', confidence: 0.75 }; +export interface PlannedActivity { personId:string; kind:ActivityKind; goal:string; targetId?:string; rationale:string; confidence:number; } + +function goalTarget(goal:string){const text=goal.toLowerCase();if(text.includes('community'))return'community-hub';if(text.includes('communicate'))return'community-hub';if(text.includes('decision'))return'clinical-centre';return undefined;} + +export function planNextActivity(state:PersistentWorldState,personId:string):PlannedActivity|undefined{ + const person=state.entities.find((entity):entity is PersonEntity=>entity.id===personId&&entity.kind==='person'); + if(!person)return undefined; + const goal=person.goals[0];if(!goal)return undefined; + const preferredTarget=goalTarget(goal),routes=routeOptions(state,personId),preferred=preferredTarget?routes.find(route=>route.targetId===preferredTarget):undefined; + if(preferredTarget&&preferred?.accessible)return{personId,kind:'travel',goal,targetId:preferredTarget,rationale:'goal-aligned accessible route is available',confidence:.95}; + const alternative=routes.find(route=>route.accessible); + if(alternative)return{personId,kind:'seek-alternative',goal,targetId:alternative.targetId,rationale:preferredTarget?'preferred route is inaccessible; preserve goal by seeking an accessible alternative':'accessible option selected from represented affordances',confidence:.8}; + return{personId,kind:'rest',goal,rationale:'no represented accessible route is currently available; do not attribute the barrier to disability',confidence:.75}; } -export function planPopulation(state: PersistentWorldState) { - return state.entities - .filter((entity): entity is PersonEntity => entity.kind === 'person') - .map((person) => planNextActivity(state, person.id)) - .filter((activity): activity is PlannedActivity => Boolean(activity)); -} +export function planPopulation(state:PersistentWorldState){return state.entities.filter((entity):entity is PersonEntity=>entity.kind==='person').map(person=>planNextActivity(state,person.id)).filter((activity):activity is PlannedActivity=>Boolean(activity));} From b4e227a86103e237c0b664ef95e3be95329361e7 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:58:24 +1000 Subject: [PATCH 40/46] Fix strict daily schedule typing --- src/world/dailyLife.ts | 69 ++++++++---------------------------------- 1 file changed, 12 insertions(+), 57 deletions(-) diff --git a/src/world/dailyLife.ts b/src/world/dailyLife.ts index 813d6fd..145f3e5 100644 --- a/src/world/dailyLife.ts +++ b/src/world/dailyLife.ts @@ -1,61 +1,16 @@ import { planNextActivity, type PlannedActivity } from './autonomy'; import type { PersonEntity, PersistentWorldState, WorldEvent } from './worldModel'; -export interface DailyScheduleEntry { - atHour: number; - goal: string; - destinationId?: string; -} - -export interface PersonSchedule { - personId: string; - entries: DailyScheduleEntry[]; -} - -export const defaultSchedules: PersonSchedule[] = [ - { - personId: 'maya', - entries: [ - { atHour: 8, goal: 'move through the community', destinationId: 'transit' }, - { atHour: 10, goal: 'communicate directly', destinationId: 'community-hub' }, - { atHour: 14, goal: 'participate in decisions', destinationId: 'clinical-centre' }, - { atHour: 17, goal: 'move through the community', destinationId: 'community-hub' }, - ], - }, -]; - -export interface DailyLifeTick { - activity?: PlannedActivity; - events: WorldEvent[]; -} - -export function simulatedHour(state: PersistentWorldState) { - return (8 + Math.floor(state.simulationSeconds / 3600)) % 24; -} - -export function scheduledGoal(state: PersistentWorldState, schedule: PersonSchedule) { - const hour = simulatedHour(state); - return [...schedule.entries].reverse().find((entry) => entry.atHour <= hour) ?? schedule.entries[0]; -} - -export function runDailyLifeTick(state: PersistentWorldState, schedule: PersonSchedule): DailyLifeTick { - const person = state.entities.find((entity): entity is PersonEntity => entity.kind === 'person' && entity.id === schedule.personId); - if (!person) return { events: [] }; - const scheduled = scheduledGoal(state, schedule); - const planningState: PersistentWorldState = { - ...state, - entities: state.entities.map((entity) => entity.id === person.id && entity.kind === 'person' ? { ...entity, goals: [scheduled.goal, ...person.goals.filter((goal) => goal !== scheduled.goal)] } : entity), - }; - const activity = planNextActivity(planningState, person.id); - if (!activity) return { events: [] }; - return { - activity, - events: [{ - id: `${state.simulationSeconds}-daily-${person.id}`, - at: state.simulationSeconds, - type: 'daily-life.activity.planned', - entityId: person.id, - detail: `hour=${simulatedHour(state)}; goal=${scheduled.goal}; activity=${activity.kind}; target=${activity.targetId ?? scheduled.destinationId ?? 'none'}`, - }], - }; +export interface DailyScheduleEntry { atHour:number; goal:string; destinationId?:string; } +export interface PersonSchedule { personId:string; entries:DailyScheduleEntry[]; } +export const defaultSchedules:PersonSchedule[]=[{personId:'maya',entries:[{atHour:8,goal:'move through the community',destinationId:'transit'},{atHour:10,goal:'communicate directly',destinationId:'community-hub'},{atHour:14,goal:'participate in decisions',destinationId:'clinical-centre'},{atHour:17,goal:'move through the community',destinationId:'community-hub'}]}]; +export interface DailyLifeTick { activity?:PlannedActivity; events:WorldEvent[]; } +export function simulatedHour(state:PersistentWorldState){return(8+Math.floor(state.simulationSeconds/3600))%24;} +export function scheduledGoal(state:PersistentWorldState,schedule:PersonSchedule){const hour=simulatedHour(state);return[...schedule.entries].reverse().find(entry=>entry.atHour<=hour)??schedule.entries[0];} +export function runDailyLifeTick(state:PersistentWorldState,schedule:PersonSchedule):DailyLifeTick{ + const person=state.entities.find((entity):entity is PersonEntity=>entity.kind==='person'&&entity.id===schedule.personId);if(!person)return{events:[]}; + const scheduled=scheduledGoal(state,schedule);if(!scheduled)return{events:[]}; + const planningState:PersistentWorldState={...state,entities:state.entities.map(entity=>entity.id===person.id&&entity.kind==='person'?{...entity,goals:[scheduled.goal,...person.goals.filter(goal=>goal!==scheduled.goal)]}:entity)}; + const activity=planNextActivity(planningState,person.id);if(!activity)return{events:[]}; + return{activity,events:[{id:`${state.simulationSeconds}-daily-${person.id}`,at:state.simulationSeconds,type:'daily-life.activity.planned',entityId:person.id,detail:`hour=${simulatedHour(state)}; goal=${scheduled.goal}; activity=${activity.kind}; target=${activity.targetId??scheduled.destinationId??'none'}`}]}; } From 2068d3289c770ae5cec7c7b7e3d6ee59dbc743e7 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:58:38 +1000 Subject: [PATCH 41/46] Collapse deprecated runtime onto WorldRuntime --- src/world/liveWorldRuntime.ts | 78 +---------------------------------- 1 file changed, 2 insertions(+), 76 deletions(-) diff --git a/src/world/liveWorldRuntime.ts b/src/world/liveWorldRuntime.ts index 881db45..869b47f 100644 --- a/src/world/liveWorldRuntime.ts +++ b/src/world/liveWorldRuntime.ts @@ -1,76 +1,2 @@ -import { planPopulation, type PlannedActivity } from './autonomy'; -import { assessClinicalTrigger, bridgeToClinicalRuntime, type ClinicalRuntimePort } from './clinicalBridge'; -import { proposeDynamics, dynamicsEvents, type DynamicsProposal } from './vnnDynamics'; -import { defaultSocialAgents, interact, socialEvents } from './socialAgents'; -import { WorldTimeline } from './timeline'; -import type { PersonEntity, PersistentWorldState, PersistentWorldStore, Vec3, WorldEvent } from './worldModel'; - -export interface LiveWorldFrame { - state: PersistentWorldState; - activities: PlannedActivity[]; - proposals: DynamicsProposal[]; - clinical: ReturnType[]; -} - -export class LiveWorldRuntime { - private accumulator = 0; - private timeline = new WorldTimeline(); - private lastFrame?: LiveWorldFrame; - - constructor(private store: PersistentWorldStore, private clinicalRuntime?: ClinicalRuntimePort) {} - - step(deltaSeconds: number): LiveWorldFrame { - this.store.tick(deltaSeconds); - this.accumulator += deltaSeconds; - if (this.lastFrame && this.accumulator < 2) return { ...this.lastFrame, state: this.store.snapshot() }; - this.accumulator = 0; - - const state = this.store.snapshot(); - const activities = planPopulation(state); - const proposals = activities.map((activity) => proposeDynamics(state, activity)); - const generated: WorldEvent[] = proposals.flatMap((proposal) => dynamicsEvents(state, proposal)); - - for (const activity of activities) this.advancePerson(activity, state); - - for (const person of state.entities.filter((entity): entity is PersonEntity => entity.kind === 'person')) { - const peer = defaultSocialAgents[0]; - const social = interact(state, peer, person.id, 'greet'); - if (social && state.simulationSeconds % 30 < 2) generated.push(...socialEvents(state, social)); - } - - this.store.appendEvents(generated); - const next = this.store.snapshot(); - const clinical = next.entities - .filter((entity): entity is PersonEntity => entity.kind === 'person') - .map((person) => assessClinicalTrigger(next, person.id, proposals)); - - if (this.clinicalRuntime) { - for (const assessment of clinical) { - const event = bridgeToClinicalRuntime(next, assessment, this.clinicalRuntime); - if (event) this.store.appendEvents([event]); - } - } - - this.timeline.capture(this.store.snapshot(), `live world at ${Math.round(next.simulationSeconds)}s`); - this.lastFrame = { state: this.store.snapshot(), activities, proposals, clinical }; - return this.lastFrame; - } - - latest() { return this.lastFrame; } - timelineStore() { return this.timeline; } - - private advancePerson(activity: PlannedActivity, state: PersistentWorldState) { - if (!activity.targetId || (activity.kind !== 'travel' && activity.kind !== 'seek-alternative')) return; - const person = state.entities.find((entity): entity is PersonEntity => entity.kind === 'person' && entity.id === activity.personId); - const target = state.entities.find((entity) => entity.id === activity.targetId); - if (!person || !target) return; - const [px, py, pz] = person.position; - const [tx, , tz] = target.position; - const dx = tx - px; const dz = tz - pz; const distance = Math.hypot(dx, dz); - if (distance < 1.5) return; - const speed = person.mobility.mode === 'powered-wheelchair' ? 1.25 : 0.9; - const step = Math.min(speed * 2, distance); - const next: Vec3 = [px + (dx / distance) * step, py, pz + (dz / distance) * step]; - this.store.move(person.id, next); - } -} +export { WorldRuntime as LiveWorldRuntime } from './liveWorldLoop'; +export type { LiveWorldFrame } from './liveWorldLoop'; From a4d4cfe7bda714f81e2e82e607ece8240b27627b Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:58:53 +1000 Subject: [PATCH 42/46] Fix Scenario Studio strict entity typing --- src/world/scenarioStudio.ts | 74 +++++-------------------------------- 1 file changed, 10 insertions(+), 64 deletions(-) diff --git a/src/world/scenarioStudio.ts b/src/world/scenarioStudio.ts index a6fcbde..7271e71 100644 --- a/src/world/scenarioStudio.ts +++ b/src/world/scenarioStudio.ts @@ -1,64 +1,10 @@ -import type { AccessModality, InfrastructureEntity, PersistentWorldState, Vec3 } from './worldModel'; - -export interface WorldStudioDraft { - title: string; - description: string; - seed: PersistentWorldState; -} - -export interface PlaceDraft { - id: string; - label: string; - position: Vec3; - tags?: string[]; -} - -export interface InfrastructureDraft { - id: string; - label: string; - position: Vec3; - feature: InfrastructureEntity['feature']; - accessibility: AccessModality[]; - operational?: boolean; -} - -export function addPlace(draft: WorldStudioDraft, place: PlaceDraft): WorldStudioDraft { - return { - ...draft, - seed: { - ...draft.seed, - entities: [...draft.seed.entities, { ...place, kind: 'place', persistent: true, tags: place.tags ?? [] }], - }, - }; -} - -export function addInfrastructure(draft: WorldStudioDraft, infrastructure: InfrastructureDraft): WorldStudioDraft { - const entity: InfrastructureEntity = { - ...infrastructure, - kind: 'infrastructure', - persistent: true, - tags: ['access'], - operational: infrastructure.operational ?? true, - }; - return { ...draft, seed: { ...draft.seed, entities: [...draft.seed.entities, entity] } }; -} - -export interface StudioValidationIssue { severity: 'error' | 'warning'; path: string; message: string; } - -export function validateWorldDraft(draft: WorldStudioDraft): StudioValidationIssue[] { - const issues: StudioValidationIssue[] = []; - const ids = new Set(); - for (const entity of draft.seed.entities) { - if (ids.has(entity.id)) issues.push({ severity: 'error', path: `entities.${entity.id}`, message: 'Entity IDs must be unique.' }); - ids.add(entity.id); - if (entity.kind === 'person' && entity.authority !== 'self') issues.push({ severity: 'error', path: `entities.${entity.id}.authority`, message: 'Open-world person authority cannot be silently reassigned by a scenario draft.' }); - if (entity.kind === 'infrastructure' && entity.accessibility.length === 0) issues.push({ severity: 'warning', path: `entities.${entity.id}.accessibility`, message: 'Infrastructure has no represented access modalities; verify this is intentional.' }); - } - return issues; -} - -export function exportWorldDraft(draft: WorldStudioDraft) { - const issues = validateWorldDraft(draft); - if (issues.some((issue) => issue.severity === 'error')) throw new Error('World draft contains validation errors.'); - return JSON.stringify(draft, null, 2); -} +import type { AccessModality, InfrastructureEntity, PersonEntity, PersistentWorldState, Vec3 } from './worldModel'; + +export interface WorldStudioDraft { title:string; description:string; seed:PersistentWorldState; } +export interface PlaceDraft { id:string; label:string; position:Vec3; tags?:string[]; } +export interface InfrastructureDraft { id:string; label:string; position:Vec3; feature:InfrastructureEntity['feature']; accessibility:AccessModality[]; operational?:boolean; } +export function addPlace(draft:WorldStudioDraft,place:PlaceDraft):WorldStudioDraft{return{...draft,seed:{...draft.seed,entities:[...draft.seed.entities,{...place,kind:'place',persistent:true,tags:place.tags??[]}]}};} +export function addInfrastructure(draft:WorldStudioDraft,infrastructure:InfrastructureDraft):WorldStudioDraft{const entity:InfrastructureEntity={...infrastructure,kind:'infrastructure',persistent:true,tags:['access'],operational:infrastructure.operational??true};return{...draft,seed:{...draft.seed,entities:[...draft.seed.entities,entity]}};} +export interface StudioValidationIssue { severity:'error'|'warning'; path:string; message:string; } +export function validateWorldDraft(draft:WorldStudioDraft):StudioValidationIssue[]{const issues:StudioValidationIssue[]=[],ids=new Set();for(const entity of draft.seed.entities){if(ids.has(entity.id))issues.push({severity:'error',path:`entities.${entity.id}`,message:'Entity IDs must be unique.'});ids.add(entity.id);if(entity.kind==='person'&&(entity as PersonEntity).authority!=='self')issues.push({severity:'error',path:`entities.${entity.id}.authority`,message:'Open-world person authority cannot be silently reassigned by a scenario draft.'});if(entity.kind==='infrastructure'&&(entity as InfrastructureEntity).accessibility.length===0)issues.push({severity:'warning',path:`entities.${entity.id}.accessibility`,message:'Infrastructure has no represented access modalities; verify this is intentional.'});}return issues;} +export function exportWorldDraft(draft:WorldStudioDraft){const issues=validateWorldDraft(draft);if(issues.some(issue=>issue.severity==='error'))throw new Error('World draft contains validation errors.');return JSON.stringify(draft,null,2);} From b23d106ee8d83db1787b2b033039a4f6e20cdd28 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:59:06 +1000 Subject: [PATCH 43/46] Fix timeline exact optional typing --- src/world/timeline.ts | 92 ++++++------------------------------------- 1 file changed, 12 insertions(+), 80 deletions(-) diff --git a/src/world/timeline.ts b/src/world/timeline.ts index 6777b65..bd7efc6 100644 --- a/src/world/timeline.ts +++ b/src/world/timeline.ts @@ -1,86 +1,18 @@ import type { PersistentWorldState, WorldEvent } from './worldModel'; -export interface TimelineSnapshot { - id: string; - label: string; - at: number; - state: PersistentWorldState; - parentId?: string; - branchReason?: string; -} - -export interface CounterfactualComparison { - leftId: string; - rightId: string; - simulationDeltaSeconds: number; - entityChanges: Array<{ entityId: string; changedFields: string[] }>; - eventTypesOnlyLeft: string[]; - eventTypesOnlyRight: string[]; -} - -const clone = (value: T): T => JSON.parse(JSON.stringify(value)) as T; +export interface TimelineSnapshot { id:string; label:string; at:number; state:PersistentWorldState; parentId?:string; branchReason?:string; } +export interface CounterfactualComparison { leftId:string; rightId:string; simulationDeltaSeconds:number; entityChanges:Array<{entityId:string;changedFields:string[]}>; eventTypesOnlyLeft:string[]; eventTypesOnlyRight:string[]; } +const clone=(value:T):T=>JSON.parse(JSON.stringify(value)) as T; export class WorldTimeline { - private snapshots = new Map(); - - capture(state: PersistentWorldState, label: string, parentId?: string, branchReason?: string): TimelineSnapshot { - const id = `snapshot-${state.simulationSeconds}-${this.snapshots.size + 1}`; - const snapshot: TimelineSnapshot = { id, label, at: state.simulationSeconds, state: clone(state), parentId, branchReason }; - this.snapshots.set(id, snapshot); - return clone(snapshot); - } - - get(id: string): TimelineSnapshot | undefined { - const snapshot = this.snapshots.get(id); - return snapshot ? clone(snapshot) : undefined; - } - - branch(snapshotId: string, label: string, mutate: (state: PersistentWorldState) => void, reason: string): TimelineSnapshot | undefined { - const parent = this.snapshots.get(snapshotId); - if (!parent) return undefined; - const state = clone(parent.state); - mutate(state); - state.events.push({ - id: `${state.simulationSeconds}-counterfactual-${state.events.length}`, - at: state.simulationSeconds, - type: 'timeline.counterfactual.branch', - detail: reason, - }); - return this.capture(state, label, parent.id, reason); - } - - replay(snapshotId: string, events: WorldEvent[]): TimelineSnapshot | undefined { - const parent = this.snapshots.get(snapshotId); - if (!parent) return undefined; - const state = clone(parent.state); - state.events.push(...clone(events)); - if (events.length > 0) state.simulationSeconds = Math.max(state.simulationSeconds, ...events.map((event) => event.at)); - return this.capture(state, `${parent.label} replay`, parent.id, 'deterministic event replay'); - } - - compare(leftId: string, rightId: string): CounterfactualComparison | undefined { - const left = this.snapshots.get(leftId); - const right = this.snapshots.get(rightId); - if (!left || !right) return undefined; - - const rightById = new Map(right.state.entities.map((entity) => [entity.id, entity])); - const entityChanges = left.state.entities.flatMap((leftEntity) => { - const rightEntity = rightById.get(leftEntity.id); - if (!rightEntity) return [{ entityId: leftEntity.id, changedFields: ['removed'] }]; - const fields = new Set([...Object.keys(leftEntity), ...Object.keys(rightEntity)]); - const changedFields = [...fields].filter((field) => JSON.stringify((leftEntity as any)[field]) !== JSON.stringify((rightEntity as any)[field])); - return changedFields.length ? [{ entityId: leftEntity.id, changedFields }] : []; - }); - - const leftTypes = new Set(left.state.events.map((event) => event.type)); - const rightTypes = new Set(right.state.events.map((event) => event.type)); - return { - leftId, - rightId, - simulationDeltaSeconds: right.at - left.at, - entityChanges, - eventTypesOnlyLeft: [...leftTypes].filter((type) => !rightTypes.has(type)), - eventTypesOnlyRight: [...rightTypes].filter((type) => !leftTypes.has(type)), - }; + private snapshots=new Map(); + capture(state:PersistentWorldState,label:string,parentId?:string,branchReason?:string):TimelineSnapshot{ + const id=`snapshot-${state.simulationSeconds}-${this.snapshots.size+1}`; + const snapshot:TimelineSnapshot={id,label,at:state.simulationSeconds,state:clone(state),...(parentId?{parentId}:{}),...(branchReason?{branchReason}:{})}; + this.snapshots.set(id,snapshot);return clone(snapshot); } + get(id:string):TimelineSnapshot|undefined{const snapshot=this.snapshots.get(id);return snapshot?clone(snapshot):undefined;} + branch(snapshotId:string,label:string,mutate:(state:PersistentWorldState)=>void,reason:string):TimelineSnapshot|undefined{const parent=this.snapshots.get(snapshotId);if(!parent)return undefined;const state=clone(parent.state);mutate(state);state.events.push({id:`${state.simulationSeconds}-counterfactual-${state.events.length}`,at:state.simulationSeconds,type:'timeline.counterfactual.branch',detail:reason});return this.capture(state,label,parent.id,reason);} + replay(snapshotId:string,events:WorldEvent[]):TimelineSnapshot|undefined{const parent=this.snapshots.get(snapshotId);if(!parent)return undefined;const state=clone(parent.state);state.events.push(...clone(events));if(events.length>0)state.simulationSeconds=Math.max(state.simulationSeconds,...events.map(event=>event.at));return this.capture(state,`${parent.label} replay`,parent.id,'deterministic event replay');} + compare(leftId:string,rightId:string):CounterfactualComparison|undefined{const left=this.snapshots.get(leftId),right=this.snapshots.get(rightId);if(!left||!right)return undefined;const rightById=new Map(right.state.entities.map(entity=>[entity.id,entity]));const entityChanges=left.state.entities.flatMap(leftEntity=>{const rightEntity=rightById.get(leftEntity.id);if(!rightEntity)return[{entityId:leftEntity.id,changedFields:['removed']}];const fields=new Set([...Object.keys(leftEntity),...Object.keys(rightEntity)]),changedFields=[...fields].filter(field=>JSON.stringify((leftEntity as any)[field])!==JSON.stringify((rightEntity as any)[field]));return changedFields.length?[{entityId:leftEntity.id,changedFields}]:[];});const leftTypes=new Set(left.state.events.map(event=>event.type)),rightTypes=new Set(right.state.events.map(event=>event.type));return{leftId,rightId,simulationDeltaSeconds:right.at-left.at,entityChanges,eventTypesOnlyLeft:[...leftTypes].filter(type=>!rightTypes.has(type)),eventTypesOnlyRight:[...rightTypes].filter(type=>!leftTypes.has(type))};} } From 2e23314a5ffe13795c14477ea94ee77a8e98a442 Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:59:19 +1000 Subject: [PATCH 44/46] Fix strict live world tests --- __tests__/liveWorldLoop.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/__tests__/liveWorldLoop.test.ts b/__tests__/liveWorldLoop.test.ts index ea3880d..ed48f0b 100644 --- a/__tests__/liveWorldLoop.test.ts +++ b/__tests__/liveWorldLoop.test.ts @@ -1,14 +1,17 @@ import { defaultSchedules, runDailyLifeTick } from '../src/world/dailyLife'; import { LiveWorldLoop } from '../src/world/liveWorldLoop'; import { addInfrastructure, exportWorldDraft, validateWorldDraft } from '../src/world/scenarioStudio'; -import { initialOpenWorldState, PersistentWorldStore } from '../src/world/worldModel'; +import { initialOpenWorldState, PersistentWorldStore, type PersonEntity } from '../src/world/worldModel'; describe('live disability world integration', () => { it('plans daily life without replacing person authority', () => { - const tick = runDailyLifeTick(initialOpenWorldState, defaultSchedules[0]); + const schedule = defaultSchedules[0]; + expect(schedule).toBeDefined(); + if (!schedule) return; + const tick = runDailyLifeTick(initialOpenWorldState, schedule); expect(tick.activity?.personId).toBe('maya'); - const maya = initialOpenWorldState.entities.find((entity) => entity.id === 'maya'); - expect(maya?.kind === 'person' && maya.authority).toBe('self'); + const maya = initialOpenWorldState.entities.find((entity): entity is PersonEntity => entity.id === 'maya' && entity.kind === 'person'); + expect(maya?.authority).toBe('self'); }); it('runs the bounded world loop and records dynamics events', () => { From 23b22a2f712230157ac7a8b67db526a1703cb7cc Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 09:59:32 +1000 Subject: [PATCH 45/46] Fix strict advanced world tests --- __tests__/worldAdvanced.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/__tests__/worldAdvanced.test.ts b/__tests__/worldAdvanced.test.ts index 48f6bee..953ed36 100644 --- a/__tests__/worldAdvanced.test.ts +++ b/__tests__/worldAdvanced.test.ts @@ -9,7 +9,10 @@ describe('advanced disability world invariants', () => { const state = JSON.parse(JSON.stringify(initialOpenWorldState)); const maya = state.entities.find((entity: any) => entity.id === 'maya'); maya.communication.access = 'unavailable'; - const interaction = interact(state, defaultSocialAgents[0], 'maya', 'ask'); + const agent = defaultSocialAgents[0]; + expect(agent).toBeDefined(); + if (!agent) return; + const interaction = interact(state, agent, 'maya', 'ask'); expect(interaction?.outcome).toBe('unknown'); expect(maya.authority).toBe('self'); }); From 9affbc33ff4c8f03804d288f05d97e2cb84f17ae Mon Sep 17 00:00:00 2001 From: ausdisau Date: Mon, 17 Aug 2026 10:01:18 +1000 Subject: [PATCH 46/46] Restore accessible companion controls and state labels --- App.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/App.tsx b/App.tsx index e41a4fb..5936ce4 100644 --- a/App.tsx +++ b/App.tsx @@ -16,12 +16,21 @@ function MobileSimulation() { const refresh = () => setRevision(runtime.kernel.snapshot().revision); const choose = (choiceId: string) => { runtime.emit('scenario.choice.committed', { choiceId }); refresh(); }; const state = runtime.kernel.snapshot(); + return MERT Engine · Companion Open the web deployment on a WebXR headset for the immersive world. - World StateHR {state.vitals.heartRate} · RR {state.vitals.respiratoryRate} · SpO₂ {state.vitals.spo2}%AAC: {state.communication.available ? 'AVAILABLE' : 'UNAVAILABLE'}Revision: {revision} - choose('restore-access')}>Restore AAC + Position - choose('escalate')}>Escalate + + World State + HR {state.vitals.heartRate} · RR {state.vitals.respiratoryRate} · SpO₂ {state.vitals.spo2}% + AAC: {state.communication.available ? 'AVAILABLE' : 'UNAVAILABLE'} · {state.communication.reliable ? 'RELIABLE' : 'UNRELIABLE'} + Decision authority: {state.authority.decisionMaker} + Revision: {revision} + + choose('restore-access')}>Restore AAC + Position + choose('escalate')}>Escalate + choose('anchor-on-disability')}>Anchor on Baseline Disability ; } + const styles=StyleSheet.create({safeArea:{flex:1},container:{padding:20,gap:12,maxWidth:820,width:'100%',alignSelf:'center'},title:{fontSize:28,fontWeight:'700'},card:{borderWidth:1,borderRadius:12,padding:16,gap:6},heading:{fontSize:19,fontWeight:'700'},button:{borderWidth:2,borderRadius:10,padding:16},buttonText:{fontWeight:'700'}});