Repository navigation
fix(metadata-protocol): a view container's reset heals the expanded views its publish registered - #23196
Conversation
… its publish registered (#23149) WIP: record each expansion hydrateExpandedViewItems registers, per registry and container name, and heal each recorded name through the same tier walk the container's plain key takes. Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
#23149) Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…t heal (#23149) Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…set-retires-view-expansions
… in the ledger (#23149) Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 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 930d01e469952b78becf8286806187b2c9e1a1bc && git checkout 930d01e469952b78becf8286806187b2c9e1a1bc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8323cad97e148fb1955511479adacad19aee10fd fd1d615cbe57e7b7e4c7fa9e7ac4931db821dba1 && git checkout -B drift-repro 8323cad97e148fb1955511479adacad19aee10fd && git merge --no-ff fd1d615cbe57e7b7e4c7fa9e7ac4931db821dba1
node scripts/docs-audit/affected-docs.mjs --json 8323cad97e148fb1955511479adacad19aee10fd
|
Fixes #23149
Clause-②: no
What was wrong
DELETE /api/v1/meta/view/NAMEon a published overlay of a packaged view container answered{ success: true, reset: true }, but the delete path's registry heal (restoreArtifactRegistryView) healed the container's plain key only. The publish had also registered every view the container expands to (hydrateExpandedViewItems, one entry perOBJECT.VIEWKEYname), and those entries stayed in the process registry until a restart.GET /meta/viewandGET /meta/view?object=OBJECTkept listing them, so the end-user list kept rendering the overlay's label and columns.Mechanism, measured (the dispatch's six premises)
Measured on the real composition at
e540e7ffb(before the change): the realObjectQLengine andSchemaRegistry, the realObjectStackProtocolImplementationon a host-config kernel (no environment), an in-memory driver, and a manifest shaped likeexamples/app-showcase'sshowcase_taskcontainer (listwith 7 columns, pluslistViews.in_progress). The registry'sviewentries:com.example.showcase:showcase_task,com.example.showcase:showcase_task.default(All Tasks, 7 columns),com.example.showcase:showcase_task.in_progressAll Tasks (X),progressdropped)showcase_task,showcase_task.default(All Tasks (X), 6 columns),showcase_task.in_progressdeleteMetaItemanswered{success: true, reset: true}showcase_task.default(All Tasks (X), 6) andshowcase_task.in_progress: only bareshowcase_taskwas liftedAfter that reset,
getMetaItems({ type: 'view' })listedshowcase_task.defaulttwice (All Tasks/7 andAll Tasks (X)/6), andgetMetaItemofshowcase_task.defaultservedAll Tasks (X)/6.hydrateExpandedViewItemsregisters, and the heal never addresses them. Confirmed by the table.showcase_task.default." Falsified. A package's expansions sit under the compositePACKAGE:NAMEkey and the publish writes the bare key, so the base is never overwritten. Lifting the bare entry (tier 1,removeRuntimeShadow) un-shadows it. Tiers 2 and 3 still run per name, for a name no package ships.anotherPackageShips. The change records only the names it registered, so the heal never touches a skipped name. Pinned and ablated below.organizationIdgate and through the same walk, so it inherits both gates: an organization-scoped heal touches nothing (and the record is kept for a later environment-wide heal), and re-registration stays control-plane only. Pinned.getMetaItems, which starts fromregistry.listItems(bare and composite entries alike). With no stored row left, nothing is merged over that list, so it is the stale source. The by-name read asks the MetadataService (its step 2) before the registry (step 3). That is why the card saw a correct item read on the showcase: the metadata plugin answers it from the artifact. With no MetadataService, the by-name read reaches the registry and is stale too. The pins run without one, so they exercise the registry.Ops (X)published, then reset toOps). It is the control.The change (
packages/metadata-protocol/src/protocol.tsonly)hydrateExpandedViewItemsrecords each name it registers, per registry and per container name (recordViewExpansion, aprivate staticWeakMapkeyed on the registry object, the waymeta-overlay-cache.tskeys its bucket on the engine).restoreArtifactRegistryView, after itsorganizationIdgate, takes the container's recorded names (takeViewExpansions, views only) and heals each one through the same tier walk, as a(type, name)of its own. It does this before the container's own walk, which returns early from three of its tiers.Why a record and not zone 3's re-derivation. Running
expandRuntimeViewContainerover the deleted body at reset time has three gaps, each measurable in the code:applyRemoteMetadataMutation), the no-row self-heal indeleteMetaItem, and any caller after the row is gone;anotherPackageShipsskips, and the re-derived set then misses a stale entry the publish really wrote.The record holds what was registered, so the two sides cannot drift.
Pins (
protocol.view-container-reset-expansions.test.ts, 6 cases)The registry double carries the real
SchemaRegistry's key shapes,getItemprecedence, artifact lookup, listing and both heal verbs with their refusals.@objectstack/objectqlcannot be imported from this package because it depends on it. The engine double isprotocol.served-content-hash.test.ts's.?object=list and the by-name read all serve the packagedshowcase_task.default(All Tasks, 7) exactly once, and the bare names are empty.The same six legs on the real composition (a scratch file in
packages/objectql/src, not committed), over the rebuiltdist/: 6/6 green, and 121/121 across it plus the 8 objectql suites driving this heal (protocol-registry-shadow,environment-overlay-five-types,protocol-packaged-view-base,protocol-org-overlay-registry-gate,protocol-delete-object-registry-heal,protocol-object-overlay-layer,view-container-divergent-name-registrars,view-container-name-refusal).Ablation (one-off, committed fix first;
scripts/ablation-replace.mjsWRAP mode; this suite imports./protocol.js, so it resolves fromsrc/and needs no build)await this.restoreArtifactRegistryView(type, expanded, organizationId);replaced by a no-op; anchor count 1 to 0, blobff5f56386to7cd556184): 5 red, 1 green (the dashboard control, as expected). Case 5 went red only at its "the registered name is healed" half. Restored: blob equals HEAD,git diff HEADempty.anotherPackageShipsskip; anchor 1 to 0, blob toaf2c40b08): only case 5 red, attoBe(held). The heal lifted the entry it must leave. Restored the same way. A first attempt at this leg was a no-op: its replacement restated the anchor, and the tool refused it with "the anchor count moved 1 to 1", so nothing ran.Verification
pnpm --filter @objectstack/metadata-protocol typecheck: exit 0.tsc --listFileslists the new test once.pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2on the merged treed03cbad29: 233 files passed, 3 skipped; 28284 tests passed, 19 skipped. Also at70a7be16abefore the merge: 232 passed, 28259 tests.dispatch-gates --repo objectstack-ai/objectstack --ranatfd1d615cbreports 73 derived, 71 run with exit 0, 2 NOT MEASURED, 0 unrun. The new test's three pinned engine doubles were added toscripts/engine-double-contract.pinned.json(--write, +15 lines, this file's rows only).check:engine-double-contractis then green at 913 rows.check-plugin-teardown-shape.mjs --self-testexited 3 because this shallow clone cannot reach its pinned fixture commit621a48760. That run grades the script, not this diff.check:dual-build-cjs-loadsexited 3 because 65 packages are unbuilt here. As a narrow stand-in,require('packages/metadata-protocol/dist/index.cjs')loadsObjectStackProtocolImplementation.dist/index.d.tsgains three untypedprivate staticmember names onObjectStackProtocolImplementation(registeredViewExpansions,recordViewExpansion,takeViewExpansions), beside the 279 private members already there. No public member, export, schema or accept-set change, soClause-②: nostands.Acceptance notes
listViews.added_by_overlay, then publish again without it. The list and the by-name read still serveshowcase_task.added_by_overlay. The write-through registers the new expansion set and retires nothing it no longer contains. A reset still heals it, because the record is the union of every name this container registered. It is listed in the report for the seat to file.examples/app-showcase. The full workspace build does not fit the foreground window here, so the real-composition harness stands in. A committed real-registry pin would live inpackages/objectql/src, which is outside this claim's file surface.mainwas merged at881421194, including fix(metadata-protocol)!: the metadata save door never sets, changes or removes an item's lock: a body's_lock*family is kept from the stored row, echoed, or refused #23167 onprotocol.ts, with no conflict.Generated by Claude Code