Skip to content

fix(plugin-approvals): the data door serves an approval request's flow and step labels in the reader's language - #27092

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-srtrac
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-srtrac

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #27041

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:通过通用数据接口读 sys_approval_request(Setup › 审批申请列表、申请的记录页、查找字段的标题、导出)时,流程名 process_label 和步骤名 current_step_label 按读者语言返回,用的就是审批接口(/api/v1/approvals/requests)已有的那一套查找;语言包没有的条目照旧返回存储文本。
  • 为什么改:zh-CN 用户在审批收件箱看到中文,在 Setup › 审批申请里看到同一批申请却是英文(HotCRM 的 "Large Deal Approval" / "Sales Manager Review")。收件箱那一半由控制台 PR fix(console): the approvals inbox names the flow, the step and the object in the reader's language #27040 修了,列表这一半只能在服务端修。
  • 风险与回滚:低。只改读出的行,存储不动;系统上下文读取、无语言、语言包无条目都返回原文。列表按列 $select 时会临时多取 process_name / current_step 用来拼键,返回前删掉,调用方拿到的列不变。回滚即 revert。
  • 先看:packages/plugins/plugin-approvals/src/reader-locale-labels-middleware.ts(借列与剥列、系统读跳过);approvals-plugin.ts 的注册位置(与读门同处,不在 disableAutoHooks 后面);content/docs/permissions/system-context.mdx 第 17 行。
  • 两分钟验证:pnpm --filter @objectstack/plugin-approvals exec vitest run src/reader-locale-labels-data-door.integration.test.ts,预期 10/10 通过;或启动 showcase 后用 Accept-Language: zh-CN 请求 GET /api/v1/data/sys_approval_request?$select=process_label,current_step_label,看到「发票双重签批(并行审批)」等中文。
  • 需要你做的:无,席位落地。

What changes

@objectstack/plugin-approvals registers one more engine middleware on sys_approval_request, bindReaderLocaleLabelsMiddleware (new file reader-locale-labels-middleware.ts), beside the snapshot redaction (payload-redaction-middleware.ts) and for its reason: one registration at the engine covers the whole generic-read family (REST data routes, ObjectQL callers, the CSV/XLSX export, the MCP data tool, the AI-context interceptor), and middleware carries the door's real ExecutionContext, whose locale is the one the approvals door reads (Accept-Language, else the workspace localization settings).

  • One lookup. The labels come from readerLabelsFor and are placed by localizeRequestLabels (reader-locale-labels.ts), the approvals door's own pair. No second translation table, no new key spelling: same keys (flows.FLOW.label, flows.FLOW.approvals.NODE.label), same locale negotiation (resolveBundleLocale), same fallback to the stored text on a miss.
  • Only the columns the read carries. process_label and current_step_label are overlaid; step_label and object_label are approvals-door fields, not columns, and this door does not add them.
  • Projection kept. Setup's all_requests view selects the labels and not the machine columns. When a read selects a label without the column its key is spelled from (process_name, current_step), the middleware borrows that column into ast.fields for the read and deletes it from every row before the rows go back, so the caller is served exactly the columns it asked for.
  • Read-time only. Storage is untouched. A system read (isSystem) is served as stored even when its context carries a locale, so nothing the platform reads back and writes again holds a reader's language. No locale, no i18n service, or a missing pack entry: the stored text.
  • Registered always. Bound beside the generic-door read gate (bindRequestReadGate), not behind disableAutoHooks, because the approvals door localizes whenever the service runs.
  • Census row. The new isSystem read is named on content/docs/permissions/system-context.mdx row 17 (the plugin-approvals generic-read row) and the generated register is regenerated with node scripts/check-system-context-census.mjs --write (claim amendment 6461039298).
  • Docblocks: reader-locale-labels.ts no longer says the data door serves the stored text; sys-approval-request.object.ts notes the overlay beside the display-column comment.

Tests

New reader-locale-labels-data-door.integration.test.ts: the rig of reader-locale-display-labels.integration.test.ts (real ObjectQL over SqlDriver better-sqlite3, the real AutomationEngine opening the request through the real approval node, the real ApprovalsServicePlugin over the real in-memory i18n service with a zh-CN pack parsed by TranslationDataSchema), read through ObjectStackProtocolImplementation.findData / getData, the calls the REST data routes make.

  • zh-CN reader: Setup's list projected to all_requests' columns, the full-column list, the by-id read, a bare zh: the zh-CN labels.
  • The borrowed key columns are not served on the projected list.
  • Parity: the same row read through svc.getRequest and through the data door (list and by-id) carries equal label text.
  • A step the pack does not carry: stored step label beside the translated flow label.
  • Fences (green on both sides): ja-JP, en and no locale serve the stored text; a system read with a locale serves the stored text; no step_label / object_label column is added; the stored row keeps the authored text.

