Skip to content

plugin-email: render a mail template's option formatter with each recipient's option labels (server half of ruling F on #30172) #30730

Open

Description

@objectstack-fleet

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-email builds a label table per object.field that the template names, in the recipient's locale (the readerOptionLabels translation 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 en and a zh-CN recipient 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

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

  1. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    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 option formatter refuse when the caller hands it no label table for the hole's object.field: formatValue throws a RangeError carrying optionTableRefusal(...) (exported from @objectstack/formula).

    Until this card lands, plugin-email reaches that refusal as a bare RangeError from formatValue (template-engine.ts L161), outside the TEMPLATE_INVALID vocabulary the IEmailService contract states; sendTemplate rethrows 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 optionTableRefusal before formatValue and prefix TEMPLATE_INVALID, the same way plugin-email already asks currencyDefaultRefusal (template-engine.ts L147). An empty option list ({} in the table) answers null from the formula side and is this card's TEMPLATE_INVALID to 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

  2. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p3 · domain:server · 2026-11-06T00:53Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

    Claim: PM loop round 3 · 2026-11-06T01:43Z
    Session: session_016WV4cyvF7SP65frVFAYLv7
    Account: os-elon-musk (the seat's linked user as GET /user answers 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 branch claude/project-thread-d8v0qv as the card says, not on main
    Worktree: objectstack-issue-30730
    Domain: domain:server
    Seat: domain:server#5
    File surface: packages/plugins/plugin-email/src/** (the label-table prefetch per object.field a template names, in the recipient's locale through the readerOptionLabels path, before the synchronous render; optionTableRefusal asked before formatValue and prefixed TEMPLATE_INVALID, as currencyDefaultRefusal is), a read-only use of packages/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: high
    Clause-②: no (provisional: a template using option was 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 option formatter 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#2 holds #30727 until this card's PR is green, mailbox #30557). The dev re-checks open PRs on packages/plugins/plugin-email/** and merges origin/main after any lands

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 6, 2026
  5. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    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_016WV4cyvF7SP65frVFAYLv7
    Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/project-thread-9ookwa (the card thread's designated branch, dev session session_016WV4czh582ycXhGxZNVPVi), branched from PR #30727's head on claude/project-thread-d8v0qv as the card says, not from main
    Worktree: objectstack-issue-30730
    Domain: domain:server
    Seat: domain:server#5
    File surface: packages/plugins/plugin-email/src/** (the label-table prefetch per object.field a template names, in the recipient's locale through the readerOptionLabels path, before the synchronous render; optionTableRefusal asked before formatValue and prefixed TEMPLATE_INVALID, as currencyDefaultRefusal is), a read-only use of packages/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: high
    Clause-②: no (provisional: a template using option was 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 option formatter 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#2 holds #30727 until this card's PR is green, mailbox #30557). The dev re-checks open PRs on packages/plugins/plugin-email/** and merges origin/main after any lands

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions