Skip to content

plugin-audit: AuditPluginOptions gains an optional host locale resolver, consulted before the settings-derived locale, so a multi-organization host writes activity summaries in each organization's language #22318

Description

@objectstack-fleet

Filing gate: a ruled enhancement with a named, measured consumer. The ruling is objectstack-ai/cloud#2435 item 14, letter D (director seat, batch #293 item 3, comment 6063207364), which the maintainer approved with 「同意」 on 2026-10-08T15:23Z. Filed by the repo:cloud seat (repo:cloud#1, R45, session session_011jobP72PwN3whNm55GetXQ), because the option lands in this repo. ⛔ Not a claim.

The ruling, as quoted

D. plugin-audit gains one optional host option, a locale resolver (getLocale(tenantId, userId)), consulted first and falling back to today's settings-derived locale when it returns nothing; the writer layer already takes getLocale, so the option only exposes what the plugin builds internally.

⛔ Not taken: read-time rendering (a change to the storage shape) and per-organization plane settings (they would reopen cloud's D1, under which the control plane's localization is one deployment-wide namespace).

The seam reading (cloud#2435 dev report 6060072840, taken at cloud's pin 56bf27af; the lines below are re-read on main 4e4111ca)

  • No host option exists. AuditPluginOptions is { readAudit } only (packages/plugins/plugin-audit/src/audit-plugin.ts:56-59). Every this.options read in the file is readAudit.
  • The locale is a closure the plugin builds itself (audit-plugin.ts:218-228): resolveLocalizationContext({ ql, settings, tenantId, userId }).locale, handed to installAuditWriters(engine, this.name, { getMessaging, getI18n, getLocale }).
  • The writer layer already takes the seam: the getLocale option on installAuditWriters (audit-writers.ts:130), read through resolveWriteLocale (:1148-1156), which the summary writers call (:1779, :2086).
  • The two near-seams are monkey-patches, and the ruling refuses both:
    • re-calling the exported installAuditWriters under the plugin's package id (it unregisters the plugin's hooks by package, audit-writers.ts:1183-1184);
    • wrapping the settings service (it falsifies the settings read contract for every reader).

The shape

  • AuditPluginOptions gains one optional member. The ruling's wording is a resolver (tenantId?: string, userId?: string) => Promise<string | undefined>; this lane names it.
  • When given, its answer is used first. undefined (or a throw, if this lane decides a throw falls through) falls back to today's resolveLocalizationContext result. With the option absent, behaviour is byte-identical to today.
  • The precedence (host first, settings second) is documented on the option. ⛔ No second locale source is added anywhere else.

