Skip to content

Widen SendEmailInput with organizationId so sys_email can be stamped at its producers (Decision 2 of #11303) #11741

Description

@os-sam

Cut out of #11303 by the domain:services seat on the maintainer's ruling of 2026-08-24, and filed for the spec seat. ⛔ No pm:queue — grading is triage's field.

The ruling this exists to carry out

Maintainer, 2026-08-24, live PM chat: 「其他按照你的建议继续」, recorded on #11303 at 5396411781, Decision 2 verbatim:

sys_email: separate card, ruled to file. Widening SendEmailInput in packages/spec/src/contracts/email-service.ts is Clause-② work with spec ownership: thread from callers that hold an organization (the email channel's delivery.notification.organizationId), absent stays legal where the caller genuinely has none (auth verification/reset mail). The services seat files it with this provenance; ⛔ never smuggled into the merged PR #11698.

The parent ruling it sits under (#11303, 5393621706): 「11303 sys_inbox_message/sys_notification/sys_email 应该写 organization_id。」 — a gap, not a design choice, with the producers as the fix site.

Why sys_email was the one table the services PR could not do

PR #11698 (merged) stamped the notification family at its two broken producers. sys_email stopped at a fence, deliberately:

  • Threading an organization to the sys_email writer requires adding organizationId to SendEmailInputpackages/spec/src/contracts/email-service.ts. The services lane has zero packages/spec ownership, so it reported rather than guessing at a public contract.
  • Measured on that branch: plugin-email has no organization handling of any kind today — zero matches across email-service.ts, email-plugin.ts, sys-email-payload.ts.
  • Its writer runs under a constant SYSTEM_CTX with no execution context to read, so there is nothing local to derive from. ⭐ That is precisely why the contract has to carry the value: any in-adapter resolution would be fabrication, which the parent ruling forbids outright.

The shape, as ruled

  • Add organizationId to SendEmailInput (optional — see below).
  • Thread it from callers that hold one. The named case: service-messaging's email channel already has delivery.notification.organizationId in hand.
  • Absent stays legal where the caller genuinely has none — auth verification and password-reset mail have no organization, and forcing one there would be the fabrication this whole card exists to avoid.

The failure direction that matters, carried over from the parent ruling and worth restating because it is the reason for the optionality: a wrong organization_id is worse than a null. A null is visibly missing; a wrong value is silently authoritative to every report, export and cleanup script that filters by organization.

⚠️ Clause-② — yes

Widening a public contract surface in packages/spec hits it on both limbs (path and content). CONTRACT_REVIEW_TIER was claude-fable-5 when the services seat last read it live — re-read it at dispatch rather than trusting this line.

Suggested pins, for whoever takes it

  • A producer holding an organization stamps it; a producer genuinely without one writes no organization and is not refused (the over-denial control — refusing would break auth mail and every first boot, which is the same trade Decision 3 confirmed on the parent card).
  • ⭐ Pin identities, not counts — assert which producer stamped, not that "some rows have it".

Not measured

  • The full SendEmailInput caller census. Only the messaging email channel is named by the ruling as holding an organization; whether others do has not been swept.
  • Whether sys_email's existing 96 org-less rows matter to anyone. They do not need to: the parent ruling is forward-stamping only, no backfill (maintainer's standing 2026-08-23 precedent 「10950 不考虑存量」).

Refs: #11303 (parent, the ruling and the producer census) · PR #11698 (the merged services half; ⛔ this must not be retrofitted into it) · #8287 (subject-record organization precedence)

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions