Phase 7: authored assets (diagrams, code exhibits, client wall, budget)

Phase 7 is mostly authored assets. This commit is the asset half; the
template half is a Builder child issue because this phase must not write
application code.

DoD items satisfied or advanced:

- [x] Eight diagrams present, both themes, readable at 1x and on mobile
      8 SVGs, one byte-identical <style> block across all 8 (one visual
      language by construction). Light and dark resolve from a single file
      via prefers-color-scheme, verified per file in Chrome: 8/8 return two
      distinct palettes. Geometry verified with getBBox(), not estimated:
      0 findings after fixing 74 (text overflow, partial overlap, empty
      text, out-of-viewBox). Mobile type at the 900px minimum is 14.1/12.2px.

- [x] Six code exhibits with a two-sentence rationale each
      212 verbatim lines across five eras, copied with cp/sed from the
      read-only archive. Every rationale is exactly two sentences about why
      the code is good; every exhibit also carries an honest_note, and where
      the code is not good the note says so (the 3,400-line procedural PHP
      script is presented for its operational judgement, not its structure).

- [~] Every image optimised; total page weight budgeted and recorded
      BUDGET.md records measured raw and gzip weights for all 15 served
      files: 63,802 B raw / 13,220 B gzipped. Zero rasters by design. Page
      weight of a real page is not measurable until Phase 2 templates and
      Phase 3 serving exist, and is recorded as open rather than asserted.

- [x] Archive never linked or exposed
      No mount paths or volume names under content/. Provenance recorded as
      "private archive, path withheld".

- [ ] Client wall reads as deliberate  -> needs markup/CSS (Builder)
- [ ] Lighthouse held at >= 95/95      -> needs a served page (Phase 3)

