The chain key, once (key-22, the project lead's screenshot of the app's key against /live, 7 October 2026): IgneumDag.legend({mine}) and renderLegend(el, {mine}) in scene/live-dag.js (2.0.5) are the one key both pages render: Pending, Included, Excluded, Selected chain, Proven, Locked checkpoint, each with the token the scene draws that state in and the swatch's shape, the app adding Your blocks (ringed, molten) first; the app's key renders from it at mount with the source line kept after the entries (the Included swatch had been ember while the scene draws included blocks in --included, and "pending" was lower-case), its CSS now colours a swatch only from the entry's token; the copies synced; tools/scene/legend.test.mjs on the gate: the export's entries, order, tokens and shapes, every colour the scene draws has an entry, the app key is the export, and the site key must equal it (binding on master, a diagnostic on a release branch whose site tree is frozen; known-failed under SCENE_SYNC_SCOPE=site today: the site's hand-written key lacks Pending and carries Your block)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
11aeaf1d68
commit
59feb51345
8 changed files with 140 additions and 12 deletions
|
|
@ -674,13 +674,14 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)}
|
||||||
.legend{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:var(--s-3);font-size:var(--t-sm);color:var(--ash);align-items:center}
|
.legend{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:var(--s-3);font-size:var(--t-sm);color:var(--ash);align-items:center}
|
||||||
.legend span{display:inline-flex;align-items:center;gap:7px}
|
.legend span{display:inline-flex;align-items:center;gap:7px}
|
||||||
/* the DAG legend swatches, scoped to the legend (a bare .lg reached label.switch.lg until 7 October 2026) */
|
/* the DAG legend swatches, scoped to the legend (a bare .lg reached label.switch.lg until 7 October 2026) */
|
||||||
.legend .lg{width:10px;height:10px;border-radius:3px;display:inline-block;border:1.5px solid var(--ash);background:var(--row)}
|
/* the key's swatches (key-22, 7 October 2026): the entries, order and tokens come from IgneumDag.legend; a swatch takes its
|
||||||
.legend .lg.you{border-color:var(--molten);box-shadow:0 0 0 2px var(--molten-40)}
|
colour from --lg (set by renderLegend to the entry's token) and its shape from the class, so no state is coloured here */
|
||||||
.legend .lg.pending{border-color:var(--ash)}
|
.legend .lg{--lg:var(--ash);width:10px;height:10px;border-radius:3px;display:inline-block;border:1.5px solid var(--lg);background:var(--lg-fill,var(--row));position:relative;flex:none}
|
||||||
.legend .lg.included{border-color:var(--ember)}
|
.legend .lg.faded{opacity:.45}
|
||||||
.legend .lg.excluded{border-color:var(--ash);opacity:.45}
|
.legend .lg.circle{border-radius:50%;box-shadow:0 0 0 2px color-mix(in srgb,var(--lg) 40%,transparent)}
|
||||||
.legend .lg.proven{background:var(--ember);border-color:var(--ember)}
|
.legend .lg.ringed{box-shadow:0 0 0 2px color-mix(in srgb,var(--lg) 40%,transparent)}
|
||||||
.legend .lg.locked{border-color:var(--ember);border-radius:50%;box-shadow:0 0 0 2px var(--ember-40)}
|
.legend .lg.tick{border-color:var(--line-2)}
|
||||||
|
.legend .lg.tick::after{content:"\2713";position:absolute;left:0;top:-4px;font-size:11px;line-height:1;color:var(--lg)}
|
||||||
/* Cards: the Tuning strip above the list */
|
/* Cards: the Tuning strip above the list */
|
||||||
.tune-strip{padding:var(--s-4) var(--card-pad)}
|
.tune-strip{padding:var(--s-4) var(--card-pad)}
|
||||||
.ts-row{display:flex;align-items:center;gap:var(--s-4);flex-wrap:wrap}
|
.ts-row{display:flex;align-items:center;gap:var(--s-4);flex-wrap:wrap}
|
||||||
|
|
|
||||||
|
|
@ -1981,6 +1981,8 @@ if (typeof document !== 'undefined') (function () {
|
||||||
}
|
}
|
||||||
function mountDag() {
|
function mountDag() {
|
||||||
if (dag || typeof IgneumDag === 'undefined' || !$('dag-live')) return;
|
if (dag || typeof IgneumDag === 'undefined' || !$('dag-live')) return;
|
||||||
|
// key-22: the key renders from the scene's one legend export (Your blocks first for this machine's view); the source line stays after it
|
||||||
|
if (IgneumDag.renderLegend) document.querySelectorAll('[data-legend]').forEach(function (el) { IgneumDag.renderLegend(el, { mine: el.getAttribute('data-legend') === 'mine' }); });
|
||||||
try {
|
try {
|
||||||
dag = IgneumDag.mount($('dag-live'), { compact: true, window: 60, fps: 60, poll: false, mine: mineFn, tooltip: $('dag-tip'), chip: $('dag-chip'), pauseButton: $('dag-pause'),
|
dag = IgneumDag.mount($('dag-live'), { compact: true, window: 60, fps: 60, poll: false, mine: mineFn, tooltip: $('dag-tip'), chip: $('dag-chip'), pauseButton: $('dag-pause'),
|
||||||
onSelect: function (b, why) { if (b && why !== 'update') openFull(b.hash); },
|
onSelect: function (b, why) { if (b && why !== 'update') openFull(b.hash); },
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,7 @@
|
||||||
<span class="dag-chip mono" id="dag-chip" hidden>scroll to zoom · Esc to release</span>
|
<span class="dag-chip mono" id="dag-chip" hidden>scroll to zoom · Esc to release</span>
|
||||||
<canvas id="dag" aria-hidden="true" hidden></canvas>
|
<canvas id="dag" aria-hidden="true" hidden></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="legend"><span><i class="lg you"></i>Your blocks</span><span><i class="lg pending"></i>pending</span><span><i class="lg included"></i>Included</span><span><i class="lg excluded"></i>Excluded</span><span><i class="lg proven"></i>Proven</span><span><i class="lg locked"></i>Locked checkpoint</span><span class="dim" id="dag-note"></span></div>
|
<div class="legend" data-legend="mine"><span class="dim" id="dag-note"></span></div>
|
||||||
<!-- the full scene with the block inspector (the words of the site's /live): opened by Inspect or by a click on a block -->
|
<!-- the full scene with the block inspector (the words of the site's /live): opened by Inspect or by a click on a block -->
|
||||||
<div class="chain-full" id="chain-full" hidden>
|
<div class="chain-full" id="chain-full" hidden>
|
||||||
<div class="cf-bar">
|
<div class="cf-bar">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
/* Igneum DAG / Ember 2.0 (the EMBER 02 renderer pack, 7 Oct 2026). THE SHARED SOURCE: scene/live-dag.js; site/live-dag.js and
|
/* Igneum DAG / Ember 2.0 (the EMBER 02 renderer pack, 7 Oct 2026). THE SHARED SOURCE: scene/live-dag.js; site/live-dag.js and
|
||||||
* app/igneum-app/ui/live-dag.js are byte-equal copies written by tools/scene/sync.mjs (the gate refuses a drifted copy). Edit here.
|
* app/igneum-app/ui/live-dag.js are byte-equal copies written by tools/scene/sync.mjs (the gate refuses a drifted copy). Edit here.
|
||||||
|
* 2.0.5 (key-22, 7 Oct 2026): IgneumDag.legend({mine}) and renderLegend(el,{mine}) are the one key both pages render; the app's
|
||||||
|
* Included swatch had been ember while the scene draws included blocks in --included, and "pending" lower-case.
|
||||||
* 2.0.4 (7 Oct 2026): the box's height follows the lanes. laneHeight (default 46 px, 40 on a phone, 26 compact) times the lanes
|
* 2.0.4 (7 Oct 2026): the box's height follows the lanes. laneHeight (default 46 px, 40 on a phone, 26 compact) times the lanes
|
||||||
* shown (at most maxLanes, narrowLanes on a phone, never under 2) plus the axis and padding is the scene's wanted height;
|
* shown (at most maxLanes, narrowLanes on a phone, never under 2) plus the axis and padding is the scene's wanted height;
|
||||||
* onSize(heightPx, {lanes, laneHeight, narrow, compact}) reports it whenever it changes, so a page can size the box, and with
|
* onSize(heightPx, {lanes, laneHeight, narrow, compact}) reports it whenever it changes, so a page can size the box, and with
|
||||||
|
|
@ -377,5 +379,25 @@
|
||||||
getSnapshot:function(){return {ok:true,state:{stale:state!=='live'},blocks:blocks.map(cloneBlock),finality:{checkpoints:cps.map(function(c){return Object.assign({},c);})}};}};
|
getSnapshot:function(){return {ok:true,state:{stale:state!=='live'},blocks:blocks.map(cloneBlock),finality:{checkpoints:cps.map(function(c){return Object.assign({},c);})}};}};
|
||||||
instances.set(canvas,api);if(chip)chip.hidden=true;theme();size();emit('onState',state,reason);if(opts.poll!==false)poll();redraw();return api;
|
instances.set(canvas,api);if(chip)chip.hidden=true;theme();size();emit('onState',state,reason);if(opts.poll!==false)poll();redraw();return api;
|
||||||
}
|
}
|
||||||
root.IgneumDag={mount:mount,version:'2.0.4'};
|
// key-22 (7 October 2026, the app's key against /live): THE key, once. Entries in the order both
|
||||||
|
// pages show them, each naming the token the scene draws that state with and the swatch's shape; the app passes
|
||||||
|
// {mine:true} and gets "Your blocks" (ringed, molten) first. tools/scene/legend.test.mjs fails when a page's key or a
|
||||||
|
// drawn colour drifts from this list. Shapes: square (outlined), circle (the checkpoint ring), tick (the proof mark),
|
||||||
|
// ringed (the own-block halo); faded dims the swatch as the scene dims excluded blocks.
|
||||||
|
function legend(o){
|
||||||
|
var own={id:'you',label:'Your blocks',token:'--molten',shape:'ringed'};
|
||||||
|
var list=[{id:'pending',label:'Pending',token:'--ash',shape:'square'},{id:'included',label:'Included',token:'--included',shape:'square'},
|
||||||
|
{id:'excluded',label:'Excluded',token:'--excluded',shape:'square',faded:true},{id:'chain',label:'Selected chain',token:'--ember',shape:'square',fill:'--ember-12'},
|
||||||
|
{id:'proven',label:'Proven',token:'--bone',shape:'tick'},{id:'locked',label:'Locked checkpoint',token:'--ember',shape:'circle'}];
|
||||||
|
return (o&&o.mine?[own]:[]).concat(list);
|
||||||
|
}
|
||||||
|
// renders the key into el: <span><i class="lg <shape> [faded]" style="--lg:var(token)"></i>Label</span>…, keeping any
|
||||||
|
// children the page already placed there (the app's source line) after the entries
|
||||||
|
function renderLegend(el,o){
|
||||||
|
if(!el)return [];var entries=legend(o),doc=el.ownerDocument,keep=Array.prototype.slice.call(el.childNodes).filter(function(n){return !(n.getAttribute&&n.getAttribute('data-lg'));});
|
||||||
|
while(el.firstChild)el.removeChild(el.firstChild);
|
||||||
|
entries.forEach(function(e){var sp=doc.createElement('span');sp.setAttribute('data-lg',e.id);var i=doc.createElement('i');i.className='lg '+e.shape+(e.faded?' faded':'');i.style.setProperty('--lg','var('+e.token+')');if(e.fill)i.style.setProperty('--lg-fill','var('+e.fill+')');sp.appendChild(i);sp.appendChild(doc.createTextNode(e.label));el.appendChild(sp);});
|
||||||
|
keep.forEach(function(n){el.appendChild(n);});return entries;
|
||||||
|
}
|
||||||
|
root.IgneumDag={mount:mount,legend:legend,renderLegend:renderLegend,version:'2.0.5'};
|
||||||
})(window);
|
})(window);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
/* Igneum DAG / Ember 2.0 (the EMBER 02 renderer pack, 7 Oct 2026). THE SHARED SOURCE: scene/live-dag.js; site/live-dag.js and
|
/* Igneum DAG / Ember 2.0 (the EMBER 02 renderer pack, 7 Oct 2026). THE SHARED SOURCE: scene/live-dag.js; site/live-dag.js and
|
||||||
* app/igneum-app/ui/live-dag.js are byte-equal copies written by tools/scene/sync.mjs (the gate refuses a drifted copy). Edit here.
|
* app/igneum-app/ui/live-dag.js are byte-equal copies written by tools/scene/sync.mjs (the gate refuses a drifted copy). Edit here.
|
||||||
|
* 2.0.5 (key-22, 7 Oct 2026): IgneumDag.legend({mine}) and renderLegend(el,{mine}) are the one key both pages render; the app's
|
||||||
|
* Included swatch had been ember while the scene draws included blocks in --included, and "pending" lower-case.
|
||||||
* 2.0.4 (7 Oct 2026): the box's height follows the lanes. laneHeight (default 46 px, 40 on a phone, 26 compact) times the lanes
|
* 2.0.4 (7 Oct 2026): the box's height follows the lanes. laneHeight (default 46 px, 40 on a phone, 26 compact) times the lanes
|
||||||
* shown (at most maxLanes, narrowLanes on a phone, never under 2) plus the axis and padding is the scene's wanted height;
|
* shown (at most maxLanes, narrowLanes on a phone, never under 2) plus the axis and padding is the scene's wanted height;
|
||||||
* onSize(heightPx, {lanes, laneHeight, narrow, compact}) reports it whenever it changes, so a page can size the box, and with
|
* onSize(heightPx, {lanes, laneHeight, narrow, compact}) reports it whenever it changes, so a page can size the box, and with
|
||||||
|
|
@ -377,5 +379,25 @@
|
||||||
getSnapshot:function(){return {ok:true,state:{stale:state!=='live'},blocks:blocks.map(cloneBlock),finality:{checkpoints:cps.map(function(c){return Object.assign({},c);})}};}};
|
getSnapshot:function(){return {ok:true,state:{stale:state!=='live'},blocks:blocks.map(cloneBlock),finality:{checkpoints:cps.map(function(c){return Object.assign({},c);})}};}};
|
||||||
instances.set(canvas,api);if(chip)chip.hidden=true;theme();size();emit('onState',state,reason);if(opts.poll!==false)poll();redraw();return api;
|
instances.set(canvas,api);if(chip)chip.hidden=true;theme();size();emit('onState',state,reason);if(opts.poll!==false)poll();redraw();return api;
|
||||||
}
|
}
|
||||||
root.IgneumDag={mount:mount,version:'2.0.4'};
|
// key-22 (7 October 2026, the app's key against /live): THE key, once. Entries in the order both
|
||||||
|
// pages show them, each naming the token the scene draws that state with and the swatch's shape; the app passes
|
||||||
|
// {mine:true} and gets "Your blocks" (ringed, molten) first. tools/scene/legend.test.mjs fails when a page's key or a
|
||||||
|
// drawn colour drifts from this list. Shapes: square (outlined), circle (the checkpoint ring), tick (the proof mark),
|
||||||
|
// ringed (the own-block halo); faded dims the swatch as the scene dims excluded blocks.
|
||||||
|
function legend(o){
|
||||||
|
var own={id:'you',label:'Your blocks',token:'--molten',shape:'ringed'};
|
||||||
|
var list=[{id:'pending',label:'Pending',token:'--ash',shape:'square'},{id:'included',label:'Included',token:'--included',shape:'square'},
|
||||||
|
{id:'excluded',label:'Excluded',token:'--excluded',shape:'square',faded:true},{id:'chain',label:'Selected chain',token:'--ember',shape:'square',fill:'--ember-12'},
|
||||||
|
{id:'proven',label:'Proven',token:'--bone',shape:'tick'},{id:'locked',label:'Locked checkpoint',token:'--ember',shape:'circle'}];
|
||||||
|
return (o&&o.mine?[own]:[]).concat(list);
|
||||||
|
}
|
||||||
|
// renders the key into el: <span><i class="lg <shape> [faded]" style="--lg:var(token)"></i>Label</span>…, keeping any
|
||||||
|
// children the page already placed there (the app's source line) after the entries
|
||||||
|
function renderLegend(el,o){
|
||||||
|
if(!el)return [];var entries=legend(o),doc=el.ownerDocument,keep=Array.prototype.slice.call(el.childNodes).filter(function(n){return !(n.getAttribute&&n.getAttribute('data-lg'));});
|
||||||
|
while(el.firstChild)el.removeChild(el.firstChild);
|
||||||
|
entries.forEach(function(e){var sp=doc.createElement('span');sp.setAttribute('data-lg',e.id);var i=doc.createElement('i');i.className='lg '+e.shape+(e.faded?' faded':'');i.style.setProperty('--lg','var('+e.token+')');if(e.fill)i.style.setProperty('--lg-fill','var('+e.fill+')');sp.appendChild(i);sp.appendChild(doc.createTextNode(e.label));el.appendChild(sp);});
|
||||||
|
keep.forEach(function(n){el.appendChild(n);});return entries;
|
||||||
|
}
|
||||||
|
root.IgneumDag={mount:mount,legend:legend,renderLegend:renderLegend,version:'2.0.5'};
|
||||||
})(window);
|
})(window);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
/* Igneum DAG / Ember 2.0 (the EMBER 02 renderer pack, 7 Oct 2026). THE SHARED SOURCE: scene/live-dag.js; site/live-dag.js and
|
/* Igneum DAG / Ember 2.0 (the EMBER 02 renderer pack, 7 Oct 2026). THE SHARED SOURCE: scene/live-dag.js; site/live-dag.js and
|
||||||
* app/igneum-app/ui/live-dag.js are byte-equal copies written by tools/scene/sync.mjs (the gate refuses a drifted copy). Edit here.
|
* app/igneum-app/ui/live-dag.js are byte-equal copies written by tools/scene/sync.mjs (the gate refuses a drifted copy). Edit here.
|
||||||
|
* 2.0.5 (key-22, 7 Oct 2026): IgneumDag.legend({mine}) and renderLegend(el,{mine}) are the one key both pages render; the app's
|
||||||
|
* Included swatch had been ember while the scene draws included blocks in --included, and "pending" lower-case.
|
||||||
* 2.0.4 (7 Oct 2026): the box's height follows the lanes. laneHeight (default 46 px, 40 on a phone, 26 compact) times the lanes
|
* 2.0.4 (7 Oct 2026): the box's height follows the lanes. laneHeight (default 46 px, 40 on a phone, 26 compact) times the lanes
|
||||||
* shown (at most maxLanes, narrowLanes on a phone, never under 2) plus the axis and padding is the scene's wanted height;
|
* shown (at most maxLanes, narrowLanes on a phone, never under 2) plus the axis and padding is the scene's wanted height;
|
||||||
* onSize(heightPx, {lanes, laneHeight, narrow, compact}) reports it whenever it changes, so a page can size the box, and with
|
* onSize(heightPx, {lanes, laneHeight, narrow, compact}) reports it whenever it changes, so a page can size the box, and with
|
||||||
|
|
@ -377,5 +379,25 @@
|
||||||
getSnapshot:function(){return {ok:true,state:{stale:state!=='live'},blocks:blocks.map(cloneBlock),finality:{checkpoints:cps.map(function(c){return Object.assign({},c);})}};}};
|
getSnapshot:function(){return {ok:true,state:{stale:state!=='live'},blocks:blocks.map(cloneBlock),finality:{checkpoints:cps.map(function(c){return Object.assign({},c);})}};}};
|
||||||
instances.set(canvas,api);if(chip)chip.hidden=true;theme();size();emit('onState',state,reason);if(opts.poll!==false)poll();redraw();return api;
|
instances.set(canvas,api);if(chip)chip.hidden=true;theme();size();emit('onState',state,reason);if(opts.poll!==false)poll();redraw();return api;
|
||||||
}
|
}
|
||||||
root.IgneumDag={mount:mount,version:'2.0.4'};
|
// key-22 (7 October 2026, the app's key against /live): THE key, once. Entries in the order both
|
||||||
|
// pages show them, each naming the token the scene draws that state with and the swatch's shape; the app passes
|
||||||
|
// {mine:true} and gets "Your blocks" (ringed, molten) first. tools/scene/legend.test.mjs fails when a page's key or a
|
||||||
|
// drawn colour drifts from this list. Shapes: square (outlined), circle (the checkpoint ring), tick (the proof mark),
|
||||||
|
// ringed (the own-block halo); faded dims the swatch as the scene dims excluded blocks.
|
||||||
|
function legend(o){
|
||||||
|
var own={id:'you',label:'Your blocks',token:'--molten',shape:'ringed'};
|
||||||
|
var list=[{id:'pending',label:'Pending',token:'--ash',shape:'square'},{id:'included',label:'Included',token:'--included',shape:'square'},
|
||||||
|
{id:'excluded',label:'Excluded',token:'--excluded',shape:'square',faded:true},{id:'chain',label:'Selected chain',token:'--ember',shape:'square',fill:'--ember-12'},
|
||||||
|
{id:'proven',label:'Proven',token:'--bone',shape:'tick'},{id:'locked',label:'Locked checkpoint',token:'--ember',shape:'circle'}];
|
||||||
|
return (o&&o.mine?[own]:[]).concat(list);
|
||||||
|
}
|
||||||
|
// renders the key into el: <span><i class="lg <shape> [faded]" style="--lg:var(token)"></i>Label</span>…, keeping any
|
||||||
|
// children the page already placed there (the app's source line) after the entries
|
||||||
|
function renderLegend(el,o){
|
||||||
|
if(!el)return [];var entries=legend(o),doc=el.ownerDocument,keep=Array.prototype.slice.call(el.childNodes).filter(function(n){return !(n.getAttribute&&n.getAttribute('data-lg'));});
|
||||||
|
while(el.firstChild)el.removeChild(el.firstChild);
|
||||||
|
entries.forEach(function(e){var sp=doc.createElement('span');sp.setAttribute('data-lg',e.id);var i=doc.createElement('i');i.className='lg '+e.shape+(e.faded?' faded':'');i.style.setProperty('--lg','var('+e.token+')');if(e.fill)i.style.setProperty('--lg-fill','var('+e.fill+')');sp.appendChild(i);sp.appendChild(doc.createTextNode(e.label));el.appendChild(sp);});
|
||||||
|
keep.forEach(function(n){el.appendChild(n);});return entries;
|
||||||
|
}
|
||||||
|
root.IgneumDag={mount:mount,legend:legend,renderLegend:renderLegend,version:'2.0.5'};
|
||||||
})(window);
|
})(window);
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ tree_checks() {
|
||||||
run "the home hero's loop never idles in view, stops hidden, resumes without a jump" node tools/site-redesign/tests/hero-loop-test.cjs
|
run "the home hero's loop never idles in view, stops hidden, resumes without a jump" node tools/site-redesign/tests/hero-loop-test.cjs
|
||||||
run "chain scene: the site's and the app's copies are scene/ byte for byte, the palette tokens live once (self-test, then the tree)" bash -c 'node tools/scene/sync.mjs --self-test && node tools/scene/sync.mjs --check'
|
run "chain scene: the site's and the app's copies are scene/ byte for byte, the palette tokens live once (self-test, then the tree)" bash -c 'node tools/scene/sync.mjs --self-test && node tools/scene/sync.mjs --check'
|
||||||
run "chain scene: a push paints with the document hidden and no animation frame (the blank /live of 7 Oct 2026; known-failed first)" node tools/scene/paint-test.cjs
|
run "chain scene: a push paints with the document hidden and no animation frame (the blank /live of 7 Oct 2026; known-failed first)" node tools/scene/paint-test.cjs
|
||||||
run "chain scene: the live feed contract (the recorded reply validates; a rewritten miner, a float now, a stray key refused)" node --test tools/scene/feed-contract.test.mjs
|
run "chain scene: the live feed contract (the recorded reply validates; a rewritten miner, a float now, a stray key refused)" node --test tools/scene/feed-contract.test.mjs tools/scene/legend.test.mjs
|
||||||
run "chain scene parity: one recorded feed through the home fold, /live and the app's Inspect view on build-2, three frames each pixel-equal apart from the app's own-key overlay (a changed token fails first; skipped with no box and no Playwright)" bash tools/scene/parity-remote.sh
|
run "chain scene parity: one recorded feed through the home fold, /live and the app's Inspect view on build-2, three frames each pixel-equal apart from the app's own-key overlay (a changed token fails first; skipped with no box and no Playwright)" bash tools/scene/parity-remote.sh
|
||||||
run "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/lib/money.test.mjs site/api/public-stats.test.mjs
|
run "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/lib/money.test.mjs site/api/public-stats.test.mjs
|
||||||
run "ship tool self-test" node tools/ship-app.mjs --self-test
|
run "ship tool self-test" node tools/ship-app.mjs --self-test
|
||||||
|
|
|
||||||
59
tools/scene/legend.test.mjs
Normal file
59
tools/scene/legend.test.mjs
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
// node --test tools/scene/legend.test.mjs (key-22, the project lead's screenshot of the app's chain key against /live, 7 October 2026)
|
||||||
|
// The key is ONE export: IgneumDag.legend({ mine }) in scene/live-dag.js (entries, order, token names, shapes); the app's
|
||||||
|
// chain card and the site's /live key render from it, so they cannot drift. This test fails when either key's entries,
|
||||||
|
// order or tokens differ from the export, and when a colour the scene draws on blocks has no key entry.
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url)), root = join(here, '../..');
|
||||||
|
const src = readFileSync(join(root, 'scene/live-dag.js'), 'utf8');
|
||||||
|
const win = { document: { hidden: false, addEventListener() {}, createElement: () => ({ style: {} }) }, addEventListener() {}, matchMedia: () => ({ matches: false, addEventListener() {} }), getComputedStyle: () => ({ getPropertyValue: () => '' }), devicePixelRatio: 1, innerWidth: 1280 };
|
||||||
|
new Function('window', 'document', src)(win, win.document);
|
||||||
|
const Dag = win.IgneumDag;
|
||||||
|
const SITE_ORDER = ['Pending', 'Included', 'Excluded', 'Selected chain', 'Proven', 'Locked checkpoint'];
|
||||||
|
// the entries a page's markup carries today, from its literal spans (a hard-coded key) or none (a rendered key)
|
||||||
|
const literal = (html, tag) => [...html.matchAll(new RegExp('<span><i class="' + tag + '[^>]*></i>([^<]+)</span>', 'g'))].map((m) => m[1].trim());
|
||||||
|
|
||||||
|
test('the export: Pending first, Included, Excluded, Selected chain, Proven, Locked checkpoint; the app adds Your blocks first; every entry names its token and shape (known-failed on 0.3.22)', () => {
|
||||||
|
assert.ok(Dag && typeof Dag.legend === 'function', 'IgneumDag.legend exists');
|
||||||
|
const site = Dag.legend({ mine: false }), app = Dag.legend({ mine: true });
|
||||||
|
assert.deepEqual(site.map((e) => e.label), SITE_ORDER);
|
||||||
|
assert.deepEqual(app.map((e) => e.label), ['Your blocks'].concat(SITE_ORDER));
|
||||||
|
for (const e of app) { assert.match(e.token, /^--[a-z-]+$/, e.label + ' token'); assert.ok(['square', 'circle', 'tick', 'ringed'].includes(e.shape), e.label + ' shape'); assert.ok(e.id); }
|
||||||
|
assert.equal(app[0].token, '--molten'); assert.equal(app[0].shape, 'ringed');
|
||||||
|
assert.equal(site.find((e) => e.label === 'Included').token, '--included', 'the key draws Included in the colour the scene draws it');
|
||||||
|
assert.equal(site.find((e) => e.label === 'Excluded').token, '--excluded');
|
||||||
|
assert.equal(site.find((e) => e.label === 'Locked checkpoint').shape, 'circle');
|
||||||
|
assert.equal(site.find((e) => e.label === 'Proven').shape, 'tick');
|
||||||
|
assert.equal(typeof Dag.renderLegend, 'function', 'one renderer for both pages');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every colour the scene draws on a block or a band has a key entry', () => {
|
||||||
|
const tokens = new Set(Dag.legend({ mine: true }).map((e) => e.token));
|
||||||
|
// the renderer's palette read: col={ember:c('--ember',…), …, blue:c('--included',…), red:c('--excluded',…)}
|
||||||
|
const read = [...src.matchAll(/c\('(--[a-z0-9-]+)'/g)].map((m) => m[1]);
|
||||||
|
const drawn = ['--included', '--excluded', '--ember', '--molten', '--bone', '--ash'];
|
||||||
|
for (const t of drawn) { assert.ok(read.includes(t), 'the scene reads ' + t); assert.ok(tokens.has(t), 'the key has an entry in ' + t); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// the site tree on a release-* branch is frozen and deploys from master alone (tools/scene/sync.mjs's scope rule), so the
|
||||||
|
// site half of the check binds on master (or with SCENE_SYNC_SCOPE=site) and is a diagnostic elsewhere
|
||||||
|
const branch = (() => { try { return execSync('git rev-parse --abbrev-ref HEAD', { cwd: root, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); } catch (e) { return ''; } })();
|
||||||
|
const siteBinds = process.env.SCENE_SYNC_SCOPE === 'site' || branch === 'master';
|
||||||
|
|
||||||
|
test('the app key and the site key are the export, not a hand-written list: no literal entries left, and any literal list that remains equals the export in labels and order', (t) => {
|
||||||
|
const app = readFileSync(join(root, 'app/igneum-app/ui/index.html'), 'utf8'), site = readFileSync(join(root, 'site/live.html'), 'utf8');
|
||||||
|
const appLit = literal(app, 'lg'), siteLit = literal(site, 'lgi');
|
||||||
|
assert.ok(/data-legend="mine"/.test(app), 'the app legend renders from IgneumDag.legend({ mine: true })');
|
||||||
|
assert.deepEqual(appLit, [], 'hand-written entries in the app key: ' + appLit.join(', '));
|
||||||
|
const siteOk = siteLit.length ? JSON.stringify(siteLit) === JSON.stringify(SITE_ORDER) : /data-legend/.test(site);
|
||||||
|
if (siteBinds) assert.ok(siteOk, 'the site key differs from the export: ' + (siteLit.length ? siteLit.join(', ') : 'no data-legend hook'));
|
||||||
|
else if (!siteOk) t.diagnostic('site/live.html still carries a hand-written key (' + siteLit.join(', ') + '); it binds on master once the site lane renders it from IgneumDag.renderLegend');
|
||||||
|
// the app's source line stays
|
||||||
|
assert.match(readFileSync(join(root, 'app/igneum-app/ui/app.js'), 'utf8'), /From Igneum’s observer\. Your blocks are ringed\./);
|
||||||
|
// the copies are the source (the sync gate says the same; this fails first when a legend edit misses the sync)
|
||||||
|
assert.equal(readFileSync(join(root, 'app/igneum-app/ui/live-dag.js'), 'utf8'), src, 'the app copy of live-dag.js is the scene source');
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue