Skip to content

fix(objectql): a tenant's rename beats a package extension's scalar in the object fold (#8460) - #8620

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-8460-extension-scalar-tenant-precedence
Aug 14, 2026
Merged

fix(objectql): a tenant's rename beats a package extension's scalar in the object fold (#8460)#8620
os-zhuang merged 3 commits into
mainfrom
claude/issue-8460-extension-scalar-tenant-precedence

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes#8460

⚠️This PR is maintainer-merged — please do not auto-merge it. The ruling amends
ADR-0029 D9.2, and the amendment rides this implementation PR, so docs/adr/** puts it
under the standing maintainer-merge guardrail. The ADR maintainer approval gate is
expected to sit unsatisfied; it is not mine to clear. Taken to green and left ready.

The ruling this implements

Maintainer ruling, 2026-08-13 (comment) — option A, "tenant wins":
an extender's scalar applies only while the fold's base still carries the packaged owner's
value; a diverged base has been authored by the tenant and the extender yields. No new
provenance flags, no migration, no escape hatch. Options B (status quo) and C (refuse the
write) were rejected, as was #8284's arm B (dropping scalars from the fold).

The defect

ADR-0029 D9.2 makes the tenant's overlay the base of the object fold, and
mergeObjectDefinitions applied an extender's label / pluralLabel / description
last, onto whatever base it was handed:

// Override scalar props (last writer wins)if(extension.label!==undefined)merged.label=extension.label;

So a rename saved through the ordinary Studio round-trip answered 200, was readable under
?layers=true as overlay.label, and was overwritten inside the fold before any read
served it. ?layers=true — documented as a diagnostic — was the only surface showing a
tenant what they had saved.

The mechanism, and how it matches #8284's

The ruling required the same comparison-based mechanism as the #8284 fix, one layer down,
explicitly not a second comparison shape. So this reuses the predicate itself rather than
re-spelling it:

#8284 (one layer up)#8460 (this PR)
Questionhas this scalar been authored away from the packaged default?the same question
PredicatescalarOverridesPackagedBasethe same function, now exported from @objectstack/spec and imported by SchemaRegistry
Compared againstthe packaged owner alonethe packaged owner alone
Loserthe i18n catalogthe extender's scalar
Provenancecomparison only, nothing stampedcomparison only, nothing stamped

One sentence now governs both layers: an explicit override beats a packaged default.

SchemaRegistry.tenantAuthoredScalars computes which of the three scalars the base has
authored away from the own contributor's value; mergeObjectDefinitions takes that set and
declines those keys. The whole change sits at foldExtendersOntoDefinition — the single fold
that resolveObject (D9.2), resolveOwnerLayer (D9.6) and foldObjectExtendersOnto (#7556)
all already funnel through, so metadata-protocol needs no change: both of its read exits
(getMetaItem, getMetaItemLayered) reach the fix through foldObjectExtendersFromRegistry.

Four properties that are load-bearing, each pinned in the doc comment:

  • Computed once, over the base the fold starts from — never re-derived from the running
    merge, which would make one extender's scalar look "authored" to the next and silently
    invert extender-vs-extender precedence, which D9.3 reserves to declared priority.
  • Compared against the owner alone, never against resolveOwnerLayer (owner with
    extenders already folded on) — that body reports every extender's scalar as "unchanged" and
    would yield nothing, ever. This is PR fix(i18n): the object catalog loses to an explicitly-set scalar (#8284) #8454's trap, in its own form one layer down.
  • The own contributor, deliberately not getPackagedObjectOwner — whose extra
    isCodeArtifactBody test (D9.8) would decline to protect a runtime-authored object, i.e.
    exactly the object whose owner row the tenant wrote by hand. The two agree wherever a
    packaged owner exists.
  • Idempotence (A tenant object overlay drops every objectExtensions field from the resolved schema, and effective contradicts code in the same response #8027) is preserved — a base already carrying an extender's scalar reads as
    diverged, so the extenders yield and the value stays what the first fold produced: the same
    answer, reached by yielding instead of re-applying.

ADR amendment

docs/adr/0029 gains D9.2a, recording the ruling, the mechanism, the four properties
above, the rejected options, and the accepted cost. It also records one honest edge the
no-flags constraint implies: because the write path persists the served body verbatim
(ADR-0005 §Validation), a tenant who round-trips an object without renaming it freezes the
extender's current scalar into the overlay row, so a later change to the package's extension
scalar will not reach that tenant. That follows from comparison-based provenance with no
flags — which is what the ruling required — so it is written down rather than papered over.

Acceptance evidence — the staged pin, flipped and otherwise unmodified

showcase-object-extension-scalar-divergence.dogfood.test.ts's second pin, staged by #8284 for
exactly this ruling, flips green as written. it.fails became it; not one assertion was
touched.

 Test Files 1 passed (1)
Tests 6 passed (6)

Reverse verification (direction predicted before running: red). With the fold change
reverted and objectql rebuilt, that pin alone fails:

 × SHOULD: a tenant's own rename reaches the reads its forms derive from
AssertionError: expected 'Account (Success Overlay)' to be 'Customer'
Tests 1 failed | 5 passed (6)

— the defect itself, and the other five stay green, which confirms the neighbouring [#8284]
case is genuinely independent of fold precedence. Restored byte-identically from the commit
(empty git diff HEAD) and re-run green.

The [#8284] case's assertions still pass untouched: it asserts that the three reads agree
and that they do not serve the catalog string — never which value they agree on. Its prose,
and the file header, are updated where this fix falsified them.

Verification

SuiteResult
@objectstack/objectql test202 files / 3559 tests passed
@objectstack/objectql typecheckclean
@objectstack/metadata-protocol test85 files / 1261 tests passed
@objectstack/spec test397 files / 10520 tests passed
dogfood acceptance file6/6 passed
eslint (3 changed sources)clean

Gates derived from the actual changed paths via scripts/pm/dispatch-gates.mjs, all green:
check:adr-anchors, check:adr-links, check:adr-0087-registration, check:changeset-gate-self-tests,
check:changeset-no-major, check:empty-changeset, check:objectui-changeset, check:spec-parsed-alias,
check:test-source-alias, check:merge-driver, check:durability-log-level, check:engine-split-ratio,
check:dev-prereqs, plus by inspection check:nul-bytes, check:i18n, check:slot-lookup,
check:doc-anchors, check:where-matcher, check:engine-double-contract, check:type-check-debt
(re-measured against a fully built closure: 33 ledger entries, none above its recorded number).

packages/spec/export-origins/system.json is regenerated (gen:export-origins) — a one-line
delta recording the newly exported predicate, which its freshness gate requires.

Changeset: patch on @objectstack/objectql and @objectstack/spec.


Generated by Claude Code

…n the object fold (#8460)
ADR-0029 D9.2 makes the tenant's overlay the BASE of the object fold, and
mergeObjectDefinitions applied an extender's label / pluralLabel / description
LAST onto whatever base it was handed. So a rename saved through Studio
answered 200, showed up under ?layers=true, and was overwritten inside the
fold before any read served it.
Per the 2026-08-13 maintainer ruling (option A), an extender's scalar now
applies only while the fold's base still carries the packaged owner's value;
a diverged base was authored by the tenant and the extender yields. This is
deliberately the SAME comparison-based mechanism #8284 established one layer
up for the i18n catalog — scalarOverridesPackagedBase is exported from
@objectstack/spec and imported by the registry rather than re-spelled, so one
sentence governs both layers: an explicit override beats a packaged default.
No provenance flags, no migration, no escape hatch. The set is computed once
over the base the fold starts from, never re-derived from the running merge,
so extender-vs-extender precedence (D9.3) is untouched. Idempotence (#8027)
is preserved by construction.
Amends ADR-0029 D9.2 (new D9.2a). Flips #8284's staged acceptance pin in
showcase-object-extension-scalar-divergence.dogfood.test.ts to green,
unmodified in its assertions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDTnVvsgA6cUZ4xFVtPZRy
@vercel

vercelBot commented Aug 14, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 14, 2026 3:55am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/spec.

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

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx(via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via packages/objectql, @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/permissions/system-context.mdx(via packages/objectql, packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via packages/objectql, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

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

  • content/docs/releases/implementation-status.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)

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.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:system tests tooling labels Aug 14, 2026
…napshot (#8460)
The `TypeScript Type Check` job's final step is `check:api-surface`, which
failed on the export #8460 adds: "0 breaking (removed/narrowed), 1 added".
Purely additive — `scalarOverridesPackagedBase` is exported so the object
fold can reuse the #8284 predicate instead of re-spelling it, per the ruling.
Sibling of the export-origins regeneration already in this branch; the two
snapshots are separate gates over the same export.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDTnVvsgA6cUZ4xFVtPZRy
`Test Core (2/3)` reddened at a1a1fc8 on
`db-job-adapter.timeout.test.ts` — a wall-clock flake filed as #8628: the
case registers a real every-minute cron and asserts exactly one run row, so
it fails when the shard straddles a minute boundary (CI: file started
03:02:55, assertion failed 03:03:00.17).
Not reachable from this diff — `@objectstack/service-job` does not depend on
`@objectstack/objectql`. Verified green locally 5/5 in isolation and 71/71
for the package, plus a full sweep of the fold's dependent class.
No re-run permission from this seat, so this empty commit retriggers CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDTnVvsgA6cUZ4xFVtPZRy
@os-zhuang
os-zhuang marked this pull request as ready for review August 14, 2026 04:11
@os-zhuang
os-zhuang added this pull request to the merge queueAug 14, 2026
Merged via the queue into main with commit 01a7337Aug 14, 2026
28 of 29 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8460-extension-scalar-tenant-precedence branch August 14, 2026 06:12
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A package extension's scalar overwrites a tenant's Studio rename inside the object fold, so the rename still reaches no read

3 participants

@os-zhuang@hotlong@claude