Changes touching this path

  • Scaffold site/ + wire Vercel #2, loot. subdomain, install proxy (#257) Thin end-to-end tracer for loot.millerbyte.com (spec docs/specs/loot-site.md SS1-3): site/ = TanStack Start SSG consuming published @millerbyte/ui@^0.1.0 (theme.css at the root), five placeholder surface routes prerendered, deployed on Vercel project #2 (root site/) at https://loot.millerbyte.com. Install proxy: the spec's vercel.json external-rewrite mechanism was falsified live (Vercel passes GitHub's releases/latest 302 through to the client), so /install.sh and /install.ps1 are Start server routes that fetch upstream and stream the bytes back 200 - spec SS2 amended in place. site/** explicitly public in .lootattributes; .vercelignore allowlists site/ so .loot/ keys and docs/pitch/ can never reach the Vercel builder; site build artifacts lootignored. 1931146c · dbf3dbe6…
  • Scaffold site/ + wire Vercel #2, loot. subdomain, install proxy (#257) (#291) * Scaffold site/ + wire Vercel #2, loot. subdomain, install proxy (#257) Thin end-to-end tracer for loot.millerbyte.com (spec docs/specs/loot-site.md SS1-3): site/ = TanStack Start SSG consuming published @millerbyte/ui@^0.1.0 (theme.css at the root), five placeholder surface routes prerendered, deployed on Vercel project #2 (root site/) at https://loot.millerbyte.com. Install proxy: the spec's vercel.json external-rewrite mechanism was falsified live (Vercel passes GitHub's releases/latest 302 through to the client), so /install.sh and /install.ps1 are Start server routes that fetch upstream and stream the bytes back 200 - spec SS2 amended in place. site/** explicitly public in .lootattributes; .vercelignore allowlists site/ so .loot/ keys and docs/pitch/ can never reach the Vercel builder; site build artifacts lootignored. Landed via break-glass git branch: the loot-native lane land was blocked by two live shared-store bugs (review-ferry folds a described WIP into a reconcile finalize; describe/status disagree on the working change under concurrent sessions) - tickets to follow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Apply #291 review findings Cache only successful upstream responses in the installer proxy (a transient GitHub failure must not be CDN-cached for 5 minutes); extract the install one-liners to one shared module (three copies drifted apart otherwise); fix the vite.config "fully static" comment and spec SS0 framing to name the two server-route exceptions; tighten the .lootattributes rationale (the interim manual deploy is gated by .vercelignore, not the GitHub projection); drop the cargo-culted overrides block (its pins targeted server deps the site lacks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com> 231eb5f8diff
  • the forge's published surfaces render themselves anonymously, and each response's cache class rides out of the selector that produced it (#751) #749 built the read tier and nothing called it. This is the six surfaces #466's table named -- profile, repo home, tree, blob, history, diff -- as SSR routes over that tier, plus robots and a per-owner sitemap. Three properties are structural rather than configural, which was the point: STRICTLY ANONYMOUS (#486 Q4). No handler in site/src/server/forge.ts reads a request, a header, a cookie or a token; each takes only what the URL already says. So 'never serve one identity's view to another' is a fact about the module's signature rather than a cache rule someone must remember. Adding an identity parameter is the one edit that would break it. THE CACHE CLASS IS CARRIED, NEVER RE-DERIVED. resolveSelector already answers how a resolution may be cached; routes read loaderData.cache and map it in one place. That is what keeps the ADR 0032 trap shut: a change_id LOOKS like an address but is the durable handle carried across amends, so it resolves 'revalidate', and a pinned version_id is the only cache-forever class. Asserted twice -- a unit test on the header text, and a live render proving one URL family splits three ways. HONEST RENDERING (#497). A locked path shows its visibility tag and no bytes; grant_log is never queried, so a grantee set cannot leak from a surface with no way to learn one. Burned reads as a tombstone, sealed as 'Not published', absent as absent -- three outcomes a blank page would have made indistinguishable. The 404 is deliberately one 404: 'no such repo' and 'metadata private' must not be told apart. Also here: a published-only repo (metadata private, paths published) gets its index and nothing else -- no tree, no history, no diff -- which is 0005's barrier showing through the UI unchanged. Per #748 the profile lists metadata-public repos only, there is no global directory, and the sitemap is therefore per-owner: readable at a known URL is not consent to be advertised. vite.config.ts grows the prerender deny-list its own comment called unnecessary -- true until a route read a database. Acceptance: 49 tests green (31 new pure ones; the 8 read-tier tests against a throwaway 0001-0005 cluster), npm run build clean with exactly the five authored surfaces prerendered, and a 34-assertion end-to-end smoke -- real sealed bytes through R2 -> decrypt -> unzstd -> highlighted HTML, all three cache classes, every honest-rendering outcome. The client bundle carries no pg, no SQL and no credential. Publication: README.md, LICENSE-MIT and LICENSE-APACHE reach @world here, the set .lootattributes has declared since #750. Operator-authorised at the landing sign; nothing else in the tree changes tier. 2aa21bbe · dbf3dbe6…diff
  • the browser mints the seed and the account binds the key: Clerk in the site's own routes, variant-B onboarding, and the account tier's one producer (#753) Migration 0007 adds account_seed (the passphrase-wrapped envelope, opaque ciphertext the server cannot open) and forge_identity, a NOLOGIN role scoped to exactly the identity tier -- SELECT/INSERT on tenant, SELECT/INSERT plus a column-scoped UPDATE of retired_at on account_key, the full row on account_seed, and nothing else (pinned live by forge_identity_is_scoped_to_the_identity_tier). The producer lives in site/src/server/identity/ behind a verified Clerk session (verifyToken in the site's own route handlers, reusing the millerbyte Clerk instance and pattern, never the gateway service). Registration is one transaction: upsert tenant on the verified clerk_user_id, add the key, store the envelope, retire what rotation names -- and never zero live keys. The Rust service still writes none of the tier; tests/account_tier.rs now guards that boundary instead of a vacancy. Onboarding is #487's variant B, the two-column contract: the app owns the 32 bytes (crypto.getRandomValues -> 24-word BIP39 over the ENTROPY, never the 64-byte derivation -> ed25519 pubkey via @noble/curves, held to the engine's from_seed by the pinned RFC 8032 vector), a WebCrypto PBKDF2+AES-GCM envelope, a .txt download that excludes the passphrase, and two consent checkboxes that no download waives. Clerk mounts only under /account, which joins /k/ on the prerender deny-list; every static surface stays static. Follow-ons filed rather than folded: #926 (CLI raw-seed import, so BIP39 recovery reaches the CLI) and #927 (account-aware may_push, burn entitlement via account, and the #506 historical-key 301s the anonymous read tier deliberately cannot serve today). 278e1886 · dbf3dbe6…diff
  • the receipts are served, not linked: the Evidence page stops pointing at a repo its readers cannot open (#840) Twelve github.com links, every one a 404 for the only audience this site has — Connor-Miller/loot is private permanently (ADR 0045). Nine of them were the Evidence page run-output and evidence-doc links, i.e. the receipt for every thesis claim that page makes, so it asserted a set of proofs and handed the reader nothing to open. The other three were the nav/footer repo links and a LICENSE link, dead twice over since no file of that name exists either. The nine receipts are now inlined at build time with `?raw` and disclosed in place under each excerpt. A link — to GitHub, to a forge surface, to a route of our own — is a promise checked at READ time by someone not here to fix it; an import is checked by `vite build`, so moving one of these files breaks the build in the commit that broke it. This page cannot go stale the way it just spent months being stale. Not served from the forge, which ADR 0045 says is where the world should read loot: measured today, /k/connor and /k/connor/loot both answer 404 — it carries no repo yet, so pointing there would have swapped nine dead links for two. When it does, these become links again and the imports stay as the build-time check. The license moved to a page of its own rather than being dropped: a reader running curl | sh has a real reason to want the terms, and both texts are imported rather than transcribed so they cannot drift from the repo. A sweep test asks the reader question of the whole src/ tree, not the two files that happened to be wrong. It bans href/to/url pointing at github.com and deliberately allows the string in prose — installerProxy documents what dist bakes into the installers, and install.tsx prints cargo install --git as text (that command is #841 own decision). ⭐ The defect was never a wrong URL: it was that nobody ever opened one anonymously, and the person editing these files is always signed in. Verified: no github.com href on any of the 6 prerendered pages, /license prerenders, suite 77 passed | 23 skipped, build green. ⚠ Residue: the inlined evidence DOCUMENTS quote issue URLs in their own prose. They render as inert text in a <pre>, not as links, and rewriting a historical evidence document to hide its citations would falsify the thing it is evidence of. ad95716f · dbf3dbe6…diff
  • every key an account ever held keeps resolving: migration 0008 publishes the one retired-to-canonical pairing the 301 itself discloses, and /k/<retired> redirects everywhere in one seam (#927, executing #506) 3a0fee42 · dbf3dbe6…diff
  • the browser opens what the forge cannot: own-private read ships an owner read tier, a once-per-device unlock, and a grant mailbox that never acks (#1043) c8dd4da1 · dbf3dbe6…diff
  • the private shell learns to diff: two addresses, two keys and two openEntry legs meet diffLines in the page, and an unreadable side is named rather than folded into the pair's verdict (#1057) 94014155 · dbf3dbe6…diff
  • the trust matrix says what each reader can actually read, and /why stops claiming the relay cannot read your code (#1130) Publishes /trust as the one canonical answer to who reads what: six readers against four tiers, with a regardless-of-tier block for path names and DAG metadata, and footnotes on the two cells whose plain answer misleads — Restricted vs the git mirror rests on ADR 0028 projection omission rather than keys, and an embargo reveal is enforced by the relay clock rather than by maths (ADR 0027, written down nowhere until now). #1089 falls out of the matrix as its ticket predicted: the /why headline was false over ordinary code, since Internal is the default and a relay reads it by design. Every other surface now cites /trust instead of answering independently, which is what let /why and /docs drift into contradicting each other. ADR 0069 records that Internal stays the default deliberately — Restricted by default would make a clone readable by nobody and empty the git projection. ADR 0068 records the at-rest design for #1138: .loot/keyring is raw oid-key pairs beside the ciphertext it opens, so the unit of exposure is the whole .loot/ directory. Carries ADR 0067 from #1132, which shared this tree uncaptured. 361b2026 · dbf3dbe6…diff
  • the Known Issues page publishes what costs a tester their work, nine findings re-run against v0.4.16, and it is updated on report against a dated last-review rather than a cadence nobody keeps (#1143) 48f5788f · dbf3dbe6…diff
  • the docs become fifty-six routed entries derived from one registry that the nav, the reading order, the search and the prerenderer all read, so those four can no longer drift apart the way ten hand-listed surfaces already had to be watched; the thirty-eight pages nobody has written yet are real bookmarkable URLs that say what they will cover and carry noindex until they earn a place in an index, rather than dead rows in a sidebar; and the byte gate learns to walk nested surfaces, which is the only reason the fifty-two new pages weigh anything at all instead of shipping past a gate that read one directory level (#1378) 424be083 · dbf3dbe6…diff
  • repo_member gets the producer it has never had and it is the site's: migration 0013 creates forge_member_writer holding SELECT, INSERT and DELETE on repo_member plus SELECT on account_key and repo and nothing else, named writer rather than forge_member because ADR 0077 reserves that name for #1726's read role - and a test enforces the reservation. The load-bearing half is that membership is addressed by pubkey and STORED BY ACCOUNT: the owner names a collaborator pubkey, the server resolves it through account_key to a tenant_id, and storing the key instead would silently un-member anyone who rotated, which ADR 0016 makes an account fact and #927 already taught the push path. That resolution deliberately ignores retired_at, proved by adding the filter and watching a rotation test refuse a member who had merely rotated. The site module is a SIBLING of identity, read and owner rather than a widening of any of them, and the route reuses withOwnedRepo for authorization while every membership statement ALSO joins repo on owner_pubkey, so the database refuses a stranger independently of the route - proved by dropping that clause and watching a stranger's repo really take the row. Thirteen guards were each broken and read before being restored, and the one worth naming is the column-ACL half: a live GRANT UPDATE (tenant_id) ON repo_member PASSES the set equality and is caught only by the column check, because has_table_privilege cannot see a column-scoped grant, and its positive control revokes 0007's own column grant to prove the query can see one at all. The accountless refusal is its own sentence rather than a foreign-key error because a keypair is free and infinitely mintable, so an accountless member can be neither quota'd nor banned; and the honest-revoke copy interpolates MAX_PRESIGN_SECONDS from read/blob.ts so it cannot drift from the sixty-second window it admits is small and not zero. account_tier.rs's assertion is untouched and passes unamended, which is what ADR 0077 predicted - what needed repointing was #1639's own replacement header, true when written and false on landing. ADR 0059's count of the site's Postgres URLs was a live count, is now four, and is named rather than counted. Membership is write-only until #1726 and #1728 give it a reader, and provisioning is a different repo, so the route throws until scripts gives the role LOGIN (#1730) (#1725) 4fac0c99 · dbf3dbe6…diff
  • the foreign grant stops applying itself, and that was a LIVE defect rather than a guard this ticket adds: pullMailbox indexed every verified blob alike and discarded the grantor pubkey, so a grant somebody else deposited in the mailbox was applied SILENTLY the first time any file at its address was opened - the exact opposite of #488's decision that nothing auto-applies. The mailbox is sorted BY GRANTOR now: a self-grant under ADR 0057 still goes straight into the key index, while anything signed by another key becomes an offer in a second map the decrypt path cannot reach, so the property is structural rather than a check somebody has to remember. Restoring the old line turns nine of sixteen cases red, headed by a stranger's key entering the key index, and the positive controls are what make locked a decision rather than a broken fixture: an accepted grant reads open with bytes equal to the content key, and a self-grant in the SAME pull still opens itself. The copy is asserted as RENDERED rather than as source, and the decline half is honest for a reason it states out loud - the only wire act that could take a grant out of your mailbox is an ack, and an ack would delete the blob for every other device you own, so this page does not have one - which is why declining revokes nothing and says so, against an absent-phrase list that refuses has been revoked and no longer has access. Expiry's first line is WELDED to the refusal constant rather than restating it, an expired offer stays visible after a refused accept, and a control files the same grant one second before it expires. The unprovisioned member tier is ASKED rather than remembered: the me route answers whether the read class is live and the browser tests that for exactly true, so a missing field lands on the arm that promises nothing, and the dark arm - the live one today - says the repo answers not yours as a 404 whether or not you have been added. Three things were found on the way and each is measured rather than argued. Importing the fingerprint helper dragged a curve library into the shared entry at eight kilobytes on every PUBLIC route, fixed by moving the function to the dependency-free module beside it. A lazy route was measured and REJECTED at 581 bytes against 270 eager, because the split chunk's manifest outweighs the code it saves, and that is recorded in the route so nobody fixes it back. And the byte record had to be raised, so the raise is SPLIT rather than apportioned: the same tree with this ticket removed already reads a thousand and thirty bytes over the recorded numbers on every surface, which is pre-existing shared-entry drift, where this page itself costs 270. The budget tool had a real bug that recording surfaced - its lowering note is anchored to the end of the line so a later lowering can replace its predecessor, but a raise appended after it buried the note out of that anchor's reach and the next lowering stamped a second one - fixed with a fixture that is red without it (#1640) 152d0635 · dbf3dbe6…diff
  • the metrics dashboard ships as SIX STATIC PANELS behind an operator allowlist, and the two reviews moved it from a client-fetched page with a 401 to a server-rendered one that cannot be probed: 1623 settled Variant A, the Briefing - one column, one section per question, each caveat as PROSE beside the number it qualifies, which is the structural reason it won, since a caveat there cannot be skipped without skipping its number. the access decision is that being signed in is not the same as being the operator. anyone can sign up to this host and the data is site-WIDE - every visitor path, referrer host and install-funnel count - so the door is a custody list in the 1045 shape: a Clerk id is on it or it is not. it fails CLOSED in every branch, and the proof is that the fail-OPEN spelling was written and run: allowed.length greater than zero AND not includes, then return true, reads as careful and admits the entire internet the moment the list is empty, which is the state a box is in before its first deploy writes one. exactly two tests went red on it. the security review then found the route around that allowlist was the wrong shape twice over. the check sat INSIDE withPrivateSession, which maps an unauthenticated caller to 401 - so a stranger probing the endpoint got a 401 where an unserved path gets 404, and the refusal meant to hide the route confirmed it, while a load-bearing comment three lines up asserted the opposite. and the page itself answered 200 to anyone, rendering a document and only saying not found after a client fetch failed. both collapse into one server function returning null for BOTH refusals, which the loader turns into a real notFound: signed-out, signed-in-not-an-operator and a path we do not serve are now one answer. that rework is also what the ticket asked for and I had not done - the panels render into the HTML from a route loader rather than from a browser round trip, so the acceptance criterion about server-side rendering stops being approximated. the second finding is sharper than it looks: the route declared no headers, and every private route sets private no-store BECAUSE an nginx proxy_cache sits in front. an operator document without it is cacheable at the edge, which is the edge handing one caller a page meant for another. it now carries PRIVATE_HEADERS, and that constant own comment is widened to say it covers two tiers rather than left reading as private-only. one real data bug came out of the same review and no test I had written could see it: ordering a to_char MM-DD label sorts the series as TEXT, so any window crossing new year draws january before december - the sparkline scrambled about thirty days a year on the one chart whose job is a shape over time. grouped and ordered on the timestamp now, formatted on the way out, and pinned by asserting the grouped expression is not a to_char, verified by restoring the bug and watching it go red. built against the PRODUCERS rather than the schema, which is what the prototype was really for: visitor_key is null on every log-sourced row and permanently will be since 1632 took the address out of the log, so there are no uniques and no sessions here at all, and device, browser, os and country have no producer either. the panels that wait on one are rendered, named and marked rather than omitted - uniques on 1757, forge usage on 1758, downloads on 1647 and telemetry on 1658 - because a dashboard that silently drops what it cannot see is how you talk yourself into a wrong conclusion. not prerendered, and that is a security property rather than a build detail: a prerendered page is served as a static FILE with the static route headers, so an operator surface would exist as bytes anyone can fetch with the door never consulted - the same reason private joined that filter in 1043. asserted, and the mutation confirms the assertion bites. it is correspondingly absent from the byte budget, which discovers prerendered surfaces, and its styles stay in the route rather than styles.css because that sheet is eager on all sixty budgeted surfaces and an operator page has no business charging them (#1794) 2c969a0e · dbf3dbe6…diff
  • the site gets the privacy notice it has been collecting without, and every sentence is written against what is ACTUALLY retained rather than against the pipeline design: 1624 says do not ship the collector before the notice is live and the ordering is the point, and the collector shipped first - this closes that, late, and says so rather than pretending the sequence held. writing it to the design would have published a claim that we collect MORE than we do. the beacon, the download hop and the telemetry channel are unbuilt, and visitor_key, country, device, browser and os have no producer at all, so a notice describing a daily visitor salt or country-level geo would be false in the direction nobody checks. what is actually kept is the access log for 30 days - time, method, address, status, bytes, referring URL, full User-Agent, cache status, response time, and NO client address, written as a literal dash - plus rows derived from it with the referrer reduced to a host and the User-Agent reduced to a coarse category. ⚠ one sentence of my own first draft was false and the scrubber refuted it: I wrote that the log records a route shape rather than the address you asked for, which is true only for repository and private-repository URLs. ordinary page addresses are recorded in full, so the claim understated collection on nearly every request. corrected to name the exception as an exception, checked against scrubPath rather than against memory. the word anonymous is deliberately not used of the log, per 1617 - the rows carry no identifier, but a full User-Agent kept for thirty days could in principle distinguish an unusual browser, and claiming anonymity is the wording that turns a data-protection question into a consumer-protection one on a product sold on privacy. the page explains why it avoids the word instead of using it. the objection is real machinery over a currently empty set, and the page says exactly that. one cookie, set only on request, holding a constant rather than an identifier - the opposite of a cookie that counts you. it suppresses collection that happens in the APP, it cannot reach the nginx access log which is written before the app sees the request, and today the log is the only source. so it presently suppresses nothing, which the notice states plainly rather than implying a working control. every future app-side collector must consult hasOptedOut, which is why it lives in lib rather than inside one caller. the retention number gets the 1779 treatment, because it is the one number here that is a published statement about personal data: 30 lives in site/src/lib/retention.ts, the bound is enforced by RETAIN_DAYS in the scripts repo, and setup-site now refuses to deploy when they disagree - a copy with no check is how the notice quietly outlives the thing it describes, and the dangerous direction is raising retention on the box while the page still says thirty. terms ships beside it with two deliberate absences: no governing law or jurisdiction, and no liability cap - inventing either would be a legal claim written by nobody qualified to make it, and a wrong one is worse than none. what is there instead is factual and checkable, including that backups are OPERATOR disaster recovery and not a per-account restore service, which is ADR 0046 and the opposite of what a reader would assume. contact is security@millerbyte.com, the address that exists and is monitored, rather than a privacy@ alias that would have to be created at the mail host first - publishing an address that does not resolve would be worse than reusing one that does. /trust gets a pointer rather than a second copy, since two copies drift and then one of them is false. the footer carries both links on every surface, which costs about 650 bytes per surface and is recorded in budget.json with that reason, because a notice reachable only by typing its URL is not published in any sense that matters (#1624) 12951042 · dbf3dbe6…diff
  • the download hop lands as a counted redirect that cannot be steered, and the two things 1647 asks for that do not exist yet are named rather than faked: GET /dl/tag/asset records a start and 302s to R2. the target is CONSTRUCTED from two validated segments and no byte of the request appears in it, because /dl sits on the hostname that also hosts the identity flow and the install one-liner - a redirect an attacker can steer is a phishing primitive wearing our provenance, a link that genuinely starts at our domain and ends wherever they chose. validating and then echoing is one refactor away from that; building from a fixed base plus two matched shapes is not. the segments are read from the RAW pathname rather than decoded route params, so this route decides what decoding means instead of inheriting it. every refusal is 404 rather than 400, because a 400 separates malformed from no-such-thing and telling a prober which guess was closer is free help. sha256.sum and the installer scripts are refused even though they are real files at that prefix: the hop counts download STARTS, and the land-time site gate fetches sha256.sum from R2 on every land - admitting it would make every land a download. ⭐ a mutation found one of my own guards doing nothing. deleting the explicit percent check turned NO test red, because the anchored patterns already reject percent - so the comment claiming it comes first because patterns are the fragile direction was describing a guard by what it looks like rather than by what it does. the code stays as a second fence and the comment now says it is one. unanchoring the tag pattern turns two tests red, which is the anchors earning their place, and the absolute-URL cases stay green under that mutation because the slash check is what catches them - a more precise picture than I had. the write-behind queue is built here because 1647 is blocked on it and it does not exist; 1757 shares it. enqueue is synchronous and never awaits, because this path serves the install one-liner where the caller is a shell script already piped to sh - a database stall there is the most expensive on the site and the least recoverable. bounded, drops the NEWEST so a burst cannot evict what preceded it, counts every drop because a silently discarded row makes the dashboard wrong in the direction that looks like not much traffic, and a failed flush puts the rows BACK rather than losing a batch to a blip. ⚠ NO visitor_key, and that is a deviation stated rather than a gap missed. 1647 asks for one derived from X-Real-IP as the beacon does - but the beacon owns the daily salt and neither exists, and deriving one here would make the privacy notice statement that these records carry no address, no cookie, no key FALSE the day it deploys. downloads-per-day needs no identifier; uniques do, and they arrive with 1757. ⚠ only ONE of the two internal arms is reachable. a request with no X-Real-IP never passed nginx so it is ours by definition, which covers the deploy probes. the edge computes loot_internal for our own hosts but forwards only Host, X-Real-IP, X-Forwarded-For and X-Forwarded-Proto, so install-detect - which runs the real one-liner through the public URL once per release - is counted as an ordinary download. a handful of rows per release rather than a distortion, and naming it beats a comment implying both arms work. EventRow source widens from log to the set migration 0001 actually permits. it said log while the shipper was the only producer, which read as a fact about the schema and was a fact about the caller. /dl joins the prerender deny list, or crawlLinks follows the new links off /install and the BUILD counts a download per platform per land (#1647) db35af90 · dbf3dbe6…diff
  • the beacon counts PEOPLE where the log can only count requests, and the notice that describes it was rewritten in the same change because deploying one without the other publishes a falsehood loot#1757, over loot#1620 and loot#1617. POST /api/beacon takes one short message per view and derives SHA-256(daily_salt || ip || ua || site) truncated to 16 bytes, in process, discarding both inputs. It is canonical for uniques and sessions and for nothing else: the log stays canonical for volume, because it sees the ~53 prerendered pages, the cache hits and curl, none of which run any JavaScript. Two sources counting volume would be two numbers to reconcile. The key is length-prefixed per field rather than concatenated, and that is a real defect closed rather than a flourish. One of the fields is the User-Agent, which the CALLER chooses: with plain concatenation ip=1.2.3.4 ua=5 and ip=1.2.3.45 ua= hash identically, so a visitor could be merged onto a neighbour. The mutation that drops the framing reddens exactly that pin and nothing else. Two decisions are recorded rather than defaulted. NO SALT, NO ROW: a beacon row without a key adds to a count of beacon rows while contributing nothing to the only two things this source is canonical for, so it would trade what we came for against a number competing with the log. And the module is EAGER, which is the answer to the ticket first hazard - ADR 0071 weighs the document plus eager assets, so a deferred chunk is paid by every visitor and counted by nobody. Measured at ~450 B on each of 62 surfaces and RECORDED, and loot#1640 is the precedent that makes this more than taste: it measured a lazy route at 581 bytes against 270 eager. The edge half is new and had to be, because this is the site first unauthenticated row-writing endpoint and the site vhost had no rate limiting at all. limit_req at the /api/ PREFIX, not at /api/beacon, so ADR 0074 telemetry arrives protected rather than silently exposed. Idempotence is remove-then- reinsert between markers, and loot#1647 install-detect arm is closed in the same patch since it needed the same only-if-absent vhost step. Its ordering is load-bearing: the blanket header insert runs while only hand-written blocks exist, which makes a duplicate unrepresentable instead of guarded against. Found by RUNNING it, which is the whole argument for the fixture: the marker ended in (generated), which awk read as a GROUP, so the strip silently never fired - the second run appended a second /api/ block while grep -v tore the limit_req line out of the first, leaving a vhost that would have failed nginx -t on the box. Pinned now, with a control proving the predicate can say no. The notice had to move with the code. Five published statements would have gone false, the worst being that these records carry no key and nothing links two requests to the same person. Rewritten to lead with the key, say the secret is DESTROYED rather than merely rotated, and disclose that a visitor spanning midnight is counted twice. FOUR more contradictions were then found only by RENDERING the page, which is the third time that has been the only thing that worked. Objecting still said the log is the only thing we collect from, so this control has nothing to suppress - three paragraphs after the new text says the browser sends nothing when you object. Erasure and Your rights both still claimed nothing is keyed to you. And JSX drops a newline beside a tag, so the page shipped visitorkey and notstored as single words. The font gate then caught a star glyph absent from the shipped subset, which would have rendered as tofu. A test that asserted we do NOT collect a visitor key survived all of this green, because its regex matched one verb phrasing the new prose never uses. It is inverted now: the page MUST name the key, MUST say destroyed, MUST disclose the midnight double-count. Leaving it would have let a later edit delete the disclosure and stay green. 532 site tests, 322 scripts tests, budget green with the notice raise on the record. 3c22dcae · dbf3dbe6…diff
  • the drill-down takes the dashboard door and pays for react-table on its own route alone, and ADR 0087 records the metrics pipeline as one document loot#1804 and loot#1636 in one land, because a docs-only land pays the full gate and the drill-down is the last code the ADR describes. The drill-down is /metrics/paths and /metrics/referrers - a react-table over the same rows each bar panel summarises, sortable, bounded at 200 with the distinct count beside it. It is the only interactive escalation and it lives here, not on the dashboard: react-table is imported by this route alone, pinned by a test and MEASURED - the dashboard chunk moved by 80 bytes gzip for its two footer links, the drill-down chunk is 13.1 KB gzip and paid only there, and every other surface grew by ~220 bytes for the route-tree entry, which is the manifest cost loot#1640 predicted. Excluded from prerender by the /metrics prefix rule, METRICS_HEADERS for the dashboard reasons. The door is the hard part and it is the SAME door. operatorReader now lives in operator-door.ts and both the dashboard and the drill-down reach it through a dynamic import inside a handler, returning null for every refusal so the loader raises one notFound - a stranger, a non-operator and an unknown kind are indistinguishable from each other and from a path we do not serve. The kind is refused by the LOADER before the server function runs, and the validator narrows rather than throws, because a thrown validator would surface as a 500, a third distinguishable answer. Two defects were found by the BUILD and the BUDGET with the typecheck green both times, which is why both are pinned as tests rather than remembered. A plain exported function that a route module imports statically is not stripped from the client bundle the way a handler body is, so the first refactor dragged @tanstack/react-start/server into the client and the build refused it. And a VALUE import of two constants from the pg-backed reader put pg into every client bundle - 62 surfaces up by 24.8 KB each - so the constants a route may need now live in lib/metrics-drill.ts with no server dependency in the graph, and read.ts imports them back. ADR 0087 records the pipeline: three collection points and why no one of them can see the surface, the loot_metrics database and its three roles, the visitor key with the two rejected salt constructions, the route-shape rule and its two enforcement points, the account-reference amendment to the map, the retention and rollup boundary, the edge-log posture as the reason uniques need a beacon at all, and the departures and residue - the kept User-Agent, the eager beacon as the answer to the gate hazard, the error log, the out-of-scope vhosts, the shared disk. It is 0087 and not the 0074 the ticket named: telemetry took 0074 the night after this claimed it, and twelve more landed while this waited on its build tickets. Four glossary entries ride the same land - Visitor key, Session, Route-shape path, Marginal rollup - each with the framing it rejects. 624 site tests, published_surfaces gate green, budget green. 984260b2 · dbf3dbe6…diff
  • the opt-in telemetry channel is built, and off means no call because a paired test says so rather than a sentence loot#1658, building what ADR 0074 specified and loot#1626 shipped only the guard for. loot telemetry on | off | status | show; an append-only accumulator that writes one line per dispatched verb, the dispatch-table name, with burn and maroon collapsed to other AT WRITE TIME so no local trace of an incident exists either; a sender that runs in main after the verb has printed and decided its exit code, at most once per 24 hours, behind a 3-second hard timeout, that can never print, never change the code, and never delay a verb; a spool that IS the outbox, so show reads the same entry the sender transmits; and a 16-byte id minted on opt-in and deleted on opt-out, never machine-id. The guarantee is the pairing. One local listener, the real binary, a sandboxed config home: init, status, doctor and show with telemetry off make ZERO connections; on, the same verbs make EXACTLY ONE, and the same verbs again in the same period make no more; off again makes none. A lone assert-zero passes when the listener is broken, the binary crashed early, or it pointed at the wrong port - the exact shape of the logrotate -f check that could never fail. The endpoint override is https only with the loopback LITERAL the sole exception, enforced twice, and localhost is refused because what it resolves to is the resolver business; the refusal happens BEFORE an id is minted, so a broken override never leaves an identifier that can never be used. Clause 5 of the field rule is a property of the wire bytes, not a promise: a test parses every string out of the JSON and requires each to be a compile-time constant, a member of a fixed array, a dispatch-table name, or the hex id. The platform is a build-script constant rather than a runtime assembly from std consts, which is what the rule forbids spelled from constants. The verb emits machine output - show --json IS the report - so it joins the emitting tier rather than the prose-only debt list the ceiling forbids growing. Every count that moved was moved in the documents that state it, because the censuses read them: 129 verb lines in three sites, 82 dispatched and 80 in the table and 25 emitting in ADR 0066 with telemetry in the named list, all 82 verbs and the block in the README, the CLI page, and ADR 0074 eight-entry allowlist - because the endpoint is now the binary ONE destination it contacts rather than prints, and its ALLOWED row says under what condition. The destination census flagged three lines that dial nothing: a printed hint naming http://127.0.0.1 and the two scheme-prefix tests in check_endpoint. They are respelled rather than exempted - the check splits on the separator and compares the scheme WORD, same rule, no allowlist row - because a row is a sentence someone has to be able to defend and these had nothing to defend. The site half: POST /api/telemetry validates a report against the SAME closed sets the binary declares, pinned cross-repo by reading the Rust source, and refuses WHOLE on any field outside them. A valid report becomes source=cli rows, one per verb invocation under /cli/<verb> plus two bucket rows, all stamped with the arrival time so no ordering exists to reconstruct; the telemetry_id rides the row and expires with the 30-day partitions. It sits behind the same /api/ limit_req the beacon needed, which is why the ADR put it there. doctor prints on/off, the EFFECTIVE endpoint, last send and the spool. The notice gains the CLI tier, as ADR 0074 §11 requires: sends nothing unless you run on, the id links reports and off deletes it, what is never in a report, show prints it before you decide, and the erasure route - quote your id within 30 days, you hold the key. The test that asserted the page does NOT collect telemetry is inverted, as it was for the visitor key. 1214 lib and 118 bin tests, 51 suites, 634 site tests, budget and font gates green, rendered and read. 8a919b01 · dbf3dbe6…diff
  • the private repo surface becomes a workbench: /private/<owner>/<repo> is one layout route that mounts a VS Code-like shell once behind one gate (activity bar, side panel, preview and pinned tabs, status bar, Ctrl+P quick open) and keeps it across navigation, each child route naming its document with OpenDocument rather than rendering a page; a view is one module plus its routes, WorkbenchView in components/workbench/view.ts is the contract and VIEWS the one list the shell derives from, and the shell names no view. two views ship, Files (explorer over one new /api/private/manifest read, file editor with gutter, minimap, #L links and per-file decrypt, folder overview) and History (ledger panel, ledger, path and two-sided diff documents moved from the old pages), History built second against the contract and needing no shell edit, which is the test of the operator requirement that new views such as proposals and tickets be easy to add. /api/private/repo answers the header without a tree read, /api/private/tree and the page chrome it fed are deleted, every URL #1043 served still resolves. ADR 0094 and CONTEXT.md record it; 768 site tests pass, each new test red under a named mutation and restored; no migration, no format byte, no forge change, site deploy only (#2327) 7462b904 · dbf3dbe6…diff
  • every /private page rendered blank live because the private CSP, script-src self and wasm-unsafe-eval with no unsafe-inline, refused the two inline scripts TanStack writes into every page, its scroll restoration and the per-page $tsr SSR payload; each private document now gets a per-request nonce, minted by a request middleware in the new src/start.ts, stamped by the router on the scripts it writes through ssr.nonce read from that middleware context, and named in the policy the middleware sets on the response, so script-src still carries no unsafe-inline and injected markup cannot run. the start instance restates TanStack default CSRF middleware for server functions, which defining one would otherwise drop. the routes keep the nonce-less policy as the fail-closed fallback, /api/private/* and public pages carry no nonce, and /private now renders under vite dev too. a new test starts the built server and checks every inline script on three private pages carries the nonce its own response names, that nonces differ per response and that a public page has none; red under mutation, counts read each time: the router never given the nonce (3 failed, 2 passed), the middleware writing a policy without it (4 and 1), a constant nonce (1 and 7), unsafe-inline beside the nonce (1 and 7), the path test matching /privateer (1 and 7), each restored to green. ADR 0094 and CONTEXT.md are amended; 777 site tests pass; no migration and no forge change, site deploy only (#2335) aa34d4f1 · dbf3dbe6…diff
  • key management on the site: a key can be retired by itself, named, and chosen at unlock. per the operator grill on #2398 (ADR 0100): POST /api/identity/retire retires one key of the signed-in account, refusing the last live key under the account-row lock registration takes and never re-dating a retirement, with no route to un-retire and no deletion, since #506 keeps every key resolvable; /account lists live keys and folds retired ones under a collapsed section, and the retire confirmation names the repos the key owns. names live in a new owner-only account_key_label table (migration 0027) that only forge_identity reaches, trimmed, at most 40 characters, with control, line-separator and direction characters refused and a CHECK repeating the cap; they reach only /account, the unlock picker and the private bar, always beside the fingerprint. the unlock picker lists every live key, defaulting to the key that owns the URL namespace, then the key this device last unlocked, then the only live key, and a key with no stored passphrase asks for its 24 words and can save one. both routes are Bearer only and refuse a key of another account; /api/identity/me stops returning the internal tenant id. rebased by hand over migration 0026 and ADR 0099. pg tests pin the role reach, route and picker tests pin the rules, each red under a named mutation; bash ci/local.sh green against Postgres 18 (4568 passed), site gate 842 passed. owes the setup-forge tripwire update, a forge deploy, then a site deploy (#2398) a90c85fb · dbf3dbe6…diff

Renames are not followed. loot's tree maps a path to an address, so a rename is a delete and an add. This list is the history of the name, not of the bytes.