Repository navigation
feat(plugin-email): every email template can read the deployment address as {{baseUrl}} - #29478
Conversation
…eUrl Claude-Session: https://claude.ai/code/session_01ADHkmStumMvkJihuBZyh4u Co-authored-by: Claude <noreply@anthropic.com>
…angeset Claude-Session: https://claude.ai/code/session_01ADHkmStumMvkJihuBZyh4u Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c49dbccb19f065263384647d331226132fdd3764 && git checkout c49dbccb19f065263384647d331226132fdd3764
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cafe8ee62eb2513cbb36ae83e73aa84b2bf84714 f224fedf2b078ea457730b7a7544b7fbc22d24af && git checkout -B drift-repro cafe8ee62eb2513cbb36ae83e73aa84b2bf84714 && git merge --no-ff f224fedf2b078ea457730b7a7544b7fbc22d24af
node scripts/docs-audit/affected-docs.mjs --json cafe8ee62eb2513cbb36ae83e73aa84b2bf84714 |
|
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67 Contract-review-claim: session_01ADHkmQJdgarUjPLNgFBY67 · head f224fed · 2026-11-02T16:10Z |
|
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67 Contract reviewServed-tier: Inputs read: card #29095 (body; triage 6572007785; director 6573083963 and ruling A 6573566446; the seat's claim 6580514849; dev report 6581879408 with its three open_questions), the PR #29478 body and its 7-file list, the net diff Check-runs on the head (48): 26 success, 12 skipped (UI, opt-in and scheduled lanes, as the path filter selects them), and 10 still in progress: ① Derived judgments
② Semver levelClause-②: yes (widening)
③ Boundary flagsopen_questions (dev report 6581879408):
Deviations:
Raised by this review (non-blocking):
Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67 席位核对 · ACCEPT · PR #29478 · 2026-11-02T16:16ZHead-sha: 维护者速读(席位终稿)
核对六项
|
Requested by Steve · project thread
Fixes #29095
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
baseUrl(与appName并列),模板可写{{baseUrl}}/OBJECT/ID生成绝对链接。data.baseUrl仍然优先。回滚即 revert 本 PR。packages/plugins/plugin-email/src/capability-arg.ts(resolveDeploymentBaseUrl的优先级链),packages/cli/src/commands/serve-email-base-url-parity.test.ts(与 serve 的地址链对齐的钉子)。pnpm --filter @objectstack/plugin-email exec vitest run src/capability-arg.base-url.test.ts,预期 10 条全绿。What changes
resolveEmailCapabilityArg(@objectstack/plugin-email) resolves abaseUrlkey into the default template context, after the author's context spread, exactly asappNameis. Chain:OS_AUTH_URL(its legacyBETTER_AUTH_URL) >OS_BASE_URL>config.email.defaultTemplateContext.baseUrl>http://localhost:PORT(OS_PORT>PORT>3000, the defaultserve's--portflag reads). Trailing slashes are trimmed.resolveAuthBaseUrlin@objectstack/cliserve.ts, which plugin-email cannot import (the CLI depends on it; no new dependency edge). The helper is module-private and its docblock names the source; the new CLI testserve-email-base-url-parity.test.tsdrives both readers over the same environment throughresolveCapabilityArgument(the callservemakes) and goes red when either chain moves. The mirror keeps the source's??semantics: a set-but-emptyOS_AUTH_URLis a value, as inserve(which warns about it at boot).EmailServicespreads the send'sdataoverdefaultTemplateContext(email-service.ts, thedataobject built beforerequireVars), so a send's ownbaseUrlstill wins. Pinned in the new test.content/docs/automation/email-templates.mdxgains a section onappNameandbaseUrl, with one example ({{baseUrl}}/crm_deal/{{deal.id}}inbodyText), saying the path after the prefix is the application's own.defaultTemplateContext, no spec change (ruling A).Tests
packages/plugins/plugin-email/src/capability-arg.base-url.test.ts(10 tests): each link of the chain in order, the local default's port, trailing-slash trim from every link, the set-but-empty case,appNameunchanged (control), a rendered template producinghttps://crm.example.com/x/1in text and html, and a send's owndata.baseUrlwinning.packages/cli/src/commands/serve-email-base-url-parity.test.ts(8 rows):resolveAuthBaseUrland the email capability argument agree on every env combination.toEqualon the context, which now carriesbaseUrl):packages/cli/src/commands/serve-email-appname-precedence.test.ts(4 expectations gainbaseUrl: 'http://localhost:3000', one title renamed from "only appName" to "only the resolved keys");packages/plugins/plugin-email/src/capability-arg.config-parity.contract.test.ts(1 expectation gains the same key).Acceptance notes
bodyHtmlcannot build an anchorhreffrombaseUrl. Measured withjudgeMailHtmlHoles(@objectstack/formula): anhrefvalue of{{baseUrl}}/x/{{id}}is judgedurl-attribute, unsupported, and so is{{{baseUrl}}}/x/1(not the whole-valuetype: 'url'shape); the authoring doors refuse both (email-template-hole-placement). So the prefix delivers a link inbodyTextandsubject, and as element text inbodyHtml; an anchor link still needs the whole URL in atype: 'url'variable. The docs say exactly this rather than advertise anhrefuse. Whether the placement rule should admit abaseUrlprefix is a spec/lint-lane question, raised in the report.content/docs/references/system/email-config.mdx(from the spec's.describe()ondefaultTemplateContext) says the context is "passed through unchanged except appName"; it now also resolvesbaseUrl. That text lives inpackages/spec(spec lane), so it is not edited here; raised in the report.httpstail of an in-process TLS listener and a dev port that auto-shifted off a busy one. Both affect only the unconfigured local default.Verification
Ablations (each red, restored, proved)
baseUrl:line from the context viaablation-replace.mjs(anchor 1 -> 0, blob 48162550 -> f7afc7db);capability-arg.base-url.test.tswent 7 failed, e.g.expected 'https://config.example.com' to be 'https://auth.example.com',expected undefined to be 'http://localhost:3000'. Restored by the tool,git diff HEADempty.OS_BASE_URL ?? OS_AUTH_URL ?? BETTER_AUTH_URL, rebuilt plugin-email,ablation-dist-preflightmarker present in 2 built files;serve-email-base-url-parity.test.ts3 failed | 5 passed (expected 'https://base.example.com'vs auth/legacy). Restored (blob back to 48162550), forced rebuild, preflight--absent: marker absent from all 6 built files, tree clean.Local runs
891d74ec(last code commit) for the full sweep; headf224fedfis that plus a merge oforigin/main(13 commits, none touching plugin-email,serve.ts,capability-composition.tsoremail-templates.mdx); scoped re-checks below ran onf224fedf.pnpm --filter @objectstack/plugin-email test: Test Files 48 passed, Tests 785 passed.typecheckfor plugin-email and cli: exit 0 (after a full-dts closure build).f224fedf: plugin-email 2 files, 18 tests passed; cli 3 files (parity, appname-precedence, auth-app-name contract), 26 tests passed.dispatch-gates --commands: 92 derived families, all run,--ranreconciliation: 92 run, 0 NOT-MEASURED, 0 UNRUN. Four first answered exit 3 (prerequisites: the teardown self-test's pinned fixture commit, spec dts,client-react/studiodist, example dist) and re-ran exit 0 after fetching the fixture and building packages and examples. Derived list identical onf224fedf; ratchet and changeset families re-run there, exit 0.pnpm lint(root eslint,--no-inline-config): exit 0.check:nul-bytes: exit 0.!in the title, no(narrowing)arm).Generated by Claude Code