Repository navigation
feat(platform-objects): sys_exchange_rate — an organization's dated exchange rates, admin-written and audited, with a Setup list under Localization - #27246
Conversation
…up list under Localization (WIP) Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
…arget classification (WIP) Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
…, its list and the Setup entry Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
…nership roster Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
…nancy census counts it Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 52 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 174 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin feca2c4fa92178393132e13ed9bb967886ead711 && git checkout feca2c4fa92178393132e13ed9bb967886ead711
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ebfadb02c3a06153ff05af1cb620dfdc29bb6157 7ad4554c39a4142810ebdcfab462967d638d6e49 && git checkout -B drift-repro ebfadb02c3a06153ff05af1cb620dfdc29bb6157 && git merge --no-ff 7ad4554c39a4142810ebdcfab462967d638d6e49
node scripts/docs-audit/affected-docs.mjs --json ebfadb02c3a06153ff05af1cb620dfdc29bb6157
|
…ency ADR by its card, not an unwritten number Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
… 24 (ruling 6469382727) Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
…platform-objects Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
The new tenant-scoped platform table carries organization_id, so `os migrate organization-ownership` refused every plan as carrying an uninventoried platform table. It takes fate 3 (attribution, no anchors: the Default Organization under single, otherwise reported), as the other organization-authored tables added since the first census do. Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
SETUP_NAV_CONTRIBUTIONS gains nav_exchange_rates, so the composition's Setup tree grows from 31 entries to 32. Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: Triage seat, under gap rule (b) (#27062): the PR has been open since 12:33Z with no record or claim on any head (named as due in R731). The review runs at Generated by Claude Code |
Contract reviewServed-tier: Governing shape: ADR-0144 (PR #27239, head ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS Generated by Claude Code |
main deleted content/docs/ui/setup-app.mdx (its Setup anchors moved to content/docs/ui/apps.mdx); the Exchange Rates row and the entry on recording a month's rates move there with it. Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
Requested by Bill · project thread
Fixes #27185
⛔ not released alone — pairs with #27192 (R2); merges in the R2 window
Clause-②: no
Metadata-protocol: none
维护者速读
sys_exchange_rate(币种 ISO 4217、汇率精确小数(小数点后 12 位、总共 24 位,裁决 6469382727)、生效日期、来源 manual/import/app),同组织内 (币种, 生效日期) 唯一;Setup → Configuration 在 Localization 下面加「Exchange Rates」列表页,页面上写明一次汇率方向;文档一行讲财务怎么录一个月的汇率。packages/platform-objects/src/system/sys-exchange-rate.object.ts(唯一键的'organization'拼法、rate的scale/min与方向句);src/plugin.ts(注册进第一个 manifest,走项目库);src/apps/setup-nav.contributions.ts(菜单位置)。pnpm --filter @objectstack/platform-objects exec vitest run src/system/sys-exchange-rate.object.test.ts,预期 15 个测试全绿。What changes
sys_exchange_rate(packages/platform-objects/src/system/sys-exchange-rate.object.ts),managedBy: 'system-data':currency— text,valueDomain: 'iso_4217_currency', required.rate—number(the platform's exact-decimal column, neverfloat),scale: 12,precision: 24,min= 10⁻¹² (strictly positive), required (ruling 6469382727).effective_date— date, required.source— selectmanual/import/app, defaultmanual.(currency, effective_date)spelledunique: 'organization'(ADR-0120 D1), so two organizations each hold their own EUR rate for a day.organization_idis injected and the Layer 0 wall scopes it.enable.trackHistory: trueplus per-fieldtrackHistoryon the four ruled columns; the object is not on plugin-audit's skip list, so every write lands insys_audit_log.organization_admin's'*'grant reaches it (notprivate, norequiredPermissions);member_defaultnames no business object, so members neither read nor write it (ruling parameter 2).EXCHANGE_RATE_DIRECTION(exported) is theratefield's description and theall_rateslist view's description, which the console renders on the list page.EXCHANGE_RATE_SCALE(12) andEXCHANGE_RATE_PRECISION(24) exported beside it.PlatformObjectsPluginregisters it in its first manifest (project database, no routing triple), besidesys_migration/sys_migration_journal/sys_secret: its reader is the ObjectQL engine's write path, present in every composition.@objectstack/spec):PLATFORM_OBJECTS_BY_PACKAGE['platform-objects']listssys_exchange_rate, so lint, the engine and the CLI treat it as a real platform object; the spec ratchetplatform-object-names.test.tsrequires it.@objectstack/cli): the ADR-0131 D10 organization-ownership inventory givessys_exchange_ratefate 3 (attribution, no anchors: the Default Organization undersingle, otherwise reported), like the other organization-authored tables added since the first census; without the rowos migrate organization-ownershiprefuses every plan as carrying an uninventoried platform table.nav_exchange_ratesingroup_configurationdirectly undernav_settings_localization, openingall_rates.en/zh-CN/ja-JP/es-ES(the three translated locales hand-written; bundles regenerated withcheck-i18n-bundles.mjs --write, extract config gains the object).content/docs/ui/apps.mdx(wheremainmoved the Setup anchors when it retiredui/setup-app.mdx) — thegroup_configurationrow and one paragraph saying how finance records a month's rates..changeset/27185-exchange-rate-table.md,@objectstack/platform-objects,@objectstack/specand@objectstack/clipatch.Why
Multi-currency shape C′ (ruling 6465431966 on #27119; epic #8345, M1-1): a record-mode amount is converted to the home currency at write from the rate in force on its document date. This is that rate table. Parameters 2 (admin-only) and 3 (1 home =
rateforeign, so home = amount ÷rate) are applied as ruled; parameter 4 as ruled in 6469382727 (scale 12, precision 24).Tests
src/system/sys-exchange-rate.object.test.ts(15 cases) pins the declaration each enforcing mechanism reads, on the built value: one unique key on(currency, effective_date)equal to'organization';resolveInjectedSystemColumnsplansorganization_id; notprivateand norequiredPermissions(the org-admin wildcard reaches it);resolveCrudAffordancesgives New/Edit/Delete and no import;trackHistoryon object and columns;iso_4217_currencywith the write predicate's member / non-member / lowercase answers;rateisnumberwithscale12,precision24 andmin10⁻¹²; the direction sentence on field and view; the nav entry sits right after Localization and names a declared view.plugin.test.ts(registration roster),platform-objects.test.ts(nav target classified as owned and listable),bundle-ownership.test.ts(bundle roster);packages/cli/test/connect-agent-both-halves-wire.pin.test.ts(the Setup tree the platform admin is served: 31 → 32 entries, 22 → 23 contributions). The spec ratchetplatform-object-names.test.tsand the CLIorganization-ownership-inventory.test.tspass unchanged with the two registry rows.Acceptance notes
default-permission-sets.ts; plugin-auditAUDIT_EXCLUDED_OBJECTS).platform-objectscannot import plugin-security or plugin-audit (they depend on it), and the claimed file surface stops at this package, so no live admin-201 / member-403 / duplicate-409 / cross-org-201 run is recorded: NOT MEASURED, carrier examples + QA: showcaseinvoicegoes multi-currency (code field, record amount, home field, document date), seeded rates, a rate-sync flow template, a platform-checklist item and a browser dogfood run (multi-currency M3-3, epic #8345) #27197 (M3-3 dogfood seeds rates and drives the browser).system-data's default) until import: verifysys_import_jobservessys_exchange_rate(CSV of currency, rate, effective_date); add the importer only if it does not (multi-currency M1-2, epic #8345) #27190 verifiessys_import_jobserves this object; turning it on isuserActions: { import: true }there.sys_exchange_rate{ currency, rate, effective_date, source } — tenant-scoped, admin-only, audited; Setup list view (multi-currency M1-1, epic #8345; released with M2) #27185): scale 12 (twelve digits after the decimal point), total precision 24, decimal arithmetic, never float. The write seam refuses a value beyond either bound rather than rounding it. The column is the platform'sDECIMAL(65,30)(NUMERIC_COLUMN_REPRESENTATION), which holds both.rate(ADR-0144). Card objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192's body writes the projection as amount × rate; that card should follow the ruling.numberas REAL (table.decimalcompiles tofloaton SQLite), and every dialect reads anumberback as a JS double (about 15–17 significant digits). A rate using all 24 digits is beyond that and is not round-tripped exactly; objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192 already owns pinning SQLite rounding.scripts/platform-object-tenancy-census.json, regenerated withnode scripts/platform-object-tenancy-census.mjs --writebecause the new tenant-scoped object joins the census population (87 → 88 registered, 44 → 45 in reach). Derived artefact, no hand edit.packages/spec/src/system/constants/platform-object-names.ts(one name in the registry list; the spec ratchet reds without it).packages/cli/src/utils/organization-ownership-inventory.ts(one inventory row; the CLI inventory test and about 15organization-ownershipintegration tests red without it).packages/cli/test/connect-agent-both-halves-wire.pin.test.ts(the Setup nav count).localization.currencyonce asys_exchange_raterow or ahomeAmountdeclaration exists; Setup shows what uses it (multi-currency M1-3, epic #8345) #27198).platform-objects, because the table's one reader is the ObjectQL engine's write path, present in every composition that can write a record; it registers with the engine-facing infrastructure (sys_migration,sys_secret) and has no on/off capability boundary of its own (ADR-0029 D2). ADR-0144's M1-1 row names the same package.Verification
Final head 7ad4554 (after merging main, fresh install and full build)
Earlier heads: package test + typecheck (lock VERDICT)
Derived gates (dispatch-gates --commands, --ran reconciliation)
Human Minimum
packages/specholds no runtime logic. (The spec change is one name in a constant list.)pnpm lintis CI's.main, one pull request.packages/spectouched only to list the new object inPLATFORM_OBJECTS_BY_PACKAGE, with itspatchbump in the changeset.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code