Repository navigation
fix(lint)!: refuse an email template HTML body hole the mail renderer cannot encode for its placement (#25189) - #25212
Conversation
Claude-Session: https://claude.ai/code/session_01Fg7vNwYgyAmHMTJm8rAYsD Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fg7vNwYgyAmHMTJm8rAYsD Co-authored-by: Claude <noreply@anthropic.com>
…le judge (#25189) plugin-email's built-in template pin now reads the shared judge, and a parity test holds the judge's hole grammar to the renderer's. The metadata-protocol door test asserts the 422 INVALID_METADATA envelope for an email_template write and that a draft is not judged. Claude-Session: https://claude.ai/code/session_01Fg7vNwYgyAmHMTJm8rAYsD Co-authored-by: Claude <noreply@anthropic.com>
… link shape; changeset (#25189) The two docs examples, one dogfood fixture and two spec conversion fixtures declare their link variable with type 'url' and place it as the whole href value. The docs page states the supported placements. Claude-Session: https://claude.ai/code/session_01Fg7vNwYgyAmHMTJm8rAYsD Co-authored-by: Claude <noreply@anthropic.com>
…ry now derives (#25189) Claude-Session: https://claude.ai/code/session_01Fg7vNwYgyAmHMTJm8rAYsD Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 145 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 8a19df698d36e8753852b0930b10f13286f28de5 && git checkout 8a19df698d36e8753852b0930b10f13286f28de5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 15764cd723e750c01416cfe91b44bbd0a4c28f37 86330c0908aa48dfcdd0c6abc8545c43667741f9 && git checkout -B drift-repro 15764cd723e750c01416cfe91b44bbd0a4c28f37 && git merge --no-ff 86330c0908aa48dfcdd0c6abc8545c43667741f9
node scripts/docs-audit/affected-docs.mjs --json 15764cd723e750c01416cfe91b44bbd0a4c28f37
|
…le (#25189) Claude-Session: https://claude.ai/code/session_01Fg7vNwYgyAmHMTJm8rAYsD Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #25189 (body and every comment), ruling Check-runs on the head, read at 2026-10-22T12:35Z: every completed run is ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…en and fails closed in a script's escape states (#25189) A character that opens no tag is reconsumed in the data state, and a script's text stays raw from a comment opener until an end tag that follows a comment closer. The inert-type-writes header now says the email_template wiring is also read by the hole placement rule's door test, and the docs' refused list names SVG and MathML content. Claude-Session: https://claude.ai/code/session_01Fg7vNwYgyAmHMTJm8rAYsD Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Round 2 of the record Check-runs on the head, read at 2026-10-22T13:34Z: every run is completed; every conclusion is ① Derived judgmentsItems 1 to 6, 8 and 9 of
② Semver levelUnchanged from ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
Requested by Bill · project thread
Fixes #25189
Part of #25082 (the email half; the parent stays open for the notification half, which is not in this PR)
Clause-②: no (narrowing)
Metadata-protocol: none
Session:
session_01Fg7vNwYgyAmHMTJm8rAYsD, the card thread ofdomain:specseat 2 (seat sessionsession_01B2EeooonSXrANpkpgUSfFZ). Ruling6335348115on #25082, letter F1 A2, items 1, 3 and 4.What changes
A new gating authoring rule,
email-template-hole-placement(validateEmailTemplateHolePlacements), reads every email template'sbodyHtmland refuses each hole that sits where the mail renderer's encoding does not suit it. It is oneCLI_AND_RUNTIMEentry inAUTHORING_RULESwithruntimeTypes: ['email_template'], so it runs onos validate,os build,os lintand the runtime publish gate.type: 'url'and forms the whole value of a double-quotedhref.styleattributes, attributes read as markup, unquoted attribute values, holes inside a tag, script- and style-like elements, comments, and SVG or MathML content. The classification is context-strict: the allow-list is read off the renderer's own encoder, and anything it does not cover is refused. Where the scan does not model a tokenizer state (a script element's escape states), it fails closed and keeps the hole in raw text.errorfinding on the CLI atemailTemplates[i].bodyHtml, and the existing422 INVALID_METADATAat the publish gate with the same issue. The message names the hole, its offset and its placement; the hint names the supported placements. No new error code.subjectandbodyText(plain-text faces), a draft save, and every stored template. A stored template keeps rendering and is judged at its next publish. No migration.The classifier is shared.
judgeMailHtmlHoleslives in@objectstack/formula, because both readers already depend on that package and on nothing else they share: the lint rule, and@objectstack/plugin-email's built-in template pin. That pin now reads the same classifier, and a parity test holds the classifier's hole grammar to the renderer's (an empty render and a markup-carrying render of every built-in body and a grammar corpus, over 200 holes). The renderer itself is not changed.Measured before crossing
c8a7a7047feb: 76 HTML bodies, 116 holes, 0 refused (re-measured with the classifier at86330c090).File surface and domain declarations
packages/lint: the rule, its registry entry, its barrel export and its tests, and the header comments ofruntime-gate.inert-type-writes.test.ts(domain:spec, by the lane table's anchoring exception).packages/formula: the shared classifier, its export and its tests, declared todomain:engine.packages/plugins/plugin-email: the built-in pin, test only.packages/metadata-protocol: one new test file, the host half of the publish door (envelopecodeandstatus, a draft not judged). Test only; declared here as part of the rule's tests.content/docs/automation/email-templates.mdx(the placement rule, its refused list including SVG and MathML content, and the two examples), declared todomain:devx. The four-doors table indeployment/validating-metadata.mdxgains the rule's row and theemail_templatesuperscript, alsodomain:devx. Four CLI transcripts (deployment/cli.mdx,deployment/validating-metadata.mdx,getting-started/build-with-claude-code.mdx,ui/react-pages.mdx) print the author-time rule count, which this entry moves from 50 to 51;check:docs-transcript-driftfound them, and they are declared todomain:devxtoo.packages/qa/dogfoodfixture, declared todomain:cli.packages/spec/src/conversions/registry.ts: one conversion's example templates only..changeset/25189-email-template-hole-placement.md:patchfor lint, formula and spec, with the BREAKING banner and the ADR-0087 disposition (not-required, no migration prescription).Tests
At head
86330c090(formula, plugin-email and the door test re-run after the scan correction; the full lint suite and spec conversions were run onf3bc62099, and since then lint changed only a test file's comments):@objectstack/formula: the classifier, class by class (46 tests); the whole package, 1413 passed.@objectstack/lint: the rule, its registry entry, every command, and the runtime gate (refused and clean writes, and the written template judged alone); withauthoring-rule-wiringandruntime-gate.inert-type-writes, 82 passed on86330c090; the whole package, 7101 passed onf3bc62099.@objectstack/metadata-protocol: the door test, 3 passed. The refusal assertscode: 'INVALID_METADATA'andstatus: 422together (ADR-0112), plus the issue's rule and path.@objectstack/plugin-email: the pin, 77 passed; the whole package, 671 passed.@objectstack/spec:src/conversions, 739 passed.typecheckfor formula, lint, metadata-protocol, plugin-email and spec: exit 0 each.@objectstack/dogfoodtypecheck: exit 0 (under the lock, with a raised heap; the default heap ran out of memory once).Gates
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon this diff, then every command run on head86330c090with its exit code recorded before any pipe:dispatch-gates --ran: 113 derived families accounted for, 113 run, 0 NOT-MEASURED (a derived zero: every one recorded an exit code and none is 3), 0 UNRUN. All 113 exited 0.06f68ec4efoundcheck:docs-transcript-driftred on the four CLI transcripts named above (rule count 50, derived 51). They are corrected inf3bc62099. The union was also all green onf3bc62099, and the run above is on the current head.check-changeset-fixed,check:meta-url-spelling,check:authz-resolver,check:error-code-casing,check:filter-alias-parity(rosters in a touched directory),check-nul-bytes,check-platform-checklist --changed-from origin/mainandcheck:stack-collection-maps: exit 0 each.Dogfood battery
On head
86330c090, after rebuilding the dogfood closure,pnpm --filter @objectstack/dogfood testunder the shared lock:VERDICT command-exit 0(lock held 1585s). Test Files 287 passed, 3 skipped (290). Tests 2398 passed, 18 skipped (2416). No failure. The same counts held onf3bc62099, where the dogfood fixture this PR edits,email-template-materialization, also passed on its own (4 of 4).Acceptance notes
plain-text-faces.test.ts,send-template.test.ts, and two rows oftemplate-loader.test.ts) render bodies this rule refuses. They feed the renderer directly and never cross an authoring door, so they stay as renderer inputs.86330c090(a character after a tag opener that opens no tag, and a script element's escape states) is not pinned by a dedicated test in this PR. The existing class-by-class rows and the renderer parity pin pass on that head.runtime-gate.inert-type-writes.test.tssaid theemail_templatemapping was proved only by a string assertion; its header now says the hole placement rule's door test reads that collection too, and that the table pin is still the whole proof formapping.email_templatewrite's finding is located positionally (emailTemplates[0].bodyHtml), not by name.emailTemplatesis not a context collection, so the gate never re-keys it, and the written template is the candidate's only entry.wherecarries the template's name.packages/lint/src/authoring-rules.ts, at a different entry. This branch merges cleanly withmainat2919ddb87(git merge-tree, exit 0).Generated by Claude Code