| … | … | @@ -0,0 +1,417 @@ |
| 1 | <title>One Record, Two Projections</title> |
| 2 | <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"> |
| 3 | <style> |
| 4 | :root { |
| 5 | --ground: #e9edf1; |
| 6 | --surface: #ffffff; |
| 7 | --plane: #dde4ea; |
| 8 | --plane-2: #d3dce4; |
| 9 | --ink: #16202b; |
| 10 | --muted: #5c6b7a; |
| 11 | --rule: #b9c5d0; |
| 12 | --bsky: #0a6fd8; |
| 13 | --fedi: #5254d6; |
| 14 | --hazard: #b4400c; |
| 15 | --hazard-t: #ffffff; |
| 16 | --serif: "IBM Plex Serif", Georgia, "Times New Roman", serif; |
| 17 | --sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif; |
| 18 | --mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace; |
| 19 | } |
| 20 | @media (prefers-color-scheme: dark) { |
| 21 | :root:not([data-theme="light"]) { |
| 22 | --ground: #0f1720; |
| 23 | --surface: #18222d; |
| 24 | --plane: #1d2833; |
| 25 | --plane-2: #22303d; |
| 26 | --ink: #e2e9f0; |
| 27 | --muted: #93a3b3; |
| 28 | --rule: #35485a; |
| 29 | --bsky: #57a9ff; |
| 30 | --fedi: #9799ff; |
| 31 | --hazard: #f0803c; |
| 32 | --hazard-t: #14202b; |
| 33 | } |
| 34 | } |
| 35 | :root[data-theme="dark"] { |
| 36 | --ground: #0f1720; |
| 37 | --surface: #18222d; |
| 38 | --plane: #1d2833; |
| 39 | --plane-2: #22303d; |
| 40 | --ink: #e2e9f0; |
| 41 | --muted: #93a3b3; |
| 42 | --rule: #35485a; |
| 43 | --bsky: #57a9ff; |
| 44 | --fedi: #9799ff; |
| 45 | --hazard: #f0803c; |
| 46 | --hazard-t: #14202b; |
| 47 | } |
| 48 | |
| 49 | body { |
| 50 | background: var(--ground); |
| 51 | color: var(--ink); |
| 52 | font-family: var(--sans); |
| 53 | font-size: 16px; |
| 54 | line-height: 1.6; |
| 55 | margin: 0; |
| 56 | padding: 0 20px 88px; |
| 57 | } |
| 58 | .wrap { max-width: 1000px; margin: 0 auto; display: flex; flex-direction: column; gap: 40px; } |
| 59 | .prose { max-width: 68ch; display: flex; flex-direction: column; gap: 16px; } |
| 60 | |
| 61 | header { padding-top: 56px; display: flex; flex-direction: column; gap: 18px; } |
| 62 | .eyebrow { |
| 63 | font-family: var(--mono); font-size: 12px; letter-spacing: .1em; |
| 64 | text-transform: uppercase; color: var(--muted); margin: 0; |
| 65 | } |
| 66 | h1 { |
| 67 | font-family: var(--serif); font-weight: 700; font-size: clamp(34px, 5.5vw, 52px); |
| 68 | line-height: 1.08; letter-spacing: -.015em; margin: 0; text-wrap: balance; |
| 69 | } |
| 70 | .standfirst { font-size: 19px; line-height: 1.5; color: var(--muted); margin: 0; max-width: 62ch; } |
| 71 | |
| 72 | .meta { |
| 73 | display: flex; flex-wrap: wrap; gap: 0 28px; |
| 74 | font-family: var(--mono); font-size: 12.5px; color: var(--muted); |
| 75 | border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); |
| 76 | padding: 12px 0; margin-top: 6px; |
| 77 | } |
| 78 | .meta b { font-weight: 500; color: var(--ink); } |
| 79 | .meta a { color: var(--ink); text-decoration-color: var(--rule); text-underline-offset: 3px; } |
| 80 | .meta a:focus-visible { outline: 2px solid var(--bsky); outline-offset: 3px; border-radius: 2px; } |
| 81 | |
| 82 | h2 { |
| 83 | font-family: var(--serif); font-weight: 600; font-size: 27px; line-height: 1.2; |
| 84 | letter-spacing: -.01em; margin: 0; text-wrap: balance; |
| 85 | } |
| 86 | p { margin: 0; } |
| 87 | .prose code, .meta code { font-family: var(--mono); font-size: .88em; } |
| 88 | |
| 89 | figure { margin: 0; display: flex; flex-direction: column; gap: 14px; } |
| 90 | .figbox { |
| 91 | background: var(--surface); border: 1px solid var(--rule); border-radius: 4px; |
| 92 | padding: 20px 18px; overflow-x: auto; |
| 93 | } |
| 94 | .figbox svg { display: block; width: 100%; min-width: 660px; height: auto; color: var(--ink); } |
| 95 | figcaption { font-size: 14.5px; line-height: 1.55; color: var(--muted); max-width: 76ch; } |
| 96 | figcaption b { color: var(--ink); font-weight: 600; } |
| 97 | figcaption code { font-family: var(--mono); font-size: .89em; } |
| 98 | |
| 99 | .keys { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; gap: 7px; } |
| 100 | .keys li { display: flex; gap: 10px; align-items: baseline; } |
| 101 | .keys .n { |
| 102 | flex: none; width: 19px; height: 19px; border-radius: 50%; |
| 103 | background: var(--hazard); color: var(--hazard-t); |
| 104 | font-family: var(--mono); font-size: 11.5px; font-weight: 500; |
| 105 | display: inline-flex; align-items: center; justify-content: center; |
| 106 | transform: translateY(2px); |
| 107 | } |
| 108 | |
| 109 | table { border-collapse: collapse; width: 100%; font-size: 15px; } |
| 110 | caption { |
| 111 | text-align: left; font-family: var(--mono); font-size: 12px; letter-spacing: .09em; |
| 112 | text-transform: uppercase; color: var(--muted); padding-bottom: 10px; |
| 113 | } |
| 114 | th, td { text-align: left; vertical-align: top; padding: 11px 16px 11px 0; border-bottom: 1px solid var(--rule); } |
| 115 | th { font-family: var(--mono); font-weight: 500; font-size: 12.5px; color: var(--muted); letter-spacing: .04em; } |
| 116 | td:first-child { font-weight: 600; white-space: nowrap; padding-right: 26px; } |
| 117 | td code { font-family: var(--mono); font-size: .87em; } |
| 118 | .tablewrap { overflow-x: auto; } |
| 119 | |
| 120 | /* SVG primitives */ |
| 121 | .plane { fill: var(--plane); stroke: none; } |
| 122 | .plane-b { fill: var(--plane-2); stroke: none; } |
| 123 | .bx { fill: var(--surface); stroke: currentColor; stroke-width: 1.4; } |
| 124 | .bx-canon { stroke-width: 3; } |
| 125 | .bx-bsky { stroke: var(--bsky); } |
| 126 | .bx-fedi { stroke: var(--fedi); } |
| 127 | .bx-haz { stroke: var(--hazard); stroke-width: 2; } |
| 128 | .ln { stroke: currentColor; stroke-width: 1.4; fill: none; } |
| 129 | .ln-bsky { stroke: var(--bsky); } |
| 130 | .ln-fedi { stroke: var(--fedi); } |
| 131 | .ln-haz { stroke: var(--hazard); stroke-width: 1.8; } |
| 132 | .ln-dash { stroke-dasharray: 5 4; } |
| 133 | .t { font-family: var(--mono); font-size: 12.5px; fill: var(--ink); } |
| 134 | .t-sm { font-family: var(--mono); font-size: 10.5px; fill: var(--muted); } |
| 135 | .t-lab { font-family: var(--mono); font-size: 11.5px; letter-spacing: .07em; fill: var(--muted); } |
| 136 | .t-haz { fill: var(--hazard); } |
| 137 | .t-bsky { fill: var(--bsky); } |
| 138 | .t-fedi { fill: var(--fedi); } |
| 139 | .mid { text-anchor: middle; } |
| 140 | .ah { fill: currentColor; } |
| 141 | .ah-bsky { fill: var(--bsky); } |
| 142 | .ah-fedi { fill: var(--fedi); } |
| 143 | .ah-haz { fill: var(--hazard); } |
| 144 | .badge { fill: var(--hazard); } |
| 145 | .badge-t { font-family: var(--mono); font-size: 11.5px; font-weight: 500; fill: var(--hazard-t); text-anchor: middle; } |
| 146 | </style> |
| 147 | |
| 148 | <div class="wrap"> |
| 149 | |
| 150 | <header> |
| 151 | <p class="eyebrow">favela-markdown · spec/library.md §10</p> |
| 152 | <h1>One Record, Two Projections</h1> |
| 153 | <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 — but moving the encoding puts the strip inside the signature, and that is where three hazards live.</p> |
| 154 | <div class="meta"> |
| 155 | <span><b>repo</b> <a href="https://7rnx.net/git/repos/favela-markdown">7rnx.net/git/repos/favela-markdown</a></span> |
| 156 | <span><b>master</b> <code>333c046</code></span> |
| 157 | <span><b>tracking</b> hyper-social #57</span> |
| 158 | <span><b>status</b> design — nothing built</span> |
| 159 | </div> |
| 160 | </header> |
| 161 | |
| 162 | <figure> |
| 163 | <div class="figbox"> |
| 164 | <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."> |
| 165 | <defs> |
| 166 | <marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"> |
| 167 | <polygon class="ah" points="0,1 10,5 0,9"></polygon> |
| 168 | </marker> |
| 169 | <marker id="ab" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"> |
| 170 | <polygon class="ah ah-bsky" points="0,1 10,5 0,9"></polygon> |
| 171 | </marker> |
| 172 | <marker id="af" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"> |
| 173 | <polygon class="ah ah-fedi" points="0,1 10,5 0,9"></polygon> |
| 174 | </marker> |
| 175 | </defs> |
| 176 | |
| 177 | <!-- PLANE 1 --> |
| 178 | <text class="t-lab" x="16" y="26">P2P PLANE — CONVERGES, ENCODING-BLIND</text> |
| 179 | <rect class="plane" x="8" y="38" width="944" height="180" rx="5"></rect> |
| 180 | |
| 181 | <rect class="bx bx-canon" x="40" y="74" width="270" height="68" rx="4"></rect> |
| 182 | <text class="t mid" x="175" y="101">records/net.favela.post/</text> |
| 183 | <text class="t mid" x="175" y="121">3msdnle65lk22<tspan font-weight="500">.md</tspan></text> |
| 184 | |
| 185 | <rect class="bx" x="390" y="74" width="250" height="68" rx="4"></rect> |
| 186 | <text class="t mid" x="515" y="101">{ $oid, size } pointer</text> |
| 187 | <text class="t-sm mid" x="515" y="121">shared StateBase autobase</text> |
| 188 | |
| 189 | <rect class="bx" x="720" y="74" width="200" height="68" rx="4"></rect> |
| 190 | <text class="t mid" x="820" y="101">every attested</text> |
| 191 | <text class="t mid" x="820" y="121">peer</text> |
| 192 | |
| 193 | <line class="ln" x1="310" y1="108" x2="384" y2="108" marker-end="url(#a)"></line> |
| 194 | <text class="t-sm mid" x="347" y="94">put()</text> |
| 195 | <text class="t-sm mid" x="347" y="128">blake2b</text> |
| 196 | |
| 197 | <line class="ln" x1="640" y1="108" x2="714" y2="108" marker-end="url(#a)"></line> |
| 198 | <text class="t-sm mid" x="677" y="94">linearizes</text> |
| 199 | <text class="t-sm mid" x="677" y="128">converges</text> |
| 200 | |
| 201 | <line class="ln ln-dash" x1="180" y1="192" x2="815" y2="192" marker-end="url(#a)" marker-start="url(#a)"></line> |
| 202 | <text class="t-sm mid" x="497" y="181">record bytes travel by oid — hyper-overlay, fetched from any peer that holds them</text> |
| 203 | |
| 204 | <!-- SPINE --> |
| 205 | <path class="ln" d="M175 142 L175 474"></path> |
| 206 | <text class="t-sm" x="188" y="239">projected on read — the canonical file is never re-authored</text> |
| 207 | |
| 208 | <!-- PLANE 2 --> |
| 209 | <text class="t-lab" x="16" y="264">PROJECTION PLANE — SIGNED, ENCODING-CRITICAL</text> |
| 210 | <rect class="plane-b" x="8" y="276" width="944" height="264" rx="5"></rect> |
| 211 | |
| 212 | <line class="ln" x1="175" y1="330" x2="224" y2="330" marker-end="url(#a)"></line> |
| 213 | <rect class="bx" x="230" y="292" width="250" height="76" rx="4"></rect> |
| 214 | <text class="t mid" x="355" y="318">elected signer</text> |
| 215 | <text class="t-sm mid" x="355" y="338">favela-markdown strip()</text> |
| 216 | <text class="t-sm mid" x="355" y="354">then DAG-CBOR encode</text> |
| 217 | |
| 218 | <rect class="bx" x="540" y="292" width="170" height="76" rx="4"></rect> |
| 219 | <text class="t mid" x="625" y="318">MST +</text> |
| 220 | <text class="t mid" x="625" y="338">signCommit</text> |
| 221 | <text class="t-sm mid" x="625" y="354">RFC6979</text> |
| 222 | |
| 223 | <rect class="bx bx-bsky" x="790" y="292" width="130" height="76" rx="4"></rect> |
| 224 | <text class="t t-bsky mid" x="855" y="330">bsky</text> |
| 225 | <text class="t-sm mid" x="855" y="350">atproto relay</text> |
| 226 | |
| 227 | <line class="ln" x1="480" y1="330" x2="534" y2="330" marker-end="url(#a)"></line> |
| 228 | <line class="ln ln-bsky" x1="710" y1="330" x2="784" y2="330" marker-end="url(#ab)"></line> |
| 229 | <text class="t-sm mid" x="747" y="318">firehose</text> |
| 230 | <text class="t-sm mid" x="747" y="352">#commit</text> |
| 231 | |
| 232 | <line class="ln" x1="175" y1="474" x2="224" y2="474" marker-end="url(#a)"></line> |
| 233 | <rect class="bx bx-fedi" x="230" y="440" width="250" height="68" rx="4"></rect> |
| 234 | <text class="t mid" x="355" y="467">ap-bridge</text> |
| 235 | <text class="t-sm mid" x="355" y="487">record → AS2 + HTML</text> |
| 236 | |
| 237 | <rect class="bx bx-fedi" x="790" y="440" width="130" height="68" rx="4"></rect> |
| 238 | <text class="t t-fedi mid" x="855" y="480">fediverse</text> |
| 239 | |
| 240 | <line class="ln ln-fedi" x1="480" y1="474" x2="784" y2="474" marker-end="url(#af)"></line> |
| 241 | <text class="t-sm mid" x="632" y="463">HTTP Signatures — an Update MUST carry `updated`</text> |
| 242 | |
| 243 | <!-- HAZARD BADGES --> |
| 244 | <circle class="badge" cx="200" cy="303" r="11"></circle> |
| 245 | <text class="badge-t" x="200" y="307">1</text> |
| 246 | <circle class="badge" cx="507" cy="303" r="11"></circle> |
| 247 | <text class="badge-t" x="507" y="307">2</text> |
| 248 | <circle class="badge" cx="507" cy="357" r="11"></circle> |
| 249 | <text class="badge-t" x="507" y="361">3</text> |
| 250 | </svg> |
| 251 | </div> |
| 252 | <figcaption> |
| 253 | <b>The canonical record is encoding-blind above the line and encoding-critical below it.</b> |
| 254 | A pointer is a hash of a file, so nothing in the top plane cares that the file became markdown — |
| 255 | convergence, tombstones and byte transport are untouched. The bottom plane is where |
| 256 | <code>strip()</code> now runs <em>inside</em> the signing path, and all three hazards land there. |
| 257 | <ul class="keys"> |
| 258 | <li><span class="n">1</span><span><code>records()</code> filters <code>endsWith('.dagcbor')</code> and is “the sole input to the MST build” — a <code>.md</code> record is invisible to the signer.</span></li> |
| 259 | <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> |
| 260 | <li><span class="n">3</span><span>The 300-grapheme truncation happens here, and grapheme segmentation is still unpinned.</span></li> |
| 261 | </ul> |
| 262 | </figcaption> |
| 263 | </figure> |
| 264 | |
| 265 | <section class="prose"> |
| 266 | <h2>Hazard 1 is the one that can destroy data</h2> |
| 267 | <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 — and an empty repo is not a no-op, it is a commit that deletes every record from the relay.</p> |
| 268 | </section> |
| 269 | |
| 270 | <figure> |
| 271 | <div class="figbox"> |
| 272 | <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."> |
| 273 | <defs> |
| 274 | <marker id="a2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"> |
| 275 | <polygon class="ah" points="0,1 10,5 0,9"></polygon> |
| 276 | </marker> |
| 277 | <marker id="a2h" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"> |
| 278 | <polygon class="ah ah-haz" points="0,1 10,5 0,9"></polygon> |
| 279 | </marker> |
| 280 | </defs> |
| 281 | |
| 282 | <text class="t-lab" x="16" y="26">ON DISK</text> |
| 283 | |
| 284 | <rect class="bx bx-canon" x="16" y="46" width="310" height="48" rx="4"></rect> |
| 285 | <text class="t" x="32" y="76">…/net.favela.post/3msdnle65lk22.md</text> |
| 286 | |
| 287 | <rect class="bx" x="16" y="122" width="310" height="48" rx="4"></rect> |
| 288 | <text class="t" x="32" y="152">…/app.bsky.feed.post/….dagcbor</text> |
| 289 | |
| 290 | <line class="ln ln-dash" x1="420" y1="34" x2="420" y2="212"></line> |
| 291 | <text class="t-sm mid" x="420" y="228">records() — n.endsWith('.dagcbor')</text> |
| 292 | |
| 293 | <line class="ln ln-haz" x1="326" y1="70" x2="400" y2="70"></line> |
| 294 | <path class="ln ln-haz" d="M412 62 L428 78 M428 62 L412 78"></path> |
| 295 | <text class="t-sm t-haz" x="326" y="52">dropped — never reaches the signer</text> |
| 296 | |
| 297 | <line class="ln" x1="326" y1="146" x2="486" y2="146" marker-end="url(#a2)"></line> |
| 298 | |
| 299 | <rect class="bx" x="492" y="118" width="196" height="56" rx="4"></rect> |
| 300 | <text class="t mid" x="590" y="142">the record set that</text> |
| 301 | <text class="t mid" x="590" y="160">reaches sign()</text> |
| 302 | |
| 303 | <line class="ln ln-haz" x1="688" y1="146" x2="742" y2="146" marker-end="url(#a2h)"></line> |
| 304 | |
| 305 | <rect class="bx bx-haz" x="748" y="106" width="176" height="80" rx="4"></rect> |
| 306 | <text class="t t-haz mid" x="836" y="132">if every record</text> |
| 307 | <text class="t t-haz mid" x="836" y="150">is .md → empty</text> |
| 308 | <text class="t-sm t-haz mid" x="836" y="170">delete-every-record</text> |
| 309 | </svg> |
| 310 | </div> |
| 311 | <figcaption> |
| 312 | <b>The extension filter is the whole bug.</b> <code>EXT</code> is a module-level constant in |
| 313 | <code>overlay-repo-store.js</code> and <code>records()</code> also calls <code>dagCbor.decode</code> |
| 314 | unconditionally, so the fix is to make both per-collection before a single <code>.md</code> record exists. |
| 315 | The existing anti-wipe guard does not catch this: it aborts only when record <em>files</em> exist, and by |
| 316 | its own count none do. |
| 317 | </figcaption> |
| 318 | </figure> |
| 319 | |
| 320 | <section class="prose"> |
| 321 | <h2>Hazard 2 turns a deploy into a fork</h2> |
| 322 | <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 — and a rolling deploy is exactly how two peers come to hold different ones.</p> |
| 323 | </section> |
| 324 | |
| 325 | <figure> |
| 326 | <div class="figbox"> |
| 327 | <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."> |
| 328 | <defs> |
| 329 | <marker id="a3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"> |
| 330 | <polygon class="ah" points="0,1 10,5 0,9"></polygon> |
| 331 | </marker> |
| 332 | <marker id="a3h" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"> |
| 333 | <polygon class="ah ah-haz" points="0,1 10,5 0,9"></polygon> |
| 334 | </marker> |
| 335 | </defs> |
| 336 | |
| 337 | <!-- LEFT --> |
| 338 | <rect class="plane" x="4" y="4" width="462" height="292" rx="5"></rect> |
| 339 | <text class="t-lab t-haz mid" x="235" y="30">NO GUARD — SILENT FORK</text> |
| 340 | |
| 341 | <rect class="bx bx-canon" x="130" y="46" width="210" height="42" rx="4"></rect> |
| 342 | <text class="t mid" x="235" y="73">the same canonical .md</text> |
| 343 | |
| 344 | <path class="ln" d="M200 88 L120 128" marker-end="url(#a3)"></path> |
| 345 | <path class="ln" d="M270 88 L350 128" marker-end="url(#a3)"></path> |
| 346 | |
| 347 | <rect class="bx" x="34" y="134" width="172" height="46" rx="4"></rect> |
| 348 | <text class="t mid" x="120" y="163">peer A · strip v1</text> |
| 349 | <rect class="bx" x="264" y="134" width="172" height="46" rx="4"></rect> |
| 350 | <text class="t mid" x="350" y="163">peer B · strip v2</text> |
| 351 | |
| 352 | <line class="ln ln-haz" x1="120" y1="180" x2="120" y2="218" marker-end="url(#a3h)"></line> |
| 353 | <line class="ln ln-haz" x1="350" y1="180" x2="350" y2="218" marker-end="url(#a3h)"></line> |
| 354 | |
| 355 | <rect class="bx bx-haz" x="34" y="224" width="172" height="46" rx="4"></rect> |
| 356 | <text class="t t-haz mid" x="120" y="252">commit CID x</text> |
| 357 | <rect class="bx bx-haz" x="264" y="224" width="172" height="46" rx="4"></rect> |
| 358 | <text class="t t-haz mid" x="350" y="252">commit CID y</text> |
| 359 | |
| 360 | <text class="t t-haz mid" x="235" y="253" font-size="19">≠</text> |
| 361 | |
| 362 | <!-- RIGHT --> |
| 363 | <rect class="plane" x="494" y="4" width="462" height="292" rx="5"></rect> |
| 364 | <text class="t-lab mid" x="725" y="30">STRIP_VERSION IN BAND — LOUD HALT</text> |
| 365 | |
| 366 | <rect class="bx bx-canon" x="620" y="46" width="210" height="42" rx="4"></rect> |
| 367 | <text class="t mid" x="725" y="73">the same canonical .md</text> |
| 368 | |
| 369 | <path class="ln" d="M690 88 L610 128" marker-end="url(#a3)"></path> |
| 370 | <path class="ln ln-dash" d="M760 88 L840 128" marker-end="url(#a3)"></path> |
| 371 | |
| 372 | <rect class="bx" x="524" y="134" width="172" height="46" rx="4"></rect> |
| 373 | <text class="t mid" x="610" y="163">peer A · strip v1</text> |
| 374 | <rect class="bx" x="754" y="134" width="172" height="46" rx="4"></rect> |
| 375 | <text class="t mid" x="840" y="163">peer B · strip v2</text> |
| 376 | |
| 377 | <line class="ln" x1="610" y1="180" x2="610" y2="218" marker-end="url(#a3)"></line> |
| 378 | <line class="ln ln-dash" x1="840" y1="180" x2="840" y2="216"></line> |
| 379 | <path class="ln ln-haz" d="M832 208 L848 224 M848 208 L832 224"></path> |
| 380 | |
| 381 | <rect class="bx" x="524" y="224" width="172" height="46" rx="4"></rect> |
| 382 | <text class="t mid" x="610" y="252">one commit chain</text> |
| 383 | <text class="t-sm mid" x="840" y="248">refuses the</text> |
| 384 | <text class="t-sm mid" x="840" y="264">signer role</text> |
| 385 | </svg> |
| 386 | </div> |
| 387 | <figcaption> |
| 388 | <b>The guard does not prevent the skew — it makes the skew loud.</b> An under-versioned peer |
| 389 | declines to sign rather than signing with an older strip, which converts an invisible divergence into a |
| 390 | visible stall. This is Keet’s interrupt pattern, already used twice in this stack: pear-git-v3’s |
| 391 | in-band schema guard in the replicated OrgEventLog, and the autobee migration guard. |
| 392 | </figcaption> |
| 393 | </figure> |
| 394 | |
| 395 | <section class="prose"> |
| 396 | <h2>What the library guarantees once</h2> |
| 397 | <p>Three packages project from this contract — 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> |
| 398 | </section> |
| 399 | |
| 400 | <div class="tablewrap"> |
| 401 | <table> |
| 402 | <caption>spec/library.md §4–§9</caption> |
| 403 | <thead> |
| 404 | <tr><th scope="col">feature</th><th scope="col">the guarantee it makes once</th></tr> |
| 405 | </thead> |
| 406 | <tbody> |
| 407 | <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> |
| 408 | <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 — while the same directive is lossless to the fediverse as <code>summary</code> + <code>sensitive</code>.</td></tr> |
| 409 | <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 — never to Public. The one place a defect is a privacy bug.</td></tr> |
| 410 | <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> |
| 411 | <tr><td>Import</td><td><code>fromAS2</code> / <code>fromBsky</code> return an <code>ImportedView</code> that can never be written back as canonical — re-encoding a record you did not author already cost two of wj62’s posts, unrecoverably.</td></tr> |
| 412 | <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> |
| 413 | </tbody> |
| 414 | </table> |
| 415 | </div> |
| 416 | |
| 417 | </div> |