Repository navigation
fix(metadata-protocol): a republish of a view container retires the views it dropped, through one reconcile - #23234
Conversation
…write (red before the fix) Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…ns after every write Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…aches the reconcile Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…mment too Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…er views Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…ntainer-expansion-reconcile
… is awaited Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 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 24ec3829d6cbc02e05c41ef85e3ffbd1a950f8f3 && git checkout 24ec3829d6cbc02e05c41ef85e3ffbd1a950f8f3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 01a3421f42b15801dd6289f9a5fdf22fa469b8f2 99dd8fda359bc1d7a9e399ba21c24de65ce259c9 && git checkout -B drift-repro 01a3421f42b15801dd6289f9a5fdf22fa469b8f2 && git merge --no-ff 99dd8fda359bc1d7a9e399ba21c24de65ce259c9
node scripts/docs-audit/affected-docs.mjs --json 01a3421f42b15801dd6289f9a5fdf22fa469b8f2
|
Part of #23198
Clause-②: no
What was wrong
A view container (a
defineViewdocument such asshowcase_task) registers its own name plus one view per entry, underOBJECT.VIEWKEY, in the process registry. A republish of the container registered the views it expanded to and retired none it no longer expanded to. After a publish that droppedlistViews.added_by_overlay,GET /api/v1/meta/view, the?object=list and the by-name read kept servingshowcase_task.added_by_overlayuntil a restart. Measured before the change, on this PR's pins (red at0219bb6a0) and over the realSchemaRegistry(see Verification).The change (
packages/metadata-protocol/src/protocol.ts)One private reconcile,
reconcileViewExpansions(type, write, organizationId). After any write that changes a container's stored row, it leaves the registry holding the views that row expands to now:restoreArtifactRegistryView). A packaged view of that name is un-shadowed, a baseline is re-registered on a control-plane kernel, and a name no layer serves is retired.Both sides call it, and nothing else registers an expansion or touches the record:
hydrateExpandedViewItemspasses the row's binding and its expansion.hydrateOverlayIntoRegistryreaches it on both of its registering paths. That covers every hydration: the write-through (save-publish, publish, rollback, revert, a replica's convergence), the list read and the boot.restoreArtifactRegistryViewpassesremoved: 'every-row'(an empty set) ahead of its tier walk. A reset, a delete, a revert's removal and a replica's removal reach it there.The walk reads the MetadataService, so the reconcile is async.
hydrateOverlayIntoRegistryandapplyRegistryWriteThroughbecameasync, and all eight call sites await them. A publish therefore answers after the heal. The#9111type assert still runs before the firstawait, and its refusal reaches the samecatchas before.protocol.hydrate-overlay-canonical-type.test.tsawaits the hydrator now. Its four affected cases assert what they asserted before: code, status, message and nothing minted.Why the record is per row (zone 2, hypothesis 2, measured). One container name can have a package-less row and a package-bound row (ADR-0048), and the list read hydrates both on every call. If the record is keyed on the name alone, each row reconciles against the other row's set. The first list read then retired the package-less row's own member (ablation B below). Keyed per row, a republish of one row heals only what that row dropped. A view both rows expand to, which one row drops, leaves the registry with that row. Both doors answer it from the other row's stored row, and the next list read registers it again (pinned). A reset still heals every row of the name, because the heal is handed no binding (#23196's behaviour, unchanged).
Zone 2 hypotheses
hydrateOverlayIntoRegistryhas three callers: the list read's hydration ingetMetaItems(not the cluster peer),applyRegistryWriteThrough, andloadMetaFromDb. The peer reaches the registry throughapplyRegistryWriteThroughorrestoreArtifactRegistryView. The completeness pin reads that population from the source.Pins
protocol.view-container-reset-expansions.test.ts, a new[#23198]block of 5 cases on #23196's doubles (so no new engine-double rows). Each case asserts the list read, the?object=list and the by-name read together:showcase_task.in_progressis served again, once;protocol.view-expansion-reconcile-call-sites.test.ts, the family's closing check (13 cases, source-reading, no CI gate). It covers three populations:sys_metadatawrite inprotocol.ts(9 sites) carries a recorded route to the write-through or the heal. A same-member route must sit after its write and before the member's next write. A write with no route turns it red, naming the write.The data door cannot write
sys_metadata: itsapiMethodsaregetandlist.protocol.register-item-call-sites.test.ts(#22057): the expansion'sregisterItem(row moves toreconcileViewExpansions.Ablation (fix committed first;
scripts/ablation-replace.mjsWRAP mode; these suites import./protocol.js, so they resolve fromsrc/and need no build)Every leg was restored with blob equal to HEAD and an empty
git diff HEAD.a9b1a41c7to5faee5a1f). 9 red, 2 green: the dashboard control and "a republish adds a member".70a74e603). Only the two-row case is red, atread 1: expected undefined to be 'A only'.return true;went red, but only at the closing check's path count. So the count now reads a trailing comment too (675c31319).return true;was refused by the tool: its replacement restated part of the anchor, and the replace count stayed 5 to 5. Nothing ran in that attempt.await this.hydrateExpandedViewItemsline (blob toc85a10a1c) turned only the closing check red, in 2 cases. All 11 behaviour pins stayed green, so this bypass is visible to the closing check alone.repo.deleteremoved (blob toe1658dc6f). The census is red:deleteMetaItem: repo.delete( → restoreArtifactRegistryView.99dd8fda3, blobe5645d2f9toa6c6cec44). The await case is red.Legs A to D ran at
a4b879a33and675c31319, on theprotocol.tsbloba9b1a41c7that the merge left unchanged. After them,protocol.tschanged only in comments.Verification (HEAD
99dd8fda35)@objectstack/metadata-protocolsuite on the merged tree4035e9baee(aftermainat75cada0ea), run in two halves: 116 files passed and 3 skipped (25931 tests), then 118 files passed (2370 tests). VERDICT command-exit 0 for both. Before the merge, ata4b879a33: 234 files passed, 3 skipped, 28301 tests.99dd8fda35adds comment lines and one closing-check case, which ran green with typecheck.pnpm --filter @objectstack/metadata-protocol typecheck: exit 0.tsc --listFileslists all four changed or new test files.packages/objectql/src, not committed, over the rebuiltdist/, with the realSchemaRegistry). The three legs are the card's repro then a reset, the delete, and the two rows. They are 3/3 green. Against the baseprotocol.ts(rebuilt, then restored to HEAD and rebuilt) they are 3/3 red, atAddedandClosedstill served anda_onlystill registered. 12 files and 178 tests are green across them plus the 11objectqlsuites that drive this heal and hydration.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat99dd8fda35derives 65 commands.--ranwith exit codes reports 64 run with exit 0 and 1 NOT MEASURED.check:lean-entry-closurefirst exited 3 becauseobjectql/distwas absent; after theobjectqlbuild it exited 0. NOT MEASURED:check:dual-build-cjs-loads, reason: exit 3, 60+ packages unbuilt in this container. Narrow stand-in:requireofpackages/metadata-protocol/dist/index.cjsloadsObjectStackProtocolImplementation(a function)..tsfiles are ineslint.config.mjs's population (--print-configresolves each).eslint --no-inline-config --format jsonreports 5 files, 0 errors and 0 warnings. Type-aware linting is not enabled: the resolved config has noparserOptions.projectorprojectService. So this diff cannot move the verdict of an untouched file. The fullpnpm lintis CI's.ObjectStackProtocolImplementationgains the untyped private namereconcileViewExpansionsand losesrecordViewExpansionandtakeViewExpansions.ViewContainerWriteis module-private and absent fromdist/index.d.ts. No public member, export, schema or accept-set changes, soClause-②: nostands.75cada0ea.Acceptance notes
bypassrow).POST /api/v1/packages/ID/adopt-orphans(reassignOrphanedMetadata) rebinds a package-less view container row to another package with a rawsys_metadataupdate and touches no registry entry. This was measured over the realSchemaRegistrywith this fix built. After adopting an overlay ofshowcase_taskinto another package, the row expands toshowcase_task.showcase_task*for that package. Meanwhile the package-less registration's bareshowcase_task,showcase_task.default(All Tasks (X), 6) andshowcase_task.in_progressstay registered, and all three doors keep servingAll Tasks (X)forshowcase_task.defaultuntil a restart. It is the same family, but a fifth write kind outside this claim's region. It is reported for the seat to route; see the report on metadata-protocol: a republish of a view container that drops a member keeps serving the dropped member's expanded view until restart #23198.protocol.code-defined-datasource-door.test.tscalls the now-async hydrator withoutawaitin itshydratehelper. It stays correct because adatasourcerow registers before the firstawait.examples/app-showcase. The real-registry scratch stands in.Generated by Claude Code