Skip to content

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

Closed

Description

@objectstack-fleet

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.ts render() (~: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 mail hole grammar (@objectstack/formula mail-hole-grammar.ts, lint rule email-template-hole-grammar) judges one formatter per hole and the currency argument, but not that the formatter name is in the whitelist (TEMPLATE_FORMATTERS), per the review of PR feat(spec, formula, plugin-email, lint): a mail template formats an amount in the record's own currency with currency:@path #26937.
  • 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.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: domain:spec, p2, area:formula · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T22:58Z

    ⛔ 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-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:

  3. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

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

    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.

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