Repository navigation
Description
domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P
Filing gate: ③ maintainer-directed task. Ruling F on #30172: the maintainer answered 「同意」 to the recommendation in the decision thread on 2026-11-05, recorded word for word by domain:server#2 in #30172 comment 6670405874. Its parameter 5 splits the work: the spec half first, then "the server card … (plugin-email prefetch + render)". This is that server card. No domain:* label: triage routes it; the ruling names domain:server.
What the maintainer ordered (parameters 3 and 5, server side)
plugin-emailbuilds a label table perobject.fieldthat the template names, in the recipient's locale (thereaderOptionLabelstranslation path,packages/objectql/src/formula-option-labels.ts), before the synchronous render.- No recipient locale ⇒ the declared labels.
- A value outside the field's options prints as stored.
- A missing object or field, or a field with no options ⇒
TEMPLATE_INVALID, and the whole send is refused. - Pins the ruling lists: an
enand azh-CNrecipient each see their own labels; a scheduled flow that runs as the system still renders per recipient; a non-option value prints as stored.
Where the spec half stands
- PR feat(spec,formula,lint): a mail template's
optionformatter prints a select value as its option label in the recipient's language #30727 (branchclaude/project-thread-d8v0qv,Fixes #30172) adds the mail formatter{{ stage | option:opportunity.stage }}: the grammar, the lint check and the formula renderer that reads a label table the caller hands in. Its contract review 6672632729 asked that the renderer refuse when no table for the key reached it, the waycurrencywith no default refuses; that patch is in progress on the same branch. - Until this card lands, nothing hands the renderer a table, so a template that uses
optionvalidates and is then refused at send. Sodomain:spec#2holds PR feat(spec,formula,lint): a mail template'soptionformatter prints a select value as its option label in the recipient's language #30727 out of the merge queue until this card's PR is green, and the two land back to back, spec first. Build on PR feat(spec,formula,lint): a mail template'soptionformatter prints a select value as its option label in the recipient's language #30727's branch, not onmain. Blocked-byis deliberately not set: it would wait on A flow notice cannot name a select field's option by its label in the recipient's language: the mail renderer has no label formatter and the flow passes no option labels to CEL label() #30172 closing, which only happens when PR feat(spec,formula,lint): a mail template'soptionformatter prints a select value as its option label in the recipient's language #30727 lands.
Business reason
HotCRM (hotcrm#2391, hotcrm#2406): a stalled-deal notice printed the stage as needs_analysis, and a scheduled flow printed English labels to a zh-CN recipient. Today every app has to build a per-locale label map by hand, and it only covers the locales the app ships.
Who acts
The domain:server seat that takes plugin-email (the ruling was recorded by domain:server#2). It reports its PR to domain:spec#2 (mailbox #30557) so both land together.
Dedupe: is:issue plugin-email option formatter label table → 8 hits (2 open seat posts, #30172 itself, 5 closed and unrelated); none carries this work.
Generated by Claude Code
Activity
objectstack-fleet commentedon Nov 6, 2026
domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P
Carry note from the spec half's contract review · 2026-11-06T00:43Z
The spec half (PR #30727, delta review 6673435245, PASS) makes the
optionformatter refuse when the caller hands it no label table for the hole'sobject.field:formatValuethrows aRangeErrorcarryingoptionTableRefusal(...)(exported from@objectstack/formula).Until this card lands, plugin-email reaches that refusal as a bare
RangeErrorfromformatValue(template-engine.ts L161), outside theTEMPLATE_INVALIDvocabulary theIEmailServicecontract states;sendTemplaterethrows it (email-service.ts L1607) and nothing is sent.What this card owes on top of the label-table prefetch and per-recipient render: ask
optionTableRefusalbeforeformatValueand prefixTEMPLATE_INVALID, the same way plugin-email already askscurrencyDefaultRefusal(template-engine.ts L147). An empty option list ({}in the table) answersnullfrom the formula side and is this card'sTEMPLATE_INVALIDto decide, not the formatter's.Landing order: the seat holds #30727 until this card's PR is green, then lands the spec half first and this one right after.
Generated by Claude Code
objectstack-fleet commentedon Nov 6, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p3 ·
domain:server· 2026-11-06T00:53Z⛔ Not a claim.
plugin-emailrenders a template'soptionformatter with each recipient's option labels. p3, inherited from A flow notice cannot name a select field's option by its label in the recipient's language: the mail renderer has no label formatter and the flow passes no option labels to CEL label() #30172. Serial after the spec half (ruling parameter 5).Generated by Claude Code
objectstack-fleet commentedon Nov 6, 2026
domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7
Claim: PM loop round 3 · 2026-11-06T01:43Z
Session:
session_016WV4cyvF7SP65frVFAYLv7Account:
os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee)Branch: the card thread's designated
claude/project-thread-*branch, named in a claim amendment once the thread reports it; built on PR #30727's branchclaude/project-thread-d8v0qvas the card says, not onmainWorktree:
objectstack-issue-30730Domain:
domain:serverSeat:
domain:server#5File surface:
packages/plugins/plugin-email/src/**(the label-table prefetch perobject.fielda template names, in the recipient's locale through thereaderOptionLabelspath, before the synchronous render;optionTableRefusalasked beforeformatValueand prefixedTEMPLATE_INVALID, ascurrencyDefaultRefusalis), a read-only use ofpackages/objectql/src/formula-option-labels.ts(an export added there only if none reaches plugin-email; say so in the report), their tests, one changeset. ⛔packages/spec/**, ⛔packages/formula/**(PR #30727's), ⛔ui/**(stop on breach; explain in the report)Container & model:
M,mode:thread,model: default,effort: highClause-②: no (provisional: a template using
optionwas refused at send before and renders after; the dev re-derives it against PR #30727's contract)Metadata-protocol: none
Responsibility: plugin-email hands the
optionformatter no label table, so a template using it validates and is then refused at send | none | HotCRM and every app mailing an option field (hotcrm#2391, hotcrm#2406)Thread-read: 6673488234
Premise: holds per ruling F on #30172 (6670405874, parameters 3 and 5) and the spec half's carry note 6673488234; the dev reproduces the send refusal on PR #30727's head first
Serial constraints cleared: lands right after PR #30727 (spec first, back to back;
domain:spec#2holds #30727 until this card's PR is green, mailbox #30557). The dev re-checks open PRs onpackages/plugins/plugin-email/**and mergesorigin/mainafter any landsobjectstack-fleet commentedon Nov 6, 2026
domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7
Claim: PM loop round 3 · 2026-11-06T01:46Z (re-posted whole with the branch, superseding the claim above)
Session:
session_016WV4cyvF7SP65frVFAYLv7Account:
os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/project-thread-9ookwa(the card thread's designated branch, dev sessionsession_016WV4czh582ycXhGxZNVPVi), branched from PR #30727's head onclaude/project-thread-d8v0qvas the card says, not frommainWorktree:
objectstack-issue-30730Domain:
domain:serverSeat:
domain:server#5File surface:
packages/plugins/plugin-email/src/**(the label-table prefetch perobject.fielda template names, in the recipient's locale through thereaderOptionLabelspath, before the synchronous render;optionTableRefusalasked beforeformatValueand prefixedTEMPLATE_INVALID, ascurrencyDefaultRefusalis), a read-only use ofpackages/objectql/src/formula-option-labels.ts(an export added there only if none reaches plugin-email; say so in the report), their tests, one changeset. ⛔packages/spec/**, ⛔packages/formula/**(PR #30727's), ⛔ui/**(stop on breach; explain in the report)Container & model:
M,mode:thread,model: default,effort: highClause-②: no (provisional: a template using
optionwas refused at send before and renders after; the dev re-derives it against PR #30727's contract)Metadata-protocol: none
Responsibility: plugin-email hands the
optionformatter no label table, so a template using it validates and is then refused at send | none | HotCRM and every app mailing an option field (hotcrm#2391, hotcrm#2406)Thread-read: 6673488234
Premise: holds per ruling F on #30172 (6670405874, parameters 3 and 5) and the spec half's carry note 6673488234; the dev reproduces the send refusal on PR #30727's head first
Serial constraints cleared: lands right after PR #30727 (spec first, back to back;
domain:spec#2holds #30727 until this card's PR is green, mailbox #30557). The dev re-checks open PRs onpackages/plugins/plugin-email/**and mergesorigin/mainafter any landsobjectstack-fleet commentedon Nov 6, 2026
domain:server#5 · os-elon-musk · session_016WV4czh582ycXhGxZNVPVi
os-dev-report
PR #30781 (draft, head
f5fee1af): plugin-email renders anoptionhole with each recipient's option labels; a hole it cannot label refuses the sendTEMPLATE_INVALID. Built on PR #30727's head98f61ec6,origin/mainmerged.{ "issue": 30730, "status": "done", "branch": "claude/project-thread-9ookwa", "pr": "https://github.com/objectstack-ai/objectstack/pull/30781", "head": "f5fee1af", "base": "built on PR #30727's head 98f61ec6 (claude/project-thread-d8v0qv), then origin/main b3098661 merged in (plain merge, no rebase)", "session": "session_016WV4czh582ycXhGxZNVPVi", "premise_still_valid": true, "summary": "Reproduced on 98f61ec6: sendTemplate of a template with an option hole threw a bare RangeError (optionTableRefusal) for en-US and zh-CN. plugin-email now builds a label table per object.field the template's option holes name, once per send, from the served object (IDataEngine.getObject) with the i18n bundle laid over the option fields by translateObject, the readerOptionLabels step sequence, in the send's own locale (the recipient's; no locale reads declared labels); faces are judged first with judgeMailHoleGrammar(face, {objects, liveUniverse: true}), so an unknown object, unknown field, field with no options, or (decided here) an empty option list refuses the whole send TEMPLATE_INVALID. The renderer asks optionTableRefusal before formatValue and prefixes TEMPLATE_INVALID, and also refuses a {} table. The plugin wires the sources lazily at each send. formula-option-labels.ts is untouched: objectql is only a devDependency of plugin-email, so no export there reaches it at runtime; the same steps go through translateObject and declaresOptions (the service-analytics precedent) and a parity test holds the answers equal to a real ObjectQL label() read.", "tests": "head f5fee1af after whole-tree turbo build (119/119): pnpm --filter @objectstack/plugin-email test && typecheck under os-verify-lock, Test Files 50 passed (50), Tests 825 passed (825), VERDICT command-exit 0. New: template-option-formatter.render.test.ts (18 cases: en-US and zh-CN faces, system-run one payload to three recipients, no locale declared labels, out-of-options as stored, multi-select, picklist translation, four refusals with nothing sent, no sources TEMPLATE_INVALID, renderer {} refusal, no option hole asks nothing, parity vs ObjectQL label() for zh-CN/zh/en-US/none) and template-option-labels.integration.test.ts (2 cases on a booted ObjectKernel + ObjectQLPlugin + EmailServicePlugin). Repro: the integration test on 98f61ec6 plugin-email sources, 2 failed / 2 (bare RangeError). Ablation via ablation-replace: buildOptionLabelTable locale arg preferred || undefined replaced by undefined (anchor 1 to 0, blob 61d0dba1 to fa994bdd), 5 failed / 15 passed; restored blob 61d0dba1 equals HEAD, git diff HEAD empty. Tests import src, no dist leg.", "gates": "head f5fee1af: dispatch-gates --commands derived 105 families, 105 exit 0; --ran reconciliation 105 derived, 105 run, 0 NOT-MEASURED, 0 UNRUN; beyond the derivation pnpm lint 0, pnpm check:cli-command-ids 0, pnpm check:adr-symbol-anchors 0; eslint --no-inline-config on the six changed ts files 0. Record: /mnt/project-files/server-lane-seat5/30730/gates-ran-f5fee1af.txt. CI on PR 30781: in progress at report time.", "dogfood_battery": "n/a — no ! in the PR title and Clause-② is no, no (narrowing) arm", "mcp_calls": "4 reads — mcp__github__issue_read get x1 and get_comments x2 (30730), get_comments x1 (30172); no MCP write tool", "api_writes": "2 relay writes — pr_create (POST /repos/objectstack-ai/objectstack/pulls, draft, no assignees, no labels) and this report comment (POST issues/30730/comments via post-stamped); plus git push (not REST)", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "carrier: seat · noted, not filed — the readerOptionLabels steps now exist twice (objectql formula-option-labels.ts and plugin-email option-label-table.ts, plus service-analytics' translateSelectOptions); a shared module both runtime packages depend on would delete the copies; PR Acceptance notes", "carrier: seat · noted, not filed — the scheduled-flow-as-system pin runs at the plugin-email layer (one payload, recipient locale, no user or organization); the notify fan-out through service-automation and service-messaging is outside this card's surface and not driven" ] }Report taken 2026-11-06T02:45Z.