~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2

Lexicons are linted, and only grow from main's

Closed
Closed chadtmiller.com asked to merge into feature/rust from grain.social/grain:infra/lexicons

What changes

A lexicons check. .knotless/workflows/lexicons.yml runs goat v0.2.6's lex lint and harness/lexicons.mjs on pulls that change lexicons/**, the script or the workflow, and on pushes to main; just lex runs the same here. The script is knotless's, checking what goat does not: knownValues not enum, objects in new arrays, open unions, a description on every main, a maxLength on 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, less unspecced/ and the ten admin methods only the old server serves (actOnReview, addRepos, getInfo, getReviewQueue, getReviewStats, listRepos, removeRepos, resyncRepos, searchRecords, startScan: none named in crates/), listed as OLD in the script, which go at cutover. The other namespaces are copies of others' schemas.

Fixed here (the trivial ones): descriptions on the main of the eight records, story.getStory and basePermissions; maxLengths on actor.defs#pinnedFeed (id, label, path), gallery.deleteGallery's rkey (512, a record key's most), and registerPush/unregisterPush's token (4096, as grain-serve's MOST_TOKEN_BYTES) and appId (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, the lex recipe in its Justfile, and the "Lexicons" section of its AGENTS.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 ones publish-lexicons.yml publishes; feature/rust's are not yet out, so changing them is free until the branch lands. Checked against origin/feature/rust instead, the bounds added here would read as narrowings, which they are not to anyone outside.

Where grain differs from knotless's script, and why:

  • What counts as "written" is followed by reachability: records, procedure input, and every def either names through refs. knotless's heuristic (a defs file is written unless the def is named *View) flags grain's defs files, which hold answer items (followerItem, cameraItem, ...) as often as records' parts: 100 false findings.
  • Bounds and handles on fields already on main are not asked for, as knotless asks for objects only in new arrays: adding them would be a narrowing the grow check refuses.
  • A handle beside a DID in the same object is not flagged: that is a view showing both, as Bluesky's profileView does.
  • goat findings kept: field-name-syntax (id, type as 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.json are skipped from goat; basePermissions' space resource kept).
  • repo.applyWrites' closed unions kept, as com.atproto.repo.applyWrites' are: a write of a kind the server does not know is one it cannot apply.
  • knotless's answers step reads answers its screenshots recorded; grain has none, and harness.yml already holds grain-serve's answers to the lexicons, so it is not copied.

How it was checked

node harness/lexicons.mjs passes here, with goat v0.2.6, against main's merge-base; against origin/feature/rust it 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:

  • Inputs in files other open pulls are changing (pools, private galleries): group.pool.deleteComment/deleteGallery rkey, privateGallery.addMember space/actor, createPrivateGallery members[], privateGallery.defs#photoInput.alt need maxLengths.
  • Arrays of bare values grain-serve and the web already read and write, which need objects, with the code on both sides: sync.indexRecords (uris, indexed, notFound), story.markViewed stories, gallery.defs#cameraItem.thumbs, #locationItem.thumbs and h3Cells, admin.defs#reviewItem.classifiers, createPrivateGallery members, getSpaceSupport missing.

Not done: deploy.yml and publish-lexicons.yml do not wait on lexicons yet. The gating pull (infra/ci-gates) is independent of this one; whichever lands second adds lexicons to their for wf in lists.

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

Approved an earlier version

This 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.

Approved an earlier version

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.

Approved

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.