Class: Email::ContentRules
- Inherits:
-
Object
- Object
- Email::ContentRules
- Defined in:
- app/services/email/content_rules.rb
Overview
Server-enforced design rules for Redactor 4 email-template body_v4, the email
analogue of Blog::ContentRules. Same idea: house conventions stated in a prompt
are advisory and the model drifts (e.g. bare unstyled , off-brand link colors,
hand-rolled column grids). These rules are DERIVED from the real corpus of
human-authored/converted R4 templates (see config/initializers/email_content_rules.rb
for the empirical basis) and enforced at save time so Sunny's output stays
consistent with what the design team actually ships.
Rules are advisory-by-default (severity :warn) — they're returned to the model so
it can self-correct, but do not hard-block a save unless registered as :error.
Styling drift is a quality issue, not a data-integrity one, so we surface it and
let the model fix it rather than reject outright (a half-styled draft a human can
still finish beats a hard failure).
Usage:
Email::ContentRules.register(
Email::ContentRules::Rule.new(
name: :headings_must_be_styled, applies_to: :body_v4, severity: :warn,
validate: ->(html) { ... nil or message ... }
)
)
violations = Email::ContentRules.validate(scope: :body_v4, html: body_v4)
=> [{ rule:, message:, severity: }, ...]
Defined Under Namespace
Modules: Helpers Classes: Rule
Class Method Summary collapse
- .all ⇒ Object
-
.any_errors?(violations) ⇒ Boolean
True if any violation is severity :error (would hard-block a save).
-
.format_advisory(violations) ⇒ Object
Format violations into a single advisory string for tool responses.
- .register(rule) ⇒ Object
- .reset! ⇒ Object
- .rules_for(scope) ⇒ Object
-
.validate(scope:, html:) ⇒ Array<Hash>
{ rule:, message:, severity: } for every violation in scope.
Class Method Details
.all ⇒ Object
64 |
# File 'app/services/email/content_rules.rb', line 64 def all = @mutex.synchronize { @rules.dup } |
.any_errors?(violations) ⇒ Boolean
True if any violation is severity :error (would hard-block a save).
75 |
# File 'app/services/email/content_rules.rb', line 75 def any_errors?(violations) = violations.any? { |v| v[:severity] == :error } |
.format_advisory(violations) ⇒ Object
Format violations into a single advisory string for tool responses. Returns
nil when there are no violations.
68 69 70 71 72 |
# File 'app/services/email/content_rules.rb', line 68 def format_advisory(violations) return nil if violations.empty? violations.map { |v| "• [#{v[:rule]}] #{v[:message]}" }.join("\n") end |
.register(rule) ⇒ Object
44 45 46 47 48 49 |
# File 'app/services/email/content_rules.rb', line 44 def register(rule) raise ArgumentError, 'rule must be an Email::ContentRules::Rule' unless rule.is_a?(Rule) @mutex.synchronize { @rules << rule } rule end |
.reset! ⇒ Object
51 52 53 |
# File 'app/services/email/content_rules.rb', line 51 def reset! @mutex.synchronize { @rules = [] } end |
.rules_for(scope) ⇒ Object
63 |
# File 'app/services/email/content_rules.rb', line 63 def rules_for(scope) = @mutex.synchronize { @rules.select { |r| r.applies_to == scope } } |
.validate(scope:, html:) ⇒ Array<Hash>
Returns { rule:, message:, severity: } for every violation in scope.
56 57 58 59 60 61 |
# File 'app/services/email/content_rules.rb', line 56 def validate(scope:, html:) return [] if html.nil? || html.to_s.strip.empty? scoped = @mutex.synchronize { @rules.select { |r| r.applies_to == scope } } scoped.flat_map { |r| r.call(html) } end |