@zixcel/dom-renderer 0.0.0-stage → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +202 -0
  2. package/LICENSE-PREVIOUS +21 -0
  3. package/NOTICE +5 -0
  4. package/README.md +32 -2
  5. package/package.json +38 -4
  6. package/src/book-reader.d.mts +1 -0
  7. package/src/book-reader.mjs +36 -0
  8. package/src/frame-layout.d.mts +13 -0
  9. package/src/frame-layout.mjs +25 -0
  10. package/src/frame.d.mts +13 -0
  11. package/src/frame.mjs +47 -0
  12. package/src/index.d.mts +57 -0
  13. package/src/index.mjs +205 -0
  14. package/src/inspection-overview.d.mts +1 -0
  15. package/src/inspection-overview.mjs +35 -0
  16. package/src/npc-motion.d.mts +5 -0
  17. package/src/npc-motion.mjs +7 -0
  18. package/src/object-presentation.d.mts +55 -0
  19. package/src/object-presentation.mjs +24 -0
  20. package/src/open-world-projection.d.mts +29 -0
  21. package/src/open-world-projection.mjs +26 -0
  22. package/src/reconcile.d.mts +1 -0
  23. package/src/reconcile.mjs +21 -0
  24. package/src/spatial-patterns.d.mts +29 -0
  25. package/src/spatial-patterns.mjs +43 -0
  26. package/src/spatial.d.mts +8 -0
  27. package/src/spatial.mjs +27 -0
  28. package/src/structure-graph.d.mts +16 -0
  29. package/src/structure-graph.mjs +97 -0
  30. package/src/style.css +128 -0
  31. package/src/symbol.d.mts +2 -0
  32. package/src/symbol.mjs +19 -0
  33. package/src/world-gpu.d.mts +15 -0
  34. package/src/world-gpu.mjs +79 -0
  35. package/src/world-input.d.mts +3 -0
  36. package/src/world-input.mjs +21 -0
  37. package/src/world-model.d.mts +49 -0
  38. package/src/world-model.mjs +83 -0
  39. package/src/world-runtime.d.mts +36 -0
  40. package/src/world-runtime.mjs +312 -0
  41. package/src/world-surface.d.mts +7 -0
  42. package/src/world-surface.mjs +14 -0
