Skip to content

fix(service-settings)!: retire date_format, time_format, number_format and first_day_of_week from the Localization manifest (#21958) - #21970

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21958-retire-locale-format-settings
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21958-retire-locale-format-settings

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21958
Clause-②: no (narrowing)

Executes the maintainer's ruling recorded on objectstack-ai/objectui#11675 (comment 6010771237, decision batch 2, item 1, B, verbatim 「同意」): dates, times, numbers and the first day of the week follow the user's locale, and date_format, time_format, number_format and first_day_of_week leave the Localization settings without being implemented. objectui#11675 remains open for its own remainder (the calendar and timeline week start); this PR does not touch it.

What changes

packages/services/service-settings only:

  • src/manifests/localization.manifest.ts: the four specifiers leave, with the Formats group they made up (it would have been an empty header). timezone, locale, default_country, currency and fiscal_year_start are unchanged. version goes from 1 to 2, because the spec's SettingsManifestSchema.version contract reads "Manifest version. Increment when keys are renamed/removed." The namespace description and the module docblock say what replaced the four. While rewriting that docblock I corrected its stale function name (resolveExecutionContext no longer exists; resolveLocalizationContext in @objectstack/core is what reads timezone, locale and currency).
  • src/translations/{en,es-ES,ja-JP,zh-CN}.ts: the four key entries and the formats group leave each bundle, and the namespace description follows the manifest.
  • Tests: see below.
  • .changeset/21958-retire-locale-format-settings.md: @objectstack/service-settings minor, BREAKING banner, Clause-②: no (narrowing), ADR-0087 disposition not-required (no-migration-prescription). The four were settings values in sys_setting, not metadata, so objectstack migrate meta has nothing to rewrite.

No packages/spec, skills/** or content/docs/releases/ edit.

Stored values: measured, not assumed

Measured twice through the real REST handlers (registerSettingsRoutes over SettingsService): on origin/main 80f9f7e6ba, with a copy of the manifest minus the four registered over a store that already held a date_format row, and on this branch with the shipped manifest. Both gave the same answers:

Door Result after the retirement
GET /api/settings/localization 200. The four are absent from manifest.specifiers and from values; the stored row is loaded with the namespace but never resolved.
PUT /api/settings/localization naming one of them 400 UNKNOWN_KEY, details: { namespace: 'localization', key }. The refusal covers the whole batch: a timezone sent beside it does not land.
PUT of a live key, with the stale row in the store 200. The stale row never blocks a save.
built-in reset action 200, "Cleared 1 saved value(s)": it clears the live key and leaves the retired row alone.
in process, settings.get('localization', 'date_format') rejects UnknownKeyError, code: 'SETTINGS_UNKNOWN_KEY'.
registering the old manifest shape again (a rollback) reads DD.MM.YYYY, source tenant: the row survived every step above.
OS_LOCALIZATION_DATE_FORMAT (and the other three) no longer read, and nothing is logged at registration. Before, it set a value that nothing read.

So a stored row is kept, never deleted: ignored on read, refused on write. No service code changed. This is the path every undeclared key already takes.

Precedent, as measured. The dispatch pointed at settings-routes.ts:25 and at settings-service.ts near line 920. Line 25 cites envelope siblings retired from the route module, not a settings key. Line 920 is the retired mail.provider OPTION values (sendgrid, ses), where a stored stale value is kept and judged only when written (the TOUCH gate). No settings key had ever left a manifest before this one. I checked every commit whose diff to a *manifest*.ts outside packages/spec touches a key: line (26 commits, on an unshallowed history of 15,821): no key disappears in any of them, and no manifest's version was ever above 1. Two contracts already in place therefore answer the question: the generic undeclared-key path above, and the spec's own version rule. A retirement entry would have needed a home in packages/spec, and nothing requires one there. SettingsManifest.version has no reader in objectstack or objectui at main (grep for manifest.version in the settings code of both), so the bump is compliance with the declared contract and changes no behaviour.

Docs

No content/docs/** page lists the four as settings. Searched outside references/ and releases/ for the four key names and for "Date format", "Time format", "Number format", "First day of week", "Localization settings" and "date/number formats". The only hits are field-type and i18n-API pages describing other things (data-modeling/validation-rules.mdx, protocol/kernel/i18n-standard.mdx, protocol/kernel/index.mdx). No page needed an edit. The repo-wide reader sweep for the four key names finds only SQL's own date_format() (driver-sql, comments in core and objectql, and the two skills/** rule files, which describe the SQL function), and the export transform enum value 'date_format'. None of these is the settings key.

Tests (head 397edabf04)

  • New pins in src/settings-routes.test.ts: four retired rows are seeded in the store, as seedGlobalSecret in the same file seeds one. Then:
    • GET offers and resolves none of them;
    • PUT naming each is refused, asserting status 400, error.code UNKNOWN_KEY and error.details;
    • a live-key save and the reset action both leave the four stored rows byte-equal.
  • src/manifests/localization.manifest.test.ts: version is 2; the key list is five keys in region/finance. A new negative pin asserts that none of the four, nor the formats group, is offered.
  • src/translations/settings-translation-coverage.test.ts now also checks the other direction, for en and the three translated bundles: no copy for a group or key that no manifest declares. It measured zero such entries across all ten manifests before it was added, so it closes the class rather than pinning only these four.
  • src/settings-service.test.ts: the two "domain-less select keeps exhaustive options" pins used first_day_of_week and date_format as their example key. They now use fiscal_year_start, also a domain-less select, so they pin the same behaviour.
  • pnpm --filter @objectstack/service-settings test: 36 files, 626 tests passed. typecheck (tsc --noEmit) exit 0; --listFiles shows it compiles 36 test files, including all four edited ones. The dependency closure (--filter '@objectstack/service-settings^...') was built first.
  • The built dist loads through both conditions: require('./dist/index.cjs') and import('./dist/index.js') each serve version 2 with five keys.

Ablation (negative pins proven able to fail)

Both ablations used scripts/ablation-replace.mjs (literal anchor that must hit; blob hashes verified; restore by blob to HEAD, git diff HEAD empty):

  1. A retired row put back into the manifest (date_format re-inserted, on-disk count 1). 8 tests went red: the three new route pins, the two manifest pins, and zh-CN, ja-JP and es-ES coverage. The tree was restored to blob e8f3039af917 (equal to HEAD). A first attempt was refused by the tool, which is why there were two: the replacement contained its own anchor, so the anchor count did not drop. Nothing was measured on that attempt.
  2. A translation entry put back (date_format re-added to en.ts with no manifest row). The new reverse-direction pin went red with en has undeclared settings copy: localization.key:date_format. Restored to blob 0b0918e9ece5 (equal to HEAD).

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on the final tree derived 63 commands. That is the dispatch's 49 plus 14 that the changeset and test edits pull in, including check-adr-0087-registration, check-empty-changeset, check:engine-double-contract and check:type-check-coverage. All 63 were run at 397edabf04 with exit codes captured before any pipe. 62 exited 0. pnpm check:dual-build-cjs-loads exited 3, PREREQUISITE NOT MET (53 packages outside this closure have no dist/): NOT MEASURED locally, and CI's full build runs it. The --ran reconciliation answers "63 derived, 62 run, 1 NOT-MEASURED, 0 UNRUN".

ESLint, narrowed: pnpm exec eslint --no-inline-config --format json over the 9 touched .ts files gave 9 files linted, 0 errors and 0 warnings. All 9 are inside the config's population (**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} minus the build dirs). The config enables no type-aware linting (no parserOptions.project), so this diff cannot move the verdict on any untouched file. The repo-wide pnpm lint is CI's.

Acceptance notes (not filed)

  • An OS_LOCALIZATION_* variable naming a retired key is now silently unread. No namespace audits OS_ variables that name an undeclared key (a typo such as OS_LOCALIZATION_TIMEZON is equally silent today). Pre-existing and general, and the value was never read before either. Carrier: none.
  • SettingsManifest.version has no reader anywhere measured, so the "increment when keys are removed" contract is honoured without being enforced. No pull. Carrier: none.
  • objectui's apps/console/src/pages/settings/__tests__/SettingsField.valueDomain.test.tsx uses date_format and first_day_of_week as hand-built fixture keys. It does not import this manifest, so it stays green and is harmless. Carrier: whoever next edits that file, possibly with objectui#11675's remainder.
  • @objectstack/service-settings' exported types are byte-unchanged (the manifest is typed SettingsManifest, and only its value moved), and no package outside this one imports localizationSettingsManifest or the bundles in code. So there is no downstream consumer sweep.

Generated by Claude Code

claude added 2 commits October 6, 2026 07:49
…t and first_day_of_week from the Localization manifest (#21958)

Formats follow the locale (ruled on objectui#11675, B). The four rows, the
now-empty Formats group and their entries in the en, es-ES, ja-JP and zh-CN
bundles leave; the manifest version goes to 2 per the spec's
SettingsManifest.version contract (increment when keys are removed).

A stored sys_setting row for one of the four is kept: it is not resolved on
read, a write naming it is refused 400 UNKNOWN_KEY, and no reset or save of
the live keys rewrites or deletes it. Pinned at the REST surface.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see

Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4c49150e0cb27a493390c4ac24e0b93e13efe662 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0bef7d483b09ff0406151d38c9aa0f58059761bb — the merge of head 397edabf048f3dab63dd651b77d28e00136dbaa1 into base 4c49150e0cb27a493390c4ac24e0b93e13efe662, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0bef7d483b09ff0406151d38c9aa0f58059761bb && git checkout 0bef7d483b09ff0406151d38c9aa0f58059761bb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4c49150e0cb27a493390c4ac24e0b93e13efe662 397edabf048f3dab63dd651b77d28e00136dbaa1 && git checkout -B drift-repro 4c49150e0cb27a493390c4ac24e0b93e13efe662 && git merge --no-ff 397edabf048f3dab63dd651b77d28e00136dbaa1

node scripts/docs-audit/affected-docs.mjs --json 4c49150e0cb27a493390c4ac24e0b93e13efe662

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants