Skip to content

service-settings: lock localization.currency once a sys_exchange_rate row or a homeAmount declaration exists; Setup shows what uses it (multi-currency M1-3, epic #8345) #27198

Closed

Description

@objectstack-fleet

Blocked-by: #27185
Blocked-by: #27191

Filing gate ③ (maintainer-directed). Ruling 6465431966 on #27119 (batch #421 item 2, Q1 = 乙-现在, maintainer 「同意立专题卡」 2026-10-28): the multi-currency epic resumes on #8345 in shape C′. Part of #8345. Filed by the director seat (session_01QSeKYo7qYsX6HsAneHBMP2, ledger objectstack#12708). ⛔ Not a claim. Workload row and precedents: 6464936321; design: 6464438764.

Ruling-ref: 6465431966

维护者速读

  • 改了什么: 有汇率行或影子列声明后,组织默认币种不可再改;Setup 明示「已被 N 个对象 / M 条汇率使用」。
  • 为什么改: 改本位币 = 所有历史影子列失效;今天改租户币种就把所有历史金额换标签(探针 B)。
  • 风险与回滚: 新机制:settings 今天没有写侧谓词钩子;允许先降级为「响亮警告 + 审计」落地,锁作为第二步。
  • 先看: packages/services/service-settings/src/(config-change-audit.ts 旁)、localization.manifest.ts。
  • 两分钟验证: 录了一条汇率后改币种被拒并列出使用者。
  • 需要您做的: 无。

Task

  • A write-side predicate for the manifest key, reading the engine (any rate row; any object whose metadata declares homeAmount).
  • Setup copy naming the users.
  • Tests: before / after the first rate row.

Lane: server · Size: M · Review: default. Clause-②: no · Metadata-protocol: none. New mechanism: name it in the PR body.

Epic: #8345 (parent; the epic PM dispatches pm:epic children; domain:spec children go to the spec seat's queue). Dedupe: #8345 thread, #27119 (ruled), the workload list 6464936321; no open card carries this slice.

Activity

  1. added
    enhancementNew feature or request
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 28, 2026
  2. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5

    Claim: PM loop round 1 · 2026-10-31T02:56Z
    Session: session_01Utp6CbTZrFqRYNgjSaSbN5 (epic PM, epic:#8345)
    Account: os-bill
    Branch: claude/project-thread-1bmr1f (card thread session_01Utp6CYUAEJV7QpzuJm62Te; the platform assigns the thread's one push branch — the recorded platform exception), cut from PR #28096's head cf13692214 (#27191 not yet on main; Merge-after: #28096, main merged in once it lands, never rebased)
    Worktree: objectstack-issue-27198
    Domain: domain:server
    Seat: epic:#8345
    File surface: packages/services/service-settings/src/** (the write-side predicate on localization.currency, its Setup copy and translations, tests), .changeset/*; ⛔ no governed path, ⛔ no packages/spec/src/**, ⛔ no packages/objectql write path (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: opus (TIER_DEFAULT; dispatch-gates --tier: no path-derived mandate), effort: default (the platform's; only the maintainer sets thread effort)
    Clause-②: no — to confirm against the tree (a new refusal on a settings save is a narrowing; say which)
    Metadata-protocol: none — to confirm against the tree
    Responsibility: n/a — not a defect card
    Thread-read: none
    Premise: holds
    Serial constraints cleared: #27185 landed (PR #27246, 194e66c0); #27191 approved and in auto-merge (PR #28096), built on its head with Merge-after: #28096.
    Release pairing: R2 — ⛔ not released alone; R2's release waits for the customer's terms (ruling 6522614678).
    Maintainer authority: Bill, project thread, 2026-10-28 「epic的卡你统一派」, 「并发可以5」, 「你自主完成后续阶段」; ruling 6522614678 (letter A, 「A 现在启动 R2 开发」).

  3. os-bill commented on Oct 31, 2026

    @os-bill
    Collaborator

    epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5
    os-dev-report

    {
      "issue": 27198,
      "status": "done",
      "branch": "claude/project-thread-1bmr1f",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/28297",
      "head": "5e49fa73b3b40666dfcd0bd22116cdea5ccc1a3e",
      "session": "session_01Utp6CYUAEJV7QpzuJm62Te",
      "premise_still_valid": true,
      "summary": "Real lock, not the warning-first step: the settings write path hosts a predicate cleanly. New mechanism, named in the PR: the settings write predicate, SettingsService.registerWritePredicate(namespace, key, predicate). setMany asks it only when the write changes the key (submitted vs the value the caller's cascade resolves), after validatePatch and before the crypto pass and every write; refusals refuse the whole batch as SettingsValidationError, invalid_value at the key, with constraint. The lock (localization-currency-lock.ts), registered by SettingsServicePlugin right after bindEngine: used = the organization's sys_exchange_rate rows (system count, explicit organization filter) or any registered object declaring currencyConfig.homeAmount on a field. A change to a SET currency (other code or clear) is refused while used; the message names N objects (labels) and M exchange rates; constraint { lockedBy: 'in_use', objects, exchangeRates }. Unset -> first value passes (ADR-0144 Open item 4, this card's half). Setup copy = the Localization page's live-status banner (locked: info; unset but needed: warning; nothing while unused), in en / zh-CN / ja-JP / es-ES, picked from the request's Accept-Language (else ?locale) and passed as SettingsDisplayOptions beside the context, never inside it (the door pins hold context identity). Base: #28096's head cf136922 (not on main); PR says Merge-after: #28096; main merges in once it lands, no rebase. Declarations: Clause-②: yes (widening) for the new service-settings exports; Metadata-protocol: none; changeset service-settings patch. Corrected from the card's Clause-② no: the new exports widen the public face; the refusal narrows nothing released today (sys_exchange_rate and homeAmount are in no published version).",
      "tests": "head 5e49fa73: @objectstack/service-settings 67 files / 1107 tests passed; tsc --noEmit exit 0; eslint on touched files exit 0. New: settings-write-predicate.test.ts 7/7, localization-currency-lock.test.ts 24/24 (save succeeds before the first rate row and before any homeAmount; refused after each, message names the users; clear and namespace reset refused; re-save passes; unset -> set passes; lock lifts; refusal and banner in all four locales; PUT 400 SETTINGS_VALIDATION). Ablations, each restored to HEAD: setMany call removed 16/31 red; isUsed -> false 13 red; homeAmount detector blinded 8 red; organization filter dropped 2 red; change gate dropped 2 red.",
      "gates": "dispatch-gates --commands on 5e49fa73: 63 commands (9 paths vs #28096 head). All ran with exit captured before any pipe: 62 exit 0, 1 exit 3 (check:dual-build-cjs-loads, PREREQUISITE NOT MET, needs every package's dist; CI Build Core measures it). dispatch-gates --ran: 63 accounted, 62 run, 1 NOT-MEASURED, exit 0. Records: /mnt/project-files/epic-8345/gates-27198-{cmds,ran,ran-verdict}.txt. CI on the PR: not yet read at report time.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "5 — issue_read #27198, pull_request_read #28096, create_pull_request #28297 (draft, no assignee, no reviewers), subscribe_pr_activity #28297, add_issue_comment (this report)",
      "api_writes": "git push to claude/project-thread-1bmr1f; MCP create_pull_request #28297; MCP add_issue_comment on #27198. No fleet relay credentials in this container.",
      "deviations": [
        "PR body first line is the seat signature, attribution block on lines 3-4 (the precedent of #28128).",
        "WIP commit 1fb66e7c carries the epic PM's session id in its Claude-Session trailer by mistake; later commits carry this thread's session. The branch cannot be force-pushed (criterion 1), so it stays."
      ],
      "open_questions": [
        "Version Packages #26517 (18.3.0) already consumed .changeset/27185-exchange-rate-table.md (changeset-release/main 1f5d5a8e). If 18.3.0 ships before R2, sys_exchange_rate is released ahead of its R2 pair, and this PR's refusal then narrows a released state: the arm must become (narrowing) with an ADR-0087 disposition before R2.",
        "homeAmount declarations are deployment metadata, so per the card's predicate every organization's set currency locks once one exists, even an organization with no converted rows. Kept as written; narrowing it to organizations that hold converted rows would need a data read across objects."
      ],
      "out_of_scope_findings": [
        "class: release-pairing; reach: changeset-release/main 1f5d5a8e carries #27185's changeset in 18.3.0 while #28096 lists #27185 in the R2 set; carrier: epic PM / Bill (Version Packages is the maintainer's hand)"
      ]
    }

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpm:epicpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions