OverviewFilesCommitsIssuesDocsReleasesPeers

favela-markdown / spec / record-path.html

434 lines · html ref:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>One Record, Two Projections</title>
<meta name="description" content="How the canonical markdown record replicates, how it projects to atproto and ActivityPub, and where the three sync hazards sit.">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Serif:wght@600;700&display=swap">
<style>
  *, *::before, *::after { box-sizing: border-box; }
  html { color-scheme: light dark; }
  body { margin: 0; }
  img { max-width: 100%; }
</style>
</head>
<body>
<style>
  :root {
    --ground:   #e9edf1;
    --surface:  #ffffff;
    --plane:    #dde4ea;
    --plane-2:  #d3dce4;
    --ink:      #16202b;
    --muted:    #5c6b7a;
    --rule:     #b9c5d0;
    --bsky:     #0a6fd8;
    --fedi:     #5254d6;
    --hazard:   #b4400c;
    --hazard-t: #ffffff;
    --serif: "IBM Plex Serif", Georgia, "Times New Roman", serif;
    --sans:  "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
    --mono:  "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
      --ground:   #0f1720;
      --surface:  #18222d;
      --plane:    #1d2833;
      --plane-2:  #22303d;
      --ink:      #e2e9f0;
      --muted:    #93a3b3;
      --rule:     #35485a;
      --bsky:     #57a9ff;
      --fedi:     #9799ff;
      --hazard:   #f0803c;
      --hazard-t: #14202b;
    }
  }
  :root[data-theme="dark"] {
    --ground:   #0f1720;
    --surface:  #18222d;
    --plane:    #1d2833;
    --plane-2:  #22303d;
    --ink:      #e2e9f0;
    --muted:    #93a3b3;
    --rule:     #35485a;
    --bsky:     #57a9ff;
    --fedi:     #9799ff;
    --hazard:   #f0803c;
    --hazard-t: #14202b;
  }

  body {
    background: var(--ground);
    color: var(--ink);
    font-family: var(--sans);
    font-size: 16px;
    line-height: 1.6;
    margin: 0;
    padding: 0 20px 88px;
  }
  .wrap { max-width: 1000px; margin: 0 auto; display: flex; flex-direction: column; gap: 40px; }
  .prose { max-width: 68ch; display: flex; flex-direction: column; gap: 16px; }

  header { padding-top: 56px; display: flex; flex-direction: column; gap: 18px; }
  .eyebrow {
    font-family: var(--mono); font-size: 12px; letter-spacing: .1em;
    text-transform: uppercase; color: var(--muted); margin: 0;
  }
  h1 {
    font-family: var(--serif); font-weight: 700; font-size: clamp(34px, 5.5vw, 52px);
    line-height: 1.08; letter-spacing: -.015em; margin: 0; text-wrap: balance;
  }
  .standfirst { font-size: 19px; line-height: 1.5; color: var(--muted); margin: 0; max-width: 62ch; }

  .meta {
    display: flex; flex-wrap: wrap; gap: 0 28px;
    font-family: var(--mono); font-size: 12.5px; color: var(--muted);
    border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
    padding: 12px 0; margin-top: 6px;
  }
  .meta b { font-weight: 500; color: var(--ink); }
  .meta a { color: var(--ink); text-decoration-color: var(--rule); text-underline-offset: 3px; }
  .meta a:focus-visible { outline: 2px solid var(--bsky); outline-offset: 3px; border-radius: 2px; }

  h2 {
    font-family: var(--serif); font-weight: 600; font-size: 27px; line-height: 1.2;
    letter-spacing: -.01em; margin: 0; text-wrap: balance;
  }
  p { margin: 0; }
  .prose code, .meta code { font-family: var(--mono); font-size: .88em; }

  figure { margin: 0; display: flex; flex-direction: column; gap: 14px; }
  .figbox {
    background: var(--surface); border: 1px solid var(--rule); border-radius: 4px;
    padding: 20px 18px; overflow-x: auto;
  }
  .figbox svg { display: block; width: 100%; min-width: 660px; height: auto; color: var(--ink); }
  figcaption { font-size: 14.5px; line-height: 1.55; color: var(--muted); max-width: 76ch; }
  figcaption b { color: var(--ink); font-weight: 600; }
  figcaption code { font-family: var(--mono); font-size: .89em; }

  .keys { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
  .keys li { display: flex; gap: 10px; align-items: baseline; }
  .keys .n {
    flex: none; width: 19px; height: 19px; border-radius: 50%;
    background: var(--hazard); color: var(--hazard-t);
    font-family: var(--mono); font-size: 11.5px; font-weight: 500;
    display: inline-flex; align-items: center; justify-content: center;
    transform: translateY(2px);
  }

  table { border-collapse: collapse; width: 100%; font-size: 15px; }
  caption {
    text-align: left; font-family: var(--mono); font-size: 12px; letter-spacing: .09em;
    text-transform: uppercase; color: var(--muted); padding-bottom: 10px;
  }
  th, td { text-align: left; vertical-align: top; padding: 11px 16px 11px 0; border-bottom: 1px solid var(--rule); }
  th { font-family: var(--mono); font-weight: 500; font-size: 12.5px; color: var(--muted); letter-spacing: .04em; }
  td:first-child { font-weight: 600; white-space: nowrap; padding-right: 26px; }
  td code { font-family: var(--mono); font-size: .87em; }
  .tablewrap { overflow-x: auto; }

  /* SVG primitives */
  .plane   { fill: var(--plane);   stroke: none; }
  .plane-b { fill: var(--plane-2); stroke: none; }
  .bx  { fill: var(--surface); stroke: currentColor; stroke-width: 1.4; }
  .bx-canon { stroke-width: 3; }
  .bx-bsky  { stroke: var(--bsky); }
  .bx-fedi  { stroke: var(--fedi); }
  .bx-haz   { stroke: var(--hazard); stroke-width: 2; }
  .ln  { stroke: currentColor; stroke-width: 1.4; fill: none; }
  .ln-bsky { stroke: var(--bsky); }
  .ln-fedi { stroke: var(--fedi); }
  .ln-haz  { stroke: var(--hazard); stroke-width: 1.8; }
  .ln-dash { stroke-dasharray: 5 4; }
  .t  { font-family: var(--mono); font-size: 12.5px; fill: var(--ink); }
  .t-sm { font-family: var(--mono); font-size: 10.5px; fill: var(--muted); }
  .t-lab { font-family: var(--mono); font-size: 11.5px; letter-spacing: .07em; fill: var(--muted); }
  .t-haz { fill: var(--hazard); }
  .t-bsky { fill: var(--bsky); }
  .t-fedi { fill: var(--fedi); }
  .mid { text-anchor: middle; }
  .ah { fill: currentColor; }
  .ah-bsky { fill: var(--bsky); }
  .ah-fedi { fill: var(--fedi); }
  .ah-haz  { fill: var(--hazard); }
  .badge { fill: var(--hazard); }
  .badge-t { font-family: var(--mono); font-size: 11.5px; font-weight: 500; fill: var(--hazard-t); text-anchor: middle; }
</style>

<div class="wrap">

  <header>
    <p class="eyebrow">favela-markdown &middot; spec/library.md &sect;10</p>
    <h1>One Record, Two Projections</h1>
    <p class="standfirst">A post is a markdown file. atproto and ActivityPub are derived from it on read. The canonical record replicates the same way it always did &mdash; but moving the encoding puts the strip inside the signature, and that is where three hazards live.</p>
    <div class="meta">
      <span><b>repo</b> <a href="https://7rnx.net/git/repos/favela-markdown">7rnx.net/git/repos/favela-markdown</a></span>
      <span><b>master</b> <code>333c046</code></span>
      <span><b>tracking</b> hyper-social #57</span>
      <span><b>status</b> design &mdash; nothing built</span>
    </div>
  </header>

  <figure>
    <div class="figbox">
      <svg viewBox="0 0 960 560" role="img" aria-label="The canonical markdown record converges across peers as a hash pointer in a shared autobase, and is separately projected on read into a signed atproto commit and an ActivityPub object. Three hazards sit on the projection path.">
        <defs>
          <marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
            <polygon class="ah" points="0,1 10,5 0,9"></polygon>
          </marker>
          <marker id="ab" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
            <polygon class="ah ah-bsky" points="0,1 10,5 0,9"></polygon>
          </marker>
          <marker id="af" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
            <polygon class="ah ah-fedi" points="0,1 10,5 0,9"></polygon>
          </marker>
        </defs>

        <!-- PLANE 1 -->
        <text class="t-lab" x="16" y="26">P2P PLANE &mdash; CONVERGES, ENCODING-BLIND</text>
        <rect class="plane" x="8" y="38" width="944" height="180" rx="5"></rect>

        <rect class="bx bx-canon" x="40" y="74" width="270" height="68" rx="4"></rect>
        <text class="t mid" x="175" y="101">records/net.favela.post/</text>
        <text class="t mid" x="175" y="121">3msdnle65lk22<tspan font-weight="500">.md</tspan></text>

        <rect class="bx" x="390" y="74" width="250" height="68" rx="4"></rect>
        <text class="t mid" x="515" y="101">{ $oid, size } pointer</text>
        <text class="t-sm mid" x="515" y="121">shared StateBase autobase</text>

        <rect class="bx" x="720" y="74" width="200" height="68" rx="4"></rect>
        <text class="t mid" x="820" y="101">every attested</text>
        <text class="t mid" x="820" y="121">peer</text>

        <line class="ln" x1="310" y1="108" x2="384" y2="108" marker-end="url(#a)"></line>
        <text class="t-sm mid" x="347" y="94">put()</text>
        <text class="t-sm mid" x="347" y="128">blake2b</text>

        <line class="ln" x1="640" y1="108" x2="714" y2="108" marker-end="url(#a)"></line>
        <text class="t-sm mid" x="677" y="94">linearizes</text>
        <text class="t-sm mid" x="677" y="128">converges</text>

        <line class="ln ln-dash" x1="180" y1="192" x2="815" y2="192" marker-end="url(#a)" marker-start="url(#a)"></line>
        <text class="t-sm mid" x="497" y="181">record bytes travel by oid &mdash; hyper-overlay, fetched from any peer that holds them</text>

        <!-- SPINE -->
        <path class="ln" d="M175 142 L175 474"></path>
        <text class="t-sm" x="188" y="239">projected on read &mdash; the canonical file is never re-authored</text>

        <!-- PLANE 2 -->
        <text class="t-lab" x="16" y="264">PROJECTION PLANE &mdash; SIGNED, ENCODING-CRITICAL</text>
        <rect class="plane-b" x="8" y="276" width="944" height="264" rx="5"></rect>

        <line class="ln" x1="175" y1="330" x2="224" y2="330" marker-end="url(#a)"></line>
        <rect class="bx" x="230" y="292" width="250" height="76" rx="4"></rect>
        <text class="t mid" x="355" y="318">elected signer</text>
        <text class="t-sm mid" x="355" y="338">favela-markdown strip()</text>
        <text class="t-sm mid" x="355" y="354">then DAG-CBOR encode</text>

        <rect class="bx" x="540" y="292" width="170" height="76" rx="4"></rect>
        <text class="t mid" x="625" y="318">MST +</text>
        <text class="t mid" x="625" y="338">signCommit</text>
        <text class="t-sm mid" x="625" y="354">RFC6979</text>

        <rect class="bx bx-bsky" x="790" y="292" width="130" height="76" rx="4"></rect>
        <text class="t t-bsky mid" x="855" y="330">bsky</text>
        <text class="t-sm mid" x="855" y="350">atproto relay</text>

        <line class="ln" x1="480" y1="330" x2="534" y2="330" marker-end="url(#a)"></line>
        <line class="ln ln-bsky" x1="710" y1="330" x2="784" y2="330" marker-end="url(#ab)"></line>
        <text class="t-sm mid" x="747" y="318">firehose</text>
        <text class="t-sm mid" x="747" y="352">#commit</text>

        <line class="ln" x1="175" y1="474" x2="224" y2="474" marker-end="url(#a)"></line>
        <rect class="bx bx-fedi" x="230" y="440" width="250" height="68" rx="4"></rect>
        <text class="t mid" x="355" y="467">ap-bridge</text>
        <text class="t-sm mid" x="355" y="487">record &rarr; AS2 + HTML</text>

        <rect class="bx bx-fedi" x="790" y="440" width="130" height="68" rx="4"></rect>
        <text class="t t-fedi mid" x="855" y="480">fediverse</text>

        <line class="ln ln-fedi" x1="480" y1="474" x2="784" y2="474" marker-end="url(#af)"></line>
        <text class="t-sm mid" x="632" y="463">HTTP Signatures &mdash; an Update MUST carry `updated`</text>

        <!-- HAZARD BADGES -->
        <circle class="badge" cx="200" cy="303" r="11"></circle>
        <text class="badge-t" x="200" y="307">1</text>
        <circle class="badge" cx="507" cy="303" r="11"></circle>
        <text class="badge-t" x="507" y="307">2</text>
        <circle class="badge" cx="507" cy="357" r="11"></circle>
        <text class="badge-t" x="507" y="361">3</text>
      </svg>
    </div>
    <figcaption>
      <b>The canonical record is encoding-blind above the line and encoding-critical below it.</b>
      A pointer is a hash of a file, so nothing in the top plane cares that the file became markdown &mdash;
      convergence, tombstones and byte transport are untouched. The bottom plane is where
      <code>strip()</code> now runs <em>inside</em> the signing path, and all three hazards land there.
      <ul class="keys">
        <li><span class="n">1</span><span><code>records()</code> filters <code>endsWith('.dagcbor')</code> and is &ldquo;the sole input to the MST build&rdquo; &mdash; a <code>.md</code> record is invisible to the signer.</span></li>
        <li><span class="n">2</span><span>Signing is byte-identical across lease-holders only if every peer strips identically. Version skew forks the chain.</span></li>
        <li><span class="n">3</span><span>The 300-grapheme truncation happens here, and grapheme segmentation is still unpinned.</span></li>
      </ul>
    </figcaption>
  </figure>

  <section class="prose">
    <h2>Hazard 1 is the one that can destroy data</h2>
    <p>It is not a divergence, it is a deletion. <code>records()</code> returns the record set that <code>sign()</code> materialises. If it cannot see the new files, the repo signs as empty &mdash; and an empty repo is not a no-op, it is a commit that deletes every record from the relay.</p>
  </section>

  <figure>
    <div class="figbox">
      <svg viewBox="0 0 940 250" role="img" aria-label="A markdown record file is dropped by the dagcbor extension filter in records(), so the record set reaching sign() is empty and the resulting commit deletes every record.">
        <defs>
          <marker id="a2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
            <polygon class="ah" points="0,1 10,5 0,9"></polygon>
          </marker>
          <marker id="a2h" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
            <polygon class="ah ah-haz" points="0,1 10,5 0,9"></polygon>
          </marker>
        </defs>

        <text class="t-lab" x="16" y="26">ON DISK</text>

        <rect class="bx bx-canon" x="16" y="46" width="310" height="48" rx="4"></rect>
        <text class="t" x="32" y="76">&hellip;/net.favela.post/3msdnle65lk22.md</text>

        <rect class="bx" x="16" y="122" width="310" height="48" rx="4"></rect>
        <text class="t" x="32" y="152">&hellip;/app.bsky.feed.post/&hellip;.dagcbor</text>

        <line class="ln ln-dash" x1="420" y1="34" x2="420" y2="212"></line>
        <text class="t-sm mid" x="420" y="228">records() &mdash; n.endsWith('.dagcbor')</text>

        <line class="ln ln-haz" x1="326" y1="70" x2="400" y2="70"></line>
        <path class="ln ln-haz" d="M412 62 L428 78 M428 62 L412 78"></path>
        <text class="t-sm t-haz" x="326" y="52">dropped &mdash; never reaches the signer</text>

        <line class="ln" x1="326" y1="146" x2="486" y2="146" marker-end="url(#a2)"></line>

        <rect class="bx" x="492" y="118" width="196" height="56" rx="4"></rect>
        <text class="t mid" x="590" y="142">the record set that</text>
        <text class="t mid" x="590" y="160">reaches sign()</text>

        <line class="ln ln-haz" x1="688" y1="146" x2="742" y2="146" marker-end="url(#a2h)"></line>

        <rect class="bx bx-haz" x="748" y="106" width="176" height="80" rx="4"></rect>
        <text class="t t-haz mid" x="836" y="132">if every record</text>
        <text class="t t-haz mid" x="836" y="150">is .md &rarr; empty</text>
        <text class="t-sm t-haz mid" x="836" y="170">delete-every-record</text>
      </svg>
    </div>
    <figcaption>
      <b>The extension filter is the whole bug.</b> <code>EXT</code> is a module-level constant in
      <code>overlay-repo-store.js</code> and <code>records()</code> also calls <code>dagCbor.decode</code>
      unconditionally, so the fix is to make both per-collection before a single <code>.md</code> record exists.
      The existing anti-wipe guard does not catch this: it aborts only when record <em>files</em> exist, and by
      its own count none do.
    </figcaption>
  </figure>

  <section class="prose">
    <h2>Hazard 2 turns a deploy into a fork</h2>
    <p>The signer is an elected, movable role. It is safe to move because ECDSA here is RFC6979-deterministic, so any lease-holder signing the same view produces byte-identical output. Once the strip runs inside that path, the guarantee rests on the strip too &mdash; and a rolling deploy is exactly how two peers come to hold different ones.</p>
  </section>

  <figure>
    <div class="figbox">
      <svg viewBox="0 0 960 300" role="img" aria-label="Without a version guard, two peers running different strip versions sign the same canonical record into two different commit CIDs. With the version stamped in the autobase, the under-versioned peer refuses the signer role and one chain survives.">
        <defs>
          <marker id="a3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
            <polygon class="ah" points="0,1 10,5 0,9"></polygon>
          </marker>
          <marker id="a3h" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
            <polygon class="ah ah-haz" points="0,1 10,5 0,9"></polygon>
          </marker>
        </defs>

        <!-- LEFT -->
        <rect class="plane" x="4" y="4" width="462" height="292" rx="5"></rect>
        <text class="t-lab t-haz mid" x="235" y="30">NO GUARD &mdash; SILENT FORK</text>

        <rect class="bx bx-canon" x="130" y="46" width="210" height="42" rx="4"></rect>
        <text class="t mid" x="235" y="73">the same canonical .md</text>

        <path class="ln" d="M200 88 L120 128" marker-end="url(#a3)"></path>
        <path class="ln" d="M270 88 L350 128" marker-end="url(#a3)"></path>

        <rect class="bx" x="34" y="134" width="172" height="46" rx="4"></rect>
        <text class="t mid" x="120" y="163">peer A &middot; strip v1</text>
        <rect class="bx" x="264" y="134" width="172" height="46" rx="4"></rect>
        <text class="t mid" x="350" y="163">peer B &middot; strip v2</text>

        <line class="ln ln-haz" x1="120" y1="180" x2="120" y2="218" marker-end="url(#a3h)"></line>
        <line class="ln ln-haz" x1="350" y1="180" x2="350" y2="218" marker-end="url(#a3h)"></line>

        <rect class="bx bx-haz" x="34" y="224" width="172" height="46" rx="4"></rect>
        <text class="t t-haz mid" x="120" y="252">commit CID x</text>
        <rect class="bx bx-haz" x="264" y="224" width="172" height="46" rx="4"></rect>
        <text class="t t-haz mid" x="350" y="252">commit CID y</text>

        <text class="t t-haz mid" x="235" y="253" font-size="19">&ne;</text>

        <!-- RIGHT -->
        <rect class="plane" x="494" y="4" width="462" height="292" rx="5"></rect>
        <text class="t-lab mid" x="725" y="30">STRIP_VERSION IN BAND &mdash; LOUD HALT</text>

        <rect class="bx bx-canon" x="620" y="46" width="210" height="42" rx="4"></rect>
        <text class="t mid" x="725" y="73">the same canonical .md</text>

        <path class="ln" d="M690 88 L610 128" marker-end="url(#a3)"></path>
        <path class="ln ln-dash" d="M760 88 L840 128" marker-end="url(#a3)"></path>

        <rect class="bx" x="524" y="134" width="172" height="46" rx="4"></rect>
        <text class="t mid" x="610" y="163">peer A &middot; strip v1</text>
        <rect class="bx" x="754" y="134" width="172" height="46" rx="4"></rect>
        <text class="t mid" x="840" y="163">peer B &middot; strip v2</text>

        <line class="ln" x1="610" y1="180" x2="610" y2="218" marker-end="url(#a3)"></line>
        <line class="ln ln-dash" x1="840" y1="180" x2="840" y2="216"></line>
        <path class="ln ln-haz" d="M832 208 L848 224 M848 208 L832 224"></path>

        <rect class="bx" x="524" y="224" width="172" height="46" rx="4"></rect>
        <text class="t mid" x="610" y="252">one commit chain</text>
        <text class="t-sm mid" x="840" y="248">refuses the</text>
        <text class="t-sm mid" x="840" y="264">signer role</text>
      </svg>
    </div>
    <figcaption>
      <b>The guard does not prevent the skew &mdash; it makes the skew loud.</b> An under-versioned peer
      declines to sign rather than signing with an older strip, which converts an invisible divergence into a
      visible stall. This is Keet&rsquo;s interrupt pattern, already used twice in this stack: pear-git-v3&rsquo;s
      in-band schema guard in the replicated OrgEventLog, and the autobee migration guard.
    </figcaption>
  </figure>

  <section class="prose">
    <h2>What the library guarantees once</h2>
    <p>Three packages project from this contract &mdash; favela-web, the atproto signer, and ap-bridge. Every feature below exists so a property is enforced in the codec instead of being a discipline all three must separately remember.</p>
  </section>

  <div class="tablewrap">
    <table>
      <caption>spec/library.md &sect;4&ndash;&sect;9</caption>
      <thead>
        <tr><th scope="col">feature</th><th scope="col">the guarantee it makes once</th></tr>
      </thead>
      <tbody>
        <tr><td>Loss report</td><td><code>project()</code> returns <code>{ output, loss[] }</code>. The honesty requirement is stated three times in the specs as non-negotiable and has no mechanism today; every loss is already enumerable from the strip rules.</td></tr>
        <tr><td>Pinned directives</td><td><code>:::spoiler</code> projects as its inner text, so a spoilered post arrives <em>un-spoilered</em> on bsky &mdash; while the same directive is lossless to the fediverse as <code>summary</code> + <code>sensitive</code>.</td></tr>
        <tr><td>Fail-closed reach</td><td><code>project()</code> refuses a bsky projection for a non-public record, and unparseable frontmatter degrades to local-only &mdash; never to Public. The one place a defect is a privacy bug.</td></tr>
        <tr><td>Conformance</td><td>The 36 vectors plus a runner and property laws, so a second implementation self-certifies. Fuzz UTF-8: byte-versus-character offsets is the defect the strip spec names as most likely.</td></tr>
        <tr><td>Import</td><td><code>fromAS2</code> / <code>fromBsky</code> return an <code>ImportedView</code> that can never be written back as canonical &mdash; re-encoding a record you did not author already cost two of wj62&rsquo;s posts, unrecoverably.</td></tr>
        <tr><td>CLI</td><td><code>strip</code>, <code>project</code>, <code>explain</code>, <code>lint</code>. Every projection defect so far was diagnosed by reading daemon logs; this makes it a one-liner against a file, offline.</td></tr>
      </tbody>
    </table>
  </div>

</div>
</body>
</html>