Skip to content

Commit abcf853

Browse files
os-zhuangclaude
andauthored
docs(spec): FieldSchema points a bare currency key at currencyConfig (#8163) (#9088)
`currency` has never been a declared FieldSchema key -- only `currencyConfig` is -- but the rejection carried only the surface history line, with no pointer to the declarable form. Adds a `guidance` prose entry (the same `storageNotNull`-style case already on this surface, not an `aliases` rename -- the target is a NESTED key a flat rename cannot express): `currency` is not a field key; a fixed currency is declared as `currencyConfig: { currencyMode: 'fixed', defaultCurrency: '...' }`. A field without one uses the tenant default at runtime. Accept/reject is byte-for-byte unchanged -- `currency` was rejected before this change and stays rejected after it. Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9bf110f commit abcf853

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): `FieldSchema` points a bare `currency` key at the declarable `currencyConfig` form (#8163)
6+
7+
`currency` has never been a declared `FieldSchema` key — only `currencyConfig`
8+
is. Writing the natural spelling was always a loud parse error, but a **bare**
9+
one: the rejection carried only the surface history line ("Until #4001 closed
10+
this shape these were dropped silently…"), with no pointer to the declarable
11+
form. The spelling is not hypothetical — objectui's `resolveFieldCurrency`
12+
reads `field.currency` first from looser grid/column configs, so it circulates
13+
in configs an AI author will have seen.
14+
15+
The target is a NESTED key (`currencyConfig.defaultCurrency` under
16+
`currencyMode: 'fixed'`), which a flat `aliases` rename cannot express — so
17+
this is prose (`guidance`), the same `storageNotNull`-style case already on
18+
this surface: `currency` is not a field key; a fixed currency is declared as
19+
`currencyConfig: { currencyMode: 'fixed', defaultCurrency: '…' }`. A field
20+
without one uses the tenant default at runtime.
21+
22+
Accept/reject is byte-for-byte unchanged — `currency` was rejected before this
23+
change and stays rejected after it; only the rejection's message gains a
24+
prescription.

packages/spec/src/data/field.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,59 @@ describe('CurrencyValueSchema', () => {
168168
});
169169
});
170170

171+
// ===========================================================================
172+
// #8163 — field-level `currency` is rejected with no pointer to the
173+
// declarable nested form (`currencyConfig.defaultCurrency`). Same
174+
// `unrecognized_keys`-probing pattern as
175+
// `shared/visible-when-alias-guidance.test.ts`: this is prose (`guidance`),
176+
// not a rename (`aliases`) — the target is a NESTED key a flat rename cannot
177+
// express — and acceptance is byte-identical: `currency` was rejected before
178+
// this card and stays rejected after it.
179+
// ===========================================================================
180+
181+
/** The `unrecognized_keys` message for `value`, or a loud test failure. */
182+
function unrecognizedKeyMessage(value: unknown): string {
183+
const r = FieldSchema.safeParse(value);
184+
expect(r.success, `expected REJECTION, got a successful parse of ${JSON.stringify(value)}`).toBe(false);
185+
const issues = (r as { success: false; error: { issues: Array<{ code?: string; message?: string }> } }).error.issues;
186+
const hit = issues.find((i) => i.code === 'unrecognized_keys');
187+
expect(hit, `no \`unrecognized_keys\` issue in ${JSON.stringify(issues)}`).toBeDefined();
188+
return hit?.message ?? '';
189+
}
190+
191+
describe('FieldSchema — field-level `currency` key guidance (#8163)', () => {
192+
const FIELD = { name: 'amount', label: 'Amount', type: 'currency' } as const;
193+
194+
it('`currency` stays rejected — a prescription is not an acceptance', () => {
195+
expect(FieldSchema.safeParse({ ...FIELD, currency: 'JPY' }).success).toBe(false);
196+
});
197+
198+
it('names the declarable nested form, `currencyConfig` / `defaultCurrency`', () => {
199+
const m = unrecognizedKeyMessage({ ...FIELD, currency: 'JPY' });
200+
expect(m).toContain('`currency` is not a field key');
201+
expect(m).toContain('currencyConfig');
202+
expect(m).toContain('defaultCurrency');
203+
// The claim the prose makes is real — the nested form it names really parses.
204+
expect(
205+
FieldSchema.safeParse({
206+
...FIELD,
207+
currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'JPY' },
208+
}).success,
209+
).toBe(true);
210+
});
211+
212+
it('is prose, not a rename — no `Did you mean` for `currency` (the target is a nested key)', () => {
213+
const m = unrecognizedKeyMessage({ ...FIELD, currency: 'JPY' });
214+
expect(m).not.toContain('Did you mean');
215+
});
216+
217+
it('a sibling unknown key does NOT get the currency prescription (anti-vacuity)', () => {
218+
const m = unrecognizedKeyMessage({ ...FIELD, totallyBogusKey: true });
219+
expect(m).not.toContain('currencyConfig');
220+
expect(m).not.toContain('defaultCurrency');
221+
});
222+
});
223+
171224
describe('FieldSchema', () => {
172225
describe('Basic Field Properties', () => {
173226
it('should accept valid field with minimal properties', () => {

packages/spec/src/data/field.zod.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,18 @@ export const FieldSchema = lazySchema(() => strictObject({
573573
'`columnName` was removed in the 16.x line (#2377) — the SQL driver hardcodes the physical '
574574
+ 'column to the field key, so a custom name was ignored. External/federated objects map '
575575
+ 'physical columns with `external.columnMap` (ADR-0062 D7).',
576+
// `currency` is not, and has never been, a declared FieldSchema key — it is
577+
// not a retirement, just a natural spelling with no landing key of its own
578+
// (#8163). Prose rather than an `aliases` rename because the target is a
579+
// NESTED key: `currencyConfig.defaultCurrency` under `currencyMode: 'fixed'`,
580+
// which a flat rename cannot express. The spelling is not hypothetical —
581+
// objectui's `resolveFieldCurrency` reads `field.currency` first from looser
582+
// grid/column configs, so it circulates in configs an AI author will have
583+
// seen.
584+
currency:
585+
'`currency` is not a field key — a fixed currency is declared as `currencyConfig: '
586+
+ '{ currencyMode: \'fixed\', defaultCurrency: \'JPY\' }`. A field without one uses '
587+
+ 'the tenant default at runtime.',
576588
referenceFilters:
577589
'`referenceFilters` (string[]) was removed in the 16.x line (#2377) — the lookup picker only '
578590
+ 'ever read the structured form. Use `lookupFilters: [{ field, operator, value }]`.',

0 commit comments

Comments
 (0)