///|
/// A focused content-health check for brand guidance that is not captured by
/// token schema validation alone.
pub struct ContentPolicy {
min_principles : Int
min_misuse_rules : Int
require_imagery_guidance : Bool
require_tagline : Bool
forbid_placeholder_words : Bool
} derive(Debug, Eq)
///|
pub fn ContentPolicy::default() -> ContentPolicy {
{
min_principles: 1,
min_misuse_rules: 1,
require_imagery_guidance: false,
require_tagline: true,
forbid_placeholder_words: true,
}
}
///|
pub fn ContentPolicy::strict() -> ContentPolicy {
{
min_principles: 3,
min_misuse_rules: 3,
require_imagery_guidance: true,
require_tagline: true,
forbid_placeholder_words: true,
}
}
///|
pub fn ContentPolicy::with_minimums(
self : ContentPolicy,
principles : Int,
misuse_rules : Int,
) -> ContentPolicy {
{
..self,
min_principles: if principles < 0 {
0
} else {
principles
},
min_misuse_rules: if misuse_rules < 0 {
0
} else {
misuse_rules
},
}
}
///|
pub fn ContentPolicy::requiring_imagery(
self : ContentPolicy,
required : Bool,
) -> ContentPolicy {
{ ..self, require_imagery_guidance: required }
}
///|
pub fn ContentPolicy::allowing_placeholders(
self : ContentPolicy,
allowed : Bool,
) -> ContentPolicy {
{ ..self, forbid_placeholder_words: !allowed }
}
///|
pub fn Voice::is_complete(self : Voice) -> Bool {
self.tone.trim().to_owned() != "" && self.writing_principles.length() > 0
}
///|
pub fn Voice::principle_count(self : Voice) -> Int {
self.writing_principles.length()
}
///|
pub fn Voice::has_principle(self : Voice, phrase : String) -> Bool {
self.writing_principles.any(fn(value) { value.contains(phrase) })
}
///|
pub fn Voice::to_json(self : Voice) -> String {
"{\"tone\":\"" +
json_escape(self.tone) +
"\",\"principles\":[" +
self.writing_principles
.map(fn(value) { "\"" + json_escape(value) + "\"" })
.join(",") +
"]}"
}
///|
pub fn Voice::to_markdown(self : Voice) -> String {
let lines : Array[String] = ["### Voice", "", "Tone: " + self.tone, ""]
for principle in self.writing_principles {
lines.push("- " + principle)
}
lines.join("\n")
}
///|
pub fn MisuseRule::is_actionable(self : MisuseRule) -> Bool {
self.title.trim().to_owned() != "" &&
self.reason.trim().to_owned() != "" &&
self.replacement.trim().to_owned() != ""
}
///|
pub fn MisuseRule::to_json(self : MisuseRule) -> String {
"{\"title\":\"" +
json_escape(self.title) +
"\",\"reason\":\"" +
json_escape(self.reason) +
"\",\"replacement\":\"" +
json_escape(self.replacement) +
"\"}"
}
///|
pub fn Meta::is_publishable(self : Meta) -> Bool {
self.source.trim().to_owned() != "" &&
self.maintainer.trim().to_owned() != "" &&
self.license.trim().to_owned() != ""
}
///|
pub fn Meta::summary(self : Meta) -> String {
self.source + " — maintained by " + self.maintainer + " — " + self.license
}
///|
pub fn Meta::to_json(self : Meta) -> String {
"{\"source\":\"" +
json_escape(self.source) +
"\",\"maintainer\":\"" +
json_escape(self.maintainer) +
"\",\"license\":\"" +
json_escape(self.license) +
"\"}"
}
///|
pub struct ContentReport {
score : Int
findings : Array[Finding]
suggestions : Array[String]
} derive(Debug, Eq)
///|
pub fn BrandBook::audit_content(
self : BrandBook,
policy : ContentPolicy,
) -> ContentReport {
let findings : Array[Finding] = []
let suggestions : Array[String] = []
if policy.require_tagline && self.tagline.trim().to_owned() == "" {
findings.push(Finding::{
severity: Error,
path: "tagline",
message: "a concise product tagline is required",
})
}
if self.voice.tone.trim().to_owned() == "" {
findings.push(Finding::{
severity: Error,
path: "voice.tone",
message: "voice tone is required",
})
}
if self.voice.writing_principles.length() < policy.min_principles {
findings.push(Finding::{
severity: Error,
path: "voice.writing_principles",
message: "voice needs more writing principles",
})
}
if self.forbidden.length() < policy.min_misuse_rules {
findings.push(Finding::{
severity: Error,
path: "forbidden",
message: "misuse guidance does not cover enough failure modes",
})
}
for index, rule in self.forbidden {
if !rule.is_actionable() {
findings.push(Finding::{
severity: Error,
path: "forbidden[" + index.to_string() + "]",
message: "misuse guidance needs reason and replacement",
})
}
}
if policy.require_imagery_guidance &&
self.assets.imagery_style.trim().to_owned() == "" {
findings.push(Finding::{
severity: Error,
path: "assets.imagery_style",
message: "imagery guidance is required",
})
}
if policy.forbid_placeholder_words {
for term in ["todo", "lorem", "placeholder", "tbd"] {
if normalize_identifier(self.name).contains(term) ||
normalize_identifier(self.tagline).contains(term) ||
normalize_identifier(self.voice.tone).contains(term) {
findings.push(Finding::{
severity: Error,
path: "content",
message: "placeholder language is not publishable: " + term,
})
}
}
}
if self.voice.writing_principles.length() < 3 {
suggestions.push("Add a principle covering examples and edge cases.")
}
if self.forbidden.length() < 3 {
suggestions.push(
"Document at least one misuse rule for color and one for assets.",
)
}
let score = 100 - findings.length() * 20
{ score: if score < 0 { 0 } else { score }, findings, suggestions }
}
///|
pub fn ContentReport::has_errors(self : ContentReport) -> Bool {
self.findings.any(fn(finding) { finding.severity == Error })
}
///|
pub fn ContentReport::summary(self : ContentReport) -> String {
self.score.to_string() +
"/100, " +
self.findings.length().to_string() +
" findings"
}
///|
pub fn ContentReport::to_markdown(self : ContentReport) -> String {
let lines : Array[String] = [
"# Content health",
"",
"Score: " + self.summary(),
"",
]
for finding in self.findings {
lines.push(
"- **" +
finding.severity.label() +
"** `" +
finding.path +
"`: " +
finding.message,
)
}
if self.suggestions.length() > 0 {
lines.push("")
lines.push("## Suggestions")
for suggestion in self.suggestions {
lines.push("- " + suggestion)
}
}
lines.join("\n")
}
///|
pub fn ContentReport::to_json(self : ContentReport) -> String {
"{\"score\":" +
self.score.to_string() +
",\"findings\":[" +
self.findings
.map(fn(finding) {
"{\"severity\":\"" +
finding.severity.label() +
"\",\"path\":\"" +
json_escape(finding.path) +
"\",\"message\":\"" +
json_escape(finding.message) +
"\"}"
})
.join(",") +
"]}"
}
///|
pub fn BrandBook::content_score(self : BrandBook) -> Int {
self.audit_content(ContentPolicy::default()).score
}
///|
pub fn BrandBook::content_is_ready(self : BrandBook) -> Bool {
!self.audit_content(ContentPolicy::strict()).has_errors()
}
///|
pub fn BrandBook::content_markdown(self : BrandBook) -> String {
let sections : Array[String] = [
self.voice.to_markdown(),
"",
"### Misuse guidance",
"",
]
for rule in self.forbidden {
sections.push(rule.to_markdown())
sections.push("")
}
sections.join("\n")
}
///|
pub fn BrandBook::content_json(self : BrandBook) -> String {
"{\"name\":\"" +
json_escape(self.name) +
"\",\"voice\":" +
self.voice.to_json() +
",\"metadata\":" +
self.metadata.to_json() +
",\"misuse\":[" +
self.forbidden.map(fn(rule) { rule.to_json() }).join(",") +
"]}"
}
///|
pub fn BrandBook::principles(self : BrandBook) -> Array[String] {
self.voice.writing_principles
}
///|
pub fn BrandBook::misuse_titles(self : BrandBook) -> Array[String] {
self.forbidden.map(fn(rule) { rule.title })
}
///|
pub fn BrandBook::find_misuse(self : BrandBook, title : String) -> MisuseRule? {
for rule in self.forbidden {
if normalize_identifier(rule.title) == normalize_identifier(title) {
return Some(rule)
}
}
None
}