Repository navigation
fix(plugin-pinyin-search): the boot companion pass no longer holds readiness, and an unchanged restart reads no rows - #25690
Conversation
…ding only rows changed since its last completed walk Claude-Session: https://claude.ai/code/session_014yYprgEHbgKXeYT8h3XRJB Co-authored-by: Claude <noreply@anthropic.com>
…gh the real kernel over SQLite Claude-Session: https://claude.ai/code/session_014yYprgEHbgKXeYT8h3XRJB Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yYprgEHbgKXeYT8h3XRJB Co-authored-by: Claude <noreply@anthropic.com>
…ouble drops its unused update Claude-Session: https://claude.ai/code/session_014yYprgEHbgKXeYT8h3XRJB Co-authored-by: Claude <noreply@anthropic.com>
…ry options in the reconcile test Claude-Session: https://claude.ai/code/session_014yYprgEHbgKXeYT8h3XRJB Co-authored-by: Claude <noreply@anthropic.com>
席位核对REWORK ·
Generated by Claude Code |
… regenerate lockfile and tenant-audit census Claude-Session: https://claude.ai/code/session_014yYprgEHbgKXeYT8h3XRJB Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 24 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ce8c628831ddb1548357f54aac324b90c7048319 && git checkout ce8c628831ddb1548357f54aac324b90c7048319
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0189524a413686fade56cd2bb603fc45b4671f3c fbbc0bc41b2759dade2c56de714570896ade074c && git checkout -B drift-repro 0189524a413686fade56cd2bb603fc45b4671f3c && git merge --no-ff fbbc0bc41b2759dade2c56de714570896ade074c
node scripts/docs-audit/affected-docs.mjs --json 0189524a413686fade56cd2bb603fc45b4671f3c
|
…m MinimalLogger Claude-Session: https://claude.ai/code/session_014yYprgEHbgKXeYT8h3XRJB Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
Fixes #25634
Clause-②: yes
Metadata-protocol: none
What was wrong (measured on
main9dac9eb)PinyinSearchPlugin.start()hookskernel:bootstrappedand awaitsbackfillSearchCompanion(engine, …).ObjectKernel.bootstrap()awaitskernel:ready, thenkernel:bootstrapped, thenkernel:listening(packages/core/src/kernel.ts, the boot-phase block).kernel:listeningis where the HTTP server opens its socket. So the backfill held readiness for as long as the walk took.__search-bearing object (keysetWalk, 1,000 rows per page) and decides per row in JavaScript. A clean second boot repeats the same full read, because__search = nullmeans both "never inspected" and "inspected, no CJK".What this changes
All changes are inside
packages/plugins/plugin-pinyin-search, plus the census regeneration listed below.kernel:bootstrappednow starts the boot pass and returns without awaiting it. Writes made meanwhile are stamped by the companion hooks, which are bound atkernel:readyas before. A failure is logged atwarn, not swallowed.destroy()aborts the pass between pages and awaits it, so it ends before the datasource plugin (destroyed after it) closes the pool. The kernel's hook dispatch is unchanged: no other plugin's contract moves.sys_migrationrow (id: 'pinyin-search-companion'). It stores, per object, the source fields andsince(the walk's start, minus a 10-minute clock margin). A later boot walks onlyupdated_at >= since AND __search IS NULL. This is the rows written since the last walk that still have no companion: rows written while the switch was off, seed rows written before the hooks were bound, and hook-bypassing writes that kept the audit stamp. Neither half of that filter works alone.__search IS NULLalone matches every non-CJK row forever, which is the card's point.updated_atalone matches every row the walk itself filled, because the walk's writes go through the non-bypassable audit stamp; that was measured red on the first draft of this branch. A changed source field, a differentSEARCH_COMPANION_RECONCILE_VERSION, or a scan with a failed page or skipped row leaves that object to be walked in full again.rebuildSearchCompanion()(theforce: truealias) still walks every row. It is the path for writers that bypass the hooks and back-dateupdated_at.The store:
sys_migration, and why it is not a protocol surfaceplatform-objects/src/system/sys-migration.object.ts). Its API is read-only (apiMethods: ['get', 'list']).DataMigrationFlagSchema. That is the same shape as the seed-tenancy receipt (metadata-protocol/src/migrations/seed-tenancy-backfill.ts, the ruled A″-2 route), whose id is spelled in its own package.@objectstack/spec: there is one writer and one reader, both here. Field reading:verified_at: null(no self-check),blocking: 0(no consumer is gated on it),advisory= rows skipped in the last run.Metadata-protocol: noneholds.sys_migration(noPlatformObjectsPlugin) keeps the old cost, a full walk each boot (now after readiness), and says so once atwarn.sys_inbox_messagekeepssearchableas it is. Search is untouched, so nothing a user reads is narrowed.Published surface (additive only)
CompanionBackfillResult.pagesCompanionBackfillOptions.signalPinyinSearchPlugin.whenReconciled()@objectstack/specadded as a runtime dependency (already in the closure through@objectstack/objectql).patchchangeset. These three additions widen the package's public surface, so the PR declaresClause-②: yes(the API is additive, there is no metadata-protocol change, andpatchstays).Proof
All tests run through the real
ObjectKernel, the real ObjectQL engine andSqlDriveron a SQLite file (two boots share one database), with the realSysMigrationobject (src/boot-reconcile.integration.test.ts):lina ln)__search=wangfang wfat onceforce)bootstrap()(throughkernel:listening) resolves while the pass's first page is held; the pass then completeswarncompanion reconcile failedwith the errorpnpm --filter @objectstack/plugin-pinyin-search test⇒ Test Files 4 passed, Tests 39 passed.typecheck(tsc andcheck:test-typecheck) clean.eslint --no-inline-config packages/plugins/plugin-pinyin-searchexit 0.Ablations (each through
scripts/ablation-replace.mjs, restored to the HEAD blob withgit diff HEADempty; tests import./*.jsrelatively, so the run readssrc/and nodistbuild is involved):previous = undefined) ⇒ 2 red:expected { queries: 3, rows: 2500 } to deeply equal { queries: 1, rows: 0 }, andexpected 2501 to be 1. Restored blob08208bf4cabb.kernel:bootstrappedagain ⇒ the readiness pin goes red (bootstrap()never resolves while the page is held: test timed out). The first attempt at this leg was refused by the tool as a no-op (the replacement contained its anchor) and did not run; it was re-run with a disjoint replacement. Restored blob28739311a522.Scale note: 100 k rows, boot-to-ready, same harness both times
100,000 rows inserted straight through the driver (no hooks,
updated_atback-dated, every 10th row CJK). SQLite file, one run each on a shared box. "Ready" =kernel.bootstrap()resolved, which includeskernel:listening.main9dac9ebThe branch's first-boot walk is about 20% slower than
main's, from the re-read before each of the 10 k writes. It is paid once, after readiness.main's second boot grows linearly with rows (production: 6.7 M rows, about 4 min 50 s on PostgreSQL). The branch's second boot reads no row. The filtered query is a scan the database runs, not rows moved into JavaScript.Gates (local)
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 111 commands at headfbbc0bc41. All 111 were run, each exit code captured before any pipe, and every one exited 0.--ranverdict:111 derived famil(ies) accounted for — 111 run, 0 NOT-MEASURED. Two of them,check:skill-examplesandcheck:dual-build-cjs-loads, first exited 3 (no builtdist/in a fresh worktree); they exited 0 afterpnpm build.check:tenant-audit-census: the receipt upsert is one new engine write call site (system context, object name an imported constant). After mergingmain(which had itself moved to 239), the census is 240. Regenerated withnode scripts/tenant-audit-census.mjs --write. The eight hand-written prose figures the gate names were updated incontent/docs/permissions/tenant-audit-census.mdx(239 → 240, 76 → 77, 136 → 137). The historical "135 (77%)" claim the page quotes was left as quoted.check:engine-double-contract: the failure-path test's fake engine carried an unusedupdate; dropped.check:test-source-alias:@objectstack/core,@objectstack/driver-sql,@objectstack/platform-objects/systemand@objectstack/spec/systemare aliased to source in this package'svitest.config.ts. The registry entry is unchanged.check:query-options-erasure: the new test's threeas anycasts on query options were removed.check:optional-error-sink(red in CI'sLint · docs & code guardsat8b1dea30b): this PR had added an optionalerrorchannel toMinimalLoggerbeside an optionalwarn, a sink type that allows silence. Nothing callslogger.error, so the member was removed (fbbc0bc41), restoringmain's shape. Local exit 0.dispatch-gates --commandsdoes not name this gate for this diff, so the derived list misses it.origin/mainat head8b1dea30b(merge commit, no rebase).content/docs/permissions/tenant-audit-census.mdx,docs/audits/2026-08-tenant-audit-write-call-sites.counts.mdandpnpm-lock.yamlconflicted. All three were taken frommainand regenerated:pnpm installfor the lockfile,node scripts/tenant-audit-census.mjs --writefor the census pair. On the merged head:node scripts/check-tenant-audit-census.mjsexit 0 (240 write call sites certified), and its--self-testexit 0.node scripts/check-changeset-no-major.mjs --base origin/mainexit 0 (This diff introduces no major bump; the LEVEL axis reads NOT APPLICABLE locally because nopull_requestpayload exists).node scripts/check-adr-0087-registration.mjs --base origin/mainexit 0 (this PR adds no declared-breaking changeset).--self-testexit 0.pnpm --filter @objectstack/plugin-pinyin-search testpassed (4 files, 39 tests) andtypecheckexit 0. Re-run atfbbc0bc41with the same result; scoped eslint exit 0.eslint --no-inline-config packages/plugins/plugin-pinyin-search(thepnpm lintcommand, scoped to this package): exit 0.Spec property livenessis red onmainsince 0189524 (refactor(service-automation): move AutomationEngine.resumeInternal's body into run-resume.ts (#25222 slice 8b) #25734), not from this PR:translation/flows.screenscitesengine.ts, whose onlyscreensmention moved torun-resume.ts; it reproduces onorigin/mainwithout this PR.Acceptance notes
engine.update, so the audit stamp moves a filled record'supdated_at/updated_byto the system at the moment of the fill. That was already true of the old boot backfill and ofrebuildSearchCompanion. The stamp is non-bypassable by design (audit-stamp-skip-automations.test.ts).#23122is not addressed here (it explains the inbox volume, not the scan).Generated by Claude Code