Package: pnpm --filter @objectstack/plugin-approvals test 105 files / 1331 tests passed; typecheck exit 0; build exit 0 (on f7d8e486; the only later commit, ee26b793, touches system-context.mdx alone).

Acceptance notes

  • display_title (a formula, process_label else process_name) is computed inside the engine before middleware runs, so it stays the stored text. It is not the title pointer (nameField: process_label) and no list view shows it.
  • Filter, sort, group and search over process_label / current_step_label compare the stored text, so a grid grouped by process_label heads its groups in the authoring language. Not filed: no reproduction on a shipped view (none groups by these columns).
  • count / aggregate are untouched; they serve no label text.

Verification

Red first, in-package (base f708cf2 + the test only, commit 36c5405)
× Setup's list, projected to its columns: the labels in zh-CN, and only the columns it selected
× the full-column list: the labels in zh-CN, the machine columns as stored
× the by-id read
× a bare `zh` reaches the zh-CN pack
× parity: the data door and the approvals door serve the same label text for the same row
× a step the pack does not carry is served as stored, beside the flow label it does
- "current_step_label": "经理审核",  - "process_label": "客户审批",
+ "current_step_label": "Manager Review",  + "process_label": "Account Approval",
Tests  6 failed | 4 passed (10)

Green on the fix: the new file plus reader-locale-display-labels.integration.test.ts and request-read-gate.integration.test.ts: 3 files, 36/36 passed.

Live red/green through the real REST data door (showcase, `pnpm dev -- --fresh`)

Signed in as the seeded dev admin; the showcase boot opens three pending requests whose flows its zh-CN pack translates. Red = the base blob of approvals-plugin.ts written over the file and the package rebuilt (bind count in dist/index.js 0); green = HEAD (count 2). Restored by git checkout HEAD --, blob 869363ef equal to HEAD, git diff HEAD empty.

GET /api/v1/data/sys_approval_request?$select=process_label,record_id,status,current_step_label,pending_approver_labels,submitter_id,updated_at, Accept-Language: zh-CN:

process_label current_step_label
base Invoice Dual Sign-off (parallel approval) Finance + Legal Sign-off
base High-Value Expense — Committee Quorum Committee Sign-off (2 of 3)
base Expense Report Sign-off Manager + Finance Sign-off
head 发票双重签批(并行审批) 财务 + 法务签批
head 大额费用 — 委员会多数签批 委员会签批(三分之二)
head 费用报告签批 经理 + 财务签批

On head, process_name / current_step absent from those rows; ja-JP and en serve the English text on both sides.

By id, zh-CN: base GET /data/sys_approval_request/ID served "Invoice Dual Sign-off (parallel approval)" while GET /approvals/requests/ID served "发票双重签批(并行审批)"; head serves "发票双重签批(并行审批)" / "财务 + 法务签批" on both doors. The data door row carries no step_label / object_label.

Ablations (src-resolved suite, `scripts/ablation-replace.mjs`, each restored to the HEAD blob with `git diff HEAD` empty)
  • A: the bindReaderLocaleLabelsMiddleware(...) call in approvals-plugin.ts replaced: 6 failed / 4 passed, the six zh-CN pins.
  • B: the projection widening disabled in borrowLabelKeyColumns: 3 failed (the three projected-list pins).
  • C: the borrowed-column strip disabled: 1 failed (the projected list serves process_name / current_step).
Gates (head ee26b79)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 92 families, all run, all exit 0; --ran reconciliation: 92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN. First pass reds re-run after the full build settled: check:dts-closure, check:dual-build-cjs-loads, check:i18n (each ran against a dist/ mid-rebuild), check-plugin-teardown-shape --self-test (shallow clone; fixture commit fetched), all exit 0 on re-run. check:system-context-census was a real red (the new isSystem read had no row) fixed by the row-17 edit. pnpm exec eslint --no-inline-config packages/plugins/plugin-approvals exit 0; check-nul-bytes exit 0.

