fix(registry): carry dependency root status across the Lua boundary - #557
Conversation
luaTableToEntry read only id, kind, meta and data, and entryToLuaTable emitted the same four. Entry.DependencyRoot was therefore invisible in both directions, so every entry written through the Lua registry module persisted with the flag cleared and every read-modify-write cleared it again. isRootDependency falls back to an empty meta.module when the flag is absent, which hides the loss on a source-run app but not on a hub deployment, where app.deps entries carry meta.module. There the root drops out of collectSnapshotDependencies before ReconcileResolution sees it, taking its parameters with it, and boot dies at link stage with "requirement X: no value available". deploymentBaselineDigest gates on the same field, so those roots also fell out of the digest. Expose the field to Lua as `root`: meta is user space and carries no trust, so the field is the sole authority for deployment-root status. It is optional on the way in and always present on the way out, so a writer may omit it while a reader can always carry it back.
Carrying the field is not enough on its own. Absence on an update meant false, so any writer unaware of root status demoted a deployment root on every rewrite -- read the entry, change one field, write it back, and the root is gone. The demotion is silent until the next boot, where the entry drops out of collectSnapshotDependencies and takes its parameters with it. Absence on an update now means unchanged: the operation inherits the stored value from the snapshot it was opened against. Only an explicit root = false demotes. Creates are unaffected and still default to false.
Root cause confirmed against live production dataThe failure was reproduced on a Postgres-history hub deployment. Its registry history contains the demotion directly. Changeset v12 is a keeper-issued
The instance had been serving fine for hours — its state was already linked in memory. The next restart, for unrelated reasons, is what detonated it: Recovery was rolling the registry head back one version, to the state carrying the pre-demotion entry. Second commit: absence on update means unchangedCarrying the field across the boundary is necessary but not sufficient. Absence still meant
This is what makes a reboot land on correct state regardless of what the writer knows about the field.
|
The defect
luaTableToEntry(runtime/lua/modules/registry/utils.go) read onlyid,kind,metaanddata.entryToLuaTableemitted the same four.Entry.DependencyRootwas therefore invisible in both directions, so every entry written through the Lua registry module persisted with the flag cleared, and every read-modify-write cleared it again. All Lua write paths go through it (changes.go:60,88,delta.go:47,58).Why it stayed hidden
isRootDependency(boot/deps/hub/dependency_control.go:16) is:With the flag gone, root status hangs entirely on the
meta.modulefallback. On a source-run appmeta.moduleis empty, so the fallback returns true and nothing looks wrong. On a hub deployment,app.depsentries carrymeta.module— the caseapi/registry/registry.go:101-104documents as the reason the field exists — and the fallback returns false.What breaks
The root drops out of
collectSnapshotDependencies(dependency_handler.go:1022-1025) beforeReconcileResolutionever sees it, takingdata.parameterswith it. Boot then dies at link stage with walls ofrequirement X: no value available.deploymentBaselineDigestgates on the same field, so those roots also silently fall out of the baseline digest.Observed on a Postgres-history hub deployment: repeated boot crash-loops, ten knowledge bases left unprovisioned, and an operator runbook that recovered by dropping the history schema and reinstalling dependencies — which is really this flag loss, not a history defect.
The fix
Expose the field to Lua as
root.metais user space and carries no trust, so the field is the sole authority for deployment-root status. It is optional on the way in and always present on the way out, so a writer may omit it while a reader can always carry it back.utils.go— readrootinluaTableToEntry, emit it inentryToLuaTabletypes.go—OptField("root", typ.Boolean)onentryType, so strict-typed Lua can touch itspec.md— document the field and the read-modify-write contractTests
utils_test.go— three unit tests: the field is read, it defaults to false when absent, and it survives a full round triptests/app/src/test/registry/dependency_root_field.lua— live registry test: write, read back, rewrite through Lua, assert the root is not demotedgo test ./...is clean. Note the app-level Lua suite could not be executed to confirm the new integration test:wippy testreports "No tests found" onmainfor bothtests/appand unrelated apps, which is a pre-existing test-discovery regression independent of this change.Follow-up, not in this PR
Callers that construct dependency entries in Lua must write
root = truefor hub-installed roots to be marked. Until they do, this change is inert for those entries — it is not a regression, but the fix is only complete once the writers are updated.