Prueba de H·M — Barrio LORE
Artifact publicado en claude.ai (app HTML interactiva). Primero, el texto visible; después, la fuente HTML íntegra, porque parte del contenido (guion, diálogos, datos) vive en el script.
Texto visible
Prueba de H·M — Barrio LORE
BOCETO · ESPEC DEL PANEL DE CEREMONIA — corrección de rumbo del PO (2026-08-03): la demo real es una adaptación de «delta» para el juego «ciudad», en el barrio Lore, sobre la DocumentMachine, con V (el Zigurat) como interfaz de H. Este artifact conserva el guion rescatado de HANDOFF-DEMO (copia única) y sirve como especificación visual del panel de ceremonia — no como la demo. Obra viva: h-sdk.
Ceremonia bilateral
sala sin abrir
Turno de H
H>
gramática cerrada · H: pregunta / dirige / concede / deniega / elige · las teclas 1–9 seleccionan opción
Fuente HTML íntegra
<!doctype html><html><head><meta charset=utf8><meta name=viewport content="width=device-width,initial-scale=1"><style>:root{color-scheme:light}body{margin:0;padding:0;font:14px -apple-system,BlinkMacSystemFont,sans-serif;background:#faf9f5;color:#141413}img{max-width:100%}</style></head><body>
<title>Prueba de H·M — Barrio LORE</title>
<style>
:root {
--bg:#F1EBDD; --panel:#FAF6EB; --panel2:#F2ECDA; --ink:#2B2413; --dim:#6E6248;
--line:#D9CEAE; --line2:#C9BC96;
--h:#8F6A10; --h-soft:#B8912F; --m:#2E7263; --ok:#4C7C3F; --bad:#A8462E; --mock:#6A5896;
--chip-real-bg:rgba(76,124,63,.10); --chip-mock-bg:rgba(106,88,150,.10);
--sel:rgba(143,106,16,.14);
}
@media (prefers-color-scheme: dark) {
:root {
--bg:#131008; --panel:#1B1710; --panel2:#211C12; --ink:#EAE0C8; --dim:#9C8F70;
--line:#3A3323; --line2:#4A4230;
--h:#E7B14C; --h-soft:#C89A45; --m:#85BAA9; --ok:#8FBF7F; --bad:#DB7A5D; --mock:#AC97CE;
--chip-real-bg:rgba(143,191,127,.10); --chip-mock-bg:rgba(172,151,206,.12);
--sel:rgba(231,177,76,.14);
}
}
:root[data-theme="dark"] {
--bg:#131008; --panel:#1B1710; --panel2:#211C12; --ink:#EAE0C8; --dim:#9C8F70;
--line:#3A3323; --line2:#4A4230;
--h:#E7B14C; --h-soft:#C89A45; --m:#85BAA9; --ok:#8FBF7F; --bad:#DB7A5D; --mock:#AC97CE;
--chip-real-bg:rgba(143,191,127,.10); --chip-mock-bg:rgba(172,151,206,.12);
--sel:rgba(231,177,76,.14);
}
:root[data-theme="light"] {
--bg:#F1EBDD; --panel:#FAF6EB; --panel2:#F2ECDA; --ink:#2B2413; --dim:#6E6248;
--line:#D9CEAE; --line2:#C9BC96;
--h:#8F6A10; --h-soft:#B8912F; --m:#2E7263; --ok:#4C7C3F; --bad:#A8462E; --mock:#6A5896;
--chip-real-bg:rgba(76,124,63,.10); --chip-mock-bg:rgba(106,88,150,.10);
--sel:rgba(143,106,16,.14);
}
* { box-sizing:border-box; }
body {
background:var(--bg); color:var(--ink);
font-family:"Cascadia Mono","Cascadia Code","JetBrains Mono","Fira Code",Consolas,"SF Mono",Menlo,monospace;
font-size:13.5px; line-height:1.55; margin:0; padding:0 16px 48px;
font-variant-numeric:tabular-nums;
}
.wrap { max-width:1240px; margin:0 auto; }
/* ── crома ceremonial ─────────────────────────────── */
.chrome { font-family:Georgia,"Times New Roman",serif; }
.eyebrow {
font-family:Georgia,serif; font-size:11px; letter-spacing:.22em; text-transform:uppercase;
color:var(--dim); margin:0 0 6px;
}
header.tui {
border:3px double var(--line2); background:var(--panel);
margin:22px 0 14px; padding:14px 18px 12px;
}
header.tui h1 {
margin:0; font-size:19px; letter-spacing:.14em; text-transform:uppercase;
font-family:inherit; font-weight:600; text-wrap:balance;
}
header.tui h1 .hh { color:var(--h); } header.tui h1 .mm { color:var(--m); }
.metaline {
display:flex; flex-wrap:wrap; gap:8px 18px; margin-top:10px; padding-top:10px;
border-top:1px solid var(--line); font-size:11.5px; color:var(--dim);
}
.metaline b { color:var(--ink); font-weight:600; }
.badge {
display:inline-block; padding:0 7px; border:1px solid var(--line2);
font-size:10.5px; letter-spacing:.08em;
}
.badge.sim { color:var(--mock); border-color:var(--mock); }
.badge.rec { color:var(--ok); border-color:var(--ok); display:none; }
/* ── layout sala de control ───────────────────────── */
.room { display:grid; grid-template-columns:minmax(0,1fr) 344px; gap:14px; align-items:start; }
@media (max-width: 980px) { .room { grid-template-columns:minmax(0,1fr); } }
/* ── transcript ───────────────────────────────────── */
.term { border:1px solid var(--line2); background:var(--panel); display:flex; flex-direction:column; min-height:520px; }
.term-head {
padding:8px 14px; border-bottom:1px solid var(--line); display:flex; justify-content:space-between;
align-items:baseline; gap:12px;
}
.term-head .eyebrow { margin:0; }
.term-head .steplbl { font-size:11px; color:var(--dim); }
#log { padding:14px 16px 10px; overflow-y:auto; flex:1; max-height:56vh; scroll-behavior:smooth; }
@media (prefers-reduced-motion: reduce) { #log { scroll-behavior:auto; } }
.ln { margin:0 0 4px; white-space:pre-wrap; overflow-wrap:anywhere; }
.ln.h { color:var(--ink); }
.ln.h .pfx { color:var(--h); font-weight:700; }
.ln.m { color:var(--ink); }
.ln.m .pfx { color:var(--m); font-weight:700; }
.ln.sys { color:var(--dim); font-size:12px; }
.ln.act { color:var(--dim); font-size:11.5px; }
.ln.act .dg { color:var(--h-soft); }
.ln.bad-in { color:var(--dim); font-style:italic; }
.ln.divider { color:var(--dim); letter-spacing:.1em; margin:10px 0 8px; }
.chip {
display:inline-block; padding:0 5px; font-size:10px; letter-spacing:.06em; border:1px solid;
vertical-align:1px;
}
.chip.real { color:var(--ok); border-color:var(--ok); background:var(--chip-real-bg); }
.chip.mock { color:var(--mock); border-color:var(--mock); background:var(--chip-mock-bg); }
@keyframes lnIn { from { opacity:0; transform:translateY(2px);} to { opacity:1; transform:none;} }
.ln { animation:lnIn .18s ease-out both; }
@media (prefers-reduced-motion: reduce) { .ln { animation:none; } }
/* ── dock de entrada H ────────────────────────────── */
.dock { border-top:1px solid var(--line); background:var(--panel2); padding:10px 14px 12px; }
.dock .eyebrow { margin-bottom:8px; }
.opts { display:flex; flex-direction:column; gap:6px; margin:0 0 10px; }
button.opt {
font:inherit; text-align:left; color:var(--ink); background:var(--panel);
border:1px solid var(--line2); padding:6px 10px; cursor:pointer; width:100%;
}
button.opt .n { color:var(--dim); margin-right:8px; }
button.opt:hover { background:var(--sel); border-color:var(--h-soft); }
button.opt:focus-visible, #say:focus-visible, button.mini:focus-visible {
outline:2px solid var(--h); outline-offset:1px;
}
.sayrow { display:flex; gap:8px; align-items:center; }
.cursor { color:var(--h); font-weight:700; animation:blink 1.1s steps(1) infinite; }
@keyframes blink { 50% { opacity:0; } }
@media (prefers-reduced-motion: reduce) { .cursor { animation:none; } }
#say {
flex:1; font:inherit; color:var(--ink); background:var(--panel);
border:1px solid var(--line2); padding:6px 10px; min-width:0;
}
#say::placeholder { color:var(--dim); opacity:.8; }
.gram { font-size:10.5px; color:var(--dim); margin-top:6px; }
/* ── raíl de estado ───────────────────────────────── */
.rail { display:flex; flex-direction:column; gap:14px; }
@media (max-width: 980px) and (min-width: 620px) {
.rail { display:grid; grid-template-columns:1fr 1fr; align-items:start; }
.panel.wide { grid-column:1 / -1; }
}
.panel { border:1px solid var(--line2); background:var(--panel); padding:10px 14px 12px; }
.panel .eyebrow { border-bottom:1px solid var(--line); padding-bottom:6px; margin-bottom:8px; }
ol.steps { list-style:none; margin:0; padding:0; font-size:12px; }
ol.steps li { display:flex; gap:8px; color:var(--dim); padding:1px 0; }
ol.steps li .k { width:2.4em; text-align:right; flex:none; }
ol.steps li.done { color:var(--ink); }
ol.steps li.done .k::after { content:" ✓"; color:var(--ok); }
ol.steps li.now { color:var(--h); }
ol.steps li.now .k::after { content:" ▸"; }
table.units { width:100%; border-collapse:collapse; font-size:12px; }
table.units td { padding:1px 0; vertical-align:baseline; }
table.units td.st { text-align:right; color:var(--dim); }
table.units td.st .u-lista, table.units td.st .u-corriendo { color:var(--ok); }
table.units td.st .u-arrendada, table.units td.st .u-materializada { color:var(--h-soft); }
table.units td.st .u-detenida { color:var(--dim); }
table.units td.st .sim { color:var(--mock); font-size:10px; margin-left:4px; }
.panel .foot { font-size:10.5px; color:var(--dim); border-top:1px solid var(--line); margin-top:8px; padding-top:6px; }
table.leases { width:100%; border-collapse:collapse; font-size:12px; }
table.leases td { padding:1px 0; }
table.leases td.st { text-align:right; }
.l-pendiente { color:var(--h-soft); } .l-concedida { color:var(--ok); }
.l-denegada { color:var(--bad); } .l-revocada { color:var(--dim); }
.l-nada { color:var(--dim); }
#evid { max-height:220px; overflow-y:auto; font-size:11.5px; }
#evid .row { display:flex; gap:8px; padding:1px 0; color:var(--dim); }
#evid .row .seq { width:2.6em; text-align:right; flex:none; }
#evid .row .vb { flex:1; color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
#evid .row .dg { color:var(--h-soft); flex:none; }
.sealflash { animation:seal .9s ease-out 1; }
@keyframes seal { 0% { box-shadow:0 0 0 3px var(--h); } 100% { box-shadow:0 0 0 0 transparent; } }
@media (prefers-reduced-motion: reduce) { .sealflash { animation:none; } }
.minirow { display:flex; gap:8px; margin-top:10px; flex-wrap:wrap; }
button.mini {
font:inherit; font-size:11px; color:var(--ink); background:var(--panel2);
border:1px solid var(--line2); padding:4px 9px; cursor:pointer;
}
button.mini:hover { background:var(--sel); }
footer.chrome {
margin-top:26px; padding-top:12px; border-top:1px solid var(--line);
color:var(--dim); font-size:12px; max-width:68ch;
}
</style>
<div class="wrap">
<div style="border:1px solid var(--h-soft); background:var(--panel2); padding:8px 14px; margin:18px 0 -6px; font-size:11.5px; color:var(--dim); line-height:1.5;">
<b style="color:var(--h);">BOCETO · ESPEC DEL PANEL DE CEREMONIA</b> — corrección de rumbo del PO (2026-08-03):
la demo real es una <b style="color:var(--ink);">adaptación de «delta» para el juego «ciudad»</b>, en el barrio Lore,
sobre la DocumentMachine, con <b style="color:var(--ink);">V (el Zigurat) como interfaz de H</b>.
Este artifact conserva el <b style="color:var(--ink);">guion rescatado de HANDOFF-DEMO</b> (copia única) y sirve como
especificación visual del panel de ceremonia — no como la demo. Obra viva: <code>h-sdk</code>.
</div>
<header class="tui">
<p class="eyebrow">Scriptorium · playground prueba-de-H-M · escenario barrio-lore</p>
<h1>Prueba de <span class="hh">H</span>·<span class="mm">M</span> — Barrio LORE</h1>
<div class="metaline">
<span>turno <b id="mTurn">0</b></span>
<span>paso <b id="mStep">0</b>/11</span>
<span class="badge sim">SIMULACRO</span>
<span class="badge" title="M es plantillas deterministas; Future Machine simulada">provider DeterministicDocumentMachine · contingency=true</span>
<span class="badge" title="8 estados del playground; proyección a las 6 fases de la lengua: pendiente">tipestate playground(8) ≠ lengua(6)</span>
<span class="badge rec" id="mRec">RECUPERADO ✓</span>
</div>
</header>
<div class="room">
<!-- ══ terminal de ceremonia ══ -->
<section class="term" aria-label="Ceremonia">
<div class="term-head">
<p class="eyebrow">Ceremonia bilateral</p>
<span class="steplbl" id="stepLbl">sala sin abrir</span>
</div>
<div id="log" role="log" aria-live="polite"></div>
<div class="dock">
<p class="eyebrow">Turno de H</p>
<div class="opts" id="opts"></div>
<div class="sayrow">
<span class="cursor">H></span>
<input id="say" type="text" autocomplete="off" spellcheck="false"
placeholder="…o escribe una frase de la gramática cerrada" aria-label="Frase de H" />
</div>
<p class="gram">gramática cerrada · H: pregunta / dirige / concede / deniega / elige · las teclas 1–9 seleccionan opción</p>
</div>
</section>
<!-- ══ raíl de estado ══ -->
<aside class="rail">
<div class="panel">
<p class="eyebrow">Ceremonia · 11 pasos bloqueantes</p>
<ol class="steps" id="steps"></ol>
</div>
<div class="panel">
<p class="eyebrow">Unidades · 10 declaradas</p>
<table class="units" id="units"></table>
<p class="foot">tipestate del playground · «·sim» = ejecutada vía máquina simulada</p>
</div>
<div class="panel">
<p class="eyebrow">Leases</p>
<table class="leases" id="leases"></table>
<p class="foot">request → grant | deny → materialize · revocable · fallo atómico</p>
</div>
<div class="panel wide" id="evPanel">
<p class="eyebrow">Evidencia · cadena de digests</p>
<div id="evid"></div>
<div class="minirow">
<button class="mini" id="dl">⬇ transcript.ndjson</button>
<button class="mini" id="rec" disabled title="Disponible tras cerrar la ceremonia">restart → recupera</button>
<button class="mini" id="zero">de cero</button>
</div>
<p class="foot">wire JSON sellado · SHA-256 real por Activity · cada digest encadena el anterior</p>
</div>
</aside>
</div>
<footer class="chrome">
Prototipo de interfaz, fuera de FC3. El guion dramatiza los 11 pasos de
<i>barrio-lore-v1</i> (WP-HUB-106). Mock honesto: M no es un LLM — sus respuestas
son plantillas deterministas; la sala, la identidad y la Future Machine son
simulacro declarado. Reales: las decisiones de H, la gramática, las leases,
el tipestate y los sellos.
</footer>
</div>
<script>
(() => {
'use strict';
/* ── datos del escenario ─────────────────────────── */
const STEPS = [
'preflight · identidad H·M', 'room y autoridad', 'leases', 'inflación',
'manifiesto y despliegue', 'ingesta Onfalo + análisis', 'vector', 'dos líneas',
'grafo', 'universos', 'cortos · trace · cierre'
];
const UNITS = ['loreador','bartleby','archivero','vector-mock','grafista',
'demiurgo','dramaturgo','pipeline','portal','cristalizador'];
const LEASED = ['bartleby','cristalizador'];
/* ── estado vivo ─────────────────────────────────── */
const S = {
turn:0, step:0, seq:0, prev:'genesis', closed:false,
units:Object.fromEntries(UNITS.map(u=>[u,'declarada'])),
sim:Object.fromEntries(UNITS.map(u=>[u,false])),
leases:Object.fromEntries(LEASED.map(u=>[u,'—'])),
ledger:[], universe:null
};
/* ── utilidades ──────────────────────────────────── */
const $ = id => document.getElementById(id);
const norm = s => s.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g,'').trim();
async function sha(str){
if (crypto && crypto.subtle) {
const b = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
return [...new Uint8Array(b)].map(x=>x.toString(16).padStart(2,'0')).join('');
}
let h = 0x811c9dc5; /* FNV de repuesto sin secure context */
for (const c of str) { h ^= c.codePointAt(0); h = Math.imul(h, 0x01000193) >>> 0; }
return h.toString(16).padStart(8,'0').repeat(8).slice(0,64);
}
/* ── render de transcript ────────────────────────── */
function line(cls, html){
const p = document.createElement('p');
p.className = 'ln ' + cls; p.innerHTML = html;
$('log').appendChild(p); $('log').scrollTop = $('log').scrollHeight;
return p;
}
const esc = s => s.replace(/&/g,'&').replace(/</g,'<');
const sayH = t => { S.turn++; $('mTurn').textContent = S.turn; line('h','<span class="pfx">H></span> '+esc(t)); };
const sayM = t => line('m','<span class="pfx">M></span> '+esc(t));
const sys = t => line('sys','· '+esc(t));
/* ── evidencia: wire sellado + cadena ────────────── */
async function act(verb, mode, note, fx){
const wire = { seq:++S.seq, verb, mode, note, fx:fx||null, prev:S.prev };
const digest = await sha(JSON.stringify(wire));
S.prev = digest; S.ledger.push({ wire, digest });
if (fx) applyFx(fx);
line('act',' ├─ activity '+esc(verb)+' <span class="chip '+mode+'">'+mode+'</span> <span class="dg">#'+digest.slice(0,10)+'</span>'+(note?' · '+esc(note):''));
evRow(wire, digest);
renderPanels();
}
function evRow(wire, digest){
const r = document.createElement('div');
r.className = 'row'; r.title = JSON.stringify(wire, null, 1);
r.innerHTML = '<span class="seq">'+wire.seq+'</span><span class="vb">'+esc(wire.verb)+
' <span class="chip '+wire.mode+'">'+wire.mode+'</span></span><span class="dg">#'+digest.slice(0,10)+'</span>';
$('evid').appendChild(r); $('evid').scrollTop = $('evid').scrollHeight;
}
function applyFx(fx){
if (fx.units) for (const [u,st] of Object.entries(fx.units)) S.units[u] = st;
if (fx.sim) for (const u of fx.sim) S.sim[u] = true;
if (fx.leases) for (const [u,st] of Object.entries(fx.leases)) S.leases[u] = st;
if (fx.step !== undefined && fx.step > S.step) S.step = fx.step;
if (fx.universe) S.universe = fx.universe;
}
/* ── paneles ─────────────────────────────────────── */
function renderPanels(){
$('mStep').textContent = S.step;
$('stepLbl').textContent = S.step ? ('paso '+S.step+'/11 · '+STEPS[S.step-1]) : 'sala sin abrir';
$('steps').innerHTML = STEPS.map((s,i)=>{
const k = i+1, cls = k < S.step ? 'done' : (k === S.step ? (S.closed?'done':'now') : '');
return '<li class="'+cls+'"><span class="k">'+String(k).padStart(2,'0')+'</span><span>'+s+'</span></li>';
}).join('');
$('units').innerHTML = UNITS.map(u =>
'<tr><td>'+u+'</td><td class="st"><span class="u-'+S.units[u]+'">'+S.units[u]+'</span>'+
(S.sim[u] ? '<span class="sim">·sim</span>' : '')+'</td></tr>').join('');
$('leases').innerHTML = LEASED.map(u => {
const st = S.leases[u], cls = st === '—' ? 'l-nada' : 'l-'+st;
return '<tr><td>pod/'+u+'</td><td class="st '+cls+'">'+st+'</td></tr>';
}).join('');
}
/* ── opciones de H ───────────────────────────────── */
let current = [];
function offer(opts){
current = opts;
$('opts').innerHTML = '';
opts.forEach((o,i) => {
const b = document.createElement('button');
b.className = 'opt'; b.type = 'button';
b.innerHTML = '<span class="n">'+(i+1)+'</span>'+esc(o.label);
b.addEventListener('click', () => pick(o));
$('opts').appendChild(b);
});
}
async function pick(o){
$('opts').querySelectorAll('button').forEach(b => b.disabled = true);
sayH(o.label);
await o.go();
}
/* ── beats del guion ─────────────────────────────── */
const B = {};
B.abrir = async () => {
await act('room.open','mock','sala barrio-lore', {step:2});
await act('peer.join','mock','H · anfitrión', null);
await act('peer.join','mock','M · operador FM', {step:2});
sayM('Estamos en simulacro. La sala y las identidades son mock declarado; tus decisiones y las leases serán reales. Puedo inspeccionar o solicitar la inflación.');
offer([
{ label:'Inspecciona primero.', match:['inspecciona','inspeccion'], go:B.inspeccion },
{ label:'Prepara Bartleby y Cristalizador.', match:['prepara','infla'], go:B.solicitud }
]);
};
B.inspeccion = async () => {
await act('unit.list','real','consulta RO', null);
sayM('Diez unidades declaradas. Bartleby y Cristalizador requieren lease para materializarse; el resto opera vía máquina, simulada. Ninguna frase mía ejecuta efectos ocultos.');
offer([{ label:'Prepara esas dos unidades.', match:['prepara','dos unidades','infla'], go:B.solicitud }]);
};
B.solicitud = async () => {
await act('lease.request','real','pod/bartleby', {leases:{bartleby:'pendiente'}, step:3});
await act('lease.request','real','pod/cristalizador', {leases:{cristalizador:'pendiente'}});
sayM('Solicito unit.inflate para Bartleby y Cristalizador. Requiere pod.lease sobre sus pods. La ceremonia espera tu concesión — yo no puedo concederme nada.');
offer([
{ label:'Concedo los leases.', match:['concedo','concede','grant','si'], go:B.grant },
{ label:'Deniego.', match:['deniego','deniega','deny','no'], go:B.deny }
]);
};
B.deny = async () => {
await act('lease.deny','real','pod/bartleby', {leases:{bartleby:'denegada'}});
await act('lease.deny','real','pod/cristalizador', {leases:{cristalizador:'denegada'}});
sayM('Denegado. Fallo atómico: cero estado parcial. Bartleby y Cristalizador siguen declaradas; ninguna Activity de inflación existe. El transcript registra la denegación, no un intento a medias.');
offer([
{ label:'Vuelve a solicitarlo.', match:['vuelve','solicita','otra vez'], go:B.solicitud },
{ label:'Cierra la sesión.', match:['cierra'], go:B.cierreVacio }
]);
};
B.cierreVacio = async () => {
await act('room.close','mock','sesión sin efectos', null);
sayM('Sesión cerrada sin efectos. La evidencia demuestra lo que no ocurrió: ésa es la mitad del valor de la ceremonia.');
S.closed = true; renderPanels(); postClose();
};
B.grant = async () => {
await act('lease.grant','real','pod/bartleby', {leases:{bartleby:'concedida'}, units:{bartleby:'arrendada'}});
await act('lease.grant','real','pod/cristalizador', {leases:{cristalizador:'concedida'}, units:{cristalizador:'arrendada'}});
await act('unit.inflate','real','bartleby', {units:{bartleby:'materializada'}, step:4});
await act('unit.inflate','real','cristalizador', {units:{cristalizador:'materializada'}});
await act('unit.ready','real','bartleby · cristalizador', {units:{bartleby:'lista', cristalizador:'lista'}});
sayM('Leases válidas. Unidades materializadas y listas. Tipestate recorrido: declarada → arrendada → materializada → lista. Toda transición ilegal es inexpresable, no solo prohibida.');
offer([{ label:'Despliega la máquina y analiza Onfalo.', match:['despliega','analiza','onfalo'], go:B.despliegue }]);
};
B.despliegue = async () => {
await act('machine.deploy','mock','manifiesto declarado', {step:5});
await act('document.ingest','mock','Onfalo · pieza 1/2', {sim:['loreador','archivero'], step:6});
await act('document.ingest','mock','Onfalo · pieza 2/2', null);
await act('document.analyze','mock','bartleby → 10 secciones', {units:{bartleby:'corriendo'}});
await act('unit.idle','real','bartleby', {units:{bartleby:'lista'}});
sayM('Despliegue mock declarado. Dos piezas de Onfalo ingeridas; Bartleby produjo diez secciones de análisis. Cada paso quedó sellado en el ledger con su digest.');
offer([{ label:'Continúa hasta el grafo.', match:['continua','grafo'], go:B.grafo }]);
};
B.grafo = async () => {
await act('vector.create','mock','VectorMock', {sim:['vector-mock'], step:7});
await act('line.materialize','real','línea 1/2', {step:8});
await act('line.materialize','real','línea 2/2', null);
await act('graph.bifurcate','real','2 ramas candidatas', {sim:['grafista'], step:9});
sayM('VectorMock creado, dos líneas validadas y grafo bifurcado. Hay dos ramas candidatas esperando tu elección.');
offer([{ label:'Muéstrame las alternativas.', match:['alternativas','muestrame'], go:B.alternativas }]);
};
B.alternativas = async () => {
await act('graph.inspect','real','consulta RO', null);
sayM('Alpha prioriza consejos. Beta prioriza partido. ¿Cuál inspeccionamos? La elección es tuya y es real: quedará en la cadena causal.');
offer([
{ label:'Alpha.', match:['alpha','alfa'], go:() => B.universo('Alpha') },
{ label:'Beta.', match:['beta'], go:() => B.universo('Beta') }
]);
};
B.universo = async (which) => {
await act('universe.instantiate','real','universo '+which, {universe:which, sim:['demiurgo'], step:10});
await act('corto.emit','real','corto #1 desde '+which, {sim:['dramaturgo','pipeline'], step:11});
sayM('Universo '+which+' instanciado. Corto emitido desde '+which+'. Puedo mostrar datos, trazabilidad o cobertura.');
offer([
{ label:'Datos.', match:['datos'], go:B.datos },
{ label:'Cobertura.', match:['cobertura'], go:B.cobertura },
{ label:'Traza y cierra.', match:['traza','cierra'], go:B.cierre }
]);
};
B.datos = async () => {
await act('corto.data','real','consulta RO', null);
sayM('Corto #1 · fuente: grafo bifurcado, rama '+S.universe+' · derivado de dos líneas canónicas · procedencia intacta hasta Onfalo.');
offer([
{ label:'Cobertura.', match:['cobertura'], go:B.cobertura },
{ label:'Traza y cierra.', match:['traza','cierra'], go:B.cierre }
]);
};
B.cobertura = async () => {
await act('chain.coverage','real','consulta RO', null);
sayM('Cobertura: 11/11 pasos con Activity sellada; 0 huecos en la cadena causal; toda decisión con autoridad tiene su lease.');
offer([
{ label:'Datos.', match:['datos'], go:B.datos },
{ label:'Traza y cierra.', match:['traza','cierra'], go:B.cierre }
]);
};
B.cierre = async () => {
const n = S.ledger.length;
let okChain = true, prev = 'genesis';
for (const e of S.ledger) {
if (e.wire.prev !== prev || (await sha(JSON.stringify(e.wire))) !== e.digest) { okChain = false; break; }
prev = e.digest;
}
await act('chain.verify', 'real', (okChain?'✓ ':'✗ ')+n+' actividades, '+(okChain?0:1)+' rupturas', null);
await act('unit.stop','real','bartleby · cristalizador', {units:{bartleby:'detenida', cristalizador:'detenida'}});
await act('lease.revoke','real','pod/bartleby', {leases:{bartleby:'revocada'}});
await act('lease.revoke','real','pod/cristalizador', {leases:{cristalizador:'revocada'}});
await act('room.close','mock','ceremonia completa', null);
sayM('Cadena verificada hasta Onfalo ('+n+' actividades, 0 rupturas). Unidades detenidas y leases revocadas. Ceremonia cerrada.');
line('sys','⟐ sello de ceremonia · digest raíz #'+S.prev.slice(0,16));
$('evPanel').classList.add('sealflash');
S.closed = true; renderPanels(); postClose();
};
function postClose(){
$('rec').disabled = false;
$('rec').title = 'Verifica la cadena y reconstruye el estado desde el transcript';
offer([]);
$('say').disabled = true;
$('say').placeholder = 'ceremonia cerrada — restart → recupera, o de cero';
}
/* ── restart → recupera (el beat U235) ───────────── */
async function recover(){
line('divider','── restart ──────────────────────────────');
sys('reinicio: estado en memoria descartado.');
Object.assign(S, {
turn:S.turn, step:0, closed:false,
units:Object.fromEntries(UNITS.map(u=>[u,'declarada'])),
sim:Object.fromEntries(UNITS.map(u=>[u,false])),
leases:Object.fromEntries(LEASED.map(u=>[u,'—'])), universe:null
});
renderPanels();
sys('leyendo transcript.ndjson · '+S.ledger.length+' líneas…');
let prev = 'genesis', ok = 0;
for (const e of S.ledger) {
const d = await sha(JSON.stringify(e.wire));
if (d === e.digest && e.wire.prev === prev) ok++;
prev = e.digest;
if (e.wire.fx) applyFx(e.wire.fx);
}
S.closed = true; renderPanels();
sys('verificando cadena de digests… ✓ '+ok+'/'+S.ledger.length);
sys('estado recuperado desde evidencia, no desde memoria.');
sayM('He vuelto. No recuerdo nada — lo he releído todo, y la cadena responde por mí.');
$('mRec').style.display = 'inline-block';
$('rec').disabled = true;
}
/* ── descarga ndjson ─────────────────────────────── */
function download(){
const nd = S.ledger.map(e => JSON.stringify({ ...e.wire, digest:e.digest })).join('\n');
const url = URL.createObjectURL(new Blob([nd], {type:'application/x-ndjson'}));
const a = Object.assign(document.createElement('a'), { href:url, download:'transcript.ndjson' });
a.click(); URL.revokeObjectURL(url);
}
/* ── entrada libre: gramática cerrada ────────────── */
$('say').addEventListener('keydown', async ev => {
if (ev.key !== 'Enter') return;
const raw = $('say').value; if (!raw.trim()) return;
$('say').value = '';
const t = norm(raw);
const hit = current.find(o => o.match && o.match.some(m => t.includes(m)));
if (hit) { await pick(hit); return; }
line('bad-in','H> '+esc(raw)+' ⌀ no compila');
sayM('Esa frase no está en la gramática cerrada. Puedo aceptar: '+current.map(o=>'«'+o.label+'»').join(' · ')+'.');
});
document.addEventListener('keydown', ev => {
if (document.activeElement === $('say') && $('say').value) return;
const k = Number(ev.key);
if (k >= 1 && k <= current.length) pick(current[k-1]);
});
$('dl').addEventListener('click', download);
$('rec').addEventListener('click', recover);
$('zero').addEventListener('click', () => location.reload());
/* ── arranque ────────────────────────────────────── */
renderPanels();
sys('sala barrio-lore en simulacro · 10 unidades declaradas · esperando a H');
offer([{ label:'M, abre el barrio LORE.', match:['abre','barrio'], go:B.abrir }]);
})();
</script>
</body></html>