Skip to content

fix(objectql): expand an aggregated views container from the nested-plugin seam too (#7163) - #7452

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-7163-nested-plugin-views-expansion
Aug 10, 2026
Merged

fix(objectql): expand an aggregated views container from the nested-plugin seam too (#7163)#7452
os-zhuang merged 1 commit into
mainfrom
claude/issue-7163-nested-plugin-views-expansion

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #7163. Follow-on to #7049.

The fact, re-measured on ba63f0e

packages/objectql/src/engine.ts reaches the registration seam from two entry points. Since #7049 both enumerate one METADATA_ARRAY_KEYS, so views is seen by both — but only the manifest seam expanded an aggregated view container (expandViewContainer imported at engine.ts:136, called at exactly one site, :3458). The nested-plugin loop had no equivalent. One container through each seam:

via manifest      → ['account', 'account.all_accounts', 'account.form']
via nested plugin → ['account']

"Object has-many View" (ADR-0017 §2, §3.2) makes the loader dual-read: the aggregated defineView container is registered under the bare <object> key for back-compatible reads and expanded into independent ViewItems under <object>.<viewKey>. Only the expanded items carry viewKind, and getViewsByObject() filters on exactly that — so a container that is registered but never expanded is invisible to it, to GET /meta/view?object=, and to the view switcher and Studio's package attribution above it. No refusal and no diagnostic.

Which seam is right — measured, not assumed

The card deliberately did not pick. Both readings were live: the nested seam gains the expansion, or the manifest seam's expansion is the anomaly. Four measurements, all on this base:

  1. ADR-0017 scopes the dual-read to "the loader" at load time, not to one entry point; §3.2 spells out that it registers both the container and every expanded item.
  2. The other loader agrees with the manifest seam. MetadataPlugin's artifact/HMR path expands too (packages/metadata/src/plugin.ts:643-655). That is why the implementation was pushed down into @objectstack/spec in the first place — ui/view.zod.ts states the reason inline: "so the two loaders cannot drift." Removing the expansion would put engine.ts alone against both the ADR and the sibling loader.
  3. Every authored stack in the tree ships views at manifest top level — ~51 files, including examples/app-crm, examples/app-todo, examples/app-showcase, packages/qa/downstream-contract. Removing the manifest seam's expansion takes the view switcher away from all of them.
  4. No in-tree package ships views through a nested plugin — swept examples/, apps/, packages/ and every fixture: zero. Every in-tree plugins: array holds runtime Plugin instances or package-name strings, none carrying metadata collections.

So the divergence is not load-bearing in both directions: one direction breaks every shipping consumer, the other breaks nobody in-tree. The nested seam catches up — the same direction #7049 took for the enumeration.

The change

The copies are gone rather than reconciled: both seams now run one registerMetadataCollections(source, ownerId, sourceLabel).

#7049 hoisted the shared METADATA_ARRAY_KEYS and measured the two loop bodies on the way past, recording that they still differed in a per-key debug line, this view expansion, and a warn-on-nameless-item. Sharing the list made "which collections does a seam see?" unanswerable-differently; it left "what does a seam do with a collection both see?" answered in two places. Both remaining differences had the same structure — a body copied, then improved on one side only — so the body is shared too, for the reason #7049 gave for the list: a divergence that cannot be written down cannot be re-introduced by the next hand patch. What legally varies is passed in (which object is read, which package id is stamped, the debug label).

Behaviour deltas for a nested plugin, both from body-sharing: an aggregated views container now expands, and a nameless item now emits the warn the manifest seam always emitted instead of being silently skipped. Nothing is removed and no manifest-seam behaviour changes.

Pins

New packages/objectql/src/engine-nested-plugin-view-expansion.test.ts:

  • parity — an aggregated container registers identically via manifest and via nested plugin (stated as seam-vs-seam equality, so a change to the expansion rules must move both or fail);
  • expanded identities — name, viewKind, object, scope, isDefault, order, config, and ADR-0010 provenance stamped to the parent package;
  • ADR-0017 dual-read — the bare <object> container survives alongside the expansion, both seams;
  • control — a non-aggregated ViewItem still registers exactly once through both seams, no expansion, no new keys; a container-free manifest still registers no views.

engine-nested-plugin-collections.test.ts: the views exclusion row — the only entry ever excluded for a behaviour difference rather than a retired kind — is removed with the divergence, and views rejoins the cross-seam property test.

Reverse-verified: with engine.ts reverted to origin/main and the new tests kept, 8 pins fail with exactly the reported symptom (['account'] alone); the two control pins still pass, which is what makes them controls.

Gates run locally (all success)

gate result
packages/objectql full vitest 176 files, 3127 passed
check:stack-collection-maps OK — 7 enumerations reconciled
check:adr-anchors OK
check:nul-bytes OK — 6830 files
check:engine-double-contract OK — 134 pinned, 2 exempt
check:durability-log-level OK — 24 seams, all loud
tsc --noEmit (objectql) clean

check-engine-split-ratio.mjs is report-only by default (ADR-0076 OQ#5) — no threshold to trip.

Changeset

patch, @objectstack/objectql. Direct precedent: #7049's own changeset (nested-plugin-collection-registration.md) shipped the same class — "this changes boot behaviour for packages that already ship today", a nested plugin beginning to register what it silently dropped — as patch. This is additive registration on one shape (isAggregatedViewContainer), removes nothing, and changes no public type. No packages/spec touch, so no #6017 declaration; not major, so no ADR-0087 trio.

Refs: #7163, #7049, #6242, #5870, ADR-0017, ADR-0010.


Generated by Claude Code

…-plugin seam too (#7163)

ADR-0017 makes the loader dual-read: an aggregated `defineView` container is
registered under the bare `<object>` key for back-compatible reads AND expanded
into independent `ViewItem`s under `<object>.<viewKey>`. Only the expanded items
carry `viewKind`, and `getViewsByObject()` filters on exactly that — so the
expanded layer, not the container, is what `GET /meta/view?object=`, the view
switcher and Studio's package attribution read.

`engine.ts` reaches the registration seam from two entry points and only the
manifest one expanded. One container measured through each:

  via manifest      → ['account', 'account.all_accounts', 'account.form']
  via nested plugin → ['account']

No refusal, no diagnostic — a package shipping views through `manifest.plugins[]`
had no views as far as every reader of the expanded layer was concerned.

The direction was measured, not assumed. ADR-0017 states the dual-read as a
property of "the loader" at load time rather than of one entry point; the other
loader (`MetadataPlugin`'s artifact/HMR path) expands too, which is why the
shared implementation was pushed into `@objectstack/spec` "so the two loaders
cannot drift"; every authored stack in the tree ships `views` at manifest top
level, so removing the manifest expansion would take the switcher from all of
them; and no in-tree package ships `views` through a nested plugin, so the seam
that gains behaviour breaks nobody. One direction is load-bearing, the other is
not.

So the copies are gone rather than reconciled: both seams now run one
`registerMetadataCollections()`. #7049 hoisted the shared `METADATA_ARRAY_KEYS`
and recorded that the bodies still differed in a per-key `debug` line, this
expansion, and a warn-on-nameless-item. Sharing the list made the seams'
collection SET unanswerable-differently; sharing the body does the same for what
they DO with a collection both see — a nested plugin now also emits the
nameless-item warning it used to swallow.

Pins: manifest-vs-nested parity for an aggregated container, the expanded
per-view identities (name, `viewKind`, object, isDefault, order, config,
ADR-0010 provenance stamped to the parent package), the ADR-0017 dual-read
back-compat half, and a control that a non-aggregated `ViewItem` still registers
identically through both seams with no expansion.
`engine-nested-plugin-collections.test.ts`'s `views` exclusion row — the only
one that was ever a behaviour difference rather than a retired kind — is removed
with the divergence, and `views` rejoins its cross-seam property test.

Refs: #7163, #7049, #6242, #5870, ADR-0017, ADR-0010.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPVc5WY75PkeXoA5NFiEEk
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 12:48pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

15 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/system-context.mdx (via packages/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants