The import is its own crate, and grain's crates no longer name hatk
MergedThis moves the import into crates/grain-import, removes the import subcommand from grain-serve, and makes migrate public. It also empties V5 of its data move, rewrites the old-server comments, and adds the h[a]tk grep step to rust.yml.
I found nothing that must change.
- The import already writes
notification_seenandnotification_prefitself (crates/grain-import/src/lib.rs:350). Its tests cover them, so dropping the V5 data move loses no behaviour. - Editing V5 in place is fine as long as no index has been deployed, as the description says.
- The binary, its integration test and the workspace and lockfile entries are consistent with the move.
One thing to check when CI runs: the grep step relies on case $? right after found=$(git grep …). If the runner runs steps under sh -e, a no-match exit of 1 would end the step with a failure before the case runs. You ran it by hand on a clean tree and it passed, so this only matters if the runner differs. The checks were still queued when I looked, so I haven't seen the step pass there.
The new commit fixes the concern from my last round. The grep step in rust.yml now runs git grep … || rc=$?, so a no-match exit of 1 can't end the step under sh -e. The case then maps 1 to pass, 0 to fail with the lines shown, and anything else to a failed search. The pattern is still bracketed as h[a]tk, so the workflow file doesn't match itself. The git config safe.directory line also guards against git refusing to search a checkout owned by another user.
My earlier approval of the crate move and the V5 change stands, and nothing in this commit touches them. The checks on cc3e40f were all still queued when I looked, so I haven't seen the step pass on the runner. Confirm that when CI finishes.
- The import is its own crate, and grain's crates no longer name hatk
- main

What changes
The import is its own crate,
crates/grain-import. The binary isgrain-import <old grain.db> [--into <index>] [--strict]and behaves asgrain-serve importdid: the same summary, exit codes,GRAIN_INDEXfallback and checks. Its library is the oldindex/import.rs, its unit tests the oldimport/tests.rs, and the binary's test moves from grain-serve'stests/main.rstocrates/grain-import/tests/main.rs. It depends on grain-serve as a library, which now exposesindex::migrate(a neutral name; nothing else needed widening). grain-serve has noimportsubcommand. The crate doc says it is a one-shot cutover tool, deleted once the production import has run, and the only code that knows the old database's layout.Migration V5 creates
notification_seenandnotification_prefand fills neither. It no longer moveslastSeenNotificationsandnotificationPrefsout ofpreference. The import writes both tables itself, so that conversion lives only in grain-import. No index has been deployed, so V5 is changed in place rather than given a later step, and the migrations keep their numbers. The test of that move (notifications_preferences_move_to_their_own_tables) is removed. V4 and V6 fill search and mentions from grain's own tables, and V1 to V3 only create tables, so no other migration reads the old server's data.Comments describe grain on its own terms. In
crates/outside grain-import and inrust.yml, comments that cited the old server's files or behaviour now say what the code does and why. Useful facts stay: the Porter/unaccented tokenizer, thetoISOStringtimestamp format and why it sorts, the stop-word list's source, the lexicon validator's spec choices. Nothing underlexicons/social/mentioned it.rust.ymlhas a new first step, "the old server is named only in grain-import". It fails ifgit grep -ilfinds the name incrates,harnessorrust.ymloutsidecrates/grain-import. The pattern is writtenh[a]tkso the workflow file does not match itself. The step catchesgit grep's status, since the runner stops a step at the first failing command: 1 (no match) passes, 0 fails with the lines found, and anything else fails as a search that could not run, so a broken checkout cannot pass silently.Why
The rule for the new stack is that it has no hatk in it. Keeping the cutover in one deletable crate means deleting that crate after the production import leaves nothing behind, and the check stops the name coming back.
How it was checked
Locally:
cargo fmt --checkandcargo clippy --workspace --all-targets --locked -D warnings. The new step was run by hand undersh -e, as the runner runs it, four ways: on a clean tree (passes), with the name added tocrates/grain(fails and lists the line), with it added incrates/grain-import(passes), and outside a repository (fails). Its first version failed on the runner on a clean tree, becausegit grep's exit 1 ended the step; the fourth commit fixes that. Tests and the coverage gate are left to CI.harness/cov.mjsbuilds--workspace, so the new crate's tests and coverage are counted without any change.Review
Four commits, best read one at a time. The first is a pure move that git detects as renames, with only paths changed. The second is the V5 change. The third rewrites the comments and adds the check. The fourth makes the check pass on a clean tree under the runner's shell.
🤖 Generated with Claude Code