The consumer (measured)

  • Who: cloud's control plane builds new AuditPlugin() with no options (packages/service-cloud/src/control-plane-preset.ts:762-765 at cloud main).
  • What it will pass: cloud already has resolveWorkspaceLocale(driver, organizationId) (workspace-invitation-locale.ts:113, cloud#2649). It returns the organization's observed_locale, else its localeSeed, validated, and undefined otherwise.
  • Today: a zh-CN workspace owner reads control-plane 活动动态 rows in English (Created Team "悦来商贸").
  • When cloud gets it: cloud consumes the framework through a pin (.objectstack-sha 56bf27af, held until v18 by objectstack#22050 ruling B). The plane wiring is one line plus its test, dispatched by the cloud seat once the pin moves (cloud#2709).

Tests

  • Pin: with the option given, a create on a tracked object writes sys_activity.summary in the resolver's locale (the zh-CN activityCreated template exists, translations/messages.ts:26).
  • Fallback: a resolver returning undefined gives today's settings-derived locale.
  • Absent: with no option, the summaries are unchanged (an existing-behaviour control).
  • Ablation: ignore the option on the committed head, and show the pin going red.

查重词

AuditPlugin locale resolver option · plugin-audit getLocale host · activity summary locale multi-org · control plane activity summary language


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, enhancement · priority:p2 · domain:services · area:i18n · pm:queue. Ruled D on objectstack-ai/cloud#2435 item 14: execute as quoted

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-08T15:56Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in packages/plugins/plugin-audit/src/audit-plugin.ts (AuditPluginOptions) ⇒ domain:services; rationale: plugin-audit is that lane's (lanes/services.md:10).

    • Why p2: the same as its consumer, objectstack-ai/cloud#2435.
    • Clause-②: yes: a new optional host option widens the plugin's published options, so the contract-review tier is owed.
    • The shape the ruling fixed: a getLocale(tenantId, userId) resolver, consulted first, falling back to today's settings-derived locale when it returns nothing. ⛔ Not read-time rendering, ⛔ not per-organization plane settings.
    • Unlock for cloud: on the install face, cloud's pin carrying the release. It lands on the v18 line.
  2. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-08T16:19Z
    Session: session_01WkL6Eijt432S1Y7ekb6ovQ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-22318-audit-host-locale
    Worktree: objectstack-issue-22318
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)

    Executes ruling D as quoted (objectstack-ai/cloud#2435 item 14, director record 6063207364; triage 6063836373). AuditPluginOptions gains one optional host locale resolver, consulted first. It falls back to today's settings-derived locale when it answers nothing. With the option absent, behaviour is byte-identical.

    File surface at origin/main 4e4111ca:

    • packages/plugins/plugin-audit/src/audit-plugin.ts: AuditPluginOptions (near :56-59), and the getLocale closure the plugin hands to installAuditWriters (near :218-228). The writer layer already takes getLocale, so audit-writers.ts is untouched.

    • Tests in plugin-audit, and one @objectstack/plugin-audit changeset (minor, additive).

      • The pin: the resolver's locale wins.
      • Fallback: undefined from the resolver gives today's locale.
      • Absent: no option leaves the summaries unchanged.
      • The ablation is named on the card.
    • Seat append, 2026-10-08T17:12Z, from the dev report (PR feat(plugin-audit): AuditPluginOptions.getLocale, a host locale resolver asked before the settings-derived locale #22324): both packages/plugins/plugin-audit/README.md and content/docs/permissions/record-view-auditing.mdx said readAudit is the only key AuditPluginOptions declares, which this change makes false. Both are corrected.

    Exclusions:

    • ⛔ No read-time rendering.
    • ⛔ No per-organization plane settings.
    • ⛔ No second locale source anywhere else.
    • ⛔ No packages/spec.

    Stop on breach; explain in the report.
    Container & model: S, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier)
    Clause-②: yes (widening)

  3. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    This amends my grade 6063836373 on one point. A Clause-②: yes card is spec-lane work (execution-duties.md:101 (「命中即 spec 车道的活」) and dispatch-gates: "a hit outside those lanes is spec-lane work and moves there"), so my domain:services route was wrong.

    The card is already claimed and in flight under domain:services (claim, Clause-② declared yes), so triage does not move it mid-flight. The PR owes the contract-review-tier review before the queue (CONTRACT_REVIEW_TIER, the at-tier subagent), as for any Clause-②: yes diff. The direction is unchanged (ruling D).

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-08T16:59Z. ⛔ Not a claim, ⛔ not a dispatch.

  4. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 22318,
      "status": "done",
      "branch": "claude/issue-22318-audit-host-locale",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/22324",
      "session": "session_01WkL6Eijt432S1Y7ekb6ovQ — the dispatching PM's session (mode:subagent, the claim's Session: line)",
      "premise_still_valid": true,
      "option": {
        "name": "AuditPluginOptions.getLocale",
        "type": "(tenantId?: string, userId?: string) => string | undefined | Promise of (string | undefined)",
        "precedence": "host first, settings second; documented in the option's TSDoc, the package README (Usage > Activity summary language) and the changeset",
        "accepted_answer": "a non-empty string that is a well-formed BCP-47 tag (Intl.getCanonicalLocales accepts it), used in canonical form (zh-cn becomes zh-CN)",
        "no_answer": "undefined, null or a blank string: silent fallback to the settings-derived locale",
        "malformed_answer": "zh_CN or a non-string: fallback to the settings-derived locale, one warn per install",
        "throw": "a throw or a rejected promise falls through to the settings-derived locale, one warn per install carrying { err: message }; the audited write lands. Measured on main: audit-writers.ts resolveWriteLocale catches a throwing getLocale and answers NO locale (English summaries for that scope, 30s memo, no log), so propagating would have cost the settings locale too"
      },
      "summary": "H1 confirmed on origin/main 4e4111ca: AuditPluginOptions was { readAudit } only, and the plugin built getLocale itself from resolveLocalizationContext. AuditPluginOptions gains optional getLocale. The plugin's getLocale closure asks it first and falls back to the unchanged settings-derived read when it answers nothing usable; with the option absent the host step is never built, so the closure is the old one line for line. audit-writers.ts and packages/spec are untouched. Validation measured, and it departs from the PM lead's 'supported locale' wording: the settings-derived locale is only trimmed at this seam (core coerceLocale; membership is enforced at the settings write door), and i18n providers disagree on what they serve (memory i18n negotiates zh to zh-CN, the file adapter looks up exactly). So the host answer is checked for BCP-47 well-formedness, not for catalog membership; an uncatalogued but well-formed locale degrades the way a settings-derived one does.",
      "files_changed": [
        "packages/plugins/plugin-audit/src/audit-plugin.ts",
        "packages/plugins/plugin-audit/src/audit-plugin-host-locale.test.ts (new, 8 cases)",
        "packages/plugins/plugin-audit/README.md",
        "content/docs/permissions/record-view-auditing.mdx",
        ".changeset/22318-audit-plugin-host-locale.md (@objectstack/plugin-audit minor; Clause-②: yes (widening))"
      ],
      "tests": "All runs under scripts/pm/os-verify-lock.sh (OS_VERIFY_LOCK_SLOT=dev-22318, NODE_OPTIONS=--max-old-space-size=3072, turbo --concurrency=1, vitest --maxWorkers=2). Final HEAD 5fc8c76371 (after merging origin/main). (1) pnpm --filter @objectstack/plugin-audit exec vitest run --maxWorkers=2: 'Test Files 42 passed (42)', 'Tests 657 passed (657)', VERDICT command-exit 0. (2) pnpm --filter @objectstack/plugin-audit typecheck: exit 0, 'check:test-typecheck: OK … 0 file(s) / 0 error(s)', so the new test file is compiled. (3) The new file's cases: the pin (option answers zh-CN, so summary = '创建了 Qualification \"OC-00001\"', resolver asked with ('org-1','user-1')); sync ' zh-cn ' canonicalized; fallback (undefined, null, blank all give the settings ja-JP summary 'Qualification「OC-00001」を作成しました', no warn); absent (no option and {} give the ja-JP summary; no option and no settings give 'Created Qualification \"OC-00001\"'); throw (two scopes, two throws, both summaries ja-JP, 2 sys_audit_log rows, exactly 1 warn with meta { err: 'locale store unreachable' }); rejected promise (ja-JP, 1 warn { err: 'timeout' }); malformed 'zh_CN' (ja-JP, 1 warn naming \"zh_CN\"). A first run, before the dependency closure was built, failed at import with 'Cannot find package @objectstack/objectql/core'. That run never reached a test, so it is not counted as a measurement.",
      "ablation": "On committed head 723b1f599e, run via node scripts/ablation-replace.mjs in WRAP mode with its trap-held restore, plus an outer trap on the absolute path. Anchor 'if (readHostLocale) {' x1 replaced by 'if (readHostLocale && false) {', so the plugin ignores the option. On-disk proof: 'anchor x1 -> x0', 'replace x0 -> x1', blob d346f4403902 -> 26dbb44c82f3. Result: 'Tests 5 failed | 3 passed (8)'; the pin received 'Qualification「OC-00001」を作成しました' (the settings locale) where it expected '创建了 Qualification \"OC-00001\"'. The canonical, throw, rejected and malformed cases also went red. The fallback and absent cases stayed green, the expected direction, 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: the test imports ./audit-plugin.js relatively, and vitest resolves that to src.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 5fc8c76371 derived 95 commands. All 95 ran. 92 exited 0 on the first pass; check:skill-examples, check:dual-build-cjs-loads and check:i18n exited 3 (PREREQUISITE NOT MET). After 'pnpm turbo run build --filter=!@objectstack/docs --concurrency=1' (VERDICT command-exit 0, 72 tasks, 71 cached), all three exited 0: '262 prose examples type-check across 3 surface(s)', the dual-build floors held, and 'check-i18n-bundles: OK (9 package(s) — all bundles in sync…)'. Verdict line: '✓ dispatch-gates --ran: 95 derived famil(ies) accounted for — 95 run, 0 NOT-MEASURED (a DERIVED zero — all 95 recorded an exit code and none of them is 3)'. Extra roster families whose roster sits under a touched directory, plus the card-kind ones: 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. Narrowed lint, declared: eslint --no-inline-config --format json on the 2 changed .ts files gave 2 files, 0 errors, 0 warnings. The population read from eslint.config.mjs is **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}, so the .md/.mdx files are outside it. The config enables no type-aware linting (no parserOptions.project), so the diff cannot move any untouched file's verdict. The repo-wide pnpm lint is CI's. CI at report time on 5fc8c76371: 10 success, 2 skipped, 20 in_progress (not waited for, per contract).",
      "deviations": [
        "Docs surface: the fence reads 'README or one docs page'. Both packages/plugins/plugin-audit/README.md and content/docs/permissions/record-view-auditing.mdx said 'readAudit is the only key AuditPluginOptions declares', which this change makes false, so both are corrected: the README with the member and its fallback table, the docs page with one sentence naming the second key.",
        "PM lead H2's 'a host answer that is not a supported locale falls back' is executed as 'not a well-formed BCP-47 tag falls back'. The reasons are measured: the settings-derived locale gets no support check at this seam, and i18n providers disagree on what they serve. Catalog membership is not checked; this is recorded in the summary and the PR body for the contract review.",
        "Commit trailers use the AGENTS.md model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By. The PR footer uses the AGENTS.md session-URL form, not the reminder's form.",
        "PR labels documentation, size/m, tests and tooling were added by the repository's labeler, not by this lane; label-write wrote only the assignee os-bill."
      ],
      "mcp_calls": "0",
      "api_writes": "3, each one fleet-write relay stroke (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) POST /repos/objectstack-ai/objectstack/pulls, draft PR #22324, relay run 37814167592, body read back as 7909 bytes identical; (2) POST /repos/objectstack-ai/objectstack/issues/22324/assignees os-bill via scripts/pm/label-write.mjs, relay run 37814264247, read-back MATCHES; (3) POST /repos/objectstack-ai/objectstack/issues/22318/comments, this os-dev-report, via scripts/pm/post-stamped.mjs. git push is not a REST write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed: AuditPluginOptions is not re-exported from @objectstack/plugin-audit's index (unchanged here). A host passes an object literal and can name the type only through ConstructorParameters of AuditPlugin. Recorded in the PR's Acceptance notes.",
        "carrier: none · noted, not filed: audit-writers.ts resolveWriteLocale swallows a throwing getLocale with no log and memoizes 'no locale' for 30s. 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. Recorded in the PR's Acceptance notes."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat review, before the contract review: PR #22324 at 5fc8c76371 · seat domain:services#1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T17:13Z

    Checked against GitHub and the branch, not the report's prose. This record answers the dev's flags. The at-tier contract review follows on this head (Clause-②: yes (widening)), and the ACCEPT waits for it.

    • Form:
      • The PR is a draft. Its first line is Fixes #22318, with a line-start Clause-②: yes (widening). The PR assignee is os-bill.
      • Five files, +393/−6. check-governed-merges --pr 22324: NOT governed.
      • It merges clean with main.
    • Diff, read by the seat:
      • AuditPluginOptions.getLocale?: (tenantId?, userId?) => string | undefined | Promise<string | undefined>.
      • The plugin's write-locale closure asks it first, through createHostLocaleReader. That reader is built only when the option is given, so with no option the closure is the old one.
      • An answer of undefined, null or blank falls back silently.
      • A throw, a rejection or a malformed tag falls back, with one warn per install. The audited write still lands.
      • audit-writers.ts and packages/spec are untouched.
    • Rulings on the dev's flags:
      • The docs surface (deviations[0]): accepted. Both the README and record-view-auditing.mdx stated that readAudit is the only key. The claim is appended.

      • Validation: a well-formed BCP-47 tag, not catalog membership (deviations[1]). The seat accepts the measured reasons:

        • the settings-derived locale gets no membership check at this seam;
        • the i18n providers disagree on what they serve;
        • an uncatalogued but well-formed tag degrades exactly as a settings-derived one does.

        For the contract review to confirm.

      • A throw falls through, logged: accepted. Measured on main: a throw that reached the writer layer would answer no locale at all (English, memoized), so letting it propagate would also have lost the settings locale.

    • out_of_scope_findings, noted and not filed:
      • AuditPluginOptions is not re-exported from the package index. A host passes an object literal.
      • installAuditWriters' own getLocale swallow is unreachable from the plugin's closure now.
      • Both are in the PR's Acceptance notes.
  6. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: 5fc8c76371108a7f2121a4657d4bd68550196c42
    Local-runs: none

    Inputs: card #22318 (body and all five comments, the dev report 6065122657 and the seat review 6065173664 included), PR #22324 (body, five-file list, net diff against main), and the 35 check-runs on the head. The required seven (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard) all conclude success; the only two non-success runs are skipped (Console Pin Gate, Packed-tarball smoke (opt-in)). Check Changeset and the docs-drift advisory are success. Not governed: no path under a governed surface. Head repo = base repo.

    ① Derived judgments

    Accept-set change, one and only one: AuditPluginOptions (a TS interface, not a Zod shape — no runtime key refusal exists on this bag, so nothing needed registering) gains the optional member getLocale, typed (tenantId?: string, userId?: string) returning string, undefined, or a Promise of either. readAudit and its three keys are unchanged. Nothing narrows. Right, and it is the shape ruling D fixed (the resolver's own name, consulted first).

    Behaviour, each read against the code at the head:

    • Host-first precedence — right. The plugin's kernel:ready closure (audit-plugin.ts:304-317) asks readHostLocale before the unchanged resolveLocalizationContext read; a defined answer returns, anything else falls through to the settings-derived locale. No second locale source is added anywhere else; audit-writers.ts and packages/spec are untouched (file list confirms).
    • Absent means byte-identical — right. readHostLocale is built only when this.options.getLocale is set (:301-303); with it unset the closure body is the main closure line for line (compared origin/main :218-226 against head :304-317 — the delta is exactly the guarded if block).
    • No answer = undefined, null, blank — silent fallback — right. Within the ruling's "when it returns nothing".
    • Malformed answer (not a well-formed BCP-47 tag, or a non-string) — fallback plus one warn per install — right. Intl.getCanonicalLocales throws on zh_CN; the non-string arm never reaches it. A functional degradation at warn is the level AGENTS.md's degradation rule prescribes; the message carries the consequence and the fallback, and no tracker number.
    • Throw or rejected promise — fallback plus one warn per install, the audited write lands — right. Measured on main: resolveWriteLocale (audit-writers.ts:1148-1160) catches a throwing getLocale, memoizes undefined for 30s and logs nothing, so leaving the throw to the writer would have cost the settings locale too (English summaries, silently). Catching in the plugin is the narrower blast radius and makes the absence loud.
    • Canonical form (zh-cn to zh-CN) — right, and harmless: the memory i18n negotiates case-insensitively and by base language anyway (memory-i18n.ts:47-68), so canonicalization changes no lookup outcome; it only makes the answer deterministic.
    • Writer memo claim (TSDoc, README) — right: LOCALE_TTL_MS = 30_000, keyed per tenant|user, covers the composed closure.
    • Catalog degradation claim (TSDoc, README) — right: memory i18n consults the declared fallbackLocale, then t returns the key verbatim, which translateWith turns into the English literal.
    • Test surface — right. Eight cases drive the plugin through init/start/kernel:ready with a real memory i18n and a ja-JP settings occupant, so host (zh-CN), settings (ja-JP) and English are three distinguishable outcomes; the pin asserts the resolver is called with ('org-1', 'user-1'). The ablation (if (readHostLocale && false)) is reported with blob hashes before/after and a git diff HEAD empty restore; five of eight red, the two option-independent cases green, which is the expected direction. CI Test Core is green on the head.
    • Docs — one claim WRONG. The three new reader-facing texts — the option's TSDoc (audit-plugin.ts:60-62), the README (README.md:67), and the changeset (:9) — each say the option picks the language of "sys_activity.summary and the assignment / @mention notification titles the plugin emits". The plugin emits no assignment notification: audit-writers.ts:1891 says so in as many words ("Assignment notifications are NOT emitted here … Applications now opt in per object with an automation flow"), the writer's only two resolveWriteLocale call sites are the summary (:1779) and the collab.mention title (:2086), the plugin's bundle carries mentionedYou keys and nothing for assignment, and notify-node.ts localizes its own titles with no dependency on this option. So the surface the diff publishes advertises a capability the runtime does not deliver (Prime Directive chore: version packages #10's corollary: declared ≠ enforced; trim the claim to what is enforced). It matters more than its size: the changeset sentence becomes CHANGELOG.md, a RELEASE-OWNED entry that ships in the tarball and can only be corrected afterwards by a dedicated docs-only PR, and a host reading the README would expect its resolver to govern assignment-bell language, which the notify node decides on its own. The three pre-existing code comments that carry the same loose word (audit-plugin.ts:278, audit-writers.ts:112, :2085) are not published surfaces and are outside this diff; correcting them in the same commit is welcome, not required.
    • The rest of the docs delta is right: the record-view-auditing.mdx sentence names the second key and says it does not change what is audited; the README's readAudit paragraph is corrected; the README anchor #activity-summary-language--getlocale matches the heading's GitHub slug and Check Documentation Links is green; no other page at the head still says readAudit is the only key (grepped content/docs, skills, package READMEs).

    ② Semver level

    @objectstack/plugin-audit: minor — right. One optional constructor option added, nothing removed, renamed or narrowed; no other published package changes (content/docs and the test file publish nothing). Clause-②: yes (widening) on the PR body's second line and in the changeset body — right, and the widening arm reads as not breaking to check-adr-0087-registration, so no disposition marker is owed. The level and the clause match what the diff publishes; what does not match is the changeset's description of the widened surface (the assignment claim above), which is the FAIL carrier because that text is what ships.

    ③ Boundary flags

    Dev flags (6065122657), each answered:

    • deviations[0] — docs on two surfaces instead of "README or one docs page": accepted. Both files carried the sentence the change falsifies; leaving either would have shipped a false statement.
    • deviations[1] — BCP-47 well-formedness instead of catalog membership: accepted, confirmed on the code. coerceLocale (resolve-authz-context.ts:1240-1243) only trims and drops a blank, so the settings-derived locale gets no membership check at this seam either; the memory i18n negotiates (zh to zh-CN, base-language and variant matches), so an exact catalog list would refuse answers the deployment serves; and an uncatalogued well-formed tag degrades exactly as a settings-derived one does. The seat's acceptance stands.
    • deviations[2] — model-free commit trailer pair: accepted. All three branch commits carry Claude-Session: plus Co-authored-by: Claude, no model identifier (the AGENTS.md form, which the harness reminder itself yields to).
    • deviations[3] — labels set by the labeler: accepted, not a lane write.
    • open_questions: none declared, none found.
    • out_of_scope[0] — AuditPluginOptions not re-exported from the package index: accepted as noted, not escalated. Pre-existing; the documented spelling is an object literal, which TypeScript checks without the name, and check-published-readme-exports reads import statements, which the README does not add. A follow-up card is optional.
    • out_of_scope[1] — resolveWriteLocale swallows a throwing getLocale with no log: accepted as noted, not escalated. The exported installer documents a best-effort posture ("no locale / no i18n / key miss all degrade to the English literal"), the plugin's own path no longer reaches it for a host fault, and the only caller in this repository is the plugin. A reproducible silence, not a contract violation.

    Flag raised by this review, to clear before a PASS record on the next head:

    • Trim "assignment /" from the three published texts — audit-plugin.ts TSDoc on getLocale, README.md:67, and the changeset's first bullet — so each names what the option governs: sys_activity.summary and the @mention notification title (the recipient's scope). Nothing else in the diff needs to move; the code, tests, level and clause are right as they stand.

    Implemented-by: claude/issue-22318-audit-host-locale
    Reviewed-by: session_01WkL6Eijt432S1Y7ekb6ovQ

    VERDICT: FAIL


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    Round 2: answers contract review 6065609320 (finding R1).

    {
      "issue": 22318,
      "round": 2,
      "status": "done",
      "branch": "claude/issue-22318-audit-host-locale",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/22324",
      "head": "1bdf559a31",
      "session": "session_01WkL6Eijt432S1Y7ekb6ovQ — the dispatching PM's session (mode:subagent, the claim's Session: line)",
      "premise_still_valid": true,
      "answers_review": "Contract review 6065609320 (VERDICT: FAIL on 5fc8c76371), seat rework round 2, finding R1",
      "summary": "R1 fixed in commit 29fa5b6818. The phrase 'the assignment / @mention notification titles' is trimmed from all three published texts. Each now names what the option governs: sys_activity.summary and the @mention notification title, asked in the mentioned recipient's user scope. The texts are the getLocale TSDoc (audit-plugin.ts:60-63), README.md:67-71 and the changeset's first bullet. The option's own runtime warn text said 'activity summaries and notification titles'; it is narrowed the same way, to '@mention notification titles'. No assertion pins that text (the tests match /host getLocale resolver threw/ and /not a well-formed BCP-47 locale tag/). Checked and needing no change: content/docs/permissions/record-view-auditing.mdx (no notification wording) and the PR body (0 hits for assignment or notification, in the local file and in the live body read back from the API). The PR body was not patched. The three pre-existing comments (audit-plugin.ts:278, audit-writers.ts:112 and :2085) are outside the diff and untouched. origin/main had moved 4 commits; none touched this lane's files, but dispatch-gates flagged its derivation as a STALE TREE (2 gate-input files changed on main). origin/main 28bff18d0c was therefore merged cleanly as 1bdf559a31, and every reading below is on that head. No code, test, level or clause change.",
      "option": {
        "name": "AuditPluginOptions.getLocale",
        "type": "(tenantId?: string, userId?: string) => string | undefined | Promise of (string | undefined)",
        "governs": "sys_activity.summary, and the collab.mention notification title (resolveWriteLocale call sites audit-writers.ts:1779 and :2086); for the mention title the userId is the mentioned recipient"
      },
      "files_changed_this_round": [
        "packages/plugins/plugin-audit/src/audit-plugin.ts (TSDoc on getLocale; the fallsBack warn text)",
        "packages/plugins/plugin-audit/README.md (Activity summary language — getLocale, first paragraph)",
        ".changeset/22318-audit-plugin-host-locale.md (first bullet)"
      ],
      "tests": "Run at 1bdf559a31 under scripts/pm/os-verify-lock.sh (OS_VERIFY_LOCK_SLOT=dev-22318, NODE_OPTIONS=--max-old-space-size=3072). Build first: pnpm turbo run build --filter=!@objectstack/docs --concurrency=1, VERDICT command-exit 0 ('Tasks: 72 successful', 65 cached). Then pnpm --filter @objectstack/plugin-audit exec vitest run --maxWorkers=2 gave 'Test Files 42 passed (42)' and 'Tests 657 passed (657)'; pnpm --filter @objectstack/plugin-audit typecheck exited 0 with 'check:test-typecheck: OK … 0 file(s) / 0 error(s)'; VERDICT command-exit 0. The same suite and typecheck also passed at 29fa5b6818, before the merge.",
      "ablation": "Not re-run: this round changes only text, and no behaviour or test. Round 1's ablation on 723b1f599e stands: the plugin ignored the option; 'Tests 5 failed | 3 passed (8)'; the restore was proved by blob == HEAD and an empty git diff HEAD.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 1bdf559a31 derived 95 commands, not stale, the same set as round 1. All 95 ran and exited 0; none refused, because the workspace dist was built first. Verdict line: '✓ dispatch-gates --ran: 95 derived famil(ies) accounted for — 95 run, 0 NOT-MEASURED (a DERIVED zero — all 95 recorded an exit code and none of them is 3)'. The changed text paths' gates: check:published-readme-links '✓ … 222 outbound link(s) across 101 published markdown file(s)'; check-empty-changeset '✓ No empty-frontmatter changeset introduced by this diff'; check-adr-0087-registration '✓ … adds no declared-breaking changeset'; check-changeset-no-major exited 0. Its LEVEL AXIS is NOT APPLICABLE locally, because no pull_request payload exists outside CI. Extra roster families: 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. Narrowed lint: eslint --no-inline-config --format json over the 2 changed .ts files gave 2 files, 0 errors, 0 warnings. The config has no type-aware linting, and the .md files are outside eslint's **/*.{ts,…} population. CI on 1bdf559a31 was not waited for.",
      "pr_body": "No correction owed for R1: the body carries no assignment or notification wording, measured on the live body. One optional refresh is left for the seat to write; the body was not patched. Its 'Runs at 5fc8c76371' and 'Gates … at 5fc8c76371' lines describe the round-1 head. At 1bdf559a31 the readings are identical in count (42/42 files, 657/657 tests; 95 derived, 95 run, 0 NOT-MEASURED). The one difference is that no gate refused this time, because the workspace was built before the battery.",
      "deviations": [
        "Beyond the three named texts, the option's own runtime warn string ('activity summaries and notification titles fall back …') is narrowed to '@mention notification titles'. It is a fourth new text in this diff carrying the same overreach. No test or consumer pins it.",
        "origin/main was merged although it had not moved onto this lane's files: dispatch-gates refused to vouch for a stale tree (2 gate-input files changed on main), and the brief asks for gates read on a current tree."
      ],
      "mcp_calls": "0",
      "api_writes": "1 this round: one fleet-write relay stroke (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]) running POST /repos/objectstack-ai/objectstack/issues/22318/comments, this round-2 os-dev-report, via scripts/pm/post-stamped.mjs. git push (29fa5b6818, 1bdf559a31) is a git op. No PR-body PATCH. Round-1 total: 3.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: 1bdf559a31f27e9a2c6d63a600ac6e584c84c226
    Local-runs: none

    Round 2, on the head that answers record 6065609320 (FAIL on 5fc8c76371). Inputs: card #22318 (body and all seven comments, the round-2 dev report 6066147177 included), PR #22324 (body, five-file list, net three-dot diff against main 28bff18d0c), and the 35 check-runs on the head, read latest-run-per-name. The required seven (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard) all conclude success; the only two non-success runs are skipped (Console Pin Gate, Packed-tarball smoke (opt-in)); Check Changeset, Check Documentation Links and Check PR Size are success. Not governed: none of the five paths sits under a governed surface. Head repo = base repo. +393/−6, well under the 5,000-line class. The net diff is exactly the PR's five files; the merge of main adds nothing to it.

    ① Derived judgments

    Accept-set change, one and only one: AuditPluginOptions (a TS interface, no runtime key refusal on the bag, so nothing needed registering) gains the optional member getLocale, typed (tenantId?: string, userId?: string) returning string, undefined, or a Promise of either. readAudit and its three keys are unchanged. Nothing narrows. Right, and it is the shape ruling D fixed: the ruling's own spelling, consulted first, settings second.

    Behaviour, each read against the code at this head:

    • Host-first precedence — right. The kernel:ready closure (audit-plugin.ts:298-317) asks readHostLocale before the unchanged resolveLocalizationContext read; a defined answer returns, anything else falls through. No second locale source anywhere: audit-writers.ts and packages/spec are untouched (file list).
    • Absent means byte-identical — right. readHostLocale is built only when this.options.getLocale is set (:301-303); every other this.options read in the file is still readAudit (:324-333). With the option unset the closure body is main's, the delta being exactly the guarded if block.
    • No answer (undefined, null, blank) — silent fallback — right. The ruling's "when it returns nothing".
    • Malformed answer (not a well-formed BCP-47 tag, or a non-string) — fallback plus one warn per install — right. Intl.getCanonicalLocales throws on zh_CN; the non-string arm never reaches it. A functional degradation at warn is the level AGENTS.md's degradation rule prescribes (the audited write lands; nothing claims to be persisted that is not); the message carries consequence and fallback and no tracker number.
    • Throw or rejected promise — fallback plus one warn per install, the write lands — right. Measured at the head: resolveWriteLocale (audit-writers.ts:1148-1162) catches a throwing getLocale, memoizes undefined for LOCALE_TTL_MS = 30_000 and logs nothing, so a throw left to the writer would have cost the settings locale too, silently. Catching in the plugin is the narrower blast radius and makes the absence loud.
    • Canonical form (zh-cn to zh-CN) — right, and harmless: the memory i18n's resolveLocale matches case-insensitively and by base language anyway (memory-i18n.ts:49-70), so canonicalization changes no lookup; it only makes the answer deterministic.
    • Writer memo claim (TSDoc, README) — right: LOCALE_TTL_MS = 30_000, keyed tenantId|userId, wraps the composed closure.
    • Catalog degradation claim (TSDoc, README, changeset) — right where it is load-bearing: the memory i18n's t() consults the declared fallbackLocale per key and then echoes the key, which translateWith turns into the English literal; a host answer with no catalog takes the same path a settings-derived one does. coerceLocale (resolve-authz-context.ts:1240-1243) only trims, so the settings path has no membership check at this seam either.
    • resolveLocalizationContext never throws — right (its docblock says so and every settings leg is caught, :1549, :1618-1683), so the plugin's closure no longer reaches the writer's silent swallow for any fault.
    • Docs — the round-1 finding is cleared. The three published texts named by 6065609320 now each say what the option governs: the TSDoc (audit-plugin.ts:60-62: "sys_activity.summary, and the title of the @mention notification it emits"), the README (README.md:67: "sys_activity.summary and the @mention notification title"), and the changeset's first bullet. The runtime warn string is narrowed the same way ("activity summaries and @mention notification titles"). Checked against the writer: the only two resolveWriteLocale call sites are the summary (:1779) and the collab.mention title (:2086), and :1891 still says assignment notifications are not emitted here. The word "assignment" appears nowhere in the net diff. The three pre-existing comments that carry the loose word (audit-plugin.ts:278, audit-writers.ts:112, :2085) are outside the diff and not published surfaces, as the round-1 record said.
    • The rest of the docs delta is right: record-view-auditing.mdx names the second key and says it does not change what is audited; the README's readAudit paragraph is corrected; the README anchor #activity-summary-language--getlocale is the heading's GitHub slug and the links gates are green; no page at the head still says readAudit is the only key (grepped content/, skills/, docs/, the package README).
    • Test surface — right. Eight cases drive the plugin through init, start and kernel:ready with a real memory i18n and a ja-JP settings occupant, so host (zh-CN), settings (ja-JP) and English are three distinguishable outcomes; the pin asserts the resolver is called with ('org-1', 'user-1'). The file is compiled by tsconfig.test.json through check:test-typecheck, which the typecheck script names. It reads nothing outside its package. The round-1 ablation (if (readHostLocale && false), five of eight red, blob hashes before and after, git diff HEAD empty) stands, since this round moved only text and no assertion pins the narrowed warn text. Test Core and every Type Check job are green on the head.
    • ADR check: no ADR names getLocale or AuditPluginOptions; ADR-0052 governs what the activity writer renders, not which catalog it renders from; the ADR-0053 citation for localization.locale is the one the pre-existing closure comment already carries (audit-plugin.ts:288).

    ② Semver level

    @objectstack/plugin-audit: minor — right. One optional constructor option added; nothing removed, renamed or narrowed; no other published package changes (content/docs and the test file publish nothing). Clause-②: yes (widening) on the PR body's second line and in the changeset body — right; the widening arm is not breaking, so no ADR-0087 disposition marker is owed, and Check Changeset is green. The changeset's description now matches the surface it widens: the summary and the @mention title, host first, settings second, nothing else. The level, the clause and the text match what the diff publishes.

    ③ Boundary flags

    Round-2 dev report (6066147177), each answered:

    • deviations[0] — the runtime warn string narrowed beyond the three named texts: accepted. It was a fourth text in this diff carrying the same overreach; the tests match only the head of each message, so no pin moved.
    • deviations[1] — origin/main merged although it had not moved onto this lane's files: accepted. The merge is clean, the net three-dot diff is exactly the five files, and the check-runs on the merged head are the gate verdicts this record reads.
    • pr_body — the body's "Runs at 5fc8c76371" and "Gates … at 5fc8c76371" lines describe the round-1 head: accepted, not a carrier. Each line names the sha it measured, the dev reports identical counts at this head, and the body is not a published surface. A refresh is the seat's option.
    • open_questions: none declared, none found. out_of_scope_findings: none this round.

    Round-1 flags (6065122657), rulings standing and re-checked at this head: docs on two surfaces (accepted, both carried the falsified sentence); BCP-47 well-formedness instead of catalog membership (accepted, confirmed on coerceLocale and the memory i18n's negotiation); model-free commit trailers (accepted; all four lane commits at this head carry Claude-Session plus Co-authored-by: Claude, the two merge commits carry none, and no model identifier appears in the diff); labeler labels (accepted, not a lane write); AuditPluginOptions not re-exported from the package index, and the writer's silent swallow on a direct installAuditWriters caller (both accepted as noted, not escalated: pre-existing, outside the ruling, recorded in the PR's acceptance notes).

    The flag raised by 6065609320 — trim "assignment /" from the three published texts — is cleared on this head, as read above. Nothing remains open.

    Implemented-by: claude/issue-22318-audit-host-locale
    Reviewed-by: session_01WkL6Eijt432S1Y7ekb6ovQ

    VERDICT: PASS

  9. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat ACCEPT: PR #22324 at 1bdf559a31 · seat domain:services#1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T18:32Z

    This completes the seat's review 6065173664.

    • Contract review: PASS at CONTRACT_REVIEW_TIER (6066491732, head 1bdf559a31f27e9a2c6d63a600ac6e584c84c226, Local-runs: none). It was rendered by an isolated reviewer from the card, the PR and the head's check-runs only.
      • Round 1's FAIL (6065609320) is cleared. The three published texts, and the runtime warn string, no longer name an "assignment" notification the plugin does not emit. Each names sys_activity.summary and the @mention notification title.
      • ① exactly one widening: the optional AuditPluginOptions.getLocale. It is host-first and byte-identical when absent.
      • ② @objectstack/plugin-audit minor, Clause-②: yes (widening).
      • ③ every flag is answered.
    • Round 2 (REWORK, text only) is resolved: the texts are trimmed, and main was merged after the gate tool flagged a stale tree. No code, test, level or clause moved. plugin-audit 657 of 657 passed, and 95 of 95 gates.
    • CI on the head: every check is success or an expected skip (Console Pin Gate, Packed-tarball smoke). It merges clean with main, and it is not governed.
    • The consumer: objectstack-ai/cloud#2435 item 14 (ruling D). It reaches cloud with the release carrying this minor once cloud's pin moves (v18). The repo:cloud seat is told at landing.
    • Landing: ready and auto-merge through the relay, the merge-queue check, and the close-out.
  10. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T19:05Z.

    PR #22324 merged through the merge queue as 3599fef1. On origin/main, @objectstack/plugin-audit (minor, additive) executes ruling D: AuditPluginOptions.getLocale is an optional host locale resolver.

    • It is consulted first for sys_activity.summary and for the @mention notification title (in the recipient's scope).
    • When it answers nothing usable, the settings-derived locale applies. A throw or a malformed tag falls back too, with one warn per install.
    • With the option absent, the plugin behaves as before.

    The PR's Fixes line closed the card completed. This note also removes pm:dispatched and the assignee.

  11. added a commit that references this issue on Oct 9, 2026
    3599fef
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:i18nThe customer's own language, across UI, metadata and notificationsdomain:servicesenhancementNew feature or requestpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions