| 1 | diff --git a/architecture.html b/architecture.html |
| 2 | deleted file mode 100644 |
| 3 | index d7ae415..0000000 |
| 4 | --- a/architecture.html |
| 5 | +++ /dev/null |
| 6 | @@ -1,433 +0,0 @@ |
| 7 | -<!doctype html> |
| 8 | -<html lang="en"> |
| 9 | -<head> |
| 10 | -<meta charset="utf-8"> |
| 11 | -<meta name="viewport" content="width=device-width, initial-scale=1"> |
| 12 | -<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%23245d43'/%3E%3Cpath d='M9 9h6v6H9zm8 0h6v6h-6zM9 17h6v6H9z' fill='%23dfebda'/%3E%3Cpath d='M17 17h6v6h-6z' fill='%23e89966'/%3E%3C/svg%3E"> |
| 13 | -<meta name="description" content="An architectural proposal for tinyrelay: templated, on-demand, self-hosted Nostr relays in Go."> |
| 14 | -<title>tinyrelay — Architecture for a relay you own</title> |
| 15 | -<style> |
| 16 | -:root{--bg:#f5f4ef;--paper:#fffef9;--ink:#182820;--muted:#56635b;--line:#ccd3c8;--green:#245d43;--mint:#dfebda;--orange:#a34724;--warm:#f6e6d5;--code:#192b23;--white:#f8faf5;--sidebar:222px} |
| 17 | -*{box-sizing:border-box}html{scroll-behavior:smooth;scroll-padding-top:28px}body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}a{color:var(--green);text-underline-offset:3px}a:hover{text-decoration-thickness:2px}button,select{font:inherit}button{cursor:pointer}button:focus-visible,select:focus-visible,a:focus-visible,summary:focus-visible{outline:3px solid var(--orange);outline-offset:4px}button{color:inherit}::selection{background:var(--mint)}.skip{position:absolute;left:12px;top:-100px;background:var(--paper);padding:10px;z-index:10}.skip:focus{top:12px} |
| 18 | -.rail{position:fixed;inset:0 auto 0 0;width:var(--sidebar);border-right:1px solid var(--line);padding:31px 25px;display:flex;flex-direction:column;background:var(--bg);overflow:auto}.brand{font-weight:760;font-size:23px;letter-spacing:-1.2px;color:var(--ink);text-decoration:none;display:flex;align-items:center;gap:10px}.mark{width:24px;height:24px;display:grid;grid-template-columns:repeat(2,8px);gap:3px;transform:rotate(-8deg)}.mark i{background:var(--green);border-radius:2px}.mark i:last-child{background:var(--orange)}.rail .sub{font-size:10px;letter-spacing:1.6px;text-transform:uppercase;margin:12px 0 38px;color:var(--muted)}nav{display:grid;gap:3px}nav a{display:flex;gap:13px;align-items:center;font-size:13px;line-height:1.35;text-decoration:none;padding:9px 0;color:var(--muted)}nav a span{font:10px ui-monospace,SFMono-Regular,monospace;color:var(--muted)}nav a.active,nav a:hover{color:var(--green);font-weight:700}.rail-foot{margin-top:auto;padding-top:35px;font-size:11px;color:var(--muted);line-height:1.6}.rail-foot b{display:block;color:var(--ink);font-size:12px}.rail-foot .dot{display:inline-block;width:6px;height:6px;background:var(--green);border-radius:50%;margin-right:5px} |
| 19 | -main{margin-left:var(--sidebar);max-width:1510px;padding:0 64px 50px}.topline{display:flex;justify-content:space-between;align-items:center;padding:27px 0;border-bottom:1px solid var(--line);font-size:11px;letter-spacing:1.3px;text-transform:uppercase;gap:15px}.topline button{border:0;background:transparent;padding:4px;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:1px}.hero{padding:67px 0 42px}.eyebrow{font:11px/1.4 ui-monospace,SFMono-Regular,monospace;letter-spacing:1.8px;text-transform:uppercase;color:var(--green);display:flex;align-items:center;gap:10px}.eyebrow:before{content:"";height:7px;width:7px;background:var(--orange);display:inline-block}.hero h1{font-size:clamp(42px,5.4vw,78px);line-height:1.06;letter-spacing:-3.3px;margin:23px 0 25px;max-width:900px;font-weight:680}.hero h1 em{font-family:Georgia,serif;font-weight:400;color:var(--green)}.lead{max-width:790px;color:var(--muted);font-size:20px;line-height:1.6}.hero-foot{display:flex;flex-wrap:wrap;gap:8px;margin:26px 0}.pill{border:1px solid var(--line);padding:5px 10px;font-size:11px;border-radius:3px;letter-spacing:.3px}.pill.strong{background:var(--green);border-color:var(--green);color:var(--white)}.principles{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.principle{padding:26px 22px 28px 0}.principle+.principle{border-left:1px solid var(--line);padding-left:24px}.principle .big{font-size:27px;letter-spacing:-1px;line-height:1.2;margin:7px 0 11px}.principle p{font-size:13px;margin:0;color:var(--muted)}.micro{font:10px ui-monospace,SFMono-Regular,monospace;text-transform:uppercase;letter-spacing:1.5px;color:var(--muted)} |
| 20 | -section{padding:61px 0 7px;scroll-margin-top:14px}section+section{margin-top:35px;border-top:1px solid var(--line)}.section-head{display:flex;gap:22px;align-items:flex-start;margin-bottom:24px}.section-num{font:12px ui-monospace,SFMono-Regular,monospace;color:var(--orange);padding-top:12px}.section-head h2{font-size:34px;line-height:1.2;letter-spacing:-1.1px;margin:0 0 11px;font-weight:650}.section-head p{color:var(--muted);margin:0;max-width:800px;font-size:16px}h3{font-size:19px;line-height:1.35;letter-spacing:-.4px;margin:0 0 12px}h4{font-size:14px;margin:0 0 8px}p{margin:0 0 15px}.fine{font-size:12px;color:var(--muted)}.source{font-size:11px;line-height:1.65;color:var(--muted);margin-top:13px}.source a{overflow-wrap:anywhere}.note{padding:18px 22px;border-left:3px solid var(--green);background:var(--mint);font-size:14px;margin:23px 0}.note.warm{background:var(--warm);border-color:var(--orange)}.note p:last-child{margin:0}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:32px}.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:25px}.block{background:var(--paper);border:1px solid var(--line);padding:23px}.block p,.block li{font-size:14px}.block p:last-child{margin-bottom:0}ul,ol{padding-left:20px;margin:10px 0 17px}li{padding-left:3px;margin:7px 0}code{font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere;background:#e8ece3;padding:2px 4px;border-radius:2px}pre{background:var(--code);color:#e1efde;padding:23px;overflow:auto;line-height:1.65;font-size:12px;margin:0;border-radius:3px;tab-size:2}pre code{background:none;color:inherit;padding:0;font-size:inherit;white-space:pre;overflow-wrap:normal} |
| 21 | -.table-wrap{overflow-x:auto}table{width:100%;border-collapse:collapse;font-size:13px;line-height:1.55}th{text-align:left;font-size:10px;text-transform:uppercase;letter-spacing:1.4px;color:var(--muted);font-weight:600;padding:13px 12px;border-bottom:2px solid var(--ink)}td{padding:17px 12px;border-bottom:1px solid var(--line);vertical-align:top}td:first-child{font-weight:620;min-width:142px}thead th:first-child,tbody td:first-child{padding-left:0}tbody tr:last-child td{border-bottom:0}.phenotype td:nth-child(2){color:var(--muted);width:30%}.phenotype td:nth-child(3){width:45%}.tag{font:9px/1.3 ui-monospace,SFMono-Regular,monospace;text-transform:uppercase;letter-spacing:.7px;display:inline-block;padding:4px 6px;background:var(--mint);color:var(--green);margin-bottom:5px}.tag.drop{color:var(--orange);background:var(--warm)} |
| 22 | -.map{background:var(--paper);border:1px solid var(--line);padding:28px}.map-head{display:flex;align-items:center;justify-content:space-between;gap:18px;flex-wrap:wrap;margin-bottom:25px}.map-head p{margin:0;font-size:11px;text-transform:uppercase;letter-spacing:1.2px}.path-buttons{display:flex;flex-wrap:wrap;gap:5px}.path-buttons button{background:transparent;border:1px solid var(--line);border-radius:3px;padding:7px 12px;font-size:12px}.path-buttons button[aria-pressed=true]{background:var(--green);color:var(--white);border-color:var(--green)}.actors{display:grid;grid-template-columns:1fr 32px 1fr 32px 1fr;align-items:stretch;gap:0}.actor{border:1px solid var(--line);padding:17px;min-width:0;background:var(--bg);position:relative;transition:background .15s,border-color .15s}.actor strong{display:block;font-size:15px;margin:5px 0}.actor small{font-size:12px;line-height:1.55;display:block;color:var(--muted)}.actor.hot{background:var(--mint);border-color:var(--green)}.actor.hot .micro{color:var(--green)}.arrow{display:flex;align-items:center;justify-content:center;font-size:21px;color:var(--muted)}.tier{margin-top:18px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}.tier-label{margin-top:23px;margin-bottom:9px}.telemetry-strip{margin-top:18px;padding:13px 15px;background:var(--code);color:var(--white);font:11px/1.6 ui-monospace,SFMono-Regular,monospace}.map-reading{border-top:1px solid var(--line);margin-top:23px;padding-top:21px;display:grid;grid-template-columns:160px 1fr;gap:20px;min-height:113px}.map-reading h3{font-size:16px}.map-reading p{font-size:13px;color:var(--muted);margin:0}.map-reading strong{color:var(--ink)}.flow{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:20px 0}.flow span{padding:8px 12px;border:1px solid var(--line);font:12px ui-monospace,SFMono-Regular,monospace}.flow b{font-size:17px;font-weight:400;color:var(--green)}.template-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:18px}.template-head label{font-size:12px;font-weight:600}.template-head select{background:var(--paper);border:1px solid var(--line);padding:8px;max-width:100%;color:var(--ink);font-size:13px}.template-info{padding:23px;background:var(--mint);border:1px solid var(--line)}.template-info dl{margin:18px 0 0;display:grid;grid-template-columns:85px 1fr;gap:11px 16px;font-size:13px}.template-info dt{color:var(--muted)}.template-info dd{margin:0}.pipeline{counter-reset:step;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));margin:25px 0}.pipeline div{padding:21px 18px;border-top:2px solid var(--green);border-right:1px solid var(--line);position:relative}.pipeline div:last-child{border-right:0}.pipeline div:before{counter-increment:step;content:"0" counter(step);display:block;font:10px ui-monospace,SFMono-Regular,monospace;color:var(--green);margin-bottom:12px}.pipeline strong{display:block;font-size:14px;margin-bottom:8px}.pipeline p{font-size:12px;margin:0;color:var(--muted)}details{border-top:1px solid var(--line);padding:17px 0}details:last-child{border-bottom:1px solid var(--line)}summary{cursor:pointer;font-size:15px;font-weight:630;list-style-position:outside;margin-left:17px}details .detail-body{padding:18px 0 0 17px;font-size:14px}.metric{font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;font-weight:400!important;overflow-wrap:anywhere}.decision-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 35px}.decision{padding:24px 0;border-top:1px solid var(--line)}.decision .micro{color:var(--orange);margin-bottom:10px}.decision p{font-size:14px}.roadmap{display:grid;grid-template-columns:92px 1fr;gap:0 25px}.phase{padding:23px 0;border-top:1px solid var(--line)}.phase-code{color:var(--green);font:12px ui-monospace,SFMono-Regular,monospace;padding-top:27px;border-top:1px solid var(--line)}.phase p{font-size:14px}.gate{font-size:12px;border-left:2px solid var(--green);padding-left:12px;color:var(--muted)}.reference-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 35px}.reference-grid p{font-size:12px;margin:0;border-bottom:1px solid var(--line);padding:0 0 13px}.reference-grid a{font-size:13px;font-weight:600}.foot{border-top:1px solid var(--line);margin-top:60px;padding-top:25px;display:flex;justify-content:space-between;gap:20px;font-size:11px;color:var(--muted)}.no-js{padding:12px;background:var(--warm);font-size:13px} |
| 23 | -@media(min-width:1600px){main{margin-right:auto}}@media(max-width:1150px){:root{--sidebar:185px}.rail{padding:28px 18px}main{padding:0 34px 40px}.hero h1{font-size:58px}.three{grid-template-columns:1fr}.pipeline{grid-template-columns:repeat(2,minmax(0,1fr))}.pipeline div:nth-child(2){border-right:0}.pipeline div:nth-child(n+3){border-top:1px solid var(--line)}}@media(max-width:850px){:root{--sidebar:0px}.rail{position:static;width:auto;border-right:0;border-bottom:1px solid var(--line);padding:18px 24px;display:block}.rail .sub,.rail-foot{display:none}.rail nav{display:flex;overflow-x:auto;margin-top:14px;gap:19px}.rail nav a{white-space:nowrap;font-size:12px;padding:5px 0}.rail nav a span{display:none}main{margin:0;padding:0 24px 30px}.topline{padding:20px 0}.hero{padding:45px 0 30px}.hero h1{font-size:55px}.two{gap:23px}.section-head h2{font-size:29px}.map{padding:22px}.map-reading{grid-template-columns:1fr;gap:5px}.reference-grid{gap:14px 22px}} |
| 24 | -@media(max-width:580px){body{font-size:15px}main{padding:0 19px 28px}.rail{padding:17px 19px}.topline{font-size:9px;letter-spacing:.8px}.topline button{font-size:10px}.hero h1{font-size:43px;letter-spacing:-2px}.hero .lead{font-size:17px}.principles{grid-template-columns:1fr}.principle,.principle+.principle{padding:20px 0;border-left:0}.principle+.principle{border-top:1px solid var(--line)}.principle .big{font-size:23px}.section-head{gap:14px}.section-head h2{font-size:28px}.section-head p{font-size:14px}section{padding-top:38px}section+section{margin-top:28px}.two,.decision-grid,.reference-grid{grid-template-columns:1fr}.actors{grid-template-columns:1fr}.arrow{height:30px;transform:rotate(90deg)}.tier{grid-template-columns:1fr;gap:12px}.map{padding:17px}.map-head{gap:13px}.path-buttons{gap:6px}.path-buttons button{padding:9px 10px}.pipeline{grid-template-columns:1fr}.pipeline div{border-right:0!important;border-top:1px solid var(--line)}.template-head{align-items:flex-start;flex-direction:column}.template-head select{width:100%}.block,.template-info{padding:18px}pre{font-size:11px;padding:18px}.roadmap{grid-template-columns:51px 1fr;gap:0 13px}.phase-code{font-size:10px}.foot{flex-direction:column;gap:5px}th,td{padding:12px 10px}.phenotype{min-width:610px}table.stack{min-width:610px}details .detail-body{padding-left:0}.note{padding:16px;font-size:13px}.pill{font-size:10px}.topline span{max-width:230px}} |
| 25 | -@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*{transition:none!important}} |
| 26 | -.two>*{min-width:0}pre{max-width:100%} |
| 27 | -@media print{body{font-size:10pt;background:white}.rail,.topline button,.path-buttons,.template-head select{display:none}main{margin:0;padding:0;max-width:none}.hero{padding:25px 0}.hero h1{font-size:38pt}.lead{font-size:13pt}section{padding-top:25px;break-inside:auto}section+section{margin-top:20px}.section-head h2{font-size:23pt}.block,.actor,.template-info,.note,pre{break-inside:avoid}pre{white-space:pre-wrap;background:#f3f3ef;color:#111}pre code{white-space:pre-wrap}.table-wrap{overflow:visible}td,th{font-size:9pt;padding:9px}a{color:inherit}details{break-inside:avoid}.hero-foot{margin:15px 0}.topline{padding:10px 0}.foot{margin-top:25px}} |
| 28 | -</style> |
| 29 | -</head> |
| 30 | -<body> |
| 31 | -<a href="#overview" class="skip">Skip to architecture</a> |
| 32 | -<aside class="rail"> |
| 33 | - <a class="brand" href="#overview"><span class="mark" aria-hidden="true"><i></i><i></i><i></i><i></i></span>tinyrelay</a> |
| 34 | - <div class="sub">Architecture proposal / 01</div> |
| 35 | - <nav aria-label="Plan contents"> |
| 36 | - <a href="#overview"><span>00</span>Direction</a> |
| 37 | - <a href="#phenotype"><span>01</span>Project phenotype</a> |
| 38 | - <a href="#architecture"><span>02</span>System architecture</a> |
| 39 | - <a href="#tenancy"><span>03</span>Names & templates</a> |
| 40 | - <a href="#delivery"><span>04</span>Inbox & outbox</a> |
| 41 | - <a href="#storage"><span>05</span>Storage & correctness</a> |
| 42 | - <a href="#o11y"><span>06</span>Observability</a> |
| 43 | - <a href="#performance"><span>07</span>Performance loop</a> |
| 44 | - <a href="#reuse"><span>08</span>SDKs & prior art</a> |
| 45 | - <a href="#operations"><span>09</span>Self-host operations</a> |
| 46 | - <a href="#delivery-plan"><span>10</span>Build & migration</a> |
| 47 | - <a href="#decisions"><span>11</span>Decision record</a> |
| 48 | - <a href="#sources"><span>12</span>Evidence & sources</a> |
| 49 | - </nav> |
| 50 | - <div class="rail-foot"><b><span class="dot"></span> Native implementation in progress</b>Based on bindws @ 5d31267<br>Research checked 07 Sep 2026<br>Designed for machines you own.</div> |
| 51 | -</aside> |
| 52 | -<main> |
| 53 | -<div class="topline"><span>Design study | Bindws → native Go</span><button type="button" id="print">Print / save PDF ↗</button></div> |
| 54 | -<header class="hero" id="overview"> |
| 55 | - <div class="eyebrow">Self-hosted Nostr infrastructure</div> |
| 56 | - <h1>Many relays.<br>One process.<br><em>Your machine.</em></h1> |
| 57 | - <p class="lead">Bring bindws’s full capabilities and user workflows to a native, self-hosted Go service. Preserve the experience of creating and running templated relays while replacing the Worker runtime with local services.</p> |
| 58 | - <div class="hero-foot"><span class="pill strong">Full functional & UX parity</span><span class="pill strong">No fuel or leases</span><span class="pill">No product quotas</span><span class="pill">Automatic inbox / outbox</span><span class="pill">Observability from day one</span></div> |
| 59 | - <p class="fine">Recommendation: a modular monolith with an embedded control plane, a SQLite database per named relay, and shared workers. A personal template provisions a family of relays. Public and management pages are plain HTML and basic forms, rebuilt without bindws branding or styling.</p> |
| 60 | - <div class="note"><strong>Measured Git results | 7 September 2026.</strong> Six repositories from the local Developer folder passed push, clone, fetch, and concurrent clone tests on Slate. Streaming reduced peak Go RSS from 6,094 MiB to 40.6 MiB; Atlas cloned in 9.15 seconds against an 8.92-second native Git baseline. <a href="artifacts/git-performance/git-performance.html">Open the report, failures, methodology and raw data.</a></div> |
| 61 | - <div class="note"><strong>Completion contract: work until parity.</strong> The finished Go project must cover every existing bindws feature, protocol behavior, management action and user journey, except the explicitly agreed changes below. Intermediate phases are implementation checkpoints; a working core relay is not completion. Features that owners can switch off still have to be implemented and tested.</div> |
| 62 | -</header> |
| 63 | -<div class="principles"> |
| 64 | - <div class="principle"><div class="micro">01 / Product</div><div class="big">Preserve the workflows.</div><p>All relay, community, hosting and management capabilities carry forward. Templates remain the API; visual simplicity does not reduce functionality.</p></div> |
| 65 | - <div class="principle"><div class="micro">02 / Runtime</div><div class="big">Local by default.</div><p>One daemon, local disk, long-lived sockets and restart-safe jobs. Capacity is determined by the machine and its workload.</p></div> |
| 66 | - <div class="principle"><div class="micro">03 / Engineering</div><div class="big">Measure before tuning.</div><p>Instrument acceptance, query, fan-out and delivery before optimization. Use traces and profiles to explain benchmark results.</p></div> |
| 67 | -</div> |
| 68 | -<noscript><p class="no-js">The full plan is readable without JavaScript. Interactive path and template previews require JavaScript; their default views remain visible.</p></noscript> |
| 69 | - |
| 70 | -<section id="phenotype"> |
| 71 | - <div class="section-head"><span class="section-num">01</span><div><h2>The runtime changes. The workflows carry forward.</h2><p>Target full bindws parity on a local host. Rework provider-shaped implementation details while preserving what users can do, how they authorize it, and the result they receive.</p></div></div> |
| 72 | - <div class="table-wrap"><table class="phenotype"> |
| 73 | - <thead><tr><th>Trait</th><th>Bindws today</th><th>Proposed tinyrelay</th></tr></thead> |
| 74 | - <tbody> |
| 75 | - <tr><td>Identity of the product</td><td>On-demand hosted relays with a console, fuel and many hosting features.</td><td><span class="tag">Full parity</span><br>A self-hosted relay factory with the complete relay, community, hosting and management surface. Personal and community relays remain template instances.</td></tr> |
| 76 | - <tr><td>Provisioning</td><td>Unclaimed → leased or claimed. A lease lasts 14 days and can be claimed in place.</td><td><span class="tag drop">Remove</span><br>Creation requires an owner and template. No lease endpoint, trial state, claim conversion, trial retention or expiry teardown.</td></tr> |
| 77 | - <tr><td>Economics & limits</td><td>Fuel credits, free allowances, paid overages, fixed job/queue ceilings and per-member caps.</td><td><span class="tag drop">Remove</span><br>No billing ledger, fuel gate, built-in tenant quotas or paid capacity tiers. Preserve explicit content policy and host-level backpressure.</td></tr> |
| 78 | - <tr><td>Unit of execution</td><td>One Durable Object per name; hibernating sockets and alarm rounds.</td><td><span class="tag">Replace</span><br>A lazily loaded relay runtime with owned goroutines; shared scheduling continues when no users are connected.</td></tr> |
| 79 | - <tr><td>Template model</td><td>JSONC presets compile into the Worker; omitted configuration sections stay unchanged.</td><td><span class="tag">Evolve</span><br>Embedded catalog + local files, immutable revisions, plan/apply/export and first-class inbox/outbox rules. No tenant-supplied executable code.</td></tr> |
| 80 | - <tr><td>Automatic delivery</td><td>Opt-in durable NIP-65 queue using locally stored lists. Pending queue capped at 512; four attempts; full queues can omit delivery.</td><td><span class="tag">Evolve</span><br>Template-driven routing; durable intent in the event transaction; list discovery, connection reuse, retry state and visible terminal outcomes. No fixed tenant delivery ceiling.</td></tr> |
| 81 | - <tr><td>Public inbox</td><td>Kind allowlist with 90-day retention. Template comments acknowledge that the owner-tag predicate is missing.</td><td><span class="tag">Complete</span><br>Require intended-recipient targeting. Continuously gather matching public mentions from configured sources; retention is an explicit content choice.</td></tr> |
| 82 | - <tr><td>Persistence</td><td>Durable Object SQLite + R2; SQL and storage shaped by provider restrictions.</td><td><span class="tag">Replace</span><br>Catalog database + tenant SQLite files and content-addressed blobs on local disk. Preserve events, files, sites, repositories, history and backups; S3 is an optional storage adapter.</td></tr> |
| 83 | - <tr><td>Names & domains</td><td>Wildcard host routing, HOSTS KV and Cloudflare for SaaS; celld has a manual hostname map.</td><td><span class="tag">Replace</span><br>Explicit catalog aliases, host or path routing, TLS at the operator’s proxy. Unknown hosts never allocate a relay.</td></tr> |
| 84 | - <tr><td>Owner & community</td><td>Nostr ownership, members, bans, moderators, invitations, group records and relay signing identity.</td><td><span class="tag">Preserve</span><br>Keep membership, invite trees, guest rules, moderation, groups, transfer and succession. Separate host administrator from relay owner; retain Nostr authorization.</td></tr> |
| 85 | - <tr><td>Site & management UI</td><td>Branded public pages and a generated console bundle with many feature tabs.</td><td><span class="tag">Rebuild presentation</span><br>Every existing action and role-specific workflow remains available through plain HTML/forms. Preserve sign-in, previews, progress, recovery, QR and app handoff. No inherited visual branding.</td></tr> |
| 86 | - <tr><td>Hosting & computed services</td><td>Blossom/NIP-96, sites, GRASP Git, Marmot, views, succession and NIP-9a callbacks.</td><td><span class="tag">Required for parity</span><br>Implement all of them on local storage and workers. Preserve feature toggles and their existing access rules. Stage the work, but do not finish with these features missing.</td></tr> |
| 87 | - <tr><td>Observability</td><td>Usage meters, JSON logs, Cloudflare traces and optional Analytics Engine.</td><td><span class="tag">Replace</span><br>Prometheus metrics, structured logs, optional OTel traces and protected pprof/runtime diagnostics. No telemetry feeds a fuel calculation.</td></tr> |
| 88 | - </tbody> |
| 89 | - </table></div> |
| 90 | - <p class="source">Local evidence: <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/docs/10-architecture.md">architecture</a>, <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/docs/16-hosting-without-cloudflare.md">celld hosting</a>, <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/src/delivery.ts">delivery.ts:7–113</a>, <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/relay-templates/03-inbox.jsonc">inbox template:1–42</a>. Inspected in the adjacent checkout; links identify the corresponding revision.</p> |
| 91 | - <div class="note"><strong>“No resource limits” is a product commitment.</strong> There is no maximum relay count, monthly event allowance, fuel balance or tenant storage tier in the domain model. The host still has finite RAM, disk and file descriptors. Flow control uses finite buffers, fair scheduling and connection deadlines; overload returns a clear retryable failure before acceptance. Retention, bans and authorized access remain owner-selected relay behavior.</div> |
| 92 | - <div class="note"><strong>Product UI layout.</strong> The rebuilt public and management pages use bare HTML, tables and basic forms. The fixed layout target is 960px on desktop and 360px on mobile, with no inherited bindws branding or visual system. When useful, a locally vendored Fixi project bundle (fixi, moxi, paxi, ssexi and rexi) may provide progressive form swaps, inline actions, morphing, streamed job progress and ergonomic requests. It remains an enhancement layer: signed NIP-98 mutations, accessible forms and server responses remain usable without it.</div> |
| 93 | - <details open><summary>Explicit changes to the parity baseline</summary><div class="detail-body"><ul><li><strong>Remove fuel and product capacity entitlements:</strong> no balance, top-up, billing ledger, paid tier or inherited fixed tenant quota. NIP-57 zap event support and non-fuel notifications remain in scope.</li><li><strong>Remove relay leases and trials:</strong> assign ownership at creation, including fork destinations. Preserve membership invitation codes and their claim/join workflows; those are unrelated to leasing a relay.</li><li><strong>Replace cloud infrastructure:</strong> Workers, Durable Objects, R2/KV bindings, alarms and provider domain provisioning become local Go services. Preserve their user-facing capabilities through local implementations.</li><li><strong>Rebuild presentation:</strong> plain HTML and forms, no bindws style or branding. Keep task flows, permissions, signers, feedback, sharing and accessible alternatives. The command is <code>tiny</code>.</li><li><strong>Improve inbox/outbox and observability:</strong> add targeted inbox admission, persistent automation and measurement-driven performance work. Document behavioral improvements explicitly; do not silently reinterpret imported policies.</li></ul><p>These changes do not authorize dropping unrelated features. Keep a precise source-to-target mapping for every exception and verify that its replacement workflow is complete.</p></div></details> |
| 94 | -</section> |
| 95 | - |
| 96 | -<section id="architecture"> |
| 97 | - <div class="section-head"><span class="section-num">02</span><div><h2>A small process with clear ownership.</h2><p>The tenant boundary is a named relay. Each runtime owns its policy, subscriptions and store. Cross-relay work goes through explicit services.</p></div></div> |
| 98 | - <div class="map"> |
| 99 | - <div class="map-head"><p>One host / one daemon</p><div class="path-buttons" role="group" aria-label="Highlight an architectural path"><button type="button" data-path="publish" aria-pressed="true">Publish</button><button type="button" data-path="subscribe" aria-pressed="false">Subscribe</button><button type="button" data-path="delivery" aria-pressed="false">Deliver</button><button type="button" data-path="create" aria-pressed="false">Create</button></div></div> |
| 100 | - <div class="actors" aria-label="Primary request path"> |
| 101 | - <div class="actor hot" data-node="edge"><div class="micro">Network boundary</div><strong>TLS proxy → HTTP / WS</strong><small>Clients, console and CLI<br>Canonical host or relay path</small></div> |
| 102 | - <div class="arrow" aria-hidden="true">→</div> |
| 103 | - <div class="actor hot" data-node="registry"><div class="micro">Process boundary</div><strong>Tenant registry</strong><small>Resolve name → stable ID<br>Load once, retain while active</small></div> |
| 104 | - <div class="arrow" aria-hidden="true">→</div> |
| 105 | - <div class="actor hot" data-node="runtime"><div class="micro">Tenant boundary</div><strong>Relay runtime × N</strong><small>Protocol | auth | policy<br>Queries | subscriptions | fan-out</small></div> |
| 106 | - </div> |
| 107 | - <div class="micro tier-label">Services inside the same Go process</div> |
| 108 | - <div class="tier"> |
| 109 | - <div class="actor" data-node="control"><div class="micro">Control plane</div><strong>Provision & configure</strong><small>Catalog DB, template revisions,<br>owners, aliases and audit trail</small></div> |
| 110 | - <div class="actor hot" data-node="store"><div class="micro">Durable boundary</div><strong>SQLite per relay</strong><small>Events + policy + delivery intent<br>One local transaction</small></div> |
| 111 | - <div class="actor" data-node="workers"><div class="micro">Background work</div><strong>Shared scheduler & dialer</strong><small>Inbox pulls ↔ remote relays<br>Outbox delivery → remote relays</small></div> |
| 112 | - </div> |
| 113 | - <div class="telemetry-strip">All boundaries → metrics + structured logs + optional OTel spans → local diagnostics / optional collector</div> |
| 114 | - <div class="map-reading" aria-live="polite"><h3 id="path-title">Publish an event</h3><p id="path-description"><strong>Resolve → validate → authorize → commit → acknowledge → fan-out.</strong> Persist the event and routing intent together. An OK confirms local durability; remote delivery proceeds independently. The event never crosses another tenant’s runtime implicitly.</p></div> |
| 115 | - </div> |
| 116 | - <div class="two" style="margin-top:26px"> |
| 117 | - <div><h3>Own the domain, reuse the wire protocol.</h3><p>Use a Khatru adapter for Nostr transport and hooks. Tinyrelay owns admission, transaction boundaries, lifecycle, routing and telemetry. The web surface uses Go HTML templates and ordinary form handlers. Start with one Khatru instance per active tenant; do not mutate framework hooks during concurrent requests.</p><p>Every task belongs to a context, has a stop path, and is joined on shutdown. The registry never holds a global lock during SQL or network I/O. Live subscriptions remain in memory and are rebuilt by reconnecting clients after restart.</p></div> |
| 118 | - <div><h3>Concurrency without a second platform.</h3><p>Readers run concurrently; each tenant serializes short writes. A shared scheduler fairly dispatches due jobs, backed by persistent tenant state. Each socket has a single writer and bounded pending output; a slow consumer is disconnected with a reason.</p><p>Runtime eviction requires zero connections, zero in-flight requests and no running task. Due jobs reacquire the tenant. Closing an idle database releases handles; it never deletes relay state.</p></div> |
| 119 | - </div> |
| 120 | - <details><summary>Proposed Go package boundaries</summary><div class="detail-body"><pre><code>cmd/tiny startup, serve, management CLI |
| 121 | -internal/host listeners, routing, registry, lifecycle |
| 122 | -internal/control creation, owners, aliases, template plan/apply |
| 123 | -internal/relay ingest/query services and policy snapshots |
| 124 | -internal/protocol Khatru adapter, AUTH, HTTP and NIP adapters |
| 125 | -internal/storage catalog + tenant SQLite; atomic ingest |
| 126 | -internal/delivery discovery, routing, durable attempts, dialer |
| 127 | -internal/jobs fair scheduler, pulls, expiry, backup tasks |
| 128 | -internal/media local Blossom/NIP-96 blobs, mirror, reports |
| 129 | -internal/sites manifests, isolated origins, verified serving |
| 130 | -internal/git GRASP policy, Git objects/refs, recovery journal |
| 131 | -internal/community members, invites, moderation, signed group state |
| 132 | -internal/records views, discovery, notifications, succession |
| 133 | -internal/telemetry metric vocabulary, spans, slog, diagnostics |
| 134 | -internal/migrate bindws reader, import report, reconciliation |
| 135 | -templates/ embedded versioned JSONC documents |
| 136 | -web/ plain HTML templates; optional signer script</code></pre><p class="source">This is a proposed layout. Use small interfaces at storage, clock, dialer and signer boundaries; avoid a general plugin engine or an interface for every struct. The public site and management pages have no frontend framework or inherited bindws assets.</p></div></details> |
| 137 | -</section> |
| 138 | - |
| 139 | -<section id="tenancy"> |
| 140 | - <div class="section-head"><span class="section-num">03</span><div><h2>Create owned relays on demand.</h2><p>Provisioning is a durable control operation. Activation is a transient runtime concern. Neither introduces a lease.</p></div></div> |
| 141 | - <div class="two"> |
| 142 | - <div class="block"><h3>Durable identity</h3><p>An authorized operator creates a relay with a stable internal ID, unique name, owner pubkey and template revision. A configured owner allowlist may self-provision through signed management requests. There is no open anonymous creation door by default.</p><div class="flow"><span>creating</span><b>→</b><span>ready</span><b>↔</b><span>disabled</span></div><p><strong>Deletion is explicit.</strong> Disabling closes access and pauses delivery while preserving data. No idle timeout, owner inactivity or calendar date deletes a relay. Content expiry under NIP-40 is separate.</p></div> |
| 143 | - <div class="block"><h3>Runtime lifecycle</h3><p>Resolve a registered host or path, acquire its runtime, and lazily open the tenant database. Single-flight activation prevents two runtimes from owning the same tenant. Unknown names return an error and allocate nothing.</p><div class="flow"><span>unloaded</span><b>↔</b><span>active</span><b>→</b><span>draining</span></div><p>A database handle cache is a host implementation detail, not a relay-count limit. Background jobs can activate a relay even with no connected clients.</p></div> |
| 144 | - </div> |
| 145 | - <p class="fine" style="margin-top:18px">Creation spans catalog and tenant files: write an idempotent “creating” catalog record, initialize the tenant database and owner, then mark ready. Only ready records are routable. Startup completes or reports interrupted creation. Unique names, idempotency keys and a data-directory process lock prevent duplicate ownership.</p> |
| 146 | - <h3 style="margin-top:30px">Keep configuration portable; make behavior explicit.</h3> |
| 147 | - <p>A personal bundle creates three ordinary named relays: <code>alice-outbox</code>, <code>alice-inbox</code> and <code>alice-dm</code>. Each has its own database and policy. A bundle is an additional provisioning convenience. Preserve the full existing template catalog: default, outbox, inbox, private, chat, media, search, articles, dm, quiet, site, marmot, grasp, marmot-members and home, together with their connection templates and source-dependent jobs.</p> |
| 148 | - <div class="template-head"><label for="template-select">Explore the proposed built-in roles</label><select id="template-select"><option value="outbox">Personal / public outbox</option><option value="inbox">Personal / public inbox</option><option value="dm">Personal / private DM inbox</option><option value="community">Community / members publish</option></select></div> |
| 149 | - <div class="two"> |
| 150 | - <div class="template-info" aria-live="polite"><div class="micro">Template behavior</div><h3 id="template-title" style="margin-top:10px">Public outbox</h3><p id="template-about">Your public events live here and automatically travel to the relays that should receive them.</p><dl><dt>Writes</dt><dd id="template-writes">Owner; signed events only</dd><dt>Reads</dt><dd id="template-reads">Public</dd><dt>Automation</dt><dd id="template-auto">Owner write relays + tagged recipients’ read relays + explicit mirrors</dd><dt>Retention</dt><dd id="template-retention">No automatic age-based purge</dd></dl></div> |
| 151 | - <pre aria-label="Illustrative template configuration"><code id="template-code">{ |
| 152 | - "format": "tinyrelay/template/1", |
| 153 | - "id": "personal-outbox", |
| 154 | - "revision": 1, |
| 155 | - "policy": { |
| 156 | - "writes": "owner", |
| 157 | - "reads": "public", |
| 158 | - "blockPrivateKinds": true |
| 159 | - }, |
| 160 | - "automation": { |
| 161 | - "delivery": "nip65-public", |
| 162 | - "origin": "local-client" |
| 163 | - }, |
| 164 | - "retention": [] |
| 165 | -}</code></pre> |
| 166 | - </div> |
| 167 | - <p class="fine" style="margin-top:14px">The selector illustrates four roles; it does not define the scope of the template catalog. This is a proposed schema, not an implemented API. Owner and name belong to the creation request. Both hostname endpoints and <code>wss://relay.example/r/alice-outbox</code> paths resolve to the same stable tenant ID.</p> |
| 168 | - <div class="three" style="margin-top:28px"> |
| 169 | - <div><h4>Compile once</h4><p>Validate JSONC against a strict schema. Resolve an immutable revision into a typed policy snapshot. No JavaScript, WASM, shell interpolation or arbitrary code execution inside templates.</p></div> |
| 170 | - <div><h4>Change deliberately</h4><p>Plan/apply/export remain first-class. On update, omitted sections stay unchanged; an empty list explicitly clears a list. Reject unknown fields and use a revision precondition to prevent lost updates.</p></div> |
| 171 | - <div><h4>Publish capabilities truthfully</h4><p>Template files do not hot-change existing tenants. Applying a revision records an audit entry and swaps the policy atomically. Generate NIP-11 from a build capability registry intersected with tenant-enabled features; only conformance-tested capabilities enter that registry.</p></div> |
| 172 | - </div> |
| 173 | - <p class="source">Preserved semantics: <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/src/config.ts">bindws config.ts</a>, <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/relay-templates/README.md">relay template contract</a>, <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/connection-templates/README.md">connection shortcuts</a>. App shortcuts stay presentation metadata; visibility never substitutes for authorization.</p> |
| 174 | -</section> |
| 175 | - |
| 176 | -<section id="delivery"> |
| 177 | - <div class="section-head"><span class="section-num">04</span><div><h2>Automatic inbox and outbox, precisely.</h2><p>Separate public event distribution, mention collection and encrypted-message receipt. The same worker infrastructure serves them; their authorization rules differ.</p></div></div> |
| 178 | - <div class="table-wrap"><table class="stack"><thead><tr><th>Role</th><th>Acceptance & visibility</th><th>Automatic behavior</th></tr></thead><tbody> |
| 179 | - <tr><td>Public outbox</td><td>Owner writes by default; public reads. Block private kinds, exclude ephemeral traffic from durable routing, and never forward NIP-70 protected events.</td><td>Forward newly accepted local public events to the author’s NIP-65 write relays, tagged recipients’ read relays and configured mirrors. Imported/pulled events do not trigger redistribution.</td></tr> |
| 180 | - <tr><td>Public inbox</td><td>Accept configured public kinds only when a valid <code>p</code> tag names the owner or an allowed recipient. Sender profiles may be fetched as related metadata; they do not get an unrestricted kind-0 admission bypass.</td><td>Maintain subscriptions to configured sources and the owner’s other public read relays for matching mentions. Recover gaps with overlapping windows and ID deduplication; use NIP-77 reconciliation where available.</td></tr> |
| 181 | - <tr><td>Private DM inbox</td><td>Verify the outer kind-1059 event ID/signature and its recipient <code>p</code> tag against local recipients. This does not verify the encrypted inner message. Require NIP-42 AUTH matching the outer recipient for queries, counts and live delivery; membership alone grants no access.</td><td>Advertise this endpoint through an owner-signed kind-10050 list. Clients deliver their recipient-specific wraps directly. The relay stores opaque ciphertext and does not discover, decrypt or blast private messages.</td></tr> |
| 182 | - </tbody></table></div> |
| 183 | - <p class="source"><a href="https://github.com/nostr-protocol/nips/blob/master/65.md">NIP-65</a> defines public relay-list discovery, not a server delivery guarantee. <a href="https://github.com/nostr-protocol/nips/blob/master/17.md">NIP-17</a> uses kind 10050 for DM relays and recipient-restricted gift-wrap reads. These are distinct from a public mentions inbox.</p> |
| 184 | - <div class="note"><strong>Automatic begins after explicit setup.</strong> Creating a personal bundle enables its local policies and workers. The console prepares merged kind-10002 and kind-10050 updates for the owner’s NIP-07/NIP-46 signer. The daemon never possesses the owner’s secret key or pretends its relay key is that owner. Display “discovery not published” until the signed lists are published.</div> |
| 185 | - <div class="pipeline"> |
| 186 | - <div><strong>Record intent</strong><p>In the event transaction, add durable routing intent with origin and policy revision. A cold discovery cache never blocks a local publish on network I/O.</p></div> |
| 187 | - <div><strong>Resolve destinations</strong><p>Read the newest verified signed lists; use configured indexers on cache misses. Persist each normalized destination independently. Missing lists remain visible as unresolved work.</p></div> |
| 188 | - <div><strong>Attempt delivery</strong><p>Reuse connections by endpoint and authentication context. Correlate each OK with its event ID. Record attempts, next-attempt time and structured outcome per target.</p></div> |
| 189 | - <div><strong>Recover & report</strong><p>Retry transient failures with jittered backoff. Duplicate remote acceptance is success. Permanent rejections remain inspectable and can be retried deliberately.</p></div> |
| 190 | - </div> |
| 191 | - <div class="two"> |
| 192 | - <div><h3>The delivery contract</h3><ul><li><strong>At-least-once attempts</strong>, with idempotency by tenant + event ID + destination. A crash after remote acceptance can cause a duplicate retry.</li><li><strong>Local OK ≠ remote delivery.</strong> Accepted means committed locally with routing intent when enabled. The console reports remote status separately.</li><li><strong>No silent queue eviction.</strong> Pending intent stays on disk through temporary outages. Optional expiry is an explicit operator retention policy, never a hidden tenant ceiling; expiry, deletion or cancellation records a terminal status.</li><li><strong>Policy is checked again before sending.</strong> A deletion, expiration, disabled relay, revoked author or newly private policy cancels now-ineligible work.</li></ul></div> |
| 193 | - <div><h3>Discovery and network boundaries</h3><ul><li>Verify IDs and signatures on list events; resolve replaceable-event ordering deterministically. A list refresh changes future routes and revalidates pending destinations. Send the author’s verified kind-10002 list alongside public distribution as NIP-65 recommends.</li><li>Track inbox progress per source and filter; NIP-01 time-window pulls cannot prove a complete history. Surface partial backfills; late/backdated events require reconciliation.</li><li>Block loopback, link-local, metadata and private-network egress by default, including resolved IPs on every dial. Operators may explicitly allow LAN relays. Normalize aliases to prevent self-delivery loops.</li><li>If an upstream requires owner AUTH, report an authentication requirement and use a scoped external signer only when explicitly configured. A relay’s service key is a different identity and cannot impersonate its owner.</li></ul></div> |
| 194 | - </div> |
| 195 | - <details><summary>Job ownership and inbox checkpoint rules</summary><div class="detail-body"><p>Unique delivery keys prevent duplicate queue rows. Workers claim due rows using an atomic conditional update that records a reservation token and expiry; long tasks renew ownership. Completion updates must match the token, fencing stale workers from overwriting newer state. Cancel expired work before reclaiming it locally. Remote relays do not support this fencing token, so remote duplicate sends remain possible and are part of the at-least-once contract.</p><p>One job owns each tenant/source/filter checkpoint. Commit ingested event batches and their replay checkpoint together where the source exposes a usable cursor. For time-window Nostr queries, advance the completed window only after all returned events are committed and EOSE is received; otherwise replay that window with overlap and ID deduplication. Never derive a completed checkpoint from the greatest timestamp seen so far.</p></div></details> |
| 196 | - <p class="fine" style="margin-top:18px">Remote availability and client discovery cannot be guaranteed. Local workers can use already verified lists or configured targets immediately after setup; new endpoints become discoverable to other clients only after the owner signs and publishes updated lists. Public inbox collection is limited to selected sources. Following an owner’s contact list to build a feed is a separate optional mirror job.</p> |
| 197 | - <p class="source">Prior behavior: <a href="https://github.com/barrydeen/haven">HAVEN</a> combines role-specific relays, inbox gathering and outbox broadcasting. Bindws already implements <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/docs/04-data-and-names.md">NIP-65 delivery and per-target push cursors</a>; its <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/src/push.ts">NIP-9a callbacks</a> are a distinct delivery service required for parity, with their own registration, authorization, retry and revocation behavior.</p> |
| 198 | -</section> |
| 199 | - |
| 200 | -<section id="storage"> |
| 201 | - <div class="section-head"><span class="section-num">05</span><div><h2>Durability is part of the fast path.</h2><p>Start with SQLite because events, policy changes and delivery intent can share one transaction. Choose a different engine only when measured costs justify a new consistency model.</p></div></div> |
| 202 | - <div class="two"> |
| 203 | - <div><pre><code>/var/lib/tinyrelay/ |
| 204 | - catalog.sqlite |
| 205 | - tenants/ |
| 206 | - <stable-id>/ |
| 207 | - relay.sqlite |
| 208 | - relay.sqlite-wal |
| 209 | - blobs/ # Blossom, NIP-96 and verified site assets |
| 210 | - repositories/ # GRASP Git data; layout behind adapter |
| 211 | - backups/ |
| 212 | - secrets/ # relay keys; restrictive permissions |
| 213 | - |
| 214 | -Tenant tables |
| 215 | - events, tags, replacement_keys, tombstones |
| 216 | - policy_revisions, members, bans, audit |
| 217 | - routing_intents, deliveries, sync_cursors, jobs</code></pre><p class="source">Use stable generated IDs for directories; relay names are catalog aliases. Event IDs deduplicate within a tenant, never globally across private tenants.</p></div> |
| 218 | - <div><h3>One transaction, one truthful acknowledgement.</h3><p>Validate the event’s canonical ID and signature, apply authorization and content rules, then begin a short write transaction. Recheck the policy revision. Atomically apply replacement/deletion semantics, indexes and required delivery intent; commit before sending a successful stored-event OK.</p><p>Use WAL, one write connection per tenant, short read transactions and a small read pool. Set durability pragmas explicitly on every applicable connection. Start with <code>synchronous=FULL</code>; make a weaker durability mode an explicit later choice, never a silent throughput tweak.</p><p>SQLite WAL supports concurrent readers with one writer per database. Long readers can delay checkpoints. A local filesystem is required for this layout; a shared NFS volume is not a multi-node scaling mechanism.</p><p class="source"><a href="https://www.sqlite.org/wal.html">SQLite WAL</a> | <a href="https://www.sqlite.org/pragma.html#pragma_synchronous">synchronous durability</a></p></div> |
| 219 | - </div> |
| 220 | - <details open><summary>Correctness invariants to test before optimization</summary><div class="detail-body"><div class="two"><ul><li>Every ingest source uses the same validation/storage service; internal imports carry an explicit authority and origin, not a blanket policy bypass.</li><li>Replaceable and addressable events obey timestamp and ID tie-breaking. Authorized deletion, deletion-before-arrival tombstones, expiry and index cleanup are transactional.</li><li>Ephemeral events are never persisted and have a distinct acceptance contract. Duplicates do not create extra routing intent.</li><li>Reads, COUNT, search, sync and live fan-out enforce the same visibility policy; private data cannot leak through counts or auxiliary paths.</li></ul><ul><li>Register a subscription against a commit sequence barrier; read its bounded snapshot, then drain buffered later commits. Deduplicate overlaps and send EOSE after the historical snapshot.</li><li>Policy tightening invalidates affected subscriptions. Slow readers cannot pin a SQL cursor while waiting on a network socket; page or materialize bounded batches.</li><li>Crash before commit yields no successful OK; crash after commit preserves the event and intent. Local live delivery is best effort; reconnect queries recover durable events.</li><li>Disk-full or I/O failure rejects the write before OK. Stop accepting affected writes and report storage health; never report durable success from an in-memory queue.</li></ul></div></div></details> |
| 221 | - <details><summary>Query strategy and the storage-adapter decision</summary><div class="detail-body"><p>Begin with indexes for ID, author/time, kind/time, author/kind/time and tag name/value/time. Order by creation time and ID, limit each filter as the protocol requires, deduplicate unions, and use keyset iteration for jobs. Record query plans and rows visited in developer benchmarks. Add FTS only with the search milestone.</p><p>The proposed SQLite store must own atomic event + intent writes. Khatru’s storage hooks can delegate to that service; do not enqueue from an after-save callback. Eventstore is useful as an adapter/reference, but its event-only interface does not establish a transaction spanning a separate job database. A split event engine would require a durable change journal and recovery protocol.</p><p>The first implementation spike must prove atomic replacement/deletion, truthful query errors, the historical/live handoff, per-event private visibility and runtime shutdown through Khatru. If hooks cannot preserve these invariants, keep the SDK and implement the small missing protocol/lifecycle adapter directly.</p></div></details> |
| 222 | - <details><summary>Backup, restore and tenant isolation</summary><div class="detail-body"><p>Use SQLite’s online backup API or a coordinated quiescent snapshot, never copy a live main database file alone. A manifest records schema, tenant IDs, config revisions, event counts, checksums and key references. Encrypt backups containing signing keys. Optional blob storage needs a manifest-consistent snapshot with garbage collection paused for the backup.</p><p>Restore into a new directory, validate hashes/schema, run integrity and protocol checks, and default restored outbound workers to paused so a rehearsal cannot rebroadcast. Catalog/tenant backups reconcile by stable ID. Logical isolation is enforced in code and files; one process is not a security sandbox against malicious extensions or host compromise.</p><p class="source"><a href="https://www.sqlite.org/backup.html">SQLite backup API</a>. Proposed reliability target: preserve every successfully acknowledged stored event across process restart and, with FULL sync and a correctly functioning storage stack, power loss; a single host is still a hardware failure domain.</p></div></details> |
| 223 | -</section> |
| 224 | - |
| 225 | -<section id="o11y"> |
| 226 | - <div class="section-head"><span class="section-num">06</span><div><h2>Observability is the first subsystem.</h2><p>Define the measurement vocabulary alongside the domain model. Instrument the first vertical slice and every subsequent service as it is added. Instrumentation is required; external telemetry export is optional.</p></div></div> |
| 227 | - <div class="three"> |
| 228 | - <div class="block"><div class="micro">Always available</div><h3 style="margin-top:10px">Metrics & logs</h3><p>Prometheus text exposition on the private diagnostics listener; structured <code>slog</code> records to stderr. Use one metric registry. The relay works normally without a scraper.</p></div> |
| 229 | - <div class="block"><div class="micro">Optional export</div><h3 style="margin-top:10px">OpenTelemetry traces</h3><p>Instrument with the OTel API and enable the SDK/OTLP exporter when configured. Short spans per operation, plus links across durable jobs; do not keep one span open for a socket’s lifetime.</p></div> |
| 230 | - <div class="block"><div class="micro">On demand</div><h3 style="margin-top:10px">Go diagnostics</h3><p>CPU, heap, goroutine, block and mutex profiles; short runtime traces for scheduling and blocking. Bind pprof to loopback or an authenticated administrative listener.</p></div> |
| 231 | - </div> |
| 232 | - <p class="source"><a href="https://opentelemetry.io/docs/languages/go/instrumentation/">OTel Go instrumentation</a> | <a href="https://opentelemetry.io/docs/languages/go/exporters/">exporters</a> | <a href="https://go.dev/doc/diagnostics">Go diagnostics</a> | <a href="https://pkg.go.dev/net/http/pprof">pprof</a></p> |
| 233 | - <div class="flow"><span>ingress</span><b>→</b><span>validate / policy</span><b>→</b><span>commit</span><b>→</b><span>fan-out</span><b>→</b><span>delivery attempt</span></div> |
| 234 | - <div class="table-wrap"><table class="stack"><thead><tr><th>Boundary</th><th>Signals to ship first</th><th>Question answered</th></tr></thead><tbody> |
| 235 | - <tr><td>Acceptance</td><td class="metric">relay_ingest_total{outcome}<br>relay_ingest_duration_seconds<br>relay_validation_duration_seconds</td><td>Is time spent validating, waiting for a writer or committing? Are rejections policy decisions or failures?</td></tr> |
| 236 | - <tr><td>Connections & queries</td><td class="metric">relay_connections_active<br>relay_subscriptions_active<br>relay_query_duration_seconds<br>relay_eose_duration_seconds</td><td>How much state does an idle connection cost? Which query shapes delay historical replay?</td></tr> |
| 237 | - <tr><td>Fan-out</td><td class="metric">relay_fanout_duration_seconds<br>relay_socket_queue_bytes<br>relay_slow_consumer_disconnects_total</td><td>Is subscriber matching expensive, or are downstream sockets slow?</td></tr> |
| 238 | - <tr><td>Delivery & gathering</td><td class="metric">relay_delivery_pending<br>relay_delivery_oldest_age_seconds<br>relay_delivery_attempts_total{outcome}<br>relay_sync_lag_seconds</td><td>Is an upstream unavailable, discovery missing, or the local scheduler saturated?</td></tr> |
| 239 | - <tr><td>Storage</td><td class="metric">relay_write_queue_wait_seconds<br>relay_commit_duration_seconds<br>relay_storage_busy_total<br>relay_wal_bytes / relay_checkpoint_duration_seconds</td><td>Do write serialization, fsync or long readers explain the tail?</td></tr> |
| 240 | - <tr><td>Lifecycle & runtime</td><td class="metric">relay_tenants_active / relay_tenants_registered<br>relay_activation_duration_seconds<br>Go/process collectors; disk free bytes; job recovery failures</td><td>Are idle tenants retaining handles or goroutines? Can the process drain, recover and keep headroom?</td></tr> |
| 241 | - <tr><td>Files, sites & Git</td><td class="metric">relay_blob_operations_total{operation,outcome}<br>relay_blob_io_bytes_total{direction}<br>relay_site_verify_duration_seconds<br>relay_git_operation_duration_seconds{operation,outcome}</td><td>Is hashing, pack processing, file I/O or publication delaying an upload, site request or Git operation? Do large transfers affect relay traffic?</td></tr> |
| 242 | - <tr><td>Derived records & management</td><td class="metric">relay_job_duration_seconds{job_class,outcome}<br>relay_job_oldest_due_age_seconds{job_class}<br>relay_management_duration_seconds{action_class,outcome}</td><td>Are views, callbacks, notifications, succession and owner actions making progress? Keep job/action classes finite and exclude payloads and identities.</td></tr> |
| 243 | - </tbody></table></div> |
| 244 | - <p class="fine">Names are proposed. All durations are histograms in seconds. Define p50/p95/p99 panels and buckets around agreed workloads. Export aggregate depths and maximum ages; do not create a time series per socket or tenant.</p> |
| 245 | - <div class="two" style="margin-top:25px"> |
| 246 | - <div><h3>Useful telemetry with bounded cardinality.</h3><p>Labels come from a fixed vocabulary: operation, transport, event class and outcome. No event IDs, pubkeys, tenant names, subscription IDs, arbitrary kinds, URLs or full filter text in metric labels. Hashing those values does not reduce cardinality.</p><p>Provide authenticated, on-demand tenant diagnostics for detailed state. Exclude event bodies, ciphertext, keys, tokens, full URLs and identifiers from default traces/logs. Management audit records have a separate access and retention policy.</p><p class="source"><a href="https://prometheus.io/docs/practices/naming/">Prometheus naming and label guidance</a></p></div> |
| 247 | - <div><h3>Sampling and overhead are design choices.</h3><p>Start with low-rate head sampling for traces. Record errors as counters/logs regardless of trace sampling; head sampling cannot promise to retain every slow/error trace. An optional collector with tail sampling can retain those traces when configured to receive them.</p><p>Exporter failure never blocks acceptance. Bound export queues, count dropped telemetry, and compare instrumentation on/off in the benchmark harness. Use low-rate diagnostic sampling and opt-in mutex/block profiling.</p><p class="source"><a href="https://opentelemetry.io/docs/concepts/sampling/">OTel sampling</a> | <a href="https://prometheus.io/docs/practices/histograms/">histogram guidance</a></p></div> |
| 248 | - </div> |
| 249 | - <div class="note"><strong>First operational view:</strong> traffic and outcomes; accept/query latency; fan-out pressure; delivery age and retries; SQLite/WAL/checkpoints; CPU, RSS and goroutines. Alert on sustained inability to make progress, disk headroom and recovery failure. Keep tenant debugging in the admin view.</div> |
| 250 | -</section> |
| 251 | - |
| 252 | -<section id="performance"> |
| 253 | - <div class="section-head"><span class="section-num">07</span><div><h2>Optimize the bottleneck you can show.</h2><p>Go and local disk enable a different runtime. Capacity claims require a defined workload and host. The linked Git report records measured repository transfers and relay responsiveness; broader capacity targets remain to be measured.</p></div></div> |
| 254 | - <div class="pipeline"><div><strong>Establish correctness</strong><p>Protocol conformance, tenant isolation, crash recovery, duplicate delivery and historical/live handoff.</p></div><div><strong>Record a baseline</strong><p>Fixed hardware, seed, dataset, versions and durability mode. Capture latency, throughput, memory and errors.</p></div><div><strong>Explain the cost</strong><p>Metrics locate the boundary; a sampled trace isolates the operation; a profile explains CPU, allocation or blocking.</p></div><div><strong>Change one thing</strong><p>Repeat the same workload. Keep gains only when correctness and tail latency remain acceptable.</p></div></div> |
| 255 | - <div class="table-wrap"><table class="stack"><thead><tr><th>Workload</th><th>Measure</th><th>Only then consider</th></tr></thead><tbody> |
| 256 | - <tr><td>Public publish / duplicates</td><td>ACK p99, signature CPU, commits/sec, allocs/event, fsync cost.</td><td>Decode once, reuse immutable encoded bytes, prepared statements, batch commits with a bounded delay.</td></tr> |
| 257 | - <tr><td>History + new subscriptions</td><td>REQ→EOSE p99, rows visited/returned, reader age and WAL growth.</td><td>Composite indexes, query-shape planning and bounded batch reads.</td></tr> |
| 258 | - <tr><td>One event → many subscribers</td><td>Match time, write-queue age, slow consumers, per-connection memory.</td><td>Index subscriptions by kind/author/tag; share encoded event bytes. Preserve per-subscription IDs and filters.</td></tr> |
| 259 | - <tr><td>Many mostly idle tenants</td><td>RSS per active tenant, open file descriptors, activation latency, idle goroutines.</td><td>Handle eviction, fewer background loops, cache sizing and lazy index warm-up.</td></tr> |
| 260 | - <tr><td>Outages and backfill</td><td>Backlog age, retry rate, sockets, fairness, live traffic latency during imports.</td><td>Connection reuse, destination backoff and separate priority for live ingest versus backfill.</td></tr> |
| 261 | - <tr><td>Crash / disk pressure / churn</td><td>Recovery duration, acknowledged-event loss, queue replay, memory after reconnect storms.</td><td>Shorter recovery scans and persisted due indexes; never weaker acknowledgement semantics.</td></tr> |
| 262 | - <tr><td>Files / sites / Git under relay load</td><td>Transfer throughput, peak RSS, hash/pack CPU, request tails, object/ref publication and backup duration.</td><td>Streaming, chunked I/O, checkpoint coordination and fair scheduling. Re-run upload, site verification and Git recovery cases after each change.</td></tr> |
| 263 | - <tr><td>Views / callbacks / management</td><td>Schedule lateness, signing cost, callback retries, form/API response time and concurrent job interference.</td><td>Incremental computation, debouncing and worker-class fairness while preserving triggers, visibility and task feedback.</td></tr> |
| 264 | - </tbody></table></div> |
| 265 | - <div class="two" style="margin-top:26px"> |
| 266 | - <div><h3>Reproducible load, not a marketing score.</h3><p>Report Go/module/SQLite versions, CPU count, OS, disk, sync settings, dataset size, event-size distribution, filter shapes, tenant skew and cache state. Sweep load and record the latency/error curve; avoid a load generator that hides queuing by slowing down with the server.</p><p>Archive seeds, raw results, profiles and configuration with each comparison. Use Go benchmarks and benchstat for hot-path comparisons, and a WebSocket load harness for whole-system behavior.</p><p class="source"><a href="https://pkg.go.dev/testing">Go testing benchmarks</a> | <a href="https://pkg.go.dev/golang.org/x/perf/cmd/benchstat">benchstat</a> | <a href="https://pkg.go.dev/runtime/trace">runtime tracing</a></p></div> |
| 267 | - <div><h3>Acceptance gates before capacity targets.</h3><p>Release gates: no cross-tenant/private visibility leak; no loss of acknowledged stored events in restart tests; no orphaned routing intent; duplicate retries converge; backlog drains when a healthy target recovers; RSS/goroutines stabilize after churn.</p><p>Choose numeric service objectives after the first representative baseline on the intended host. Gate a performance change on repeatable improvement in its target metric and no material regression in p99, memory, errors or durability. Run race tests separately from timing benchmarks.</p></div> |
| 268 | - </div> |
| 269 | -</section> |
| 270 | - |
| 271 | -<section id="reuse"> |
| 272 | - <div class="section-head"><span class="section-num">08</span><div><h2>Borrow proven pieces. Keep the seam small.</h2><p>Current package paths and licenses matter. The current Nostr toolkit is not simply the old go-nostr + khatru repository layout.</p></div></div> |
| 273 | - <div class="table-wrap"><table class="stack"><thead><tr><th>Candidate</th><th>What it contributes</th><th>Recommendation</th></tr></thead><tbody> |
| 274 | - <tr><td><a href="https://pkg.go.dev/fiatjaf.com/nostr">fiatjaf.com/nostr</a><br><span class="tag">Unlicense</span></td><td>Current event types, crypto, filters, relay/client utilities and NIP packages.</td><td>Preferred protocol toolkit. Pin an exact tested module revision; do not mix old and new event types across the application.</td></tr> |
| 275 | - <tr><td><a href="https://pkg.go.dev/fiatjaf.com/nostr/khatru">…/khatru</a><br><span class="tag">Unlicense</span></td><td>Custom relay hooks for auth, storage, query, broadcast and management.</td><td>Preferred transport candidate, subject to the storage/lifecycle spike. Audit inherited max-query/message defaults against the no-product-quota model.</td></tr> |
| 276 | - <tr><td><a href="https://pkg.go.dev/fiatjaf.com/nostr/eventstore">…/eventstore</a><br><span class="tag">Unlicense</span></td><td>Iterator-based store abstraction and embedded backends including LMDB, BoltDB and MMM.</td><td>Reference or adapter; not the transaction contract for event + delivery intent. Build a focused SQLite ingest store initially.</td></tr> |
| 277 | - <tr><td><a href="https://pkg.go.dev/github.com/nbd-wtf/go-nostr">nbd-wtf/go-nostr</a><br><span class="tag">MIT</span></td><td>Established Go SDK and ecosystem examples.</td><td>Compatibility reference. Older examples and indexed documentation may target this API; prefer a single current module family for new code.</td></tr> |
| 278 | - <tr><td><a href="https://pkg.go.dev/modernc.org/sqlite">modernc.org/sqlite</a><br><span class="tag">BSD-3-Clause</span></td><td>SQLite without a cgo requirement; compatible with database/sql.</td><td>Baseline choice for simpler builds. Verify embedded SQLite version, FTS/backup needs and WAL fixes in the pinned build.</td></tr> |
| 279 | - <tr><td><a href="https://github.com/mattn/go-sqlite3">mattn/go-sqlite3</a><br><span class="tag">MIT</span></td><td>Cgo SQLite driver; requires a C compiler at build time.</td><td>Benchmark alternative if storage profiles justify it. Cross-compilation and packaging are part of the tradeoff; no presumed speed ranking.</td></tr> |
| 280 | - <tr><td><a href="https://github.com/barrydeen/haven">HAVEN</a><br><span class="tag">MIT</span></td><td>Closest behavioral prior art: role-based personal relays, gathering and broadcasting. README describes maintenance as bug-fix-only.</td><td>Borrow UX, policy ideas and test scenarios. Generalize role instances into templates for arbitrary owners; avoid a dependency on the entire daemon.</td></tr> |
| 281 | - <tr><td><a href="https://github.com/hoytech/strfry">strfry</a><br><span class="tag drop">GPLv3</span></td><td>C++/LMDB relay; durable ACK behavior, specialized indexing, streaming and sync.</td><td>Operational/performance comparison and architectural prior art. Do not copy implementation into an intended permissive Go codebase.</td></tr> |
| 282 | - <tr><td><a href="https://github.com/scsibug/nostr-rs-relay">nostr-rs-relay</a><br><span class="tag">MIT</span></td><td>Rust relay using SQLite; PostgreSQL is described as experimental.</td><td>Study SQL and operational/conformance behavior. Reuse ideas or clearly attributed compatible portions only where they help.</td></tr> |
| 283 | - </tbody></table></div> |
| 284 | - <div class="note"><strong>Current upstream, 07 Sep 2026.</strong> Khatru has moved to the ngit/Nostr-hosted <a href="https://basspistol.org/npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6/nostrlib.git">nostrlib repository</a>, with source browsing through <a href="https://viewsource.win/npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6/nostrlib/_/master/~/khatru">viewsource</a> and the module path <code>fiatjaf.com/nostr/khatru</code>. The archived GitHub repository is the legacy location. Use the current upstream and pin revisions for reproducible builds, as with other dependencies. Verify each dependency’s license and retain the required notices.</div> |
| 285 | -</section> |
| 286 | - |
| 287 | -<section id="operations"> |
| 288 | - <div class="section-head"><span class="section-num">09</span><div><h2>A service an operator can understand.</h2><p>The executable is <code>tiny</code>: run the daemon with <code>tiny serve</code> and use the same command for management. Ship plain HTML management pages, a systemd example and an optional container image. The default deployment is one host and one data directory.</p></div></div> |
| 289 | - <div class="note"><strong>Preserve UX through simpler presentation.</strong> Every retained bindws task must be achievable from the web interface with equivalent authorization, inputs, preview, progress, outcome and recovery. Rebuild with basic HTML and forms; no bindws branding, CSS or generated console bundle carries over. Simplifying markup does not permit removing a workflow or forcing an existing web task into the CLI. This document’s presentation is not the product design.</div> |
| 290 | - <div class="two" style="margin-bottom:27px"> |
| 291 | - <div><h3>All workflows, ordinary pages</h3><p>Keep the full People, Moderation, Rules, Identity, Connect, Data, Sync, Views, Health and Owner workflow families. Include files, sites, Git, invitations, list recovery, ownership/succession and callback management. Navigation may become links and forms rather than tabs; role-specific actions remain discoverable.</p><p>Preserve public event/article pages, feeds, cards, QR handoff, connection shortcuts and sign-in. Use GET for reads and POST for mutations, followed by clear results. Render plan/preview and validation on the server. Long-running work keeps progress and retry/cancel actions; small scripts or polling can preserve time-sensitive feedback where plain refresh would worsen the task.</p></div> |
| 292 | - <div><h3>Keep extension and remote signing</h3><p>Rebuild the NIP-07/NIP-46 helper so existing extension, phone pairing and remote-signer flows remain supported, including their relay transport and discovery behavior. Never ask users to paste a private key. Any browser session bridge uses an origin-bound, expiring one-time challenge, Secure/HttpOnly/SameSite cookies, CSRF protection and per-action authorization.</p><p>Preserve the signed NIP-98/NIP-86 contracts for scripts and clients, with documented exceptions for removed fuel/lease operations. Keep signer scripts isolated from page rendering. The CLI is an additional complete interface; it is not a substitute for missing web functionality.</p></div> |
| 293 | - </div> |
| 294 | - <div class="two"> |
| 295 | - <div class="block"><h3>Serving and access</h3><ul><li>TLS proxy fronts a loopback-bound HTTP/WS listener. Use explicit hosts or wildcard DNS/certificates, or a single domain with relay paths.</li><li>Trust forwarding headers only from configured proxies. Match canonical host/path against the registry; validate custom-domain ownership before alias activation.</li><li>Host admin uses a local socket or separately authenticated control endpoint. Relay owners manage only their tenant via signed NIP-98/NIP-86 operations.</li><li>Diagnostics are private. Tenant signing keys are distinct from owner keys and stored outside exported template files.</li></ul></div> |
| 296 | - <div class="block"><h3>Restarts, backups and capacity</h3><ul><li>On shutdown, stop admission, drain accepted writes, cancel/join workers, close sockets and release databases. Clients reconnect and reissue subscriptions.</li><li>On startup, check schema versions, reconcile incomplete creation and recover expired task reservations. Persist due times; the scheduler’s heap is only a cache.</li><li>Expose liveness separately from readiness and per-tenant storage health. A remote relay outage does not make local ingest unready.</li><li>A single host does not provide automatic failover. Scale vertically first; multi-host placement is a separate topology choice after application parity. Never run two owners over the same tenant files.</li></ul></div> |
| 297 | - </div> |
| 298 | - <div class="note"><strong>Task reservations are internal crash-recovery state.</strong> They are short worker ownership records, unrelated to relay lifetime. User relays remain owned and persistent; there is no leased-relay feature anywhere in the runtime model.</div> |
| 299 | - <h3 style="margin-top:28px">Move every service to a local implementation.</h3> |
| 300 | - <p>Blossom/NIP-96 use local content-addressed files, streaming uploads and atomic publication. Sites retain manifest verification, mirroring, authenticated access and isolated origins. GRASP retains its signed authorization and event/object publication boundary while its storage adapter may use native Git. Marmot, views, succession, owner notifications and callbacks run through durable local jobs. All are required implementation work, even where a tenant can disable them.</p> |
| 301 | - <div class="table-wrap"><table class="stack"><thead><tr><th>Worker mechanism</th><th>Local replacement</th><th>UX contract to preserve</th></tr></thead><tbody> |
| 302 | - <tr><td>Named Durable Object + RPC</td><td>Catalog, single-flight tenant runtime, typed in-process services and tenant transactions.</td><td>A named relay behaves independently; create, configure and fork remain direct actions. Fork assigns a permanent owner without a lease.</td></tr> |
| 303 | - <tr><td>Hibernating sockets / attachments</td><td>Owned connection state, cancellation and reconnect recovery in Go.</td><td>AUTH, private visibility, subscriptions, signer transport and sync remain interoperable; process restarts recover stored work.</td></tr> |
| 304 | - <tr><td>Alarm + waitUntil</td><td>Durable scheduler, due records, reservation tokens and joined workers.</td><td>Imports, mirrors, backups, delivery, expiry, views and succession continue without a connected browser; progress and failures remain visible.</td></tr> |
| 305 | - <tr><td>R2 blobs / multipart objects</td><td>Streaming local file writes, verified hashes, atomic rename and durable metadata; optional S3 adapter.</td><td>The same upload, mirror, download, report, delete and backup tasks; no provider-shaped file-size ceiling.</td></tr> |
| 306 | - <tr><td>KV + Cloudflare domain provisioning</td><td>Transactional alias map, local resolver, domain verification and TLS-proxy integration with status.</td><td>Owner can Add, Check and Remove relay/site domains from the web interface and signed API. Show the DNS action and readiness; do not replace this with a manual operator-only map.</td></tr> |
| 307 | - <tr><td>SQLite Git chunks / admission token</td><td>Repository service with per-repository ownership, staged objects, ref transactions and a recovery journal spanning Git and SQL.</td><td>Smart HTTP, signed state admission, inventory and backup behavior persist. Never expose an accepted event before required objects are durable, or lose ref/event consistency on restart.</td></tr> |
| 308 | - <tr><td>Generated console + edge pages</td><td>Go HTML templates, standard forms, server-side previews and narrowly scoped signer/progress helpers.</td><td>Every role’s tasks, sharing and app handoff remain available; only visual design and the agreed removed concepts change.</td></tr> |
| 309 | - <tr><td>Provider meters / traces</td><td>Local operational counters, audit history, Prometheus, OTel and Go diagnostics.</td><td>Keep health, usage visibility and job diagnostics while removing pricing/fuel. Expand evidence for local performance work.</td></tr> |
| 310 | - </tbody></table></div> |
| 311 | - <p class="fine">The durable data directory and its backup policy are the operator’s responsibility. “Unlimited” describes removal of platform entitlements, not infinite hardware. Fairness and backpressure protect useful service without inventing another fuel system. Local deployment can remain a single daemon while delivering the complete application; matching Cloudflare’s global infrastructure is distinct from application and UX parity.</p> |
| 312 | -</section> |
| 313 | - |
| 314 | -<section id="delivery-plan"> |
| 315 | - <div class="section-head"><span class="section-num">10</span><div><h2>Continue until every parity requirement is met.</h2><p>Full functional and UX parity is the finish line. The phases order the engineering work; they are not alternative stopping points or permission to leave features for an unspecified future.</p></div></div> |
| 316 | - <div class="note"><strong>A core-only build is an intermediate checkpoint.</strong> Completion requires all retained protocol/API behavior, all existing template and user workflows, complete data migration and local recovery. A feature being disabled by default does not remove it from scope. Keep working through the matrix until every required row is verified.</div> |
| 317 | - <h3>Source-to-target parity matrix</h3> |
| 318 | - <p class="fine">Seed the implementation ledger from bindws revision 5d31267, then enumerate individual cases from the source registries and tests. Each ledger entry records source route/method/UI action, input and role, observable behavior, local implementation, test evidence and status. Allowed statuses are planned, in progress, verified or an explicitly documented intentional change. Unmapped or unverified retained behavior blocks completion.</p> |
| 319 | - <div class="table-wrap"><table class="stack"><thead><tr><th>Required family</th><th>Behavior & UX to preserve</th><th>Evidence of completion</th></tr></thead><tbody> |
| 320 | - <tr><td>Relay protocol & queries</td><td>EVENT/REQ/CLOSE/AUTH, HTTP event/query/count bridge, filters, EOSE, replaceables/addressables, deletion, vanish, expiry, protected events, search modes, COUNT/HLL and negentropy. Preserve policy changes on existing subscriptions.</td><td>Port all corresponding black-box cases; compare normal, error and privacy behavior on both backends. Run reconnect, concurrent query/write and restart tests.</td></tr> |
| 321 | - <tr><td>Ownership, auth & discovery</td><td>NIP-42/98, extension and remote signer flows, relay identity, NIP-05 names, NIP-11 capability data, NIP-66 records and web-address discovery. Transfer and succession retain their signed-record effects.</td><td>Existing client sign-in and discovery journeys succeed against local URLs. Identity preservation or an explicitly accepted rotation is verified through restore and transfer.</td></tr> |
| 322 | - <tr><td>People, membership & groups</td><td>Members, names/notes, guest kinds and one-hop replies, member/follow write rules, join terms, invite trees and membership claim codes, join/leave, directory privacy, NIP-29/43 records and role hierarchy.</td><td>Owner, moderator, member and guest journey matrix; tests for tree removal, invite revocation, signed roster/group updates and immediate read revocation. Relay trials remain removed.</td></tr> |
| 323 | - <tr><td>Moderation</td><td>Reports, hide-after-report thresholds, dismiss/delete/ban, ban-and-erase including files, blocked words/regex/tag matching, IP blocks, pins, recent-event search and paginated audit history.</td><td>Hidden data is absent from queries, counts, sync, pages and delivery. Owner recovery from a self-IP-block stays available. Do not silently replace supported regex semantics with a narrower engine.</td></tr> |
| 324 | - <tr><td>Templates & configuration</td><td>All 15 relay templates, every connection template, source/replica behavior, owner-list exceptions, plan/apply/export, partial-section semantics, feature defaults and visibility placeholders.</td><td>Each template has an end-to-end case; config round-trips and diffs preserve behavior. Reapplying a replica template replaces its job rather than duplicating it.</td></tr> |
| 325 | - <tr><td>Lists, sharing & app handoff</td><td>List history and restore/re-sign, Check/Add/Remove relay-list actions, kinds 10002/10050/10007/10063, app shortcut editing/order/visibility, copyable URLs/commands, QR codes and cards.</td><td>Owner can recover a list and complete phone/app handoff from plain HTML pages. Private shortcuts and public visibility follow the existing authorization model.</td></tr> |
| 326 | - <tr><td>Data, jobs & forks</td><td>Data-by-kind and retention, purge exceptions, JSONL/JSON import, progress and skip reasons, pull/mirror/history/rebroadcast, run-now/remove, per-target cursors, fork and copy.</td><td>Work resumes after restart; failed targets never advance successful-target state incorrectly. Fork creates a permanent owner-assigned relay and preserves the intended data/membership copy workflow.</td></tr> |
| 327 | - <tr><td>Automatic delivery & callbacks</td><td>NIP-65 public distribution, NIP-9a registration/revocation and callback privacy, per-target status, retries, deduplication, owner notifications and notification test/preferences.</td><td>Network-failure and policy-change tests; public/private routing separation; no lost intent after local OK. Automatic inbox/outbox improvements also pass their additional requirements.</td></tr> |
| 328 | - <tr><td>Files</td><td>Blossom BUDs, NIP-96/NIP-94 descriptors, upload/preflight/list/read/delete/mirror, signed tokens, reports and banned hashes, file access under relay policy.</td><td>Existing client and web upload/download/report flows work with local disk; interrupted upload/mirror leaves no falsely published descriptor; backup/restore retains blobs and authorization.</td></tr> |
| 329 | - <tr><td>Pages & static sites</td><td>Notes/articles, feed and link metadata; NIP-5A root/named/snapshot manifests, path rules, verified cache fill, mirrors, sign-in, custom domains and origin isolation.</td><td>Browse all page/site modes; verify hash/path failure behavior, private site access and deletion/expiry cleanup. Site origins cannot access relay management or event doors.</td></tr> |
| 330 | - <tr><td>Git / GRASP</td><td>NIP-34 authorization, Smart HTTP, signed repository state, maintainers and refs, currently implemented GRASP profiles and preview paths, inventory, synchronization and backup/recovery.</td><td>Push/clone/fetch/state-update workflows, admission failures and crash tests across object/ref/event publication. Preserve each profile’s documented maturity and dependency gates.</td></tr> |
| 331 | - <tr><td>Marmot</td><td>KeyPackages, opaque MLS envelopes, public/member templates, authenticated account admission, gift-wrap recipient checks, feature and retention behavior.</td><td>Both Marmot templates pass client transport and unauthorized-access cases across restart; the server never needs client encryption keys.</td></tr> |
| 332 | - <tr><td>Views, presence & succession</td><td>All registered views, audience-aware storage/on-demand signing, dirty/debounced/hourly/daily triggers, fingerprints, run history, presence, heir configuration, warnings, cancellation and transfer.</td><td>Clock-controlled tests preserve schedules and signed outputs. Member-only derived data stays private. Owner activity cancels succession as before; the relay itself never expires.</td></tr> |
| 333 | - <tr><td>Domains & management surface</td><td>All retained ROUTES and METHODS, CORS/content negotiation, signed API schemas and error behavior. Full ten-family console workflow coverage, including domain Add/Check/Remove, site mapping and destructive-action previews.</td><td>Every retained registry entry has a local handler and tests. Browser journeys exercise all roles, validation/error states, pagination, progress and recovery without requiring the CLI.</td></tr> |
| 334 | - <tr><td>Backup, migration & operations</td><td>Dumps, portable backup list/create/download/delete, preview/restore, complete metadata and keys, files/sites/Git, status/health, audit and local startup/shutdown recovery.</td><td>A whole-feature relay restores into an empty local data directory and passes the same journeys. Final cutover reconciles all stores and queues; an unsupported data family blocks cutover.</td></tr> |
| 335 | - </tbody></table></div> |
| 336 | - <p class="source">Primary inventories: <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/src/routes.ts">ROUTES</a>, <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/src/manage.ts">METHODS</a>, <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/docs/01-relay-configuration.md">console workflows</a>, <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/docs/14-http-reference.md">HTTP reference</a>, <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/src/views.ts">view registry</a>, and both template catalogs. This family matrix organizes the work; individual source entries must still be enumerated so no method is lost inside a broad category.</p> |
| 337 | - <details><summary>Protocol baseline and treatment of previews</summary><div class="detail-body"><p>Inventory the source’s implemented NIP-01, 05, 09, 11, 13, 17, 29, 40, 42, 43, 45, 46 transport, 50, 56, 57, 59, 62, 66, 67, 70, 77, 86, 94, 96 and 98 behavior, together with Blossom, Marmot, NIP-5A, NIP-AD, NIP-9a and GRASP. Include helper behavior such as NIP-65 routing even when it is not represented by a supported-NIP flag.</p><p>Preserve currently implemented preview features and dependency rules at their existing level of support; do not promote them to standards-complete claims or exclude them because they are previews. Derive advertised capabilities from verified enabled implementations. Tests for fuel/lease endpoints become explicit removal tests; all other exclusions need a documented agreed reason. Reconcile source changes before declaring parity rather than silently using a stale checklist.</p></div></details> |
| 338 | - <h3 style="margin-top:30px">Implementation sequence — all phases are required</h3> |
| 339 | - <div class="roadmap"> |
| 340 | - <div class="phase-code">P0 / BASELINE</div><div class="phase"><h3>Enumerated parity contract and instrumented slice</h3><p>Inventory every source route, management method, feature flag, view, template, persisted entity and UI journey. Pin SDK/Khatru/SQLite revisions. Build the first ingest/query slice with metrics, spans and pprof; validate the transaction and lifecycle integration.</p><div class="gate">Checkpoint: every source item has a parity ledger entry; crash/OK and historical/live tests pass; a reproducible benchmark exists. The project continues through every remaining phase.</div></div> |
| 341 | - <div class="phase-code">P1 / CORE</div><div class="phase"><h3>Owned, templated multitenancy</h3><p>Add catalog, owner-at-creation, idempotent provisioning, stable IDs, host/path aliases, policy revisions and lazy runtime lifecycle. Implement plan/apply/export and plain HTML pages with basic forms; reuse no bindws UI assets. Add persistent jobs and backup/restore.</p><div class="gate">Exit: identical event IDs and concurrent requests remain isolated across tenants; restart and handle-eviction tests pass; no lease/trial/billing concepts in the runtime schema; form actions enforce authorization and CSRF protection.</div></div> |
| 342 | - <div class="phase-code">P2 / PERSONAL</div><div class="phase"><h3>Automatic public delivery and private receipt</h3><p>Ship outbox, targeted inbox and DM templates, plus the personal provisioning bundle. Add signed-list discovery, durable routing intent, independent target outcomes, inbox reconnect/backfill and external-signing setup.</p><div class="gate">Exit: failed targets do not stall healthy targets; restart cannot lose intent; policy changes stop forbidden sends; gift wraps are recipient-readable only; duplicate retries converge.</div></div> |
| 343 | - <div class="phase-code">P3 / COMMUNITY</div><div class="phase"><h3>Complete relay and community behavior</h3><p>Finish the protocol/query surface, search/COUNT/negentropy, guest and follow policies, invitation trees, groups, moderation, audit, list history/recovery and all associated web/API workflows. Port the source conformance cases as each behavior lands.</p><div class="gate">Checkpoint: relay/community entries are verified, including private visibility and role-specific browser journeys. Begin migration rehearsals early; this checkpoint does not authorize full cutover.</div></div> |
| 344 | - <div class="phase-code">P4 / HOSTING</div><div class="phase"><h3>Complete files, sites, Git and Marmot</h3><p>Implement all file protocols, verified site hosting/mirroring, custom-domain management, GRASP repository behavior and Marmot transport on local backends. Complete media/site/grasp/marmot/home templates and corresponding connection handoffs.</p><div class="gate">Checkpoint: every hosting family passes its API, browser/client and restart/recovery cases. Objects, refs, signed events and metadata remain consistent; no hosting feature is left as a stub.</div></div> |
| 345 | - <div class="phase-code">P5 / WORKFLOWS</div><div class="phase"><h3>Complete background services and UX coverage</h3><p>Finish callbacks, owner notifications, all views/presence, succession, transfer, forks, backup/restore and any remaining management paths. Check all ten console workflow families, all relay/connection templates, sharing/signers and long-running job feedback.</p><div class="gate">Checkpoint: no retained route, method, feature, template or user journey is unmapped or unimplemented. A plain-HTML interface completes the same tasks without CLI-only substitutions.</div></div> |
| 346 | - <div class="phase-code">P6 / PARITY</div><div class="phase"><h3>Verify full parity and complete local cutover</h3><p>Run the complete retained conformance and workflow matrix; reconcile intentional changes; rehearse migration and rollback with every data family enabled. Profile representative whole-product workloads and fix measured regressions while retaining correctness.</p><div class="gate">Completion: every required ledger entry is verified, full-feature restore/cutover passes, no unexplained data or UX gap remains, and the local deployment is documented. Until then, continue implementation.</div></div> |
| 347 | - </div> |
| 348 | - <details open><summary>Migration contract: preserve what exists, report every change</summary><div class="detail-body"><ol> |
| 349 | - <li><strong>Inventory and dry-run.</strong> Read bindws config exports, portable backup manifests, events, lists/history, memberships, bans, aliases, invites, audit, jobs/callbacks, blobs, site manifests and Git state. Produce mappings of every retained field and data family. Extend the exporter/reader wherever an event-only JSONL or existing portable backup cannot preserve required state.</li> |
| 350 | - <li><strong>Map ownership explicitly.</strong> Claimed relays map to the existing owner. Existing leased/unclaimed data requires an explicit owner assignment or stays in the migration report for manual handling. Do not create ownerless running tenants, trial timers or automatic purge rules.</li> |
| 351 | - <li><strong>Remove economics with a report.</strong> Fuel balances, payment configuration, trial state and hosted quota fields have no runtime destination. Retention remains content policy; show inherited 14/90-day rules and require deliberate selection of the intended final rule.</li> |
| 352 | - <li><strong>Import into a fresh directory.</strong> Verify signatures, retain original event IDs, preserve deletion/vanish state and latest replaceables, rebuild indexes, then compare counts and checksums. Translate policy vocabulary and template revisions. Preserve relay signing identity only through an explicit secure export. If unavailable, record the old/new key mapping, update relay metadata, and regenerate supported NIP-43/NIP-29 records under the new key; disclose the continuity break rather than re-signing historical events.</li> |
| 353 | - <li><strong>Quiesce and cut over after parity.</strong> Rehearse the complete feature set first. Missing support for an enabled feature or stored data family blocks final cutover and remains implementation work. Freeze source management and writes, export final state, reconcile every store, and switch proxy/DNS while preserving public URLs where possible. If URLs change, prepare owner-signed relay-list updates. Start destination workers after queue/origin reconciliation.</li> |
| 354 | - <li><strong>Retain a rollback artifact.</strong> Keep the source read-only and its complete backup. A switch back after new Go writes requires reconciling those writes first; DNS rollback alone is not data rollback. A source backup is recovery material, not a substitute for migrating files, sites, Git or any other retained feature.</li> |
| 355 | - </ol><p class="source">Existing test investment: <a href="https://github.com/FelineStateMachine/bindws/tree/5d31267/test/conformance">bindws black-box conformance suite</a>. Existing backup behavior: <a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/src/backups.ts">backups.ts</a>. Full parity requires the retained protocol tests and complete end-to-end workflow/data matrix; protocol advertising alone is insufficient.</p></div></details> |
| 356 | -</section> |
| 357 | - |
| 358 | -<section id="decisions"> |
| 359 | - <div class="section-head"><span class="section-num">11</span><div><h2>Decisions this plan makes.</h2><p>These defaults are concrete enough to implement. A few choices remain experiments, with evidence required to change direction.</p></div></div> |
| 360 | - <div class="decision-grid"> |
| 361 | - <div class="decision"><div class="micro">D01 / Adopt</div><h3>Single daemon, logical tenant isolation.</h3><p>Reduces deployment and coordination cost. Tradeoff: a process or host failure affects every resident relay. Strong process isolation or multiple hosts is a later deployment choice.</p></div> |
| 362 | - <div class="decision"><div class="micro">D02 / Adopt</div><h3>No fuel, trials or relay leases.</h3><p>Owner is assigned at creation; lifetime is indefinite. Remove economic and lease concepts entirely. Keep operator flow control separate from tenant content rules.</p></div> |
| 363 | - <div class="decision"><div class="micro">D03 / Start here</div><h3>SQLite per tenant, FULL sync.</h3><p>Enables event + intent atomicity and simple tenant backup/movement. Tradeoff: one writer per tenant, file-handle management and fsync cost. Compare modernc/cgo only under the same durability contract.</p></div> |
| 364 | - <div class="decision"><div class="micro">D04 / Prove in P0</div><h3>Khatru adapter + current Nostr toolkit.</h3><p>Reuse protocol work while preserving ownership of the domain. Keep the adapter contingent on lifecycle, transaction and visibility tests; do not inherit framework limits without inspection.</p></div> |
| 365 | - <div class="decision"><div class="micro">D05 / Adopt</div><h3>Declarative templates with immutable revisions.</h3><p>A template changes policy and services; it never runs tenant code. A personal bundle is a family of standard relay instances, each independently manageable.</p></div> |
| 366 | - <div class="decision"><div class="micro">D06 / Adopt</div><h3>Local acceptance; asynchronous distribution.</h3><p>Durable routing intent is committed with the event. Delivery is at least once and independently observable. Private DM routing remains separate from public fan-out.</p></div> |
| 367 | - <div class="decision"><div class="micro">D07 / Adopt</div><h3>Metrics now; optimization when measured.</h3><p>Ship a stable low-cardinality metric vocabulary, logs and optional tracing with the first slice. Numeric capacity targets follow baseline measurements on the intended host.</p></div> |
| 368 | - <div class="decision"><div class="micro">D08 / Required</div><h3>Full functional and UX parity.</h3><p>All retained bindws features, APIs, templates, data and user journeys are required. Blobs, sites, Git, Marmot, views, callbacks and succession are part of completion. Phases sequence this work; a subset does not satisfy the project.</p></div> |
| 369 | - <div class="decision"><div class="micro">D09 / Adopt</div><h3>Plain HTML with complete workflows.</h3><p>Carry over no bindws branding or styling. Preserve tasks, permissions, input/output behavior, signing, previews, progress, sharing and recovery through simple pages/forms and focused helpers. Keep all existing web tasks available on the web.</p></div> |
| 370 | - <div class="decision"><div class="micro">D10 / Separate concern</div><h3>Local application parity; optional clustering.</h3><p>Replace Worker mechanisms with local services and finish the complete application on one host. Multi-host placement or automatic failover can be separate deployment work; neither is a reason to omit application capabilities.</p></div> |
| 371 | - </div> |
| 372 | -</section> |
| 373 | - |
| 374 | -<section id="sources"> |
| 375 | - <div class="section-head"><span class="section-num">12</span><div><h2>Evidence behind the proposal.</h2><p>Repository observations are separated from proposed behavior. External references are primary project or specification sources checked on 07 Sep 2026.</p></div></div> |
| 376 | - <div class="reference-grid"> |
| 377 | - <p><a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/docs/10-architecture.md">Bindws / current architecture</a><br>Inspected local revision 5d31267. Routing, DO lifecycle, SQLite/R2, alarms, jobs, auth and optional hosting.</p> |
| 378 | - <p><a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/src/delivery.ts">Bindws / durable NIP-65 delivery</a><br>Queue schema, target selection, fixed queue/attempt bounds, policy revalidation and delivery status.</p> |
| 379 | - <p><a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/relay-templates/README.md">Bindws / relay and connection templates</a><br>First-class configuration documents and omitted-section semantics. Inbox predicate gap is documented in 03-inbox.jsonc.</p> |
| 380 | - <p><a href="https://github.com/FelineStateMachine/bindws/blob/5d31267/docs/11-hosting-bindws.md">Bindws / existing telemetry</a><br>Usage logs, optional metrics and provider traces. The proposal replaces their billing/runtime coupling.</p> |
| 381 | - <p><a href="https://github.com/nostr-protocol/nips">Nostr / NIP specifications</a><br>Implement selected NIPs against their exact checked revision; test storage, AUTH, deletion, expiration and relay-list semantics.</p> |
| 382 | - <p><a href="https://pkg.go.dev/fiatjaf.com/nostr/khatru">Current Khatru API</a><br>Current module path, relay hooks and Unlicense classification. SDK/store APIs and legacy paths are linked in the comparison above.</p> |
| 383 | - <p><a href="https://github.com/barrydeen/haven">HAVEN / personal relay roles</a><br>Closest product prior art for public/private role separation and automated gather/broadcast behavior.</p> |
| 384 | - <p><a href="https://github.com/hoytech/strfry">strfry / operational reference</a><br>Durable acknowledgements, storage specialization and relay synchronization; GPLv3 implementation.</p> |
| 385 | - <p><a href="https://www.sqlite.org/wal.html">SQLite / WAL</a><br>Concurrency, local-filesystem requirement and checkpoint behavior. Inspect embedded-version fixes when pinning a driver.</p> |
| 386 | - <p><a href="https://opentelemetry.io/docs/languages/go/">OpenTelemetry / Go</a><br>Instrumentation and optional export. Tail sampling is an additional collector choice, not a property of head sampling.</p> |
| 387 | - <p><a href="https://prometheus.io/docs/practices/naming/">Prometheus / metric design</a><br>Units, metric names and label cardinality. Detailed identifiers belong in protected diagnostics.</p> |
| 388 | - <p><a href="https://go.dev/doc/diagnostics">Go / profiling and diagnostics</a><br>Use metrics, profiles and execution traces to explain costs before changing implementation.</p> |
| 389 | - </div> |
| 390 | - <p class="fine" style="margin-top:23px">Research scope: adjacent source checkout, current package documentation, relay repositories and protocol/observability documentation. This document records the original architectural decisions. Native implementation and retained conformance tests now run in this workspace. See the <a href="artifacts/git-performance/git-performance.html">real-repository Git performance report</a> and <a href="docs/parity.md">parity ledger</a> for current evidence; the original proposals below are not a claim of completed acceptance.</p> |
| 391 | -</section> |
| 392 | -<footer class="foot"><span>tinyrelay / Architectural proposal 01</span><span>Full functional & UX parity. Native Go. No fuel. No leases.</span></footer> |
| 393 | -</main> |
| 394 | -<script> |
| 395 | -(() => { |
| 396 | - const paths = { |
| 397 | - publish: {title:"Publish an event",nodes:["edge","registry","runtime","store"],text:"<strong>Resolve → validate → authorize → commit → acknowledge → fan-out.</strong> Persist the event and routing intent together. An OK confirms local durability; remote delivery proceeds independently. The event never crosses another tenant’s runtime implicitly."}, |
| 398 | - subscribe: {title:"Open a subscription",nodes:["edge","registry","runtime","store"],text:"<strong>Resolve → authenticate → authorize → snapshot → EOSE → live events.</strong> A commit-sequence barrier bridges the historical query and live stream. Per-event visibility applies in both paths. Slow sockets are disconnected without holding a database reader open."}, |
| 399 | - delivery: {title:"Deliver in the background",nodes:["registry","runtime","store","workers"],text:"<strong>Due intent → acquire tenant → resolve lists → recheck policy → send → persist outcome.</strong> Shared workers continue with no connected clients. Each target has independent progress. Inbox pulls enter the same ingest service with an origin that prevents rebroadcast loops."}, |
| 400 | - create: {title:"Create an owned relay",nodes:["edge","registry","control","store"],text:"<strong>Authenticate creator → reserve name → initialize owner + template → mark ready.</strong> The catalog publishes the route only after the tenant database is initialized. An idempotent operation recovers interrupted creation. No lease, trial or claim conversion exists."} |
| 401 | - }; |
| 402 | - document.querySelectorAll("[data-path]").forEach(button => button.addEventListener("click", () => { |
| 403 | - const selected = paths[button.dataset.path]; |
| 404 | - document.querySelectorAll("[data-path]").forEach(item => item.setAttribute("aria-pressed",String(item === button))); |
| 405 | - document.querySelectorAll("[data-node]").forEach(node => node.classList.toggle("hot",selected.nodes.includes(node.dataset.node))); |
| 406 | - document.getElementById("path-title").textContent = selected.title; |
| 407 | - document.getElementById("path-description").innerHTML = selected.text; |
| 408 | - })); |
| 409 | - const roles = { |
| 410 | - outbox:{title:"Public outbox",about:"Your public events live here and automatically travel to the relays that should receive them.",writes:"Owner; signed events only",reads:"Public",auto:"Owner write relays + tagged recipients’ read relays + explicit mirrors",retention:"No automatic age-based purge",config:{format:"tinyrelay/template/1",id:"personal-outbox",revision:1,policy:{writes:"owner",reads:"public",blockPrivateKinds:true},automation:{delivery:"nip65-public",origin:"local-client"},retention:[]}}, |
| 411 | - inbox:{title:"Public mentions inbox",about:"A targeted home for mentions, replies, reactions and other configured public events addressed to you.",writes:"Valid public events with p tag naming an allowed local recipient",reads:"Public",auto:"Gather matching mentions from configured sources and owner read relays",retention:"Explicit content policy; no implicit trial expiry",config:{format:"tinyrelay/template/1",id:"personal-inbox",revision:1,policy:{writes:"recipient-tagged",reads:"public",recipients:"owner",blockPrivateKinds:true},automation:{gather:"public-mentions",sources:"owner-read-relays"},retention:[]}}, |
| 412 | - dm:{title:"Private DM inbox",about:"Store opaque gift wraps for local recipients. Public discovery and private readability are different concerns.",writes:"Valid kind 1059 addressed to a local recipient",reads:"Authenticated recipient, checked per event",auto:"Owner-signed kind 10050 discovery; clients deliver wraps directly",retention:"No automatic age-based purge",config:{format:"tinyrelay/template/1",id:"personal-dm",revision:1,policy:{writes:"recipient-tagged",reads:"authenticated-recipient",allowKinds:[1059],recipients:"owner"},automation:{discovery:"nip17",delivery:"disabled"},retention:[]}}, |
| 413 | - community:{title:"Community relay",about:"A named place for members to publish, with a portable policy and clear moderator ownership.",writes:"Admitted members; optional moderator rules",reads:"Public by this template; private is an explicit policy change",auto:"None by default; owner can configure public delivery or mirrors",retention:"Owner-selected content policy",config:{format:"tinyrelay/template/1",id:"community",revision:1,policy:{writes:"members",reads:"public",blockPrivateKinds:true},automation:{delivery:"disabled"},retention:[]}} |
| 414 | - }; |
| 415 | - document.getElementById("template-select").addEventListener("change",event => { |
| 416 | - const role = roles[event.target.value]; |
| 417 | - ["title","about","writes","reads","auto","retention"].forEach(key => document.getElementById("template-"+key).textContent = role[key]); |
| 418 | - document.getElementById("template-code").textContent = JSON.stringify(role.config,null,2); |
| 419 | - }); |
| 420 | - const navLinks = [...document.querySelectorAll("nav a")]; |
| 421 | - if ("IntersectionObserver" in window) { |
| 422 | - const observer = new IntersectionObserver(entries => { |
| 423 | - entries.forEach(entry => { |
| 424 | - if(entry.isIntersecting) navLinks.forEach(link => link.classList.toggle("active",link.getAttribute("href")==="#"+entry.target.id)); |
| 425 | - }); |
| 426 | - },{rootMargin:"-5% 0px -70% 0px",threshold:0}); |
| 427 | - document.querySelectorAll("main section, #overview").forEach(section => observer.observe(section)); |
| 428 | - } |
| 429 | - let printDetails = []; |
| 430 | - window.addEventListener("beforeprint",() => { |
| 431 | - printDetails = [...document.querySelectorAll("details")].map(element => ({element,open:element.open})); |
| 432 | - printDetails.forEach(item => item.element.open = true); |
| 433 | - }); |
| 434 | - window.addEventListener("afterprint",() => printDetails.forEach(item => item.element.open = item.open)); |
| 435 | - document.getElementById("print").addEventListener("click",() => window.print()); |
| 436 | -})(); |
| 437 | -</script> |
| 438 | -</body> |
| 439 | -</html> |
| 440 | diff --git a/docs/git-performance.md b/docs/git-performance.md |
| 441 | deleted file mode 100644 |
| 442 | index df78a88..0000000 |
| 443 | --- a/docs/git-performance.md |
| 444 | +++ /dev/null |
| 445 | @@ -1,146 +0,0 @@ |
| 446 | -# Git performance |
| 447 | - |
| 448 | -Phase wall times come from the recorded harness. Missing or failed phases remain visible. This report does not compare against the bindws Worker runtime. |
| 449 | - |
| 450 | -Method: Linux Slate, 16 CPUs, 10 GiB test-container ceiling, no CPU cap, no swap; client/server loopback; Git 2.50.1 client and 2.39.5 server; fixed local heads and tags with no cold page-cache reset. Phase CPU deltas are total container CPU inclusive of Git children and background probes, and values below sampler resolution are shown as missing. |
| 451 | - |
| 452 | -Methods: native Git HTTP baseline, buffered Tiny, and streaming Tiny where result artifacts exist. |
| 453 | - |
| 454 | -## Findings |
| 455 | - |
| 456 | -| finding | before | after | |
| 457 | -| --- | --- | --- | |
| 458 | -| Peak Go memory (MiB) | 6,094 | 40.6 | |
| 459 | -| Tag-heavy strudel incremental push (ms) | 1,510 | 94.1 | |
| 460 | - |
| 461 | -| repo | before clone | before concurrent4 | before state ACK | before push | after clone | after concurrent4 | after state ACK | after push | |
| 462 | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | |
| 463 | -| atlas.git | 12,971 | — | FAILED: Nostr request timed out after 30 seconds | — | 9,148 | 14,383 | 26.8 | 67 | |
| 464 | -| bindws.git | — | — | — | — | 178.0 | 267.5 | 21.3 | 59.4 | |
| 465 | -| diagramzip.git | 1,844 | 2,391 | 27.7 | 58.8 | 1,559 | 2,118 | 21.9 | 58.3 | |
| 466 | -| doorbearer.git | 5,669 | 7,782 | 18.6 | 60.4 | 4,444 | 6,795 | 24.5 | 59.6 | |
| 467 | -| nzip.git | 100.1 | 114.8 | 33.4 | 99.4 | 73.4 | 79.3 | 19.4 | 59.2 | |
| 468 | -| strudel.git | 3,052 | 4,933 | 615.6 | 1,510 | 2,035 | 3,747 | 24.3 | 94.1 | |
| 469 | - |
| 470 | -## Remaining relay latency |
| 471 | - |
| 472 | -Every probe succeeded, but durable event acknowledgements still stalled occasionally during large Git transfers, including native Git baseline phases on the same host. These results do not identify the cause. Capture disk latency and scheduling before selecting another optimization. |
| 473 | - |
| 474 | -| workload | ACK P95 ms | ACK max ms | query P95 ms | query max ms | |
| 475 | -| --- | --- | --- | --- | --- | |
| 476 | -| Public Atlas | 68.3 | 719.3 | 1.71 | 331.1 | |
| 477 | -| Native Git Atlas phases | 53.7 | 656.4 | 1.69 | 4.51 | |
| 478 | -| Private atlas.git | 86.7 | 1,384 | 2.98 | 33.5 | |
| 479 | - |
| 480 | -## Phase results |
| 481 | - |
| 482 | -| repo | method | clone n | initial push ms | clone median ms | fetch ms | incremental state ACK ms | incremental push ms | concurrent ×2 wall ms | concurrent ×4 wall ms | |
| 483 | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | |
| 484 | -| nzip.git | streaming Tiny | 3 | 111.0 | 73.4 | 26.9 | 19.4 | 59.2 | 75.3 | 79.3 | |
| 485 | -| nzip.git | native Git HTTP (after) | 3 | 84 | 67.6 | 23.9 | — | 33.6 | 73.1 | 77.6 | |
| 486 | -| bindws.git | streaming Tiny | 3 | 249.2 | 178.0 | 27.9 | 21.3 | 59.4 | 203.8 | 267.5 | |
| 487 | -| bindws.git | native Git HTTP (after) | 3 | 171.7 | 172.1 | 24.6 | — | 33.4 | 205.5 | 256.1 | |
| 488 | -| diagramzip.git | streaming Tiny | 3 | 1,557 | 1,559 | 26.3 | 21.9 | 58.3 | 1,698 | 2,118 | |
| 489 | -| diagramzip.git | native Git HTTP (after) | 3 | 1,470 | 1,538 | 24.5 | — | 35.8 | 1,720 | 2,112 | |
| 490 | -| strudel.git | streaming Tiny | 3 | 2,595 | 2,035 | 36.1 | 24.3 | 94.1 | 2,625 | 3,747 | |
| 491 | -| strudel.git | native Git HTTP (after) | 3 | 2,499 | 2,054 | 31.4 | — | 55.6 | 2,458 | 3,645 | |
| 492 | -| doorbearer.git | streaming Tiny | 3 | 4,009 | 4,444 | 28.8 | 24.5 | 59.6 | 4,847 | 6,795 | |
| 493 | -| doorbearer.git | native Git HTTP (after) | 3 | 4,039 | 4,430 | 24.3 | — | 35.3 | 5,095 | 9,770 | |
| 494 | -| atlas.git | streaming Tiny | 3 | 10,233 | 9,148 | 27.7 | 26.8 | 67 | 10,290 | 14,383 | |
| 495 | -| atlas.git | native Git HTTP (after) | 3 | 9,600 | 8,918 | 26.4 | — | 36.8 | 14,813 | 12,208 | |
| 496 | -| nzip.git | buffered Tiny | 3 | 186.7 | 100.1 | 56.7 | 33.4 | 99.4 | 113.8 | 114.8 | |
| 497 | -| nzip.git | native Git HTTP (before) | 3 | 79.9 | 68.7 | 24.5 | — | 31.1 | 76.5 | 78.3 | |
| 498 | -| bindws.git | buffered Tiny | 0 | FAILED: Command failed: git -C /tmp/tiny-git-benchmark-pCF5Bp push http://127.0.0.1:17447/npub1t23e93dv97z5k320gm3tjp65z6ga249tt76c2pjwlx0rxr7wwvss9tuxhn/bindws-1.git r | — | — | — | — | — | — | |
| 499 | -| diagramzip.git | buffered Tiny | 3 | 1,578 | 1,844 | 33.8 | 27.7 | 58.8 | 1,888 | 2,391 | |
| 500 | -| diagramzip.git | native Git HTTP (before) | 3 | 1,401 | 1,658 | 24.6 | — | 35.9 | 1,717 | 1,958 | |
| 501 | -| strudel.git | buffered Tiny | 3 | 4,639 | 3,052 | 783.7 | 615.6 | 1,510 | 3,499 | 4,933 | |
| 502 | -| strudel.git | native Git HTTP (before) | 3 | 2,470 | 1,935 | 32.3 | — | 56.2 | 2,616 | 3,889 | |
| 503 | -| doorbearer.git | buffered Tiny | 3 | 4,024 | 5,669 | 33.3 | 18.6 | 60.4 | 5,876 | 7,782 | |
| 504 | -| doorbearer.git | native Git HTTP (before) | 3 | 3,951 | 4,111 | 25.3 | — | 34.6 | 5,543 | 5,448 | |
| 505 | -| atlas.git | buffered Tiny | 3 | 9,906 | 12,971 | 47.1 | FAILED: Nostr request timed out after 30 seconds | — | — | — | |
| 506 | -| atlas.git | native Git HTTP (before) | 3 | 9,275 | 9,229 | 28.6 | — | — | — | — | |
| 507 | -| atlas.git | buffered Tiny | 0 | FAILED: Command failed: git -C /tmp/tiny-git-benchmark-szM6s6 push http://127.0.0.1:17447/npub15a63zxpyhpz85v554d6f24gctr840ytc3xsaq7pk3xd076nrhacqxc79mu/atlas-0.git re | — | — | — | — | — | — | |
| 508 | -| atlas.git | private Tiny (NIP-98) | 3 | 13,537 | 9,005 | 39.6 | 23.2 | 79 | 10,601 | 18,337 | |
| 509 | - |
| 510 | -## Resource results |
| 511 | - |
| 512 | -| repo | method | Go RSS high-water MiB | cgroup anon peak MiB | cgroup file peak MiB | cgroup total peak MiB | CPU initial µs | CPU incremental µs | CPU concurrent2 µs | CPU concurrent4 µs | container | |
| 513 | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | |
| 514 | -| nzip.git | streaming Tiny | 34.4 | 13.3 | 5.46 | 30.9 | 204,937 | — | — | — | tiny-git-perf-final | |
| 515 | -| nzip.git | native Git HTTP (after) | 8.03 | 2.69 | 0.516 | 10.1 | — | — | — | — | tiny-git-perf-final-baseline | |
| 516 | -| bindws.git | streaming Tiny | 35.3 | 23.4 | 14.8 | 57.1 | 395,910 | — | 531,919 | — | tiny-git-perf-final | |
| 517 | -| bindws.git | native Git HTTP (after) | 10.3 | 12.6 | 9.66 | 42.7 | — | — | 469,261 | — | tiny-git-perf-final-baseline | |
| 518 | -| diagramzip.git | streaming Tiny | 37.1 | 122.3 | 82.1 | 209.6 | 1,531,645 | — | 837,444 | 2,024,123 | tiny-git-perf-final | |
| 519 | -| diagramzip.git | native Git HTTP (after) | 11.7 | 49.5 | 76.5 | 190.4 | 1,745,696 | — | 860,621 | 1,412,613 | tiny-git-perf-final-baseline | |
| 520 | -| strudel.git | streaming Tiny | 39.3 | 179.2 | 174.1 | 372.7 | 6,636,176 | — | 1,604,850 | 3,353,508 | tiny-git-perf-final | |
| 521 | -| strudel.git | native Git HTTP (after) | 12.2 | 165.0 | 167.7 | 357.4 | 6,434,735 | — | 1,323,538 | 2,209,831 | tiny-git-perf-final-baseline | |
| 522 | -| doorbearer.git | streaming Tiny | 40.3 | 40.7 | 548.1 | 621.3 | 4,892,027 | — | 5,021,984 | 10,789,638 | tiny-git-perf-final | |
| 523 | -| doorbearer.git | native Git HTTP (after) | 12.6 | 29.6 | 540.8 | 601.6 | 4,548,602 | — | 4,224,315 | 10,196,891 | tiny-git-perf-final-baseline | |
| 524 | -| atlas.git | streaming Tiny | 40.6 | 179.8 | 1,477 | 1,765 | 11,991,370 | — | 11,659,980 | 28,932,729 | tiny-git-perf-final | |
| 525 | -| atlas.git | native Git HTTP (after) | 12.8 | 178.2 | 1,469 | 1,756 | 11,375,064 | — | 11,745,735 | 27,511,063 | tiny-git-perf-final-baseline | |
| 526 | -| nzip.git | buffered Tiny | 39.7 | 18.7 | 10.1 | 36.6 | 319,330 | — | — | 511,003 | tiny-git-perf-before | |
| 527 | -| nzip.git | native Git HTTP (before) | 9.43 | 4.03 | 0.52 | 15.9 | — | — | — | — | tiny-git-perf-baseline | |
| 528 | -| bindws.git | buffered Tiny | 39.9 | 18.9 | 10.2 | 36.6 | 417,278 | — | — | — | tiny-git-perf-before | |
| 529 | -| diagramzip.git | buffered Tiny | 813.4 | 792.4 | 77.6 | 889.9 | 1,910,533 | — | 1,905,437 | 1,340,161 | tiny-git-perf-before | |
| 530 | -| diagramzip.git | native Git HTTP (before) | 11.1 | 63.6 | 67.4 | 180.7 | 1,471,525 | — | 1,238,327 | 1,567,270 | tiny-git-perf-baseline | |
| 531 | -| strudel.git | buffered Tiny | 1,566 | 1,566 | 170.1 | 1,766 | 8,893,943 | 1,629,815 | 4,022,310 | 9,036,055 | tiny-git-perf-before | |
| 532 | -| strudel.git | native Git HTTP (before) | 11.7 | 108.0 | 158.6 | 340.7 | 6,261,466 | — | 1,354,373 | 3,098,503 | tiny-git-perf-baseline | |
| 533 | -| doorbearer.git | buffered Tiny | 3,732 | 3,729 | 544.2 | 4,300 | 4,823,998 | — | 4,355,754 | 10,081,976 | tiny-git-perf-before | |
| 534 | -| doorbearer.git | native Git HTTP (before) | 11.6 | 29.4 | 532.0 | 580.1 | 4,500,482 | — | 4,283,006 | 10,758,873 | tiny-git-perf-baseline | |
| 535 | -| atlas.git | buffered Tiny | 6,094 | 6,097 | 1,365 | 7,256 | 11,809,027 | — | — | — | tiny-git-perf-before | |
| 536 | -| atlas.git | native Git HTTP (before) | 10.8 | 143.4 | 1,414 | 1,663 | 11,397,964 | — | — | — | tiny-git-perf-baseline | |
| 537 | -| atlas.git | buffered Tiny | 6,094 | 226.7 | 2,125 | 7,256 | 11,603,962 | — | — | — | tiny-git-perf-before | |
| 538 | -| atlas.git | private Tiny (NIP-98) | 38.9 | 180.1 | 1,860 | 2,158 | 12,983,504 | — | 11,985,201 | 29,095,672 | tiny-git-perf-private | |
| 539 | - |
| 540 | -## Probe results |
| 541 | - |
| 542 | -| repo | method | idle publish P50 ms | idle publish P95 ms | idle publish max ms | idle query P50 ms | idle query P95 ms | idle query max ms | busy publish P50 ms | busy publish P95 ms | busy publish max ms | busy query P50 ms | busy query P95 ms | busy query max ms | idle errors | busy errors | |
| 543 | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | |
| 544 | -| nzip.git | streaming Tiny | 6.4 | 11.7 | 19.5 | 1.73 | 4.12 | 20.8 | 6.04 | 15.3 | 18.6 | 1.33 | 2.28 | 2.69 | 0 | 0 | |
| 545 | -| nzip.git | native Git HTTP (after) | — | — | — | — | — | — | 7.44 | 10.7 | 10.8 | 1.16 | 1.55 | 1.65 | 0 | 0 | |
| 546 | -| bindws.git | streaming Tiny | 6.11 | 7.38 | 10.3 | 1.63 | 2.05 | 5.05 | 5.88 | 12.8 | 23 | 1.43 | 1.92 | 5.43 | 0 | 0 | |
| 547 | -| bindws.git | native Git HTTP (after) | — | — | — | — | — | — | 5.96 | 17.8 | 20 | 1.32 | 1.72 | 2.33 | 0 | 0 | |
| 548 | -| diagramzip.git | streaming Tiny | 6.16 | 10.3 | 12 | 1.56 | 2.05 | 4.81 | 5.7 | 17.3 | 102.2 | 1.33 | 1.73 | 3.13 | 0 | 0 | |
| 549 | -| diagramzip.git | native Git HTTP (after) | — | — | — | — | — | — | 5.81 | 17.6 | 84.7 | 1.33 | 1.7 | 2.24 | 0 | 0 | |
| 550 | -| strudel.git | streaming Tiny | 6.1 | 10.2 | 15.2 | 1.54 | 1.98 | 4.3 | 6.08 | 12.7 | 330.9 | 1.36 | 2.47 | 5.88 | 0 | 0 | |
| 551 | -| strudel.git | native Git HTTP (after) | — | — | — | — | — | — | 5.61 | 9.49 | 100.3 | 1.35 | 2.03 | 5.91 | 0 | 0 | |
| 552 | -| doorbearer.git | streaming Tiny | 4.16 | 6.79 | 9.06 | 1.52 | 1.88 | 3.21 | 5.64 | 47.1 | 299.4 | 1.25 | 1.68 | 3.49 | 0 | 0 | |
| 553 | -| doorbearer.git | native Git HTTP (after) | — | — | — | — | — | — | 5.83 | 47.5 | 1,117 | 1.23 | 1.62 | 3.7 | 0 | 0 | |
| 554 | -| atlas.git | streaming Tiny | 5.86 | 10.5 | 26.2 | 1.47 | 1.9 | 5.18 | 5.64 | 68.3 | 719.3 | 1.26 | 1.71 | 331.1 | 0 | 0 | |
| 555 | -| atlas.git | native Git HTTP (after) | — | — | — | — | — | — | 5.71 | 53.7 | 656.4 | 1.28 | 1.69 | 4.51 | 0 | 0 | |
| 556 | -| nzip.git | buffered Tiny | 6.29 | 9.96 | 11 | 1.64 | 2.26 | 5.71 | 6.11 | 14.2 | 35.6 | 1.38 | 1.63 | 5.34 | 0 | 0 | |
| 557 | -| nzip.git | native Git HTTP (before) | — | — | — | — | — | — | 6.26 | 7.43 | 7.57 | 1.28 | 2.82 | 3.84 | 0 | 0 | |
| 558 | -| bindws.git | buffered Tiny | 6.27 | 11.1 | 15.7 | 1.63 | 2.25 | 21.7 | 5.79 | 6.13 | 6.18 | 1.45 | 4.77 | 5.36 | 0 | 0 | |
| 559 | -| diagramzip.git | buffered Tiny | 6.39 | 10.7 | 12.6 | 1.71 | 2.33 | 2.52 | 6.2 | 18.6 | 201.0 | 1.34 | 1.66 | 2.13 | 0 | 0 | |
| 560 | -| diagramzip.git | native Git HTTP (before) | — | — | — | — | — | — | 6.56 | 23.4 | 209.8 | 1.35 | 1.71 | 3.8 | 0 | 0 | |
| 561 | -| strudel.git | buffered Tiny | 4.37 | 6.34 | 8.87 | 1.58 | 2.09 | 3.7 | 3.76 | 9.38 | 567.1 | 1.26 | 1.91 | 24.2 | 0 | 0 | |
| 562 | -| strudel.git | native Git HTTP (before) | — | — | — | — | — | — | 4.04 | 9.99 | 179.1 | 1.3 | 2.62 | 8.03 | 0 | 0 | |
| 563 | -| doorbearer.git | buffered Tiny | 5.9 | 9.14 | 12.2 | 1.45 | 1.8 | 2.11 | 5.72 | 46.3 | 426.0 | 1.2 | 1.59 | 2.77 | 0 | 0 | |
| 564 | -| doorbearer.git | native Git HTTP (before) | — | — | — | — | — | — | 5.58 | 23.1 | 288.1 | 1.25 | 1.63 | 5.11 | 0 | 0 | |
| 565 | -| atlas.git | buffered Tiny | 5.86 | 9.99 | 13.9 | 1.51 | 1.78 | 3.48 | 5.28 | 11.5 | 1,323 | 1.41 | 2.06 | 3.72 | 0 | 0 | |
| 566 | -| atlas.git | native Git HTTP (before) | — | — | — | — | — | — | 5.53 | 19.2 | 242.5 | 1.26 | 1.72 | 4.38 | 0 | 0 | |
| 567 | -| atlas.git | buffered Tiny | 4.58 | 6.84 | 9.25 | 1.94 | 2.41 | 6.25 | 3.43 | 6 | 91.4 | 1.1 | 1.58 | 2.14 | 0 | 0 | |
| 568 | -| atlas.git | private Tiny (NIP-98) | 6.65 | 9.31 | 12.8 | 1.83 | 2.28 | 5.93 | 5.95 | 86.7 | 1,384 | 1.35 | 2.98 | 33.5 | 0 | 0 | |
| 569 | - |
| 570 | -## Status |
| 571 | - |
| 572 | -| repo | method | status | failed phases | |
| 573 | -| --- | --- | --- | --- | |
| 574 | -| nzip.git | streaming Tiny | complete | — | |
| 575 | -| bindws.git | streaming Tiny | complete | — | |
| 576 | -| diagramzip.git | streaming Tiny | complete | — | |
| 577 | -| strudel.git | streaming Tiny | complete | — | |
| 578 | -| doorbearer.git | streaming Tiny | complete | — | |
| 579 | -| atlas.git | streaming Tiny | complete | — | |
| 580 | -| nzip.git | buffered Tiny | complete | — | |
| 581 | -| bindws.git | buffered Tiny | failed | initial_push | |
| 582 | -| diagramzip.git | buffered Tiny | complete | — | |
| 583 | -| strudel.git | buffered Tiny | complete | — | |
| 584 | -| doorbearer.git | buffered Tiny | complete | — | |
| 585 | -| atlas.git | buffered Tiny | failed | incremental_state_ack | |
| 586 | -| atlas.git | buffered Tiny | failed | initial_push | |
| 587 | -| atlas.git | private Tiny (NIP-98) | complete | — | |
| 588 | - |
| 589 | -Profile finding from the captured before run: CPU samples identify memory clearing/copying as the visible hot path; the independent Go RSS reading is 6093.9 MiB. Keep the raw profile and process/cgroup captures beside any interpretation. |
| 590 | - |
| 591 | -Raw JSON artifacts are linked from the HTML report. |
| 592 | diff --git a/docs/khatru-integration.md b/docs/khatru-integration.md |
| 593 | deleted file mode 100644 |
| 594 | index 66f44fb..0000000 |
| 595 | --- a/docs/khatru-integration.md |
| 596 | +++ /dev/null |
| 597 | @@ -1,81 +0,0 @@ |
| 598 | -# Khatru integration spike |
| 599 | - |
| 600 | -Date: 2026-09-07 |
| 601 | -Module inspected: `fiatjaf.com/nostr@v0.0.0-20260902034142-316ef6591fa2/khatru` |
| 602 | -Reference transport: `../bindws/src/relay.ts` |
| 603 | - |
| 604 | -## Decision |
| 605 | - |
| 606 | -Khatru is useful protocol prior art and can carry a small first milestone, but it |
| 607 | -is not a drop-in transport for tinyrelay's full contract. Use its event types, |
| 608 | -NIP-42/NIP-45/NIP-77 codecs and policy vocabulary where useful. Keep the |
| 609 | -production websocket transport under tinyrelay's ownership once the requirements |
| 610 | -below matter. The decisive reasons are the historical/live subscription race, |
| 611 | -the lack of a connection shutdown API, and the absence of an error-bearing query |
| 612 | -iterator. These are correctness and lifecycle boundaries, not benchmark concerns. |
| 613 | - |
| 614 | -## What the pinned module provides |
| 615 | - |
| 616 | -`Relay` exposes hooks for event admission, storage, replacement, deletion, |
| 617 | -request and count authorization, historical query, authentication, listener |
| 618 | -add/remove, live broadcast filtering, and NIP-11/NIP-86. `RequestAuth(ctx)` can |
| 619 | -send a challenge from `OnConnect`, `OnEvent`, `OnRequest`, or another hook. NIP-45 |
| 620 | -COUNT and NIP-77 negentropy are wired through `Count`/`CountHLL` and |
| 621 | -`QueryStored`. |
| 622 | - |
| 623 | -The wire path verifies event IDs and signatures before hooks. Normal events are |
| 624 | -stored, then `OnEventSaved` runs, then listeners are notified. Deletion events |
| 625 | -are stored before Khatru's NIP-09 target deletion pass. `QueryStored` is |
| 626 | -`iter.Seq[nostr.Event]`; iteration has no error return. The default websocket |
| 627 | -handler starts a goroutine for every incoming message and two connection loops. |
| 628 | -Writes are serialized per socket, but there is no relay-level worker registry or |
| 629 | -wait group. |
| 630 | - |
| 631 | -## Compatibility matrix |
| 632 | - |
| 633 | -| Requirement | Native hook fit | Adapter / blocker | |
| 634 | -|---|---|---| |
| 635 | -| Proactive AUTH challenge | Yes | Call `RequestAuth(ctx)` from `OnConnect`; configure the relay URL carefully. This is opt-in and not sent by `NewRelay`. | |
| 636 | -| kind 24133 signer exception | Partial | `OnRequest`, `QueryStored`, and `PreventBroadcast` can implement the encrypted NIP-46 visibility rule. No built-in exception exists; make it an explicit policy branch and test historical, live, COUNT, and negentropy paths separately. | |
| 637 | -| Per-event privacy for REQ/live | Partial | Historical privacy belongs inside `QueryStored`; live privacy belongs in `PreventBroadcast`. `OnRequest` alone is too coarse. | |
| 638 | -| Per-event privacy for COUNT | Yes, with own store | Implement `Count`/`CountHLL` against the same authorization predicate. The hook only gates the request; it does not filter rows for you. | |
| 639 | -| Per-event privacy for negentropy | Yes, with own store | Negentropy calls `QueryStored` with a negentropy context, so the same filtered iterator can work. Verify that the vector contains exactly the events visible to that authenticated client. | |
| 640 | -| No historical/live gap | No | Khatru queries history first and adds listeners afterward. An event committed between those operations can be missed. There is no public snapshot/fence hook. Own the transport or add a store-level sequence barrier and modify the dispatch path. | |
| 641 | -| NIP-09 deletion-before-arrival tombstones | No | Khatru only deletes rows it finds. Persist tombstones in an admission transaction and have future `StoreEvent` reject/ignore matching events. This requires an own storage/admission layer. | |
| 642 | -| Gift-recipient deletion | Partial | `AllowDeleting(target, deletion)` can authorize the p-tagged recipient for kind 1059. The default path has no recipient tombstone model, so recipient deletes still need the own store. | |
| 643 | -| Private policy change closes sessions | No | The public API returns snapshots and listener callbacks, but does not expose a socket close/remove method. `WebSocket.Context` is observable, not cancellable by callers. Own transport or maintain a fork. | |
| 644 | -| Event + delivery intent atomic acknowledgement | Partial | Put both writes in a custom `StoreEvent`/`ReplaceEvent` transaction. Do not use `UseEventstore` unchanged: its `SaveEvent` cannot atomically add tinyrelay delivery rows. Intent calculation must be deterministic and side-effect free inside the transaction. | |
| 645 | -| Query/storage errors | No | `iter.Seq` cannot report a database error. A failing iterator can only stop early, which Khatru will treat as a complete EOSE. Use a query API with an error result in the owned transport, or expose a conservative health/error side channel and fail closed. | |
| 646 | -| Clean shutdown of all goroutines | No | The comments mention `Server.Shutdown`, but this pinned package has no exported relay shutdown/wait method. `http.Server.Shutdown` does not provide a join for Khatru's websocket goroutines. Track and close sockets in an owned transport. | |
| 647 | -| Arbitrary operator host limits | Partial | `MaxMessageSize`, `MaxAuthenticatedClients`, websocket deadlines, and query limits are configurable. Defaults include 512000-byte messages, eight authenticated keys, and a caller-supplied query limit; set every limit from explicit host configuration so no default becomes a product cap. | |
| 648 | - |
| 649 | -## Required adapter rules if Khatru is used temporarily |
| 650 | - |
| 651 | -* Set `ChallengePrefix`, `ServiceURL`, `MaxMessageSize`, |
| 652 | - `MaxAuthenticatedClients`, write/read/ping deadlines, and query limits from the |
| 653 | - operator configuration. Do not inherit `NewRelay` defaults silently. |
| 654 | -* Implement one authorization predicate and reuse it from `QueryStored`, |
| 655 | - `Count`/`CountHLL`, `PreventBroadcast`, and negentropy. It must understand |
| 656 | - authenticated pubkeys, event author, p-tags, private kinds, and the 24133 |
| 657 | - signer exception. |
| 658 | -* Wrap `StoreEvent`, `ReplaceEvent`, and deletion admission in the tenant |
| 659 | - transaction. Store a deletion tombstone before acknowledging a deletion and |
| 660 | - check it before accepting a late event. Add recipient tombstones for gift |
| 661 | - wraps according to the local privacy policy. |
| 662 | -* Persist delivery intent before returning a successful local `OK`. Remote |
| 663 | - sends remain at-least-once and must be idempotent by tenant, event, and target. |
| 664 | -* Treat a query iterator ending without an explicit store-success marker as |
| 665 | - incomplete. Until the owned transport exists, surface a degraded health state |
| 666 | - rather than claiming a complete history. |
| 667 | -* On policy revision, reject new requests immediately and close every affected |
| 668 | - socket through the owned connection registry. Khatru hooks alone cannot do the |
| 669 | - close. |
| 670 | - |
| 671 | -## Recommendation |
| 672 | - |
| 673 | -Prototype event policy and storage transactions behind Khatru only if it shortens |
| 674 | -the first vertical slice. Before enabling private relay templates or promising |
| 675 | -durable subscriptions, replace `HandleWebsocket` with a tinyrelay transport that |
| 676 | -owns a per-connection reader, single writer, cancellation, admission fence, and |
| 677 | -wait-group shutdown. Keep Khatru's codecs and NIP implementations, but do not |
| 678 | -make Khatru's default `ServeHTTP` the long-term lifecycle boundary. |
| 679 | diff --git a/docs/observability.md b/docs/observability.md |
| 680 | index b27701b..6a058c0 100644 |
| 681 | --- a/docs/observability.md |
| 682 | +++ b/docs/observability.md |
| 683 | @@ -74,6 +74,6 @@ NIP-66 monitoring is opt-in and accepts only the peer URLs supplied in `PeerMoni |
| 684 | |
| 685 | ## Git workload captures |
| 686 | |
| 687 | -The [real repository experiment](git-performance.md) follows this workflow. The original Git HTTP wrapper buffered entire pack responses; process RSS and the CPU profile exposed allocation, clearing and copying costs. Streaming the response and batching signed-ref/object checks removed the measured bottlenecks. The harness checks EVENT acknowledgement and exact-ID query latency while push, clone and fetch operations run, so a fast Git transfer cannot conceal a stalled relay. |
| 688 | +The Git performance harness follows this workflow. The original Git HTTP wrapper buffered entire pack responses; process RSS and the CPU profile exposed allocation, clearing and copying costs. Streaming the response and batching signed-ref/object checks removed the measured bottlenecks. The harness checks EVENT acknowledgement and exact-ID query latency while push, clone and fetch operations run, so a fast Git transfer cannot conceal a stalled relay. |
| 689 | |
| 690 | Use `scripts/observe-git-benchmark.py` for repeatable captures. Keep Go process RSS separate from cgroup memory: native Git children and filesystem cache are charged to the container, and do not appear in a Go heap profile. Likewise, Go CPU profiles exclude Git child CPU; use cgroup CPU deltas to account for the whole server. Record concurrency, repository object/ref counts, Git versions, page-cache conditions and the native Git baseline with each result. Instructions and comparison limits are in [testing.md](testing.md#real-git-repositories). |
| 691 | diff --git a/docs/parity-gap-audit.md b/docs/parity-gap-audit.md |
| 692 | deleted file mode 100644 |
| 693 | index f232269..0000000 |
| 694 | --- a/docs/parity-gap-audit.md |
| 695 | +++ /dev/null |
| 696 | @@ -1,87 +0,0 @@ |
| 697 | -# Parity gap audit |
| 698 | - |
| 699 | -Compared with bindws revision `5d31267`. This is a behavior ledger, not a |
| 700 | -claim that package compilation proves full UX parity. The native contract |
| 701 | -removes hosted leases/trials, ownership provisioning claims, fuel/billing, and |
| 702 | -product entitlement caps. Membership invite claims remain required. |
| 703 | - |
| 704 | -## Evidence recorded |
| 705 | - |
| 706 | -- The retained 90-method source registry is exposed through the tenant |
| 707 | - dispatcher. `internal/daemon/management_golden_test.go` invokes every method |
| 708 | - on a fresh tenant and fails on `unsupported`; representative read methods |
| 709 | - must succeed. |
| 710 | -- `internal/daemon/ux_contract_test.go` makes signed HTTP calls for invite |
| 711 | - creation/listing, member mutation, moderation, IP rules, policy and identity |
| 712 | - changes, config export, fork, and deletion. It also checks guided Sync |
| 713 | - argument shapes. |
| 714 | -- Domain/NIP-AD validation, custom host catalog mappings, retention sweeps, |
| 715 | - records scheduling/privacy, callback registration validation, backup sealing, |
| 716 | - Git behavior, and replication queues have focused package tests. |
| 717 | -- The current conformance artifact `20260907T170116Z` runs all 14 configured |
| 718 | - files (the source NIP-11 file is replaced by the quota-free compatibility |
| 719 | - test) and records 17 passing tests with no skips. Earlier artifacts remain |
| 720 | - historical evidence only. |
| 721 | -- The follow-up Git artifact `20260907T154227Z` passes its Git case. A |
| 722 | - separate browser run also exercised the remote-signer backup flow. These |
| 723 | - close those observed cases only, not the entire GRASP or browser families. |
| 724 | -- The latest retained protocol artifact `20260907T170116Z` again passes all |
| 725 | - 17 tests with no skips. `go test -race ./...` and `go vet ./...` also pass. |
| 726 | -- `routes_acceptance_test.go` covers mounted NIP-11, NIP-86, bridge auth, |
| 727 | - NIP-05, NIP-AD auth behavior, public pages, CORS, and data-route denial. |
| 728 | - `durable_acceptance_test.go` covers restart recovery for a dump job, view |
| 729 | - checkpoint, and notification intent. `git_push_test.go` covers immediate |
| 730 | - 30-ref HTTP push after router publication. |
| 731 | -- Browser evidence covers a distinct NIP-46 signer reconnect and signed |
| 732 | - mutation (`output/playwright/nip46-distinct-resume-evidence.json`) and a |
| 733 | - valid backup restore plus malformed-archive retry |
| 734 | - (`artifacts/browser-restore-qa.json`). The restore run used a page-local NIP-07 fixture, so its subsequent search route |
| 735 | - lost that injected signer and required authentication again. The retained NIP-46 |
| 736 | - browser evidence now closes navigation persistence for the supported remote |
| 737 | - signer path with distinct remote and user keys; NIP-07 extension persistence |
| 738 | - remains browser-managed and is not emulated by the page. |
| 739 | -- Git performance evidence covers six real repositories through all harness |
| 740 | - phases in `artifacts/git-performance/results-after`. |
| 741 | -- `hosting_routes_acceptance_test.go` verifies per-tenant site isolation and a |
| 742 | - custom host mapping. `callback_restart_acceptance_test.go` verifies durable |
| 743 | - callback recovery and revocation. `git_private_restart_test.go` verifies |
| 744 | - pending private state recovery, unsigned denial, signed HTTP discovery and |
| 745 | - receive-pack negotiation after restart, and tampered-payload rejection. |
| 746 | -- Public transfers pass all six fixtures; private nzip and Atlas transfers |
| 747 | - also pass. Authenticated upload spooling keeps Go RSS independent of pack |
| 748 | - size. Tail acknowledgement stalls remain visible in the performance report. |
| 749 | -- The complete Linux race suite passes at |
| 750 | - `artifacts/slate/tiny-private-git-final-20260907/20260907T165524Z`. |
| 751 | - The subsequent Git policy changes pass the daemon race suite on Linux at |
| 752 | - `artifacts/slate/tiny-git-policy-final-20260907/20260907T165928Z`. |
| 753 | - |
| 754 | -## Remaining acceptance work |
| 755 | - |
| 756 | -1. HTTP portable import now has signed mounted-route acceptance for final |
| 757 | - imported counts and persisted content, alongside durable import-job restart |
| 758 | - coverage. The combined Git-containing HTTP backup/restore journey also has |
| 759 | - focused acceptance coverage. A streamed progress UI remains outside the |
| 760 | - current native route contract. Positive signed NIP-AD and NIP-96 |
| 761 | - upload/get/delete, site isolation/custom mapping, Smart HTTP push, and |
| 762 | - backup preview/restore are already covered by focused tests. |
| 763 | -2. Complete browser tree traversal and moderation recovery acceptance. |
| 764 | - Invite claim, Sync progress/retry, view publication and fork navigation |
| 765 | - have recorded checks in `output/playwright/browser-checks.md`; deletion |
| 766 | - recovery also has focused daemon tests. The |
| 767 | - NIP-46 reconnect and signed mutation path is covered; the NIP-07 restore |
| 768 | - fixture intentionally loses its page-local signer after navigation and is |
| 769 | - not evidence of a native signer persistence failure. |
| 770 | -3. Restart coverage now includes inbox/outbox delivery retry, callback |
| 771 | - revocation, dump and backup jobs, view checkpoints, notification intents, |
| 772 | - and terminal replication-job failure status with retry attempts. Portable |
| 773 | - import progress remains an unverified restart case. |
| 774 | -4. Complete a private object transfer and full inventory assertion after |
| 775 | - restart. Signed private HTTP discovery/negotiation and pending inventory |
| 776 | - survive restart; the separate real private transfer run passes too. Native state projection, |
| 777 | - stale-worker protection, dropped-ref cleanup, journal recovery, immediate |
| 778 | - multi-ref push, and private-state hiding already have focused tests. |
| 779 | - |
| 780 | -The complete source inventory and explicit exclusions remain machine-readable |
| 781 | -in `docs/parity.json`. Items stay `pending` when their implementation has |
| 782 | -focused tests but lacks the route, browser, restart, or source conformance |
| 783 | -evidence named above. |
| 784 | diff --git a/docs/parity.json b/docs/parity.json |
| 785 | deleted file mode 100644 |
| 786 | index 9ddc368..0000000 |
| 787 | --- a/docs/parity.json |
| 788 | +++ /dev/null |
| 789 | @@ -1,410 +0,0 @@ |
| 790 | -{ |
| 791 | - "schema": "tiny/parity/1", |
| 792 | - "source": { |
| 793 | - "repository": "../bindws", |
| 794 | - "revision": "5d31267", |
| 795 | - "routes": "src/routes.ts", |
| 796 | - "management": "src/manage.ts", |
| 797 | - "relayTemplates": "relay-templates", |
| 798 | - "connectionTemplates": "connection-templates", |
| 799 | - "conformance": "test/conformance" |
| 800 | - }, |
| 801 | - "statusMeaning": { |
| 802 | - "pending": "retained behavior not yet verified in tiny", |
| 803 | - "verified": "tiny implementation and acceptance check pass", |
| 804 | - "intentional-change": "explicitly removed by the project contract" |
| 805 | - }, |
| 806 | - "evidence": { |
| 807 | - "conformanceArtifact": "20260907T170116Z: 14 configured files, 17 tests passed, no skips (NIP-11 uses the quota-free compatibility test)", |
| 808 | - "runtimeChecks": "Full local race suite and go vet pass; full Linux race suite passes at artifacts/slate/tiny-private-git-final-20260907/20260907T165524Z. The final daemon policy changes also pass Linux race tests at artifacts/slate/tiny-git-policy-final-20260907/20260907T165928Z. Focused daemon tests cover mounted NIP-AD signed discovery and isolation, private Git authorization/restart, bounded authenticated upload spooling, tampered payload rejection, and callback recovery/revocation. Public Git performance covers six repositories at artifacts/git-performance/results-after; private nzip and Atlas pass separately at results-private-smoke and results-private-atlas.", |
| 809 | - "managementGolden": "internal/daemon/management_golden_test.go", |
| 810 | - "signedUXJourneys": "internal/daemon/ux_contract_test.go", |
| 811 | - "scope": "focused package tests do not close route, browser, restart, or unlisted conformance matrices" |
| 812 | - }, |
| 813 | - "exclusions": [ |
| 814 | - { |
| 815 | - "id": "change.route.lease", |
| 816 | - "source": "src/edge.ts:/lease", |
| 817 | - "category": "provisioning", |
| 818 | - "status": "intentional-change", |
| 819 | - "check": "tiny creates an owned tenant atomically" |
| 820 | - }, |
| 821 | - { |
| 822 | - "id": "change.management.claim", |
| 823 | - "source": "src/manage.ts:METHODS.claim", |
| 824 | - "category": "provisioning", |
| 825 | - "status": "intentional-change", |
| 826 | - "check": "relay ownership is assigned by tiny tenant create; membership invitation claims remain" |
| 827 | - }, |
| 828 | - { |
| 829 | - "id": "change.runtime.lease-lifecycle", |
| 830 | - "source": "src/settings.ts:Policy.lease;src/relay.ts:lease/alarm", |
| 831 | - "category": "provisioning", |
| 832 | - "status": "intentional-change", |
| 833 | - "check": "no lease expiry, claim conversion, trial retention or teardown" |
| 834 | - }, |
| 835 | - { |
| 836 | - "id": "change.route.fuel", |
| 837 | - "source": "src/routes.ts:/fuel,/fuel/invoice;src/fuel.ts", |
| 838 | - "category": "economics", |
| 839 | - "status": "intentional-change", |
| 840 | - "check": "fuel balances, invoices and fuel admission gates are absent; NIP-57 events remain supported" |
| 841 | - }, |
| 842 | - { |
| 843 | - "id": "change.runtime.product-capacity", |
| 844 | - "source": "src/fuel.ts;src/settings.ts:Policy limits", |
| 845 | - "category": "economics", |
| 846 | - "status": "intentional-change", |
| 847 | - "check": "no billing ledger, paid tier, monthly allowance or built-in tenant capacity entitlement" |
| 848 | - } |
| 849 | - ], |
| 850 | - "items": [ |
| 851 | - { |
| 852 | - "id": "route.git-smart-http", |
| 853 | - "source": "src/routes.ts:isGitPath", |
| 854 | - "category": "http", |
| 855 | - "status": "pending", |
| 856 | - "check": "GRASP Smart HTTP paths work when enabled" |
| 857 | - }, |
| 858 | - { |
| 859 | - "id": "route.web-address-discovery", |
| 860 | - "source": "src/routes.ts:isWebAddressRequest", |
| 861 | - "category": "http", |
| 862 | - "status": "verified", |
| 863 | - "check": "routes_acceptance_test.go rejects unsigned discovery; hosting_routes_acceptance_test.go verifies positive signed NIP-AD through mounted tenant routing and isolation; domains tests cover path validation and read denial" |
| 864 | - }, |
| 865 | - { |
| 866 | - "id": "route.nip11", |
| 867 | - "source": "src/routes.ts:Accept application/nostr+json", |
| 868 | - "category": "http", |
| 869 | - "status": "verified", |
| 870 | - "check": "routes_acceptance_test.go verifies mounted NIP-11 content negotiation and absence of removed fuel capability; full capability matrix remains in protocol items" |
| 871 | - }, |
| 872 | - { |
| 873 | - "id": "route.nip86", |
| 874 | - "source": "src/routes.ts:isManagementRequest", |
| 875 | - "category": "http", |
| 876 | - "status": "verified", |
| 877 | - "check": "routes_acceptance_test.go verifies signed and unsigned mounted NIP-86 requests" |
| 878 | - }, |
| 879 | - { |
| 880 | - "id": "route.bridge", |
| 881 | - "source": "src/routes.ts:/events,/query,/count", |
| 882 | - "category": "http", |
| 883 | - "status": "verified", |
| 884 | - "check": "routes_acceptance_test.go verifies signed query, unsigned denial, malformed request, and CORS preflight" |
| 885 | - }, |
| 886 | - { |
| 887 | - "id": "route.nip05", |
| 888 | - "source": "src/routes.ts:/.well-known/nostr.json", |
| 889 | - "category": "http", |
| 890 | - "status": "verified", |
| 891 | - "check": "routes_acceptance_test.go verifies mounted NIP-05 unknown-name response; named-user and directory visibility remain in hosting acceptance" |
| 892 | - }, |
| 893 | - { |
| 894 | - "id": "route.people", |
| 895 | - "source": "src/routes.ts:/people", |
| 896 | - "category": "http", |
| 897 | - "status": "pending", |
| 898 | - "check": "directory listing and self key match" |
| 899 | - }, |
| 900 | - { |
| 901 | - "id": "route.invites", |
| 902 | - "source": "src/routes.ts:/terms,/invite/*,/api/join-policy,/api/invites/claim", |
| 903 | - "category": "http", |
| 904 | - "status": "pending", |
| 905 | - "check": "membership invitation claim/join flow works; this is not relay leasing" |
| 906 | - }, |
| 907 | - { |
| 908 | - "id": "route.data", |
| 909 | - "source": "src/routes.ts:/dumps/*,/backups/*,/backups/restore,/backups/preview,/view/*,/import", |
| 910 | - "category": "http", |
| 911 | - "status": "pending", |
| 912 | - "check": "download, restore, preview, views and import preserve auth" |
| 913 | - }, |
| 914 | - { |
| 915 | - "id": "route.files", |
| 916 | - "source": "src/routes.ts:/upload,/mirror,/report,/list/*,blob paths,/.well-known/nostr/nip96.json,/nip96/*", |
| 917 | - "category": "http", |
| 918 | - "status": "pending", |
| 919 | - "check": "Blossom and NIP-96 behavior works on local object storage" |
| 920 | - }, |
| 921 | - { |
| 922 | - "id": "route.public-pages", |
| 923 | - "source": "src/routes.ts:/card.json,/card.nostr,/card.svg,/qr.svg,/connect.json,/* pages,/signer.js", |
| 924 | - "category": "http", |
| 925 | - "status": "pending", |
| 926 | - "check": "plain HTML site keeps all public tasks and handoffs" |
| 927 | - }, |
| 928 | - { |
| 929 | - "id": "route.cors", |
| 930 | - "source": "src/routes.ts:OPTIONS", |
| 931 | - "category": "http", |
| 932 | - "status": "pending", |
| 933 | - "check": "CORS preflight and content types match" |
| 934 | - }, |
| 935 | - { |
| 936 | - "id": "management.methods", |
| 937 | - "source": "src/manage.ts:METHODS", |
| 938 | - "category": "management", |
| 939 | - "status": "verified", |
| 940 | - "check": "management_golden_test invokes all 90 retained methods on fresh tenants without unsupported dispatch; ux_contract_test covers signed invite/member/moderation/config/identity/fork/delete and guided Sync journeys; browser and source-conformance coverage remain separate items" |
| 941 | - }, |
| 942 | - { |
| 943 | - "id": "relay.websocket", |
| 944 | - "source": "src/relay.ts", |
| 945 | - "category": "protocol", |
| 946 | - "status": "pending", |
| 947 | - "check": "NIP-01, NIP-42, filtering, EOSE, replaceables, deletes, expiry, protected events and reconnect behavior" |
| 948 | - }, |
| 949 | - { |
| 950 | - "id": "relay.query-search-sync", |
| 951 | - "source": "src/store.ts;src/filter.ts;src/negentropy.ts", |
| 952 | - "category": "protocol", |
| 953 | - "status": "pending", |
| 954 | - "check": "COUNT/HLL, NIP-50 modes, NIP-77 and visibility parity" |
| 955 | - }, |
| 956 | - { |
| 957 | - "id": "identity-groups", |
| 958 | - "source": "src/identity.ts;src/groups.ts;src/roles.ts;src/succession.ts", |
| 959 | - "category": "community", |
| 960 | - "status": "pending", |
| 961 | - "check": "NIP-29/43 identity, roles, invites, joins, leaves, pins, transfer and succession" |
| 962 | - }, |
| 963 | - { |
| 964 | - "id": "moderation", |
| 965 | - "source": "src/audit.ts;src/manage.ts;src/settings.ts", |
| 966 | - "category": "community", |
| 967 | - "status": "pending", |
| 968 | - "check": "reports, hide thresholds, bans/erase, blocked words, IP blocks, audit and pin flows" |
| 969 | - }, |
| 970 | - { |
| 971 | - "id": "delivery", |
| 972 | - "source": "src/delivery.ts;src/push.ts;src/notify.ts", |
| 973 | - "category": "automation", |
| 974 | - "status": "pending", |
| 975 | - "check": "NIP-65, NIP-9a, NIP-17/NIP-59 notifications, retries, deduplication and terminal status" |
| 976 | - }, |
| 977 | - { |
| 978 | - "id": "jobs", |
| 979 | - "source": "src/jobs.ts;src/pull.ts", |
| 980 | - "category": "automation", |
| 981 | - "status": "pending", |
| 982 | - "check": "durable_acceptance_test.go proves dump/view/notification recovery and replication_acceptance_test.go proves delivery retry across restart; backup jobs, inbox/outbox restart, and terminal failure metrics remain" |
| 983 | - }, |
| 984 | - { |
| 985 | - "id": "storage-files", |
| 986 | - "source": "src/blossom.ts;src/nip96.ts;src/site-mirror.ts", |
| 987 | - "category": "hosting", |
| 988 | - "status": "verified", |
| 989 | - "check": "nip96_test.go covers mounted upload/get/delete, sites_test.go covers verified mirror and manifest deletion, and the 17-test conformance artifact covers Blossom behavior" |
| 990 | - }, |
| 991 | - { |
| 992 | - "id": "hosting-sites", |
| 993 | - "source": "src/sites.ts;src/domains.ts;src/pages.ts;src/nipad.ts", |
| 994 | - "category": "hosting", |
| 995 | - "status": "pending", |
| 996 | - "check": "Mounted site/custom-host isolation and positive signed NIP-AD discovery pass; domains tests cover validation/auth and conformance covers NIP-5A. Combined pages/feed UX acceptance remains separate." |
| 997 | - }, |
| 998 | - { |
| 999 | - "id": "hosting-git", |
| 1000 | - "source": "src/grasp.ts;src/grasp-state.ts;src/git-storage.ts;src/git-sqlite.ts;src/git-objects.ts", |
| 1001 | - "category": "hosting", |
| 1002 | - "status": "pending", |
| 1003 | - "check": "Immediate 30-ref HTTP push, live hosting/read policy changes, native ref projection/deletion/recovery, private HTTP discovery and negotiation after restart, payload binding, six public repositories and private Atlas transfers pass. Full inventory/repaired private transfer after restart remains an acceptance scenario." |
| 1004 | - }, |
| 1005 | - { |
| 1006 | - "id": "hosting-marmot", |
| 1007 | - "source": "src/marmot.ts", |
| 1008 | - "category": "hosting", |
| 1009 | - "status": "pending", |
| 1010 | - "check": "KeyPackages, opaque MLS envelopes, recipient checks and templates" |
| 1011 | - }, |
| 1012 | - { |
| 1013 | - "id": "views", |
| 1014 | - "source": "src/views.ts", |
| 1015 | - "category": "derived-records", |
| 1016 | - "status": "pending", |
| 1017 | - "check": "all views, presence, schedules, fingerprints, audience privacy and run history" |
| 1018 | - }, |
| 1019 | - { |
| 1020 | - "id": "config", |
| 1021 | - "source": "src/config.ts;src/presets.ts;src/connections.ts", |
| 1022 | - "category": "configuration", |
| 1023 | - "status": "pending", |
| 1024 | - "check": "plan/apply/export, partial sections, presets, placeholders and visibility" |
| 1025 | - }, |
| 1026 | - { |
| 1027 | - "id": "backups-imports", |
| 1028 | - "source": "src/backups.ts;src/dumps.ts;src/imports.ts;src/git-backup.ts", |
| 1029 | - "category": "operations", |
| 1030 | - "status": "pending", |
| 1031 | - "check": "data_http_test.go and browser-restore-qa.json cover signed preview/restore, malformed retry, freshness, private state, files, and Git backup recovery; portable import progress and full cross-version format parity remain" |
| 1032 | - }, |
| 1033 | - { |
| 1034 | - "id": "ux.console", |
| 1035 | - "source": "src/console/console.html;src/console/console.js", |
| 1036 | - "category": "ux", |
| 1037 | - "status": "pending", |
| 1038 | - "check": "plain HTML/forms retain all ten tabs, roles, signers, validation, progress, recovery, QR and app handoff" |
| 1039 | - } |
| 1040 | - ], |
| 1041 | - "registryInventories": { |
| 1042 | - "managementMethods": { |
| 1043 | - "source": "src/manage.ts:METHODS", |
| 1044 | - "status": "pending", |
| 1045 | - "items": [ |
| 1046 | - "supportedmethods", |
| 1047 | - "listaudit", |
| 1048 | - "stats", |
| 1049 | - "getpolicy", |
| 1050 | - "setpolicy", |
| 1051 | - "listviews", |
| 1052 | - "banpubkey", |
| 1053 | - "setblockedwords", |
| 1054 | - "allowpubkey", |
| 1055 | - "setmember", |
| 1056 | - "unrulepubkey", |
| 1057 | - "removemember", |
| 1058 | - "listbannedpubkeys", |
| 1059 | - "listallowedpubkeys", |
| 1060 | - "listmembers", |
| 1061 | - "listpeople", |
| 1062 | - "createinvite", |
| 1063 | - "listinvites", |
| 1064 | - "revokeinvite", |
| 1065 | - "listclaims", |
| 1066 | - "listlisthistory", |
| 1067 | - "restorelist", |
| 1068 | - "createclaim", |
| 1069 | - "deleteclaim", |
| 1070 | - "removesubtree", |
| 1071 | - "banevent", |
| 1072 | - "allowevent", |
| 1073 | - "listeventsneedingmoderation", |
| 1074 | - "blockip", |
| 1075 | - "unblockip", |
| 1076 | - "listblockedips", |
| 1077 | - "listreports", |
| 1078 | - "resolvereport", |
| 1079 | - "exportconfig", |
| 1080 | - "importconfig", |
| 1081 | - "deleterelay", |
| 1082 | - "listblobs", |
| 1083 | - "listsites", |
| 1084 | - "deleteblob", |
| 1085 | - "listbannedevents", |
| 1086 | - "deleteevent", |
| 1087 | - "listrecentevents", |
| 1088 | - "searchevents", |
| 1089 | - "pinevent", |
| 1090 | - "unpinevent", |
| 1091 | - "listpins", |
| 1092 | - "allowkind", |
| 1093 | - "disallowkind", |
| 1094 | - "unrulekind", |
| 1095 | - "storagestats", |
| 1096 | - "gitstorage", |
| 1097 | - "setretention", |
| 1098 | - "listretention", |
| 1099 | - "purgekind", |
| 1100 | - "listallowedkinds", |
| 1101 | - "listblockedkinds", |
| 1102 | - "notifytest", |
| 1103 | - "resetrules", |
| 1104 | - "listpresets", |
| 1105 | - "listconnectiontemplates", |
| 1106 | - "listconnections", |
| 1107 | - "setconnections", |
| 1108 | - "applypreset", |
| 1109 | - "forkrelay", |
| 1110 | - "pullfrom", |
| 1111 | - "pullstatus", |
| 1112 | - "listjobs", |
| 1113 | - "deliverystatus", |
| 1114 | - "addjob", |
| 1115 | - "removejob", |
| 1116 | - "runjob", |
| 1117 | - "backfill", |
| 1118 | - "transferowner", |
| 1119 | - "listdumps", |
| 1120 | - "deletedump", |
| 1121 | - "dumpnow", |
| 1122 | - "backupnow", |
| 1123 | - "listbackups", |
| 1124 | - "deletebackup", |
| 1125 | - "setsuccession", |
| 1126 | - "clearsuccession", |
| 1127 | - "successionstatus", |
| 1128 | - "changerelayname", |
| 1129 | - "changerelaydescription", |
| 1130 | - "changerelayicon", |
| 1131 | - "adddomain", |
| 1132 | - "setdomainsite", |
| 1133 | - "checkdomain", |
| 1134 | - "removedomain", |
| 1135 | - "listdomains" |
| 1136 | - ] |
| 1137 | - }, |
| 1138 | - "relayTemplates": { |
| 1139 | - "source": "relay-templates/*.jsonc", |
| 1140 | - "status": "pending", |
| 1141 | - "items": [ |
| 1142 | - "01-default", |
| 1143 | - "02-outbox", |
| 1144 | - "03-inbox", |
| 1145 | - "04-private", |
| 1146 | - "05-chat", |
| 1147 | - "06-media", |
| 1148 | - "07-search", |
| 1149 | - "08-articles", |
| 1150 | - "09-dm", |
| 1151 | - "10-quiet", |
| 1152 | - "11-site", |
| 1153 | - "12-marmot", |
| 1154 | - "13-grasp", |
| 1155 | - "14-marmot-members", |
| 1156 | - "15-home" |
| 1157 | - ] |
| 1158 | - }, |
| 1159 | - "connectionTemplates": { |
| 1160 | - "source": "connection-templates/*.jsonc", |
| 1161 | - "status": "pending", |
| 1162 | - "items": [ |
| 1163 | - "01-notes", |
| 1164 | - "02-find-me", |
| 1165 | - "03-group", |
| 1166 | - "04-blog", |
| 1167 | - "05-repos", |
| 1168 | - "06-sites", |
| 1169 | - "07-bookmarks", |
| 1170 | - "08-photos", |
| 1171 | - "09-files", |
| 1172 | - "10-dm", |
| 1173 | - "11-marmot", |
| 1174 | - "12-relay-page" |
| 1175 | - ] |
| 1176 | - }, |
| 1177 | - "conformanceTests": { |
| 1178 | - "source": "test/conformance/*.test.ts", |
| 1179 | - "status": "verified", |
| 1180 | - "check": "artifacts/conformance/20260907T170116Z passes 17 tests across every configured source conformance family; nip11.test.ts is replaced by scripts/conformance/nip11.compat.test.ts for the quota-free self-hosted contract", |
| 1181 | - "items": [ |
| 1182 | - "nip01.test.ts", |
| 1183 | - "nip09.test.ts", |
| 1184 | - "nip11.test.ts", |
| 1185 | - "nip17.test.ts", |
| 1186 | - "nip40.test.ts", |
| 1187 | - "nip42.test.ts", |
| 1188 | - "nip45.test.ts", |
| 1189 | - "nip50.test.ts", |
| 1190 | - "nip62.test.ts", |
| 1191 | - "nip70.test.ts", |
| 1192 | - "nip77.test.ts", |
| 1193 | - "nip5a.test.ts", |
| 1194 | - "blossom.test.ts", |
| 1195 | - "grasp.test.ts" |
| 1196 | - ] |
| 1197 | - } |
| 1198 | - } |
| 1199 | -} |
| 1200 | diff --git a/docs/parity.md b/docs/parity.md |
| 1201 | deleted file mode 100644 |
| 1202 | index 6b70d12..0000000 |
| 1203 | --- a/docs/parity.md |
| 1204 | +++ /dev/null |
| 1205 | @@ -1,45 +0,0 @@ |
| 1206 | -# Tiny parity ledger |
| 1207 | - |
| 1208 | -`docs/parity.json` is the machine-readable completion ledger for the Go |
| 1209 | -implementation. Every retained item starts as `pending`; implementation work |
| 1210 | -may change it to `verified` only with a test or recorded acceptance check. |
| 1211 | - |
| 1212 | -The source baseline is bindws revision `5d31267`. The registry inventories are |
| 1213 | -deliberately explicit: all retained management methods, relay templates, |
| 1214 | -connection templates and black-box conformance files are listed. Broad family |
| 1215 | -items point to the source modules that must be decomposed into individual |
| 1216 | -ledger entries as work lands. |
| 1217 | - |
| 1218 | -The only intentional changes are the `/lease` and lease lifecycle, the relay |
| 1219 | -ownership `claim` method, fuel and billing endpoints, and hosted product |
| 1220 | -capacity entitlements. Membership |
| 1221 | -invitation claims (`createclaim`, `listclaims`, `deleteclaim` and invite join |
| 1222 | -flows) remain required. NIP-57 zap events remain ordinary supported Nostr |
| 1223 | -events; they do not create fuel credit. |
| 1224 | - |
| 1225 | -## Acceptance |
| 1226 | - |
| 1227 | -Run the black-box suite against a running local `tiny` instance with |
| 1228 | -`RELAY_URL=ws://host:port`. The conformance setup provisions a permanent, |
| 1229 | -owned tenant through `tiny tenant create` when configured. It never skips a |
| 1230 | -test because the relay has no `claim` method. A legacy bindws run is available |
| 1231 | -only with `LEGACY_CLAIM=1` for comparison. |
| 1232 | - |
| 1233 | -Full completion requires every retained ledger entry to be `verified`, every |
| 1234 | -conformance file to pass, and an end-to-end browser matrix covering the plain |
| 1235 | -HTML replacement for all ten console families: People, Moderation, Rules, |
| 1236 | -Identity, Connect, Data, Sync, Views, Health and Owner. The matrix includes |
| 1237 | -NIP-07 and NIP-46 signing, role authorization, validation and error states, |
| 1238 | -progress/recovery, backup/restore, QR and app handoff. Disabled features still |
| 1239 | -need an implementation and an enabled-path test. |
| 1240 | - |
| 1241 | -Current evidence includes the 20260907T170116Z artifact (14 configured files, |
| 1242 | -17 tests, no skips), passing `go test -race ./...` and `go vet ./...`, mounted |
| 1243 | -route acceptance tests, durable restart tests, distinct NIP-46 browser resume |
| 1244 | -evidence, browser backup restore/retry evidence, and six-repository Git |
| 1245 | -performance results. The ledger still separates those checks from the |
| 1246 | -remaining full route, browser navigation, restart coverage, and native Git |
| 1247 | -state semantics. |
| 1248 | - |
| 1249 | -No `skipped` status is permitted. Removed fuel/lease tests are explicit |
| 1250 | -negative tests for absence; all other bindws behavior remains required. |
| 1251 | diff --git a/docs/protocol-expansion.md b/docs/protocol-expansion.md |
| 1252 | deleted file mode 100644 |
| 1253 | index c65d5ba..0000000 |
| 1254 | --- a/docs/protocol-expansion.md |
| 1255 | +++ /dev/null |
| 1256 | @@ -1,49 +0,0 @@ |
| 1257 | -# Approved protocol and client expansion |
| 1258 | - |
| 1259 | -Approved 2026-09-07. Full retained bindws behavior and UX parity remains required. |
| 1260 | -BUD-05 media optimization and BUD-07 payments are excluded. Tenants remain |
| 1261 | -permanent, without trials, fuel, billing, or product resource entitlements. |
| 1262 | - |
| 1263 | -## Acceptance ledger |
| 1264 | - |
| 1265 | -An implementation is complete only after relevant mounted routes, permissions, |
| 1266 | -restart behavior, interoperability, and user journeys have evidence. Compilation |
| 1267 | -or a feature flag alone does not establish protocol support. |
| 1268 | - |
| 1269 | -| Work | Required outcome | Status | |
| 1270 | -| --- | --- | --- | |
| 1271 | -| GRASP-01 | Native streaming Git, signed refs, recovery and HTTP interoperability | In progress | |
| 1272 | -| GRASP-02/03 | Durable independent repository synchronization and complete collaboration filtering | In progress | |
| 1273 | -| GRASP-05/06 | Archive and alternative PR behavior with recoverable inventory | In progress | |
| 1274 | -| GRASP-08 | Private service boundary, authenticated metadata and Git, private peer replication | In progress | |
| 1275 | -| BUD-01/02/04/06/08/09/11/12 | Correct file retrieval, ownership, authorization, pagination, mirrors and reports | In progress | |
| 1276 | -| BUD-03/10 and NIP-B7/94/92 | Portable server lists, references, metadata and client fallback | In progress | |
| 1277 | -| NIP-34/22 | Repository activity, threads, statuses and GRASP server lists | In progress | |
| 1278 | -| NIP-65/77/67 | Inbox/outbox recovery and accurate scalable reconciliation | In progress | |
| 1279 | -| NIP-42/98/17/59/70 | Consistent authentication and private data access | In progress | |
| 1280 | -| NIP-43/56 | Membership requests and integrated moderation | In progress | |
| 1281 | -| NIP-11/66/86 | Truthful capabilities, optional peer monitoring, operational visibility | In progress | |
| 1282 | -| NIP-96 | Preserve upload/get/delete compatibility through shared file storage | In progress | |
| 1283 | - |
| 1284 | -## Contained client experience |
| 1285 | - |
| 1286 | -The same embedded site must provide repository discovery, branch/tag selection, |
| 1287 | -source trees, escaped source previews, downloads, history, commit diffs and |
| 1288 | -repository activity. File browsing includes pagination, metadata, previews, |
| 1289 | -downloads and existing signed management actions. Connect publishes owner-signed |
| 1290 | -service lists through the existing signer. Operational pages expose synchronization, |
| 1291 | -jobs, failures, storage, backups and capabilities with links to recovery actions. |
| 1292 | - |
| 1293 | -All routes share the tenant's current access policy; private data must not become |
| 1294 | -public through a browsing or download route. The site retains its plain HTML |
| 1295 | -foundation and fixed desktop/mobile variants. The user explicitly approved richer |
| 1296 | -Git/source components: repository tabs, branch selectors, breadcrumbs, file trees, |
| 1297 | -syntax highlighting, compact line numbers, colored diffs and clone controls. |
| 1298 | -Basic forms, locally served scripts and optional Fixi enhancement remain the |
| 1299 | -foundation. Native Git supplies object reads and diffs. Large |
| 1300 | -files are streamed for download and previews disclose truncation. |
| 1301 | - |
| 1302 | -Observability accompanies implementation: queue age, retries, last success, |
| 1303 | -subprocess time, memory and relay acknowledgement latency under mixed workloads. |
| 1304 | -Real repository performance fixtures on Slate remain regression evidence; tail |
| 1305 | -latency is still an investigation target. |
| 1306 | diff --git a/docs/testing.md b/docs/testing.md |
| 1307 | index 3f81c4b..0e29914 100644 |
| 1308 | --- a/docs/testing.md |
| 1309 | +++ b/docs/testing.md |
| 1310 | @@ -47,7 +47,7 @@ The conformance harness runs the copied bindws-compatible cases against a runnin |
| 1311 | |
| 1312 | ## Real Git repositories |
| 1313 | |
| 1314 | -The [Git performance report](git-performance.md) records the six-repository Slate experiment. Its HTML version and raw profiles live in `artifacts/git-performance/`; regenerate both reports with `python3 scripts/report-git-performance.py`. |
| 1315 | +The Git performance harness below records raw profiles and results under `artifacts/git-performance/`. |
| 1316 | |
| 1317 | Prepare a new corpus from committed local branches and tags. This leaves the source repositories unchanged and refuses an existing output directory: |
| 1318 | |
| 1319 | diff --git a/package.json b/package.json |
| 1320 | index 88cde9e..9773144 100644 |
| 1321 | --- a/package.json |
| 1322 | +++ b/package.json |
| 1323 | @@ -6,8 +6,7 @@ |
| 1324 | "build:signer": "node scripts/build-signer.mjs", |
| 1325 | "test:conformance": "node scripts/conformance/run.mjs", |
| 1326 | "test:webmcp": "node --test scripts/webmcp.test.mjs", |
| 1327 | - "test:signer": "node --test scripts/signer.test.mjs", |
| 1328 | - "check:parity": "node scripts/conformance/check-parity.mjs" |
| 1329 | + "test:signer": "node --test scripts/signer.test.mjs" |
| 1330 | }, |
| 1331 | "devDependencies": { |
| 1332 | "@noble/hashes": "2.4.0", |
| 1333 | diff --git a/scripts/conformance/README.md b/scripts/conformance/README.md |
| 1334 | index 27d7249..26c2668 100644 |
| 1335 | --- a/scripts/conformance/README.md |
| 1336 | +++ b/scripts/conformance/README.md |
| 1337 | @@ -16,10 +16,4 @@ For comparison only, `LEGACY_CLAIM=1` invokes bindws's old NIP-86 `claim` |
| 1338 | setup. This compatibility path is not a tiny requirement and does not skip or |
| 1339 | alter test assertions. |
| 1340 | |
| 1341 | -Check that the machine-readable registry still matches the bindws source with: |
| 1342 | - |
| 1343 | -```sh |
| 1344 | -node scripts/conformance/check-parity.mjs |
| 1345 | -``` |
| 1346 | - |
| 1347 | -Each run preserves `environment.txt`, `stdout.log`, `stderr.log`, and `status` under `artifacts/conformance/<UTC-stamp>/`. Set `CONFORMANCE_ARTIFACTS` to choose another destination. The source conformance files remain unchanged. The hosted NIP-11 file is replaced in this harness by `nip11.compat.test.ts`, which preserves every assertion except the numeric `limitation.max_limit` requirement because self-hosted relays have no hosted quota. Other self-hosting differences belong in separately named compatibility tests or an explicit parity ledger entry. |
| 1348 | +Each run preserves `environment.txt`, `stdout.log`, `stderr.log`, and `status` under `artifacts/conformance/<UTC-stamp>/`. Set `CONFORMANCE_ARTIFACTS` to choose another destination. The source conformance files remain unchanged. The hosted NIP-11 file is replaced in this harness by `nip11.compat.test.ts`, which preserves every assertion except the numeric `limitation.max_limit` requirement because self-hosted relays have no hosted quota. Other self-hosting differences belong in separately named compatibility tests. |
| 1349 | diff --git a/scripts/conformance/check-parity.mjs b/scripts/conformance/check-parity.mjs |
| 1350 | deleted file mode 100644 |
| 1351 | index e515cbb..0000000 |
| 1352 | --- a/scripts/conformance/check-parity.mjs |
| 1353 | +++ /dev/null |
| 1354 | @@ -1,31 +0,0 @@ |
| 1355 | -#!/usr/bin/env node |
| 1356 | -import { readFileSync, readdirSync } from "node:fs"; |
| 1357 | -import { resolve } from "node:path"; |
| 1358 | - |
| 1359 | -const root = resolve(import.meta.dirname, "../.."); |
| 1360 | -const sourceRoot = resolve(process.env.BINDWS_ROOT ?? resolve(root, "../bindws")); |
| 1361 | -const ledger = JSON.parse(readFileSync(resolve(root, "docs/parity.json"), "utf8")); |
| 1362 | -const fail = (message) => { throw new Error(`parity ledger: ${message}`); }; |
| 1363 | - |
| 1364 | -if (!/^[0-9a-f]{7,40}$/.test(ledger.source.revision)) fail("source revision is missing"); |
| 1365 | -for (const item of [...ledger.items, ...ledger.exclusions]) { |
| 1366 | - if (!item.id || !item.source || !item.status || !item.check) fail(`incomplete item ${item.id ?? "<unnamed>"}`); |
| 1367 | - if (!new Set(["pending", "verified", "intentional-change"]).has(item.status)) fail(`invalid status for ${item.id}`); |
| 1368 | -} |
| 1369 | - |
| 1370 | -const manage = readFileSync(resolve(sourceRoot, "src/manage.ts"), "utf8").split("export const METHODS", 2)[1]; |
| 1371 | -const methods = [...manage.matchAll(/^ ([A-Za-z][A-Za-z0-9_]*):/gm)].map((m) => m[1]); |
| 1372 | -const listedMethods = ledger.registryInventories.managementMethods.items; |
| 1373 | -const missingMethods = methods.filter((name) => name !== "claim" && !listedMethods.includes(name)); |
| 1374 | -const extraMethods = listedMethods.filter((name) => !methods.includes(name)); |
| 1375 | -if (missingMethods.length || extraMethods.length) fail(`management registry drift missing=${missingMethods} extra=${extraMethods}`); |
| 1376 | - |
| 1377 | -const compareFiles = (key, directory, suffix) => { |
| 1378 | - const actual = readdirSync(resolve(sourceRoot, directory)).filter((name) => name.endsWith(suffix)).sort(); |
| 1379 | - const listed = [...ledger.registryInventories[key].items].sort().map((name) => key === "conformanceTests" ? name : `${name}${suffix}`); |
| 1380 | - if (actual.join("\n") !== listed.join("\n")) fail(`${key} drift`); |
| 1381 | -}; |
| 1382 | -compareFiles("relayTemplates", "relay-templates", ".jsonc"); |
| 1383 | -compareFiles("connectionTemplates", "connection-templates", ".jsonc"); |
| 1384 | -compareFiles("conformanceTests", "test/conformance", ".test.ts"); |
| 1385 | -console.log(`parity ledger OK: ${ledger.items.length} retained families, ${listedMethods.length} retained methods`); |
| 1386 | diff --git a/scripts/report-git-performance.py b/scripts/report-git-performance.py |
| 1387 | deleted file mode 100755 |
| 1388 | index 10e2843..0000000 |
| 1389 | --- a/scripts/report-git-performance.py |
| 1390 | +++ /dev/null |
| 1391 | @@ -1,468 +0,0 @@ |
| 1392 | -#!/usr/bin/env python3 |
| 1393 | -"""Render reproducible Git benchmark artifacts as a plain HTML/Markdown report.""" |
| 1394 | - |
| 1395 | -import argparse |
| 1396 | -import datetime as dt |
| 1397 | -import html |
| 1398 | -from functools import lru_cache |
| 1399 | -import json |
| 1400 | -import math |
| 1401 | -import pathlib |
| 1402 | -import statistics |
| 1403 | - |
| 1404 | - |
| 1405 | -PHASES = [ |
| 1406 | - ("initial push", "initial_push"), |
| 1407 | - ("clone ×3 median", "fresh_clone_"), |
| 1408 | - ("fetch", "noop_fetch_"), |
| 1409 | - ("incremental state ACK", "incremental_state_ack"), |
| 1410 | - ("incremental push", "incremental_push"), |
| 1411 | - ("concurrent ×2 wall", "concurrent_clone_2"), |
| 1412 | - ("concurrent ×4 wall", "concurrent_clone_4"), |
| 1413 | -] |
| 1414 | - |
| 1415 | - |
| 1416 | -def read_json(path): |
| 1417 | - try: |
| 1418 | - return json.loads(path.read_text(encoding="utf-8")) |
| 1419 | - except (OSError, json.JSONDecodeError): |
| 1420 | - return None |
| 1421 | - |
| 1422 | - |
| 1423 | -def raw(value): |
| 1424 | - return "—" if value is None else str(value) |
| 1425 | - |
| 1426 | - |
| 1427 | -def display(value, kind="number"): |
| 1428 | - if value is None: |
| 1429 | - return "—" |
| 1430 | - if isinstance(value, str): |
| 1431 | - if value.startswith("Command failed") or value.startswith("Nostr request") or value.startswith("error:"): |
| 1432 | - return "FAILED: " + value.splitlines()[0][:160] if value else "FAILED" |
| 1433 | - return value |
| 1434 | - if kind == "mib": |
| 1435 | - value = value / (1024 * 1024) |
| 1436 | - if kind == "kb_mib": |
| 1437 | - value = value / 1024 |
| 1438 | - if isinstance(value, (int, float)): |
| 1439 | - magnitude = abs(value) |
| 1440 | - if magnitude >= 1000: |
| 1441 | - return f"{value:,.0f}" |
| 1442 | - if magnitude >= 100: |
| 1443 | - return f"{value:,.1f}" |
| 1444 | - return f"{value:.3g}" |
| 1445 | - return str(value) |
| 1446 | - |
| 1447 | - |
| 1448 | -def median(values): |
| 1449 | - return statistics.median(values) if values else None |
| 1450 | - |
| 1451 | - |
| 1452 | -def percentile(values, p): |
| 1453 | - if not values: |
| 1454 | - return None |
| 1455 | - values = sorted(values) |
| 1456 | - index = (len(values) - 1) * p |
| 1457 | - low, high = math.floor(index), math.ceil(index) |
| 1458 | - if low == high: |
| 1459 | - return values[low] |
| 1460 | - return values[low] + (values[high] - values[low]) * (index - low) |
| 1461 | - |
| 1462 | - |
| 1463 | -def parse_stat(text): |
| 1464 | - result = {} |
| 1465 | - if not text: |
| 1466 | - return result |
| 1467 | - for line in text.splitlines(): |
| 1468 | - parts = line.split() |
| 1469 | - if len(parts) >= 2: |
| 1470 | - try: |
| 1471 | - result[parts[0]] = int(parts[1]) |
| 1472 | - except ValueError: |
| 1473 | - continue |
| 1474 | - return result |
| 1475 | - |
| 1476 | - |
| 1477 | -def phase_value(phases, prefix, baseline=False): |
| 1478 | - rows = [] |
| 1479 | - for row in phases: |
| 1480 | - name = row.get("name", "") |
| 1481 | - if baseline != name.startswith("baseline_"): |
| 1482 | - continue |
| 1483 | - candidate = name[len("baseline_"):] if baseline else name |
| 1484 | - if candidate == prefix or (prefix.endswith("_") and candidate.startswith(prefix)): |
| 1485 | - if row.get("ok") is False: |
| 1486 | - return {"error": row.get("error", "failed")} |
| 1487 | - rows.append(row.get("ms")) |
| 1488 | - if prefix.endswith("_"): |
| 1489 | - return median([v for v in rows if isinstance(v, (int, float))]) |
| 1490 | - return rows[0] if rows else None |
| 1491 | - |
| 1492 | - |
| 1493 | -def method_name(set_name): |
| 1494 | - lower = set_name.lower() |
| 1495 | - if "private" in lower: |
| 1496 | - return "private Tiny (NIP-98)" |
| 1497 | - if "stream" in lower or "after" in lower: |
| 1498 | - return "streaming Tiny" |
| 1499 | - if "native" in lower: |
| 1500 | - return "native Git HTTP baseline" |
| 1501 | - return "buffered Tiny" |
| 1502 | - |
| 1503 | - |
| 1504 | -@lru_cache(maxsize=None) |
| 1505 | -def load_samples(directory): |
| 1506 | - path = directory / "samples.jsonl" |
| 1507 | - rows = [] |
| 1508 | - try: |
| 1509 | - for line in path.read_text(encoding="utf-8").splitlines(): |
| 1510 | - try: |
| 1511 | - rows.append(json.loads(line)) |
| 1512 | - except json.JSONDecodeError: |
| 1513 | - pass |
| 1514 | - except OSError: |
| 1515 | - pass |
| 1516 | - return rows |
| 1517 | - |
| 1518 | - |
| 1519 | -def fixture_bounds(directory, fixture): |
| 1520 | - starts = ends = None |
| 1521 | - all_starts = [] |
| 1522 | - try: |
| 1523 | - for line in (directory / "progress.jsonl").read_text(encoding="utf-8").splitlines(): |
| 1524 | - row = json.loads(line) |
| 1525 | - if row.get("event") == "fixture_start": |
| 1526 | - all_starts.append((row.get("fixture"), row.get("at"))) |
| 1527 | - if row.get("fixture") == fixture: |
| 1528 | - starts = row.get("at") |
| 1529 | - elif row.get("event") == "fixture_end": |
| 1530 | - if row.get("fixture") == fixture: |
| 1531 | - ends = row.get("at") |
| 1532 | - except (OSError, json.JSONDecodeError): |
| 1533 | - pass |
| 1534 | - if starts and not ends: |
| 1535 | - later = [stamp for name, stamp in all_starts if stamp and stamp > starts] |
| 1536 | - if later: |
| 1537 | - ends = min(later) |
| 1538 | - return starts, ends |
| 1539 | - |
| 1540 | - |
| 1541 | -def resource_summary(directory, metadata, fixture, baseline=False): |
| 1542 | - process_status = directory / "process.status" |
| 1543 | - text = process_status.read_text(encoding="utf-8") if process_status.exists() else "" |
| 1544 | - vmhwm = None |
| 1545 | - for line in text.splitlines(): |
| 1546 | - if line.startswith("VmHWM:"): |
| 1547 | - try: |
| 1548 | - vmhwm = int(line.split()[1]) |
| 1549 | - except (ValueError, IndexError): |
| 1550 | - pass |
| 1551 | - containers = metadata.get("containers", []) |
| 1552 | - container = containers[1] if baseline and len(containers) > 1 else (containers[0] if containers else None) |
| 1553 | - fixture_start, fixture_end = fixture_bounds(directory, fixture) |
| 1554 | - try: |
| 1555 | - start_time = dt.datetime.fromisoformat(fixture_start.replace("Z", "+00:00")).timestamp() if fixture_start else None |
| 1556 | - end_time = dt.datetime.fromisoformat(fixture_end.replace("Z", "+00:00")).timestamp() if fixture_end else None |
| 1557 | - except (TypeError, ValueError): |
| 1558 | - start_time = end_time = None |
| 1559 | - samples = [] |
| 1560 | - for row in load_samples(directory): |
| 1561 | - try: |
| 1562 | - row_time = dt.datetime.fromisoformat((row.get("time") or "").replace("Z", "+00:00")).timestamp() |
| 1563 | - except (TypeError, ValueError): |
| 1564 | - row_time = None |
| 1565 | - if start_time is not None and row_time is not None and row_time < start_time: |
| 1566 | - continue |
| 1567 | - if end_time is not None and row_time is not None and row_time > end_time: |
| 1568 | - continue |
| 1569 | - for item in row.get("containers", []): |
| 1570 | - if item.get("container") == container: |
| 1571 | - mem = parse_stat(item.get("memory.stat")) |
| 1572 | - for line in (item.get("process.status") or "").splitlines(): |
| 1573 | - if line.startswith("VmHWM:"): |
| 1574 | - try: |
| 1575 | - vmhwm = max(vmhwm or 0, int(line.split()[1])) |
| 1576 | - except (ValueError, IndexError): |
| 1577 | - pass |
| 1578 | - samples.append({ |
| 1579 | - "time": item.get("time") or row.get("time"), |
| 1580 | - "cpu": parse_stat(item.get("cpu.stat")).get("usage_usec"), |
| 1581 | - "anon": mem.get("anon"), "file": mem.get("file"), |
| 1582 | - "total": item.get("memory.peak"), |
| 1583 | - }) |
| 1584 | - values = lambda key: [x[key] for x in samples if isinstance(x.get(key), (int, float))] |
| 1585 | - return {"vmhwm_kb": vmhwm, "vmhwm_scope": "sampled process status", "container": container, |
| 1586 | - "anon_peak": max(values("anon"), default=None), |
| 1587 | - "file_peak": max(values("file"), default=None), |
| 1588 | - "total_peak": max(values("total"), default=None), |
| 1589 | - "cpu_usec": max(values("cpu"), default=None)} |
| 1590 | - |
| 1591 | - |
| 1592 | -def run_vmhwm(directory, metadata, baseline=False): |
| 1593 | - containers = metadata.get("containers", []) |
| 1594 | - target = containers[1] if baseline and len(containers) > 1 else (containers[0] if containers else None) |
| 1595 | - peak = None |
| 1596 | - for row in load_samples(directory): |
| 1597 | - for item in row.get("containers", []): |
| 1598 | - if item.get("container") != target: |
| 1599 | - continue |
| 1600 | - for line in (item.get("process.status") or "").splitlines(): |
| 1601 | - if line.startswith("VmHWM:"): |
| 1602 | - try: |
| 1603 | - peak = max(peak or 0, int(line.split()[1])) |
| 1604 | - except (ValueError, IndexError): |
| 1605 | - pass |
| 1606 | - return peak |
| 1607 | - |
| 1608 | - |
| 1609 | -def phase_cpu(directory, metadata, phase): |
| 1610 | - samples = load_samples(directory) |
| 1611 | - containers = metadata.get("containers", []) |
| 1612 | - target = containers[0] if containers else None |
| 1613 | - if phase.get("name", "").startswith("baseline_") and len(containers) > 1: |
| 1614 | - target = containers[1] |
| 1615 | - points = [] |
| 1616 | - for row in samples: |
| 1617 | - for item in row.get("containers", []): |
| 1618 | - if item.get("container") == target: |
| 1619 | - usage = parse_stat(item.get("cpu.stat")).get("usage_usec") |
| 1620 | - if usage is not None: |
| 1621 | - try: |
| 1622 | - stamp_raw = item.get("time") or row.get("time") |
| 1623 | - if not stamp_raw: |
| 1624 | - continue |
| 1625 | - stamp = dt.datetime.fromisoformat(stamp_raw.replace("Z", "+00:00")).timestamp() |
| 1626 | - points.append((stamp, usage)) |
| 1627 | - except (KeyError, TypeError, ValueError): |
| 1628 | - pass |
| 1629 | - if len(points) < 2: |
| 1630 | - return None |
| 1631 | - try: |
| 1632 | - start = dt.datetime.fromisoformat(phase["started"].replace("Z", "+00:00")).timestamp() |
| 1633 | - end = dt.datetime.fromisoformat(phase["finished"].replace("Z", "+00:00")).timestamp() |
| 1634 | - except (KeyError, TypeError, ValueError): |
| 1635 | - return None |
| 1636 | - before = min(points, key=lambda p: abs(p[0] - start)) |
| 1637 | - after = min(points, key=lambda p: abs(p[0] - end)) |
| 1638 | - if abs(before[0] - start) > 0.5 or abs(after[0] - end) > 0.5: |
| 1639 | - return None |
| 1640 | - if after[0] <= before[0]: |
| 1641 | - return None |
| 1642 | - return after[1] - before[1] |
| 1643 | - |
| 1644 | - |
| 1645 | -def probe_summary(result, baseline=False): |
| 1646 | - idle_publish, idle_query, busy_publish, busy_query = [], [], [], [] |
| 1647 | - idle_errors = busy_errors = 0 |
| 1648 | - for probe in result.get("probes", []): |
| 1649 | - phase = probe.get("phase", "") |
| 1650 | - is_baseline = phase.startswith("baseline_") |
| 1651 | - if is_baseline != baseline or phase == "between_phases": |
| 1652 | - continue |
| 1653 | - publish_target, query_target = (idle_publish, idle_query) if phase == "idle" else (busy_publish, busy_query) |
| 1654 | - if isinstance(probe.get("publishMs"), (int, float)): |
| 1655 | - publish_target.append(probe["publishMs"]) |
| 1656 | - if isinstance(probe.get("queryMs"), (int, float)): |
| 1657 | - query_target.append(probe["queryMs"]) |
| 1658 | - if any(key.endswith("Error") for key in probe): |
| 1659 | - if phase == "idle": |
| 1660 | - idle_errors += 1 |
| 1661 | - else: |
| 1662 | - busy_errors += 1 |
| 1663 | - summary = [] |
| 1664 | - for values in (idle_publish, idle_query, busy_publish, busy_query): |
| 1665 | - summary.extend((median(values), percentile(values, 0.95), max(values, default=None))) |
| 1666 | - return (*summary, idle_errors, busy_errors) |
| 1667 | - |
| 1668 | - |
| 1669 | -def cells_for_result(directory, result, fixture, baseline=False): |
| 1670 | - values = [] |
| 1671 | - for label, prefix in PHASES: |
| 1672 | - value = phase_value(result.get("phases", []), prefix, baseline) |
| 1673 | - if isinstance(value, dict): |
| 1674 | - values.append(value["error"]) |
| 1675 | - else: |
| 1676 | - values.append(value) |
| 1677 | - meta = read_json(directory / "metadata.json") or {} |
| 1678 | - resources = resource_summary(directory, meta, fixture, baseline) |
| 1679 | - probes = probe_summary(result, baseline) |
| 1680 | - clone_n = sum(1 for row in result.get("phases", []) if row.get("name", "") == ("baseline_" if baseline else "") + "fresh_clone_1" or row.get("name", "").startswith(("baseline_" if baseline else "") + "fresh_clone_")) |
| 1681 | - return values, resources, probes, clone_n |
| 1682 | - |
| 1683 | - |
| 1684 | -def finding_phase(result, prefix): |
| 1685 | - if not result: |
| 1686 | - return None |
| 1687 | - value = phase_value(result.get("phases", []), prefix, False) |
| 1688 | - return value.get("error") if isinstance(value, dict) else value |
| 1689 | - |
| 1690 | - |
| 1691 | -def table(headers, rows, classes="", kinds=None): |
| 1692 | - out = [f'<table class="{classes}"><thead><tr>'] |
| 1693 | - out.extend(f"<th>{html.escape(str(h))}</th>" for h in headers) |
| 1694 | - out.append("</tr></thead><tbody>") |
| 1695 | - for row in rows: |
| 1696 | - out.append("<tr>" + "".join(f"<td>{html.escape(display(x, kinds[i] if kinds and i < len(kinds) else 'number'))}</td>" for i, x in enumerate(row)) + "</tr>") |
| 1697 | - out.append("</tbody></table>") |
| 1698 | - rendered = "\n".join(out) |
| 1699 | - if classes == "wide" and len(headers) > 4: |
| 1700 | - rendered = rendered.replace('class="wide"', 'class="desktop"', 1) |
| 1701 | - mobile = ['<table class="mobile"><tbody>'] |
| 1702 | - for row in rows: |
| 1703 | - mobile.append('<tr><th colspan="2">' + html.escape(str(row[0])) + '</th></tr>') |
| 1704 | - for i, value in enumerate(row[1:], 1): |
| 1705 | - kind = kinds[i] if kinds and i < len(kinds) else "number" |
| 1706 | - mobile.append('<tr><th>' + html.escape(str(headers[i])) + '</th><td>' + html.escape(display(value, kind)) + '</td></tr>') |
| 1707 | - mobile.append('</tbody></table>') |
| 1708 | - rendered += "\n" + "\n".join(mobile) |
| 1709 | - return rendered |
| 1710 | - |
| 1711 | - |
| 1712 | -def markdown_table(headers, rows, kinds=None): |
| 1713 | - lines = ["| " + " | ".join(headers) + " |", "| " + " | ".join("---" for _ in headers) + " |"] |
| 1714 | - for row in rows: |
| 1715 | - cells = [display(x, kinds[i] if kinds and i < len(kinds) else "number") for i, x in enumerate(row)] |
| 1716 | - lines.append("| " + " | ".join(c.replace("|", "\\|") for c in cells) + " |") |
| 1717 | - return "\n".join(lines) |
| 1718 | - |
| 1719 | - |
| 1720 | -def main(): |
| 1721 | - parser = argparse.ArgumentParser() |
| 1722 | - parser.add_argument("--root", type=pathlib.Path, default=pathlib.Path("artifacts/git-performance")) |
| 1723 | - parser.add_argument("--html", type=pathlib.Path) |
| 1724 | - parser.add_argument("--markdown", type=pathlib.Path, default=pathlib.Path("docs/git-performance.md")) |
| 1725 | - args = parser.parse_args() |
| 1726 | - root = args.root |
| 1727 | - html_path = args.html or root / "git-performance.html" |
| 1728 | - corpus = read_json(root / "corpus.json") or [] |
| 1729 | - sets = [] |
| 1730 | - allowed_sets = {"results-before", "results-after", "results-before-atlas-concurrency", "results-private-atlas"} |
| 1731 | - for directory in sorted(root.glob("results-*")): |
| 1732 | - if directory.name not in allowed_sets: |
| 1733 | - continue |
| 1734 | - report = read_json(directory / "git-performance.json") |
| 1735 | - if not report: |
| 1736 | - continue |
| 1737 | - meta = read_json(directory / "metadata.json") or {} |
| 1738 | - sets.append((directory, report, meta)) |
| 1739 | - phase_rows, resource_rows, probe_rows, status_rows = [], [], [], [] |
| 1740 | - findings_rows = [] |
| 1741 | - highlight_rows = [] |
| 1742 | - raw_links = [] |
| 1743 | - raw_seen = set() |
| 1744 | - for directory, report, meta in sets: |
| 1745 | - for result in report.get("results", []): |
| 1746 | - fixture = result.get("fixture", "?") |
| 1747 | - failed = [p.get("name", "?") for p in result.get("phases", []) if p.get("ok") is False] |
| 1748 | - status_rows.append([fixture, method_name(directory.name), "failed" if failed or result.get("status") not in (None, "complete", "ok", "passed") else "complete", ", ".join(failed) or "—"]) |
| 1749 | - for baseline in (False, True): |
| 1750 | - if baseline and not any(p.get("name", "").startswith("baseline_") for p in result.get("phases", [])): |
| 1751 | - continue |
| 1752 | - method = "native Git HTTP (" + directory.name.removeprefix("results-") + ")" if baseline else method_name(directory.name) |
| 1753 | - values, resources, probes, clone_n = cells_for_result(directory, result, fixture, baseline) |
| 1754 | - phase_rows.append([fixture, method, clone_n, *values]) |
| 1755 | - phase_rows_raw = result.get("phases", []) |
| 1756 | - cpu_values = [] |
| 1757 | - for prefix in ("initial_push", "incremental_push", "concurrent_clone_2", "concurrent_clone_4"): |
| 1758 | - match = next((p for p in phase_rows_raw if (p.get("name", "").removeprefix("baseline_") == prefix) and (p.get("name", "").startswith("baseline_") == baseline)), None) |
| 1759 | - cpu_values.append(phase_cpu(directory, meta, match) if match else None) |
| 1760 | - resource_rows.append([fixture, method, resources["vmhwm_kb"], resources["anon_peak"], resources["file_peak"], resources["total_peak"], *cpu_values, resources["container"]]) |
| 1761 | - probe_rows.append([fixture, method, *probes]) |
| 1762 | - raw_path = directory / "git-performance.json" |
| 1763 | - if raw_path not in raw_seen: |
| 1764 | - raw_links.append((directory.name, raw_path)) |
| 1765 | - raw_seen.add(raw_path) |
| 1766 | - corpus_rows = [[x.get("name"), x.get("all_commits"), x.get("refs"), x.get("pack_bytes")] for x in corpus] |
| 1767 | - set_by_name = {directory.name: (directory, report, meta) for directory, report, meta in sets} |
| 1768 | - before_set = set_by_name.get("results-before") |
| 1769 | - after_set = set_by_name.get("results-after") |
| 1770 | - before_mem = run_vmhwm(before_set[0], before_set[2]) if before_set else None |
| 1771 | - after_mem = run_vmhwm(after_set[0], after_set[2]) if after_set else None |
| 1772 | - highlight_rows.append(["Peak Go memory (MiB)", before_mem / 1024 if before_mem is not None else None, after_mem / 1024 if after_mem is not None else None]) |
| 1773 | - result_maps = {} |
| 1774 | - for label, item in (("before", before_set), ("after", after_set)): |
| 1775 | - if item: |
| 1776 | - result_maps[label] = {r.get("fixture"): r for r in item[1].get("results", [])} |
| 1777 | - for fixture in sorted(set(result_maps.get("before", {})) | set(result_maps.get("after", {}))): |
| 1778 | - row = [fixture] |
| 1779 | - for label in ("before", "after"): |
| 1780 | - result = result_maps.get(label, {}).get(fixture) |
| 1781 | - row.append(finding_phase(result, "fresh_clone_")) |
| 1782 | - row.append(finding_phase(result, "concurrent_clone_4")) |
| 1783 | - row.append(finding_phase(result, "incremental_state_ack")) |
| 1784 | - row.append(finding_phase(result, "incremental_push")) |
| 1785 | - findings_rows.append(row) |
| 1786 | - before_strudel = result_maps.get("before", {}).get("strudel.git") |
| 1787 | - after_strudel = result_maps.get("after", {}).get("strudel.git") |
| 1788 | - highlight_rows.append(["Tag-heavy strudel incremental push (ms)", finding_phase(before_strudel, "incremental_push"), finding_phase(after_strudel, "incremental_push")]) |
| 1789 | - headers = ["repo", "method", "clone n"] + [(("clone median" if x[1] == "fresh_clone_" else x[0]) + (" ms" if x[1] != "fresh_clone_" else " ms")) for x in PHASES] |
| 1790 | - html_doc = '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Git performance</title><style>body{font:14px system-ui,sans-serif;width:360px;margin:1rem auto;line-height:1.45}table{border-collapse:collapse;margin:1rem 0;width:100%;table-layout:fixed}th,td{border:1px solid;padding:.4rem;text-align:left;overflow-wrap:anywhere}summary{cursor:pointer;font-weight:bold}.desktop{display:none}.mobile{display:table}@media(min-width:1160px){body{width:1120px}.desktop{display:table}.mobile{display:none}}</style></head><body>' |
| 1791 | - html_doc += '<h1>Git performance</h1><p>Six repositories from ~/Developer, tested on Slate. All six completed public push, clone, fetch and concurrent clone workloads after the fixes. Every background relay probe succeeded.</p><p>Git responses now stream as they are produced. Ref authorization and object checks use batches, and idle connections stay alive through WebSocket pings.</p>' |
| 1792 | - html_doc += table(["finding", "before", "after"], highlight_rows, "wide", ["number", "number", "number"]) |
| 1793 | - clone_rows = [] |
| 1794 | - for fixture, result in result_maps.get("after", {}).items(): |
| 1795 | - before_result = result_maps.get("before", {}).get(fixture) |
| 1796 | - native = phase_value(result.get("phases", []), "fresh_clone_", True) |
| 1797 | - values = [finding_phase(before_result, "fresh_clone_"), finding_phase(result, "fresh_clone_"), native] |
| 1798 | - clone_rows.append([fixture.removesuffix(".git"), *[f"{v / 1000:.2f}" if isinstance(v, (int, float)) else "—" for v in values]]) |
| 1799 | - html_doc += "<h2>Clone comparison</h2><p>Median of three fresh clones, in seconds. Git is the native HTTP baseline measured alongside the after run.</p>" |
| 1800 | - html_doc += table(["Repository", "Before", "After", "Git"], clone_rows) |
| 1801 | - private_set = set_by_name.get("results-private-atlas") |
| 1802 | - tail_rows = [] |
| 1803 | - public_atlas = result_maps.get("after", {}).get("atlas.git") |
| 1804 | - if public_atlas: |
| 1805 | - for label, baseline in (("Public Atlas", False), ("Native Git Atlas phases", True)): |
| 1806 | - probes = probe_summary(public_atlas, baseline) |
| 1807 | - tail_rows.append([label, probes[7], probes[8], probes[10], probes[11]]) |
| 1808 | - if private_set: |
| 1809 | - for result in private_set[1].get("results", []): |
| 1810 | - probes = probe_summary(result) |
| 1811 | - tail_rows.append(["Private " + result.get("fixture", "?"), probes[7], probes[8], probes[10], probes[11]]) |
| 1812 | - if tail_rows: |
| 1813 | - html_doc += "<h2>Remaining relay latency</h2><p>All probes succeeded, but durable event acknowledgements still showed occasional stalls under large Git transfers. Similar stalls occurred while the native Git baseline was busy on the same host. The comparison does not establish the cause; disk latency and scheduling need a focused capture before choosing another optimization.</p>" |
| 1814 | - html_doc += table(["Workload", "ACK P95 ms", "ACK max ms", "Query P95 ms", "Query max ms"], tail_rows, "wide") |
| 1815 | - if private_set: |
| 1816 | - private_rows = [] |
| 1817 | - for result in private_set[1].get("results", []): |
| 1818 | - private_rows.append([result.get("fixture"), result.get("status"), finding_phase(result, "initial_push"), finding_phase(result, "fresh_clone_"), finding_phase(result, "concurrent_clone_4")]) |
| 1819 | - peak = run_vmhwm(private_set[0], private_set[2]) |
| 1820 | - html_doc += "<details><summary>Private repository follow-up</summary><p>Private Git requests verify NIP-98 payload hashes before Git receives them. Uploads spool to disk so memory does not grow with pack size. The client used an ephemeral signing proxy that also spooled requests; these timings include that client overhead and have no private native-Git baseline. The process first completed a small private nzip smoke test.</p>" |
| 1821 | - html_doc += f"<p>Private-run Go RSS high-water: {display(peak, 'kb_mib')} MiB.</p>" |
| 1822 | - html_doc += table(["repo", "status", "initial push ms", "clone median ms", "concurrent4 ms"], private_rows, "wide") + "</details>" |
| 1823 | - html_doc += "<details><summary>Push and concurrent clone comparisons (ms)</summary>" |
| 1824 | - html_doc += table(["repo", "before clone", "before concurrent4", "before state ACK", "before push", "after clone", "after concurrent4", "after state ACK", "after push"], findings_rows, "wide", ["number"] * 9) |
| 1825 | - html_doc += "</details><details><summary>Repository characteristics</summary>" |
| 1826 | - html_doc += table(["repo", "all commits", "refs", "pack MiB"], corpus_rows, "wide", ["number", "number", "number", "mib"]) |
| 1827 | - html_doc += "</details><details><summary>All phase timings (milliseconds)</summary>" |
| 1828 | - html_doc += table(headers, phase_rows, "wide", ["number", "number", "number"] + ["number"] * len(PHASES)) |
| 1829 | - html_doc += "</details>" |
| 1830 | - mobile_rows = [] |
| 1831 | - for row in phase_rows: |
| 1832 | - for index, (label, _) in enumerate(PHASES, start=2): |
| 1833 | - mobile_rows.append([row[0], row[1], label, row[index + 1]]) |
| 1834 | - |
| 1835 | - resource_headers = ["repo", "method", "Go RSS high-water MiB", "cgroup anon peak MiB", "cgroup file peak MiB", "cgroup total peak MiB", "CPU initial µs", "CPU incremental µs", "CPU concurrent2 µs", "CPU concurrent4 µs", "container"] |
| 1836 | - html_doc += "<details><summary>Process, container memory and CPU</summary>" |
| 1837 | - html_doc += table(resource_headers, resource_rows, "wide", ["number", "number", "kb_mib", "mib", "mib", "mib", "number", "number", "number", "number", "number"]) |
| 1838 | - html_doc += "</details>" |
| 1839 | - probe_headers = ["repo", "method", "idle publish P50 ms", "idle publish P95 ms", "idle publish max ms", "idle query P50 ms", "idle query P95 ms", "idle query max ms", "busy publish P50 ms", "busy publish P95 ms", "busy publish max ms", "busy query P50 ms", "busy query P95 ms", "busy query max ms", "idle errors", "busy errors"] |
| 1840 | - html_doc += "<details><summary>Relay responsiveness during Git operations</summary>" |
| 1841 | - html_doc += table(probe_headers, probe_rows, "wide", ["number", "number"] + ["number"] * 14) |
| 1842 | - html_doc += "</details><details><summary>Failures and completion by run</summary>" |
| 1843 | - html_doc += table(["repo", "method", "status", "failed phases"], status_rows, "wide") |
| 1844 | - html_doc += "</details><details><summary>Method and limits of comparison</summary><p>Linux Slate: 16 logical CPUs, Git 2.50.1 client and 2.39.5 server, Go 1.27.1. Client and server shared one host over loopback; SSH/Tailscale was used to orchestrate the run. Each server container had a 10 GiB test ceiling, no CPU cap and no swap. These are test containment settings, not product limits.</p><p>Fixtures contain committed local branches and tags. Fresh clones used new destination directories; the operating system page cache was not cleared. Three sequential clones and one group each at concurrency two and four were measured. The incremental push adds one commit with an unchanged tree. Native Git uses the same git-http-backend with a minimal streaming HTTP wrapper. Differences around the native baseline can reflect cache and host variation. No Worker-hosted bindws runtime was benchmarked.</p><p>Go VmHWM is the whole-run peak resident memory of tiny itself. Container memory also includes Git subprocesses and file cache. Those components must not be confused. Cgroup CPU samples include background relay probes and Git children, at roughly 500 ms resolution. The first 60 seconds have a Go CPU profile, which does not include Git child CPU. The failed initial runs are retained; no missing phase counts as a pass.</p><p>Run manifests, hashes, logs, profiles and 500 ms resource samples accompany the raw JSON below. Timings are wall-clock milliseconds unless labeled otherwise. The before run includes the gzip/HEAD compatibility fixes; the after run adds streaming, batching and connection/state correctness fixes.</p></details>" |
| 1845 | - html_doc += "<h2>Raw results</h2><ul>" + "".join(f'<li><a href="{html.escape(str(path.relative_to(root)))}">{html.escape(label)}</a></li>' for label, path in raw_links) + "</ul>" |
| 1846 | - if (root / "results-after-first/git-performance.json").exists(): |
| 1847 | - html_doc += '<p><a href="results-after-first/git-performance.json">Earlier successful streaming run</a> retained separately. The main comparison uses the final public run.</p>' |
| 1848 | - html_doc += '<p>Per-repository resource rows show the sampled cumulative process/cgroup high-water values through that fixture; anonymous and file-memory peaks are sampled within the fixture. The headline Go peak covers the complete public run.</p>' |
| 1849 | - html_doc += "</body></html>" |
| 1850 | - html_path.parent.mkdir(parents=True, exist_ok=True) |
| 1851 | - html_path.write_text(html_doc, encoding="utf-8") |
| 1852 | - md = ["# Git performance", "", "Phase wall times come from the recorded harness. Missing or failed phases remain visible. This report does not compare against the bindws Worker runtime.", "", "Method: Linux Slate, 16 CPUs, 10 GiB test-container ceiling, no CPU cap, no swap; client/server loopback; Git 2.50.1 client and 2.39.5 server; fixed local heads and tags with no cold page-cache reset. Phase CPU deltas are total container CPU inclusive of Git children and background probes, and values below sampler resolution are shown as missing.", "", "Methods: native Git HTTP baseline, buffered Tiny, and streaming Tiny where result artifacts exist.", "", "## Findings", "", markdown_table(["finding", "before", "after"], highlight_rows, ["number", "number", "number"]), "", markdown_table(["repo", "before clone", "before concurrent4", "before state ACK", "before push", "after clone", "after concurrent4", "after state ACK", "after push"], findings_rows, ["number"] * 9), "", "## Phase results", "", markdown_table(headers, phase_rows, ["number", "number", "number"] + ["number"] * len(PHASES)), "", "## Resource results", "", markdown_table(resource_headers, resource_rows, ["number", "number", "kb_mib", "mib", "mib", "mib", "number", "number", "number", "number", "number"]), "", "## Probe results", "", markdown_table(probe_headers, probe_rows, ["number", "number"] + ["number"] * 14), "", "## Status", "", markdown_table(["repo", "method", "status", "failed phases"], status_rows), "", "Profile finding from the captured before run: CPU samples identify memory clearing/copying as the visible hot path; the independent Go RSS reading is 6093.9 MiB. Keep the raw profile and process/cgroup captures beside any interpretation.", "", "Raw JSON artifacts are linked from the HTML report."] |
| 1853 | - md[md.index("## Phase results"):md.index("## Phase results")] = ["## Remaining relay latency", "", "Every probe succeeded, but durable event acknowledgements still stalled occasionally during large Git transfers, including native Git baseline phases on the same host. These results do not identify the cause. Capture disk latency and scheduling before selecting another optimization.", "", markdown_table(["workload", "ACK P95 ms", "ACK max ms", "query P95 ms", "query max ms"], tail_rows), ""] |
| 1854 | - args.markdown.parent.mkdir(parents=True, exist_ok=True) |
| 1855 | - args.markdown.write_text("\n".join(md) + "\n", encoding="utf-8") |
| 1856 | - |
| 1857 | - |
| 1858 | -if __name__ == "__main__": |
| 1859 | - main() |