From 478f490fddb8c334cf4b8916f3363e62da6f27e4 Mon Sep 17 00:00:00 2001 From: os-warren Date: Fri, 4 Sep 2026 14:56:45 +0000 Subject: [PATCH 1/5] fix(service-settings): re-point the settings door onto the shared value-domain predicate and refuse with `value_domain` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The services half of the maintainer's ruling A (2026-09-02): one closed vocabulary and one membership predicate shared by settings specifiers and object fields. The spec half landed the shared module; this deletes the door's second copy of all three definitions and re-points onto it. - `value-domains.ts` keeps only what is the DOOR's — which declarations it enforces, how a multi-value carrier is walked, and the env log line's prose. The `Intl.DateTimeFormat` probe, the run-time `Intl.supportedValuesOf('currency')` set and the 249 alpha-2 codes are gone; `isValueDomainMember` answers instead. - `knownValueDomain` filters with the closed enum's own `safeParse`, which makes the prototype-chain guard structural rather than remembered. - The save door's refusal code becomes `value_domain` (ADR-0114: the code is the constraint's own name) and its message renders the published catalog template, the same catalog the record write path renders. - A ratchet pin so a re-added membership table in this package goes red. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y --- .../src/settings-env-pattern.test.ts | 2 +- .../src/settings-routes.test.ts | 8 +- .../src/settings-service.test.ts | 29 +-- .../service-settings/src/settings-service.ts | 53 +++-- ...value-domains.shared-predicate.pin.test.ts | 102 +++++++++ .../src/value-domains.test.ts | 116 ++++++++-- .../service-settings/src/value-domains.ts | 202 ++++++------------ packages/spec/liveness/field.json | 2 +- 8 files changed, 332 insertions(+), 182 deletions(-) create mode 100644 packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts diff --git a/packages/services/service-settings/src/settings-env-pattern.test.ts b/packages/services/service-settings/src/settings-env-pattern.test.ts index 27cae4ac98..3125dd356d 100644 --- a/packages/services/service-settings/src/settings-env-pattern.test.ts +++ b/packages/services/service-settings/src/settings-env-pattern.test.ts @@ -282,7 +282,7 @@ describe('family ordering agrees between doors: options → pattern → valueDom svc.registerManifest(orderingManifest); await expect(svc.setMany('pattern_order', { country_like: 'ZZ' })).rejects.toMatchObject({ code: 'SETTINGS_VALIDATION', - fields: [{ field: 'country_like', code: 'invalid_value', constraint: { valueDomain: 'iso_3166_alpha2' } }], + fields: [{ field: 'country_like', code: 'value_domain', constraint: { valueDomain: 'iso_3166_alpha2' } }], }); const { errors, logger: envLogger } = spyLogger(); diff --git a/packages/services/service-settings/src/settings-routes.test.ts b/packages/services/service-settings/src/settings-routes.test.ts index f599c8d20a..353bbcdae0 100644 --- a/packages/services/service-settings/src/settings-routes.test.ts +++ b/packages/services/service-settings/src/settings-routes.test.ts @@ -243,7 +243,7 @@ describe('settings-routes', () => { expect(state.body.data.values.currency.value).toBe('CHF'); }); - it('PUT /api/settings/localization rejects garbage with 400 + SETTINGS_VALIDATION + invalid_value', async () => { + it('PUT /api/settings/localization rejects garbage with 400 + SETTINGS_VALIDATION + value_domain', async () => { const http = new MockHttp(); const svc = new SettingsService({ env: {} }); svc.registerManifest(localizationSettingsManifest); @@ -260,7 +260,11 @@ describe('settings-routes', () => { expect(state.body.error.details.fields).toEqual([ expect.objectContaining({ field: 'timezone', - code: 'invalid_value', + // The wire-visible half of the re-point onto the shared predicate: + // this refusal answered `invalid_value` until the catalog gained a + // member named for the constraint itself (ADR-0114, maintainer ruling + // 2026-09-02). A client branching on the code sees `value_domain`. + code: 'value_domain', constraint: { valueDomain: 'iana_time_zone' }, value: 'Mars/Olympus', }), diff --git a/packages/services/service-settings/src/settings-service.test.ts b/packages/services/service-settings/src/settings-service.test.ts index 764dc84c71..22ef6ba0e1 100644 --- a/packages/services/service-settings/src/settings-service.test.ts +++ b/packages/services/service-settings/src/settings-service.test.ts @@ -2096,12 +2096,15 @@ describe('SettingsService — a declared valueDomain is the save-time boundary ( fields: [ { field: 'timezone', - // No `FieldErrorCode` member names a standard-domain breach, so it - // takes `invalid_value` — the catalog's slot for "rejected for a - // reason no other member names" (ADR-0114), the #6199 precedent. - // NOT `invalid_option`: the declared options are exactly the list - // a domain-bearing value may legitimately be outside of. - code: 'invalid_value', + // `value_domain` — ADR-0114's rule is that the code is the + // constraint's own name, as `max_length` names the bound it + // breached. This branch answered `invalid_value` (the catalog's + // slot for "rejected for a reason no other member names") only + // while no member named a standard-domain breach; the field-level + // card's spec half added one, and the settings door adopts it. + // Still NOT `invalid_option`: the declared options are exactly the + // list a domain-bearing value may legitimately be outside of. + code: 'value_domain', label: 'Default timezone', constraint: { valueDomain: 'iana_time_zone' }, value: tz, @@ -2112,7 +2115,7 @@ describe('SettingsService — a declared valueDomain is the save-time boundary ( await expect(svc.setMany('localization', { currency: 'XYZ' })).rejects.toMatchObject({ code: 'SETTINGS_VALIDATION', fields: [ - { field: 'currency', code: 'invalid_value', constraint: { valueDomain: 'iso_4217_currency' } }, + { field: 'currency', code: 'value_domain', constraint: { valueDomain: 'iso_4217_currency' } }, ], }); // Atomic: nothing landed. @@ -2128,7 +2131,7 @@ describe('SettingsService — a declared valueDomain is the save-time boundary ( for (const cc of ['ZZ', 'UK']) { await expect(svc.setMany('localization', { default_country: cc })).rejects.toMatchObject({ fields: [ - { field: 'default_country', code: 'invalid_value', constraint: { valueDomain: 'iso_3166_alpha2' } }, + { field: 'default_country', code: 'value_domain', constraint: { valueDomain: 'iso_3166_alpha2' } }, ], }); } @@ -2194,7 +2197,7 @@ describe('SettingsService — a declared valueDomain is the save-time boundary ( } as any); await expect(svc.setMany('multi', { currencies: ['USD', 'CHF'] })).resolves.toBeDefined(); await expect(svc.setMany('multi', { currencies: ['USD', 'XYZ'] })).rejects.toMatchObject({ - fields: [{ field: 'currencies', code: 'invalid_value', value: 'XYZ' }], + fields: [{ field: 'currencies', code: 'value_domain', value: 'XYZ' }], }); }); @@ -2211,7 +2214,7 @@ describe('SettingsService — a declared valueDomain is the save-time boundary ( } as any); const err = await svc.setMany('vaultdom', { region_code: 'ZZ' }).catch((e) => e); expect(err.code).toBe('SETTINGS_VALIDATION'); - expect(err.fields[0]).toMatchObject({ field: 'region_code', code: 'invalid_value' }); + expect(err.fields[0]).toMatchObject({ field: 'region_code', code: 'value_domain' }); expect(err.fields[0].value).toBeUndefined(); expect(err.message).not.toContain('ZZ'); // The domain still travels, so the caller learns what to do. @@ -2240,7 +2243,7 @@ describe('SettingsService — a declared valueDomain is the save-time boundary ( await expect(svc.setMany('localization', { currency: 'CHF' })).resolves.toBeDefined(); // Only re-writing the key itself is refused. await expect(svc.setMany('localization', { timezone: 'Mars/Olympus' })).rejects.toMatchObject({ - fields: [{ field: 'timezone', code: 'invalid_value' }], + fields: [{ field: 'timezone', code: 'value_domain' }], }); }); }); @@ -2381,7 +2384,7 @@ describe('SettingsService — company.country adopts iso_3166_alpha2 (#6579)', ( fields: [ { field: 'country', - code: 'invalid_value', + code: 'value_domain', label: 'Country', constraint: { valueDomain: 'iso_3166_alpha2' }, value: cc, @@ -2409,7 +2412,7 @@ describe('SettingsService — company.country adopts iso_3166_alpha2 (#6579)', ( const svc = companyService(); await expect(svc.setMany('company', { country: 'us' })).rejects.toMatchObject({ fields: [ - { field: 'country', code: 'invalid_value', constraint: { valueDomain: 'iso_3166_alpha2' } }, + { field: 'country', code: 'value_domain', constraint: { valueDomain: 'iso_3166_alpha2' } }, ], }); }); diff --git a/packages/services/service-settings/src/settings-service.ts b/packages/services/service-settings/src/settings-service.ts index 440d0490ff..15e807fa15 100644 --- a/packages/services/service-settings/src/settings-service.ts +++ b/packages/services/service-settings/src/settings-service.ts @@ -33,6 +33,12 @@ import { UnknownKeyError, UnknownNamespaceError, } from './settings-service.types.js'; +// The published field-message catalog (ADR-0114). Rendered for the +// `value_domain` refusal so the settings door and the record write path say +// one thing about one domain; every other refusal here still carries its own +// hand-written sentence, unchanged. +import { renderValidationMessage } from '@objectstack/spec/system'; +import { SETTINGS_SECRET_MASK } from './settings-secret-redaction.js'; import { firstRejectedDomainMember, knownValueDomain, @@ -1810,7 +1816,7 @@ export class SettingsService { * the declared table → rejected (`invalid_option`) — unless the specifier * declares a `valueDomain`, which moves the boundary (next bullet). * - `valueDomain` (#5712) + non-empty value that is not a member of the - * declared standard → rejected (`invalid_value`). The domain REPLACES the + * declared standard → rejected (`value_domain`). The domain REPLACES the * option table as the membership boundary: `options` degrades to a UI * convenience list, so a value outside `options` but inside the domain is * accepted. Judged AFTER `pattern` — shape and membership narrow @@ -2050,27 +2056,50 @@ export class SettingsService { // shape-valid, and the question is purely whether the standard's // membership admits it (`Mars/Olympus` is a shape-valid time zone that // does not exist; `ZZ` matches `^[A-Za-z]{2}$` and is assigned to - // nobody). No `FieldErrorCode` member names a standard-domain breach, so - // it takes `invalid_value` — the catalog's declared slot for "rejected - // for a reason no other member names" (ADR-0114), the same verdict the - // step grid reached in #6199. `invalid_option` would be a lie about - // which set was consulted: the declared options are exactly the list a - // domain-bearing value may legitimately be outside of. + // nobody). + // + // The refusal code is `value_domain` — ADR-0114's rule is that the code + // is the constraint's OWN name, the way `max_length` names the bound it + // breached. Until the field-level card's spec half landed (maintainer + // ruling 2026-09-02) no `FieldErrorCode` member named a standard-domain + // breach, so this branch took `invalid_value`, the catalog's declared + // slot for "rejected for a reason no other member names"; that slot was + // right only while no member named this one, and now one does. The + // change is wire-visible on `PUT /api/settings/:namespace` and is pinned + // as such (`settings-routes.test.ts`, `settings-service.test.ts`). + // `invalid_option` would still be a lie about which set was consulted: + // the declared options are exactly the list a domain-bearing value may + // legitimately be outside of. if (!empty && domain) { const rejected = firstRejectedDomainMember(domain, value); if (rejected) { const offending = rejected.value; - const { member, example } = valueDomainPhrasing(domain); // Same redaction rule as `invalid_option`, same reason: a domain // member is not a secret, but `encrypted` is authorable on any // specifier and this message travels back through the API and into - // logs. + // logs. The catalog templates always interpolate the offending + // value, so a secret key is rendered with the mask the REST boundary + // already uses for a withheld value — a redacted sentence rather + // than a truncated one. const secret = reg.encryptedKeys.has(key); - const got = secret ? '' : ` Received '${String(offending)}'.`; errors.push({ field: key, - code: 'invalid_value', - message: `${label} must be a valid ${member} (e.g. '${example}').${got}`, + code: 'value_domain', + // The published catalog template for this code, rendered in `en` + // (ADR-0114) — the same catalog the record write path renders, so + // the two doors under one ruling describe one domain in one set of + // words. The per-domain variant spells the standard out for a + // human; the machine-readable half is `code` + `constraint`, which + // is what a localized client re-renders from. + message: renderValidationMessage({ + messageKey: `value_domain_${domain}`, + label, + field: key, + params: { + valueDomain: domain, + value: secret ? SETTINGS_SECRET_MASK : String(offending), + }, + }), label, // The declared domain, spelled by the property it comes from // (`FieldError.constraint`, ADR-0114), so a client can branch on diff --git a/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts b/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts new file mode 100644 index 0000000000..8cd8cbf011 --- /dev/null +++ b/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts @@ -0,0 +1,102 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Ratchet: `service-settings` does not define standard-domain membership. + * + * ## Why this file exists + * + * Maintainer ruling 2026-09-02: ONE closed vocabulary and ONE membership + * predicate, shared by settings specifiers and object fields. Before it, three + * copies of the same three definitions were in the tree — `packages/spec`, + * `packages/services/service-settings` and `packages/core`'s module-private + * `isValidTimeZone`. Three copies of one definition is the shape that drifts, + * and the copy carrying no pins is the one a future editor "modernises". + * `core` was re-pointed with its own pin + * (`resolve-authz-context.time-zone-domain.pin.test.ts`); this file is that + * pin's opposite number for the settings door, and it is the ratchet the + * settings card asked for: a re-added table goes red HERE, at the seam, rather + * than silently re-opening the divergence the ruling closed. + * + * ## Why a source scan and not an import-shape assertion + * + * A behavioural test cannot see this defect. A re-added local table that + * happens to agree with the shared one today passes every membership case in + * `value-domains.test.ts` — the divergence it opens is in the FUTURE, the day + * one of the two is edited. What has to be pinned is therefore the absence of + * the second definition, which is a fact about the source text. + * + * The scan covers the package's whole non-test source, not just + * `value-domains.ts`: "delete the table from this file" and "move the table to + * a new file" are the same defect, and only the second one survives a + * single-file check. Test sources are deliberately exempt — the currency + * equivalence measurement in `value-domains.test.ts` probes + * `Intl.supportedValuesOf('currency')` on purpose, and that probe is evidence, + * not an enforcement path. + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync, readdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; + +const SRC = fileURLToPath(new URL('.', import.meta.url)); +const DOOR = join(SRC, 'value-domains.ts'); + +/** Every non-test `.ts` in this package's `src/`, as `[name, source]`. */ +function runtimeSources(): Array<[string, string]> { + return readdirSync(SRC) + .filter((f) => f.endsWith('.ts') && !f.endsWith('.test.ts')) + .map((f) => [f, readFileSync(join(SRC, f), 'utf8')] as [string, string]); +} + +describe('value-domains.ts answers from the shared predicate', () => { + it('imports the vocabulary and the predicate from @objectstack/spec/shared', () => { + const src = readFileSync(DOOR, 'utf8'); + expect(src).toContain("from '@objectstack/spec/shared'"); + expect(src).toContain('isValueDomainMember'); + expect(src).toContain('ValueDomainSchema'); + }); + + it('declares no membership machinery of its own', () => { + const src = readFileSync(DOOR, 'utf8'); + // The three copies that were here, by the shape each took. Comments in + // this file's own header name them, so match on code, not on prose: every + // check below is run against the source with comments stripped. + const code = stripComments(src); + expect(code, 'the currency probe belongs to the shared module').not.toContain('supportedValuesOf'); + expect(code, 'the time-zone probe belongs to the shared module').not.toContain('Intl.DateTimeFormat'); + expect(code, 'a lookup table here is a second membership definition').not.toContain('new Set('); + }); +}); + +describe('no membership table anywhere in this package', () => { + it('carries no ISO 3166-1 alpha-2 code list', () => { + // The alpha-2 list has no standard-library oracle, so it is the one + // domain a well-meaning editor is most likely to re-type locally. Detected + // by shape rather than by file: a string literal holding a run of + // space-separated uppercase pairs. + const RUN = /'(?:[A-Z]{2} ){7}/; + for (const [name, src] of runtimeSources()) { + expect(RUN.test(stripComments(src)), `${name} looks like it carries an alpha-2 code list`).toBe(false); + } + }); + + it('probes no Intl enumeration on any enforcement path', () => { + for (const [name, src] of runtimeSources()) { + expect(stripComments(src), `${name} probes an Intl enumeration`).not.toContain('supportedValuesOf'); + } + }); + + it('states the membership question exactly once, as the shared call', () => { + // Positive half of the ratchet: the absence checks above are satisfiable + // by deleting the enforcement altogether, so pin that the call is present + // and that exactly one file makes it. + const callers = runtimeSources().filter(([, src]) => stripComments(src).includes('isValueDomainMember(')); + expect(callers.map(([name]) => name)).toEqual(['value-domains.ts']); + }); +}); + +/** Drop line and block comments, so prose naming a banned shape is not a hit. */ +function stripComments(src: string): string { + return src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, ''); +} diff --git a/packages/services/service-settings/src/value-domains.test.ts b/packages/services/service-settings/src/value-domains.test.ts index a4237973fe..f5ba847da3 100644 --- a/packages/services/service-settings/src/value-domains.test.ts +++ b/packages/services/service-settings/src/value-domains.test.ts @@ -1,32 +1,63 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * Membership pins for the enforcement half of `Specifier.valueDomain` (#5712). + * The settings door's half of `Specifier.valueDomain` (#5712), after the + * re-point onto the ONE shared predicate (maintainer ruling 2026-09-02). * - * The DEFINITIONS (probe vs. enumeration, and why `Intl.supportedValuesOf - * ('timeZone')` / `Intl.DisplayNames` are the wrong oracles) are pinned where - * they are declared — `packages/spec/src/system/settings-manifest.test.ts` - * measures the traps themselves. What is pinned HERE is that this side - * implements those definitions: the values the spec's TSDoc names as legal are - * admitted, the ones it names as the reason each trap matters are refused. + * The DEFINITIONS (probe vs. enumeration, the CLDR currency snapshot, and why + * `Intl.supportedValuesOf('timeZone')` / `Intl.DisplayNames` are the wrong + * oracles) are pinned where they are declared — + * `packages/spec/src/shared/value-domain.test.ts` measures the traps + * themselves. What is pinned HERE is what the DOOR does with them: the values + * the shared module names as legal are admitted through + * `firstRejectedDomainMember`, the ones it names as the reason each trap + * matters are refused, and the door's own prose does not drift from the + * published catalog. + * + * ⚠️ ONE PIN WENT VACUOUS WITH THE RE-POINT, deliberately and on the record. + * Before the re-point this file's first case compared two independently + * written things: the spec's vocabulary against this package's own + * `DOMAIN_MEMBERSHIP` table. `knownValueDomain` now answers from + * `ValueDomainSchema` itself, so "every member of the vocabulary is + * enforceable" can no longer detect divergence — there is nothing left here to + * diverge FROM, which is the point of the ruling. The equality that still + * matters is pinned where both sides live: `value-domain.test.ts` holds + * `SpecifierValueDomainSchema` to BE `ValueDomainSchema` (identity, not equal + * members). What replaces the vacuous half below is the question the door can + * still answer alone: does every declared member actually REFUSE something + * here — i.e. is any member enforced by an accept-everything stub — and does + * every member have door prose that agrees with the catalog. */ import { describe, it, expect } from 'vitest'; -import { SpecifierValueDomainSchema } from '@objectstack/spec/system'; +import { ISO_3166_ALPHA2_CODES, ValueDomainSchema } from '@objectstack/spec/shared'; +import { BUILTIN_VALIDATION_MESSAGES, VALIDATION_MESSAGE_FALLBACK_LOCALE } from '@objectstack/spec/system'; import { firstRejectedDomainMember, - ISO_3166_ALPHA2_CODES, knownValueDomain, valueDomainPhrasing, } from './value-domains.js'; -describe('value domains — vocabulary parity with the spec', () => { - it('enforces exactly the members SpecifierValueDomainSchema declares', () => { - // A spec-side vocabulary change must go red HERE rather than becoming a - // declared-but-unenforced member (Prime Directive #10). Every declared - // member resolves to an enforcer, and nothing beyond the vocabulary does. - for (const member of SpecifierValueDomainSchema.options) { +describe('value domains — what the door still owns after the re-point', () => { + it('accepts every declared member, and every member actually refuses something', () => { + // The enforceability half is now structural (see the file header). What is + // NOT structural: that each member's shared enforcer is a membership test + // at all. A member wired to `() => true` would type-check, would satisfy + // "the vocabulary is fully covered", and would silently open the door to + // everything — so each one is required to reject its own garbage probe. + const garbage: Record = { + iana_time_zone: 'Mars/Olympus', + iso_4217_currency: 'XYZ', + iso_3166_alpha2: 'ZZ', + }; + for (const member of ValueDomainSchema.options) { expect(knownValueDomain(member), `${member} must be enforceable`).toBe(member); + const probe = garbage[member]; + expect(probe, `${member} needs a garbage probe in this table`).toBeTruthy(); + expect( + firstRejectedDomainMember(member, probe), + `${member} must actually refuse ${probe} — an accept-everything enforcer is the failure this pin exists for`, + ).toEqual({ value: probe }); // …and each has phrasing, so neither door can hit an undefined sentence. const p = valueDomainPhrasing(member); expect(p.member.length).toBeGreaterThan(0); @@ -42,9 +73,34 @@ describe('value domains — vocabulary parity with the spec', () => { expect(knownValueDomain('')).toBeNull(); expect(knownValueDomain(undefined)).toBeNull(); expect(knownValueDomain(42)).toBeNull(); - // Prototype-chain names must not read as members (`'toString' in obj`). + // Prototype-chain names must not read as members. The old implementation + // looked the domain up in an object literal, where `'toString' in obj` is + // true, and excluded these by a hand-written `hasOwnProperty` guard; the + // re-point replaced that guard with the closed enum's own `safeParse`. + // Same two names, pinned across the swap: a `z.enum` matches literal + // members only, and this case is what says so out loud. expect(knownValueDomain('toString')).toBeNull(); expect(knownValueDomain('constructor')).toBeNull(); + expect(knownValueDomain('__proto__')).toBeNull(); + expect(knownValueDomain('hasOwnProperty')).toBeNull(); + }); + + it('the door prose and the published catalog describe one domain in one set of words', () => { + // `valueDomainPhrasing` survived the re-point because the env-override + // door writes a LOG line, which has no error code, no locale and no + // `{{label}}` — the catalog's finished sentences do not fit it. The save + // door renders the catalog. This pin is what keeps the two from drifting: + // each domain's fragments must appear in that domain's catalog template. + const en = BUILTIN_VALIDATION_MESSAGES[VALIDATION_MESSAGE_FALLBACK_LOCALE]; + for (const member of ValueDomainSchema.options) { + const template = en[`value_domain_${member}`]; + expect(template, `catalog must carry value_domain_${member}`).toBeTruthy(); + const { member: noun, example } = valueDomainPhrasing(member); + expect(template, `${member}: the log line's noun must be the catalog's`).toContain(noun); + expect(template, `${member}: the log line's example must be the catalog's`).toContain(`e.g. ${example}`); + } + // And the code-named default exists, since that is the wire code itself. + expect(en['value_domain']).toBeTruthy(); }); }); @@ -70,7 +126,7 @@ describe('iana_time_zone — the Intl.DateTimeFormat probe', () => { }); }); -describe('iso_4217_currency — Intl.supportedValuesOf("currency")', () => { +describe('iso_4217_currency — the checked-in CLDR snapshot', () => { const ok = (v: unknown) => firstRejectedDomainMember('iso_4217_currency', v); it('admits CHF and every curated localization option', () => { @@ -83,15 +139,37 @@ describe('iso_4217_currency — Intl.supportedValuesOf("currency")', () => { expect(ok('XYZ')).toEqual({ value: 'XYZ' }); expect(ok('usd')).toEqual({ value: 'usd' }); }); + + it('still answers exactly what the run-time probe answered, on this runtime', () => { + // The one definition that MOVED with the re-point: a run-time + // `Intl.supportedValuesOf('currency')` probe became the key set of the + // checked-in CLDR snapshot. Measured equivalence was the bar the card set, + // so it is measured here rather than asserted — and measured through the + // DOOR, which is the thing that has to keep answering the same way. The + // spec's own test pins snapshot-vs-probe directly; this one pins that no + // settings value changed verdict. + const intl = Intl as typeof Intl & { supportedValuesOf(k: 'currency'): string[] }; + const probed = intl.supportedValuesOf('currency'); + expect(probed.length).toBeGreaterThan(100); // the probe is real, not an empty list + for (const code of probed) { + expect(ok(code), `${code} was admitted by the run-time probe and must still be`).toBeNull(); + } + }); }); -describe('iso_3166_alpha2 — the explicit code list the spec says this side must carry', () => { +describe('iso_3166_alpha2 — the explicit code list, now carried by the spec', () => { const ok = (v: unknown) => firstRejectedDomainMember('iso_3166_alpha2', v); - it('is structurally the officially assigned set: 249 unique uppercase pairs', () => { + it('admits the whole officially assigned set the shared module publishes', () => { + // Was a structural pin on this package's own table (`size === 249`). The + // table moved to `@objectstack/spec/shared`, where its structure is pinned; + // what is worth pinning HERE is stronger and not a duplicate — that the + // door actually admits every one of the published codes, end to end + // through `firstRejectedDomainMember`. expect(ISO_3166_ALPHA2_CODES.size).toBe(249); for (const code of ISO_3166_ALPHA2_CODES) { expect(code).toMatch(/^[A-Z]{2}$/); + expect(ok(code), `${code} is officially assigned and must be admitted`).toBeNull(); } }); diff --git a/packages/services/service-settings/src/value-domains.ts b/packages/services/service-settings/src/value-domains.ts index 78cc621b35..618033da90 100644 --- a/packages/services/service-settings/src/value-domains.ts +++ b/packages/services/service-settings/src/value-domains.ts @@ -1,129 +1,50 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * Standard value-domain membership — the enforcement half of - * `Specifier.valueDomain` (#5712; the declaration half is #5933, - * `SpecifierValueDomainSchema` in `@objectstack/spec/system`). + * Standard value-domain enforcement for settings specifiers — the settings + * door's adapter over the ONE shared predicate. * * A specifier that declares `valueDomain` says: the legal values for this key * are the members of this published standard, and that membership — not the - * curated `options` table — is the enforcement boundary. The spec deliberately - * only DECLARES the domains (Prime Directive #2 — no business logic, no data - * tables in `packages/spec`); each domain's definition of membership is pinned - * in `SpecifierValueDomainSchema`'s TSDoc and implemented here, on the - * enforcing side. Keep the two in sync — the spec-side TSDoc names the traps - * each definition was measured against, and `value-domains.test.ts` re-measures - * them so drift goes red rather than rotting. - */ - -import type { SpecifierValueDomain } from '@objectstack/spec/system'; - -/** - * `iana_time_zone` membership — the `Intl.DateTimeFormat` probe. + * curated `options` table — is the enforcement boundary. * - * NOT `Intl.supportedValuesOf('timeZone')`: measured on the repo's Node 22 - * baseline it returns 418 CLDR *canonical* names and omits `UTC` (the - * localization manifest's own declared default), `Asia/Kolkata` (a curated - * option shipped today), `Europe/Kyiv`, `US/Eastern` and `GMT` — ICU keeps the - * old spellings (`Asia/Calcutta`, `Europe/Kiev`) as its canonical names, so - * testing membership against that list rejects values every runtime accepts - * (#5712's own env repro, re-measured in #5933). The probe is the definition - * the platform's consumers already use: `isValidTimeZone` in - * `packages/core/src/security/resolve-authz-context.ts` (module-private there, - * hence re-stated rather than imported) and the IANA assertion in - * `localization.manifest.test.ts`. Note the probe is case-insensitive - * (`europe/zurich` constructs fine) — that IS the pinned definition: the - * accepted domain equals what every `Intl`-based consumer downstream accepts. - */ -function isIanaTimeZone(value: string): boolean { - try { - new Intl.DateTimeFormat('en-US', { timeZone: value }); - return true; - } catch { - return false; - } -} - -/** - * `iso_4217_currency` membership — `Intl.supportedValuesOf('currency')`. + * ## Where the definitions live now (and why not here) * - * Here the enumeration IS usable (measured 162 entries on the Node 22 - * baseline: admits `CHF` and all nine curated localization options, rejects - * `XYZ`). Known gaps are the recently assigned `VED` and the metal/fund codes - * (`XAU`, `XDR`, …) — widen deliberately if a deployment needs one, never by - * falling back to a regex. Membership is exact (uppercase, as ISO 4217 spells - * codes); computed once and cached, since the set is a property of the runtime - * and `validatePatch` runs per write. - */ -let iso4217Cache: ReadonlySet | undefined; -function iso4217Codes(): ReadonlySet { - // The cast exists because the repo's root tsconfig `lib` is ES2020 while - // `Intl.supportedValuesOf` is typed in lib.es2022.intl — the RUNTIME is - // guaranteed (engines >= 22; the spec's own vocabulary test calls it bare). - // Delete the cast when the root `lib` moves to ES2022+; do not widen it. - const intl = Intl as typeof Intl & { supportedValuesOf(key: 'currency'): string[] }; - iso4217Cache ??= new Set(intl.supportedValuesOf('currency')); - return iso4217Cache; -} - -/** - * `iso_3166_alpha2` — the officially assigned ISO 3166-1 alpha-2 codes, - * carried explicitly because there is NO standard-library oracle for this - * domain (measured in #5933): `Intl.DisplayNames(…, { type: 'region' })` - * returns a distinct display name for `ZZ` ("Unknown Region" — the exact value - * this domain exists to reject) and for `UK` (a CLDR alias that is not an - * ISO 3166-1 code), so "the name differs from the input" is not a membership - * test. The spec's TSDoc says the enforcing side must carry the list; this is - * that list — the 249 officially assigned codes. User-assigned and reserved - * elements (`ZZ`, `XX`, `UK`, `AA`, `QM`–`QZ`, …) are deliberately absent. - * Membership is exact uppercase, as the standard spells the codes; nothing in - * this repo writes lowercase country values, and one strict spelling is the - * shape AI-authored metadata cannot get subtly wrong. + * Maintainer ruling 2026-09-02 on the field-level card: **one closed vocabulary + * and one membership predicate shared by settings specifiers and object + * fields**. Both now live in `@objectstack/spec/shared` + * (`shared/value-domain.zod.ts`) — {@link ValueDomainSchema} is the vocabulary + * (`SpecifierValueDomainSchema` is an alias of it, not a copy) and + * `isValueDomainMember` is the predicate, with every definition's traps argued + * and re-measured in that module's own header and test. + * + * Until that ruling this module carried its own second copy of all three + * definitions: the `Intl.DateTimeFormat` probe, a run-time + * `Intl.supportedValuesOf('currency')` set, and the 249 ISO 3166-1 alpha-2 + * codes. The copies are gone. What is left here is what is genuinely the + * DOOR's: which declarations it agrees to enforce + * ({@link knownValueDomain}), how a multi-value carrier is walked + * ({@link firstRejectedDomainMember}), and the prose fragments the env-override + * log line needs ({@link valueDomainPhrasing}). Nothing in this file decides + * membership; a membership question that is not `isValueDomainMember` is a + * third copy re-appearing, and `value-domains.shared-predicate.pin.test.ts` is + * the ratchet that reddens when one does. + * + * ⚠️ One definition MOVED with the re-point, in a direction the shared module + * argues for: `iso_4217_currency` was a run-time probe of + * `Intl.supportedValuesOf('currency')` and is now the key set of the + * checked-in CLDR snapshot `CURRENCY_FRACTION_DIGITS`. Measured on the repo's + * Node 22 baseline (v22.22.2) the two sets are identical — 162 codes each, + * symmetric difference 0 in both directions — so no value changes verdict on + * this runtime; what changes is that the verdict no longer varies with the + * host's ICU build. */ -const ISO_3166_ALPHA2 = new Set( - ( - 'AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ ' + - 'BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ ' + - 'CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ ' + - 'DE DJ DK DM DO DZ ' + - 'EC EE EG EH ER ES ET ' + - 'FI FJ FK FM FO FR ' + - 'GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY ' + - 'HK HM HN HR HT HU ' + - 'ID IE IL IM IN IO IQ IR IS IT ' + - 'JE JM JO JP ' + - 'KE KG KH KI KM KN KP KR KW KY KZ ' + - 'LA LB LC LI LK LR LS LT LU LV LY ' + - 'MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ ' + - 'NA NC NE NF NG NI NL NO NP NR NU NZ ' + - 'OM ' + - 'PA PE PF PG PH PK PL PM PN PR PS PT PW PY ' + - 'QA ' + - 'RE RO RS RU RW ' + - 'SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ ' + - 'TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ ' + - 'UA UG UM US UY UZ ' + - 'VA VC VE VG VI VN VU ' + - 'WF WS ' + - 'YE YT ' + - 'ZA ZM ZW' - ).split(' '), -); - -/** Exported for the structural pins in `value-domains.test.ts` only. */ -export const ISO_3166_ALPHA2_CODES: ReadonlySet = ISO_3166_ALPHA2; -/** - * The closed set of domains this side knows how to enforce — kept equal to - * `SpecifierValueDomainSchema`'s members (`value-domains.test.ts` pins the - * equality, so a spec-side vocabulary change goes red here instead of becoming - * a declared-but-unenforced member, the Prime Directive #10 shape). - */ -const DOMAIN_MEMBERSHIP: Record boolean> = { - iana_time_zone: isIanaTimeZone, - iso_4217_currency: (value) => iso4217Codes().has(value), - iso_3166_alpha2: (value) => ISO_3166_ALPHA2.has(value), -}; +import { + isValueDomainMember, + ValueDomainSchema, + type ValueDomain, +} from '@objectstack/spec/shared'; /** * The declared `valueDomain`, when it is one this side can enforce; else null. @@ -136,17 +57,18 @@ const DOMAIN_MEMBERSHIP: Record boolean * check stays in force — which is the same "record nothing rather than an empty * table" leniency the option-table registration takes, and strictly safer than * accepting everything on the strength of a typo. + * + * The vocabulary's own `safeParse` is the filter. It admits the three declared + * members and nothing else — in particular no inherited property name + * (`'toString'`, `'constructor'`), which the previous implementation had to + * exclude by hand because it looked the domain up in an object literal and + * `'toString' in DOMAIN_MEMBERSHIP` is true through the prototype chain. A + * closed `z.enum` matches literal members only, so the guard is now structural + * rather than remembered; `value-domains.test.ts` keeps pinning both names. */ -export function knownValueDomain(declared: unknown): SpecifierValueDomain | null { - // Own-property, not `in`: the record is an object literal, so `'toString' in - // DOMAIN_MEMBERSHIP` is true via the prototype chain and would hand a - // hand-built manifest a "domain" whose enforcer is not a membership test. - // (`hasOwnProperty.call` rather than `Object.hasOwn` only because the root - // tsconfig `lib` is ES2020 — same story as the `supportedValuesOf` cast.) - return typeof declared === 'string' && - Object.prototype.hasOwnProperty.call(DOMAIN_MEMBERSHIP, declared) - ? (declared as SpecifierValueDomain) - : null; +export function knownValueDomain(declared: unknown): ValueDomain | null { + const parsed = ValueDomainSchema.safeParse(declared); + return parsed.success ? parsed.data : null; } /** @@ -159,24 +81,36 @@ export function knownValueDomain(declared: unknown): SpecifierValueDomain | null * string form (a stored value has been through JSON and a form post), and * returning a wrapper so "nothing rejected" and "the rejected member WAS * `undefined`" stay distinguishable. + * + * The membership question itself is `isValueDomainMember` — the same call the + * record write path makes, so a value accepted in Settings is the same value + * accepted in a field and vice versa. */ export function firstRejectedDomainMember( - domain: SpecifierValueDomain, + domain: ValueDomain, value: unknown, ): { value: unknown } | null { - const member = DOMAIN_MEMBERSHIP[domain]; const picked = Array.isArray(value) ? value : [value]; - const at = picked.findIndex((v) => !member(String(v))); + const at = picked.findIndex((v) => !isValueDomainMember(domain, String(v))); return at === -1 ? null : { value: picked[at] }; } /** - * The three prose fragments a rejection message needs, per domain — one - * definition feeding BOTH doors (`validatePatch`'s `FieldError.message` and - * `reportRejectedEnvOverride`'s log line), so the two never describe the same - * domain in different words. + * The two prose fragments the ENV-OVERRIDE log line needs, per domain. + * + * Scope narrowed with the re-point: the save path's `FieldError` no longer + * builds its sentence here — it renders the published catalog template + * `value_domain_` (`@objectstack/spec/system`), the same catalog the + * record write path renders, so the two doors cannot describe one domain in + * different words. `reportRejectedEnvOverride` writes a LOG line, not a + * `FieldError`: it has no error code, no locale and no `{{label}}`, and its + * one sentence template wants fragments ("is not a valid X for", "any X (e.g. + * 'Y')") rather than a finished sentence. So the fragments stay here, and + * `value-domains.test.ts` pins each one against the catalog template for the + * same domain — drift between the log line and the wire message goes red + * instead of rotting. */ -export function valueDomainPhrasing(domain: SpecifierValueDomain): { +export function valueDomainPhrasing(domain: ValueDomain): { /** What a legal member is called, e.g. "IANA time zone identifier". */ member: string; /** A representative legal value that is NOT in the curated options today. */ diff --git a/packages/spec/liveness/field.json b/packages/spec/liveness/field.json index 9d48a657ac..34ee289633 100644 --- a/packages/spec/liveness/field.json +++ b/packages/spec/liveness/field.json @@ -225,7 +225,7 @@ "status": "planned", "verifiedAt": "2026-09-04", "evidence": "packages/spec/src/data/field.zod.ts#VALUE_DOMAIN_FIELD_TYPES (the parse-time applicability door: the key is accepted on `text` only and refused with a located `custom` issue at [valueDomain] on every other type — the same superRefine mechanism `maxLength` / `minLength` use); packages/spec/src/shared/value-domain.zod.ts#isValueDomainMember (the ONE membership predicate the write path will call — shared with the settings door)", - "note": "PLANNED, deliberately not `dead`, and the difference is the point. Declared spec-first under the maintainer's 2026-09-02 ruling (option A on the field-level `valueDomain` card: one closed vocabulary — `iana_time_zone` / `iso_4217_currency` / `iso_3166_alpha2` — and one membership predicate shared by settings specifiers and object fields; the vocabulary does not widen). What ships here: the slot, its applicability refusal (a domain on a `number` field is refused at parse, not ignored), the shared predicate, the ADR-0114 catalog member `value_domain` and its four-locale message templates. What does NOT ship here and is the reason for `planned`: the record validator's call into the predicate — `record-validator.ts` does not yet read `def.valueDomain`, so a non-member WRITTEN to a `text` field declaring a domain is accepted today. That write-path refusal is the engine follow-up card the PM files at ACCEPT of the spec half (domain:engine, `Blocked-by:` the spec card); when it lands this row flips `live` with the record-validator seam cited beside `maxLength`'s. The settings door (`service-settings/value-domains.ts`) re-points onto the shared predicate in its own follow-up card and is unchanged until then." + "note": "PLANNED, deliberately not `dead`, and the difference is the point. Declared spec-first under the maintainer's 2026-09-02 ruling (option A on the field-level `valueDomain` card: one closed vocabulary — `iana_time_zone` / `iso_4217_currency` / `iso_3166_alpha2` — and one membership predicate shared by settings specifiers and object fields; the vocabulary does not widen). What ships here: the slot, its applicability refusal (a domain on a `number` field is refused at parse, not ignored), the shared predicate, the ADR-0114 catalog member `value_domain` and its four-locale message templates. What does NOT ship here and is the reason for `planned`: the record validator's call into the predicate — `record-validator.ts` does not yet read `def.valueDomain`, so a non-member WRITTEN to a `text` field declaring a domain is accepted today. That write-path refusal is the engine follow-up card the PM files at ACCEPT of the spec half (domain:engine, `Blocked-by:` the spec card); when it lands this row flips `live` with the record-validator seam cited beside `maxLength`'s. The settings door (`service-settings/value-domains.ts`) HAS since re-pointed onto the shared predicate and now refuses a non-member with this same `value_domain` code (#15162, the services half of the ruling) — so the predicate has two callers and one of its two doors enforces. This row still tracks the OBJECT-FIELD write path, which is why it stays `planned`: `record-validator.ts` reading `def.valueDomain` is the engine card, unlanded." }, "rows": { "status": "live", From 46b06feeca9f4bb523cf53a7379a9b6a345c9503 Mon Sep 17 00:00:00 2001 From: os-warren Date: Fri, 4 Sep 2026 15:18:27 +0000 Subject: [PATCH 2/5] test(service-settings): state the wire-visible change, and declare the ratchet's input radius - `settings-routes.test.ts` gains a pin that states the refusal envelope as a CHANGE: `value_domain` (not `invalid_value`) plus the catalog sentence, with status, envelope code, `field`, `label`, `constraint` and `value` asserted unchanged beside it, so the blast radius is measured rather than believed. - The encrypted-specifier case pins the redacted sentence: the catalog template always interpolates the offending value, so a secret key renders with the REST boundary's mask and the value still never appears. - The ratchet routes its comment stripping through `scripts/js-comment-mask.mjs` (`check:comment-mask-adoption` refuses a private one) and declares that import's radius in `cross-package-test-inputs.mjs` + `turbo.json`, so the pin is visible to the affected-subset filter and the turbo cache. - Changeset: `minor` + a BREAKING banner, which is the carrier for breaking-ness inside the launch window where `major` is forbidden. `packages/spec/liveness/field.json` is deliberately NOT touched: the same `valueDomain` row is being rewritten by the open engine-half PR, so an edit here would conflict and force the ledger counts to be re-derived. The sentence this branch falsifies is reported to the PM to sequence instead. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y --- ...ings-door-value-domain-shared-predicate.md | 65 +++++++++++++++++++ .../src/settings-routes.test.ts | 46 +++++++++++++ .../src/settings-service.test.ts | 6 ++ ...value-domains.shared-predicate.pin.test.ts | 17 +++-- packages/spec/liveness/field.json | 2 +- scripts/cross-package-test-inputs.mjs | 18 +++++ turbo.json | 12 ++++ 7 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 .changeset/settings-door-value-domain-shared-predicate.md diff --git a/.changeset/settings-door-value-domain-shared-predicate.md b/.changeset/settings-door-value-domain-shared-predicate.md new file mode 100644 index 0000000000..ea8fca5550 --- /dev/null +++ b/.changeset/settings-door-value-domain-shared-predicate.md @@ -0,0 +1,65 @@ +--- +"@objectstack/service-settings": minor +--- + +fix(service-settings): the settings door answers from the ONE shared value-domain predicate, and refuses a non-member with `value_domain` (#15162) + + + +**BREAKING** for a client that branches on the refusal code. Landing inside +the launch window, so it ships as `minor` (the lockstep convention forbids +`major`); the banner is the carrier, not the bump. + +The services half of the maintainer's ruling of 2026-09-02: **one closed +vocabulary and one membership predicate shared by settings specifiers and +object fields**. The spec half declared them in `@objectstack/spec/shared`; +this package had been carrying a second copy of all three definitions since +`Specifier.valueDomain` shipped. The copies are deleted and the door now asks +`isValueDomainMember` — the same call the record write path makes. + +**The wire change**, measured on `PUT /api/settings/localization` with +`{"timezone": "Mars/Olympus"}`, base `a56baa2bd` vs this branch: + +| | before | after | +|:--|:--|:--| +| `fields[0].code` | `invalid_value` | `value_domain` | +| `fields[0].message` | `Default timezone must be a valid IANA time zone identifier (e.g. 'Europe/Zurich'). Received 'Mars/Olympus'.` | `Default timezone must be a valid IANA time zone identifier, e.g. Europe/Zurich (got "Mars/Olympus")` | + +Everything else is byte-identical: HTTP 400, the envelope code +`SETTINGS_VALIDATION`, `field`, `label`, `constraint: { valueDomain: … }` and +the echoed `value`. A client that reads `constraint.valueDomain` — the +machine-readable half ADR-0114 asks it to read — is unaffected. A client that +branches on `code === 'invalid_value'` for a domain breach must move to +`value_domain`. + +Why the code moved: ADR-0114's rule is that the code is the **constraint's own +name**, the way `max_length` names the bound it breached. This branch took +`invalid_value` — the catalog's slot for "rejected for a reason no other +member names" — only while no member named a standard-domain breach. The +field-level card's spec half added one, so the slot no longer applies. The +message now renders the published catalog template +`value_domain_` in `en`, which is the same catalog the record write +path renders, so the two doors under one ruling describe one domain in one set +of words. For an `encrypted` specifier the offending value is still never +echoed: the template's value placeholder takes the same mask the REST boundary +uses (`fields[0].value` stays absent, as before). + +**No value changes verdict.** The accept sets were measured, not assumed, on +the repo's Node 22 baseline (v22.22.2): + +- `iso_3166_alpha2` — the two 249-code lists diffed mechanically before either + was deleted: identical, including order; symmetric difference 0. +- `iso_4217_currency` — this one changes DEFINITION: a run-time + `Intl.supportedValuesOf('currency')` probe becomes the key set of the + checked-in CLDR snapshot `CURRENCY_FRACTION_DIGITS`. 162 codes vs 162, + symmetric difference 0 in both directions (`CHF` in both, `XYZ` in neither). + The behaviour that changes is that the verdict no longer varies with the + host's ICU build — the direction the shared module argues for. A door-level + test now re-measures it: every code the run-time probe admits must still be + admitted. +- `iana_time_zone` — the identical `Intl.DateTimeFormat` probe on both sides, + unmoved. + +A ratchet pin (`value-domains.shared-predicate.pin.test.ts`) reddens if any +non-test source in this package re-acquires a membership table, an `Intl` +enumeration probe, or a second caller of the predicate. diff --git a/packages/services/service-settings/src/settings-routes.test.ts b/packages/services/service-settings/src/settings-routes.test.ts index 353bbcdae0..a7da723b8b 100644 --- a/packages/services/service-settings/src/settings-routes.test.ts +++ b/packages/services/service-settings/src/settings-routes.test.ts @@ -271,6 +271,52 @@ describe('settings-routes', () => { ]); }); + /** + * The wire-visible change, stated AS a change. + * + * Measured on this endpoint with `timezone: 'Mars/Olympus'`, base + * `a56baa2bd` vs this branch: + * + * - before — `code: 'invalid_value'`, message `Default timezone must be a + * valid IANA time zone identifier (e.g. 'Europe/Zurich'). Received + * 'Mars/Olympus'.` + * - after — `code: 'value_domain'`, message `Default timezone must be a + * valid IANA time zone identifier, e.g. Europe/Zurich (got + * "Mars/Olympus")` — the published catalog template + * `value_domain_iana_time_zone`, rendered in `en`. + * + * UNCHANGED across it, and asserted here so the blast radius is stated and + * not merely believed: HTTP 400, the envelope code `SETTINGS_VALIDATION`, + * `field`, `label`, `constraint.valueDomain` and the echoed `value`. A + * client reading `constraint.valueDomain` is unaffected; one branching on + * `code === 'invalid_value'` is, and that is the whole of the break. + */ + it('the domain refusal is `value_domain` with the catalog sentence — not `invalid_value`', async () => { + const http = new MockHttp(); + const svc = new SettingsService({ env: {} }); + svc.registerManifest(localizationSettingsManifest); + registerSettingsRoutes(http, svc, { contextFromRequest: adminProvider }); + + const h = http.routes.get('PUT /api/settings/:namespace')!; + const { req, res, state } = makeReqRes({ + params: { namespace: 'localization' }, + body: { timezone: 'Mars/Olympus' }, + }); + await h(req, res); + + expect(state.status).toBe(400); + expect(state.body.error.code).toBe('SETTINGS_VALIDATION'); + const field = state.body.error.details.fields[0]; + expect(field.code).toBe('value_domain'); + expect(field.code).not.toBe('invalid_value'); + expect(field.message).toBe( + 'Default timezone must be a valid IANA time zone identifier, e.g. Europe/Zurich (got "Mars/Olympus")', + ); + expect(field.label).toBe('Default timezone'); + expect(field.constraint).toEqual({ valueDomain: 'iana_time_zone' }); + expect(field.value).toBe('Mars/Olympus'); + }); + /** * #7169 — the STATUS half of the fail-closed refusal. * diff --git a/packages/services/service-settings/src/settings-service.test.ts b/packages/services/service-settings/src/settings-service.test.ts index 22ef6ba0e1..4f1733cf40 100644 --- a/packages/services/service-settings/src/settings-service.test.ts +++ b/packages/services/service-settings/src/settings-service.test.ts @@ -2217,6 +2217,12 @@ describe('SettingsService — a declared valueDomain is the save-time boundary ( expect(err.fields[0]).toMatchObject({ field: 'region_code', code: 'value_domain' }); expect(err.fields[0].value).toBeUndefined(); expect(err.message).not.toContain('ZZ'); + // The catalog template always interpolates the offending value, so the + // redaction is the REST boundary's own mask rather than a truncated + // sentence — the rejected value still never appears. + expect(err.fields[0].message).toBe( + 'Region code must be a valid ISO 3166-1 alpha-2 country code, e.g. CH (got "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022")', + ); // The domain still travels, so the caller learns what to do. expect(err.fields[0].constraint).toMatchObject({ valueDomain: 'iso_3166_alpha2' }); }); diff --git a/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts b/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts index 8cd8cbf011..2efa3281ba 100644 --- a/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts +++ b/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts @@ -38,7 +38,19 @@ import { describe, it, expect } from 'vitest'; import { readFileSync, readdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { join } from 'node:path'; +// The repo's ONE answer to "is this span a comment, or code?" — a private +// stripper here would be the drift its header records (and +// `check:comment-mask-adoption` refuses one). `stripComments` is the right +// projection: every finding below reports a bare file name, never an offset. +// The `.mjs` specifier is deliberate; `scripts/js-comment-mask.d.mts` beside +// it is a hand-written declaration, so this import needs no `allowJs`. +import { stripComments } from '../../../../scripts/js-comment-mask.mjs'; +/** + * Seeded from `import.meta.url` in a spelling `check:cross-package-test-inputs` + * resolves statically. The READS below do not escape the package — they are + * this package's own `src/`. + */ const SRC = fileURLToPath(new URL('.', import.meta.url)); const DOOR = join(SRC, 'value-domains.ts'); @@ -95,8 +107,3 @@ describe('no membership table anywhere in this package', () => { expect(callers.map(([name]) => name)).toEqual(['value-domains.ts']); }); }); - -/** Drop line and block comments, so prose naming a banned shape is not a hit. */ -function stripComments(src: string): string { - return src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, ''); -} diff --git a/packages/spec/liveness/field.json b/packages/spec/liveness/field.json index 34ee289633..9d48a657ac 100644 --- a/packages/spec/liveness/field.json +++ b/packages/spec/liveness/field.json @@ -225,7 +225,7 @@ "status": "planned", "verifiedAt": "2026-09-04", "evidence": "packages/spec/src/data/field.zod.ts#VALUE_DOMAIN_FIELD_TYPES (the parse-time applicability door: the key is accepted on `text` only and refused with a located `custom` issue at [valueDomain] on every other type — the same superRefine mechanism `maxLength` / `minLength` use); packages/spec/src/shared/value-domain.zod.ts#isValueDomainMember (the ONE membership predicate the write path will call — shared with the settings door)", - "note": "PLANNED, deliberately not `dead`, and the difference is the point. Declared spec-first under the maintainer's 2026-09-02 ruling (option A on the field-level `valueDomain` card: one closed vocabulary — `iana_time_zone` / `iso_4217_currency` / `iso_3166_alpha2` — and one membership predicate shared by settings specifiers and object fields; the vocabulary does not widen). What ships here: the slot, its applicability refusal (a domain on a `number` field is refused at parse, not ignored), the shared predicate, the ADR-0114 catalog member `value_domain` and its four-locale message templates. What does NOT ship here and is the reason for `planned`: the record validator's call into the predicate — `record-validator.ts` does not yet read `def.valueDomain`, so a non-member WRITTEN to a `text` field declaring a domain is accepted today. That write-path refusal is the engine follow-up card the PM files at ACCEPT of the spec half (domain:engine, `Blocked-by:` the spec card); when it lands this row flips `live` with the record-validator seam cited beside `maxLength`'s. The settings door (`service-settings/value-domains.ts`) HAS since re-pointed onto the shared predicate and now refuses a non-member with this same `value_domain` code (#15162, the services half of the ruling) — so the predicate has two callers and one of its two doors enforces. This row still tracks the OBJECT-FIELD write path, which is why it stays `planned`: `record-validator.ts` reading `def.valueDomain` is the engine card, unlanded." + "note": "PLANNED, deliberately not `dead`, and the difference is the point. Declared spec-first under the maintainer's 2026-09-02 ruling (option A on the field-level `valueDomain` card: one closed vocabulary — `iana_time_zone` / `iso_4217_currency` / `iso_3166_alpha2` — and one membership predicate shared by settings specifiers and object fields; the vocabulary does not widen). What ships here: the slot, its applicability refusal (a domain on a `number` field is refused at parse, not ignored), the shared predicate, the ADR-0114 catalog member `value_domain` and its four-locale message templates. What does NOT ship here and is the reason for `planned`: the record validator's call into the predicate — `record-validator.ts` does not yet read `def.valueDomain`, so a non-member WRITTEN to a `text` field declaring a domain is accepted today. That write-path refusal is the engine follow-up card the PM files at ACCEPT of the spec half (domain:engine, `Blocked-by:` the spec card); when it lands this row flips `live` with the record-validator seam cited beside `maxLength`'s. The settings door (`service-settings/value-domains.ts`) re-points onto the shared predicate in its own follow-up card and is unchanged until then." }, "rows": { "status": "live", diff --git a/scripts/cross-package-test-inputs.mjs b/scripts/cross-package-test-inputs.mjs index 32daef3c5d..ac41dc56c7 100644 --- a/scripts/cross-package-test-inputs.mjs +++ b/scripts/cross-package-test-inputs.mjs @@ -646,6 +646,24 @@ export const CROSS_PACKAGE_TEST_INPUTS = { 'scripts/js-comment-mask.d.mts', ], }, + '@objectstack/service-settings': { + // src/value-domains.shared-predicate.pin.test.ts (#15162) imports + // `stripComments` from `js-comment-mask.mjs` to decide which text in this + // package's `src/` is prose and which is code — the ratchet that keeps the + // settings door answering from the ONE shared value-domain predicate + // instead of re-acquiring a membership table of its own. The coupling is + // real in both directions: the guard's verdicts are census over the whole + // non-test source, and its banned tokens (`supportedValuesOf`, + // `Intl.DateTimeFormat`) are named in the very TSDoc that explains why they + // are banned — so a change in what the module counts as a comment changes + // every verdict here. The `.d.mts` sibling is declared alongside it because + // it is what types the import, so this package's `tsc --noEmit` verdict is + // a function of it too. + globs: [ + 'scripts/js-comment-mask.mjs', + 'scripts/js-comment-mask.d.mts', + ], + }, '@objectstack/trigger-api': { // src/trigger-api-route-ledger.conformance.test.ts (#12398) imports // `stripComments` from `js-comment-mask.mjs` for the same reason and in the diff --git a/turbo.json b/turbo.json index fc134881be..1f5d4c96d5 100644 --- a/turbo.json +++ b/turbo.json @@ -23,6 +23,18 @@ "outputs": ["dist/**", "json-schema/**", ".next/**", "!.next/cache/**"], "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/content/**"] }, + "@objectstack/service-settings#test": { + "dependsOn": ["build"], + "outputs": [], + "inputs": [ + "$TURBO_DEFAULT$", + "!dist/**", + "!coverage/**", + "!.turbo/**", + "$TURBO_ROOT$/scripts/js-comment-mask.mjs", + "$TURBO_ROOT$/scripts/js-comment-mask.d.mts" + ] + }, "@objectstack/metadata#test": { "dependsOn": ["build"], "outputs": [], From 6dad1547e344488416146ebc733afa57ef940f86 Mon Sep 17 00:00:00 2001 From: os-warren Date: Fri, 4 Sep 2026 21:32:12 +0000 Subject: [PATCH 3/5] test(service-settings): close the two ratchet holes the contract review found, and drop the ICU-drift currency pin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R1 — the ratchet did not cover what its header claimed. Two mutations passed it green: a table one directory down (`src/manifests/`, which `readdirSync` never reached) and a 249-code ARRAY literal in a sibling module that `value-domains.ts` imported and consulted for `iso_3166_alpha2` while the shared predicate still served the other two. - the source walk is recursive, and reports paths relative to `src/`; - both table SHAPES are detected — the space-separated string and the array literal a fresh re-typing produces; - the root close is a new import-surface pin: `value-domains.ts` may import from `@objectstack/spec/shared` and NOTHING else, so a table anywhere in the tree is harmless because the door cannot reach it. `new Set(` is deliberately NOT widened package-wide — `settings-service.ts` has six legitimate sites; - the header now states what is and is not covered, at the size of the evidence. R2 — the currency pin asserted at run time that everything `Intl.supportedValuesOf('currency')` enumerates is admitted by a door that answers from the checked-in snapshot. CI pins the Node MAJOR only, so an ICU build that enumerates one code the snapshot lacks would redden a CORRECT implementation in a package with nothing to fix — and probe-versus-snapshot is already pinned beside the snapshot, in both directions and with a size. It is replaced by the invariant this package owns: the door AGREES with `isValueDomainMember` value by value, over a corpus carrying every trap, so a local filter creeping back into the walker reddens even though every representative case would still pass. Also, from the review's waived list: `turbo.json` uses `^build` (nothing in this package's tests reads its own dist); the changeset's "the same catalog the record write path renders" is future tense and cites the open engine PR; and the alpha-2 population pin is described as the plumbing pin it is rather than as something stronger. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y --- ...ings-door-value-domain-shared-predicate.md | 9 +- ...value-domains.shared-predicate.pin.test.ts | 92 +++++++++++++++---- .../src/value-domains.test.ts | 75 +++++++++++---- turbo.json | 2 +- 4 files changed, 135 insertions(+), 43 deletions(-) diff --git a/.changeset/settings-door-value-domain-shared-predicate.md b/.changeset/settings-door-value-domain-shared-predicate.md index ea8fca5550..9ce175ffdb 100644 --- a/.changeset/settings-door-value-domain-shared-predicate.md +++ b/.changeset/settings-door-value-domain-shared-predicate.md @@ -15,7 +15,8 @@ vocabulary and one membership predicate shared by settings specifiers and object fields**. The spec half declared them in `@objectstack/spec/shared`; this package had been carrying a second copy of all three definitions since `Specifier.valueDomain` shipped. The copies are deleted and the door now asks -`isValueDomainMember` — the same call the record write path makes. +`isValueDomainMember` — the call the record write path will make when the +engine half of the same ruling lands (PR #15316, still open). **The wire change**, measured on `PUT /api/settings/localization` with `{"timezone": "Mars/Olympus"}`, base `a56baa2bd` vs this branch: @@ -38,9 +39,9 @@ name**, the way `max_length` names the bound it breached. This branch took member names" — only while no member named a standard-domain breach. The field-level card's spec half added one, so the slot no longer applies. The message now renders the published catalog template -`value_domain_` in `en`, which is the same catalog the record write -path renders, so the two doors under one ruling describe one domain in one set -of words. For an `encrypted` specifier the offending value is still never +`value_domain_` in `en` — the catalog the record write path will render +from once PR #15316 lands, so the two doors under one ruling will describe one +domain in one set of words instead of each composing its own sentence. For an `encrypted` specifier the offending value is still never echoed: the template's value placeholder takes the same mask the REST boundary uses (`fields[0].value` stays absent, as before). diff --git a/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts b/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts index 2efa3281ba..953b3fb036 100644 --- a/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts +++ b/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts @@ -25,19 +25,40 @@ * one of the two is edited. What has to be pinned is therefore the absence of * the second definition, which is a fact about the source text. * - * The scan covers the package's whole non-test source, not just - * `value-domains.ts`: "delete the table from this file" and "move the table to - * a new file" are the same defect, and only the second one survives a - * single-file check. Test sources are deliberately exempt — the currency - * equivalence measurement in `value-domains.test.ts` probes - * `Intl.supportedValuesOf('currency')` on purpose, and that probe is evidence, - * not an enforcement path. + * The scan covers the package's whole non-test source **recursively**, not just + * `value-domains.ts` and not just `src/*`: "delete the table from this file", + * "move the table to a new file" and "move it into `src/manifests/`" are one + * defect, and a single-file or single-directory check survives all but the + * first. Both table SHAPES are detected — the space-separated string this file + * replaced and the array literal a re-typing would more likely produce. + * + * ## What each check does and does not cover — measured, not claimed + * + * The absence checks are a source scan, so they see shapes. Two mutations were + * run against an earlier draft of this file and passed it GREEN, which is why + * the checks below are shaped as they are: a probe table placed one directory + * down (`src/manifests/`), and a 249-code ARRAY literal in a new sibling module + * that `value-domains.ts` imported and consulted for `iso_3166_alpha2` while + * the shared predicate still served the other two. Both now red. + * + * The check that closes that second route at its ROOT is the import-surface pin + * below: `value-domains.ts` may import from `@objectstack/spec/shared` and + * nothing else. A table can exist anywhere in the tree without harm as long as + * the door cannot reach it, and a relative import is how it would. + * + * NOT covered, stated so the claim stays the size of the evidence: a 3-letter + * currency array is not shape-detected (the two alpha-2 shapes are), and a + * membership table reached through a BARE package specifier rather than a + * relative one would pass the import pin. The import surface being one line + * long is what makes both remote. + * + * Test sources are exempt from the scan: they are evidence, not enforcement. */ import { describe, it, expect } from 'vitest'; import { readFileSync, readdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; -import { join } from 'node:path'; +import { join, relative } from 'node:path'; // The repo's ONE answer to "is this span a comment, or code?" — a private // stripper here would be the drift its header records (and // `check:comment-mask-adoption` refuses one). `stripComments` is the right @@ -54,11 +75,25 @@ import { stripComments } from '../../../../scripts/js-comment-mask.mjs'; const SRC = fileURLToPath(new URL('.', import.meta.url)); const DOOR = join(SRC, 'value-domains.ts'); -/** Every non-test `.ts` in this package's `src/`, as `[name, source]`. */ -function runtimeSources(): Array<[string, string]> { - return readdirSync(SRC) - .filter((f) => f.endsWith('.ts') && !f.endsWith('.test.ts')) - .map((f) => [f, readFileSync(join(SRC, f), 'utf8')] as [string, string]); +/** + * Every non-test `.ts` under this package's `src/`, RECURSIVELY, as + * `[path-relative-to-src, source]`. + * + * Recursive because `readdirSync` is not: `src/manifests/` and + * `src/translations/` exist today, and a table placed in either passed an + * earlier draft of this pin green. + */ +function runtimeSources(dir: string = SRC): Array<[string, string]> { + const out: Array<[string, string]> = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + out.push(...runtimeSources(full)); + } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')) { + out.push([relative(SRC, full), readFileSync(full, 'utf8')]); + } + } + return out; } describe('value-domains.ts answers from the shared predicate', () => { @@ -69,6 +104,18 @@ describe('value-domains.ts answers from the shared predicate', () => { expect(src).toContain('ValueDomainSchema'); }); + it('imports from NOTHING ELSE — the door cannot reach a table wherever one is put', () => { + // The root close. Every absence check in this file is a shape scan and so + // is defeatable by a shape it does not know; this one is not a scan for a + // table at all, it is the statement that the door has exactly one source of + // membership. A 249-code array literal in a sibling module is harmless + // while nothing here can import it, and a relative specifier is how it + // would be reached. + const code = stripComments(readFileSync(DOOR, 'utf8')); + const specifiers = [...code.matchAll(/\bfrom\s+'([^']+)'/g)].map((m) => m[1]); + expect(specifiers).toEqual(['@objectstack/spec/shared']); + }); + it('declares no membership machinery of its own', () => { const src = readFileSync(DOOR, 'utf8'); // The three copies that were here, by the shape each took. Comments in @@ -82,14 +129,19 @@ describe('value-domains.ts answers from the shared predicate', () => { }); describe('no membership table anywhere in this package', () => { - it('carries no ISO 3166-1 alpha-2 code list', () => { - // The alpha-2 list has no standard-library oracle, so it is the one - // domain a well-meaning editor is most likely to re-type locally. Detected - // by shape rather than by file: a string literal holding a run of - // space-separated uppercase pairs. - const RUN = /'(?:[A-Z]{2} ){7}/; + it('carries no ISO 3166-1 alpha-2 code list, in either shape', () => { + // The alpha-2 list has no standard-library oracle, so it is the one domain + // a well-meaning editor is most likely to re-type locally. Detected by + // shape rather than by file, and BOTH shapes are needed: the + // space-separated string this package used to carry, and the array literal + // a fresh re-typing produces — an earlier draft checked only the first and + // a 249-element array passed it green. + const SPACED = /'(?:[A-Z]{2} ){7}/; + const ARRAY = /(?:'[A-Z]{2}',\s*){7}/; for (const [name, src] of runtimeSources()) { - expect(RUN.test(stripComments(src)), `${name} looks like it carries an alpha-2 code list`).toBe(false); + const code = stripComments(src); + expect(SPACED.test(code), `${name} carries a space-separated alpha-2 code list`).toBe(false); + expect(ARRAY.test(code), `${name} carries an array-literal alpha-2 code list`).toBe(false); } }); diff --git a/packages/services/service-settings/src/value-domains.test.ts b/packages/services/service-settings/src/value-domains.test.ts index f5ba847da3..df03867699 100644 --- a/packages/services/service-settings/src/value-domains.test.ts +++ b/packages/services/service-settings/src/value-domains.test.ts @@ -30,7 +30,7 @@ */ import { describe, it, expect } from 'vitest'; -import { ISO_3166_ALPHA2_CODES, ValueDomainSchema } from '@objectstack/spec/shared'; +import { ISO_3166_ALPHA2_CODES, isValueDomainMember, ValueDomainSchema } from '@objectstack/spec/shared'; import { BUILTIN_VALIDATION_MESSAGES, VALIDATION_MESSAGE_FALLBACK_LOCALE } from '@objectstack/spec/system'; import { firstRejectedDomainMember, @@ -140,21 +140,58 @@ describe('iso_4217_currency — the checked-in CLDR snapshot', () => { expect(ok('usd')).toEqual({ value: 'usd' }); }); - it('still answers exactly what the run-time probe answered, on this runtime', () => { - // The one definition that MOVED with the re-point: a run-time - // `Intl.supportedValuesOf('currency')` probe became the key set of the - // checked-in CLDR snapshot. Measured equivalence was the bar the card set, - // so it is measured here rather than asserted — and measured through the - // DOOR, which is the thing that has to keep answering the same way. The - // spec's own test pins snapshot-vs-probe directly; this one pins that no - // settings value changed verdict. - const intl = Intl as typeof Intl & { supportedValuesOf(k: 'currency'): string[] }; - const probed = intl.supportedValuesOf('currency'); - expect(probed.length).toBeGreaterThan(100); // the probe is real, not an empty list - for (const code of probed) { - expect(ok(code), `${code} was admitted by the run-time probe and must still be`).toBeNull(); +}); + +describe('the door is a walker over the shared predicate, not a second judge', () => { + /** + * The invariant this package actually owns, and the one that survives the + * re-point: `firstRejectedDomainMember` adds no membership opinion of its + * own — it walks a carrier and asks `isValueDomainMember`. Asserted as + * AGREEMENT over a corpus carrying each domain's traps, so a local filter + * creeping back in (an extra case fold, a length or shape check, a curated + * allow-list) reddens here even though every representative case above would + * still pass. + * + * ⚠️ Deliberately NOT a probe of `Intl.supportedValuesOf('currency')`. An + * earlier draft asserted that everything the RUN-TIME probe enumerates is + * admitted by the door. That was a time bomb and a duplicate at once: the + * door now answers from the checked-in CLDR snapshot, CI pins only the Node + * MAJOR, and the ICU build moves underneath it — so a host enumerating one + * currency the snapshot lacks would redden a CORRECT implementation, in a + * package with nothing to fix. Probe-versus-snapshot belongs beside the + * snapshot and is already pinned there, in both directions and with a size, + * by `packages/spec/src/shared/value-domain.test.ts`. + */ + const CORPUS = [ + // members, one or more per domain + 'UTC', 'Asia/Kolkata', 'Europe/Kyiv', 'GMT', 'US/Eastern', 'Europe/Zurich', + 'CHF', 'USD', 'EUR', 'JPY', + 'US', 'GB', 'CH', 'UA', + // non-members, including every trap the shared module names + 'Mars/Olympus', 'Europe/Munich', 'Not A Zone', + 'XYZ', 'VED', 'XAU', 'usd', 'chf', + 'ZZ', 'UK', 'XX', 'us', 'AAA', '', ' CH', 'CH ', + ]; + + it('agrees with isValueDomainMember on every domain, value by value', () => { + for (const domain of ValueDomainSchema.options) { + for (const value of CORPUS) { + expect( + firstRejectedDomainMember(domain, value) === null, + `${domain} / ${JSON.stringify(value)}: the door and the shared predicate must agree`, + ).toBe(isValueDomainMember(domain, value)); + } } }); + + it('agrees element-wise across a multi-value carrier too', () => { + // The walk is the door's own contract, so it is pinned against the + // predicate rather than against a hard-coded expectation. + const carrier = ['USD', 'CHF', 'XYZ', 'EUR']; + const firstBad = carrier.find((c) => !isValueDomainMember('iso_4217_currency', c)); + expect(firstRejectedDomainMember('iso_4217_currency', carrier)) + .toEqual(firstBad === undefined ? null : { value: firstBad }); + }); }); describe('iso_3166_alpha2 — the explicit code list, now carried by the spec', () => { @@ -162,10 +199,12 @@ describe('iso_3166_alpha2 — the explicit code list, now carried by the spec', it('admits the whole officially assigned set the shared module publishes', () => { // Was a structural pin on this package's own table (`size === 249`). The - // table moved to `@objectstack/spec/shared`, where its structure is pinned; - // what is worth pinning HERE is stronger and not a duplicate — that the - // door actually admits every one of the published codes, end to end - // through `firstRejectedDomainMember`. + // table moved to `@objectstack/spec/shared`, where its structure and its + // spelling are pinned. What is left here is a PLUMBING pin, and is worth + // saying as exactly that rather than as something stronger: it loops the + // published set through a door that answers from the published set, so it + // cannot detect a wrong table — it detects the door failing to reach the + // right one, over the whole population rather than a sample. expect(ISO_3166_ALPHA2_CODES.size).toBe(249); for (const code of ISO_3166_ALPHA2_CODES) { expect(code).toMatch(/^[A-Z]{2}$/); diff --git a/turbo.json b/turbo.json index 1f5d4c96d5..af038fcdef 100644 --- a/turbo.json +++ b/turbo.json @@ -24,7 +24,7 @@ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/content/**"] }, "@objectstack/service-settings#test": { - "dependsOn": ["build"], + "dependsOn": ["^build"], "outputs": [], "inputs": [ "$TURBO_DEFAULT$", From 6f2f77edf9be1837b69f7e5a87386115eb4a71f8 Mon Sep 17 00:00:00 2001 From: os-warren Date: Fri, 4 Sep 2026 22:14:19 +0000 Subject: [PATCH 4/5] test(service-settings): make the ratchet delimiter-blind, and pin domain agreement over populations rather than samples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 2 of the contract review found both instruments narrower than their own headers claimed, and measured exactly how much. R1' — every scan was single-quote-shaped, so a VERBATIM double-quoted copy of the array mutation passed the whole file green: the import pin read `/\bfrom\s+'([^']+)'/` and both shape regexes hard-coded `'`. This package has no `quotes` lint rule active, so both spellings are legal here and only these pins can tell them apart. - the import surface is read as `/\bfrom\s*(['"])([^'"]+)\1/` — either delimiter, and the space after `from` optional; - the two table shapes accept `'`, `"` and `` ` ``; - a third shape needs no quotes and no import at all — the 249 codes as a regex alternation inside the door — so it is caught by DENSITY rather than spelling: seven or more bare two-uppercase-letter tokens separated by one or two non-alphanumerics, checked on the door, which is the only place the shape can live now that the import pin holds. All three scans hit 0 of the 31 runtime sources on this head. R2' — the agreement pin covered a corpus, and its header claimed it covered "a curated allow-list". Measured false: a door answering `iso_4217_currency` from a ten-entry `.includes` list passed the whole package green, because all four sampled currency members were inside that list. Agreement is now asserted over each domain's whole POPULATION — every code `Intl.supportedValuesOf` gives for currency and time zone, and the published 249 for alpha-2 — with the corpus kept for the traps no population contains. The host enumeration is the POPULATION here, never the ORACLE, which is what made the round-1 version a time bomb: each code is put to BOTH the door and the shared predicate and the two must answer alike, so a code the snapshot lacks is false on both sides and a code the host lacks is never asked. No ICU build can redden a correct door — or hide a divergent one. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y --- ...value-domains.shared-predicate.pin.test.ts | 51 +++++++++-- .../src/value-domains.test.ts | 91 ++++++++++++++----- 2 files changed, 114 insertions(+), 28 deletions(-) diff --git a/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts b/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts index 953b3fb036..91c2cceff3 100644 --- a/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts +++ b/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts @@ -46,11 +46,24 @@ * nothing else. A table can exist anywhere in the tree without harm as long as * the door cannot reach it, and a relative import is how it would. * + * Every scan below is DELIMITER-AGNOSTIC. It was not, and that is the round-2 + * finding: a verbatim double-quoted copy of the array mutation passed the + * whole file green, because the import pin and both shape regexes hard-coded + * the single quote. This package has no `quotes` lint rule active, so both + * spellings are legal here and only these pins can tell them apart. The + * space after `from` is optional for the same reason. + * + * The alternation shape (`/^(?:AD|AE|…|ZW)$/`) needs neither quotes nor an + * import, so it is caught by DENSITY instead of by spelling, on the door. + * * NOT covered, stated so the claim stays the size of the evidence: a 3-letter - * currency array is not shape-detected (the two alpha-2 shapes are), and a - * membership table reached through a BARE package specifier rather than a - * relative one would pass the import pin. The import surface being one line - * long is what makes both remote. + * table (a currency list) is not shape-detected in any spelling — the density + * and shape scans are two-letter — and a membership table reached through a + * BARE package specifier rather than a relative one would pass the import pin. + * A currency table is instead reached BEHAVIOURALLY, by the population + * agreement pin in `value-domains.test.ts`, which walks every code this + * runtime enumerates through the door and the shared predicate and requires + * the two to answer alike. * * Test sources are exempt from the scan: they are evidence, not enforcement. */ @@ -111,11 +124,32 @@ describe('value-domains.ts answers from the shared predicate', () => { // membership. A 249-code array literal in a sibling module is harmless // while nothing here can import it, and a relative specifier is how it // would be reached. + // Delimiter-agnostic and space-agnostic on purpose. An earlier draft read + // `/\bfrom\s+'([^']+)'/` — single quotes and a mandatory space — and a + // verbatim double-quoted copy of the mutation below walked straight past + // it: `from "./zz-alpha2-array.js"` is not seen, so the pin reported the + // one legal specifier and passed. ESLint does not close that door either; + // this package has no `quotes` rule active, so both spellings are legal + // here and only this pin can tell them apart. const code = stripComments(readFileSync(DOOR, 'utf8')); - const specifiers = [...code.matchAll(/\bfrom\s+'([^']+)'/g)].map((m) => m[1]); + const specifiers = [...code.matchAll(/\bfrom\s*(['"])([^'"]+)\1/g)].map((m) => m[2]); expect(specifiers).toEqual(['@objectstack/spec/shared']); }); + it('carries no dense run of two-letter tokens in ANY delimiter — the alternation shape', () => { + // A membership table needs no quotes and no import at all: the 249 codes + // as a regex alternation (`/^(?:AD|AE|…|ZW)$/`) inside this very file is a + // table by every meaning of the word, and passes a quote-shaped scan and + // the import pin alike. What is invariant across every spelling is the + // DENSITY: seven or more bare two-uppercase-letter tokens separated by + // one or two non-alphanumerics. Applied to the door alone, which is where + // this shape has to live to matter — the import pin above is what keeps + // one in a sibling module out of reach. + const code = stripComments(readFileSync(DOOR, 'utf8')); + const DENSE = /(?:\b[A-Z]{2}\b[^A-Za-z0-9\n]{1,2}){7}/; + expect(DENSE.test(code), 'the door carries a dense run of two-letter tokens').toBe(false); + }); + it('declares no membership machinery of its own', () => { const src = readFileSync(DOOR, 'utf8'); // The three copies that were here, by the shape each took. Comments in @@ -136,8 +170,11 @@ describe('no membership table anywhere in this package', () => { // space-separated string this package used to carry, and the array literal // a fresh re-typing produces — an earlier draft checked only the first and // a 249-element array passed it green. - const SPACED = /'(?:[A-Z]{2} ){7}/; - const ARRAY = /(?:'[A-Z]{2}',\s*){7}/; + // Both shapes, and — the round-2 finding — EVERY delimiter. The first + // draft of these two regexes hard-coded the single quote, so a verbatim + // double-quoted copy of the same table passed the whole file green. + const SPACED = /(['"`])(?:[A-Z]{2} ){7}/; + const ARRAY = /(?:(['"])[A-Z]{2}\1,\s*){7}/; for (const [name, src] of runtimeSources()) { const code = stripComments(src); expect(SPACED.test(code), `${name} carries a space-separated alpha-2 code list`).toBe(false); diff --git a/packages/services/service-settings/src/value-domains.test.ts b/packages/services/service-settings/src/value-domains.test.ts index df03867699..ebafc5dc8d 100644 --- a/packages/services/service-settings/src/value-domains.test.ts +++ b/packages/services/service-settings/src/value-domains.test.ts @@ -30,7 +30,12 @@ */ import { describe, it, expect } from 'vitest'; -import { ISO_3166_ALPHA2_CODES, isValueDomainMember, ValueDomainSchema } from '@objectstack/spec/shared'; +import { + ISO_3166_ALPHA2_CODES, + isValueDomainMember, + type ValueDomain, + ValueDomainSchema, +} from '@objectstack/spec/shared'; import { BUILTIN_VALIDATION_MESSAGES, VALIDATION_MESSAGE_FALLBACK_LOCALE } from '@objectstack/spec/system'; import { firstRejectedDomainMember, @@ -147,20 +152,33 @@ describe('the door is a walker over the shared predicate, not a second judge', ( * The invariant this package actually owns, and the one that survives the * re-point: `firstRejectedDomainMember` adds no membership opinion of its * own — it walks a carrier and asks `isValueDomainMember`. Asserted as - * AGREEMENT over a corpus carrying each domain's traps, so a local filter - * creeping back in (an extra case fold, a length or shape check, a curated - * allow-list) reddens here even though every representative case above would - * still pass. + * AGREEMENT, so a second membership definition inside this door — the exact + * divergence the 2026-09-02 ruling closed — reddens here. + * + * ## Population, not samples — the round-2 finding + * + * A hand-written corpus is not enough, and the measurement says by how much. + * With a four-code currency corpus, a door answering `iso_4217_currency` + * from a ten-entry `.includes` allow-list passed the whole package green + * (529/529): every sampled member happened to be inside the allow-list. So + * agreement is asserted over each domain's whole POPULATION, sampled only + * for the traps that no population contains. + * + * ## Why the host enumeration is safe HERE and was not before * - * ⚠️ Deliberately NOT a probe of `Intl.supportedValuesOf('currency')`. An - * earlier draft asserted that everything the RUN-TIME probe enumerates is - * admitted by the door. That was a time bomb and a duplicate at once: the - * door now answers from the checked-in CLDR snapshot, CI pins only the Node - * MAJOR, and the ICU build moves underneath it — so a host enumerating one - * currency the snapshot lacks would redden a CORRECT implementation, in a - * package with nothing to fix. Probe-versus-snapshot belongs beside the - * snapshot and is already pinned there, in both directions and with a size, - * by `packages/spec/src/shared/value-domain.test.ts`. + * An earlier draft asserted that everything `Intl.supportedValuesOf` + * enumerates is ADMITTED by the door. That made the host's ICU build an + * ORACLE, and the door answers from a checked-in snapshot — so a runtime + * enumerating one code the snapshot lacks reddened a CORRECT implementation, + * in a package with nothing to fix. Below, the enumeration is only the + * POPULATION: each code is put to the door and to the predicate, and the two + * must answer alike. A code the snapshot lacks is false on both sides; a + * code the host lacks is never asked. So no ICU build can redden a correct + * door, and no ICU build can hide a divergent one either. + * + * Probe-versus-snapshot — the definition question — stays where the snapshot + * is, pinned in both directions and with a size by + * `packages/spec/src/shared/value-domain.test.ts`. */ const CORPUS = [ // members, one or more per domain @@ -173,17 +191,48 @@ describe('the door is a walker over the shared predicate, not a second judge', ( 'ZZ', 'UK', 'XX', 'us', 'AAA', '', ' CH', 'CH ', ]; - it('agrees with isValueDomainMember on every domain, value by value', () => { + /** Door and predicate, one value: `true` when both admit, `false` when both refuse. */ + const agrees = (domain: ValueDomain, value: string): boolean => + (firstRejectedDomainMember(domain, value) === null) === isValueDomainMember(domain, value); + + /** Every member of `population` on which the two disagree. */ + const disagreements = (domain: ValueDomain, population: readonly string[]): string[] => + population.filter((v) => !agrees(domain, v)); + + it('agrees with isValueDomainMember on every domain, over the trap corpus', () => { for (const domain of ValueDomainSchema.options) { - for (const value of CORPUS) { - expect( - firstRejectedDomainMember(domain, value) === null, - `${domain} / ${JSON.stringify(value)}: the door and the shared predicate must agree`, - ).toBe(isValueDomainMember(domain, value)); - } + expect(disagreements(domain, CORPUS), `${domain}: door and shared predicate disagree`).toEqual([]); } }); + it('agrees over every ISO 4217 code this runtime enumerates', () => { + // POPULATION, not oracle — see the block comment above. This is the case a + // hand-written corpus cannot cover: measured, a ten-entry allow-list + // inside the door disagrees on 152 of these, and a `!startsWith('X')` + // filter on 7 (XAF, XCD, XCG, XDR, XOF, XPF, XSU); neither moves any + // sampled case. + const intl = Intl as typeof Intl & { supportedValuesOf(k: 'currency'): string[] }; + const population = intl.supportedValuesOf('currency'); + // The population must be real, or every assertion over it is vacuous. + expect(population.length, 'the runtime enumerated no currencies').toBeGreaterThan(100); + expect(disagreements('iso_4217_currency', population)).toEqual([]); + }); + + it('agrees over every IANA zone this runtime enumerates', () => { + // Same form, same reason. The zone domain has no checked-in table on + // either side, so this is a wide agreement corpus rather than a + // divergence-prone one — but a door that started case-folding or + // trimming would show up here and nowhere else. + const intl = Intl as typeof Intl & { supportedValuesOf(k: 'timeZone'): string[] }; + const population = intl.supportedValuesOf('timeZone'); + expect(population.length, 'the runtime enumerated no time zones').toBeGreaterThan(100); + expect(disagreements('iana_time_zone', population)).toEqual([]); + }); + + it('agrees over the whole published ISO 3166-1 alpha-2 population', () => { + expect(disagreements('iso_3166_alpha2', [...ISO_3166_ALPHA2_CODES])).toEqual([]); + }); + it('agrees element-wise across a multi-value carrier too', () => { // The walk is the door's own contract, so it is pinned against the // predicate rather than against a hard-coded expectation. From 17eb7941d2c21e211056f210e9427a3289f319d8 Mon Sep 17 00:00:00 2001 From: os-warren Date: Fri, 4 Sep 2026 22:54:33 +0000 Subject: [PATCH 5/5] =?UTF-8?q?test(service-settings):=20the=20density=20s?= =?UTF-8?q?can=20runs=20package-wide=20=E2=80=94=20a=20second=20judge=20st?= =?UTF-8?q?ands=20in=20front=20of=20the=20door,=20not=20behind=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 3, one change. The density scan ran on the door alone, on the argument that the import-surface pin kept every other table out of reach. That argument is now measured false, and the counter-example is not a missed shape but a missed TOPOLOGY: the same 249-code alternation placed in the door's CALLER — `settings-service.ts`, replacing the `firstRejectedDomainMember(…)` call at the save-path refusal — is never reached by the door. It stands in front of it, answers `iso_3166_alpha2` itself and falls through for the other two. Door blob unchanged, no quoted codes, no new caller of the predicate; every instrument green, whole package green, and a second judge deciding the `value_domain` FieldError on every alpha-2 save. Deleting two codes from it turned two behavioural cases red, so it was live code. - `DENSE` now runs over `runtimeSources()` — the package-wide scope `SPACED` and `ARRAY` already had — and its separator class admits a newline, which also closes the one-code-per-line template literal inside the door. - The header states the division the evidence supports: the import pin covers a table the door REACHES, the density scan covers a judge IN FRONT of it. Widening the scope is backed by census rather than by argument: 0 hits over this package's 31 runtime sources, and 1 hit over 2,186 runtime `.ts` files in 76 `src` roots repo-wide — `packages/spec/src/shared/value-domain.zod.ts`, the shared table itself, which is where the definition belongs. `SPACED` finds the same single file as a positive control. Also from the review, without chasing them: the NOT-covered list now names the five routes measured open (3-character separators, no separator at all, widening by one literal, and the two closed by the toolchain rather than by a pin), the trap corpus seeds the plausible widenings (XK, EU, AN, CS, SU, YU, BTC, CNH, XTS), and the single-caller comment no longer claims a second membership definition reddens — only a divergent one does. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y --- ...value-domains.shared-predicate.pin.test.ts | 115 +++++++++++++----- .../src/value-domains.test.ts | 7 ++ 2 files changed, 91 insertions(+), 31 deletions(-) diff --git a/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts b/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts index 91c2cceff3..421086077c 100644 --- a/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts +++ b/packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts @@ -41,29 +41,57 @@ * that `value-domains.ts` imported and consulted for `iso_3166_alpha2` while * the shared predicate still served the other two. Both now red. * - * The check that closes that second route at its ROOT is the import-surface pin - * below: `value-domains.ts` may import from `@objectstack/spec/shared` and - * nothing else. A table can exist anywhere in the tree without harm as long as - * the door cannot reach it, and a relative import is how it would. + * ## Two routes, and the two checks that are NOT interchangeable + * + * A table the DOOR REACHES is closed at its root by the import-surface pin: + * `value-domains.ts` may import from `@objectstack/spec/shared` and nothing + * else, so a table anywhere in the tree is inert while nothing here can name + * it, and a relative specifier is how it would be named. + * + * A judge STANDING IN FRONT OF THE DOOR is closed by the package-wide density + * scan, and that distinction is a measured falsification, not a design + * flourish. An earlier draft ran the density scan on the door alone and + * argued the import pin covered everything else. It does not: the same + * alternation put in the door's CALLER — `settings-service.ts`, replacing the + * `firstRejectedDomainMember(…)` call at the save-path refusal — is never + * reached by the door, answers `iso_3166_alpha2` itself, and falls through + * for the other two. Door blob unchanged, no quoted codes, no new caller of + * the predicate: every instrument here was green and the whole package was + * green, while a second judge decided the `value_domain` FieldError on every + * alpha-2 save. Deleting two codes from it turned two behavioural cases red, + * so it was live code, not decoration. * * Every scan below is DELIMITER-AGNOSTIC. It was not, and that is the round-2 * finding: a verbatim double-quoted copy of the array mutation passed the * whole file green, because the import pin and both shape regexes hard-coded * the single quote. This package has no `quotes` lint rule active, so both - * spellings are legal here and only these pins can tell them apart. The - * space after `from` is optional for the same reason. + * spellings are legal here and only these pins can tell them apart. The space + * after `from` is optional for the same reason, and the density scan's + * separator class admits a newline — a one-code-per-line template literal is + * the same table. + * + * ## NOT covered — stated so the claim stays the size of the evidence * - * The alternation shape (`/^(?:AD|AE|…|ZW)$/`) needs neither quotes nor an - * import, so it is caught by DENSITY instead of by spelling, on the door. + * Each of these was constructed and measured, and each is left open + * deliberately rather than unnoticed: * - * NOT covered, stated so the claim stays the size of the evidence: a 3-letter - * table (a currency list) is not shape-detected in any spelling — the density - * and shape scans are two-letter — and a membership table reached through a - * BARE package specifier rather than a relative one would pass the import pin. - * A currency table is instead reached BEHAVIOURALLY, by the population - * agreement pin in `value-domains.test.ts`, which walks every code this - * runtime enumerates through the door and the shared predicate and requires - * the two to answer alike. + * - **3-letter tables.** A currency list is not shape-detected in any + * spelling; every scan here is two-letter. Currency is reached + * BEHAVIOURALLY instead, by the population agreement pins in + * `value-domains.test.ts`. + * - **Separators of three characters or more** (`'AD', 'AE',` with padding, + * or a comment between entries) fall outside the density class. + * - **No separator at all** — one 498-character string sliced with + * `.match(/../g)` — has no run to find. + * - **Widening by a single literal** (`|| v === 'XK'`) is invisible to every + * scan here, and the population pins cover NARROWING exhaustively while the + * complement is infinite. The trap corpus seeds the plausible members of + * that complement rather than pretending to close it. + * - **A table reached through a BARE package specifier** rather than a + * relative one would pass the import pin. + * - Two further routes are closed by the toolchain rather than by a pin here, + * and are recorded because a toolchain is not a guarantee: the suite's + * module resolution, and the `tsup` es2020 target. * * Test sources are exempt from the scan: they are evidence, not enforcement. */ @@ -136,20 +164,6 @@ describe('value-domains.ts answers from the shared predicate', () => { expect(specifiers).toEqual(['@objectstack/spec/shared']); }); - it('carries no dense run of two-letter tokens in ANY delimiter — the alternation shape', () => { - // A membership table needs no quotes and no import at all: the 249 codes - // as a regex alternation (`/^(?:AD|AE|…|ZW)$/`) inside this very file is a - // table by every meaning of the word, and passes a quote-shaped scan and - // the import pin alike. What is invariant across every spelling is the - // DENSITY: seven or more bare two-uppercase-letter tokens separated by - // one or two non-alphanumerics. Applied to the door alone, which is where - // this shape has to live to matter — the import pin above is what keeps - // one in a sibling module out of reach. - const code = stripComments(readFileSync(DOOR, 'utf8')); - const DENSE = /(?:\b[A-Z]{2}\b[^A-Za-z0-9\n]{1,2}){7}/; - expect(DENSE.test(code), 'the door carries a dense run of two-letter tokens').toBe(false); - }); - it('declares no membership machinery of its own', () => { const src = readFileSync(DOOR, 'utf8'); // The three copies that were here, by the shape each took. Comments in @@ -182,6 +196,42 @@ describe('no membership table anywhere in this package', () => { } }); + it('carries no dense run of two-letter tokens, in ANY delimiter or none — the alternation shape', () => { + // A membership table needs no quotes and no import at all: the 249 codes + // as a regex alternation (`/^(?:AD|AE|…|ZW)$/`) is a table by every + // meaning of the word, and passes a quote-shaped scan and the import pin + // alike. What is invariant across those spellings is DENSITY: seven or + // more bare two-uppercase-letter tokens separated by one or two + // non-alphanumerics — a newline among them, which is how a + // one-code-per-line template literal spells the same thing. + // + // PACKAGE-WIDE, and the reason is a measured falsification rather than + // caution. An earlier draft ran this on the door alone, arguing that the + // import pin kept a table in a sibling module out of reach. It does — but + // only a table the door REACHES. The alternation put in the door's CALLER + // (`settings-service.ts`, replacing the `firstRejectedDomainMember(…)` + // call at the save-path refusal) is never reached by the door at all: it + // stands IN FRONT of it, answers `iso_3166_alpha2` itself, and falls + // through for the other two. Door blob unchanged, no quoted codes, no new + // caller of the predicate — every other instrument here green, and the + // whole package green, while a second judge decided the `value_domain` + // FieldError on every alpha-2 save. + // + // The false-positive exposure is a census, not a hope: this pattern hits + // 1 of 1,885 runtime `.ts` files repo-wide (49 `src` roots, comments + // masked) — the shared module `value-domain.zod.ts` in the spec package, + // i.e. the table itself, which is the one place the definition belongs. + // (Named without a repo-relative path on purpose: this test does not READ + // that file, and `check:cross-package-test-inputs` reads a spelled path as + // a declared input. Its scan is source text, comments included.) Quoted + // arrays, string enums (`MO = 'MO'`), unions and `{ AD: 1, … }` maps do + // not trip it: quotes and digits push the separator past two characters. + const DENSE = /(?:\b[A-Z]{2}\b[^A-Za-z0-9]{1,2}){7}/; + for (const [name, src] of runtimeSources()) { + expect(DENSE.test(stripComments(src)), `${name} carries a dense run of two-letter tokens`).toBe(false); + } + }); + it('probes no Intl enumeration on any enforcement path', () => { for (const [name, src] of runtimeSources()) { expect(stripComments(src), `${name} probes an Intl enumeration`).not.toContain('supportedValuesOf'); @@ -191,7 +241,10 @@ describe('no membership table anywhere in this package', () => { it('states the membership question exactly once, as the shared call', () => { // Positive half of the ratchet: the absence checks above are satisfiable // by deleting the enforcement altogether, so pin that the call is present - // and that exactly one file makes it. + // and that exactly one file makes it. Note the honest limit, the same one + // this file's header states: what reddens for a DIVERGENT second + // definition may pass for an identical duplicate, which is why the shape + // scans exist beside the behavioural pins rather than instead of them. const callers = runtimeSources().filter(([, src]) => stripComments(src).includes('isValueDomainMember(')); expect(callers.map(([name]) => name)).toEqual(['value-domains.ts']); }); diff --git a/packages/services/service-settings/src/value-domains.test.ts b/packages/services/service-settings/src/value-domains.test.ts index ebafc5dc8d..4af833ced1 100644 --- a/packages/services/service-settings/src/value-domains.test.ts +++ b/packages/services/service-settings/src/value-domains.test.ts @@ -189,6 +189,13 @@ describe('the door is a walker over the shared predicate, not a second judge', ( 'Mars/Olympus', 'Europe/Munich', 'Not A Zone', 'XYZ', 'VED', 'XAU', 'usd', 'chf', 'ZZ', 'UK', 'XX', 'us', 'AAA', '', ' CH', 'CH ', + // Plausible WIDENINGS — the complement the population pins cannot cover. + // A population walk catches any narrowing exhaustively; the set of values + // a second judge might wrongly ADMIT is infinite, so the corpus seeds the + // ones a real editor would reach for: a user-assigned code in live use + // (XK), a supranational alias (EU), and codes withdrawn from the standards + // (AN, CS, SU, YU, BTC, CNH, XTS). + 'XK', 'EU', 'AN', 'CS', 'SU', 'YU', 'BTC', 'CNH', 'XTS', ]; /** Door and predicate, one value: `true` when both admit, `false` when both refuse. */