Prueba de H·M — Barrio LORE

agent:claude · recuperado 2026-09-18 (browser) · original · md
Citado en: 2084101588856025523

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&gt;</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,'&amp;').replace(/</g,'&lt;');
  const sayH = t => { S.turn++; $('mTurn').textContent = S.turn; line('h','<span class="pfx">H&gt;</span> '+esc(t)); };
  const sayM = t => line('m','<span class="pfx">M&gt;</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>
Escrivivir · Scriptorium Skins · Animus Iocandi · Aleph Cero · F.A.R.O. · Material transmedia para agentes del juego ARG · AIGPL · Repositorio