Repository navigation
Commit c6139f2
fix(metadata-protocol): a view container's reset heals the expanded views its publish registered (#23196)
Fixes #23149
Clause-②: no
## What was wrong
`DELETE /api/v1/meta/view/NAME` on 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 per
`OBJECT.VIEWKEY` name), and those entries stayed in the process registry
until a restart. `GET /meta/view` and `GET /meta/view?object=OBJECT`
kept 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
real `ObjectQL` engine and `SchemaRegistry`, the real
`ObjectStackProtocolImplementation` on a host-config kernel (no
environment), an in-memory driver, and a manifest shaped like
`examples/app-showcase`'s `showcase_task` container (`list` with 7
columns, plus `listViews.in_progress`). The registry's `view` entries:
| step | entries |
|---|---|
| boot | `com.example.showcase:showcase_task`,
`com.example.showcase:showcase_task.default` (All Tasks, 7 columns),
`com.example.showcase:showcase_task.in_progress` |
| publish of the overlay (`All Tasks (X)`, `progress` dropped) | the
three above, unchanged, plus bare `showcase_task`,
`showcase_task.default` (All Tasks (X), 6 columns),
`showcase_task.in_progress` |
| `deleteMetaItem` answered `{success: true, reset: true}` | the three
package entries, plus bare `showcase_task.default` (All Tasks (X), 6)
and `showcase_task.in_progress`: only bare `showcase_task` was lifted |
After that reset, `getMetaItems({ type: 'view' })` listed
`showcase_task.default` twice (`All Tasks`/7 and `All Tasks (X)`/6), and
`getMetaItem` of `showcase_task.default` served `All Tasks (X)`/6.
1. **The stale entries are the expansions `hydrateExpandedViewItems`
registers, and the heal never addresses them.** Confirmed by the table.
2. **"The publish overwrote the packaged base under the same key, so
retiring alone could leave no `showcase_task.default`."** Falsified. A
package's expansions sit under the composite `PACKAGE:NAME` key 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.
3. **Another package's name.** The hydration skips a name
`anotherPackageShips`. The change records only the names it registered,
so the heal never touches a skipped name. Pinned and ablated below.
4. **Scope gates.** The expansion heal runs after the `organizationId`
gate 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.
5. **Which source each door reads.** Confirmed, with one correction.
Both list doors read `getMetaItems`, which starts from
`registry.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.
6. **The dashboard leg resets cleanly because a dashboard expands to
nothing.** Confirmed on the same harness (`Ops (X)` published, then
reset to `Ops`). It is the control.
## The change (`packages/metadata-protocol/src/protocol.ts` only)
- `hydrateExpandedViewItems` records each name it registers, per
registry and per container name (`recordViewExpansion`, a `private
static` `WeakMap` keyed on the registry object, the way
`meta-overlay-cache.ts` keys its bucket on the engine).
- `restoreArtifactRegistryView`, after its `organizationId` gate, 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
`expandRuntimeViewContainer` over the deleted body at reset time has
three gaps, each measurable in the code:
- three of the five heal callers have no body: the cluster peer's
convergence (`applyRemoteMetadataMutation`), the no-row self-heal in
`deleteMetaItem`, and any caller after the row is gone;
- a view row bound to a package under a name another package ships
(#22057) registers no container entry to read back;
- the answer depends on registry state that can move between publish and
reset. A package installed in between changes which names
`anotherPackageShips` skips, 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,
`getItem` precedence, artifact lookup, listing and both heal verbs with
their refusals. `@objectstack/objectql` cannot be imported from this
package because it depends on it. The engine double is
`protocol.served-content-hash.test.ts`'s.
1. The card's repro: after the reset, the list, the `?object=` list and
the by-name read all serve the packaged `showcase_task.default` (`All
Tasks`, 7) exactly once, and the bare names are empty.
2. A view the overlay added, which no package ships, is retired by the
reset (tier 3).
3. Publish, reset, publish serves the second overlay on all three doors,
and its reset heals again.
4. Control: a packaged dashboard resets as before.
5. An expansion name another package ships is never touched: a bare
entry held under it survives the reset by identity, and the other
package's artifact is unchanged. The name the container did register is
healed.
6. An organization-scoped heal leaves every expansion and the container
in place, and an environment-wide heal after it retires them.
**The same six legs on the real composition** (a scratch file in
`packages/objectql/src`, not committed), over the rebuilt `dist/`: 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.mjs` WRAP mode; this suite imports
`./protocol.js`, so it resolves from `src/` and needs no build)
- **Heal loop removed** (`await this.restoreArtifactRegistryView(type,
expanded, organizationId);` replaced by a no-op; anchor count 1 to 0,
blob `ff5f56386` to `7cd556184`): 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 HEAD` empty.
- **The skipped name recorded too** (the record call moved ahead of the
`anotherPackageShips` skip; anchor 1 to 0, blob to `af2c40b08`): only
case 5 red, at `toBe(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
--listFiles` lists the new test once.
- `pnpm --filter @objectstack/metadata-protocol exec vitest run
--maxWorkers=2` on the merged tree `d03cbad29`: 233 files passed, 3
skipped; 28284 tests passed, 19 skipped. Also at `70a7be16a` before the
merge: 232 passed, 28259 tests.
- Gates: `dispatch-gates --repo objectstack-ai/objectstack --ran` at
`fd1d615cb` reports 73 derived, 71 run with exit 0, 2 NOT MEASURED, 0
unrun. The new test's three pinned engine doubles were added to
`scripts/engine-double-contract.pinned.json` (`--write`, +15 lines, this
file's rows only). `check:engine-double-contract` is then green at 913
rows.
- NOT MEASURED: `check-plugin-teardown-shape.mjs --self-test` exited 3
because this shallow clone cannot reach its pinned fixture commit
`621a48760`. That run grades the script, not this diff.
`check:dual-build-cjs-loads` exited 3 because 65 packages are unbuilt
here. As a narrow stand-in,
`require('packages/metadata-protocol/dist/index.cjs')` loads
`ObjectStackProtocolImplementation`.
- Built declarations: `dist/index.d.ts` gains three untyped `private
static` member names on `ObjectStackProtocolImplementation`
(`registeredViewExpansions`, `recordViewExpansion`,
`takeViewExpansions`), beside the 279 private members already there. No
public member, export, schema or accept-set change, so `Clause-②: no`
stands.
## Acceptance notes
- **Sibling defect, not addressed here.** A republish of the container
that drops a member keeps serving that member's expanded view until
restart. This was measured on the real composition: publish with an
added `listViews.added_by_overlay`, then publish again without it. The
list and the by-name read still serve `showcase_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.
- **Residual, documented in the docblock.** An expansion whose walk
stops on a degraded MetadataService read (tier 2, which a packaged name
never reaches) stays registered until the container is published and
reset again, or until a restart. The container's own name keeps its
existing "a later delete heals it" behaviour.
- **Two rows of one container name** (a package-less row and a
package-bound one) record into one set. A reset of either heals the set,
as the plain-key heal already lifts the name's single bare entry, and
the remaining row is hydrated again on the next list read.
- **Not done:** a live API run on `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 in `packages/objectql/src`, which is outside this claim's file
surface.
- `main` was merged at `881421194`, including #23167 on `protocol.ts`,
with no conflict.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 44e2d50 commit c6139f2
4 files changed
Lines changed: 576 additions & 0 deletions
File tree
- .changeset
- packages/metadata-protocol/src
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19485 | 19485 | | |
19486 | 19486 | | |
19487 | 19487 | | |
| 19488 | + | |
| 19489 | + | |
| 19490 | + | |
| 19491 | + | |
| 19492 | + | |
| 19493 | + | |
| 19494 | + | |
19488 | 19495 | | |
19489 | 19496 | | |
19490 | 19497 | | |
| |||
19497 | 19504 | | |
19498 | 19505 | | |
19499 | 19506 | | |
| 19507 | + | |
| 19508 | + | |
| 19509 | + | |
19500 | 19510 | | |
19501 | 19511 | | |
19502 | 19512 | | |
| 19513 | + | |
| 19514 | + | |
| 19515 | + | |
| 19516 | + | |
| 19517 | + | |
| 19518 | + | |
| 19519 | + | |
| 19520 | + | |
| 19521 | + | |
| 19522 | + | |
| 19523 | + | |
| 19524 | + | |
| 19525 | + | |
| 19526 | + | |
| 19527 | + | |
| 19528 | + | |
| 19529 | + | |
| 19530 | + | |
| 19531 | + | |
| 19532 | + | |
| 19533 | + | |
| 19534 | + | |
| 19535 | + | |
| 19536 | + | |
| 19537 | + | |
| 19538 | + | |
| 19539 | + | |
| 19540 | + | |
| 19541 | + | |
| 19542 | + | |
| 19543 | + | |
| 19544 | + | |
| 19545 | + | |
| 19546 | + | |
| 19547 | + | |
| 19548 | + | |
| 19549 | + | |
| 19550 | + | |
| 19551 | + | |
| 19552 | + | |
| 19553 | + | |
| 19554 | + | |
| 19555 | + | |
| 19556 | + | |
| 19557 | + | |
| 19558 | + | |
| 19559 | + | |
| 19560 | + | |
| 19561 | + | |
| 19562 | + | |
| 19563 | + | |
19503 | 19564 | | |
19504 | 19565 | | |
19505 | 19566 | | |
| |||
19735 | 19796 | | |
19736 | 19797 | | |
19737 | 19798 | | |
| 19799 | + | |
| 19800 | + | |
| 19801 | + | |
| 19802 | + | |
| 19803 | + | |
| 19804 | + | |
| 19805 | + | |
| 19806 | + | |
| 19807 | + | |
| 19808 | + | |
| 19809 | + | |
| 19810 | + | |
| 19811 | + | |
| 19812 | + | |
| 19813 | + | |
| 19814 | + | |
| 19815 | + | |
| 19816 | + | |
| 19817 | + | |
| 19818 | + | |
| 19819 | + | |
| 19820 | + | |
| 19821 | + | |
| 19822 | + | |
| 19823 | + | |
19738 | 19824 | | |
19739 | 19825 | | |
19740 | 19826 | | |
| |||
19756 | 19842 | | |
19757 | 19843 | | |
19758 | 19844 | | |
| 19845 | + | |
| 19846 | + | |
| 19847 | + | |
| 19848 | + | |
| 19849 | + | |
| 19850 | + | |
| 19851 | + | |
| 19852 | + | |
| 19853 | + | |
19759 | 19854 | | |
19760 | 19855 | | |
19761 | 19856 | | |
| |||
0 commit comments