You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] Template-path notify's inbox channel degrades to topic-as-title: IEmailService has no render-only seam, so localized templates cannot reach sys_inbox_message #9225
Measured while landing #9205 (PR #9224 — notify nodes reference email templates, resolved (name, recipient locale) at delivery). Filed unassigned — recording, not claiming.
The gap
A template-path notify node with channels: ['inbox', 'email'] now delivers a localized email and an inbox row whose title is the topic and whose body is empty (honest degraded rendering, deliberately not an empty string — see PR #9224's "Inbox channel: gap documented" section).
The inbox channel writes sys_inbox_message rows straight from the notification title/body; it is not wired to any template subsystem (NotificationTemplateStore is keyed by (topic, channel, locale) — a different subsystem from sys_email_template anyway).
IEmailService exposes only send/sendTemplate — there is NO render-only method. The locale ladder + {{var}} renderer (with ADR-0053 format filters) live inside plugin-email, unreachable without sending mail.
Natural shape (from the landing analysis, for triage to price)
A small render-only method on IEmailService (e.g. renderTemplate({ template, locale, data }) → { subject, html/text }), implemented once in plugin-email next to the existing resolver, consumed by the inbox channel the same way the email channel consumes sendTemplate. Contract change on a service interface ⇒ if promoted, the implementing card is domain:spec-adjacent (contract face) and per standing tiering carries the fable floor.
Consumers waiting
hotcrm's 16 notify nodes (4 locales) unlock at #9205's landing for the email channel; their inbox half stays degraded until this seam exists.
First-touch triage: promoted finding → needs-user-decision, type Feature, domain:spec. The proposed seam is a new method on IEmailService (packages/spec/src/contracts/email-service.ts, verified on origin/main) — a public contract expansion, which is the human floor by standing rule; no seat may adjudicate it.
Decision requested: add a render-only method to IEmailService (e.g. renderTemplate({ template, locale, data }) → { subject, body }), implemented once in plugin-email beside the existing resolver and consumed by the inbox channel — versus accepting the documented degraded inbox rendering (topic-as-title).
Four prisms:
Platform long-term coherence — the seam removes the forced choice between "duplicate the resolver in service-messaging" (a second de-facto contract that drifts) and "inbox stays degraded". One resolver, two channels; shrinks special-casing.
AI-agent error-resistance — a declared contract method is the shape an AI consumer cannot get wrong; per-channel hand-rolled template resolution is the drift bed.
Startup scope discipline — the against-axis: every contract method is a permanent obligation, and the degraded state is honest and documented. If inbox localization has no pull beyond hotcrm polish, defer is defensible.
Recommendation: approve the render-only method, scoped strictly to rendering (no send-path change). If approved, the implementing card is domain:spec contract face ⇒ clause-② tier floor.
Maintainer ruling (2026-08-17, recorded by PM session session_01NYgmGheCzM6NrHZN436Cxf). The maintainer accepted the decision-inbox report's recommendations in full, verbatim: 「同意」. For this card:
Ruled: approved — IEmailService gains a render-only method (renderTemplate({ template, locale, data }) → { subject, body }), implemented once in plugin-email beside the existing locale ladder + {{var}} renderer, consumed by the inbox channel the way the email channel consumes sendTemplate.
Strictly render-only: zero send-path changes; the method must not touch delivery, queueing, or outbox behaviour.
Basis: one resolver, two channels (kills the forced choice between a duplicated resolver in service-messaging and a permanently degraded inbox); the consumer is concrete and present — hotcrm's 16 notify nodes × 4 locales get localized inbox rows the day this lands.
Contract expansion on packages/spec/src/contracts/email-service.ts ⇒ claude-fable-5 mandatory, spec lane; consumer-package fixture triage per standing rule.
First-touch grading completes: label flipped needs-user-decision → pm:queue in the same stroke.
Claim: PM loop round 1
Session: session_01Fs18A2DdXLVN2h8PaaFBcP
Branch: claude/issue-9225-email-render-only-seam
Worktree: objectstack-issue-9225
Domain: domain:spec
File surface: packages/spec/src/contracts/email-service.ts (contract method, region: IEmailService interface + input/result types), packages/plugins/plugin-email/** (single implementation beside the existing resolver), packages/services/service-messaging/** (inbox channel consumption only), tests + .changeset/ (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: fable — mandatory per the maintainer's 2026-08-17 ruling on this card (contract expansion on packages/spec/src/contracts/email-service.ts); this round's node scripts/pm/dispatch-gates.mjs --tier output flags the same path as clause-② suspect surface with no path-derived mandate — tier judged from card content, which is exactly the clause-② limb (public surface widens)
Serial constraints cleared: #9228 in flight (packages/spec/src/data/** filter door, direct-dispatch channel) — disjoint; #9182 in flight (session session_01Y26DJEHSBhhAQ6wwfsHNza, references generator + generated content/docs/references/api/**) — file-disjoint but read-coupled if the new contract method regenerates reference pages; second lander runs scripts/pm/os-regen-merge.sh; #9157 / PR #9191 (metadata-protocol/protocol.ts) — disjoint; no open PR touches email-service.ts, plugin-email, or service-messaging (checked this round). No in-flight sibling's pins assert this card's surface.
{
"issue": 9225,
"status": "done",
"branch": "claude/issue-9225-email-render-only-seam",
"pr": "https://github.com/objectstack-ai/objectstack/pull/9248",
"premise_still_valid": true,
"summary": "Implemented the ruled render-only seam: IEmailService.renderTemplate({ template, data, locale, timezone }) -> { subject, html, text }, implemented once in plugin-email by extracting the resolver sendTemplate already used (ladder, active check, variables_json validation, row-locale render opts moved verbatim; sendTemplate now delivers what the shared resolver renders — zero send-path changes, existing sendTemplate suite passes unchanged). The inbox channel consumes it the way the email channel consumes sendTemplate: payload.template renders subject -> title, text -> body_md per recipient; missing service/method or renderer error codes fail the delivery loudly and classifyError grades TEMPLATE_NOT_FOUND/TEMPLATE_INACTIVE/MISSING_VARIABLES/TEMPLATE_UNSUPPORTED permanent. Return-shape divergence (dispatch assumption 1) measured and recorded in the PR body: ruling spelled { subject, body }, card body { subject, html/text }; sys_email_template carries subject/body_html/body_text? and the inbox needs title+body_md, so the honest shape is { subject, html, text }. Assumption 2 held: inbox write site consumed the rendered content with no new contract face. Consumer fixture triage (direction: downstream consumers of @objectstack/spec): repo-wide sweep for 'implements IEmailService' and ': IEmailService' found exactly one non-plugin-email implementer (a plugin-auth test fake), which gained the required method.",
"tests": "All at 567619ff7, clean tree, after final commit. pnpm --filter @objectstack/spec test: 407 files / 10846 passed. plugin-email test: 26 files / 417 passed. service-messaging test: 22 files / 242 passed. plugin-auth test: 55 files / 1261 passed. typecheck (spec, plugin-email, service-messaging, plugin-auth, rest): clean — the new-contract-name imports passing typecheck proves the rebuilt spec .d.ts was read. Reverse verification from committed state: inbox-channel.ts restored to origin/main => 5 of 6 new template-path tests red ('1 failed | 21 passed', the sixth green vacuously on the old channel — direction as observed, not templated); restored, green. Gates re-derived from actual diff paths and all green: changeset-gate-self-tests, cross-package-test-inputs, merge-driver, objectui-changeset, spec-parsed-alias, test-source-alias, type-source-resolution, query-options-erasure, nul-bytes, spec empty-state/liveness/strictness-ledger/variant-docs, doc-formula-expressions, adr-0087-registration, changeset-no-major, empty-changeset, dev-prereqs, affected-docs, type-check-coverage, type-check-debt --re-measure (none above recorded), engine-double-contract, where-matcher, i18n. Re-derivation added beyond the dispatch list: the test-file convention gates and changeset gates (all run green); spec check:generated --fix regenerated exactly api-surface + export-origins (the two new interface exports).",
"open_questions": [],
"out_of_scope_findings": []
}
Verified on GitHub against the review checklist, not from the report's self-description:
Ruling compliance: the maintainer-ruled seam is implemented exactly — renderTemplate on IEmailService, one resolver extracted in plugin-email (send path byte-for-byte unchanged; the existing sendTemplate suite passing unmodified is the zero-send-path evidence), inbox channel consumes it the way the email channel consumes sendTemplate.
Return-shape divergence handled through the pre-authorized mechanism-assumption channel: ruling spelled { subject, body }, card spelled { subject, html/text }; the dev measured sys_email_template's actual columns and implemented { subject, html, text }, with the reasoning recorded in the PR body. Accepted as the ruling's substance (render-only, one resolver) made concrete.
Scope: 11 files, all within the claimed surface plus two disciplined increments — the plugin-auth test fake gaining the now-required method (consumer fixture triage; the only non-plugin-email IEmailService implementer in the sweep, direction stated) and the regenerated api-surface/export-origins pair (exactly the two new interface exports).
Clause-② enqueue gate: diff touches packages/spec/src/contracts/email-service.ts; dispatch tier was claude-fable-5 = CONTRACT_REVIEW_TIER (single-sourced from scripts/pm/dispatch-gates.mjs) — not below the floor, so the PR may enqueue without needs:contract-review.
Changeset: minor for the three published packages, present.
Evidence: real commands and counts per consumer package; reverse verification is directional (5/6 new tests red on the restored channel, the vacuous sixth reported honestly).
Landing: ESLint and TypeScript Type Check conclusions at head 567619ff7 will be verified before flipping ready; this PR carries generated artifacts, so if main moves on them before merge, the landing runs scripts/pm/os-regen-merge.sh per standing procedure.
Activity
os-zhuang commentedon Aug 17, 2026
First-touch triage: promoted
finding→needs-user-decision, type Feature,domain:spec. The proposed seam is a new method onIEmailService(packages/spec/src/contracts/email-service.ts, verified onorigin/main) — a public contract expansion, which is the human floor by standing rule; no seat may adjudicate it.Decision requested: add a render-only method to
IEmailService(e.g.renderTemplate({ template, locale, data }) → { subject, body }), implemented once in plugin-email beside the existing resolver and consumed by the inbox channel — versus accepting the documented degraded inbox rendering (topic-as-title).Four prisms:
notifynodes cannot be localized:title/messageare raw strings with no template or locale channel, so a four-locale app sends English-only notifications #9205's landing but topic-as-title inbox rows until this seam exists. A concrete consumer, not speculative surface.Recommendation: approve the render-only method, scoped strictly to rendering (no send-path change). If approved, the implementing card is
domain:speccontract face ⇒ clause-② tier floor.Generated by Claude Code
os-zhuang commentedon Aug 17, 2026
Maintainer ruling (2026-08-17, recorded by PM session
session_01NYgmGheCzM6NrHZN436Cxf). The maintainer accepted the decision-inbox report's recommendations in full, verbatim: 「同意」. For this card:Ruled: approved —
IEmailServicegains a render-only method (renderTemplate({ template, locale, data }) → { subject, body }), implemented once in plugin-email beside the existing locale ladder +{{var}}renderer, consumed by the inbox channel the way the email channel consumessendTemplate.packages/spec/src/contracts/email-service.ts⇒claude-fable-5mandatory, spec lane; consumer-package fixture triage per standing rule.First-touch grading completes: label flipped
needs-user-decision→pm:queuein the same stroke.Generated by Claude Code
os-steve commentedon Aug 17, 2026
Claim: PM loop round 1
Session:
session_01Fs18A2DdXLVN2h8PaaFBcPBranch:
claude/issue-9225-email-render-only-seamWorktree:
objectstack-issue-9225Domain:
domain:specFile surface:
packages/spec/src/contracts/email-service.ts(contract method, region:IEmailServiceinterface + input/result types),packages/plugins/plugin-email/**(single implementation beside the existing resolver),packages/services/service-messaging/**(inbox channel consumption only), tests +.changeset/(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: fable— mandatory per the maintainer's 2026-08-17 ruling on this card (contract expansion onpackages/spec/src/contracts/email-service.ts); this round'snode scripts/pm/dispatch-gates.mjs --tieroutput flags the same path as clause-② suspect surface with no path-derived mandate — tier judged from card content, which is exactly the clause-② limb (public surface widens)Serial constraints cleared: #9228 in flight (
packages/spec/src/data/**filter door, direct-dispatch channel) — disjoint; #9182 in flight (sessionsession_01Y26DJEHSBhhAQ6wwfsHNza, references generator + generatedcontent/docs/references/api/**) — file-disjoint but read-coupled if the new contract method regenerates reference pages; second lander runsscripts/pm/os-regen-merge.sh; #9157 / PR #9191 (metadata-protocol/protocol.ts) — disjoint; no open PR touchesemail-service.ts, plugin-email, or service-messaging (checked this round). No in-flight sibling's pins assert this card's surface.Generated by Claude Code
feat(messaging): IEmailService render-only renderTemplate seam, consu…
os-steve commentedon Aug 17, 2026
{ "issue": 9225, "status": "done", "branch": "claude/issue-9225-email-render-only-seam", "pr": "https://github.com/objectstack-ai/objectstack/pull/9248", "premise_still_valid": true, "summary": "Implemented the ruled render-only seam: IEmailService.renderTemplate({ template, data, locale, timezone }) -> { subject, html, text }, implemented once in plugin-email by extracting the resolver sendTemplate already used (ladder, active check, variables_json validation, row-locale render opts moved verbatim; sendTemplate now delivers what the shared resolver renders — zero send-path changes, existing sendTemplate suite passes unchanged). The inbox channel consumes it the way the email channel consumes sendTemplate: payload.template renders subject -> title, text -> body_md per recipient; missing service/method or renderer error codes fail the delivery loudly and classifyError grades TEMPLATE_NOT_FOUND/TEMPLATE_INACTIVE/MISSING_VARIABLES/TEMPLATE_UNSUPPORTED permanent. Return-shape divergence (dispatch assumption 1) measured and recorded in the PR body: ruling spelled { subject, body }, card body { subject, html/text }; sys_email_template carries subject/body_html/body_text? and the inbox needs title+body_md, so the honest shape is { subject, html, text }. Assumption 2 held: inbox write site consumed the rendered content with no new contract face. Consumer fixture triage (direction: downstream consumers of @objectstack/spec): repo-wide sweep for 'implements IEmailService' and ': IEmailService' found exactly one non-plugin-email implementer (a plugin-auth test fake), which gained the required method.", "tests": "All at 567619ff7, clean tree, after final commit. pnpm --filter @objectstack/spec test: 407 files / 10846 passed. plugin-email test: 26 files / 417 passed. service-messaging test: 22 files / 242 passed. plugin-auth test: 55 files / 1261 passed. typecheck (spec, plugin-email, service-messaging, plugin-auth, rest): clean — the new-contract-name imports passing typecheck proves the rebuilt spec .d.ts was read. Reverse verification from committed state: inbox-channel.ts restored to origin/main => 5 of 6 new template-path tests red ('1 failed | 21 passed', the sixth green vacuously on the old channel — direction as observed, not templated); restored, green. Gates re-derived from actual diff paths and all green: changeset-gate-self-tests, cross-package-test-inputs, merge-driver, objectui-changeset, spec-parsed-alias, test-source-alias, type-source-resolution, query-options-erasure, nul-bytes, spec empty-state/liveness/strictness-ledger/variant-docs, doc-formula-expressions, adr-0087-registration, changeset-no-major, empty-changeset, dev-prereqs, affected-docs, type-check-coverage, type-check-debt --re-measure (none above recorded), engine-double-contract, where-matcher, i18n. Re-derivation added beyond the dispatch list: the test-file convention gates and changeset gates (all run green); spec check:generated --fix regenerated exactly api-surface + export-origins (the two new interface exports).", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
os-steve commentedon Aug 17, 2026
ACCEPT — PR #9248 (reviewer of record: spec seat, session
session_01Fs18A2DdXLVN2h8PaaFBcP, round 1).Verified on GitHub against the review checklist, not from the report's self-description:
renderTemplateonIEmailService, one resolver extracted in plugin-email (send path byte-for-byte unchanged; the existingsendTemplatesuite passing unmodified is the zero-send-path evidence), inbox channel consumes it the way the email channel consumessendTemplate.{ subject, body }, card spelled{ subject, html/text }; the dev measuredsys_email_template's actual columns and implemented{ subject, html, text }, with the reasoning recorded in the PR body. Accepted as the ruling's substance (render-only, one resolver) made concrete.TEMPLATE_UNSUPPORTED, graded permanent) when no render-capable email service exists, instead of silently degrading to topic-as-title. Declared = enforced doctrine; the template path itself only landed with Flownotifynodes cannot be localized:title/messageare raw strings with no template or locale channel, so a four-locale app sends English-only notifications #9205 yesterday, so blast radius is effectively zero.IEmailServiceimplementer in the sweep, direction stated) and the regeneratedapi-surface/export-originspair (exactly the two new interface exports).packages/spec/src/contracts/email-service.ts; dispatch tier wasclaude-fable-5=CONTRACT_REVIEW_TIER(single-sourced fromscripts/pm/dispatch-gates.mjs) — not below the floor, so the PR may enqueue withoutneeds:contract-review.Landing: ESLint and TypeScript Type Check conclusions at head
567619ff7will be verified before flipping ready; this PR carries generated artifacts, so ifmainmoves on them before merge, the landing runsscripts/pm/os-regen-merge.shper standing procedure.Generated by Claude Code
feat(messaging): IEmailService gains a render-only renderTemplate sea…
chore(spec): re-anchor six liveness ledgers to consuming symbols (obj…