Skip to content

fix(plugin-audit): render the activity summary in the reader's language - #26258

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-xshjzo
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-xshjzo

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #26167

Clause-②: no (no exported type, schema field or spec key changes; the code rides in the existing sys_activity.metadata JSON, and summary keeps its type and meaning)
Metadata-protocol: none

Maintainer ruling on the card: 「同意,立卡」. The activity summary is stored as its message key plus params and rendered in the reader's language, as the approvals precedents do (#25627, the admin_rescue note; #24977). The English sentence stays in summary as the fallback text.

What changed

The code is all in packages/plugins/plugin-audit. One docs row changes too: content/docs/permissions/system-context.mdx row 45 now cites the new read site, because check-system-context-census requires every elevation read to have an anchor. No packages/spec/** file and no ui/** file is touched, so the serial rule with PR #26055 holds.

  • New activity-summary-message.ts. It holds one composer, composeActivitySummary, which the writer and the reader both use. The code is one of three shapes:

    • activityCreated / activityDeleted / activityUpdated with {object, label}
    • activityUpdatedFields with {object, label, fields, more}
    • fieldChanges with {object, changes: [{field, from, to}]}, where each side is {text, option?}

    The object param is the object name. Object, field and option labels are resolved per reader from that reader's pack (objects.NAME.label, …fields.FIELD.label, …options.VALUE), with the authored label as the fallback.

  • The writer (audit-writers.ts). Each CRUD mirror row stores the English sentence in summary and the code in metadata.summary_message. The write no longer asks for a locale for the summary. Milestone summaries (the plugin-audit: activityMilestones summary templates still interpolate raw lookup ids — {owner_id} renders oBK25… after #7230 #7290 path) store no code and keep their authored text.

  • The reader (installActivitySummaryLocale, registered in audit-plugin.ts after installActivityFieldRedaction). It is an engine middleware on sys_activity find / findOne.

    • It negotiates ctx.context.locale against the held packs with resolveBundleLocale, so a bare zh reads the zh-CN pack.
    • It composes the summary in that locale and overlays summary.
    • When the projection names summary only, it adds metadata and removes it again afterwards.
    • It drops the code from metadata for every caller that is not the system, because its params are field values.
    • Middleware is an onion, so the field redaction, registered earlier, still judges the composed text.
    • A reader locale with no template, or a code this version cannot compose, keeps the stored sentence.

Hypothesis A held: rendering happens on read, on the server. The Discussion feed fetches its rows with dataSource.find('sys_activity', …) (ui/packages/plugin-detail/src/renderers/record-activity.tsx) and shows row.summary (recordActivityFeed.ts). The authenticated fetch sends Accept-Language on every API call (ui/packages/auth/src/createAuthenticatedFetch.ts), and the REST door resolves it into the caller context's locale. So the server overlay changes what the feed shows, with no UI change, and Hypothesis B was not needed.

Rows written before this change

They stay as written, for every reader. There is no migration and no backfill, and no row is dropped. A row with no summary_message in its metadata is served its stored summary unchanged. This is pinned by the last case of the integration test, which uses a row in the earlier { old, new } metadata shape.

Tests

The red test came first, in commit cb6b6af: src/activity-summary-reader-locale.integration.test.ts. It runs on a real kernel with ObjectQL, SQLite, a security double and AuditPlugin. It writes as the system with the workspace locale left at en, then reads as a zh-CN caller.

  • On main (fbb7599) it fails 4 and passes 3: Expected: "创建了 合同 "NDA"", Received: "Created Contract "NDA"", and the lock reported VERDICT command-exit 1.
  • On this branch all cases pass. A zh-CN reader gets 创建了 合同 "NDA", 阶段: 草稿 → 已签署 and 更新了 合同 "NDA":备注. The system, an en-US reader and a reader with no locale get the English sentences. A bare zh is negotiated to zh-CN, and fr-FR keeps the stored text. The code is served only to the system. A fields: ['summary'] projection still composes. A reader who is not served note gets no summary for that row. A row written before the change keeps its text.

The unit tests for the composer and the overlay are in src/activity-summary-message.test.ts.

Six existing suites pinned the summary being localized at write time. They now assert the stored English sentence plus the text a reader gets through the same overlay, using src/__tests__/summary-as-read.ts. The workspace-locale and host-locale suites now pin the remaining write-locale consumer, which is the @mention notification title (Ada 提到了你). One new case asserts that an audited write asks for no locale at all.

Local results:

  • pnpm --filter @objectstack/plugin-audit exec vitest run --maxWorkers=2: Test Files 62 passed (62), Tests 1013 passed (1013), exit 0.
  • pnpm --filter @objectstack/plugin-audit typecheck (which includes check:test-typecheck): exit 0.
  • eslint scoped to packages/plugins/plugin-audit: exit 0.
  • Ablation. I disabled the one registration line, installActivitySummaryLocale(engine as any, getI18n); in audit-plugin.ts, using scripts/ablation-replace.mjs. The integration test then failed 4 and passed 3, the same as on main. The restore matched the HEAD blob and git diff HEAD was empty.
  • dispatch-gates. --commands on this diff derives 102 families. All 102 ran and exit 0, and --ran reconciles as 102 derived, 102 run, 0 NOT-MEASURED. Five of them were re-run before they went green:
    • check-system-context-census found the new read site with no anchor on the page. The docs row above fixes it.
    • check:query-options-erasure found one new erased options bag in the tests. The new tests now pass typed options.
    • check-plugin-teardown-shape --self-test failed on a shallow clone and passed after unshallowing.
    • check:i18n and check:dual-build-cjs-loads failed for missing builds and passed after building.

Acceptance notes

  • Search reads English now. summary is searchable, and its stored text is now the English sentence wherever the workspace locale is set. So in a Chinese workspace, full-text search over summary matches the English words for rows written after this change. This follows the ruling that the English sentence is the fallback text. The same goes for an export of the raw column.
  • The write locale still chooses the language of @mention notification titles. That is AuditPluginOptions.getLocale, otherwise the localization settings. Only its doc comment changed.
  • Not measured: the hotclm pnpm demo:zh Console check. That repo is outside this session. The read path was measured from the code above and is pinned end to end by the integration test.

Generated by Claude Code

…t in the reader's language (WIP: write-time locale pins to migrate)

Claude-Session: https://claude.ai/code/session_01UBPXQdapfjkjYdQck9dtTa
Co-authored-by: Claude <noreply@anthropic.com>
…e write locale to mention titles

Claude-Session: https://claude.ai/code/session_01UBPXQdapfjkjYdQck9dtTa
Co-authored-by: Claude <noreply@anthropic.com>
…ext page; type the new tests' engine options

Claude-Session: https://claude.ai/code/session_01UBPXQdapfjkjYdQck9dtTa
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-audit, touching 49 documentable anchor(s).

21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 81bba6c3f5be5b6cbabf76511383940349f23b15.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 7 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 — 10 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 81bba6c3f5be5b6cbabf76511383940349f23b15 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7e7197baf5095f8865c6b14952ecdc21fa92f81c — the merge of head fa1d3912d6abbbe1b77583276439c88048666bf3 into base 81bba6c3f5be5b6cbabf76511383940349f23b15, 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 7e7197baf5095f8865c6b14952ecdc21fa92f81c && git checkout 7e7197baf5095f8865c6b14952ecdc21fa92f81c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 81bba6c3f5be5b6cbabf76511383940349f23b15 fa1d3912d6abbbe1b77583276439c88048666bf3 && git checkout -B drift-repro 81bba6c3f5be5b6cbabf76511383940349f23b15 && git merge --no-ff fa1d3912d6abbbe1b77583276439c88048666bf3

node scripts/docs-audit/affected-docs.mjs --json 81bba6c3f5be5b6cbabf76511383940349f23b15

⚠️ 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 81bba6c3f5be5b6cbabf76511383940349f23b15 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:server seat 1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-10-26T17:51Z

Head fa1d3912d6, report 6422788312 (on #26167), claim 6420565917 and amendment 6422838713. No contract review is owed: there is no packages/spec/** diff and no narrowing. The code rides in the existing sys_activity.metadata JSON, so no column, object schema or spec key moves.

item reading
Shape Draft, base main, Fixes #26167, a patch changeset for @objectstack/plugin-audit, Clause-②: no, Metadata-protocol: none (measured, as the claim asked), mergeable clean.
Files 14 files, +1294/−269. All but one are in packages/plugins/plugin-audit/**. The exception is content/docs/permissions/system-context.mdx row 45, the new read-site anchor that check-system-context-census requires, as the amendment covers. No ui/ file changes, so the serial rule with #26055 holds.
Fix Hypothesis A held: the server renders on read. The writer stores the summary as a code (template key, object name, field keys, rendered values plus the matched option) in metadata.summary_message. It also keeps the English-fallback sentence in summary, composed through the same composeActivitySummary. A sys_activity engine middleware re-composes summary in the reader's ExecutionContext.locale, negotiated by resolveBundleLocale and rendered with II18nService.t(), the same sequence plugin-approvals uses. A miss, a throw, or a read with no locale keeps the stored sentence. For a non-system caller the code is stripped from the served metadata, and the middleware runs inside the field redaction, so the composed text is judged by the same sources.
Rows written before the change Served as written, with no migration and no backfill. That is the claim's default, and the report states it.
Evidence Red first on main: 4 failed and 3 passed, and the ablation reproduces the same. 102/102 derived gates exit 0 and --ran reconciles with 0 NOT-MEASURED. On this head, 38 check runs succeeded and 10 were expected skips, with 0 failed.
Landing 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay.

Generated by Claude Code

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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-audit: an activity summary is rendered once, at write time, in the workspace locale — store it as a key and render it in the reader's language

2 participants