Repository navigation
feat(plugin-audit): AuditPluginOptions.getLocale, a host locale resolver asked before the settings-derived locale - #22324
Conversation
…ver asked before the settings-derived locale Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
…Audit; changeset Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
… localization resolver requires Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 9 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 d06554839e89d8601690455855df0c4b15459837 && git checkout d06554839e89d8601690455855df0c4b15459837
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 28bff18d0c4013db86d61eba87c739c3145e17fd 1bdf559a31f27e9a2c6d63a600ac6e584c84c226 && git checkout -B drift-repro 28bff18d0c4013db86d61eba87c739c3145e17fd && git merge --no-ff 1bdf559a31f27e9a2c6d63a600ac6e584c84c226
node scripts/docs-audit/affected-docs.mjs --json 28bff18d0c4013db86d61eba87c739c3145e17fd
|
…tle only; the plugin emits no assignment notification Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #22318
Clause-②: yes (widening)
Executes ruling D as the card quotes it (objectstack-ai/cloud#2435 item 14, director record
6063207364):plugin-auditgains one optional host option, a locale resolver, consulted first and falling back to the settings-derived locale when it answers nothing. Dispatched under claim6064241770(sessionsession_01WkL6Eijt432S1Y7ekb6ovQ).What changes
AuditPluginOptions.getLocale(packages/plugins/plugin-audit/src/audit-plugin.ts), typed(tenantId?: string, userId?: string) => string | undefined | Promise of (string | undefined). The name is the ruling's own spelling and the name of the writer-layer seam it feeds (installAuditWriters'getLocale).getLocaleclosure asks it first. A usable answer is returned; otherwise the closure runs the sameresolveLocalizationContext({ ql, settings, tenantId, userId })read it ran before. With the option absent, the host step is never built and the closure is the old one, line for line.## Usage, new subsection "Activity summary language") lists the member with its fallback table.content/docs/permissions/record-view-auditing.mdxsaid "readAuditis the only keyAuditPluginOptionsdeclares", which this change makes false; that sentence now names the second key. The README carried the same sentence and is corrected in the same edit.@objectstack/plugin-auditminor, additive.audit-writers.ts,packages/spec. No read-time rendering, no per-organization settings, no second locale source.Decisions taken in this lane
undefined,nulland a blank string are "no answer" and fall back silently. A non-empty string is accepted when it is a well-formed BCP-47 tag (Intl.getCanonicalLocalesaccepts it), and is used in canonical form (zh-cnbecomeszh-CN). Anything else (zh_CN, a number) falls back to the settings-derived locale and is logged once atwarn.coerceLocaleinpackages/core/src/security/resolve-authz-context.tsonly trims it and drops a blank value. Its membership check (thelocalization.localeselect options) lives at the settings write door, which a host answer never passes. A membership check against the i18n service'sgetLocales()was weighed and not taken, because providers disagree on what they serve. The memory i18n negotiateszhtozh-CN, and the file adapter looks locales up exactly, so an exact list would refuse answers a deployment does serve. A locale the i18n service has no catalog for degrades the way a settings-derived one does: to the service's declared fallback, then to the English literal. The locale is never stored on the row; it only picks the catalog.warnwith the error. Measured today:resolveWriteLocale(audit-writers.ts, the writer's memoized lookup) catches a throwinggetLocaleand answers no locale, so a throw left to propagate would give English summaries for that scope, silently. Catching it in the plugin limits a host fault to the host's half: the settings locale still applies, and the audited write lands. "Once" is per install, because the writer asks per tenant/user scope and a broken resolver would otherwise log once per scope. A rejected promise is handled the same way.Tests
packages/plugins/plugin-audit/src/audit-plugin-host-locale.test.ts(8 cases). They boot the plugin throughinit,startandkernel:readywith a real memory i18n and a settings occupant answeringja-JP, so the host locale, the settings fallback and English are distinguishable.getLocaleansweringzh-CN, a create writessys_activity.summary=创建了 Qualification "OC-00001", and the resolver is asked with('org-1', 'user-1').zh-cnanswer counts and is canonicalized.undefined,nulland a blank string give the settings-derived Japanese summary, with no warning.{}, give the settings-derived summary. No option and no settings service gives English.warncarrying{ err: 'locale store unreachable' }. A rejected promise behaves the same way.zh_CNgives the settings locale and onewarnnaming the answer.Runs at
5fc8c76371, underscripts/pm/os-verify-lock.sh:pnpm --filter @objectstack/plugin-audit exec vitest run --maxWorkers=2:Test Files 42 passed (42),Tests 657 passed (657).pnpm --filter @objectstack/plugin-audit typecheck: exit 0, includingcheck:test-typecheck: OK … 0 file(s) / 0 error(s).Ablation: the plugin ignores the option
Run on committed head
723b1f599ethroughnode scripts/ablation-replace.mjs, which holds the restore on a trap. The anchorif (readHostLocale) {(hit once) becameif (readHostLocale && false) {.anchor x1 -> x0,replace x0 -> x1, blobd346f4403902became26dbb44c82f3.audit-plugin-host-locale.test.ts:Tests 5 failed | 3 passed (8). The pin receivedQualification「OC-00001」を作成しました(the settings locale) where it expected the Chinese summary. The canonical, throw, rejected and malformed cases also went red. The fallback and absent cases stayed green, as expected, because they do not depend on the option.blob after restore d346f4403902 == blob at HEAD,git diff HEADempty. An independentgit hash-objectgave the same answer.dist/step was needed. The test imports./audit-plugin.jsrelatively, so vitest reads the source.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat5fc8c76371(after mergingorigin/main) derived 95 commands. All 95 ran. 92 exited 0 on the first pass.PREREQUISITE NOT MET(exit 3):check:skill-examples,check:dual-build-cjs-loadsandcheck:i18n. Each exited 0 after a workspace build.--ran:✓ dispatch-gates --ran: 95 derived famil(ies) accounted for — 95 run, 0 NOT-MEASURED.check-changeset-fixed,check:authz-resolver,check:error-code-casing,check:filter-alias-parity,check:published-readme-exportsandcheck:engine-double-contract. All exit 0.eslint --no-inline-config --format jsonover the two changed.tsfiles reports 2 files, 0 errors and 0 warnings. A file eslint ignored would show a warning, so 0 warnings means both were linted.eslint.config.mjs:**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}, so the.md/.mdxedits are outside it.parserOptions.project), so this diff cannot move a verdict on any untouched file.pnpm lintis CI's.Acceptance notes
AuditPluginOptionsis not re-exported from the package index, as before this change. A host can pass an object literal, but can name the type only throughConstructorParametersofAuditPlugin. Not changed here. Noted, not filed; carrier: none.resolveWriteLocaleinaudit-writers.tsswallows a throwinggetLocalewithout a log. The plugin's own closure no longer reaches that path for a host fault, becauseresolveLocalizationContextnever throws and the host step catches. A direct caller of the exportedinstallAuditWritersstill can. Noted, not filed; carrier: none.Generated by Claude Code