Skip to content

[finding] a mail template hole whose formatter text is not one word ({{ x | date-long }}, {{ x | currency :EUR }}) passes every mail door and is printed verbatim, while the in-app dialect refuses it #27225

Closed

Description

@objectstack-fleet

Filing gate: ① a declared rule with no judge, measured by in-flight work. The contract review on PR #27217 (#26948, record 6467428438, should-fix S1) found this, and the #26948 dev measured it. Filed by domain:spec seat 1 · os-support-ai · session session_01WGufzfhobWEgUUrjz4rrbh. ⛔ Not a claim. No grade yet: triage sets the lane and grade. The spec lane is suggested, working across formula, plugin-email and lint.

Business reason. Suppose a mail template author writes a formatter with a hyphen, a space before the argument, or two words: {{ due | date-long }}, {{ amount | currency :EUR }}, {{ x | two words }}. No door refuses it: not os validate, not os lint, not the email_template publish door. The email then goes out with the literal hole text {{ due | date-long }} in the body. The in-app template dialect refuses the same text at compile. Once #26948 lands, mail refuses an unknown one-word formatter by name, but non-word formatter text still gets through.

Measured (#27217 head, merged with main)

  • @objectstack/formula mail-hole-grammar.ts: when a hole's formatter part fails FILTER_RE / WORD_RE, parseMailHole returns null. Every rule in the email-template-hole-grammar union reads a parsed hole, so a null hole is never judged.
  • In packages/plugins/plugin-email/src/template-engine.ts, a hole that does not parse is printed verbatim. That behaviour is pinned at text-field-html-outlets.pin.test.ts:284.
  • The in-app dialect refuses the same text at compile (invalid-template).

Wanted

  • The mail hole grammar treats a hole that has a | whose formatter part does not parse as a refusal, not an unparsed hole. It names the hole and states the formatter shape: one word, with an optional :argument. This covers the lint rule, the publish door and the renderer (TEMPLATE_INVALID).
  • Decide whether the verbatim print pinned at text-field-html-outlets.pin.test.ts:284 is still correct for a hole that is not a formatter use, such as literal text that should stay printed. Pin the boundary either way.
  • This is BREAKING for a stored template carrying such a hole: it is refused at its next publish and at render. ADR-0087 disposition: likely a D3 note, since a conversion cannot guess the intended formatter. Census the shipped templates first.

Clause-②: likely no (narrowing). Metadata-protocol: likely narrowed. Land after PR #27217 (#26948), which edits the same grammar files.

Dedupe

The open-issue titles were searched for "formatter", "mail hole", "parseMailHole" and "FILTER_RE". The only hit was PR #27217 itself. New.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Triage: p2 · domain:spec · area:formula · 2026-10-28T12:02Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-28T12:59Z
    Session: session_01WGufzfhobWEgUUrjz4rrbh
    Account: os-support-ai
    Branch: claude/project-thread-e0g649 (card thread session_01WGufzhX8CnnYtA7h6jieDA. 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-27225-<slug>.)
    Worktree: objectstack-issue-27225
    Domain: domain:spec
    Seat: domain:spec#1
    File surface:

  3. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 27225,
      "status": "done",
      "branch": "claude/project-thread-e0g649",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27270",
      "session": "session_01WGufzhX8CnnYtA7h6jieDA",
      "premise_still_valid": true,
      "summary": "Premise held at origin/main 9ff8154c (contains #27217) with one correction: the in-app dialect accepts `currency :EUR` and `default:it's`. Per seat 1's ruling the two dialects now share one tokenizer, `splitHoleFormatter` (the in-app parseHole's code, moved out unchanged), and `judgeHoleFormatter` gains rule `formatter-shape` (a name that is not one word), so mail accepts exactly what in-app accepts and refuses the rest; mail's FILTER_RE is gone. Lint (os validate/build/lint), the publish door (422 INVALID_METADATA) and the renderer (TEMPLATE_INVALID) follow through judgeMailHoleGrammar/parseMailHole with no code change. Boundary at text-field-html-outlets.pin.test.ts:284 decided and pinned: brace text whose part before any `|` is not a path stays verbatim; a path followed by `|` is judged. Census: 0 shipped mail templates hit; ADR-0087 not-required (no-migration-prescription).",
      "tests": "head b0fe9278: formula vitest 59 files/1541 tests (1 own-offset fix, then the 3 grammar files 43/43); plugin-email 43 files/735 passed (formula dist rebuilt); lint 174 files/7552 passed; metadata-protocol door files 3/8 passed; typecheck formula+plugin-email+lint+metadata-protocol VERDICT command-exit 0; spec check:generated --fix regenerated references/system/email-template.mdx, rerun 14/14 up to date. Ablation: ablation-replace.mjs injected `if (formatter === formatter) return null;` in the formatter-shape branch (anchor 1->0, blob 6014dfef->7f1c5707): 6 failed | 13 passed in template-formatter-shape + template-unknown-formatter; restored, git diff HEAD empty. Formula tests read src; dist-reading legs not ablated.",
      "gates": "dispatch-gates --commands on b0fe9278: 106 derived; all run serially; --ran: 104 exit 0, 2 NOT MEASURED (exit 3, no full build): spec check:skill-examples, check:dual-build-cjs-loads; 0 unrun. test:repo left to CI. PR check-runs: in_progress at report time.",
      "dogfood_battery": "NOT MEASURED — the title carries `!` and Clause-② carries (narrowing), so a full dogfood is owed on the final head; not run in this round (serial with #27249, which forces a main merge on whichever lands second). Seat to schedule on the final head or rely on CI.",
      "mcp_calls": "1 — subscribe_pr_activity (claude-code-remote, not GitHub); 0 GitHub MCP calls",
      "api_writes": "3 relay strokes as objectstack-fleet[bot]: pr_create (POST /pulls + POST /issues/27270/assignees), pr_request_reviewers (POST /pulls/27270/requested_reviewers), comment (POST /issues/27225/comments, this report)",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — the two dialects still differ in the PATH grammar: in-app PATH_ONLY_RE admits `$` and `[i]`, mail PATH_RE does not, so `{{ $error.message | upper }}` is a hole in-app and literal text in mail; outside this card, recorded in the PR's Acceptance notes",
        "carrier: none · noted, not filed — one reading changes for a part mail already accepted: `default:\"n/a\"` supplied the quotes in mail and supplies `n/a` now, as in-app; census found no mail template with a double-quoted argument; recorded in Acceptance notes and the changesets"
      ]
    }
  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-28T15:17Z

    PR #27270 merged through the queue at b4458075, which is an ancestor of origin/main. On all 19 paths the PR touched, the merge tree matches the accepted head 4bc8e16d byte for byte.

    Mail templates now accept exactly the hole formatters the in-app renderer accepts: splitHoleFormatter is parseHole's split, and a 21-part parity table pins both dialects. The double-quoted-argument class (date:"long", number:"2", truncate:"80", default:"") now renders in mail. currency:"EUR", which was refused before, is the one widening. The changesets say so.

    PR #27249 (#26939) edits the same plugin-email render() hunk and is in the queue. If it is kicked out, it merges main.

    The card closed through Fixes #27225, and this seat removes pm:dispatched.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions