Uh oh!
There was an error while loading. Please reload this page.
fix(objectql): a tenant's rename beats a package extension's scalar in the object fold (#8460) - #8620
Conversation
…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
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
…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
Uh oh!
There was an error while loading. Please reload this page.
Closes#8460
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
mergeObjectDefinitionsapplied an extender'slabel/pluralLabel/descriptionlast, onto whatever base it was handed:
So a rename saved through the ordinary Studio round-trip answered
200, was readable under?layers=trueasoverlay.label, and was overwritten inside the fold before any readserved it.
?layers=true— documented as a diagnostic — was the only surface showing atenant 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:
scalarOverridesPackagedBase@objectstack/specand imported bySchemaRegistryOne sentence now governs both layers: an explicit override beats a packaged default.
SchemaRegistry.tenantAuthoredScalarscomputes which of the three scalars the base hasauthored away from the
owncontributor's value;mergeObjectDefinitionstakes that set anddeclines those keys. The whole change sits at
foldExtendersOntoDefinition— the single foldthat
resolveObject(D9.2),resolveOwnerLayer(D9.6) andfoldObjectExtendersOnto(#7556)all already funnel through, so
metadata-protocolneeds no change: both of its read exits(
getMetaItem,getMetaItemLayered) reach the fix throughfoldObjectExtendersFromRegistry.Four properties that are load-bearing, each pinned in the doc comment:
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.
resolveOwnerLayer(owner withextenders 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.
owncontributor, deliberately notgetPackagedObjectOwner— whose extraisCodeArtifactBodytest (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.
objectExtensionsfield from the resolved schema, andeffectivecontradictscodein the same response #8027) is preserved — a base already carrying an extender's scalar reads asdiverged, 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/0029gains D9.2a, recording the ruling, the mechanism, the four propertiesabove, 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 forexactly this ruling, flips green as written.
it.failsbecameit; not one assertion wastouched.
Reverse verification (direction predicted before running: red). With the fold change
reverted and objectql rebuilt, that pin alone fails:
— 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 agreeand 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
@objectstack/objectqltest@objectstack/objectqltypecheck@objectstack/metadata-protocoltest@objectstack/spectestGates 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 inspectioncheck: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.jsonis regenerated (gen:export-origins) — a one-linedelta recording the newly exported predicate, which its freshness gate requires.
Changeset:
patchon@objectstack/objectqland@objectstack/spec.Generated by Claude Code