Lexicons are linted, and only grow from main's
ClosedThis adds a lexicons workflow and harness/lexicons.mjs. The script runs goat's lint and some checks goat doesn't make. It also checks that every schema only grows from main's merge-base. The pull also adds just lex and an AGENTS.md section, and fixes the trivial findings in grain's own schemas.
I read the script and the workflow. The grow check, the reachability rule for what people write, and the PENDING and OLD lists all look correct and well scoped. I checked the claim that the new bounds narrow nothing published. registerPush, unregisterPush, pinnedFeed and gallery.deleteGallery don't exist on main under those names, and MOST_TOKEN_BYTES is 4096 in crates/grain-serve/src/push/mod.rs, matching the new token bound. I did not read the generated lexicon JSON hunks line by line. The check runs were still queued, so I haven't seen them pass. The first lexicons run is the real test: its last line should read "only grown since …", which shows the grow check ran and origin/main was present on the runner.
Nothing here needs to change before merge.
Now stacked on the CI gates pull (Deploys and lexicon publishes wait on the Rust checks too), merged in, so that deploy and publish-lexicons wait on lexicons too (8eed7cc). It lands after that one.
Since round 1 this pull merged infra/ci-gates and added lexicons to the for wf in lists in deploy.yml (both wait steps) and publish-lexicons.yml, plus the matching comments. That was the "not done" item in the description. The three lists are consistent, and the 25-minute budget is still shared across the five workflows.
The checks on this commit are all still queued, so I haven't seen any of them pass. The lexicons run's last line should read "only grown since …", which shows origin/main was present on the runner. Nothing needs to change before merge.
Since round 2 this pull only merged infra/ci-gates again, which brought in batch/one. That brings new Rust-side work and a few new lexicons (getSitemap, muteActor, getArchive and others). I read the new lexicons I sampled against the script's rules: knownValues, a description on every main, DIDs not handles, and objects in new arrays. They look fine. getArchive's actor is a query parameter with at-identifier, which the rules allow.
I could not read the full 7,000-line merge diff, so I only sampled those files. The checks on this commit are all still queued, so I haven't seen the lexicons run pass. Its output is the real test of whether the merged-in schemas satisfy the script. Its last line should read "only grown since …". If it names a finding in one of the new batch lexicons, fix it or add it to PENDING. Nothing else needs to change.
Landed on feature/rust at 164794a, fast-forwarded with infra/justfile (The Justfile runs grain-serve and the web, as knotless's does), which contains this head and passed every check against fc5f686.
What changes
A
lexiconscheck..knotless/workflows/lexicons.ymlruns goat v0.2.6'slex lintandharness/lexicons.mjson pulls that changelexicons/**, the script or the workflow, and on pushes to main;just lexruns the same here. The script is knotless's, checking what goat does not:knownValuesnotenum, objects in new arrays, open unions, a description on everymain, amaxLengthon strings without a format in what people write, accounts by DID not handle, and that every schema only grows from main's (nothing removed, renamed, retyped, made required or narrowed). AGENTS.md has a "Lexicons" section with knotless's rules, scoped as below.Scope. grain's own schemas under
lexicons/social/grain, lessunspecced/and the ten admin methods only the old server serves (actOnReview,addRepos,getInfo,getReviewQueue,getReviewStats,listRepos,removeRepos,resyncRepos,searchRecords,startScan: none named incrates/), listed asOLDin the script, which go at cutover. The other namespaces are copies of others' schemas.Fixed here (the trivial ones): descriptions on the
mainof the eight records,story.getStoryandbasePermissions;maxLengths onactor.defs#pinnedFeed(id,label,path),gallery.deleteGallery'srkey(512, a record key's most), andregisterPush/unregisterPush'stoken(4096, as grain-serve'sMOST_TOKEN_BYTES) andappId(256). None of these is on main, so no published schema narrows. The old server's embedded copies are regenerated to match.Mirrors knotless's
.knotless/workflows/lexicons.yml,harness/lexicons.mjs, thelexrecipe in itsJustfile, and the "Lexicons" section of itsAGENTS.md.Why
knotless's lexicons are held to the style guide and only grow from what is published; grain's were not, and feature/rust is adding many. A PDS validates records against the published schemas, so a narrowed one breaks writes out there.
Base for "only grows": main, where the branch left it (
git merge-base HEAD origin/main), as knotless does. main's schemas are the onespublish-lexicons.ymlpublishes; feature/rust's are not yet out, so changing them is free until the branch lands. Checked againstorigin/feature/rustinstead, the bounds added here would read as narrowings, which they are not to anyone outside.Where grain differs from knotless's script, and why:
*View) flags grain's defs files, which hold answer items (followerItem,cameraItem, ...) as often as records' parts: 100 false findings.profileViewdoes.field-name-syntax(id,typeas field names, as Bluesky's report and label views use, on published schemas), and permissioned spaces, which goat v0.2.6 cannot read (group.json,privateGallery.jsonare skipped from goat;basePermissions'spaceresource kept).repo.applyWrites' closed unions kept, ascom.atproto.repo.applyWrites' are: a write of a kind the server does not know is one it cannot apply.answersstep reads answers its screenshots recorded; grain has none, andharness.ymlalready holds grain-serve's answers to the lexicons, so it is not copied.How it was checked
node harness/lexicons.mjspasses here, with goat v0.2.6, against main's merge-base; againstorigin/feature/rustit names the new bounds, as it should. The workflow's first run is this pull's.Shortcomings
Left in the script's
PENDING, not yet published, each to fix before main:group.pool.deleteComment/deleteGalleryrkey,privateGallery.addMemberspace/actor,createPrivateGallerymembers[],privateGallery.defs#photoInput.altneedmaxLengths.sync.indexRecords(uris,indexed,notFound),story.markViewedstories,gallery.defs#cameraItem.thumbs,#locationItem.thumbsandh3Cells,admin.defs#reviewItem.classifiers,createPrivateGallerymembers,getSpaceSupportmissing.Not done:
deploy.ymlandpublish-lexicons.ymldo not wait onlexiconsyet. The gating pull (infra/ci-gates) is independent of this one; whichever lands second addslexiconsto theirfor wf inlists.If the runner's checkout has no
origin/main, the grow check is skipped and the last line says so by leaving out "only grown since"; the first run's log shows which.Review
The design: the four differences from knotless's script above. Start at
harness/lexicons.mjs.🤖 Generated with Claude Code