Files
ARCADIA-Studio/harness/experiments/process-column-slots-20260923/experiment.cjs
T

120 lines
6.6 KiB
JavaScript

// Exploratory model, not production code or a test of the Rust implementation.
const fs = require('fs');
const assert = require('assert/strict');
let count = 0, graphCount = 0, admitted = 0, refused = 0, noops = 0;
function* permutations(a) {
if (!a.length) { yield []; return; }
for (let i=0;i<a.length;i++) for (const tail of permutations(a.filter((_,j)=>i!==j))) yield [a[i],...tail];
}
function* partitions(n, prefix=[]) {
if(prefix.length===n) { yield prefix; return; }
const max=prefix.length?Math.max(...prefix)+1:0;
for(let x=0;x<=max;x++) yield* partitions(n,[...prefix,x]);
}
function plan(steps, ranks, selected, rawGap) {
const rank=ranks[selected[0]];
if(!selected.length || selected.some(id=>ranks[id]!==rank)) return null;
const indices=steps.flatMap((id,i)=>ranks[id]===rank?[i]:[]);
const column=indices.map(i=>steps[i]), set=new Set(selected);
assert(rawGap>=0 && rawGap<=column.length);
const block=column.filter(id=>set.has(id));
const remainder=column.filter(id=>!set.has(id));
const gap=column.slice(0,rawGap).filter(id=>!set.has(id)).length;
const order=[...remainder.slice(0,gap),...block,...remainder.slice(gap)];
const result=steps.slice();
indices.forEach((index,i)=>result[index]=order[i]);
return {result, order, gap, changed:result.some((id,i)=>id!==steps[i])};
}
function verify(steps,ranks,selected,rawGap,p) {
const set=new Set(selected), rank=ranks[selected[0]];
const old=steps.filter(id=>ranks[id]===rank), got=p.result.filter(id=>ranks[id]===rank);
assert.deepEqual([...p.result].sort(),[...steps].sort());
steps.forEach((id,i)=>{if(ranks[id]!==rank) assert.equal(p.result[i],id);});
assert.deepEqual(got.filter(id=>set.has(id)),old.filter(id=>set.has(id)));
assert.deepEqual(got.filter(id=>!set.has(id)),old.filter(id=>!set.has(id)));
const first=got.findIndex(id=>set.has(id));
assert.equal(got.slice(first,first+set.size).every(id=>set.has(id)),true);
// Independent gap oracle: every original unselected item before boundary
// precedes every selected item; all remaining unselected items follow them.
old.forEach((id,i)=>{if(!set.has(id)) selected.forEach(s=>assert.equal(got.indexOf(id)<got.indexOf(s),i<rawGap));});
const stableAlready=old.every((id,i)=>id===got[i]);
assert.equal(p.changed,!stableAlready);
const source=JSON.stringify({steps,untouched:'sentinel'});
const after=p.changed?JSON.stringify({steps:p.result,untouched:'sentinel'}):source;
if(stableAlready) {assert.equal(after,source); noops++;}
count++;
}
for(let n=1;n<=5;n++) {
const ids=Array.from({length:n},(_,i)=>i);
for(const ranks of partitions(n)) for(const steps of permutations(ids)) {
for(let mask=1;mask<(1<<n);mask++) {
const selected=ids.filter(id=>mask&(1<<id));
if(selected.some(id=>ranks[id]!==ranks[selected[0]])) {
assert.equal(plan(steps,ranks,selected,0),null); refused++; continue;
}
const column=steps.filter(id=>ranks[id]===ranks[selected[0]]);
const equivalents=new Map();
for(let gap=0;gap<=column.length;gap++) {
const p=plan(steps,ranks,selected,gap);
verify(steps,ranks,selected,gap,p);
assert.deepEqual(p,plan(steps,ranks,[...selected].reverse(),gap));
if(equivalents.has(p.gap)) assert.deepEqual(p.result,equivalents.get(p.gap));
equivalents.set(p.gap,p.result);
}
}
}
}
// Semantic column model: longest-path Kahn ranks and deterministic residual
// fallback, matching the documented algorithm, independently of planner.
function ranksOf(steps,edges) {
const indegree=Object.fromEntries(steps.map(id=>[id,0]));
const ranks=Object.fromEntries(steps.map(id=>[id,0]));
edges.forEach(([,b])=>indegree[b]++);
const queue=steps.filter(id=>indegree[id]===0), done=new Set();
for(let i=0;i<queue.length;i++) {
const id=queue[i]; if(done.has(id))continue; done.add(id);
for(const [a,b] of edges) if(a===id) {
ranks[b]=Math.max(ranks[b],ranks[a]+1);
if(--indegree[b]===0)queue.push(b);
}
}
let fallback=Math.max(...Object.values(ranks))+1;
steps.forEach(id=>{if(!done.has(id))ranks[id]=fallback++;});
return ranks;
}
const ids=[0,1,2,3], possible=ids.flatMap(a=>ids.filter(b=>a!==b).map(b=>[a,b]));
for(let mask=0;mask<(1<<possible.length);mask++) {
const edges=possible.filter((_,i)=>mask&(1<<i));
for(const steps of permutations(ids)) {
const ranks=ranksOf(steps,edges); graphCount++;
for(let subset=1;subset<16;subset++) {
const selected=ids.filter(id=>subset&(1<<id));
if(selected.some(id=>ranks[id]!==ranks[selected[0]]))continue;
const column=steps.filter(id=>ranks[id]===ranks[selected[0]]);
for(let gap=0;gap<=column.length;gap++) {
const p=plan(steps,ranks,selected,gap);
assert.deepEqual(ranksOf(p.result,edges),ranks); admitted++;
}
}
}
}
const concrete=[];
function example(name,steps,ranks,selected,gap,expected) {
const p=plan(steps,ranks,selected,gap); assert.deepEqual(p?.result??null,expected);
concrete.push({name,steps,ranks,selected,rawGap:gap,result:p?.result??null,changed:p?.changed??false});
}
example('interleaved visual no-op',['a','u','b','v','c'],{a:0,b:0,c:0,u:1,v:1},['c','b'],1,['a','u','b','v','c']);
example('column slot move preserves other indices',['a','u','b','v','c'],{a:0,b:0,c:0,u:1,v:1},['c','b'],0,['b','u','c','v','a']);
example('selected-anchor normalization is not always no-op',['a','b','c','d'],{a:0,b:0,c:0,d:0},['b','d'],3,['a','c','b','d']);
example('legacy successive-rank refusal',['a','b','c'],{a:0,b:1,c:2},['a','c'],0,null);
const cyclicRanks=ranksOf([0,1,2,3],[[2,3],[3,2]]);
example('cycle fallback group refusal',[0,1,2,3],cyclicRanks,[2,3],0,null);
example('acyclic peers beside cycle',[0,1,2,3],cyclicRanks,[1],0,[1,0,2,3]);
const besideCycleSteps=['a','u','b','v','c'], besideCycleEdges=[['u','v'],['v','u']];
const besideCycleRanks=ranksOf(besideCycleSteps,besideCycleEdges);
example('changed multiple-member group beside cycle',besideCycleSteps,besideCycleRanks,['c','b'],0,['b','u','c','v','a']);
assert.deepEqual(ranksOf(['b','u','c','v','a'],besideCycleEdges),besideCycleRanks);
const result={expectation:'Column-slot permutation satisfies stable-block/gap/index invariants; recomputed ranks remain exact.',method:'All permutations and set partitions through n=5; every nonempty subset and raw column gap. Every directed simple graph on 4 IDs, all 24 step permutations, all admitted subsets/gaps. No Rust, source serializer, or GUI exercised.',passed:true,counts:{propertyCases:count,crossColumnRefusals:refused,noops,graphOrders:graphCount,rankCheckedPlans:admitted},concrete};
fs.writeFileSync(__dirname+'/results.json',JSON.stringify(result,null,2)+'\n');
console.log(JSON.stringify(result,null,2));