package/src/index.mjs ADDED
@@ -0,0 +1,205 @@
1
+ // Trusted installed DOM primitives. All external values are text, never HTML.
2
+ // Hatter downstream 2026: one bounded DOM tree for spatial and structured Scenes.
3
+ import {validateInputDeclaration,validateInputValues} from '@zixcel/interaction/input'
4
+ import {spatialView} from './spatial.mjs'
5
+ import {symbol} from './symbol.mjs'
6
+ import {reconcileChildren} from './reconcile.mjs'
7
+ import {createWorldSurface} from './world-surface.mjs'
8
+ import {bookReader} from './book-reader.mjs'
9
+ export {createWorldSurface} from './world-surface.mjs'
10
+ export {symbol} from './symbol.mjs'
11
+ export {windowFrame} from './frame.mjs'
12
+ export {worldControlGuide} from './world-input.mjs'
13
+ export {spatialView} from './spatial.mjs'
14
+ export {structureGraph} from './structure-graph.mjs'
15
+ export const renderBounds=Object.freeze({nodes:8192,depth:20,textBytes:524288})
16
+ const fail=code=>{throw Object.assign(Error(code),{code})}
17
+ /** @template {keyof HTMLElementTagNameMap} K
18
+ * @param {Document} document
19
+ * @param {K} tag
20
+ * @param {unknown} [text]
21
+ * @param {Record<string,unknown>} [attrs]
22
+ * @returns {HTMLElementTagNameMap[K]} */
23
+ export function element(document,tag,text=null,attrs={}){
24
+ const node=document.createElement(tag);if(text!==null)node.textContent=String(text)
25
+ for(const [key,value]of Object.entries(attrs))if(value!==null)node.setAttribute(key,String(value))
26
+ return node
27
+ }
28
+ /** @param {Document} document
29
+ * @param {unknown} value
30
+ * @param {{nodes:number,bytes:number}} [budget]
31
+ * @returns {HTMLElement} */
32
+ export function valueView(document,value,budget={nodes:0,bytes:0}){
33
+ const walk=(value,depth)=>{
34
+ if(++budget.nodes>renderBounds.nodes||depth>renderBounds.depth)fail('RenderLimitExceeded')
35
+ if(value===null||typeof value!=='object'){
36
+ const text=JSON.stringify(value);if(typeof text!=='string')fail('InvalidRenderValue')
37
+ budget.bytes+=new TextEncoder().encode(text).length;if(budget.bytes>renderBounds.textBytes)fail('RenderLimitExceeded')
38
+ return element(document,'span',text,{'data-projection-scalar':text})
39
+ }
40
+ if(Object.keys(value).length>4096)fail('RenderLimitExceeded')
41
+ if(!Object.keys(value).length)return element(document,'span',Array.isArray(value)?'[]':'{}',{'data-empty-value':Array.isArray(value)?'array':'object'})
42
+ const list=element(document,Array.isArray(value)?'ol':'dl')
43
+ for(const [key,child]of Object.entries(value)){
44
+ if(Array.isArray(value)){const item=element(document,'li');item.append(walk(child,depth+1));list.append(item)}
45
+ else{const name=element(document,'dt',key),item=element(document,'dd');item.append(walk(child,depth+1));list.append(name,item)}
46
+ }
47
+ return list
48
+ }
49
+ return walk(value,0)
50
+ }
51
+ export class SceneRenderer{
52
+ #root;#events;#abort=null;#actionHandles=new Map();#spatial=null;#slots;#pose=null;#poseKey=null;#speaker=null;#surface;#ownsSurface;#garden=null
53
+ /** @param {HTMLElement} root
54
+ * @param {(kind:string,value:string|import('@zixcel/projection-client').ActionHandle|undefined)=>void} onEvent
55
+ * @param {{tools?:HTMLElement,details?:HTMLElement,actions?:HTMLElement}} [slots]
56
+ * @param {ReturnType<typeof createWorldSurface>|null} [surface] */
57
+ constructor(root,onEvent,slots={},surface=null){this.#root=root;this.#events=onEvent;this.#slots=slots;this.#surface=surface??createWorldSurface();this.#ownsSurface=!surface}
58
+ /** @param {import('@zixcel/projection-client').RenderPlan} plan */
59
+ render(plan){
60
+ const root=this.#root,document=root.ownerDocument,focused=document.activeElement?.dataset.control
61
+ const previous=root.querySelector('[data-render-key]'),oldViewport=root.querySelector('.scene-viewport')
62
+ const scroll=previous?.dataset.renderKey===plan.projectionKey&&previous.dataset.viewMode===plan.viewMode&&oldViewport
63
+ ?{left:oldViewport.scrollLeft,top:oldViewport.scrollTop}:null
64
+ if(this.#poseKey===plan.projectionKey)this.#pose=this.#spatial?.snapshot()??this.#pose
65
+ else this.#pose=null
66
+ this.#poseKey=plan.projectionKey
67
+ this.#spatial?.close();this.#spatial=null;this.#abort?.abort();this.#abort=new AbortController();this.#actionHandles.clear()
68
+ if(!['spatial','structured'].includes(plan.viewMode)||!Array.isArray(plan.relations)||plan.relations.length>512)fail('InvalidRenderPlan')
69
+ const section=element(document,'section',null,{'data-render-revision':plan.projectionRevision,'data-render-key':plan.projectionKey,'data-view-mode':plan.viewMode,'aria-label':'Scene content',tabindex:'-1',class:'semantic-scene'})
70
+ const button=(label,kind,id,attrs={})=>element(document,'button',label,{type:'button','data-kind':kind,'data-control':JSON.stringify([kind,id]),'data-target':id,...attrs})
71
+ const views=element(document,'div',null,{role:'group','aria-label':'Scene view',class:'scene-view-controls'})
72
+ for(const [mode,label]of [['spatial','Spatial view'],['structured','List view']])views.append(button(label,'view',mode,{'aria-pressed':plan.viewMode===mode}))
73
+ const tools=this.#slots.tools??section;tools.replaceChildren();tools.append(views)
74
+ const viewport=element(document,'div',null,{class:'scene-viewport',tabindex:'0','aria-label':'Explore information in this space'}),board=element(document,'div',null,{class:'spatial-board'})
75
+ for(const person of this.#garden?.actors??(this.#speaker?[this.#speaker]:[])){const actor=button('',this.#garden?'actor':'conversation',person.id,{'data-world-object':person.id,'data-selected':person.id===this.#speaker?.id,'data-observation-label':person.preparation?.label??''});actor.append(element(document,'strong',person.title));actor.disabled=person.status!==undefined&&person.status!=='Available';board.append(actor)}
76
+ const camera=element(document,'div',null,{role:'group','aria-label':'View position',class:'scene-camera'})
77
+ for(const [id,label,icon]of [['left','Move left','←'],['right','Move right','→'],['in','Zoom in','+'],['out','Zoom out','−'],['reset','View all','⛶']])camera.append(button(icon,'camera',id,{'aria-label':label,title:label}))
78
+ if(plan.viewMode==='spatial')tools.append(camera)
79
+ viewport.append(board);(plan.viewMode==='structured'?tools:section).append(viewport)
80
+ if(plan.empty)section.append(element(document,'p','No projected information is available.',{role:'status'}))
81
+ if(plan.truncated)section.append(element(document,'p','The source reports incomplete information.',{role:'status'}))
82
+ const groups=[...plan.regions,...(plan.unplaced.length?[{id:'unplaced',role:'Context',items:plan.unplaced}]:[])]
83
+ if(groups.length>17||plan.actions.length>64)fail('RenderLimitExceeded')
84
+ for(const group of groups){
85
+ const region=element(document,'section',null,{'data-region':group.id}),list=element(document,'ul',null,{class:'scene-items'})
86
+ region.append(element(document,'h2',group.role),list)
87
+ for(const item of group.items){
88
+ const row=element(document,'li',null,{'data-item-id':item.id,'data-selected':item.selected,'data-related':item.related,'data-focused':item.focused}),toolbar=element(document,'div',null,{class:'item-tools'})
89
+ const itemButton=(label,kind,attrs={})=>button(label,kind,item.id,{'data-control':JSON.stringify([group.id,kind,item.id]),...attrs})
90
+ toolbar.append(itemButton(item.display?.title??item.content.reason??'Display not configured','select',{'aria-pressed':item.selected}),itemButton('View details','inspect',{'aria-expanded':plan.inspector?.id===item.id,'aria-haspopup':'dialog'}))
91
+ if(item.content.reason)toolbar.append(itemButton('Review','resolve'))
92
+ const solid=element(document,'span',null,{class:'object-solid','aria-hidden':'true'})
93
+ solid.append(symbol(document,item.display?.symbol??'nodes'));row.append(solid,toolbar)
94
+ if(item.display?.summary)row.append(element(document,'p',item.display.summary,{'data-item-summary':item.id}))
95
+ const fields=element(document,'dl',null,{class:'object-fields'})
96
+ for(const field of item.display?.fields??[])fields.append(element(document,'dt',field.label),element(document,'dd',field.value===null?'Not set':String(field.value),{'data-projection-scalar':JSON.stringify(field.value)}))
97
+ row.append(fields)
98
+ if(!item.display)row.append(element(document,'p',item.content.reason?'This information is not yet resolved. Review the available choices or the missing requirements.':'Display information is not configured. Inspect this object in System.'))
99
+ list.append(row)
100
+ }
101
+ board.append(region)
102
+ }
103
+ const time=element(document,'section',null,{'aria-label':'Scene time',class:'scene-time'})
104
+ time.append(element(document,'h2',plan.temporal?.label??'Time'))
105
+ if(plan.temporal?.items.length){
106
+ time.append(button('All times','time','',{'aria-pressed':plan.temporal.cursor===null}))
107
+ for(const item of plan.temporal.items){const control=button(item.at+' · '+item.itemId,'time',item.itemId,{'aria-pressed':plan.temporal.cursor===item.itemId});time.append(control)}
108
+ }else time.append(element(document,'p','No event time has been declared by this source.',{role:'status'}))
109
+ tools.append(time)
110
+ if(plan.relations.length){
111
+ const relations=element(document,'details',null,{'aria-label':'Relationships'}),list=element(document,'ul',null,{class:'scene-relations'})
112
+ relations.append(element(document,'summary','Relationships ('+plan.relations.length+')'),list)
113
+ for(const relation of plan.relations){
114
+ const row=element(document,'li',null,{'data-relation-id':relation.id,'data-active':relation.active})
115
+ row.append(button(relation.from,'select',relation.from,{'data-control':JSON.stringify(['relation',relation.id,'from']),'aria-label':'Select related '+relation.from}),element(document,'span','→ '+relation.relationRef+' →'),button(relation.to,'select',relation.to,{'data-control':JSON.stringify(['relation',relation.id,'to']),'aria-label':'Select related '+relation.to}))
116
+ list.append(row)
117
+ }
118
+ tools.append(relations)
119
+ }
120
+ const actions=this.#slots.actions??tools;if(this.#slots.actions)actions.replaceChildren()
121
+ // Unresolved owner observations are conversation material too, including
122
+ // when no executable choice exists. This does not manufacture an action.
123
+ const pending=groups.flatMap(group=>group.items).filter(item=>item.content.reason)
124
+ if(pending.length){
125
+ const questions=element(document,'section',null,{'aria-label':'Information needing clarification'})
126
+ questions.append(element(document,'h3','Why this information needs review'))
127
+ for(const item of pending){
128
+ const row=element(document,'div',null,{'data-unresolved-ref':item.id})
129
+ const explanation=this.#slots.describeUnresolved?.(item.content.reason)??{label:String(item.content.reason),detail:'The source owner reported this unresolved reason.'}
130
+ row.append(button(item.display?.title??explanation.label,'resolve',item.id),element(document,'small',explanation.detail))
131
+ questions.append(row)
132
+ }
133
+ actions.append(questions)
134
+ }
135
+ for(const action of plan.actions){this.#actionHandles.set(action.handle.actionId,action.handle);actions.append(button(action.descriptor.label,'action',action.handle.actionId))}
136
+ const lastBook=(this.#slots.details??root).querySelector('[data-book-id]'),bookPosition=lastBook?{id:lastBook.dataset.bookId,revision:lastBook.dataset.bookRevision,page:Number(lastBook.dataset.bookPage)}:null
137
+ this.#slots.details?.replaceChildren()
138
+ if(plan.inspector){
139
+ const chosen=groups.flatMap(r=>r.items).find(i=>i.id===plan.inspector.id)
140
+ if(chosen)(this.#slots.details??section).append(bookReader(document,chosen,plan.projectionRevision,button('Close book','dismiss',plan.inspector.id),this.#abort.signal,bookPosition))
141
+ }
142
+ reconcileChildren(root,[section])
143
+ const activeViewport=plan.viewMode==='spatial'?root.querySelector('.scene-viewport'):viewport,activeBoard=activeViewport.querySelector('.spatial-board')
144
+ if(plan.viewMode==='spatial'||this.#garden)this.#spatial=spatialView(this.#garden?.host??activeViewport,activeBoard,plan.camera,plan.relations,this.#abort.signal,this.#pose,null,{
145
+ surface:this.#surface,activity:this.#garden?.activity,target:this.#garden?'garden':plan.projectionKey,context:this.#garden?plan.projectionKey:null,pattern:this.#garden?'garden':'constellation',items:new Map(groups.flatMap(region=>region.items.map(item=>[item.id,{
146
+ anchor:this.#speaker?.id,group:item.content.group,semanticRef:item.content.semanticRef,sourceRefs:item.content.sourceRefs,scalars:(item.display?.fields??[]).map(f=>f.value),unresolved:Boolean(item.content.reason),changeKey:JSON.stringify(item.display?.fields??[])
147
+ }])))
148
+ })
149
+ if(this.#garden&&plan.viewMode==='structured')activeBoard.hidden=false
150
+ for(const host of new Set([root,this.#slots.tools,this.#slots.details,this.#slots.actions].filter(Boolean)))host.addEventListener('click',event=>{
151
+ const target=event.target.closest('button[data-kind]');if(!target||!host.contains(target))return
152
+ if(target.dataset.kind==='camera'){this.#spatial?.change(target.dataset.target);return}
153
+ this.#events(target.dataset.kind,target.dataset.kind==='action'?this.#actionHandles.get(target.dataset.target):target.dataset.target)
154
+ },{signal:this.#abort.signal})
155
+ if(focused)[...root.querySelectorAll('[data-control]')].find(n=>n.dataset.control===focused)?.focus({preventScroll:true})
156
+ if(scroll){viewport.scrollLeft=scroll.left;viewport.scrollTop=scroll.top}
157
+ }
158
+ /** @param {import('@zixcel/projection-client').Camera} value */
159
+ camera(value){this.#spatial?.apply(value)}
160
+ speaker(value){this.#speaker=value}
161
+ garden(value){this.#garden=value}
162
+ activity(value){this.#spatial?.activity(value)}
163
+ /** @param {'in'|'out'|'up'|'down'|'left'|'right'|'reset'} action */
164
+ changeCamera(action){this.#spatial?.change(action)}
165
+ close(){this.#spatial?.close();this.#spatial=null;this.#abort?.abort();this.#actionHandles.clear();this.#root.replaceChildren();for(const slot of new Set([this.#slots.tools,this.#slots.details,this.#slots.actions].filter(Boolean)))slot.replaceChildren();if(this.#ownsSurface){this.#surface.close();this.#surface=createWorldSurface()}}
166
+ dispose(){this.close();if(this.#ownsSurface)this.#surface.close()}
167
+ }
168
+ /** @param {Document} document
169
+ * @param {import('@zixcel/interaction/input').InputDeclaration} declaration
170
+ * @param {(values:Record<string,string|boolean|number>)=>unknown|Promise<unknown>} onSubmit */
171
+ export function inputForm(document,declaration,onSubmit){
172
+ if(validateInputDeclaration(declaration).length)fail('InvalidInputDeclaration')
173
+ const form=element(document,'form'),fields=new Map(),abort=new AbortController(),prefix='input-'+crypto.randomUUID()
174
+ const fieldIds=[...new Set([...declaration.action.input_schema.required,...Object.keys(declaration.fields)])]
175
+ for(const id of fieldIds){
176
+ const field=declaration.fields[id]
177
+ const schema=declaration.action.input_schema.fields[id],label=element(document,'label'),name=element(document,'span',field.label)
178
+ let input
179
+ if(field.choices){input=element(document,'select');input.append(element(document,'option','Choose…',{value:''}));for(const choice of field.choices)input.append(element(document,'option',choice.label,{value:choice.value}))}
180
+ else if(schema.type==='string'&&!field.sensitive&&schema.max_length>255){input=element(document,'textarea');input.rows=4;input.maxLength=schema.max_length}
181
+ else{input=element(document,'input');input.type=schema.type==='boolean'?'checkbox':['integer','number'].includes(schema.type)?'number':field.sensitive?'password':'text'
182
+ if(schema.type==='string')input.maxLength=schema.max_length??4096
183
+ if(schema.type==='integer')input.step='1'
184
+ if(schema.type==='number')input.step='any'
185
+ if(schema.minimum!==undefined)input.min=String(schema.minimum)
186
+ if(schema.maximum!==undefined)input.max=String(schema.maximum)
187
+ }
188
+ input.id=prefix+'-'+fields.size;input.name=id;input.autocomplete='off';label.htmlFor=input.id;label.append(name);form.append(label,input);fields.set(id,{input,schema})
189
+ }
190
+ const submit=element(document,'button','Submit',{type:'submit'}),status=element(document,'div','',{role:'status'});form.append(submit,status)
191
+ form.addEventListener('submit',async event=>{
192
+ event.preventDefault();if(submit.disabled)return
193
+ const values={};for(const [id,{input,schema}]of fields){
194
+ // An absent optional value is not zero, null, false or an empty string.
195
+ if(input.value===''&&schema.type!=='boolean'&&!declaration.action.input_schema.required.includes(id))continue
196
+ values[id]=schema.type==='boolean'?input.checked:['number','integer'].includes(schema.type)?(input.value===''?null:Number(input.value)):input.value
197
+ }
198
+ if(validateInputValues(declaration,values).length){status.textContent='Check the declared input constraints.';return}
199
+ submit.disabled=true;form.setAttribute('aria-busy','true')
200
+ try{await onSubmit(values);if(!abort.signal.aborted)status.textContent='Submitted.'}
201
+ catch(error){if(!abort.signal.aborted){status.setAttribute('role','alert');status.replaceChildren(valueView(document,{code:error.code??'OperationUnavailable',...(error.ownerFailure?{ownerFailure:error.ownerFailure}:{})}))}}
202
+ finally{for(const {input}of fields.values())if(input.type==='password')input.value='';submit.disabled=false;form.setAttribute('aria-busy','false')}
203
+ },{signal:abort.signal})
204
+ return {form,close(){abort.abort();for(const {input}of fields.values())input.value='';form.remove()}}
205
+ }
@@ -0,0 +1 @@
1
+ export function inspectionOverview(document: any, host: any, view: any, onSource: any, signal: any): void;
@@ -0,0 +1,35 @@
1
+ // Owner-projected facts only. This renderer does not classify semantic modules.
2
+ import {element} from './index.mjs'
3
+ export function inspectionOverview(document,host,view,onSource,signal){
4
+ host.replaceChildren()
5
+ if(!view){host.append(element(document,'p','Responsibility overview is unavailable.'));return}
6
+ host.append(element(document,'h2',view.title),element(document,'p',view.note,{class:'inspection-note'}))
7
+ const groups=element(document,'ul',null,{class:'scene-items'})
8
+ groups.style.gridTemplateColumns='repeat(auto-fit,minmax(min(250px,100%),1fr))'
9
+ for(const group of view.groups){
10
+ const section=element(document,'li',null,{'aria-label':group.title,'data-responsibility':group.id})
11
+ const info=element(document,'details'),description=element(document,'summary','About these observations')
12
+ info.append(description,element(document,'p',group.note))
13
+ section.append(element(document,'h3',group.title),element(document,'p',group.state,{class:'inspection-state'}))
14
+ for(const measure of group.measures){
15
+ const row=element(document,'div',null,{class:'item-tools','data-measure':measure.unit})
16
+ row.append(element(document,'strong',measure.value===null?'Unavailable':String(measure.value)+(measure.partial?' +':'')),element(document,'span',measure.unit))
17
+ if(measure.documentId){const button=element(document,'button','Inspect',{type:'button','aria-label':'Inspect '+measure.unit});button.addEventListener('click',()=>onSource(measure.documentId),{signal});row.append(button)}
18
+ section.append(row)
19
+ }
20
+ if(group.values?.length)section.append(element(document,'p',group.values.join(' · ')))
21
+ section.append(info);groups.append(section)
22
+ }
23
+ host.append(groups,element(document,'h3','Adopted definition modules'))
24
+ if(!view.modulesAvailable){host.append(element(document,'p','Module data unavailable.'));return}
25
+ if(!view.modules.length){host.append(element(document,'p','No definition modules in the observed role.'));return}
26
+ const list=element(document,'ul',null,{class:'inspection-modules'})
27
+ for(const module of view.modules){
28
+ const item=element(document,'li'),button=element(document,'button',module.id,{type:'button'})
29
+ button.addEventListener('click',()=>onSource('definitions'),{signal})
30
+ item.append(button,element(document,'p',`${module.version} · ${module.terms} terms · ${module.expressions} expressions`),element(document,'p',module.dependencies.length?'Imports: '+module.dependencies.join(', '):'No declared imports'))
31
+ list.append(item)
32
+ }
33
+ host.append(list)
34
+ if(view.modulesPartial)host.append(element(document,'p','Module inventory is incomplete.'))
35
+ }
@@ -0,0 +1,5 @@
1
+ export function npcMotion(id: any, milliseconds: any, state?: string): {
2
+ x: number;
3
+ y: number;
4
+ swing: number;
5
+ };
@@ -0,0 +1,7 @@
1
+ // Ambient presentation only. This never changes semantic position or Work.
2
+ export function npcMotion(id,milliseconds,state='Idle'){
3
+ const seed=[...id].reduce((n,c)=>(Math.imul(n,31)+c.charCodeAt(0))|0,0)>>>0
4
+ const active=['Runnable','Running','AwaitingResult'].includes(state)
5
+ const phase=milliseconds/1000*(active?2.2:1.1)*(0.85+(seed%7)/20)+(seed%97)*.31
6
+ return {x:Math.sin(phase)*(active?.2:.08),y:Math.max(0,Math.sin(phase*1.7))*(active?.12:.04),swing:Math.sin(phase*1.4)*(active?.16:.08)}
7
+ }
@@ -0,0 +1,55 @@
1
+ export function objectPresentation(item: any): {
2
+ form: any;
3
+ dataType?: undefined;
4
+ needsReview?: undefined;
5
+ } | {
6
+ form: string;
7
+ dataType: any;
8
+ needsReview: boolean;
9
+ };
10
+ export const spacingPresets: Readonly<{
11
+ compact: {
12
+ person: number;
13
+ group: number;
14
+ member: number;
15
+ };
16
+ balanced: {
17
+ person: number;
18
+ group: number;
19
+ member: number;
20
+ };
21
+ spacious: {
22
+ person: number;
23
+ group: number;
24
+ member: number;
25
+ };
26
+ }>;
27
+ export const objectForms: Readonly<{
28
+ person: "Person";
29
+ organization: "Organization";
30
+ system: "System";
31
+ shelf: "Bookshelf";
32
+ book: "Book";
33
+ }>;
34
+ export const libraryLayout: Readonly<{
35
+ columns: 6;
36
+ rows: 4;
37
+ slotWidth: 0.46;
38
+ rowHeight: 0.63;
39
+ baseY: 0.32;
40
+ frontZ: 0.32;
41
+ }>;
42
+ export const objectBounds: Readonly<{
43
+ person: number[];
44
+ organization: number[];
45
+ system: number[];
46
+ shelf: number[];
47
+ book: number[];
48
+ }>;
49
+ export const objectTemplates: Readonly<{
50
+ person: (string | number)[][];
51
+ organization: (string | number)[][];
52
+ system: (string | number)[][];
53
+ shelf: (string | number)[][];
54
+ book: (string | number)[][];
55
+ }>;
@@ -0,0 +1,24 @@
1
+ // Trusted presentation policy, not semantic classification or operation authority.
2
+ // Never infer meaning from a title, locator or a numeric magnitude.
3
+ export const spacingPresets=Object.freeze({compact:{person:5,group:3,member:1.3},balanced:{person:7,group:4,member:1.8},spacious:{person:10,group:6,member:2.6}})
4
+ export const objectForms=Object.freeze({person:'Person',organization:'Organization',system:'System',shelf:'Bookshelf',book:'Book'})
5
+ // Temporary placement policy. Furniture is derived from nonempty view groups,
6
+ // not persisted as semantic data. A page occupies six columns × four shelves.
7
+ export const libraryLayout=Object.freeze({columns:6,rows:4,slotWidth:.46,rowHeight:.63,baseY:.32,frontZ:.32})
8
+ export const objectBounds=Object.freeze({person:[1.5,2.5,1.1],organization:[2.8,3.2,2],system:[1.8,2,1],shelf:[3.4,3.2,.9],book:[.4,.62,.6]})
9
+ // Each part is [x,y,z,width,height,depth,tint]. Local coordinates and closed
10
+ // tint tokens keep assets declarative: no external scripts, images or model IO.
11
+ export const objectTemplates=Object.freeze({
12
+ person:[[-.2,.48,0,.28,.8,.34,'dark'],[.2,.48,0,.28,.8,.34,'dark'],[0,1.26,0,.88,.86,.48,'accent'],[-.59,1.22,0,.25,.9,.3,'accent'],[.59,1.22,0,.25,.9,.3,'accent'],[0,2,0,.62,.62,.58,'face'],[0,2.34,0,.68,.15,.63,'dark'],[-.15,2.04,.302,.075,.08,.025,'dark'],[.15,2.04,.302,.075,.08,.025,'dark']],
13
+ organization:[[0,1.55,0,2.6,3,1.8,'accent'],[0,3.1,0,2.8,.2,2,'dark'],[-.6,1.8,.92,.55,.65,.08,'light'],[.6,1.8,.92,.55,.65,.08,'light'],[0,.5,.94,.7,.95,.08,'dark']],
14
+ system:[[0,.25,0,1.5,.5,.9,'dark'],[0,1.2,0,1.4,1.5,.3,'accent'],[0,1.35,.18,1.1,1,.05,'light'],[0,.6,.45,1.2,.1,.15,'warning']],
15
+ shelf:[[-1.55,1.5,0,.15,3,.65,'dark'],[1.55,1.5,0,.15,3,.65,'dark'],[0,1.5,-.32,3.25,3,.08,'accent'],...Array.from({length:5},(_,i)=>[0,.25+i*.63,0,3.25,.1,.7,'dark'])],
16
+ book:[[-.15,.3,0,.05,.58,.5,'accent'],[.15,.3,0,.05,.58,.5,'accent'],[0,.3,-.22,.3,.58,.06,'accent'],[0,.3,.025,.24,.5,.39,'light']]
17
+ })
18
+ export function objectPresentation(item){
19
+ if(item.form&&Object.hasOwn(objectForms,item.form))return {form:item.form}
20
+ if(item.kind==='character')return {form:'person'}
21
+ if(item.kind==='group')return {form:'shelf'}
22
+ const values=item.scalars??[],types=[...new Set(values.filter(v=>v!==null).map(v=>typeof v))]
23
+ return {form:'book',dataType:types.length===1?({string:'text',number:'number',boolean:'boolean'}[types[0]]??'record'):'record',needsReview:Boolean(item.unresolved)}
24
+ }
@@ -0,0 +1,29 @@
1
+ export function visibleProjection(sources: any, { x, z, radius, limit, marked }?: {
2
+ x?: number;
3
+ z?: number;
4
+ radius?: number;
5
+ limit?: number;
6
+ marked?: any;
7
+ }): {
8
+ items: any[];
9
+ markers: any[];
10
+ total: number;
11
+ omitted: number;
12
+ };
13
+ export function mapProjection(sources: any, { selected, marked, limit }?: {
14
+ selected?: any;
15
+ marked?: any;
16
+ limit?: number;
17
+ }): {
18
+ items: {
19
+ id: any;
20
+ title: any;
21
+ kind: any;
22
+ x: any;
23
+ z: any;
24
+ selected: boolean;
25
+ marked: boolean;
26
+ }[];
27
+ total: number;
28
+ omitted: number;
29
+ };
@@ -0,0 +1,26 @@
1
+ // Read-only, bounded views over exact owner IDs. Coordinates are presentation,
2
+ // never evidence of a semantic relation or an action authority.
3
+ export function visibleProjection(sources,{x=0,z=0,radius=24,limit=200,marked=null}={}){
4
+ if(!Array.isArray(sources)||sources.length>10000||!Number.isFinite(x)||!Number.isFinite(z)
5
+ ||!Number.isFinite(radius)||radius<=0||!Number.isSafeInteger(limit)||limit<1||limit>200)throw Error('WorldProjectionLimitExceeded')
6
+ const candidates=[],seen=new Set();let mark=null
7
+ for(const source of sources){
8
+ if(typeof source.id!=='string'||!source.id||seen.has(source.id)||!Number.isFinite(source.x)||!Number.isFinite(source.z))throw Error('WorldProjectionSourceInvalid')
9
+ seen.add(source.id)
10
+ const dx=source.x-x,dz=source.z-z,distance=dx*dx+dz*dz
11
+ if(distance<=radius*radius)candidates.push({source,distance})
12
+ if(source.id===marked)mark=source
13
+ }
14
+ candidates.sort((a,b)=>a.distance-b.distance||a.source.id.localeCompare(b.source.id))
15
+ const items=candidates.slice(0,limit).map(({source})=>source)
16
+ return {items,markers:mark&&!items.some(item=>item.id===mark.id)?[mark]:[],total:sources.length,omitted:candidates.length-items.length}
17
+ }
18
+
19
+ export function mapProjection(sources,{selected=null,marked=null,limit=128}={}){
20
+ if(!Array.isArray(sources)||sources.length>10000||!Number.isSafeInteger(limit)||limit<1||limit>128)throw Error('WorldProjectionLimitExceeded')
21
+ const seen=new Set()
22
+ for(const item of sources){if(typeof item.id!=='string'||!item.id||seen.has(item.id)||!Number.isFinite(item.x)||!Number.isFinite(item.z))throw Error('WorldProjectionSourceInvalid');seen.add(item.id)}
23
+ const priority=sources.filter(item=>item.id===selected||item.id===marked),base=sources.filter(item=>item.id!==selected&&item.id!==marked)
24
+ const chosen=sources.length<=limit?sources:[...priority,...base].slice(0,limit)
25
+ return {items:chosen.map(item=>({id:item.id,title:item.title,kind:item.kind,x:item.x,z:item.z,selected:item.id===selected,marked:item.id===marked})),total:sources.length,omitted:sources.length-chosen.length}
26
+ }
@@ -0,0 +1 @@
1
+ export function reconcileChildren(parent: any, children: any): void;
@@ -0,0 +1,21 @@
1
+ // Trusted renderer output only. No user forms or independently owned DOM trees.
2
+ const key=(node,index)=>node.nodeType===1
3
+ ?[node.tagName,...['data-item-id','data-world-object','data-region','data-control'].flatMap(k=>node.hasAttribute(k)?[k,node.getAttribute(k)]:[])].join(':')+(node.matches('[data-item-id],[data-world-object],[data-region],[data-control]')?'':':'+index)
4
+ :node.nodeType+':'+index
5
+ export function reconcileChildren(parent,children){
6
+ const old=new Map([...parent.childNodes].map((node,index)=>[key(node,index),node])),wanted=[]
7
+ for(const [index,fresh]of [...children].entries()){
8
+ const identity=key(fresh,index),existing=old.get(identity)
9
+ if(existing&&existing.nodeType===fresh.nodeType&&existing.nodeName===fresh.nodeName){
10
+ old.delete(identity)
11
+ if(existing.nodeType===1){
12
+ for(const attribute of [...existing.attributes])if(!fresh.hasAttribute(attribute.name))existing.removeAttribute(attribute.name)
13
+ for(const attribute of fresh.attributes)if(existing.getAttribute(attribute.name)!==attribute.value)existing.setAttribute(attribute.name,attribute.value)
14
+ reconcileChildren(existing,fresh.childNodes)
15
+ }else if(existing.nodeValue!==fresh.nodeValue)existing.nodeValue=fresh.nodeValue
16
+ wanted.push(existing)
17
+ }else wanted.push(fresh)
18
+ }
19
+ for(const node of old.values())node.remove()
20
+ for(let index=0;index<wanted.length;index++)if(parent.childNodes[index]!==wanted[index])parent.insertBefore(wanted[index],parent.childNodes[index]??null)
21
+ }
@@ -0,0 +1,29 @@
1
+ export function spatialPattern(objects: any, relations?: any[], options?: {}): {
2
+ objects: any;
3
+ relations: any[];
4
+ groups: any[];
5
+ state: {
6
+ pattern: any;
7
+ groupBy: any;
8
+ group: any;
9
+ page: number;
10
+ };
11
+ total: any;
12
+ pages: number;
13
+ label: string;
14
+ } | {
15
+ objects: any[];
16
+ relations: any[];
17
+ groups: any[];
18
+ state: {
19
+ pattern: any;
20
+ groupBy: any;
21
+ group: any;
22
+ page: number;
23
+ };
24
+ total: number;
25
+ pages: number;
26
+ label: any;
27
+ };
28
+ export const spatialPatterns: readonly string[][];
29
+ export const spatialGroupings: readonly string[][];
@@ -0,0 +1,43 @@
1
+ import {layoutWorld} from './world-model.mjs'
2
+
3
+ // View-local partitions, never entities, inferred meanings, relationships or commands.
4
+ export const spatialPatterns=Object.freeze([['garden','Garden'],['ground','Ground · people and places'],['constellation','Constellation · information']])
5
+ export const spatialGroupings=Object.freeze([['declared','Declared structure'],['meaning','Meaning reference'],['source','Source owner']])
6
+ const pageSize=24
7
+ export function spatialPattern(objects,relations=[],options={}){
8
+ layoutWorld(objects,relations) // Validate even when a collapsed view would hide bad input.
9
+ const pattern=options.pattern??'ground',groupBy=options.groupBy??'declared'
10
+ if(!spatialPatterns.some(([id])=>id===pattern))throw Error('SpatialPatternInvalid')
11
+ if(!spatialGroupings.some(([id])=>id===groupBy))throw Error('SpatialGroupingInvalid')
12
+ const state={pattern,groupBy,group:null,page:0}
13
+ if(pattern==='ground')return {objects,relations,groups:[],state,total:objects.length,pages:1,label:'All objects'}
14
+ const actors=objects.filter(i=>i.kind==='character'),records=objects.filter(i=>i.kind!=='character'),partitions=new Map(),ids=new Set(objects.map(i=>i.id))
15
+ for(const item of records){
16
+ let key=null,title='Unclassified'
17
+ if(groupBy==='declared'&&item.group?.kind&&item.group?.ref){key=[item.group.kind,item.group.ref];title=item.groupLabel??item.group.ref}
18
+ if(groupBy==='meaning'&&typeof item.semanticRef==='string'){key=item.semanticRef;title='Meaning · '+key}
19
+ if(groupBy==='source'){
20
+ const owners=[...new Set((item.sourceRefs??[]).map(s=>s.owner).filter(v=>typeof v==='string'))].sort()
21
+ if(owners.length){key=owners;title=owners.join(' + ')}
22
+ }
23
+ const identity=JSON.stringify([groupBy,key]);let group=partitions.get(identity)
24
+ if(!group){let id='view:group:'+identity;while(ids.has(id))id='view:'+id;ids.add(id);group={id,kind:'group',title,memberIds:[],members:[]};partitions.set(identity,group)}
25
+ group.memberIds.push(item.id);group.members.push(item)
26
+ }
27
+ const groups=[...partitions.values()],selected=groups.find(g=>g.id===options.group)
28
+ state.group=selected?.id??null
29
+ if(pattern==='garden'){
30
+ if(actors.length>32)throw Error('WorldCapacityExceeded')
31
+ // Records remain in the exact focused projection. A shelf is a local index
32
+ // for that person's information, not an entity standing beside the person.
33
+ state.group=null;state.page=0
34
+ const actorIds=new Set(actors.map(i=>i.id))
35
+ return {objects:actors,relations:relations.filter(r=>actorIds.has(r.from)&&actorIds.has(r.to)),groups:groups.map(({members,...g})=>g),state,total:actors.length,pages:1,label:'People'}
36
+ }
37
+ const candidates=selected?selected.members:groups.map(({members,...g})=>({...g,summary:g.memberIds.length+' records · Browse shelf'}))
38
+ // Actors share the same paging budget; no silent truncation or unbounded group expansion.
39
+ const all=[...actors,...candidates],pages=Math.max(1,Math.ceil(all.length/pageSize))
40
+ state.page=state.group!==options.group&&options.group?0:Math.min(pages-1,Math.max(0,Number.isSafeInteger(options.page)?options.page:0))
41
+ const visible=all.slice(state.page*pageSize,(state.page+1)*pageSize),visibleIds=new Set(visible.filter(i=>i.kind!=='group').map(i=>i.id))
42
+ return {objects:visible,relations:relations.filter(r=>visibleIds.has(r.from)&&visibleIds.has(r.to)),groups:groups.map(({members,...g})=>g),state,total:all.length,pages,label:selected?.title??'All groups'}
43
+ }
@@ -0,0 +1,8 @@
1
+ export function spatialView(viewport: any, board: any, camera: any, relations: any, signal: any, pose: any, conversation?: any, presentation?: {}): {
2
+ change: any;
3
+ apply: any;
4
+ snapshot: any;
5
+ pause: any;
6
+ activity: any;
7
+ close: () => void;
8
+ };
@@ -0,0 +1,27 @@
1
+ import {createWorld} from './world-runtime.mjs'
2
+ // Installed RenderPlan/role controls remain the only operation authority.
3
+ export function spatialView(viewport,board,camera,relations,signal,pose,conversation=null,presentation={}){
4
+ const nodes=()=>[...board.querySelectorAll('[data-item-id],[data-world-object]')]
5
+ const declarations=()=>nodes().filter(node=>!node.hidden).map(node=>({
6
+ id:node.dataset.itemId??node.dataset.worldObject,
7
+ ...presentation.items?.get(node.dataset.itemId),
8
+ kind:node.hasAttribute('data-world-object')?'character':'record',
9
+ title:node.querySelector('strong,[data-kind=select]')?.textContent??'Information',
10
+ summary:node.querySelector('[data-item-summary]')?.textContent?.slice(0,80)??'',
11
+ observationLabel:node.dataset.observationLabel??'',
12
+ disabled:node.disabled===true,selected:node.dataset.selected==='true'
13
+ }))
14
+ const options={objects:declarations(),relations,pose,conversation,activity:presentation.activity,target:presentation.target,pattern:presentation.pattern,context:presentation.context,onActivate:id=>{
15
+ const node=nodes().find(node=>(node.dataset.itemId??node.dataset.worldObject)===id)
16
+ if(!node||node.hidden||node.disabled)return
17
+ ;(node.hasAttribute('data-world-object')?node:node.querySelector('[data-kind=inspect]'))?.click()
18
+ }}
19
+ const lease=presentation.surface?.attach(viewport,options),world=lease?.world??createWorld(viewport,options)
20
+ board.hidden=true;world.apply(camera)
21
+ const observer=new viewport.ownerDocument.defaultView.MutationObserver(()=>world.update(declarations(),relations))
22
+ observer.observe(board,{subtree:true,childList:true,characterData:true,attributes:true,attributeFilter:['hidden','data-selected','disabled']})
23
+ let closed=false
24
+ const close=()=>{if(closed)return;closed=true;observer.disconnect();if(lease)lease.release();else world.close();board.hidden=false}
25
+ signal.addEventListener('abort',close,{once:true})
26
+ return {change:world.change,apply:world.apply,snapshot:world.snapshot,pause:world.pause,activity:world.activity,close}
27
+ }
@@ -0,0 +1,16 @@
1
+ export function graphDocuments(documents: any, { component, query, depth, maximum }?: {
2
+ component?: string;
3
+ query?: string;
4
+ depth?: number;
5
+ maximum?: number;
6
+ }): {
7
+ nodes: any[];
8
+ edges: any[];
9
+ truncated: boolean;
10
+ };
11
+ export function structureGraph(document: any, host: any, view: any, onRole: any, onRefresh: any, { embedded }?: {
12
+ embedded?: boolean;
13
+ }): {
14
+ update(next: any): void;
15
+ close(): void;
16
+ };