Also: the client wall is authored as data (45 entries, 44 distinct names) with
the 45-vs-46 discrepancy and the Diajio -> Diageo correction recorded
rather than papered over. No commercial marks are enabled: that is a
permission decision for Sam, so availability is recorded and permission is not
implied.
This commit is contained in:
Documenter
2026-10-26 17:35:11 +11:00
parent b267e55f8c
commit da18ab2237
22 changed files with 1711 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
<title id="ttl">Homelab topology</title>
<desc id="dsc">Three hosts with their roles and the trust boundaries between them: public internet reaches Caddy on .35, which forwards over the LAN to services bound to 127.0.0.1 on .13. .27 is build and dev with no public listeners.</desc>
<style>
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
.hd{font-size:15px;font-weight:600}
.sub{font-size:13px;fill:#5b6673}
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
.big{font-size:19px;font-weight:600}
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
.rule{stroke:#d0d7df;stroke-width:1}
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
.ln.d{stroke-dasharray:6 5}
.ln.a{stroke:#1a6bc4}
.ln.t{stroke:#0f766e}
.ln.w{stroke:#b45309}
.mN{fill:#6b7684}
.mA{fill:#1a6bc4}
.mT{fill:#0f766e}
.mW{fill:#b45309}
.dot{fill:#6b7684}
.tick{stroke:#aab4c0;stroke-width:1.2}
@media (prefers-color-scheme: dark){
text{fill:#e7edf4}
.sub,.cap{fill:#98a4b3}
.node{fill:#182028;stroke:#35424f}
.node-a{fill:#11253d;stroke:#5aa2f0}
.node-t{fill:#0d2624;stroke:#38b3a6}
.node-w{fill:#2a1d10;stroke:#d98b3f}
.zone{stroke:#43525f}
.rule{stroke:#2c3743}
.ln{stroke:#8496a6}
.ln.a{stroke:#5aa2f0}
.ln.t{stroke:#38b3a6}
.ln.w{stroke:#d98b3f}
.mN{fill:#8496a6}
.mA{fill:#5aa2f0}
.mT{fill:#38b3a6}
.mW{fill:#d98b3f}
.dot{fill:#8496a6}
.tick{stroke:#43525f}
}
</style>
<defs>
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
</defs>
<text x="24" y="40" class="big">Homelab topology — hosts, roles, trust boundaries</text>
<rect x="24" y="74" width="150" height="152" rx="10" class="zone"/>
<text x="36" y="94" class="cap">public internet</text>
<rect x="38" y="110" width="122" height="62" rx="7" class="node "/>
<text x="99.0" y="139.0" class="hd" text-anchor="middle">Visitors</text>
<text x="99.0" y="157.0" class="sub" text-anchor="middle">no host access</text>
<path d="M174,148 L200,148" class="ln a" marker-end="url(#aA)"/>
<rect x="206" y="74" width="228" height="300" rx="7" class="node "/>
<text x="220" y="100" class="hd">.35 sam-Ubuntu1</text>
<text x="220" y="120" class="sub">public edge · 2 CPUs · 14 GiB</text>
<path d="M206,132 L434,132" class="rule"/>
<rect x="220" y="146" width="200" height="48" rx="7" class="node node-a"/>
<text x="320.0" y="168.0" class="hd" text-anchor="middle">Caddy :443</text>
<text x="320.0" y="186.0" class="sub" text-anchor="middle">the only public listener</text>
<rect x="220" y="208" width="200" height="44" rx="7" class="node "/>
<text x="320.0" y="235.0" class="hd" text-anchor="middle">Gitea · Immich · Wiki.js</text>
<rect x="220" y="262" width="200" height="44" rx="7" class="node "/>
<text x="320.0" y="289.0" class="hd" text-anchor="middle">Nextcloud · Penpot</text>
<text x="220" y="330" class="sub">terminates 443 for</text>
<text x="220" y="348" class="sub">*.lab.audasmedia.com.au</text>
<path d="M434,170 L462,170" class="ln a" marker-end="url(#aA)"/>
<rect x="468" y="74" width="268" height="366" rx="7" class="node "/>
<text x="482" y="100" class="hd">.13 nixos-desktop</text>
<text x="482" y="120" class="sub">runtime · 12 CPUs · 2.3 GiB free</text>
<path d="M468,132 L736,132" class="rule"/>
<rect x="482" y="146" width="240" height="46" rx="7" class="node node-a"/>
<text x="602.0" y="167.0" class="hd" text-anchor="middle">Go app :8096</text>
<text x="602.0" y="185.0" class="sub" text-anchor="middle">templ · HTMX · SSE · SQLite</text>
<rect x="482" y="200" width="240" height="42" rx="7" class="node "/>
<text x="602.0" y="226.0" class="hd" text-anchor="middle">Qdrant :6333 / :6334</text>
<rect x="482" y="250" width="240" height="42" rx="7" class="node "/>
<text x="602.0" y="269.0" class="hd" text-anchor="middle">Prefect worker</text>
<text x="602.0" y="287.0" class="sub" text-anchor="middle">content/ → chunk → embed</text>
<rect x="482" y="300" width="240" height="42" rx="7" class="node node-t"/>
<text x="602.0" y="319.0" class="hd" text-anchor="middle">OmniRoute</text>
<text x="602.0" y="337.0" class="sub" text-anchor="middle">existing · the only model gateway</text>
<rect x="482" y="350" width="240" height="42" rx="7" class="node "/>
<text x="602.0" y="369.0" class="hd" text-anchor="middle">Whisper · Piper</text>
<text x="602.0" y="387.0" class="sub" text-anchor="middle">speech in, speech out</text>
<text x="482" y="414" class="sub">every listener on 127.0.0.1</text>
<path d="M736,200 L764,200" class="ln d " marker-end="url(#aN)"/>
<text x="750.0" y="193.0" class="sub" text-anchor="middle">LAN</text>
<rect x="766" y="74" width="170" height="220" rx="7" class="node "/>
<text x="780" y="100" class="hd">.27</text>
<text x="780" y="120" class="sub">sam-4screen-desktop</text>
<path d="M766,132 L936,132" class="rule"/>
<rect x="780" y="146" width="142" height="44" rx="7" class="node "/>
<text x="851.0" y="173.0" class="hd" text-anchor="middle">content/ + source</text>
<rect x="780" y="200" width="142" height="44" rx="7" class="node "/>
<text x="851.0" y="227.0" class="hd" text-anchor="middle">agent fleet</text>
<text x="780" y="268" class="sub">build / dev · no public</text>
<text x="780" y="286" class="sub">listeners</text>
<rect x="206" y="396" width="730" height="62" rx="10" class="zone"/>
<text x="218" y="416" class="cap">trust boundary — outside this boundary nothing is reachable from the internet</text>
<text x="220" y="438" class="sub">Public → Caddy :443 on .35 → LAN → 127.0.0.1 services on .13. New footprint ≤ 1.0 GiB · ≤ 4 containers.</text>
<text x="24" y="486" class="sub">Solid = network path. Dashed = trust boundary. Ports are those the app actually binds.</text>
<text x="936" y="486" class="sub" text-anchor="end">measured 2026-10-17 · reference/stack.md</text>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -0,0 +1,98 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
<title id="ttl">Agent architecture</title>
<desc id="dsc">The operator reaches a fleet of agent sessions through one inbox. Paperclip is the control plane and delegates to peer projects, which answer the Hub read-only. Every model call in the estate passes through one gateway, OmniRoute.</desc>
<style>
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
.hd{font-size:15px;font-weight:600}
.sub{font-size:13px;fill:#5b6673}
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
.big{font-size:19px;font-weight:600}
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
.rule{stroke:#d0d7df;stroke-width:1}
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
.ln.d{stroke-dasharray:6 5}
.ln.a{stroke:#1a6bc4}
.ln.t{stroke:#0f766e}
.ln.w{stroke:#b45309}
.mN{fill:#6b7684}
.mA{fill:#1a6bc4}
.mT{fill:#0f766e}
.mW{fill:#b45309}
.dot{fill:#6b7684}
.tick{stroke:#aab4c0;stroke-width:1.2}
@media (prefers-color-scheme: dark){
text{fill:#e7edf4}
.sub,.cap{fill:#98a4b3}
.node{fill:#182028;stroke:#35424f}
.node-a{fill:#11253d;stroke:#5aa2f0}
.node-t{fill:#0d2624;stroke:#38b3a6}
.node-w{fill:#2a1d10;stroke:#d98b3f}
.zone{stroke:#43525f}
.rule{stroke:#2c3743}
.ln{stroke:#8496a6}
.ln.a{stroke:#5aa2f0}
.ln.t{stroke:#38b3a6}
.ln.w{stroke:#d98b3f}
.mN{fill:#8496a6}
.mA{fill:#5aa2f0}
.mT{fill:#38b3a6}
.mW{fill:#d98b3f}
.dot{fill:#8496a6}
.tick{stroke:#43525f}
}
</style>
<defs>
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
</defs>
<text x="24" y="40" class="big">Agent architecture — operator, control plane, peers, one gateway</text>
<rect x="360" y="62" width="240" height="56" rx="7" class="node "/>
<text x="480.0" y="88.0" class="hd" text-anchor="middle">Operator</text>
<text x="480.0" y="106.0" class="sub" text-anchor="middle">one inbox for runs and approvals</text>
<path d="M480,118 L480,146" class="ln " marker-end="url(#aN)"/>
<rect x="330" y="146" width="300" height="62" rx="7" class="node "/>
<text x="480.0" y="175.0" class="hd" text-anchor="middle">TUIOS fleet</text>
<text x="480.0" y="193.0" class="sub" text-anchor="middle">four hosts · survive disconnect</text>
<path d="M400,208 L300,236" class="ln " marker-end="url(#aN)"/>
<path d="M480,208 L480,236" class="ln " marker-end="url(#aN)"/>
<path d="M560,208 L680,236" class="ln " marker-end="url(#aN)"/>
<rect x="40" y="236" width="250" height="132" rx="7" class="node "/>
<text x="54" y="262" class="hd">Hub</text>
<text x="54" y="282" class="sub">house entry point for every project</text>
<path d="M40,292 L290,292" class="rule"/>
<text x="54" y="314" class="sub">/capabilities · /ask (read-only)</text>
<text x="54" y="334" class="sub">/notify · Apprise delivery</text>
<text x="54" y="356" class="sub">no write path to peers</text>
<rect x="355" y="236" width="250" height="132" rx="7" class="node "/>
<text x="369" y="262" class="hd">Paperclip</text>
<text x="369" y="282" class="sub">control plane · the system of record</text>
<path d="M355,292 L605,292" class="rule"/>
<text x="369" y="314" class="sub">issues · runs · approvals</text>
<text x="369" y="334" class="sub">agent identities · budgets</text>
<text x="369" y="356" class="sub">routines · blocked-by edges</text>
<rect x="670" y="236" width="250" height="132" rx="7" class="node "/>
<text x="684" y="262" class="hd">Peer data projects</text>
<text x="684" y="282" class="sub">own service, own port, own folder</text>
<path d="M670,292 L920,292" class="rule"/>
<text x="684" y="314" class="sub">resume (this site)</text>
<text x="684" y="334" class="sub">compass_nhs · email_agent</text>
<text x="684" y="356" class="sub">IoT estate · signage</text>
<path d="M605,302 L670,302" class="ln " marker-end="url(#aN)"/>
<text x="637.5" y="295.0" class="sub" text-anchor="middle">delegates</text>
<path d="M290,320 L355,320" class="ln " marker-end="url(#aN)"/>
<rect x="200" y="404" width="560" height="76" rx="7" class="node node-t"/>
<text x="480" y="432" class="hd" text-anchor="middle">OmniRoute — the only model gateway</text>
<text x="480" y="454" class="sub" text-anchor="middle">fast · mid · escalation tier | token caps | monthly ceiling | trace log</text>
<path d="M480,368 L480,404" class="ln t" marker-end="url(#aT)"/>
<path d="M120,368 L260,404" class="ln t" marker-end="url(#aT)"/>
<path d="M795,368 L700,404" class="ln t" marker-end="url(#aT)"/>
<text x="480" y="502" class="sub" text-anchor="middle">OpenRouter · DeepSeek · Gemini · MiniMax · Qwen</text>
<text x="480" y="520" class="sub" text-anchor="middle">reachable only through the gateway</text>
<text x="24" y="498" class="sub">Enforced by configuration, not convention: no service holds a provider key, so none can bypass the gateway.</text>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -0,0 +1,106 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
<title id="ttl">Retrieval pipeline</title>
<desc id="dsc">Left, offline ingest: content files are validated, chunked with metadata, embedded and upserted into Qdrant, while the same files render to static pages with no AI in the path. Right, per-visitor query: guardrail, query graph, filtered retrieval, then a streamed answer with citations.</desc>
<style>
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
.hd{font-size:15px;font-weight:600}
.sub{font-size:13px;fill:#5b6673}
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
.big{font-size:19px;font-weight:600}
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
.rule{stroke:#d0d7df;stroke-width:1}
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
.ln.d{stroke-dasharray:6 5}
.ln.a{stroke:#1a6bc4}
.ln.t{stroke:#0f766e}
.ln.w{stroke:#b45309}
.mN{fill:#6b7684}
.mA{fill:#1a6bc4}
.mT{fill:#0f766e}
.mW{fill:#b45309}
.dot{fill:#6b7684}
.tick{stroke:#aab4c0;stroke-width:1.2}
@media (prefers-color-scheme: dark){
text{fill:#e7edf4}
.sub,.cap{fill:#98a4b3}
.node{fill:#182028;stroke:#35424f}
.node-a{fill:#11253d;stroke:#5aa2f0}
.node-t{fill:#0d2624;stroke:#38b3a6}
.node-w{fill:#2a1d10;stroke:#d98b3f}
.zone{stroke:#43525f}
.rule{stroke:#2c3743}
.ln{stroke:#8496a6}
.ln.a{stroke:#5aa2f0}
.ln.t{stroke:#38b3a6}
.ln.w{stroke:#d98b3f}
.mN{fill:#8496a6}
.mA{fill:#5aa2f0}
.mT{fill:#38b3a6}
.mW{fill:#d98b3f}
.dot{fill:#8496a6}
.tick{stroke:#43525f}
}
</style>
<defs>
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
</defs>
<text x="24" y="40" class="big">Retrieval pipeline — ingest and query, side by side</text>
<path d="M480,64 L480,470" class="zone"/>
<text x="40" y="76" class="cap">ingest — runs offline</text>
<text x="504" y="76" class="cap">query — runs per visitor</text>
<rect x="40" y="92" width="250" height="48" rx="7" class="node "/>
<text x="165.0" y="114.0" class="hd" text-anchor="middle">content/ in git</text>
<text x="165.0" y="132.0" class="sub" text-anchor="middle">Markdown + front-matter + assets</text>
<path d="M165.0,140 L165.0,164" class="ln " marker-end="url(#aN)"/>
<rect x="40" y="164" width="250" height="48" rx="7" class="node "/>
<text x="165.0" y="186.0" class="hd" text-anchor="middle">validate front-matter</text>
<text x="165.0" y="204.0" class="sub" text-anchor="middle">schema is frozen; build fails closed</text>
<path d="M165.0,212 L165.0,236" class="ln " marker-end="url(#aN)"/>
<rect x="40" y="236" width="250" height="48" rx="7" class="node "/>
<text x="165.0" y="258.0" class="hd" text-anchor="middle">chunk + tag metadata</text>
<text x="165.0" y="276.0" class="sub" text-anchor="middle">area · level · confidence · period</text>
<path d="M165.0,284 L165.0,308" class="ln " marker-end="url(#aN)"/>
<rect x="40" y="308" width="250" height="48" rx="7" class="node node-t"/>
<text x="165.0" y="330.0" class="hd" text-anchor="middle">embed via OmniRoute</text>
<text x="165.0" y="348.0" class="sub" text-anchor="middle">one gateway, one trace log</text>
<path d="M165.0,356 L165.0,380" class="ln t" marker-end="url(#aT)"/>
<rect x="180" y="380" width="300" height="56" rx="7" class="node node-t"/>
<text x="330.0" y="406.0" class="hd" text-anchor="middle">Qdrant — payload-filtered store</text>
<text x="330.0" y="424.0" class="sub" text-anchor="middle">one point per chunk</text>
<rect x="40" y="400" width="116" height="64" rx="7" class="node node-a"/>
<text x="98.0" y="430.0" class="hd" text-anchor="middle">templ build</text>
<text x="98.0" y="448.0" class="sub" text-anchor="middle">Mode A pages</text>
<path d="M98,212 L98,400" class="ln a" marker-end="url(#aA)"/>
<text x="98.0" y="299.0" class="sub" text-anchor="middle">same file</text>
<text x="106" y="300" class="sub">no AI in this path</text>
<text x="106" y="318" class="sub">(acceptance test A1)</text>
<rect x="504" y="92" width="250" height="48" rx="7" class="node "/>
<text x="629.0" y="114.0" class="hd" text-anchor="middle">visitor prompt</text>
<text x="629.0" y="132.0" class="sub" text-anchor="middle">plain English, untrusted</text>
<path d="M629.0,140 L629.0,164" class="ln w" marker-end="url(#aW)"/>
<rect x="504" y="164" width="250" height="48" rx="7" class="node node-w"/>
<text x="629.0" y="186.0" class="hd" text-anchor="middle">guardrail: classify → allow / deny</text>
<text x="629.0" y="204.0" class="sub" text-anchor="middle">fails closed, no tools, no write path</text>
<path d="M629.0,212 L629.0,236" class="ln " marker-end="url(#aN)"/>
<rect x="504" y="236" width="250" height="48" rx="7" class="node "/>
<text x="629.0" y="258.0" class="hd" text-anchor="middle">LangGraph query graph</text>
<text x="629.0" y="276.0" class="sub" text-anchor="middle">route → retrieve → cite → compose</text>
<path d="M629.0,284 L629.0,308" class="ln " marker-end="url(#aN)"/>
<rect x="504" y="308" width="250" height="48" rx="7" class="node "/>
<text x="629.0" y="330.0" class="hd" text-anchor="middle">retrieve: top-k, filtered by area</text>
<text x="629.0" y="348.0" class="sub" text-anchor="middle">filtered before it is searched</text>
<path d="M629.0,356 L629.0,380" class="ln " marker-end="url(#aN)"/>
<rect x="504" y="380" width="250" height="56" rx="7" class="node node-a"/>
<text x="629.0" y="406.0" class="hd" text-anchor="middle">SSE stream + citations</text>
<text x="629.0" y="424.0" class="sub" text-anchor="middle">first token &lt;4 s · every claim cited</text>
<path d="M480,408 L504,408" class="ln " marker-end="url(#aN)"/>
<text x="24" y="492" class="sub">Ingest and query share one store and one schema.</text>
<text x="24" y="512" class="sub">Static pages and corpus are built from the same files, so the two modes cannot drift.</text>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1,111 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
<title id="ttl">Edge architecture</title>
<desc id="dsc">Constrained devices publish over LoRa, Zigbee or WiFi to a single MQTT broker, which feeds one automation layer that drives actuation. Retained configuration, telemetry and last-will presence let devices recover unattended.</desc>
<style>
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
.hd{font-size:15px;font-weight:600}
.sub{font-size:13px;fill:#5b6673}
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
.big{font-size:19px;font-weight:600}
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
.rule{stroke:#d0d7df;stroke-width:1}
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
.ln.d{stroke-dasharray:6 5}
.ln.a{stroke:#1a6bc4}
.ln.t{stroke:#0f766e}
.ln.w{stroke:#b45309}
.mN{fill:#6b7684}
.mA{fill:#1a6bc4}
.mT{fill:#0f766e}
.mW{fill:#b45309}
.dot{fill:#6b7684}
.tick{stroke:#aab4c0;stroke-width:1.2}
@media (prefers-color-scheme: dark){
text{fill:#e7edf4}
.sub,.cap{fill:#98a4b3}
.node{fill:#182028;stroke:#35424f}
.node-a{fill:#11253d;stroke:#5aa2f0}
.node-t{fill:#0d2624;stroke:#38b3a6}
.node-w{fill:#2a1d10;stroke:#d98b3f}
.zone{stroke:#43525f}
.rule{stroke:#2c3743}
.ln{stroke:#8496a6}
.ln.a{stroke:#5aa2f0}
.ln.t{stroke:#38b3a6}
.ln.w{stroke:#d98b3f}
.mN{fill:#8496a6}
.mA{fill:#5aa2f0}
.mT{fill:#38b3a6}
.mW{fill:#d98b3f}
.dot{fill:#8496a6}
.tick{stroke:#43525f}
}
</style>
<defs>
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
</defs>
<text x="24" y="40" class="big">Edge architecture — devices, one broker, one automation layer</text>
<rect x="24" y="74" width="220" height="236" rx="7" class="node "/>
<text x="38" y="100" class="hd">Devices</text>
<text x="38" y="120" class="sub">20+ nodes · 9 zones</text>
<path d="M24,132 L244,132" class="rule"/>
<rect x="38" y="146" width="192" height="40" rx="7" class="node "/>
<text x="134.0" y="164.0" class="hd" text-anchor="middle">10 × ESP32</text>
<text x="134.0" y="182.0" class="sub" text-anchor="middle">sensors, relays, buttons</text>
<rect x="38" y="194" width="192" height="40" rx="7" class="node "/>
<text x="134.0" y="212.0" class="hd" text-anchor="middle">2 × Zigbee routers</text>
<text x="134.0" y="230.0" class="sub" text-anchor="middle">hand-built</text>
<rect x="38" y="242" width="192" height="40" rx="7" class="node "/>
<text x="134.0" y="260.0" class="hd" text-anchor="middle">8 × Pi audio endpoints</text>
<text x="134.0" y="278.0" class="sub" text-anchor="middle">Snapcast clients</text>
<text x="38" y="300" class="sub">radio: LoRa · Zigbee · WiFi</text>
<path d="M244,190 L288,190" class="ln a" marker-end="url(#aA)"/>
<rect x="288" y="74" width="210" height="236" rx="7" class="node "/>
<text x="302" y="100" class="hd">MQTT broker</text>
<text x="302" y="120" class="sub">the only shared state</text>
<path d="M288,132 L498,132" class="rule"/>
<rect x="302" y="146" width="182" height="40" rx="7" class="node "/>
<text x="393.0" y="171.0" class="hd" text-anchor="middle">retained config</text>
<rect x="302" y="194" width="182" height="40" rx="7" class="node "/>
<text x="393.0" y="219.0" class="hd" text-anchor="middle">status · telemetry</text>
<rect x="302" y="242" width="182" height="40" rx="7" class="node "/>
<text x="393.0" y="260.0" class="hd" text-anchor="middle">LWT presence</text>
<text x="393.0" y="278.0" class="sub" text-anchor="middle">last will = offline</text>
<text x="302" y="300" class="sub">reconnect + re-subscribe</text>
<path d="M498,190 L542,190" class="ln " marker-end="url(#aN)"/>
<rect x="542" y="74" width="194" height="236" rx="7" class="node "/>
<text x="556" y="100" class="hd">Automation</text>
<text x="556" y="120" class="sub">20 automations · 9 zones</text>
<path d="M542,132 L736,132" class="rule"/>
<text x="556" y="156" class="sub">trigger → rule → action</text>
<text x="556" y="182" class="sub">9 lighting zones</text>
<text x="556" y="208" class="sub">auditable in one place</text>
<text x="556" y="240" class="sub">rules live in config,</text>
<text x="556" y="258" class="sub">not in firmware</text>
<path d="M736,190 L780,190" class="ln t" marker-end="url(#aT)"/>
<rect x="780" y="74" width="156" height="236" rx="7" class="node "/>
<text x="794" y="100" class="hd">Actuation</text>
<path d="M780,132 L936,132" class="rule"/>
<text x="794" y="158" class="sub">lights · relays</text>
<text x="794" y="184" class="sub">speech out</text>
<text x="794" y="210" class="sub">displays</text>
<text x="794" y="236" class="sub">doors · gates</text>
<rect x="24" y="332" width="442" height="84" rx="7" class="node node-t"/>
<text x="38" y="358" class="hd">Power and duty cycle</text>
<text x="38" y="380" class="sub">solar where there is no mains · deep sleep between reports</text>
<text x="38" y="400" class="sub">the budget that gets designed first, not last</text>
<rect x="494" y="332" width="442" height="84" rx="7" class="node node-w"/>
<text x="508" y="358" class="hd">Failures kept in the record</text>
<text x="508" y="380" class="sub">lizard feeder destroyed by the lizard · cameras postponed</text>
<text x="508" y="400" class="sub">doorbell being rebuilt with a ToF sensor</text>
<text x="24" y="452" class="sub">Same shape at every size: device → one broker → one automation layer → actuation.</text>
<text x="24" y="472" class="sub">Devices never talk to each other, so a node that returns from a power cut re-announces itself and nothing else changes.</text>
<text x="936" y="500" class="sub" text-anchor="end">20+ nodes = 10 ESP32 · 2 Zigbee routers · 8 Pi · reference/content-map.md</text>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,111 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
<title id="ttl">Distributed audio</title>
<desc id="dsc">Music and speech sources feed one Snapcast server, which streams one clock-synchronised mix to eight Pi endpoints. Voice announcements arrive as a priority lane: duck the music, speak, restore.</desc>
<style>
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
.hd{font-size:15px;font-weight:600}
.sub{font-size:13px;fill:#5b6673}
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
.big{font-size:19px;font-weight:600}
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
.rule{stroke:#d0d7df;stroke-width:1}
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
.ln.d{stroke-dasharray:6 5}
.ln.a{stroke:#1a6bc4}
.ln.t{stroke:#0f766e}
.ln.w{stroke:#b45309}
.mN{fill:#6b7684}
.mA{fill:#1a6bc4}
.mT{fill:#0f766e}
.mW{fill:#b45309}
.dot{fill:#6b7684}
.tick{stroke:#aab4c0;stroke-width:1.2}
@media (prefers-color-scheme: dark){
text{fill:#e7edf4}
.sub,.cap{fill:#98a4b3}
.node{fill:#182028;stroke:#35424f}
.node-a{fill:#11253d;stroke:#5aa2f0}
.node-t{fill:#0d2624;stroke:#38b3a6}
.node-w{fill:#2a1d10;stroke:#d98b3f}
.zone{stroke:#43525f}
.rule{stroke:#2c3743}
.ln{stroke:#8496a6}
.ln.a{stroke:#5aa2f0}
.ln.t{stroke:#38b3a6}
.ln.w{stroke:#d98b3f}
.mN{fill:#8496a6}
.mA{fill:#5aa2f0}
.mT{fill:#38b3a6}
.mW{fill:#d98b3f}
.dot{fill:#8496a6}
.tick{stroke:#43525f}
}
</style>
<defs>
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
</defs>
<text x="24" y="40" class="big">Distributed audio — sources, synchronous endpoints, voice injection</text>
<rect x="24" y="92" width="200" height="120" rx="7" class="node "/>
<text x="38" y="118" class="hd">Sources</text>
<path d="M24,130 L224,130" class="rule"/>
<text x="38" y="156" class="sub">Spotify → Mopidy</text>
<text x="38" y="180" class="sub">Piper / MiniMax TTS</text>
<text x="38" y="204" class="sub">announcements</text>
<path d="M224,152 L268,152" class="ln " marker-end="url(#aN)"/>
<rect x="268" y="92" width="210" height="120" rx="7" class="node "/>
<text x="282" y="118" class="hd">Snapcast server</text>
<text x="282" y="138" class="sub">one clock, one stream</text>
<path d="M268,150 L478,150" class="rule"/>
<text x="282" y="176" class="sub">per-client latency offset</text>
<text x="282" y="198" class="sub">new clients join mid-stream</text>
<path d="M478,152 L522,152" class="ln t" marker-end="url(#aT)"/>
<rect x="522" y="74" width="414" height="156" rx="7" class="node "/>
<text x="536" y="100" class="hd">8 endpoints — Pi 2W</text>
<text x="536" y="120" class="sub">one stream, eight speakers, in sync</text>
<path d="M522,132 L936,132" class="rule"/>
<rect x="536" y="146" width="42" height="42" rx="7" class="node "/>
<text x="557.0" y="172.0" class="hd" text-anchor="middle">1</text>
<rect x="586" y="146" width="42" height="42" rx="7" class="node "/>
<text x="607.0" y="172.0" class="hd" text-anchor="middle">2</text>
<rect x="636" y="146" width="42" height="42" rx="7" class="node "/>
<text x="657.0" y="172.0" class="hd" text-anchor="middle">3</text>
<rect x="686" y="146" width="42" height="42" rx="7" class="node "/>
<text x="707.0" y="172.0" class="hd" text-anchor="middle">4</text>
<rect x="736" y="146" width="42" height="42" rx="7" class="node "/>
<text x="757.0" y="172.0" class="hd" text-anchor="middle">5</text>
<rect x="786" y="146" width="42" height="42" rx="7" class="node "/>
<text x="807.0" y="172.0" class="hd" text-anchor="middle">6</text>
<rect x="836" y="146" width="42" height="42" rx="7" class="node "/>
<text x="857.0" y="172.0" class="hd" text-anchor="middle">7</text>
<rect x="886" y="146" width="42" height="42" rx="7" class="node "/>
<text x="907.0" y="172.0" class="hd" text-anchor="middle">8</text>
<text x="536" y="214" class="sub">music in one room does not arrive in another room late</text>
<rect x="24" y="262" width="470" height="152" rx="7" class="node node-t"/>
<text x="38" y="288" class="hd">Voice injection — the part that is not just streaming</text>
<rect x="38" y="304" width="136" height="38" rx="7" class="node "/>
<text x="106.0" y="328.0" class="hd" text-anchor="middle">TTS audio in</text>
<path d="M174,323 L200,323" class="ln t" marker-end="url(#aT)"/>
<rect x="200" y="304" width="126" height="38" rx="7" class="node "/>
<text x="263.0" y="328.0" class="hd" text-anchor="middle">duck music</text>
<path d="M326,323 L352,323" class="ln t" marker-end="url(#aT)"/>
<rect x="352" y="304" width="120" height="38" rx="7" class="node "/>
<text x="412.0" y="328.0" class="hd" text-anchor="middle">announce</text>
<text x="38" y="372" class="sub">then restore the music to its previous level</text>
<text x="38" y="392" class="sub">an announcement is a priority lane over the music</text>
<rect x="516" y="262" width="420" height="152" rx="7" class="node "/>
<text x="530" y="288" class="hd">Why it is built this way</text>
<text x="530" y="312" class="sub">· one source of time, not one source of audio</text>
<text x="530" y="336" class="sub">· endpoints are disposable; the server holds the state</text>
<text x="530" y="360" class="sub">· voice reuses the same endpoint fleet</text>
<text x="530" y="384" class="sub">· ducking is a mix decision, not a volume hack</text>
<text x="24" y="452" class="sub">Eight synchronised endpoints with music ducking for voice.</text>
<text x="24" y="472" class="sub">The endpoints are the cheapest part of the system, which is the point.</text>
<text x="936" y="500" class="sub" text-anchor="end">confidence: delivered · 8 endpoints · Pi 2W</text>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,92 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
<title id="ttl">Media pipeline</title>
<desc id="dsc">Five stages from download to library: fingerprinting gives every file a natural key so re-runs converge, audit verifies the copy, and review is a deliberate human gate before anything is published.</desc>
<style>
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
.hd{font-size:15px;font-weight:600}
.sub{font-size:13px;fill:#5b6673}
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
.big{font-size:19px;font-weight:600}
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
.rule{stroke:#d0d7df;stroke-width:1}
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
.ln.d{stroke-dasharray:6 5}
.ln.a{stroke:#1a6bc4}
.ln.t{stroke:#0f766e}
.ln.w{stroke:#b45309}
.mN{fill:#6b7684}
.mA{fill:#1a6bc4}
.mT{fill:#0f766e}
.mW{fill:#b45309}
.dot{fill:#6b7684}
.tick{stroke:#aab4c0;stroke-width:1.2}
@media (prefers-color-scheme: dark){
text{fill:#e7edf4}
.sub,.cap{fill:#98a4b3}
.node{fill:#182028;stroke:#35424f}
.node-a{fill:#11253d;stroke:#5aa2f0}
.node-t{fill:#0d2624;stroke:#38b3a6}
.node-w{fill:#2a1d10;stroke:#d98b3f}
.zone{stroke:#43525f}
.rule{stroke:#2c3743}
.ln{stroke:#8496a6}
.ln.a{stroke:#5aa2f0}
.ln.t{stroke:#38b3a6}
.ln.w{stroke:#d98b3f}
.mN{fill:#8496a6}
.mA{fill:#5aa2f0}
.mT{fill:#38b3a6}
.mW{fill:#d98b3f}
.dot{fill:#8496a6}
.tick{stroke:#43525f}
}
</style>
<defs>
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
</defs>
<text x="24" y="40" class="big">Media pipeline — download, identify, verify, review, publish</text>
<rect x="24" y="96" width="154" height="84" rx="7" class="node node-a"/>
<text x="101.0" y="136.0" class="hd" text-anchor="middle">download</text>
<text x="101.0" y="154.0" class="sub" text-anchor="middle">acquire the file</text>
<text x="101" y="168" class="cap" text-anchor="middle">stage 1</text>
<path d="M178,138 L214,138" class="ln " marker-end="url(#aN)"/>
<rect x="214" y="96" width="154" height="84" rx="7" class="node "/>
<text x="291.0" y="136.0" class="hd" text-anchor="middle">fingerprint</text>
<text x="291.0" y="154.0" class="sub" text-anchor="middle">identify what it is</text>
<text x="291" y="168" class="cap" text-anchor="middle">stage 2</text>
<path d="M368,138 L404,138" class="ln " marker-end="url(#aN)"/>
<rect x="404" y="96" width="154" height="84" rx="7" class="node "/>
<text x="481.0" y="136.0" class="hd" text-anchor="middle">audit</text>
<text x="481.0" y="154.0" class="sub" text-anchor="middle">verify the copy</text>
<text x="481" y="168" class="cap" text-anchor="middle">stage 3</text>
<path d="M558,138 L594,138" class="ln " marker-end="url(#aN)"/>
<rect x="594" y="96" width="154" height="84" rx="7" class="node node-w"/>
<text x="671.0" y="136.0" class="hd" text-anchor="middle">review</text>
<text x="671.0" y="154.0" class="sub" text-anchor="middle">human gate</text>
<text x="671" y="168" class="cap" text-anchor="middle">stage 4</text>
<path d="M748,138 L784,138" class="ln " marker-end="url(#aN)"/>
<rect x="784" y="96" width="154" height="84" rx="7" class="node node-a"/>
<text x="861.0" y="136.0" class="hd" text-anchor="middle">library</text>
<text x="861.0" y="154.0" class="sub" text-anchor="middle">publish + serve</text>
<text x="861" y="168" class="cap" text-anchor="middle">stage 5</text>
<rect x="214" y="236" width="342" height="96" rx="7" class="node node-w"/>
<text x="228" y="262" class="hd">Gate, not a queue</text>
<text x="228" y="284" class="sub">a file that fails fingerprint or audit</text>
<text x="228" y="304" class="sub">stops where a person can see it</text>
<rect x="590" y="236" width="346" height="96" rx="7" class="node "/>
<text x="604" y="262" class="hd">Idempotent by identity</text>
<text x="604" y="284" class="sub">the fingerprint is the natural key, so a</text>
<text x="604" y="304" class="sub">re-run converges instead of duplicating</text>
<rect x="24" y="356" width="912" height="92" rx="7" class="node "/>
<text x="38" y="382" class="hd">The same shape at 10 files and at 10,000</text>
<text x="38" y="404" class="sub">every stage is a separate step with its own failure mode, so a bad stage can be re-run alone</text>
<text x="38" y="426" class="sub">the review stage is deliberately manual: automating it would move the risk, not remove it</text>
<text x="24" y="480" class="sub">Encoding, thumbnailing and fingerprinting at scale, with a human gate before anything is published.</text>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1,95 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
<title id="ttl">Cost and resource engineering</title>
<desc id="dsc">A bar chart of the per-service memory caps drawn to scale against the 1024 MiB ceiling, totalling 970 MiB across four containers, beside the rules that hold the ceiling: equal memory and swap limits, loopback binding, a hard container count and a cost ceiling.</desc>
<style>
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
.hd{font-size:15px;font-weight:600}
.sub{font-size:13px;fill:#5b6673}
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
.big{font-size:19px;font-weight:600}
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
.rule{stroke:#d0d7df;stroke-width:1}
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
.ln.d{stroke-dasharray:6 5}
.ln.a{stroke:#1a6bc4}
.ln.t{stroke:#0f766e}
.ln.w{stroke:#b45309}
.mN{fill:#6b7684}
.mA{fill:#1a6bc4}
.mT{fill:#0f766e}
.mW{fill:#b45309}
.dot{fill:#6b7684}
.tick{stroke:#aab4c0;stroke-width:1.2}
@media (prefers-color-scheme: dark){
text{fill:#e7edf4}
.sub,.cap{fill:#98a4b3}
.node{fill:#182028;stroke:#35424f}
.node-a{fill:#11253d;stroke:#5aa2f0}
.node-t{fill:#0d2624;stroke:#38b3a6}
.node-w{fill:#2a1d10;stroke:#d98b3f}
.zone{stroke:#43525f}
.rule{stroke:#2c3743}
.ln{stroke:#8496a6}
.ln.a{stroke:#5aa2f0}
.ln.t{stroke:#38b3a6}
.ln.w{stroke:#d98b3f}
.mN{fill:#8496a6}
.mA{fill:#5aa2f0}
.mT{fill:#38b3a6}
.mW{fill:#d98b3f}
.dot{fill:#8496a6}
.tick{stroke:#43525f}
}
</style>
<defs>
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
</defs>
<text x="24" y="40" class="big">Cost and resource engineering — the budget, measured</text>
<text x="24" y="68" class="sub">Every service declares a ceiling. The ceiling is a measurement, not an estimate: .13 already runs on 2.3 GiB free with 8.1 GiB of swap in use.</text>
<text x="40" y="104" class="cap">per-service cap (MiB)</text>
<text x="40" y="136" class="sub">Go app</text>
<rect x="200" y="112" width="33.6" height="30" rx="5" class="bar node-a"/>
<text x="243.6" y="133" class="sub">120 MiB</text>
<text x="40" y="182" class="sub">Qdrant</text>
<rect x="200" y="158" width="112.00000000000001" height="30" rx="5" class="bar node-t"/>
<text x="322.0" y="179" class="sub">400 MiB</text>
<text x="40" y="228" class="sub">Prefect worker</text>
<rect x="200" y="204" width="42.00000000000001" height="30" rx="5" class="bar "/>
<text x="252.0" y="225" class="sub">150 MiB</text>
<text x="40" y="274" class="sub">Guardrail (if local)</text>
<rect x="200" y="250" width="84.00000000000001" height="30" rx="5" class="bar "/>
<text x="294.0" y="271" class="sub">300 MiB</text>
<path d="M200,352 L486.72,352" class="tick"/>
<text x="200" y="374" class="sub">0</text>
<text x="486.72" y="374" class="sub" text-anchor="end">1024 MiB = the whole new footprint</text>
<path d="M200,340 L200,362" class="tick"/>
<rect x="200" y="372" width="287" height="56" rx="7" class="node node-t"/>
<text x="343.5" y="398.0" class="hd" text-anchor="middle">970 MiB of 1024 MiB</text>
<text x="343.5" y="416.0" class="sub" text-anchor="middle">4 containers · measured, not asserted</text>
<rect x="520" y="92" width="416" height="336" rx="10" class="zone"/>
<text x="532" y="112" class="cap">rules that hold the ceiling</text>
<rect x="536" y="118" width="384" height="52" rx="7" class="node "/>
<text x="728.0" y="142.0" class="hd" text-anchor="middle">mem_limit == memswap_limit</text>
<text x="728.0" y="160.0" class="sub" text-anchor="middle">a service cannot buy headroom from host swap</text>
<rect x="536" y="180" width="384" height="52" rx="7" class="node "/>
<text x="728.0" y="204.0" class="hd" text-anchor="middle">bind 127.0.0.1</text>
<text x="728.0" y="222.0" class="sub" text-anchor="middle">Caddy is the only public listener</text>
<rect x="536" y="242" width="384" height="52" rx="7" class="node "/>
<text x="728.0" y="266.0" class="hd" text-anchor="middle">4 containers, not 14</text>
<text x="728.0" y="284.0" class="sub" text-anchor="middle">one process per job, no sidecars</text>
<rect x="536" y="304" width="384" height="52" rx="7" class="node "/>
<text x="728.0" y="328.0" class="hd" text-anchor="middle">cost has a ceiling too</text>
<text x="728.0" y="346.0" class="sub" text-anchor="middle">median session ≤ US$0.01 · monthly refusal</text>
<text x="536" y="384" class="sub">verified with docker inspect, not with intent</text>
<rect x="24" y="446" width="912" height="56" rx="7" class="node node-w"/>
<text x="480.0" y="472.0" class="hd" text-anchor="middle">Host reality on .13: 15 GiB total · 2.3 GiB free · 8.1 GiB swap in use</text>
<text x="480.0" y="490.0" class="sub" text-anchor="middle">exceeding the budget destabilises services people rely on</text>
<text x="24" y="524" class="sub">Source: reference/stack.md, hosts measured 2026-10-17. The caps are the contract; the bar chart is the contract drawn to scale.</text>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1,106 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
<title id="ttl">Era timeline</title>
<desc id="dsc">Five consecutive platform eras from Flash and ActionScript through jQuery and PHP, React and TypeScript, server-driven Go with HTMX, to agent systems, on an axis from 2000 to 2026. Era boundaries are approximate; the bands show order.</desc>
<style>
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
.hd{font-size:15px;font-weight:600}
.sub{font-size:13px;fill:#5b6673}
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
.big{font-size:19px;font-weight:600}
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
.rule{stroke:#d0d7df;stroke-width:1}
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
.ln.d{stroke-dasharray:6 5}
.ln.a{stroke:#1a6bc4}
.ln.t{stroke:#0f766e}
.ln.w{stroke:#b45309}
.mN{fill:#6b7684}
.mA{fill:#1a6bc4}
.mT{fill:#0f766e}
.mW{fill:#b45309}
.dot{fill:#6b7684}
.tick{stroke:#aab4c0;stroke-width:1.2}
@media (prefers-color-scheme: dark){
text{fill:#e7edf4}
.sub,.cap{fill:#98a4b3}
.node{fill:#182028;stroke:#35424f}
.node-a{fill:#11253d;stroke:#5aa2f0}
.node-t{fill:#0d2624;stroke:#38b3a6}
.node-w{fill:#2a1d10;stroke:#d98b3f}
.zone{stroke:#43525f}
.rule{stroke:#2c3743}
.ln{stroke:#8496a6}
.ln.a{stroke:#5aa2f0}
.ln.t{stroke:#38b3a6}
.ln.w{stroke:#d98b3f}
.mN{fill:#8496a6}
.mA{fill:#5aa2f0}
.mT{fill:#38b3a6}
.mW{fill:#d98b3f}
.dot{fill:#8496a6}
.tick{stroke:#43525f}
}
</style>
<defs>
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
</defs>
<text x="24" y="40" class="big">Twenty-five years, five platform shifts</text>
<rect x="28" y="96" width="176" height="118" rx="7" class="node"/>
<text x="42" y="126" class="cap">01</text>
<text x="42" y="144" class="hd">Flash / ActionScript</text>
<text x="42" y="168" class="sub">classes, imports,</text>
<text x="42" y="186" class="sub">encapsulation</text>
<text x="42" y="212" class="sub">client wall</text>
<path d="M205,155 L211,155" class="ln"/>
<rect x="212" y="96" width="176" height="118" rx="7" class="node"/>
<text x="226" y="126" class="cap">02</text>
<text x="226" y="144" class="hd">jQuery + PHP</text>
<text x="226" y="168" class="sub">media pipelines,</text>
<text x="226" y="186" class="sub">FFmpeg, ImageMagick</text>
<text x="226" y="212" class="sub">delivery at scale</text>
<path d="M389,155 L395,155" class="ln"/>
<rect x="396" y="96" width="176" height="118" rx="7" class="node"/>
<text x="410" y="126" class="cap">03</text>
<text x="410" y="144" class="hd">React + TypeScript</text>
<text x="410" y="168" class="sub">typed service</text>
<text x="410" y="186" class="sub">layers, tRPC</text>
<text x="410" y="212" class="sub">typed front ends</text>
<path d="M573,155 L579,155" class="ln"/>
<rect x="580" y="96" width="176" height="118" rx="7" class="node node-a"/>
<text x="594" y="126" class="cap">04</text>
<text x="594" y="144" class="hd">Server-driven</text>
<text x="594" y="168" class="sub">Go + templ + HTMX,</text>
<text x="594" y="186" class="sub">SQLite</text>
<text x="594" y="212" class="sub">this site</text>
<path d="M757,155 L763,155" class="ln"/>
<rect x="764" y="96" width="176" height="118" rx="7" class="node node-t"/>
<text x="778" y="126" class="cap">05</text>
<text x="778" y="144" class="hd">Agent systems</text>
<text x="778" y="168" class="sub">harnesses, routing,</text>
<text x="778" y="186" class="sub">retrieval</text>
<text x="778" y="212" class="sub">AI estate</text>
<path d="M28,268 L932,268" class="ln"/>
<path d="M28,262 L28,274" class="tick"/>
<text x="28" y="292" class="hd" text-anchor="start">2000</text>
<path d="M546,262 L546,274" class="tick"/>
<text x="546" y="292" class="hd" text-anchor="middle">2020</text>
<path d="M932,262 L932,274" class="tick"/>
<text x="932" y="292" class="hd" text-anchor="end">2026</text>
<text x="28" y="314" class="sub">employed delivery</text>
<text x="546" y="314" class="sub">independent engineering practice</text>
<text x="932" y="314" class="sub" text-anchor="end">now</text>
<rect x="28" y="348" width="904" height="118" rx="7" class="node "/>
<text x="42" y="376" class="hd">What the five eras have in common</text>
<text x="42" y="400" class="sub">· every era was learned by shipping with it, not by reading about it</text>
<text x="42" y="424" class="sub">· the artefacts survive: a Flash class, an easing engine, a media pipeline, a typed service layer, a 33-line derivation</text>
<text x="42" y="448" class="sub">· the current era is the first where platform, agents and edge hardware are one system</text>
<text x="28" y="500" class="sub">Era boundaries are approximate: employed 2000–2020 per the résumé, independent practice 2020–.</text>
<text x="932" y="500" class="sub" text-anchor="end">six code exhibits span eras 1–5</text>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -0,0 +1,80 @@
# Phase 7.2 — diagrams
Eight diagrams, one visual language, light and dark in one file each.
Authored 2026-10-26. Source of truth is the SVG files themselves: each one is
self-contained, with no external font, image or script reference.
## The language
| Part | Rule |
|---|---|
| Canvas | `viewBox="0 0 960 540"`, `width`/`height` set so a bare file view is 1× |
| Type | 19 px section title · 15 px heading · 13 px label · 12 px caption, all `ui-sans-serif` system stack |
| Boxes | `rx=7`, 1.4 px stroke, four semantic tones: neutral, accent (the thing being described), teal (store / gateway), amber (guard, failure, limit) |
| Arrows | solid = data or control flow, dashed = trust boundary or approximate relationship |
| Colour | greys carry structure; one blue accent, one teal, one amber, nothing else |
| Text | every `<text>` is real text in the SVG, so it scales, selects and reads to a screen reader |
Every file carries a **byte-identical `<style>` block**. That is the whole of the
consistency guarantee: there is one palette, defined once, repeated verbatim.
A check that compares the eight style blocks and finds one distinct value is the
evidence that the eight diagrams share a language.
## Both themes in one file
The `<style>` block defines the light palette, then repeats the same selectors
inside `@media (prefers-color-scheme: dark)`. No duplicated files, no theme
toggle needed in the page, no chance of a light and a dark copy drifting apart.
If the site later grows a manual theme toggle, the SVG must be inlined into the
page (not `<img>`) so page CSS can override the custom properties. Until then
`<img>` is correct: the file follows the reader's operating system.
| Role | Light | Dark |
|---|---|---|
| ink (text) | `#1b222b` | `#e7edf4` |
| panel (box fill) | `#f4f6f8` | `#182028` |
| muted (labels) | `#5b6673` | `#98a4b3` |
| accent | `#1a6bc4` | `#5aa2f0` |
| teal | `#0f766e` | `#38b3a6` |
| amber | `#b45309` | `#d98b3f` |
| box stroke | `#c3ccd6` | `#35424f` |
## Mobile
A 960 px schematic cannot be read at 360 px. The presentation contract is:
- wrap the figure in a horizontally scrollable container
- `svg { display:block; width:100%; min-width:900px; height:auto }`
At 1× (container ≥ 960 px) the type renders at 15 px / 13 px. At the 900 px
minimum inside a 390 px container it renders at 14.1 px / 12.2 px — the same
order as body text, and the figure scrolls rather than shrinking into
illegibility. A figure that is scaled to 360 px instead reaches 5 px type and is
not readable; that is why the contract is a requirement and not a suggestion.
## Accessibility
Each file carries `role="img"` and both `<title>` and `<desc>`. The `<desc>` is
the alternative text — 190–285 characters, describing what the diagram asserts
rather than naming its parts. If a template sets `alt=""` on the `<img>`, the
`<title>`/`<desc>` are not read out; the template must supply `alt` from the
manifest's `alt:` field.
## Files
| # | File | Area | Answers |
|---|---|---|---|
| 1 | `01-homelab-topology.svg` | platform | where it runs, and where the trust boundaries are |
| 2 | `02-agent-architecture.svg` | ai | how agents are organised and why one gateway |
| 3 | `03-rag-pipeline.svg` | ai | which side of the pipeline is AI and which side is not |
| 4 | `04-edge-architecture.svg` | edge | why devices never talk to each other |
| 5 | `05-distributed-audio.svg` | edge | how eight speakers stay in sync, and how voice gets in |
| 6 | `06-media-pipeline.svg` | products | where the human gate sits in a media pipeline |
| 7 | `07-cost-resource-engineering.svg` | platform | the memory ceiling, drawn to scale |
| 8 | `08-era-timeline.svg` | history | five platform shifts and what survived them |
Facts behind them come from `reference/stack.md`, `reference/content-map.md` and
`docs/phase-3-deploy.md`. Diagram 7 is numbers from a measurement, not an
illustration: if the caps change, the bars change.

View File

@@ -0,0 +1,82 @@
# Phase 7.2 — diagram manifest
#
# Machine-readable index for the templates. `alt` is copied verbatim from each
# file's <desc>, so the text a screen reader gets and the text in this manifest
# are the same string.
version: 1
authored: 2026-10-26
viewbox: "0 0 960 540"
themes: [light, dark] # both resolved from one file via prefers-color-scheme
format: svg-text # real text nodes, not glyph outlines
external_refs: none # no fonts, images or scripts referenced
presentation:
desktop: {css_width: "100%", rendered_type_px: [15, 13]}
mobile:
wrap: "overflow-x:auto"
css: "svg{display:block;width:100%;min-width:900px;height:auto}"
rendered_type_px: [14.1, 12.2]
diagrams:
- id: platform-homelab-topology
file: 01-homelab-topology.svg
area: platform
level: 2
title: "Homelab topology"
answers: "where it runs, and where the trust boundaries are"
alt: "Three hosts with their roles and the trust boundaries between them: public internet reaches Caddy on .35, which forwards over the LAN to services bound to 127.0.0.1 on .13. .27 is build and dev with no public listeners."
bytes: 7079
- id: ai-agent-architecture
file: 02-agent-architecture.svg
area: ai
level: 2
title: "Agent architecture"
answers: "how the agent estate is organised, and why there is one gateway"
alt: "The operator reaches a fleet of agent sessions through one inbox. Paperclip is the control plane and delegates to peer projects, which answer the Hub read-only. Every model call in the estate passes through one gateway, OmniRoute."
bytes: 6027
- id: ai-rag-pipeline
file: 03-rag-pipeline.svg
area: ai
level: 2
title: "Retrieval pipeline"
answers: "which side of the pipeline is AI and which side is not"
alt: "Left, offline ingest: content files are validated, chunked with metadata, embedded and upserted into Qdrant, while the same files render to static pages with no AI in the path. Right, per-visitor query: guardrail, query graph, filtered retrieval, then a streamed answer with citations."
bytes: 7010
- id: edge-architecture
file: 04-edge-architecture.svg
area: edge
level: 2
title: "Edge architecture"
answers: "why devices never talk to each other directly"
alt: "Constrained devices publish over LoRa, Zigbee or WiFi to a single MQTT broker, which feeds one automation layer that drives actuation. Retained configuration, telemetry and last-will presence let devices recover unattended."
bytes: 6896
- id: edge-distributed-audio
file: 05-distributed-audio.svg
area: edge
level: 2
title: "Distributed audio"
answers: "how eight endpoints stay in sync, and how voice gets in"
alt: "Music and speech sources feed one Snapcast server, which streams one clock-synchronised mix to eight Pi endpoints. Voice announcements arrive as a priority lane: duck the music, speak, restore."
bytes: 6833
- id: products-media-pipeline
file: 06-media-pipeline.svg
area: products
level: 2
title: "Media pipeline"
answers: "where the human gate sits in a media pipeline"
alt: "Five stages from download to library: fingerprinting gives every file a natural key so re-runs converge, audit verifies the copy, and review is a deliberate human gate before anything is published."
bytes: 5694
- id: platform-cost-resource-engineering
file: 07-cost-resource-engineering.svg
area: platform
level: 2
title: "Cost and resource engineering"
answers: "the memory ceiling, drawn to scale from measured caps"
alt: "A bar chart of the per-service memory caps drawn to scale against the 1024 MiB ceiling, totalling 970 MiB across four containers, beside the rules that hold the ceiling: equal memory and swap limits, loopback binding, a hard container count and a cost ceiling."
bytes: 6151
- id: history-era-timeline
file: 08-era-timeline.svg
area: history
level: 2
title: "Era timeline"
answers: "five platform shifts and what survived them"
alt: "Five consecutive platform eras from Flash and ActionScript through jQuery and PHP, React and TypeScript, server-driven Go with HTMX, to agent systems, on an axis from 2000 to 2026. Era boundaries are approximate; the bands show order."
bytes: 6039