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
mail templates: an unknown hole formatter (e.g. a misspelled curency) is refused nowhere and renders the raw value; the in-app dialect refuses it (found reviewing #26660) #26948
Filing gate: ① a declared rule with no judge, measured by in-flight work. Found by the contract review on PR #26937 (#26660 item (b); record 6455368454, should-fix S2). Filed by domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP. ⛔ Not a claim. No grade: triage sets lane and grade (spec lane suggested, vertical with plugin-email and lint).
Business reason. A mail template author who misspells a formatter ({{ amount | curency:EUR }}, {{ due | dat:long }}) gets no refusal at os validate, os lint or the email_template publish door, and the email goes out with the raw value (1234.5, an ISO timestamp) where a formatted one was meant. The in-app template dialect refuses an unknown formatter by name; mail does not.
Measured (origin/main)
packages/plugins/plugin-email/src/template-engine.tsrender() (~:124–134): formatValue(parsed.formatter, …) returns undefined for a formatter it does not know, and the hole then renders the raw value (// unknown formatter → raw).
The in-app dialect refuses an unknown formatter at compile (invalid-template), so the two dialects that share the formatter disagree on this.
Wanted
email-template-hole-grammar (and the publish door through it) refuses a mail hole whose formatter is not in TEMPLATE_FORMATTERS, naming the hole and listing the known formatters.
The renderer refuses it too (TEMPLATE_INVALID, like the other grammar refusals), never rendering the raw value silently, so a stored template that predates the rule fails loudly.
BREAKING for a stored template with a misspelled formatter: refused at its next publish and at render. ADR-0087 disposition: likely a D3 note (a conversion cannot guess the intended formatter). Census the shipped templates first.
Clause-②: likely no (narrowing). Metadata-protocol: likely narrowed. Land after PR #26937, which edits the same grammar files.
Dedupe
search_issues on this repo: "mail template unknown formatter renders raw" (open, 0 hits). New.
Read fresh. Mail templates accept an unknown formatter and render the raw value, while the in-app dialect refuses it. Refusing it is a narrowing (no protocol addition), so it is p2.
Claim: PM loop round 2 · 2026-10-28T09:35Z
Session: session_01WGufzfhobWEgUUrjz4rrbh
Account: os-support-ai
Branch: claude/project-thread-j1jtvb (card thread session_01WGufzgBzVZJqh8CQJLrZu8. The platform assigns the thread's branch and the thread can push only that one, so the branch is written as measured instead of claude/issue-26948-<slug>.)
Worktree: objectstack-issue-26948
Domain: domain:spec
Seat: domain:spec#1
File surface:
@objectstack/formulatemplate-engine.ts: judgeHoleFormatter gains a third rule, known-formatter, placed after one-formatter and before the currency rule. Through it, the mail hole grammar, the lint rule email-template-hole-grammar, os validate and the email_template publish door all refuse an unknown formatter by name and list the known formatters.
plugin-emailtemplate-engine.tsrender(): the raw-value fallback goes, and an unknown formatter throws TEMPLATE_INVALID.
{
"issue": 26948,
"status": "done",
"branch": "claude/project-thread-j1jtvb",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27217",
"session": "session_01WGufzgBzVZJqh8CQJLrZu8",
"premise_still_valid": true,
"summary": "judgeHoleFormatter (formula) gains a third rule, known-formatter, judged after one-formatter and before the currency argument: a single-word formatter name not in TEMPLATE_FORMATTERS is refused, naming the hole and listing the known formatters. parseMailHole already calls it, so the mail grammar, lint email-template-hole-grammar (os validate/build/lint) and the email_template publish door (422 INVALID_METADATA) refuse the hole with no further code; the plugin-email renderer's raw-value fallback is removed and a refused hole throws TEMPLATE_INVALID. Census on 72629f38: examples 0, seeds 0, skills 0, apps 0 shipped mail templates use an unknown formatter. Side effect in-app: an inherited object key (toString) that used to run as a formatter and print [object Object] is now refused.",
"tests": "head 857e69625. Locked: formula 57/57 files, 1493 tests; plugin-email 41/41, 717; lint 172/172, 7530; metadata-protocol 278 passed 4 skipped, 21750 passed 26 skipped; service-messaging 61/61, 734; typecheck formula, plugin-email, lint, metadata-protocol, spec Done; spec check:generated all 14 up to date; eslint --no-inline-config on changed .ts exit 0. Ablation: ablation-replace --hold made the known-formatter branch unreachable, formula rebuilt, ablation-dist-preflight marker in dist/index.js and index.mjs; formula new file 5/9 red, plugin-email 3/36 red (the three face cases; the send case and the template-engine re-pin stay green through the renderer's own formatted===undefined guard), lint new file 7/8 red, metadata-protocol new file 1/1 red; restored (blob == HEAD, git diff HEAD empty), rebuilt, preflight --absent clean.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands: 105 derived, run serially; 103 exit 0; 2 exit 3 PREREQUISITE NOT MET (spec check:skill-examples, check:dual-build-cjs-loads: need a full build); --ran: 105 derived, 103 run, 2 NOT-MEASURED, 0 UNRUN. PR check-runs: in_progress at report time.",
"dogfood_battery": "857e69625 · os-verify-lock VERDICT command-exit 0 (closure build + suite, 30m57s) · Test Files 348 passed 3 skipped (351) · Tests 2804 passed 20 skipped (2824) · no failures",
"mcp_calls": "5 — issue_read x3 (26948 get, 26948 get_comments, 26939 get), search_pull_requests x1, pull_request_read get_files x1 (on #26937); no write tool",
"api_writes": "3 relay strokes via scripts/pm/fleet-write/dispatch.mjs — pr_create (POST /pulls draft + POST /issues/27217/assignees), pr_request_reviewers (POST /pulls/27217/requested_reviewers), comment (this report on #26948)",
"open_questions": [
{
"question": "When #26937 lands and this branch merges main, its spec MAIL_TEMPLATE_HOLES sentence and comment (which say no unknown-formatter refusal exists yet) need one more clause. Do it in this PR at the merge, or as a follow-up?",
"options": [
"A in this PR at the merge, with gen:schema && gen:docs and a spec changeset line",
"B a separate follow-up card"
],
"recommendation": "A, because the sentence becomes false the moment both land and the merge is already owed"
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed: the mail grammar leaves text after the | that is not one word ({{ x | two words }}, {{ x | }}) verbatim, pinned by text-field-html-outlets.pin.test.ts, while the in-app dialect refuses it as an invalid hole; in the PR's Acceptance notes"
]
}
PR #27217 merged at 40320f18, which is an ancestor of origin/main. On all 18 paths the PR touched, the merge tree matches the accepted head 97613711 byte for byte.
A mail template hole whose formatter is not in the template engine's whitelist ({{ amount | curency:EUR }}) is now refused by name under the known-formatter rule, and the refusal lists the known formatters. The refusal applies at os validate / os lint, at the email_template publish door, and at send (TEMPLATE_INVALID). The raw value is never rendered any more. This matches the in-app dialect. MAIL_TEMPLATE_HOLES and the lint roster name the clause. The residual, non-word formatter text printed verbatim, is #27225.
The card closed through Fixes #26948, and this seat removes pm:dispatched.
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage:
domain:spec, p2,area:formula· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T22:58Z⛔ Not a claim.
@objectstack/formula,plugin-emailandlint, anchored in the spec lane as the card says. Land after PR feat(spec, formula, plugin-email, lint): a mail template formats an amount in the record's own currency with currency:@path #26937; same family as spec(formula): acurrencyhole with no argument still formats in US dollars — refuse it at authoring in every template dialect (follow-up of #26660) #26939, so one PR may carry both.(narrowing)arm, so aSkills-docs:line ([Decision] pm-dispatch: which PRs owe aSkills-docs:line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369 B), a census of shipped templates, and the ADR-0087 disposition.bug,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
Claim: PM loop round 2 · 2026-10-28T09:35Z
Session:
session_01WGufzfhobWEgUUrjz4rrbhAccount:
os-support-aiBranch:
claude/project-thread-j1jtvb(card threadsession_01WGufzgBzVZJqh8CQJLrZu8. The platform assigns the thread's branch and the thread can push only that one, so the branch is written as measured instead ofclaude/issue-26948-<slug>.)Worktree:
objectstack-issue-26948Domain:
domain:specSeat:
domain:spec#1File surface:
@objectstack/formulatemplate-engine.ts:judgeHoleFormattergains a third rule,known-formatter, placed afterone-formatterand before the currency rule. Through it, the mail hole grammar, the lint ruleemail-template-hole-grammar,os validateand theemail_templatepublish door all refuse an unknown formatter by name and list the known formatters.plugin-emailtemplate-engine.tsrender(): the raw-value fallback goes, and an unknown formatter throwsTEMPLATE_INVALID..changesetentries.Container & model:
M,mode:thread,model: TIER_DEFAULT,effort: highClause-②: no (narrowing). A mail template hole whose formatter is not in
TEMPLATE_FORMATTERSis refused. Before this, it rendered the raw value.Metadata-protocol: narrowed
Responsibility: n/a. A should-fix that the contract review on PR feat(spec, formula, plugin-email, lint): a mail template formats an amount in the record's own currency with currency:@path #26937 (record 6455368454) raised.
Thread-read: 6455660116
Premise: held at
origin/main72629f38, as measured by the dev.judgeHoleFormatterjudges only one-formatter and the currency code literal,parseMailHolereturns an unknown formatter without refusing it, andrender()falls back to the raw value. The in-appparseHolerefuses it.Serial constraints cleared: yes, with one note. PR feat(spec, formula, plugin-email, lint): a mail template formats an amount in the record's own currency with currency:@path #26937 (Mail templates cannot format an amount in the record's currency:
currency:ARGtakes only a literal ISO code, and a path argument silently renders USD #26660, Tier H, open) edits the same grammar files, and spec(formula): acurrencyhole with no argument still formats in US dollars — refuse it at authoring in every template dialect (follow-up of #26660) #26939 (seat 1) runs in parallel on them. This branch starts frommain, and whichever lands later mergesmain.Ruling-ref: none. Triage
6455660116.objectstack-fleet commentedon Oct 28, 2026
os-dev-report
{ "issue": 26948, "status": "done", "branch": "claude/project-thread-j1jtvb", "pr": "https://github.com/objectstack-ai/objectstack/pull/27217", "session": "session_01WGufzgBzVZJqh8CQJLrZu8", "premise_still_valid": true, "summary": "judgeHoleFormatter (formula) gains a third rule, known-formatter, judged after one-formatter and before the currency argument: a single-word formatter name not in TEMPLATE_FORMATTERS is refused, naming the hole and listing the known formatters. parseMailHole already calls it, so the mail grammar, lint email-template-hole-grammar (os validate/build/lint) and the email_template publish door (422 INVALID_METADATA) refuse the hole with no further code; the plugin-email renderer's raw-value fallback is removed and a refused hole throws TEMPLATE_INVALID. Census on 72629f38: examples 0, seeds 0, skills 0, apps 0 shipped mail templates use an unknown formatter. Side effect in-app: an inherited object key (toString) that used to run as a formatter and print [object Object] is now refused.", "tests": "head 857e69625. Locked: formula 57/57 files, 1493 tests; plugin-email 41/41, 717; lint 172/172, 7530; metadata-protocol 278 passed 4 skipped, 21750 passed 26 skipped; service-messaging 61/61, 734; typecheck formula, plugin-email, lint, metadata-protocol, spec Done; spec check:generated all 14 up to date; eslint --no-inline-config on changed .ts exit 0. Ablation: ablation-replace --hold made the known-formatter branch unreachable, formula rebuilt, ablation-dist-preflight marker in dist/index.js and index.mjs; formula new file 5/9 red, plugin-email 3/36 red (the three face cases; the send case and the template-engine re-pin stay green through the renderer's own formatted===undefined guard), lint new file 7/8 red, metadata-protocol new file 1/1 red; restored (blob == HEAD, git diff HEAD empty), rebuilt, preflight --absent clean.", "gates": "node scripts/pm/dispatch-gates.mjs --commands: 105 derived, run serially; 103 exit 0; 2 exit 3 PREREQUISITE NOT MET (spec check:skill-examples, check:dual-build-cjs-loads: need a full build); --ran: 105 derived, 103 run, 2 NOT-MEASURED, 0 UNRUN. PR check-runs: in_progress at report time.", "dogfood_battery": "857e69625 · os-verify-lock VERDICT command-exit 0 (closure build + suite, 30m57s) · Test Files 348 passed 3 skipped (351) · Tests 2804 passed 20 skipped (2824) · no failures", "mcp_calls": "5 — issue_read x3 (26948 get, 26948 get_comments, 26939 get), search_pull_requests x1, pull_request_read get_files x1 (on #26937); no write tool", "api_writes": "3 relay strokes via scripts/pm/fleet-write/dispatch.mjs — pr_create (POST /pulls draft + POST /issues/27217/assignees), pr_request_reviewers (POST /pulls/27217/requested_reviewers), comment (this report on #26948)", "open_questions": [ { "question": "When #26937 lands and this branch merges main, its spec MAIL_TEMPLATE_HOLES sentence and comment (which say no unknown-formatter refusal exists yet) need one more clause. Do it in this PR at the merge, or as a follow-up?", "options": [ "A in this PR at the merge, with gen:schema && gen:docs and a spec changeset line", "B a separate follow-up card" ], "recommendation": "A, because the sentence becomes false the moment both land and the merge is already owed" } ], "out_of_scope_findings": [ "carrier: none · noted, not filed: the mail grammar leaves text after the | that is not one word ({{ x | two words }}, {{ x | }}) verbatim, pinned by text-field-html-outlets.pin.test.ts, while the in-app dialect refuses it as an invalid hole; in the PR's Acceptance notes" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
Landed ·
domain:specseat 1 (#6017) · sessionsession_01WGufzfhobWEgUUrjz4rrbh· 2026-10-28T12:49ZPR #27217 merged at
40320f18, which is an ancestor oforigin/main. On all 18 paths the PR touched, the merge tree matches the accepted head97613711byte for byte.A mail template hole whose formatter is not in the template engine's whitelist (
{{ amount | curency:EUR }}) is now refused by name under theknown-formatterrule, and the refusal lists the known formatters. The refusal applies atos validate/os lint, at theemail_templatepublish door, and at send (TEMPLATE_INVALID). The raw value is never rendered any more. This matches the in-app dialect.MAIL_TEMPLATE_HOLESand the lint roster name the clause. The residual, non-word formatter text printed verbatim, is #27225.The card closed through
Fixes #26948, and this seat removespm:dispatched.