Not run locally: test:repo (CI Test Core); full dogfood (no !, no (narrowing) arm).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (spec untouched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (scoped to the package).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

🤖 Generated with Claude Code


Generated by Claude Code

…in the reader's locale (red)

Claude-Session: https://claude.ai/code/session_01FEkQ5kcTaxH7yNuUvHxaPR
Co-authored-by: Claude <noreply@anthropic.com>
…erves the flow and step labels in the reader's language

Engine middleware on sys_approval_request, beside the snapshot redaction,
overlays process_label / current_step_label by the approvals door's own
readerLabelsFor + localizeRequestLabels. A projected label borrows its key
columns for the read and the rows are served without them. Storage and
system reads are untouched.

Claude-Session: https://claude.ai/code/session_01FEkQ5kcTaxH7yNuUvHxaPR
Co-authored-by: Claude <noreply@anthropic.com>
…er-locale label seam

Claude-Session: https://claude.ai/code/session_01FEkQ5kcTaxH7yNuUvHxaPR
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-approvals, touching 13 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/plugins/plugin-approvals/src/reader-locale-labels.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/data-flow.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/automation/hook-bodies.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/automation/webhooks.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/kernel/contracts/data-engine.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/kernel/contracts/index.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/kernel/events.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/permissions/attachments-access.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/permissions/field-level-security.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/permissions/record-view-auditing.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/permissions/rls.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/permissions/system-context.mdx (via bindReaderLocaleLabelsMiddleware (symbol, a top-level function), current_step_label (symbol, a field of const object LABEL_KEY_COLUMNS), process_label (symbol, a field of const object LABEL_KEY_COLUMNS), findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/protocol/objectql/query-syntax.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/protocol/objectql/schema.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/ui/react-pages.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))

⛔ 7 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/releases/v16.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/releases/v17/17-0.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/releases/v17/17-5.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/releases/v17/17-6.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/releases/v17/17-7.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))
  • content/docs/releases/v17/index.mdx (via findOne (literal, a string literal in bindReaderLocaleLabelsMiddleware))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-approvals/src/reader-locale-labels.ts) — 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 55 of 207 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 — 6 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 fbc16563a65f0352527ea97aaf55c540e89ae4da → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json fbc16563a65f0352527ea97aaf55c540e89ae4da

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-28T05:21Z · head ee26b793 · #27041

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:
    • A draft against main, assignee os-project-manager. The body carries Fixes #27041, Clause-②: no and Metadata-protocol: none, all line-initial.
    • The changeset is a patch on @objectstack/plugin-approvals.
    • check-governed-merges --pr 27092: 477 changed lines, under the human-merge threshold.
    • The claim names this PR's branch. The file surface was amended by 6461039298 to add system-context.mdx (the row-17 cell and the regenerated register line), and the diff stays inside it.
  2. Against triage (6459448516) and the order:
    • One lookup. The new engine middleware bindReaderLocaleLabelsMiddleware takes its labels from readerLabelsFor and places them with localizeRequestLabels, the approvals door's own pair. There is no second table, no new key spelling, and the same fallback to stored text.
    • The seat's lean on the seam held. The middleware is registered on sys_approval_request beside bindSnapshotRedactionMiddleware and the read gate, not behind disableAutoHooks. It reads context.locale from the door's real ExecutionContext. There is no data-door, plugin-list or plugin-grid special case.
    • Read-time only. It overlays only the process_label and current_step_label columns the row carries, and adds no step_label or object_label column. A system read is passed through as stored, so nothing the platform writes back holds a reader's language. Storage, ui/** and packages/spec/** are untouched.
    • Projection kept. A projected read borrows process_name / current_step for the key and deletes them from every row before return, including on the per-row failure path. Ablation C shows the strip is load-bearing.
  3. Red first (the dev's): the test-only commit 36c54057 on base failed 6 of 10, serving "Account Approval" / "Manager Review" to a zh-CN reader. The 4 controls passed. Live on the showcase REST data door, base served English while /approvals/requests/ID served zh-CN for the same row; the head serves zh-CN on both doors. Ablations, each restored to HEAD: A (bind removed) 6 red, B (no projection widening) 3 red, C (no borrowed-column strip) 1 red.
  4. Parity pinned: one test reads the same row through svc.getRequest and through the data door (list and by-id) and asserts equal label text.
  5. Tests: package 105 files, 1,331 tests passed; typecheck and build exit 0 (on f7d8e486; ee26b793 touches only the mdx).
  6. Gates:
    • Locally 92 of 92 derived families exit 0, with 0 NOT-MEASURED. The first-pass reds were dist-rebuild races and a shallow-clone fixture, each exit 0 on re-run. The one real red, the system-context census, was fixed by the row-17 edit.
    • CI on head ee26b793: 37 success, 11 skipped, 0 red.
  7. Overlap: git merge-tree reports no conflict against main fbc16563.
  8. Noted, not filed:
    • display_title (a formula over process_label) is computed before the middleware runs, so it stays the stored text. No view shows it.
    • Filter, sort and group on the two label columns compare stored text. No shipped view groups by them.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 05:23
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 05:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 58d4ee6 Oct 28, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-srtrac branch October 28, 2026 06:09
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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants