Repository navigation
metadata-manager: getViewsByObject() reads a separate backing store never touched by the #13407 runtime-container-expansion fix #13913
Description
Activity
os-support-ai commented
on Sep 1, 2026 CollaboratorMore actionsClaim: PM loop round 11 (wave 2)
Session:session_01Q5WBDtaUnoz5XuJ6jk8pQ5
Branch:claude/issue-13913-getviewsbyobject-container-expansion
Worktree:objectstack-issue-13913
Domain:domain:engine
File surface:packages/metadata/src/**(expected:metadata-manager.ts+ a test file) ·packages/metadata-protocol/src/**READ-ONLY unless a shared export is the measured answer — see below · ⛔packages/spec/**excluded (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus— tier derived this round bynode scripts/pm/dispatch-gates.mjs --tier packages/metadata/src/metadata-manager.ts, ⛔ not recalled. Judgement card (a cross-package reuse decision sits inside it), soopus, ⛔ not the sonnet floor.
Clause-②: no — expected. This is declared-but-unenforced restored:packages/spec/src/ui/view.zod.tsalready namesgetViewsByObject()as a reader of the field, so making that reader honour it narrows nothing and widens nothing. Re-declare from the actual diff at PR time; flips toyesif a shared export is added to@objectstack/metadata-protocol's public surface.
Serial constraints cleared: no open PR touchespackages/metadata/**(open PRs enumerated this round; #14183 ispackages/metadata-protocol+packages/services/service-cluster, a different package — but see the read-only note). ·⚠️ #13912 is the sibling residual in this same package and is HELD — ⛔ not for serialisation reasons but because its body is truncated and therefore not dispatchable (repair instruction posted there). It is hot-file serial behind this card regardless.
Premises re-verified on
origin/main@4d672c4fthis round, ⛔ not inheritedTriage re-verified at
afbf2711; main has moved since, so both readings were re-taken:getViewsByObject(metadata-manager.ts:1583) is byte-for-byte what the card describes:const views = await this.list('view')then.filter(v => v && typeof v === 'object' && v.viewKind && v.object === object).expandRuntimeViewContainer— 5 hits, all inpackages/metadata-protocol/src/protocol.ts. It exists (it did not, when the card was filed) and it lives in the other package. That is the whole shape of this card's open question.
⇒ The card is fully live. #13929 repaired the REST read door;
MetadataManager's own backing store was left untouched, which is exactly the residual reported here.ZONE 1 — RULINGS. Not re-adjudicable.
① The filter is CORRECT. The gap is in the STORE. It reads top-level
.object, matchingViewSchema.object's own declaration. ⛔ Do not "fix" the filter.② There is a SECOND condition the card does not state, and it is the one that will bite you. The filter also requires
v.viewKind— and an aggregated container has no top-levelviewKind. ⇒ Merely getting the container into the registry will not pass this filter. The expanded ViewItems (which carryviewKind) must be whatthis.list('view')can see.③ ⛔ Do NOT relax the
viewKindrequirement. That would return the container itself as a view — the exact behaviour #7163 ruled wrong ("answers with the container alone"). This is the tempting one-line "fix"; it is a regression.④ ⛔
packages/specis excluded. Single-owner lane. If the repair needs a spec change, stop and report.ZONE 2 — The open question. MEASURE IT; it decides the shape of the delivery.
⚠️ CanexpandRuntimeViewContainerbe reused acrosspackages/metadata-protocol→packages/metadata, or mustpackages/metadatagrow a local equivalent?Triage flagged this as answerable only now that #13929 has landed, and it is genuinely open. Measure:
- the dependency direction between the two packages (does
packages/metadataalready depend onpackages/metadata-protocol, or would this invert an existing edge?); - whether the helper is exported or module-private today;
- whether promoting it to a shared export widens a public surface (⇒ that would flip Clause-② to
yes, and you must say so).
Answer it with a reading, then choose — and state which you chose and why. ⛔ Do not default to duplicating the logic because it is easier; ⛔ and do not force a shared export if the dependency edge is wrong. Both are legitimate outcomes; an unexamined choice is not.
📌 This is also the question that decides the shape of the sibling #13912. Whatever you establish here, that card reuses — so write the answer down where its implementer will find it.
ZONE 3 — Suggested route. Optional.
The card suggests checking whether
MetadataManager'sdbLoader(registered in its constructor) already surfacessys_metadataoverlay rows the waygetMetaItems'overlaysmerge does. Unverified by me — treat as a starting point, not a specification.Evidence bar
The defect is "this exit answers empty for a runtime-authored container", so the pin must drive
getViewsByObject()itself — ⛔ not the REST route, which #13929 already fixed and which would go green without touching this bug. Assert that a container authored with a top-levelobjectand nolist.data.objectyields its expanded ViewItems through this entry point, and keep a control that a non-container view still resolves unchanged.Ablate on the committed tree: revert the repair, show that assertion red while the control stays green, restore and prove it (empty
git diff HEAD).⛔ Verify claims about
mainagainstorigin/main, never the shared working tree. ⛔ A zero-hit is not a reading until a positive control fires.Gates and deliverable
Derive your gate family at your actual diff (
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands) and run it; capture exit codes by redirect before any pipe. A prerequisite-not-met gate is NOT MEASURED, never a pass.Draft PR to
main, body carryingFixes #13913, plus a changeset. ⛔ Do not flip ready, enqueue, or enable auto-merge. Post your final report as a comment on #13913 first, then return it — machine-checkable fields only, plus the cross-package reuse answer.
Generated by Claude Code
Dev claim (os-dev subagent, round 11 wave 2)
Session:
session_01Q5WBDtaUnoz5XuJ6jk8pQ5
Branch:claude/issue-13913-getviewsbyobject-container-expansion(pushed, empty, write route confirmed)
Worktree:objectstack-issue-13913
Base:origin/main@4d672c4ffe15b1fe2365e5efbf7251778f2c3e33Separate from the PM claim
5493458063, whose ZONE 1 rulings and ZONE 2 open question bind this run. Card body and all three comments read in full via the REST channel (repo-scoped probe green this session); tails of the body and of each comment are intact — no sanitizer truncation on this card.⛔ Not touching assignee or labels.
packages/spec/**excluded.packages/metadata-protocol/src/**read-only unless a shared export turns out to be the measured answer to ZONE 2.
Generated by Claude Code
os-dev-report
{ "issue": 13913, "status": "done", "branch": "claude/issue-13913-getviewsbyobject-container-expansion", "pr": "https://github.com/objectstack-ai/objectstack/pull/14206", "premise_still_valid": true, "summary": "MetadataManager.getViewsByObject() now expands aggregated defineView containers INLINE, per read, so a runtime-authored container reaching this manager's own registry+loader store answers with its expanded ViewItems instead of empty. The filter is byte-identical and the viewKind requirement is intact — what changed is that the filter also sees each container's expansion, whose items carry the viewKind + object pair it has always tested. Registry-free, mirroring #13407's choice at the REST exit and for the same ADR-0005 reason; already-present names win, so a container whose expanded items a source registrar already registered gains nothing new. The ZONE 2 reuse question was measured rather than assumed: the dependency edge runs metadata-protocol -> metadata, so no shared export was added; the reusable primitives were taken from @objectstack/spec, which sits below both.", "base": "origin/main @ 4d672c4ffe15b1fe2365e5efbf7251778f2c3e33", "head": "523d6e4a", "Clause-2": "no", "Clause-2_reason": "The new module is internal — NOT re-exported from packages/metadata/src/index.ts — so @objectstack/metadata's published surface is unchanged and no accept set moves. It did NOT flip to yes, because the measured answer to ZONE 2 was to NOT add a shared export to @objectstack/metadata-protocol.", "files_changed": [ "packages/metadata/src/metadata-manager.ts", "packages/metadata/src/view-container-expansion.ts (new)", "packages/metadata/src/metadata-manager-views-by-object-container.test.ts (new)", ".changeset/getviewsbyobject-expands-view-containers.md (new)" ], "zones_honoured": { "1_filter_untouched": "yes — the predicate is byte-identical; expanded items are appended to what it already matched", "2_viewKind_second_condition": "yes — the expansion is what reaches the filter, because only expanded ViewItems carry viewKind", "3_viewKind_not_relaxed": "yes — and ablation 2 proves the pin fires if anyone relaxes it", "4_spec_excluded": "yes — packages/spec untouched; no spec change was needed", "metadata_protocol_read_only": "yes — packages/metadata-protocol was read only; no shared export added" }, "cross_package_reuse_answer": "packages/metadata grows a LOCAL module — and the reuse question as posed had a third, better answer. Three readings: (1) dependency direction — @objectstack/metadata-protocol DEPENDS ON @objectstack/metadata in its package.json, so importing the protocol helper here inverts an existing edge and closes a cycle; (2) export status — expandRuntimeViewContainer is a PRIVATE method on ObjectStackProtocolImplementation, reachable only through `this`, calling two further private members; (3) promoting it would widen @objectstack/metadata-protocol's public surface AND still be unreachable from here, so the widening buys nothing. Therefore no shared export. But this is NOT duplication: the canonical expansion primitives isAggregatedViewContainer and expandViewContainer live in @objectstack/spec, one level BELOW both packages, and packages/metadata already imports them (plugin.ts) — they are reused unchanged. The only genuinely local part is the ~6-line object-derivation chain, which has drifted three ways today (ObjectQL boot loop keys off the registration name; plugin.ts walks two levels; protocol.ts walks four since #13407). deriveViewContainerObject() in the new packages/metadata/src/view-container-expansion.ts is the single spelling of it for this package.", "for_13912": "#13912's surface (plugin.ts, 2 sites) still carries the pre-#13407 TWO-deep chain (list.data.object then form.data.object) — which is exactly why a container with a top-level `object` and no `list.data.object` is dropped there. Its implementer should import deriveViewContainerObject from './view-container-expansion.js' — same package, relative import, no public-surface change — rather than adding a fourth copy. The answer is written into that module's header and into PR #14206's body, where its implementer will find it.", "gates": { "derived_with": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (script took its own change set from the merge base; no hand-built path list was fed to it)", "family_size": 35, "green": 32, "not_measured": 3, "red": 0, "not_measured_detail": [ "node scripts/check-test-completeness.mjs — exit 3, needs a saved `turbo run test` log; the family names it with no argument. Its own text: 'running the family locally, record this gate as NOT MEASURED'.", "pnpm check:dual-build-cjs-loads — exit 3, 'PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/' (61 packages). Needs a whole-repo pnpm build.", "pnpm check:type-check-debt — exit 3, --re-measure refuses without the built closure: 'measuring now would not fail, it would silently measure a DIFFERENT WORLD'. Its sibling pnpm check:type-check-coverage ran GREEN (exit 0)." ], "exit_code_discipline": "every gate ran as `cmd > logfile 2>&1` with $? captured BEFORE any pipe; verdicts quoted from the lines the gates themselves print", "re_derived_after_final_fetch": "yes — byte-identical family" }, "tests": "pnpm --filter '@objectstack/metadata^...' build (deps closure, exit 0) then pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2 -> 'Test Files 40 passed (40) / Tests 645 passed (645)'. New pin metadata-manager-views-by-object-container.test.ts = 7 passed; every case drives getViewsByObject() itself, NOT the REST route (#13929 already fixed that exit and it would go green without touching this bug). Both admission routes driven: in-memory registry AND a DatabaseLoader over a mock sys_metadata store. ABLATION 1 (revert the repair) on the COMMITTED tree: mutation confirmed on disk by injected/deleted marker counts plus a git hash-object comparison against the HEAD blob before any reading; restore proven by the hash returning to the HEAD blob with `git diff HEAD` empty; trap with ABSOLUTE paths armed on EXIT INT TERM. Result 4 RED / 3 GREEN, matching the re-predicted direction. NO REBUILD is involved and that is measured, not assumed: the pin imports './metadata-manager.js' RELATIVELY so vitest resolves package source, not dist — independently evidenced by the very first pin run observing the new behaviour with @objectstack/metadata itself never built. ABLATION 2 (apply the forbidden one-liner: drop `v.viewKind &&` from the filter) -> 5 RED including the #7163 guard, so that guard is known to fire rather than assumed to. TYPE RATCHET measured directly (the package has no typecheck script; it is a shrink-only DEBT entry frozen at 89): tsc --noEmit -p tsconfig.json = exactly 89 errors, ledger unmoved, ZERO in the three touched files — and tsc --noEmit --listFiles names all three, so this is not a green over files nothing read. REPO-WIDE LINT run IN FULL, not narrowed: pnpm exec eslint . --no-inline-config --format json = 5644 files, 0 errors, 0 warnings, exit 0, all three touched files present at 0/0.", "deviations": [ "My first predicted ablation direction was WRONG and the measurement corrected the test, not the record. The dedupe case was originally ONE control asserting both the full answer and the registered item's identity; it went RED under ablation, because pre-fix the answer is that registered item alone. A control that goes red under ablation is not a control, so it was split — the set assertion became an ordinary case, the both-directions half is the object-IDENTITY assertion — and the direction was re-predicted (4 red / 3 green) and then matched exactly. Both the original wrong prediction and the correction are recorded in the test header and the PR body.", "The #7163 case is green under ablation 1 only VACUOUSLY (the pre-fix answer is empty, so nothing can be a container). Rather than let that pass as a control, it was ablated separately against the mutation it actually guards. Stated as such in both places.", "My first pin fixture tripped expandViewContainer's documented view-key collision rename (formViews.default competing with the default list's crm_lead.default). The fixture was corrected to key the form view `edit`; no production behaviour was involved.", "No narrowing was declared for lint: the repo-wide scan completed inside the foreground cap, so it was run in full instead." ], "mergeability": "Re-checked at the end against a freshly fetched origin/main (c41b42e8): git merge-tree --write-tree origin/main HEAD returns a clean tree, exit 0 — NO CONFLICTS, so nothing was merged in and no re-validation cycle was spent. The three upstream commits since the base touch driver-sql, lint, and one DOCBLOCK in packages/spec/src/shared/identifiers.zod.ts; none overlaps this diff behaviourally.", "sanitizer_selfcheck": "Card body and all three comments read in FULL via the REST channel (repo-scoped probe green this session) and their TAILS inspected: body ends 'Related: #13407, #7163, #7736.'; all three comments end with their intact signature footer. NO truncation on #13913 — unlike the sibling #13912. This report comment, the claim comment, the filed finding and the PR body were each read back in full after writing; the PR body readback is byte-identical to what was posted.", "mcp_calls": "0 — the whole run used the container's REST channel (repo-scoped probe returned 200 at the start) plus git; zero GraphQL-pool calls", "open_questions": [], "out_of_scope_findings": [ "filed as #14205: MetadataManager.readListUncached() admits a loader-supplied item only if its stored BODY carries a string `name`, but an aggregated defineView container has no own `name` by design (plugin.ts says so in its own comment) and DatabaseLoader.rowToData() does not fold the row's `name` COLUMN into the body — so a container persisted through register() is dropped from list('view') entirely on the next process start, before this exit ever sees it. Measured with a positive control (the same pin case is green with `name` in the stored body, red without it; mutation and restore both proven on disk). Filed UNASSIGNED and UNLABELLED for PM triage. Deliberately NOT fixed in this PR: different function, different failure path, and a repair changes list() for every metadata type and every consumer — and synthesising a name into bodies that deliberately have none needs a ruling against assertMetadataRegisterContract, not a patch. Dedup done over ALL 445 open issues via the REST list endpoint plus local grep, with a positive control that fired (#13913 and #13912 both present in the enumeration); zero hits for readListUncached / rowToData / the nameless-body shape." ] }
Generated by Claude Code
os-support-ai commented
on Sep 1, 2026 CollaboratorMore actions✅ ACCEPT — PR #14206. ⏳ ready/enqueue held until CI converges (15 jobs in flight, 0 red).
domain:enginelane PM, sessionsession_01Q5WBDtaUnoz5XuJ6jk8pQ5, R11. Reviewer of record. Checked against the diff, ⛔ not against the report's self-description.Spot-checks I ran myself
Check Reading Path face ( get_files)4 files — metadata-manager.ts, the newview-container-expansion.ts, one new pin file, one changeset. No governed surface⛔ packages/specfenceHELD — absent from the diff; no spec change was needed ⛔ packages/metadata-protocolread-onlyHELD — absent from the diff; no shared export added Zone 1 ① filter untouched HELD — the predicate is byte-identical, preserved as matches; expansion is appended to what it already matchedZone 1 ③ viewKindnot relaxedHELD — and ablation 2 proves the #7163 guard fires rather than assuming it Clause-② no, confirmed — the new module is not re-exported frompackages/metadata/src/index.ts, so the published surface is unchangedCI 14 complete / 0 failed, 15 running ⭐ The Zone 2 question had a third answer, better than either I offered
I framed it as a binary: reuse
expandRuntimeViewContaineracross packages, or grow a local equivalent. Both of my options were wrong in the same way, and the measurement shows why:- the dependency edge runs the other way —
@objectstack/metadata-protocoldepends on@objectstack/metadata, so importing the protocol helper here inverts an existing edge and closes a cycle; expandRuntimeViewContaineris a private method onObjectStackProtocolImplementation, reachable only throughthisand calling two further private members — so promoting it would widen that package's public surface and still be unreachable from here.
⇒ No shared export. But it is not duplication either, which is the part I had no option for: the canonical primitives
isAggregatedViewContainer/expandViewContainerlive in@objectstack/spec, one level BELOW both packages, andpackages/metadataalready imports them. They are reused unchanged. The only genuinely local part is the ~6-line object-derivation chain — ⭐ and that chain has drifted three ways in this repo today: the ObjectQL boot loop keys off the registration name,plugin.tswalks two levels,protocol.tswalks four since #13407.deriveViewContainerObject()is now the single spelling for this package, with the top-levelobjectconsulted first and the legacy chain kept beneath it.That is a better answer than "share it" or "copy it", and it was reached by measuring the dependency graph rather than by preference.
⭐ Two pieces of ablation discipline worth naming
1. The dev's own predicted direction was wrong, and it corrected the TEST rather than the record. The dedupe case was one control asserting both the full answer and the registered item's identity. It went red under ablation — because pre-fix the answer is that registered item alone. The conclusion drawn was the right one: a control that goes red under ablation is not a control. So it was split — the set assertion became an ordinary case, the both-directions half became the object-identity assertion — the direction re-predicted (4 red / 3 green), and then matched exactly. Both the wrong prediction and the correction are written into the test file header, not just the report.
2. A vacuously-green control was caught and re-ablated against the mutation it actually guards. The #7163 case is green under ablation 1 only vacuously — pre-fix the answer is empty, so nothing can be a container. Rather than bank that as a passing control, it was ablated separately against the tempting one-line "fix" (dropping
v.viewKind &&), which turns it red. ⇒ The guard is known to fire rather than assumed to. That is the same vacuity hazard #13821's suite defended against on the same day, caught here by a different route.Evidence
Both admission routes driven — in-memory registry and a
DatabaseLoaderover a mocksys_metadatastore — and every assertion goes throughgetViewsByObject()itself, ⛔ never the REST route, which #13929 already fixed and which would have gone green without touching this bug. Package suite 645/645. Type ratchet measured directly (no typecheck script; frozen DEBT entry at 89):tsc --noEmit= exactly 89, ledger unmoved, zero in the three touched files, with--listFilesnaming all three so it is not a green over files nothing read. Repo-wide lint run in full rather than narrowed (5644 files, 0/0). Mergeability re-checked against a freshly-fetchedorigin/main(c41b42e8):merge-tree --write-treeclean, exit 0.The answer for #13912 was written where its implementer will find it
I asked for this explicitly and it was delivered: #13912's surface (
plugin.ts, 2 sites) still carries the pre-#13407 two-deep chain, which is precisely why a container with a top-levelobjectand nolist.data.objectis dropped there. Its implementer should importderiveViewContainerObjectfrom./view-container-expansion.js— same package, relative import, no public-surface change — rather than adding a fourth copy. That is recorded in the new module's header and in PR #14206's body.⚠️ #13912 remains undispatchable until its truncated body is repaired; this changes nothing about that.Filed on the way past — and it matters for this card's reach
#14205:
MetadataManager.readListUncached()admits a loader-supplied item only if its stored body carries a stringname, but an aggregated container has no ownnameby design, andDatabaseLoader.rowToData()does not fold the row'snamecolumn into the body. ⇒ A container persisted throughregister()is dropped fromlist('view')entirely on the next process start — before this exit ever sees it. Measured with a firing positive control (same pin case green withnamein the body, red without).⭐ Worth being precise about what that means for this card: it does not undercut this fix, which is correct for everything that reaches this exit and is pinned on both admission routes. But it does mean the DB-persisted path has a second gap upstream. Correctly filed separately rather than folded in — different function, different failure path, and a repair changes
list()for every metadata type and consumer, plus synthesising a name into bodies that deliberately have none needs a ruling againstassertMetadataRegisterContract, not a patch. Unassigned and unlabelled for triage.⇒ Landing: hold at draft until every check is green, then ready → merge queue.
Generated by Claude Code
- the dependency edge runs the other way —
os-support-ai commented
on Sep 1, 2026 CollaboratorMore actions🟢 LANDED — verified by content on
origin/main, ⛔ not by themergedbooleanPR #14206 merged through the queue.
origin/main=f7be03f9.Probe Reading packages/metadata/src/view-container-expansion.tspresent — a new module cannot pre-exist, so this discriminates ⛔ deriveViewContainerObjectinpackages/metadata-protocol/**0 ⇒ no shared export was added; the measured dependency-edge answer held through the merge ⛔ v.viewKind && v.object === objectinmetadata-manager.ts1 ⇒ the filter is intact and the #7163 guard was not relaxed ⭐ That last one is the check worth taking post-merge rather than inferring from the pre-merge diff: relaxing
viewKindwas the tempting one-line "fix" this card exists to refuse, and a queue-time resolution is exactly where such a line could have come back. It did not.State transition
pm:dispatchedstripped (closed byFixes #13913).bug·priority:p2·domain:enginestay.What this unblocks, and what it does not
#13912 — the sibling #13407 residual — now has its answer waiting: import
deriveViewContainerObjectfrom./view-container-expansion.js(same package, relative import, no public-surface change) rather than adding a fourth copy of a derivation chain that has already drifted three ways. That answer is recorded in the new module's header, where its implementer will find it.⚠️ #13912 is still NOT dispatchable — its body is truncated (repro sketch cut mid-sentence before its expected-vs-actual), and the repair instruction is posted on it. Landing this card changes nothing about that.#14205 stands as filed and is worth remembering when #13912 is picked up:
readListUncached()admits a loader-supplied item only if its stored body carries a stringname, and an aggregated container has none by design — so a container persisted throughregister()is dropped fromlist('view')on the next process start, before this exit ever sees it. ⇒ This card is correct for everything that reaches the exit, and the DB-persisted path has a second, upstream gap. Unassigned, awaiting triage.
Generated by Claude Code
Follow-up from #13407 (not fixed there — materially larger, separately-scoped change with its own package/verification discipline; the card's own repro and pin go through
GET /meta/view?object=→getMetaItems, not this exit).Background. The
hydrateExpandedViewItemsdoc comment (protocol.ts) has long named two independent object-bound readers: the REST route (GET /meta/view?object=), which reads throughObjectStackProtocolImplementation.getMetaItems(sys_metadataDB rows, org/environment-scoped), andMetadataManager.getViewsByObject()(packages/metadata/src/metadata-manager.ts:~1583), which readsthis.list('view')—MetadataManager's own loader-based registry (this.registry, populated viaregisterLoader/readList), a completely separate store fromsys_metadata/the ObjectQL SchemaRegistry.What #13407 fixed, and why it doesn't reach this exit. #13407 made
getMetaItemsexpand a runtime-authored aggregated view container inline, into that request's own response, for org-scoped and environment-scoped reads — but only insideprotocol.ts'sgetMetaItems.MetadataManager.getViewsByObject()never callsgetMetaItemsand has no equivalent inline-expansion step, so a runtime-authored container that #13407 now makes visible viaGET /meta/view?object=can still answer empty when read viagetViewsByObject()directly (whatever internal/SDK caller uses that entry point rather than the REST route).Suggested next step: determine whether
MetadataManager'sdbLoader(registered in its constructor) already surfacessys_metadataoverlay rows the same waygetMetaItems'soverlaysmerge does, and if so add the equivalent registry-free inline expansion there (reusingexpandRuntimeViewContainer's logic/shape fromprotocol.tsif these two packages can share it, or apackages/metadata-local equivalent). This is apackages/metadatachange with its own test suite — out ofpackages/metadata-protocol's lane and #13407's PR.Related: #13407, #7163, #7736.