120 lines
6.6 KiB
JavaScript
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));
|