Skip to content

feat(plugin-audit): AuditPluginOptions.getLocale, a host locale resolver asked before the settings-derived locale - #22324

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-22318-audit-host-locale
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-22318-audit-host-locale

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22318
Clause-②: yes (widening)

Executes ruling D as the card quotes it (objectstack-ai/cloud#2435 item 14, director record 6063207364): plugin-audit gains one optional host option, a locale resolver, consulted first and falling back to the settings-derived locale when it answers nothing. Dispatched under claim 6064241770 (session session_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).
  • The plugin's getLocale closure asks it first. A usable answer is returned; otherwise the closure runs the same resolveLocalizationContext({ 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.
  • Docs. The option's TSDoc states the precedence (host first, settings second). The package README (## Usage, new subsection "Activity summary language") lists the member with its fallback table. content/docs/permissions/record-view-auditing.mdx said "readAudit is the only key AuditPluginOptions declares", 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.
  • Changeset: @objectstack/plugin-audit minor, additive.
  • Untouched: audit-writers.ts, packages/spec. No read-time rendering, no per-organization settings, no second locale source.

Decisions taken in this lane

  • What counts as an answer. undefined, null and 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.getCanonicalLocales accepts it), and is used in canonical form (zh-cn becomes zh-CN). Anything else (zh_CN, a number) falls back to the settings-derived locale and is logged once at warn.
    • Measured: the settings-derived locale gets no support check at this seam either. coerceLocale in packages/core/src/security/resolve-authz-context.ts only trims it and drops a blank value. Its membership check (the localization.locale select options) lives at the settings write door, which a host answer never passes. A membership check against the i18n service's getLocales() was weighed and not taken, because providers disagree on what they serve. The memory i18n negotiates zh to zh-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.
  • A throw falls through, logged once at warn with the error. Measured today: resolveWriteLocale (audit-writers.ts, the writer's memoized lookup) catches a throwing getLocale and 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 through init, start and kernel:ready with a real memory i18n and a settings occupant answering ja-JP, so the host locale, the settings fallback and English are distinguishable.

  • Pin: with getLocale answering zh-CN, a create writes sys_activity.summary = 创建了 Qualification "OC-00001", and the resolver is asked with ('org-1', 'user-1').
  • A synchronous zh-cn answer counts and is canonicalized.
  • Fallback: undefined, null and a blank string give the settings-derived Japanese summary, with no warning.
  • Absent: no option, and {}, give the settings-derived summary. No option and no settings service gives English.
  • Throw: two scopes, two throws, both summaries in the settings locale, both ledger rows written, exactly one warn carrying { err: 'locale store unreachable' }. A rejected promise behaves the same way.
  • Malformed: zh_CN gives the settings locale and one warn naming the answer.

Runs at 5fc8c76371, under scripts/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, including check:test-typecheck: OK … 0 file(s) / 0 error(s).

Ablation: the plugin ignores the option

Run on committed head 723b1f599e through node scripts/ablation-replace.mjs, which holds the restore on a trap. The anchor if (readHostLocale) { (hit once) became if (readHostLocale && false) {.

  • The tool's on-disk proof: anchor x1 -> x0, replace x0 -> x1, blob d346f4403902 became 26dbb44c82f3.
  • audit-plugin-host-locale.test.ts: Tests 5 failed | 3 passed (8). The pin received Qualification「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.
  • Restore: blob after restore d346f4403902 == blob at HEAD, git diff HEAD empty. An independent git hash-object gave the same answer.
  • No dist/ step was needed. The test imports ./audit-plugin.js relatively, so vitest reads the source.

Gates

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 5fc8c76371 (after merging origin/main) derived 95 commands. All 95 ran. 92 exited 0 on the first pass.
    • Three refused at the first pass with PREREQUISITE NOT MET (exit 3): check:skill-examples, check:dual-build-cjs-loads and check:i18n. Each exited 0 after a workspace build.
    • --ran: ✓ dispatch-gates --ran: 95 derived famil(ies) accounted for — 95 run, 0 NOT-MEASURED.
  • Roster families whose roster sits under a touched directory, plus two named by the card's kind: check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:published-readme-exports and check:engine-double-contract. All exit 0.
  • Lint, narrowed and declared: eslint --no-inline-config --format json over the two changed .ts files reports 2 files, 0 errors and 0 warnings. A file eslint ignored would show a warning, so 0 warnings means both were linted.
    • The population read from eslint.config.mjs: **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}, so the .md/.mdx edits are outside it.
    • The config enables no type-aware linting (no parserOptions.project), so this diff cannot move a verdict on any untouched file.
    • The repo-wide pnpm lint is CI's.

Acceptance notes

  • AuditPluginOptions is not re-exported from the package index, as before this change. A host can pass an object literal, but can name the type only through ConstructorParameters of AuditPlugin. Not changed here. Noted, not filed; carrier: none.
  • resolveWriteLocale in audit-writers.ts swallows a throwing getLocale without a log. The plugin's own closure no longer reaches that path for a host fault, because resolveLocalizationContext never throws and the host step catches. A direct caller of the exported installAuditWriters still can. Noted, not filed; carrier: none.

Generated by Claude Code

claude added 4 commits October 8, 2026 16:27
…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>
@github-actions github-actions Bot added the size/m label Oct 8, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-audit, touching 4 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/plugins/plugin-audit/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/record-view-auditing.mdx (via AuditPluginOptions (symbol, a top-level interface), getLocale (symbol, a field of interface AuditPluginOptions), getLocale (literal, a string literal in createHostLocaleReader))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-audit/README.md) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 9 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 28bff18d0c4013db86d61eba87c739c3145e17fd → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d06554839e89d8601690455855df0c4b15459837 — the merge of head 1bdf559a31f27e9a2c6d63a600ac6e584c84c226 into base 28bff18d0c4013db86d61eba87c739c3145e17fd, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 28bff18d0c4013db86d61eba87c739c3145e17fd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

claude added 2 commits October 8, 2026 17:41
…tle only; the plugin emits no assignment notification

Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 18:33
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 18:33
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 3599fef Oct 8, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22318-audit-host-locale branch October 8, 2026 19:02
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