Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-protocol): key the /meta list merge on the i18n bundle discriminator (#7774) - #7931
Conversation
…iscriminator (#7774) `GET /meta/email_template` served one locale of a declared i18n bundle. #7730 taught the SchemaRegistry that the type's identity is `(name, locale)`, so `listItems` returns every member; `getMetaItems` then merged that listing with two higher layers, both keyed by `(package, name)` with no discriminator. - The MetadataService merge (`metaItemKey`) overwrote the first member with the second. This is the path the issue named; it runs only when a `metadata` service is installed and answers non-empty for the type. - The `sys_metadata` overlay merge (`mergePackageAwareOverlay`) was predicted to need no change because overlay rows are unique on `type+name+organization_id+package_id` and carry no locale. True of the rows, and beside the point: the merge's BASE is the registry's bundle, bucketed by bare name, so one overlay row for the type dropped a locale and the survivor was the overlay body regardless of which member it customizes. `metaItemKey` takes an optional third component and `mergePackageAwareOverlay` buckets per slot; both omit the component for a type that declares no discriminator, so every other type's key is byte-identical. The discriminator table moves from `@objectstack/objectql`'s `registry.ts` to `@objectstack/metadata-core` — objectql depends on metadata-protocol, so the protocol package could not import it without closing a cycle, and metadata-core is the package both already depend on. `registry.ts` re-exports it under its original name from its original module, so no public surface changes. The registry's storage-key format stays where it is; only the identity question moved. First coverage for this path: `protocol.i18n-bundle-list-merge.test.ts`, 13 cases, each installing the precondition the merge block it targets hides behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxLeX1UvWRzjJ1UUpd7dGt
…heck DEBT ledger (#7774) The new test's `row()` helper took `Partial<Row> & { metadata: unknown }`, so `metadata` resolved to `string & unknown` = `string` and every call site passing an object body was a TS2322. `pnpm check:type-check-debt` measured metadata-protocol at 73 against a frozen 63 and failed — the ledger is a ratchet and may only shrink (#5278), so this is a repair rather than a raised entry. `Omit<Partial<Row>, 'metadata'>` restores the intended `unknown`. No behavioural change: 13/13 still pass and the reverse verification is unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxLeX1UvWRzjJ1UUpd7dGt
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 3 package(s): 16 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also reference the affected code. These are read-only:
|
huangyiirene
commented
Aug 12, 2026
ACCEPT — flipped ready, auto-merge SQUASH armed @ |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7774
What
GET /meta/email_templateserved one locale of a declared i18n bundle. #7730 taught theSchemaRegistrythat anemail_template's identity is(name, locale), solistItems('email_template')returns every member;getMetaItemsthen merges that listing with two higher layers, and both merges keyed by(package, name)with no discriminator.Both now key on the pair, and both are byte-identical for a type that declares no discriminator — which is every type except
email_templatetoday.The card's premise failed —
premise_still_valid: falseThe issue said the
sys_metadataoverlay merge "needs no change: overlay rows are unique ontype+name+organd carry no locale." The claim about the rows is true; the conclusion about the merge is not.sys_metadataoverlay uniquenessidx_sys_metadata_overlay_active=(type, name, organization_id, package_id); no locale column anywhere in the table — the premise, confirmedmergePackageAwareOverlaybucketspush(item.name, …)overbaseItems— the SchemaRegistry listing, which since #7730 is the whole bundle(bucket, package)⇒ a single overlay row for the type dropped a locale, and the survivor was the overlay body regardless of which member it customizesThe rows were never the problem; the base of that merge is. Reverting only the overlay half of this fix turns 3 of the 13 new cases red — see the table below.
The four keying sites, and what changed at each
Anchored by function name (
protocol.tsline anchors drift within hours — the issue's:4363/:4377were still accurate at dispatch time, but the file moved again mid-task).getMetaItems)itemMaploopsmetaItemKey(pkg, name); second member'ssetoverwrote the firstmetaItemKey(pkg, name, itemDiscriminator(type, entry))mergePackageAwareOverlay— active overlaysnamename(+ NUL + discriminator when declared)mergePackageAwareOverlay— draft previewnamemetaItemKey(r.package_id, r.name)+ storedRowDiscriminator(type, r), read off the row's serialized bodyThe last one is the widening the failed premise forces. Within one org it changes nothing — the store's unique index cannot hold two rows differing only by body locale. Across the env-wide and org tiers it can, and keying them together made an org's zh-CN row silently displace an env-wide en-US one. Org-over-env precedence is unchanged where it was ever meaningful (a case pins that an org row still overrides the env-wide row of the same member). Reading the discriminator off the raw stored body rather than the converted one is safe here and checked: no ADR-0087 conversion entry touches
email_template.Where the table lives — and what deliberately did not move
ITEM_KEY_DISCRIMINATORSmoves from@objectstack/objectql'sregistry.tsto@objectstack/metadata-core.@objectstack/objectqldepends on@objectstack/metadata-protocol, so the protocol package cannot import the registry's copy without closing a cycle turbo rejects; metadata-core is the package both already depend on and depends on neither — the criterion that sank the engine write-verb dispatch predicates (#5619), the audit-field governance table (#4513) and the injected-system-column definitions (#6562) there. Measured:metadata-coredepends on only@objectstack/spec+zod.No public surface changes.
registry.tsre-exportsITEM_KEY_DISCRIMINATORSunder its original name from its original module, soregistry-i18n-bundle-key.test.ts'simport { SchemaRegistry, ITEM_KEY_DISCRIMINATORS } from './registry'keeps working — itsObject.keys(...)).toEqual(['email_template'])pin included.metadata-coregainsreadDiscriminatorValue/itemDiscriminatoras additive exports.Not moved: the registry's storage-key format —
BUNDLE_KEY_SEPARATOR,withDiscriminator,bundleBaseKey,collectBundle. Those encode a discriminator into the registry's own compositepackageId:nameMap keys and parse it back; the protocol layer builds its own NUL-separated merge key and never parses a registry key, so it needs the identity question answered, not the encoding. Moving the encoding would have published a registry-internal key format as a cross-package contract. This is a deliberate half-move, and the TSDoc on both sides says so.Tests — the first coverage this path has ever had
git grep metaItemKeyacrosspackages/returned exactly one file before this PR:protocol.tsitself. New:packages/metadata-protocol/src/protocol.i18n-bundle-list-merge.test.ts, 13 cases.Both merge blocks are conditional — the MetadataService block runs only with a
metadataservice installed and answering non-empty for the type; the overlay block only with at least one matchingsys_metadatarow. A harness that omits either passes against the bug, so every case installs the precondition it targets, and one case (serves the bundle when NEITHER merge block runs) exists precisely to show the shape of harness that made the defect invisible.Reverse verification
Committed the fix first (
dd64b71), then re-introduced the undiscriminated key on top of it. Direction predicted before running: bundle cases red, undiscriminated guards green.metaItemKeyonlypage/homeSample red,
revert both:Three cases stay green under the full revert, and that is honest rather than a hole:
sibling in another packageis separated by ADR-0048's package dimension, which the revert does not touch;org overrides env for the SAME localeandno-locale member is canonicalassert that the fix did not split things it shouldn't. Every undiscriminated guard staying green under the revert is the evidence that non-bundled keys are byte-identical — a guard that went red would mean this change altered a key it promised not to.Gates
Run twice. First on
dd64b71+21fc024; thenorigin/mainmoved 11 commits — includingpackages/metadata-protocol/src/protocol.ts(#7906's$icontainsfix) and a 345-line rewrite ofscripts/check-type-check-coverage.mjs(#7921) — so the full set was re-run on the merge commit3319a62rather than a scoped subset. Numbers below are the post-merge run.@objectstack/metadata-coretest@objectstack/metadata-protocoltest@objectstack/objectqltestregistry-i18n-bundle-key.test.tsand itsITEM_KEY_DISCRIMINATORSby-name pin@objectstack/runtimetest@objectstack/clitesttypecheck— metadata-core, objectql, runtime, clitypecheckscript; it is a measured DEBT entry, covered by the ratchet below)check:type-check-coveragecheck:type-check-debt(--re-measure)Partial<Row> & { metadata: unknown }collapsing tostring); repaired in21fc024rather than raising the ledgereslinton changed filescheck:i18ncheck:nul-bytespnpm --filter @objectstack/spec check:generatedpnpm buildOut of scope, recorded not fixed
duplicatePackage's source-row scan dedupes its env-wide ⊕ org rows by(type, name)with the same locale-blind key. Different feature, different endpoint, outside this PR's declared surface — filed unassigned as #7932 rather than bundled here.#7491 (stale
AUTHORING_RULEScomment counts) is unaffected in substance and moved in position: its anchor was already atprotocol.ts:2698onorigin/main(not the:2632the card records), and this PR pushes it further down. The comment still reads "26" against 38 rules actually declared inpackages/lint/src/authoring-rules.ts. Not touched here.Generated by Claude Code