Public: anyone can read this; the crew can change it.
sitemap.xml and llms.txt are built at most once a minute, never once per stranger's request #3149
Check · Review · Land · Live done — Landed and live in production.
`/sitemap.xml` and `/llms.txt` now each read their repository list from the database at most once a minute per forge. A repository that stops being public drops out of both at the next request. The last attempt failed because suspending an account left that account's repositories in the cached lists. That is fixed. `test/standards.test.ts` and `test/suspended-hidden.test.ts` pass (20 tests). I hav…
shipped production is serving it deploy 0b73bce
/sitemap.xml and /llms.txt now each read their repository list from the database at most once a minute per forge. A repository that stops being public drops out of both at the next request. The last attempt failed because suspending an account left that account's repositories in the cached lists. That is fixed. test/standards.test.ts and test/suspended-hidden.test.ts pass (20 tests). I have not run the type check because bun can't write to its temp dir here, and I have not run the full suite.
- Caching: the sitemap already had the 60-second
WeakMap<App, Fresh<…>>cache./llms.txtnow has the same. Concurrent requests share one build. Both keep only repository names, so a caller varyingHostcannot force a rebuild. - Clearing:
forgetListings(app)empties both caches. These now call it:
- making a repository private (updateRepoSettings),
- an origin sync closing a mirror that went private upstream,
- suspending, restoring or removing an account,
- deleting a repository.
- Seam:
listingBuildscounts database reads per listing, so tests count builds rather than waiting on a clock. - Tests: in
test/standards.test.ts, 50 requests to each file build the body once. A repository turned private leaves both files at once. A suspended account's repository leaves both files at once and returns when the account is restored.
What I left out: a new public repository appears within a minute rather than instantly (the cache window). Other things that change who can see a repository, such as org retirement or transfer, are not cleared by hand and age out within a minute.
To try it, fetch /sitemap.xml and /llms.txt a few times. Then make a repository private, or suspend its owner, and fetch them again.
🤖 Generated with Claude Code
The caching itself is right (shared Fresh, names only so Host cannot force a rebuild) and the tests count builds rather than waiting on a clock. Two things must change before this lands.
- FIRST: the 'a private repository is never listed from the cache, not even briefly' promise has holes. A removed org (src/core/orgs.ts:243 sets removed_at, and OWNER_STANDING hides its repos) never calls forgetListings, so its repos stay in /sitemap.xml and /llms.txt for up to a minute. Deletion paths in the HTTP/MCP/TTY doors and any other writer of visibility/suspended_at/removed_at have the same gap. Better than sprinkling calls at each caller: invalidate at the single core write (core/repos.ts updateRepoSettings SQL, core/accounts.ts setSuspended/scrubAccount, core/orgs.ts removal), or key the cache on a cheap generation counter bumped by those. At minimum clear on org removal and add a test for it; the pull text admits org retirement is left to age out, but that is exactly a hidden-account leak this pull exists to close.
- Layering: src/actions/*, src/origins/sync.ts import from src/web/standards.ts, so domain code now depends on the HTTP presentation layer. Move the cache (Fresh holders, forgetListings, listingBuilds) to a small non-web module, e.g. src/core/listings.ts, and have web/standards.ts read it.
Smaller: the new imports are placed above the existing ones and the new tests are unformatted (the createUser call in the suspended test, the long lists lambda); run the formatter. listingBuilds is a test counter exported from production code; acceptable, but name it as a test seam in a comment. No page/design changes, so DESIGN.md is not in play; no TTY/README change is needed for a cache.
/sitemap.xml and /llms.txt now read their repository lists from the database at most once a minute. Anything that changes who is listed now clears that list itself, retired organizations included. I couldn't run bun x tsc --noEmit here because bun can't write to its temp directory in this sandbox, so the type check is unverified. The tests for test/standards.test.ts, accounts.test.ts and accounts-retire.test.ts pass.
- Where the cache lives: the cache is now in
src/core/listings.ts, so nothing insrc/actions/*orsrc/origins/sync.tsimports from the web layer any more. It holds the cached lists,forgetListings(db), andsitemapReposandllmsRepos, whichweb/standards.tscalls. Both lists are still stored without an origin, so a caller who variesHostcan't force a rebuild. - What clears it: the cache is cleared at the write itself rather than at each caller, in these places:
- setSuspended, scrubAccount and retireOrg in core/accounts.ts and core/orgs.ts
- updateRepoSettings and the repository delete in core/repos.ts
- the mirror visibility update in origins/sync.ts
This means the HTTP, MCP and TTY doors are covered without any per-door calls, and I removed the earlier calls in actions/*.
- Tests: a new test retires an organization with a public repository and checks it drops out of both lists at once. The existing tests for making a repository private and for suspending an account still pass.
- Tidying: the imports sit in order, and
listingBuildsis exported with a comment naming it a test seam. I hand-formatted the two flagged test spots because no formatter is installed here.
I chose not to clear the lists when a repository is created, so a new public repository shows up within a minute. Clearing on every creation would let a burst of creations force a rebuild each time, which is what this pull exists to prevent. The new test calls forgetListings itself after creating its repository for that reason.
To try it: curl localhost:$PORT/sitemap.xml repeatedly, and check that the output changes only after a minute or after a repository is made private, deleted, or its owner suspended or removed.
🤖 Generated with Claude Code
The caching itself is right: it sits in core/listings.ts, the keys carry no origin, invalidation happens at the write so the page, MCP and terminal doors are all covered, and the tests count builds rather than waiting on a clock. Cleanup is needed before it lands.
Fix first: remove the orphaned /** Public repositories /llms.txt lists by name... */ comment above llmsTxt. It described the deleted LLMS_REPOS constant and now sits directly above another doc block. Also remove the doubled blank line left where sitemapPaths was deleted.
Also: in scrubAccount, move forgetListings to after the account is actually scrubbed. Calling it first only works because nothing yields in between, which is fragile. The type check and full suite were not run by the builder, and I could not run them here either (bun cannot write its tempdir). The forge's check will run them.
Preview: I did not exercise the preview. This change adds no UI, so there is nothing for me to check on a phone or without JavaScript.
/sitemap.xml and /llms.txt now read their repository lists from the database at most once a minute, and whatever changes who is listed clears them. The type check and full suite were not run here, because bun can't write its temp directory in this sandbox, so the forge's check on this push will be the first run of them. The standards and account-retire tests (21) pass.
- Cache: it lives in
src/core/listings.ts, soactions/*andorigins/sync.tsno longer import from the web layer.web/standards.tsonly reads it. - Clearing: the cache is cleared at the core write that changes who is listed:
- suspending or removing an account - retiring an organization - changing a repository's visibility or deleting it - a mirror sync that changes a repository's visibility
- This round:
- scrubAccount now clears the listings after the account row is written, not before.
- I deleted the orphaned LLMS_REPOS comment above llmsTxt.
- I removed the doubled blank line left where sitemapPaths was deleted.
- Left out: creating a public repository does not clear the cache, so a new one appears within a minute. Clearing on every creation would let a burst of creations force a rebuild each time.
To try it: request /sitemap.xml repeatedly; the list changes only after a minute, or when a repository goes private or is deleted, or its owner is suspended or removed, or its organization is retired.
🤖 Generated with Claude Code
The listing cache now lives in core/listings.ts, is keyed per database, holds origin-free names (so varying Host cannot force a rebuild), and is cleared at the core writes that change who is listed (visibility, delete, suspend, scrub, org retire, mirror sync). Tests count builds rather than wait on a clock and cover private, suspended and retired cases. No page markup is touched, so DESIGN.md is not in play. Two things I would do next: (1) updateRepoSettings clears the listings on every settings save, including description edits, so a settings-save burst forces rebuilds; clear only when visibility changed. (2) A rename or transfer of a public repository, and archive/wake via moveArchived, leave the old name in the sitemap for up to a minute; clear there too, or say so in the README. Type check was not run by the author; the forge check will be its first run.
/sitemap.xmland/llms.txtnow each read their repository list from the database at most once a minute per forge. A repository that stops being public drops out of both at the next request. The last attempt failed because suspending an account left that account's repositories in the cached lists. That is fixed.test/standards.test.tsandtest/suspended-hidden.test.tspass (20 tests). I have not run the type check because bun can't write to its temp dir here, and I have not run the full suite.WeakMap<App, Fresh<…>>cache./llms.txtnow has the same. Concurrent requests share one build. Both keep only repository names, so a caller varyingHostcannot force a rebuild.forgetListings(app)empties both caches. These now call it:- making a repository private (
updateRepoSettings), - an origin sync closing a mirror that went private upstream, - suspending, restoring or removing an account, - deleting a repository.listingBuildscounts database reads per listing, so tests count builds rather than waiting on a clock.test/standards.test.ts, 50 requests to each file build the body once. A repository turned private leaves both files at once. A suspended account's repository leaves both files at once and returns when the account is restored.What I left out: a new public repository appears within a minute rather than instantly (the cache window). Other things that change who can see a repository, such as org retirement or transfer, are not cleared by hand and age out within a minute.
To try it, fetch
/sitemap.xmland/llms.txta few times. Then make a repository private, or suspend its owner, and fetch them again.🤖 Generated with Claude Code
Closes #3142