From 21306be7b4bdccf43624202bcb555313b41666d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 05:52:45 +0000 Subject: [PATCH 1/2] fix(core): the id_field no-declared-twin diagnostic carries the spec's published FIELD_KEY_GUIDANCE reason (objectui#7650) The leave arm's diagnostic for a stored `id_field` now appends `FIELD_KEY_GUIDANCE.id_field.why`, read unconditionally off the installed `@objectstack/spec/data` on the dev-only emitting path. No local copy of the prose and no optional read with a fallback (both refused on the card): a spec without the row throws in dev instead of warning with less, and production never reads it. The fold is unchanged: `id_field` is never folded, no `idField` read is added, and a typo gets no suggestion. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude --- .../7650-id-field-published-guidance.md | 19 ++++ ...reference-keys.retiredDialect-7650.test.ts | 106 +++++++++++++++++- packages/core/src/utils/reference-keys.ts | 45 +++++--- 3 files changed, 147 insertions(+), 23 deletions(-) create mode 100644 .changeset/7650-id-field-published-guidance.md diff --git a/.changeset/7650-id-field-published-guidance.md b/.changeset/7650-id-field-published-guidance.md new file mode 100644 index 0000000000..cc6630cf19 --- /dev/null +++ b/.changeset/7650-id-field-published-guidance.md @@ -0,0 +1,19 @@ +--- +'@object-ui/core': patch +--- + +The ingestion choke point's diagnostic for a stored `id_field` now carries the reason +`@objectstack/spec` publishes for that key (objectui#7650). + +`FieldSchema` declares no successor for `id_field`, so `normalizeSchemaReferenceKeys` +leaves the key exactly as served and, in dev, says it cannot canonicalize it (the +`no-declared-twin` diagnostic). That line now also carries `FIELD_KEY_GUIDANCE.id_field.why`, +read from the installed `@objectstack/spec/data`: the spec's own sentence on why the key has +no successor and what to author instead. objectui keeps no copy of that sentence, and the +read has no fallback: on a spec without the row, the dev-mode diagnostic throws instead of +printing less. Production never reads the row. + +Nothing else moves. `id_field` is still never folded and no `idField` is read; a typo such as +`sortible` still gets no suggestion; no accepted document, published type or served value +changes. No floor raise: `@object-ui/core` already declares `@objectstack/spec ^17.5.0`, and +17.5.0 is the first published release carrying the row (17.4.0 has none). diff --git a/packages/core/src/utils/__tests__/reference-keys.retiredDialect-7650.test.ts b/packages/core/src/utils/__tests__/reference-keys.retiredDialect-7650.test.ts index 39fe16b751..1e4cafea38 100644 --- a/packages/core/src/utils/__tests__/reference-keys.retiredDialect-7650.test.ts +++ b/packages/core/src/utils/__tests__/reference-keys.retiredDialect-7650.test.ts @@ -27,10 +27,11 @@ * * - `id_field` is NOT folded. It has no declared successor (`FieldSchema` has * no `idField`; the spec's only `idField` sits on `InlineGridColumnSchema`), - * and the ruling re-blocked that slice on a `@objectstack/spec` RELEASE - * carrying the `FIELD_KEY_GUIDANCE.id_field` row — which is in NO published - * version. The point of the derived rule is that this falls out of it for - * free rather than being written as a special case. + * and the spec's own `FIELD_KEY_GUIDANCE.id_field` row says so: a `why`, no + * `to`. The point of the derived rule is that the no-fold falls out of it + * for free rather than being written as a special case. The row reaches the + * DIAGNOSTIC only — pinned by the describe `the id_field diagnostic carries + * the PUBLISHED spec reason (objectui#7650)`. * - `sortible`, a pure TYPO, is NOT folded. The refused alternative was to * call the spec's `lintAuthoredRecordKeys` and fold on its `suggestion`: * that function falls through to a Levenshtein matcher when no `to` row @@ -64,7 +65,7 @@ * instead of the contract. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { FieldSchema } from '@objectstack/spec/data'; +import { FIELD_KEY_GUIDANCE, FieldSchema } from '@objectstack/spec/data'; import { normalizeFieldReferenceKeys, normalizeSchemaReferenceKeys, @@ -144,7 +145,7 @@ describe('retired-dialect canonicalization — the three unblocked keys (objectu }); describe('the NEGATIVE pins the ruling required (objectui#7650)', () => { - it('does NOT fold `id_field` — no declared successor, and the slice is blocked on a spec release', () => { + it('does NOT fold `id_field` — no declared successor', () => { const f: Record = plainField({ id_field: 'code' }); normalizeFieldReferenceKeys(f, 'owner', 'account'); expect(f.idField).toBeUndefined(); @@ -246,6 +247,99 @@ describe('the dev-mode warning (objectui#7650)', () => { }); }); +describe('the id_field diagnostic carries the PUBLISHED spec reason (objectui#7650)', () => { + // Every read of the row below is unconditional — no `?.` — and ⛔ no copy of + // its prose lives in this file: each pin reads the installed spec at run time. + + it('the installed row is a retirement — a `why`, and no `to`', () => { + const { why, to } = FIELD_KEY_GUIDANCE.id_field; + expect(typeof why).toBe('string'); + expect((why as string).trim().length).toBeGreaterThan(0); + // No `to`: the spec names no successor, which is why the fold leaves the key + // alone. A `to` appearing here reopens the route, not just this diagnostic. + expect(to).toBeUndefined(); + }); + + it('the `no-declared-twin` line for `id_field` carries that `why`, and the key is still not folded', () => { + const why = FIELD_KEY_GUIDANCE.id_field.why as string; + const f: Record = plainField({ id_field: 'code' }); + normalizeFieldReferenceKeys(f, 'owner', 'account'); + expect(f.idField).toBeUndefined(); + expect(f.id_field).toBe('code'); + expect(warn).toHaveBeenCalledTimes(1); + const message = String(warn.mock.calls[0]?.[0]); + expect(message).toContain('CANNOT'); + expect(message).toContain(why); + }); + + it('the reason is read off the spec module when the line is built — not from a copy', async () => { + // A local copy of the prose would pass the pin above verbatim. Substituting + // the row is what tells the two apart: the line must carry whatever the spec + // module publishes, and none of the installed sentence. + const installed = FIELD_KEY_GUIDANCE.id_field.why as string; + const substituted = 'SUBSTITUTED reason, published by a stand-in spec module.'; + vi.resetModules(); + vi.doMock('@objectstack/spec/data', async (importOriginal) => ({ + ...(await importOriginal()), + FIELD_KEY_GUIDANCE: { id_field: { why: substituted } }, + })); + try { + const fresh = await import('../reference-keys'); + fresh.normalizeFieldReferenceKeys(plainField({ id_field: 'code' }), 'owner', 'account'); + expect(warn).toHaveBeenCalledTimes(1); + const message = String(warn.mock.calls[0]?.[0]); + expect(message).toContain(substituted); + expect(message).not.toContain(installed); + } finally { + vi.doUnmock('@objectstack/spec/data'); + vi.resetModules(); + } + }); + + it('a spec WITHOUT the row fails loudly in dev instead of warning with less, and production never reads it', async () => { + vi.resetModules(); + vi.doMock('@objectstack/spec/data', async (importOriginal) => ({ + ...(await importOriginal()), + FIELD_KEY_GUIDANCE: {}, + })); + try { + const fresh = await import('../reference-keys'); + // ⛔ No optional read and no fallback sentence: what objectui#7650 refused + // is exactly a diagnostic that quietly degrades on an older spec. + const call = () => + fresh.normalizeFieldReferenceKeys(plainField({ id_field: 'code' }), 'owner', 'account'); + expect(call).toThrow(TypeError); + // The throw comes before the memo is taken, so it does not go quiet on + // the second read of the same def. + expect(call).toThrow(TypeError); + expect(warn).not.toHaveBeenCalled(); + // CONTROL for the two throws above, on the same stand-in module: the read + // sits on the dev-only emitting path, so production never reaches it. + vi.stubEnv('NODE_ENV', 'production'); + const f: Record = plainField({ id_field: 'code' }); + expect(() => fresh.normalizeFieldReferenceKeys(f, 'owner', 'account')).not.toThrow(); + expect(f.id_field).toBe('code'); + } finally { + vi.unstubAllEnvs(); + vi.doUnmock('@objectstack/spec/data'); + vi.resetModules(); + } + }); + + it('a TYPO still gets no suggestion — and not the `id_field` reason either', () => { + const why = FIELD_KEY_GUIDANCE.id_field.why as string; + const f: Record = plainField({ sortible: true }); + normalizeFieldReferenceKeys(f, 'owner', 'account'); + expect(f.sortable).toBeUndefined(); + expect(f.sortible).toBe(true); + expect(warn).toHaveBeenCalledTimes(1); + const message = String(warn.mock.calls[0]?.[0]); + expect(message).toContain('sortible'); + expect(message).not.toContain('sortable'); + expect(message).not.toContain(why); + }); +}); + describe('derives the fold from the contract, not from a table (objectui#7650)', () => { const declared = Object.keys(FieldSchema.shape as Record); diff --git a/packages/core/src/utils/reference-keys.ts b/packages/core/src/utils/reference-keys.ts index 9878aaabf7..8347e30f9e 100644 --- a/packages/core/src/utils/reference-keys.ts +++ b/packages/core/src/utils/reference-keys.ts @@ -14,7 +14,7 @@ * here from a grep. */ -import { FieldSchema } from '@objectstack/spec/data'; +import { FIELD_KEY_GUIDANCE, FieldSchema } from '@objectstack/spec/data'; /** * Backend object schemas follow the ObjectStack convention and name a @@ -161,20 +161,20 @@ import { FieldSchema } from '@objectstack/spec/data'; * typed`, which drives every snake twin the live spec implies through this * choke point. * - * ⛔ The `id_field` slice is NOT TAKEN, and not on a card. `@objectstack/spec`'s - * `FIELD_KEY_GUIDANCE` grew an `id_field` row explaining why the key has no - * successor. When this was written the row was in no published version — - * measured then against 17.3.0 and 17.4.0, both zero occurrences, with - * `startingNumber` as the lit control in the same read — so what was missing - * was a RELEASE, not a decision. The release has shipped: the installed 17.5.0 - * carries the row (objectui#11073 re-measured it: one `id_field:` guidance entry - * in `dist/data`). Quoting that sentence from a local copy here was refused (a - * second copy of contract prose is the drift AGENTS.md #0.1 exists to stop) and - * so was reading it optionally with a local fallback (an invisible fallback - * that silently degrades on an older spec is this card's own defect class); - * neither refusal moves. Reading the published row is now possible and is its - * own change — the bump that made it possible did not take it, so `id_field` - * is still left alone here. + * ## `id_field`: never folded, and the diagnostic carries the spec's own reason + * + * `@objectstack/spec`'s `FIELD_KEY_GUIDANCE` publishes an `id_field` row with a + * `why` and no `to`: a retirement, not a rename. So `id_field` is still never + * folded and nothing here reads an `idField`. What the row changes is the + * `no-declared-twin` diagnostic, which appends that `why`, read off the + * installed spec when the warning is emitted ({@link warnUnfoldableRetiredKey}; + * objectui#7650). Two other ways of getting the sentence there were refused on + * the card, and neither refusal moves: quoting it from a local copy (a second + * copy of contract prose is the drift AGENTS.md #0.1 exists to stop), and + * reading it optionally with a local fallback (an invisible fallback that + * silently degrades on an older spec is this card's own defect class). The read + * is therefore unconditional: on a spec without the row it throws in dev rather + * than printing less. * * ## What this arm deliberately does NOT do * @@ -276,7 +276,12 @@ type UnfoldableReason = 'no-declared-twin' | 'ambiguous-probe' | 'canonical-occu * ⛔ the diagnostic deliberately does NOT offer a near match, for the same * reason the fold does not — the spec's `lintAuthoredRecordKeys` falls * through to a Levenshtein matcher, and a serve path that suggests a - * correction is one revision away from applying it. + * correction is one revision away from applying it. For `id_field` alone + * the line also carries the reason the spec publishes for that exact + * spelling, `FIELD_KEY_GUIDANCE.id_field.why`, read unconditionally where + * the line is built: the contract's own sentence about the key, not a near + * match this path computed. The spec keys that row snake_case on purpose — + * a `why` row matches only the exact authored spelling. * - `ambiguous-probe` — two or more declared keys share the probe, so the * fold refuses to choose. Unreachable against a spec with no collision; * the pin that exercises it substitutes a colliding `FieldSchema`. @@ -314,10 +319,16 @@ function warnUnfoldableRetiredKey( const owner = objectName ?? '(unknown object)'; const memo = `${owner}:${named}:${key}:${reason}`; if (warnedUnfoldableSpelling.has(memo)) return; + // ⛔ No `?.` and no `??` here: a spec without the row throws before the memo + // is taken, rather than warning with less (objectui#7650). + const published = + reason === 'no-declared-twin' && key === 'id_field' + ? ` \`@objectstack/spec\`'s \`FIELD_KEY_GUIDANCE\` gives the reason: ${FIELD_KEY_GUIDANCE.id_field.why}` + : ''; warnedUnfoldableSpelling.add(memo); console.warn( `[ObjectUI] Object \`${owner}\`, field \`${named}\`: the ingestion choke point CANNOT ` + - `canonicalize the retired spelling \`${key}\`. ${detail} The key and its value are LEFT ` + + `canonicalize the retired spelling \`${key}\`. ${detail}${published} The key and its value are LEFT ` + `on the def exactly as served — nothing is dropped here — but the consumers read only the ` + `spelling \`@objectstack/spec\`'s \`FieldSchema\` declares, so this value reaches no ` + `reader. Fix the PRODUCER, or migrate the stored document. (maintainer ruling item 3 on ` + From a71744d4fcde0691bd32a22bf0776f338a3fad63 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 06:29:25 +0000 Subject: [PATCH 2/2] fix(core): the unfoldable-key diagnostic no longer claims no reader sees the value (objectui#7650) Round 2 text correction, words only. The no-declared-twin / occupied / ambiguous line closed on "so this value reaches no reader", which is false where a retired spelling is still read on purpose (resolveActionParam reads id_field and title_format, kept by objectui#7435; resolveGroupByLabels reads id_field). It now says a consumer that reads only the spellings FieldSchema declares will not see the value. The module docblock names those kept readers; the pending 8938 changeset gets one appended, dated correction note (no existing line edited); this PR's own changeset gets one sentence. No read, no fold and no id_field reason logic changes. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude --- .../7650-id-field-published-guidance.md | 5 ++ .../8938-retired-dialect-measured-surface.md | 11 +++++ ...reference-keys.retiredDialect-7650.test.ts | 38 ++++++++++++++- packages/core/src/utils/reference-keys.ts | 47 +++++++++++++------ 4 files changed, 85 insertions(+), 16 deletions(-) diff --git a/.changeset/7650-id-field-published-guidance.md b/.changeset/7650-id-field-published-guidance.md index cc6630cf19..338e6cc4ba 100644 --- a/.changeset/7650-id-field-published-guidance.md +++ b/.changeset/7650-id-field-published-guidance.md @@ -17,3 +17,8 @@ Nothing else moves. `id_field` is still never folded and no `idField` is read; a `sortible` still gets no suggestion; no accepted document, published type or served value changes. No floor raise: `@object-ui/core` already declares `@objectstack/spec ^17.5.0`, and 17.5.0 is the first published release carrying the row (17.4.0 has none). + +The line's closing clause is corrected for every refusal: it said the value "reaches no reader", +which is false where a retired spelling is still read on purpose (`resolveActionParam` reads +`id_field`), and it now says that a consumer reading only the spellings `FieldSchema` declares +will not see the value. diff --git a/.changeset/8938-retired-dialect-measured-surface.md b/.changeset/8938-retired-dialect-measured-surface.md index 08d9b1315b..2f4466a7f4 100644 --- a/.changeset/8938-retired-dialect-measured-surface.md +++ b/.changeset/8938-retired-dialect-measured-surface.md @@ -37,3 +37,14 @@ the intended accept set is an open decision on objectui#7650, untouched here. Landed with the width pin objectui#8938 asked for, which drives every snake twin the linked `@objectstack/spec` implies through the public choke point, and with the correction to the objectui#7650 changeset that presented a handful of keys as the accepted set. + +**Correction, 2026-09-30 (objectui#7650).** Two sentences above claim more than holds. "A key +that does not reaches none of them" is true only of the consumers narrowed to the canonical +spelling: some reads of a retired spelling were kept on purpose, so a value the choke point +leaves alone can still reach a reader — `resolveActionParam` in `@object-ui/app-shell` reads +`id_field` and `title_format` (kept by objectui#7435), and `resolveGroupByLabels` in +`@object-ui/plugin-charts` reads `id_field`. Likewise, "the retired value is inert" holds only +for consumers of the declared spelling (`deriveColumns` in `@object-ui/plugin-form` reads +`display_field`). The diagnostic line now claims only what holds for every refusal: a consumer +that reads only the spellings `FieldSchema` declares will not see the value. No read changes, +and nothing about which keys fold changes. diff --git a/packages/core/src/utils/__tests__/reference-keys.retiredDialect-7650.test.ts b/packages/core/src/utils/__tests__/reference-keys.retiredDialect-7650.test.ts index 1e4cafea38..e5fa486f54 100644 --- a/packages/core/src/utils/__tests__/reference-keys.retiredDialect-7650.test.ts +++ b/packages/core/src/utils/__tests__/reference-keys.retiredDialect-7650.test.ts @@ -194,8 +194,9 @@ describe('the NEGATIVE pins the ruling required (objectui#7650)', () => { expect(f.display_field).toBe('legacy_name'); // objectui#8938: was `not.toHaveBeenCalled()`. The producer's value still // stands — that is what the two assertions above pin, and it is unchanged — - // but the retired spelling beside it reaches no consumer, and that was the - // third silent refusal. The diagnostic names the occupied canonical key; + // but the retired spelling beside it reaches no consumer of the declared + // spelling (a kept snake read can still see it — objectui#7650), and that + // was the third silent refusal. The diagnostic names the occupied canonical key; // the behaviour here is untouched. expect(warn).toHaveBeenCalledTimes(1); expect(String(warn.mock.calls[0]?.[0])).toContain('CANNOT'); @@ -326,6 +327,39 @@ describe('the id_field diagnostic carries the PUBLISHED spec reason (objectui#76 } }); + it('the `id_field` line never claims NO reader sees the value — kept snake reads do', () => { + // `resolveActionParam` (app-shell) reads `id_field` on purpose — objectui#7435 + // kept it — and `resolveGroupByLabels` (plugin-charts) reads it too. So the + // line may only speak for the consumers of the declared spellings. + normalizeFieldReferenceKeys(plainField({ id_field: 'code' }), 'owner', 'account'); + expect(warn).toHaveBeenCalledTimes(1); + const message = String(warn.mock.calls[0]?.[0]); + expect(message).not.toContain('reaches no reader'); + // LIT, same line: it still closes on a claim about readers, so the negative + // above is not satisfied by a line that merely lost its closing clause. + expect(message).toContain('a consumer that reads only the spellings'); + expect(message).toContain('will not see this value'); + }); + + it('CONTROL — the scoped claim is kept where it is true, beside the fact it states', () => { + // Occupied canonical: the declared spelling carries the PRODUCER's value, so + // a consumer of the declared spelling really does not see the retired one. + const occupied: Record = plainField({ + display_field: 'legacy_name', + displayField: 'canonical_name', + }); + normalizeFieldReferenceKeys(occupied, 'owner', 'account'); + expect(occupied.displayField).toBe('canonical_name'); + // A typo: no declared spelling carries it at all. + const typo: Record = plainField({ sortible: true }); + normalizeFieldReferenceKeys(typo, 'owner', 'account'); + expect(typo.sortable).toBeUndefined(); + expect(warn).toHaveBeenCalledTimes(2); + for (const call of warn.mock.calls) { + expect(String(call[0])).toContain('will not see this value'); + } + }); + it('a TYPO still gets no suggestion — and not the `id_field` reason either', () => { const why = FIELD_KEY_GUIDANCE.id_field.why as string; const f: Record = plainField({ sortible: true }); diff --git a/packages/core/src/utils/reference-keys.ts b/packages/core/src/utils/reference-keys.ts index 8347e30f9e..5cc832c5b3 100644 --- a/packages/core/src/utils/reference-keys.ts +++ b/packages/core/src/utils/reference-keys.ts @@ -145,8 +145,10 @@ import { FIELD_KEY_GUIDANCE, FieldSchema } from '@objectstack/spec/data'; * canonical, and silent data loss on a serve path is the worst of the shapes. * * ⭐ The leave arm is no longer SILENT, which is the other half of that - * sentence: the document keeps the value, and the consumers — narrowed to the - * canonical spelling by objectui#7155 / #7166 / #7435 — read none of it. See + * sentence: the document keeps the value, and the consumers narrowed to the + * canonical spelling by objectui#7155 / #7166 / #7435 read none of it (not + * every consumer was narrowed — the kept retired-spelling readers are named in + * {@link warnUnfoldableRetiredKey}'s docblock). See * {@link warnUnfoldableRetiredKey} (objectui#8938) for the diagnostic maintainer * ruling item 3 asked for, and for the two cases it deliberately stays out of. * @@ -259,14 +261,29 @@ type UnfoldableReason = 'no-declared-twin' | 'ambiguous-probe' | 'canonical-occu * point cannot fold". The fold arm shipped with a warning for the spelling it * CAN fold ({@link canonicalizeRetiredFieldKeys}) and nothing for the one it * cannot, which is the half the ruling named: a key that folds still reaches - * every consumer, while a key that does not fold reaches none of them. + * every consumer, while a key that does not fold reaches none of the consumers + * of the declared spellings. * * ⛔ "Not a silent DROP" is about the reader, not about the document. Nothing * here removes a key or a value — the leave arm is lossless and stays lossless. - * What is lost is the READ: the retirement cards in this family (objectui#7155, - * #7166, #7435) narrowed the consumers to the canonical spelling, so a value - * that never reaches a canonical key is a value nothing reads. That is the - * silence this makes audible. + * What is lost is the READ by those consumers: the retirement cards in this + * family (objectui#7155, #7166, #7435) narrowed them to the canonical spelling, + * so a value that never reaches a canonical key is a value none of them reads. + * That is the silence this makes audible. + * + * ⚠️ Not NO reader, and the line never says so (objectui#7650). Some reads of a + * retired spelling were KEPT on purpose and still see a value this arm leaves + * alone. For the two `no-declared-twin` keys: `resolveActionParam` in + * `@object-ui/app-shell` reads `id_field` and `title_format` (objectui#7435 + * kept them; its pin `resolveActionParams.declaredLookupLegs-7435.test.ts` + * calls them the snake spellings that must keep working), and + * `resolveGroupByLabels` in `@object-ui/plugin-charts` reads + * `fieldDef.id_field || 'id'`. Those two are named, not counted: other kept + * snake reads exist (`deriveColumns` in `@object-ui/plugin-form` reads + * `display_field`, for one), and nothing re-derives a complete list. So the + * line claims only what holds for every refusal: a consumer that reads only + * declared spellings will not see the value. Retiring any of those reads is a + * separate decision, not taken here. * * ## The three refusals, and why each is a different sentence * @@ -287,10 +304,12 @@ type UnfoldableReason = 'no-declared-twin' | 'ambiguous-probe' | 'canonical-occu * the pin that exercises it substitutes a colliding `FieldSchema`. * - `canonical-occupied` — the declared twin is already on the def carrying a * DIFFERENT value, and the fold never overwrites a value the producer set. - * The producer's value is served, the retired one is inert, and NOTHING said - * so before this. ⛔ Same value under both spellings is NOT this case: that - * is the state this pass leaves behind on its own second run, and reporting - * it would make every re-normalization of a correctly folded def noisy. + * The producer's value is served under the declared key, the retired one + * reaches no consumer of the declared spelling (a kept snake read can still + * see it — see above), and NOTHING said so before this. ⛔ Same value under + * both spellings is NOT this case: that is the state this pass leaves + * behind on its own second run, and reporting it would make every + * re-normalization of a correctly folded def noisy. * * ## ⛔ What it does NOT fire on — the two exclusions that keep it honest * @@ -329,9 +348,9 @@ function warnUnfoldableRetiredKey( console.warn( `[ObjectUI] Object \`${owner}\`, field \`${named}\`: the ingestion choke point CANNOT ` + `canonicalize the retired spelling \`${key}\`. ${detail}${published} The key and its value are LEFT ` + - `on the def exactly as served — nothing is dropped here — but the consumers read only the ` + - `spelling \`@objectstack/spec\`'s \`FieldSchema\` declares, so this value reaches no ` + - `reader. Fix the PRODUCER, or migrate the stored document. (maintainer ruling item 3 on ` + + `on the def exactly as served — nothing is dropped here — but a consumer that reads only ` + + `the spellings \`@objectstack/spec\`'s \`FieldSchema\` declares will not see this value. ` + + `Fix the PRODUCER, or migrate the stored document. (maintainer ruling item 3 on ` + `objectui#7650; objectui#8938)`, ); }