88 lines
5.0 KiB
JavaScript
88 lines
5.0 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { writeFile } from 'node:fs/promises';
|
|
import { performance } from 'node:perf_hooks';
|
|
// Isolated policy comparison; these illustrative metrics are not Studio rules.
|
|
const separation = 10;
|
|
function absolute(rows) {
|
|
let end = -separation;
|
|
return rows.map(r => { const y = Math.max(r.y, end + separation); end=y+r.height; return {...r,y}; });
|
|
}
|
|
function gaps(rows) {
|
|
let end=0;
|
|
return rows.map((r,i) => { assert(r.gap >= (i?separation:0)); const y=end+r.gap; end=y+r.height; return {...r,y}; });
|
|
}
|
|
function encodeGaps(rows) {
|
|
return rows.map((r,i)=>({...r,gap:r.y-(i?rows[i-1].y+rows[i-1].height:0)}));
|
|
}
|
|
const positions = rows=>rows.map(r=>r.y);
|
|
function moveAbsolute(rows,index,requested) {
|
|
const current=absolute(rows);
|
|
const floor=index?current[index-1].y+current[index-1].height+separation:0;
|
|
const candidate=rows.map((r,i)=>({...r,y:i===index?Math.max(requested,floor):r.y}));
|
|
return {candidate,preview:absolute(candidate)};
|
|
}
|
|
function changeCount(before,after,key) {return before.reduce((n,r,i)=>n+(r[key]!==after[i][key]),0);}
|
|
const initial=[{id:'A',y:100,height:20},{id:'B',y:200,height:20}];
|
|
const initialG=encodeGaps(initial);
|
|
const cases=[];
|
|
function named(name,a,g,expectedA,expectedG) {
|
|
assert.deepEqual(positions(a),expectedA,name+' absolute');
|
|
assert.deepEqual(positions(g),expectedG,name+' gaps');
|
|
cases.push({name,absolute:positions(a),gapsWithoutInverse:positions(g)});
|
|
}
|
|
named('move A to50',absolute([{...initial[0],y:50},initial[1]]),gaps([{...initialG[0],gap:50},initialG[1]]),[50,200],[50,150]);
|
|
named('move A to250',absolute([{...initial[0],y:250},initial[1]]),gaps([{...initialG[0],gap:250},initialG[1]]),[250,280],[250,350]);
|
|
named('resize A to80',absolute([{...initial[0],height:80},initial[1]]),gaps([{...initialG[0],height:80},initialG[1]]),[100,200],[100,260]);
|
|
named('resize A to120',absolute([{...initial[0],height:120},initial[1]]),gaps([{...initialG[0],height:120},initialG[1]]),[100,230],[100,300]);
|
|
named('source reorder B A',absolute([initial[1],initial[0]]),gaps([initialG[1],initialG[0]]),[200,230],[80,200]);
|
|
named('delete A',absolute([initial[1]]),gaps([initialG[1]]),[200],[80]);
|
|
assert.deepEqual(positions(moveAbsolute(initial,1,110).preview),[100,130]);
|
|
let feasible=0,moves=0,absoluteChanges=0,gapChanges=0;
|
|
const started=performance.now();
|
|
for(const ha of [10,20,30])for(const hb of [10,20,30])for(const hc of [10,20,30])
|
|
for(let a=0;a<=40;a+=10)for(let b=a+ha+10;b<=90;b+=10)for(let c=b+hb+10;c<=140;c+=10) {
|
|
const rows=[{id:'A',height:ha,y:a},{id:'B',height:hb,y:b},{id:'C',height:hc,y:c}];
|
|
assert.deepEqual(positions(absolute(rows)),[a,b,c]);
|
|
assert.deepEqual(positions(gaps(encodeGaps(rows))),[a,b,c]);
|
|
feasible++;
|
|
for(let i=0;i<3;i++)for(const delta of [-50,-20,0,10,40]) {
|
|
const requested=rows[i].y+delta, {candidate,preview}=moveAbsolute(rows,i,requested);
|
|
const independent=[];
|
|
for(let j=0;j<3;j++) {
|
|
const floor=j?independent[j-1]+rows[j-1].height+10:0;
|
|
independent.push(Math.max(j===i?requested:rows[j].y,floor));
|
|
}
|
|
assert.deepEqual(positions(preview),independent);
|
|
assert.deepEqual(positions(absolute(preview)),independent);
|
|
assert.deepEqual(positions(absolute(JSON.parse(JSON.stringify(candidate)))),independent);
|
|
const encoded=encodeGaps(preview);
|
|
assert.deepEqual(positions(gaps(encoded)),independent);
|
|
assert.deepEqual(candidate.map(r=>r.id),['A','B','C']);
|
|
for(let j=0;j<i;j++) assert.deepEqual(candidate[j],rows[j]);
|
|
absoluteChanges+=changeCount(rows,candidate,'y');
|
|
gapChanges+=changeCount(encodeGaps(rows),encoded,'gap');
|
|
moves++;
|
|
}
|
|
}
|
|
// Independent event/frame attachment probes: no actual Scene/activation engine.
|
|
const selfCall=absolute([{id:'call',y:250,height:24},{id:'reply',y:320,height:0}]);
|
|
assert.equal(selfCall[0].y+24,274);
|
|
assert.equal(selfCall[1].y,320);
|
|
const children=[{y:160,height:20},{y:260,height:20}];
|
|
const requiredFrameBottom=Math.max(...children.map(r=>r.y+r.height))+20;
|
|
assert.equal(requiredFrameBottom,300);
|
|
assert.equal(Math.max(300,requiredFrameBottom),300);
|
|
assert.equal(Math.max(220,requiredFrameBottom),300);
|
|
assert.deepEqual(children,[{y:160,height:20},{y:260,height:20}]);
|
|
assert.equal(220-90,130); // delayed header top from moved create receive and header height
|
|
const result={
|
|
scope:'Illustrative linear policy and anchor experiment; no production parser, REST, Scene, GUI or full fragment/lifecycle acceptance.',
|
|
separation, cases, feasibleLayouts:feasible, moveCases:moves,
|
|
changedAuthoredCoordinateRecords:{absolute:absoluteChanges,gapsWithInverse:gapChanges},
|
|
elapsedMs:performance.now()-started,
|
|
assertions:'passed',
|
|
interpretation:'Both policies express all enumerated feasible layouts. Absolute coordinates preserve unaffected positions with fewer authored coordinate changes under these fixtures. Choose no production schema from this alone; full scope/anchor/inverse and source lifecycle validation remain required.'
|
|
};
|
|
await writeFile(new URL('./results.json',import.meta.url),JSON.stringify(result,null,2)+'\n',{flag:'wx'});
|
|
export default result;
|